Parsing serial text based commands
One of the fun tasks I have at work is creating fun lab projects for teaching classes. In this case, the class was on basic embedded programming and the object was to show how serial input command strings are parsed into their basic components. My pseudo project was a coffee maker, and the command set was relatively simple;
T 12:00 sets the real time clock to 12:00
A 1:00 sets the start time for the coffee maker to 1:00
M turns on the timer
m turns off the timer
? returns the time, start time, and current temperature of the coffee
Now, while the command set looks relatively simple, I also had to make it reasonably fool proof to use as well. So;
1. I had to be able to accept both upper and lower case for the time and alarm commands.
2. I had to accept both single and double digit input for the hours.
3. I had to accept both a ‘ ‘ or a ‘:’ as a data delimiter.
4. And, I had to accept a cr as a delimiter and the termination of the command string.
So, I started out by creating a set a of variables to hold the parsed data;
A CHAR to hold the command character. Note: mt_char is a default character.
cmd_char = mt_char;
And a 4 data variables for the individual nibbles of the hours and minutes
data_var1a = 0;
data_var2a = 0;
data_var1b = 0;
data_var2b = 0;
I then created an init routine to preset the variables to their default values. Note, I use this both at startup, and after I decode the commands, to preset the variables for the next command string.
/******************************************************************************
* Function: void pars_init(void)
*
* Overview: This function configures the parser for operation
*
* Input: none
*
* Output: none
*
******************************************************************************/
void pars_init(void)
{
cmd_char = mt_char;
data_var1a = 0;
data_var2a = 0;
data_var1b = 0;
data_var2b = 0;
mode = cmd;
pars_done = 0;
pars_falt = 0;
}
Note: resetting the variables to zero also provides any leading zeros required for single digit entries.
Next, I created my parser routine in two halves; the first which accepts the individual characters from the serial input and parses the data into blocks, and a second which executes the actual command.
The first section is implemented as a statemachine with 4 states, CMD for command, VAR1 for the first data variables, VAR2 for the second data variable, and DEFAULT which is a general error handler.
In the CMD state, any character that is not a number or a delimiter is stored in the cmd_char variable. If the character is a ‘ ‘, then the statemachine is advanced to the VAR1 state. If the character is a cr, then the parser is done and the command can be decoded by the second half of the routine. If any other character is detected than a pars_falt is set indicating an invalid command.
In the VAR1 state, any number is stored in the data_var1a, after the contents of data_var1a has been shifted into data_var1b. This shift register arrangement allows us to either enter 1 or 2 values because both variables were cleared prior to the start of the command, leaving a leading zero for any single value entry. If the character is a ‘:’, then the statemachine advances to the state VAR2 for a second value entry. If the character is a cr, then the parser is done and the command can be decoded by the second half of the routine. If any other character is detected than a pars_falt is set indicating an invalid command.
The VAR2 state operates nearly identically to VAR1, with the exception that the only delimiter accepted is the cr to terminate the command.
The only function of the default state is to set the fault flag so the second half of the routine can reset the statemachine to its default condition.
When the first half of the function is complete, and the pars_done variable is set, then the 5 data variables ( cmd_char, data_var1a, data_var2a, data_var1b, & data_var2b) should have legal values in them, either from data received from the serial port, or default data loaded during the initialization function.
/******************************************************************************
* Function: void parser(void)
*
* Overview: This function disassembles serial command strings
*
* Input: none
*
* Output: values
*
******************************************************************************/
void parser(void)
{
switch(mode)
{
Case cmd: if (((hold >= ‘a’)&(hold <= ‘z’)) | ((hold >= ‘A’)&(hold <= ‘Z’)) | (hold == ‘?’))
{
cmd_char = hold; // a-z or A-Z or ? detected store as command
}
else if (hold == ‘ ‘)
{
mode = var1; // ‘ ‘ delimiter detected goto next field of data
}
else if (hold == CR)
{
pars_done = 1; // CR detected terminate parser and decode command
}
else
{
pars_falt = 1; // no clue send a ? and restart
}
break;
case var1: if ((hold >= ‘0′) && (hold <= ‘9′))
{
data_var1b = data_var1a; // 0-9 detected store as first data
data_var1a = hold – ‘0′;
}
else if (hold == ‘:’)
{
mode = var2; // ‘:’ delimiter detected goto next field of data
}
else if (hold == CR)
{
pars_done = 1; // CR detected terminate parser and decode command
}
else
{
pars_falt = 1; // no clue send a ? and restart
}
break;
case var2: if ((hold >= ‘0′) && (hold <= ‘9′))
{
data_var2b = data_var2a; // 0-9 detected store as second data
data_var2a = hold – ‘0′;
}
else if (hold == CR)
{
pars_done = 1; // CR detected terminate parser and decode command
}
else
{
pars_falt = 1; // no clue send a ? and restart
}
break;
default: pars_falt = 1; // definitely lost, send a ?
break;
}
Once the data is parsed into its separate elements, it is a simple matter to decode the command and routine the new data into the appropriate variables. In this case; rtc_hours, rtc_mins, alrm_hours, alrm_min, & alrm_on. Based on the value in cmd_char, a simple switch statement is all that is needed to routine the incoming data to the appropriate variables. Once the command decode is complete, then the routine is reset by calling the initialization routine to clear the variables and set them to their default values. In the event that their was a communications fault during the parse or decode operation, then the data is discarded and the system is reset using the initialization routine.
if (pars_done == 1) // if CR detected, then decode the command
{
switch(cmd_char)
{
case ‘T’: // time command uses both data blocks
case ‘t’: rtc_hours = (data_var1b * 10) + data_var1a;
rtc_mins = (data_var2b * 10) + data_var2a;
break;
case ‘A’: // alarm set command uses both data blocks
case ‘a’: alrm_hours = (data_var1b * 10) + data_var1a;
alrm_mins = (data_var2b * 10) + data_var2a;
break;
case ‘M’: alarm_on = 1;
break;
case ‘m’: alarm_on = 0;
break;
case ‘?’: send_all_data(); // ? sends the time, alarm, and temperature
break;
}
pars_init();
}
if (pars_falt == 1)
{
Tx_send(’?'); // send ‘huh?’ message to user
Tx_save(CR);
Tx_save(LF);
pars_init();
}
}