Send 8-bit integers over serial to Arduino

Question:

I created a music visualizer and I want to send three 8-bit integers (0-255) over serial to an Arduino using Python’s pyserial library.

I have a text file called rgb.txt on my computer which has the data: 8,255,255. I am sending the data over serial with this code:

import serial, time
arduino = serial.Serial('COM3', 9600, timeout=.1)
time.sleep(2) #give the connection a second to settle

while True:
    with open("rgb.txt") as f:
        rgb = f.read().strip("n")
    arduino.write(rgb.encode("ASCII"))

    data = arduino.readline()
    if data:
        try:
            print(data.decode('ASCII').strip("r").strip("n")) # (better to do .read() in the long run for this reason
        except UnicodeDecodeError:
            pass
    time.sleep(0.1)

And I’m receiving it with this code:

#include <stdio.h>
int r = A0;
int g = A1;
int b = A2;
void setup() {
  Serial.begin(9600);
  analogWrite(r, 255); delay(333); analogWrite(r, 0);
  analogWrite(g, 255); delay(333); analogWrite(g, 0);
  analogWrite(b, 255); delay(334); analogWrite(b, 0);
}

void loop() {
  if (Serial.available() > 0) {
    char data = Serial.read();
    char str[2];
    str[0] = data;
    str[1] = '';
    Serial.println(str);
  }
}

The output I am getting is:

8
,
2
5
5
,
2
5
5

How can I parse it so I receive:

8
255
255

And preferably in 3 different variables (r g b).

Asked By: Quintin Dunn

||

Answers:

If you have always a ‘,’ character, you can convert it to int.

uint8_t rgb[3] = {0,0,0};
uint8_t rgbIndex = 0;

void loop() {
  if(Serial.available() > 0) {
    char data = Serial.read();
    if(data < '0' || data > '9')
      rgbIndex++;
    else
      rgb[rgbIndex] = rgb[rgbIndex] * 10 + (data - '0');
  }
}
Answered By: Adriano

What you do now is read a char, turn it into a CString str, and then you println() it before you go on to the next char.

You could probably stick the bytes together the way you want from what you got, but it is easier to read the received bytes into a buffer and split the result:

Send the RGB values from Python separated with commas and with a 'n' on the end, and then on the Arduino do something like this (untested, but you get the idea):

void loop() {
  static char buffer[12];
  static uint8_t rgb[3];

  if (Serial.available() > 0) {
    Serial.readBytesUntil('n', buffer, 12);
    int i = 0;
    char *p = strtok(buffer, ",");
    while (p) {
      rgb[i++] = (uint8_t)atoi(p);
      p = strtok(NULL, ",");
    }
    // You now have uint8s in rgb[]
    Serial.println(rgb[0]);
    Serial.println(rgb[1]);
    Serial.println(rgb[2]); 
  }
}

Note: no checks and error handling in this code.

There are no doubt prettier ways, but this came to mind first and I think it will work. It could also be done using a String object, but I try to avoid those.

For the code in this other answer to work, some things need to be added (but I haven’t tested if these additions are enough):

void loop() {
  if (Serial.available() > 0) {
    char data = Serial.read();
    if (data < '0' || data > '9')
      rgbIndex++;
    else
      rgb[rgbIndex] = rgb[rgbIndex] * 10 + (data - 48);
    if (rgbIndex == 3) {
      // You now have uint_8s in rgb[]
      Serial.println(rgb[0]);
      Serial.println(rgb[1]);
      Serial.println(rgb[2]);
      rgbIndex = 0;
      for (int i=0; i<3; i++)
        rgb[i] = 0;
    }
  }
}

Note that converting what you read from the file to chars or integers on the Python side and simply sending three bytes would greatly simplify things on the Arduino side.

Answered By: ocrdu

Arduino Code:

#include <stdio.h>
int r = A0;
int g = A1;
int b = A2;
void setup() {
  Serial.begin(115200);
  analogWrite(r, 255); delay(333); analogWrite(r, 0);
  analogWrite(g, 255); delay(333); analogWrite(g, 0);
  analogWrite(b, 255); delay(334); analogWrite(b, 0);
}
void loop() {
  static char buffer[12];
  static uint8_t rgb[3];

  if (Serial.available() > 0) {
    Serial.readBytesUntil('n', buffer, 12);
    int i = 0;
    char *p = strtok(buffer, ",");
    while (p) {
      rgb[i++] = (uint8_t)atoi(p);
      p = strtok(NULL, ",");
    }
    // You now have uint8s in rgb[]
    analogWrite(A0, rgb[0]);
    analogWrite(A1, rgb[1]);
    analogWrite(A2, rgb[2]); 
    Serial.println(rgb[0]);
    Serial.println(rgb[1]);
    Serial.println(rgb[2]);
  }
}

Python Code:

import serial, time
def run():
    arduino = serial.Serial('COM3', 115200, timeout=.1)
    time.sleep(2) #give the connection a second to settle

    while True:
        with open("rgb.txt") as f:
            rgb = f.read()
            rgb += "n"
        arduino.write(rgb.encode("ASCII"))

        data = arduino.readline()
        if data:
            try:
                print(data.decode('ASCII').strip("r").strip("n")) # (better to do .read() in the long run for this reason
            except UnicodeDecodeError:
                pass
        time.sleep(0.1)
while True:
    try:
        run()
    except serial.serialutil.SerialTimeoutException:
        print("Is your com port correct?")
Answered By: Quintin Dunn

Your original code is almost correct.

You just need to change the format a little bit to synchronise to newline characters.

Python Code

import serial, time
arduino = serial.Serial('COM3', 9600, timeout=.1)
time.sleep(2) #give the connection a second to settle

while True:
    with open("rgb.txt") as f:
        rgb = f.read()
        rgb = rgb + 'n'  # Add a newline character after the RGB values to aid sychronisation in the Arduino code.
    arduino.write(rgb.encode("ASCII"))

    data = arduino.readline()
    if data:
        try:
             print(data.decode('ASCII').strip("r").strip("n"))
        except UnicodeDecodeError:
            pass
    time.sleep(0.1)

Arduino Code

I’ve used sscanf() to read the 3 integers from the buffer because it returns a count of the number of items it successfully scanned into variables. I’ve also added some #defines to make it more readable and maintainable.

#include <stdio.h>

#define PORT_R A0
#define PORT_G A1
#define PORT_B A2

void setup()
{
  Serial.begin(9600);
  analogWrite(PORT_R, 255); delay(333); analogWrite(PORT_R, 0);
  analogWrite(PORT_G, 255); delay(333); analogWrite(PORT_G, 0);
  analogWrite(PORT_B, 255); delay(334); analogWrite(PORT_B, 0);
}

void loop()
{
  uint8_t r, g, b;
  if (ReadRGB(r, g, b))
  {
    analogWrite(PORT_R, r);
    analogWrite(PORT_G, g);
    analogWrite(PORT_B, b);
    Serial.println(r);
    Serial.println(g);
    Serial.println(b);
  }
}

bool ReadRGB(uint8_t &r, uint8_t &g, uint8_t &b)
{
  if (Serial.available() > 0)
  {
    const int LENGTH = 13;  // nnn,nnn,nnnr
    char buffer[LENGTH];
    size_t count = Serial.readBytesUntil('n', buffer, LENGTH - 1);  // Allow room for NULL terminator.
    buffer[count] = 0;  // Place the NULL terminator after the last character that was read.
    int i = sscanf(buffer, "%d,%d,%d", &r, &g, &b);
    return i == 3;  // Notify whether we successfully read 3 integers.
  }
  return false;
}

Regarding Serial.parseInt(), it doesn’t notify when it times out. Instead, it simply returns 0, which is a valid value, so the caller has no idea whether this was due to a timeout.

The same issue exists with Serial.readBytesUntil() because it doesn’t notify the caller whether the returned byte count is the result of encountering the search character or enduring a timeout. What if 255,255,25 was received due to a timeout caused by a communication error instead of the expected 255,255,255? The caller would be unaware.

Compare with the robust methodology of int.TryParse() in C#.NET which returns a bool to indicate success/failure and passes the parsed int by reference.

More Robust Arduino Code

To overcome the issues of Serial.parseInt() and Serial.readBytesUntil() timing out without returning an error code when the serial input buffer is empty, it’s possible to use a non-blocking algorithm algorithm, something like this which reads one character per loop() until it reaches a newline character before scanning the buffer for 3 integers:

#define PORT_R A0
#define PORT_G A1
#define PORT_B A2

const int LENGTH = 12;  // nnn,nnn,nnn
char buffer[LENGTH];

void setup()
{
  Serial.begin(9600);
  Serial.println("setup()");
  analogWrite(PORT_R, 255); delay(333); analogWrite(PORT_R, 0);
  analogWrite(PORT_G, 255); delay(333); analogWrite(PORT_G, 0);
  analogWrite(PORT_B, 255); delay(334); analogWrite(PORT_B, 0);
}

void loop()
{
  Serial.println("loop()");
  uint8_t r, g, b;
  if (ReadRGB(r, g, b))
  {
    analogWrite(PORT_R, r);
    analogWrite(PORT_G, g);
    analogWrite(PORT_B, b);
    Serial.println(r);
    Serial.println(g);
    Serial.println(b);
  }
}

bool ReadRGB(uint8_t &r, uint8_t &g, uint8_t &b)
{
  if (ReadLine(buffer, LENGTH))
  {
    Serial.println("ReadRGB() read a line.");
    int i = sscanf(buffer, "%d,%d,%d", &r, &g, &b);
    return i == 3;  // Notify whether 3 integers were successfully read.
  }
  return false;
}

int ReadLine(char *buffer, const int length)
{
  static int index = 0;
  int last_index = 0;
  int ch = Serial.read();
  if (ch > 0)
  {
    switch(ch)
    {
      case 'r':
        break;
      case 'n':
        last_index = index;
        index = 0;
        return last_index;
      default:
        if (index < length - 1)
        {
          buffer[index++] = ch;
          buffer[index] = 0;
        }
    }
  }
  return 0;
}
Answered By: tim

If you want to send 3 bytes over serial, which is typically very low bandwidth, you really should just send 3 bytes rather than the ASCII representations, along with commas and newlines. If you send:

255,255,255

with a newline at the end, you are actually sending 12 bytes instead of the 3 you need.

Say you have 3 variables, a, b and c you want to send:

a, b, c = 1, 8, 255

you can pack them as 3 unsigned bytes (B) like this:

import struct

packet = struct.pack('3B',a,b,c)

Then if you check the contents, you will see your 3 bytes:

b'x01x08xff'
Answered By: Mark Setchell