Skip to main content

Serial Commands for Dynamic Control

In other examples, we show how to program the Quarto to do a task, such as act like a PID Servo. But often you want to interact with the Quarto to change settings, read values, etc. To make that process easier we have a library, qCommand, for registering serial commands and mapping them to functions. This let you enter commands into the serial port to change the behavior of the Quarto.

Setup

First, we need to include the qCommand library and instantiate it:

#include "qCommand.h"
qCommand qC;

This new qC object has a function readSerial that will parse incoming Serial data. We want it to continually process new Serial data, so we call this function in the main loop() :

void loop() {
qC.readSerial(Serial);
}

With that, we have the basics working. If you load this program on the Quarto, you can type commands on the Serial port and get responses:

>> Hello
<< Unknown command: Hello
>> Hi
<< Unknown command: Hi

At this point, we have no commands registered, so everything we type results in an unknown command.

The Quarto actually has two Serial ports. The main one (Serial) is the default and is used by the Arduino IDE for reprogramming the device. But the second one (Serial2) can be used as well. If you like using a terminal program outside of the Arduino IDE, it can be nice to use the second port and leave the main one available to the Arduino IDE for reprogramming. To use the command system on both Serial ports, we need have qC object read both serial devices:

void loop() {
qC.readSerial(Serial);
qC.readSerial(Serial2);
}

Now, you can type commands on either Serial port and get a response.

Adding Commands

Adding commands looks a little strange at first, but its a very powerful structure. We will map a string (or command) to a function. That function must take exactly two arguments: the qC object and a Stream. A Stream is just a more generalized version of the Serial object. By using this Stream instead of directly using Serial, the command can respond on the Serial port that issued the command. The Stream object has all the same functions like print and println and write that the Serial object has.

Here's an example function:

void hello(qCommand& qC, Stream& S) {
S.println("Hello there.");
}

Now to add that command, we put the following into our setup() function:

void setup() {
qC.addCommand("Hello", hello);
}

The addCommand line assigns the string "Hello" to the hello function we wrote. Now, we type into (either) Serial port, we'll get:

>> Hello
<< Hello there.
>> HELLO
<< Hello there.
>> hello2
<< Unknown command: hello2

You'll notice that commands are case-insensitive as 'HELLO' works the same as 'Hello'. This is the default, but you can make the commands case-sensitive by initializing the qC object with the optional case-sensitive parameter set to true:

//qCommand qc; //Shorted version when case-sensitivity is false
qCommand qC = qCommand(true); //case-sensitive instantiation

Commands with Arguments

To pass arguments to your functions, add them after the command string with a space. The syntax is

command arg1 arg2 arg3 etc

To access each string argument, we use the next() and current() functions. If there isn't another argument, next() will return NULL. The current() function is used to get the current argument. Here's an example function:

void hello(qCommand& qC, Stream& S) {
if ( qC.next() == NULL) {
S.println("Hello.");
} else {
S.printf("Hello %s, it is nice to meet you.\n",qC.current());
}
}

And if we want our hello function to respond to both 'Hello' and 'Hi', we simply run addCommand a second time:

void setup() {
qC.addCommand("Hello", hello);
qC.addCommand("Hi", hello);
}

Let's see that in action:

>> Hi
<< Hello.
>> Hello Angela
<< Hello Angela, it is nice to meet you.

Setting Variables

Often you will want a command to read and write to a variable. For example, maybe you want the 'gain' command to let you adjust the gain in your servo loop. This structure for reading and writing a variable is very common, so qCommon provides a shortcut for it. Instead of using the addCommand function to map a function to a command, we use the assignVariable function to assign a command to read and writing a variable:

qC.assignVariable("Gain",&loopGain);

This helper function works for most number types including float, double , int , uint, short, unsigned short, uint8_t and int8_t. For all integer types, values outside of their range will be coerced to the nearest possible value. Here's a short program to show how it works:

#include "qCommand.h"
qCommand qC;

uint longInt;
int8_t int8;
double doubleVar;

void setup() {
qC.assignVariable("uint",&longInt);
qC.assignVariable("int8",&int8);
qC.assignVariable("double",&doubleVar);
}

void loop() {
qC.readSerial(Serial);
qC.readSerial(Serial2);
}
>> uint -4
<< uint is 0
>> int8 500
<< int8 is 127
>> int8 -50
<< int8 is -50
>> double 1.2345
<< double is 1.234500
>> double 3.14e-5
<< double is 3.140000e-05

Custom Functions

While the assignVariable function is very handy, sometimes you'll want to write a custom function to get more control. Maybe we want to control the gain to a PID Servo, but we want to restrict the gain to be positive and less then 10. In this situation, we have to write a custom function. The structure will be same as the hello function discussed above, but it will need to take a string argument and convert it to a double. To do this, we will use the atof function. Similarly, to convert a string to an integer, we would use the atoi function. Here's an example:

double loopGain = 1.021; // global for the gain
void gain(qCommand& qC, Stream& S) {
if ( qC.next() != NULL) {
loopGain = atof(qC.current());
if (loopGain < 0) {
loopGain = 0;
} else if (loopGain > 10) {
loopGain = 10;
}
}
S.printf("The gain is %f\n",loopGain);
}

If we assign this function to the string 'Gain' with

qC.addCommand("Gain", gain);

then we interact with the variable gain like this:

>> gain
<< The gain is 1.021000
>> gain -3.25
<< The gain is 0.000000
>> gain 11.25
<< The gain is 10.000000
>> gain 1.234567
<< The gain is 1.234567

Multiple Arguments

To use multiple arguments for your functions, simply repeat calling qC.next() Also, if you want to use an integer instead of a double, use the atoi command. Here's an example that takes a floating point number (first argument) and an integer (second argument) and multiplies them together:

void multiply(qCommand& qC, Stream& S) {
double a;
int b;
if ( qC.next() == NULL) {
S.println("The multiply command needs two arguments, none given.");
return;
} else {
a = atof(qC.current());
}
if ( qC.next() == NULL) {
S.println("The multiply command needs two arguments, only one given.");
return;
} else {
b = atoi(qC.current());
}
S.printf("%e times %d is %e\n",a,b,a*b);
}

If we assign this function the command 'Mult', then we get the following behavior:

>> mult
<< The multiply command needs two arguments, none given.
>> mult 1.23
<< The multiply command needs two arguments, only one given.
>> mult 1.23 -3
<< 1.230000e+00 times -3 is -3.690000e-00
>> mult 1.23e1 2.25
<< 1.230000e+01 times 2 is 2.460000e+01

List All Commands

All the available commands can be listed with the qCommand's printAvailableCommands function (which takes the Stream as an argument). So we can write a help function as

void help(qCommand& qC, Stream& S) {
S.println("Available commands are:");
qC.printAvailableCommands(S);
}

Custom Unknown Command

One last customization option for the qCommand library is to customize the response to an unknown command. By default, the response is just "Unknown command: " followed by the input command. But we can write our own function to response to unknown commands:

void UnknownCommand(const char* command, qCommand& qC, Stream& S) {
S.printf("I'm sorry, I didn't understand that. (You said '%s'?)\n",command);
S.println("You can type 'help' for a list of commands");
}

Then in the setup, we configure qCommand to use our new function:

void setup() {
qC.setDefaultHandler(UnknownCommand);
qC.addCommand("Hello", hello);
...
}

Full Code

Here's the full code (also available in the Examples / qCommand in the Arduino IDE.)

#include "qCommand.h"
qCommand qC;
//qCommand qC = qCommand(true); //Use this line instead for case-sensitive commands

double loopGain = 1.021;
int anInteger;

void setup() {
qC.setDefaultHandler(UnknownCommand);
//qC.setCaseSensitive(true); //uncomment to make commands case-sensitive
qC.addCommand("Hello", hello);
qC.addCommand("Hi", hello);
qC.addCommand("Gain",gain);
qC.assignVariable("Int",&anInteger);
qC.addCommand("Mult",multiply);
qC.addCommand("Help", help);
}

void loop() {
qC.readSerial(Serial);
qC.readSerial(Serial2);
}

void hello(qCommand& qC, Stream& S) {
if ( qC.next() == NULL) {
S.println("Hello.");
} else {
S.printf("Hello %s, it is nice to meet you.\n",qC.current());
}
}

void gain(qCommand& qC, Stream& S) {
if ( qC.next() != NULL) {
loopGain = atof(qC.current());
if (loopGain < 0) {
loopGain = 0;
} else if (loopGain > 10) {
loopGain = 10;
}
}
S.printf("The gain is %f\n",loopGain);
}

void multiply(qCommand& qC, Stream& S) {
double a;
int b;
if ( qC.next() == NULL) {
S.println("The multiply command needs two arguments, none given.");
return;
} else {
a = atof(qC.current());
}
if ( qC.next() == NULL) {
S.println("The multiply command needs two arguments, only one given.");
return;
} else {
b = atoi(qC.current());
}
S.printf("%e times %d is %e\n",a,b,a*b);
}

void help(qCommand& qC, Stream& S) {
S.println("Available commands are:");
qC.printAvailableCommands(S);
}

void UnknownCommand(const char* command, qCommand& qC, Stream& S) {
S.printf("I'm sorry, I didn't understand that. (You said '%s'?)\n",command);
S.println("You can type 'help' for a list of commands");
}