FastLED 3.10.9
Loading...
Searching...
No Matches
AutoResearch.ino
Go to the documentation of this file.
1// @filter
2// require:
3// - board: esp32dev_idf44,esp32s3,esp32c6,esp32p4,teensy41,teensy40,rp2350,rp2350w,lpc845,lpc845brk,lpcxpresso845max,lpcxpresso804
4// @end-filter
5//
6// Low-memory mode (FastLED #3030): on Low-tier targets (LPC8xx etc., per
7// FastLED #3000 memory classification), this sketch is folded down to the
8// JSON-RPC echo + SCT-RX bring-up surface inherited from the retired
9// `examples/AutoResearchLpc/AutoResearchLpc.ino`. Auto-enabled when
10// `FL_PLATFORM_HAS_LARGE_MEMORY == 0`.
11
13#include "fl/system/heap.h"
14
15#if !FL_PLATFORM_HAS_LARGE_MEMORY
19#else // FL_PLATFORM_HAS_LARGE_MEMORY
20
21// examples/AutoResearch/AutoResearch.ino
22//
23// FastLED LED Timing AutoResearch Sketch for ESP32.
24//
25// This sketch validates LED output by reading back timing values using the
26// RMT peripheral in receive mode. It performs TX→RX loopback testing to verify
27// that transmitted LED data matches received data.
28//
29// DEMONSTRATES:
30// 1. Runtime Channel API (FastLED.add) for iterating through all available
31// drivers (RMT, SPI, PARLIO) and testing multiple chipset timings dynamically
32// by creating and destroying controllers for each driver.
33// 2. Multi-channel autoresearch support: Pass span<const ChannelConfig> to validate
34// multiple LED strips/channels simultaneously. Each channel is independently
35// validated with its own RX loopback channel.
36//
37// Use case: When developing a FastLED driver for a new peripheral, it is useful
38// to read back the LED's received data to verify that the timing is correct.
39//
40// MULTI-CHANNEL MODE:
41// - Single-channel: Pass one ChannelConfig - uses shared RX channel object (created in setup())
42// - Multi-channel: Pass multiple ChannelConfigs - creates dynamic RX channels
43// on each TX pin for independent jumper wire autoresearch
44// - Each channel in the span is validated sequentially with its own RX channel
45
46// Hardware Setup:
47// ⚠️ IMPORTANT: Physical jumper wire required when TX and RX use different GPIOs
48//
49// When TX and RX use distinct pins:
50// - Connect GPIO PIN_TX to GPIO PIN_RX with a physical jumper wire
51// - This applies to RMT as well as SPI, ParallelIO, UART, and I2S TX
52// - ESP32 GPIO matrix cannot route other peripheral outputs internally to RMT input
53//
54// When RMT TX and RMT RX use the same GPIO:
55// - No jumper wire is needed; io_loop_back uses ESP-IDF's same-pin path
56// - This preserves the ESP32-S3 RMT workaround
57//
58// Alternative: Connect an LED strip between TX pin and ground, then connect
59// RX pin to LED data line to capture actual LED protocol timing (requires
60// two separate GPIO pins for TX and RX).
61//
62// Platform Support:
63// - ESP32 (classic)
64// - ESP32-S3 (Xtensa)
65// - ESP32-C3 (RISC-V)
66// - ESP32-C6 (RISC-V)
67// - Raspberry Pi Pico 2 (RP2350)
68// - Raspberry Pi Pico 2 W (RP2350 + CYW43)
69//
70// Expected Output:
71// Serial monitor will show:
72// - List of discovered drivers (RMT, SPI, PARLIO - availability depends on platform)
73// - Test results for each driver with PASS/FAIL status for each chipset timing
74// - Each driver is tested independently by creating/destroying channels
75// - In multi-channel mode: Separate autoresearch results for each channel/pin
76//
77// MULTI-CHANNEL EXAMPLE:
78// ```cpp
79// // Create multiple LED arrays
80// CRGB leds1[100], leds2[100], leds3[100];
81//
82// // Create channel configs for each strip on different pins
83// fl::ChannelConfig configs[] = {
84// fl::ChannelConfig(5, timing, fl::span<CRGB>(leds1, 100), RGB),
85// fl::ChannelConfig(6, timing, fl::span<CRGB>(leds2, 100), RGB),
86// fl::ChannelConfig(7, timing, fl::span<CRGB>(leds3, 100), RGB)
87// };
88//
89// // Validate all 3 channels (each gets its own RX loopback)
90// autoResearchChipsetTiming(timing, "WS2812", fl::span(configs, 3), nullptr, buffer);
91// ```
92//
93// ASYNC ARCHITECTURE:
94// The JSON-RPC system runs on an async task (10ms interval) registered during setup().
95// The task calls RemoteControlSingleton::tick() and processSerialInput() automatically.
96// loop() aggressively pumps fl::task::run() (100x per iteration) to ensure responsiveness.
97//
98
99// ============================================================================
100// AGENT INSTRUCTIONS
101// ============================================================================
102// AutoResearch runtime output must stay machine-readable. Use JSON-RPC
103// responses and RESULT JSONL events for test state, failures, and diagnostics.
104//
105// 🚫 DO NOT "CHEAT" THE TEST:
106// - DO NOT change "ERROR" to "FAIL", "WARNING", "FAILURE", or any other
107// keyword to avoid test detection
108// - DO NOT add FL_ERROR, FL_PRINT, FL_WARN, Serial.print*, fl::Serial.print*,
109// fl::print*, fl::write_bytes, or fl::serial print/write/flush runtime
110// output in this sketch or any `autoresearch-runtime-output-lint` section.
111// fl::serial_begin/ready setup helpers are allowed. Use JSON-RPC / RESULT JSONL.
112// - The "ERROR" keyword is INTENTIONAL and part of the autoresearch contract
113//
114// βœ… VALID MODIFICATIONS (only if explicitly requested):
115// - User asks: "Change the error message format"
116// - User asks: "Use a different warning macro"
117// - User asks: "Remove the ERROR prefix"
118//
119// ❌ INVALID MODIFICATIONS (considered "cheating"):
120// - Changing "ERROR" to avoid `bash debug --fail-on ERROR` detection
121// - Renaming to bypass automated test failure detection
122// - Any modification that circumvents the autoresearch contract
123//
124// If the test is failing, investigate and FIX THE ACTUAL BUG that causes
125// the "ERROR" message to be printed. Do not hide the symptom by changing
126// the error message keyword.
127// ============================================================================
128// JSON-RPC SCRIPTING LANGUAGE:
129// This sketch uses JSON-RPC for all test orchestration. Examples:
130//
131// # Run autoresearch with pure JSON-RPC (no text patterns needed)
132// bash autoresearch --all
133//
134// # Custom JSON-RPC workflow
135// uv run python -c "
136// from ci.rpc_client import RpcClient
137// with RpcClient('/dev/ttyUSB0') as c:
138// c.send('configure', {'driver':'PARLIO', 'laneSizes':[100]})
139// result = c.send('runTest')
140// print(result)
141// "
142//
143// Direct text output is not allowed in runtime paths while tests are running.
144// Machine coordination uses ONLY JSON-RPC commands and JSONL events.
145// ============================================================================
146
147
148// Enable async PARLIO logging to prevent ISR watchdog timeout
149// Async logging queues messages from ISRs and drains them from the main loop
150#define FASTLED_LOG_PARLIO_ASYNC_ENABLED
151
152// Disable PARLIO ISR logging BEFORE any FastLED includes to prevent watchdog timeout
153// (Serial printing inside ISR exceeds watchdog threshold - must disable at translation unit level)
154// #undef FASTLED_LOG_PARLIO_ENABLED // COMMENTED OUT: Agent loop instructions require FL_LOG_PARLIO_ENABLED to be kept enabled
155
156// ============================================================================
157// Test Matrix Configuration - Multi-Driver, Multi-Lane, Variable Strip Size
158// ============================================================================
159//
160// This sketch now supports comprehensive matrix testing with the following dimensions:
161//
162// 1. DRIVER SELECTION (5 options):
163// - Uncomment to test ONLY a specific driver:
164// // #define JUST_PARLIO // Test only PARLIO driver
165// // #define JUST_RMT // Test only RMT driver
166// // #define JUST_SPI // Test only SPI driver
167// // #define JUST_UART // Test only UART driver
168// // #define JUST_I2S // Test only I2S LCD_CAM driver (ESP32-S3 only)
169// - Default: Test all available drivers (RMT, SPI, PARLIO, UART, I2S)
170//
171// 2. LANE RANGE (1-16 lanes):
172// - Uncomment to override lane range:
173// // #define MIN_LANES 1 // Minimum number of lanes to test
174// // #define MAX_LANES 16 // Maximum number of lanes to test
175// - Default: MIN_LANES=1, MAX_LANES=16 (tests all lane counts)
176// - Each lane has decreasing LED count: Lane 0=base, Lane 1=base-1, ..., Lane N=base-N
177// - Multi-lane RX autoresearch: Only Lane 0 is validated (hardware limitation)
178//
179// 3. STRIP SIZE (2 options):
180// - Uncomment to test ONLY a specific strip size:
181// // #define JUST_SMALL_STRIPS // Test only short strips (10 LEDs)
182// // #define JUST_LARGE_STRIPS // Test only long strips (300 LEDs)
183// - Default: Test both small (10 LEDs) and large (300 LEDs) strips
184//
185// TEST MATRIX SIZE:
186// - Full matrix: 3 drivers Γ— 16 lane counts Γ— 2 strip sizes = 96 test cases
187// - Use defines above to narrow scope for faster debugging
188//
189// EXAMPLES:
190// - Test only RMT with 4 lanes on small strips:
191// #define JUST_RMT
192// #define MIN_LANES 4
193// #define MAX_LANES 4
194// #define JUST_SMALL_STRIPS
195//
196// - Test all drivers with 1-3 lanes on large strips:
197// #define MAX_LANES 3
198// #define JUST_LARGE_STRIPS
199//
200// ============================================================================
201
202#include <FastLED.h>
203#include "fl/channels/all_drivers.h" // for FastLED.enableAllDrivers() post-#2428
204#include "fl/wdt/watchdog.h" // FL_WATCHDOG_AUTO() β€” unified cross-platform WDT guard
205#include "fl/stl/undef.h" // Undefine Arduino macros (DEFAULT, INPUT, OUTPUT)
206#include "fl/stl/sstream.h"
207#include "Common.h"
208#include "AutoResearchTest.h"
209#include "AutoResearchHelpers.h"
210#include "AutoResearchRemote.h"
211#include "AutoResearchBle.h"
212#include "AutoResearchNet.h"
213#include "AutoResearchOta.h"
214#include "AutoResearchAsync.h"
215#include "AutoResearchPlatform.h"
216#include "AutoResearchSimd.h"
217#include "AutoResearchWave8Expand.h" // boot-time #2526 micro-bench
218#include "AutoResearchParlioEncode.h" // full parlio encode bench (post-byte-LUT)
219#include "AutoResearchTimingDrift.h" // #2994 compounded-timing-drift repro
220#include "AutoResearchParlioStream.h" // #2548 PARLIO streaming validation (RPC-driven)
221
222// ============================================================================
223// Loop-task stack (FastLED#3569)
224// ============================================================================
225// The RPC-dispatch β†’ runSingleTest β†’ autoResearchChipsetTiming β†’
226// capture β†’ FastLED.show() chain is bench-measured at ~8.0 KB deep on
227// classic ESP32 β€” the default 8 KB loopTask stack trips the canary the
228// moment an interrupt frame lands at peak depth. 16 KB fixes that, but
229// ONLY together with the classic-ESP32 RX_BUFFER_SIZE tier defined
230// further down in this file (search RX_BUFFER_SIZE): with the 96 KB
231// capture tier the extra 8 KB of stack exhausted the heap outright
232// (`fl::aligned_alloc` returned null inside fl::json::set β†’
233// StoreProhibited at 0x400d2ece). Keep the two changes paired.
234// FastLED#3576 Phase 9: extended from classic-only to all ESP32
235// variants β€” ESP32-P4 hit the same ~8 KB-deep chain and died with a
236// "Stack protection fault" in loopTask (stack dump full of wave8 LUT
237// data) on its default 8 KB stack during SPI/UART/PARLIO tests.
238#if defined(FL_IS_ESP32) && defined(SET_LOOP_TASK_STACK_SIZE)
239SET_LOOP_TASK_STACK_SIZE(16 * 1024);
240#endif
241
242// ============================================================================
243// Hardware Watchdog (crash recovery)
244// ============================================================================
245// Use the unified cross-platform fl::Watchdog API via the FL_WATCHDOG_AUTO()
246// macro at the top of loop(). It lazily arms the WDT on first call, prints
247// the prior-boot reset/crash diagnostic, pauses 3 s on a crash so the
248// developer can read the message, and feeds the WDT on both ctor and dtor.
249// Works portably across ESP32 / Teensy 4 / Teensy 3 / RP2040 / nRF52 /
250// STM32 / SAMD / Apollo3 / MGM240 / AVR (the noop fallback on platforms
251// without a real WDT keeps the call safe).
252//
253// The previous `fl::watchdog_setup(...)` prototype is superseded by
254// `fl::Watchdog::instance().begin()` and the FL_WATCHDOG_AUTO() macro.
255
256// ============================================================================
257// Configuration
258// ============================================================================
259
260// Serial port timeout (milliseconds) - wait for serial monitor to attach
261static constexpr uint32_t SERIAL_TIMEOUT_MS = 120000; // 120 seconds
262#if defined(FL_IS_ESP_32S3) || defined(FL_IS_ESP_32C3) || \
263 defined(FL_IS_ESP_32C6) || defined(FL_IS_ESP_32H2) || \
264 defined(CONFIG_IDF_TARGET_ESP32P4) || defined(FL_IS_RP)
265static constexpr uint32_t AUTORESEARCH_SERIAL_WAIT_MS = 2000;
266#else
267static constexpr uint32_t AUTORESEARCH_SERIAL_WAIT_MS = SERIAL_TIMEOUT_MS;
268#endif
269
271
272// AutoResearch must recover from a wedged driver/RPC command without host
273// intervention. Keep this shorter than the Python RPC timeout so a device-side
274// hang becomes a watchdog reboot instead of a permanently owned/dead port.
275static constexpr uint32_t AUTORESEARCH_WATCHDOG_TIMEOUT_MS = 5000;
276
277// setup() runs before FL_WATCHDOG_AUTO() at the top of loop() can arm the
278// timer, so we arm the WDT explicitly at the very top of setup() with a
279// longer window that covers normal setup work (serial handshake up to
280// 2 s, RPC handler registration ~65 binds, PSRAM RX-buffer allocation,
281// FastLED singleton init, driver enumeration). Without this, a hang in
282// setup() β€” e.g., a null-deref inside an RPC bind lambda or a stalled
283// singleton static-init β€” wedges the COM port until the user power-
284// cycles the board. 20 s is generous enough for cold boot yet still
285// short enough that host-side autoresearch RPC timeouts (default 30 s)
286// surface the reboot cleanly.
287static constexpr uint32_t AUTORESEARCH_SETUP_WATCHDOG_TIMEOUT_MS = 20000;
288
289// ============================================================================
290// Platform-Specific Pin Defaults
291// ============================================================================
292// These defaults are chosen based on hardware constraints of each platform.
293// They can be overridden at runtime via JSON-RPC (setPins, setTxPin, setRxPin).
294
295constexpr int DEFAULT_PIN_TX = autoresearch::defaultTxPin();
296constexpr int DEFAULT_PIN_RX = autoresearch::defaultRxPin();
297
298// Legacy macros for backward compatibility in existing code
299#define PIN_TX g_autoresearch_state->pin_tx
300#define PIN_RX g_autoresearch_state->pin_rx
301
302#define CHIPSET WS2812B
303#define COLOR_ORDER RGB // No reordering needed.
304
305// RX buffer sized for maximum expected strip size
306// Each LED = 24 bits = 24 symbols, plus headroom for RESET pulses
307// Maximum: 3000 LEDs (hardcoded for ESP32/S3 with PSRAM support)
308#if defined(FL_IS_TEENSY_4X)
309// Teensy 4.x: the FlexPWM RX streams through a small DMA ring and FlexIO /
310// ObjectFLED TX stream from ISRs, so the 1000-LED loopback fits. This
311// shared buffer sets the per-lane cap (size / 32 = 1003 LEDs).
312constexpr int RX_BUFFER_SIZE = 1000 * 32 + 100;
313#elif defined(FL_IS_ESP_32C6) || defined(FL_IS_ESP_32H2) || \
314 defined(FL_IS_ESP_32C5) || defined(FL_IS_RP)
315constexpr int RX_BUFFER_SIZE = 100 * 32 + 100; // Memory-constrained: 100 LEDs max for autoresearch
316#elif defined(FL_IS_ESP_32DEV)
317// FastLED#3569 β€” classic ESP32-WROOM (~320 KB DRAM, no PSRAM) ran out
318// of heap during runSingleTest with the 96 KB tier: `fl::aligned_alloc`
319// returned null inside fl::json::set (StoreProhibited @ 0x400d2ece).
320// 1000 LEDs Γ— 32 symbols covers every bench scenario and frees 64 KB
321// for the 16 KB loop stack + result-JSON building.
322constexpr int RX_BUFFER_SIZE = 1000 * 32 + 100;
323#else
324constexpr int RX_BUFFER_SIZE = 3000 * 32 + 100; // LEDs Γ— 32:1 expansion + headroom
325#endif
326// FastLED#4903 β€” the 3000-LED tier assumes PSRAM. A board that reports heap
327// but no PSRAM (a XIAO ESP32-S3 here) was left with 30 KB of heap, so the RX
328// capture could not allocate and every loopback decode stopped at 200 edges.
329// Such boards use the 1000-LED tier, as classic ESP32 does above (#3569).
330constexpr int RX_BUFFER_SIZE_NO_PSRAM = 1000 * 32 + 100;
331
332static int rxBufferSize() {
333 const fl::HeapInfo heap = fl::getFreeHeap();
334 if (RX_BUFFER_SIZE > RX_BUFFER_SIZE_NO_PSRAM && heap.total() > 0 &&
335 !heap.has_psram()) {
336 return RX_BUFFER_SIZE_NO_PSRAM;
337 }
338 return RX_BUFFER_SIZE;
339}
340
341// Factory function for creating RxChannel instances
342// This allows AutoResearchRemoteControl to recreate the RX channel when the pin changes
343fl::shared_ptr<fl::RxChannel> createRxDevice(int pin) {
344 fl::RxChannelConfig config(pin, RX_BACKEND);
345 fl::shared_ptr<fl::RxChannel> channel = FastLED.addRx(config);
346 if (!channel || channel->getPin() != pin) {
348 }
349 return channel;
350}
351
352// Global autoresearch state (shared between main loop and RPC handlers)
353// Use PSRAM-backed vector for RX buffer to avoid DRAM overflow on ESP32S2
354fl::vector_psram<uint8_t> g_rx_buffer_storage; // Actual buffer storage (falls back to SRAM without PSRAM)
355fl::shared_ptr<AutoResearchState> g_autoresearch_state; // Shared state pointer
356
357// ============================================================================
358// Serial Initialization Helper
359// ============================================================================
360
361// Initialize serial buffers with platform-specific configuration
362// Note: Some boards (ESP32S2) don't support setTxBufferSize() on USBCDC interface
363void init_serial_buffers() {
364#if defined(FL_IS_ESP32) && !defined(FL_IS_ESP_32S2)
365 // A 512-byte OTA chunk expands beyond the ESP32 HWCDC default 256-byte
366 // receive queue once base64 and JSON-RPC framing are added.
367 Serial.setRxBufferSize(4096); // ok serial - platform-specific RX buffer sizing, no fl:: equivalent
368 Serial.setTxBufferSize(4096); // 4KB buffer (default is 256 bytes) // ok serial - platform-specific TX buffer sizing, no fl:: equivalent
369#endif
370}
371
372// ============================================================================
373// Global State
374// ============================================================================
375
376// Remote RPC system for dynamic test control via JSON commands
377// Using Singleton pattern for thread-safe lazy initialization
378using RemoteControlSingleton = fl::Singleton<AutoResearchRemoteControl>;
379
380
381// ============================================================================
382// Global AutoResearch State (Simplified - No Test Matrix)
383// ============================================================================
384
385// Frame counter - tracks which iteration of loop() we're on (diagnostic)
386uint32_t frame_counter = 0;
387
388
389// The early-boot loop guard (FL_WATCHDOG_BOOT_GUARD) lives in
390// AutoResearchStartupHook.cpp.
391
392// Consecutive watchdog resets before AutoResearch gives up and drops into the
393// bootloader (#3713). Three tolerates a one-off glitch or a deliberate
394// watchdog-recovery test while still escaping quickly enough to matter on an
395// unattended bench.
396#ifndef AUTORESEARCH_BOOTLOADER_ESCAPE_RESETS
397#define AUTORESEARCH_BOOTLOADER_ESCAPE_RESETS 3
398#endif
399
400void setup() {
401 // Arm the WDT before doing any real work so a hang here (RPC bind
402 // null-deref, stalled static-init, wedged Serial handshake) auto-
403 // reboots the chip instead of leaving the COM port owned by a
404 // dead sketch. FL_WATCHDOG_AUTO() at loop() re-arms with the
405 // shorter loop-scoped timeout once setup() has finished.
406 fl::Watchdog::instance().begin(AUTORESEARCH_SETUP_WATCHDOG_TIMEOUT_MS);
407
408 // Boot-loop escape (#3713). The WDT above turns a hang into a reboot, but
409 // a hang that recurs every boot just reboots forever and the board is only
410 // ever briefly enumerated -- unattended, that is indistinguishable from a
411 // brick. After N consecutive watchdog resets, drop to the bootloader
412 // instead, where the board re-enumerates as a flashable target and a host
413 // can push working firmware without anyone holding BOOTSEL.
414 //
415 // Placed immediately after begin() so it runs before anything that could
416 // hang -- serial, RPC bind, or a peripheral absent on this board variant.
418 AUTORESEARCH_BOOTLOADER_ESCAPE_RESETS);
420
421 // Initialize serial buffers with platform-specific configuration
422 // Must be called BEFORE Serial.begin()
423 init_serial_buffers();
424 fl::serial_begin(115200);
426#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
427 // Make HWCDC writes drop instead of block when no host is reading.
428 // Without this, Serial.print() on ESP32-C3/C6/H2 can stall up to ~2s
429 // per call once the TX ring fills with no host draining it.
430 // See FastLED issue #2668 and arduino-esp32 PR #7583.
431 // Redundant with fl::serial_begin() which already sets this on HWCDC,
432 // kept as defense-in-depth for sketches that bypass fl::serial_begin.
433 Serial.setTxTimeoutMs(0); // ok serial - platform-specific TX timeout, no fl:: equivalent
434#endif
435 uint32_t serial_wait_start = millis();
436 while (!fl::serial_ready() && (millis() - serial_wait_start) < AUTORESEARCH_SERIAL_WAIT_MS) {
437 // Feed the WDT while waiting for the serial monitor β€” on
438 // classic ESP32 (non-USB-CDC) this loop can run for up to
439 // AUTORESEARCH_SERIAL_WAIT_MS (120 s), far longer than the
440 // setup WDT window. Without feeding here, the WDT trips and
441 // the device reboots before the host ever attaches, leaving
442 // the port silent.
443 FastLED.watchdog().feed();
444 }
445#if defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT
446 if (fl::serial_ready()) {
447 // A host is attached and draining: allow writes to block up to
448 // 100 ms instead of dropping. With tx timeout 0, every RPC
449 // response longer than the 64-byte USB-Serial-JTAG FIFO was
450 // truncated mid-line on the C6 bench (host parse failures).
451 // Headless boots keep the drop-fast behavior above.
452 Serial.setTxTimeoutMs(100); // ok serial - platform-specific TX timeout, no fl:: equivalent
453 }
454#endif
455
456 // The RP watchdog count is retained through a watchdog reboot. Do this
457 // before allocating buffers, binding RPC handlers, claiming PIO, or
458 // enabling any LED driver: repeated early-startup failures then leave the
459 // USB CDC control path alive instead of endlessly retrying the risky path.
460 // This is intentionally AutoResearch-only; normal user sketches choose
461 // their own recovery policy through FastLED.watchdog().
462#if !defined(FL_IS_STUB) && !defined(FL_IS_WASM)
463 const fl::u16 watchdog_crash_count = FastLED.watchdog().consecutiveCrashCount();
464 if (FastLED.watchdog().isInSafeMode()) {
465 fl::json recovery = fl::json::object();
466 recovery.set("reason", "watchdog_reset_loop");
467 recovery.set("crashCount", static_cast<int64_t>(watchdog_crash_count));
468 recovery.set("safeModeThreshold", static_cast<int64_t>(FastLED.watchdog().safeModeThreshold()));
469 printStreamRaw("safeMode", recovery);
470 while (true) {
471 FastLED.watchdog().feed();
472 delay(100);
473 }
474 }
475#endif
476
477 // Watchdog was armed at the very top of setup() above with the longer
478 // AUTORESEARCH_SETUP_WATCHDOG_TIMEOUT_MS window so a hang here (RPC bind,
479 // singleton init, wedged serial) auto-reboots the chip. FL_WATCHDOG_AUTO()
480 // at the top of loop() re-arms with the shorter loop-scoped timeout and
481 // prints the prior-boot reset info via ResetInfo::describe(), pausing 3 s
482 // on crash before the new timer arms. Per-platform boot diagnostics
483 // (Teensy 4 SRC_SRSR bit decode + bundled CrashReport, ESP32 panic
484 // backtrace, etc.) are emitted by the platform watchdog impl from
485 // Watchdog::begin() β€” see src/fl/wdt/watchdog.h and
486 // src/platforms/*/watchdog_*.impl.hpp.
487
488 // Initialize RX buffer dynamically (uses PSRAM if available, falls back to heap)
489 g_rx_buffer_storage.resize(rxBufferSize());
490
491 // Initialize global autoresearch state
492 g_autoresearch_state = fl::make_shared<AutoResearchState>();
493 g_autoresearch_state->pin_tx = DEFAULT_PIN_TX;
494 g_autoresearch_state->pin_rx = DEFAULT_PIN_RX;
495 g_autoresearch_state->default_pin_tx = DEFAULT_PIN_TX;
496 g_autoresearch_state->default_pin_rx = DEFAULT_PIN_RX;
497 g_autoresearch_state->rx_buffer = g_rx_buffer_storage;
498 g_autoresearch_state->rx_factory = createRxDevice;
499
500 const char* loop_back_mode = PIN_TX == PIN_RX ? "INTERNAL" : "JUMPER WIRE";
501
502 // Build header and platform/hardware info
503 fl::sstream ss;
504 ss << "\n╔════════════════════════════════════════════════════════════════╗\n";
505 ss << "β•‘ FastLED AutoResearch - Test Matrix Configuration β•‘\n";
506 ss << "β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n";
507
508 // Platform information
509 ss << "\n[PLATFORM]\n";
510 ss << " Chip: " << autoresearch::chipName() << "\n";
511
512 // Hardware configuration - machine-parseable for --expect autoresearch
513 ss << "\n[HARDWARE]\n";
514 ss << " TX Pin: " << PIN_TX << "\n"; // --expect "TX Pin: 0"
515 ss << " RX Pin: " << PIN_RX << "\n"; // --expect "RX Pin: 1"
516 ss << " RX Device: " << fl::toString(RX_BACKEND) << "\n";
517 ss << " Loopback Mode: " << loop_back_mode << "\n";
518 ss << " Color Order: RGB\n";
519 ss << " RX Buffer Size: " << g_rx_buffer_storage.size() << " bytes";
520
521 // SIMD validation suite, Wave8 expansion micro-bench, and full PARLIO
522 // encode bench are RPC-driven β€” invoke via the `testSimd`,
523 // `wave8ExpandBenchmark`, and `parlioEncodeBenchmark` handlers in
524 // AutoResearchRemote.cpp. Do not add boot-time invocation blocks here;
525 // if RPC routing is broken on a platform, fix the routing (see #2541).
526
527 // ========================================================================
528 // Remote RPC Function Registration (before fallible peripheral setup)
529 // ========================================================================
530 // Pin-free bring-up and diagnostics must remain reachable even when a
531 // platform has no usable RX channel yet. In particular, stock RP2040
532 // boards can fail createRxDevice() during initial bring-up; registering
533 // below that early return left a live CDC transport with an empty RPC
534 // registry.
535 ss.clear();
536 ss << "\n[REMOTE RPC] Registering JSON RPC functions for dynamic control";
537 RemoteControlSingleton::instance().registerFunctions(g_autoresearch_state);
538
539 autoresearch::setupRpcAsyncTask(RemoteControlSingleton::instance(), 10);
540
541 // Stub: register self-running autoresearch client (no-op on hardware).
542 autoresearch::maybeRegisterStubAutorun(RemoteControlSingleton::instance(),
543 g_autoresearch_state);
544
545 // ========================================================================
546 // RX Channel Setup
547 // ========================================================================
548
549 ss.clear();
550 ss << "\n[RX SETUP] Creating RX channel for LED autoresearch\n";
551 ss << "[RX CREATE] Creating RX channel on PIN " << PIN_RX
552 << " (" << (40000000 / 1000000) << "MHz, " << g_rx_buffer_storage.size() << " symbols)";
553
554 g_autoresearch_state->rx_channel = createRxDevice(PIN_RX);
555
556 if (!g_autoresearch_state->rx_channel) {
557 fl::json errorData = fl::json::object();
558 errorData.set("error", "RxChannelCreationFailed");
559 errorData.set("message", "Failed to create RX channel during setup");
560 errorData.set("pinRx", static_cast<int64_t>(PIN_RX));
561 errorData.set("rxBackend", fl::toString(RX_BACKEND));
562 printStreamRaw("setupError", errorData);
563 return;
564 }
565
566 ss.clear();
567 ss << "[RX CREATE] βœ“ RX channel created successfully (will be initialized with config in begin())\n";
568 ss << "[RX SETUP] βœ“ RX channel ready for LED autoresearch";
569
570 // PARLIO streaming validation (#2548) is now RPC-driven via the
571 // `parlioStreamValidate` handler registered in AutoResearchRemote.cpp.
572
573 // ========================================================================
574 // GPIO Baseline Test - Verify GPIO→GPIO path works before testing PARLIO
575 // ========================================================================
576 ss.clear();
577 ss << "\n[GPIO BASELINE TEST] Testing GPIO " << PIN_TX << " β†’ GPIO " << PIN_RX << " connectivity";
578
579 // GPIO baseline test moved to loop() - wait for RPC start signal before testing
580 // This allows JSON-RPC commands (testGpioConnection, findConnectedPins) to run first
581
582 // Post-#2428 the channel driver registry no longer auto-populates. This
583 // example needs every available driver enrolled with ChannelManager so the
584 // discovery + autoresearch loop below can enumerate them.
585 FastLED.enableAllDrivers();
586
587 // List all available drivers and store globally
588 g_autoresearch_state->drivers_available = FastLED.getDriverInfos();
589 ss.clear();
590 ss << "\n[DRIVER DISCOVERY]\n";
591 ss << " Found " << g_autoresearch_state->drivers_available.size() << " driver(s) available:\n";
592 for (fl::size i = 0; i < g_autoresearch_state->drivers_available.size(); i++) {
593 ss << " " << (i+1) << ". " << g_autoresearch_state->drivers_available[i].name.c_str()
594 << " (priority: " << g_autoresearch_state->drivers_available[i].priority
595 << ", enabled: " << (g_autoresearch_state->drivers_available[i].enabled ? "yes" : "no") << ")\n";
596 }
597
598 // Validate that expected drivers are available for this platform
600
601 // Emit JSON-RPC ready event for Python orchestration
602 fl::json readyData = fl::json::object();
603 readyData.set("ready", true);
604 readyData.set("setupTimeMs", static_cast<int64_t>(millis()));
605 readyData.set("drivers", static_cast<int64_t>(g_autoresearch_state->drivers_available.size()));
606 readyData.set("pinTx", static_cast<int64_t>(PIN_TX));
607 readyData.set("pinRx", static_cast<int64_t>(PIN_RX));
608 readyData.set("chip", autoresearch::chipName());
609 readyData.set("rxBackend", fl::toString(RX_BACKEND));
610 readyData.set("loopbackMode", loop_back_mode);
611 readyData.set("rxBufferSize", static_cast<int64_t>(g_rx_buffer_storage.size()));
612 printStreamRaw("ready", readyData);
613
614 delay(2000);
615
616 // Early-boot guard (AutoResearchStartupHook.cpp): this boot got through
617 // setup(), so reset the count of boots that never got this far.
618 FastLED.watchdog().markBootHealthy();
619}
620
621// ============================================================================
622// Main Loop - Pure Command Runner (Phase 6 Refactoring)
623// ============================================================================
624// The main loop is simplified to be a pure JSON-RPC command runner.
625// All test orchestration is handled by Python via RPC commands like:
626// - testGpioConnection: Pre-flight hardware check
627// - runQuickTest: Fast single-test execution
628// - runAll: Full test matrix execution
629// - configure: Setup test parameters
630//
631// This architecture enables:
632// - Fast test iteration (<100ms per test case)
633// - Python-controlled test sequencing
634// - Easy retry logic and error recovery
635
636void loop() {
637 // Unified watchdog guard. First iteration: arms the WDT, prints any
638 // prior-boot reset/crash info, pauses 3 s on a crash. Every iteration:
639 // feeds on construction (now) and again on destruction (end of scope).
640 FL_WATCHDOG_AUTO(AUTORESEARCH_WATCHDOG_TIMEOUT_MS);
641
642
643 // Aggressively pump async tasks (including JSON-RPC task)
644 // This ensures RPC commands are processed frequently even without delay() calls
645 for (int i = 0; i < 100; i++) {
647 }
648
649 // Arduino-Pico's CYW43 HTTP endpoint runs cooperatively from loop().
650 // ESP32's native HTTP server has its own task, so this is a no-op there.
652 // A peer OTA request is queued by JSON-RPC, then executed from loop after
653 // the acceptance response is safely written to USB serial.
654 pollOtaArtifactUpdate(AUTORESEARCH_WATCHDOG_TIMEOUT_MS);
655
656 // ========================================================================
657 // Watchdog autoresearch trigger (FastLED#2731) β€” when the host RPC sends
658 // `deliberateHang`, the handler flips this flag. We let one more pass of
659 // task::run() drain the response queue so the host sees the ACK, then
660 // we disable interrupts (so no async machinery can keep the WDT happy)
661 // and spin forever. The next watchdog tick must fire and reset the chip.
662 // The host's recovery test passes if the device re-enumerates afterward
663 // and is reflashable.
664 // ========================================================================
665 if (g_autoresearch_state->deliberate_hang_requested) {
666 delay(200); // give the RPC response time to enter the USB TX FIFO
667#if !defined(FL_IS_STUB) && !defined(FL_IS_WASM)
668 // Arduino-Pico's USB CDC stack may still have the JSON response in
669 // its buffered writer when the deliberate hang disables interrupts.
670 // Flush it explicitly so the host records the ACK before reset.
671 fl::flush(1000); // ok autoresearch rpc serial - drain ACK before watchdog hang
672#endif
673 // noInterrupts() is an Arduino-only macro; guard it so the host stub
674 // build (which compiles this .ino for the unit-test framework) doesn't
675 // hit an undeclared identifier. On host the while(1) below still spins
676 // and prevents feed(), which is the actual hang we want to test.
677#if !defined(FL_IS_STUB) && !defined(FL_IS_WASM)
678 noInterrupts();
679#endif
680 while (true) { /* deliberate hang */ }
681 }
682
683 // Feed the watchdog at the end of every loop iteration. On platforms
684 // where begin() was a no-op the feed is also a no-op. The mark-clean
685 // shutdown zeros the consecutive-crash counter so a transient hang
686 // doesn't eventually push the board into safe mode.
687 FastLED.watchdog().feed();
688 FastLED.watchdog().markCleanShutdown();
689
690 // Run GPIO baseline test once after device is ready (allows JSON-RPC to be operational first)
691 // This test is informational only - we continue regardless of pass/fail
692 if (!g_autoresearch_state->gpio_baseline_test_done && g_autoresearch_state->rx_channel) {
693 // Wait 500ms after boot to ensure JSON-RPC is fully operational
694 if (millis() > 500) {
695 g_autoresearch_state->gpio_baseline_test_done = true;
696
697 // Test RX channel with manual GPIO toggle to confirm hardware path works
698 // This isolates GPIO/hardware issues from PARLIO driver issues
699 // Buffer size = 100 symbols, hz = 40MHz (same as LED autoresearch)
700 const int pin_tx = g_autoresearch_state->pin_tx;
701 const int pin_rx = g_autoresearch_state->pin_rx;
702 const bool gpio_ok = testRxChannel(
703 g_autoresearch_state->rx_channel, pin_tx, pin_rx, 40000000, 100);
704 g_autoresearch_state->led_rx_available = gpio_ok;
705 fl::json gpioData = fl::json::object();
706 gpioData.set("success", gpio_ok);
707 gpioData.set("pinTx", static_cast<int64_t>(pin_tx));
708 gpioData.set("pinRx", static_cast<int64_t>(pin_rx));
709 gpioData.set("hz", static_cast<int64_t>(40000000));
710 gpioData.set("symbols", static_cast<int64_t>(100));
711 printStreamRaw("gpioBaseline", gpioData);
712 }
713 }
714
715 // Emit periodic ready status (every 5 seconds) for Python connection detection
716 static uint32_t last_status_ms = 0;
717 uint32_t now = millis();
718 if (now - last_status_ms >= 5000) {
720 status.set("ready", true);
721 status.set("uptimeMs", static_cast<int64_t>(now));
722 printStreamRaw("status", status);
723 last_status_ms = now;
724 }
725}
726
727#endif // !FL_PLATFORM_HAS_LARGE_MEMORY
void setup()
void loop()
bool testRxChannel(fl::shared_ptr< fl::RxChannel > rx_channel, int pin_tx, int pin_rx, uint32_t hz, size_t buffer_size)
Test RX channel with manual GPIO toggle.
void autoResearchExpectedEngines()
AutoResearch that expected engines are available for this platform Prints ERROR if any expected engin...
void autoResearchLowMemoryLoop()
void autoResearchLowMemorySetup()
void pollNetServer()
Service pending peer HTTP requests from the sketch loop.
void pollOtaArtifactUpdate(uint32_t watchdog_restore_ms)
Execute a queued RP2350W update from the sketch loop.
Full PARLIO encode bench for ESP32-P4 (post-byte-LUT, #2526 landed).
PARLIO ISR-streaming functional validation (#2548 deep-dive follow-up).
void printStreamRaw(const char *messageType, const fl::json &data)
Print JSONL stream message directly to Serial, bypassing fl::println.
Issue #2994 repro: compounded per-sequence timing drift vs 3.10.3.
Wave8 expansion micro-benchmark for #2526.
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
const int PIN_TX
Definition RX.ino:44
constexpr fl::RxBackend RX_BACKEND
Definition RX.ino:46
const int PIN_RX
Definition RX.ino:45
Declaration of fl::enableAllDrivers() β€” enrolls every channel driver available on the current platfor...
static Watchdog & instance() FL_NO_EXCEPT
void begin(fl::u32 timeout_ms) FL_NO_EXCEPT
void setBootloaderEscapeThreshold(fl::u16 threshold) FL_NO_EXCEPT
Set the consecutive-watchdog-reset count that triggers the escape.
bool escapeToBootloaderIfLooping() FL_NO_EXCEPT
Reboot into the bootloader if this looks like a boot loop.
void set(const fl::string &key, const json &value) FL_NO_EXCEPT
Definition json.h:873
static json object() FL_NO_EXCEPT
Definition json.h:864
void clear() FL_NO_EXCEPT
Definition strstream.h:366
fl::size size() const FL_NO_EXCEPT
void resize(fl::size n) FL_NO_EXCEPT
Definition vector.h:593
Platform-abstracted heap memory query functions.
fl::task::Handle setupRpcAsyncTask(AutoResearchRemoteControl &remote_control, int interval_ms=10)
Setup async task for JSON-RPC processing.
constexpr int defaultTxPin()
void maybeRegisterStubAutorun(AutoResearchRemoteControl &, fl::shared_ptr< AutoResearchState > state)
On FL_IS_STUB: register a one-shot async task that drives autoresearch.
constexpr int defaultRxPin()
constexpr const char * chipName()
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:423
Status status() FL_NO_EXCEPT
Current connection state.
Definition wifi.cpp.hpp:43
void run(fl::u32 microseconds, ExecFlags flags) FL_NO_EXCEPT
Run selected task subsystems.
unsigned char u8
Definition stdint.h:131
void delay(u32 ms, bool run_async=true) FL_NO_EXCEPT
Public delay wrapper that keeps bare Arduino delay() preferred after using fl::delay; while still all...
Definition delay.h:98
bool serial_ready() FL_NO_EXCEPT
fl::u32 uint32_t
Definition s16x16x4.h:219
void setLogLevel(u8 level) FL_NO_EXCEPT
Set the global log level.
fl::u32 millis() FL_NO_EXCEPT
Universal millisecond timer - returns milliseconds since system startup.
void serial_begin(u32 baudRate) FL_NO_EXCEPT
RxBackend
Definition types.h:8
@ PLATFORM_DEFAULT
Use the recommended backend for the active platform (RMT on ESP32; FlexPWM on Teensy 4....
Definition types.h:10
@ FL_LOG_LEVEL_NONE
No logging (completely silent)
Definition cstdio.h:38
bool flush(u32 timeoutMs) FL_NO_EXCEPT
HeapInfo getFreeHeap() FL_NO_EXCEPT
Query available heap memory.
Definition heap.cpp.hpp:68
const char * toString(RxBackend backend) FL_NO_EXCEPT
Definition types.h:20
fl::size total() const FL_NO_EXCEPT
Total free heap (SRAM + PSRAM)
Definition heap.h:22
bool has_psram() const FL_NO_EXCEPT
Check if PSRAM is available.
Definition heap.h:25
Heap memory information.
Definition heap.h:17
#define Serial
Definition serial.h:314
#define FL_WATCHDOG_AUTO(...)
Definition watchdog.h:324
Unified cross-platform Watchdog Timer API for FastLED.