Simple command parser (Part 1)

Part 1, 2

Many applications require some interaction with the user. And one of the most basic manner to interact with an application is to use the serial communication port. Using Serial.print command is probably one the earliest command we all used to debug applications. The proposed command parser uses Serial.available() and Serial.read() functions and few lines of code.

As usual, I tried to make the code as simple and explicit as possible. It does not use additional libraries or tricky functions. This principle of operation is illustrated by the following code, which might be called “The ultimate blink sketch” or “Why make things simple when you can make them complex?”. The idea is to interact with the led blinking process by setting the blink period and the on/off ratio. Additional commands will blink the led, turn it steady on or off.

Instructions are passed to the application using opcodes and arguments. As it is easier to memorize a letter – typically the initial of an instruction – opcodes are single characters as proposed below:

const uint8_t OPC_NONE = 0x00;
const uint8_t OPC_BLINKS = 'B';
const uint8_t OPC_ACTIVATED = 'A';
const uint8_t OPC_ON_RATIO = 'O';
const uint8_t OPC_PERIOD = 'P';
const uint8_t OPC_LST_PARAM = 'L';
const uint8_t OPC_RST_PARAM = 'R';
const uint8_t OPC_SAV_PARAM = 'S';
const uint8_t OPC_ERROR = 'E';

The argument can be a signed integer or a floating point value. So that setting the blink duration for 1000 ms will result from the D1000 instruction. On the other hand, setting the on/off ratio to 10/90% will result from the O0.1 instruction. Note that D 1000 or O 0.1 will work too.

Parsing the instructions is performed as per the following sequence:

  • Read available characters up to the carriage return and/or line feed characters. Alphabetical characters are recognized as opcodes while numerical characters are recognized as numerical arguments.
  • Apply some basic math to format the argument properly.
  • Parse opcodes and set variables using the formatted argument values

The whole code sits in one single routine which is to be periodically executed.

void ParseUartData(void)
{
	if (Serial.available() > 0) {
		/* As long as data is available from the com port */
		while (Serial.available() > 0) {
			/* Set variables to their default values */
			uint8_t opcode = 0;
			bool opcodeDetected = false;
			int32_t intArgument = 0;
			float fltArgument = 0.0;
			bool negative = false;
			uint32_t argumentDivisor = 1;
			bool decSepDetected = false;
			/* While cr or nl charaters are not met */
			while (true) {
				/* Read next char */
				uint8_t dataIn = Serial.read();
				/* Interpret character */
				if ((dataIn == ' ')) {
					/* Spaces are accepted */
				} else if ((dataIn == 13) || (dataIn == 10)) {
					break;
				} else if ((dataIn >= 'A') && (dataIn <= 'Z')) {
					/* Parse opcode */
					if (!opcodeDetected) {
						opcode = dataIn;
						opcodeDetected = true;
					}
				} else if ((dataIn == '-')) {
					/* Parse sign */
					if (opcodeDetected) {
						if (intArgument == 0) {
							negative = true; /* Set sign */
						} else {
							/* Improper sign location */
							opcode = OPC_ERROR;
							break;
						}
					}
				} else if ((dataIn == '.')) {
					/* Parse decimals */
					if (opcodeDetected) {
						if (!decSepDetected) {
							decSepDetected = true;
						} else {
							/* Improper decimal separator location */
							opcode = OPC_ERROR;
							break;
						}
					}
				} else if ((dataIn >= '0') && (dataIn <= '9')) {
					/* Parse argument */
					if (opcodeDetected) {
						/* Shift previous value */
						intArgument *= 10;
						/* Add data to previous value */
						intArgument += (dataIn - '0'); 
						if (decSepDetected) {
							argumentDivisor *= 10;
						}
					}
				} else {
					/* Unexpected character */
					opcode = OPC_ERROR;
					break;					
				}
				delay(1);
			}
			/* Apply argument divisor and sign */
			if (negative) {
				intArgument = -intArgument;
			}
			fltArgument = (float(intArgument) / float(argumentDivisor));
			/* Parse opcodes */
			if (opcode != OPC_NONE) {
				/* Uncomment next lines for debug */
				// Serial.print("opcode: ");
				// Serial.print(char(opcode));
				// Serial.print(", argument: ");
				// Serial.print(intArgument);
				// Serial.print(" (");
				// Serial.print(fltArgument, 4);
				// Serial.print(")");
				// Serial.println();			
				switch(opcode) {
				case OPC_ACTIVATED:
					Serial.print("Turn led ");
					if (intArgument == 1) {
						*(_ledPort) |= _ledPinMask; 
						Serial.println("on");
					} else if (intArgument == 0) {
						*(_ledPort) &= ~_ledPinMask; 					
						Serial.println("off");
					}
					_blinks = 0;
					break;			
				case OPC_BLINKS:
					Serial.print("Blinks = ");
					Serial.println(intArgument);
					_blinks = intArgument;
					/* Reset blink counter */
					_blinkCounter = 0;
					break;
				case OPC_ON_RATIO:
					Serial.print("On/off ratio = ");
					Serial.println(fltArgument, 2);
					_onRatio = fltArgument;
					break;
				case OPC_PERIOD:
					Serial.print("Period = ");
					Serial.println(intArgument);
					_period = intArgument;
					break;
				case OPC_LST_PARAM: /* List parameters */
					PrintParameters();
					break;
				case OPC_RST_PARAM: /* Reset parameters */
					Serial.print("Reset parameters... ");
					GetDefaultParameters(bool(intArgument));
					Serial.println("done");
					break;
				case OPC_SAV_PARAM: /* Save parameters */
					Serial.print("Save parameters... ");
					SetDefaultParameters();
					Serial.println("done");
					break;
				case OPC_ERROR: /* Error */
					/* Report error */
					break;
				}
				/* Update subsidiary variables */
				_onDuration = (_period * _onRatio);
				_offDuration = (_period - _onDuration);
			}
		}
	}
}

Check the few tricks within the code which care about the sign, and the recognition of floats. The ‘-‘ should always lead the argument, and one  single decimal point is accepted. The .1 notation is accepted. These are basic protections and one could easily improve the ruggedness of this code. This code has few limitations such as numerical capacity, however it will work fine in 99% cases. Integers and floating point values must fit the −2147483648 to 2147483647 range and floats are limited to the same range. The precision of floating point values is affected by the loss of precision during floating point arithmetic. Spaces are not illegal, however all other characters generate an error.

Next post on same subject

 

Leave a Reply

You must be logged in to post a comment.