Building a Ping Pong Scoreboard

Dan Barber

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 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.

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.

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 and, because I’d recently heard good things about it, build the front-end using Riot.js. 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 to handle keeping the front-end in sync, which meant that updating the board from the back-end was as simple as saying:

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:

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

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


    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 gets included and mounted in our main HTML template like so:

<!-- index.erb -->


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

<script src="/tags/scores.tag" type="riot/tag"></script>
<script src="/javascripts/main.js"></script>
<script src="//"></script>

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

Then it all gets tied together with a small amount of 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:

The source is on Github.

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.

Flashing the ESP01 board. You can see the FDTI board in the background.

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:

-- Setup the Wi-Fi connection

-- Read from GPIO0 (the first button)

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

-- 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, "") -- 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)

function buttonHandler()
  -- Read the button's new state
  button_state_new =

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

  -- Store the new state
  button_state = button_state_new

-- Run the button handler at the polling rate
tmr.alarm(1, TIME_ALARM, 1, buttonHandler)
Testing the buttons with an early version of the scoreboard app

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.

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. 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.

It took me a few hours to translate the code we had written for NodeMCU to the Wiring code that the Photon supports. The final code can be found in this gist.

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


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 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!

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!

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.

More automotive tape secures the Photon to the underside of the table.
The iPad running the scoreboard app.

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.

Obligatory action shot!