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.