Advanced serial command processor

Hi All,

In a recent project I've been having a bit of trouble programming a command processor. Basically I need the Arduino to take a serial command and break it up into three components, a designator character, sub-designator integer and value, that reads as follows: "V1 0.025" or "V2 100".
Once broken up, these values will then need to pass through a nested switch-case to in turn call a function with the value present in the above command.

Commands V1-V4 & G1 require float values, as they will be used for on the fly PID tuning, while values V5-V8 require integers. Commands that don't require a value are: G2,G3, M1-M4.

I've definitely though of using if-else statements, however, performance is absolutely critical in this application.

Any help would be appreciated, safe to say this is driving me insane!

Post the code you've written already.

Where are these commands coming from? A person typing on a keyboard? If so, then I have a hard time reconciling that with this statement:

Sorry, I should have clarified a bit better.
Yes, a person will be sending commands via a serial terminal, but I'm also executing a PID control loop in the background to drive a motor.

Code can be found below. The command processor section was modified from a forum post on a different website, unfortunately I can't find exactly where.

*Edit, fixed typo in code.

#define BUF_LEN 6

void handleSerialDebug(){
static char sdata[BUF_LEN], *pSdata=sdata;
char ch;
  if(Serial.available()){
    ch = Serial.read();
    *pSdata = (char)ch;
    *sdata = (char)ch;
    pSdata--; //Dont add \r to string
    //pSdata = sdata;
    char Com1 = sdata[0];
    int Com2 = atoi(sdata[1]);
    *pSdata = '\0'; //Null Terminate String

    Serial.print(Com1);
    Serial.println(Com2);
    
      switch(Com1){
          case 'G':               //G Commands
            switch(Com2){
              case 1:             //G1 Traverse
              
              break;
              case 2:             //G2 Home
                RunMode = 1;
              break;
              case 3:             //G3 Print RS485
                
              break;
            }
          break;
          case 'V':               //V Commands
            switch(Com2){
              case 1:             //V1 Set P
              
              break;
              case 2:             //V2 Set I
              
              break;
              case 3:             //V3 Set D
        
              break;
              case 4:             //V4 Set Deadzone
              
              break;
              case 5:             //V5 Set EndStop upper
              
              break;
              case 6:             //V6 PID profile
              
              break;
              case 7:             //V7 Assigns Controller ID
              
              break;
            }
          break;
          case 'M':
            switch(Com2){
              case 1:             //V1 Default Settings
                Serial.print("Default Settings Written");
                EEPROMWriteDefault();
              break;
              case 2:             //V2 Save Settings
                Serial.print("Settings Saved");
                EEPROMSaveSettings();
              break;
              case 3:             //V3 Print Settings
                EEPROMPrintSettings();
              break;
              case 4:             //V4 Clear Error Codes
              
              break;
            }    
          break;
          
          default:
          Serial.println("Invalid Command");
          break;
        }
        pSdata = sdata;
  }
}

Then you need to grab one single character at a time, if it available, until you get to the end of a command. I assume you have something to tell you that, like a "cr" or "lf". When you get the whole command in YOUR buffer, then process it.
This lets your other processing continue until you have to stop and take care of the command. You can't continue processing because the new command will alter the parameters of the other processing.

Have you placed the function in the context of anything that compiles and runs?

It looks like a work in progress, nevertheless it might be nice to see how it is to be, um, contextualized.

There are things already in there that don't make sense to me.

a7

You found this code somewhere, and it is supposed to work? The above probably doesn't do anything useful.

Forget the PID for the time being, and get the command interpreter working alone.

Must have saved some of my 'insanity code'.
It's meant to read as follows:

    char Com1 = sdata[0];
    int Com2 = atoi(sdata[1]);

Here's the loop function for context

void loop() {
  handleRsNet();
  if(RunMode == 1){homeAxis();}else{runAxis();}      //Calibration sequence stuff
  handleSerialDebug();
}

Please do not go back and edit posted code. It makes the thread almost impossible to follow.

Post REVISED code in a new post. ALL of it.

1 Like

Thanks for the loop. But.

Please post a complete program that compiles. Given that you've called what you posted insanity code, and only also showing a loop function leaves too much to do out here without the possibility of us doing something wrong trying to figure it out.

Post it in a fresh addition to this thread if you do.

TIA

a7

So, it looks like everything you need is in the sdata character array. The main thing I see missing is then converting the numeric value in the array into either an Integer or a Float variable.

You could use something like this...

char inputA[] = "123";
char inputB[] = "789.456";



void setup()
{
  Serial.begin(115200);

  Serial.println(inputA);
  Serial.println(convertToInt(inputA, sizeof(inputA)));

  Serial.println(inputB);
  Serial.println(convertToFloat(inputB, sizeof(inputB)),4);
}

void loop()
{
  
}

int convertToInt(char *input, uint8_t len)
{
  int numberA = 0;
  
  for (uint8_t x = 0; x < len - 1; x++)
  {
    numberA = numberA * 10 + input[x] - 48;
  }
  
  return numberA;
}



float convertToFloat(char *input, uint8_t len)
{
   float numberB = 0.0;
   uint8_t dp = 0;
  
  for (uint8_t x = 0; x < len - 1; x++)
  {
    if (input[x] == '.')
      dp = 1;
    else if (dp == 0)
      numberB = numberB * 10 + input[x] - 48;
    else
      numberB = numberB + (input[x] - 48) * pow(0.1,dp++);
  }

  return numberB;
}

Sorry for wasting your time but it might be easier to start from scratch.

I tried to run a basic version of the code in a separate IDE window, which compiled but wouldn't send anything back to the computer through serial, please see link attached.

There's no change between anything in the snippet and the code required in the main program.

cmd_dec.cpp (12.3 KB)
cmd_dec.h (2.8 KB)

Here is my command processor (it will not work directly w/o other code, e.g. UART, MEM_Pool,
but easy to provide the interfaces).

Features:

  • it parses commands line:
    [-OPTION] [par0 [par1 ...]]

  • it allows several commands on a single line, separated via semicolon ; as well as commented lines via hash #

Examples (for syntax):

  • help

  • debug 0x3 #set my debug flags

  • spitr 0 1 2 3 4 5 6 7 8 #N numbers of values

  • spitr -A 0x1 0x2 0x3 0x4 0x5 0x6 #values as decimal or hex

  • print AAAA BBBB CCCC #even strings or rest of line

It parses for CMDKEYWORD, can have (optional) OPTION (with -), a string parameter (rest of line), up to N numbers of values (where omitted values at the end are taken as zero)

It has a command definition table with help text and function pointers to the command handler.

  • help
    displays all commands with short help for each

  • help
    displays the second longer command help text (just for this CMD)

In order to make it to work:

  • call the function *ECMD_DEC_Status CMD_DEC_execute(char cmd, EResultOut out)
    with a NUL terminated string (it can have a "\r", "\n" or both at the end and even neither of them,
    OUT is just an enum where to send the response (for me: UART of HTTP request or SILENT

  • it needs a UART_Send() function for the output

  • This MEM_PoolAlloc(sizeof(TCMD_DEC_Results)); can be used as a single pointer to memory.
    I have it there and use it when this command interpreter is called from different parallel
    threads (multi-tasking, e.g. UART and from HTTP Web Server, concurrent).
    Function is like malloc(). But for single use: use a pointer to a memory.

Have fun.
(I am using it for long time for many FW projects, I think, quite flexible).

Sorry, my "syntax" description has messed up the page.

Syntax is this:

CMD -OPTION PAR1 PAR2 ... ; CMD2 -OPTION PAR1 ...

  • CMD is the keyword

  • -OPTION is really optional (can be omitted), e.g. -A, -d (see code)

  • PAR1 - a list of N numbers of values (see macro how many) are optional:
    not given or any not given is zero, otherwise a hex or decimal value

The function parses all for you and creates a structure where you have access to:

  • the number of parameters specified on command line

  • the option, if there was one

  • the values (parameters) as well

  • a string (if there was one) or as rest of line

help or help CMD
It is thread-save (multi-tasking) with supporting the MEM_Pool functions
(I avoid to use malloc, MEM_Pool is like malloc but with fix sized chunks and much faster,
w/o risk of memory fragmentation)

Thanks!

If this works, you're a lifesaver.

give it a try. Not very complicated.
Just see which external functions are needed (at least UART_Tx, other functions can be empty or very simple, e.g. this MEM_Pool stuff).

The "beauty": if you get it compile clean, it is very easy to add a new command, help and implementation for it.

Maybe I have commands already there which you do not need or hard to compile (missing stuff).
Just let the "help" command there, remove all other stuff, bring it to work and then add you functions.
You will easily understand how to add a new command:

  • define a prototype for the command handler (all have the same syntax, parameters etc.)

  • create a new entry in the command table, where keyword, help and the pointer to the command handler is defined

  • implement the command handler: parameters on call give you full access to all parts from the command line, e.g. res->val[0] ... for the parsed values (just see the code there)

Have fun and let me know if it helps.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.