Go Down

Topic: [library] SerialCommands: easy command-based sketches (Read 1 time) previous topic - next topic

tuxduino

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:
Code: [Select]

#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...

PaulS

Quote
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?

tuxduino

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).

PaulS

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.

tuxduino

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 :)

tuxduino

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)

Go Up