Simple-yet-powerfull - Serial Command input with numbers

This is an invitation for comments by the those who feel experienced enough (you know who you are :slight_smile: )

Sometime(often) people ask here how to use the Serial input to give simple commands, possibly with some numeric argument to their sketch. The thread then revolves around ASCII <-> Integer conversion and what a "terminator" is. For my own use I have evolved the simple syntax: first the number is entered (ASCIIwise) in the Serial input and then terminated by a command character. The code accumulates so it does not block. Sending the ASCII sequence "67x3yp" would be interpreted as three commands: Command X with argument 67, command Y with argument 3 and Command P with argument 0.

Until recently I usually cut-n-paste this codesnippet in my various sketches, I have now rewritten it as a very small library for the community.

The nnnC.h module

/*--------------------------------------------------------------------------------*\
|   nnnC - Number Command input                                                    |
|                                                                                  |
|  Read serial input of simple argument values in format "nnnC"                    |
|   nnn is a series of (ASCII) digits, with optional sign (& decimalpoint for nnnF)|
|   C is a single character that is the command or argument name terminates string |
|   C is converted to uppercase.                                                   |
| Call "fffC" instead of "nnnC" for float value (See prototype declaration below)  |
|                                                                                  |
| The call is non-blocking, so command interpretation is done in its own "thread", |
| meaning : You can call it as often as you want in the loop()                     |
| Only when it returns true will the the arguments C and V contain valid values.   |
|                                                                                  |
| Example use:                                                                     |
|   static char C ; static int V ;                                                 |
|   if ( nnnC( &C, &V) ) {                                                         |
|     Serial.print("Command ");Serial.print(C);                                    |
|     Serial.print(" : ");Serial.print(V,DEC);                                     |
|     }                                                                            |
\*--------------------------------------------------------------------------------*/

/*-------------------------------------------------------------------------*\
| Not-a-bug; known limitations:                                             |
| Illegal numeric syntax, how the "-", "+" and "." are placed, are accepted |
| If you use both nnnC and fffC only "switch" between them after a command  |
| Note the parameters must be global or static                              |
| the nnnC only takes int - maximum is +/-32767 as usual                    |
| (But it is easy - just change "int" to "long" and you can do longs :-) )  |
\*-------------------------------------------------------------------------*/

#ifndef _nnnC
#define _nnnC

#if ARDUINO < 100
#include <WProgram.h>
#else
#include <Arduino.h>
#endif

boolean nnnC( char *, int * ) ;
boolean fffC( char *, float * ) ;

#endif

The nnnC.cpp module

/*--------------------------------------------------------------------------------*\
|   nnnC - Number Command input                                                    |
|                                                                                  |
|  Read serial input of simple argument values in format "nnnC"                    |
|   nnn is a series of (ASCII) digits, with optional sign                          |
|   C is a single character that is the command or argument name terminates string |
|   C is converted to uppercase.                                                   |
|                                                                                  |
| The call is non-blocking, so command interpretation is done in its own "thread", |
| meaning : You can call it as often as you want in the loop()                     |
| Only when it returns true will the the arguments C and V contain valid values.   |
|                                                                                  |
| Example use:                                                                     |
|   static char C ; static int V ;                                                 |
|   if ( nnnC( &C, &V) ) {                                                         |
|     Serial.print("Command ");Serial.print(C);                                    |
|     Serial.print(" : ");Serial.print(V,DEC);                                     |
|     }                                                                            |
| Use "fffC" instead of "nnnC" for float value                                     |
\*--------------------------------------------------------------------------------*/

#include "nnnC.h"

// Integer version
// NB: The "." acts a command character, not part of a number.
boolean nnnC (char * Cmd, int * Val) {
  static boolean Done = true ;
  static boolean Negative = false ;
  if ( Done ) {                        // if a valid value has been returned before
    Negative = Done = false ;          //   reset for new command
    *Val = 0 ;
    }
  while ( Serial.available()>0 ) {     // interpret all serial bytes ...
    *Cmd = Serial.read() ;
  	if (*Cmd == '-')                     // negative value
  	  Negative = true  ;                 //    set flag
    else if ('0' <= *Cmd && *Cmd <= '9') // valid digit
          *Val = (*Val) * 10 + (*Cmd) - '0' ; //    accumulate input
  	else if (*Cmd == '+')                // A "+" sign
      ;                                  // ignore
    else {                             	 // It is a non-numeric character, i.e. the command.
      if ('a' <= *Cmd && *Cmd <= 'z') *Cmd &= B01011111 ; // forces uppercase (for 7bit ASCII)
      if ( Negative ) *Val = -*Val ;     // Adjust for negative value
      Done = true ;                      // note we are done
  	  return true ;                      // and exit
     }
   }
  return false ;
}

 // Float version
boolean fffC (char * Cmd, float * Val) {
  static boolean Done = true ;
  static boolean Negative  ;
  static byte Fraction ;
  if ( Done ) {                         // reset for new interpretation.
    Negative = Done = false ;
    *Val = 0 ; Fraction = 0 ;
    }
  while ( Serial.available()>0 ) {      // interpret all serial bytes ...
    *Cmd = Serial.read() ;
	if (*Cmd == '-') {                  // negative value
	  Negative = true ;                 //    set flag
	  return false ;                    //    and exit
	  }
	if (*Cmd == '.') {                  // decimal point
	  Fraction = 1 ;                    //    start counting, and thus flag decimal seen.
	  return false ;                    //    and exit
	  }
    if ('0' <= *Cmd && *Cmd <= '9') {   // valid digit
      *Val = *Val * 10 + *Cmd - '0' ;   // accumulate input
	  if ( Fraction > 0 ) Fraction++ ;  // adjusting for after decimal
      return false ;                    // and exit
	  }
	if (*Cmd == '+')                    // Accept and ignore a "+" sign
	  return false ;
	// It is a non-numeric character - treated as command character
    if ('a' <= *Cmd && *Cmd <= 'z') *Cmd &= B01011111 ; // forces uppercase (for 7bit ASCII)
    if ( Negative ) *Val = -*Val ;      //adjust for negive value
	for( ; Fraction > 0 ; Fraction-- ) *Val /= 10.0 ;  // divide down decimal
    Done = true ;                       // note we are done
	return true ;
   }
 }

Note: This is intended as a very lightweight routine, when a simple input commandparser is needed.

(Edit: fixed a bug on float fractions, cleaned up some comments, added some brackets for clarity)

Inside both functions:

  while ( Serial.available()>0 ) {     // interpret all serial bytes ...

blocks other code.

You also seem to think that the whole command string is going to come through in one go every time. But you can debug that later, after you get bit.

Those who feel experienced enough expect things to go wrong and write error handing for it.

It only "blocks" if there are characters to process though so that's not really a block.

I'd be inclined to ad some parens here

if ( Negative ) *Val = -(*Val) ;

just to force the operator precedence and make it more human friendly.

However I wonder if it would be simpler to just accumulate the string then apply ato?() or scanf() functions to it based on what you see in the string.


Rob

I write code that gets serial and processes it a character at a time (and have posted examples), usually for user I/O but not always. In between serial arriving there's lots of time to do other things like check light level, make sound, etc, which in many cases is the main task with user I/O as the last priority. In those cases, doing nothing but waiting for serial to arrive mos' defnit'lee blocks!

Don't we have an abs() function?

Hi GoForSmoke - thanks for the comment, but you are reading the test the wrong way round: the while is ONLY busy while there ARE characters in the buffer. No characters, it will return at once. Likewise if the number/command is complete. At least that is the intention. And as far as I have tested, it seems to work that way. Here is my little test.

/* Test the nnnC library */

#include "nnnC.h"

void setup() {
  Serial.begin(9600);
  Serial.println("nnnC test");
}
void loop() {
   static char C ; static int V ; static float F ;                                
   if ( nnnC( &C, &V) ) {                           
     Serial.print("Command ");Serial.print(C);      
     Serial.print(" : ");Serial.println(V,DEC);       
     }                                

// An idle LED blink-without-delay to verify nonbloking
   static unsigned long Timer = 0 ;
   static boolean OnOff = true ;
   if ( millis() - Timer > 333 ) {
     Timer = millis() ;
     digitalWrite(13,(OnOff=!OnOff)?HIGH:LOW) ;
   }
}

Graynomad: thanks for the comment about brackets. Using atoi/atof and those require using a buffer (precious RAM bytes) and suddenly there is maximum inputsize (nnnC accepts a 100 digit number - it wont fit in an integer :slight_smile: but no parser or memory overflow). I have not exactly measured it, but have experienced that including the atoi/f will appreciably increase the code size.

Any/Everybody: The design is "minimalistic": It is not foolproof (like "7-5" is legal interpreted as -75) but functional enough.

I read your code, I know what it says. Read my words again please: I do things in between serial characters coming in even the times it is only to evaluate the text as it comes in. Usually though in my code getting commands takes a total back seat to whatever the commands are -for-. Waiting for 2 to 20 serial characters before getting back to watching sensors and controlling leds or other things (gotta have the blinking lights!) -is- blocking.

Playing "little PC" with Arduino is a good way to waste the cycles you get.

GoForSmoke:
Waiting for 2 to 20 serial characters before getting back to watching sensors and controlling leds or other things (gotta have the blinking lights!) -is- blocking.

I don't understand why you say that. Maybe I'm missing something, but it looks to me as if nnnC() will consume all characters that have already been received and then return; I assume it would be called repeatedly and the return value indicates whether a complete command has been received. This looks to me like a sensible way to read and process incoming serial input. I don't see anything in it that would block waiting for new input on the serial port.

It won't block serial.... and it doesn't wait for available so no it's not really blocking.

(Embarassment :astonished: Found a bug in the float version. Now fixed. Original post edited.)