Serial object stops updating after a few seconds (native USB CDC board - Seeed XIAO ESP32S3)

F_Dos's icon

Hi everyone,

(Note: this post was drafted with the help of an AI assistant, but reflects my own project and the actual issue I'm experiencing.)

I'm receiving simple button-state data from a Seeed XIAO ESP32S3 board over USB (native USB CDC, not an external FTDI/CH340 chip) into Max via the [serial] object, and after a few seconds it simply stops updating entirely - no errors immediately obvious in the patch, values just freeze and nothing further comes through.

Setup:

- Board: Seeed XIAO ESP32S3, using native USB CDC ("USB CDC On Boot: Enabled" in Arduino board settings)

- Two boards actually - a sender (reads two buttons, transmits over ESP-NOW) and a receiver (gets the ESP-NOW packet, prints it over USB serial to the computer running Max)

- The receiver sends simple lines like "A 1", "B 0" over Serial.println() at 115200 baud, only when a button state changes (so it's not a high data rate - maybe a few messages per second at most when pressing buttons)

- Confirmed via Arduino IDE Serial Monitor (reading the receiver board directly) that data comes through continuously and correctly - no gaps, no dropped messages - so this looks isolated to Max/the serial object rather than the firmware or hardware.

Sender code:

#include <esp_now.h>
#include <WiFi.h>
#include "esp_wifi.h"

// ==================== Configuration ====================
const uint8_t RECEIVER_MAC[] = {0x68, 0xEE, 0x8F, 0x46, 0x4E, 0xFC};
const int WIFI_CHANNEL = 1;

const int numButtons = 2;
const int buttonPins[numButtons] = {D1, D3};
const char buttonIDs[numButtons] = {'A', 'B'};

const unsigned long debounceDelay = 20; // ms

int buttonStates[numButtons];      // confirmed, stable state
int lastButtonStates[numButtons];  // raw last reading (may bounce)
unsigned long lastDebounceTime[numButtons] = {0};

// ==================== Function declarations ====================
void initESPNow();
void registerPeer();
void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status);
void readButtons();
void sendButtonState(char id, int pressed);

// ==================== Setup ====================
void setup() {
  Serial.begin(115200);
  delay(1000);

  for (int i = 0; i < numButtons; i++) {
    pinMode(buttonPins[i], INPUT_PULLUP);
    buttonStates[i] = HIGH;
    lastButtonStates[i] = HIGH;
  }

  initESPNow();
}

// ==================== Main loop ====================
void loop() {
  readButtons();
}

// ==================== ESP-NOW setup ====================
void initESPNow() {
  WiFi.mode(WIFI_STA);
  esp_wifi_start();
  esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);

  if (esp_now_init() != ESP_OK) {
    Serial.println("ESP-NOW init failed");
    while (true);
  }

  esp_now_register_send_cb(onDataSent);
  registerPeer();
}

void registerPeer() {
  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, RECEIVER_MAC, 6);
  peerInfo.channel = WIFI_CHANNEL;
  peerInfo.encrypt = false;

  if (esp_now_add_peer(&peerInfo) != ESP_OK) {
    Serial.println("Failed to add peer");
    while (true);
  }
}

// ==================== Send callback ====================
void onDataSent(const wifi_tx_info_t *tx_info, esp_now_send_status_t status) {
  // Kept silent to avoid cluttering Serial - uncomment for debugging
  // Serial.println(status == ESP_NOW_SEND_SUCCESS ? "SUCCESS" : "FAIL");
}

// ==================== Button handling (array-based, proven debounce logic) ====================
void readButtons() {
  unsigned long currentMillis = millis();

  for (int i = 0; i < numButtons; i++) {
    int reading = digitalRead(buttonPins[i]);

    if (reading != lastButtonStates[i]) {
      lastDebounceTime[i] = currentMillis;
    }

    if ((currentMillis - lastDebounceTime[i]) > debounceDelay) {
      if (buttonStates[i] != reading) {
        buttonStates[i] = reading;
        int pressed = !reading; // 1 = pressed, 0 = released
        sendButtonState(buttonIDs[i], pressed);
      }
    }

    lastButtonStates[i] = reading;
  }
}

void sendButtonState(char id, int pressed) {
  // Simple 2-byte message: button ID + value
  uint8_t payload[2] = {(uint8_t)id, (uint8_t)pressed};
  esp_now_send(RECEIVER_MAC, payload, sizeof(payload));

  Serial.print((char)id);
  Serial.print(" ");
  Serial.println(pressed);
}

Receiver firmware (this is the board Max actually reads from over USB):

#include <esp_now.h>
#include <WiFi.h>
#include "esp_wifi.h"

// ==================== Configuration ====================
const int WIFI_CHANNEL = 1;

// ==================== Function declarations ====================
void initESPNow();
void onDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len);

// ==================== Setup ====================
void setup() {
  Serial.begin(115200);
  delay(1000);

  initESPNow();
}

void loop() {
  // Nothing to do here — everything happens in onDataRecv
}

// ==================== ESP-NOW setup ====================
void initESPNow() {
  WiFi.mode(WIFI_STA);
  esp_wifi_start();
  esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE);

  if (esp_now_init() != ESP_OK) {
    Serial.println("ESP-NOW init failed");
    while (true);
  }

  esp_now_register_recv_cb(onDataRecv);
}

// ==================== Receive callback ====================
void onDataRecv(const esp_now_recv_info_t *info, const uint8_t *incomingData, int len) {
  if (len < 2) return; // safety check

  char id = (char)incomingData[0];
  int value = incomingData[1];

  Serial.print(id);
  Serial.print(" ");
  Serial.println(value);
}

Questions:

1. Is there a known issue specifically with native USB CDC boards (as opposed to FTDI/CH340-based boards) and the [serial] object freezing after establishing a connection?

2. Aside from dtr 1, are there other serial object attributes worth trying (baud handshake settings, flow control, buffer size)?

3. Would switching to a different serial implementation (e.g. the jasch comport external, or a Node for Max serial bridge) be a more reliable path for USB CDC boards specifically, based on others' experience?

Any help appreciated - this is for a project with real-time button/sensor feedback so the freezing is a blocker.

Max patch:

Max Patch
Copy patch and select New From Clipboard in Max.

Thanks!

F_Dos's icon

I tried to use jsach comport instead of serial object but the same problem happens

F_Dos's icon

Some important update: I found something interesting. When the Arduino IDE Serial Monitor is open on the sender board, the receiver's data comes through smoothly in Max. When I close that Serial Monitor, the connection in Max starts freezing after a few button presses.

another update:

I disconnected the sender board from the Mac entirely and powered it with a standalone 5V wall adapter. With only the receiver board connected to the Mac via USB, the data now comes through in Max smoothly with no freezing at all.

So it seems the issue was specifically caused by having two USB CDC devices connected to the same Mac at once - removing one from USB (while keeping it powered separately) resolved it completely.