Pages: [1]   Go Down
Author Topic: [library] SerialCommands: easy command-based sketches  (Read 1766 times)
0 Members and 1 Guest are viewing this topic.
Offline Offline
Edison Member
*
Karma: 26
Posts: 1339
You do some programming to solve a problem, and some to solve it in a particular language. (CC2)
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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:
#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 (108.63 KB - downloaded 16 times.)
Logged

Seattle, WA USA
Online Online
Brattain Member
*****
Karma: 654
Posts: 50946
Seattle, WA USA
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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

Offline Offline
Edison Member
*
Karma: 26
Posts: 1339
You do some programming to solve a problem, and some to solve it in a particular language. (CC2)
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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

Seattle, WA USA
Online Online
Brattain Member
*****
Karma: 654
Posts: 50946
Seattle, WA USA
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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

Offline Offline
Edison Member
*
Karma: 26
Posts: 1339
You do some programming to solve a problem, and some to solve it in a particular language. (CC2)
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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 smiley
Logged

Offline Offline
Edison Member
*
Karma: 26
Posts: 1339
You do some programming to solve a problem, and some to solve it in a particular language. (CC2)
View Profile
 Bigger Bigger  Smaller Smaller  Reset Reset

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.37 KB - downloaded 24 times.)
Logged

Pages: [1]   Go Up
Jump to: