This year, for the Chrome Dev Summit, I built a real-life version of the Chrome T-Rex offline game. I have already told the story of how the project was born, and the hardware challenges we faced while building it in my previous blog post, I Saw a Dinosaur, or How I Built a Real-Life Version of Chrome T-Rex Game. This post will be all about the software.
As I mentioned before, the main CPU board I used for the project was powered by a chip from Nordic Semiconductor, called nRF52832. This is one of my favorite chips for hardware projects for two reasons: it combines a powerful ARM Cortex-M4 processor with Bluetooth Low Energy functionality, while on the other hand, it is really really power efficient and has a really small physical size.
In the beginning… there was no scoring!
If you check the first commit for the firmware code of the game, you will immediately notice a few things.
First of all, everything is in one big file, about 100 lines of code. It is much easier to prototype this way, as you don't have to set up any build steps before uploading the code to the device, and you can easily tinker with implementation minutes (like the timings of the jump() function) directly on the device while adjusting the hardware and fine tuning the mechanics accordingly.
You will also note some scary constants related to Bluetooth, such as 0xfefe, 0xfe02, and a lot of "magic" numbers in the setName() functions. We won't dive into the bluetooth specifics, but you can learn about the basics of Bluetooth Low Energy in this blog post. Initially, I thought about creating a Web interface that will connect to the game and control it, allowing you to start/stop the game, make the dino jump and change the speed of the game, but eventually I decided that the hardware interface would be enough.
Finally, you will see a lot of calls to functions like digitalWrite, analogWrite, Serial1.write, servo.move, etc. - these are functions that controls that actual hardware that was connected to the CPU board by sending it electronic signals. If you ever used the Arduino environment, these names will probably be familiar to you - Espruino borrowed many of the hardware APIs from Arduino.
The onInit() function runs automatically whenever the board is powered on, and sets up all the hardware peripherals. At the time this code was written, the game only had the following hardware: a Stepper motor for moving the track where the cacti travelled, a Servo motor for the T-Rex jump mechanism, a big button to trigger the jumps (you hit the button - the T-Rex jumps), and an small board that would play the sound effects from an SD card (it's called DFPlayer Mini and has pretty messy protocol).
So at this stage, with just a little more than 100 lines of code, and quickly prototyped hardware - the game was playable, albeit it would not detect the t-rex hitting a cactus, had no scoring mechanism, and the cacti wouldn't loop back to the end of the track. It was a trust based game - you had to keep track of the score yourself, and pull the plug as soon as the t-rex was hit.
Leveling up - Let there be modules!
Next, when I wanted to implement scoring, I needed to add two more pieces of code: a display, and a magnetic sensor to detect the cacti passing by. For the display, I went with a 2.9" E-Paper display as it is able to show black text over a white background with very high contrast, matching the design and style of the original game. These are the kind of displays used by e-readers such as the Kindle.
For instance, before you could interact with the display, you had to write 0 to the wire connected to the RST pin of the display (meaning it'd have low voltage), wait for 200ms, then write 1 (high voltage), waiting for an extra 200 ms, and only then the display was ready at your command. I ended up using Promises - as you can see in the source code (the resetModule() function).
Similarly, whenever you send a command to the module, you have to wait for the display to process it. Once it's done, it will signal by writing 0 (low voltage) to the BUSY pin. Again, I wrapped this with a promise, as you can see in the implementation of waitReady().
Overall, the ability of chaining promises greatly simplified the async code, and eventually I even got it to work. At that point, the code for driving the display grew to around 150 lines, and it started feeling like breaking the code base into modules made more sense.
Espruino does have some kind of module support, but these modules are downloaded from the internet, minified and bundled whenever you upload your code, which is not what I was looking for. So, I decided to tackle it using the same tools I use when I write frontend code - module bundlers. After looking at several alternatives (namely WebPack and FuseBox), I decided to go with rollup.js, as it also does tree-shaking, dead code elimination and can creates flat-bundles. This is especially important for code running on an embedded processor with a so little memory where every single byte matters.
Other than easier to maintain code structure, there was an additional advantage for breaking down the code into modules - the code was much more testing-friendly.
Testing Firmware Code
Many would argue unit-testing improves your productivity as a developer. While working the display driving code, I discovered that the display had no built-in font, so I'd need to store the bitmaps for all the characters that I wanted to display, and send them down the wire whenever I wanted to draw some character. Furthermore, it turned out that I in some cases, depending on where I wanted the character to appear on the screen, I had to split the bitmap payload into several chunks, due to how the protocol worked.
This all turned out to be pretty complicated. The development cycle of changing one or two lines of code, sending it to the hardware and watching the display to figure out the result was exhausting. At that point, I decided to set up unit-tests so that I'd be able to test the logic of my code without having to run it on the actual hardware.
Once I configured Jest, I was able to quickly iterate with the more complex display control functions. If you look into the test code, you can see how I mocked the Espruino functions that interface with the hardware - such as digitalWrite and pinMode. This allowed me to make assertions on the actual bytes that would go down the wire whenever I call the writeChar function.
Another place where the unit-testing proved very useful was persisting the high scores. The embedded processor had Flash Memory, just like your smartphone. Flash memory has an interesting property that is usually abstracted by the Operating System (meaning we, as developers, don't have to worry about it). Generally speaking, you can only write to each memory location once, and then, if you ever want to update that memory location, you need to erase the entire 4KB memory block, which is a time consuming operation, and also slowly wears out the memory cells.
Thus, we aim to minimize the cases where a memory address has to be rewritten and cause a block to be erased. As the nRF52832 chip does not have an operating system, I had to come up with such implementation when I wanted the game to persist the high score. The code would always store the latest high score at the next available memory location until it would reach the end of the block. In this case, it would erase the entire block and start over. This got further complicated by the fact I wanted to store the high score as a short integer (2-bytes), but the hardware only supports 4-byte writes.
This all required some bitwise trickery, as you can see in the final code. It was also the very last feature I added just a day before the Dev Summit, so I was writing it in the hotel room in San Francisco and I had no spare hardware. Having the ability to prototype and test the logic with unit-tests and not on the real hardware was a blessing.
First, I created a mock for the Flash API of Espruino (you can read about this API here), and then created individual test cases for all the different scenarios I could think of. So what did the test suite look like? You can find out here.
To sum up, when working with frontend code, having a good unit-testing infrastructure can be a big time saver, especially when testing features that are buried in the application, and a fortiori in hardware projects such as this one.