ESP32 loses data when receiving 2067 bytes over serial

Good day, Fellas.

I have a python application that sends 2068 bytes of image RGB data over serial to an ESP32, which is supposed to receive it and push the data to the LED strip. Think of a crude pixel art image display.

For the time being I'm just sending flat color values but the final intention is to open a 25x24px resolution video, rip the frames, break the pixels down into raw rgb values and send over serial to esp32 to display.

Python application:

import serial
import time
import cv2

ser = serial.Serial('COM3', 921600,  timeout=1,  bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE)
time.sleep(0.2)


# create packet of data
packet = bytearray()
packet.append(0b00000001) # first byte to be read by controller, determines type operation


vfile = cv2.VideoCapture("test_video.mp4")
ret, frame = vfile.read()

for x in range(27):
    for y in range(26):
        if x % 2 == 1 and y == 0: continue # skip the firt pixel of every odd row 
        # packet.append(int(frame[y,x,2]))
        # packet.append(int(frame[y,x,1]))
        # packet.append(int(frame[y,x,0]))
        packet.append(255) # test by sending white
        packet.append(255)
        packet.append(255)

       # print(x,":",y, " rgb: ",frame[y,x,2],frame[y,x,1],frame[y,x,0])

ser.write(packet)
print(packet)

print(len(packet), " written")
input("press any to quit, this will reset controller")

#cv2.imwrite("frame_test.jpg", frame)

vfile.release()

ESP32 Arduino Application.

#include "ESP32_WS2812_Lib.h"

#define LEDS_COUNT 689
#define LEDS_PIN 17
#define CHANNEL 0

ESP32_WS2812 strip = ESP32_WS2812(LEDS_COUNT, LEDS_PIN, CHANNEL, TYPE_GRB);

uint16_t led_id = 0;

void setup() {
  Serial.begin(921600);

  pinMode(LED_BUILTIN, OUTPUT); // test led
  digitalWrite(LED_BUILTIN, 0);


  while (!Serial) {
    Serial.println("waiting for serial");
  }

  Serial.println("serial running");
  Serial.setRxBufferSize(2100); // set serial buffer size

  strip.begin();
  strip.setBrightness(30);

  for (int i = 0; i < LEDS_COUNT; i++) { // clear all leds
    strip.setLedColorData(i, 0, 0, 0);
  }
  strip.show();

}

void loop() {
  
  if (Serial.available() > 0) {
    uint8_t cmd_byte = Serial.read(); // read command byte to determine type of operation

    if (cmd_byte == 0b00000001){ // if this is a 25x24 image data stream
      //delay(10);
      digitalWrite(LED_BUILTIN, 1); // show that commad of 0x01 was detected

      led_id = 0; // reset led index to start applying from the beginning.

      // enter loop and stay in this state until all data is transfered
      while(true){
        if (Serial.available() > 3){
          delay(10);
          uint8_t r = Serial.read(); 
          uint8_t g = Serial.read(); 
          uint8_t b = Serial.read(); 

         
   
          strip.setLedColorData(led_id, r, g, b);
          led_id += 1;

          strip.show(); // for testing purposes each image is shown
          
          
          if (led_id == 688){ // check if last led was set
            // !!!!!!!  never reaches this
            break; // Consider all data received 
            
          }
        } // end of if serial has 3 bytes
      } // end of while loop
    } // end of if cmd == 0x01
  } // end of is serial available

  
}

I have noted a couple of peculiar behaviors:

  1. The esp stops reading serial data once it reaches around 720-ish bytes but it's not consistent. Could be more, could be less by a margin of anywhere between a 3 to 30 or more bytes.

When I send one color set to 255 while others are 0, it seems to be reading the data out of order because it just toggles primary colors?

I have included the mp4 file that the python app is requesting as the loops are based on it's dimensions.

test_video.zip (2.2 KB)

I checked if perhaps the controller freezes or restarts during the data transfer by setting up a timer interrupt an a boot led flash pattern.

If the LED fast flashes then it indicates the setup was executed, which means the ESP32 crashed and rebooted.

If the interrupt stops flashing the blue led then it means the controller froze during data upload.

It doesn't seem to be the case but I may not know everything there is to how ESP32 handles these things.

Check the Arduino.cc documentation on Serial, I know there is a buffer size but I don't recall the number but I bet it's smaller than what you are sending.

if you wait 10ms for every 3 bytes you receive, you will wait so long that your buffer will pretty much always overflow. Unless you wait a similar amount between sending the pixels at the other end, but you send it all in one go. I think you can modify the buffer to any size if you look thru the core (different versions of it have different ways of doing that) But i would start by not slowing down reception buffer reading.
Depending on the way your chosen ledstrip library perform show() this may even compound this issue.

regarding your

This is not a very reliable way of looking a a start byte, i mean if your values are always between 0 & 255 a '1' may anyway occur. As long as you are only sending a single frame that won't matter i guess.

If you want to follow the process you can always redfuce the baud rate.

Arduino Uno has a buffer size of 64 bytes. ESP has 256 bytes . That size can be increased by using Serial. setRxBufferSize() up to 4k bytes from what I read

So I only send 1 single frame worth of data which totals to 2067 bytes + the 0x01 data byte at the beginning to indicate the start of the frame. I know it's not idea but it has to at least work once before I can improve upon it. It clearly does work...

Ok so here's what I've done. Instead of sending the whole packed all as one big chunk. I sent it three bytes at a time and I set a small sleep delay on the python side to wait for esp32 to catch up. Now it's slow as heck but maybe now I can start looking into speeding things up.

import serial
import time
import cv2

ser = serial.Serial('COM3', 921600,  timeout=1,  bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE)
time.sleep(0.5)


# create packet of data
packet = bytearray()
packet.append(0b00000001) # ficalcrst byte to be read by controller, determines type operation
ser.write(packet)

vfile = cv2.VideoCapture("test_video.mp4")
ret, frame = vfile.read()

for x in range(27):
    for y in range(26):
        if x % 2 == 1 and y == 0: continue # skip the firt pixel of every odd row 
        packet = bytearray()
        # packet.append(int(frame[y,x,2]))
        # packet.append(int(frame[y,x,1]))
        # packet.append(int(frame[y,x,0]))
        packet.append(255) # test by sending white
        packet.append(255)
        packet.append(255)

        time.sleep(0.05)

        ser.write(packet)

        print(x,":",y, " rgb: ",frame[y,x,2],frame[y,x,1],frame[y,x,0])


print(packet)

print(len(packet), " written")
input("press any to quit, this will reset controller")

#cv2.imwrite("frame_test.jpg", frame)

vfile.release()

ESP32:

#include "ESP32_WS2812_Lib.h"
#include <Ticker.h>

#define TIMER_BASE_CLK 8000000  // had to define it because library was crashing.. legacy?
Ticker secondTick;

#define LEDS_COUNT 689
#define LEDS_PIN 17
#define CHANNEL 0

ESP32_WS2812 strip = ESP32_WS2812(LEDS_COUNT, LEDS_PIN, CHANNEL, TYPE_GRB);

uint16_t led_id = 0;

#define _TIMERINTERRUPT_LOGLEVEL_ 3
#include "ESP32TimerInterrupt.h"
#define TIMER0_INTERVAL_MS 100
ESP32Timer ITimer0(0);

void ISRwatchdog() {
  // separate timer interrupt. Flash an LED every half second. If stops = controller crashed.
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

void setup() {


  Serial.begin(921600);

  pinMode(LED_BUILTIN, OUTPUT);  // test led
  digitalWrite(LED_BUILTIN, 0);


  while (!Serial) {
    Serial.println("waiting for serial");
  }

  Serial.println("serial running");
  Serial.setRxBufferSize(2100);  // set serial buffer size


  // flash LED 6 times to indicated boot has happaned (to detect reboots)
  for (uint8_t x = 0; x < 12; x++) {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
    delay(50);
  }

  strip.begin();
  strip.setBrightness(30);

  for (int i = 0; i < LEDS_COUNT; i++) {  // clear all leds
    strip.setLedColorData(i, 0, 0, 0);
  }
  strip.show();

  secondTick.attach(0.5, ISRwatchdog); // set up watchdog timer to flash an led (if stops, controller crashed)
}



void loop() {

  if (Serial.available() > 0) {
    uint8_t cmd_byte = Serial.read();  // read command byte to determine type of operation

    if (cmd_byte == 0b00000001) {  // if this is a 25x24 image data stream
      //delay(10);

      led_id = 0;  // reset led index to start applying from the beginning.

      // enter loop and stay in this state until all data is transfered
      while (true) {
        if (Serial.available() > 3) {
          delay(10);

          uint8_t r = Serial.read();
          uint8_t g = Serial.read();
          uint8_t b = Serial.read();



          strip.setLedColorData(led_id, r, g, b);
          led_id += 1;

          strip.show();  // for testing purposes each image is shown


          if (led_id == 688) {  // check if last led was set
            // !!!!!!!  never reaches this
            break;  // Consider all data received
          }
        }  // end of if serial has 3 bytes
      }    // end of while loop
    }      // end of if cmd == 0x01
  }        // end of is serial available
}

Now this works. but what does this tell me? That it's the serial buffer being overflown?

What is the maximum baud rate that an ESP32 can handle with USB connection to the PC?

what does the datasheet say ? it also depends a bit on the USB to TTL converter.

Well yes. the buffer has a static size, and full is full. There is a FIFO which is already quite big, but yeah if you wait 10ms before you start reading the next pixel.

In a way the best would be if you read the whole thing as quickly as you can and then do whatever you want with it after.

yeah there is quite a big chance of it going in sync, if there is a transmission before the ESP is ready, and so on and forth.

What i would like to tell you is that if you intend to control ledstrip remotely, there is software for that and there is a good protocol for that, which is easy enough to implement on an ESP.

What's the existing protocol?

Before doing serial, I was using an Ethernet adapter, thinking it would have a speed boost. I suppose that the Ethernet chip had a much larger buffer because I had no issues reading a whole frame worth of data in one loop.

So what do you suppose I should do? Break the frame data into chunks and send on packets of 100 or so bytes? That seams reasonable.

What about the issue where the colors seem to be not arriving in the right order?

if I send 255, 255, 255, I get white. but if I send 255,0,0, then the strip seems to rotate the red, green, blue or sometimes even mixed colors together. So either the data is not coming in in the right order or maybe the esp is skipping bytes?

Perhaps I'm also not setting up the serial port connection correctly on the python side. What settings should I be using?

https://pyserial.readthedocs.io/en/latest/pyserial_api.html

I'm trying out:

ser = serial.Serial(
    port=port,
    baudrate=baudrate,
    bytesize=serial.EIGHTBITS,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_ONE,
    timeout=1  # Set a read timeout
)

Ok update. I've reduced the sketch down to most minimum basics. Which is getting a serial response. I couldn't get it to work stably until I turned the baud rate down to 115200 rate... maybe I was running it too hot?

ArtNet

First of all make sure that when reception starts, there is nothing slowing down the reception until the whole frame has been received.

probably the latter.

A bit quicker should be possible i think

Can you show us what it looks like now ?

Every time it 'rotates', you have lost one or more bytes, likely by overrunning the serial buffer. Think about it, if you lost one zero, the 255 shifts to the next color for the next pixel, etc. etc.

You have nothing to keep you in sync pixel to pixel(admittedly, adding a flag for each pixel necessitates 4/3 the data, so impact on throughput will be large), your protocol simply isn't, so you're extremely vulnerable.

Write yourself a piece of 'kleenex code' that simply tests how long it takes for a call to
strip.show();
That, and the
delay(10);
are taking your receive loop to it's knees.

After some tinkering and optimization I got it working.

You fellas were right. a bit more handshaking was necessary to get the whole thing synchronized.

First of all instead of sending RGB data, I created an 8 bit color palette giving me 256 preset colors to work with. from the color palette I created an encoding and decoding lookup tables. On the python side it's a dictionary that uses R+G+B as a key and an index as value so now I have 1/3 as much data that needs to be sent.

Next I managed to divide the frame into 3 packets and got the arduino to respond with an OK after receiving each packet. Seems without that OK things fall apart out of sync quickly.

So now I seem to be getting around 20 or so fames per second.