When I received my WaterRower, I found that there was no way to upload activities to Strava (the Facebook of workout tracking). There were multiple repositories which interfaced with the microcontroller on the WaterRower, but none exported the data to any useful format. I figured this would be a good excuse to learn Ada, a language used at my work. While I have written Ada for work, I’ve never used any of the recent or more advanced features. This was a chance to deep-dive into the language.
I started writing this sort of late in the process so there’s a good chance I’m misremembering details.
Before I began programming, I wanted to setup a good environment. The first
thing I did was install gprbuild. This would simplify the compilation process.
Allowing me to setup different build targets and handling parallel compilation.
Since this was a learning experience, a lot of the code has been rewritten in one way or another as I learned about different ways to introduce specific functionality. This went hand-in-hand with the desire to develop the architecture in a well thought out manner. The first thing I did was setup the directory structure to look something like this:
src
├── packets
│ ├── packets.adb
│ └── packets.ads
└── serial_com
├── serial_com.adb
└── serial_com.ads
test
├── packets_tests.adb
├── packets_tests.ads
├── test_waterrower.adb
├── waterrower_test_suite.adb
└── waterrower_test_suite.ads
src/packets would contain the all the “packet” definitions. Those would be the
definitions of the messages sent and received over the serial communication.src/serial_com would support all the sending and receiving of data. At this
point I didn’t have a great understanding of how to do that besides using
GNAT.Serial_Communications to interact with the serial port.Initially I wanted to verify that I could get communication to the S4 (the
monitor of the WaterRower). So, I created a test project that would send canned
data over the serial port to see if a response could be read back. This would
verify that both serialization and deserialization were working. Sending a
message required populating an Ada.Streams.Stream_Element_Array with binary
data. Receiving a message required declaring an
Ada.Streams.Stream_Element_Array of the correct size and reading into it.
Before going into why this might not have been the best approach, let me give an
overview of the message format.
Each message is encoded in ASCII and has an ASCII letter indicating the type of message data that follows. The first letter is not necessarily unique – there may be subsequent letters which need to be read in order to determine the message type. Each message is a static size and is ended with ‘0x0D0A’ i.e. CRLF. For example, consider the following example from the protocol documentation:
| Prefix | Command Suffix | Terminator | Length in bytes |
|---|---|---|---|
E |
RROR |
0x0D0A |
7 |
In this example, the first byte read in would be the letter ‘E’. There are no other messages which start with that letter and the rest of the message is static so we know we would need a buffer of 7 bytes to read in the entire message.
My initial thought of reading in messages was to determine the maximum size of a
message (13 bytes), declaring an array of that size, and using a bunch of if,
elsif, and else blocks to read in and parse each message byte by byte. This
would have worked fine, but was a bit verbose especially given the number of
messages.
Writing messages to the serial port was another issue. I knew I wanted to create
a base record that each type of packet would extend. The hope was that I could
use that inheritance to leverage polymorphism. In Ada, you can declare
class-wide types which can be thought of somewhat like a C style union in which
depending on the tag of the class-wide type, it contains the data of the tag’s
respective record.
I intended on defining an abstract function called Serialize which would
return an Ada.Streams.Stream_Element_Array. The problem with this was that it
required me to implement a Serialize function for every message even if the
implementation was largely the same between messages. It also got more complex
for messages which contained dynamic data.
I started to rethink my approach when I looked into implementing messages with dynamic data such as the following:
| Prefix | Command Suffix | Data | Terminator | Length in bytes |
|---|---|---|---|---|
I |
DD |
XXX + Y2 + Y1 |
0x0D0A |
12 |
Just as before, this message has a static length. The difference is that the
five bytes XXXY2Y1 contain dynamic data where XXX are three hexadecimal
letters e.g. F3D, Y2 is the upper byte of data encoded as ASCII characters
and Y1 is the lower byte of data encoded as two ASCII characters.
At this point I got stumped and put the project on hold for a good while. At
work, I happened across the Read, Write, Input, and Output attributes.
This paired with this example on Wikibooks
pointed me in a new direction.
Basically, this boils down to mapping some data received in the message to an
Ada.Tags.Tag. Then Ada.Tags.Generic_Dispatching_Constructor can be used in
conjunction with that Tag to construct a class-wide variable with the
specified Tag. That variable can then be filled with data from the stream with
the Read or Input attribute.