crosspoint-reader/claude_notes/usb-serial-blocking-fix-2026-01-28.md
cottongin ffe2aebd7e
fix: guard Serial input calls when USB not connected at boot
Fixes device hanging when booted without USB connected. The root cause
was calling Serial.available() and Serial.read() in checkForFlashCommand()
when Serial.begin() was never called (USB not connected at boot).

Changes:
- Add if (!Serial) return guard to checkForFlashCommand()
- Restore upstream while (!Serial) wait loop with 3s timeout
- Remove Serial.setTxTimeoutMs(0) (not in upstream, may cause issues)
- Remove unnecessary if (Serial) guards from EpubReaderActivity.cpp
  (Serial.printf is safe without guards, only input calls need them)

Key insight: Serial.printf() is safe without guards (returns 0 when not
initialized), but Serial.available()/Serial.read() cause undefined
behavior on ESP32-C3 USB CDC when called without Serial.begin().

See: claude_notes/usb-serial-blocking-fix-2026-01-28.md
2026-01-28 17:45:00 -05:00

4.4 KiB

USB Serial Blocking Issue - Root Cause and Fix

Date: 2026-01-28
Issue: Device blocking/hanging when USB is not connected at boot


Problem Description

The device would hang or behave unpredictably when booted without USB connected. This was traced to improper Serial handling on ESP32-C3 with USB CDC.

Root Cause Analysis

Factor A: checkForFlashCommand() Called Without Serial Initialization

The most critical issue was in checkForFlashCommand(), which is called at the start of every loop() iteration:

void loop() {
  checkForFlashCommand();  // Called EVERY loop iteration
  // ...
}

void checkForFlashCommand() {
  while (Serial.available()) {  // Called even when Serial.begin() was never called!
    char c = Serial.read();
    // ...
  }
}

When USB is not connected at boot, Serial.begin() is never called. Then in loop(), checkForFlashCommand() calls Serial.available() and Serial.read() on an uninitialized Serial object. On ESP32-C3 with USB CDC, this causes undefined behavior or blocking.

Factor B: Removed while (!Serial) Wait Loop

The upstream 0.16.0 code included a 3-second wait loop after Serial.begin():

if (isUsbConnected()) {
  Serial.begin(115200);
  unsigned long start = millis();
  while (!Serial && (millis() - start) < 3000) {
    delay(10);
  }
}

This wait loop was removed in an earlier attempt to fix boot delays, but it may be necessary for proper USB CDC initialization.

Factor C: Serial.setTxTimeoutMs(0) Added Too Early

Serial.setTxTimeoutMs(0) was added immediately after Serial.begin() to make TX non-blocking. However, calling this before the Serial connection is fully established may interfere with USB CDC initialization.


The Fix

1. Guard checkForFlashCommand() with Serial Check

void checkForFlashCommand() {
  if (!Serial) return;  // Early exit if Serial not initialized
  while (Serial.available()) {
    // ... rest unchanged
  }
}

2. Restore Upstream Serial Initialization Pattern

void setup() {
  t1 = millis();

  // Only start serial if USB connected
  pinMode(UART0_RXD, INPUT);
  if (isUsbConnected()) {
    Serial.begin(115200);
    // Wait up to 3 seconds for Serial to be ready to catch early logs
    unsigned long start = millis();
    while (!Serial && (millis() - start) < 3000) {
      delay(10);
    }
  }
  // ... rest of setup
}

3. Remove Serial.setTxTimeoutMs(0)

This call was removed entirely as it's not present in upstream and may cause issues.

4. Remove Unnecessary if (Serial) Guards

The 15 if (Serial) guards added to EpubReaderActivity.cpp were removed. Serial.printf() is safe to call when Serial isn't initialized (it simply returns 0), so guards around output calls are unnecessary.

Key distinction:

  • Serial.printf() / Serial.println() - Safe without guards (no-op when not initialized)
  • Serial.available() / Serial.read() - MUST be guarded (undefined behavior when not initialized)

Files Changed

File Change
src/main.cpp Removed Serial.setTxTimeoutMs(0), restored while (!Serial) wait, added guard to checkForFlashCommand()
src/activities/reader/EpubReaderActivity.cpp Removed all 15 if (Serial) guards

Testing Checklist

After applying fixes, verify:

  1. Boot with USB connected, serial monitor open - should work
  2. Boot with USB connected, NO serial monitor - should work (3s delay then continue)
  3. Boot without USB - should work immediately (no blocking)
  4. Sleep without USB, plug in USB during sleep, wake - should work
  5. Sleep with USB, unplug during sleep, wake - should work

Lessons Learned

  1. Always guard Serial input operations: Serial.available() and Serial.read() must be guarded with if (Serial) or if (!Serial) return when Serial initialization is conditional.

  2. Serial output is safe without guards: Serial.printf() and similar output functions are safe to call even when Serial is not initialized - they simply return 0.

  3. Don't remove initialization waits without understanding why they exist: The while (!Serial) wait loop exists for proper USB CDC initialization and shouldn't be removed without careful testing.

  4. Upstream patterns exist for a reason: When diverging from upstream behavior, especially around low-level hardware initialization, be extra cautious and test thoroughly.