FastLED 3.10.6
Loading...
Searching...
No Matches
AutoResearchRemoteRunSingleTest.cpp
Go to the documentation of this file.
1// runSingleTestImpl + anonymous-namespace helpers used by it (measureTightTiming etc).
2// Extracted from AutoResearchRemote.cpp as part of #3132 / meta #3127.
3
5#if FL_PLATFORM_HAS_LARGE_MEMORY
6
7// Legacy debug macros (no-ops, kept for debugTest RPC function)
8#define DEBUG_PRINT(x) do {} while(0)
9#define DEBUG_PRINTLN(x) do {} while(0)
10
11#include "AutoResearchRemote.h"
12#include "AutoResearchBle.h"
13#include "AutoResearchNet.h"
14#include "AutoResearchOta.h"
16#include "fl/system/heap.h"
17#include "Common.h"
18#include "AutoResearchTest.h"
19#include "AutoResearchHelpers.h"
20#include "fl/stl/sstream.h"
21#include "fl/stl/unique_ptr.h"
22#include "fl/stl/optional.h"
23#include "fl/stl/json.h"
24#include "fl/stl/cstdio.h"
25#include "fl/task/task.h"
26#include "fl/task/executor.h"
28#include "fl/stl/atomic.h"
29#include "fl/task/promise.h"
30#include "fl/math/simd.h"
31#include "AutoResearchSimd.h"
37#include "platforms/arm/teensy/teensy4_common/drivers/objectfled/objectfled_diagnostics.h"
38#include "platforms/arm/rp/is_rp.h"
39#if defined(FL_IS_TEENSY_4X)
40#include "platforms/arm/teensy/teensy4_common/rx_flexpwm_channel.h"
41#include "platforms/arm/teensy/teensy4_common/drivers/flexio/flexio_driver.h"
42#endif
43#include "fl/chipsets/spi.h"
44#include "fl/channels/config.h"
45#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
46#include "platforms/arm/rp/rpcommon/rp_pio_tx_bus_traits.h"
47#include "platforms/arm/rp/rpcommon/rp_uart_bus_traits.h"
48#include "platforms/arm/rp/rpcommon/rx_pio_channel.h"
49#endif
52#include <Arduino.h>
53
54#include "fl/net/ble.h"
55
56#include "fl/codec/h264.h"
57#include "fl/codec/mp4_parser.h"
59#include "fl/fx/frame.h"
60
61namespace {
62
72class StandingRxChannelSwap {
73 public:
74 explicit StandingRxChannelSwap(AutoResearchState* state)
75 : mState(state), mReleased(false) {}
76
80 void release(fl::shared_ptr<fl::RxChannel>* alias) {
81 if (mReleased || mState == nullptr) {
82 return;
83 }
84 if (alias != nullptr) {
85 alias->reset();
86 }
87 mState->rx_channel.reset();
88 mReleased = true;
89 }
90
91 ~StandingRxChannelSwap() {
92 if (!mReleased || mState == nullptr || mState->rx_factory == nullptr) {
93 return;
94 }
95 // Rebuild, do not restore: the previous device is gone. A failure here
96 // leaves rx_channel null, which the next call reports as a missing RX
97 // channel rather than as a mystery zero capture.
98 mState->rx_channel = mState->rx_factory(mState->pin_rx);
99 }
100
101 StandingRxChannelSwap(const StandingRxChannelSwap&) = delete;
102 StandingRxChannelSwap& operator=(const StandingRxChannelSwap&) = delete;
103
104 private:
105 AutoResearchState* mState;
106 bool mReleased;
107};
108
109#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
110
115bool ucs7604ModeForEncoder(fl::ClocklessEncoder encoder,
116 fl::UCS7604Mode* out_mode) {
117 switch (encoder) {
120 return true;
123 return true;
126 return true;
130 return false;
131 }
132 return false;
133}
134
146fl::size rpPioWireFrameBytes(fl::size leds, fl::ClocklessEncoder encoder,
147 bool rgbw) {
148 fl::UCS7604Mode mode;
149 if (ucs7604ModeForEncoder(encoder, &mode)) {
150 return fl::ucs7604FrameBytes(leds, mode, rgbw);
151 }
153 return leds * (rgbw ? 4u : 3u);
154 }
155 return 0;
156}
157
159fl::size rpPioMaxLedsForWireBytes(fl::size wire_bytes,
160 fl::ClocklessEncoder encoder, bool rgbw) {
161 fl::UCS7604Mode mode;
162 if (ucs7604ModeForEncoder(encoder, &mode)) {
163 constexpr fl::size kPreambleLen = 15;
164 if (wire_bytes <= kPreambleLen) {
165 return 0;
166 }
167 return (wire_bytes - kPreambleLen) / fl::ucs7604BytesPerLed(mode, rgbw);
168 }
169 return wire_bytes / (rgbw ? 4u : 3u);
170}
171
172#endif // FL_IS_RP2040 || FL_IS_RP2350
173
174uint32_t expectedClocklessWireUs(const fl::ChipsetTimingConfig& timing,
175 uint32_t max_leds) {
176 const uint64_t bit_period_ns = timing.total_period_ns();
177 const uint64_t payload_ns =
178 static_cast<uint64_t>(max_leds) * 24ULL * bit_period_ns;
179 return static_cast<uint32_t>((payload_ns + 999ULL) / 1000ULL) +
180 timing.reset_us;
181}
182
183uint32_t maxLaneLeds(const fl::vector<fl::ChannelConfig>& tx_configs) {
184 uint32_t max_leds = 0;
185 for (fl::size i = 0; i < tx_configs.size(); i++) {
186 const uint32_t count =
187 static_cast<uint32_t>(tx_configs[i].mLeds.size());
188 if (count > max_leds) {
189 max_leds = count;
190 }
191 }
192 return max_leds;
193}
194
195class ScopedFastLedBrightness {
196 public:
197 explicit ScopedFastLedBrightness(uint8_t brightness) FL_NO_EXCEPT
198 : mSavedBrightness(FastLED.getBrightness()) {
200 }
201
202 ~ScopedFastLedBrightness() FL_NO_EXCEPT {
203 FastLED.setBrightness(mSavedBrightness);
204 }
205
206 private:
207 uint8_t mSavedBrightness;
208};
209
210bool contaminateTxMuxWithPwm(int pin) {
211#if defined(FL_IS_TEENSY_4X)
212 analogWriteFrequency(pin, 1000.0f);
213 analogWrite(pin, 128);
214 delay(2);
215 return true;
216#else
217 (void)pin;
218 return false;
219#endif
220}
221
222// Diagnostic probe (Teensy 4.x): manually drive TX via the standard GPIO1-4
223// alias and read RX with digitalRead, so we can tell whether the
224// IOMUXC_GPR_GPR26..29 fast->standard remap that ObjectFLED relies on
225// actually reaches the pad on the connected board. Saves and restores all
226// touched IOMUXC/GPIO state; never modifies FlexPWM or DMA.
227fl::json probeStandardGpioPad(int tx_pin, int rx_pin) {
228 fl::json probe = fl::json::object();
229 probe.set("supported", false);
230 probe.set("txPin", static_cast<int64_t>(tx_pin));
231 probe.set("rxPin", static_cast<int64_t>(rx_pin));
232#if !defined(FL_IS_TEENSY_4X)
233 probe.set("message", "Teensy 4.x only");
234 return probe;
235#else
236 probe.set("supported", true);
237 if (tx_pin < 0 || tx_pin >= NUM_DIGITAL_PINS ||
238 rx_pin < 0 || rx_pin >= NUM_DIGITAL_PINS) {
239 probe.set("success", false);
240 probe.set("error", "InvalidPin");
241 return probe;
242 }
243
244 volatile uint32_t* fast_output = portOutputRegister(tx_pin);
245 volatile uint32_t* standard_output = reinterpret_cast<volatile uint32_t*>( // ok reinterpret cast - Teensy GPIO alias address map
246 reinterpret_cast<uintptr_t>(fast_output) - 0x01E48000u);
247 volatile uint32_t* fast_mode = portModeRegister(tx_pin);
248 volatile uint32_t* standard_mode = reinterpret_cast<volatile uint32_t*>( // ok reinterpret cast - Teensy GPIO alias address map
249 reinterpret_cast<uintptr_t>(fast_mode) - 0x01E48000u);
250 volatile uint32_t* tx_mux = portConfigRegister(tx_pin);
251 volatile uint32_t* tx_pad = portControlRegister(tx_pin);
252 volatile uint32_t* rx_mux = portConfigRegister(rx_pin);
253 volatile uint32_t* rx_pad = portControlRegister(rx_pin);
254
255 const uint32_t gpio6_base =
256 static_cast<uint32_t>(reinterpret_cast<uintptr_t>(&GPIO6_DR));
257 const uint32_t output_addr =
258 static_cast<uint32_t>(reinterpret_cast<uintptr_t>(fast_output));
259 const uint8_t offset =
260 static_cast<uint8_t>((output_addr - gpio6_base) >> 14);
261 const uint8_t bit = digitalPinToBit(tx_pin);
262 const uint32_t mask = 1u << bit;
263 probe.set("txBit", static_cast<int64_t>(bit));
264 probe.set("txMask", static_cast<int64_t>(mask));
265 probe.set("txOffset", static_cast<int64_t>(offset));
266 if (offset > 3) {
267 probe.set("success", false);
268 probe.set("error", "UnsupportedGpioBank");
269 return probe;
270 }
271
272 volatile uint32_t* gpr = &IOMUXC_GPR_GPR26 + offset;
273 // Save state we are about to clobber.
274 const uint32_t saved_gpr = *gpr;
275 const uint32_t saved_tx_mux = *tx_mux;
276 const uint32_t saved_tx_pad = *tx_pad;
277 const uint32_t saved_rx_mux = *rx_mux;
278 const uint32_t saved_rx_pad = *rx_pad;
279 const uint32_t saved_fast_mode = *fast_mode;
280 const uint32_t saved_standard_mode = *standard_mode;
281 const uint32_t saved_fast_out = *fast_output;
282 const uint32_t saved_standard_out = *standard_output;
283
284 // Make sure standard GPIO1-4 banks are clocked. Teensy startup usually
285 // leaves them on, but assert it explicitly so this probe is independent
286 // of boot order.
287 CCM_CCGR1 |= CCM_CCGR1_GPIO1(CCM_CCGR_ON);
288 CCM_CCGR0 |= CCM_CCGR0_GPIO2(CCM_CCGR_ON);
289 CCM_CCGR2 |= CCM_CCGR2_GPIO3(CCM_CCGR_ON);
290 CCM_CCGR3 |= CCM_CCGR3_GPIO4(CCM_CCGR_ON);
291
292 // RX pin: simple input, no pull, so we can sample what the pad sees.
293 *rx_mux = 5 | 0x10;
294 *rx_pad = (5u << 3) | (3u << 6); // DSE=5, SPEED=3, no pull/keeper
295 pinMode(rx_pin, INPUT);
296
297 // TX pin: ALT5 (GPIO) + SION, mapped to standard bank.
298 *tx_mux = 5 | 0x10;
299 *tx_pad = (3u << 3) | (3u << 6); // DSE=3 (matches ObjectFLED), SPEED=3
300 *gpr &= ~mask; // route pad to standard alias
301 *standard_mode |= mask; // output mode in standard bank
302 // DR_CLEAR = +0x88, DR_SET = +0x84
303 *reinterpret_cast<volatile uint32_t*>( // ok reinterpret cast - Teensy GPIO alias address map
304 reinterpret_cast<uintptr_t>(standard_output) + 0x88u) = mask;
306 const int rx_initial = digitalReadFast(rx_pin);
307
308 int rx_high_count = 0;
309 int rx_low_count = 0;
310 for (int i = 0; i < 5; ++i) {
311 *reinterpret_cast<volatile uint32_t*>( // ok reinterpret cast - Teensy GPIO alias address map
312 reinterpret_cast<uintptr_t>(standard_output) + 0x84u) = mask;
314 if (digitalReadFast(rx_pin) == HIGH) {
315 ++rx_high_count;
316 }
317 *reinterpret_cast<volatile uint32_t*>( // ok reinterpret cast - Teensy GPIO alias address map
318 reinterpret_cast<uintptr_t>(standard_output) + 0x88u) = mask;
320 if (digitalReadFast(rx_pin) == LOW) {
321 ++rx_low_count;
322 }
323 }
324
325 probe.set("standardGpioRxInitial",
326 rx_initial == HIGH ? "HIGH" : "LOW");
327 probe.set("standardGpioRxHighCount", static_cast<int64_t>(rx_high_count));
328 probe.set("standardGpioRxLowCount", static_cast<int64_t>(rx_low_count));
329 probe.set("standardGpioConnected",
330 (rx_high_count == 5) && (rx_low_count == 5));
331
332 // Tight-loop fast toggle: write HIGH, read RX, write LOW, read RX, with
333 // no delay between write and read. Measures whether the standard GPIO
334 // alias propagates to the pad and back to digitalReadFast within a
335 // CPU-bus-cycle window comparable to ObjectFLED's DMA write rate.
336 int fast_high_count = 0;
337 int fast_low_count = 0;
338 int fast_high_zero_delay = 0;
339 int fast_low_zero_delay = 0;
340 constexpr int kFastIters = 50;
341 for (int i = 0; i < kFastIters; ++i) {
342 *reinterpret_cast<volatile uint32_t*>( // ok reinterpret cast - Teensy GPIO alias address map
343 reinterpret_cast<uintptr_t>(standard_output) + 0x84u) = mask;
344 if (digitalReadFast(rx_pin) == HIGH) {
345 ++fast_high_zero_delay;
346 }
347 delayNanoseconds(200);
348 if (digitalReadFast(rx_pin) == HIGH) {
349 ++fast_high_count;
350 }
351 *reinterpret_cast<volatile uint32_t*>( // ok reinterpret cast - Teensy GPIO alias address map
352 reinterpret_cast<uintptr_t>(standard_output) + 0x88u) = mask;
353 if (digitalReadFast(rx_pin) == LOW) {
354 ++fast_low_zero_delay;
355 }
356 delayNanoseconds(200);
357 if (digitalReadFast(rx_pin) == LOW) {
358 ++fast_low_count;
359 }
360 }
361 probe.set("fastToggleHighSettled", static_cast<int64_t>(fast_high_count));
362 probe.set("fastToggleLowSettled", static_cast<int64_t>(fast_low_count));
363 probe.set("fastToggleHighZeroDelay",
364 static_cast<int64_t>(fast_high_zero_delay));
365 probe.set("fastToggleLowZeroDelay",
366 static_cast<int64_t>(fast_low_zero_delay));
367 probe.set("fastToggleIterations", static_cast<int64_t>(kFastIters));
368
369 // Restore TX/RX pad state.
370 if ((saved_fast_out & mask) != 0) {
371 *reinterpret_cast<volatile uint32_t*>( // ok reinterpret cast - Teensy GPIO alias address map
372 reinterpret_cast<uintptr_t>(fast_output) + 0x84u) = mask;
373 } else {
374 *reinterpret_cast<volatile uint32_t*>( // ok reinterpret cast - Teensy GPIO alias address map
375 reinterpret_cast<uintptr_t>(fast_output) + 0x88u) = mask;
376 }
377 if ((saved_standard_out & mask) != 0) {
378 *reinterpret_cast<volatile uint32_t*>( // ok reinterpret cast - Teensy GPIO alias address map
379 reinterpret_cast<uintptr_t>(standard_output) + 0x84u) = mask;
380 } else {
381 *reinterpret_cast<volatile uint32_t*>( // ok reinterpret cast - Teensy GPIO alias address map
382 reinterpret_cast<uintptr_t>(standard_output) + 0x88u) = mask;
383 }
384 *fast_mode = saved_fast_mode;
385 *standard_mode = saved_standard_mode;
386 *gpr = saved_gpr;
387 *tx_pad = saved_tx_pad;
388 *tx_mux = saved_tx_mux;
389 *rx_pad = saved_rx_pad;
390 *rx_mux = saved_rx_mux;
391
392 probe.set("success", true);
393 return probe;
394#endif
395}
396
397fl::json measureTightTiming(const fl::string& driver_name,
398 const fl::ChipsetTimingConfig& timing,
399 const fl::vector<fl::ChannelConfig>& tx_configs,
400 int iterations,
401 uint32_t max_allowed_overhead_us,
402 bool& out_passed) {
403 fl::json metric = fl::json::object();
404 out_passed = false;
405
406 metric.set("requested", true);
407 metric.set("supported", false);
408 metric.set("driver", driver_name.c_str());
409
410 if (iterations < 1) {
411 metric.set("message", "iterations must be >= 1");
412 return metric;
413 }
414 if (tx_configs.empty()) {
415 metric.set("message", "no channel configs");
416 return metric;
417 }
418 for (fl::size i = 0; i < tx_configs.size(); i++) {
419 if (tx_configs[i].isSpi()) {
420 metric.set("message", "clocked SPI timing metric is not supported");
421 return metric;
422 }
423 }
424
425 fl::vector<fl::ChannelConfig> sample_configs = tx_configs;
427 for (fl::size i = 0; i < sample_configs.size(); i++) {
428 fl::ChannelConfig channel_config(
429 sample_configs[i].getDataPin(),
430 timing,
431 sample_configs[i].mLeds,
432 sample_configs[i].rgb_order);
433 fl::shared_ptr<fl::Channel> channel = FastLED.add(channel_config);
434 if (!channel) {
436 metric.set("message", "failed to create channel");
437 return metric;
438 }
439 channels.push_back(channel);
440 }
441
442 ScopedFastLedBrightness scoped_brightness(255);
443 for (fl::size lane = 0; lane < sample_configs.size(); lane++) {
444 fill_solid(sample_configs[lane].mLeds.data(),
445 sample_configs[lane].mLeds.size(),
447 }
448 FastLED.show();
449 if (!FastLED.wait(1000)) {
451 metric.set("message", "warmup wait timeout");
452 return metric;
453 }
454 delay(2);
455
456 const uint32_t expected_wire_us =
457 expectedClocklessWireUs(timing, maxLaneLeds(sample_configs));
458 uint32_t min_show_us = 0xFFFFFFFFu;
459 uint32_t max_show_us = 0;
460 uint32_t min_wait_us = 0xFFFFFFFFu;
461 uint32_t max_wait_us = 0;
462 uint32_t min_total_us = 0xFFFFFFFFu;
463 uint32_t max_total_us = 0;
464 uint32_t min_overhead_us = 0xFFFFFFFFu;
465 uint32_t max_overhead_us = 0;
466 uint64_t sum_show_us = 0;
467 uint64_t sum_wait_us = 0;
468 uint64_t sum_total_us = 0;
469 uint64_t sum_overhead_us = 0;
470 int samples = 0;
471 bool timed_out = false;
472
473 for (int sample = 0; sample < iterations; sample++) {
474 for (fl::size lane = 0; lane < sample_configs.size(); lane++) {
475 const uint8_t r = static_cast<uint8_t>(31 + sample * 17 + lane * 11);
476 const uint8_t g = static_cast<uint8_t>(67 + sample * 23 + lane * 7);
477 const uint8_t b = static_cast<uint8_t>(103 + sample * 29 + lane * 5);
478 fill_solid(sample_configs[lane].mLeds.data(),
479 sample_configs[lane].mLeds.size(),
480 CRGB(r, g, b));
481 }
482
483 const uint32_t t0 = micros();
484 FastLED.show();
485 const uint32_t t1 = micros();
486 const bool ok = FastLED.wait(1000);
487 const uint32_t t2 = micros();
488 if (!ok) {
489 timed_out = true;
490 break;
491 }
492
493 const uint32_t show_us = t1 - t0;
494 const uint32_t wait_us = t2 - t1;
495 const uint32_t total_us = t2 - t0;
496 const uint32_t overhead_us =
497 total_us > expected_wire_us ? total_us - expected_wire_us : 0;
498
499 if (show_us < min_show_us) min_show_us = show_us;
500 if (show_us > max_show_us) max_show_us = show_us;
501 if (wait_us < min_wait_us) min_wait_us = wait_us;
502 if (wait_us > max_wait_us) max_wait_us = wait_us;
503 if (total_us < min_total_us) min_total_us = total_us;
504 if (total_us > max_total_us) max_total_us = total_us;
505 if (overhead_us < min_overhead_us) min_overhead_us = overhead_us;
506 if (overhead_us > max_overhead_us) max_overhead_us = overhead_us;
507
508 sum_show_us += show_us;
509 sum_wait_us += wait_us;
510 sum_total_us += total_us;
511 sum_overhead_us += overhead_us;
512 samples++;
513 }
514
516
517 metric.set("supported", true);
518 metric.set("samples", static_cast<int64_t>(samples));
519 metric.set("iterations", static_cast<int64_t>(iterations));
520 metric.set("expected_wire_us", static_cast<int64_t>(expected_wire_us));
521 metric.set("max_allowed_overhead_us",
522 static_cast<int64_t>(max_allowed_overhead_us));
523
524 if (samples == 0) {
525 metric.set("passed", false);
526 metric.set("message", timed_out ? "timing wait timeout" : "no samples");
527 return metric;
528 }
529
530 metric.set("min_show_us", static_cast<int64_t>(min_show_us));
531 metric.set("max_show_us", static_cast<int64_t>(max_show_us));
532 metric.set("avg_show_us",
533 static_cast<int64_t>(sum_show_us / static_cast<uint64_t>(samples)));
534 metric.set("min_wait_us", static_cast<int64_t>(min_wait_us));
535 metric.set("max_wait_us", static_cast<int64_t>(max_wait_us));
536 metric.set("avg_wait_us",
537 static_cast<int64_t>(sum_wait_us / static_cast<uint64_t>(samples)));
538 metric.set("min_total_us", static_cast<int64_t>(min_total_us));
539 metric.set("max_total_us", static_cast<int64_t>(max_total_us));
540 metric.set("avg_total_us",
541 static_cast<int64_t>(sum_total_us / static_cast<uint64_t>(samples)));
542 metric.set("min_overhead_us", static_cast<int64_t>(min_overhead_us));
543 metric.set("max_overhead_us", static_cast<int64_t>(max_overhead_us));
544 metric.set("avg_overhead_us",
545 static_cast<int64_t>(sum_overhead_us / static_cast<uint64_t>(samples)));
546
547 out_passed = !timed_out && max_overhead_us <= max_allowed_overhead_us;
548 metric.set("passed", out_passed);
549 if (timed_out) {
550 metric.set("message", "timing wait timeout");
551 } else {
552 metric.set("message", out_passed ? "tight timing within budget"
553 : "tight timing exceeded budget");
554 }
555 return metric;
556}
557
558} // namespace
559
560// ============================================================================
561// AutoResearchRemoteControl Private Helper Functions
562// ============================================================================
563
565 fl::json response = fl::json::object();
566
567 // RPC system unwraps single-element arrays, so args is the config object directly
568 if (!args.is_object()) {
569 response.set("success", false);
570 response.set("error", "InvalidArgs");
571 response.set("message", "Expected {driver, laneSizes, pattern?, iterations?, pinTx?, pinRx?, timing?}");
572 return response;
573 }
574
575 fl::json config = args;
576
577 // ========== REQUIRED PARAMETERS ==========
578
579 // 1. Extract driver (required)
580 if (!config.contains("driver") || !config["driver"].is_string()) {
581 response.set("success", false);
582 response.set("error", "MissingDriver");
583 response.set("message", "Required field 'driver' (string) missing");
584 return response;
585 }
586 fl::string driver_name = config["driver"].as_string().value();
587 const bool is_object_fled_driver = (driver_name == "OBJECT_FLED");
588
589 // Validate driver exists
590 bool driver_found = false;
591 for (fl::size i = 0; i < mState->drivers_available.size(); i++) {
592 if (mState->drivers_available[i].name == driver_name) {
593 driver_found = true;
594 break;
595 }
596 }
597 if (!driver_found) {
598 response.set("success", false);
599 response.set("error", "UnknownDriver");
600 fl::sstream msg;
601 msg << "Driver '" << driver_name.c_str() << "' not available";
602 response.set("message", msg.str().c_str());
603 return response;
604 }
605
606 // 2. Extract laneSizes (required)
607 if (!config.contains("laneSizes") || !config["laneSizes"].is_array()) {
608 response.set("success", false);
609 response.set("error", "MissingLaneSizes");
610 response.set("message", "Required field 'laneSizes' (array) missing");
611 return response;
612 }
613
614 fl::json lane_sizes_json = config["laneSizes"];
615 if (lane_sizes_json.size() == 0 || lane_sizes_json.size() > 16) {
616 response.set("success", false);
617 response.set("error", "InvalidLaneCount");
618 response.set("message", "laneSizes must have 1-16 elements");
619 return response;
620 }
621
622 fl::vector<int> lane_sizes;
623 // A coarse, platform-independent backstop, kept only so an absurd request
624 // is rejected before anything is allocated. It is not the capture limit:
625 // on RP the real, encoder- and timing-aware bound is applied further down,
626 // once the timing is resolved, and is strictly tighter than this one
627 // (FastLED#4371).
628 const int max_leds_per_lane = mState->rx_buffer.size() / 32;
629 for (fl::size i = 0; i < lane_sizes_json.size(); i++) {
630 if (!lane_sizes_json[i].is_int()) {
631 response.set("success", false);
632 response.set("error", "InvalidLaneSizeType");
633 fl::sstream msg;
634 msg << "laneSizes[" << i << "] must be integer";
635 response.set("message", msg.str().c_str());
636 return response;
637 }
638 int size = static_cast<int>(lane_sizes_json[i].as_int().value());
639 if (size <= 0) {
640 response.set("success", false);
641 response.set("error", "InvalidLaneSize");
642 fl::sstream msg;
643 msg << "laneSizes[" << i << "] = " << size << " must be > 0";
644 response.set("message", msg.str().c_str());
645 return response;
646 }
647 if (size > max_leds_per_lane) {
648 response.set("success", false);
649 response.set("error", "LaneSizeTooLarge");
650 fl::sstream msg;
651 msg << "laneSizes[" << i << "] = " << size << " exceeds max " << max_leds_per_lane;
652 response.set("message", msg.str().c_str());
653 return response;
654 }
655 lane_sizes.push_back(size);
656 }
657
658 // ========== OPTIONAL PARAMETERS ==========
659
660 // 3. Extract pattern (optional, default: "MSB_LSB_A")
661 fl::string pattern = "MSB_LSB_A";
662 if (config.contains("pattern") && config["pattern"].is_string()) {
663 pattern = config["pattern"].as_string().value();
664 }
665
666 // 4. Extract iterations (optional, default: 1)
667 int iterations = 1;
668 if (config.contains("iterations") && config["iterations"].is_int()) {
669 iterations = static_cast<int>(config["iterations"].as_int().value());
670 if (iterations < 1) {
671 response.set("success", false);
672 response.set("error", "InvalidIterations");
673 response.set("message", "iterations must be >= 1");
674 return response;
675 }
676 }
677
678 // 4b. Extract frameCount (optional, default: 1)
679 // Number of back-to-back captures of the SAME LED buffer within a single
680 // pattern. frameCount >= 2 exposes second-frame degradation bugs where a
681 // driver corrupts or zeroes its DMA buffer after the first show() — see
682 // issues #2254 and #2288 (ESP32-S3 SPI \"black frame between displays\").
683 int frame_count = 1;
684 if (config.contains("frameCount") && config["frameCount"].is_int()) {
685 frame_count = static_cast<int>(config["frameCount"].as_int().value());
686 if (frame_count < 1 || frame_count > 16) {
687 response.set("success", false);
688 response.set("error", "InvalidFrameCount");
689 response.set("message", "frameCount must be in [1, 16]");
690 return response;
691 }
692 }
693
694 // 5. Extract pinTx (optional, default: use mState->pin_tx)
695 int pin_tx = mState->pin_tx;
696 if (config.contains("pinTx") && config["pinTx"].is_int()) {
697 pin_tx = static_cast<int>(config["pinTx"].as_int().value());
698 if (pin_tx < 0 || pin_tx > 48) {
699 response.set("success", false);
700 response.set("error", "InvalidPinTx");
701 response.set("message", "pinTx must be 0-48");
702 return response;
703 }
704 }
705
706 // 6. Extract pinRx (optional, default: use mState->pin_rx)
707 int pin_rx = mState->pin_rx;
708 if (config.contains("pinRx") && config["pinRx"].is_int()) {
709 pin_rx = static_cast<int>(config["pinRx"].as_int().value());
710 if (pin_rx < 0 || pin_rx > 48) {
711 response.set("success", false);
712 response.set("error", "InvalidPinRx");
713 response.set("message", "pinRx must be 0-48");
714 return response;
715 }
716 }
717
718 // 7. Extract timing (optional, default: "WS2812B-V5")
719 fl::string timing_name = "WS2812B-V5";
720 if (config.contains("timing") && config["timing"].is_string()) {
721 timing_name = config["timing"].as_string().value();
722 }
723
724 // 8. Extract useLegacyApi (optional, default: false)
725 bool use_legacy_api = false;
726 if (config.contains("useLegacyApi") && config["useLegacyApi"].is_bool()) {
727 use_legacy_api = config["useLegacyApi"].as_bool().value();
728 }
729
730 bool legacy_rgbw = false;
731 if (config.contains("legacyRgbw")) {
732 if (!use_legacy_api) {
733 response.set("success", false);
734 response.set("error", "LegacyRgbwRequiresLegacyApi");
735 response.set("message", "legacyRgbw requires useLegacyApi=true");
736 return response;
737 }
738 if (!config["legacyRgbw"].is_bool()) {
739 response.set("success", false);
740 response.set("error", "InvalidLegacyRgbw");
741 response.set("message", "legacyRgbw must be a boolean");
742 return response;
743 }
744 legacy_rgbw = config["legacyRgbw"].as_bool().value();
745 }
746
747 fl::vector<LegacyClocklessChipset> legacy_chipsets;
748 if (config.contains("legacyChipsets") && !use_legacy_api) {
749 response.set("success", false);
750 response.set("error", "LegacyChipsetsRequireLegacyApi");
751 response.set("message", "legacyChipsets requires useLegacyApi=true");
752 return response;
753 }
754 if (use_legacy_api) {
755 for (fl::size i = 0; i < lane_sizes.size(); i++) {
757 }
758 if (config.contains("legacyChipsets")) {
759 if (!config["legacyChipsets"].is_array()) {
760 response.set("success", false);
761 response.set("error", "InvalidLegacyChipsets");
762 response.set("message", "legacyChipsets must be an array");
763 return response;
764 }
765 fl::json legacy_chipsets_json = config["legacyChipsets"];
766 if (legacy_chipsets_json.size() != lane_sizes.size()) {
767 response.set("success", false);
768 response.set("error", "InvalidLegacyChipsetCount");
769 response.set("message", "legacyChipsets length must match laneSizes");
770 return response;
771 }
772 for (fl::size i = 0; i < legacy_chipsets_json.size(); i++) {
773 if (!legacy_chipsets_json[i].is_string()) {
774 response.set("success", false);
775 response.set("error", "InvalidLegacyChipsetType");
776 response.set("message", "legacyChipsets entries must be strings");
777 return response;
778 }
779 fl::string chipset_name =
780 legacy_chipsets_json[i].as_string().value();
782 if (!legacyClocklessChipsetFromName(chipset_name, &chipset)) {
783 response.set("success", false);
784 response.set("error", "UnsupportedLegacyChipset");
785 response.set(
786 "message",
787 "supported legacyChipsets: WS2812B, WS2814, WS2818, SK6812");
788 return response;
789 }
790 legacy_chipsets[i] = chipset;
791 }
792 }
793 }
794
795 const char* legacy_driver_name = nullptr;
796 if (use_legacy_api) {
797 legacy_driver_name = legacyClocklessBoundDriverName();
798 if (legacy_driver_name != nullptr &&
799 driver_name != legacy_driver_name) {
800 response.set("success", false);
801 response.set("error", "LegacyDriverMismatch");
802 fl::sstream msg;
803 msg << "Legacy templates are compile-time bound to "
804 << legacy_driver_name << ", but the request selected "
805 << driver_name.c_str();
806 response.set("message", msg.str().c_str());
807 return response;
808 }
809 }
810
811 // 9. Extract contaminateTxMux (optional, default: false)
812 bool contaminate_tx_mux = false;
813 if (config.contains("contaminateTxMux") &&
814 config["contaminateTxMux"].is_bool()) {
815 contaminate_tx_mux = config["contaminateTxMux"].as_bool().value();
816 }
817
818 // 10. Extract tightTiming (optional, default: false)
819 bool measure_tight_timing = false;
820 if (config.contains("tightTiming") && config["tightTiming"].is_bool()) {
821 measure_tight_timing = config["tightTiming"].as_bool().value();
822 }
823
824 int tight_timing_iterations = 8;
825 if (config.contains("tightTimingIterations") &&
826 config["tightTimingIterations"].is_int()) {
827 tight_timing_iterations =
828 static_cast<int>(config["tightTimingIterations"].as_int().value());
829 if (tight_timing_iterations < 1 || tight_timing_iterations > 64) {
830 response.set("success", false);
831 response.set("error", "InvalidTightTimingIterations");
832 response.set("message", "tightTimingIterations must be in [1, 64]");
833 return response;
834 }
835 }
836
837 uint32_t tight_timing_max_overhead_us = 2000;
838 if (config.contains("tightTimingMaxOverheadUs") &&
839 config["tightTimingMaxOverheadUs"].is_int()) {
840 const int max_overhead =
841 static_cast<int>(config["tightTimingMaxOverheadUs"].as_int().value());
842 if (max_overhead < 1) {
843 response.set("success", false);
844 response.set("error", "InvalidTightTimingMaxOverheadUs");
845 response.set("message", "tightTimingMaxOverheadUs must be >= 1");
846 return response;
847 }
848 tight_timing_max_overhead_us = static_cast<uint32_t>(max_overhead);
849 }
850
851 // Legacy API: pins 0-8 are the historical compile-time template range.
852 // On Teensy 4.x, pin 22 is also included for current ObjectFLED loopback
853 // wiring. Other targets may mark GPIO22 invalid at compile time.
854 // Multi-lane uses consecutive pins starting at pin_tx.
855 if (use_legacy_api) {
856 int max_pin = pin_tx + (int)lane_sizes.size() - 1;
857 const bool historical_pin_range = pin_tx >= 0 && max_pin <= 8;
858#if AUTORESEARCH_LEGACY_SUPPORTS_PIN_22
859 const bool current_loopback_pin =
860 lane_sizes.size() == 1 && pin_tx == 22;
861#else
862 const bool current_loopback_pin = false;
863#endif
864 if (!historical_pin_range && !current_loopback_pin) {
865 response.set("success", false);
866 response.set("error", "LegacyApiPinRange");
867 fl::sstream msg;
868 msg << "Legacy template API supports pins 0-8";
869#if AUTORESEARCH_LEGACY_SUPPORTS_PIN_22
870 msg << ", plus single-lane pin 22 for current ObjectFLED loopback";
871#endif
872 msg << ", got pins " << pin_tx << "-" << max_pin;
873 response.set("message", msg.str().c_str());
874 return response;
875 }
876 }
877
878 // ========== EXECUTION ==========
879
880 uint32_t start_ms = millis();
881
882 fl::json standard_gpio_pad_probe;
883 if (is_object_fled_driver) {
884 standard_gpio_pad_probe = probeStandardGpioPad(pin_tx, pin_rx);
885 }
886
887 if (contaminate_tx_mux) {
888 if (!is_object_fled_driver) {
889 response.set("success", false);
890 response.set("error", "MuxContaminationDriverUnsupported");
891 response.set("message", "contaminateTxMux is only supported for OBJECT_FLED");
892 return response;
893 }
894 if (!contaminateTxMuxWithPwm(pin_tx)) {
895 response.set("success", false);
896 response.set("error", "MuxContaminationUnsupported");
897 response.set("message", "contaminateTxMux requires Teensy 4.x analogWriteFrequency support");
898 return response;
899 }
900 }
901
902 // Set driver as exclusive (by-name path: driver_name comes from RPC)
903 if (!autoResearchSetExclusiveDriverByName(driver_name.c_str())) {
904 response.set("success", false);
905 response.set("error", "DriverSetupFailed");
906 fl::sstream msg;
907 msg << "Failed to set " << driver_name.c_str() << " as exclusive driver";
908 response.set("message", msg.str().c_str());
909 return response;
910 }
911
912 // Get timing configuration
913 // Legacy API: resolve timing from the selected public chipset template.
914 // If mixed legacy chipsets are requested, lane 0 timing is used for RX
915 // decode because AutoResearch captures only lane 0.
916 // Channel API: Uses timing_name from RPC (default: WS2812B-V5)
917 // RX decode timing MUST match actual TX timing for correct capture
918 fl::ChipsetTimingConfig resolved_timing;
920 if (use_legacy_api) {
922 if (!legacy_chipsets.empty()) {
923 first_chipset = legacy_chipsets[0];
924 }
925 if (first_chipset == LegacyClocklessChipset::WS2814) {
926 resolved_timing = fl::makeTimingConfig<fl::TIMING_WS2814>();
927 resolved_encoder = fl::encoder_for<fl::TIMING_WS2814>();
928 } else if (first_chipset == LegacyClocklessChipset::WS2818) {
929 resolved_timing = fl::makeTimingConfig<fl::TIMING_WS2818>();
930 resolved_encoder = fl::encoder_for<fl::TIMING_WS2818>();
931 } else if (first_chipset == LegacyClocklessChipset::SK6812) {
932 resolved_timing = fl::makeTimingConfig<fl::TIMING_SK6812>();
933 resolved_encoder = fl::encoder_for<fl::TIMING_SK6812>();
934 } else {
937 }
938 timing_name = legacyClocklessChipsetName(first_chipset);
939 } else if (timing_name == "UCS7604-800KHZ") {
942 } else if (timing_name == "WS2811-400KHZ") {
943 // 2500 ns period. The RP PL011 tops out at clk_peri/16 = 3.0 Mbaud,
944 // below the >=3.2 Mbaud that WS2812's 1250 ns needs at the coarsest
945 // (4 pulses/bit) UART geometry -- so 800 kHz parts cannot be driven
946 // over UART there at all. At 2500 ns the requirement drops to
947 // 1.6 Mbaud, which fits, and exercises the backend on RP for the
948 // first time. See FastLED#3899.
951 } else if (timing_name == "SK6812") {
952 // SK6812 exercises the UART wave8-frame geometry (its T0H/T1H
953 // are exact multiples of period/4 — FastLED#3572 follow-up).
954 resolved_timing = fl::makeTimingConfig<fl::TIMING_SK6812>();
955 resolved_encoder = fl::encoder_for<fl::TIMING_SK6812>();
956 } else if (timing_name == "WS2818") {
957 resolved_timing = fl::makeTimingConfig<fl::TIMING_WS2818>();
958 resolved_encoder = fl::encoder_for<fl::TIMING_WS2818>();
959 } else {
961 resolved_encoder = fl::encoder_for<fl::TIMING_WS2812B_V5>();
962 }
963 fl::NamedTimingConfig timing_config(resolved_timing, timing_name.c_str(), resolved_encoder);
964
965 // Dynamically allocate LED arrays for each lane
966 fl::vector<fl::unique_ptr<fl::vector<CRGB>>> led_arrays;
967 fl::vector<fl::ChannelConfig> tx_configs;
968
969 // SPI chipset drivers use APA102 protocol with data+clock pins.
970 // Clockless drivers use WS2812B timing on a single data pin
971 bool is_spi_chipset_driver = (driver_name == "LCD_SPI" ||
972 driver_name == "I2S_SPI" ||
973 driver_name == "SPI_UNIFIED");
974
975 for (fl::size i = 0; i < lane_sizes.size(); i++) {
976 auto leds = fl::make_unique<fl::vector<CRGB>>(lane_sizes[i]);
977 if (is_spi_chipset_driver) {
978 // SPI chipset: APA102 with data pin = pin_tx (lane 0 only)
979 // Multi-lane SPI validation is not currently supported because
980 // the I80 bus transposes all lanes onto a single bus — only
981 // lane 0 (D0) can be captured via the TX→RX jumper wire.
982 // Clock pin must avoid TX, RX, and DC (21) pins.
983 int data_pin = pin_tx;
984 int clock_pin = pin_rx + 1;
985 // Use 2.4MHz SPI clock (matches I2S LCD_CAM default, ~417ns/bit)
986 // Lower frequencies (e.g., 800kHz) fail with ESP_ERR_NOT_SUPPORTED
987 // on the I80 panel IO.
988 fl::SpiChipsetConfig spi_cfg(data_pin, clock_pin, fl::SpiEncoder::apa102(2400000));
989 tx_configs.push_back(fl::ChannelConfig(
990 spi_cfg,
991 fl::span<CRGB>(leds->data(), leds->size()),
992 RGB
993 ));
994 } else {
995 tx_configs.push_back(fl::ChannelConfig(
996 pin_tx + i, // Consecutive pins for multi-lane
997 timing_config.timing,
998 fl::span<CRGB>(leds->data(), leds->size()),
999 RGB // Default color order
1000 ));
1001 }
1002 led_arrays.push_back(fl::move(leds));
1003 }
1004
1005 // Create temporary RX channel if pinRx differs from default
1006 fl::shared_ptr<fl::RxChannel> rx_channel_to_use = mState->rx_channel;
1007
1008 // Optional rxBackend override (FastLED#3576 Phase 3) — lets the
1009 // host select a specific capture backend per test, e.g. "I2S_RX"
1010 // (classic-ESP32 oversampler, no 10-LED ceiling) vs "RMT".
1011 fl::RxBackend rx_backend_override = fl::RxBackend::PLATFORM_DEFAULT;
1012 bool have_backend_override = false;
1013 if (config.contains("rxBackend") && config["rxBackend"].is_string()) {
1014 fl::string rb = config["rxBackend"].as_string().value();
1015 if (rb == "RMT") { rx_backend_override = fl::RxBackend::RMT; have_backend_override = true; }
1016 else if (rb == "ISR") { rx_backend_override = fl::RxBackend::ISR; have_backend_override = true; }
1017 else if (rb == "I2S_RX") { rx_backend_override = fl::RxBackend::I2S_RX; have_backend_override = true; }
1018 else if (rb != "PLATFORM_DEFAULT") {
1019 response.set("success", false);
1020 response.set("error", "UnknownRxBackend");
1021 response.set("message", "rxBackend must be PLATFORM_DEFAULT, RMT, ISR or I2S_RX");
1022 return response;
1023 }
1024 }
1025
1026 // ESP32-C6 RMT RX merges PARLIO's waveform into its frame envelope even
1027 // while the GPIO input sees every edge (FastLED#3586). Keep explicit
1028 // backend overrides available for diagnostics, but use the independent
1029 // GPIO ISR timestamp path for normal C6 PARLIO wire validation.
1030#if defined(FL_IS_ESP_32C6)
1031 constexpr bool is_esp32_c6 = true;
1032#else
1033 constexpr bool is_esp32_c6 = false;
1034#endif
1035 rx_backend_override = fl::validation::resolveCaptureBackend(
1036 rx_backend_override, have_backend_override,
1037 driver_name == "PARLIO", is_esp32_c6);
1038 if (rx_backend_override != fl::RxBackend::PLATFORM_DEFAULT) {
1039 have_backend_override = true;
1040 }
1041
1042 // A per-call RX channel has to displace the standing one, not sit
1043 // alongside it.
1044 //
1045 // The RP PIO capture buffer is a single global, claimed by whichever
1046 // device calls begin() first and released only in stop() -- so a standing
1047 // channel that has already captured once still owns it. A second channel
1048 // built next to it fails begin() with `dma_buffer_busy` and captures
1049 // nothing, while the response echoes the requested pins back through
1050 // actualRxPin as though the change took effect. `setPins` never hit this
1051 // because it *replaces* mState->rx_channel, destroying the old device and
1052 // releasing the claim. Confirmed on an RP2350W: the failing run reports
1053 // rxBeginError='dma_buffer_busy'. See FastLED#4374.
1054 //
1055 // Restoring it afterwards keeps inline pins scoped to the one call, which
1056 // is what a caller probing alternative wiring means by them -- adopting
1057 // the pin as standing state would trade a silent failure for a silent
1058 // reconfiguration.
1059 StandingRxChannelSwap standing_rx_swap(mState.get());
1060 if (have_backend_override) {
1061 standing_rx_swap.release(&rx_channel_to_use);
1062 fl::RxChannelConfig rx_cfg(pin_rx, rx_backend_override);
1063 rx_channel_to_use = FastLED.addRx(rx_cfg);
1064 if (!rx_channel_to_use) {
1065 response.set("success", false);
1066 response.set("error", "RxChannelCreationFailed");
1067 response.set("message", "Failed to create RX channel for requested rxBackend");
1068 return response;
1069 }
1070 } else if (pin_rx != mState->pin_rx && mState->rx_factory) {
1071 standing_rx_swap.release(&rx_channel_to_use);
1072 rx_channel_to_use = mState->rx_factory(pin_rx);
1073 if (!rx_channel_to_use) {
1074 response.set("success", false);
1075 response.set("error", "RxChannelCreationFailed");
1076 response.set("message", "Failed to create RX channel on custom pin");
1077 return response;
1078 }
1079 }
1080
1081#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
1082 // The `laneSizes` guard above bounds the request with
1083 // `rx_buffer.size() / 32`, which measures the wrong thing: `rx_buffer` is
1084 // the decode *output*, and 32 has no relation to the capture. It advertised
1085 // 103 LEDs, and 101, 102 and 103 were all accepted and then failed in
1086 // capture with no error at all -- the outcome a bounds check exists to
1087 // prevent (FastLED#4371).
1088 //
1089 // Two things decide where this check can live. The real ceiling needs the
1090 // resolved timing and encoder, so it cannot sit with the rest of the
1091 // argument validation; and it belongs to the backend that *captures*, not
1092 // the driver that transmits, so it has to follow the RX channel. On RP,
1093 // PLATFORM_DEFAULT resolves to PIO RX whatever the TX driver is -- keying
1094 // off `driver_name` would skip the bound for a UART0 request, which is
1095 // captured by the same pool and hits the same ceiling, while applying a
1096 // PIO-only limit to a PIO TX pointed at some other backend.
1097 const fl::RxBackend capture_backend_for_limit =
1098 rx_channel_to_use ? rx_channel_to_use->backend()
1100 if (capture_backend_for_limit == fl::RxBackend::PLATFORM_DEFAULT ||
1101 capture_backend_for_limit == fl::RxBackend::PIO) {
1102 // Reserve for the gap between arming the capture and the transmitter's
1103 // first edge, which the sampler also spends words on. Measured on an
1104 // RP2350W it sits between 4 us and 64 us: a 63-LED 400 kHz frame passes
1105 // with 64 us of budget to spare and a 64-LED one fails with 4 us. Take
1106 // the upper end -- advertising a maximum that does not work is the
1107 // defect being removed here, so erring long is not symmetric with
1108 // erring short.
1109 constexpr fl::u32 kArmingLeadInNs = 64000;
1110
1111 const fl::u32 bit_period_ns =
1112 static_cast<fl::u32>(resolved_timing.total_period_ns());
1113 const fl::size max_wire_bytes = fl::validation::rpPioMaxWireBytes(
1116 1000000000u / fl::kRpPioRxClockHz,
1117 fl::RxChannelConfig(pin_rx).signal_range_max_ns,
1118 kArmingLeadInNs, bit_period_ns);
1119
1120 for (fl::size i = 0; i < lane_sizes.size(); i++) {
1121 const fl::size leds = static_cast<fl::size>(lane_sizes[i]);
1122 // Same predicate the comparison uses (`legacyLaneUsesRgbw`): an
1123 // RGBW lane is four bytes per LED, so a 100-LED RGBW frame is 400
1124 // wire bytes and does not fit where an RGB one does.
1125 const bool lane_rgbw =
1126 legacy_rgbw ||
1127 (i < legacy_chipsets.size() &&
1128 legacyClocklessChipsetHasAutomaticRgbw(legacy_chipsets[i]));
1129 const fl::size wire_bytes =
1130 rpPioWireFrameBytes(leds, timing_config.encoder, lane_rgbw);
1131 if (wire_bytes > max_wire_bytes) {
1132 response.set("success", false);
1133 response.set("error", "LaneSizeTooLarge");
1134 fl::sstream msg;
1135 msg << "laneSizes[" << i << "] = " << lane_sizes[i] << " puts "
1136 << static_cast<int>(wire_bytes) << " bytes on the wire at "
1137 << timing_name.c_str() << "; PIO RX capture holds "
1138 << static_cast<int>(max_wire_bytes) << " ("
1139 << static_cast<int>(
1140 rpPioMaxLedsForWireBytes(max_wire_bytes,
1141 timing_config.encoder,
1142 lane_rgbw))
1143 << " LEDs)";
1144 response.set("message", msg.str().c_str());
1145 return response;
1146 }
1147 }
1148 }
1149#endif
1150
1151 // Create validation configuration
1152 fl::AutoResearchConfig autoresearch_config(
1153 timing_config.timing,
1154 timing_config.name,
1155 tx_configs,
1156 driver_name.c_str(),
1157 rx_channel_to_use,
1158 mState->rx_buffer,
1159 lane_sizes[0], // base_strip_size (used for logging)
1160 fl::RxDeviceType::RMT, // Default RX device type
1161 timing_config.encoder,
1162 fl::span<const LegacyClocklessChipset>(legacy_chipsets),
1163 legacy_rgbw
1164 );
1165
1166 // Run test with debug output suppressed
1167 int total_tests = 0;
1168 int passed_tests = 0;
1169 bool passed = false;
1170 uint32_t show_duration_ms = 0;
1171 fl::vector<fl::RunResult> run_results;
1172 fl::json tight_timing_response = fl::json::object();
1173 bool tight_timing_passed = true;
1174
1175 {
1176 // Keep timing-sensitive hardware validation quiet. On Teensy 4,
1177 // Serial-backed FL_WARN output can block long enough to starve
1178 // ObjectFLED's DMA refill path, which is exactly what #3343 is
1179 // trying to prove or eliminate. JSON-RPC responses bypass this guard.
1180 //
1181 // LPUART note: empirically tried both directions (logs disabled vs
1182 // logs enabled during LPUART tests). Logs ENABLED yielded ~0/3
1183 // success (USB-CDC backed up during the 4-pattern loop, RPC
1184 // response never reaches host). Logs DISABLED yielded ~1/3 to
1185 // ~1/5 success. ScopedLogDisable stays in scope for LPUART too.
1186 fl::ScopedLogDisable quiet_logs;
1187
1188 if (is_object_fled_driver) {
1189 fl::objectFledDiagnosticsReset();
1190 fl::objectFledDiagnosticsSetRxPin(pin_rx);
1191 }
1192
1193 if (use_legacy_api) {
1194 // Legacy API path: selected CHIPSET<PIN> template instantiation
1195 for (int iter = 0; iter < iterations; iter++) {
1196 int iter_total = 0, iter_passed = 0;
1197 autoResearchChipsetTimingLegacy(autoresearch_config, iter_total, iter_passed, show_duration_ms, &run_results, frame_count);
1198 total_tests += iter_total;
1199 passed_tests += iter_passed;
1200 }
1201 } else {
1202 // Channel API path: FastLED.add(channel_config)
1203 for (int iter = 0; iter < iterations; iter++) {
1204 int iter_total = 0, iter_passed = 0;
1205 autoResearchChipsetTiming(autoresearch_config, iter_total, iter_passed, show_duration_ms, &run_results, frame_count);
1206 total_tests += iter_total;
1207 passed_tests += iter_passed;
1208 }
1209 }
1210
1211 passed = (total_tests > 0) && (passed_tests == total_tests);
1212 }
1213
1214 if (measure_tight_timing) {
1215 if (use_legacy_api) {
1216 tight_timing_passed = false;
1217 tight_timing_response.set("requested", true);
1218 tight_timing_response.set("supported", false);
1219 tight_timing_response.set("passed", false);
1220 tight_timing_response.set("driver", driver_name.c_str());
1221 tight_timing_response.set("message", "legacy API timing metric is not supported");
1222 } else {
1223 tight_timing_response = measureTightTiming(
1224 driver_name,
1225 timing_config.timing,
1226 tx_configs,
1227 tight_timing_iterations,
1228 tight_timing_max_overhead_us,
1229 tight_timing_passed);
1230 }
1231 bool tight_timing_supported = false;
1232 if (tight_timing_response.contains("supported") &&
1233 tight_timing_response["supported"].is_bool()) {
1234 tight_timing_supported =
1235 tight_timing_response["supported"].as_bool().value();
1236 }
1237 if (tight_timing_supported) {
1238 passed = passed && tight_timing_passed;
1239 }
1240 }
1241
1242 uint32_t duration_ms = millis() - start_ms;
1243 int capture_evidence_bytes = 0;
1244 int capture_evidence_raw_edges = 0;
1245 for (fl::size ri = 0; ri < run_results.size(); ri++) {
1246 const auto& rr = run_results[ri];
1247 if (rr.capturedBytes > 0) {
1248 capture_evidence_bytes += rr.capturedBytes;
1249 }
1250 if (rr.rawEdgesAfterWait > capture_evidence_raw_edges) {
1251 capture_evidence_raw_edges = rr.rawEdgesAfterWait;
1252 }
1253 }
1254
1255 // ========== RESPONSE ==========
1256 response.set("success", true);
1257 response.set("passed", passed);
1258 response.set("totalTests", static_cast<int64_t>(total_tests));
1259 response.set("passedTests", static_cast<int64_t>(passed_tests));
1260 response.set("captureEvidenceExpected", true);
1261 response.set("captureEvidenceBytes",
1262 static_cast<int64_t>(capture_evidence_bytes));
1263 response.set("captureEvidenceRawEdges",
1264 static_cast<int64_t>(capture_evidence_raw_edges));
1265 response.set("duration_ms", static_cast<int64_t>(duration_ms));
1266 response.set("show_duration_ms", static_cast<int64_t>(show_duration_ms));
1267 response.set("driver", driver_name.c_str());
1268 response.set("requestedTxPin", static_cast<int64_t>(pin_tx));
1269 response.set("requestedRxPin", static_cast<int64_t>(pin_rx));
1270 response.set("actualTxPin", static_cast<int64_t>(pin_tx));
1271 response.set("actualRxPin", static_cast<int64_t>(
1272 rx_channel_to_use ? rx_channel_to_use->getPin() : pin_rx));
1273 fl::string capture_backend = rx_channel_to_use
1274 ? rx_channel_to_use->getEngineName()
1275 : fl::string("none");
1276 response.set("captureBackend", capture_backend.c_str());
1277 response.set("laneCount", static_cast<int64_t>(lane_sizes.size()));
1278#if defined(FL_IS_RP2040) || defined(FL_IS_RP2350)
1279 if (driver_name == "UART0" || driver_name == "UART1") {
1280 auto& uart = driver_name == "UART0"
1281 ? fl::BusTraits<fl::Bus::UART, 0>::instance()
1282 : fl::BusTraits<fl::Bus::UART, 1>::instance();
1283 response.set("rpUartStartAttempted", uart.lastStartAttempted());
1284 response.set("rpUartStartSucceeded", uart.lastStartSucceeded());
1285 response.set("rpUartEncodedSize",
1286 static_cast<int64_t>(uart.lastEncodedSize()));
1287 response.set("rpUartActualBaud",
1288 static_cast<int64_t>(uart.lastActualBaud()));
1289 response.set("rpUartLastError", uart.lastError().c_str());
1290 }
1291 if (driver_name == "PIO0" || driver_name == "PIO1") {
1292 auto& rp_pio = driver_name == "PIO0"
1293 ? fl::BusTraits<fl::Bus::FLEX_IO, 0>::instance()
1294 : fl::BusTraits<fl::Bus::FLEX_IO, 1>::instance();
1295 response.set("rpPioActive", rp_pio.isActive());
1296 response.set("rpPioLastError", rp_pio.lastError().c_str());
1297 response.set("rpPioStartAttempted", rp_pio.lastStartAttempted());
1298 response.set("rpPioStartSucceeded", rp_pio.lastStartSucceeded());
1299 response.set("rpPioWordCount", static_cast<int64_t>(rp_pio.lastWordCount()));
1300 }
1301 #if defined(FL_IS_RP2350)
1302 if (driver_name == "PIO2") {
1303 auto& rp_pio = fl::BusTraits<fl::Bus::FLEX_IO, 2>::instance();
1304 response.set("rpPioActive", rp_pio.isActive());
1305 response.set("rpPioLastError", rp_pio.lastError().c_str());
1306 response.set("rpPioStartAttempted", rp_pio.lastStartAttempted());
1307 response.set("rpPioStartSucceeded", rp_pio.lastStartSucceeded());
1308 response.set("rpPioWordCount", static_cast<int64_t>(rp_pio.lastWordCount()));
1309 }
1310 #endif
1311#endif
1312
1313 fl::json sizes_response = fl::json::array();
1314 for (int size : lane_sizes) {
1315 sizes_response.push_back(static_cast<int64_t>(size));
1316 }
1317 response.set("laneSizes", sizes_response);
1318 response.set("pattern", pattern.c_str());
1319 response.set("useLegacyApi", use_legacy_api);
1320 if (use_legacy_api) {
1321 if (legacy_driver_name != nullptr) {
1322 response.set("legacyDriver", legacy_driver_name);
1323 }
1324 response.set("legacyRgbw", legacy_rgbw);
1325 fl::json legacy_chipsets_response = fl::json::array();
1326 for (LegacyClocklessChipset chipset : legacy_chipsets) {
1327 legacy_chipsets_response.push_back(
1329 }
1330 response.set("legacyChipsets", legacy_chipsets_response);
1331 }
1332 response.set("frameCount", static_cast<int64_t>(frame_count));
1333 response.set("contaminateTxMux", contaminate_tx_mux);
1334 if (contaminate_tx_mux) {
1335 response.set("contaminateTxMuxPin", static_cast<int64_t>(pin_tx));
1336 }
1337 if (measure_tight_timing) {
1338 response.set("tightTiming", tight_timing_response);
1339 }
1340 if (is_object_fled_driver) {
1341 response.set("objectFledDiagnostics",
1342 fl::objectFledDiagnosticsToJson());
1343 response.set("standardGpioPadProbe", standard_gpio_pad_probe);
1344 }
1345#if defined(FL_IS_TEENSY_4X)
1346 // FlexPWM RX diagnostics: useful for ANY clockless TX routed through
1347 // pin 22. Skip for LPUART (LPUART has its own status path) AND for
1348 // FLEX_IO (PR #3421 framework-hang investigation found that the
1349 // ~20 fj.set() calls for flexIoDiagnostics below push the heap into
1350 // a state where downstream destructors hang -- emit only for
1351 // OBJECT_FLED where the diagnostics were originally added for
1352 // #3343 / #3410 bring-up).
1353 const bool emit_flex_diagnostics = (driver_name == "OBJECT_FLED");
1354 if (emit_flex_diagnostics) {
1355 response.set("flexPwmRxDiagnostics",
1356 fl::FlexPwmRxChannel::diagnosticsToJson(pin_rx));
1357 }
1358 if (emit_flex_diagnostics) {
1359 // #3410 Round 7: snapshot FlexIO2 register state so we can tell
1360 // whether the peripheral is actually running, what the
1361 // shifter/timer/DMA state is after show(), and whether CTRL,
1362 // SHIFTSDEN, TCD etc match what flexio_show() programmed. Without
1363 // this we are guessing in the dark about why zero_capture happens.
1364 fl::FlexIODiagnostics fd{};
1365 fl::flexio_read_diagnostics(&fd);
1366 fl::json fj = fl::json::object();
1367 fj.set("ctrl", static_cast<int64_t>(fd.ctrl));
1368 fj.set("shiftstat", static_cast<int64_t>(fd.shiftstat));
1369 fj.set("shifterr", static_cast<int64_t>(fd.shifterr));
1370 fj.set("timstat", static_cast<int64_t>(fd.timstat));
1371 fj.set("shiftsden", static_cast<int64_t>(fd.shiftsden));
1372 fj.set("shiftctl0", static_cast<int64_t>(fd.shiftctl0));
1373 fj.set("shiftcfg0", static_cast<int64_t>(fd.shiftcfg0));
1374 fj.set("timctl0", static_cast<int64_t>(fd.timctl0));
1375 fj.set("timcfg0", static_cast<int64_t>(fd.timcfg0));
1376 fj.set("timcmp0", static_cast<int64_t>(fd.timcmp0));
1377 fj.set("ccmCcgr3", static_cast<int64_t>(fd.ccm_ccgr3));
1378 fj.set("ccmCscmr2", static_cast<int64_t>(fd.ccm_cscmr2));
1379 fj.set("ccmCs1cdr", static_cast<int64_t>(fd.ccm_cs1cdr));
1380 fj.set("muxRegValue", static_cast<int64_t>(fd.muxRegValue));
1381 fj.set("padRegValue", static_cast<int64_t>(fd.padRegValue));
1382 fj.set("tcdSaddr", static_cast<int64_t>(fd.tcd_saddr));
1383 fj.set("tcdDaddr", static_cast<int64_t>(fd.tcd_daddr));
1384 fj.set("tcdCiter", static_cast<int64_t>(fd.tcd_citer));
1385 fj.set("tcdBiter", static_cast<int64_t>(fd.tcd_biter));
1386 fj.set("tcdCsr", static_cast<int64_t>(fd.tcd_csr));
1387 fj.set("initialized", fd.initialized);
1388 fj.set("dmaComplete", fd.dmaComplete);
1389 response.set("flexIoDiagnostics", fj);
1390 }
1391#endif
1392
1393 // Free run_results before building response to reclaim heap
1394 // Only serialize pattern details when tests FAIL (saves heap on passing tests).
1395 // WS2814 legacy runs are the exception: the host requires a size summary
1396 // for every run as proof that 4 bytes/LED (RGBW) went on the wire, so
1397 // passing runs emit just runNumber/totalLeds/totalBytes/passed (#4578).
1398 const bool ws2814_legacy = use_legacy_api && !legacy_chipsets.empty() &&
1399 legacy_chipsets[0] == LegacyClocklessChipset::WS2814;
1400 if ((!passed || ws2814_legacy) && !run_results.empty()) {
1401 fl::json patterns = fl::json::array();
1402 for (fl::size ri = 0; ri < run_results.size(); ri++) {
1403 const auto& rr = run_results[ri];
1404 if (rr.passed && !ws2814_legacy) continue; // Skip passing patterns
1405 fl::json pat = fl::json::object();
1406 pat.set("runNumber", static_cast<int64_t>(rr.run_number));
1407 pat.set("totalLeds", static_cast<int64_t>(rr.total_leds));
1408 pat.set("totalBytes", static_cast<int64_t>(rr.totalBytes));
1409 pat.set("passed", rr.passed);
1410 if (rr.passed) {
1411 patterns.push_back(pat);
1412 continue;
1413 }
1414 pat.set("mismatchedLeds", static_cast<int64_t>(rr.mismatches));
1415 pat.set("capturedBytes", static_cast<int64_t>(rr.capturedBytes));
1416 pat.set("captureWaitResult", static_cast<int64_t>(rr.captureWaitResult));
1417 pat.set("rawEdgesAfterWait", static_cast<int64_t>(rr.rawEdgesAfterWait));
1418 pat.set("decodeOk", static_cast<int64_t>(rr.decodeOk));
1419 pat.set("decodeError", static_cast<int64_t>(rr.decodeError));
1420 pat.set("decodeBytes", static_cast<int64_t>(rr.decodeBytes));
1421 pat.set("decodeOutputCapacity",
1422 static_cast<int64_t>(rr.decodeOutputCapacity));
1423 if (rr.rpPioGpioTransitions >= 0) {
1424 pat.set("rpPioGpioTransitions",
1425 static_cast<int64_t>(rr.rpPioGpioTransitions));
1426 }
1427 if (!rr.rxBeginError.empty()) {
1428 pat.set("rxBeginError", rr.rxBeginError.c_str());
1429 }
1430 if (!rr.rawEdgeSample.empty()) {
1431 pat.set("rawEdgeSample", rr.rawEdgeSample.c_str());
1432 }
1433 pat.set("captureFailed", rr.captureFailed);
1434 pat.set("mismatchedBytes", static_cast<int64_t>(rr.mismatchedBytes));
1435 pat.set("lsbOnlyErrors", static_cast<int64_t>(rr.lsbOnlyErrors));
1436
1437 // Serialize first N LED errors (limit to prevent OOM on small MCUs)
1438 constexpr fl::size kMaxSerializedErrors = 5;
1439 if (!rr.errors.empty()) {
1440 fl::json errs = fl::json::array();
1441 const fl::size errLimit = rr.errors.size() < kMaxSerializedErrors
1442 ? rr.errors.size()
1443 : kMaxSerializedErrors;
1444 for (fl::size ei = 0; ei < errLimit; ei++) {
1445 const auto& e = rr.errors[ei];
1446 fl::json err = fl::json::object();
1447 err.set("led", static_cast<int64_t>(e.led_index));
1448 fl::json expected = fl::json::array();
1449 expected.push_back(static_cast<int64_t>(e.expected_r));
1450 expected.push_back(static_cast<int64_t>(e.expected_g));
1451 expected.push_back(static_cast<int64_t>(e.expected_b));
1452 if (e.has_white) {
1453 expected.push_back(
1454 static_cast<int64_t>(e.expected_w));
1455 }
1456 err.set("expected", expected);
1457 fl::json actual = fl::json::array();
1458 actual.push_back(static_cast<int64_t>(e.actual_r));
1459 actual.push_back(static_cast<int64_t>(e.actual_g));
1460 actual.push_back(static_cast<int64_t>(e.actual_b));
1461 if (e.has_white) {
1462 actual.push_back(static_cast<int64_t>(e.actual_w));
1463 }
1464 err.set("actual", actual);
1465 errs.push_back(err);
1466 }
1467 pat.set("errors", errs);
1468 pat.set("totalErrors", static_cast<int64_t>(rr.errors.size()));
1469 }
1470 patterns.push_back(pat);
1471 }
1472 response.set("patterns", patterns);
1473 }
1474 run_results.clear(); // Free memory before serialization
1475
1476 // Return the response — the lambda wrapper will call sendAsyncResponse
1477 return response;
1478}
1479
1480#endif // FL_PLATFORM_HAS_LARGE_MEMORY
CRGB leds[1]
fl::UISlider brightness("Brightness", BRIGHTNESS, 0, 255)
bool autoResearchSetExclusiveDriverByName(const char *name)
AutoResearch-style helper: set an exclusive driver by name.
Full PARLIO encode bench for ESP32-P4 (post-byte-LUT, #2526 landed).
PARLIO ISR-streaming functional validation (#2548 deep-dive follow-up).
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)
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)
Issue #2994 repro: compounded per-sequence timing drift vs 3.10.3.
Wave8 expansion micro-benchmark for #2526.
TestState state
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
LegacyClocklessChipset
bool legacyClocklessChipsetFromName(const fl::string &name, LegacyClocklessChipset *out)
const char * legacyClocklessChipsetName(LegacyClocklessChipset chipset)
const char * legacyClocklessBoundDriverName()
Name of the driver to which public legacy templates are bound.
bool legacyClocklessChipsetHasAutomaticRgbw(LegacyClocklessChipset chipset)
Rgbw rgbw
int passed_tests
Definition SIMD.ino:75
int total_tests
Definition SIMD.ino:74
fl::net::ble — BLE GATT transport layer for JSON-RPC
SPI encoder configuration for clocked LED chipsets.
fl::json runSingleTestImpl(const fl::json &args)
fl::shared_ptr< AutoResearchState > mState
static fl::RxChannelPtr addRx(const fl::RxChannelConfig &config)
Add an RX channel with runtime configuration.
fl::u8 getBrightness()
Get the current global brightness setting.
Definition FastLED.h:1605
void setBrightness(fl::u8 scale)
Set the global brightness scaling.
Definition FastLED.h:1601
const char * c_str() const FL_NO_EXCEPT
size_t size() const FL_NO_EXCEPT
Definition json.h:798
fl::optional< fl::string > as_string() const FL_NO_EXCEPT
Definition json.h:443
fl::optional< i64 > as_int() const FL_NO_EXCEPT
Definition json.h:398
void set(const fl::string &key, const json &value) FL_NO_EXCEPT
Definition json.h:866
fl::optional< bool > as_bool() const FL_NO_EXCEPT
Definition json.h:397
void push_back(const json &value) FL_NO_EXCEPT
Definition json.h:918
bool contains(size_t idx) const FL_NO_EXCEPT
Definition json.h:790
static json object() FL_NO_EXCEPT
Definition json.h:857
static json array() FL_NO_EXCEPT
Definition json.h:853
void reset() FL_NO_EXCEPT
Definition shared_ptr.h:286
string str() const FL_NO_EXCEPT
Definition strstream.h:43
fl::size size() const FL_NO_EXCEPT
bool empty() const FL_NO_EXCEPT
void clear() FL_NO_EXCEPT
Definition vector.h:634
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:9
fl::UISlider offset("Offset", 0.0f, 0.0f, 1.0f, 0.01f)
UCS7604 LED chipset encoder implementation.
constexpr EOrder RGB
Definition eorder.h:17
Task executor — runs registered task runners and manages the run loop.
@ CHANNELS
Remove all channels from controller list.
Definition FastLED.h:668
fl::CRGB CRGB
Definition crgb.h:25
Platform-abstracted heap memory query functions.
FastLED's Elegant JSON Library: fl::json
RxBackend resolveCaptureBackend(RxBackend requested_backend, bool has_explicit_override, bool is_parlio_driver, bool is_esp32_c6) FL_NO_EXCEPT
Select the independent capture backend used by AutoResearch.
Definition validation.h:50
size_t rpPioMaxWireBytes(size_t edge_capacity, size_t dma_tail_words, u32 samples_per_dma_word, u32 sample_period_ns, u32 idle_tail_ns, u32 arming_lead_in_ns, u32 bit_period_ns) FL_NO_EXCEPT
Largest frame, in wire bytes, an RP PIO RX capture can hold.
Definition validation.h:156
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
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_TM1812_RGBWW
TM1812: two 5-byte RGBWW pixels in each 12-byte IC frame.
@ CLOCKLESS_ENCODER_UCS7604_16BIT
UCS7604 16-bit 800KHz.
@ CLOCKLESS_ENCODER_UCS7604_16BIT_1600
UCS7604 16-bit 1600KHz.
@ CLOCKLESS_ENCODER_WS2812
Default, no preamble (WS2812 and compatible)
@ CLOCKLESS_ENCODER_TM1908
TM1908: command prefix followed by RGB pixels.
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
constexpr size_t kRpPioRxDmaTailWords
Reset-tail words appended to the DMA transfer.
constexpr u32 kRpPioRxSamplesPerDmaWord
Pin samples packed into each 32-bit DMA word.
constexpr ChipsetTimingConfig makeTimingConfig() FL_NO_EXCEPT
Convert compile-time CHIPSET type to runtime timing config.
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NO_EXCEPT
Definition unique_ptr.h:261
size_t ucs7604FrameBytes(size_t num_leds, UCS7604Mode mode, bool is_rgbw) FL_NO_EXCEPT
Total wire bytes of a complete UCS7604 frame.
Definition ucs7604.h:253
fl::u32 millis()
Universal millisecond timer - returns milliseconds since system startup.
fl::u32 micros()
Universal microsecond timer - returns microseconds since system startup.
RxBackend
Definition types.h:8
@ I2S_RX
Classic-ESP32 I2S 1-bit oversampling backend — DMA sampling at 16 MHz, capture length limited by RAM ...
Definition types.h:17
@ PIO
RP2040/RP2350 PIO receive backend.
Definition types.h:9
@ PLATFORM_DEFAULT
Use the recommended backend for the active platform (RMT on ESP32; FlexPWM on Teensy 4....
Definition types.h:10
@ RMT
ESP32-only RMT capture backend.
Definition types.h:11
@ ISR
Platform-neutral interrupt-driven edge capture backend when available.
Definition types.h:12
CRGB sample(const CRGB *grid, const XYMap &xyMap, float x, float y, SampleMode mode)
Sample a pixel from a 2D CRGB grid at floating-point coordinates.
Definition sample.cpp.hpp:9
constexpr size_t kRpPioRxEdgeCapacity
Phase slots in the shared capture pool.
Bus
Portable public bus selector.
Definition bus.h:27
@ FLEX_IO
Definition bus.h:36
@ UART
Definition bus.h:30
constexpr u32 kRpPioRxClockHz
After synchronizing to the first rising edge the PIO runs one IN PINS instruction per cycle,...
void pinMode(int pin, PinMode mode)
Set pin mode (input, output, pull-up, pull-down)
Definition pin.cpp.hpp:378
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
void analogWrite(int pin, u16 val)
Write analog value to pin (PWM)
Definition pin.cpp.hpp:227
InputGamut g
Definition rgbw.h:124
void delayNanoseconds(fl::u32 ns) FL_NO_EXCEPT
Delay for a runtime number of nanoseconds.
Definition delay.cpp.hpp:44
constexpr ClocklessEncoder encoder_for() FL_NO_EXCEPT
Extract the encoder selector from a compile-time TIMING type.
size_t ucs7604BytesPerLed(UCS7604Mode mode, bool is_rgbw) FL_NO_EXCEPT
Wire bytes each LED contributes to a UCS7604 frame.
Definition ucs7604.h:240
@ RMT
RMT-based receiver (ESP32)
Definition rx.h:167
void delayMicroseconds(u32 us)
Delay for a given number of microseconds.
corkscrew_args args
Definition old.h:149
Promise-based fluent API for FastLED - standalone async primitives.
#define FL_NO_EXCEPT
Umbrella header for SIMD subsystem.
fl::uptr uintptr_t
Definition stdint.h:223
unsigned char uint8_t
Definition stdint.h:208
fl::u32 uint32_t
Definition stdint.h:218
@ Black
<div style='background:#000000;width:4em;height:4em;'></div>
Definition crgb.h:517
Configuration for a single LED channel.
Definition config.h:167
constexpr u32 total_period_ns() const FL_NO_EXCEPT
Get total bit period (T1 + T2 + T3)
u32 reset_us
Reset/latch time (microseconds)
Runtime bit-period timing for a clockless chipset.
static SpiEncoder apa102(u32 clock_hz=6000000) FL_NO_EXCEPT
Create APA102 encoder configuration.
Definition spi.h:44