FastLED 3.10.8
Loading...
Searching...
No Matches
AutoResearchTest.cpp
Go to the documentation of this file.
1// AutoResearchTest.cpp - Generic LED autoresearch testing infrastructure
2// Driver-agnostic test function implementations
3
4// Gate out under low-memory mode -- this file pulls in <FastLED.h> and
5// the full chipset / encoder matrix (UCS7604, wave3, pixel iterators).
6// LowMemory targets don't use those (their RPC surface is in
7// AutoResearchLowMemory.h, which only uses fl::Remote + the LPC SCT
8// driver). Matches the conditional structure in AutoResearch.ino itself.
10#if FL_PLATFORM_HAS_LARGE_MEMORY
11
12#include "AutoResearchTest.h"
13
14#include "platforms/arm/rp/is_rp.h"
15#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
16// kRpPioRxEdgeCapacity bounds the static DMA/edge pool the RP PIO RX device
17// writes into; the capture request must never exceed it.
18#include "platforms/arm/rp/rpcommon/rx_pio_channel.h" // IWYU pragma: keep
19#endif
20#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
21#include "fl/stl/atomic.h"
22#include "fl/stl/isr.h"
23// lastActualBaud() identifies the UART geometry the transmitter actually ran,
24// which is what the capture has to be decoded against.
25#include "platforms/arm/rp/rpcommon/rp_uart_bus_traits.h"
26#endif
29#include "platforms/arm/teensy/teensy4_common/drivers/objectfled/objectfled_diagnostics.h"
30#include <FastLED.h>
31#include "fl/stl/cstdio.h"
32#include "fl/stl/sstream.h"
33
34// PR #3421 framework-hang fix:
35// FL_WARN expansion ALWAYS constructs `fl::sstream() << X` regardless
36// of log level -- `fl::ScopedLogDisable` only suppresses OUTPUT inside
37// `log_emit`. Under autoresearch's RPC ASYNC handler (which wraps the
38// test in ScopedLogDisable), the cumulative `fl::sstream`/`fl::string`
39// heap alloc/free cycles across this file's many FL_WARN sites leave
40// the Teensy 4 heap in a state where a later function-exit destructor
41// of a heap-backed fl::string hangs indefinitely (~80% of runs on
42// LPUART, FlexIO, and ObjectFLED autoresearch tests).
43//
44// AR_FL_WARN gates the FL_WARN expansion at log level so the sstream
45// is never constructed when output is suppressed. Used for every
46// FL_WARN in this file. Marker-narrowed investigation confirmed the
47// fix moves LPUART from ~1/5 success to 5/5 -- see session research.
48#define AR_FL_WARN(...) do { \
49 if (fl::getLogLevel() >= static_cast<fl::u8>(fl::LogLevel::FL_LOG_LEVEL_WARN)) { \
50 FL_WARN(__VA_ARGS__); \
51 } \
52} while (0)
56#include "fl/channels/wave3.h"
58#include "fl/math/ease.h"
59#include "pixel_controller.h"
60#include "color.h"
61#include "fl/stl/vector.h"
62#include "fl/stl/iterator.h"
63
64#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
65void countRpPioRxRisingEdges(void* user_data) {
66 auto* edges = static_cast<fl::atomic_int*>(user_data);
67 if (edges != nullptr) {
68 edges->fetch_add(1);
69 }
70}
71#endif
72
73// Phase 0: Include PARLIO debug instrumentation
74#if defined(ESP32) && FASTLED_ESP32_HAS_PARLIO
75#include "platforms/esp/32/drivers/parlio/channel_driver_parlio.h"
76#endif
77
82void dumpRawEdgeTiming(fl::shared_ptr<fl::RxChannel> rx_channel,
83 const fl::ChipsetTimingConfig& timing,
84 fl::EdgeRange range) {
85 if (!rx_channel) {
86 AR_FL_WARN("[RAW EDGE TIMING] ERROR: RX channel is null");
87 return;
88 }
89
90 // Heap-allocated edge buffer (was a 2 KB FixedVector on the stack —
91 // the capture path already runs the loopTask close to its limit,
92 // FastLED#3569).
94 size_t buffer_size = range.count < 256 ? range.count : 256;
95 edges.resize(buffer_size); // Default initializes to EdgeTime()
96
97 // Get edges starting at offset
98 size_t edge_count = rx_channel->getRawEdgeTimes(edges, range.offset);
99
100 if (edge_count == 0) {
101 AR_FL_WARN("[RAW EDGE TIMING] WARNING: No edge data captured at offset " << range.offset);
102 return;
103 }
104
105 // Calculate actual range printed (start from offset)
106 size_t start_idx = range.offset;
107 size_t end_idx = range.offset + edge_count;
108
109 // Single-line collated edge dump. Was one FL_WARN per edge (~32-256
110 // lines per call) which dominated the Teensy 4 UART TX FIFO on every
111 // decode-failure path. Compact format keeps the diagnostic value
112 // (each edge as `H580` or `L640`) while shipping in a single WS
113 // frame. See fbuild#755 follow-up FL_WARN audit.
114 {
115 fl::sstream edge_dump;
116 edge_dump << "[RAW EDGES " << start_idx << ".." << (end_idx - 1) << "]";
117 for (size_t i = 0; i < edge_count; i++) {
118 edge_dump << " " << (edges[i].high ? "H" : "L") << edges[i].ns;
119 }
120 AR_FL_WARN(edge_dump.str());
121 }
122
123 // Pattern analysis (only if showing edges from start)
124 if (range.offset == 0 && edge_count >= 16) {
125 // Calculate expected timings from config (3-phase to 4-phase conversion)
126 uint32_t expected_bit0_high = timing.t1_ns;
127 uint32_t expected_bit0_low = timing.t2_ns + timing.t3_ns;
128 uint32_t expected_bit1_high = timing.t1_ns + timing.t2_ns;
129 uint32_t expected_bit1_low = timing.t3_ns;
130
131 const uint32_t tolerance = 150;
132
133 bool has_short_high = false, has_long_high = false;
134 bool has_short_low = false, has_long_low = false;
135
136 for (size_t i = 0; i < edge_count; i++) {
137 uint32_t ns = edges[i].ns;
138 if (edges[i].high) {
139 if (ns >= expected_bit0_high - tolerance && ns <= expected_bit0_high + tolerance)
140 has_short_high = true;
141 if (ns >= expected_bit1_high - tolerance && ns <= expected_bit1_high + tolerance)
142 has_long_high = true;
143 } else {
144 if (ns >= expected_bit1_low - tolerance && ns <= expected_bit1_low + tolerance)
145 has_short_low = true;
146 if (ns >= expected_bit0_low - tolerance && ns <= expected_bit0_low + tolerance)
147 has_long_low = true;
148 }
149 }
150
151 fl::sstream ss;
152 ss << "\n[RAW EDGE TIMING] Pattern Analysis:\n";
153 ss << " Short HIGH (~" << expected_bit0_high << "ns, Bit 0): " << (has_short_high ? "FOUND ✓" : "MISSING ✗") << "\n";
154 ss << " Long HIGH (~" << expected_bit1_high << "ns, Bit 1): " << (has_long_high ? "FOUND ✓" : "MISSING ✗") << "\n";
155 ss << " Short LOW (~" << expected_bit1_low << "ns, Bit 1): " << (has_short_low ? "FOUND ✓" : "MISSING ✗") << "\n";
156 ss << " Long LOW (~" << expected_bit0_low << "ns, Bit 0): " << (has_long_low ? "FOUND ✓" : "MISSING ✗");
157 AR_FL_WARN(ss.str());
158
159 if (has_short_high && has_long_high && has_short_low && has_long_low) {
160 AR_FL_WARN("\n[RAW EDGE TIMING] ✓ Encoder appears to be working correctly (varied timing patterns)");
161 } else if (!has_short_high && !has_long_high) {
162 ss.clear();
163 ss << "[RAW EDGE TIMING] ✗ ENCODER BROKEN: No valid HIGH pulses detected!\n";
164 ss << "[RAW EDGE TIMING] Possible causes:\n";
165 ss << "[RAW EDGE TIMING] 1. Encoder not reading pixel buffer data\n";
166 ss << "[RAW EDGE TIMING] 2. Bytes encoder state machine stuck\n";
167 ss << "[RAW EDGE TIMING] 3. Data pointer not passed correctly to encoder";
168 FL_ERROR(ss.str());
169 } else if (!has_short_low && !has_long_low) {
170 // Use FL_WARN to avoid triggering bash autoresearch early exit
171 AR_FL_WARN("[RAW EDGE TIMING] ✗ ENCODER BROKEN: No valid LOW pulses detected!");
172 } else {
173 AR_FL_WARN("[RAW EDGE TIMING] ⚠ Partial pattern match - encoder may have issues");
174 }
175 }
176 AR_FL_WARN("");
177}
178
180static bool isUCS7604(fl::ClocklessEncoder encoder) {
184}
185
186static bool legacyLaneUsesRgbw(const fl::AutoResearchConfig& config,
187 fl::size lane) {
188 if (config.legacy_rgbw) {
189 return true;
190 }
191 return lane < config.legacy_chipsets.size() &&
193 config.legacy_chipsets[lane]);
194}
195
196static fl::vector<uint8_t> buildExpectedClockless(
197 fl::span<CRGB> leds, bool rgbw) {
199 leds.data(), static_cast<int>(leds.size()),
201 PixelIterator iterator = pixels.as_iterator(
203 fl::vector<uint8_t> expected;
204 iterator.writeWS2812(&expected);
205 return expected;
206}
207
212static fl::vector<uint8_t> buildExpectedUCS7604(fl::span<CRGB> leds, fl::ClocklessEncoder encoder) {
213 fl::vector<uint8_t> expected;
214
215 // Map encoder to UCS7604Mode
216 fl::UCS7604Mode mode;
217 switch (encoder) {
220 break;
223 break;
226 break;
227 default:
228 return expected;
229 }
230
231 // Default current control (0x0F for all channels) matching channel.cpp.hpp defaults
232 fl::UCS7604CurrentControl current; // defaults to 0xF for all channels
233
234 // Create PixelIterator from LED data (RGB order, no color adjustment, no dithering)
236 auto pixel_iter = pc.as_iterator(RgbwInvalid());
237
238 // For 16-bit modes, use default gamma 2.8
240 bool use_gamma = (mode != fl::UCS7604Mode::UCS7604_MODE_8BIT_800KHZ);
241 if (use_gamma) {
243 }
244
245 // Encode using the same function the driver uses
246 fl::encodeUCS7604(pixel_iter, leds.size(), fl::back_inserter(expected),
247 mode, current, false /* is_rgbw */, gamma.get());
248
249 return expected;
250}
251
252// Decode SPI bit stream from raw RMT edges into LED RGB bytes
253// The SPI data pin carries APA102 encoded data clocked by PCLK.
254// RMT RX captures edges on the data pin. Each edge duration / bit_period_ns
255// gives the number of consecutive same-value bits.
256// Returns number of LED RGB bytes written to rx_buffer, or 0 on error.
257static size_t decodeSpiEdges(fl::shared_ptr<fl::RxChannel> rx_channel,
258 fl::span<uint8_t> rx_buffer,
259 uint32_t clock_hz) {
260 if (!rx_channel || clock_hz == 0) {
261 AR_FL_WARN("[SPI DECODE] Invalid parameters");
262 return 0;
263 }
264
265 const uint32_t bit_period_ns = static_cast<uint32_t>(1000000000ULL / clock_hz);
266 const uint32_t half_bit_ns = bit_period_ns / 2;
267 AR_FL_WARN("[SPI DECODE] clock=" << clock_hz << " Hz, bit_period=" << bit_period_ns << " ns");
268
269 // Read raw edges (up to 4096 to handle large strips)
270 constexpr size_t MAX_EDGES = 4096;
271 fl::vector<fl::EdgeTime> edges(MAX_EDGES);
272 fl::span<fl::EdgeTime> edge_span(edges.data(), edges.size());
273 size_t edge_count = rx_channel->getRawEdgeTimes(edge_span, 0);
274
275 if (edge_count == 0) {
276 AR_FL_WARN("[SPI DECODE] No edges captured");
277 return 0;
278 }
279 AR_FL_WARN("[SPI DECODE] Captured " << edge_count << " edges");
280
281 // Reconstruct bit stream from edges
282 // Each edge has a level (high/low) and duration in ns.
283 // Number of bits = round(duration_ns / bit_period_ns)
285 bits.reserve(edge_count * 4); // Rough estimate
286
287 for (size_t i = 0; i < edge_count; i++) {
288 uint32_t duration = edges[i].ns;
289 uint32_t num_bits = (duration + half_bit_ns) / bit_period_ns;
290 if (num_bits == 0) num_bits = 1; // At least 1 bit per edge
291 uint8_t bit_val = edges[i].high ? 1 : 0;
292 for (uint32_t b = 0; b < num_bits; b++) {
293 bits.push_back(bit_val);
294 }
295 }
296
297 AR_FL_WARN("[SPI DECODE] Reconstructed " << bits.size() << " bits");
298
299 if (bits.size() < 32) {
300 AR_FL_WARN("[SPI DECODE] Too few bits for APA102 frame");
301 return 0;
302 }
303
304 // Convert bits to bytes (MSB first)
305 size_t total_bytes = bits.size() / 8;
306 fl::vector<uint8_t> raw_bytes(total_bytes);
307 for (size_t i = 0; i < total_bytes; i++) {
308 uint8_t byte_val = 0;
309 for (int bit = 7; bit >= 0; bit--) {
310 size_t bit_idx = i * 8 + (7 - bit);
311 if (bit_idx < bits.size() && bits[bit_idx]) {
312 byte_val |= (1 << bit);
313 }
314 }
315 raw_bytes[i] = byte_val;
316 }
317
318 AR_FL_WARN("[SPI DECODE] Decoded " << total_bytes << " raw bytes");
319
320 // Log first few bytes for debugging
321 {
322 fl::sstream dbg;
323 dbg << "[SPI DECODE] First bytes:";
324 size_t show = total_bytes < 20 ? total_bytes : 20;
325 for (size_t i = 0; i < show; i++) {
326 dbg << " 0x";
327 uint8_t hi = raw_bytes[i] >> 4;
328 uint8_t lo = raw_bytes[i] & 0xF;
329 dbg << (char)(hi < 10 ? '0' + hi : 'A' + hi - 10);
330 dbg << (char)(lo < 10 ? '0' + lo : 'A' + lo - 10);
331 }
332 AR_FL_WARN(dbg.str());
333 }
334
335 // Find APA102 start frame (4 bytes of 0x00)
336 // The RMT may not capture the start frame since it's all zeros and
337 // the idle state is also LOW. In that case, the first captured data
338 // is the first LED frame header (0xE0 | brightness).
339 size_t data_start = 0;
340
341 // Check if we captured the start frame
342 if (total_bytes >= 4 && raw_bytes[0] == 0x00 && raw_bytes[1] == 0x00 &&
343 raw_bytes[2] == 0x00 && raw_bytes[3] == 0x00) {
344 data_start = 4; // Skip start frame
345 AR_FL_WARN("[SPI DECODE] Found start frame at offset 0");
346 } else {
347 // No start frame captured (RMT started at first edge = first HIGH bit)
348 // First byte should be 0xE0|brightness or 0xFF
349 AR_FL_WARN("[SPI DECODE] No start frame (first edge = first LED data)");
350 }
351
352 // Extract LED RGB data from APA102 frames
353 // Each LED: [0xE0|brightness] [color0] [color1] [color2] (4 bytes)
354 // With EOrder=RGB in the validation config, the wire order after the
355 // brightness header is [R, G, B] — we copy these directly to rx_buffer.
356 // NOTE: The APA102 end frame (all 0xFF) also has (0xFF & 0xE0) == 0xE0,
357 // so we cannot distinguish end frame from a max-brightness LED purely
358 // from the header byte. We stop after we've exhausted captured data.
359 size_t led_bytes_written = 0;
360 size_t pos = data_start;
361
362 while (pos + 4 <= total_bytes) {
363 uint8_t header = raw_bytes[pos];
364
365 // Check for valid LED frame header (top 3 bits must be 111)
366 if ((header & 0xE0) != 0xE0) {
367 AR_FL_WARN("[SPI DECODE] End of LED data at byte " << pos
368 << " (header=0x" << ((header >> 4) < 10 ? '0' + (header >> 4) : 'A' + (header >> 4) - 10)
369 << ((header & 0xF) < 10 ? '0' + (header & 0xF) : 'A' + (header & 0xF) - 10) << ")");
370 break;
371 }
372
373 uint8_t c0 = raw_bytes[pos + 1];
374 uint8_t c1 = raw_bytes[pos + 2];
375 uint8_t c2 = raw_bytes[pos + 3];
376
377 if (led_bytes_written + 3 <= rx_buffer.size()) {
378 rx_buffer[led_bytes_written + 0] = c0;
379 rx_buffer[led_bytes_written + 1] = c1;
380 rx_buffer[led_bytes_written + 2] = c2;
381 led_bytes_written += 3;
382 } else {
383 AR_FL_WARN("[SPI DECODE] rx_buffer full at LED " << (led_bytes_written / 3));
384 break;
385 }
386 pos += 4;
387 }
388
389 size_t num_leds = led_bytes_written / 3;
390 AR_FL_WARN("[SPI DECODE] Extracted " << num_leds << " LEDs (" << led_bytes_written << " RGB bytes)");
391 return led_bytes_written;
392}
393
394// Collect expensive raw-edge evidence only for a failed run. The normal RPC
395// soak path must remain allocation-quiet, but a failed decode or byte compare
396// still needs the same postmortem snapshot as a failed RX wait.
397void collectRawEdgeDiagnostics(fl::shared_ptr<fl::RxChannel> rx_channel,
398 fl::RunResult* diagnostics) {
399 if (!rx_channel || !diagnostics || !diagnostics->rawEdgeSample.empty()) {
400 return;
401 }
402
404 edges.resize(256);
405 diagnostics->rawEdgesAfterWait =
406 static_cast<int>(rx_channel->getRawEdgeTimes(edges, 0));
407
408 fl::vector<fl::EdgeTime> sample_edges;
409 sample_edges.resize(32);
410 const size_t sample_count = rx_channel->getRawEdgeTimes(sample_edges, 0);
412 for (size_t i = 0; i < sample_count; ++i) {
413 if (i > 0) {
414 sample << ' ';
415 }
416 sample << (sample_edges[i].high ? 'H' : 'L') << sample_edges[i].ns;
417 }
418 diagnostics->rawEdgeSample = sample.str();
419}
420
421// Build the 4-phase RX decode thresholds for a TX driver's waveform.
422static fl::ChipsetTiming4Phase buildRxDecodeTiming(const fl::ChipsetTimingConfig& timing,
423 const char* driver_name) {
424 // Create 4-phase RX timing from the TX timing
425 //
426 // Wave8 drivers (SPI, PARLIO, I2S) use 8-bit expansion encoding:
427 // Bit 0: round(T1/(T1+T2+T3)*8) HIGH pulses, rest LOW
428 // Bit 1: round((T1+T2)/(T1+T2+T3)*8) HIGH pulses, rest LOW
429 // The actual pulse widths are quantized to tick boundaries, so we must
430 // compute the exact wave8 timing for RX decode thresholds.
431 // Without this correction, the RX decoder may reject valid waveforms when
432 // the nominal timing period differs from the quantized 8-tick period.
433 // Example: WS2812_800KHZ has T0L=1000ns nominal but PARLIO wave8 produces 750ns.
434 //
435 // Clock sources differ by driver:
436 // SPI/I2S: clock = 8/(T1+T2+T3) Hz, tick = (T1+T2+T3)/8 ns (variable)
437 // PARLIO: clock = 8 MHz fixed, tick = 125 ns (fixed)
438 bool is_spi_driver = (fl::strcmp(driver_name, "SPI") == 0);
439 bool is_parlio_driver = (fl::strcmp(driver_name, "PARLIO") == 0);
440 bool is_i2s_driver = (fl::strcmp(driver_name, "I2S") == 0);
441 // LCD_CLOCKLESS auto-selects wave3 vs wave8 based on canUseWave3() of the
442 // chipset timing. Compute the same eligibility check on the host side so
443 // the RX decoder reconstructs the correct quantized waveform.
444 bool is_lcd_clockless_driver = (fl::strcmp(driver_name, "LCD_CLOCKLESS") == 0);
445 bool lcd_clockless_uses_wave3 = false;
446 if (is_lcd_clockless_driver) {
447 fl::ChipsetTiming probe{timing.t1_ns, timing.t2_ns, timing.t3_ns, timing.reset_us, timing.name};
448 lcd_clockless_uses_wave3 = fl::canUseWave3(probe);
449 }
450 bool uses_wave8 = is_spi_driver || is_parlio_driver || is_i2s_driver
451 || (is_lcd_clockless_driver && !lcd_clockless_uses_wave3);
452 bool uses_wave3 = is_lcd_clockless_driver && lcd_clockless_uses_wave3;
453 fl::ChipsetTiming tx_timing;
454 if (uses_wave8) {
455 // Compute actual wave8 timing from chipset timing
456 const uint32_t period = timing.t1_ns + timing.t2_ns + timing.t3_ns;
457 // PARLIO uses fixed 8MHz clock (125ns/tick), not derived from period
458 // SPI/I2S/LCD_CLOCKLESS derive clock from period: tick = period/8
459 const uint32_t tick_ns = is_parlio_driver ? 125 : (period / 8);
460 // Wave8 LUT computes: pulses = round(fraction * 8)
461 const uint32_t pulses_bit0 = static_cast<uint32_t>(
462 static_cast<float>(timing.t1_ns) / period * 8.0f + 0.5f);
463 const uint32_t pulses_bit1 = static_cast<uint32_t>(
464 static_cast<float>(timing.t1_ns + timing.t2_ns) / period * 8.0f + 0.5f);
465 // Convert back to 3-phase timing (T1=T0H, T2=T1H-T0H, T3=actual_period-T1H)
466 const uint32_t actual_t0h = pulses_bit0 * tick_ns;
467 const uint32_t actual_t1h = pulses_bit1 * tick_ns;
468 const uint32_t actual_period = 8 * tick_ns;
469 const char* wave8_name = is_spi_driver ? "SPI_wave8" :
470 is_parlio_driver ? "PARLIO_wave8" :
471 is_lcd_clockless_driver ? "LCD_CLOCKLESS_wave8" : "I2S_wave8";
472 tx_timing = fl::ChipsetTiming{
473 actual_t0h, // T1 = T0H
474 actual_t1h - actual_t0h, // T2 = T1H - T0H
475 actual_period - actual_t1h, // T3 = actual_period - T1H
476 timing.reset_us,
477 wave8_name
478 };
479 AR_FL_WARN("[RX TIMING] " << wave8_name << ": pulses_bit0=" << pulses_bit0
480 << " pulses_bit1=" << pulses_bit1
481 << " tick_ns=" << tick_ns
482 << " -> T1=" << tx_timing.T1 << " T2=" << tx_timing.T2
483 << " T3=" << tx_timing.T3);
484 } else if (uses_wave3) {
485 // Wave3 encoding: 3 ticks per LED bit, clock = 3/(T1+T2+T3) Hz
486 const uint32_t period = timing.t1_ns + timing.t2_ns + timing.t3_ns;
487 const uint32_t tick_ns = period / 3;
488 const uint32_t ticks_bit0 = static_cast<uint32_t>(
489 static_cast<float>(timing.t1_ns) / period * 3.0f + 0.5f);
490 const uint32_t ticks_bit1 = static_cast<uint32_t>(
491 static_cast<float>(timing.t1_ns + timing.t2_ns) / period * 3.0f + 0.5f);
492 const uint32_t actual_t0h = ticks_bit0 * tick_ns;
493 const uint32_t actual_t1h = ticks_bit1 * tick_ns;
494 const uint32_t actual_period = 3 * tick_ns;
495 tx_timing = fl::ChipsetTiming{
496 actual_t0h,
497 actual_t1h - actual_t0h,
498 actual_period - actual_t1h,
499 timing.reset_us,
500 "LCD_CLOCKLESS_wave3"
501 };
502 AR_FL_WARN("[RX TIMING] LCD_CLOCKLESS_wave3: ticks_bit0=" << ticks_bit0
503 << " ticks_bit1=" << ticks_bit1
504 << " tick_ns=" << tick_ns
505 << " -> T1=" << tx_timing.T1 << " T2=" << tx_timing.T2
506 << " T3=" << tx_timing.T3);
507 } else {
508 tx_timing = fl::ChipsetTiming{timing.t1_ns, timing.t2_ns, timing.t3_ns, timing.reset_us, timing.name};
509 }
510 // Wave8/wave3 encoding has timing jitter due to clock quantization and GPIO matrix latency
511 // Use wider tolerance (200ns) to accommodate clock rounding
512 const uint32_t tolerance = (uses_wave8 || uses_wave3) ? 200 : 170;
513 auto rx_timing = fl::make4PhaseTiming(tx_timing, tolerance);
514
515 // Enable gap tolerance for PARLIO/SPI DMA gaps
516 // PARLIO: ~20µs typical gaps during buffer transitions
517 // SPI: Can have longer inter-frame gaps due to software encoding timing
518 // Increased to 100µs to accommodate SPI driver timing variations
519 rx_timing.gap_tolerance_ns = 100000; // 100µs (was 30µs)
520 return rx_timing;
521}
522
523// Capture transmitted LED data via RX loopback
524// - rx_channel: Shared pointer to RX device (persistent across calls)
525// - rx_buffer: Buffer to store received bytes
526// - expected_data_bytes: bytes in the current frame
527// - tx_pin: GPIO driven by the tested TX controller
528// - timing: Chipset timing configuration for RX decoder
529// - driver_name: Name of the TX driver being tested (e.g., "RMT", "PARLIO")
530// Returns number of bytes captured, or 0 on error
531size_t capture(fl::shared_ptr<fl::RxChannel> rx_channel,
532 fl::span<uint8_t> rx_buffer,
533 size_t expected_data_bytes,
534 int tx_pin,
535 const fl::ChipsetTimingConfig& timing,
536 const char* driver_name,
537 fl::RunResult* diagnostics) {
538 // The RPC runner disables log output for normal tests. Keep successful
539 // captures allocation-quiet too: constructing diagnostic strings and
540 // copying edge snapshots on every frame defeats that guard during a
541 // repeated-frame soak. Failure paths still collect full evidence below.
542 const bool logs_enabled = fl::getLogLevel() >=
544 if (diagnostics) {
545 diagnostics->captureWaitResult = -1;
546 diagnostics->rawEdgesAfterWait = 0;
547 diagnostics->rpPioGpioTransitions = -1;
548 diagnostics->rxBeginError.clear();
549 }
550
551 if (!rx_channel) {
552 FL_ERROR("RX channel is null");
553 return 0;
554 }
555
556 // Clear RX buffer
557 fl::memset(rx_buffer.data(), 0, rx_buffer.size());
558
559 // Prepare RX config (but don't arm yet to avoid locking TX resources)
560 fl::RxChannelConfig rx_config(rx_channel->getPin());
561 rx_config.hz = 40000000; // 40MHz for high-precision LED timing capture
562
563 // Buffer size: 1 LED byte = 8 bits = 8 RMT symbols
564 // UART with TX inversion produces standard WS2812 waveform, same symbol count
565 bool is_uart_driver = (fl::strcmp(driver_name, "UART") == 0)
566 || (fl::strcmp(driver_name, "LPUART") == 0)
567 || (fl::strcmp(driver_name, "UART0") == 0)
568 || (fl::strcmp(driver_name, "UART1") == 0);
569 bool is_object_fled_driver = (fl::strcmp(driver_name, "OBJECT_FLED") == 0);
570 rx_config.edge_capacity = fl::validation::captureEdgeCapacity(
571 rx_buffer.size(), expected_data_bytes, rx_channel->backend());
572#if defined(FL_IS_TEENSY_4X)
573 // Teensy 4.x FlexPWM RX streams captures through a small DMA ring and
574 // keeps only decoded bytes, so size it to this frame rather than to the
575 // shared result buffer. FlexIO RX still stores edges: leave it alone.
576 if (rx_channel->backend() != fl::RxBackend::FLEXIO) {
577 rx_config.edge_capacity = fl::validation::captureEdgeCapacity(
578 rx_buffer.size(), expected_data_bytes, fl::RxBackend::ISR);
579 }
580#endif
581 if (rx_config.edge_capacity == 0) {
582 FL_ERROR("[CAPTURE] RX edge-capacity overflow");
583 return 0;
584 }
585#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
586 // RP's platform-default RX resolves to PIO, while RxChannel retains the
587 // requested PLATFORM_DEFAULT enum. Recognize both that public default and
588 // an explicit PIO backend so the DMA capture is sized per frame.
589 const bool is_rp_pio_capture =
590 rx_channel->backend() == fl::RxBackend::PLATFORM_DEFAULT ||
591 rx_channel->backend() == fl::RxBackend::PIO ||
592 fl::strcmp(driver_name, "PIO0") == 0 || fl::strcmp(driver_name, "PIO1") == 0 ||
593 fl::strcmp(driver_name, "PIO2") == 0;
594 if (is_rp_pio_capture) {
595 // The shared result buffer is intentionally sized for the largest
596 // AutoResearch run, not this frame. PIO DMA stores one word for each
597 // high and low phase, so using that whole buffer here would request
598 // several MiB on a Pico and leave DMA with an invalid destination.
599 constexpr size_t kPioPhasesPerDataByte = 16;
600 // The guard must cover the headroom multiplier too: checking only
601 // against kPioPhasesPerDataByte would let an oversized byte count
602 // wrap during the *2 below and produce a small `requested` that
603 // sails past the clamp into RpPioRxDevice::begin().
604 constexpr size_t kPioPhaseHeadroomNumerator = 2;
605 constexpr size_t kPioPhasesPerDataByteWithHeadroom =
606 kPioPhasesPerDataByte * kPioPhaseHeadroomNumerator;
607 if (expected_data_bytes >
608 (static_cast<size_t>(-1) - 1u) / kPioPhasesPerDataByteWithHeadroom) {
609 FL_ERROR("[CAPTURE] PIO edge-capacity overflow");
610 return 0;
611 }
612 // 16 phases per byte is a *clean* frame: 8 bits x (high + low). Slower
613 // chipsets exceed it -- a 400 kHz capture overflows and is discarded
614 // (RxWaitResult::BUFFER_OVERFLOW), because long runs do not always
615 // land in one entry. Ask for headroom, then clamp to the static pool
616 // so the request can never exceed the FixedVector backing it:
617 // over-requesting authorises appendDuration() to write past the end.
618 size_t requested =
619 expected_data_bytes * kPioPhasesPerDataByteWithHeadroom + 1u;
620 if (requested > fl::kRpPioRxEdgeCapacity) {
621 requested = fl::kRpPioRxEdgeCapacity;
622 }
623 rx_config.edge_capacity = requested;
624 }
625#endif
626
627 bool is_rmt_driver = (fl::strcmp(driver_name, "RMT") == 0);
628 const int rx_pin = rx_channel->getPin();
629 // ESP-IDF's RMT io_loop_back path requires TX and RX to share one GPIO.
630 // AutoResearch normally discovers a physical TX->RX jumper on two
631 // different pins; enabling io_loop_back in that case disconnects the RX
632 // channel from the external GPIO path and yields a successful wait with
633 // zero edges on ESP32-C6. Keep the ESP32-S3 same-pin workaround, but use
634 // the physical jumper whenever TX and RX are distinct.
635 const bool use_rmt_internal_loopback =
637 is_rmt_driver, tx_pin, rx_pin);
638 rx_config.io_loop_back = use_rmt_internal_loopback;
639 // RX DMA streaming: extends capture past the non-DMA cap by sizing
640 // mem_block_symbols = 14336 so ESP-IDF allocates ~14 DMA descriptor
641 // nodes (each 4092 bytes), yielding a 57 KB user-buffer cap — enough to
642 // capture ~583 WS2812B LEDs in a single rmt_receive() call. Safe for
643 // non-RMT TX paths (SPI, PARLIO, I2S, UART, LCD_*) that don't contend
644 // for the shared ESP32-S3 DMA slot. RMT loopback stays non-DMA.
645 // See issue #2254.
646 rx_config.use_dma = !is_rmt_driver;
647 // Preserve the channel's explicitly-selected capture backend —
648 // RxChannel::begin() recreates the device when the backend field
649 // differs, and this config is built fresh (default =
650 // PLATFORM_DEFAULT), which silently reverted rxBackend overrides
651 // like I2S_RX back to RMT (FastLED#3576 Phase 3).
652 rx_config.backend = rx_channel->backend();
653 // Backends that decode while capturing (Teensy 4.x FlexPWM) need the
654 // decode thresholds up front.
655 const fl::ChipsetTiming4Phase rx_timing = buildRxDecodeTiming(timing, driver_name);
656 rx_config.stream_timing = rx_timing;
657 if (use_rmt_internal_loopback) {
658 AR_FL_WARN("[CAPTURE] RMT TX -> RMT RX: Same-pin internal loopback enabled");
659 } else {
660 // The RX peripheral is platform-dependent (RMT on ESP32, FlexPWM on
661 // Teensy 4, LPC_SCT on LPC845, …). Don't claim "RMT RX" on platforms
662 // that have no RMT — that label burned an hour of Teensy-4 debug
663 // (FastLED#3059). Just say "external RX" so the label stays correct
664 // regardless of backend.
665 if (logs_enabled) {
666 AR_FL_WARN("[CAPTURE] " << driver_name
667 << " TX GPIO " << tx_pin << " -> RX GPIO " << rx_pin
668 << ": External jumper "
669 << "(io_loop_back=false, use_dma="
670 << (rx_config.use_dma ? "true" : "false") << ")");
671 }
672 }
673
674 // Driver-aware capture strategy:
675 // - RMT: Two-TX approach (ESP32-S3 workaround - TX GPIO blocked when RX active)
676 // - PARLIO/SPI/other: Single-TX approach (arm RX first, then TX)
677
678 // TX wait timeout: 1 second max per frame - prevents infinite hang if driver stalls
679 // Even a 10000-LED strip at 800kbps takes only ~300ms, so 1s is very safe
680 const uint32_t TX_WAIT_TIMEOUT_MS = 1000;
681
682 if (is_rmt_driver) {
683 // RMT: Two-TX approach for ESP32-S3 compatibility
684 // First TX without RX armed (diagnostics), then arm RX, then second TX
685 if (logs_enabled) {
686 AR_FL_WARN("[CAPTURE] RMT: Two-TX approach (ESP32-S3 workaround)");
687 }
688 FastLED.show();
689 if (!FastLED.wait(TX_WAIT_TIMEOUT_MS)) {
690 FL_ERROR("[CAPTURE] TX wait timeout (pre-arm) - driver may be stalled");
691 return 0;
692 }
693
694 // Arm RX for capture
695 if (!rx_channel->begin(rx_config)) {
696 if (diagnostics) diagnostics->rxBeginError = rx_channel->lastBeginError();
697 FL_ERROR("Failed to arm RX receiver");
698 return 0;
699 }
700 // Second TX with RX armed
701 FastLED.show();
702 if (!FastLED.wait(TX_WAIT_TIMEOUT_MS)) {
703 FL_ERROR("[CAPTURE] TX wait timeout (capture) - driver may be stalled");
704 return 0;
705 }
706 } else {
707 // Non-RMT (PARLIO, SPI, etc.): Single-TX approach
708 if (!rx_channel->begin(rx_config)) {
709 if (diagnostics) diagnostics->rxBeginError = rx_channel->lastBeginError();
710 AR_FL_WARN("[CAPTURE] RX begin() failed for pin " << rx_channel->getPin());
711 return 0;
712 }
713 if (logs_enabled) {
714 AR_FL_WARN("[CAPTURE] RX armed, calling FastLED.show()...");
715 }
716
717#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
718 fl::atomic_int rising_edges(0);
719 fl::isr::handle edge_counter;
720 bool edge_counter_attached = false;
721 const bool measure_rp_pio =
722 (fl::strcmp(driver_name, "PIO0") == 0 || fl::strcmp(driver_name, "PIO1") == 0 ||
723 fl::strcmp(driver_name, "PIO2") == 0) &&
724 diagnostics != nullptr;
725 if (measure_rp_pio) {
726 // FastLED.show() drives the channel engine until it reaches its
727 // terminal state, so polling GPIO after show() is too late to
728 // observe a short clockless frame. Count rising edges through the
729 // shared FastLED ISR dispatcher while show() is running instead.
730 // This is deliberately a binary physical oracle: an IRQ count
731 // above zero proves the jumper carries TX activity, while the PIO
732 // RX backend remains responsible for exact-byte verification.
733 fl::isr::config edge_config;
734 edge_config.handler = countRpPioRxRisingEdges;
735 edge_config.user_data = &rising_edges;
737 edge_counter_attached = fl::isr::attach_external_handler(
738 static_cast<fl::u8>(rx_channel->getPin()), edge_config, &edge_counter) == 0;
739 }
740#endif
741 FastLED.show();
742 if (logs_enabled) {
743 AR_FL_WARN("[CAPTURE] FastLED.show() returned, calling wait...");
744 }
745#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
746 if (edge_counter_attached && edge_counter.is_valid()) {
747 fl::isr::detach_handler(edge_counter);
748 }
749 if ((fl::strcmp(driver_name, "PIO0") == 0 || fl::strcmp(driver_name, "PIO1") == 0 ||
750 fl::strcmp(driver_name, "PIO2") == 0) &&
751 diagnostics != nullptr) {
752 diagnostics->rpPioGpioTransitions = edge_counter_attached ? rising_edges.load() : -1;
753 }
754#endif
755 if (!FastLED.wait(TX_WAIT_TIMEOUT_MS)) {
756 if (is_object_fled_driver) {
757 fl::objectFledDiagnosticsRecord("afterFastLedWaitTimeout");
758 }
759 AR_FL_WARN("[CAPTURE] FastLED.wait() timed out");
760 return 0;
761 }
762 if (is_object_fled_driver) {
763 fl::objectFledDiagnosticsRecord("afterFastLedWait");
764 }
765 if (logs_enabled) {
766 AR_FL_WARN("[CAPTURE] FastLED.wait() done");
767 }
768 }
769
770
771
772
773 // Wait for RX completion
774 // WS2812B: ~30μs per LED → 3000 LEDs = 90ms, use 150ms
775 // UART with inverted TX produces same waveform timing as WS2812
776 const uint32_t rx_wait_ms = is_uart_driver ? 500 : 150;
777 if (logs_enabled) {
778 AR_FL_WARN("[CAPTURE] Waiting for RX completion (" << rx_wait_ms
779 << "ms timeout)...");
780 }
781 auto wait_result = rx_channel->wait(rx_wait_ms);
782 if (diagnostics) {
783 diagnostics->captureWaitResult = static_cast<int>(wait_result);
784 diagnostics->decodeOutputCapacity = static_cast<int>(rx_buffer.size());
785 if (wait_result != fl::RxWaitResult::SUCCESS) {
786 collectRawEdgeDiagnostics(rx_channel, diagnostics);
787 }
788 }
789 if (logs_enabled) {
790 AR_FL_WARN("[CAPTURE] RX wait returned: "
791 << static_cast<int>(wait_result));
792 }
793
794 if (wait_result != fl::RxWaitResult::SUCCESS) {
795 AR_FL_WARN("RX wait failed (timeout or no data received)");
796 fl::sstream ss;
797 ss << "\n⚠️ TROUBLESHOOTING:\n";
798 ss << " 1. Connect physical jumper wire from TX GPIO to RX GPIO " << rx_channel->getPin() << "\n";
799 ss << " 2. Check that both TX and RX pins are correctly configured\n";
800 ss << " 3. Verify the GPIO connection is working (GPIO baseline test should pass)\n";
801 ss << " 4. RMT internal loopback requires TX and RX to use the same GPIO";
802 AR_FL_WARN(ss.str());
803 if (!is_uart_driver) {
804 return 0;
805 }
806 // UART: still attempt decode with captured edges (timeout may be expected)
807 AR_FL_WARN("[CAPTURE] UART: attempting decode with captured edges despite timeout");
808 }
809
810 // UART TX with inversion puts a WS2812-shaped waveform on the wire, but
811 // quantized onto the UART pulse grid -- exactly like the wave8 and wave3
812 // cases handled above, and for the same reason: the decoder has to be told
813 // what the transmitter actually emitted, not what the datasheet says.
814 //
815 // This used to hardcode T0H=250 / T1H=750 / T1L=500, the shape an ESP32
816 // UART produces at 4 Mbps for a WS2812. That is right for one chipset on
817 // one platform and wrong everywhere else. At 400 kHz the real wire is
818 // 625 / 1250 / 1250, every phase of which falls outside those hardcoded
819 // windows -- so a correct frame decoded to nothing. See FastLED#4379.
820 if (is_uart_driver) {
821 AR_FL_WARN("[CAPTURE] UART (inverted TX): using standard WS2812 decoder with UART timing...");
822 // Take the geometry the engine recorded when it transmitted, rather
823 // than re-deriving it. Which geometry gets chosen depends on the
824 // backend's *maximum* baud, not on anything visible here, and the
825 // achieved baud is the wrong input: it is allowed to sit up to 1%
826 // under the requested one, and fed back as a ceiling that shortfall
827 // makes the transmitted geometry look infeasible and selects the
828 // other. Re-deriving from the generic ceiling is the fallback for
829 // backends that do not report one.
830 fl::ChipsetTiming uart_timing{0, 0, 0, 0, nullptr};
831#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
832 if (fl::strcmp(driver_name, "UART0") == 0) {
833 uart_timing =
835 } else if (fl::strcmp(driver_name, "UART1") == 0) {
836 uart_timing =
838 }
839#endif
840 if (uart_timing.T1 == 0) {
841 // uartWireTiming() carries timing.reset_us and names the result
842 // "uart_wire", so nothing further needs setting here.
843 uart_timing = fl::uartWireTiming(timing, fl::kMaxUartBaudRate);
844 }
845 if (uart_timing.T1 == 0 && uart_timing.T2 == 0) {
846 // No representable geometry. Decoding against zeros would reject
847 // every symbol and report it as a capture fault; say what it is.
848 AR_FL_WARN("[CAPTURE] UART: no representable wave geometry for "
849 << timing.name);
850 return 0;
851 }
852 // Use wider tolerance (250ns) for UART because the UART clock and RMT
853 // sample clock are asynchronous, and GPIO matrix adds ~10-20ns jitter
854 auto rx_timing = fl::make4PhaseTiming(uart_timing, 250);
855 rx_timing.gap_tolerance_ns = 100000; // 100µs for UART inter-frame gaps
856
857 AR_FL_WARN("[CAPTURE] UART RX timing: T0H=" << uart_timing.T1
858 << " T1H=" << (uart_timing.T1 + uart_timing.T2)
859 << " T0L=" << (uart_timing.T2 + uart_timing.T3)
860 << " T1L=" << uart_timing.T3);
861
862 auto decode_result = rx_channel->decode(rx_timing, rx_buffer);
863 if (!decode_result.ok()) {
864 AR_FL_WARN("[CAPTURE] UART decode failed (error: " << static_cast<int>(decode_result.error()) << ")");
865 // Was EdgeRange(0, 256) -- 257 FL_WARN lines per decode failure
866// drowned the Teensy 4 UART TX FIFO and stalled autoresearch
867// between patterns (fbuild#755). 32 edges is enough to see whether
868// the first transition is a clean bit-0 or a glitch.
869dumpRawEdgeTiming(rx_channel, timing, fl::EdgeRange(0, 32));
870 return 0;
871 }
872 AR_FL_WARN("[CAPTURE] UART decoded " << decode_result.value() << " LED bytes");
873 return decode_result.value();
874 }
875
876 // SPI chipset drivers: decode raw SPI bit stream.
877 // These drivers use platform hardware SPI/I80/I2S to output APA102 data.
878 // RMT RX captures edges on the data pin; clock pin is ignored.
879 bool is_lcd_spi_driver = (fl::strcmp(driver_name, "LCD_SPI") == 0);
880 bool is_i2s_spi_driver = (fl::strcmp(driver_name, "I2S_SPI") == 0);
881 bool is_spi_unified_driver = (fl::strcmp(driver_name, "SPI_UNIFIED") == 0);
882 if (is_lcd_spi_driver || is_i2s_spi_driver || is_spi_unified_driver) {
883 // SPI clock used for validation: 2.4MHz (matches ValidationRemote.cpp)
884 const uint32_t spi_clock_hz = 2400000;
885 AR_FL_WARN("[CAPTURE] SPI chipset decode: clock=" << spi_clock_hz << " Hz");
886 size_t decoded = decodeSpiEdges(rx_channel, rx_buffer, spi_clock_hz);
887 if (decoded == 0) {
888 AR_FL_WARN("[CAPTURE] SPI decode failed");
889 // Was EdgeRange(0, 256) -- 257 FL_WARN lines per decode failure
890// drowned the Teensy 4 UART TX FIFO and stalled autoresearch
891// between patterns (fbuild#755). 32 edges is enough to see whether
892// the first transition is a clean bit-0 or a glitch.
893dumpRawEdgeTiming(rx_channel, timing, fl::EdgeRange(0, 32));
894 }
895 return decoded;
896 }
897
898 // Decode received data directly into rx_buffer (rx_timing built above).
899
900 AR_FL_WARN("[CAPTURE] Decoding...");
901 auto decode_result = rx_channel->decode(rx_timing, rx_buffer);
902 if (diagnostics) {
903 diagnostics->decodeOk = decode_result.ok() ? 1 : 0;
904 diagnostics->decodeError =
905 decode_result.ok() ? -1 : static_cast<int>(decode_result.error());
906 diagnostics->decodeBytes =
907 decode_result.ok() ? static_cast<int>(decode_result.value()) : 0;
908 }
909
910 if (!decode_result.ok()) {
911 // Use FL_WARN instead of FL_ERROR to avoid triggering bash autoresearch exit
912 // This can happen during warmup/setup and is not fatal
913 AR_FL_WARN("Decode failed (error code: " << static_cast<int>(decode_result.error()) << ")");
914 // Print raw edge timing on decode failure to diagnose the issue
915 // Was EdgeRange(0, 256) -- 257 FL_WARN lines per decode failure
916// drowned the Teensy 4 UART TX FIFO and stalled autoresearch
917// between patterns (fbuild#755). 32 edges is enough to see whether
918// the first transition is a clean bit-0 or a glitch.
919dumpRawEdgeTiming(rx_channel, timing, fl::EdgeRange(0, 32));
920 return 0;
921 }
922
923 AR_FL_WARN("[CAPTURE] Decoded " << decode_result.value() << " bytes");
924 return decode_result.value();
925}
926
927// Generic driver-agnostic autoresearch test runner
928// Tests all channels using the specified configuration
929void runTest(const char* test_name,
931 int& total, int& passed) {
932 // Multi-lane limitation: Only test Lane 0 (first channel)
933 // Hardware constraint: Only one TX channel can be read from via RX loopback
934 size_t channels_to_test = config.tx_configs.size() > 1 ? 1 : config.tx_configs.size();
935
936 if (config.tx_configs.size() > 1) {
937 AR_FL_WARN("\n[MULTI-LANE] Testing " << config.tx_configs.size() << " lanes, testing Lane 0 only (hardware limitation)");
938 }
939
940 // Test enabled configs (Lane 0 only for multi-lane)
941 for (size_t config_idx = 0; config_idx < channels_to_test; config_idx++) {
942 total++;
943
944 // Build test context for detailed error reporting
945 const auto& leds = config.tx_configs[config_idx].mLeds;
946 size_t num_leds = leds.size();
947 const bool lane_uses_rgbw =
948 legacyLaneUsesRgbw(config, config_idx);
949 // UCS7604 puts a 15-byte preamble and 16-bit channels on the wire, so
950 // its frame runs several times the pixel byte count. This number sizes
951 // the RX edge capacity as well as the comparison below, so deriving it
952 // from the pixel count truncates the capture (FastLED#4373).
953 const bool lane_uses_ucs7604 = isUCS7604(config.encoder);
954 fl::vector<uint8_t> expected_encoded;
955 if (lane_uses_ucs7604) {
956 expected_encoded = buildExpectedUCS7604(
957 config.tx_configs[config_idx].mLeds, config.encoder);
958 }
959 const size_t expected_data_bytes =
960 lane_uses_ucs7604 ? expected_encoded.size()
961 : num_leds * (lane_uses_rgbw ? 4u : 3u);
962
963 fl::TestContext ctx{
964 config.driver_name,
965 config.timing_name,
966 fl::toString(config.rx_type),
967 test_name,
968 static_cast<int>(config.tx_configs.size()),
969 static_cast<int>(config_idx),
970 config.base_strip_size,
971 static_cast<int>(num_leds),
972 config.tx_configs[config_idx].getDataPin()
973 };
974
975 AR_FL_WARN("\n=== " << test_name << " [Lane " << config_idx << "/" << config.tx_configs.size()
976 << ", Pin " << config.tx_configs[config_idx].getDataPin()
977 << ", LEDs " << config.tx_configs[config_idx].mLeds.size() << "] ===");
978
979 // Use RX channel provided via config (created in .ino file, never created dynamically here)
980 if (!config.rx_channel) {
981 FL_ERROR("[" << ctx.driver_name << "/" << ctx.timing_name << "/" << ctx.pattern_name
982 << " | Lane " << ctx.lane_index << "/" << ctx.lane_count
983 << " (Pin " << ctx.pin_number << ", " << ctx.num_leds << " LEDs) | RX:" << ctx.rx_type_name << "] "
984 << "RX channel is null - must be created in .ino and passed via AutoResearchConfig");
985 FL_ERROR("Result: FAIL ✗ (RX channel not provided)");
986 continue;
987 }
988
989 size_t bytes_captured = capture(config.rx_channel, config.rx_buffer,
990 expected_data_bytes,
991 config.tx_configs[config_idx].getDataPin(),
992 config.timing, config.driver_name);
993
994 if (bytes_captured == 0) {
995 FL_ERROR("[" << ctx.driver_name << "/" << ctx.timing_name << "/" << ctx.pattern_name
996 << " | Lane " << ctx.lane_index << "/" << ctx.lane_count
997 << " (Pin " << ctx.pin_number << ", " << ctx.num_leds << " LEDs) | RX:" << ctx.rx_type_name << "] "
998 << "Result: FAIL ✗ (capture failed)");
999 continue;
1000 }
1001
1002 int mismatches = 0;
1003
1004 if (lane_uses_ucs7604) {
1005 // UCS7604: Compare full encoded frame (preamble + padding + pixel data) byte-for-byte
1006 size_t expected_len = expected_encoded.size();
1007
1008 AR_FL_WARN("UCS7604 encoded comparison: expected " << expected_len << " bytes, captured " << bytes_captured);
1009
1010 size_t compare_len = (bytes_captured < expected_len) ? bytes_captured : expected_len;
1011 for (size_t i = 0; i < compare_len; i++) {
1012 if (expected_encoded[i] != config.rx_buffer[i]) {
1013 mismatches++;
1014 }
1015 }
1016 // Count any missing bytes as mismatches
1017 if (bytes_captured < expected_len) {
1018 mismatches += static_cast<int>(expected_len - bytes_captured);
1019 }
1020
1021 AR_FL_WARN("Bytes Captured: " << bytes_captured << " (expected: " << expected_len << ")");
1022 int total_bytes = static_cast<int>(expected_len);
1023 AR_FL_WARN("Accuracy: " << (100.0 * (total_bytes - mismatches) / total_bytes) << "% ("
1024 << (total_bytes - mismatches) << "/" << total_bytes << " bytes match)");
1025 } else if (lane_uses_rgbw) {
1026 const fl::vector<uint8_t> expected =
1027 buildExpectedClockless(
1028 config.tx_configs[config_idx].mLeds, true);
1029 const size_t expected_len = expected.size();
1030 const size_t compare_len =
1031 bytes_captured < expected_len ? bytes_captured : expected_len;
1032 for (size_t i = 0; i < compare_len; ++i) {
1033 if (expected[i] != config.rx_buffer[i]) {
1034 ++mismatches;
1035 }
1036 }
1037 if (bytes_captured < expected_len) {
1038 mismatches +=
1039 static_cast<int>(expected_len - bytes_captured);
1040 }
1041
1042 AR_FL_WARN("RGBW bytes captured: " << bytes_captured
1043 << " (expected: " << expected_len << ")");
1044 AR_FL_WARN("Accuracy: "
1045 << (100.0 * (expected_len - mismatches) / expected_len)
1046 << "% (" << (expected_len - mismatches) << "/"
1047 << expected_len << " bytes match)");
1048 } else {
1049 // WS2812: Compare raw RGB per-LED
1050 size_t bytes_expected = num_leds * 3;
1051
1052 // NEVER EVER CHANGE THIS!!!!!
1053 const size_t front_padding_bytes = 0; // No front padding: PARLIO FRONT_PAD_BYTES=0
1054 const size_t rx_buffer_offset = front_padding_bytes;
1055
1056 if (bytes_captured > bytes_expected + front_padding_bytes) {
1057 AR_FL_WARN("Info: Captured " << bytes_captured << " bytes ("
1058 << front_padding_bytes << " front pad + "
1059 << bytes_expected << " LED data + "
1060 << (bytes_captured - bytes_expected - front_padding_bytes) << " back pad/RESET)");
1061 }
1062
1063 // Compare: byte-level comparison (COLOR_ORDER is RGB, so no reordering)
1064 size_t bytes_to_check = (bytes_captured < bytes_expected + rx_buffer_offset) ?
1065 (bytes_captured > rx_buffer_offset ? bytes_captured - rx_buffer_offset : 0) :
1066 bytes_expected;
1067 (void)bytes_to_check;
1068
1069 for (size_t i = 0; i < num_leds; i++) {
1070 size_t byte_offset = rx_buffer_offset + i * 3; // Skip front padding
1071 if (byte_offset + 2 >= bytes_captured) { // Check against total captured bytes
1072 FL_ERROR("[" << ctx.driver_name << "/" << ctx.timing_name << "/" << ctx.pattern_name
1073 << " | Lane " << ctx.lane_index << "/" << ctx.lane_count
1074 << " (Pin " << ctx.pin_number << ", " << ctx.num_leds << " LEDs) | RX:" << ctx.rx_type_name << "] "
1075 << "Incomplete data for LED[" << static_cast<int>(i)
1076 << "] (only " << bytes_captured << " bytes captured)");
1077 break;
1078 }
1079
1080 uint8_t expected_r = leds[i].r;
1081 uint8_t expected_g = leds[i].g;
1082 uint8_t expected_b = leds[i].b;
1083
1084 uint8_t actual_r = config.rx_buffer[byte_offset + 0];
1085 uint8_t actual_g = config.rx_buffer[byte_offset + 1];
1086 uint8_t actual_b = config.rx_buffer[byte_offset + 2];
1087
1088 if (expected_r != actual_r || expected_g != actual_g || expected_b != actual_b) {
1089 mismatches++;
1090 }
1091 }
1092
1093 AR_FL_WARN("Bytes Captured: " << bytes_captured << " (expected: " << bytes_expected << ")");
1094 AR_FL_WARN("Accuracy: " << (100.0 * (num_leds - mismatches) / num_leds) << "% ("
1095 << (num_leds - mismatches) << "/" << num_leds << " LEDs match)");
1096 }
1097
1098 if (mismatches == 0) {
1099 AR_FL_WARN("Result: PASS ✓");
1100 passed++;
1101 } else {
1102 FL_ERROR("[" << ctx.driver_name << "/" << ctx.timing_name << "/" << ctx.pattern_name
1103 << " | Lane " << ctx.lane_index << "/" << ctx.lane_count
1104 << " (Pin " << ctx.pin_number << ", " << ctx.num_leds << " LEDs) | RX:" << ctx.rx_type_name << "] "
1105 << "Result: FAIL ✗");
1106 }
1107 }
1108}
1109
1110// Multi-run autoresearch test runner
1111// Runs the same test multiple times and tracks errors across runs
1112void runMultiTest(const char* test_name,
1113 fl::AutoResearchConfig& config,
1114 const fl::MultiRunConfig& multi_config,
1115 int& total, int& passed,
1116 fl::vector<fl::RunResult>* out_results) {
1117
1118 // PR #3421 framework-hang fix:
1119 // FL_WARN expansion ALWAYS constructs `fl::sstream() << X` regardless
1120 // of log level -- `ScopedLogDisable` only suppresses the OUTPUT inside
1121 // `log_emit`. Under autoresearch's RPC ASYNC handler (which wraps the
1122 // test in `ScopedLogDisable`), the cumulative `fl::sstream`/`fl::string`
1123 // heap alloc/free cycles in runMultiTest's banner + per-run + summary
1124 // FL_WARNs were leaving the Teensy 4 heap in a state where the
1125 // function-exit destructor of the heap-backed `fl::string` hangs
1126 // indefinitely (reproduced ~80% of runs on LPUART, FlexIO, and
1127 // ObjectFLED autoresearch tests). Skipping the sstream construction
1128 // when log level is below WARN avoids the heap thrash entirely.
1129 // See: marker-narrowed investigation in PR #3421 / session research.
1130 const bool kLogsEnabled = fl::getLogLevel() >=
1132
1133 fl::sstream ss;
1134 if (kLogsEnabled) {
1135 ss << "\n╔════════════════════════════════════════════════════════════════╗\n";
1136 ss << "║ MULTI-RUN TEST: " << test_name << "\n";
1137 ss << "║ Runs: " << multi_config.num_runs << " | Print Mode: "
1138 << (multi_config.print_all_runs ? "All" : "Errors ONLY") << "\n";
1139 ss << "╚════════════════════════════════════════════════════════════════╝";
1140 AR_FL_WARN(ss.str());
1141 }
1142
1143 fl::vector<fl::RunResult> run_results;
1144 int total_passed = 0;
1145 int total_failed = 0;
1146 bool retained_success = false;
1147
1148 // Multi-lane limitation: Only test Lane 0
1149 size_t channels_to_test = config.tx_configs.size() > 1 ? 1 : config.tx_configs.size();
1150
1151 if (config.tx_configs.size() > 1) {
1152 AR_FL_WARN("[MULTI-LANE] Testing " << config.tx_configs.size() << " lanes, testing Lane 0 only");
1153 }
1154
1155 // The tested lane and its pixel buffer are stable across repeated frames,
1156 // so construct the RGBW wire-order expectation once instead of allocating
1157 // and rebuilding it for every capture.
1158 fl::vector<uint8_t> expected_rgbw;
1159 if (channels_to_test > 0 && legacyLaneUsesRgbw(config, 0)) {
1160 expected_rgbw =
1161 buildExpectedClockless(config.tx_configs[0].mLeds, true);
1162 }
1163
1164 // Same reasoning for the UCS7604 encoded frame, plus one more: its length
1165 // is what sizes the RX edge capacity in capture(). UCS7604 carries a
1166 // 15-byte preamble and 16-bit channels, so the pixel byte count is several
1167 // times short of the wire frame and truncates the capture (FastLED#4373).
1168 const bool lane_uses_ucs7604 = isUCS7604(config.encoder);
1169 fl::vector<uint8_t> expected_ucs7604;
1170 if (channels_to_test > 0 && lane_uses_ucs7604) {
1171 expected_ucs7604 =
1172 buildExpectedUCS7604(config.tx_configs[0].mLeds, config.encoder);
1173 }
1174
1175 // Execute multiple runs
1176 for (int run = 1; run <= multi_config.num_runs; run++) {
1177 // runSingleTest executes synchronously inside the RPC task, so the
1178 // sketch loop cannot feed its five-second watchdog while a legitimate
1179 // repeated-frame soak is in progress. Each capture is independently
1180 // bounded by the TX/RX waits below; feed between them rather than
1181 // relaxing the watchdog or adding serial progress chatter.
1182 FastLED.watchdog().feed();
1183 // Do not construct progress strings under ScopedLogDisable: a long
1184 // hardware soak otherwise performs throwaway heap allocations.
1185 if (kLogsEnabled && (run % 3 == 1 || multi_config.num_runs <= 5)) {
1186 AR_FL_WARN("[Run " << run << "/" << multi_config.num_runs << "] Testing...");
1187 }
1188
1190 result.run_number = run;
1191
1192 // Test Lane 0 only
1193 for (size_t config_idx = 0; config_idx < channels_to_test; config_idx++) {
1194 const auto& leds = config.tx_configs[config_idx].mLeds;
1195 size_t num_leds = leds.size();
1196 const bool lane_uses_rgbw =
1197 legacyLaneUsesRgbw(config, config_idx);
1198 const size_t expected_data_bytes =
1199 lane_uses_ucs7604 ? expected_ucs7604.size()
1200 : num_leds * (lane_uses_rgbw ? 4u : 3u);
1201 result.total_leds = num_leds;
1202 result.totalBytes = static_cast<int>(expected_data_bytes);
1203
1204 // Capture RX data
1205 size_t bytes_captured =
1206 capture(config.rx_channel, config.rx_buffer,
1207 expected_data_bytes,
1208 config.tx_configs[config_idx].getDataPin(),
1209 config.timing,
1210 config.driver_name, &result);
1211 result.capturedBytes = static_cast<int>(bytes_captured);
1212
1213 if (bytes_captured == 0) {
1214 AR_FL_WARN("[Run " << run << "] Capture failed");
1215 result.mismatches = static_cast<int>(num_leds);
1216 result.mismatchedBytes = static_cast<int>(result.totalBytes);
1217 result.captureFailed = true;
1218 result.passed = false;
1219 // This also covers decoder failures after a successful RX
1220 // wait: capture() reports zero bytes, so preserve the raw
1221 // edge snapshot before this run exits.
1222 collectRawEdgeDiagnostics(config.rx_channel, &result);
1223 break;
1224 }
1225
1226 // Check pixel data
1227 int mismatches = 0;
1228
1229 // Diagnostic: single-line driver + first-24-bytes dump. Was 26
1230 // FL_WARNs (one per byte) -- on a 4-pattern test that produced 104
1231 // serial lines of pure hex dump, which dominated the Teensy 4 UART
1232 // TX FIFO under bursty autoresearch loads (fbuild#755 followup,
1233 // FastLED#3219 stack of fixes). Collapsing into a single
1234 // space-separated hex string keeps the diagnostic value while
1235 // dropping per-pattern serial overhead from ~2.1 KB to ~80 bytes.
1236 if (kLogsEnabled) {
1237 fl::sstream hex_dump;
1238 size_t dump_count = (bytes_captured < 24) ? bytes_captured : 24;
1239 for (size_t i = 0; i < dump_count; i++) {
1240 if (i > 0) hex_dump << " ";
1241 hex_dump << fl::hex << static_cast<int>(config.rx_buffer[i]) << fl::dec;
1242 }
1243 AR_FL_WARN("[RUN " << run << "] Driver=" << config.driver_name
1244 << " bytes_captured=" << bytes_captured
1245 << " first" << static_cast<int>(dump_count) << "=" << hex_dump.str());
1246 }
1247
1248 if (lane_uses_ucs7604) {
1249 // UCS7604: Compare full encoded frame byte-for-byte
1250 const fl::vector<uint8_t>& expected_encoded = expected_ucs7604;
1251 size_t expected_len = expected_encoded.size();
1252 result.totalBytes = static_cast<int>(expected_len);
1253
1254 size_t compare_len = (bytes_captured < expected_len) ? bytes_captured : expected_len;
1255 for (size_t i = 0; i < compare_len; i++) {
1256 if (expected_encoded[i] != config.rx_buffer[i]) {
1257 result.mismatchedBytes++;
1258 if ((expected_encoded[i] ^ config.rx_buffer[i]) == 0x01) {
1259 result.lsbOnlyErrors++;
1260 }
1261 mismatches++;
1262
1263 // Print corruption context for first mismatch only
1264 if (mismatches == 1) {
1265 AR_FL_WARN("\n[CORRUPTION @ byte " << static_cast<int>(i) << ", Run " << run
1266 << "] expected=0x" << fl::hex << static_cast<int>(expected_encoded[i])
1267 << " actual=0x" << static_cast<int>(config.rx_buffer[i]) << fl::dec);
1268 }
1269
1270 // Store first N errors (using byte-level reporting)
1271 if (result.errors.size() < static_cast<size_t>(multi_config.max_errors_per_run)) {
1272 result.errors.push_back(fl::LEDError(
1273 static_cast<int>(i), expected_encoded[i], 0, 0,
1274 config.rx_buffer[i], 0, 0
1275 ));
1276 }
1277 }
1278 }
1279 // Count missing bytes as mismatches
1280 if (bytes_captured < expected_len) {
1281 mismatches += static_cast<int>(expected_len - bytes_captured);
1282 }
1283 } else if (lane_uses_rgbw) {
1284 const fl::vector<uint8_t>& expected = expected_rgbw;
1285 size_t verified_leds = 0;
1286 for (size_t i = 0; i < num_leds; ++i) {
1287 const size_t byte_offset = i * 4;
1288 if (byte_offset + 3 >= bytes_captured) {
1289 break;
1290 }
1291 verified_leds = i + 1;
1292
1293 bool led_mismatch = false;
1294 for (size_t channel = 0; channel < 4; ++channel) {
1295 const uint8_t expected_byte =
1296 expected[byte_offset + channel];
1297 const uint8_t actual_byte =
1298 config.rx_buffer[byte_offset + channel];
1299 if (expected_byte != actual_byte) {
1300 ++result.mismatchedBytes;
1301 if ((expected_byte ^ actual_byte) == 0x01) {
1302 ++result.lsbOnlyErrors;
1303 }
1304 led_mismatch = true;
1305 }
1306 }
1307
1308 if (led_mismatch) {
1309 if (mismatches == 0) {
1310 AR_FL_WARN("\n[RGBW CORRUPTION @ LED "
1311 << static_cast<int>(i) << ", Run "
1312 << run << "]");
1313 const size_t corruption_edge_index = i * 64;
1314 const size_t offset =
1315 corruption_edge_index > 4
1316 ? corruption_edge_index - 4
1317 : 0;
1318 dumpRawEdgeTiming(
1319 config.rx_channel, config.timing,
1320 fl::EdgeRange(offset, 9));
1321 }
1322 ++mismatches;
1323
1324 if (result.errors.size() <
1325 static_cast<size_t>(
1326 multi_config.max_errors_per_run)) {
1327 result.errors.push_back(fl::LEDError(
1328 static_cast<int>(i),
1329 expected[byte_offset + 0],
1330 expected[byte_offset + 1],
1331 expected[byte_offset + 2],
1332 expected[byte_offset + 3],
1333 config.rx_buffer[byte_offset + 0],
1334 config.rx_buffer[byte_offset + 1],
1335 config.rx_buffer[byte_offset + 2],
1336 config.rx_buffer[byte_offset + 3]));
1337 }
1338 }
1339 }
1340
1341 if (verified_leds < num_leds) {
1342 const size_t unchecked = num_leds - verified_leds;
1343 mismatches += static_cast<int>(unchecked);
1344 result.mismatchedBytes +=
1345 static_cast<int>(unchecked * 4);
1346 AR_FL_WARN("[TRUNCATED RGBW CAPTURE] Only verified "
1347 << verified_leds << "/" << num_leds
1348 << " LEDs (" << bytes_captured
1349 << " bytes captured, needed "
1350 << expected_data_bytes << "). Marking "
1351 << unchecked
1352 << " unchecked LEDs as mismatches.");
1353 }
1354 } else {
1355 // WS2812: Per-LED RGB comparison
1356 size_t bytes_expected = num_leds * 3;
1357
1358 // Determine front padding offset (PARLIO only)
1359 const size_t rx_buffer_offset = 0; // No front padding: PARLIO FRONT_PAD_BYTES=0
1360
1361 size_t bytes_to_check = (bytes_captured < bytes_expected + rx_buffer_offset) ?
1362 (bytes_captured > rx_buffer_offset ? bytes_captured - rx_buffer_offset : 0) :
1363 bytes_expected;
1364 (void)bytes_to_check;
1365
1366 size_t verified_leds = 0;
1367 for (size_t i = 0; i < num_leds; i++) {
1368 size_t byte_offset = rx_buffer_offset + i * 3; // Apply offset for PARLIO front padding
1369 if (byte_offset + 2 >= bytes_captured) { // Check against total captured bytes
1370 break;
1371 }
1372 verified_leds = i + 1;
1373
1374 uint8_t expected_r = leds[i].r;
1375 uint8_t expected_g = leds[i].g;
1376 uint8_t expected_b = leds[i].b;
1377
1378 uint8_t actual_r = config.rx_buffer[byte_offset + 0];
1379 uint8_t actual_g = config.rx_buffer[byte_offset + 1];
1380 uint8_t actual_b = config.rx_buffer[byte_offset + 2];
1381
1382 // Per-byte comparison for byte-level stats
1383 uint8_t exp_bytes[3] = {expected_r, expected_g, expected_b};
1384 uint8_t act_bytes[3] = {actual_r, actual_g, actual_b};
1385 bool led_mismatch = false;
1386 for (int ch = 0; ch < 3; ch++) {
1387 if (exp_bytes[ch] != act_bytes[ch]) {
1388 result.mismatchedBytes++;
1389 if ((exp_bytes[ch] ^ act_bytes[ch]) == 0x01) {
1390 result.lsbOnlyErrors++;
1391 }
1392 led_mismatch = true;
1393 }
1394 }
1395
1396 if (led_mismatch) {
1397 // Print corruption context for first mismatch only
1398 if (mismatches == 0) {
1399 AR_FL_WARN("\n[CORRUPTION @ LED " << static_cast<int>(i) << ", Run " << run << "]");
1400
1401 // Calculate edge index and print timing around corruption point
1402 size_t corruption_edge_index = i * 48;
1403 size_t offset = corruption_edge_index > 4 ? corruption_edge_index - 4 : 0;
1404
1405 // Dump raw edge timing around corruption point (9 edges: -4 to +4)
1406 dumpRawEdgeTiming(config.rx_channel, config.timing, fl::EdgeRange(offset, 9));
1407 }
1408
1409 mismatches++;
1410
1411 // Store first N errors
1412 if (result.errors.size() < static_cast<size_t>(multi_config.max_errors_per_run)) {
1413 result.errors.push_back(fl::LEDError(
1414 i, expected_r, expected_g, expected_b,
1415 actual_r, actual_g, actual_b
1416 ));
1417 }
1418 }
1419 }
1420
1421 // Truncated capture: RX returned fewer bytes than expected. Count
1422 // the unchecked LEDs as mismatches rather than silently passing.
1423 // A short capture means we did NOT verify those LEDs, so the run
1424 // MUST fail. The old silent break hid real bugs on strips longer
1425 // than the RMT DMA buffer (~170 LEDs for WS2812B). See issue #2254.
1426 if (verified_leds < num_leds) {
1427 size_t unchecked = num_leds - verified_leds;
1428 mismatches += static_cast<int>(unchecked);
1429 result.mismatchedBytes += static_cast<int>(unchecked * 3);
1430 AR_FL_WARN("[TRUNCATED CAPTURE] Only verified " << verified_leds
1431 << "/" << num_leds << " LEDs (" << bytes_captured
1432 << " bytes captured, needed " << (num_leds * 3)
1433 << "). Marking " << unchecked
1434 << " unchecked LEDs as mismatches.");
1435 }
1436 }
1437
1438 result.mismatches = mismatches;
1439 result.passed = (mismatches == 0);
1440 if (!result.passed) {
1441 collectRawEdgeDiagnostics(config.rx_channel, &result);
1442 }
1443 }
1444
1445 // Print run result if configured (sstream construction skipped
1446 // when logs disabled -- see kLogsEnabled note at function top).
1447 if (kLogsEnabled && (multi_config.print_all_runs || !result.passed)) {
1448 AR_FL_WARN("[Run " << run << "/" << multi_config.num_runs << "] "
1449 << (result.passed ? "PASS" : "FAIL")
1450 << " | Errors: " << result.mismatches << "/" << result.total_leds
1451 << " (" << (100.0 * (result.total_leds - result.mismatches) / result.total_leds) << "%)");
1452
1453 // Print error details if enabled
1454 if (!result.passed && multi_config.print_per_led_errors && !result.errors.empty()) {
1455 AR_FL_WARN(" First " << result.errors.size() << " error(s):");
1456 for (size_t i = 0; i < result.errors.size(); i++) {
1457 const auto& err = result.errors[i];
1458 AR_FL_WARN(" LED[" << err.led_index << "]: expected RGB("
1459 << static_cast<int>(err.expected_r) << ","
1460 << static_cast<int>(err.expected_g) << ","
1461 << static_cast<int>(err.expected_b) << ") got RGB("
1462 << static_cast<int>(err.actual_r) << ","
1463 << static_cast<int>(err.actual_g) << ","
1464 << static_cast<int>(err.actual_b) << ")");
1465 }
1466 }
1467 }
1468
1469 if (result.passed) {
1470 ++total_passed;
1471 } else {
1472 ++total_failed;
1473 }
1474
1475 // The RPC response includes only failed pattern details. Keeping every
1476 // successful result during a long frame soak needlessly grows the
1477 // transient heap (and used to make the 16-frame RP PIO run wedge).
1478 // Retain one representative successful capture for byte-evidence;
1479 // retain every failure so its full postmortem remains available.
1480 if (!result.passed || !retained_success || multi_config.num_runs == 1) {
1481 if (result.passed) {
1482 retained_success = true;
1483 }
1484 run_results.push_back(fl::move(result));
1485 }
1486 FastLED.watchdog().feed();
1487 }
1488
1489 // Summary statistics
1490 // Summary FL_WARN -- gated on log level (see kLogsEnabled note above)
1491 // to prevent heap thrash under autoresearch ScopedLogDisable.
1492 if (kLogsEnabled) {
1493 ss.clear();
1494 ss << "\n╔════════════════════════════════════════════════════════════════╗\n";
1495 ss << "║ MULTI-RUN SUMMARY\n";
1496 ss << "╚════════════════════════════════════════════════════════════════╝\n";
1497 ss << "Total Runs: " << multi_config.num_runs << "\n";
1498 ss << "Passed: " << total_passed << " (" << (100.0 * total_passed / multi_config.num_runs) << "%)\n";
1499 ss << "Failed: " << total_failed << " (" << (100.0 * total_failed / multi_config.num_runs) << "%)";
1500 AR_FL_WARN(ss.str());
1501
1502 if (total_failed > 0) {
1503 ss.clear();
1504 ss << "\nFailed Run Numbers:\n";
1505 for (const auto& r : run_results) {
1506 if (!r.passed) {
1507 ss << " Run #" << r.run_number << " - " << r.mismatches << " errors\n";
1508 if (!r.errors.empty()) {
1509 ss << " First error at LED[" << r.errors[0].led_index << "]: "
1510 << "expected RGB(" << static_cast<int>(r.errors[0].expected_r) << ","
1511 << static_cast<int>(r.errors[0].expected_g) << ","
1512 << static_cast<int>(r.errors[0].expected_b) << ") got RGB("
1513 << static_cast<int>(r.errors[0].actual_r) << ","
1514 << static_cast<int>(r.errors[0].actual_g) << ","
1515 << static_cast<int>(r.errors[0].actual_b) << ")\n";
1516 }
1517 }
1518 }
1519 AR_FL_WARN(ss.str());
1520 }
1521 }
1522
1523 // Copy results to caller if requested
1524 if (out_results) {
1525 for (const auto& r : run_results) {
1526 out_results->push_back(r);
1527 }
1528 }
1529
1530 // Update totals
1531 total++;
1532 if (total_failed == 0) {
1533 passed++;
1534 if (kLogsEnabled) {
1535 AR_FL_WARN("\n[OVERALL] PASS ✓ - All " << multi_config.num_runs << " runs succeeded");
1536 }
1537 } else {
1538 if (kLogsEnabled) {
1539 AR_FL_WARN("\n[OVERALL] FAIL ✗ - " << total_failed << "/" << multi_config.num_runs << " runs failed");
1540 }
1541 }
1542}
1543
1544// AutoResearch a specific chipset timing configuration
1545// Creates channels, runs tests, destroys channels
1547 int& driver_total, int& driver_passed,
1548 uint32_t& out_show_duration_ms,
1549 fl::vector<fl::RunResult>* out_results,
1550 int num_runs_per_pattern) {
1551 fl::sstream ss;
1552 ss << "\n========================================\n";
1553 ss << "Testing: " << config.timing_name << "\n";
1554 bool has_spi_config = config.tx_configs.size() > 0 && config.tx_configs[0].isSpi();
1555 if (has_spi_config) {
1556 const auto* spi_cfg = config.tx_configs[0].getSpiChipset();
1557 ss << " Protocol: SPI (APA102)\n";
1558 ss << " Clock: " << (spi_cfg ? spi_cfg->timing.clock_hz : 0) << " Hz\n";
1559 } else {
1560 ss << " T0H: " << config.timing.t1_ns << "ns\n";
1561 ss << " T1H: " << (config.timing.t1_ns + config.timing.t2_ns) << "ns\n";
1562 ss << " T0L: " << config.timing.t3_ns << "ns\n";
1563 ss << " RESET: " << config.timing.reset_us << "us\n";
1564 }
1565 ss << " Channels: " << config.tx_configs.size() << "\n";
1566 ss << "========================================";
1567 AR_FL_WARN(ss.str());
1568
1569 // Create ALL channels from tx_configs (multi-channel support)
1571 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1573 if (config.tx_configs[i].isSpi()) {
1574 // SPI chipset: pass through the SPI config directly
1575 channel = FastLED.add(config.tx_configs[i]);
1576 } else {
1577 // Clockless chipset: re-create with runtime timing. The encoder
1578 // has to come with it. ChannelConfig(pin, timing, ...) defaults
1579 // the encoder to WS2812, so a UCS7604 timing used to put raw
1580 // pixel bytes on the wire while the comparison below expected the
1581 // encoded frame -- every UCS7604 run failed at every LED count
1582 // with a captured pattern that looked like a driver fault
1583 // (FastLED#4373).
1584 fl::ClocklessChipset clockless_chipset(
1585 config.tx_configs[i].getDataPin(), config.timing, config.encoder);
1586 fl::ChannelConfig channel_config(clockless_chipset, config.tx_configs[i].mLeds, config.tx_configs[i].rgb_order);
1587 channel = FastLED.add(channel_config);
1588 }
1589 if (!channel) {
1590 FL_ERROR("Failed to create channel " << i << " (pin " << config.tx_configs[i].getDataPin() << ") - platform not supported");
1591 // Clean up previously created channels
1592 for (auto& ch : channels) {
1593 ch.reset();
1594 }
1595 return;
1596 }
1597 channels.push_back(channel);
1598 }
1599
1600 FastLED.setBrightness(255);
1601
1602 // Pre-initialize the TX engine to avoid first-call setup delays
1603 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1604 fill_solid(config.tx_configs[i].mLeds.data(), config.tx_configs[i].mLeds.size(), CRGB::Black);
1605 }
1606 FastLED.show();
1607 if (!FastLED.wait(1000)) { // 1s timeout - driver stall guard
1608 FL_ERROR("[PREINIT] TX wait timeout - driver may be stalled on this platform");
1610 return;
1611 }
1612 // TX engine pre-init logging silenced for speed
1613
1614 // CRITICAL: Wait for PARLIO streaming transmission to complete before starting tests
1615 // Without this delay, the RX will capture the pre-initialization BLACK frame instead of the test pattern
1616 // PARLIO is a streaming engine with ring buffers - need time to drain the initial frame
1617 delay(5); // Reduced from 100ms - just enough for buffer drain
1618
1619 // Run test patterns (mixed bit patterns to test MSB vs LSB handling)
1620 int total = 0;
1621 int passed = 0;
1622
1623 // Multi-run configuration - optimized for speed
1624 fl::MultiRunConfig multi_config;
1625 // num_runs=1: Python orchestrates retries (default).
1626 // num_runs>=2: Back-to-back captures of the same buffer to expose second-frame
1627 // degradation (e.g., SPI driver zeroing its DMA buffer after the first show()).
1628 multi_config.num_runs = (num_runs_per_pattern > 0) ? num_runs_per_pattern : 1;
1629 multi_config.print_all_runs = (multi_config.num_runs > 1);
1630 multi_config.print_per_led_errors = false; // Errors reported via JSON-RPC
1631 multi_config.max_errors_per_run = 10; // Store first 10 errors for JSON response
1632
1633 // Measure show-only duration (excludes setup/teardown overhead)
1634 uint32_t show_start_ms = millis();
1635
1636 // Test all 4 bit patterns (0-3)
1637 for (int pattern_id = 0; pattern_id < 4; pattern_id++) {
1638 // Apply pattern to all lanes
1639 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1641 config.tx_configs[i].mLeds.data(),
1642 config.tx_configs[i].mLeds.size(),
1643 pattern_id
1644 );
1645 }
1646 runMultiTest(getBitPatternName(pattern_id), config, multi_config, total, passed, out_results);
1647 }
1648
1649 out_show_duration_ms += millis() - show_start_ms;
1650
1651 // Results reported via JSON-RPC (verbose logging silenced for speed)
1652
1653 // Accumulate results for driver-level tracking
1654 driver_total += total;
1655 driver_passed += passed;
1656
1657 // Destroy ALL channels before testing next timing configuration
1658 // CRITICAL: Clear FastLED's global channel registry to prevent accumulation
1659 // If we only destroy local shared_ptrs, FastLED still holds references
1661 if (fl::strcmp(config.driver_name, "OBJECT_FLED") == 0) {
1662 fl::objectFledDiagnosticsRecord("afterFastLedClear");
1663 }
1664 // Channel destruction is synchronous - no delay needed
1665}
1666
1667// AutoResearch using the legacy template addLeds API (supports multi-lane)
1668// Nearly identical to autoResearchChipsetTiming() — only channel creation differs:
1669// Normal: FastLED.add(channel_config) → Channel
1670// Legacy: LegacyClocklessProxy(pin, leds, numLeds) -> CHIPSET<PIN> -> ClocklessIdf5 -> SlimBridgeController -> RMT5 driver
1672 int& driver_total, int& driver_passed,
1673 uint32_t& out_show_duration_ms,
1674 fl::vector<fl::RunResult>* out_results,
1675 int num_runs_per_pattern) {
1676 fl::sstream ss;
1677 ss << "\n========================================\n";
1678 ss << "Testing (LEGACY API): " << config.timing_name << "\n";
1679 ss << " T0H: " << config.timing.t1_ns << "ns\n";
1680 ss << " T1H: " << (config.timing.t1_ns + config.timing.t2_ns) << "ns\n";
1681 ss << " T0L: " << config.timing.t3_ns << "ns\n";
1682 ss << " RESET: " << config.timing.reset_us << "us\n";
1683 ss << " Lanes: " << config.tx_configs.size() << "\n";
1684 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1685 ss << " Lane " << i << ": pin=" << config.tx_configs[i].getDataPin()
1686 << " LEDs=" << config.tx_configs[i].mLeds.size() << "\n";
1687 }
1688 ss << "========================================";
1689 AR_FL_WARN(ss.str());
1690
1691 // Create one legacy proxy per lane. By default every lane uses WS2812B,
1692 // but AutoResearch can supply per-lane chipsets to exercise multiple
1693 // ObjectFLED timing groups in a single FastLED.show().
1695 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1696 int pin = config.tx_configs[i].getDataPin();
1697 CRGB* leds = config.tx_configs[i].mLeds.data();
1698 int numLeds = static_cast<int>(config.tx_configs[i].mLeds.size());
1700 if (!config.legacy_chipsets.empty() &&
1701 i < config.legacy_chipsets.size()) {
1702 chipset = config.legacy_chipsets[i];
1703 }
1704
1706 pin, leds, numLeds, chipset, config.legacy_rgbw);
1707 if (!proxy->valid()) {
1708 FL_ERROR("Legacy proxy invalid for lane " << i << " (pin " << pin
1709 << ", chipset " << legacyClocklessChipsetName(chipset)
1710 << " not in supported set)");
1711 return; // vector destructor cleans up already-created proxies
1712 }
1713 proxies.push_back(fl::move(proxy));
1714 }
1715
1716 FastLED.setBrightness(255);
1717
1718 // Pre-initialize the TX engine to avoid first-call setup delays
1719 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1720 fill_solid(config.tx_configs[i].mLeds.data(), config.tx_configs[i].mLeds.size(), CRGB::Black);
1721 }
1722 FastLED.show();
1723 if (!FastLED.wait(1000)) {
1724 FL_ERROR("[LEGACY] TX wait timeout - driver may be stalled");
1725 return; // vector destructor cleans up proxies
1726 }
1727
1728 delay(5); // Buffer drain (same as autoResearchChipsetTiming)
1729
1730 // Run test patterns (identical to autoResearchChipsetTiming)
1731 int total = 0;
1732 int passed = 0;
1733
1734 fl::MultiRunConfig multi_config;
1735 multi_config.num_runs = (num_runs_per_pattern > 0) ? num_runs_per_pattern : 1;
1736 multi_config.print_all_runs = (multi_config.num_runs > 1);
1737 multi_config.print_per_led_errors = false;
1738 multi_config.max_errors_per_run = 10;
1739
1740 // Measure show-only duration (excludes setup/teardown overhead)
1741 uint32_t show_start_ms = millis();
1742
1743 const bool ws2814_visual_validation =
1744 !config.legacy_chipsets.empty() &&
1746 const int pattern_count = ws2814_visual_validation ? 8 : 4;
1747 for (int pattern_id = 0; pattern_id < pattern_count; pattern_id++) {
1748 // Apply pattern to all lanes
1749 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1751 config.tx_configs[i].mLeds.data(),
1752 config.tx_configs[i].mLeds.size(),
1753 pattern_id
1754 );
1755 }
1756 runMultiTest(getBitPatternName(pattern_id), config, multi_config, total, passed, out_results);
1757 if (ws2814_visual_validation && pattern_id >= 4) {
1758 // Leave each isolated R/G/B/W frame visible long enough for the
1759 // attached physical strip to be checked while RX independently
1760 // verifies the exact four-byte frame.
1761 delay(250);
1762 }
1763 }
1764
1765 out_show_duration_ms += millis() - show_start_ms;
1766
1767 driver_total += total;
1768 driver_passed += passed;
1769
1770 // proxies vector destructor deletes all controllers → ~CLEDController removes from draw list
1771}
1772
1773// Set mixed RGB bit patterns to test MSB vs LSB handling
1774void setMixedBitPattern(CRGB* leds, size_t count, int pattern_id) {
1775 switch (pattern_id) {
1776 case 0:
1777 // Pattern A: R=0xF0 (high nibble), G=0x0F (low nibble), B=0xAA (alternating)
1778 // Tests: High bits in R, low bits in G, mixed bits in B
1779 for (size_t i = 0; i < count; i++) {
1780 leds[i] = CRGB(0xF0, 0x0F, 0xAA);
1781 }
1782 break;
1783
1784 case 1:
1785 // Pattern B: R=0x55 (alternating 01010101), G=0xFF (all high), B=0x00 (all low)
1786 // Tests: Alternating bits, all-high boundary, all-low boundary
1787 for (size_t i = 0; i < count; i++) {
1788 leds[i] = CRGB(0x55, 0xFF, 0x00);
1789 }
1790 break;
1791
1792 case 2:
1793 // Pattern C: R=0x0F (low nibble), G=0xAA (alternating), B=0xF0 (high nibble)
1794 // Tests: Rotated pattern from A, ensures driver handles different channel values
1795 for (size_t i = 0; i < count; i++) {
1796 leds[i] = CRGB(0x0F, 0xAA, 0xF0);
1797 }
1798 break;
1799
1800 case 3:
1801 // Pattern D: Solid colors alternating (Red, Green, Blue repeating)
1802 // Baseline test - ensures basic RGB transmission works
1803 for (size_t i = 0; i < count; i++) {
1804 int color_index = i % 3;
1805 if (color_index == 0) {
1806 leds[i] = CRGB::Red; // RGB(255, 0, 0)
1807 } else if (color_index == 1) {
1808 leds[i] = CRGB::Green; // RGB(0, 255, 0)
1809 } else {
1810 leds[i] = CRGB::Blue; // RGB(0, 0, 255)
1811 }
1812 }
1813 break;
1814
1815 case 4:
1816 fill_solid(leds, count, CRGB::Red);
1817 break;
1818
1819 case 5:
1820 fill_solid(leds, count, CRGB::Green);
1821 break;
1822
1823 case 6:
1824 fill_solid(leds, count, CRGB::Blue);
1825 break;
1826
1827 case 7:
1828 fill_solid(leds, count, CRGB::White);
1829 break;
1830
1831 default:
1832 // Fallback: all black
1833 fill_solid(leds, count, CRGB::Black);
1834 break;
1835 }
1836}
1837
1838// Get name of bit pattern for logging
1839const char* getBitPatternName(int pattern_id) {
1840 switch (pattern_id) {
1841 case 0: return "Pattern A (R=0xF0, G=0x0F, B=0xAA)";
1842 case 1: return "Pattern B (R=0x55, G=0xFF, B=0x00)";
1843 case 2: return "Pattern C (R=0x0F, G=0xAA, B=0xF0)";
1844 case 3: return "Pattern D (RGB Solid Alternating)";
1845 case 4: return "Pattern E (Isolated Red)";
1846 case 5: return "Pattern F (Isolated Green)";
1847 case 6: return "Pattern G (Isolated Blue)";
1848 case 7: return "Pattern H (Isolated White)";
1849 default: return "Unknown Pattern";
1850 }
1851}
1852
1853#endif // FL_PLATFORM_HAS_LARGE_MEMORY
CRGB leds[1]
size_t capture(fl::shared_ptr< fl::RxChannel > rx_channel, fl::span< uint8_t > rx_buffer, size_t expected_data_bytes, int tx_pin, const fl::ChipsetTimingConfig &timing, const char *driver_name, fl::RunResult *diagnostics=nullptr)
void runMultiTest(const char *test_name, fl::AutoResearchConfig &config, const fl::MultiRunConfig &multi_config, int &total, int &passed, fl::vector< fl::RunResult > *out_results=nullptr)
void autoResearchChipsetTimingLegacy(fl::AutoResearchConfig &config, int &total, int &passed, uint32_t &out_show_duration_ms, fl::vector< fl::RunResult > *out_results=nullptr, int num_runs_per_pattern=1)
const char * getBitPatternName(int pattern_id)
void runTest(const char *test_name, fl::AutoResearchConfig &config, int &total, int &passed)
void autoResearchChipsetTiming(fl::AutoResearchConfig &config, int &total, int &passed, uint32_t &out_show_duration_ms, fl::vector< fl::RunResult > *out_results=nullptr, int num_runs_per_pattern=1)
void setMixedBitPattern(CRGB *leds, size_t count, int pattern_id)
uint8_t pos
Definition Blur.ino:11
FastLED show()
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
LegacyClocklessChipset
const char * legacyClocklessChipsetName(LegacyClocklessChipset chipset)
bool legacyClocklessChipsetHasAutomaticRgbw(LegacyClocklessChipset chipset)
Rgbw rgbw
Runtime chipset timing configuration for clockless LED drivers.
static fl::shared_ptr< const Gamma8 > getOrCreate(float gamma) FL_NO_EXCEPT
Definition ease.cpp.hpp:460
void writeWS2812(CONTAINER_UIN8_T *out) FL_NO_EXCEPT
void clear(bool freeMemory=false) FL_NO_EXCEPT
bool empty() const FL_NO_EXCEPT
constexpr bool empty() const FL_NO_EXCEPT
Definition span.h:535
const T * data() const FL_NO_EXCEPT
Definition span.h:486
constexpr fl::size size() const FL_NO_EXCEPT
Definition span.h:483
void clear() FL_NO_EXCEPT
Definition strstream.h:366
string str() const FL_NO_EXCEPT
Definition strstream.h:43
fl::size size() const FL_NO_EXCEPT
void reserve(fl::size n) FL_NO_EXCEPT
Definition vector.h:591
void resize(fl::size n) FL_NO_EXCEPT
Definition vector.h:593
T * data() FL_NO_EXCEPT
Definition vector.h:619
void push_back(const T &value) FL_NO_EXCEPT
Definition vector.h:624
void fill_solid(CRGB *targetArray, int numToFill, const CRGB &color) FL_NO_EXCEPT
Fill a range of LEDs with a solid color.
Definition fill.cpp.hpp:10
fl::UISlider offset("Offset", 0.0f, 0.0f, 1.0f, 0.01f)
#define DISABLE_DITHER
Disable dithering.
Definition dither_mode.h:14
UCS7604 LED chipset encoder implementation.
@ CHANNELS
Remove all channels from controller list.
Definition FastLED.h:668
Non-templated low level pixel data writing class.
fl::CRGB CRGB
Definition crgb.h:25
Umbrella header for ISR subsystem.
Centralized LED chipset timing definitions with nanosecond precision.
#define FL_ERROR(...)
Definition log.h:373
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
Definition move.h:30
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NO_EXCEPT
Definition unique_ptr.h:263
constexpr u32 ISR_FLAG_EDGE_RISING
Definition constants.h:22
int attach_external_handler(u8 pin, const config &cfg, handle *out_handle) FL_NO_EXCEPT
Attach an external interrupt handler (GPIO-based).
int detach_handler(handle &h) FL_NO_EXCEPT
Detach an ISR handler.
void run(fl::u32 microseconds, ExecFlags flags) FL_NO_EXCEPT
Run selected task subsystems.
bool useRmtInternalLoopback(bool is_rmt_driver, int tx_pin, int rx_pin) FL_NO_EXCEPT
Whether an RMT validation capture should use ESP-IDF loopback.
Definition validation.h:33
size_t captureEdgeCapacity(size_t shared_buffer_bytes, size_t expected_data_bytes, RxBackend backend) FL_NO_EXCEPT
Compute the RX edge-buffer capacity for a validation frame.
Definition validation.h:65
back_insert_iterator< Container > back_inserter(Container &c) FL_NO_EXCEPT
Helper function to create a back_insert_iterator.
Definition iterator.h:139
unsigned char u8
Definition stdint.h:131
u8 getLogLevel() FL_NO_EXCEPT
Get the current global log level.
ClocklessEncoder
Identifies which encoder to use for clockless chipsets in the Channel API.
@ CLOCKLESS_ENCODER_UCS7604_8BIT
UCS7604 8-bit 800KHz.
@ CLOCKLESS_ENCODER_UCS7604_16BIT
UCS7604 16-bit 800KHz.
@ CLOCKLESS_ENCODER_UCS7604_16BIT_1600
UCS7604 16-bit 1600KHz.
const dec_t dec
Definition ios.cpp.hpp:13
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
CRGB sample(const CRGB *grid, const XYMap &xyMap, float x, float y, SampleMode mode) FL_NO_EXCEPT
Sample a pixel from a 2D CRGB grid at floating-point coordinates.
constexpr u32 gamma(float g) FL_NO_EXCEPT
Definition gamma_lut.h:36
FASTLED_FORCE_INLINE int strcmp(const char *s1, const char *s2) FL_NO_EXCEPT
Definition cstring.h:60
fl::u32 millis() FL_NO_EXCEPT
Universal millisecond timer - returns milliseconds since system startup.
ChipsetTiming4Phase make4PhaseTiming(const ChipsetTiming &timing_3phase, u32 tolerance_ns) FL_NO_EXCEPT
Create 4-phase RX timing from 3-phase chipset timing with tolerance.
Definition rx.cpp.hpp:72
constexpr u32 kMaxUartBaudRate
Maximum UART baud rate supported by ESP32 variants.
FASTLED_FORCE_INLINE void * memset(void *s, int c, size_t n) FL_NO_EXCEPT
Definition cstring.h:148
@ PIO
RP2040/RP2350 PIO receive backend.
Definition types.h:9
@ FLEXIO
Teensy 4.x-only FlexIO capture backend (FLEXIO1 — FLEXIO2 is owned by the WS2812 TX driver)....
Definition types.h:14
@ PLATFORM_DEFAULT
Use the recommended backend for the active platform (RMT on ESP32; FlexPWM on Teensy 4....
Definition types.h:10
@ ISR
Platform-neutral interrupt-driven edge capture backend when available.
Definition types.h:12
constexpr size_t kRpPioRxEdgeCapacity
Phase slots in the shared capture pool.
@ FL_LOG_LEVEL_WARN
Errors and warnings.
Definition cstdio.h:40
const hex_t hex
Definition ios.cpp.hpp:11
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
UCS7604Mode
UCS7604 protocol configuration modes.
Definition ucs7604.h:30
@ UCS7604_MODE_8BIT_800KHZ
Definition ucs7604.h:31
@ UCS7604_MODE_16BIT_1600KHZ
Definition ucs7604.h:33
@ UCS7604_MODE_16BIT_800KHZ
Definition ucs7604.h:32
FL_OPTIMIZE_FUNCTION bool canUseWave3(const ChipsetTiming &timing) FL_NO_EXCEPT
Check if a chipset timing is eligible for wave3 encoding.
Definition wave3.cpp.hpp:26
void encodeUCS7604(PixelIterator &pixel_iter, size_t num_leds, OutputIterator out, UCS7604Mode mode, const UCS7604CurrentControl &current, bool is_rgbw, const Gamma8 *gamma=nullptr, bool wide_source=false) FL_NO_EXCEPT
Encode complete UCS7604 frame (preamble + padding + pixel data)
Definition ucs7604.h:273
const char * toString(RxDeviceType type) FL_NO_EXCEPT
Convert RxDeviceType to human-readable string.
Definition rx.h:181
@ SUCCESS
Operation completed successfully.
Definition rx.h:153
ChipsetTiming uartWireTiming(const ChipsetTimingConfig &timing, u32 max_baud_rate) FL_NO_EXCEPT
The wire timing the UART encoder actually produces for a chipset.
atomic< int > atomic_int
Definition atomic.h:30
u32 T2
Additional high time for bit 1 (nanoseconds)
Definition led_timing.h:88
u32 gap_tolerance_ns
Maximum gap duration to tolerate (0 = no gap tolerance, treat as error)
Definition rx.h:105
u32 T3
Low tail duration (nanoseconds)
Definition led_timing.h:89
u32 T1
High time for bit 0 (nanoseconds)
Definition led_timing.h:87
4-phase RX timing thresholds for chipset detection
Definition rx.h:88
Test context for detailed error reporting Aggregates all test configuration parameters for error mess...
Primary template — intentionally undefined.
Definition bus_traits.h:31
Generic chipset timing entry Provides T1, T2, T3 timing parameters in nanoseconds for any LED protoco...
Definition led_timing.h:86
Low level pixel data writing class.
fl::PixelIterator PixelIterator
unsigned char uint8_t
Definition stdint.h:208
fl::u32 uint32_t
Definition stdint.h:218
static ColorAdjustment noAdjustment() FL_NO_EXCEPT
the per-channel scale values premixed with brightness.
Pixel controller class.
static Rgbw value() FL_NO_EXCEPT
Definition rgbw.h:290
static Rgbw value() FL_NO_EXCEPT
Definition rgbw.h:279
const fl::ChipsetTimingConfig & timing
Chipset timing configuration to test.
fl::RxDeviceType rx_type
RX device type (RMT or ISR)
const char * timing_name
Timing name for logging (e.g., "WS2812B-V5")
const char * driver_name
Driver name for logging (e.g., "RMT", "SPI", "PARLIO")
int base_strip_size
Base strip size (10 or 300 LEDs)
bool legacy_rgbw
Enable RGBW on legacy controllers.
fl::span< fl::ChannelConfig > tx_configs
TX channel configurations to test (mutable for LED manipulation)
fl::ClocklessEncoder encoder
Encoder selector (peer of timing; see issue #2467)
fl::shared_ptr< fl::RxChannel > rx_channel
RX channel for loopback capture (created in .ino, passed in)
fl::span< uint8_t > rx_buffer
Buffer to store received bytes.
fl::span< const LegacyClocklessChipset > legacy_chipsets
Optional per-lane legacy templates.
Configuration for driver-agnostic autoresearch testing Contains all input parameters needed for autor...
@ Green
<div style='background:#008000;width:4em;height:4em;'></div>
Definition crgb.h:565
@ Red
<div style='background:#FF0000;width:4em;height:4em;'></div>
Definition crgb.h:629
@ Blue
<div style='background:#0000FF;width:4em;height:4em;'></div>
Definition crgb.h:519
@ White
<div style='background:#FFFFFF;width:4em;height:4em;'></div>
Definition crgb.h:653
@ Black
<div style='background:#000000;width:4em;height:4em;'></div>
Definition crgb.h:517
Configuration for a single LED channel.
Definition config.h:167
u32 t1_ns
T0H: High time for bit 0 (nanoseconds)
u32 t2_ns
T1H-T0H: Additional high time for bit 1 (nanoseconds)
u32 reset_us
Reset/latch time (microseconds)
const char * name
Human-readable chipset name.
u32 t3_ns
T0L: Low tail duration (nanoseconds)
Runtime bit-period timing for a clockless chipset.
Clockless chipset configuration (single data pin)
Definition config.h:33
size_t offset
Starting edge index.
Definition rx.h:57
size_t count
Number of edges to extract.
Definition rx.h:58
Edge range specification for getRawEdgeTimes() debugging.
Definition rx.h:56
LED error information for a single run.
bool print_per_led_errors
Print every LED error (default: false)
int max_errors_per_run
Max errors to store per run (default: 5)
bool print_all_runs
Print all run results (default: only errors)
int num_runs
Number of runs to execute.
Multi-run test configuration.
int captureWaitResult
RxWaitResult value, or -1 before wait.
int decodeOutputCapacity
Output span size passed to decode()
int rawEdgesAfterWait
Raw edges visible after RX wait/decode.
int decodeError
DecodeError value, or -1 when decode succeeded/not run.
fl::string rawEdgeSample
Compact first-edge sample for failures.
fl::string rxBeginError
Machine-readable reason when RX begin() fails.
int decodeBytes
Bytes reported directly by decode()
int rpPioGpioTransitions
CPU-observed RX-pin transitions during RP PIO TX.
int decodeOk
1 if RX decode returned success, 0 on error, -1 before decode
Single run result with error tracking.
UCS7604 current control structure with 4-bit fields for each channel.
Definition ucs7604.h:37
handler_fn handler
Definition handler.h:20
void * user_data
Definition handler.h:21
Configuration for ISR attachment.
Definition handler.h:19
bool is_valid() const FL_NO_EXCEPT
Definition handler.h:49
Opaque handle to an attached ISR.
Definition handler.h:36
Platform-neutral UART wave4/wave5 encoder API.