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