Capture transmitted LED data via RX loopback - rx_channel: Shared pointer to RX device (persistent across calls) - rx_buffer: Buffer to store received bytes - timing: Chipset timing configuration for RX decoder - driver_name: Name of the TX driver being tested (e.g., "RMT", "PARLIO") - enables io_loop_back only for RMT Returns number of bytes captured, or 0 on error
| 315 | // - driver_name: Name of the TX driver being tested (e.g., "RMT", "PARLIO") - enables io_loop_back only for RMT |
| 316 | // Returns number of bytes captured, or 0 on error |
| 317 | size_t capture(fl::shared_ptr<fl::RxChannel> rx_channel, fl::span<uint8_t> rx_buffer, const fl::ChipsetTimingConfig& timing, const char* driver_name) { |
| 318 | if (!rx_channel) { |
| 319 | FL_ERROR("RX channel is null"); |
| 320 | return 0; |
| 321 | } |
| 322 | |
| 323 | // Clear RX buffer |
| 324 | fl::memset(rx_buffer.data(), 0, rx_buffer.size()); |
| 325 | |
| 326 | // Prepare RX config (but don't arm yet to avoid locking TX resources) |
| 327 | fl::RxChannelConfig rx_config(rx_channel->getPin()); |
| 328 | rx_config.hz = 40000000; // 40MHz for high-precision LED timing capture |
| 329 | |
| 330 | // Buffer size: 1 LED byte = 8 bits = 8 RMT symbols |
| 331 | // UART with TX inversion produces standard WS2812 waveform, same symbol count |
| 332 | bool is_uart_driver = (fl::strcmp(driver_name, "UART") == 0); |
| 333 | rx_config.edge_capacity = rx_buffer.size() * 8; |
| 334 | |
| 335 | // Internal loopback configuration: Enable ONLY for RMT TX -> RMT RX scenarios |
| 336 | // When driver_name == "RMT", enable io_loop_back to route RMT TX output to RMT RX internally |
| 337 | // This is REQUIRED for ESP32-S3 because TX GPIO output stops when RX is active on different GPIO |
| 338 | // For other drivers (PARLIO, SPI, UART, I2S), disable io_loop_back (use external GPIO wire) |
| 339 | bool is_rmt_driver = (fl::strcmp(driver_name, "RMT") == 0); |
| 340 | rx_config.io_loop_back = is_rmt_driver; |
| 341 | // RX DMA streaming: extends capture past the non-DMA cap by sizing |
| 342 | // mem_block_symbols = 14336 so ESP-IDF allocates ~14 DMA descriptor |
| 343 | // nodes (each 4092 bytes), yielding a 57 KB user-buffer cap — enough to |
| 344 | // capture ~583 WS2812B LEDs in a single rmt_receive() call. Safe for |
| 345 | // non-RMT TX paths (SPI, PARLIO, I2S, UART, LCD_*) that don't contend |
| 346 | // for the shared ESP32-S3 DMA slot. RMT loopback stays non-DMA. |
| 347 | // See issue #2254. |
| 348 | rx_config.use_dma = !is_rmt_driver; |
| 349 | if (is_rmt_driver) { |
| 350 | FL_WARN("[CAPTURE] RMT TX -> RMT RX: Internal loopback enabled (io_loop_back=true)"); |
| 351 | } else { |
| 352 | // The RX peripheral is platform-dependent (RMT on ESP32, FlexPWM on |
| 353 | // Teensy 4, LPC_SCT on LPC845, …). Don't claim "RMT RX" on platforms |
| 354 | // that have no RMT — that label burned an hour of Teensy-4 debug |
| 355 | // (FastLED#3059). Just say "external RX" so the label stays correct |
| 356 | // regardless of backend. |
| 357 | FL_WARN("[CAPTURE] " << driver_name << " TX -> external RX: External GPIO wire (io_loop_back=false, use_dma=true)"); |
| 358 | } |
| 359 | |
| 360 | // Driver-aware capture strategy: |
| 361 | // - RMT: Two-TX approach (ESP32-S3 workaround - TX GPIO blocked when RX active) |
| 362 | // - PARLIO/SPI/other: Single-TX approach (arm RX first, then TX) |
| 363 | |
| 364 | // TX wait timeout: 1 second max per frame - prevents infinite hang if driver stalls |
| 365 | // Even a 10000-LED strip at 800kbps takes only ~300ms, so 1s is very safe |
| 366 | const uint32_t TX_WAIT_TIMEOUT_MS = 1000; |
| 367 | |
| 368 | if (is_rmt_driver) { |
| 369 | // RMT: Two-TX approach for ESP32-S3 compatibility |
| 370 | // First TX without RX armed (diagnostics), then arm RX, then second TX |
| 371 | FL_WARN("[CAPTURE] RMT: Two-TX approach (ESP32-S3 workaround)"); |
| 372 | FastLED.show(); |
| 373 | if (!FastLED.wait(TX_WAIT_TIMEOUT_MS)) { |
| 374 | FL_ERROR("[CAPTURE] TX wait timeout (pre-arm) - driver may be stalled"); |
no test coverage detected