The Physical Build Project based on the game Keep Talking And Nobody Explodes
Dev blog | Project aims and goals | Project to-do | Youtube playlist | KTANE IRL Discord server |
I’m currently making short update videos on progress on my Youtube playlist. It’s certainly much quicker to film/narrate what’s new than type up a new blog post! Check there for more up-to-date info, meanwhile a more detailed write-up will appear here going into much more detail.
Prev post: Moving to the ESP32 (+ Coding The Keypad module) | Next post: Communications (part 2): BLE |
Another catch-up update covering about August to November 2020.
As discussed in the previous post, now that I would be using one ESP32 per module there would need to be some way for the microcontrollers to communicate with one another. Having converted a good portion of the old code to create basic Timer and Keypad modules, I began to break down potential types of messages that would need to be exchanged to ensure a game flow and consistent states across the system.
An early flow diagram showing messages moving between devices.
After a quick search as for communication options, there appeared to be three common protocols used with microcontrollers:
Although I2C and SPI were by far the most used protocols, being used to support data transfer with many displays and chips, they both appeared the weakest choice:
I2C is a multi-master, multi-slave architecture, with two lines: data and clock. Devices are assigned an address, allowing the aiming of messages to certain devices only. I started exploring this as a promising protocol… However discovered that the slave-side implementation of the protocol was not supported by the ESP32!
SPI uses a single-master, multi-slave architecture, with two data lines (Master-In Slave-Out and Master-Out Slave-In), one clock line and slave-select lines. Typically, SPI is used with one slave-select line per slave. The master would set a line high if it wanted to communicate with that slave. So for this application, assuming a maximum of 11 modules alongside the Timer, that’s a lot of pins! There are alternatives to this, such as creating a daisy chain where messages to circulated around each device, however this wasn’t the largest issue. SPI is driven by the master. For example: a Keypad (slave) records a strike and wishes to send this to the Timer (master), it cannot choose to do so. It will need to wait for the master to select this device, open communications and then read from the slave. So a couple of challenges present to work around with this protocol.
Looking into CAN, it appeared a perfect fit. Any microcontroller could initiate sending a message and, with its flexible addressing system, could target that message to one or more particular devices. Although requiring CAN bus chips to be wired up, CAN is natively supported on the ESP32, including buffers, interrupts and checks to ensure the line is free before attempting to send a message.
So a little more on how CAN works, which will set the scene for how I used the protocol to send messages for KTOME:
Devices connected to a CAN bus will have an ID: an 11-bit string (or a 29-bit string when “extended frame” IDs are used). Individual messages are sent with an ID attached, and the connected device will filter out any messages which do not match the ID exactly. Essentially:
if (message_id == device_id) {
// Message is accepted
}
To show an example:
Example 1: Basic ID matching
Source ID IDs equal? Message 0b00010011110 - Device 1 0b00010011010 9th bit mismatch: message ignored Device 2 0b00000011110 4th bit mismatch: message ignored Device 3 0b00010011110 All bits match: message read
Hmm ok… well how can a message with one ID be received by multiple devices with different IDs?
Alongside the ID, a device may optionally register a mask of the same length as the ID. This bit-mask acts allows for partial matches to be done. Any bits marked with a ‘0’ in the mask will not be checked when comparing the message ID with the device ID. So now the pseudo-code would be:
if ((message_id & device_mask) == (device_id & device_mask)) {
// Message is accepted
}
Example 2: IDs and masks
Source ID mask ID & mask IDs equal? Message 0b00010011110 - 0b00010011110 - Device 1 0b00010011010 0b11111111111 0b00010011010 9th bit mismatch: message ignored This case was the same as not setting a mask, as every bit is compared.
Source ID mask ID & mask IDs equal? Message 0b00010011110 - 0b00010010000 - Device 2 0b00010011010 0b11111110000 0b00010010000 All bits match: message read The last 4 bits are not compared, enabling this device with a different ID to still accept this message.
Source ID mask ID & mask IDs equal? Message 0b00010011110 - 0b00000000000 - Device 3 0b00010011010 0b00000000000 0b00000000000 All bits match: message read An all-zero mask will allow the device to read every message, no matter its ID!
Message IDs are followed by the body of the message. CAN messages are pretty short: only 8-bytes maximum! So you won’t be able to send a huge status update with one message, however they are quick to send. It appears appropriate to use CAN to send quick flags, triggers and other short pieces of info, but so long as we’re smart about encoding it down to keep it brief. Better yet, we can use the IDs to assist in keeping things moving by being smart about where messages are sent.
Firstly, credit to where credit’s due. I used the CAN library for ESP32 forked by timurrrr, originally from Sandeep Mistry. These were excellent at keeping things simple, with easy function calls to set up the CAN bus on the right pins, sending and receiving messages.
For hardware, all the ESP32 needed as an extra was a bus board. I chose the cheap and simple TJA1050 CAN chips which worked without problems - although I did need to set up a 5V line to power these chips, and they were not spaced to fit breadboards, leaving them dangling at the end of cables.
Since the prototype, I had been thinking about how to get a plug-and-play architecture with as little manual intervention required, it was clear that the CAN IDs would be ideal to become a distinguishing characteristic. After a few attempts at drawing up different ways of using the IDs, I settled on using the following key:
CAN ID bit | ‘1’ in this bit means… |
---|---|
29 | Timer (master) |
28 | Wires |
27 | Button |
26 | Keypad |
25 | Simon Says |
24 | Who’s on First |
23 | Memory |
22 | Morse |
21 | Complicated Wires |
20 | Wire Sequence |
19 | Maze |
18 | Password |
17 | Venting Gas |
16 | Capacitor Discharge |
15 | Knobs |
14 | (not used) |
13 | (not used) |
12 | (not used) |
11 | Unique ID #1 |
10 | Unique ID #2 |
9 | Unique ID #3 |
8 | Unique ID #4 |
7 | Unique ID #5 |
6 | Unique ID #6 |
5 | Unique ID #7 |
4 | Unique ID #8 |
3 | Unique ID #9 |
2 | Unique ID #10 |
1 | Unique ID #11 |
The first 18 bits are used to define the module type (with 3 of these currently unused but available for future modded or custom modules. The remaining 11 bits denote unique identifiers for each module type. This means a bomb with multiple Keypad modules would be able to distinguish the two modules by the position of their unique identifier bit.
For example:
0b10000000000000000000000000000
is the Timer
0b01000000000000000010000000000
is the 1st Wires module
0b01000000000000000000001000000
is the 5th Wires module
A special case: the ESP32 controlling the Widgets is given a blank ID (all zeroes). It will also always be present but will not transmit any messages, and so this ID is acceptable as we’ll discuss later.
It would have been a more economical use of the ID to consider assigning the totals rather than individual bits, e.g. 0b0001… is the Timer, 0b0010… is Wires, 0b0011… is Button, 0b0100… is Keypad, etc. However doing so would have made it challenging to target messages as specific modules, therefore the bits were considered separately.
The 8-byte maximum CAN messages would need to be planned out to prevent multiple messages having to be sent when not necessary, helping to prevent the bus becoming too burdened.
I planned out a system where the majority of messages could be communicated in one byte, assigned a letter to keep it easy to remember and use. Messages from the master would be upper case and messages from slaves would be lower case. Some comms would require more info than just stating an event has occurred, so some would have a defined use for the remaining bytes.
Message to send | CAN message | Comments |
---|---|---|
Master to poll if modules is connected | P | |
Module to reply that it’s connected | p | |
Master to have modules initialise a game | I | |
Module to reply when it’s initialised a game | i | |
Master to ask module to manual setup info | C | |
Module to reply with manual setup info | c… | The content following the ‘c’ is module specific, e.g. wire colours |
Master to confirm manual setup aligned with inputs | M | |
Module does not confirm inputs match | m0 | |
Module confirms inputs match expected setup | m1 | |
Master to send game start | A | |
Master to send game stop | Z$ | The character following ‘Z’ denotes whether bomb exploded, bomb defused or game aborted |
Master to send edgework setup | W##### | Characters after ‘W’ denote counts for batteries, ports, useful indicators, serial number factors |
Master to send serial number | S###### | Characters after ‘S’ denote serial number |
Master to send strike count | X# | Number after ‘X’ denotes the number of strikes |
Master to send heartbeat for ticking sound | H | Send every second decrement on the timer, to trigger ticking |
Module to tell master of a strike | x | |
Module to tell master of a defusal | d | |
Module to request time from master | t | |
Master to transmit time on display | T#### | Numbers after ‘T’ denote the digits of the timer display |
I created a short library to use CAN in the project. Although the main CAN library was already pretty simple, I could incorporate some formatting and quality of life improvements, such as the option to serial print the message contents and IDs being sent and received. It also contained some definitions for the IDs, as explained above.
Modules would be registered to the bus with its type and a unique number, e.g. A Keypad module might be 0b00010000000000000000000000000 & 0b00000000000000000010000000000 = 0b00010000000000000010000000000
. The mask would typically equal the ID, resulting in the module only listening out for any message which contained the module’s ID, e.g. A message with ID 0b00010000000000000011111111111
is designed to target every Keypad module (as it is composed of the Keypad bit following by all unique identifier bits, so it would be accepted by the previous Keypad module.
As the timer is assigned the unofficial “Master” of the network, it typically initiates a back-and-forth with the other modules.
One of the first messages sent by the Timer would be a ‘P’ to see which modules are connected to the bus. As this needs to pick up every module, it sends ‘P’ with the message ID of 0b01111111111111111111111111111
- encompassing the ID of every module, bar the Timer itself.
A slave module, when replying to the Timer, is programmed to include its own ID in the message ID. For example, on receiving the ‘P’ message, the Keypad replies to the Timer to let it know it is connected. It sends ‘p’ in a message with ID 0b10010000000000000010000000000
. The Timer receives this as the first bit is a one, it removes the first one and so it is left with the ID of the Keypad! The Timer can now store this ID and remember which modules are plugged in, and so can tell how many modules are attached, which module types and specifically address this module in particular (when the manual setup of attaching the correct Keypad keys is required).
Many of the messages are obviously exchanged in the order they appear in the above table, from registering modules, to asking them to set up a game, to starting a game. Some messages occur on specific events, such as a module declaring the user has entered a wrong input so sends a ‘x’ strike to the Timer, which is then followed up by the Timer broadcasting the strike total to all modules who require it (such as Simon Says).
The CAN was a pretty easy protocol to setup, however needed a fair bit of planning (and a couple of revisions) to make best use of it and to tidy up any omissions.
One of my brief video updates covers much of what is discussed here in context; you can view it here. This is a pretty early test of the CAN comms, so further CAN usage is shown in the following two videos of the project playlist.
And since I started with the Keypad module as our only slave and that we have been mentioning manual setup in this post and last, it’s no spoiler that I needed to test that Keypad module by knowing which symbols are being generated and whether it is correctly striking or defusing. After thinking of options, it seemed plausible to create a phone app which could pair to the Timer via Bluetooth and give the user a visual read-out of the Keypad symbols. The next blog post will cover the creation of the companion app, how to communicate with the ESP32 with Bluetooth Low Energy, and how the message protocol was created.
Prev post: Moving to the ESP32 (+ Coding The Keypad module) | Next post: Communications (part 2): BLE |