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