Notes about my WaterRower personal project

Introduction

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.

First steps

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.

Getting going with the code

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

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.

Polymorphism and class-wide streams

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.