Two previous projects had an overlapping need to process commands, either from a user or another embedded computer. I used the same solution for both, what I called a command manager class. It was very successful in both cases, but I’m going to meaningfully use it in the future, and especially in LiteMon, I need it to be more generic and have considerably better documentation. This post is going to be the start of a move in this direction.
At the start of the second project, I tried to continue developing the idea of a command manager in its own repository, including it as a submodule in the larger project. My inexperience with managing git submodules over multiple computers and with distant developers eventually made me scrap the submodule idea and include the code directly. The code kept getting better, but without the clear distinction of a submodule, it became blended with the project. Time to get over the hump of submodules, and enforce a release strategy on the library.
As it stands, the command manager has the following characteristics:
- It suits the super-loop (Arduino sketch) layout.
- Commands are defined in setup(). The serial port is polled in loop().
- Supports single and two-byte commands. The big asterisk here is that the two-byte command definition is weakly defined and confusing to the new user.
- Commands are encoded as ASCII characters and sent in triplicates. Triples that don’t match are ignored by the receiver.
- There are some error codes defined to help identify problems such as command IDs not being recognized, and if invalid triplicates being received.
The code to the library is currently located on this GitLab repo. I’m most likely going to either fork or restart a repo for the updated version.
Examples of Usage
I’m in the process of pulling the code out of the projects where I’m currently using it. During this, I’m taking some time to revisit some improvements I didn’t have time for previously and to brainstorm some new updates.
Creating Commands
I am mostly happy with how easy it is to add a new command to the hander. The code below provides an example of adding a simple heartbeat command to verify that commands are flowing. Commands are added to an instance of the command manager. In this case, a lambda is defined inline with the add Command call, but a function pointer is also possible. Note the character argument for the lambda. The user can choose to ignore the argument as in the example below, but this is the two-byte command implementation. The second command byte is sent to the function of the first byte. Inside the resulting function, the user can switch between functionality based on the second byte. As mentioned above, not the smoothest, but powerful.
It’s also easy to note the ‘h’ character as the identifier for the command. This command would then be called upon receiving three ‘h’ characters of the serial port. The 8 represents the index of this command in the command array. If this doesn’t give away some inner workings then I’m not sure what will. Yes – The commands are stored in an array 😢. It’s fixed length… ðŸ˜
comms.addCommand(8,'h',0,[] (char) { // Heartbeat
pal.log("[h], Heartbeat");
});
Pooling the Commands during Loop
There are two approaches available, one is a very quick poll, and the other provides a flow back for error messages.
comms.check();
The snippet above is quite obviously the quick, quiet, and easy solution. Below includes some handling of error messages through a struct that is defined in the same file as the command handler class. All the functions added to the command handler have access to this struct (it lives on the global scope) and can then populate it when something goes wrong. There are several error codes already defined in the command handler, but the user is free to expand with error codes specific to their functionality. It’s very easy to overlap the error codes, which can cause some frustrating debugging so some care is needed.
struct CommunicationManagerResponse commErr = comms.check();
if(commErr.errorCode == 51) {
pal.log("[com], " + (String)commErr.errorCode + ", " + commErr.message);
}
else if(commErr.errorCode > 0) {
pal.urgent("[com], " + (String)commErr.errorCode + ", " + commErr.message);
}
Areas of Improvement
- At the moment, only the first (or only depending on the P/N) UART serial port on the Arduino can be used to receive commands. Removing this dependency on a specific serial port opens doors to different use cases. In another program, I had great success creating a generic communication interface where the user had to implement a version of read, write, and available to get a modified command handler to understand.
- The command manager will only understand triplicate commands. This made sense in the specific context where I came up with it. There needed to be some protection against accidental signals. However, it’s overkill in general use. I think having the triplicate functionality in its back pocket is the best way to move the command handler forward. An option the user can select by pulling in a certain version of the command handler class.
- In addition to the previous point, I am hopeful to be able to vary the command identifier types. The current configuration only accepts ASCII characters as command identifiers, and further, I restricted it to the lowercase alphabet to make it more human-readable. I could see this as another way to generalize, a command interface could replace the need for the identifier and lambda separately. They could simply be members inside a command class, each class has a template ID type, with a method to check if it matches the incoming command, and an execute function to perform its action. The user would be responsible for implementing this command interface for every command they need and then for passing the instances to the handler.
- Initially, there was a desire to have the receive buffers (three characters in an array cause triplicate) be cleared on a timeout for the edge case where a whole byte is accidentally dropped. Clearing them periodically would avoid the edge case where a single-byte reception could trigger a command prematurely if the buffers were ‘preloaded’. Although included, this functionality was not very graceful and introduces another barrier to portability as it makes use of the Arduino delay function.
- There is some coupling between the command manager and another library called pal (Pit Abstraction Layer). The naming of pal is somewhat of a joke as Pit and Pal are both nicknames of mine. The goal of pal was to house some helper functions that I always like to have in projects. I need to make a call to cut ties or find a more sustainable way for these libraries to interact.
- Do I need to say that the documentation needs improvement? Check out the repo, it’s laughable.