Commands over Serial

Any help/direction would be much appreciated. I want to send commands over serial to my arduino from a simple python app. Currently the arduino is setup with a 16,2 LCD and a Keypad. Input the correct keycode and I can rewrite what is displayed on the LED, turn leds on, so on and so forth. This is achieved by sending commands i.e. "LED ON" etc. over serial. At the moment it's currently devolving into a bunch of if {} else if {} and was curious as to if anyone knew of a more elegant way to approach this. Relevant code below. Any help would be greatly appreciated.

      // Keycode was correct so unlock and wait for input on serial port for screen
      while(!customKeypad.getKey()) {
        
        // Do nothing while serial buffer is empty
        while (Serial.available() == 0 ) { }
        
        // If serial buffer isn't empty clear screen and print buffer to screen
        if (Serial.available() > 0) {
          lcd_1.clear();
        }
        serialInput = Serial.readString();
        serialInput.trim();
        
        if (!strcmp(serialInput.c_str(), "LED ON")) {
          
          digitalWrite(ledPin, HIGH);
          delay(1000);

        } else if (!strcmp(serialInput.c_str(), "LED OFF")) {
          
          digitalWrite(ledPin, LOW);
        } else {
          
          lcd_1.print(serialInput);
        }


      }

    } else { // Incorrect keycode entered
      lcd_1.print("Incorrect");
      delay(1000);
    }

Take a look at the "simpleParser" that is part of this project: Duino-hacks/SAMD_Explorer at master · WestfW/Duino-hacks · GitHub

    static const char *cmd_strings =
      PSTR("board ""bytes ""hwords "   "words " "pwm " "interrupts " "sercom "   "pins "   "help "    "? ");
    enum {
      CMD_BOARD, CMD_BYTE, CMD_HWORD, CMD_WORD, CMD_PWM, CMD_INT,    CMD_SERCOM, CMD_PINS, CMD_HELP, CMD_HELP2  // make sure this matches the string
    };

    cmd = ttycli.keyword(cmd_strings); // look for a command.

    switch (cmd) {
1 Like

Cool, thank you.

Or decompose your textual commands into single character commands.
A - set pin 3 to 1
a - set pin 3 to 0
etc.
And simply read a character from serial, and put the character into a switch statement. Easy to test with Serial Monitor, easy to understand if monitoring, easy to debug.
YMMV

3 Likes

Thank you. I was looking at that as an option, but since the hiccup was with using full word commands I didn't want to just give up on it. I wanted to figure out how to solve that specific issue.

2 Likes

I deviated a bit from westfw's suggestion but I found a solution that I think is a bit better than my original code. I was unfamiliar with PSTR() but that directed me to:

PROGMEM | Arduino Documentation

That led me to this solution.

Needed declarations for my current application.

//Define commands
const char LED_ON[] PROGMEM = "LED ON";
const char LED_OFF[] PROGMEM = "LED OFF";

//Array of commands
const char *const commands[] PROGMEM = {LED_ON, LED_OFF};

//Hold current command for comparison
char buffer[10];

//For clarity in switch statement
enum {CMD_LED_ON, CMD_LED_OFF};

//Matched command
int matchedCommand;

Then the functional code.

        //Iterate through list of commands and look for a match
        for (int i = 0; i < 2; i++) {
          
          strcpy_P(buffer, (char *)pgm_read_ptr(&(commands[i])));
          if (!strcmp(serialInput.c_str(), buffer)) {
              matchedCommand = i;
              break;
          } else { //No match default to writing buffer to LCD
            matchedCommand = 3;
          }
        }

        switch(matchedCommand) {
          case CMD_LED_ON:
            digitalWrite(ledPin, HIGH);
            break;
          case CMD_LED_OFF:
            digitalWrite(ledPin, LOW);
            break;
          default:
            lcd_1.print(serialInput);
            break;
        }

Hello bibbityboppityboop

Take a view to get some ideas.

https://www.norwegiancreations.com/2018/02/creating-a-command-line-interface-in-arduinos-serial-monitor/

2 Likes

Cool! Thank you very much :pray:

There is a library that will allow 'commands' to be sent via the Serial port to an Arduino.

There is an example where for instance if you send over serial;

LEDBL,250

The LED on the Arduino blinks with a period of 250mS.

Library here;

https://github.com/gpb01/SerialCmd

2 Likes

Thank you very much.

this may not be better than what you've already started, it uses single letter commands with an option numeric prefix

// pcRead - debugging using serial monitor

const char version [] = "PcRead 240209a";

int debug = 0;

// -----------------------------------------------------------------------------
// process single character commands from the PC
#define MAX_CHAR  10
char s [MAX_CHAR] = {};

int  analogPin = 0;

void
pcRead (void)
{

    static int  val = 0;

    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':
#if !defined(ARDUINO_ARCH_ESP32)
            analogWrite (analogPin, val);
            Serial.print   ("analogWrite: pin ");
            Serial.print   (analogPin);
            Serial.print   (", ");
            Serial.println (val);
            val = 0;
#endif
            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);
            digitalWrite (val, ! digitalRead (val));
            val = 0;
            break;

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

        case '\n':          // ignore
            break;

        case '?':
            Serial.println ("\npcRead:\n");
            Serial.println ("    [0-9] append to #");
            Serial.println ("  # A - set analog pin #");
            Serial.println ("  # D - set debug to #");
            Serial.println ("  # I - set pin # to INPUT");
            Serial.println ("  # O - set pin # to OUTPUT");
            Serial.println ("  # P - set pin # to INPUT_PULLUP");
            Serial.println ("  # a - analogRead (pin #)");
            Serial.println ("  # c - digitalWrite (pin #, LOW)");
            Serial.println ("  # p - analogWrite (analogPin, #)");
            Serial.println ("  # r - digitalRead (pin #)");
            Serial.println ("  # s  - digitalWrite (pin #, HIGH)");
            Serial.println ("    t  - toggle pin output");
            Serial.println ("    v  - print version");
            Serial.println ("    ?  - list of commands");
            break;

        default:
            Serial.print ("unknown char ");
            Serial.println (c,HEX);
            break;
        }
    }
}

// -----------------------------------------------------------------------------
void
loop (void)
{
    pcRead ();
}

// -----------------------------------------------------------------------------
void
setup (void)
{
    Serial.begin(9600);

    Serial.println (version);
#if defined(ARDUINO_ARCH_ESP32)
    Serial.println ("esp32");
#endif
}
2 Likes

Thank you! This is where I'm currently at.

Respective declarations:

//Function declarations
int findCommand(const char* const* cmds, String sInput, const int numCmds);

//Define commands
const char LED_ON[] PROGMEM = "LED ON";
const char LED_OFF[] PROGMEM = "LED OFF";

//Array of commands
const char *const commands[] PROGMEM = {LED_ON, LED_OFF};

//For clarity in switch statement
enum {CMD_LED_ON, CMD_LED_OFF};

//Matched command
int matchedCommand;
const int numOfCommands = sizeof(commands)/sizeof(char*);

Runtime code for original question:

        serialInput = Serial.readString();
        serialInput.trim();
        
        matchedCommand = findCommand(commands, serialInput, numOfCommands);

        switch(matchedCommand) {
          case CMD_LED_ON:
            digitalWrite(ledPin, HIGH);
            break;
          case CMD_LED_OFF:
            digitalWrite(ledPin, LOW);
            break;
          default:
            lcd_1.print(serialInput);
            break;
        }

Actual function handling the sorting.

int findCommand(const char* const* cmds, String sInput, const int numCmds) {

  int mCmd;
  char buff[10];
  //Iterate through list of commands and look for a match
  for (int i = 0; i < numCmds; i++) {

    strcpy_P(buff, (char *)pgm_read_ptr(&(cmds[i])));
  
    if (!strcmp(sInput.c_str(), buff)) {
      mCmd = i;
      break;
    } else {
      mCmd = 3;
  }


  }
  return mCmd;
}

Now I'm on to seperating "commands" from "args" i.e. parsing
LED ON as LED ON
I want to add on more like a servo so I can do SERVO POS etc.
Might have to completely scrap everything I have but oh well.

Thanks to everyone for the input. It's nice to see there're libraries already for this (should've figured as much)
This has turned into a great learning exercise.

Hello Mate Take a view to get some ideas that help to solve your problem. (Python - Syntax Scenarios)

might also consider this approach which splits a multi word command (e.g. led 10 on) into "tokens" and a pass the array of tokens to a command without worrying about the # and types of arguments a cmd might need

output


processCmd: martha
  tokenize: martha
 d:
  tokDisp:
      0 martha

processCmd: tom 1 2 3
  tokenize: tom 1 2 3
 a:
  tokDisp:
      0 tom
      1 1
      2 2
      3 3

processCmd: dick xyz
  tokenize: dick xyz
 b:
  tokDisp:
      0 dick
      0 xyz

processCmd: harry 1 abc 2
  tokenize: harry 1 abc 2
 c:
  tokDisp:
      0 harry
      1 1
      0 abc
      2 2

updated with comments

// multiword command processing
//   split separate word/values (i.e. tokens) in command line
//   search for cmd in command-table, cmds[], and execute if found
//   test with example command lines

char s [90];        // for sprintf()

// -----------------------------------------------------------------------------
// arrays of tokens (char *) and corresponging values if numeric
// a token in one word of text/numeric

#define MAX_TOKS   10
char * toks [MAX_TOKS];
int    vals [MAX_TOKS];
int    nTok;

char buf [90];      // char buffer when input is const char

// -------------------------------------
// display the current arrays of tokens and values
void
tokDisp ()
{
    Serial.println ("  tokDisp:");
    for (int n = 0; n < nTok; n++)  {
        sprintf (s, " %6d %s\n", vals [n], toks [n]);
        Serial.print (s);
    }
}

// -------------------------------------
// locate tokens in provide string, set ptrs in tokens
int
tokenize (
    char *str )
{
    nTok = 0;

    sprintf (s, "  tokenize: %s\n", str);
    Serial.print (s);

    strcpy (buf, str);  // copy possible const char array into char array

    // initially call strtok() with char array, subsequent calls are passed nul
    // continue processing until nul token found
    // attempt to translate all tokens into numberic value
    for (toks [nTok]  = strtok (buf, " "); toks [nTok]; )  {
        vals [nTok]   = atoi (toks [nTok]);
        toks [++nTok] = strtok (NULL, " ");
    }

    return nTok;
}

// -----------------------------------------------------------------------------
// test command functions used to demonstrate processing
void a (char *toks [], int n) { Serial.print (" a:\n"); tokDisp (); }
void b (char *toks [], int n) { Serial.print (" b:\n"); tokDisp (); }
void c (char *toks [], int n) { Serial.print (" c:\n"); tokDisp (); }
void d (char *toks [], int n) { Serial.print (" d:\n"); tokDisp (); }

// -------------------------------------
// command table - function ptr and command string
struct Cmd {
    void (*f) (char *tok [], int nTok);
    const char *name;
}
cmds [] = {             // table for above command functions
    { a, "tom" },
    { b, "dick" },
    { c, "harry" },
    { d, "martha" },
};
const int Ncmd = sizeof(cmds)/sizeof(Cmd);

// -------------------------------------
// tokenize command line and search for cmd, 1st token, in cmds []
void
processCmd (
    char *buf )
{
    sprintf (s, "\nprocessCmd: %s\n", buf);
    Serial.print (s);

    tokenize (buf);

    for (int n = 0; n < Ncmd; n++)  {
        // execute cmd when found and return
        if (! strcmp (cmds [n].name, toks [0]))  {
            cmds [n].f (toks, nTok);
            return;
        }
    }

    // only reached if cmd not found
    sprintf (s, " processCmd: unknown cmd %s\n", toks [0]);
    Serial.print (s);
}

// -----------------------------------------------------------------------------
// define test command lines
char *str [] = {
    (char *)"martha",
    (char *)"tom 1 2 3",
    (char *)"dick xyz",
    (char *)"harry 1 abc 2",
};
const int Nstr = 4;

void setup ()
{
    Serial.begin (9600);
    delay (200);

    // process each test cmd line
    for (int n = 0; n < Nstr; n++)
        processCmd (str [n]);
}

void loop () { }
1 Like

Thank you! This is actually the direction I had started heading.

at least for me personal this is a way too short description of what is happening in your code.

You would have to expand your explanation to an example
"led 10 on"
as far as I understand it
"tokenised" means
The character-sequences

  1. "led"
  2. "10"
  3. "on"

are sperated from each other

But what exactly does it mean if an "array of tokens" (here with three elements are
passed

= single command

How does a single command handle three elements?
and at the same time will be able to
not

?????
For making this understandable your would have to write down two different examples
with different numbers of array-elements and different types of array-elements to demonstrate how THIS works

what is the difference between

and

except for the first word the output looks exactly the same
What is the difference?
and what gets me completely puzzled is
the tokDisp output

characters get a leading 0
numbers are repeated
what rules is behind that?
and how do these rules help to dinstinguish between all kinds of different input?

Without having analysed your code in detail this looks like
as if the only thing what your code does
is separating a longre string
and
at the end again a lot of
if-conditions will be needed to dinstinguish what has to be done with what kind of input

1 Like

presumably in this Arduino case, a command is some function with a varied # and types of arguments. controlling an LED requires a pin # and state. of course you can have an ledOn and ledOff command

the table of commands could identify the # and types of arguement to the command funciton, recast the function ptr to that type of fucntion pointer and invoke that re-casted funciton ptr with the appropriate values

the alternative is that all the command functions have a single argument, a string array and the command argument translate strings to values if necessary or processess the argument as a string

tokDispl() demonstrates this by printing each argment as both an integer and string

deviates from a standard for-loop and requires explanation.
There is a single assigment

Why and how does a single assignment "loop" = run through multiple times?
especcially as your variable

The for-loop is missing the third argument.

what does ntok represent?
and how is it related to the rest of the code?

what is the mechanism behind these functions?

what do the parameters represent?
the functions call

tokDisp ();

which is

How are these functions related to the rest ??? No idea???

You should build up two specific examples that do something like
switch on LED
out a tone

and explain everything step by step what os doing what and what function is calling what

various parts of the code generate some output. the output is presented to see what the code would do. Looking at the code and the output together is intended to help the reader understand what the code is doing

would it be clearer if i didn't post the output?

of course not !

Though as far as your demo-code does output it does not help to explain how it all works.

You mentioned

[quote="gcjr, post:17, topic:1320091"]
the table of commands could identify the # and types of arguement to the command funciton,

what is "the command-function"??

where in which exact line of code does this happen?

where in which exact line of code does this happen?

by all means: you are presenting a high advanced complex coding technique including pointers and function-casting combined with too short to be understandable variable-names and some rather small output.

It would have been helpful if you would have taken it to the level of a library that has a setup on the surface like

hand over a list of parameters wher you can specifiy type char, int, float
and registering a callback-function which the user defines himself and this call-back-function takes the parameters in the same sequence as the list