FastLED 3.9.15
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#include "AutoResearchTest.h"
6#include <FastLED.h>
7#include "fl/stl/sstream.h"
11#include "fl/channels/wave3.h"
13#include "fl/math/ease.h"
14#include "pixel_controller.h"
15#include "color.h"
16#include "fl/stl/vector.h"
17#include "fl/stl/iterator.h"
18
19// Phase 0: Include PARLIO debug instrumentation
20#if defined(ESP32) && FASTLED_ESP32_HAS_PARLIO
21#include "platforms/esp/32/drivers/parlio/channel_driver_parlio.h"
22#endif
23
29 const fl::ChipsetTimingConfig& timing,
30 fl::EdgeRange range) {
31 if (!rx_channel) {
32 FL_WARN("[RAW EDGE TIMING] ERROR: RX channel is null");
33 return;
34 }
35
36 // Allocate edge buffer sized to requested count (max 256 to avoid stack overflow)
38 size_t buffer_size = range.count < 256 ? range.count : 256;
39 edges.resize(buffer_size); // Default initializes to EdgeTime()
40
41 // Get edges starting at offset
42 size_t edge_count = rx_channel->getRawEdgeTimes(edges, range.offset);
43
44 if (edge_count == 0) {
45 FL_WARN("[RAW EDGE TIMING] WARNING: No edge data captured at offset " << range.offset);
46 return;
47 }
48
49 // Calculate actual range printed (start from offset)
50 size_t start_idx = range.offset;
51 size_t end_idx = range.offset + edge_count;
52
53 FL_WARN("[RAW EDGES " << start_idx << ".." << (end_idx - 1) << "]");
54
55 // Display edges (edges buffer contains data starting from offset)
56 for (size_t i = 0; i < edge_count; i++) {
57 const char* level = edges[i].high ? "H" : "L";
58 size_t absolute_index = start_idx + i;
59 FL_WARN(" [" << absolute_index << "] " << level << " " << edges[i].ns);
60 }
61
62 // Pattern analysis (only if showing edges from start)
63 if (range.offset == 0 && edge_count >= 16) {
64 // Calculate expected timings from config (3-phase to 4-phase conversion)
65 uint32_t expected_bit0_high = timing.t1_ns;
66 uint32_t expected_bit0_low = timing.t2_ns + timing.t3_ns;
67 uint32_t expected_bit1_high = timing.t1_ns + timing.t2_ns;
68 uint32_t expected_bit1_low = timing.t3_ns;
69
70 const uint32_t tolerance = 150;
71
72 bool has_short_high = false, has_long_high = false;
73 bool has_short_low = false, has_long_low = false;
74
75 for (size_t i = 0; i < edge_count; i++) {
76 uint32_t ns = edges[i].ns;
77 if (edges[i].high) {
78 if (ns >= expected_bit0_high - tolerance && ns <= expected_bit0_high + tolerance)
79 has_short_high = true;
80 if (ns >= expected_bit1_high - tolerance && ns <= expected_bit1_high + tolerance)
81 has_long_high = true;
82 } else {
83 if (ns >= expected_bit1_low - tolerance && ns <= expected_bit1_low + tolerance)
84 has_short_low = true;
85 if (ns >= expected_bit0_low - tolerance && ns <= expected_bit0_low + tolerance)
86 has_long_low = true;
87 }
88 }
89
90 fl::sstream ss;
91 ss << "\n[RAW EDGE TIMING] Pattern Analysis:\n";
92 ss << " Short HIGH (~" << expected_bit0_high << "ns, Bit 0): " << (has_short_high ? "FOUND ✓" : "MISSING ✗") << "\n";
93 ss << " Long HIGH (~" << expected_bit1_high << "ns, Bit 1): " << (has_long_high ? "FOUND ✓" : "MISSING ✗") << "\n";
94 ss << " Short LOW (~" << expected_bit1_low << "ns, Bit 1): " << (has_short_low ? "FOUND ✓" : "MISSING ✗") << "\n";
95 ss << " Long LOW (~" << expected_bit0_low << "ns, Bit 0): " << (has_long_low ? "FOUND ✓" : "MISSING ✗");
96 FL_WARN(ss.str());
97
98 if (has_short_high && has_long_high && has_short_low && has_long_low) {
99 FL_WARN("\n[RAW EDGE TIMING] ✓ Encoder appears to be working correctly (varied timing patterns)");
100 } else if (!has_short_high && !has_long_high) {
101 ss.clear();
102 ss << "[RAW EDGE TIMING] ✗ ENCODER BROKEN: No valid HIGH pulses detected!\n";
103 ss << "[RAW EDGE TIMING] Possible causes:\n";
104 ss << "[RAW EDGE TIMING] 1. Encoder not reading pixel buffer data\n";
105 ss << "[RAW EDGE TIMING] 2. Bytes encoder state machine stuck\n";
106 ss << "[RAW EDGE TIMING] 3. Data pointer not passed correctly to encoder";
107 FL_ERROR(ss.str());
108 } else if (!has_short_low && !has_long_low) {
109 // Use FL_WARN to avoid triggering bash autoresearch early exit
110 FL_WARN("[RAW EDGE TIMING] ✗ ENCODER BROKEN: No valid LOW pulses detected!");
111 } else {
112 FL_WARN("[RAW EDGE TIMING] ⚠ Partial pattern match - encoder may have issues");
113 }
114 }
115 FL_WARN("");
116}
117
124
130 fl::vector<uint8_t> expected;
131
132 // Map encoder to UCS7604Mode
133 fl::UCS7604Mode mode;
134 switch (encoder) {
137 break;
140 break;
143 break;
144 default:
145 return expected;
146 }
147
148 // Default current control (0x0F for all channels) matching channel.cpp.hpp defaults
149 fl::UCS7604CurrentControl current; // defaults to 0xF for all channels
150
151 // Create PixelIterator from LED data (RGB order, no color adjustment, no dithering)
153 auto pixel_iter = pc.as_iterator(RgbwInvalid());
154
155 // For 16-bit modes, use default gamma 2.8
157 bool use_gamma = (mode != fl::UCS7604Mode::UCS7604_MODE_8BIT_800KHZ);
158 if (use_gamma) {
159 gamma = fl::Gamma8::getOrCreate(2.8f);
160 }
161
162 // Encode using the same function the driver uses
163 fl::encodeUCS7604(pixel_iter, leds.size(), fl::back_inserter(expected),
164 mode, current, false /* is_rgbw */, gamma.get());
165
166 return expected;
167}
168
169// Decode SPI bit stream from raw RMT edges into LED RGB bytes
170// The SPI data pin carries APA102 encoded data clocked by PCLK.
171// RMT RX captures edges on the data pin. Each edge duration / bit_period_ns
172// gives the number of consecutive same-value bits.
173// Returns number of LED RGB bytes written to rx_buffer, or 0 on error.
175 fl::span<uint8_t> rx_buffer,
176 uint32_t clock_hz) {
177 if (!rx_channel || clock_hz == 0) {
178 FL_WARN("[SPI DECODE] Invalid parameters");
179 return 0;
180 }
181
182 const uint32_t bit_period_ns = static_cast<uint32_t>(1000000000ULL / clock_hz);
183 const uint32_t half_bit_ns = bit_period_ns / 2;
184 FL_WARN("[SPI DECODE] clock=" << clock_hz << " Hz, bit_period=" << bit_period_ns << " ns");
185
186 // Read raw edges (up to 4096 to handle large strips)
187 constexpr size_t MAX_EDGES = 4096;
188 fl::vector<fl::EdgeTime> edges(MAX_EDGES);
189 fl::span<fl::EdgeTime> edge_span(edges.data(), edges.size());
190 size_t edge_count = rx_channel->getRawEdgeTimes(edge_span, 0);
191
192 if (edge_count == 0) {
193 FL_WARN("[SPI DECODE] No edges captured");
194 return 0;
195 }
196 FL_WARN("[SPI DECODE] Captured " << edge_count << " edges");
197
198 // Reconstruct bit stream from edges
199 // Each edge has a level (high/low) and duration in ns.
200 // Number of bits = round(duration_ns / bit_period_ns)
202 bits.reserve(edge_count * 4); // Rough estimate
203
204 for (size_t i = 0; i < edge_count; i++) {
205 uint32_t duration = edges[i].ns;
206 uint32_t num_bits = (duration + half_bit_ns) / bit_period_ns;
207 if (num_bits == 0) num_bits = 1; // At least 1 bit per edge
208 uint8_t bit_val = edges[i].high ? 1 : 0;
209 for (uint32_t b = 0; b < num_bits; b++) {
210 bits.push_back(bit_val);
211 }
212 }
213
214 FL_WARN("[SPI DECODE] Reconstructed " << bits.size() << " bits");
215
216 if (bits.size() < 32) {
217 FL_WARN("[SPI DECODE] Too few bits for APA102 frame");
218 return 0;
219 }
220
221 // Convert bits to bytes (MSB first)
222 size_t total_bytes = bits.size() / 8;
223 fl::vector<uint8_t> raw_bytes(total_bytes);
224 for (size_t i = 0; i < total_bytes; i++) {
225 uint8_t byte_val = 0;
226 for (int bit = 7; bit >= 0; bit--) {
227 size_t bit_idx = i * 8 + (7 - bit);
228 if (bit_idx < bits.size() && bits[bit_idx]) {
229 byte_val |= (1 << bit);
230 }
231 }
232 raw_bytes[i] = byte_val;
233 }
234
235 FL_WARN("[SPI DECODE] Decoded " << total_bytes << " raw bytes");
236
237 // Log first few bytes for debugging
238 {
239 fl::sstream dbg;
240 dbg << "[SPI DECODE] First bytes:";
241 size_t show = total_bytes < 20 ? total_bytes : 20;
242 for (size_t i = 0; i < show; i++) {
243 dbg << " 0x";
244 uint8_t hi = raw_bytes[i] >> 4;
245 uint8_t lo = raw_bytes[i] & 0xF;
246 dbg << (char)(hi < 10 ? '0' + hi : 'A' + hi - 10);
247 dbg << (char)(lo < 10 ? '0' + lo : 'A' + lo - 10);
248 }
249 FL_WARN(dbg.str());
250 }
251
252 // Find APA102 start frame (4 bytes of 0x00)
253 // The RMT may not capture the start frame since it's all zeros and
254 // the idle state is also LOW. In that case, the first captured data
255 // is the first LED frame header (0xE0 | brightness).
256 size_t data_start = 0;
257
258 // Check if we captured the start frame
259 if (total_bytes >= 4 && raw_bytes[0] == 0x00 && raw_bytes[1] == 0x00 &&
260 raw_bytes[2] == 0x00 && raw_bytes[3] == 0x00) {
261 data_start = 4; // Skip start frame
262 FL_WARN("[SPI DECODE] Found start frame at offset 0");
263 } else {
264 // No start frame captured (RMT started at first edge = first HIGH bit)
265 // First byte should be 0xE0|brightness or 0xFF
266 FL_WARN("[SPI DECODE] No start frame (first edge = first LED data)");
267 }
268
269 // Extract LED RGB data from APA102 frames
270 // Each LED: [0xE0|brightness] [color0] [color1] [color2] (4 bytes)
271 // With EOrder=RGB in the validation config, the wire order after the
272 // brightness header is [R, G, B] — we copy these directly to rx_buffer.
273 // NOTE: The APA102 end frame (all 0xFF) also has (0xFF & 0xE0) == 0xE0,
274 // so we cannot distinguish end frame from a max-brightness LED purely
275 // from the header byte. We stop after we've exhausted captured data.
276 size_t led_bytes_written = 0;
277 size_t pos = data_start;
278
279 while (pos + 4 <= total_bytes) {
280 uint8_t header = raw_bytes[pos];
281
282 // Check for valid LED frame header (top 3 bits must be 111)
283 if ((header & 0xE0) != 0xE0) {
284 FL_WARN("[SPI DECODE] End of LED data at byte " << pos
285 << " (header=0x" << ((header >> 4) < 10 ? '0' + (header >> 4) : 'A' + (header >> 4) - 10)
286 << ((header & 0xF) < 10 ? '0' + (header & 0xF) : 'A' + (header & 0xF) - 10) << ")");
287 break;
288 }
289
290 uint8_t c0 = raw_bytes[pos + 1];
291 uint8_t c1 = raw_bytes[pos + 2];
292 uint8_t c2 = raw_bytes[pos + 3];
293
294 if (led_bytes_written + 3 <= rx_buffer.size()) {
295 rx_buffer[led_bytes_written + 0] = c0;
296 rx_buffer[led_bytes_written + 1] = c1;
297 rx_buffer[led_bytes_written + 2] = c2;
298 led_bytes_written += 3;
299 } else {
300 FL_WARN("[SPI DECODE] rx_buffer full at LED " << (led_bytes_written / 3));
301 break;
302 }
303 pos += 4;
304 }
305
306 size_t num_leds = led_bytes_written / 3;
307 FL_WARN("[SPI DECODE] Extracted " << num_leds << " LEDs (" << led_bytes_written << " RGB bytes)");
308 return led_bytes_written;
309}
310
311// Capture transmitted LED data via RX loopback
312// - rx_channel: Shared pointer to RX device (persistent across calls)
313// - rx_buffer: Buffer to store received bytes
314// - timing: Chipset timing configuration for RX decoder
315// - driver_name: Name of the TX driver being tested (e.g., "RMT", "PARLIO") - enables io_loop_back only for RMT
316// Returns number of bytes captured, or 0 on error
317size_t capture(fl::shared_ptr<fl::RxChannel> rx_channel, fl::span<uint8_t> rx_buffer, const fl::ChipsetTimingConfig& timing, const char* driver_name) {
318 if (!rx_channel) {
319 FL_ERROR("RX channel is null");
320 return 0;
321 }
322
323 // Clear RX buffer
324 fl::memset(rx_buffer.data(), 0, rx_buffer.size());
325
326 // Prepare RX config (but don't arm yet to avoid locking TX resources)
327 fl::RxChannelConfig rx_config(rx_channel->getPin());
328 rx_config.hz = 40000000; // 40MHz for high-precision LED timing capture
329
330 // Buffer size: 1 LED byte = 8 bits = 8 RMT symbols
331 // UART with TX inversion produces standard WS2812 waveform, same symbol count
332 bool is_uart_driver = (fl::strcmp(driver_name, "UART") == 0);
333 rx_config.edge_capacity = rx_buffer.size() * 8;
334
335 // Internal loopback configuration: Enable ONLY for RMT TX -> RMT RX scenarios
336 // When driver_name == "RMT", enable io_loop_back to route RMT TX output to RMT RX internally
337 // This is REQUIRED for ESP32-S3 because TX GPIO output stops when RX is active on different GPIO
338 // For other drivers (PARLIO, SPI, UART, I2S), disable io_loop_back (use external GPIO wire)
339 bool is_rmt_driver = (fl::strcmp(driver_name, "RMT") == 0);
340 rx_config.io_loop_back = is_rmt_driver;
341 // RX DMA streaming: extends capture past the non-DMA cap by sizing
342 // mem_block_symbols = 14336 so ESP-IDF allocates ~14 DMA descriptor
343 // nodes (each 4092 bytes), yielding a 57 KB user-buffer cap — enough to
344 // capture ~583 WS2812B LEDs in a single rmt_receive() call. Safe for
345 // non-RMT TX paths (SPI, PARLIO, I2S, UART, LCD_*) that don't contend
346 // for the shared ESP32-S3 DMA slot. RMT loopback stays non-DMA.
347 // See issue #2254.
348 rx_config.use_dma = !is_rmt_driver;
349 if (is_rmt_driver) {
350 FL_WARN("[CAPTURE] RMT TX -> RMT RX: Internal loopback enabled (io_loop_back=true)");
351 } else {
352 // The RX peripheral is platform-dependent (RMT on ESP32, FlexPWM on
353 // Teensy 4, LPC_SCT on LPC845, …). Don't claim "RMT RX" on platforms
354 // that have no RMT — that label burned an hour of Teensy-4 debug
355 // (FastLED#3059). Just say "external RX" so the label stays correct
356 // regardless of backend.
357 FL_WARN("[CAPTURE] " << driver_name << " TX -> external RX: External GPIO wire (io_loop_back=false, use_dma=true)");
358 }
359
360 // Driver-aware capture strategy:
361 // - RMT: Two-TX approach (ESP32-S3 workaround - TX GPIO blocked when RX active)
362 // - PARLIO/SPI/other: Single-TX approach (arm RX first, then TX)
363
364 // TX wait timeout: 1 second max per frame - prevents infinite hang if driver stalls
365 // Even a 10000-LED strip at 800kbps takes only ~300ms, so 1s is very safe
366 const uint32_t TX_WAIT_TIMEOUT_MS = 1000;
367
368 if (is_rmt_driver) {
369 // RMT: Two-TX approach for ESP32-S3 compatibility
370 // First TX without RX armed (diagnostics), then arm RX, then second TX
371 FL_WARN("[CAPTURE] RMT: Two-TX approach (ESP32-S3 workaround)");
372 FastLED.show();
373 if (!FastLED.wait(TX_WAIT_TIMEOUT_MS)) {
374 FL_ERROR("[CAPTURE] TX wait timeout (pre-arm) - driver may be stalled");
375 return 0;
376 }
377
378 // Arm RX for capture
379 if (!rx_channel->begin(rx_config)) {
380 FL_ERROR("Failed to arm RX receiver");
381 return 0;
382 }
383 // Second TX with RX armed
384 FastLED.show();
385 if (!FastLED.wait(TX_WAIT_TIMEOUT_MS)) {
386 FL_ERROR("[CAPTURE] TX wait timeout (capture) - driver may be stalled");
387 return 0;
388 }
389 } else {
390 // Non-RMT (PARLIO, SPI, etc.): Single-TX approach
391 if (!rx_channel->begin(rx_config)) {
392 FL_WARN("[CAPTURE] RX begin() failed for pin " << rx_channel->getPin());
393 return 0;
394 }
395 FL_WARN("[CAPTURE] RX armed, calling FastLED.show()...");
396
397 FastLED.show();
398 FL_WARN("[CAPTURE] FastLED.show() returned, calling wait...");
399 if (!FastLED.wait(TX_WAIT_TIMEOUT_MS)) {
400 FL_WARN("[CAPTURE] FastLED.wait() timed out");
401 return 0;
402 }
403 FL_WARN("[CAPTURE] FastLED.wait() done");
404 }
405
406
407
408
409 // Wait for RX completion
410 // WS2812B: ~30μs per LED → 3000 LEDs = 90ms, use 150ms
411 // UART with inverted TX produces same waveform timing as WS2812
412 const uint32_t rx_wait_ms = is_uart_driver ? 500 : 150;
413 FL_WARN("[CAPTURE] Waiting for RX completion (" << rx_wait_ms << "ms timeout)...");
414 auto wait_result = rx_channel->wait(rx_wait_ms);
415 FL_WARN("[CAPTURE] RX wait returned: " << static_cast<int>(wait_result));
416
417 if (wait_result != fl::RxWaitResult::SUCCESS) {
418 FL_WARN("RX wait failed (timeout or no data received)");
419 fl::sstream ss;
420 ss << "\n⚠️ TROUBLESHOOTING:\n";
421 ss << " 1. Connect physical jumper wire from TX GPIO to RX GPIO " << rx_channel->getPin() << "\n";
422 ss << " 2. Check that both TX and RX pins are correctly configured\n";
423 ss << " 3. Verify the GPIO connection is working (GPIO baseline test should pass)\n";
424 ss << " 4. For RMT TX → RMT RX: Ensure io_loop_back=true in RxConfig";
425 FL_WARN(ss.str());
426 if (!is_uart_driver) {
427 return 0;
428 }
429 // UART: still attempt decode with captured edges (timeout may be expected)
430 FL_WARN("[CAPTURE] UART: attempting decode with captured edges despite timeout");
431 }
432
433 // UART with TX inversion at 4 Mbps produces a standard WS2812-compatible
434 // waveform on the wire. Each 10-bit UART frame (2500ns) encodes exactly 2
435 // LED bits with correct WS2812 timing:
436 // T0H=250ns, T0L=1000ns (LED bit "0")
437 // T1H=750ns, T1L=500ns (LED bit "1")
438 // Use the standard WS2812 decoder with UART-specific timing thresholds.
439 if (is_uart_driver) {
440 FL_WARN("[CAPTURE] UART (inverted TX): using standard WS2812 decoder with UART timing...");
441 // UART timing at 4 Mbps: T0H=250ns, T1H-T0H=500ns, T0L=1000ns
442 fl::ChipsetTiming uart_timing{
443 250, // T1 = T0H (1 UART bit = 250ns)
444 500, // T2 = T1H - T0H (3 bits - 1 bit = 2 bits = 500ns)
445 500, // T3 = T1L (2 UART bits = 500ns, stop + next start)
446 50, // reset_us (WS2812 minimum)
447 "UART_4Mbps"
448 };
449 // Use wider tolerance (250ns) for UART because the UART clock and RMT
450 // sample clock are asynchronous, and GPIO matrix adds ~10-20ns jitter
451 auto rx_timing = fl::make4PhaseTiming(uart_timing, 250);
452 rx_timing.gap_tolerance_ns = 100000; // 100µs for UART inter-frame gaps
453
454 FL_WARN("[CAPTURE] UART RX timing: T0H=" << uart_timing.T1
455 << " T1H=" << (uart_timing.T1 + uart_timing.T2)
456 << " T0L=" << (uart_timing.T2 + uart_timing.T3)
457 << " T1L=" << uart_timing.T3);
458
459 auto decode_result = rx_channel->decode(rx_timing, rx_buffer);
460 if (!decode_result.ok()) {
461 FL_WARN("[CAPTURE] UART decode failed (error: " << static_cast<int>(decode_result.error()) << ")");
462 dumpRawEdgeTiming(rx_channel, timing, fl::EdgeRange(0, 256));
463 return 0;
464 }
465 FL_WARN("[CAPTURE] UART decoded " << decode_result.value() << " LED bytes");
466 return decode_result.value();
467 }
468
469 // SPI chipset drivers (LCD_SPI, I2S_SPI): decode raw SPI bit stream
470 // These drivers use LCD_CAM I80 bus or I2S to output APA102 data.
471 // RMT RX captures edges on the data pin; clock pin is ignored.
472 bool is_lcd_spi_driver = (fl::strcmp(driver_name, "LCD_SPI") == 0);
473 bool is_i2s_spi_driver = (fl::strcmp(driver_name, "I2S_SPI") == 0);
474 if (is_lcd_spi_driver || is_i2s_spi_driver) {
475 // SPI clock used for validation: 2.4MHz (matches ValidationRemote.cpp)
476 const uint32_t spi_clock_hz = 2400000;
477 FL_WARN("[CAPTURE] SPI chipset decode: clock=" << spi_clock_hz << " Hz");
478 size_t decoded = decodeSpiEdges(rx_channel, rx_buffer, spi_clock_hz);
479 if (decoded == 0) {
480 FL_WARN("[CAPTURE] SPI decode failed");
481 dumpRawEdgeTiming(rx_channel, timing, fl::EdgeRange(0, 256));
482 }
483 return decoded;
484 }
485
486 // Decode received data directly into rx_buffer
487 // Create 4-phase RX timing from the TX timing
488 //
489 // Wave8 drivers (SPI, PARLIO, I2S) use 8-bit expansion encoding:
490 // Bit 0: round(T1/(T1+T2+T3)*8) HIGH pulses, rest LOW
491 // Bit 1: round((T1+T2)/(T1+T2+T3)*8) HIGH pulses, rest LOW
492 // The actual pulse widths are quantized to tick boundaries, so we must
493 // compute the exact wave8 timing for RX decode thresholds.
494 // Without this correction, the RX decoder may reject valid waveforms when
495 // the nominal timing period differs from the quantized 8-tick period.
496 // Example: WS2812_800KHZ has T0L=1000ns nominal but PARLIO wave8 produces 750ns.
497 //
498 // Clock sources differ by driver:
499 // SPI/I2S: clock = 8/(T1+T2+T3) Hz, tick = (T1+T2+T3)/8 ns (variable)
500 // PARLIO: clock = 8 MHz fixed, tick = 125 ns (fixed)
501 bool is_spi_driver = (fl::strcmp(driver_name, "SPI") == 0);
502 bool is_parlio_driver = (fl::strcmp(driver_name, "PARLIO") == 0);
503 bool is_i2s_driver = (fl::strcmp(driver_name, "I2S") == 0);
504 // LCD_CLOCKLESS auto-selects wave3 vs wave8 based on canUseWave3() of the
505 // chipset timing. Compute the same eligibility check on the host side so
506 // the RX decoder reconstructs the correct quantized waveform.
507 bool is_lcd_clockless_driver = (fl::strcmp(driver_name, "LCD_CLOCKLESS") == 0);
508 bool lcd_clockless_uses_wave3 = false;
509 if (is_lcd_clockless_driver) {
510 fl::ChipsetTiming probe{timing.t1_ns, timing.t2_ns, timing.t3_ns, timing.reset_us, timing.name};
511 lcd_clockless_uses_wave3 = fl::canUseWave3(probe);
512 }
513 bool uses_wave8 = is_spi_driver || is_parlio_driver || is_i2s_driver
514 || (is_lcd_clockless_driver && !lcd_clockless_uses_wave3);
515 bool uses_wave3 = is_lcd_clockless_driver && lcd_clockless_uses_wave3;
516 fl::ChipsetTiming tx_timing;
517 if (uses_wave8) {
518 // Compute actual wave8 timing from chipset timing
519 const uint32_t period = timing.t1_ns + timing.t2_ns + timing.t3_ns;
520 // PARLIO uses fixed 8MHz clock (125ns/tick), not derived from period
521 // SPI/I2S/LCD_CLOCKLESS derive clock from period: tick = period/8
522 const uint32_t tick_ns = is_parlio_driver ? 125 : (period / 8);
523 // Wave8 LUT computes: pulses = round(fraction * 8)
524 const uint32_t pulses_bit0 = static_cast<uint32_t>(
525 static_cast<float>(timing.t1_ns) / period * 8.0f + 0.5f);
526 const uint32_t pulses_bit1 = static_cast<uint32_t>(
527 static_cast<float>(timing.t1_ns + timing.t2_ns) / period * 8.0f + 0.5f);
528 // Convert back to 3-phase timing (T1=T0H, T2=T1H-T0H, T3=actual_period-T1H)
529 const uint32_t actual_t0h = pulses_bit0 * tick_ns;
530 const uint32_t actual_t1h = pulses_bit1 * tick_ns;
531 const uint32_t actual_period = 8 * tick_ns;
532 const char* wave8_name = is_spi_driver ? "SPI_wave8" :
533 is_parlio_driver ? "PARLIO_wave8" :
534 is_lcd_clockless_driver ? "LCD_CLOCKLESS_wave8" : "I2S_wave8";
535 tx_timing = fl::ChipsetTiming{
536 actual_t0h, // T1 = T0H
537 actual_t1h - actual_t0h, // T2 = T1H - T0H
538 actual_period - actual_t1h, // T3 = actual_period - T1H
539 timing.reset_us,
540 wave8_name
541 };
542 FL_WARN("[RX TIMING] " << wave8_name << ": pulses_bit0=" << pulses_bit0
543 << " pulses_bit1=" << pulses_bit1
544 << " tick_ns=" << tick_ns
545 << " -> T1=" << tx_timing.T1 << " T2=" << tx_timing.T2
546 << " T3=" << tx_timing.T3);
547 } else if (uses_wave3) {
548 // Wave3 encoding: 3 ticks per LED bit, clock = 3/(T1+T2+T3) Hz
549 const uint32_t period = timing.t1_ns + timing.t2_ns + timing.t3_ns;
550 const uint32_t tick_ns = period / 3;
551 const uint32_t ticks_bit0 = static_cast<uint32_t>(
552 static_cast<float>(timing.t1_ns) / period * 3.0f + 0.5f);
553 const uint32_t ticks_bit1 = static_cast<uint32_t>(
554 static_cast<float>(timing.t1_ns + timing.t2_ns) / period * 3.0f + 0.5f);
555 const uint32_t actual_t0h = ticks_bit0 * tick_ns;
556 const uint32_t actual_t1h = ticks_bit1 * tick_ns;
557 const uint32_t actual_period = 3 * tick_ns;
558 tx_timing = fl::ChipsetTiming{
559 actual_t0h,
560 actual_t1h - actual_t0h,
561 actual_period - actual_t1h,
562 timing.reset_us,
563 "LCD_CLOCKLESS_wave3"
564 };
565 FL_WARN("[RX TIMING] LCD_CLOCKLESS_wave3: ticks_bit0=" << ticks_bit0
566 << " ticks_bit1=" << ticks_bit1
567 << " tick_ns=" << tick_ns
568 << " -> T1=" << tx_timing.T1 << " T2=" << tx_timing.T2
569 << " T3=" << tx_timing.T3);
570 } else {
571 tx_timing = fl::ChipsetTiming{timing.t1_ns, timing.t2_ns, timing.t3_ns, timing.reset_us, timing.name};
572 }
573 // Wave8/wave3 encoding has timing jitter due to clock quantization and GPIO matrix latency
574 // Use wider tolerance (200ns) to accommodate clock rounding
575 const uint32_t tolerance = (uses_wave8 || uses_wave3) ? 200 : 170;
576 auto rx_timing = fl::make4PhaseTiming(tx_timing, tolerance);
577
578 // Enable gap tolerance for PARLIO/SPI DMA gaps
579 // PARLIO: ~20µs typical gaps during buffer transitions
580 // SPI: Can have longer inter-frame gaps due to software encoding timing
581 // Increased to 100µs to accommodate SPI driver timing variations
582 rx_timing.gap_tolerance_ns = 100000; // 100µs (was 30µs)
583
584 FL_WARN("[CAPTURE] Decoding...");
585 auto decode_result = rx_channel->decode(rx_timing, rx_buffer);
586
587 if (!decode_result.ok()) {
588 // Use FL_WARN instead of FL_ERROR to avoid triggering bash autoresearch exit
589 // This can happen during warmup/setup and is not fatal
590 FL_WARN("Decode failed (error code: " << static_cast<int>(decode_result.error()) << ")");
591 // Print raw edge timing on decode failure to diagnose the issue
592 dumpRawEdgeTiming(rx_channel, timing, fl::EdgeRange(0, 256));
593 return 0;
594 }
595
596 FL_WARN("[CAPTURE] Decoded " << decode_result.value() << " bytes");
597 return decode_result.value();
598}
599
600// Generic driver-agnostic autoresearch test runner
601// Tests all channels using the specified configuration
602void runTest(const char* test_name,
604 int& total, int& passed) {
605 // Multi-lane limitation: Only test Lane 0 (first channel)
606 // Hardware constraint: Only one TX channel can be read from via RX loopback
607 size_t channels_to_test = config.tx_configs.size() > 1 ? 1 : config.tx_configs.size();
608
609 if (config.tx_configs.size() > 1) {
610 FL_WARN("\n[MULTI-LANE] Testing " << config.tx_configs.size() << " lanes, testing Lane 0 only (hardware limitation)");
611 }
612
613 // Test enabled configs (Lane 0 only for multi-lane)
614 for (size_t config_idx = 0; config_idx < channels_to_test; config_idx++) {
615 total++;
616
617 // Build test context for detailed error reporting
618 const auto& leds = config.tx_configs[config_idx].mLeds;
619 size_t num_leds = leds.size();
620
621 fl::TestContext ctx{
622 config.driver_name,
623 config.timing_name,
624 fl::toString(config.rx_type),
625 test_name,
626 static_cast<int>(config.tx_configs.size()),
627 static_cast<int>(config_idx),
628 config.base_strip_size,
629 static_cast<int>(num_leds),
630 config.tx_configs[config_idx].getDataPin()
631 };
632
633 FL_WARN("\n=== " << test_name << " [Lane " << config_idx << "/" << config.tx_configs.size()
634 << ", Pin " << config.tx_configs[config_idx].getDataPin()
635 << ", LEDs " << config.tx_configs[config_idx].mLeds.size() << "] ===");
636
637 // Use RX channel provided via config (created in .ino file, never created dynamically here)
638 if (!config.rx_channel) {
639 FL_ERROR("[" << ctx.driver_name << "/" << ctx.timing_name << "/" << ctx.pattern_name
640 << " | Lane " << ctx.lane_index << "/" << ctx.lane_count
641 << " (Pin " << ctx.pin_number << ", " << ctx.num_leds << " LEDs) | RX:" << ctx.rx_type_name << "] "
642 << "RX channel is null - must be created in .ino and passed via AutoResearchConfig");
643 FL_ERROR("Result: FAIL ✗ (RX channel not provided)");
644 continue;
645 }
646
647 size_t bytes_captured = capture(config.rx_channel, config.rx_buffer, config.timing, config.driver_name);
648
649 if (bytes_captured == 0) {
650 FL_ERROR("[" << ctx.driver_name << "/" << ctx.timing_name << "/" << ctx.pattern_name
651 << " | Lane " << ctx.lane_index << "/" << ctx.lane_count
652 << " (Pin " << ctx.pin_number << ", " << ctx.num_leds << " LEDs) | RX:" << ctx.rx_type_name << "] "
653 << "Result: FAIL ✗ (capture failed)");
654 continue;
655 }
656
657 int mismatches = 0;
658
659 if (isUCS7604(config.encoder)) {
660 // UCS7604: Compare full encoded frame (preamble + padding + pixel data) byte-for-byte
661 fl::vector<uint8_t> expected_encoded = buildExpectedUCS7604(
662 config.tx_configs[config_idx].mLeds, config.encoder);
663 size_t expected_len = expected_encoded.size();
664
665 FL_WARN("UCS7604 encoded comparison: expected " << expected_len << " bytes, captured " << bytes_captured);
666
667 size_t compare_len = (bytes_captured < expected_len) ? bytes_captured : expected_len;
668 for (size_t i = 0; i < compare_len; i++) {
669 if (expected_encoded[i] != config.rx_buffer[i]) {
670 mismatches++;
671 }
672 }
673 // Count any missing bytes as mismatches
674 if (bytes_captured < expected_len) {
675 mismatches += static_cast<int>(expected_len - bytes_captured);
676 }
677
678 FL_WARN("Bytes Captured: " << bytes_captured << " (expected: " << expected_len << ")");
679 int total_bytes = static_cast<int>(expected_len);
680 FL_WARN("Accuracy: " << (100.0 * (total_bytes - mismatches) / total_bytes) << "% ("
681 << (total_bytes - mismatches) << "/" << total_bytes << " bytes match)");
682 } else {
683 // WS2812: Compare raw RGB per-LED
684 size_t bytes_expected = num_leds * 3;
685
686 // NEVER EVER CHANGE THIS!!!!!
687 const size_t front_padding_bytes = 0; // No front padding: PARLIO FRONT_PAD_BYTES=0
688 const size_t rx_buffer_offset = front_padding_bytes;
689
690 if (bytes_captured > bytes_expected + front_padding_bytes) {
691 FL_WARN("Info: Captured " << bytes_captured << " bytes ("
692 << front_padding_bytes << " front pad + "
693 << bytes_expected << " LED data + "
694 << (bytes_captured - bytes_expected - front_padding_bytes) << " back pad/RESET)");
695 }
696
697 // Compare: byte-level comparison (COLOR_ORDER is RGB, so no reordering)
698 size_t bytes_to_check = (bytes_captured < bytes_expected + rx_buffer_offset) ?
699 (bytes_captured > rx_buffer_offset ? bytes_captured - rx_buffer_offset : 0) :
700 bytes_expected;
701 (void)bytes_to_check;
702
703 for (size_t i = 0; i < num_leds; i++) {
704 size_t byte_offset = rx_buffer_offset + i * 3; // Skip front padding
705 if (byte_offset + 2 >= bytes_captured) { // Check against total captured bytes
706 FL_ERROR("[" << ctx.driver_name << "/" << ctx.timing_name << "/" << ctx.pattern_name
707 << " | Lane " << ctx.lane_index << "/" << ctx.lane_count
708 << " (Pin " << ctx.pin_number << ", " << ctx.num_leds << " LEDs) | RX:" << ctx.rx_type_name << "] "
709 << "Incomplete data for LED[" << static_cast<int>(i)
710 << "] (only " << bytes_captured << " bytes captured)");
711 break;
712 }
713
714 uint8_t expected_r = leds[i].r;
715 uint8_t expected_g = leds[i].g;
716 uint8_t expected_b = leds[i].b;
717
718 uint8_t actual_r = config.rx_buffer[byte_offset + 0];
719 uint8_t actual_g = config.rx_buffer[byte_offset + 1];
720 uint8_t actual_b = config.rx_buffer[byte_offset + 2];
721
722 if (expected_r != actual_r || expected_g != actual_g || expected_b != actual_b) {
723 mismatches++;
724 }
725 }
726
727 FL_WARN("Bytes Captured: " << bytes_captured << " (expected: " << bytes_expected << ")");
728 FL_WARN("Accuracy: " << (100.0 * (num_leds - mismatches) / num_leds) << "% ("
729 << (num_leds - mismatches) << "/" << num_leds << " LEDs match)");
730 }
731
732 if (mismatches == 0) {
733 FL_WARN("Result: PASS ✓");
734 passed++;
735 } else {
736 FL_ERROR("[" << ctx.driver_name << "/" << ctx.timing_name << "/" << ctx.pattern_name
737 << " | Lane " << ctx.lane_index << "/" << ctx.lane_count
738 << " (Pin " << ctx.pin_number << ", " << ctx.num_leds << " LEDs) | RX:" << ctx.rx_type_name << "] "
739 << "Result: FAIL ✗");
740 }
741 }
742}
743
744// Multi-run autoresearch test runner
745// Runs the same test multiple times and tracks errors across runs
746void runMultiTest(const char* test_name,
748 const fl::MultiRunConfig& multi_config,
749 int& total, int& passed,
750 fl::vector<fl::RunResult>* out_results) {
751
752 fl::sstream ss;
753 ss << "\n╔════════════════════════════════════════════════════════════════╗\n";
754 ss << "║ MULTI-RUN TEST: " << test_name << "\n";
755 ss << "║ Runs: " << multi_config.num_runs << " | Print Mode: "
756 << (multi_config.print_all_runs ? "All" : "Errors ONLY") << "\n";
757 ss << "╚════════════════════════════════════════════════════════════════╝";
758 FL_WARN(ss.str());
759
760 fl::vector<fl::RunResult> run_results;
761
762 // Multi-lane limitation: Only test Lane 0
763 size_t channels_to_test = config.tx_configs.size() > 1 ? 1 : config.tx_configs.size();
764
765 if (config.tx_configs.size() > 1) {
766 FL_WARN("[MULTI-LANE] Testing " << config.tx_configs.size() << " lanes, testing Lane 0 only");
767 }
768
769 // Execute multiple runs
770 for (int run = 1; run <= multi_config.num_runs; run++) {
771 // Print progress to keep output flowing (prevents auto-exit timeout)
772 if (run % 3 == 1 || multi_config.num_runs <= 5) {
773 FL_WARN("[Run " << run << "/" << multi_config.num_runs << "] Testing...");
774 }
775
776 fl::RunResult result;
777 result.run_number = run;
778
779 // Test Lane 0 only
780 for (size_t config_idx = 0; config_idx < channels_to_test; config_idx++) {
781 const auto& leds = config.tx_configs[config_idx].mLeds;
782 size_t num_leds = leds.size();
783 result.total_leds = num_leds;
784 result.totalBytes = num_leds * 3;
785
786 // Capture RX data
787 size_t bytes_captured = capture(config.rx_channel, config.rx_buffer, config.timing, config.driver_name);
788
789 if (bytes_captured == 0) {
790 FL_WARN("[Run " << run << "] Capture failed");
791 result.passed = false;
792 break;
793 }
794
795 // Check pixel data
796 int mismatches = 0;
797
798 // DEBUG: Print first 24 bytes of captured data
799 FL_WARN("[RUN " << run << "] Driver=" << config.driver_name << ", bytes_captured=" << bytes_captured);
800 FL_WARN("[RUN " << run << "] First 24 bytes:");
801 for (size_t i = 0; i < 24 && i < bytes_captured; i++) {
802 FL_WARN(" [" << i << "] = 0x" << fl::hex << static_cast<int>(config.rx_buffer[i]) << fl::dec);
803 }
804
805 if (isUCS7604(config.encoder)) {
806 // UCS7604: Compare full encoded frame byte-for-byte
807 fl::vector<uint8_t> expected_encoded = buildExpectedUCS7604(
808 config.tx_configs[config_idx].mLeds, config.encoder);
809 size_t expected_len = expected_encoded.size();
810 result.totalBytes = static_cast<int>(expected_len);
811
812 size_t compare_len = (bytes_captured < expected_len) ? bytes_captured : expected_len;
813 for (size_t i = 0; i < compare_len; i++) {
814 if (expected_encoded[i] != config.rx_buffer[i]) {
815 result.mismatchedBytes++;
816 if ((expected_encoded[i] ^ config.rx_buffer[i]) == 0x01) {
817 result.lsbOnlyErrors++;
818 }
819 mismatches++;
820
821 // Print corruption context for first mismatch only
822 if (mismatches == 1) {
823 FL_WARN("\n[CORRUPTION @ byte " << static_cast<int>(i) << ", Run " << run
824 << "] expected=0x" << fl::hex << static_cast<int>(expected_encoded[i])
825 << " actual=0x" << static_cast<int>(config.rx_buffer[i]) << fl::dec);
826 }
827
828 // Store first N errors (using byte-level reporting)
829 if (result.errors.size() < static_cast<size_t>(multi_config.max_errors_per_run)) {
830 result.errors.push_back(fl::LEDError(
831 static_cast<int>(i), expected_encoded[i], 0, 0,
832 config.rx_buffer[i], 0, 0
833 ));
834 }
835 }
836 }
837 // Count missing bytes as mismatches
838 if (bytes_captured < expected_len) {
839 mismatches += static_cast<int>(expected_len - bytes_captured);
840 }
841 } else {
842 // WS2812: Per-LED RGB comparison
843 size_t bytes_expected = num_leds * 3;
844
845 // Determine front padding offset (PARLIO only)
846 const size_t rx_buffer_offset = 0; // No front padding: PARLIO FRONT_PAD_BYTES=0
847
848 size_t bytes_to_check = (bytes_captured < bytes_expected + rx_buffer_offset) ?
849 (bytes_captured > rx_buffer_offset ? bytes_captured - rx_buffer_offset : 0) :
850 bytes_expected;
851 (void)bytes_to_check;
852
853 size_t verified_leds = 0;
854 for (size_t i = 0; i < num_leds; i++) {
855 size_t byte_offset = rx_buffer_offset + i * 3; // Apply offset for PARLIO front padding
856 if (byte_offset + 2 >= bytes_captured) { // Check against total captured bytes
857 break;
858 }
859 verified_leds = i + 1;
860
861 uint8_t expected_r = leds[i].r;
862 uint8_t expected_g = leds[i].g;
863 uint8_t expected_b = leds[i].b;
864
865 uint8_t actual_r = config.rx_buffer[byte_offset + 0];
866 uint8_t actual_g = config.rx_buffer[byte_offset + 1];
867 uint8_t actual_b = config.rx_buffer[byte_offset + 2];
868
869 // Per-byte comparison for byte-level stats
870 uint8_t exp_bytes[3] = {expected_r, expected_g, expected_b};
871 uint8_t act_bytes[3] = {actual_r, actual_g, actual_b};
872 bool led_mismatch = false;
873 for (int ch = 0; ch < 3; ch++) {
874 if (exp_bytes[ch] != act_bytes[ch]) {
875 result.mismatchedBytes++;
876 if ((exp_bytes[ch] ^ act_bytes[ch]) == 0x01) {
877 result.lsbOnlyErrors++;
878 }
879 led_mismatch = true;
880 }
881 }
882
883 if (led_mismatch) {
884 // Print corruption context for first mismatch only
885 if (mismatches == 0) {
886 FL_WARN("\n[CORRUPTION @ LED " << static_cast<int>(i) << ", Run " << run << "]");
887
888 // Calculate edge index and print timing around corruption point
889 size_t corruption_edge_index = i * 48;
890 size_t offset = corruption_edge_index > 4 ? corruption_edge_index - 4 : 0;
891
892 // Dump raw edge timing around corruption point (9 edges: -4 to +4)
894 }
895
896 mismatches++;
897
898 // Store first N errors
899 if (result.errors.size() < static_cast<size_t>(multi_config.max_errors_per_run)) {
900 result.errors.push_back(fl::LEDError(
901 i, expected_r, expected_g, expected_b,
902 actual_r, actual_g, actual_b
903 ));
904 }
905 }
906 }
907
908 // Truncated capture: RX returned fewer bytes than expected. Count
909 // the unchecked LEDs as mismatches rather than silently passing.
910 // A short capture means we did NOT verify those LEDs, so the run
911 // MUST fail. The old silent break hid real bugs on strips longer
912 // than the RMT DMA buffer (~170 LEDs for WS2812B). See issue #2254.
913 if (verified_leds < num_leds) {
914 size_t unchecked = num_leds - verified_leds;
915 mismatches += static_cast<int>(unchecked);
916 result.mismatchedBytes += static_cast<int>(unchecked * 3);
917 FL_WARN("[TRUNCATED CAPTURE] Only verified " << verified_leds
918 << "/" << num_leds << " LEDs (" << bytes_captured
919 << " bytes captured, needed " << (num_leds * 3)
920 << "). Marking " << unchecked
921 << " unchecked LEDs as mismatches.");
922 }
923 }
924
925 result.mismatches = mismatches;
926 result.passed = (mismatches == 0);
927 }
928
929 run_results.push_back(result);
930
931 // Print run result if configured
932 if (multi_config.print_all_runs || !result.passed) {
933 FL_WARN("[Run " << run << "/" << multi_config.num_runs << "] "
934 << (result.passed ? "PASS" : "FAIL")
935 << " | Errors: " << result.mismatches << "/" << result.total_leds
936 << " (" << (100.0 * (result.total_leds - result.mismatches) / result.total_leds) << "%)");
937
938 // Print error details if enabled
939 if (!result.passed && multi_config.print_per_led_errors && !result.errors.empty()) {
940 FL_WARN(" First " << result.errors.size() << " error(s):");
941 for (size_t i = 0; i < result.errors.size(); i++) {
942 const auto& err = result.errors[i];
943 FL_WARN(" LED[" << err.led_index << "]: expected RGB("
944 << static_cast<int>(err.expected_r) << ","
945 << static_cast<int>(err.expected_g) << ","
946 << static_cast<int>(err.expected_b) << ") got RGB("
947 << static_cast<int>(err.actual_r) << ","
948 << static_cast<int>(err.actual_g) << ","
949 << static_cast<int>(err.actual_b) << ")");
950 }
951 }
952 }
953 }
954
955 // Summary statistics
956 int total_passed = 0;
957 int total_failed = 0;
958 for (const auto& r : run_results) {
959 if (r.passed) total_passed++;
960 else total_failed++;
961 }
962
963 ss.clear();
964 ss << "\n╔════════════════════════════════════════════════════════════════╗\n";
965 ss << "║ MULTI-RUN SUMMARY\n";
966 ss << "╚════════════════════════════════════════════════════════════════╝\n";
967 ss << "Total Runs: " << multi_config.num_runs << "\n";
968 ss << "Passed: " << total_passed << " (" << (100.0 * total_passed / multi_config.num_runs) << "%)\n";
969 ss << "Failed: " << total_failed << " (" << (100.0 * total_failed / multi_config.num_runs) << "%)";
970 FL_WARN(ss.str());
971
972 if (total_failed > 0) {
973 ss.clear();
974 ss << "\nFailed Run Numbers:\n";
975 for (const auto& r : run_results) {
976 if (!r.passed) {
977 ss << " Run #" << r.run_number << " - " << r.mismatches << " errors\n";
978 if (!r.errors.empty()) {
979 ss << " First error at LED[" << r.errors[0].led_index << "]: "
980 << "expected RGB(" << static_cast<int>(r.errors[0].expected_r) << ","
981 << static_cast<int>(r.errors[0].expected_g) << ","
982 << static_cast<int>(r.errors[0].expected_b) << ") got RGB("
983 << static_cast<int>(r.errors[0].actual_r) << ","
984 << static_cast<int>(r.errors[0].actual_g) << ","
985 << static_cast<int>(r.errors[0].actual_b) << ")\n";
986 }
987 }
988 }
989 FL_WARN(ss.str());
990 }
991
992 // Copy results to caller if requested
993 if (out_results) {
994 for (const auto& r : run_results) {
995 out_results->push_back(r);
996 }
997 }
998
999 // Update totals
1000 total++;
1001 if (total_failed == 0) {
1002 passed++;
1003 FL_WARN("\n[OVERALL] PASS ✓ - All " << multi_config.num_runs << " runs succeeded");
1004 } else {
1005 FL_WARN("\n[OVERALL] FAIL ✗ - " << total_failed << "/" << multi_config.num_runs << " runs failed");
1006 }
1007}
1008
1009// AutoResearch a specific chipset timing configuration
1010// Creates channels, runs tests, destroys channels
1012 int& driver_total, int& driver_passed,
1013 uint32_t& out_show_duration_ms,
1014 fl::vector<fl::RunResult>* out_results,
1015 int num_runs_per_pattern) {
1016 fl::sstream ss;
1017 ss << "\n========================================\n";
1018 ss << "Testing: " << config.timing_name << "\n";
1019 bool has_spi_config = config.tx_configs.size() > 0 && config.tx_configs[0].isSpi();
1020 if (has_spi_config) {
1021 const auto* spi_cfg = config.tx_configs[0].getSpiChipset();
1022 ss << " Protocol: SPI (APA102)\n";
1023 ss << " Clock: " << (spi_cfg ? spi_cfg->timing.clock_hz : 0) << " Hz\n";
1024 } else {
1025 ss << " T0H: " << config.timing.t1_ns << "ns\n";
1026 ss << " T1H: " << (config.timing.t1_ns + config.timing.t2_ns) << "ns\n";
1027 ss << " T0L: " << config.timing.t3_ns << "ns\n";
1028 ss << " RESET: " << config.timing.reset_us << "us\n";
1029 }
1030 ss << " Channels: " << config.tx_configs.size() << "\n";
1031 ss << "========================================";
1032 FL_WARN(ss.str());
1033
1034 // Create ALL channels from tx_configs (multi-channel support)
1036 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1038 if (config.tx_configs[i].isSpi()) {
1039 // SPI chipset: pass through the SPI config directly
1040 channel = FastLED.add(config.tx_configs[i]);
1041 } else {
1042 // Clockless chipset: re-create with runtime timing
1043 fl::ChannelConfig channel_config(config.tx_configs[i].getDataPin(), config.timing, config.tx_configs[i].mLeds, config.tx_configs[i].rgb_order);
1044 channel = FastLED.add(channel_config);
1045 }
1046 if (!channel) {
1047 FL_ERROR("Failed to create channel " << i << " (pin " << config.tx_configs[i].getDataPin() << ") - platform not supported");
1048 // Clean up previously created channels
1049 for (auto& ch : channels) {
1050 ch.reset();
1051 }
1052 return;
1053 }
1054 channels.push_back(channel);
1055 }
1056
1057 FastLED.setBrightness(255);
1058
1059 // Pre-initialize the TX engine to avoid first-call setup delays
1060 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1061 fill_solid(config.tx_configs[i].mLeds.data(), config.tx_configs[i].mLeds.size(), CRGB::Black);
1062 }
1063 FastLED.show();
1064 if (!FastLED.wait(1000)) { // 1s timeout - driver stall guard
1065 FL_ERROR("[PREINIT] TX wait timeout - driver may be stalled on this platform");
1067 return;
1068 }
1069 // TX engine pre-init logging silenced for speed
1070
1071 // CRITICAL: Wait for PARLIO streaming transmission to complete before starting tests
1072 // Without this delay, the RX will capture the pre-initialization BLACK frame instead of the test pattern
1073 // PARLIO is a streaming engine with ring buffers - need time to drain the initial frame
1074 delay(5); // Reduced from 100ms - just enough for buffer drain
1075
1076 // Run test patterns (mixed bit patterns to test MSB vs LSB handling)
1077 int total = 0;
1078 int passed = 0;
1079
1080 // Multi-run configuration - optimized for speed
1081 fl::MultiRunConfig multi_config;
1082 // num_runs=1: Python orchestrates retries (default).
1083 // num_runs>=2: Back-to-back captures of the same buffer to expose second-frame
1084 // degradation (e.g., SPI driver zeroing its DMA buffer after the first show()).
1085 multi_config.num_runs = (num_runs_per_pattern > 0) ? num_runs_per_pattern : 1;
1086 multi_config.print_all_runs = (multi_config.num_runs > 1);
1087 multi_config.print_per_led_errors = false; // Errors reported via JSON-RPC
1088 multi_config.max_errors_per_run = 10; // Store first 10 errors for JSON response
1089
1090 // Measure show-only duration (excludes setup/teardown overhead)
1091 uint32_t show_start_ms = millis();
1092
1093 // Test all 4 bit patterns (0-3)
1094 for (int pattern_id = 0; pattern_id < 4; pattern_id++) {
1095 // Apply pattern to all lanes
1096 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1098 config.tx_configs[i].mLeds.data(),
1099 config.tx_configs[i].mLeds.size(),
1100 pattern_id
1101 );
1102 }
1103 runMultiTest(getBitPatternName(pattern_id), config, multi_config, total, passed, out_results);
1104 }
1105
1106 out_show_duration_ms += millis() - show_start_ms;
1107
1108 // Results reported via JSON-RPC (verbose logging silenced for speed)
1109
1110 // Accumulate results for driver-level tracking
1111 driver_total += total;
1112 driver_passed += passed;
1113
1114 // Destroy ALL channels before testing next timing configuration
1115 // CRITICAL: Clear FastLED's global channel registry to prevent accumulation
1116 // If we only destroy local shared_ptrs, FastLED still holds references
1118 // Channel destruction is synchronous - no delay needed
1119}
1120
1121// AutoResearch using the legacy template addLeds API (supports multi-lane)
1122// Nearly identical to autoResearchChipsetTiming() — only channel creation differs:
1123// Normal: FastLED.add(channel_config) → Channel
1124// Legacy: LegacyClocklessProxy(pin, leds, numLeds) → WS2812B<PIN> → ClocklessIdf5 → Channel
1126 int& driver_total, int& driver_passed,
1127 uint32_t& out_show_duration_ms,
1128 fl::vector<fl::RunResult>* out_results,
1129 int num_runs_per_pattern) {
1130 fl::sstream ss;
1131 ss << "\n========================================\n";
1132 ss << "Testing (LEGACY API): " << config.timing_name << "\n";
1133 ss << " T0H: " << config.timing.t1_ns << "ns\n";
1134 ss << " T1H: " << (config.timing.t1_ns + config.timing.t2_ns) << "ns\n";
1135 ss << " T0L: " << config.timing.t3_ns << "ns\n";
1136 ss << " RESET: " << config.timing.reset_us << "us\n";
1137 ss << " Lanes: " << config.tx_configs.size() << "\n";
1138 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1139 ss << " Lane " << i << ": pin=" << config.tx_configs[i].getDataPin()
1140 << " LEDs=" << config.tx_configs[i].mLeds.size() << "\n";
1141 }
1142 ss << "========================================";
1143 FL_WARN(ss.str());
1144
1145 // Create one legacy proxy per lane (each maps runtime pin to WS2812B<PIN> template)
1147 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1148 int pin = config.tx_configs[i].getDataPin();
1149 CRGB* leds = config.tx_configs[i].mLeds.data();
1150 int numLeds = static_cast<int>(config.tx_configs[i].mLeds.size());
1151
1152 auto proxy = fl::make_unique<LegacyClocklessProxy>(pin, leds, numLeds);
1153 if (!proxy->valid()) {
1154 FL_ERROR("Legacy proxy invalid for lane " << i << " (pin " << pin << " out of range 0-8)");
1155 return; // vector destructor cleans up already-created proxies
1156 }
1157 proxies.push_back(fl::move(proxy));
1158 }
1159
1160 FastLED.setBrightness(255);
1161
1162 // Pre-initialize the TX engine to avoid first-call setup delays
1163 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1164 fill_solid(config.tx_configs[i].mLeds.data(), config.tx_configs[i].mLeds.size(), CRGB::Black);
1165 }
1166 FastLED.show();
1167 if (!FastLED.wait(1000)) {
1168 FL_ERROR("[LEGACY] TX wait timeout - driver may be stalled");
1169 return; // vector destructor cleans up proxies
1170 }
1171
1172 delay(5); // Buffer drain (same as autoResearchChipsetTiming)
1173
1174 // Run test patterns (identical to autoResearchChipsetTiming)
1175 int total = 0;
1176 int passed = 0;
1177
1178 fl::MultiRunConfig multi_config;
1179 multi_config.num_runs = (num_runs_per_pattern > 0) ? num_runs_per_pattern : 1;
1180 multi_config.print_all_runs = (multi_config.num_runs > 1);
1181 multi_config.print_per_led_errors = false;
1182 multi_config.max_errors_per_run = 10;
1183
1184 // Measure show-only duration (excludes setup/teardown overhead)
1185 uint32_t show_start_ms = millis();
1186
1187 for (int pattern_id = 0; pattern_id < 4; pattern_id++) {
1188 // Apply pattern to all lanes
1189 for (size_t i = 0; i < config.tx_configs.size(); i++) {
1191 config.tx_configs[i].mLeds.data(),
1192 config.tx_configs[i].mLeds.size(),
1193 pattern_id
1194 );
1195 }
1196 runMultiTest(getBitPatternName(pattern_id), config, multi_config, total, passed, out_results);
1197 }
1198
1199 out_show_duration_ms += millis() - show_start_ms;
1200
1201 driver_total += total;
1202 driver_passed += passed;
1203
1204 // proxies vector destructor deletes all controllers → ~CLEDController removes from draw list
1205}
1206
1207// Set mixed RGB bit patterns to test MSB vs LSB handling
1208void setMixedBitPattern(CRGB* leds, size_t count, int pattern_id) {
1209 switch (pattern_id) {
1210 case 0:
1211 // Pattern A: R=0xF0 (high nibble), G=0x0F (low nibble), B=0xAA (alternating)
1212 // Tests: High bits in R, low bits in G, mixed bits in B
1213 for (size_t i = 0; i < count; i++) {
1214 leds[i] = CRGB(0xF0, 0x0F, 0xAA);
1215 }
1216 break;
1217
1218 case 1:
1219 // Pattern B: R=0x55 (alternating 01010101), G=0xFF (all high), B=0x00 (all low)
1220 // Tests: Alternating bits, all-high boundary, all-low boundary
1221 for (size_t i = 0; i < count; i++) {
1222 leds[i] = CRGB(0x55, 0xFF, 0x00);
1223 }
1224 break;
1225
1226 case 2:
1227 // Pattern C: R=0x0F (low nibble), G=0xAA (alternating), B=0xF0 (high nibble)
1228 // Tests: Rotated pattern from A, ensures driver handles different channel values
1229 for (size_t i = 0; i < count; i++) {
1230 leds[i] = CRGB(0x0F, 0xAA, 0xF0);
1231 }
1232 break;
1233
1234 case 3:
1235 // Pattern D: Solid colors alternating (Red, Green, Blue repeating)
1236 // Baseline test - ensures basic RGB transmission works
1237 for (size_t i = 0; i < count; i++) {
1238 int color_index = i % 3;
1239 if (color_index == 0) {
1240 leds[i] = CRGB::Red; // RGB(255, 0, 0)
1241 } else if (color_index == 1) {
1242 leds[i] = CRGB::Green; // RGB(0, 255, 0)
1243 } else {
1244 leds[i] = CRGB::Blue; // RGB(0, 0, 255)
1245 }
1246 }
1247 break;
1248
1249 default:
1250 // Fallback: all black
1251 fill_solid(leds, count, CRGB::Black);
1252 break;
1253 }
1254}
1255
1256// Get name of bit pattern for logging
1257const char* getBitPatternName(int pattern_id) {
1258 switch (pattern_id) {
1259 case 0: return "Pattern A (R=0xF0, G=0x0F, B=0xAA)";
1260 case 1: return "Pattern B (R=0x55, G=0xFF, B=0x00)";
1261 case 2: return "Pattern C (R=0x0F, G=0xAA, B=0xF0)";
1262 case 3: return "Pattern D (RGB Solid Alternating)";
1263 default: return "Unknown Pattern";
1264 }
1265}
fl::CRGB leds[NUM_LEDS]
const char * getBitPatternName(int pattern_id)
void autoResearchChipsetTiming(fl::AutoResearchConfig &config, int &driver_total, int &driver_passed, uint32_t &out_show_duration_ms, fl::vector< fl::RunResult > *out_results, int num_runs_per_pattern)
void autoResearchChipsetTimingLegacy(fl::AutoResearchConfig &config, int &driver_total, int &driver_passed, uint32_t &out_show_duration_ms, fl::vector< fl::RunResult > *out_results, int num_runs_per_pattern)
static bool isUCS7604(fl::ClocklessEncoder encoder)
Check if an encoder selector identifies a UCS7604 variant.
void runMultiTest(const char *test_name, fl::AutoResearchConfig &config, const fl::MultiRunConfig &multi_config, int &total, int &passed, fl::vector< fl::RunResult > *out_results)
void runTest(const char *test_name, fl::AutoResearchConfig &config, int &total, int &passed)
void dumpRawEdgeTiming(fl::shared_ptr< fl::RxChannel > rx_channel, const fl::ChipsetTimingConfig &timing, fl::EdgeRange range)
Dump raw edge timing data to console for debugging.
static fl::vector< uint8_t > buildExpectedUCS7604(fl::span< CRGB > leds, fl::ClocklessEncoder encoder)
Build expected UCS7604 encoded bytes from LED data.
size_t capture(fl::shared_ptr< fl::RxChannel > rx_channel, fl::span< uint8_t > rx_buffer, const fl::ChipsetTimingConfig &timing, const char *driver_name)
static size_t decodeSpiEdges(fl::shared_ptr< fl::RxChannel > rx_channel, fl::span< uint8_t > rx_buffer, uint32_t clock_hz)
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:580
Runtime chipset timing configuration for clockless LED drivers.
void resize(fl::size n) FL_NOEXCEPT
Definition vector.h:173
static fl::shared_ptr< const Gamma8 > getOrCreate(float gamma) FL_NOEXCEPT
Definition ease.cpp.hpp:459
const T * data() const FL_NOEXCEPT
Definition span.h:461
constexpr fl::size size() const FL_NOEXCEPT
Definition span.h:458
string str() const FL_NOEXCEPT
Definition strstream.h:43
void clear() FL_NOEXCEPT
Definition strstream.h:358
fl::size size() const FL_NOEXCEPT
T * data() FL_NOEXCEPT
Definition vector.h:619
void reserve(fl::size n) FL_NOEXCEPT
Definition vector.h:591
void push_back(const T &value) FL_NOEXCEPT
Definition vector.h:624
void fill_solid(CRGB *targetArray, int numToFill, const CRGB &color) FL_NOEXCEPT
Fill a range of LEDs with a solid color.
Definition fill.cpp.hpp:9
fl::UISlider offset("Offset", 0.0f, 0.0f, 1.0f, 0.01f)
#define DISABLE_DITHER
Disable dithering.
Definition dither_mode.h:10
UCS7604 LED chipset encoder implementation.
Non-templated low level pixel data writing class.
fl::CRGB CRGB
Definition crgb.h:25
Centralized LED chipset timing definitions with nanosecond precision.
#define FL_WARN(X)
Definition log.h:276
#define FL_ERROR(X)
Definition log.h:219
constexpr remove_reference< T >::type && move(T &&t) FL_NOEXCEPT
Definition move.h:28
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:7
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NOEXCEPT
Definition unique_ptr.h:261
back_insert_iterator< Container > back_inserter(Container &c) FL_NOEXCEPT
Helper function to create a back_insert_iterator.
Definition iterator.h:139
void * memset(void *s, int c, size_t n) FL_NOEXCEPT
const hex_t hex
Definition ios.cpp.hpp:6
ChipsetTiming4Phase make4PhaseTiming(const ChipsetTiming &timing_3phase, u32 tolerance_ns) FL_NOEXCEPT
Create 4-phase RX timing from 3-phase chipset timing with tolerance.
Definition rx.cpp.hpp:51
UCS7604Mode
UCS7604 protocol configuration modes.
Definition ucs7604.h:28
@ UCS7604_MODE_8BIT_800KHZ
Definition ucs7604.h:29
@ UCS7604_MODE_16BIT_1600KHZ
Definition ucs7604.h:31
@ UCS7604_MODE_16BIT_800KHZ
Definition ucs7604.h:30
@ SUCCESS
Operation completed successfully.
Definition rx.h:152
int strcmp(const char *s1, const char *s2) FL_NOEXCEPT
FL_OPTIMIZE_FUNCTION bool canUseWave3(const ChipsetTiming &timing)
Check if a chipset timing is eligible for wave3 encoding.
Definition wave3.cpp.hpp:25
const char * toString(RxDeviceType type) FL_NOEXCEPT
Convert RxDeviceType to human-readable string.
Definition rx.h:177
void encodeUCS7604(PixelIterator &pixel_iter, size_t num_leds, OutputIterator out, UCS7604Mode mode, const UCS7604CurrentControl &current, bool is_rgbw, const Gamma8 *gamma=nullptr)
Encode complete UCS7604 frame (preamble + padding + pixel data)
Definition ucs7604.h:209
u32 T2
Additional high time for bit 1 (nanoseconds)
Definition led_timing.h:88
u32 T3
Low tail duration (nanoseconds)
Definition led_timing.h:89
u32 T1
High time for bit 0 (nanoseconds)
Definition led_timing.h:87
Test context for detailed error reporting Aggregates all test configuration parameters for error mess...
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.
static ColorAdjustment noAdjustment()
the per-channel scale values premixed with brightness.
FASTLED_FORCE_INLINE fl::PixelIterator as_iterator(const Rgbw &rgbw)
Pixel controller class.
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)
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.
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:558
@ Red
<div style='background:#FF0000;width:4em;height:4em;'></div>
Definition crgb.h:622
@ Blue
<div style='background:#0000FF;width:4em;height:4em;'></div>
Definition crgb.h:512
@ Black
<div style='background:#000000;width:4em;height:4em;'></div>
Definition crgb.h:510
Configuration for a single LED channel.
Definition config.h:163
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.
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.
Single run result with error tracking.
fl::optional< u32 > hz
Definition config.h:18
size_t edge_capacity
Definition config.h:17
UCS7604 current control structure with 4-bit fields for each channel.
Definition ucs7604.h:35