Reputation: 25
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] = '\0';
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
).
Upvotes: 1
Views: 1415
Reputation: 2183
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.
Upvotes: 1
Reputation: 482
Your original code is almost correct.
You just need to change the format a little bit to synchronise to newline characters.
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)
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,nnn\r\0
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.
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\0
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;
}
Upvotes: 0
Reputation: 207425
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'\x01\x08\xff'
Upvotes: 0
Reputation: 25
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?")
Upvotes: 0
Reputation: 1862
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');
}
}
Upvotes: 0