Jake Robert Read

Log Machine Systems Stray Projects About RSS

Delete Your '\n' Delimited Firmware Interface!

  • open-hardware
  • systems-design

TL:DR, I wrote an RPC protocol and compiler tool for Arduino that lets us quickly turn hardware modules into software modules. I think it’s a good candidate design pattern for replacing newline-delimited string-parsing protocols.

Remote Function Calls

aka RPCs - are a pretty common design pattern in distributed systems. The basic idea is simple: we use a network connection to ask another computer to run a function for us. In the case of modular, embedded devices (like those in Modular Things), it’s a great way to centralize systems assembly: each of our modules exposes an API of RPCs and we string those function calls together into new programs - building systems that are collections of modules becomes as simple as writing programs that coordinate multiple software objects1.

Discovery

One of the main troubles with RPCs is that we need to know what functions we have available on our network. In many systems, we simply have to know this information before we start programming: what devices are attached, where are they located, and what functions do they contain … and then, what are the names, types, and argument lists for those functions. This kind of “feedforward systems assembly” can cause all kinds of headaches when we go to run our program, because if we are just a little bit misaligned between what our code expects and what is configured in reality, we run into crashes and failures.

So, the RPC code I’m presenting here additionally exposes a discovery layer - just a small addition to the protocol that lets other network participants ask for function signatures (names, types…). This means that we can do some measure of “feedback systems assembly” - we can inspect our remotes and check our local codes against their reported functions at runtime.

Discovery == Lower Documentation Requirements

Discovery is also a great way to handle documentation: rather than write datasheets about what functions our devices expose, we can rely on API discovery to handle that… basically, I am proposing to put the documentation in the device itself. I suspect that this could help a great deal with version control as well, since each firmware build can report exactly what it contains (whereas it is common for docs and codes to be out of sync with one another).

Auto Rollup

Older versions of Modular Things also basically deployed RPCs, but the serialization was left up to the module author. As I have discussed, making it easy for authors to include new modules into a commons is important, so the major effort I’ve made with this code is to make the function-to-rpc rollup really straightforward (for module developers).

Previously, authors of new modules needed to write matching .js codes for their firmwares. While that pattern is still present in Modular Things (and is useful for more involved device design), it is liable to cause errors when firmwares and higher-level descriptions don’t match up. The new pattern automates this process, and means that we build systems with less sources of truth.

Prototype Protocols, Serialization and Deserialization

Any RPC code relies on some protocol - for this project I threw together a semi adhoc protocol since I think a protocol is better designed once we have some experience with a system… and I know that my buddy Quentin is working on a more robust protocol design for a similar system.

At the core of this protocol we have a small set of type-keys for float, int, boolean and string - then we use those to furnish calls to get function signatures and names, and to serialize arguments (when calling functions) and reply types (on the way back up).

At the moment, this all works within OSAP, which has transport protocol built in; I simply instantiate a new OSAP port for each RPC class, and they are each individually addressable in the network… I’ll write more about OSAP hopefully in the next few weeks.

Type Keys and Data Serialization

Type Keys
void int bool float string
0 1 2 3 4

Serializations of these are straightforwards, i.e.:

template<typename T>
void serialize(T var, uint8_t* buffer, size_t* wptr){}

template<>inline
void serialize<int>(int var, uint8_t* buffer, size_t* wptr){
  buffer[(*wptr) ++] = var & 255;
  buffer[(*wptr) ++] = (var >> 8) & 255;
  buffer[(*wptr) ++] = (var >> 16) & 255;
  buffer[(*wptr) ++] = (var >> 24) & 255;
}

template<>inline 
void serialize<bool>(bool var, uint8_t* buffer, size_t* wptr){
  buffer[(*wptr) ++] = var ? 1 : 0;
}

template<typename T>
T deserialize(uint8_t* buffer, size_t* rptr){}

template<>inline 
int deserialize<int>(uint8_t* buffer, size_t* rptr){
  int val = 0;
  val |= buffer[(*rptr ++)];
  val |= buffer[(*rptr ++)] << 8;
  val |= buffer[(*rptr ++)] << 16;
  val |= buffer[(*rptr ++)] << 24;
  return val; 
}

template<>inline 
bool deserialize<bool>(uint8_t* buffer, size_t* rptr){
  return buffer[(*rptr ++)];
}

Protocol Message Types

Function Signature Request
B0 B1
PRPC_KEY_SIGREQ MSG ID
Function Signature Response
B0 B1 B2 B3 ... ... ...
PRPC_KEY_SIGRES MSG ID Return Type Key Number of Args ... Arg Type Keys Function Name ... Arg Names
Function Call
B0 B1 B2 ...
PRPC_KEY_FUNCCALL MSG ID ... Arg Data
Function Return
B0 B1 B2 ...
PRPC_KEY_FUNCRETURN MSG ID Return Data

Spec / Protocol Based Systems allow Interoperability without Open Source

This is probably worth a lengthier discussion, but I think that we often reach for Open Source when all we want is Open Interoperability. This may be especially true in industrial settings, where closed protocols prevent interoperation in order to lock customers into particular ecosystems.

This is also a current site of tension in Open Hardware: folks are struggling to understand how to make money while exercising their craft as hardware engineers. Personally, I think the overarching goal is maybe more aligned with a theme of “Collective Intelligence,” of which Open Source is just one flavour… What we want is to be able to put our heads together, and build big complex systems using contributions made by many individuals. Interoperation achieves that, and allows individual contributors a clearer path to profitability - and we shouldn’t be afraid of that.

Towards Defeating the Newline Delimiter

All highfalutin discussions aside, there is one nasty design pattern that I think this design pattern can help to vanquish, which is the \n newline-delimited string parser. By which I mean any (mostly Arduino-based) firmware that contains some snippet like this:

void loop(void){
  int received_bytes = Serial.readBytesUntil('\n',input_buffer,INPUT_BUFFER_LENGTH-1);
  if(received_bytes > 0){
    input_buffer[received_bytes] = '\0';
    String command = String(input_buffer);

    /* .... */

    // basically just a function call to "move_axes:" 
    if(command.startsWith("move_rel ") or command.startsWith("mr ")){ //relative move
      int preceding_space = -1;
      long displacement[n_motors];
      EACH_MOTOR{ //read three integers and store in steps_remaining
        preceding_space = command.indexOf(' ',preceding_space+1);
        if(preceding_space<0){
          Serial.println(F("Error: command is mr <int> <int> <int>"));
          break;
        }
        displacement[i] = command.substring(preceding_space+1).toInt();
      }
      move_axes(displacement);
      Serial.println("done.");
      return;
    }

    /* .... */
    // basic getters 
    if(command.startsWith("ramp_time?")){
      Serial.print("ramp time ");
      Serial.println(ramp_time);
      return;
    }

    /* .... */

  }
}

This is a snippet from the sangaboard; the firwmare that runs the open flexure, an exemplary piece of Open Hardware. I don’t include it here to pick on them (this is a really well-written parser), but just to point out that these string-based protocols (that are prevalent in OH), are often re-written project-to-project.

String Parsers are often shaky, ad-hoc protocols, that suck time and FLASH from our codes, and make difficult to maintain, read, and extend embedded systems.So, if you’ve a hardware project in need of a comms layer, perhaps give modular-things a shot. At the moment, this works just in the browser in Modular Things, but next weeks’ update will deploy a design pattern that makes it easy to drop these into native JS, TS and Python projects.

Limits and Future Work

Compiler Caveats

This works right now where c++17 or newer is available; of my favourite microcontrollers (RP2040, SAMD51, the Teensy, and the SAMD21) (all deployed on various arduino cores), that leaves just the RP2040. So, that’s a problem.

AFAIK, this is mostly due to my use of std::apply and also some fold expressions, both of which are c++17 onwards. However, the core utilities that I use (std::tuple, decltype and parameter packs) are all available from c++11 - so I should be able to hack this back to thence, using recursive techniques.

There is also the trouble of templating as-well against the full mixture of void-void, void-args, ret-void, ret-args - for those I currently use if constexpr (c++17).

Anyways, that’s all a TODO. AFAIK, c++14 is available across the SAMD core (which makes up very-many arduinos), and the Teensy core, so bumping down to that level would at least satisfy most of my constraints. IDK what other popular MCUs use, i.e. the ESP32 series would be awesome to include. AVR is probably another question.

Stronger Type Serialization / Deserialization

Alongside the templating tricks, the core of this protocol is a prototype of a set of serialization (and deserialization!) functions to turn all of our typed values into bytes-on-the-wire and back. These are valuable across protocols, so I would like to have a core set that work well across platforms. Requirements would be like:

  • hot
  • fast
  • flexible
  • self reporting
  • small code size
  • highly portable

There are many systems out there that do this type of work, including:

  • cbor: Concise Binary Object Representation
  • msgpack: “It’s like JSON but Fast and Small”
  • capn’proto: ex ProtoBuf engineer
  • protobuf: the serdes tool from Google

AFAIK, capn’proto and protobuf don’t support discoverability, and the other two have been on my homework list for a while. Anyways, we need good serdes for this kind of stuff.

Embedded Device to Embedded Device

The current pattern works well to centralize embedded modules in one higher level code, but we can’t chat between devices. It would be awesome to be able to configure an “RPC Caller” as well as the implementer, in embedded codes. This would be an odd way to configure systems though, because the whole “view” of the system would be distributed across multiple firmware source codes…

In my Masters degree I made a totally bonkers system called squidworks where embedded devices talked directly to one another and systems were represented in a graph view.

Co-ordinating and designing large distributed systems can be a major PITA, especially when it comes to orchestrating direct communication between multiple embedded devices. In my own prior work (above), squidworks used a graph representation to do so. RPCs on the other hand are typically used distributed systems where most control is centralized within one computer and ‘other’ devices are simple service workers, as is the case with Modular Things. However, it may still be useful to be able to implement RPC Callers in firmwares (the current build only builds RPC Implementers).

Graphs make for an excellent overview tool (to grep the whole state of some distributed computer program), but are sometimes clunky programming tools (or perhaps they just expose the spaghetti that is fundamentally inevitable). In any case, some “heavy mixture” of code- and graph-based programming seems like a likely candidate for sense-making of modular embedded worlds, and I would like to return to that question.

.py to .js, and vise-versa.

Along the same lines, I simply haven’t written all of the code with-which a .py code would announce itself to a .js code, etc etc etc.

Everything in time, perhaps.


  1. This is an idea with a long legacy at the CBA, notably in Ilan Moyer and Nadya Peek’s theses.