[library] SerialCommands: easy command-based sketches

The purpose of this library is to ease the implementation of a command set.

Each command has an identifier, which is just a simple string. The identifier can be followed by 0 or more characters, which are passed the command implementation as its only argument.
Defining what those characters mean is up to the function that executes the command.
Each command + parameters string is supposed to be terminated by \r or \n or both. These characters are not passed to the command function.

A simple example:

LIGHTON
LIGHTOFF

the command identifier here is "LIGHT". The function implementing it would work more or less like this:

if arg == "ON"
turn on the light
else if arg == "OFF"
turn off the light
else
print "ERR"
endif

If we had a single temperature sensor we could have a

TEMP

command that would just ignore its argument and simpli print back the sensor value.

A command to get or set the current date could be:

DATE2012-01-01

which would work as follow:

if strlen(arg) > 0
d = parse date (arg)
if date was properly formatted
set current date = d
else
print "ERR"
endif
else
d = curr date
print(d)
endif

Here's a complete example sketch:

#include <SerialCommands.h>

const byte LED_PIN = 13;

SerialCommands commands;

// Purpose: making sure the sketch is still alive.
// Expected argument: none.
// Behaviour: prints back the command identifier.
void pingFunction(struct SerialCommand* cmd, const char* str) {
    Serial.println(cmd->cmdString);
}

// Purpose: turn on or off a led. The pin number is code-defined.
// Expected argument: '0' or '1' to turn the led off or on respectively.
// Behaviour: if the argument is '0', drives the pin LOW
// if the argument is '1', drivers the pin HIGH.
void ledFunction(struct SerialCommand* cmd, const char* str) {
    if (str == NULL) {
        printErr();
    }
    
    if (str[0] == '0') {
        digitalWrite(LED_PIN, LOW);
        printOk();
    }
    else if (str[0] == '1') {
        digitalWrite(LED_PIN, HIGH);
        printErr();
    }
    else {
        printErr();
    }
}

// Purpose: read one or more analog channels.
// Expected argument: a list of analog channels to read, for example 01234
// Behaviour: for each char, reads the corresponding analog channel, or prints error if the char is outside the '0'..'5' range.
void readAnalogsFunction(struct SerialCommand* cmd, const char* str) {
    if (str == NULL) {
        printErr();
    }
    
    for (byte i = 0; i < strlen(str); i++) {
        char ch = str[i];
        if (ch >= '0' && ch <='5') {
            byte anCh = ch - '0';
            Serial.println(analogRead(anCh));
        }
        else {
            printErr();
        }
    }
}


void cmdNotFound(const char* receivedString) {
    if (receivedString != NULL) {
        Serial.print(receivedString);
        Serial.println(": command not found");
    }
}


void bufferFull() {
    Serial.print("Serial buffer full. Size = ");
    Serial.println(commands.BUFFER_SIZE);
}


void printOk() {
    Serial.println("OK");
}

void printErr() {
    Serial.println("ERR");
}


// Print a list of available commands.
void listCommands() {
    Serial.print(commands.getNumCommands());
    Serial.println(" commands defined.");
    for (byte i = 0; i < commands.getNumCommands(); i++) {
        Serial.print("command ");
        Serial.print(i, DEC);
        Serial.print(": ");
        Serial.println(commands.getCmdString(i));
    }
}


void setup() {
    Serial.begin(115200);
    pinMode(LED_PIN, OUTPUT);
    
    commands.setCmdNotFoundCallback(cmdNotFound);
    commands.setBufferFullCallback(bufferFull);

    commands.addCommand("PING", pingFunction);
    commands.addCommand("LED", ledFunction);
    commands.addCommand("ANREAD", readAnalogsFunction);
    
    listCommands();
}


void loop() {
    if (Serial.available() > 0) {
        commands.parseCh(Serial.read());
    }
}

The attached zip file contains the library, an example and doxygen-generated documentation (work in progress...).

I hope somebody will find this useful. Comments and suggestions are welcome.

TODO: the SerialCommand struct should probably be turned into a proper class, to avoid exposing its internals to the command implementations...

SerialCommands.zip (109 KB)

A simple example:

LIGHTON
LIGHTOFF

the command identifier here is "LIGHT".

If the command identifier can be 0 to 5 characters, how does the library parse the command identifier as LIGHT, rather than L or LI or LIG or nothing?

Conceptually, the library tests if the received string startsWith(command identifier), and stops at the first command that satisfies this condition.

There's no 5 characters limitation to the command identifier length, although I admit that may seem the case by looking at my example above.
The command ids are not copied to an internal array, just their pointers are stored. That's why there's a warning about the need to pass string literals to the addCommand() method (global const char* strings would be fine, too).

I think it is easier to understand the process if the command name is separated from the command data by some sort of separator.

Afterallthatisafairlystandardpartofanycommunicationsprocess.

Currently nothing stops one from writing

LIGHT ON
LIGHT OFF

The command routine would just see " ON" and " OFF" instead of "ON" and "OFF".

But you got me thinking... I could just ignore the first char after the command, so it would appear as if a blank was required between the command identifier and its arguments. That would certainly make for more readable examples :slight_smile:

This new version ignores any space (0 or more) between the end of the command identifier and the first non-blank char.

That is, all these three strings:

LIGHTON
LIGHT ON
LIGHT ON

are "parsed" as command id "LIGHT", argument "ON". The blanks between the command and ON are discarded. Any blank after the first non blank char are not ignored, though. Therefore LIGHT O N would be invalid.

(doxygen docs and an example included)

SerialCommands.zip (118 KB)