Creating a Command Line Interface in Arduino’s Serial Monitor

We briefly touched upon this topic in our post about Arduino serial input, but inspired by this excellent tutorial by Stephen Brennan we thought it would be fun to create a more rigid command line interface (aka. CLI) for the Arduino than what we did in the serial input post. We recommend you read Brennan’s tutorial regardless since it contains a lot of important info about this topic and programming in general which we won’t mention here.

Having a CLI on an embedded system like the one in this blog post is quite different from a shell in Linux for instance since you usually don’t have an OS to back you up with multiprocessing and all that jazz. There are ways around this, but we’ve kept everything sequential for simplicity’s sake in this example. This means that several tasks can’t run simultaneously and that one task has to finish before starting a new one.

What our CLI looks like in Arduino’s serial monitor.

The purpose of a CLI like this on an Arduino is to have the ability to send text commands to it during runtime to execute certain tasks, such as controlling a servo, displaying text on a screen or launching a rocket.

We’re basically going to go quickly go through the code in chunks in this post. But first we’ll briefly explain what our program does.

Our CLI

The primary purpose of this specific code is to control the on-board LED tied to pin 13 on the Arduino. You can either turn it on, off or having it blink 10 times at a given frequency. We’ve also included a help command which explains what each command does as well as an exit command which just puts the program in a while(1) state.

The Code

The code itself is just over 200 lines where many are just pure printing functions, so the core functionality is not that complex.

Setting Things up

Let’s start with the global variables, defines and the setup() and loop() functions.

#define LINE_BUF_SIZE 128	//Maximum input string length
#define ARG_BUF_SIZE 64		//Maximum argument string length
#define MAX_NUM_ARGS 8		//Maximum number of arguments

int LEDpin = 13;
int blink_cycles = 10;		//How many times the LED will blink
bool error_flag = false;

char line[LINE_BUF_SIZE];
char args[MAX_NUM_ARGS][ARG_BUF_SIZE];

//Function declarations
int cmd_help();
int cmd_led();
int cmd_exit();

//List of functions pointers corresponding to each command
int (*commands_func[])(){
	&cmd_help,
	&cmd_led,
	&cmd_exit
};

//List of command names
const char *commands_str[] = {
	"help",
	"led",
	"exit"
};

//List of LED sub command names
const char *led_args[] = {
	"on",
	"off",
	"blink"
};

int num_commands = sizeof(commands_str) / sizeof(char *);

void setup() {
	Serial.begin(115200);
	pinMode(13, OUTPUT);

	cli_init();
}

void loop() {
	my_cli();
}

When the user enters a command, the program will compare it to the strings in commands_str and call the function in commands_func with the same index.

Top-level Functions

The cli_init() function which is called in setup() just displays a short welcome message while my_cli() is where the the magic starts to happen.

void cli_init(){
	Serial.println("Welcome to this simple Arduino command line interface (CLI).");
	Serial.println("Type \"help\" to see a list of commands.");
}


void my_cli(){
	Serial.print("> ");

	read_line();
	if(!error_flag){
		parse_line();
	}
	if(!error_flag){
		execute();
	}

	memset(line, 0, LINE_BUF_SIZE);
	memset(args, 0, sizeof(args[0][0]) * MAX_NUM_ARGS * ARG_BUF_SIZE);

	error_flag = false;
}

The Meat of the CLI

Three important functions are called within my_cli(). These are:

  • read_line() – this waits for input from the user and stores it in the line string.
  • parse_line() – this divides the input into arguments and stores them in the args list. The delimiter used is a space.
  • execute() – this calls the correct function based on the user’s input.

The memset functions reset the line string and the args list to zero.

void read_line(){
	String line_string;

	while(!Serial.available());

	if(Serial.available()){
		line_string = Serial.readStringUntil("\n");
		if(line_string.length() < LINE_BUF_SIZE){
		  line_string.toCharArray(line, LINE_BUF_SIZE);
		  Serial.println(line_string);
		}
		else{
		  Serial.println("Input string too long.");
		  error_flag = true;
		}
	}
}

void parse_line(){
	char *argument;
	int counter = 0;

	argument = strtok(line, " ");

	while((argument != NULL)){
		if(counter < MAX_NUM_ARGS){
			if(strlen(argument) < ARG_BUF_SIZE){
				strcpy(args[counter],argument);
				argument = strtok(NULL, " ");
				counter++;
			}
			else{
				Serial.println("Input string too long.");
				error_flag = true;
				break;
			}
		}
		else{
			break;
		}
	}
}

int execute(){  
	for(int i=0; i<num_commands; i++){
		if(strcmp(args[0], commands_str[i]) == 0){
			return(*commands_func[i])();
		}
	}

	Serial.println("Invalid command. Type \"help\" for more.");
	return 0;
}

In the read_line() function we don’t really guard against very long inputs. It is still stored in the line_string Arduino string. This means that very long inputs can still mess with our memory.

In the parse_line() function, strtok() is the real work horse. This function splits a string into several substrings where it finds the delimiter specified, which in our case is a white space. Read more about strtok() here.

The execute() function is pretty straight forward. Notice that the function in commands_func at index i is called when returning from this function within the for loop.

Getting Help

int cmd_help(){
	if(args[1] == NULL){
		help_help();
	}
	else if(strcmp(args[1], commands_str[0]) == 0){
		help_help();
	}
	else if(strcmp(args[1], commands_str[1]) == 0){
		help_led();
	}
	else if(strcmp(args[1], commands_str[2]) == 0){
		help_exit();
	}
	else{
		help_help();
	}
}

void help_help(){
	Serial.println("The following commands are available:");

	for(int i=0; i<num_commands; i++){
		Serial.print("  ");
		Serial.println(commands_str[i]);
	}
	Serial.println("");
	Serial.println("You can for instance type \"help led\" for more info on the LED command.");
}

void help_led(){
	Serial.print("Control the on-board LED, either on, off or blinking ");
	Serial.print(blink_cycles);
	Serial.println(" times:");
	Serial.println("  led on");
	Serial.println("  led off");
	Serial.println("  led blink hz");
	Serial.println("    where \"hz\" is the blink frequency in Hz.");
}

void help_exit(){
	Serial.println("This will exit the CLI. To restart the CLI, restart the program.");
}

Here you have all the help functionality. When the user types help and press enter, the cmd_help() function is called. This function checks what the user wrote after help and call the correct function based on that. help_help(), help_led() and help_exit() are just functions that print stuff to the terminal.

LED Control and a Silly Exit Function

int cmd_led(){
	if(strcmp(args[1], led_args[0]) == 0){
		Serial.println("Turning on the LED.");
		digitalWrite(LEDpin, HIGH);
	}
	else if(strcmp(args[1], led_args[1]) == 0){
		Serial.println("Turning off the LED.");
		digitalWrite(LEDpin, LOW);
	}
	else if(strcmp(args[1], led_args[2]) == 0){
		if(atoi(args[2]) > 0){
			Serial.print("Blinking the LED ");
			Serial.print(blink_cycles);
			Serial.print(" times at ");
			Serial.print(args[2]);
			Serial.println(" Hz.");
			
			int delay_ms = (int)round(1000.0/atoi(args[2])/2);
			
			for(int i=0; i<blink_cycles; i++){
				digitalWrite(LEDpin, HIGH);
				delay(delay_ms);
				digitalWrite(LEDpin, LOW);
				delay(delay_ms);
			}
		}
		else{
			Serial.println("Invalid frequency.");
		}
	}
	else{
		Serial.println("Invalid command. Type \"help led\" to see how to use the LED command.");
	}
}

int cmd_exit(){
	Serial.println("Exiting CLI.");

	while(1);
}

The cmd_led() function is the only one that uses three arguments. The third argument (blinking frequency) is used directly in the calculation and not just to to define what function to call.

The cmd_exit() doesn’t check other arguments, so no matter what you write after exit, it will run normally.

Summary

The most important thing to learn from this blog post is the core building blocks my_cli(), read_line(), parse_line() and execute() and the general flow of the program. To make it possible for commands to run “simultaneously” you will have to make some fundamental changes to the program flow and make the read function non-blocking.

Also, as already mentioned, make sure you take a look at Brennan’s tutorial.

Related Posts