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