Serial input into Byte array

Hi all,

Working on a project for an arduino328 based device (Uno, pro mini etc) which can control 2 channels of RGB lighting with fading between colours and transistions. It takes it’s commands via I2C or TTL serial and responds appropriately.

For example, if I sent the TTL command “L1S,255,255,255.\n” , this would tell the module that I want:
Light channel 1, Set RGB values R, G, B, stop**.**

What I have a problem with is extracting that 8 bit value from the serial string to a usable integer. It’s probably simple but I always fall at this stage. I2C works fine because I just treat the RGB values as raw 8-bit data during transfers.

The current command handling is probably sloppy and also doesn’t use the ttlColour array at the minute since I’m focusing on getting the parser working then update the second stage. Also I don’t want to use my old method of Alt-0"value" for the RGB values since, altough it worked, it wasn’t convenient especially when using a terminal which I couldn’t use that keyboard combo with

Any help would be appreicated

The current code I have is as follows (edited out none related functions):

#include <Wire.h>
//#include <SPI.h>
#include "defs.h"
#include <stdlib.h>

unsigned long currentTime = 0;

char iicReceived[16] = {};
byte iicToSend[8] = {};
byte iicLength = 0;
byte iicSendLength = 0;

// 8-Bit array to hold RGB/w? brightness for comms
byte ttlColour[4] = {};


struct RGB {
  byte red;
  byte green;
  byte blue;
  byte setRed;
  byte setGreen;
  byte setBlue;
  bool fade = false;
};

// Dual channel RGB
RGB rgb1 = {};
RGB rgb2 = {};

bool cyclicPattern = 0;

void setup() {
  Wire.begin(moduleAddr);
  Wire.onRequest(iicReq);
  Wire.onReceive(iicRec);
  Serial.begin(57600);

  pinMode(red1, OUTPUT);
  pinMode(green1, OUTPUT);
  pinMode(blue1, OUTPUT);
  pinMode(red2, OUTPUT);
  pinMode(green2, OUTPUT);
  pinMode(blue2, OUTPUT);

  // Intro
  Serial.println();
  Serial.print(F(moduleID));
  Serial.print(F(" - "));
  Serial.println(programVer);
  Serial.println(F(make));
  Serial.println(F("---------"));
  Serial.print(F("Type: "));
  Serial.println(moduleType);
  Serial.print(F("Type Ver: "));
  Serial.println(moduleVer);
  Serial.print(F("Chipset: "));
  Serial.println(AVR_BOARD);
  Serial.print( F("Compiled: "));
  Serial.print( F(__DATE__));
  Serial.print( F(", "));
  Serial.println( F(__TIME__));
  Serial.println(F("-----------------------"));
}

void loop() {
  // Receive from host
  if (Serial.available()) {
    serialRec();
  }
  // Transmit to host
  if ( iicSendLength > 0) {
    for (int i = 0; i < iicSendLength; i++) {
      Serial.print(iicToSend[i], HEX);
      Serial.print(", ");
    }
    Serial.println();
    for (int i = 0; i < iicSendLength; i++) {
      char temp = iicToSend[i];
      Serial.print(temp);
      Serial.print(", ");
    }
    Serial.println();
    memset(iicToSend, 0, sizeof(iicToSend));
    iicSendLength = 0;
    //serialCom = 0;
  }
  
  if (cyclicPattern == 1) cycleRGB(1);
  currentTime = millis();
  fadeRGB(currentTime);
  ioUpdate(currentTime);
}

// Parse input from TTL
void serialRec() {
  bool endMsg = 0;
  while (Serial.available() > 0 && endMsg == 0) {
    char rc = Serial.read();
    if (rc == '\n') {
      endMsg = 1;
    } else {
      iicReceived[iicLength] = rc;
      iicLength++;
    }
  }
  if (endMsg == 1) {
    byte colour = 0;                      // 0 = red, 1 = green, 2 = blue, 3 = white?
    Serial.print(F("Serial: "));
    for (int i = 0; i < iicLength; i++) {
      // Parse for RGB values if present
      Serial.print(iicReceived[i]);
      if (iicReceived[i] == ',') {
        Serial.println();
        char val[4] = {};
        for (byte c = 0; c < 3; c++) {
          i++;
          Serial.print(iicReceived[i]);
          if (iicReceived[i] == ',' || iicReceived[i] == '.') {
            val[c] = '\0';
            ttlColour[0] = atoi(val);
            Serial.println(val);
            Serial.println(ttlColour[0]);
            break;
          }
          else val[c] += iicReceived[i];
        }

        //ttlColour[colour] =
      }
    }
    Serial.println();
    if (!commandCode()) {
      Serial.println(F("Command not found"));
    }
    //serialCom = 1;
    memset(iicReceived, 0, sizeof(iicReceived));
    iicLength = 0;
    endMsg = 0;
  }
}


bool commandCode() {
  bool exitCode = 0;
  switch (iicReceived[0]) {
    case 'B': // Assume broadcast ID.
      switch (iicReceived[1]) {
        case 'I':
          //Serial.println(F("Broadcast ID"));
          iicToSend[0] = moduleAddr;
          iicSendLength = 1;
          exitCode = 1;
          break;
        case 'N':
          //Serial.println(F("Broadcast Name"));
          iicToSend[0] = moduleAddr;
          iicSendLength = 1;
          exitCode = 1;
          break;
      }
      break;
    case 'C':
      Serial.print("Cycle pattern ");
      if (cyclicPattern == 0) {
        cyclicPattern = 1;
        fadeRate = 50;
        Serial.println("on.");
      }
      else {
        Serial.println("off.");
        cyclicPattern = 0;
        fadeRate = 30;
        setRGB(0, 0, 0, 1);
      }
      exitCode = 1;
      break;
    case 'L':
      //Serial.print(F("Light Channel "));
      byte channel = iicReceived[1] - '0';
      //Serial.print(channel);
      char mode = iicReceived[2];
      switch (mode) {
        case 'S':
          //Serial.println(F(" set"));
          setRGB(iicReceived[3], iicReceived[4], iicReceived[5], channel);
          exitCode = 1;
          break;
        case '0':
          //Serial.println(F(" blank"));
          setRGB(0, 0, 0, channel);
          exitCode = 1;
          break;
        case 'T':

          //Serial.print(F(" target"));
          if (channel == 1) {
            iicToSend[0] = rgb1.setRed;
            iicToSend[1] = rgb1.setGreen;
            iicToSend[2] = rgb1.setBlue;
            iicSendLength = 3;
            exitCode = 1;
          } else if (channel == 2) {
            iicToSend[0] = rgb2.setRed;
            iicToSend[1] = rgb2.setGreen;
            iicToSend[2] = rgb2.setBlue;
            iicSendLength = 3;
            exitCode = 1;
          }
          break;
        case 'A':

          //Serial.print(F(" actual"));
          if (channel == 1) {
            iicToSend[0] = rgb1.red;
            iicToSend[1] = rgb1.green;
            iicToSend[2] = rgb1.blue;
            iicSendLength = 3;
            exitCode = 1;
          } else if (channel == 2) {
            iicToSend[0] = rgb2.red;
            iicToSend[1] = rgb2.green;
            iicToSend[2] = rgb2.blue;
            iicSendLength = 3;
            exitCode = 1;
          }
          break;
        case 'D':
          Serial.println("Daylight White");
          setRGB(255, 255, 255, channel);
          exitCode = 1;
          break;
        case 'W':
          Serial.println("Warm White");
          setRGB(255, 103, 64, channel);
          exitCode = 1;
          break;
        case 'R':
          setRGB(255, 0, 0, channel);
          exitCode = 1;
          break;
        case 'B':
          setRGB(0, 255, 0, channel);
          exitCode = 1;
          break;
        case 'G':
          setRGB(0, 0, 255, channel);
          exitCode = 1;
          break;
        case 'C':

          //Serial.println(F(" program colour"));
          char colour = iicReceived[3];
          if (colour == 'R') {
            if (channel == 1) rgb1.setRed = iicReceived[4];
            else if (channel == 2) rgb2.setRed = iicReceived[4];
            rgb1.fade = true;
            rgb2.fade = true;
            exitCode = 1;
          } else if (colour == 'G') {
            if (channel == 1) rgb1.setGreen = iicReceived[4];
            else if (channel == 2) rgb2.setGreen = iicReceived[4];
            rgb1.fade = true;
            rgb2.fade = true;
            exitCode = 1;
          } else if (colour == 'B') {
            if (channel == 1) rgb1.setBlue = iicReceived[4];
            else if (channel == 2) rgb2.setBlue = iicReceived[4];
            rgb1.fade = true;
            rgb2.fade = true;
            exitCode = 1;
          }
          break;

      }
      //Serial.println();
      if (exitCode != 0 && cyclicPattern == 1) {
        Serial.println("Cycle pattern off.");
        cyclicPattern = 0;
        fadeRate = 30;
      }
      break;
  }
  return exitCode;
}

Have a look at the examples in Serial Input Basics - simple reliable ways to receive data. There is also a parse example to illustrate how to extract numbers from the received text.

I would just send the data in the form <1,255,255,255> then the position of the items determines how they are interpreted. I don’t see any value in the letters ‘L’ and ‘S’

…R

Seems like you are overcomplicating things, look at this:

#define MAX_LINE_LEN 25

bool readCommand(char* cmd, byte *rgb)
{
  char line[MAX_LINE_LEN]; //One line of data
  char idx = 0, cc; //Buffer index
  while ((idx < MAX_LINE_LEN - 1) && Serial.available())
  {
    cc = Serial.read();
    if (cc == '\n')
    {
      line[idx] = 0;
      break;
    }
    else line[idx++] = cc;
  }
  if (idx >= MAX_LINE_LEN) return false; //Newline was not found
  char* token = strtok(line, ",");
  idx = 0;
  while (token && (idx < 4))
  {
    if (idx == 0) memcpy(cmd, token, strlen(token) + 1); //+1 to include \0
    else rgb[idx-1] = atoi(token);
    token = strtok(NULL, ",");
    idx++;
  }
  return (idx == 4); //Success if 4 tokens was received
}

void loop()
{
  char cmd[20];
  byte rgb[3];
  if (readCommand(cmd, rgb))
  {
    Serial.print("Command: ");
    Serial.print(cmd);
    Serial.print(" R=");
    Serial.print(rgb[0]);
    Serial.print(" G=");
    Serial.print(rgb[1]);
    Serial.print(" B=");
    Serial.println(rgb[2]);
  }
}

Robin2:
Have a look at the examples in Serial Input Basics - simple reliable ways to receive data. There is also a parse example to illustrate how to extract numbers from the received text.

I would just send the data in the form <1,255,255,255> then the position of the items determines how they are interpreted. I don’t see any value in the letters ‘L’ and ‘S’

…R

The only reason I do that is because I also need to read module ids, capabilities and sometimes not a light command so using L is simple and says the following is a light command. S is similar since im setting rgb values. Sending L10 clears the light data, L1W sets it to warm white etc. But i appreciate the layout suggestion, just don’t get how to take 3 chars and interpret that into a byte.

Danios i will look at that code later tonight, am on phone at the minute, thabks for the example

NaokiS:
The only reason I do that is because I also need to read module ids, capabilities and sometimes not a light command so using L is simple and says the following is a light command. S is similar since im setting rgb values.

Then if you send the ‘L’ or ‘S’ as separate entities - like this <L,1,S,255,255,255> - it will be easier to parse.

…R

In my example he could (after readCommand in loop) just get "L" from cmd[0], "1" from cmd[1] and "S" from cmd[2]. If the command always consists of 3 characters there is no real need to separate it further.

Danois90:
Seems like you are overcomplicating things, look at this:

  char* token = strtok(line, ",");

idx = 0;
  while (token && (idx < 4))
  {
    if (idx == 0) memcpy(cmd, token, strlen(token) + 1); //+1 to include \0
    else rgb[idx-1] = atoi(token);
    token = strtok(NULL, “,”);
    idx++;
  }
  return (idx == 4); //Success if 4 tokens was received
}

Sorry to sound a pain, but could you explain how this part works? I’m all for using it but I just dont understand what exactly it does :confused:

I just dont understand what exactly it does

Solve that problem by mentally stepping through the code, line by line, and determine what each line does and why.

Look up what strtok() does.

If you get stuck on a particular point , post again and ask about that point.

jremington: Solve that problem by mentally stepping through the code, line by line, and determine what each line does and why.

Look up what strtok() does.

If you get stuck on a particular point , post again and ask about that point.

Thats the problem, I get the general idea of it but I don't understand how strtok is seperating the string up in such a manageable way. I've been reading on the C++ reference site and even with the description I still dont understand how it's sorted out.

Other than that I can see how the code does seperate out commands and data from positions, just the whole strtok and indexing parts

Which part don't you understand? Source code for strtok and strtok_r / __strtok_r.

Danois90:
Which part don’t you understand?

That snippet. I get the flow of it but not why it works. For example, I don’t know why setting token = strtok gives you multiple sections, or how you can know which part of the string you put in is being read.

Same with the memcpy. I know what memcpy does but not how it gets the seperate values from the input string. or why you later NULL it out

how you can know which part of the string you put in is being read.

It is very simple. Successive calls to strtok() point you to successive pieces of the character string. The first one, second one, etc.

This action is very clearly described in several C/C++ reference sites on the web, like this one.

Please study them, we don't have time or interest to write a new tutorial for people who can't be bothered to read the existing tutorials.

jremington: It is very simple. Successive calls to strtok() point you to successive pieces of the character string. The first one, second one, etc.

Ok, but I only see the call being used with NULL in that while loop, so that confuses me... Unless it will advance until you give it a second string to parse i guess?

I believe the NULL tells it to continue with the cstring it was previously using. The first call the strtok() identifies the ccstring.

Note that strtok() alters the cstring as it works through it so you can't process the same cstring twice unless you made a copy of it before starting with strtok()

...R

When you pass a cstring to strtok, it will return a pointer to the first token of the cstring. When you pass a NULL to strtok, it will return the next token from a previously passed cstring. If you look at the source code of strtok, you can see that there is a static variable in that function that will store the pointer to the cstring. Static variables will "live" even after the method has returned.

As for figuring out where to put the tokens, the variable "idx" (index) is used. When idx is 0, the token is stored to "cmd" using memcpy. When idx is above 0, the token is converted to an integer and stored to "idx-1" in "rgb". As soon as 4 tokens have been properly decoded, the while loop end and a success is returned. If 4 tokens are not decoded ("token" is NULL before idx is 4), failure is returned.

Danois90:
Seems like you are overcomplicating things, look at this:

#define MAX_LINE_LEN 25

bool readCommand(char* cmd, byte rgb)
{
  char line[MAX_LINE_LEN]; //One line of data
  char idx = 0, cc; //Buffer index
  while ((idx < MAX_LINE_LEN - 1) && Serial.available())
  {
    cc = Serial.read();
    if (cc == ‘\n’)
    {
      line[idx] = 0;
      break;
    }
    else line[idx++] = cc;
  }
  if (idx >= MAX_LINE_LEN) return false; //Newline was not found
  char
token = strtok(line, “,”);
  idx = 0;
  while (token && (idx < 4))
  {
    if (idx == 0) memcpy(cmd, token, strlen(token) + 1); //+1 to include \0
    else rgb[idx-1] = atoi(token);
    token = strtok(NULL, “,”);
    idx++;
  }
  return (idx == 4); //Success if 4 tokens was received
}

void loop()
{
  char cmd[20];
  byte rgb[3];
  if (readCommand(cmd, rgb))
  {
    Serial.print(“Command: “);
    Serial.print(cmd);
    Serial.print(” R=”);
    Serial.print(rgb[0]);
    Serial.print(" G=");
    Serial.print(rgb[1]);
    Serial.print(" B=");
    Serial.println(rgb[2]);
  }
}

Sorry to bring this topic back up, only recently got a chance to try this. I’ve tried this in my sketch and it fails to read anything. Making sure it wasn’t my own code causing it, I put it in a blank sketch with only Serial.begin(9600); in the setup and still it is playing up.

What I find is that if I run it as it, I get no output on Arduino 1.8.7. It doesn’t run the if in the main loop. I’ve tried “L1S,255,255,255” and “L1S,255,255,255,” but it doesn’t seem to register it correctly. If I change idx to a byte and have the serial monitor print idx’s value during the while(token && (idx < 4)) loop, it will print indefinitely. The loop repeats forever except in some cases of re-entering the same command it will stop after “processing it” but still it will not parse.

Doing a little extra digging, it seems to loop as token is set to 0xFF from strtok and thus the second while loop is always repeating.

Edit: I had set idx = 0; at the start, removing it fixes the loop but the code still has no proper output. In fact reverting it back to a char causes the serial monitor to scroll endlessly without any command. Code below:

#define MAX_LINE_LEN 25

bool readCommand(char* cmd, byte *rgb)
{
  char line[MAX_LINE_LEN]; //One line of data
  char cc, idx; //Buffer index
  while ((idx < MAX_LINE_LEN - 1) && Serial.available())
  {
    cc = Serial.read();
    if (cc == '\n')
    {
      line[idx] = 0;
      break;
    }
    else line[idx++] = cc;
  }
  if (idx >= MAX_LINE_LEN) return false; //Newline was not found
  char* token = strtok(line, ",");
  idx = 0;
  while (token && (idx < 4))
  {
    if (idx == 0) {
      memcpy(cmd, token, strlen(token) + 1);  //+1 to include \0
    }
    else {
      rgb[idx - 1] = atoi(token);
    }
    token = strtok(NULL, ",");
    Serial.println(idx);
    idx++;
  }
  return (idx == 4); //Success if 4 tokens was received
}

void loop()
{
  char cmd[20];
  byte rgb[3];
  if (readCommand(cmd, rgb))
  {
    Serial.print("Command: ");
    Serial.print(cmd);
    Serial.print(" R=");
    Serial.print(rgb[0]);
    Serial.print(" G=");
    Serial.print(rgb[1]);
    Serial.print(" B=");
    Serial.println(rgb[2]);
  }
}

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

Further digging, it turns out that it was reading from serial far too quickly and resetting itself before the whole command was even registered in the serial buffer. Can confirm this by adding delayMicroseconds(100) at the end of the Serial.available() while loop and it will function properly. Will probably add a timeout routine which will wait for a valid start character, a valid end code and a timer to make sure the total transmission is fully read before it times out and voids the entire command in case of a rogue or misformed command. Hopefully it will also mean it will can track a command and know when to proceed.

EDIT:

I have done the above, posted below for future reference and also maybe someone has some optimisation. Changed the command set to <CMD, DATA, DATA, DATA>\n. Also changed so sending less
or more than 4 is accepted even though parsing into the RGB array doesn’t account for this (will change later, tired as now lol). Sleep is for the weak. Code is in my eyes 100% what I need now. Thanks guys

#define MAX_LINE_LEN 25
#define COMM_TIMEOUT 500

bool readCommand(char* cmd, byte *data)
{
  static char line[MAX_LINE_LEN] = {}; //One line of data
  char cc;
  static byte idx = 0, numOfBytes = 0; //Buffer index
  static long commandTimer;
  static bool startChar, validCommand;

  while ((idx < MAX_LINE_LEN - 1) && Serial.available()) {
    // Read command in and look for newline. If not found, store char in array.
    if (startChar) commandTimer = millis();
    cc = Serial.read();
    //Serial.println(cc);
    if (cc == '\n')
    {
      line[idx] = 0;
      break;
    }
    else line[idx++] = cc;

    if (cc == ',') numOfBytes++;

    // Look for valid start of command. If not received, cancel the command
    if (idx - 1 == 0 && line[idx - 1] == '<' && !startChar) {
      //Serial.println("Start char");
      startChar = 1;
      commandTimer = millis();
      idx = 0;
    } else if (idx - 1 == 0 && line[idx - 1] != '<' && !startChar) {
      //Serial.println("Bad start");
      commandTimer = 0; startChar = 0; validCommand = 0; idx = 0; numOfBytes = 0;
      return false;
    }

    // Look for valid end of command.
    if (line[idx - 1] == '>' && startChar) {
      validCommand = 1;
      line[idx - 1] = 0;
      commandTimer = 0; startChar = 0; idx = 0;
      Serial.println("<OK>");
    }
  }

  // If timer expires since last command and it's not been valid
  if (startChar && (millis() - commandTimer) >= COMM_TIMEOUT && !validCommand) {
    commandTimer = 0; startChar = 0; validCommand = 0; idx = 0; numOfBytes = 0;
    //Serial.println("Comm Timeout");
    return false;
  }

  // If too many chars are detected, cancel
  if (idx >= MAX_LINE_LEN) {
    commandTimer = 0; startChar = 0; validCommand = 0; idx = 0; numOfBytes = 0;
    return false; //Newline was not found
  }

  if (validCommand) {
    //Serial.println("Decode");
    // Set up stringtoken with the received command and the denominator.
    char* token = strtok(line, ",");
    idx = 0;
    // Whilst there is a valid token and number of data bytes, repeat.
    while (token && (idx < numOfBytes+1)) {
      if (idx == 0) {
        // Copy the entire first command into the cmd with memory copy.
        memcpy(cmd, token, strlen(token) + 1);  //+1 to include \0
      }
      else {
        // Convert the current command into an integer and store in the RGB array.
        data[idx - 1] = atoi(token);
      }
      token = strtok(NULL, ","); // I dont know what this bit is but it seems to help so I'll leave it be.
      idx++;
    }
    commandTimer = 0; startChar = 0; validCommand = 0; idx = 0; numOfBytes = 0;
    return 1; // Success 
  }

  // Nothing happened, cancel
  return false;
}

void loop()
{
  char cmd[20];
  byte data[4];   // Most can be received is 4 bytes of data
  if (readCommand(cmd, data))
  {
    Serial.print("Command: ");
    Serial.print(cmd);
    if (cmd[0] == 'L') {
      Serial.print(" R=");
      Serial.print(data[0]);
      Serial.print(" G=");
      Serial.print(data[1]);
      Serial.print(" B=");
      Serial.println(data[2]);
    }
    else Serial.println(" Not RGB");
  }
}

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