AT*LED=0,0,0,0\n
typedef struct {
uint8_t * command_template;
uint8_t * command_description;
AtCommandHandler_CommandFunc_t command_func;
AtCommandHandler_ReadFunc_t read_func;
AtCommandHandler_WriteFunc_t write_func;
} AtCommandHandler_t;
So my first thought was to look for an open source library to factor this framework stuff out. It's not complicated software and in fact I had close to an exact idea of what it should be. So I basically searched to find what I would have written myself.
However, I was surprised to find nothing.
I found several frameworks and libraries dealing with the host side
of AT Command communication,
which of course makes sense because generally the host side is what is being customized
and the target side is a third party module that will not be changed.
In this case, we were working on the receiving side ie. target side. I could have looked farther, but at a certain point it's not worth it when I only wanted maybe 100 lines of code anyway. Plus I thought it would be a good exercise, which would take a fraction of a day to complete. So I set out to do that.
AT Commands are commonly used to control modems such as cellular modems. Although very outdated, AT Commands are still in use today.
AT Commands are a natural text based interface which can be commanded manually by a keyboard.
AT Commands are extremely simple. They start with a prefix and then a command name. Then there there is a common syntax for the suffix part. Put simply, there are three suffixes.
There is the command which is basically no suffix, which is just a side effect. There is a test which is a suffix ending with question mark which is basically a read. And then there is a write where you can give parameters. If you squint really hard. It is a little bit similar to URL queries. So when receiving one of these commands, it is very simple. You just validate the string which could be as simple as checking that it begins with the letters AT. After that you find the name of the command which appears after the AT and before the equal sign. This is a key to look up the handler in a table of function pointers. Then if there are parameters, then they will be parsed out of the suffix. Then you simply call the function and pass the parameters.
This is very simple of course, but the thing is this is being implemented on a bare metal target in C/C++. The legacy implementation had no abstraction. In other words, it was just a giant if else block with tons of duplication on the validation and extracting of query parameters. And there was no function table. The handling of commands was done in place at the point of parsing. So the approach I took is very obvious. I'm sure we've all seen a similar pattern.
I'm not sure what to call it, but basically I defined a struct to represent an AT Command and all of its suffix types. So for example, AT Command for LED control would allow you to set or get an LED brightness. So what I would define is what I call a command template, which is a string that can be used to uniquely match the prefix of the command and associate it with handler functions. Then there's also a description which would be useful for auto generating a help message. And then there are up to three function pointer fields or members for a write read and command. Some of these command definitions will define all three command types and others will only define one. The feature or value gained by doing this is that the logic for extracting arguments can be factored out. So for example, if a command is going to write a value, then that determines that arguments need to be parsed out as well as determines that a certain function handler pointer needs to be called with those arguments.
So that is the data structure which I call ATCommandHandler.
Then there is ATCommandParser, which takes a line, meaning an input string such as from a serial port. It tries to match the line with a struct from a set of structs that is defined at initialization. Initialization is done by defining all of the ATCommandHandler structs that are part of the command set. Then a main application simply waits for lines, then takes each line, tries to match it with a handler struct. Then if there is a match, it may extract arguments out and then will call the appropriate function. And as I mentioned before, another advantage of this framework is that a help message can be generated automatically by scanning through all of the defined at command handlers. So, quite simple and straightforward. This is actually also a good exercise in writing good quality software with unit tests, end to end tests and an exercise in portability.