Fastest python big array to arduino

Hello everyone,

I'm trying to program an ambilight on my computer using Python on Windows and Arduino.
With Python, I extract the colors from my screen and get an array of 100 rgb lights wanted (for my 100 LED), like this : [[255,0,0],[254,3,0], etc...]

My problem is... how can I send this to arduino really fast ?
I've tried to send it with the "write" command but if i send too much data in too less time, everything is messed up.

For instance, with p the position wanted :
225r225g225b1p
237r238g238b2p
255r255g255b3p
255r255g255b4p
255r25525255r255g255b11p

As you can see, the 5th "5p" didnt get to arduino and was replaced by something else...
If i put a time.sleep of at least 0.5s between each write, it's working unless i concatenate the strings.

Do you have any way to send multiple arrays at once and in a fast way ? I need to send around 200*3 values in 0.1s.

Thanks in advance !

Here's the arduino code :

#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
  #include <avr/power.h>
#endif

#define PIN 12

String readString;
Adafruit_NeoPixel strip = Adafruit_NeoPixel(144, PIN, NEO_RGBW + NEO_KHZ800);

void setup() {
  Serial.begin (2000000);
  strip.begin();
  strip.show(); // Initialize all pixels to 'off'
  Serial.println ("Booted successfully");
}

void loop() {
  int r=0;
  int g=0;
  int b=0;
  int pos=0;
  while (Serial.available()) {
    delay(1);  //small delay to allow input buffer to fill
    if (Serial.available() >0){
      char c = Serial.read();  //gets one byte from serial buffer
      Serial.println(c);
      if (c == 'r') {
        r = readString.toInt();
        readString = "";
      } else if (c == 'g') {
        g = readString.toInt();
        readString = "";
      } else if (c == 'b') {
        b = readString.toInt();
        readString = "";
      } else if (c == 'p') {
        pos = readString.toInt();
        readString = "";
        strip.setPixelColor(pos, strip.Color(g,r,b));
      } else if (c == '|') {
        strip.show();
      } else {
        readString += c;
      }
    }
  }
}

And the Python code :

arduino = serial.Serial("COM4",2000000,timeout=1)
px=""
for i in range(1,colors_w.shape[0]):
    color = colors_w[i]
    px += str(color[0])+"r"+str(color[1])+"g"+str(color[2])+"b"+str(i)+"p"
    arduino.write(px.encode())
    px = ""
    time.sleep(0.02)
arduino.write("|".encode())

(i've tried with multiple baud rates with no success)

You'd be far far better off sending binary data, then you won't get the bottleneck of having to parse strings
on a slow 8 bit microcontroller.

you got 4 bytes per value and 200*3 = 600 values should be transferred

4*600 = 2400 bytes

200000 baud means 200000 / 9 = 22222 bytes per second (there is a stopbit in the serial transmission.

2400 / 22222 = 0,108 seconds.

You got 45 microseconds per byte to process the byte. Otherwise the buffer will overflow over time, because if you process slower the bytes rush in faster than you process them to get them out of the hardware-receive-buffer. I haven't done any timemeasurings on any board but it seems to be very tight.

So sending binary data needs only one byte per value instead of four bytes "2" "2" "5" "r" the position of the byte corresponds to the color.

best regards

Stefan

It would be much faster and reliable to interface Python with your Arduino using the compatible libraries: pySerialTransfer and SerialTransfer.h.

pySerialTransfer is pip-installable and cross-platform compatible. SerialTransfer.h runs on the Arduino platform and can be installed through the Arduino IDE's Libraries Manager.

Both of these libraries have highly efficient and robust packetizing/parsing algorithms with easy to use APIs.

Example Python Script:

import time
from pySerialTransfer import pySerialTransfer as txfer


if __name__ == '__main__':
    try:
        link = txfer.SerialTransfer('COM17')
        
        link.open()
        time.sleep(2) # allow some time for the Arduino to completely reset
        
        while True:
            send_size = 0
            
            ###################################################################
            # Send a list
            ###################################################################
            list_ = [1, 3]
            list_size = link.tx_obj(list_)
            send_size += list_size
            
            ###################################################################
            # Send a string
            ###################################################################
            str_ = 'hello'
            str_size = link.tx_obj(str_, send_size) - send_size
            send_size += str_size
            
            ###################################################################
            # Send a float
            ###################################################################
            float_ = 5.234
            float_size = link.tx_obj(float_, send_size) - send_size
            send_size += float_size
            
            ###################################################################
            # Transmit all the data to send in a single packet
            ###################################################################
            link.send(send_size)
            
            ###################################################################
            # Wait for a response and report any errors while receiving packets
            ###################################################################
            while not link.available():
                if link.status < 0:
                    if link.status == -1:
                        print('ERROR: CRC_ERROR')
                    elif link.status == -2:
                        print('ERROR: PAYLOAD_ERROR')
                    elif link.status == -3:
                        print('ERROR: STOP_BYTE_ERROR')
            
            ###################################################################
            # Parse response list
            ###################################################################
            rec_list_  = link.rx_obj(obj_type=type(list_),
                                     obj_byte_size=list_size,
                                     list_format='i')
            
            ###################################################################
            # Parse response string
            ###################################################################
            rec_str_   = link.rx_obj(obj_type=type(str_),
                                     obj_byte_size=str_size,
                                     start_pos=list_size)
            
            ###################################################################
            # Parse response float
            ###################################################################
            rec_float_ = link.rx_obj(obj_type=type(float_),
                                     obj_byte_size=float_size,
                                     start_pos=(list_size + str_size))
            
            ###################################################################
            # Display the received data
            ###################################################################
            print('SENT: {} {} {}'.format(list_, str_, float_))
            print('RCVD: {} {} {}'.format(rec_list_, rec_str_, rec_float_))
            print(' ')
    
    except KeyboardInterrupt:
        link.close()
    
    except:
        import traceback
        traceback.print_exc()
        
        link.close()

Example Arduino Sketch:

#include "SerialTransfer.h"


SerialTransfer myTransfer;


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


void loop()
{
  if(myTransfer.available())
  {
    // send all received data back to Python
    for(uint16_t i=0; i < myTransfer.bytesRead; i++)
      myTransfer.txBuff[i] = myTransfer.rxBuff[i];
    
    myTransfer.sendData(myTransfer.bytesRead);
  }
}

On the Arduino side, you can use myTransfer.txObj() and myTransfer.rxObj() to copy values to the library's RX buffer and parse multi-byte variables out of the library's TX buffer.

For theory behind robust serial communication, check out the tutorials Serial Input Basics and Serial Input Advanced.

One thing is that you're limited to 254 bytes per packet, so if you need to send more than that amount of data, you'll need to figure out how to send multiple packets and reconstruct all the data on the RX side.

soldierftl:
With Python, I extract the colors from my screen and get an array of 100 rgb lights wanted (for my 100 LED), like this : [[255,0,0],[254,3,0], etc...]

If each group of 3 values is intended for a specific LED then I would send the data like this (binary values without spaces
1 255 0 0
where the first byte identifies the intended LED

Do as much of the calculation as you can on your PC because it is so much faster than an Arduino.

...R

Thank you very much for all these answers, I think I understand arduino a bit better. I thought baud rate = 2 000 000 would allow me to send more data without consequences but it does not permit more data and it presents the risk of bottleneck, so the greater is not necessarily the better ^^ I'll stick to 115 200.

I have two questions if you still have time to answer, regarding what you told me :

  • I am now using SerialTransfer which is awesome and simple, but I cannot send more than 64 bytes in one shot
list_ = colors_w.flatten().tolist()[:63]
send_size = 0
list_size = link.tx_obj(list_)
send_size += list_size

If I replace 63 by 64 I have an error. Is it possible to have more bytes per call ? Since in the end I need to send 200*3 bytes (for the position and rgb i'm using the position in the list)

  • Secondly, on arduino, I'm not used to work with bytes, so just to make it clear : myTransfer.rxBuff is a byte, so do I have to convert it to an int (how then) or can i directly write something like strip.setPixelColor(myTransfer.rxBuff*) (it's expecting an integer : the position of the led) ?*
    Bonus : in this loop
    *_ <em>*for(uint16_t i=0; i < myTransfer.bytesRead; i++)       myTransfer.txBuff[i] = myTransfer.rxBuff[i];*</em> _*
    is the full myTransfer.rxBuff available ? i mean, can i write myTransfer.rxBuff[i+1] or is the buffer feeding and do I have to keep the loop ?
    Bonus 2 : is Arduino also sending data at max 64 bytes per sending ?
    Thanks again :slight_smile:

I've continued today and i have one "last" mystery to solve : when i send message to the arduino, it does not treat all of them.

For instance with this code :

offset = 0
list_ = [offset]+colors_w[:60]
list_size = link.tx_obj(list_)
link.send(list_size)

offset = 20
list_ = [offset]+colors_w[60:]
list_size = link.tx_obj(list_)
link.send(list_size)

the leds 0 to 20 and 20 to 40 should receive the order to change color, but if I execute the whole code only the 20st LED will light up the first time i execute the block. Then it will only be the 20 next. Then the 20 first, etc...

Do you have an explanation and a way to avoid that ? I know you told me the buffer can get overriden if a new message comes and if i'm not fast enough to treat the previous one, but i don't do much on arduino (baud 115200) :

void loop() {
  if(myTransfer.available())
  {
    // send all received data back to Python
    int offset = bytesToInt(myTransfer.rxBuff,0);
    for(uint16_t i=4; i < myTransfer.bytesRead; i=i+12){
      int r = bytesToInt(myTransfer.rxBuff,i);
      int g = bytesToInt(myTransfer.rxBuff,i+4);
      int b = bytesToInt(myTransfer.rxBuff,i+8);
      strip.setPixelColor(offset + i/12, strip.Color(g,r,b));
    }
    strip.show();
  }
}

unsigned long bytesToInt(byte *bytes, int pos)
{
  unsigned long n = bytes[pos+3];
  n = n*256 + bytes[pos+2];
  n = n*256 + bytes[pos+1];
  n = n*256 + bytes[pos];
  return n;
}

When all the problems are solved I'll post the final solution here in case someone needs it later :slight_smile:

soldierftl:
I have two questions if you still have time to answer, regarding what you told me :

  • I am now using SerialTransfer which is awesome and simple, but I cannot send more than 64 bytes in one shot

You can send up to 254 bytes of data at a time. However, this assumes the receiver code is efficient enough to read-in bytes at least as fast as they arrive - else the 64-byte UART buffer on the Arduino will overflow. I think the reason you can only reliably send 64 bytes is because of this overflow issue.

Can you post your entire TX and RX code?

soldierftl:
If I replace 63 by 64 I have an error. Is it possible to have more bytes per call ? Since in the end I need to send 200*3 bytes (for the position and rgb i'm using the position in the list)

Please see Reply #4

soldierftl:

  • Secondly, on arduino, I'm not used to work with bytes, so just to make it clear : myTransfer.rxBuff is a byte, so do I have to convert it to an int (how then) or can i directly write something like strip.setPixelColor(myTransfer.rxBuff*) (it's expecting an integer : the position of the led) ?*
    [/quote]
    myTransfer.rxBuff is an array of uint8_t values. If you need to transfer 16-bit values or larger, you can use myTransfer.rxObj() to parse out multi-byte values. See this example:
    ```
    *#include "SerialTransfer.h"

SerialTransfer myTransfer;

struct STRUCT {
 char z;
 float y;
} testStruct;

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

void loop()
{
 if(myTransfer.available())
 {
   myTransfer.rxObj(testStruct, sizeof(testStruct));
   Serial.print(testStruct.z);
   Serial.print(' ');
   Serial.println(testStruct.y);
   Serial.println();
 }
 else if(myTransfer.status < 0)
 {
   Serial.print("ERROR: ");

if(myTransfer.status == -1)
     Serial.println(F("CRC_ERROR"));
   else if(myTransfer.status == -2)
     Serial.println(F("PAYLOAD_ERROR"));
   else if(myTransfer.status == -3)
     Serial.println(F("STOP_BYTE_ERROR"));
 }
}*

*_ _*> soldierftl:*_ _*> Bonus : in this loop*_ _*>*_ _*>*_ _*>
> for(uint16_t i=0; i < myTransfer.bytesRead; i++)
     myTransfer.txBuff[i] = myTransfer.rxBuff[i];

> ```
>
>
> is the full myTransfer.rxBuff available ? i mean, can i write myTransfer.rxBuff[i+1] or is the buffer feeding and do I have to keep the loop ?
> [/quote]
> You can access all elements of the TX and RX buffers up to (but not including) 254. In other words, i can take on values 0-253.
> > soldierftl:
> > Bonus 2 : is Arduino also sending data at max 64 bytes per sending ?
> It sends up to 254 bytes of payload per packet.

Thanks @Power_Broker !

Here's the full code :
Arduino

#include <Adafruit_NeoPixel.h>
#include "SerialTransfer.h"
#ifdef __AVR__
  #include <avr/power.h>
#endif

#define PIN 12

SerialTransfer myTransfer;

String readString;
Adafruit_NeoPixel strip = Adafruit_NeoPixel(144, PIN, NEO_RGBW + NEO_KHZ800);

void setup() {
  Serial.begin (115200);
  strip.begin();
  strip.show(); // Initialize all pixels to 'off'
  myTransfer.begin(Serial);
  Serial.println ("Booted successfully");
}

void loop() {
  if(myTransfer.available())
  {
    int offset = bytesToInt(myTransfer.rxBuff,0);
    byte *data = myTransfer.rxBuff;
    for(int i=4; i < myTransfer.bytesRead; i=i+12){
      int r = bytesToInt(data,i);
      int g = bytesToInt(data,i+4);
      int b = bytesToInt(data,i+8);
      strip.setPixelColor(offset + i/12, strip.Color(g,r,b));
    }
    strip.show();
  }
}

int bytesToInt(byte *bytes, int pos)
{
  int n = bytes[pos+3];
  n = n*256 + bytes[pos+2];
  n = n*256 + bytes[pos+1];
  n = n*256 + bytes[pos];
  return n;
}

(i've tried with myTransfer.rxObj() but didn't succeed for now, but since this code is doing what i want i'll optimize it later)

As you can see, i'm sending in python an array like [20, 255,0,0, 255,0,0] saying to update the leds 20 and 21 (offset of 20 from the first one) to put them red.

And here's the python code :

import time
from pySerialTransfer import pySerialTransfer as txfer


link = txfer.SerialTransfer('COM4',baud=115200)
link.open()
time.sleep(2)

colors_w = [0]*105
for i in range(2):
    offset = 20*i
    list_size = 0
    if i == 0:
        list_ = [offset]+colors_w[60:]
    else:
        list_ = [offset]+colors_w[:60]
    list_size += link.tx_obj(list_)
    link.send(list_size)

with this code i don't understand why my python loop executes (i=0 and i=1), but only one link.send is executed : i=0 if first time i execute the whole loop, i=1 if it's the second time, i=0 if it's the third, etc...
this is really strange imo (if it was because the second call is not executed until the first one is over or because the second call is written over, it wouldn't be so periodical).

I've verified that this works:

Python:

import time
from pySerialTransfer import pySerialTransfer as txfer


if __name__ == '__main__':
    try:
        link = txfer.SerialTransfer('COM7')
        link.open()
        time.sleep(2)
        
        while True:
            num_pixels = 100
            
            for pixel in range(num_pixels):
                link.txBuff[0] = pixel # Pixel number
                link.txBuff[1] = 234   # Red
                link.txBuff[2] = 12    # Green
                link.txBuff[3] = 42    # Blue
                link.send(4)
                
                # Adjust to prevent buffer overflow on Arduino
                time.sleep(0.1)
        
    except KeyboardInterrupt:
        link.close()
    
    except:
        import traceback
        traceback.print_exc()
        
        link.close()

Arduino:

#include <Adafruit_NeoPixel.h>
#include "SerialTransfer.h"
#ifdef __AVR__
  #include <avr/power.h>
#endif

#define PIN 12


SerialTransfer myTransfer;
Adafruit_NeoPixel strip = Adafruit_NeoPixel(144, PIN, NEO_RGBW + NEO_KHZ800);

struct pixel_data{
  uint8_t pixNum;
  uint8_t r;
  uint8_t g;
  uint8_t b;
} pixelData;


void setup()
{
  Serial.begin(115200);
  myTransfer.begin(Serial);
  strip.begin();
  strip.show();
}


void loop()
{
  if(myTransfer.available())
  {
    myTransfer.rxObj(pixelData, sizeof(pixelData));
    
    strip.setPixelColor(pixelData.pixNum, strip.Color(pixelData.g, pixelData.r, pixelData.b));
    strip.show();
  }
}

Let me know if you have trouble with it or have any questions.

I have it running at 10 pixels per second, but you can adjust the time.sleep(0.1) call in the Python (or delete it entirely) to send pixel values faster

Hi Power_Broker,

I confirm your code is working, and with a time.sleep mine too. I've done some tests and with this code :

colors_w = [255,0,0]*15 + [0,255,0]*15 + [0,0,255]*30
nb_led = 10
for i in range(int(len(colors_w)/(3*nb_led))):
    offset = i*nb_led
    list_ = [offset]+colors_w[3*i*nb_led:3*(i+1)*nb_led]
    
    list_size = link.tx_obj(list_)
    link.send(list_size)
    
    time.sleep(0.01)

if i change nb_led for 20 then not all LED will light up, but for 10 it works nicely. So you are right with your approach, my arduino was too slow to handle more LED in one shot :frowning:
even if i still don't get why calling link.send twice is perfectly periodical, i'll investigate more your solution since it's too slow for now (even with time.sleep of 0.01) as i want the led to respond to my screen changes with less delay ^^

but in any case, the code is working and i have a solid base to go further :smiley: thanks !

PS : if i delete the time.sleep, 1 led is up, 10 are down then 1 up 10 down, etc...

Here's the final code, thank you everyone for your help and particularly Power_Broker :smiley:

Arduino :

#include <Adafruit_NeoPixel.h>
#include "SerialTransfer.h"
#ifdef __AVR__
  #include <avr/power.h>
#endif

#define PIN 12


SerialTransfer myTransfer;
Adafruit_NeoPixel strip = Adafruit_NeoPixel(144, PIN, NEO_RGBW + NEO_KHZ800);

struct pixel_data{
  uint8_t pixNum;   //offset
  uint8_t data[90]; //30 pixels of r,g,b i.e. 3*30
} pixelData;


void setup()
{
  Serial.begin(115200);
  myTransfer.begin(Serial);
  strip.begin();
  strip.show();
}


void loop()
{
  if(myTransfer.available())
  {
    myTransfer.rxObj(pixelData, sizeof(pixelData));
    for(uint8_t i=0; i < sizeof(pixelData.data); i=i+3){
      //Be careful, setPixelColor or Color are inverting red and green, so we have to write (g,r,b) instead of (r,g,b)
      strip.setPixelColor(pixelData.pixNum + i/3, strip.Color(pixelData.data[i+1],pixelData.data[i],pixelData.data[i+2]));
    }
    strip.show();
  }
}

Python 3 :

import time
from pySerialTransfer import pySerialTransfer as txfer

if __name__ == '__main__':
    try:
        link = txfer.SerialTransfer('COM4')
        link.open()
        time.sleep(2)

        num_pixels = 120
       
        package = 30
        for pixel in range(int(num_pixels/package)):
            link.txBuff = [pixel*package]+[234,12,42]*package
            link.txBuff = [pixel*package]+[0,0,0]*package
            link.send(len(link.txBuff))
            # Adjust to prevent buffer overflow on Arduino Uno (30 LED at a time is ok for a time sleep of 0.01)
            time.sleep(0.01)
       
    except KeyboardInterrupt:
        link.close()
   
    except:
        import traceback
        traceback.print_exc()
       
        link.close()