Serial Input Command Structure

Hello,

I have been experimenting with different ways to process commands through Serial inputs. I am looking to have the functionality to both input commands to execute programming, as well as change program settings, such as by changing the value of various variables.

First, I should mention that I am still fairly new to Arduino, especially working with Serial inputs, so this may be a perplexing approach to those that have more experience. What I have now involves reading and storing Serial inputs into a character array one character at a time, with an index variable keeping track of the current "write" position in the array. If the next character read is '\n', then rather than storing the character, the current contents of the array are compared against a series of preset commands, and various actions are executed. Then, the array is "emptied" with memset() and index is reset to 0.

While this does certainly work, the programming structure seems to be a bit messy and cumbersome for just a few simple commands. I imagine there must be a far simpler and more elegant approach, so I was hoping to see what everyone's thoughts are. How do you structure Serial input commands? What is the simplest and best organized way to structure this?

Also, here's an example of what I have been trying:

//commands
#define COMMAND1 "command1"
#define COMMAND2 "command2"
#define COMMAND3 "command3"
#define COMMAND4 "command4"

char input[100],input_buffer;
int setting1=0,setting2=0,input_mode=0,index=0;

void setup() {
Serial.begin(9600);
}

void loop() {
if (Serial.available()){
  input_buffer=Serial.read();
  if (input_buffer!='\n'){
    input[index]=input_buffer;
    index++;
  }
  else {
    switch (input_mode){

      case 0:
      if (strcmp(input,COMMAND1)==0){
        //do something
        Serial.println(COMMAND1);
      }

      else if (strcmp(input,COMMAND2)==0){
        //do something else
        Serial.println(COMMAND2);
      }

      else if (strcmp(input,COMMAND3)==0){
        input_mode=1;
        //next input changes setting1
        Serial.println(COMMAND3);
      }

      else if (strcmp(input,COMMAND4)==0){
        input_mode=2;
        //next input changes setting2
        Serial.println(COMMAND4);
      }
      break;

      case 1:
      //change setting1
      setting1=atoi(input);
      Serial.println(setting1);
      input_mode=0;
      break;

      case 2:
      //change setting2
      setting2=atoi(input);
      Serial.println(setting2);
      input_mode=0;
      break;
    }
    memset(input, '\0', sizeof input);
    index=0;
  }
}

}

Take look at Serial input basics - updated

this is where almost all my arduino programs start. it evolves as needed

void
pcRead (void)
{
    static int  analogPin = 0;
    static int  val = 13;

    if (Serial.available()) {
        int c = Serial.read ();

        switch (c)  {
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            val = c - '0' + (10 * val);
            break;

        case 'A':
            analogPin = val;
            Serial.print   ("analogPin = ");
            Serial.println (val);
            val = 0;
            break;

        case 'D':
            debug ^= 1;
            break;

        case 'I':
            pinMode (val, INPUT);
            Serial.print   ("pinMode ");
            Serial.print   (val);
            Serial.println (" INPUT");
            val = 0;
            break;

        case 'O':
            pinMode (val, OUTPUT);
            Serial.print   ("pinMode ");
            Serial.print   (val);
            Serial.println (" OUTPUT");
            val = 0;
            break;

        case 'P':
            pinMode (val, INPUT_PULLUP);
            Serial.print   ("pinMode ");
            Serial.print   (val);
            Serial.println (" INPUT_PULLUP");
            val = 0;
            break;


        case 'a':
            Serial.print   ("analogRead: ");
            Serial.println (analogRead (val));
            val = 0;
            break;

        case 'c':
            digitalWrite (val, LOW);
            Serial.print   ("digitalWrite: LOW  ");
            Serial.println (val);
            val = 0;
            break;

        case 'p':
            analogWrite (analogPin, val);
            Serial.print   ("analogWrite: pin ");
            Serial.print   (analogPin);
            Serial.print   (", ");
            Serial.println (val);
            val = 0;
            break;

        case 'r':
            Serial.print   ("digitalRead: pin ");
            Serial.print   (val);
            Serial.print   (", ");
            Serial.println (digitalRead (val));
            val = 0;
            break;

        case 's':
            digitalWrite (val, HIGH);
            Serial.print   ("digitalWrite: HIGH ");
            Serial.println (val);
            val = 0;
            break;

        case 't':
            Serial.print   ("pinToggle ");
            Serial.println (val);
            pinToggle (val);
            val = 0;
            break;

        case 'v':
            Serial.print ("\nversion: ");
            Serial.println (version);
            break;

        default:
            break;
        }
    }
}

You're protocol does not have a way of detecting data corruption or dropped packets. Also, it would be more efficient to send a "command id" as a byte inside the actual packet. If you do this, you can get around having to use strcmp().

Also, once you're done with Serial Input Basics, you should check out Serial Input Advanced (note that the advanced tutorial is currently a work-in-progress).

Lastly, if you want any inspiration code to reference when making your own serial protocol, feel free to check out the source code for this library or this one.

You can try playing with this simple example to see if it does what you want:

const byte pinLED = LED_BUILTIN;

#define BUFF_SIZE       128     //#     message buffer size
#define MSG_TIMEOUT     1000    //mS    timeout from last character received

//create a buffer to hold the received message
byte 
    RxBuffer[BUFF_SIZE];
    
//used to timeout a message that is cut off or too slow
unsigned long
    lastCharacterTime;

//this creates a type that is a pointer to a command function, used in the structure below
typedef void (*CmdFunc)();

//command structure
typedef struct
{
    char    *szCommand;         //command string e.g. "ON", "OFF"; use caps for alpha characters 
    CmdFunc CmdFunction;        //pointer to the function that will execute if this command matches

}structCmdStruct;

//for this example, there are two commands; on and off
#define NUM_CMDS        2

//prototypes of command functions
//need these prototypes before the following CmdTable variable or the compiler
//will complain
void CommandON( void );
void CommandOFF( void );

structCmdStruct CmdTable[NUM_CMDS] = 
{
    {   
        .szCommand = "ON",              //capitalized command for turning the LED on
        .CmdFunction = &CommandON       //if "ON" is received, run CommandON
    },
    {
        .szCommand = "OFF",             //off
        .CmdFunction = &CommandOFF      //off command function
    }
    
};

void setup( void )
{
    //usual start-up drivel; 115200 baud and an LED pin to show functions work
    Serial.begin(115200);
    pinMode( pinLED, OUTPUT );
    digitalWrite( pinLED, LOW );
    
}//setup

void loop( void )
{
    //can be called as part of a loop doing lots of other things
    ReceiveMessage();
    
}//loop

void ReceiveMessage( void )
{
    byte
        ch;
    static byte
        idxMsg = 0;

    //if we've received at least one character, check for a timeout
    if( idxMsg > 0 )
    {
        //if MSG_TIMEOUT mS has passed since the last character, reset the
        //buffer index
        //(i.e. ignore whatever message this might have been...)
        if( (millis() - lastCharacterTime) > MSG_TIMEOUT )
        {
            idxMsg = 0;
            Serial.println( "Message timeout..." );
            
        }//if
        
    }//if

    //if there's a character waiting...
    if( Serial.available() > 0 )
    {
        //update the last char timer
        lastCharacterTime = millis();
        
        //read the character
        ch = Serial.read();
        
        //if it's the newline (example delimter for typical serial monitor messages)
        //this is the end of the message...
        if( ch == '\n' )
        {
            //insert a NULL to "null-terminate" the message
            RxBuffer[idxMsg] = '\0';
            //and go process it
            ProcessMsg();
            //when we come back, reset the buffer index ready for the next msg
            idxMsg = 0;
            
        }//if
        else
        {
            //still receiving the msg
            //convert alphabetical characters to uppercase (don't have to do this but be sure
            //to watch case when typing in the serial monitor!) and store in the buffer
            RxBuffer[idxMsg] = toupper(ch);
            //bump the buffer pointer but not beyond the limits of the buffer
            idxMsg++;
            if( idxMsg == BUFF_SIZE )
                idxMsg--;
        }//else
        
    }//if
    
}//ReceiveMessage

void ProcessMsg( void )
{
    //got a message...walk through the command table to try to find a match
    for( int i=0; i<NUM_CMDS; i++ )
    {
        //if the received message matches the NULL-terminated command string, we
        //have a match...
        if( strcmp( RxBuffer, CmdTable[i].szCommand ) == 0 )
        {
            //...so execute that command's function
            CmdTable[i].CmdFunction();
            //and leave
            return;
                
        }//if
        
    }//for

    //if we make it here we didn't find a match in the command table
    //so just send an "Unrecognized command" response and what the offending message was
    Serial.print( "Unrecognized command: " ); Serial.println( (char *)RxBuffer );
    
}//ProcessMsg

// Command Functions
//these functions execute the commands. They are pointed to in the command table
void CommandON( void )
{
    //turn on the LED and send a message
    Serial.println( "LED on" );
    digitalWrite( pinLED, HIGH );
        
}//CommandON

void CommandOFF( void )
{
    //turn off the LED and send a message
    Serial.println( "LED off" );
    digitalWrite( pinLED, LOW );
    
}//CommandOFF