---
title: Building a Ping Pong Scoreboard
teaser: How we built a scoreboard for our ping pong table as a hack day project.
tags: web,ruby,javascript,hardware,hack day,project,ping pong
author: Dan Barber
published_on: 2016-03-01
---

![](https://images.thoughtbot.com/building-a-ping-pong-scoreboard/pD1CcUHSOKN94OV7LwSf_pandabutton.jpg)

We're big fans of table tennis (otherwise known as ping pong or whiff-whaff)
here at thoughtbot London but it can be tricky to keep track of the score while
playing. This gave Damien and I an idea for a hack day project combining
hardware with web services.

We decided that we would build a scoreboard system for the table tennis table so
that players didn't have to keep track of their scores.

The initial idea was to use an [ESP-01][esp01] wifi module, a couple of buttons,
and a nice little web app to implement a scoreboard which incremented the score
for a player by pressing one of the buttons which would be mounted at each end
of the table. In order to make development simpler we used a Lua based firmware
called [NodeMCU][nodemcu].

Flashing the firmware onto the module was somewhat of a challenge due to the
fact that the ESP-01 does not have USB connectivity, just a basic serial
interface, so we needed to hook it up via an FTDI module. This was relatively
straightforward but the software tools are flaky and only seem to be available
from suspect looking websites! The development tools are similarly esoteric,
being written in Java with fairly poor UX.

<figure style="margin-left: -5.4%; margin-right: -5.4%; max-width: none;">
  <img
    src="https://images.thoughtbot.com/building-a-ping-pong-scoreboard/JAsuHYb1Ti6bzsFuTj0q_esplorer.png"
    style="margin-top: -2.8%; margin-bottom: -7%;"
  />
</figure>

After getting to grips with the development tools we started to get some coding
done.

## The scoreboard app

I decided to build the app back-end in [Sinatra][sinatra] and, because I'd
recently heard good things about it, build the front-end using
[Riot.js][riotjs]. Having only one thing to learn during a hack day project is
just no fun!

We decided to encapsulate all the scoring logic within the Sinatra app, using
a couple of models; one to represent the match and one to represent a player.
This meant the frontend was only responsible for rendering the data served by
the API.

We used [Pusher][pusher] to handle keeping the front-end in sync, which meant
that updating the board from the back-end was as simple as saying:

```ruby
Pusher['scores'].trigger('update_scores', match.scores.to_json)
```

Building the front-end in Riot.js ended up being rather simple. Components are
implemented as custom tags. Here is the custom tag I implemented for the
scoreboard:

```html
<!-- tags/scores.tag -->

<scores>
  <div class="scores">
    <div id="blue-score">
      <span class="name">{ players.blue.name }</span>
      <span class="score">{ players.blue.score }</span>
      <span class="games">{ players.blue.games }</span>
    </div>
    <div id="red-score">
      <span class="name">{ players.red.name }</span>
      <span class="score">{ players.red.score }</span>
      <span class="games">{ players.red.games }</span>
    </div>
  </div>

  <script>

    this.players = opts.players;

    // bind to the pusher event to update the scores
    // channel is defined in main.js
    channel.bind('update_scores', function(data) {
      this.players = data;
      this.update();
    }.bind(this));

  </script>
</scores>
```

This gets included and mounted in our main HTML template like so:

```html
<!-- index.erb -->

<scores></scores>

<div class="controls">
  <button id="reset">Reset</button>
</div>

<script src="/tags/scores.tag" type="riot/tag"></script>
<script src="/javascripts/main.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/riot/2.0.15/riot+compiler.min.js"></script>

<script>
  riot.mount('scores', { players: <%= scores.to_json %> })
</script>
```

Then it all gets tied together with a small amount of Javascript:

```javascript
// main.js

// setup Pusher and subscribe to the channel
var pusher = new Pusher(pusherKey);
var channel = pusher.subscribe('scores');

resetScores = function () {
  $.ajax({method: 'PUT', url: '/reset_scores', data: ''});
}

$(function() {
  // bind the click event to reset the scores
  $('#reset').on('click', resetScores);
})
```

Add a sprinkle of CSS:  
![](https://images.thoughtbot.com/building-a-ping-pong-scoreboard/KW4qmuOTHGQiGw7wOBD1_scoreboard.png)

The source is on [Github](https://github.com/danbee/scoreboard).

## The hardware part

Getting a prototype up and running with the ESP01 board was reasonably
straightforward. Getting the FTDI board and the ESP board wired
together took some trial and error, as did actually flashing the NodeMCU
firmware onto the board.

<figure>
  <img src="https://images.thoughtbot.com/building-a-ping-pong-scoreboard/NrLLedMiRMa6VV0E4nZl_DSCF2651.jpg">
  <figcaption>Flashing the ESP01 board. You can see the FDTI board in the
  background.</figcaption>
</figure>

Writing code to handle the button presses was interesting. It's easy to get
spoilt by the Javascript event model. Attach a function to an event and you
know it will run once every time the element is clicked.

In this case we needed to poll the buttons to figure out their state and react
to that state. This meant introducing a latching mechanism in order to only
fire one event per button press.

We started with the simplest possible code that would let us register a button
press and send an HTTP request:

```lua
-- Setup the Wi-Fi connection
wifi.setmode(wifi.STATION)
wifi.sta.config("thoughtbot","**************")

-- Read from GPIO0 (the first button)
PIN_BUTTON = 3

-- Set the polling rate to 40hz (every 0.025 seconds)
TIME_ALARM = 25

-- Set the GPIO mode to pullup (close to fire)
gpio.mode(PIN_BUTTON, gpio.INPUT, gpio.PULLUP)

-- The button open state will read a 1
button_state = 1

function addPoint()
  -- Create an HTTP connection and send a request to the scoreboard endpoint
  conn = net.createConnection(net.TCP, 0)
  conn:connect(80, "192.168.0.10") -- IP address of the hosted scoreboard app
  conn:send("PUT /blue_scores HTTP/1.1\r\nHost: scoreboard-host\r\nConnection: keep-alive\r\nAccept: */*\r\nContent-Length: 0\r\n\r\n", function(sck) sck:close(); end)
end

function buttonHandler()
  -- Read the button's new state
  button_state_new = gpio.read(PIN_BUTTON)

  -- Compare the state to the previous state
  if button_state == 1 and button_state_new == 0 then -- button down
    addPoint()
  end

  -- Store the new state
  button_state = button_state_new
end

-- Run the button handler at the polling rate
tmr.alarm(1, TIME_ALARM, 1, buttonHandler)
```

<figure>
  <img src="https://images.thoughtbot.com/building-a-ping-pong-scoreboard/emVZf3axQCyk4zBgbzkh_DSCF2667.jpg">
  <figcaption>Testing the buttons with an early version of the scoreboard app</figcaption>
</figure>

We spent some time adding an undo feature as well, which enabled the player to
undo a point by holding the button down for a second or so. This meant setting
up timers and more latches to handle this.

The final code for all this can be found in [this gist](https://gist.github.com/danbee/afe3376289462ddb90cc).

## A change of platform

Unfortunately, we found that the ESP01 boards we were using were becoming
unreliable rather quickly, eventually dying (we suspect ESD), and we found
ourselves with five boards, only two of which still functioned at all! We
realised that if this was going to work reliably we would have to look for
a different hardware platform. We found it in the form of the [Particle
Photon][particle]. This was about four times the price of the ESP boards---about
£13, compared to £4 for the ESP01---but turned out to be much more robust and
suitable for the kind of build we were working on.

Particle provide both the development board and a rather nifty cloud service
that allowed us to write our code in a browser and push it to the device from
anywhere!

This is the future.

![](https://images.thoughtbot.com/building-a-ping-pong-scoreboard/8DxVhav7SuG59H2h9bsI_particle.png)

It took me a few hours to translate the code we had written for NodeMCU to the
[Wiring][wiring] code that the Photon supports. The final code can be found in
[this gist](https://gist.github.com/danbee/820a17d0aa75f8900250).

Once we had the code running on the Photon it was time to put it all together!

## Installation

Our plan was to repurpose some of the left over buttons from the arcade machine
build so we needed some sort of bracket to mount them. We designed a simple
L shaped bracket in [TinkerCAD][tinkercad] which we then had 3D printed. An
earlier design of this bracket lacked the triangular side supports which we
quickly discovered was a bad idea because they broke very quickly!

![](https://images.thoughtbot.com/building-a-ping-pong-scoreboard/wWApuZomQ2S3gFnOwUzr_button-bracket.png)

The buttons were mounted in these and then stuck to the underside of the table
tennis table using some automotive grade double sided sticky tape. The results
look great!

<figure>
  <img src="https://images.thoughtbot.com/building-a-ping-pong-scoreboard/YlOt0wZsRbCbNsRlP3Sr_DSCF6506.jpg">
</figure>

<figure>
  <img src="https://images.thoughtbot.com/building-a-ping-pong-scoreboard/1rAp4j0EQWuoBaGAyUGb_DSCF6509.jpg">
</figure>

An old iPad 2 that we used for the table tennis ladder app was moved and
repurposed as the scoreboard and the Photon board was mounted in the middle of
the underside of the table with more automotive tape.

<figure>
  <img src="https://images.thoughtbot.com/building-a-ping-pong-scoreboard/hyNFmr35SQSgZPKq2tsG_DSCF6511.jpg">
  <figcaption>More automotive tape secures the Photon to the underside of the table.</figcaption>
</figure>

<figure>
  <img src="https://images.thoughtbot.com/building-a-ping-pong-scoreboard/MS1INjxIS4Gcd13zYhuR_DSCF6512.jpg">
  <figcaption>The iPad running the scoreboard app.</figcaption>
</figure>

Overall this hack day project was a great success! The scoreboard system gets
used every day, and we've since expanded it with a nicer design and a serve
indicator.

<figure>
  <img src="https://images.thoughtbot.com/building-a-ping-pong-scoreboard/Dm1pPm24TPSMP7HcRnKW_DSCF6518.jpg">
  <figcaption>Obligatory action shot!</figcaption>
</figure>

[esp01]: http://esp8266.co.uk/modules/esp-01
[nodemcu]: https://github.com/nodemcu/nodemcu-firmware
[sinatra]: http://www.sinatrarb.com
[riotjs]: http://riotjs.com
[pusher]: https://pusher.com
[particle]: https://www.particle.io
[wiring]: http://www.wiring.org.co
[tinkercad]: https://www.tinkercad.com
