Objective: A lightweight shell that runs on a microcontroller (MSP430) and parses incoming data into a command and additional parameters.
Requirements:
- Support of quoting in parameters using
"
- Escaping special characters using
\
in parameters- Ending commands with
\r
or\n
- Separating parameters using
or
\t
- Sending NACK followed by a line break in the event of an error
- Locking (not accepting new commands) the shell during processing
NOT required:
- Support for entering special characters with leading backslash (
\n
,\015
, ...)- Support for backspace
- Support for ending commands using
\r\n
Circumstances:
Though the microcontroller in use is a pretty powerful one, the shell shall run in a low power mode and only activate the main program once the command is fully read in. When an character is received an interrupt is triggered, parsing should be done in that ISR routine to leave the program in low power mode – therefore the implementation used needs to be fast and small.
I thought a finite state machine is good enough for my parsing requirements and this is what I ended up with:
/* [...] */
// Control characters.
#define ACK 0x06
#define NACK 0x15
// Limitations for the shell.
#define SHELL_MAX_PARAMETER_LENGTH 32
#define SHELL_MAX_PARAMETERS 6
#define SHELL_MAX_CMD_LENGTH 32
// Shell signals.
#define SHELL_SIGNAL_OK {USART_Send_Data(ACK); USART_Send_String("\r\n");}
#define SHELL_SIGNAL_ERROR {USART_Send_Data(NACK); USART_Send_String("\r\n");}
/* [...] */
// Global variables.
/**
* @var
* The command that shall be executed by the shell.
*/
char Cmd[SHELL_MAX_CMD_LENGTH] = "";
/**
* @var
* An array of arguments for Cmd. Not including the command itself.
*/
char Parameters[SHELL_MAX_PARAMETERS][SHELL_MAX_PARAMETER_LENGTH];
/**
* @var
* The current index in Parameters.
*/
int Parameters_i = 0;
/**
* @var
* Flag that signals that the program is busy and will not process new commands.
*/
int Busy = 1;
/**
* @var
* Flag that signals that a new command has been received by the shell.
*/
int NewCommand = 0;
void main(void)
{
/* [...] */
// Start listening for commands.
Busy = 0;
while(1) {
// Sleep until the shell wakes up the main program.
LPM3; // Enter low power mode.
// Check if there is a new command waiting for execution.
if(NewCommand) {
// Process the new command.
exec_command();
// Listen for new commands.
Busy = 0;
NewCommand = 0;
}
/* [...] */
}
}
/**
* Executes the current command.
*/
void exec_command()
{
#define CMD_IS(X) (strcasecmp(Cmd, X) == 0)
if (CMD_IS("help")|| CMD_IS("?"))
shell_help(); // Prints a helpage.
else if (CMD_IS("Acknowledge"))
SHELL_SIGNAL_OK
/* [...] (More commands) */
else
SHELL_SIGNAL_ERROR
#undef CMD_IS
}
/**
* Interrupt that gets triggered when a character is received on the UART.
*
* A finate state machine interprets the input on the serial interface and
* extracts command and parameters. Supports quoting using '"' and
* escaping special characters using '\' in parameters.
*/
#pragma vector=USCIAB0RX_VECTOR
__interrupt void USCIAB0RX_ISR(void)
{
enum TStates {
CMD,
PARAMETER,
PARAMETER_BACKSLASH,
PARAMETER_QUOTE,
PARAMETER_QUOTE_BACKSLASH
};
static enum TStates state = CMD;
static int cmd_i = 0, parameter_i = 0, parameters_i = 0;
// Save the current received character in current_char.
char current_char = USART_RxData_Reg;
fUSART_Receive_Set;
// Check that the program currently accepts new commands.
if (Busy) {
goto ERROR;
}
// Transitions.
//
// Decide into which state to change.
// Then jump to one of the actions listed below.
if (state == CMD) {
switch (current_char) {
case ' ':
case '\t':
state = PARAMETER;
goto FINISH;
case '\n':
case '\r':
state = CMD;
goto CALL;
default:
state = CMD;
goto NEXT_CMD_CHAR;
}
} else if (state == PARAMETER) {
switch (current_char) {
case '\\':
state = PARAMETER_BACKSLASH;
goto FINISH;
case '"':
state = PARAMETER_QUOTE;
goto FINISH;
case ' ':
case '\t':
state = PARAMETER;
goto NEXT_PARAMETER;
case '\n':
case '\r':
state = CMD;
goto CALL;
default:
state = PARAMETER;
goto NEXT_PARAMETER_CHAR;
}
} else if (state == PARAMETER_QUOTE) {
switch (current_char) {
case '"':
state = PARAMETER;
goto FINISH;
case '\\':
state = PARAMETER_QUOTE_BACKSLASH;
goto FINISH;
default:
state = PARAMETER_QUOTE;
goto NEXT_PARAMETER_CHAR;
}
}
// Do not interpret characters after a backslash,
// treat them as a normal character.
// Go back into the previous state.
else if (state == PARAMETER_QUOTE_BACKSLASH) {
state = PARAMETER_QUOTE;
goto NEXT_PARAMETER_CHAR;
} else if (state == PARAMETER_BACKSLASH) {
state = PARAMETER;
goto NEXT_PARAMETER_CHAR;
}
// Actions.
NEXT_CMD_CHAR:
if (cmd_i < SHELL_MAX_CMD_LENGTH) {
Cmd[cmd_i++] = current_char;
} else {
goto ERROR;
}
goto FINISH;
NEXT_PARAMETER:
if (parameters_i < SHELL_MAX_PARAMETERS) {
Parameters[parameters_i++][parameter_i] = '\0';
parameter_i = 0;
} else {
goto ERROR;
}
goto FINISH;
NEXT_PARAMETER_CHAR:
if (parameter_i < SHELL_MAX_PARAMETER_LENGTH) {
Parameters[parameters_i][parameter_i++] = current_char;
} else {
goto ERROR;
}
goto FINISH;
CALL:
// Copy current index of parameters.
Parameters_i = parameters_i;
// End strings.
Cmd[cmd_i] = '\0';
Parameters[parameters_i][parameter_i] = '\0';
// Reset counters.
parameter_i = 0;
cmd_i = 0;
parameters_i = 0;
// Wake up the main program and signal that their is a new command.
// Mark the program as busy.
LPM3_EXIT; // Exit low power mode.
NewCommand = 1;
Busy = 1;
goto FINISH;
ERROR:
// Reset counters and read in new commands.
state = CMD;
cmd_i = 0;
parameters_i = 0;
parameter_i = 0;
// Signal the error.
SHELL_SIGNAL_ERROR
goto FINISH;
FINISH:
return;
}
Which works but also looks pretty scary.
Do you have any suggestions about improving this code, or can you think of a smarter way? Since I'm not a native – remarks concerning grammar, spelling and the usage of English are welcome too.
Follow-Up
Based on the posted answers, I've restructured my code to use a table based mealy machine and asked for another review here.