NOTE: Look for a new article on the replacement/upgraded version of this solution which uses COBS byte stuffing. WAY better. More elegant. New fruity taste.
The ultra-swanky control panel and the wireless remote control for the Office Chairiot Mark II both communicate motor control and lighting commands to the chassis via the Arduino UARTs (serial ports). It's easy to do this in code and the AVR microcontrollers at the core of the Arduino boards make it trivial to do. A couple of wires and BOOM. Serial's your uncle.
However, like anything electronic, or manmade for that matter, the real world doesn't work like the blueprints say it should. Once in a while a byte or two of important motor commands from the remote controlling unit get scrambled on their way to the chassis control system and it causes unintentional things to happen, like a sudden right turn into a guy seated at a table at a demonstration of the Office Chairiot, for instance. It happened. I'm not proud. It was funny. Nobody was hurt.
I'd like to avoid having to say, "Well, at least nobody was hurt." How to do???
Error-Detecting Serial Packets
I wrote a little C++ class called, "SerialPacket" (on GitHub) that wraps a byte array or struct and sends it across an Arduino HardwareSerial port (Serial, Serial1, Serial2, etc.). In the process, it wraps the bytes with a few other useful bytes to help the receiving end know whether or not the stuff it gets arrives intact. The wrapper contains add a byte to the front and back ends of the data, plus a couple of extry bytes inside to help the receiving end check the integrity of the payload.
How Works??
It's actually very simple, even though it took me a couple of weeks to get it to work. :)
First, let's talk about the format of the frames that carry the data across the wire. There are four bytes that get added around the bytes being sent:
- FRAME_START (1 byte) (decimal 170)
- CRC (1 byte)
- DATA LENGTH (1 byte)
- DATA (1 to 251 bytes) (bytes being sent)
- FRAME_END (1 byte) (decimal 85)
Sending Packets
Sending code looks something like this (just a snippet, of course):
I use the Delegation Pattern extensively in my code for the Office Chairiot. SerialPacket uses it, as well. It's easy to follow, especially when you step away from your code for weeks or months at a time. You'll how I use it in this library more in the "Receiving Packets" section below.
To send data, you instantiate a SerialPacket object (line 24). Give the object a delegate to call (line 25), optionally change the receive timeout (line 26), which defaults to 1000 milliseconds, tell the object which serial port to use (line27) and then call its send() method (line 39).
The SerialPacket::send() method takes a pointer to the data to send and the size of that data's memory footprint. The send() method blocks, meaning you don't do anything else while it's preparing and sending the frame. As you'll see below, receiving is asynchronous, so you can do other stuff while it's collecting bytes for the next frame.
Receiving Packets
Receiving a SerialPacket is easy, as well. Here's a snippet showing it:
For this whole scheme to work, you need to make sure the sending and receiving systems agree on the data structure, or at the very least its length. If the receiving end sees a length that doesn't match what it expects, it calls the error method on its delegate.
The didReceiveGoodPacket() method gets called by the SerialPacket object (line 9) when a packet makes it alive, intact. The calling SerialPacket object gets passed in on that delegate method call so the main app can inspect it and pull the data out of the buffer (that memcpy() on line 12).
I struggled with whether or not you should have to explicitly tell the SerialPacket object to continue receiving data or not. After playing with it, I decided you need to tell it to keep receiving (line 14). When the SerialPacket object gets the FRAME_END byte, it switches out of receiving mode. You could change that if you needed continuous receiving.
If there were any problems with the received packet, the SerialPacket object will call the delegate's didReceiveBadPacket() method (line16) and pass itself in along with an error code.
The receiving end of the Serial line works in states. They go in this order and do these things:
STATE_START_WAIT: Looks for the FRAME_START byte (170 decimal). Once it finds one of those, it switches to the next state.
STATE_CRC: The next byte to arrive is assumed to be the 8-bit CRC of the data payload being sent (payload does not include the frame marks, CRC or length bytes wrapped around it).
STATE_LENGTH: This next byte is used to determine how many bytes following are data bytes.
STATE_DATA/STATE_ESCAPE: During this state, the code watches the stream of bytes for the special byte values (FRAME_START, FRAME_END and ESCAPE), most important of which is the ESCAPE code ('\' or 92 decimal). If it sees an escape, it switches to STATE_ESCAPE. If it find the other two, it calls the delegate's error method because this is a bad or unknown state. Otherwise, if there are no weird things going on, each byte gets added into the buffer.
STATE_ESCAPE: If we are in this state, an ESCAPE byte was found. The byte we're looking at at this moment, then, is a special value, but it's just normal data, so we add it to the buffer and switch back to STATE_DATA.
STATE_END_WAIT: Once the LENGTH number of bytes have been added to the buffer, we switch to this state and we must see the FRAME_END byte. If not, we call the delegate's error method, as a frame must be properly bookended by FRAME_START and FRAME_END.
Rinse and repeat and you've got a solid error-detecting packetized serial communications channel! Woot!
What's Next?
I'm doing the torture tests on the library, right now. Once I'm confident it runs without letting any bad packets through, I'm going to splice it into the firmwares of the chassis and remote control devices. I let my two test Arduino Mega2560 board yack at each other over night at the office and in the morning they'd exchanged over 2.1 million packets without a single issue, so that's VERY encouraging.
As always, I'll keep you posted on this site.