My last Project post (Arduino remote control panel) focused on receiving primitive commands from a remote computer. Once I had the serial communication and the primitive command parsing working, I wanted to generalize the Arduino side.
I think it will be a useful "building block", as I work on Arduino projects, to have the ability to receive text-string commands, parse them and execute them. The actual commands and what they execute is arbitrary; the goal is to have a "template" program (er, "sketch" in Arduino-speak) that can be used for any application that needs remote commands.
In my previous post, I mentioned two classes of applications where this might be useful:
1) to make an "operator's console" to control a complex Arduino project (permanently connected)
2) to make a "maintenance console" to tweak/calibrate/configure even a simple Arduino project (occasionally connected)
Another type of project came to mind this week:
3) A 3- or 4-axis robot arm, one Arduino controls each axis and its stepper-motor, while one Arduino Mega (with 4 serial ports) coordinates the sequence of movements by sending commands via serial port to the axis controllers.
So it seemed like I'd want to do something like this often enough that it was worth a good, clean generalized implementation that I could use as a template. And once I got it working, I went back over it with comments to make it (hopefully) clear to others how to use the template themselves...
So the program design is this:
Arduino's sit in an infinite loop, so we do the following forever:
Code:
1) See if any input has started arriving
If so, gather it all up until it stops (or we fill the buffer)
2) If we got something, then
parse it into its component pieces (a command and some arguments)
3) execute the command
4) Check for any timer-based events needing attention
5) Do any other work that needs doing
Wait a millisecond
The design allows commands to set up a timer, to blink an LED for a variable time period, say. The loop has a 1 ms delay at the end, so fairly precise timing is available.
The step numbers at the left above are keyed to comments in the source code that follows. The code as written implements a trivial case of turning an LED on/off or blinking it for N milliseconds. There are print commands that allow you to enter command strings in the Arduino serial monitor and see the results. Comment these out in a real application (or not). Define new commands by adding new else clauses in Step 3.
Code:
/*
Program to receive and exceute commands via serial port
Reads lines of text containing "commands" and optional arguments (pararmeters).
This instance turns an LED on/off, or blinks it every N milliseconds.
Modify Steps 3, 4 & 5 as desired to implement new commands...
Larry Walker Walker Energy Systems LLC Dec. 5, 2010
*/
#include <Stdio.h> // need this so we can use sscanf(), atoi(), strcmp()
// adjust the number and length of args and the input line length
// as needed per application, to minimize space consumed...
const int MAXARGNUM = 10;
const int MAXARGLEN = 16;
const int MAXBUFLEN = 64;
char *argv[MAXARGNUM]; // the arg vector (ptrs to the char strings)
char arg[MAXARGNUM][MAXARGLEN+1]; // the args themselves
int argc; // the arg count
int delayVal = 0; // duration of blink, in millis (0 means no blinking in effect)
int delayCnt = 0; // counter to step through the delayVal
int blinkState = LOW; // state variable for blinking LED
const int LED1 = 13; // define pin for LED1
char ascii[64]; // output buffer for debug prints
void setup()
{
int i;
Serial.begin(9600);
// initialize the array of argv pointers
// to point to an array of char variables
// init the argv pointers
for (i=0; i<MAXARGNUM; i++) {
argv[i] = &arg[i][0];
}
}
void loop()
{
int i, inPtr, n; // indexes used in processing the args
char inputLine[MAXBUFLEN+1]; // buffer for incoming lines
/*
Step 1: each time around, check to see if a line of input has begun
*/
// suck up characters until they stop (or buffer fills)
// this should (nominally, theoretically) be one "line" of input
i = 0;
while ((Serial.available() > 0) && (i<MAXBUFLEN)) {
inputLine[i] = Serial.read();
i++;
// need a brief delay to give any chars remaining in the line time to arrive
delay(1);
}
/*
Step 2: if/when a line has arrived, parse it
*/
// check if we got anything this time around
if (i > 0) {
// terminate the incoming line
inputLine[i] = '\0';
sprintf(ascii, "inputLine = %s", inputLine);
Serial.println(ascii);
// use sscanf() to pull the args apart and count them
// arguments are strings of consecutive alphanumeric chars,
// separated by spaces or tabs. They must be explicitly converted if numeric
argc = 0;
inPtr = 0;
// loop through the input, pulling out one arg each time and noting its length
// [ cryptic incantation follows: see any good C book for details on sscanf ]
while (sscanf(&inputLine[inPtr], "%s %n", argv[argc], &n) > 0) {
// confirmation/debugging output
sprintf(ascii, "argv[%d] => %s ", argc, argv[argc]);
Serial.println(ascii);
// found another arg, so bump the pointer past it for next sscanf call
inPtr = inPtr + n;
argc++;
// enforce the max args limit!
if (argc >= MAXARGNUM) {
break;
}
} // end of while
sprintf(ascii, "argc = %d", argc);
Serial.println(ascii);
/*
Step 3: the incoming line has been parsed into argv[], so perform argv[0] as a command
*/
// command parsed, now do it
if (strcmp(argv[0], "ON") == 0) {
// do the ON command
digitalWrite(LED1, HIGH);
Serial.println(" turning LED on...");
delayVal = 0; // suppress any pending blinking
}
else if (strcmp(argv[0], "OFF") == 0) {
// do the OFF command
digitalWrite(LED1, LOW);
Serial.println(" turning LED off...");
delayVal = 0; // suppress any pending blinking
}
else if (strcmp(argv[0], "BLINK") == 0) {
// do the BLINK command
delayVal = atoi(argv[1]); // the first/only arg is the blink-time in ms
delayCnt = 0;
Serial.print(" blinking LED every ");
Serial.print(delayVal, DEC);
Serial.println(" ms...");
delayCnt = delayVal-1; // terminate any pending blink
}
else {
// if nothing else matches, do the default
Serial.println(" Invalid command!");
}
Serial.println();
} // end of if (got a line)
/*
Step 4: tend to any timer-like things that command(s) may have started
*/
// see if we need to do any timer-/counter-based things
// just one, for now, the blink timer
if (delayVal != 0) {
// we have a delayVal, so we're blinking
delayCnt++;
// time to toggle the LED?
if (delayCnt >= delayVal) {
blinkState = !blinkState;
digitalWrite(LED1, blinkState);
delayCnt = 0; // restart the delay period
} // end of if (delay expired)
} //end of if (delay in effect)
// add additional timer-related checks here
/*
Step 5: (Optional) do any additional application-specific tasks here
*/
// this code will get executed every ms
// (except during brief bursts of char-reading during incoming lines)
// do any other things that your application needs to do here...
/*
end of Steps: wait briefly and go around again
*/
delay(1);
/*
This whole thing is a 1-ms clockwork mechanism,
getting interrupted by a line of input now
and then...
*/
}
Larry Walker