View full example on Github
Abstract
This is a basic terminal example implemented on an Arduino. The purpose is to provide an easy way to be able to GET/SET various settings/registers and execute functions from a terminal program such as RealTerm, Tera Term, or Putty. The basic commands implemented are as follows:
Command | Description |
---|---|
Ver | Prints the firmware version to the console. |
SettingA | First settings register. Accepts one argument between 0 and 10. |
SettingB | Second settings register. Accepts one argument between 0 and 10. |
TaskA | First task |
TaskB | Second task |
Settings
No argument being sent is a GET command. Which means that the Arduino will respond to the terminal with whatever value is currently in memory for that setting.
One argument for the setting will overwrite the current value and print one of the responses below.
Response | Description |
---|---|
NR | Not Recognized |
A | Accepted |
E | Error |
Tasks
Sending the TASK command will toggle to that task and process whatever code you have in the processTasks() function. By toggling, I mean if the current state is IDLE and you send TaskA then it will begin processing TaskA. If the current state is TaskA and you send TaskA then it will return to IDLE. You can switch directly from TaskA to TaskB without returning to IDLE.
Typically, this state machine will be controlled by some timer so it’ll update every 10ms or 100ms but to keep this example simple it’s simply called in the main loop.
Breakdown of the Code
Check the github repo for the full files.
Setup and Main Loop
Our setup is simple. For this example we’re just initializing our state machine and starting our serial. For this example we’re just responding over serial and not performing any other action.
In our main loop we’re constantly checking for incoming data and then processing that incoming data whenever we raise a flag. We’re also executing our state machine.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Setup our hardware void setup() { task = IDLE; Serial.begin(115200); } // Main loop void loop(){ processIncomingSerial(); if(packetDetected) { packetDetected = 0; processPacket(); } processTasks(); } |
Incoming Serial Parsing
Our incoming data is handled in two functions. The first being processIncomingSerial() which fills our buffer and processPacket() which deconstructs that packet.
To build our buffer we’re checking to see if there’s any data to read, reading it, and adding it to our buffer with incrementing our index. To detect if a complete packet has arrived we’re looking for some delimiter. For this example I chose the carriage return character 0x0D since it always appears when you hit the enter key. When that value is observed we stuff that position in the buffer with NULL (0x00) to help our next function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Add incoming serial to our buffer, // raise a flag when the EOL character is found void processIncomingSerial() { unsigned char incomingByte; if (Serial.available() > 0) { incomingByte = Serial.read(); buffer[bufferIndex] = incomingByte; if (incomingByte == delimiter) { packetDetected = 1; buffer[bufferIndex] = 0x00; } bufferIndex ++; } } |
Processing the packet relies on the strtok function. This accepts two arguments. The first being our string that we’re deconstructing and what we’re using for a delimiter. This searches our string until NULL is found, hence us stuffing it in the last entry instead of the carriage return in the previous function. The strtok function is static so if you call it again with NULL as your input string it’ll look for the next entry instead of starting from the beginning. So if we’re expecting a SPACE between the commands and the data being sent then SPACE will be our delimiter. Here I’m just calling it three times to capture the input command and then two arguments. If we’ve reached the end of the string (NULL character) then the strtok calls will just return NULL themselves. Based on what command you sent to the Arduino will determine which handler function is used. Any additional arguments sent will just be ignored.
One additional thing to note is the equalsIgnoreCase. As the name implies it ignores case of the input so it could be lowercase, UPPERCASE, or CaMeLcAsE. Using a couple of if/else statements here is fine if you just have a couple but you could create a list of these and cycle through it to determine which handler function to call. For this example I kept it as simple as possible.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
void processPacket() { String command, arg1, arg2; command = strtok(buffer,commandDelimiter); // Retrieve our command. arg1 = strtok(NULL,commandDelimiter); // Add more strtok commands arg2 = strtok(NULL,commandDelimiter); // if more arguments are needed // Look for our commands if (command.equalsIgnoreCase(command_firmwareVersion)) { Serial.println(firmwareVersion); } else if (command.equalsIgnoreCase(command_settingsA)) { settingA_handler(arg1, arg2); } else if (command.equalsIgnoreCase(command_settingsB)) { settingB_handler(arg1, arg2); } else if (command.equalsIgnoreCase(command_taskA)) { taskA_handler(); } else if (command.equalsIgnoreCase(command_taskB)) { taskB_handler(); } // Print command not recognized response else { Serial.println(response_notRecognized); } bufferIndex = 0; // Reset our buffer } |
Handler Functions
For the handlers for the settings, I’m just sending all the available arguments. If the argument is NULL then we’re assuming the user wants to retrieve the data so it’s sent down. If it’s not then we convert that string to an int, check it, and then change the global variable. If it’s within bounds we’ll respond with an ACCEPT and if it isn’t then we’ll respond with an ERROR
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void settingA_handler(String arg1, String arg2) { unsigned int temp_settingA = settingA; // If there's an argument, attempt to set it if (arg1 != NULL) { temp_settingA = atoi(arg1.c_str()); if ((temp_settingA > 0) && (temp_settingA <= 10)){ settingA = temp_settingA; Serial.println(response_accepted); } else { Serial.println(response_error); } } // If there isn't an argument, then we're getting the value else { Serial.println(String(settingA)); } } |
The tasks are very similar. There’s no arguments for these so we adjust the current state. The below function is for TASK_A so if we’re not in that state we change to it and if we are we return to an idle state.
1 2 3 4 5 6 7 8 9 10 11 |
void taskA_handler() { // Toggle if (task != TASK_A) task = TASK_A; else task = IDLE; // Debug print Serial.println(stateNames[task]); } |
Tasks and States
Our states are defined globally and just have the two for the tasks and an idle.
1 2 3 4 |
// Our list of tasks enum Tasks { IDLE, TASK_A, TASK_B } task; #define IDNAME(name) #name const char* stateNames[] = {IDNAME(IDLE), IDNAME(TASK_A), IDNAME(TASK_B)}; |
The state machine for this is called in the main loop constantly. Typically, this will be driven by a timer but for brevity and learning purposes it’s just called every loop. The state machine here is just a case statement. To observe the different states you can uncomment the debug code in the processTasks() function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Our state machine to process our tasks. // Typically this would be called based on some timer. void processTasks() { switch(task){ case TASK_A: // perform taskA funcitons // Serial.println("TaskA"); // delay(100); return; case TASK_B: // perform taskB funcitons // Serial.println("TaskB"); // delay(100); return; default: // default state should be idle return; } } |