Skip to content

GettingStarted

CreateiveRobotics edited this page Mar 9, 2020 · 5 revisions

The goal of Commander was quite simple. Make it quick, and relatively easy, to build flexible and useful text based command systems for Arduinos.

Commander does this by reading a Stream object, usually a Serial port, but it can also read File Streams, or any other Stream object. When it reads a Stream it adds incoming bytes to its internal buffer, a String object called bufferString. When Commander encounters an end of line character (usually an ASCII newline character) it checks to see if the contents of the buffer begin with any of its user commands or internal commands. If it finds a match it then calls the command handler.

This command handler is written by the user to do whatever job they want doing when that command is sent.

The process of building your command system has two parts - the command list, and the command handlers.

Command lists

Command lists are arrays of data, each item in the array contains three pieces of data:

  • The command text
  • The command handler function name
  • Some optional help text that explains what the command does

The command list needs to be declared in your sketch, here is an example of a command list with a single command:

const commandList_t commands[] = {
  {"hello",      helloHandler, "Say hello"},
};

The process of adding a new command to the list is just a matter of copying the first item, pasting it into the list, and editing the contents, for example here we will add a command called goodbye:

const commandList_t commands[] = {
  {"hello",      helloHandler, "Say hello"},
  {"goodbye",      goodbyeHandler, "Say goodbye"},
};

This has added a new command called goodbye, but on its own this won't compile because we also need to create functions - command handlers - that will be called when these commands are sent.

Command handlers

Command handlers are functions that will be called when a particular command is found. These will do whatever you want to do when their command is issued, but the function needs to have the right format. It needs to return a boolean value, and it needs to have a single argument - a reference to a Commander object.

Here is an example of a blank template for a command handler, you can base all your own commands on this template by copying and pasting it, and editing the function name to match the item in your command list, and the contents to do whatever you want.

bool myFunc(Commander &Cmdr){
  //put your command handler code here
  return 0;
}

To make command handlers to work with the command list above, we need to create two handlers, one for hello and one for goodbye. We start by creating two of these functions, and changing their names as follows

bool helloHandler(Commander &Cmdr){
  //put your command handler code here
  return 0;
}

bool goodbyeHandler(Commander &Cmdr){
  //put your command handler code here
	return 0;
}

Now, when Commander identifies the 'hello' command it will be able to call the helloHandler function, or the goodbyeHandler function if it finds the command 'goodbye'.

Adding a Commander object to your sketch

Adding Commander to your sketch is not quite as simple as a lot of Arduino libraries because of the way it works. There are two little issues to deal with:

1 Because the command list array contains the names of the command handler functions, the Arduino compiler needs to know about those functions before we put them in the command list.

2 When we initialise Commander in our setup() function we have to tell it the name of the command list. This list needs to have been defined before Setup so that the Arduino compiler knows it exists.

There are several ways to deal with these two issues. The first involves putting the command handler functions, and the command list, in the sketch before the setup() function.

Method 1 - put it all at the top of the sketch.

An example file called Template1-SingleTab shows how to do this. Here we put the two command handler functions near the top of the sketch, followed by the command list array. The setup() and loop() functions come afterwards. When Arduino compiles the sketch into machine code, it sees that the command handlers exist, then it recognises them in the command array, then it recognises the command array being used to initialise the Commander object in the setup() function.

#include <Commander.h>
Commander cmd;

//command handler functions
bool helloHandler(Commander &Cmdr){
  Cmdr.print("Hello! this is ");
  Cmdr.println(Cmdr.commanderName);
  return 0;
}

bool goodbyeHandler(Commander &Cmdr){
  Cmdr.print("Goodbye from ");
  Cmdr.println(Cmdr.commanderName);
  return 0;
}

//Now the command list must be defined
const commandList_t commands[] = {
  {"hello",       helloHandler,     "Say hello"},
  {"set",         goodbyeHandler,   "Say goodbye"},
};

//Now the sketch setup, loop and any other functions
void setup() {
  Serial.begin(115200);
  while(!Serial){;}                               //Wait for the serial port to open (if using USB)
  cmd.begin(&Serial, commands, sizeof(commands)); //start Commander on Serial
  cmd.commandPrompt(ON);                          //enable the command prompt
  cmd.echo(true);                                 //Echo incoming characters to theoutput port
  Serial.println("Hello: Type 'help' to get help");
  cmd.printCommandPrompt();
}

void loop() {
  // put your main code here, to run repeatedly:
  cmd.update();
}

A note about how Arduino builds things: Arduino has its own non-standard way of compiling code that is meant to make sketches simpler and easier to write. When Arduino is compiling it looks through your sketch from start to end, and if the sketch is divided into tabs in the IDE, it connects them all together to make one long page before looking through it all. When it gets to the first function in your code, it will look through the whole sketch and make a record of all the function names in the sketch.

This is why, if the command list array is declared first and before any functions, the compiler won't recognise the names of the command handler functions. If we put the command list array after the first function in your sketch it will suddenly work, even if most or all of the command handlers appear after the setup() function.

To illustrate this, here is the Template1SingleTab sketch, but it has been re-arranged so that all the command handler functions are at the bottom. In order to get it to compile without errors, we have put in a simple function at the top called testFunc(). This function isn't even used anywhere else in out code, but it ensures that the Arduino compiler will create a list of all the functions before it gets to the command list, and enable it to compile without any errors.

What we can't do here is move the command list to after the setup() function. Because the list is a variable, not a function, the way Arduino compiles the code will mean that the array is only 'visible' to any functions that appear after it in the sketch.

#include <Commander.h>
Commander cmd;

void testFunc(){
  cmd.println("Test");
}

//Now the command list must be defined
const commandList_t commands[] = {
  {"hello",       helloHandler,     "Say hello"},
  {"set",         goodbyeHandler,   "Say goodbye"},
};

//Now the sketch setup, loop and any other functions
void setup() {
  Serial.begin(115200);
  while(!Serial){;}                               //Wait for the serial port to open (if using USB)
  cmd.begin(&Serial, commands, sizeof(commands)); //start Commander on Serial
  cmd.commandPrompt(ON);                          //enable the command prompt
  cmd.echo(true);                                 //Echo incoming characters to theoutput port
  Serial.println("Hello: Type 'help' to get help");
  cmd.printCommandPrompt();
}

void loop() {
  // put your main code here, to run repeatedly:
  cmd.update();
}

//command handler functions
bool helloHandler(Commander &Cmdr){
  Cmdr.print("Hello! this is ");
  Cmdr.println(Cmdr.commanderName);
  return 0;
}

bool goodbyeHandler(Commander &Cmdr){
  Cmdr.print("Goodbye from ");
  Cmdr.println(Cmdr.commanderName);
  return 0;
}

Method 2 - Use forward declarations.

Sometimes we might want to put the command list and the command handlers at the bottom of a sketch, or organise them in their own tab. One way of doing this is to 'forward declare' the command list. Forward declarations are a way of telling a compiler that something like a variable, object or function exists, but is properly defined somewhere else in your code. With forward declarations we can tell the Arduino compiler that the command list exists even if it hasn't been defined when we initialise the Commander object in the setup() function.

In the example Template2ForwardDeclaration we have moved the command handlers and the command list to the bottom of the sketch, but forward declared the command list at the top of the sketch so Adruino knows it will be defined later on.

There is one complication we have to deal with. When we initialise Commander we need to tell it the size of the command array so it can work out how many items there are. We can use an in-built Arduino function called sizeof() to get the size of the the command list, but this won't work on the forward declared command list. To get around this we create a new variable after the command list called sizeOfCommands, and initialise it using the sizeof() function. We still need to tell Arduino that this variable exists for when it is used in the setup() function, so its time for another forward declaration.

#include <Commander.h>
Commander cmd;

//forward declare the command list and the size variable
extern const commandList_t commands[];
extern const uint16_t sizeOfCommands;

//Now the sketch setup, loop and any other functions
void setup() {
  Serial.begin(115200);
  while(!Serial){;}                               //Wait for the serial port to open (if using USB)
  cmd.begin(&Serial, commands, sizeOfCommands); //start Commander on Serial
  cmd.commandPrompt(ON);                          //enable the command prompt
  cmd.echo(true);                                 //Echo incoming characters to theoutput port
  Serial.println("Hello: Type 'help' to get help");
  cmd.printCommandPrompt();
}

void loop() {
  // put your main code here, to run repeatedly:
  cmd.update();
}

//Now the command list must be defined
const commandList_t commands[] = {
  {"hello",       helloHandler,     "Say hello"},
  {"set",         goodbyeHandler,   "Say goodbye"},
};
const uint16_t sizeOfCommands = sizeof(commands); //get the size of the command list

bool helloHandler(Commander &Cmdr){
  Cmdr.print("Hello! this is ");
  Cmdr.println(Cmdr.commanderName);
  return 0;
}

bool goodbyeHandler(Commander &Cmdr){
  Cmdr.print("Goodbye from ");
  Cmdr.println(Cmdr.commanderName);
  return 0;
}

This code will compile without any complaints, and we can now move the command list, and all the command handlers, to their own tab in the sketch if we want to.

Method 3 - Create an initialiser function

This final method is a neat trick, and is used in most of the example files. Instead of forward declaring anything, we just move the bit where we initialise the Commander object to its own function, and then call that function from setup().

Now, we can make sure that when Commander is initialised with its command list, and the size of the list, it all happens after the list is declared so the compiler knows about it. All we need to do is put the line cmd.begin(&Serial, commands, sizeof(commands)); in its own function, put that function after the command list, and then call that function from setup().

The example sketch Template3InitFunction illustrates this.

#include <Commander.h>
Commander cmd;

//Now the sketch setup, loop and any other functions
void setup() {
  Serial.begin(115200);
  while(!Serial){;}                               //Wait for the serial port to open (if using USB)
  initialiseCommander();
  cmd.commandPrompt(ON);                          //enable the command prompt
  cmd.echo(true);                                 //Echo incoming characters to theoutput port
  Serial.println("Hello: Type 'help' to get help");
  cmd.printCommandPrompt();
}

void loop() {
  // put your main code here, to run repeatedly:
  cmd.update();
}

//Now the command list must be defined
const commandList_t commands[] = {
  {"hello",       helloHandler,     "Say hello"},
  {"set",         goodbyeHandler,   "Say goodbye"},
};

//Initialisation function
void initialiseCommander(){
  cmd.begin(&Serial, commands, sizeof(commands)); //start Commander on Serial
}

bool helloHandler(Commander &Cmdr){
  Cmdr.print("Hello! this is ");
  Cmdr.println(Cmdr.commanderName);
  return 0;
}

bool goodbyeHandler(Commander &Cmdr){
  Cmdr.print("Goodbye from ");
  Cmdr.println(Cmdr.commanderName);
  return 0;
}
Clone this wiki locally