fix: prevent Serial.printf from blocking when USB disconnected
All checks were successful
CI / build (push) Successful in 2m23s
All checks were successful
CI / build (push) Successful in 2m23s
On ESP32-C3 with USB CDC, Serial.printf() blocks indefinitely when USB is not connected. This caused device freezes when booted without USB. Solution: Call Serial.setTxTimeoutMs(0) after Serial.begin() to make all Serial output non-blocking. Also added if (Serial) guards to high-traffic logging paths in EpubReaderActivity as belt-and-suspenders protection. Includes documentation of the debugging process and Serial call inventory. Also applies clang-format to fix pre-existing formatting issues.
This commit is contained in:
70
claude_notes/missing-serial-guards-2026-01-28.md
Normal file
70
claude_notes/missing-serial-guards-2026-01-28.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Serial.printf Calls Without `if (Serial)` Guards
|
||||
|
||||
**Date:** 2026-01-28
|
||||
**Status:** Informational (not blocking issues)
|
||||
|
||||
## Summary
|
||||
|
||||
The codebase contains **408 Serial print calls** across 27 files in `src/`. Of these, only **16 calls** (in 2 files) have explicit `if (Serial)` guards.
|
||||
|
||||
**This is not a problem** because `Serial.setTxTimeoutMs(0)` is called in `setup()` before any activity code runs, making all Serial output non-blocking globally.
|
||||
|
||||
## Protection Mechanism
|
||||
|
||||
In `src/main.cpp` (lines 467-468):
|
||||
```cpp
|
||||
Serial.begin(115200);
|
||||
Serial.setTxTimeoutMs(0); // Non-blocking TX - critical for USB disconnect handling
|
||||
```
|
||||
|
||||
This ensures that even without `if (Serial)` guards, Serial.printf calls will return immediately when USB is disconnected instead of blocking indefinitely.
|
||||
|
||||
## Files with `if (Serial)` Guards (16 calls)
|
||||
|
||||
| File | Protected Calls |
|
||||
|------|-----------------|
|
||||
| `src/activities/reader/EpubReaderActivity.cpp` | 15 |
|
||||
| `src/main.cpp` | 1 |
|
||||
|
||||
## Files Without Guards (392 calls)
|
||||
|
||||
These calls are protected by `Serial.setTxTimeoutMs(0)` but don't have explicit guards:
|
||||
|
||||
| File | Unguarded Calls |
|
||||
|------|-----------------|
|
||||
| `src/network/CrossPointWebServer.cpp` | 106 |
|
||||
| `src/activities/network/CrossPointWebServerActivity.cpp` | 49 |
|
||||
| `src/activities/boot_sleep/SleepActivity.cpp` | 33 |
|
||||
| `src/BookManager.cpp` | 25 |
|
||||
| `src/activities/reader/TxtReaderActivity.cpp` | 20 |
|
||||
| `src/activities/home/HomeActivity.cpp` | 16 |
|
||||
| `src/network/OtaUpdater.cpp` | 16 |
|
||||
| `src/util/Md5Utils.cpp` | 15 |
|
||||
| `src/main.cpp` | 13 (plus 1 guarded) |
|
||||
| `src/WifiCredentialStore.cpp` | 12 |
|
||||
| `src/network/HttpDownloader.cpp` | 12 |
|
||||
| `src/BookListStore.cpp` | 11 |
|
||||
| `src/activities/network/WifiSelectionActivity.cpp` | 11 |
|
||||
| `src/activities/settings/OtaUpdateActivity.cpp` | 9 |
|
||||
| `src/activities/browser/OpdsBookBrowserActivity.cpp` | 9 |
|
||||
| `src/activities/settings/ClearCacheActivity.cpp` | 7 |
|
||||
| `src/BookmarkStore.cpp` | 6 |
|
||||
| `src/RecentBooksStore.cpp` | 5 |
|
||||
| `src/activities/reader/ReaderActivity.cpp` | 4 |
|
||||
| `src/activities/Activity.h` | 3 |
|
||||
| `src/CrossPointSettings.cpp` | 3 |
|
||||
| `src/activities/network/CalibreConnectActivity.cpp` | 2 |
|
||||
| `src/activities/home/ListViewActivity.cpp` | 2 |
|
||||
| `src/activities/home/MyLibraryActivity.cpp` | 1 |
|
||||
| `src/activities/dictionary/DictionarySearchActivity.cpp` | 1 |
|
||||
| `src/CrossPointState.cpp` | 1 |
|
||||
|
||||
## Recommendation
|
||||
|
||||
No immediate action required. The global `Serial.setTxTimeoutMs(0)` protection is sufficient.
|
||||
|
||||
If desired, `if (Serial)` guards could be added to high-frequency logging paths for minor performance optimization (skipping format string processing), but this is low priority.
|
||||
|
||||
## Note on open-x4-sdk
|
||||
|
||||
The `open-x4-sdk` submodule also contains Serial calls (in `EInkDisplay.cpp`, `SDCardManager.cpp`). These are also protected by the global timeout setting since `Serial.begin()` and `setTxTimeoutMs()` are called before any SDK code executes.
|
||||
125
claude_notes/serial-blocking-debug-2026-01-28.md
Normal file
125
claude_notes/serial-blocking-debug-2026-01-28.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Serial Blocking Debug Session Summary
|
||||
|
||||
**Date:** 2026-01-28
|
||||
**Issue:** Device freezes when booted without USB connected
|
||||
**Resolution:** `Serial.setTxTimeoutMs(0)` - make Serial TX non-blocking
|
||||
|
||||
## Problem Description
|
||||
|
||||
During release preparation for ef-0.15.9, the device was discovered to freeze completely when:
|
||||
1. Unplugged from USB
|
||||
2. Powered on via power button
|
||||
3. Book page displays, then device becomes unresponsive
|
||||
4. No button presses register
|
||||
|
||||
The device worked perfectly when USB was connected.
|
||||
|
||||
## Investigation Process
|
||||
|
||||
### Initial Hypotheses Tested
|
||||
|
||||
Multiple hypotheses were systematically investigated:
|
||||
|
||||
1. **Hypothesis A-D:** Display/rendering mutex issues
|
||||
- Added mutex logging to SD card
|
||||
- Mutex operations completed successfully
|
||||
- Ruled out as root cause
|
||||
|
||||
2. **Hypothesis E:** FreeRTOS task creation issues
|
||||
- Task created and ran successfully
|
||||
- First render completed normally
|
||||
- Ruled out
|
||||
|
||||
3. **Hypothesis F-G:** Main loop execution
|
||||
- Added loop counter logging to SD card
|
||||
- **Key finding:** Main loop never started logging
|
||||
- Setup() completed but loop() never executed meaningful work
|
||||
|
||||
4. **Hypothesis H-J:** Various timing and initialization issues
|
||||
- Tested different delays and initialization orders
|
||||
- No improvement
|
||||
|
||||
### Root Cause Discovery
|
||||
|
||||
The breakthrough came from analyzing the boot sequence:
|
||||
|
||||
1. `setup()` completes successfully
|
||||
2. `EpubReaderActivity::onEnter()` runs and calls `Serial.printf()` to log progress
|
||||
3. **Device hangs at Serial.printf() call**
|
||||
|
||||
On ESP32-C3 with USB CDC (USB serial), `Serial.printf()` blocks indefinitely waiting for the TX buffer to drain when USB is not connected. The default behavior expects a host to read the data.
|
||||
|
||||
### Evidence
|
||||
|
||||
- When USB connected: `Serial.printf()` returns immediately (data sent to host)
|
||||
- When USB disconnected: `Serial.printf()` blocks forever waiting for TX buffer space
|
||||
- The hang occurred specifically in `EpubReaderActivity.cpp` during progress logging
|
||||
|
||||
## Solution
|
||||
|
||||
### Primary Fix
|
||||
|
||||
Configure Serial to be non-blocking in `src/main.cpp`:
|
||||
|
||||
```cpp
|
||||
// Always initialize Serial but make it non-blocking
|
||||
Serial.begin(115200);
|
||||
Serial.setTxTimeoutMs(0); // Non-blocking TX - critical for USB disconnect handling
|
||||
```
|
||||
|
||||
`Serial.setTxTimeoutMs(0)` tells the ESP32 Arduino core to return immediately from Serial write operations if the buffer is full, rather than blocking.
|
||||
|
||||
### Secondary Protection (Belt and Suspenders)
|
||||
|
||||
Added `if (Serial)` guards to high-traffic Serial calls in `EpubReaderActivity.cpp`:
|
||||
|
||||
```cpp
|
||||
if (Serial) Serial.printf("[%lu] [ERS] Loaded progress...\n", millis());
|
||||
```
|
||||
|
||||
This provides an additional check before attempting to print, though it's not strictly necessary with the timeout set to 0.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/main.cpp` | Added `Serial.setTxTimeoutMs(0)` after `Serial.begin()` |
|
||||
| `src/main.cpp` | Added `if (Serial)` guard to auto-sleep log |
|
||||
| `src/main.cpp` | Added `if (Serial)` guard to max loop duration log |
|
||||
| `src/activities/reader/EpubReaderActivity.cpp` | Added 16 `if (Serial)` guards |
|
||||
|
||||
## Verification
|
||||
|
||||
After applying the fix:
|
||||
1. Device boots successfully when unplugged from USB
|
||||
2. Book pages render correctly
|
||||
3. Button presses register normally
|
||||
4. Sleep/wake cycle works
|
||||
5. No functionality lost when USB is connected
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **ESP32-C3 USB CDC behavior:** Serial output can block indefinitely without a connected host
|
||||
2. **Always set non-blocking:** `Serial.setTxTimeoutMs(0)` should be standard for battery-powered devices
|
||||
3. **Debug logging location matters:** When debugging hangs, SD card logging proved essential since Serial was the problem
|
||||
4. **Systematic hypothesis testing:** Ruled out many red herrings (mutex, task, rendering) before finding the true cause
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Why This Affects ESP32-C3 Specifically
|
||||
|
||||
The ESP32-C3 uses native USB CDC for serial communication (no external USB-UART chip). The Arduino core's default behavior is to wait for TX buffer space, which requires an active USB host connection.
|
||||
|
||||
### Alternative Approaches Considered
|
||||
|
||||
1. **Only initialize Serial when USB connected:** Partially implemented, but insufficient because USB can be disconnected after boot
|
||||
2. **Add `if (Serial)` guards everywhere:** Too invasive (400+ calls)
|
||||
3. **Disable Serial entirely:** Would lose debug output when USB connected
|
||||
|
||||
The chosen solution (`setTxTimeoutMs(0)`) provides the best balance: debug output works when USB is connected, device operates normally when disconnected.
|
||||
|
||||
## References
|
||||
|
||||
- ESP32 Arduino Core Serial documentation
|
||||
- ESP-IDF USB CDC documentation
|
||||
- FreeRTOS queue behavior (initial red herring investigation)
|
||||
Reference in New Issue
Block a user