Not so simple CLI lib wanted!

Hi,

I'm heavily using serial commands with my arduino to control things and have written a very small and stupid cli interface for that purpose.
I searched through the internet for some lib that is out there that supports commands with flags like in your standard shell e.g.:

mycmd --list -a 1 -b 5% -D

There are a lot of libs out there but they mostly do only support single words as commands.

Any good source I could port something like this into arduino :slight_smile:

scratch

There is nothing preventing you from using one of the countless standard command line parsing utilities, like getopt, in your program.

Regards,
Ray L.

RayLivingston:
There is nothing preventing you from using one of the countless standard command line parsing utilities, like getopt, in your program.

Regards,
Ray L.

Thanks, what I struggle so far is the argv stuff. "getopt" and other mostly want a "char* const*" and this seems hard to get.

The arduino receives a custom string (e.g. led1 -b 50% -t 10) this should result basically in switching the led #1 on for 10 seconds with 50% brightness.

The 'const' designation is just a promise by the function that it won't modify the string via the pointer. The same function will happily accept a 'char *'.

Post your code. Which board?

scratch85:
The arduino receives a custom string (e.g. led1 -b 50% -t 10) this should result basically in switching the led #1 on for 10 seconds with 50% brightness.

For something that trivial, all you need is strtok. Google it. It will do all you need, and a lot more.
Regards,
Ray L.

I’ve this basic code that I use from time to time. Not heavily tested, does the job for me

const size_t maxCLSize = 60;
const size_t maxParameters = 10;

// Crude parser
// we recognize the form: command [option]
// option can be either label=value or just label
// eg accelerate speed=100 distance=33 noCrash
// once parsed we will have
//    instruction.parsingStatus = PARSING_OK            --> the parsing was fine
//    instruction.count = 3                             --> number of parameters
//    instruction.command = "accelerate"                --> what to do
//    instruction.parameters[] = {{"speed", "100"}, {"distance","33"}, {"noCrash", NULL}, {NULL, NULL} ...}
//
// we do not handle quotes in the command such as --> print "Hello World"
// that will lead to
//    instruction.parsingStatus = PARSING_OK
//    instruction.count = 2
//    instruction.command = "print"
//    instruction.parameters[] = {{"\"Hello", NULL}, {"World\"",NULL"}, {NULL, NULL} ...}

struct CLItem_t {
  const char* label;
  const char* value;
};

enum parsingStatus_t : byte {PARSING_OK, PARSING_COMMAND_EMPTY, PARSING_COMMAND_TOO_LONG};

struct cli_t {
  parsingStatus_t parsingStatus;
  size_t count;
  const char * command;
  CLItem_t parameters[maxParameters];
} instruction;



boolean splitCommand(char * c_line, cli_t& instruction)
{
  if (strlen(c_line) == 0) {
    instruction.parsingStatus = PARSING_COMMAND_EMPTY;
    return false; // error, empty command
  }

  char * splitPtr;
  size_t index = 0;

  splitPtr = strtok(c_line, " ");
  instruction.parsingStatus = PARSING_OK;
  instruction.command = splitPtr;
  instruction.count = 0;

  splitPtr = strtok(NULL, " ");
  while (splitPtr != NULL)
  {
    if (index >= maxParameters) {
      instruction.parsingStatus = PARSING_COMMAND_TOO_LONG;
      break;
    } else {
      char * ptrEqual = strchr(splitPtr, '=');
      instruction.count++;
      instruction.parameters[index].label = splitPtr;
      if (ptrEqual) {
        *ptrEqual = '\0';
        instruction.parameters[index].value = ptrEqual + 1;
      } else instruction.parameters[index].value = NULL;
      index++;
      splitPtr = strtok(NULL, " ");
    }
  }

  return (instruction.parsingStatus == PARSING_OK);
}

boolean listenStream(Stream& incomingStream, char *c_line, cli_t& instruction)
{
  static byte indexMessage = 0;
  boolean messageReceived = false;

  if (indexMessage == 0) instruction.parsingStatus = PARSING_COMMAND_EMPTY;

  while (incomingStream.available() && !messageReceived) {
    int c = incomingStream.read();
    if (c != -1) { // -1 indicates an error
      switch (c) {
        case '\n':
          c_line[indexMessage] = '\0'; // c-string correct ending
          indexMessage = 0; // get ready for next time
          messageReceived = true;
          break;

        default:
          if (indexMessage <= maxCLSize - 1) {
            if (!isspace(c) || (indexMessage != 0 && !isspace(c_line[indexMessage - 1])))
              c_line[indexMessage++] = (char) c; // store the received character, but get rid of multiple spaces
          } else {
            instruction.parsingStatus = PARSING_COMMAND_TOO_LONG;
          }
          break;
      }
    }
  }
  return messageReceived;
}


boolean checkCommandLine(Stream& incomingStream, cli_t& instruction)
{
  static char commandLine[maxCLSize + 1] = {'\0'};
  boolean newCommand = listenStream(incomingStream, commandLine, instruction);
  if (newCommand && instruction.parsingStatus != PARSING_COMMAND_TOO_LONG) splitCommand(commandLine, instruction);
  return newCommand;
}

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

void parseCommand( cli_t& instruction)
{

  if (instruction.parsingStatus != PARSING_OK) {
    Serial.print(F("Error parsing new command line: "));
    Serial.println(instruction.parsingStatus);
  } else {
    Serial.print(F("\nWe dealt with a new command line -> "));
    Serial.println(instruction.command);
    for (size_t i = 0; i < instruction.count; i++) {
      Serial.print(F("P")); Serial.print(i);
      Serial.print(F("\t")); Serial.print(instruction.parameters[i].label);
      if (instruction.parameters[i].value) {
        Serial.print(F(" <- ")); Serial.print(instruction.parameters[i].value);
      } // end value
      Serial.println();
    } // end for
  }
}


void loop() {
  if (checkCommandLine(Serial, instruction)) parseCommand(instruction);
  // you can do other stuff here


}

(console set @115200 bauds, sending either CR+LF or LF)

if you type in the Serial console [color=purple]accelerate speed=100 distance=33 noCrash[/color] and hit return

you will see in the console the result of the split of the command line:

[color=blue]We dealt with a new command line -> accelerate
P0	speed <- 100
P1	distance <- 33
P2	noCrash
[/color]

if you type in the Serial console [color=purple]Hello World[/color] and hit return

you will see in the console the result of the split of the command line:

[color=blue]We dealt with a new command line -> Hello
P0	World[/color]

This is just a command splitter, you still need to parse the resulting structure.

Note that in order to conserve memory and not use dynamic allocation, there is only one static buffer shared for all instructions, so if you need to remember something across commands, then you need to do that in the parsing stage because next input will mess with the pointers.