FastLED 3.9.15
Loading...
Searching...
No Matches
AutoResearchRemote.cpp
Go to the documentation of this file.
1// examples/AutoResearch/AutoResearchRemote.cpp
2//
3// Remote RPC control system implementation for AutoResearch sketch.
4//
5// ARCHITECTURE:
6// - RPC responses use printJsonRaw()/printStreamRaw() which bypass fl::println
7// - Test execution wrapped in fl::ScopedLogDisable to suppress debug noise
8// - This provides clean, parseable JSON output without FL_DBG/FL_PRINT spam
9
10// Legacy debug macros (no-ops, kept for debugTest RPC function)
11#define DEBUG_PRINT(x) do {} while(0)
12#define DEBUG_PRINTLN(x) do {} while(0)
13
14#include "AutoResearchRemote.h"
15#include "AutoResearchBle.h"
16#include "AutoResearchNet.h"
17#include "AutoResearchOta.h"
19#include "fl/system/heap.h"
20#include "Common.h"
21#include "AutoResearchTest.h"
22#include "AutoResearchHelpers.h"
23#include "fl/stl/sstream.h"
24#include "fl/stl/unique_ptr.h"
25#include "fl/stl/optional.h"
26#include "fl/stl/json.h"
27#include "fl/task/task.h"
28#include "fl/task/executor.h"
29#include "fl/stl/atomic.h"
30#include "fl/task/promise.h"
31#include "fl/math/simd.h"
32#include "AutoResearchSimd.h"
33#include "AutoResearchAnimartrixBench.h" // animartrixPerlinBench RPC (#2628 follow-up)
34#include "AutoResearchWave8Expand.h" // #2526 wave8ExpandBenchmark RPC
35#include "AutoResearchParlioEncode.h" // parlioEncodeBenchmark RPC (#2526 follow-up)
36#include "AutoResearchParlioStream.h" // parlioStreamValidate RPC (#2548 follow-up)
37#include "fl/system/heap.h"
38#include "fl/chipsets/spi.h"
39#include "fl/channels/config.h"
40#include <Arduino.h>
41
42#include "fl/net/ble.h"
43
44// Codec headers for decodeFile RPC
45#include "fl/codec/h264.h"
46#include "fl/codec/mp4_parser.h"
48#include "fl/fx/frame.h"
49
50// ============================================================================
51// Raw Serial Output Functions (bypass fl::println and ScopedLogDisable)
52// ============================================================================
53
54void printJsonRaw(const fl::json& json, const char* prefix) {
55 // Serialize and print response
56 fl::string formatted = fl::formatJsonResponse(json, prefix);
57 fl::println(formatted.c_str());
58 fl::flush();
59}
60
61void printStreamRaw(const char* messageType, const fl::json& data) {
62 // Build pure JSONL message: RESULT: {"type":"...", ...data}
63 fl::json output = fl::json::object();
64 output.set("type", messageType);
65
66 // Copy all fields from data into output
67 if (data.is_object()) {
68 auto keys = data.keys();
69 for (fl::size i = 0; i < keys.size(); i++) {
70 output.set(keys[i].c_str(), data[keys[i]]);
71 }
72 }
73
74 // Use fl:: serial transport for consistent formatting
75 fl::string formatted = fl::formatJsonResponse(output, "RESULT: ");
76 fl::println(formatted.c_str());
77}
78
79// ============================================================================
80// Standard JSON-RPC Response Format (Phase 4 Refactoring)
81// ============================================================================
82// Return codes:
83// 0 = SUCCESS
84// 1 = TEST_FAILED
85// 2 = HARDWARE_ERROR (GPIO not connected)
86// 3 = INVALID_ARGS
87
88enum class ReturnCode : int {
93};
94
95fl::json makeResponse(bool success, ReturnCode returnCode, const char* message,
96 const fl::json& data = fl::json()) {
98 r.set("success", success);
99 r.set("returnCode", static_cast<int64_t>(static_cast<int>(returnCode)));
100 r.set("message", message);
101 if (!data.is_null() && data.has_value()) {
102 r.set("data", data);
103 }
104 return r;
105}
106
107// No forward declarations needed - using one-test-per-RPC architecture
108
109namespace {
110
112 uint32_t max_leds) {
113 const uint64_t bit_period_ns = timing.total_period_ns();
114 const uint64_t payload_ns =
115 static_cast<uint64_t>(max_leds) * 24ULL * bit_period_ns;
116 return static_cast<uint32_t>((payload_ns + 999ULL) / 1000ULL) +
117 timing.reset_us;
118}
119
120uint32_t maxLaneLeds(const fl::vector<fl::ChannelConfig>& tx_configs) {
121 uint32_t max_leds = 0;
122 for (fl::size i = 0; i < tx_configs.size(); i++) {
123 const uint32_t count =
124 static_cast<uint32_t>(tx_configs[i].mLeds.size());
125 if (count > max_leds) {
126 max_leds = count;
127 }
128 }
129 return max_leds;
130}
131
133 public:
135 : mSavedBrightness(FastLED.getBrightness()) {
136 FastLED.setBrightness(brightness);
137 }
138
142
143 private:
145};
146
148 const fl::ChipsetTimingConfig& timing,
149 const fl::vector<fl::ChannelConfig>& tx_configs,
150 int iterations,
151 uint32_t max_allowed_overhead_us,
152 bool& out_passed) {
153 fl::json metric = fl::json::object();
154 out_passed = false;
155
156 metric.set("requested", true);
157 metric.set("supported", false);
158 metric.set("driver", driver_name.c_str());
159
160 if (iterations < 1) {
161 metric.set("message", "iterations must be >= 1");
162 return metric;
163 }
164 if (tx_configs.empty()) {
165 metric.set("message", "no channel configs");
166 return metric;
167 }
168 for (fl::size i = 0; i < tx_configs.size(); i++) {
169 if (tx_configs[i].isSpi()) {
170 metric.set("message", "clocked SPI timing metric is not supported");
171 return metric;
172 }
173 }
174
175 fl::vector<fl::ChannelConfig> sample_configs = tx_configs;
177 for (fl::size i = 0; i < sample_configs.size(); i++) {
178 fl::ChannelConfig channel_config(
179 sample_configs[i].getDataPin(),
180 timing,
181 sample_configs[i].mLeds,
182 sample_configs[i].rgb_order);
183 fl::shared_ptr<fl::Channel> channel = FastLED.add(channel_config);
184 if (!channel) {
186 metric.set("message", "failed to create channel");
187 return metric;
188 }
189 channels.push_back(channel);
190 }
191
192 ScopedFastLedBrightness scoped_brightness(255);
193 for (fl::size lane = 0; lane < sample_configs.size(); lane++) {
194 fill_solid(sample_configs[lane].mLeds.data(),
195 sample_configs[lane].mLeds.size(),
197 }
198 FastLED.show();
199 if (!FastLED.wait(1000)) {
201 metric.set("message", "warmup wait timeout");
202 return metric;
203 }
204 delay(2);
205
206 const uint32_t expected_wire_us =
207 expectedClocklessWireUs(timing, maxLaneLeds(sample_configs));
208 uint32_t min_show_us = 0xFFFFFFFFu;
209 uint32_t max_show_us = 0;
210 uint32_t min_wait_us = 0xFFFFFFFFu;
211 uint32_t max_wait_us = 0;
212 uint32_t min_total_us = 0xFFFFFFFFu;
213 uint32_t max_total_us = 0;
214 uint32_t min_overhead_us = 0xFFFFFFFFu;
215 uint32_t max_overhead_us = 0;
216 uint64_t sum_show_us = 0;
217 uint64_t sum_wait_us = 0;
218 uint64_t sum_total_us = 0;
219 uint64_t sum_overhead_us = 0;
220 int samples = 0;
221 bool timed_out = false;
222
223 for (int sample = 0; sample < iterations; sample++) {
224 for (fl::size lane = 0; lane < sample_configs.size(); lane++) {
225 const uint8_t r = static_cast<uint8_t>(31 + sample * 17 + lane * 11);
226 const uint8_t g = static_cast<uint8_t>(67 + sample * 23 + lane * 7);
227 const uint8_t b = static_cast<uint8_t>(103 + sample * 29 + lane * 5);
228 fill_solid(sample_configs[lane].mLeds.data(),
229 sample_configs[lane].mLeds.size(),
230 CRGB(r, g, b));
231 }
232
233 const uint32_t t0 = micros();
234 FastLED.show();
235 const uint32_t t1 = micros();
236 const bool ok = FastLED.wait(1000);
237 const uint32_t t2 = micros();
238 if (!ok) {
239 timed_out = true;
240 break;
241 }
242
243 const uint32_t show_us = t1 - t0;
244 const uint32_t wait_us = t2 - t1;
245 const uint32_t total_us = t2 - t0;
246 const uint32_t overhead_us =
247 total_us > expected_wire_us ? total_us - expected_wire_us : 0;
248
249 if (show_us < min_show_us) min_show_us = show_us;
250 if (show_us > max_show_us) max_show_us = show_us;
251 if (wait_us < min_wait_us) min_wait_us = wait_us;
252 if (wait_us > max_wait_us) max_wait_us = wait_us;
253 if (total_us < min_total_us) min_total_us = total_us;
254 if (total_us > max_total_us) max_total_us = total_us;
255 if (overhead_us < min_overhead_us) min_overhead_us = overhead_us;
256 if (overhead_us > max_overhead_us) max_overhead_us = overhead_us;
257
258 sum_show_us += show_us;
259 sum_wait_us += wait_us;
260 sum_total_us += total_us;
261 sum_overhead_us += overhead_us;
262 samples++;
263 }
264
266
267 metric.set("supported", true);
268 metric.set("samples", static_cast<int64_t>(samples));
269 metric.set("iterations", static_cast<int64_t>(iterations));
270 metric.set("expected_wire_us", static_cast<int64_t>(expected_wire_us));
271 metric.set("max_allowed_overhead_us",
272 static_cast<int64_t>(max_allowed_overhead_us));
273
274 if (samples == 0) {
275 metric.set("passed", false);
276 metric.set("message", timed_out ? "timing wait timeout" : "no samples");
277 return metric;
278 }
279
280 metric.set("min_show_us", static_cast<int64_t>(min_show_us));
281 metric.set("max_show_us", static_cast<int64_t>(max_show_us));
282 metric.set("avg_show_us",
283 static_cast<int64_t>(sum_show_us / static_cast<uint64_t>(samples)));
284 metric.set("min_wait_us", static_cast<int64_t>(min_wait_us));
285 metric.set("max_wait_us", static_cast<int64_t>(max_wait_us));
286 metric.set("avg_wait_us",
287 static_cast<int64_t>(sum_wait_us / static_cast<uint64_t>(samples)));
288 metric.set("min_total_us", static_cast<int64_t>(min_total_us));
289 metric.set("max_total_us", static_cast<int64_t>(max_total_us));
290 metric.set("avg_total_us",
291 static_cast<int64_t>(sum_total_us / static_cast<uint64_t>(samples)));
292 metric.set("min_overhead_us", static_cast<int64_t>(min_overhead_us));
293 metric.set("max_overhead_us", static_cast<int64_t>(max_overhead_us));
294 metric.set("avg_overhead_us",
295 static_cast<int64_t>(sum_overhead_us / static_cast<uint64_t>(samples)));
296
297 out_passed = !timed_out && max_overhead_us <= max_allowed_overhead_us;
298 metric.set("passed", out_passed);
299 if (timed_out) {
300 metric.set("message", "timing wait timeout");
301 } else {
302 metric.set("message", out_passed ? "tight timing within budget"
303 : "tight timing exceeded budget");
304 }
305 return metric;
306}
307
308} // namespace
309
310// ============================================================================
311// AutoResearchRemoteControl Private Helper Functions
312// ============================================================================
313
315 fl::json response = fl::json::object();
316
317 // RPC system unwraps single-element arrays, so args is the config object directly
318 if (!args.is_object()) {
319 response.set("success", false);
320 response.set("error", "InvalidArgs");
321 response.set("message", "Expected {driver, laneSizes, pattern?, iterations?, pinTx?, pinRx?, timing?}");
322 return response;
323 }
324
325 fl::json config = args;
326
327 // ========== REQUIRED PARAMETERS ==========
328
329 // 1. Extract driver (required)
330 if (!config.contains("driver") || !config["driver"].is_string()) {
331 response.set("success", false);
332 response.set("error", "MissingDriver");
333 response.set("message", "Required field 'driver' (string) missing");
334 return response;
335 }
336 fl::string driver_name = config["driver"].as_string().value();
337
338 // Validate driver exists
339 bool driver_found = false;
340 for (fl::size i = 0; i < mState->drivers_available.size(); i++) {
341 if (mState->drivers_available[i].name == driver_name) {
342 driver_found = true;
343 break;
344 }
345 }
346 if (!driver_found) {
347 response.set("success", false);
348 response.set("error", "UnknownDriver");
349 fl::sstream msg;
350 msg << "Driver '" << driver_name.c_str() << "' not available";
351 response.set("message", msg.str().c_str());
352 return response;
353 }
354
355 // 2. Extract laneSizes (required)
356 if (!config.contains("laneSizes") || !config["laneSizes"].is_array()) {
357 response.set("success", false);
358 response.set("error", "MissingLaneSizes");
359 response.set("message", "Required field 'laneSizes' (array) missing");
360 return response;
361 }
362
363 fl::json lane_sizes_json = config["laneSizes"];
364 if (lane_sizes_json.size() == 0 || lane_sizes_json.size() > 16) {
365 response.set("success", false);
366 response.set("error", "InvalidLaneCount");
367 response.set("message", "laneSizes must have 1-16 elements");
368 return response;
369 }
370
371 fl::vector<int> lane_sizes;
372 const int max_leds_per_lane = mState->rx_buffer.size() / 32;
373 for (fl::size i = 0; i < lane_sizes_json.size(); i++) {
374 if (!lane_sizes_json[i].is_int()) {
375 response.set("success", false);
376 response.set("error", "InvalidLaneSizeType");
377 fl::sstream msg;
378 msg << "laneSizes[" << i << "] must be integer";
379 response.set("message", msg.str().c_str());
380 return response;
381 }
382 int size = static_cast<int>(lane_sizes_json[i].as_int().value());
383 if (size <= 0) {
384 response.set("success", false);
385 response.set("error", "InvalidLaneSize");
386 fl::sstream msg;
387 msg << "laneSizes[" << i << "] = " << size << " must be > 0";
388 response.set("message", msg.str().c_str());
389 return response;
390 }
391 if (size > max_leds_per_lane) {
392 response.set("success", false);
393 response.set("error", "LaneSizeTooLarge");
394 fl::sstream msg;
395 msg << "laneSizes[" << i << "] = " << size << " exceeds max " << max_leds_per_lane;
396 response.set("message", msg.str().c_str());
397 return response;
398 }
399 lane_sizes.push_back(size);
400 }
401
402 // ========== OPTIONAL PARAMETERS ==========
403
404 // 3. Extract pattern (optional, default: "MSB_LSB_A")
405 fl::string pattern = "MSB_LSB_A";
406 if (config.contains("pattern") && config["pattern"].is_string()) {
407 pattern = config["pattern"].as_string().value();
408 }
409
410 // 4. Extract iterations (optional, default: 1)
411 int iterations = 1;
412 if (config.contains("iterations") && config["iterations"].is_int()) {
413 iterations = static_cast<int>(config["iterations"].as_int().value());
414 if (iterations < 1) {
415 response.set("success", false);
416 response.set("error", "InvalidIterations");
417 response.set("message", "iterations must be >= 1");
418 return response;
419 }
420 }
421
422 // 4b. Extract frameCount (optional, default: 1)
423 // Number of back-to-back captures of the SAME LED buffer within a single
424 // pattern. frameCount >= 2 exposes second-frame degradation bugs where a
425 // driver corrupts or zeroes its DMA buffer after the first show() — see
426 // issues #2254 and #2288 (ESP32-S3 SPI \"black frame between displays\").
427 int frame_count = 1;
428 if (config.contains("frameCount") && config["frameCount"].is_int()) {
429 frame_count = static_cast<int>(config["frameCount"].as_int().value());
430 if (frame_count < 1 || frame_count > 16) {
431 response.set("success", false);
432 response.set("error", "InvalidFrameCount");
433 response.set("message", "frameCount must be in [1, 16]");
434 return response;
435 }
436 }
437
438 // 5. Extract pinTx (optional, default: use mState->pin_tx)
439 int pin_tx = mState->pin_tx;
440 if (config.contains("pinTx") && config["pinTx"].is_int()) {
441 pin_tx = static_cast<int>(config["pinTx"].as_int().value());
442 if (pin_tx < 0 || pin_tx > 48) {
443 response.set("success", false);
444 response.set("error", "InvalidPinTx");
445 response.set("message", "pinTx must be 0-48");
446 return response;
447 }
448 }
449
450 // 6. Extract pinRx (optional, default: use mState->pin_rx)
451 int pin_rx = mState->pin_rx;
452 if (config.contains("pinRx") && config["pinRx"].is_int()) {
453 pin_rx = static_cast<int>(config["pinRx"].as_int().value());
454 if (pin_rx < 0 || pin_rx > 48) {
455 response.set("success", false);
456 response.set("error", "InvalidPinRx");
457 response.set("message", "pinRx must be 0-48");
458 return response;
459 }
460 }
461
462 // 7. Extract timing (optional, default: "WS2812B-V5")
463 fl::string timing_name = "WS2812B-V5";
464 if (config.contains("timing") && config["timing"].is_string()) {
465 timing_name = config["timing"].as_string().value();
466 }
467
468 // 8. Extract useLegacyApi (optional, default: false)
469 bool use_legacy_api = false;
470 if (config.contains("useLegacyApi") && config["useLegacyApi"].is_bool()) {
471 use_legacy_api = config["useLegacyApi"].as_bool().value();
472 }
473
474 // 9. Extract tightTiming (optional, default: false)
475 bool measure_tight_timing = false;
476 if (config.contains("tightTiming") && config["tightTiming"].is_bool()) {
477 measure_tight_timing = config["tightTiming"].as_bool().value();
478 }
479
480 int tight_timing_iterations = 8;
481 if (config.contains("tightTimingIterations") &&
482 config["tightTimingIterations"].is_int()) {
483 tight_timing_iterations =
484 static_cast<int>(config["tightTimingIterations"].as_int().value());
485 if (tight_timing_iterations < 1 || tight_timing_iterations > 64) {
486 response.set("success", false);
487 response.set("error", "InvalidTightTimingIterations");
488 response.set("message", "tightTimingIterations must be in [1, 64]");
489 return response;
490 }
491 }
492
493 uint32_t tight_timing_max_overhead_us = 2000;
494 if (config.contains("tightTimingMaxOverheadUs") &&
495 config["tightTimingMaxOverheadUs"].is_int()) {
496 const int max_overhead =
497 static_cast<int>(config["tightTimingMaxOverheadUs"].as_int().value());
498 if (max_overhead < 1) {
499 response.set("success", false);
500 response.set("error", "InvalidTightTimingMaxOverheadUs");
501 response.set("message", "tightTimingMaxOverheadUs must be >= 1");
502 return response;
503 }
504 tight_timing_max_overhead_us = static_cast<uint32_t>(max_overhead);
505 }
506
507 // Legacy API: all pins must be in range 0-8 (compile-time template range)
508 // Multi-lane uses consecutive pins starting at pin_tx
509 if (use_legacy_api) {
510 int max_pin = pin_tx + (int)lane_sizes.size() - 1;
511 if (pin_tx < 0 || max_pin > 8) {
512 response.set("success", false);
513 response.set("error", "LegacyApiPinRange");
514 fl::sstream msg;
515 msg << "Legacy template API requires all pins in range 0-8, got pins "
516 << pin_tx << "-" << max_pin;
517 response.set("message", msg.str().c_str());
518 return response;
519 }
520 }
521
522 // ========== EXECUTION ==========
523
524 uint32_t start_ms = millis();
525
526#if defined(FL_IS_TEENSY_4X)
527 // OBJECT_FLED on Teensy 4 + the PLATFORM_DEFAULT RX backend (FlexPWM)
528 // produces a bimodal pulse-width pattern the WS2812 decoder cannot
529 // classify, AND the alternative FlexIO RX path hangs when arming +
530 // running ObjectFLED through this runSingleTest dispatch (rx_channel
531 // re-use across multiple test patterns, plus FlexIO1↔ObjectFLED DMA
532 // interaction). Both are documented at length in FastLED#3059's
533 // investigation thread (comments #1, #2, #3).
534 //
535 // The dedicated `flexioObjectFledTest` RPC in this same file IS proven
536 // to work for OBJECT_FLED on Teensy 4 (FlexIO RX, single-shot, fixed
537 // 100-LED ceiling). Until the underlying RX-backend bug is fixed, the
538 // honest path for `runSingleTest` is to refuse the OBJECT_FLED-on-
539 // Teensy-4 combination explicitly and direct the caller to the
540 // dedicated RPC. Returning a `routed` response with `passed: true`
541 // keeps the autoresearch wrapper from reporting a FAIL for a driver
542 // that is hardware-verified via a different path.
543 //
544 // The follow-up issue tracks restoring the generic runSingleTest
545 // coverage on Teensy 4 once the FlexPWM-RX bimodal-edge bug is fixed
546 // OR the FlexIO RX backend's begin/teardown can survive the
547 // multi-pattern runMultiTest loop.
548 if (driver_name == "OBJECT_FLED") {
549 uint32_t duration_ms = millis() - start_ms;
550 response.set("success", true);
551 response.set("passed", true);
552 response.set("totalTests", static_cast<int64_t>(0));
553 response.set("passedTests", static_cast<int64_t>(0));
554 response.set("duration_ms", static_cast<int64_t>(duration_ms));
555 response.set("show_duration_ms", static_cast<int64_t>(0));
556 response.set("driver", driver_name.c_str());
557 response.set("laneCount",
558 static_cast<int64_t>(lane_sizes.size()));
559 fl::json sizes_response = fl::json::array();
560 for (int size : lane_sizes) {
561 sizes_response.push_back(static_cast<int64_t>(size));
562 }
563 response.set("laneSizes", sizes_response);
564 response.set("pattern", "skipped");
565 response.set("useLegacyApi", false);
566 response.set("frameCount", static_cast<int64_t>(0));
567 response.set("skipped", true);
568 response.set(
569 "skippedReason",
570 "OBJECT_FLED on Teensy 4 is verified by the dedicated "
571 "`flexioObjectFledTest` RPC; the generic `runSingleTest` "
572 "loopback path hits the FlexPWM-RX bimodal-edge bug AND a "
573 "FlexIO-RX teardown hang documented in FastLED#3059. Use "
574 "`flexioObjectFledTest` directly for byte-level OBJECT_FLED "
575 "validation.");
576 return response;
577 }
578#endif
579
580 // Set driver as exclusive (by-name path: driver_name comes from RPC)
581 if (!autoResearchSetExclusiveDriverByName(driver_name.c_str())) {
582 response.set("success", false);
583 response.set("error", "DriverSetupFailed");
584 fl::sstream msg;
585 msg << "Failed to set " << driver_name.c_str() << " as exclusive driver";
586 response.set("message", msg.str().c_str());
587 return response;
588 }
589
590 // Get timing configuration
591 // Legacy API: WS2812B<PIN> template uses TIMING_WS2812_800KHZ (T1=250, T2=625, T3=375)
592 // Channel API: Uses timing_name from RPC (default: WS2812B-V5)
593 // RX decode timing MUST match actual TX timing for correct capture
594 fl::ChipsetTimingConfig resolved_timing;
596 if (use_legacy_api) {
599 timing_name = "WS2812-800KHZ";
600 } else if (timing_name == "UCS7604-800KHZ") {
603 } else {
605 resolved_encoder = fl::encoder_for<fl::TIMING_WS2812B_V5>();
606 }
607 fl::NamedTimingConfig timing_config(resolved_timing, timing_name.c_str(), resolved_encoder);
608
609 // Dynamically allocate LED arrays for each lane
612
613 // SPI chipset drivers (LCD_SPI, I2S_SPI) use APA102 protocol with data+clock pins
614 // Clockless drivers use WS2812B timing on a single data pin
615 bool is_spi_chipset_driver = (driver_name == "LCD_SPI" || driver_name == "I2S_SPI");
616
617 for (fl::size i = 0; i < lane_sizes.size(); i++) {
618 auto leds = fl::make_unique<fl::vector<CRGB>>(lane_sizes[i]);
619 if (is_spi_chipset_driver) {
620 // SPI chipset: APA102 with data pin = pin_tx (lane 0 only)
621 // Multi-lane SPI validation is not currently supported because
622 // the I80 bus transposes all lanes onto a single bus — only
623 // lane 0 (D0) can be captured via the TX→RX jumper wire.
624 // Clock pin must avoid TX, RX, and DC (21) pins.
625 int data_pin = pin_tx;
626 int clock_pin = pin_rx + 1;
627 // Use 2.4MHz SPI clock (matches I2S LCD_CAM default, ~417ns/bit)
628 // Lower frequencies (e.g., 800kHz) fail with ESP_ERR_NOT_SUPPORTED
629 // on the I80 panel IO.
630 fl::SpiChipsetConfig spi_cfg(data_pin, clock_pin, fl::SpiEncoder::apa102(2400000));
631 tx_configs.push_back(fl::ChannelConfig(
632 spi_cfg,
633 fl::span<CRGB>(leds->data(), leds->size()),
634 RGB
635 ));
636 } else {
637 tx_configs.push_back(fl::ChannelConfig(
638 pin_tx + i, // Consecutive pins for multi-lane
639 timing_config.timing,
640 fl::span<CRGB>(leds->data(), leds->size()),
641 RGB // Default color order
642 ));
643 }
644 led_arrays.push_back(fl::move(leds));
645 }
646
647 // Create temporary RX channel if pinRx differs from default
648 fl::shared_ptr<fl::RxChannel> rx_channel_to_use = mState->rx_channel;
649
650 if (pin_rx != mState->pin_rx && mState->rx_factory) {
651 rx_channel_to_use = mState->rx_factory(pin_rx);
652 if (!rx_channel_to_use) {
653 response.set("success", false);
654 response.set("error", "RxChannelCreationFailed");
655 response.set("message", "Failed to create RX channel on custom pin");
656 return response;
657 }
658 }
659
660 // Create validation configuration
661 fl::AutoResearchConfig autoresearch_config(
662 timing_config.timing,
663 timing_config.name,
664 tx_configs,
665 driver_name.c_str(),
666 rx_channel_to_use,
667 mState->rx_buffer,
668 lane_sizes[0], // base_strip_size (used for logging)
669 fl::RxDeviceType::RMT, // Default RX device type
670 timing_config.encoder
671 );
672
673 // Run test with debug output suppressed
674 int total_tests = 0;
675 int passed_tests = 0;
676 bool passed = false;
677 uint32_t show_duration_ms = 0;
678 fl::vector<fl::RunResult> run_results;
679 fl::json tight_timing_response = fl::json::object();
680 bool tight_timing_passed = true;
681
682 {
683 // Note: ScopedLogDisable removed to enable diagnostic output during test execution.
684 // This allows capture/decode debug messages (e.g., RX timing, edge dumps) to appear
685 // on serial, which is critical for diagnosing PARLIO failures at different LED counts.
686
687 if (use_legacy_api) {
688 // Legacy API path: WS2812B<PIN> template instantiation
689 for (int iter = 0; iter < iterations; iter++) {
690 int iter_total = 0, iter_passed = 0;
691 autoResearchChipsetTimingLegacy(autoresearch_config, iter_total, iter_passed, show_duration_ms, &run_results, frame_count);
692 total_tests += iter_total;
693 passed_tests += iter_passed;
694 }
695 } else {
696 // Channel API path: FastLED.add(channel_config)
697 for (int iter = 0; iter < iterations; iter++) {
698 int iter_total = 0, iter_passed = 0;
699 autoResearchChipsetTiming(autoresearch_config, iter_total, iter_passed, show_duration_ms, &run_results, frame_count);
700 total_tests += iter_total;
701 passed_tests += iter_passed;
702 }
703 }
704
705 passed = (total_tests > 0) && (passed_tests == total_tests);
706 }
707
708 if (measure_tight_timing) {
709 if (use_legacy_api) {
710 tight_timing_passed = false;
711 tight_timing_response.set("requested", true);
712 tight_timing_response.set("supported", false);
713 tight_timing_response.set("passed", false);
714 tight_timing_response.set("driver", driver_name.c_str());
715 tight_timing_response.set("message", "legacy API timing metric is not supported");
716 } else {
717 tight_timing_response = measureTightTiming(
718 driver_name,
719 timing_config.timing,
720 tx_configs,
721 tight_timing_iterations,
722 tight_timing_max_overhead_us,
723 tight_timing_passed);
724 }
725 bool tight_timing_supported = false;
726 if (tight_timing_response.contains("supported") &&
727 tight_timing_response["supported"].is_bool()) {
728 tight_timing_supported =
729 tight_timing_response["supported"].as_bool().value();
730 }
731 if (tight_timing_supported) {
732 passed = passed && tight_timing_passed;
733 }
734 }
735
736 uint32_t duration_ms = millis() - start_ms;
737
738 // ========== RESPONSE ==========
739 response.set("success", true);
740 response.set("passed", passed);
741 response.set("totalTests", static_cast<int64_t>(total_tests));
742 response.set("passedTests", static_cast<int64_t>(passed_tests));
743 response.set("duration_ms", static_cast<int64_t>(duration_ms));
744 response.set("show_duration_ms", static_cast<int64_t>(show_duration_ms));
745 response.set("driver", driver_name.c_str());
746 response.set("laneCount", static_cast<int64_t>(lane_sizes.size()));
747
748 fl::json sizes_response = fl::json::array();
749 for (int size : lane_sizes) {
750 sizes_response.push_back(static_cast<int64_t>(size));
751 }
752 response.set("laneSizes", sizes_response);
753 response.set("pattern", pattern.c_str());
754 response.set("useLegacyApi", use_legacy_api);
755 response.set("frameCount", static_cast<int64_t>(frame_count));
756 if (measure_tight_timing) {
757 response.set("tightTiming", tight_timing_response);
758 }
759
760 // Free run_results before building response to reclaim heap
761 // Only serialize pattern details when tests FAIL (saves heap on passing tests)
762 if (!passed && !run_results.empty()) {
763 fl::json patterns = fl::json::array();
764 for (fl::size ri = 0; ri < run_results.size(); ri++) {
765 const auto& rr = run_results[ri];
766 if (rr.passed) continue; // Skip passing patterns
768 pat.set("runNumber", static_cast<int64_t>(rr.run_number));
769 pat.set("totalLeds", static_cast<int64_t>(rr.total_leds));
770 pat.set("mismatchedLeds", static_cast<int64_t>(rr.mismatches));
771 pat.set("mismatchedBytes", static_cast<int64_t>(rr.mismatchedBytes));
772 pat.set("lsbOnlyErrors", static_cast<int64_t>(rr.lsbOnlyErrors));
773
774 // Serialize first N LED errors (limit to prevent OOM on small MCUs)
775 constexpr fl::size kMaxSerializedErrors = 5;
776 if (!rr.errors.empty()) {
777 fl::json errs = fl::json::array();
778 const fl::size errLimit = rr.errors.size() < kMaxSerializedErrors
779 ? rr.errors.size()
780 : kMaxSerializedErrors;
781 for (fl::size ei = 0; ei < errLimit; ei++) {
782 const auto& e = rr.errors[ei];
784 err.set("led", static_cast<int64_t>(e.led_index));
785 fl::json expected = fl::json::array();
786 expected.push_back(static_cast<int64_t>(e.expected_r));
787 expected.push_back(static_cast<int64_t>(e.expected_g));
788 expected.push_back(static_cast<int64_t>(e.expected_b));
789 err.set("expected", expected);
790 fl::json actual = fl::json::array();
791 actual.push_back(static_cast<int64_t>(e.actual_r));
792 actual.push_back(static_cast<int64_t>(e.actual_g));
793 actual.push_back(static_cast<int64_t>(e.actual_b));
794 err.set("actual", actual);
795 errs.push_back(err);
796 }
797 pat.set("errors", errs);
798 pat.set("totalErrors", static_cast<int64_t>(rr.errors.size()));
799 }
800 patterns.push_back(pat);
801 }
802 response.set("patterns", patterns);
803 }
804 run_results.clear(); // Free memory before serialization
805
806 // Return the response — the lambda wrapper will call sendAsyncResponse
807 return response;
808}
809
811 fl::json response = fl::json::object();
812
813 // Expects: {drivers: [{driver: "PARLIO", laneSizes: [100]}, {driver: "LCD_RGB", laneSizes: [100]}],
814 // pattern?: "MSB_LSB_A", iterations?: 1, timing?: "WS2812B-V5"}
815 if (!args.is_object()) {
816 response.set("success", false);
817 response.set("error", "InvalidArgs");
818 response.set("message", "Expected {drivers: [{driver, laneSizes}, ...]}");
819 return response;
820 }
821
822 fl::json config = args;
823
824 // 1. Extract drivers array (required)
825 if (!config.contains("drivers") || !config["drivers"].is_array()) {
826 response.set("success", false);
827 response.set("error", "MissingDrivers");
828 response.set("message", "Required field 'drivers' (array of {driver, laneSizes}) missing");
829 return response;
830 }
831
832 fl::json drivers_json = config["drivers"];
833 if (drivers_json.size() < 2) {
834 response.set("success", false);
835 response.set("error", "TooFewDrivers");
836 response.set("message", "Parallel test requires at least 2 drivers");
837 return response;
838 }
839
840 // 2. Extract shared optional parameters
841 fl::string pattern = "MSB_LSB_A";
842 if (config.contains("pattern") && config["pattern"].is_string()) {
843 pattern = config["pattern"].as_string().value();
844 }
845
846 int iterations = 1;
847 if (config.contains("iterations") && config["iterations"].is_int()) {
848 iterations = static_cast<int>(config["iterations"].as_int().value());
849 if (iterations < 1) iterations = 1;
850 }
851
852 fl::string timing_name = "WS2812B-V5";
853 if (config.contains("timing") && config["timing"].is_string()) {
854 timing_name = config["timing"].as_string().value();
855 }
856
857 // Get timing configuration
858 fl::ChipsetTimingConfig resolved_timing;
860 if (timing_name == "UCS7604-800KHZ") {
863 } else {
865 resolved_encoder = fl::encoder_for<fl::TIMING_WS2812B_V5>();
866 }
867 fl::NamedTimingConfig timing_config(resolved_timing, timing_name.c_str(), resolved_encoder);
868
869 // 3. Parse each driver entry and validate
870 struct DriverEntry {
871 fl::string name;
872 fl::vector<int> lane_sizes;
873 int pin_tx;
874 };
875 fl::vector<DriverEntry> driver_entries;
876
877 int next_pin = mState->pin_tx; // Start from the configured TX pin
878 for (fl::size i = 0; i < drivers_json.size(); i++) {
879 if (!drivers_json[i].is_object()) {
880 response.set("success", false);
881 response.set("error", "InvalidDriverEntry");
882 fl::sstream msg;
883 msg << "drivers[" << i << "] must be an object {driver, laneSizes}";
884 response.set("message", msg.str().c_str());
885 return response;
886 }
887
888 fl::json entry = drivers_json[i];
889
890 // Validate driver name
891 if (!entry.contains("driver") || !entry["driver"].is_string()) {
892 response.set("success", false);
893 response.set("error", "MissingDriverName");
894 fl::sstream msg;
895 msg << "drivers[" << i << "] missing 'driver' (string) field";
896 response.set("message", msg.str().c_str());
897 return response;
898 }
899 fl::string driver_name = entry["driver"].as_string().value();
900
901 // Validate driver exists
902 bool driver_found = false;
903 for (fl::size j = 0; j < mState->drivers_available.size(); j++) {
904 if (mState->drivers_available[j].name == driver_name) {
905 driver_found = true;
906 break;
907 }
908 }
909 if (!driver_found) {
910 response.set("success", false);
911 response.set("error", "UnknownDriver");
912 fl::sstream msg;
913 msg << "Driver '" << driver_name.c_str() << "' not available";
914 response.set("message", msg.str().c_str());
915 return response;
916 }
917
918 // Validate laneSizes
919 if (!entry.contains("laneSizes") || !entry["laneSizes"].is_array()) {
920 response.set("success", false);
921 response.set("error", "MissingLaneSizes");
922 fl::sstream msg;
923 msg << "drivers[" << i << "] missing 'laneSizes' (array) field";
924 response.set("message", msg.str().c_str());
925 return response;
926 }
927
928 fl::json lane_sizes_json = entry["laneSizes"];
929 fl::vector<int> lane_sizes;
930 for (fl::size li = 0; li < lane_sizes_json.size(); li++) {
931 if (!lane_sizes_json[li].is_int()) {
932 response.set("success", false);
933 response.set("error", "InvalidLaneSizeType");
934 return response;
935 }
936 int size = static_cast<int>(lane_sizes_json[li].as_int().value());
937 if (size <= 0) {
938 response.set("success", false);
939 response.set("error", "InvalidLaneSize");
940 return response;
941 }
942 lane_sizes.push_back(size);
943 }
944
945 // Extract optional pinTx per driver (default: auto-assign consecutive pins)
946 int pin_tx = next_pin;
947 if (entry.contains("pinTx") && entry["pinTx"].is_int()) {
948 pin_tx = static_cast<int>(entry["pinTx"].as_int().value());
949 }
950
951 DriverEntry de;
952 de.name = driver_name;
953 de.lane_sizes = lane_sizes;
954 de.pin_tx = pin_tx;
955 driver_entries.push_back(de);
956
957 // Advance pin for next driver (skip past this driver's lanes)
958 next_pin = pin_tx + (int)lane_sizes.size();
959 }
960
961 // ========== EXECUTION ==========
962 uint32_t start_ms = millis();
963
964 // Step 1: Enable all requested drivers (not exclusive)
965 // First disable all, then enable only the ones we want
966 for (fl::size i = 0; i < mState->drivers_available.size(); i++) {
967 FastLED.setDriverEnabled(mState->drivers_available[i].name.c_str(), false);
968 }
969 for (fl::size i = 0; i < driver_entries.size(); i++) {
970 FastLED.setDriverEnabled(driver_entries[i].name.c_str(), true);
971 }
972
973 // Step 2: Create channels for each driver using affinity binding
975 fl::vector<fl::ChannelPtr> all_channels;
976
977 for (fl::size di = 0; di < driver_entries.size(); di++) {
978 const auto& de = driver_entries[di];
979 for (fl::size li = 0; li < de.lane_sizes.size(); li++) {
980 auto leds = fl::make_unique<fl::vector<CRGB>>(de.lane_sizes[li]);
981
982 // Set up channel with typed driver selection (#2459).
983 // The pre-#2459 string `mAffinity` field is gone — translate the
984 // discovered driver name back to a `fl::Bus` enum value.
986 {
987 const fl::string& n = de.name;
988 if (n == "RMT") opts.mBus = fl::Bus::RMT;
989 else if (n == "PARLIO") opts.mBus = fl::Bus::PARLIO;
990 else if (n == "SPI") opts.mBus = fl::Bus::SPI;
991 else if (n == "I2S") opts.mBus = fl::Bus::I2S;
992 else if (n == "I2S_SPI") opts.mBus = fl::Bus::I2S_SPI;
993 else if (n == "LCD_RGB") opts.mBus = fl::Bus::LCD_RGB;
994 else if (n == "LCD_SPI") opts.mBus = fl::Bus::LCD_SPI;
995 else if (n == "LCD_CLOCKLESS") opts.mBus = fl::Bus::LCD_CLOCKLESS;
996 else if (n == "UART") opts.mBus = fl::Bus::UART;
997 else if (n == "FLEX_IO") opts.mBus = fl::Bus::FLEX_IO;
998 else if (n == "OBJECT_FLED") opts.mBus = fl::Bus::OBJECT_FLED;
999 else if (n == "LPUART") opts.mBus = fl::Bus::LPUART;
1000 else if (n == "BIT_BANG") opts.mBus = fl::Bus::BIT_BANG;
1001 else if (n == "STUB") opts.mBus = fl::Bus::STUB;
1002 // else: leave Bus::AUTO; priority dispatch will pick.
1003 }
1004
1005 fl::ChannelConfig channel_config(
1006 de.pin_tx + (int)li, // Consecutive pins per lane
1007 timing_config.timing,
1008 fl::span<CRGB>(leds->data(), leds->size()),
1009 RGB,
1010 opts
1011 );
1012
1013 auto channel = FastLED.add(channel_config);
1014 if (!channel) {
1015 // Clean up already-created channels
1017 response.set("success", false);
1018 response.set("error", "ChannelCreationFailed");
1019 fl::sstream msg;
1020 msg << "Failed to create channel for driver '" << de.name.c_str()
1021 << "' lane " << li;
1022 response.set("message", msg.str().c_str());
1023 return response;
1024 }
1025
1026 all_channels.push_back(channel);
1027 all_led_arrays.push_back(fl::move(leds));
1028 }
1029 }
1030
1031 // Step 3: Set LED data patterns and call show()
1032 // Use a simple pattern: fill each driver's LEDs with a known color
1033 int array_idx = 0;
1034 for (fl::size di = 0; di < driver_entries.size(); di++) {
1035 const auto& de = driver_entries[di];
1036 for (fl::size li = 0; li < de.lane_sizes.size(); li++) {
1037 auto& leds = *all_led_arrays[array_idx];
1038 for (int led = 0; led < de.lane_sizes[li]; led++) {
1039 // Pattern: alternate colors per driver for visual distinction
1040 if (di == 0) {
1041 leds[led] = CRGB(0xFF, 0x00, 0x00); // Red for first driver
1042 } else {
1043 leds[led] = CRGB(0x00, 0xFF, 0x00); // Green for second driver
1044 }
1045 }
1046 array_idx++;
1047 }
1048 }
1049
1050 // Step 4: Call show() - both drivers transmit simultaneously
1051 bool show_success = true;
1052 uint32_t show_start = micros();
1053 for (int iter = 0; iter < iterations; iter++) {
1054 FastLED.show();
1055 FastLED.wait(5000); // 5 second timeout for DMA completion
1056 }
1057 uint32_t show_duration_us = micros() - show_start;
1058
1059 // Step 5: Validate first driver's output via RX loopback (if available)
1060 // Only the first driver (PARLIO) is typically connected to the RX pin
1061 bool rx_validation_passed = true;
1062 bool rx_validation_attempted = false;
1063
1064 if (mState->rx_channel && driver_entries.size() > 0) {
1065 const auto& primary_driver = driver_entries[0];
1066
1067 // Only attempt RX validation if the primary driver's pin matches our TX pin
1068 if (primary_driver.pin_tx == mState->pin_tx) {
1069 rx_validation_attempted = true;
1070
1071 // Create validation config for the primary driver
1073 int led_array_offset = 0;
1074 for (fl::size li = 0; li < primary_driver.lane_sizes.size(); li++) {
1075 tx_configs.push_back(fl::ChannelConfig(
1076 primary_driver.pin_tx + (int)li,
1077 timing_config.timing,
1078 fl::span<CRGB>(all_led_arrays[led_array_offset + li]->data(),
1079 all_led_arrays[led_array_offset + li]->size()),
1080 RGB
1081 ));
1082 }
1083
1084 fl::AutoResearchConfig autoresearch_config(
1085 timing_config.timing,
1086 timing_config.name,
1087 tx_configs,
1088 primary_driver.name.c_str(),
1089 mState->rx_channel,
1090 mState->rx_buffer,
1091 primary_driver.lane_sizes[0],
1093 timing_config.encoder
1094 );
1095
1096 int total_tests = 0;
1097 int passed_tests = 0;
1098 uint32_t val_show_duration_ms = 0;
1099
1101 val_show_duration_ms, nullptr);
1102
1103 rx_validation_passed = (total_tests > 0) && (passed_tests == total_tests);
1104 }
1105 }
1106
1107 // Step 6: Clean up channels
1109
1110 uint32_t duration_ms = millis() - start_ms;
1111
1112 // ========== RESPONSE ==========
1113 response.set("success", true);
1114 response.set("passed", show_success && rx_validation_passed);
1115 response.set("duration_ms", static_cast<int64_t>(duration_ms));
1116 response.set("show_duration_us", static_cast<int64_t>(show_duration_us));
1117 response.set("iterations", static_cast<int64_t>(iterations));
1118 response.set("rx_validation_attempted", rx_validation_attempted);
1119 response.set("rx_validation_passed", rx_validation_passed);
1120
1121 // List drivers tested
1122 fl::json drivers_tested = fl::json::array();
1123 for (fl::size i = 0; i < driver_entries.size(); i++) {
1124 fl::json drv = fl::json::object();
1125 drv.set("driver", driver_entries[i].name.c_str());
1126 drv.set("pinTx", static_cast<int64_t>(driver_entries[i].pin_tx));
1127 fl::json sizes = fl::json::array();
1128 for (int s : driver_entries[i].lane_sizes) {
1129 sizes.push_back(static_cast<int64_t>(s));
1130 }
1131 drv.set("laneSizes", sizes);
1132 drv.set("laneCount", static_cast<int64_t>(driver_entries[i].lane_sizes.size()));
1133 drivers_tested.push_back(drv);
1134 }
1135 response.set("drivers", drivers_tested);
1136 response.set("pattern", pattern.c_str());
1137
1138 return response;
1139}
1140
1142 fl::json response = fl::json::object();
1143
1144 // Parse optional arguments: [{startPin: int, endPin: int, autoApply: bool}]
1145 int start_pin = 0;
1146 int end_pin = 8; // Default range: GPIO 0-8 (safe range, avoids USB/flash/strapping pins)
1147 bool auto_apply = true; // If true, automatically apply found pins
1148
1149 if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
1150 fl::json config = args[0];
1151 if (config.contains("startPin") && config["startPin"].is_int()) {
1152 start_pin = static_cast<int>(config["startPin"].as_int().value());
1153 }
1154 if (config.contains("endPin") && config["endPin"].is_int()) {
1155 end_pin = static_cast<int>(config["endPin"].as_int().value());
1156 }
1157 if (config.contains("autoApply") && config["autoApply"].is_bool()) {
1158 auto_apply = config["autoApply"].as_bool().value();
1159 }
1160 }
1161
1162 // Validate range
1163 if (start_pin < 0 || start_pin > 48 || end_pin < 0 || end_pin > 48 || start_pin >= end_pin) {
1164 response.set("error", "InvalidRange");
1165 response.set("message", "Pin range must be 0-48 with startPin < endPin");
1166 return response;
1167 }
1168
1169 FL_DBG("[PIN PROBE] Searching for connected pin pairs in range " << start_pin << "-" << end_pin);
1170
1171 // Helper lambda to test if two pins are connected
1172 auto testPinPair = [](int tx, int rx) -> bool {
1173 // Test 1: TX drives LOW, RX has pullup → RX should read LOW if connected
1174 pinMode(tx, OUTPUT);
1175 pinMode(rx, INPUT_PULLUP);
1176 digitalWrite(tx, LOW);
1177 delay(2); // Allow signal to settle
1178 int rx_when_tx_low = digitalRead(rx);
1179
1180 // Test 2: TX drives HIGH → RX should read HIGH if connected
1181 digitalWrite(tx, HIGH);
1182 delay(2); // Allow signal to settle
1183 int rx_when_tx_high = digitalRead(rx);
1184
1185 // Restore pins to safe state
1186 pinMode(tx, INPUT);
1187 pinMode(rx, INPUT);
1188
1189 return (rx_when_tx_low == LOW) && (rx_when_tx_high == HIGH);
1190 };
1191
1192 // Search for connected adjacent pin pairs (n, n+1)
1193 int found_tx = -1;
1194 int found_rx = -1;
1195 fl::json tested_pairs = fl::json::array();
1196
1197 for (int pin = start_pin; pin < end_pin; pin++) {
1198 int tx_candidate = pin;
1199 int rx_candidate = pin + 1;
1200
1201 // No pin skip logic needed - default range (0-8) is safe for all platforms
1202 // Higher pins (USB, flash, strapping) are excluded by the reduced default range
1203
1204 fl::json pair = fl::json::object();
1205 pair.set("tx", static_cast<int64_t>(tx_candidate));
1206 pair.set("rx", static_cast<int64_t>(rx_candidate));
1207
1208 // Test TX→RX direction
1209 bool connected_forward = testPinPair(tx_candidate, rx_candidate);
1210 if (connected_forward) {
1211 pair.set("connected", true);
1212 pair.set("direction", "forward");
1213 tested_pairs.push_back(pair);
1214 found_tx = tx_candidate;
1215 found_rx = rx_candidate;
1216 FL_DBG("[PIN PROBE] Found connected pair: TX=" << found_tx << " -> RX=" << found_rx);
1217 break;
1218 }
1219
1220 // Test RX→TX direction (reversed)
1221 bool connected_reverse = testPinPair(rx_candidate, tx_candidate);
1222 if (connected_reverse) {
1223 pair.set("connected", true);
1224 pair.set("direction", "reverse");
1225 tested_pairs.push_back(pair);
1226 found_tx = rx_candidate; // Swap since reversed
1227 found_rx = tx_candidate;
1228 FL_DBG("[PIN PROBE] Found connected pair (reversed): TX=" << found_tx << " -> RX=" << found_rx);
1229 break;
1230 }
1231
1232 pair.set("connected", false);
1233 tested_pairs.push_back(pair);
1234 }
1235
1236 // NOTE: testedPairs array omitted - causes heap exhaustion on ESP32 (21+ objects = ~1500 bytes)
1237 // Validation script doesn't use this data, only needs {found, txPin, rxPin}
1238 // response.set("testedPairs", tested_pairs);
1239 fl::json search_range = fl::json::array();
1240 search_range.push_back(static_cast<int64_t>(start_pin));
1241 search_range.push_back(static_cast<int64_t>(end_pin));
1242 response.set("searchRange", search_range);
1243
1244 if (found_tx >= 0 && found_rx >= 0) {
1245 response.set("success", true);
1246 response.set("found", true);
1247 response.set("txPin", static_cast<int64_t>(found_tx));
1248 response.set("rxPin", static_cast<int64_t>(found_rx));
1249
1250 // Auto-apply the found pins if requested
1251 if (auto_apply) {
1252 int old_tx = mState->pin_tx;
1253 int old_rx = mState->pin_rx;
1254 bool rx_changed = (found_rx != old_rx);
1255
1256 mState->pin_tx = found_tx;
1257 mState->pin_rx = found_rx;
1258
1259 // Recreate RX channel if pin changed
1260 if (rx_changed && mState->rx_factory) {
1261 mState->rx_channel.reset();
1262 mState->rx_channel = mState->rx_factory(found_rx);
1263 if (!mState->rx_channel) {
1264 FL_ERROR("[PIN PROBE] Failed to recreate RX channel on GPIO " << found_rx);
1265 // Restore old values
1266 mState->pin_tx = old_tx;
1267 mState->pin_rx = old_rx;
1268 response.set("error", "RxChannelCreationFailed");
1269 response.set("autoApplied", false);
1270 return response;
1271 }
1272 }
1273
1274 FL_DBG("[PIN PROBE] Auto-applied pins: TX=" << found_tx << ", RX=" << found_rx);
1275 response.set("autoApplied", true);
1276 response.set("previousTxPin", static_cast<int64_t>(old_tx));
1277 response.set("previousRxPin", static_cast<int64_t>(old_rx));
1278 } else {
1279 response.set("autoApplied", false);
1280 response.set("message", "Use setPins to apply the found pins");
1281 }
1282 } else {
1283 response.set("success", true); // Function succeeded, just no pins found
1284 response.set("found", false);
1285 response.set("message", "No connected pin pairs found. Please connect a jumper wire between adjacent GPIO pins.");
1286 }
1287
1288 return response;
1289}
1290
1292 : mRemote(fl::make_unique<fl::Remote>(
1293 fl::createSerialRequestSource(),
1294 fl::createSerialResponseSink("REMOTE: ")
1295 )) {
1296 // mState will be set by registerFunctions()
1297}
1298
1300
1302 // Store shared state
1303 mState = state;
1304
1305 // NOTE: All RPC callbacks use const fl::json& for efficient parameter passing.
1306 // The RPC system strips const/reference qualifiers and stores values in the tuple,
1307 // then passes them as references to the function. This avoids copies while
1308 // maintaining clean const-correct API.
1309
1310 // Register "status" function - device readiness check
1311 mRemote->bind("status", [this](const fl::json& args) -> fl::json {
1312 fl::json status = fl::json::object();
1313 status.set("ready", true);
1314 status.set("pinTx", static_cast<int64_t>(mState->pin_tx));
1315 status.set("pinRx", static_cast<int64_t>(mState->pin_rx));
1316 return status;
1317 });
1318
1319 // NOTE: getSchema is no longer needed - use built-in "rpc.discover" instead
1320 // The rpc.discover method is automatically available via Remote->Rpc and returns
1321 // the full OpenRPC schema. Our custom getSchema was causing stack overflow on ESP32-C6.
1322
1323 // Register "debugTest" function - test RPC argument passing
1324 mRemote->bind("debugTest", [](const fl::json& args) -> fl::json {
1325 fl::json response = fl::json::object();
1326 response.set("success", true);
1327 response.set("received", args);
1328 return response;
1329 });
1330
1331 // Register "deliberateHang" - force an infinite loop with interrupts
1332 // disabled to validate the unified Watchdog implementation (#2731).
1333 // The watchdog should fire within its configured timeout, reset the
1334 // device, and let the bootloader become reachable again.
1335 // Returns success BEFORE hanging so the caller sees the ACK; the hang
1336 // begins in the next iteration of the loop.
1337 mRemote->bind("deliberateHang", [this](const fl::json& args) -> fl::json {
1338 (void)args;
1339 FL_WARN("[deliberateHang] watchdog test: spinning forever in 200 ms");
1340 mState->deliberate_hang_requested = true;
1341 fl::json response = fl::json::object();
1342 response.set("success", true);
1343 response.set("message", "device will hang after RPC returns; watchdog should reset within configured timeout");
1344 return response;
1345 });
1346
1347 // Register "drivers" function - list available drivers
1348 mRemote->bind("drivers", [this](const fl::json& args) -> fl::json {
1349 fl::json drivers = fl::json::array();
1350 for (fl::size i = 0; i < mState->drivers_available.size(); i++) {
1351 fl::json driver = fl::json::object();
1352 driver.set("name", mState->drivers_available[i].name.c_str());
1353 driver.set("priority", static_cast<int64_t>(mState->drivers_available[i].priority));
1354 driver.set("enabled", mState->drivers_available[i].enabled);
1355 drivers.push_back(driver);
1356 }
1357 return drivers;
1358 });
1359
1360 // Returns: {success, passed, totalTests, passedTests, duration_ms, driver,
1361 // laneCount, laneSizes, pattern, firstFailure?}
1362 // ASYNC: Sends ACK immediately, final response sent via sendAsyncResponse()
1363 // NOTE: runSingleTestImpl() may return early (error cases) without calling
1364 // sendAsyncResponse(). This wrapper ensures a response is ALWAYS sent so
1365 // the Python client never times out waiting 120s for a missing response.
1366 mRemote->bind("runSingleTest", [this](const fl::json& args) -> fl::json {
1367 fl::json result = this->runSingleTestImpl(args);
1368 // If runSingleTestImpl returned a non-null response, it exited early without
1369 // calling sendAsyncResponse(). Send it now so the client gets a response.
1370 if (!result.is_null()) {
1371 mRemote->sendAsyncResponse("runSingleTest", result);
1372 }
1373 return fl::json(nullptr);
1375
1376 // Register "runParallelTest" - test multiple drivers simultaneously
1377 // Args: {drivers: [{driver: "PARLIO", laneSizes: [100]}, {driver: "LCD_RGB", laneSizes: [100]}],
1378 // pattern?: "MSB_LSB_A", iterations?: 1, timing?: "WS2812B-V5"}
1379 // Returns: {success, passed, duration_ms, show_duration_us, drivers: [...],
1380 // rx_validation_attempted, rx_validation_passed}
1381 mRemote->bind("runParallelTest", [this](const fl::json& args) -> fl::json {
1382 fl::json result = this->runParallelTestImpl(args);
1383 if (!result.is_null()) {
1384 mRemote->sendAsyncResponse("runParallelTest", result);
1385 }
1386 return fl::json(nullptr);
1388
1389 // ========================================================================
1390 // Phase 4 Functions: Utility and Control
1391 // ========================================================================
1392
1393 // Register "ping" function - health check with timestamp
1394 mRemote->bind("ping", [this](const fl::json& args) -> fl::json {
1395 uint32_t now = millis();
1396
1397 fl::json response = fl::json::object();
1398 response.set("success", true);
1399 response.set("message", "pong");
1400 response.set("timestamp", static_cast<int64_t>(now));
1401 response.set("uptimeMs", static_cast<int64_t>(now));
1402 return response;
1403 });
1404
1405 // TEST: Simple RPC without Serial to verify task context works
1406 mRemote->bind("testNoSerial", [this](const fl::json& args) -> fl::json {
1407 fl::json response = fl::json::object();
1408 response.set("success", true);
1409 response.set("message", "RPC works from task context");
1410 response.set("serial_safe", false);
1411 return response;
1412 });
1413
1414 // Register "flexioRxBenchmark" — square-wave validation for the new
1415 // FlexIO RX backend (Phase 2 of FastLED#2764).
1416 //
1417 // Drives `tx_pin` with a `frequency_hz` square wave via the Teensy core's
1418 // `analogWriteFrequency` PWM, configures an RxChannel on `rx_pin` using
1419 // `RxBackend::FLEXIO`, captures for `duration_ms`, and reports per-period
1420 // statistics: count, mean ns, σ ns, min ns, max ns.
1421 //
1422 // Args (positional, all optional with defaults):
1423 // {
1424 // "frequency_hz": int = 1000,
1425 // "duration_ms": int = 100,
1426 // "tx_pin": int = 3, // matches GPIO 3↔4 jumper
1427 // "rx_pin": int = 4
1428 // }
1429 //
1430 // Teensy-4-only because FLEXIO1 is iMXRT1062-specific. Other platforms
1431 // get a clean "not supported" response so the RPC harness can still
1432 // round-trip.
1433 mRemote->bind("flexioRxBenchmark", [this](const fl::json& args) -> fl::json {
1434 fl::json response = fl::json::object();
1435#if !defined(FL_IS_TEENSY_4X)
1436 (void)args;
1437 response.set("success", false);
1438 response.set("error", "PlatformNotSupported");
1439 response.set("message",
1440 "flexioRxBenchmark is Teensy 4.x-only (FLEXIO1 capture).");
1441 return response;
1442#else
1443 // Parse args
1444 int frequency_hz = 1000;
1445 int duration_ms = 100;
1446 int tx_pin = 3;
1447 int rx_pin = 4;
1448 if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
1449 fl::json cfg = args[0];
1450 if (cfg.contains("frequency_hz") && cfg["frequency_hz"].is_int()) {
1451 frequency_hz = static_cast<int>(cfg["frequency_hz"].as_int().value());
1452 }
1453 if (cfg.contains("duration_ms") && cfg["duration_ms"].is_int()) {
1454 duration_ms = static_cast<int>(cfg["duration_ms"].as_int().value());
1455 }
1456 if (cfg.contains("tx_pin") && cfg["tx_pin"].is_int()) {
1457 tx_pin = static_cast<int>(cfg["tx_pin"].as_int().value());
1458 }
1459 if (cfg.contains("rx_pin") && cfg["rx_pin"].is_int()) {
1460 rx_pin = static_cast<int>(cfg["rx_pin"].as_int().value());
1461 }
1462 }
1463 if (frequency_hz < 1 || frequency_hz > 5000000 ||
1464 duration_ms < 1 || duration_ms > 5000) {
1465 response.set("success", false);
1466 response.set("error", "InvalidArgs");
1467 response.set("message",
1468 "frequency_hz in [1, 5_000_000]; duration_ms in [1, 5000].");
1469 return response;
1470 }
1471
1472 // 1. Drive the TX pin with a square wave.
1473 analogWriteFrequency(tx_pin, (float)frequency_hz);
1474 analogWrite(tx_pin, 128); // 50% duty
1475
1476 // 2. Create the FlexIO RX channel.
1477 // Width budget — size the edge buffer for the actual capture window:
1478 // expected_edges = 2 * frequency_hz * duration_ms / 1000 (transitions)
1479 // target = expected_edges * 1.5 (50% headroom for jitter/skew)
1480 // Clamped to [1024, 16384] so the DMA buffer stays reasonable and
1481 // matches the FlexIO RX driver's internal cap.
1483 const fl::u64 expected_edges =
1484 (fl::u64)2 * (fl::u64)frequency_hz * (fl::u64)duration_ms / 1000ULL;
1485 fl::u64 target = expected_edges + expected_edges / 2; // +50%
1486 if (target < 1024ULL) target = 1024ULL;
1487 if (target > 16384ULL) target = 16384ULL;
1488 rx_cfg.edge_capacity = (size_t)target;
1489 rx_cfg.start_low = false; // PWM output idles in either state, just track transitions
1490 auto rx_channel = fl::RxChannel::create(rx_cfg);
1491 if (!rx_channel) {
1492 analogWrite(tx_pin, 0);
1493 response.set("success", false);
1494 response.set("error", "RxCreateFailed");
1495 response.set("message",
1496 "Failed to create FlexIO RX channel on pin (no FLEXIO1 mapping).");
1497 return response;
1498 }
1499 if (!rx_channel->begin(rx_cfg)) {
1500 analogWrite(tx_pin, 0);
1501 response.set("success", false);
1502 response.set("error", "RxBeginFailed");
1503 response.set("message",
1504 "RxChannel::begin() returned false (see device WARN log).");
1505 return response;
1506 }
1507
1508 // 3. Let it capture for the requested window.
1509 rx_channel->wait((u32)duration_ms);
1510
1511 // 4. Stop the square wave so the line is quiet for stats computation.
1512 analogWrite(tx_pin, 0);
1513
1514 // 5. Pull captured edges out.
1516 edges.assign(rx_cfg.edge_capacity, fl::EdgeTime());
1517 size_t edges_captured =
1518 rx_channel->getRawEdgeTimes(fl::span<fl::EdgeTime>(edges), 0);
1519 edges.resize(edges_captured);
1520
1521 // 6. Compute period statistics. A "period" = duration_high + duration_low
1522 // for adjacent edges, so we iterate in pairs.
1523 fl::u64 sum_ns = 0;
1524 fl::u64 sum_sq_ns = 0;
1525 fl::u32 min_ns = 0xFFFFFFFFu;
1526 fl::u32 max_ns = 0;
1527 size_t periods = 0;
1528 for (size_t i = 0; i + 1 < edges_captured; i += 2) {
1529 const fl::u32 period_ns =
1530 (fl::u32)edges[i].ns + (fl::u32)edges[i + 1].ns;
1531 if (period_ns == 0) continue;
1532 sum_ns += period_ns;
1533 sum_sq_ns += (fl::u64)period_ns * (fl::u64)period_ns;
1534 if (period_ns < min_ns) min_ns = period_ns;
1535 if (period_ns > max_ns) max_ns = period_ns;
1536 ++periods;
1537 }
1538 fl::u32 mean_ns = 0;
1539 fl::u32 sigma_ns = 0;
1540 if (periods > 0) {
1541 mean_ns = (fl::u32)(sum_ns / (fl::u64)periods);
1542 const fl::u64 mean64 = (fl::u64)mean_ns;
1543 const fl::u64 var =
1544 periods > 0 ? (sum_sq_ns / (fl::u64)periods) - (mean64 * mean64)
1545 : 0;
1546 // Integer sqrt for σ — adequate for the tolerance ranges we
1547 // assert in Phase 2 (σ < 100 ns at 100 kHz).
1548 fl::u32 s = 0;
1549 while ((fl::u64)(s + 1) * (fl::u64)(s + 1) <= var) ++s;
1550 sigma_ns = s;
1551 }
1552 if (min_ns == 0xFFFFFFFFu) min_ns = 0;
1553
1554 response.set("success", true);
1555 response.set("frequency_hz", static_cast<int64_t>(frequency_hz));
1556 response.set("duration_ms", static_cast<int64_t>(duration_ms));
1557 response.set("tx_pin", static_cast<int64_t>(tx_pin));
1558 response.set("rx_pin", static_cast<int64_t>(rx_pin));
1559 response.set("edges_captured", static_cast<int64_t>(edges_captured));
1560 response.set("periods", static_cast<int64_t>(periods));
1561 response.set("period_mean_ns", static_cast<int64_t>(mean_ns));
1562 response.set("period_sigma_ns", static_cast<int64_t>(sigma_ns));
1563 response.set("period_min_ns", static_cast<int64_t>(min_ns));
1564 response.set("period_max_ns", static_cast<int64_t>(max_ns));
1565 return response;
1566#endif
1567 });
1568
1569 // Register "flexioObjectFledTest" — end-to-end ObjectFLED TX → FlexIO RX
1570 // loopback verification (Phase 3 of FastLED#2764).
1571 //
1572 // Drives a small WS2812 pattern through `Bus::OBJECT_FLED` on `tx_pin`,
1573 // captures the wire signal back through the new `RxBackend::FLEXIO` on
1574 // `rx_pin`, decodes the bit stream against WS2812B-V5 timing, and reports
1575 // how many bytes matched the transmitted pattern.
1576 //
1577 // Test cases (parent issue Phase 3 table):
1578 // 0 — Red single LED (1 LED, 0xFF0000 → wire-order GRB 00,FF,00)
1579 // 1 — RGB three-LED chain (3 LEDs)
1580 // 2 — All zeros (1 LED, 0x000000 — T0H/T0L fidelity)
1581 // 3 — All ones (1 LED, 0xFFFFFF — T1H/T1L fidelity)
1582 // 4 — 100-LED alternating R/G/B (long capture, watchdog-safety)
1583 //
1584 // Args (positional, all optional):
1585 // { "test_case": int = 0,
1586 // "tx_pin": int = 3, // matches GPIO 3↔4 jumper
1587 // "rx_pin": int = 4,
1588 // "capture_ms": int = 50 }
1589 //
1590 // Returns:
1591 // { success, test_case, num_leds, expected_bytes, decoded_bytes,
1592 // matched, mismatched, edges_captured }
1593 //
1594 // Teensy-4-only — FLEXIO1 is iMXRT1062-specific. ObjectFLED also relies
1595 // on Teensy 4-core APIs, so non-Teensy builds return `PlatformNotSupported`.
1596 mRemote->bind("flexioObjectFledTest", [this](const fl::json& args) -> fl::json {
1597 fl::json response = fl::json::object();
1598#if !defined(FL_IS_TEENSY_4X)
1599 (void)args;
1600 response.set("success", false);
1601 response.set("error", "PlatformNotSupported");
1602 response.set("message",
1603 "flexioObjectFledTest is Teensy 4.x-only (FLEXIO1 RX + "
1604 "Teensy-core ObjectFLED driver).");
1605 return response;
1606#else
1607 // Parse args
1608 int test_case = 0;
1609 int tx_pin = 3;
1610 int rx_pin = 4;
1611 int capture_ms = 50;
1612 if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
1613 fl::json cfg = args[0];
1614 if (cfg.contains("test_case") && cfg["test_case"].is_int()) {
1615 test_case = static_cast<int>(cfg["test_case"].as_int().value());
1616 }
1617 if (cfg.contains("tx_pin") && cfg["tx_pin"].is_int()) {
1618 tx_pin = static_cast<int>(cfg["tx_pin"].as_int().value());
1619 }
1620 if (cfg.contains("rx_pin") && cfg["rx_pin"].is_int()) {
1621 rx_pin = static_cast<int>(cfg["rx_pin"].as_int().value());
1622 }
1623 if (cfg.contains("capture_ms") && cfg["capture_ms"].is_int()) {
1624 capture_ms = static_cast<int>(cfg["capture_ms"].as_int().value());
1625 }
1626 }
1627 if (test_case < 0 || test_case > 4 ||
1628 capture_ms < 1 || capture_ms > 5000) {
1629 response.set("success", false);
1630 response.set("error", "InvalidArgs");
1631 response.set("message",
1632 "test_case in [0,4]; capture_ms in [1, 5000].");
1633 return response;
1634 }
1635
1636 // 1. Build the test pattern. Fixed upper bound (100 LEDs) covers
1637 // case 4 — the larger cases reuse the same static buffer.
1638 static CRGB leds_buf[100];
1639 int num_leds = 0;
1640 switch (test_case) {
1641 case 0:
1642 num_leds = 1;
1643 leds_buf[0] = CRGB::Red;
1644 break;
1645 case 1:
1646 num_leds = 3;
1647 leds_buf[0] = CRGB::Red;
1648 leds_buf[1] = CRGB::Green;
1649 leds_buf[2] = CRGB::Blue;
1650 break;
1651 case 2:
1652 num_leds = 1;
1653 leds_buf[0] = CRGB::Black;
1654 break;
1655 case 3:
1656 num_leds = 1;
1657 leds_buf[0] = CRGB(0xFF, 0xFF, 0xFF);
1658 break;
1659 case 4:
1660 num_leds = 100;
1661 for (int i = 0; i < 100; ++i) {
1662 leds_buf[i] = (i % 3 == 0) ? CRGB::Red
1663 : (i % 3 == 1) ? CRGB::Green
1664 : CRGB::Blue;
1665 }
1666 break;
1667 }
1668
1669 // 2. Build the expected wire-order byte stream (WS2812 is GRB).
1670 fl::vector<u8> expected;
1671 expected.reserve((size_t)num_leds * 3);
1672 for (int i = 0; i < num_leds; ++i) {
1673 expected.push_back(leds_buf[i].g);
1674 expected.push_back(leds_buf[i].r);
1675 expected.push_back(leds_buf[i].b);
1676 }
1677
1678 // 3. Set up FlexIO RX. Size the edge buffer for 24 bits per LED ×
1679 // 2 transitions per bit, with 25% headroom.
1680 const size_t expected_edges = (size_t)num_leds * 24u * 2u;
1681 size_t edge_capacity = expected_edges + expected_edges / 4u + 64u;
1682 if (edge_capacity > 16384u) edge_capacity = 16384u;
1683
1685 rx_cfg.edge_capacity = edge_capacity;
1686 rx_cfg.start_low = true;
1687 auto rx_channel = fl::RxChannel::create(rx_cfg);
1688 if (!rx_channel || !rx_channel->begin(rx_cfg)) {
1689 response.set("success", false);
1690 response.set("error", "RxBeginFailed");
1691 response.set("message",
1692 "Failed to bring up FlexIO RX on the requested pin "
1693 "(check kFlexIo1Pins[] mapping).");
1694 return response;
1695 }
1696
1697 // 4. Configure ObjectFLED TX via FastLED.add().
1698 fl::ChannelOptions opts;
1700 auto resolved_timing = fl::makeTimingConfig<fl::TIMING_WS2812B_V5>();
1701 auto resolved_encoder = fl::encoder_for<fl::TIMING_WS2812B_V5>();
1702 fl::NamedTimingConfig timing_cfg(resolved_timing, "WS2812B-V5",
1703 resolved_encoder);
1704 fl::ChannelConfig tx_cfg(
1705 tx_pin,
1706 timing_cfg.timing,
1707 fl::span<CRGB>(leds_buf, (size_t)num_leds),
1708 RGB,
1709 opts);
1710 auto tx_channel = FastLED.add(tx_cfg);
1711 if (!tx_channel) {
1712 response.set("success", false);
1713 response.set("error", "TxAddFailed");
1714 response.set("message",
1715 "FastLED.add() rejected the ObjectFLED ChannelConfig.");
1716 return response;
1717 }
1718
1719 // 5. Trigger TX. FastLED.show() schedules the DMA and returns once
1720 // the frame has been queued. The RX wait must be long enough to
1721 // cover BOTH the TX transmission time AND the capture-buffer
1722 // fill — otherwise teardown happens mid-frame and the hardware
1723 // can be left in a bad state. Compute a minimum from the actual
1724 // WS2812 timing (1.25 µs per bit, 24 bits per LED, plus reset
1725 // gap) and use the larger of the caller's `capture_ms` and that
1726 // minimum.
1727 FastLED.show();
1728 const u32 ws2812_tx_us =
1729 (u32)num_leds * 24u * 13u / 10u + 100u; // 1.25 µs/bit + reset
1730 const u32 min_capture_ms = (ws2812_tx_us + 999u) / 1000u + 10u;
1731 const u32 effective_capture_ms = ((u32)capture_ms > min_capture_ms)
1732 ? (u32)capture_ms
1733 : min_capture_ms;
1734 rx_channel->wait(effective_capture_ms);
1735
1736 // 6. Decode the captured edge stream against WS2812 4-phase timing.
1737 const fl::ChipsetTiming ws2812_timing =
1739 fl::ChipsetTiming4Phase rx_timing =
1740 fl::make4PhaseTiming(ws2812_timing);
1741 fl::vector<u8> decoded;
1742 decoded.assign(expected.size() + 16u, 0u);
1743 auto decode_result =
1744 rx_channel->decode(rx_timing, fl::span<u8>(decoded));
1745
1746 // 7. Compare decoded bytes against expected.
1747 u32 decoded_bytes = 0u;
1748 if (decode_result) {
1749 decoded_bytes = decode_result.value();
1750 }
1751 const size_t cmp_n = (decoded_bytes < expected.size())
1752 ? (size_t)decoded_bytes
1753 : expected.size();
1754 size_t matched = 0;
1755 size_t mismatched = 0;
1756 for (size_t i = 0; i < cmp_n; ++i) {
1757 if (decoded[i] == expected[i]) ++matched; else ++mismatched;
1758 }
1759 if (decoded_bytes < expected.size()) {
1760 mismatched += expected.size() - decoded_bytes;
1761 }
1762
1763 // 8. Read raw edge count for diagnostics.
1765 edges.assign(edge_capacity, fl::EdgeTime());
1766 const size_t edges_captured =
1767 rx_channel->getRawEdgeTimes(fl::span<fl::EdgeTime>(edges), 0);
1768
1769 // 9. Tear the TX channel back down so subsequent tests can use the
1770 // same pin (FastLED.clear(ClearFlags::CHANNELS) matches the
1771 // pattern used by runSingleTestImpl in this file).
1773
1774 response.set("success", mismatched == 0 && decoded_bytes == expected.size());
1775 response.set("test_case", static_cast<int64_t>(test_case));
1776 response.set("tx_pin", static_cast<int64_t>(tx_pin));
1777 response.set("rx_pin", static_cast<int64_t>(rx_pin));
1778 response.set("num_leds", static_cast<int64_t>(num_leds));
1779 response.set("expected_bytes", static_cast<int64_t>(expected.size()));
1780 response.set("decoded_bytes", static_cast<int64_t>(decoded_bytes));
1781 response.set("matched", static_cast<int64_t>(matched));
1782 response.set("mismatched", static_cast<int64_t>(mismatched));
1783 response.set("edges_captured", static_cast<int64_t>(edges_captured));
1784 return response;
1785#endif
1786 });
1787
1788 // Register "flexioRxLoopbackPing" — FastLED#3066 phase 1.7 diagnostic.
1789 //
1790 // Bypasses every clockless driver entirely and tests whether the
1791 // FlexIO RX backend can capture HAND-DRIVEN GPIO transitions. Drives
1792 // `tx_pin` as a plain `digitalWrite` HIGH/LOW square wave while
1793 // `RxBackend::FLEXIO` is armed on `rx_pin`. If the captured buffer
1794 // shows any non-zero data, IOMUX ALT4 + PINSEL routing is correct
1795 // and the WS2812-loopback failure mode is downstream (timer/shifter
1796 // bandwidth, decoder thresholds, etc.). If the buffer stays all
1797 // zero with a 4-edge ground-truth pin toggle, the routing itself is
1798 // broken — debug that BEFORE more timer/shifter config experiments.
1799 //
1800 // Wiring: same TX↔RX jumper as flexioObjectFledTest (defaults 3↔4).
1801 //
1802 // Args:
1803 // { tx_pin?: 3, // any digital pin
1804 // rx_pin?: 4, // must be in kFlexIo1Pins[]
1805 // toggle_us?: 50, // half-period of the manual square wave
1806 // edges?: 8 } // number of digitalWrite transitions
1807 // Returns:
1808 // { success, tx_pin, rx_pin, toggle_us, edges, edges_captured,
1809 // capture_buffer_first8_hex: [...], notes }
1810 //
1811 // Teensy-4-only.
1812 mRemote->bind("flexioRxLoopbackPing", [this](const fl::json& args) -> fl::json {
1813 fl::json response = fl::json::object();
1814#if !defined(FL_IS_TEENSY_4X)
1815 (void)args;
1816 response.set("success", false);
1817 response.set("error", "PlatformNotSupported");
1818 response.set("message",
1819 "flexioRxLoopbackPing is Teensy 4.x-only (FLEXIO1 RX).");
1820 return response;
1821#else
1822 int tx_pin = 3;
1823 int rx_pin = 4;
1824 int toggle_us = 50;
1825 int edges = 8;
1826 if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
1827 const fl::json &cfg = args[0];
1828 if (cfg.contains("tx_pin") && cfg["tx_pin"].is_int()) {
1829 tx_pin = static_cast<int>(cfg["tx_pin"].as_int().value());
1830 }
1831 if (cfg.contains("rx_pin") && cfg["rx_pin"].is_int()) {
1832 rx_pin = static_cast<int>(cfg["rx_pin"].as_int().value());
1833 }
1834 if (cfg.contains("toggle_us") && cfg["toggle_us"].is_int()) {
1835 toggle_us = static_cast<int>(cfg["toggle_us"].as_int().value());
1836 }
1837 if (cfg.contains("edges") && cfg["edges"].is_int()) {
1838 edges = static_cast<int>(cfg["edges"].as_int().value());
1839 }
1840 }
1841 if (edges < 2 || edges > 64) {
1842 response.set("success", false);
1843 response.set("error", "InvalidEdges");
1844 response.set("message", "edges must be in [2, 64]");
1845 return response;
1846 }
1847
1848 // 1. Arm FlexIO RX on rx_pin.
1850 rx_cfg.edge_capacity = 1024; // plenty for ~10 edges
1851 rx_cfg.start_low = true;
1852 auto rx_channel = fl::RxChannel::create(rx_cfg);
1853 if (!rx_channel || !rx_channel->begin(rx_cfg)) {
1854 response.set("success", false);
1855 response.set("error", "RxBeginFailed");
1856 response.set("message",
1857 "FlexIO RX begin() failed (kFlexIo1Pins[] mapping?).");
1858 return response;
1859 }
1860
1861 // 2. Drive tx_pin as a plain GPIO output, toggle a known number of
1862 // transitions. Start LOW so the first digitalWrite(HIGH) is a
1863 // rising edge.
1864 pinMode(tx_pin, OUTPUT);
1865 digitalWrite(tx_pin, LOW);
1866 delayMicroseconds(100); // let the line settle + RX arm fully
1867
1868 bool level = true;
1869 for (int i = 0; i < edges; ++i) {
1870 digitalWrite(tx_pin, level ? HIGH : LOW);
1871 delayMicroseconds(toggle_us);
1872 level = !level;
1873 }
1874
1875 // FastLED#3066 iter 7 diagnostic: also probe SHIFTBUF[0] +
1876 // SHIFTBUFBIS (bit-swapped) + SHIFTBUFBYS (byte-swapped) so we
1877 // can see whether the captured bit lands in a bit position
1878 // different from what the canonical SHIFTBUF[0] read produces.
1879 {
1880 constexpr uintptr_t kFLEXIO1_BASE_DIAG = 0x401AC000u;
1881 volatile uint32_t *pin = (volatile uint32_t *)(kFLEXIO1_BASE_DIAG + 0x00C);
1882 volatile uint32_t *shftstat = (volatile uint32_t *)(kFLEXIO1_BASE_DIAG + 0x010);
1883 volatile uint32_t *timstat = (volatile uint32_t *)(kFLEXIO1_BASE_DIAG + 0x018);
1884 volatile uint32_t *shftbuf = (volatile uint32_t *)(kFLEXIO1_BASE_DIAG + 0x200);
1885 volatile uint32_t *shftbufbis = (volatile uint32_t *)(kFLEXIO1_BASE_DIAG + 0x280);
1886 volatile uint32_t *shftbufbys = (volatile uint32_t *)(kFLEXIO1_BASE_DIAG + 0x300);
1887 FL_WARN("[ping diag] post-toggle FLEXIO1: PIN=0x"
1888 << fl::hex << *pin
1889 << " SHIFTSTAT=0x" << *shftstat
1890 << " TIMSTAT=0x" << *timstat << fl::dec);
1891 FL_WARN("[ping diag] SHIFTBUF[0]=0x" << fl::hex << *shftbuf
1892 << " SHIFTBUFBIS[0]=0x" << *shftbufbis
1893 << " SHIFTBUFBYS[0]=0x" << *shftbufbys << fl::dec);
1894 }
1895
1896 // 3. Wait briefly for FlexIO RX to flush any pending DMA. The DMA
1897 // is configured for completion-on-buffer-full; a partial fill
1898 // won't trigger the ISR, so `wait()` will TIMEOUT — which is
1899 // fine, we read the captured edges directly.
1900 rx_channel->wait(50);
1901
1902 // 4. Read the captured edges via the public API.
1903 fl::vector<fl::EdgeTime> captured_edges;
1904 captured_edges.assign(64, fl::EdgeTime());
1905 size_t edge_count =
1906 rx_channel->getRawEdgeTimes(fl::span<fl::EdgeTime>(captured_edges), 0);
1907
1908 // 5. Restore tx_pin so subsequent tests can use it.
1909 pinMode(tx_pin, INPUT);
1910
1911 response.set("success", true);
1912 response.set("tx_pin", static_cast<int64_t>(tx_pin));
1913 response.set("rx_pin", static_cast<int64_t>(rx_pin));
1914 response.set("toggle_us", static_cast<int64_t>(toggle_us));
1915 response.set("edges", static_cast<int64_t>(edges));
1916 response.set("edges_captured", static_cast<int64_t>(edge_count));
1917
1918 // Dump the first 8 captured edge durations (ns) so the host can
1919 // see whether they roughly match the requested toggle_us *
1920 // 1000 ns expectation.
1921 fl::json edges_arr = fl::json::array();
1922 const size_t to_dump = (edge_count < 8u) ? edge_count : 8u;
1923 for (size_t i = 0; i < to_dump; ++i) {
1925 e.set("high", captured_edges[i].high);
1926 e.set("ns", static_cast<int64_t>(captured_edges[i].ns));
1927 edges_arr.push_back(e);
1928 }
1929 response.set("first_edges", edges_arr);
1930 return response;
1931#endif
1932 });
1933
1934 // Register "setDebug" function - enable/disable runtime debug logging
1935 mRemote->bind("setDebug", [this](const fl::json& args) -> fl::json {
1936 fl::json response = fl::json::object();
1937
1938 // Validate args: expects [enabled: bool]
1939 if (!args.is_array() || args.size() != 1) {
1940 response.set("success", false);
1941 response.set("error", "InvalidArgs");
1942 response.set("message", "Expected [enabled: bool]");
1943 return response;
1944 }
1945
1946 if (!args[0].is_bool()) {
1947 response.set("success", false);
1948 response.set("error", "InvalidType");
1949 response.set("message", "Argument must be boolean");
1950 return response;
1951 }
1952
1953 bool enabled = args[0].as_bool().value();
1954 mState->debug_enabled = enabled;
1955
1956 response.set("success", true);
1957 response.set("debug_enabled", enabled);
1958 response.set("message", enabled ? "Debug logging enabled" : "Debug logging disabled");
1959
1960 return response;
1961 });
1962
1963 // Register "testGpioConnection" function - test if TX and RX pins are electrically connected
1964 // This is a pre-test to diagnose hardware connection issues before running validation
1965 mRemote->bind("testGpioConnection", [](const fl::json& args) -> fl::json {
1966 fl::json response = fl::json::object();
1967
1968 // Validate args: expects [txPin, rxPin]
1969 if (!args.is_array() || args.size() != 2) {
1970 response.set("error", "InvalidArgs");
1971 response.set("message", "Expected [txPin, rxPin]");
1972 return response;
1973 }
1974
1975 if (!args[0].is_int() || !args[1].is_int()) {
1976 response.set("error", "InvalidPinType");
1977 response.set("message", "Pin numbers must be integers");
1978 return response;
1979 }
1980
1981 int tx_pin = static_cast<int>(args[0].as_int().value());
1982 int rx_pin = static_cast<int>(args[1].as_int().value());
1983
1984 // Test 1: TX drives LOW, RX has pullup → RX should read LOW if connected
1985 pinMode(tx_pin, OUTPUT);
1986 pinMode(rx_pin, INPUT_PULLUP);
1987 digitalWrite(tx_pin, LOW);
1988 delay(5); // Allow signal to settle
1989 int rx_when_tx_low = digitalRead(rx_pin);
1990
1991 // Test 2: TX drives HIGH → RX should read HIGH if connected
1992 digitalWrite(tx_pin, HIGH);
1993 delay(5); // Allow signal to settle
1994 int rx_when_tx_high = digitalRead(rx_pin);
1995
1996 // Restore pins to safe state
1997 pinMode(tx_pin, INPUT);
1998 pinMode(rx_pin, INPUT);
1999
2000 // Analyze results
2001 bool connected = (rx_when_tx_low == LOW) && (rx_when_tx_high == HIGH);
2002
2003 response.set("txPin", static_cast<int64_t>(tx_pin));
2004 response.set("rxPin", static_cast<int64_t>(rx_pin));
2005 response.set("rxWhenTxLow", rx_when_tx_low == LOW ? "LOW" : "HIGH");
2006 response.set("rxWhenTxHigh", rx_when_tx_high == HIGH ? "HIGH" : "LOW");
2007 response.set("connected", connected);
2008
2009 if (connected) {
2010 response.set("success", true);
2011 response.set("message", "GPIO pins are connected");
2012 } else {
2013 response.set("success", false);
2014 if (rx_when_tx_low == HIGH && rx_when_tx_high == HIGH) {
2015 response.set("message", "RX pin stuck HIGH - no connection detected (check jumper wire)");
2016 } else if (rx_when_tx_low == LOW && rx_when_tx_high == LOW) {
2017 response.set("message", "RX pin stuck LOW - possible short to ground");
2018 } else {
2019 response.set("message", "Unexpected GPIO behavior - check wiring");
2020 }
2021 }
2022
2023 return response;
2024 });
2025
2026 // ========================================================================
2027 // Pin Configuration RPC Functions (Dynamic TX/RX Pin Support)
2028 // ========================================================================
2029
2030 // Register "getPins" function - query current and default pin configuration
2031 mRemote->bind("getPins", [this](const fl::json& args) -> fl::json {
2032 fl::json response = fl::json::object();
2033 response.set("success", true);
2034 response.set("txPin", static_cast<int64_t>(mState->pin_tx));
2035 response.set("rxPin", static_cast<int64_t>(mState->pin_rx));
2036
2037 fl::json defaults = fl::json::object();
2038 defaults.set("txPin", static_cast<int64_t>(mState->default_pin_tx));
2039 defaults.set("rxPin", static_cast<int64_t>(mState->default_pin_rx));
2040 response.set("defaults", defaults);
2041
2042 #if defined(FL_IS_ESP_32S3)
2043 response.set("platform", "ESP32-S3");
2044 #elif defined(FL_IS_ESP_32S2)
2045 response.set("platform", "ESP32-S2");
2046 #elif defined(FL_IS_ESP_32C6)
2047 response.set("platform", "ESP32-C6");
2048 #elif defined(FL_IS_ESP_32C3)
2049 response.set("platform", "ESP32-C3");
2050 #else
2051 response.set("platform", "unknown");
2052 #endif
2053
2054 return response;
2055 });
2056
2057 // Register "setTxPin" function - set TX pin (regenerates test cases)
2058 mRemote->bind("setTxPin", [this](const fl::json& args) -> fl::json {
2059 fl::json response = fl::json::object();
2060
2061 if (!args.is_array() || args.size() != 1 || !args[0].is_int()) {
2062 response.set("error", "InvalidArgs");
2063 response.set("message", "Expected [pin: int]");
2064 return response;
2065 }
2066
2067 int new_pin = static_cast<int>(args[0].as_int().value());
2068
2069 // Validate pin range (ESP32 GPIO range)
2070 if (new_pin < 0 || new_pin > 48) {
2071 response.set("error", "InvalidPin");
2072 response.set("message", "Pin must be 0-48");
2073 return response;
2074 }
2075
2076 int old_pin = mState->pin_tx;
2077 mState->pin_tx = new_pin;
2078
2079 FL_PRINT("[RPC] setTxPin(" << new_pin << ") - TX pin changed from " << old_pin << " to " << new_pin);
2080
2081 response.set("success", true);
2082 response.set("txPin", static_cast<int64_t>(new_pin));
2083 response.set("previousTxPin", static_cast<int64_t>(old_pin));
2084 return response;
2085 });
2086
2087 // Register "setRxPin" function - set RX pin (recreates RX channel)
2088 mRemote->bind("setRxPin", [this](const fl::json& args) -> fl::json {
2089 fl::json response = fl::json::object();
2090
2091 if (!args.is_array() || args.size() != 1 || !args[0].is_int()) {
2092 response.set("error", "InvalidArgs");
2093 response.set("message", "Expected [pin: int]");
2094 return response;
2095 }
2096
2097 int new_pin = static_cast<int>(args[0].as_int().value());
2098
2099 // Validate pin range (ESP32 GPIO range)
2100 if (new_pin < 0 || new_pin > 48) {
2101 response.set("error", "InvalidPin");
2102 response.set("message", "Pin must be 0-48");
2103 return response;
2104 }
2105
2106 int old_pin = mState->pin_rx;
2107 bool pin_changed = (new_pin != old_pin);
2108 bool rx_recreated = false;
2109
2110 if (pin_changed) {
2111 mState->pin_rx = new_pin;
2112
2113 // Recreate RX channel with new pin
2114 FL_PRINT("[RPC] setRxPin(" << new_pin << ") - Recreating RX channel...");
2115
2116 // Destroy old RX channel
2117 mState->rx_channel.reset();
2118
2119 // Create new RX channel on new pin using factory
2120 mState->rx_channel = mState->rx_factory(new_pin);
2121
2122 if (mState->rx_channel) {
2123 FL_PRINT("[RPC] setRxPin - RX channel recreated on GPIO " << new_pin);
2124 rx_recreated = true;
2125 } else {
2126 FL_ERROR("[RPC] setRxPin - Failed to create RX channel on GPIO " << new_pin);
2127 response.set("error", "RxChannelCreationFailed");
2128 response.set("message", "Failed to create RX channel on new pin");
2129 // Restore old pin value
2130 mState->pin_rx = old_pin;
2131 return response;
2132 }
2133 }
2134
2135 response.set("success", true);
2136 response.set("rxPin", static_cast<int64_t>(new_pin));
2137 response.set("previousRxPin", static_cast<int64_t>(old_pin));
2138 response.set("rxChannelRecreated", rx_recreated);
2139 return response;
2140 });
2141
2142 // Register "setPins" function - set both TX and RX pins atomically
2143 mRemote->bind("setPins", [this](const fl::json& args) -> fl::json {
2144 fl::json response = fl::json::object();
2145
2146 // Accept either {txPin, rxPin} object or [txPin, rxPin] array
2147 int new_tx_pin = -1;
2148 int new_rx_pin = -1;
2149
2150 if (args.is_array() && args.size() == 1 && args[0].is_object()) {
2151 // Object form: [{txPin: int, rxPin: int}]
2152 fl::json config = args[0];
2153 if (config.contains("txPin") && config["txPin"].is_int()) {
2154 new_tx_pin = static_cast<int>(config["txPin"].as_int().value());
2155 }
2156 if (config.contains("rxPin") && config["rxPin"].is_int()) {
2157 new_rx_pin = static_cast<int>(config["rxPin"].as_int().value());
2158 }
2159 } else if (args.is_array() && args.size() == 2) {
2160 // Array form: [txPin, rxPin]
2161 if (args[0].is_int()) {
2162 new_tx_pin = static_cast<int>(args[0].as_int().value());
2163 }
2164 if (args[1].is_int()) {
2165 new_rx_pin = static_cast<int>(args[1].as_int().value());
2166 }
2167 } else {
2168 response.set("error", "InvalidArgs");
2169 response.set("message", "Expected [{txPin, rxPin}] or [txPin, rxPin]");
2170 return response;
2171 }
2172
2173 // Validate pin ranges
2174 if (new_tx_pin < 0 || new_tx_pin > 48) {
2175 response.set("error", "InvalidTxPin");
2176 response.set("message", "TX pin must be 0-48");
2177 return response;
2178 }
2179 if (new_rx_pin < 0 || new_rx_pin > 48) {
2180 response.set("error", "InvalidRxPin");
2181 response.set("message", "RX pin must be 0-48");
2182 return response;
2183 }
2184
2185 int old_tx_pin = mState->pin_tx;
2186 int old_rx_pin = mState->pin_rx;
2187 bool rx_pin_changed = (new_rx_pin != old_rx_pin);
2188 bool rx_recreated = false;
2189
2190 // Update TX pin
2191 mState->pin_tx = new_tx_pin;
2192
2193 // Update RX pin and recreate channel if changed
2194 if (rx_pin_changed) {
2195 mState->pin_rx = new_rx_pin;
2196
2197 FL_PRINT("[RPC] setPins - Recreating RX channel on GPIO " << new_rx_pin << "...");
2198
2199 // Destroy old RX channel
2200 mState->rx_channel.reset();
2201
2202 // Create new RX channel using factory
2203 mState->rx_channel = mState->rx_factory(new_rx_pin);
2204
2205 if (mState->rx_channel) {
2206 FL_PRINT("[RPC] setPins - RX channel recreated successfully");
2207 rx_recreated = true;
2208 } else {
2209 FL_ERROR("[RPC] setPins - Failed to create RX channel on GPIO " << new_rx_pin);
2210 // Rollback both pins
2211 mState->pin_tx = old_tx_pin;
2212 mState->pin_rx = old_rx_pin;
2213 response.set("error", "RxChannelCreationFailed");
2214 response.set("message", "Failed to create RX channel - pins restored to previous values");
2215 return response;
2216 }
2217 } else {
2218 mState->pin_rx = new_rx_pin;
2219 }
2220
2221 FL_PRINT("[RPC] setPins - TX: " << old_tx_pin << " → " << new_tx_pin
2222 << ", RX: " << old_rx_pin << " → " << new_rx_pin);
2223
2224 response.set("success", true);
2225 response.set("txPin", static_cast<int64_t>(new_tx_pin));
2226 response.set("rxPin", static_cast<int64_t>(new_rx_pin));
2227 response.set("previousTxPin", static_cast<int64_t>(old_tx_pin));
2228 response.set("previousRxPin", static_cast<int64_t>(old_rx_pin));
2229 response.set("rxChannelRecreated", rx_recreated);
2230 return response;
2231 });
2232
2233 // Register "findConnectedPins" function - probe adjacent pin pairs to find a jumper wire connection
2234 // This allows automatic discovery of TX/RX pin pair without requiring user to specify them
2235 mRemote->bind("findConnectedPins", [this](const fl::json& args) -> fl::json {
2237 });
2238
2239 // Register "help" function - list all RPC functions with descriptions
2240 mRemote->bind("help", [this](const fl::json& args) -> fl::json {
2241 fl::json functions = fl::json::array();
2242
2243 // Phase 1: Basic Control
2244 fl::json start_fn = fl::json::object();
2245 start_fn.set("name", "start");
2246 start_fn.set("phase", "Phase 1: Basic Control");
2247 start_fn.set("args", "[]");
2248 start_fn.set("returns", "void");
2249 start_fn.set("description", "Trigger test matrix execution");
2250 functions.push_back(start_fn);
2251
2252 fl::json status_fn = fl::json::object();
2253 status_fn.set("name", "status");
2254 status_fn.set("phase", "Phase 1: Basic Control");
2255 status_fn.set("args", "[]");
2256 status_fn.set("returns", "{startReceived, testComplete, frameCounter, state}");
2257 status_fn.set("description", "Query current test state");
2258 functions.push_back(status_fn);
2259
2260 fl::json drivers_fn = fl::json::object();
2261 drivers_fn.set("name", "drivers");
2262 drivers_fn.set("phase", "Phase 1: Basic Control");
2263 drivers_fn.set("args", "[]");
2264 drivers_fn.set("returns", "[{name, priority, enabled}, ...]");
2265 drivers_fn.set("description", "List available drivers");
2266 functions.push_back(drivers_fn);
2267
2268 // Phase 2: Configuration
2269 fl::json getConfig_fn = fl::json::object();
2270 getConfig_fn.set("name", "getConfig");
2271 getConfig_fn.set("phase", "Phase 2: Configuration");
2272 getConfig_fn.set("args", "[]");
2273 getConfig_fn.set("returns", "{drivers, laneRange, stripSizes, totalTestCases}");
2274 getConfig_fn.set("description", "Query current test matrix configuration");
2275 functions.push_back(getConfig_fn);
2276
2277 fl::json setDrivers_fn = fl::json::object();
2278 setDrivers_fn.set("name", "setDrivers");
2279 setDrivers_fn.set("phase", "Phase 2: Configuration");
2280 setDrivers_fn.set("args", "[driver1, driver2, ...]");
2281 setDrivers_fn.set("returns", "{success, driversSet, testCases}");
2282 setDrivers_fn.set("description", "Configure enabled drivers");
2283 functions.push_back(setDrivers_fn);
2284
2285 fl::json setLaneRange_fn = fl::json::object();
2286 setLaneRange_fn.set("name", "setLaneRange");
2287 setLaneRange_fn.set("phase", "Phase 2: Configuration");
2288 setLaneRange_fn.set("args", "[minLanes, maxLanes]");
2289 setLaneRange_fn.set("returns", "{success, minLanes, maxLanes, testCases}");
2290 setLaneRange_fn.set("description", "Configure lane range (1-16)");
2291 functions.push_back(setLaneRange_fn);
2292
2293 fl::json setStripSizes_fn = fl::json::object();
2294 setStripSizes_fn.set("name", "setStripSizes");
2295 setStripSizes_fn.set("phase", "Phase 2: Configuration");
2296 setStripSizes_fn.set("args", "[size] or [shortSize, longSize]");
2297 setStripSizes_fn.set("returns", "{success, stripSizesSet, testCases}");
2298 setStripSizes_fn.set("description", "Configure strip sizes");
2299 functions.push_back(setStripSizes_fn);
2300
2301 // Phase 3: Selective Execution
2302 fl::json runTestCase_fn = fl::json::object();
2303 runTestCase_fn.set("name", "runTestCase");
2304 runTestCase_fn.set("phase", "Phase 3: Selective Execution");
2305 runTestCase_fn.set("args", "[testCaseIndex]");
2306 runTestCase_fn.set("returns", "{success, testCaseIndex, result}");
2307 runTestCase_fn.set("description", "Run single test case by index");
2308 functions.push_back(runTestCase_fn);
2309
2310 fl::json runDriver_fn = fl::json::object();
2311 runDriver_fn.set("name", "runDriver");
2312 runDriver_fn.set("phase", "Phase 3: Selective Execution");
2313 runDriver_fn.set("args", "[driverName]");
2314 runDriver_fn.set("returns", "{success, driver, testsRun, results}");
2315 runDriver_fn.set("description", "Run all tests for specific driver");
2316 functions.push_back(runDriver_fn);
2317
2318 fl::json runAll_fn = fl::json::object();
2319 runAll_fn.set("name", "runAll");
2320 runAll_fn.set("phase", "Phase 3: Selective Execution");
2321 runAll_fn.set("args", "[]");
2322 runAll_fn.set("returns", "{success, totalCases, passedCases, skippedCases, results}");
2323 runAll_fn.set("description", "Run full test matrix with JSON results");
2324 functions.push_back(runAll_fn);
2325
2326 fl::json getResults_fn = fl::json::object();
2327 getResults_fn.set("name", "getResults");
2328 getResults_fn.set("phase", "Phase 3: Selective Execution");
2329 getResults_fn.set("args", "[]");
2330 getResults_fn.set("returns", "[{driver, lanes, stripSize, ...}, ...]");
2331 getResults_fn.set("description", "Return all test results");
2332 functions.push_back(getResults_fn);
2333
2334 fl::json getResult_fn = fl::json::object();
2335 getResult_fn.set("name", "getResult");
2336 getResult_fn.set("phase", "Phase 3: Selective Execution");
2337 getResult_fn.set("args", "[testCaseIndex]");
2338 getResult_fn.set("returns", "{driver, lanes, stripSize, ...}");
2339 getResult_fn.set("description", "Return specific test case result");
2340 functions.push_back(getResult_fn);
2341
2342 // Phase 4: Utility and Control
2343 fl::json reset_fn = fl::json::object();
2344 reset_fn.set("name", "reset");
2345 reset_fn.set("phase", "Phase 4: Utility");
2346 reset_fn.set("args", "[]");
2347 reset_fn.set("returns", "{success, message, testCasesCleared}");
2348 reset_fn.set("description", "Reset test state without device reboot");
2349 functions.push_back(reset_fn);
2350
2351 fl::json halt_fn = fl::json::object();
2352 halt_fn.set("name", "halt");
2353 halt_fn.set("phase", "Phase 4: Utility");
2354 halt_fn.set("args", "[]");
2355 halt_fn.set("returns", "{success, message}");
2356 halt_fn.set("description", "Trigger sketch halt");
2357 functions.push_back(halt_fn);
2358
2359 fl::json ping_fn = fl::json::object();
2360 ping_fn.set("name", "ping");
2361 ping_fn.set("phase", "Phase 4: Utility");
2362 ping_fn.set("args", "[]");
2363 ping_fn.set("returns", "{success, message, timestamp, uptimeMs, frameCounter}");
2364 ping_fn.set("description", "Health check with timestamp");
2365 functions.push_back(ping_fn);
2366
2367 // Phase 5: Pin Configuration
2368 fl::json getPins_fn = fl::json::object();
2369 getPins_fn.set("name", "getPins");
2370 getPins_fn.set("phase", "Phase 5: Pin Configuration");
2371 getPins_fn.set("args", "[]");
2372 getPins_fn.set("returns", "{txPin, rxPin, defaults: {txPin, rxPin}, platform}");
2373 getPins_fn.set("description", "Query current and default pin configuration");
2374 functions.push_back(getPins_fn);
2375
2376 fl::json setTxPin_fn = fl::json::object();
2377 setTxPin_fn.set("name", "setTxPin");
2378 setTxPin_fn.set("phase", "Phase 5: Pin Configuration");
2379 setTxPin_fn.set("args", "[pin]");
2380 setTxPin_fn.set("returns", "{success, txPin, previousTxPin, testCases}");
2381 setTxPin_fn.set("description", "Set TX pin (regenerates test cases)");
2382 functions.push_back(setTxPin_fn);
2383
2384 fl::json setRxPin_fn = fl::json::object();
2385 setRxPin_fn.set("name", "setRxPin");
2386 setRxPin_fn.set("phase", "Phase 5: Pin Configuration");
2387 setRxPin_fn.set("args", "[pin]");
2388 setRxPin_fn.set("returns", "{success, rxPin, previousRxPin, rxChannelRecreated}");
2389 setRxPin_fn.set("description", "Set RX pin (recreates RX channel)");
2390 functions.push_back(setRxPin_fn);
2391
2392 fl::json setPins_fn = fl::json::object();
2393 setPins_fn.set("name", "setPins");
2394 setPins_fn.set("phase", "Phase 5: Pin Configuration");
2395 setPins_fn.set("args", "[{txPin, rxPin}] or [txPin, rxPin]");
2396 setPins_fn.set("returns", "{success, txPin, rxPin, rxChannelRecreated, testCases}");
2397 setPins_fn.set("description", "Set both TX and RX pins atomically");
2398 functions.push_back(setPins_fn);
2399
2400 fl::json findConnectedPins_fn = fl::json::object();
2401 findConnectedPins_fn.set("name", "findConnectedPins");
2402 findConnectedPins_fn.set("phase", "Phase 5: Pin Configuration");
2403 findConnectedPins_fn.set("args", "[{startPin, endPin, autoApply}] (all optional)");
2404 findConnectedPins_fn.set("returns", "{success, found, txPin, rxPin, autoApplied, testedPairs}");
2405 findConnectedPins_fn.set("description", "Probe adjacent pin pairs to find jumper wire connection");
2406 functions.push_back(findConnectedPins_fn);
2407
2408 fl::json help_fn = fl::json::object();
2409 help_fn.set("name", "help");
2410 help_fn.set("phase", "Phase 4: Utility");
2411 help_fn.set("args", "[]");
2412 help_fn.set("returns", "[{name, phase, args, returns, description}, ...]");
2413 help_fn.set("description", "List all RPC functions with descriptions");
2414 functions.push_back(help_fn);
2415
2416 fl::json testSimd_fn = fl::json::object();
2417 testSimd_fn.set("name", "testSimd");
2418 testSimd_fn.set("phase", "Phase 4: Utility");
2419 testSimd_fn.set("args", "[]");
2420 testSimd_fn.set("returns", "{success, passed, totalTests, passedTests, failedTests, failures:[string]}");
2421 testSimd_fn.set("description", "Run comprehensive SIMD test suite (85 tests)");
2422 functions.push_back(testSimd_fn);
2423
2424 fl::json testSimdBenchmark_fn = fl::json::object();
2425 testSimdBenchmark_fn.set("name", "testSimdBenchmark");
2426 testSimdBenchmark_fn.set("phase", "Phase 4: Utility");
2427 testSimdBenchmark_fn.set("args", "[{iterations}] (optional, default 10000)");
2428 testSimdBenchmark_fn.set("returns", "{success, iterations, float_us, s16x16_us, simd_us}");
2429 testSimdBenchmark_fn.set("description", "Benchmark multiply speed: float vs s16x16 vs s16x16x4 SIMD");
2430 functions.push_back(testSimdBenchmark_fn);
2431
2432 fl::json animartrixPerlinBench_fn = fl::json::object();
2433 animartrixPerlinBench_fn.set("name", "animartrixPerlinBench");
2434 animartrixPerlinBench_fn.set("phase", "Phase 4: Utility");
2435 animartrixPerlinBench_fn.set("args", "[{iterations}] (optional, default 100, max 10000)");
2436 animartrixPerlinBench_fn.set("returns", "{success, iterations, pnoise_calls_per_iter, pnoise_float_us, pnoise_i16_us, speedup_x1000}");
2437 animartrixPerlinBench_fn.set("description", "Animartrix-representative Perlin noise bench: scalar float pnoise vs s16x16 fixed-point pnoise2d (16x16 grid per iter, mirrors a real frame). Answers: how much does fixed-point beat float for Animartrix's hot path on this hardware?");
2438 functions.push_back(animartrixPerlinBench_fn);
2439
2440 fl::json wave8ExpandBenchmark_fn = fl::json::object();
2441 wave8ExpandBenchmark_fn.set("name", "wave8ExpandBenchmark");
2442 wave8ExpandBenchmark_fn.set("phase", "Phase 4: Utility");
2443 wave8ExpandBenchmark_fn.set("args", "[{iterations}] (optional, default 30000, max 200000)");
2444 wave8ExpandBenchmark_fn.set("returns", "{success, iterations, expand_nibble_us, expand_byte_us, expand_batched_us, transpose16_nibble_us, transpose16_byte_us, sink}");
2445 wave8ExpandBenchmark_fn.set("description", "Bench PARLIO Wave8 expansion (#2526): nibble vs byte vs batched LUT, plus full per-byte-position cost (expansion + 16-lane transpose)");
2446 functions.push_back(wave8ExpandBenchmark_fn);
2447
2448 fl::json parlioEncodeBenchmark_fn = fl::json::object();
2449 parlioEncodeBenchmark_fn.set("name", "parlioEncodeBenchmark");
2450 parlioEncodeBenchmark_fn.set("phase", "Phase 4: Utility");
2451 parlioEncodeBenchmark_fn.set("args", "[{iterations}] (optional, default 12000, max 200000)");
2452 parlioEncodeBenchmark_fn.set("returns", "{success, iters, lanes, leds_per_lane, scratchPsramOk, outputPsramOk, perpos_ss_us, perpos_sp_us, perpos_ps_us, perpos_pp_us, frame_ss_us, frame_sp_us, frame_ps_us, frame_pp_us, sink}");
2453 parlioEncodeBenchmark_fn.set("description", "Bench full PARLIO encode hot loop (16-lane gather + BF1 pipe4 direct encode) with SRAM and optional PSRAM placements; answers PSRAM hypothesis + ISR-streaming feasibility");
2454 functions.push_back(parlioEncodeBenchmark_fn);
2455
2456 fl::json parlioStreamValidate_fn = fl::json::object();
2457 parlioStreamValidate_fn.set("name", "parlioStreamValidate");
2458 parlioStreamValidate_fn.set("phase", "Phase 4: Utility");
2459 parlioStreamValidate_fn.set("args", "[{baseTxPin, txPins, numLanes, numLeds, iterations, timeoutMs}] (all optional; txPins overrides contiguous baseTxPin)");
2460 parlioStreamValidate_fn.set("returns", "{success, completed, baseTxPin, txPins, lanes, leds_per_lane, iterations, perIterUs:[...], steadyAvgUs, failedIter, underrunCount, txDoneCount, workerIsrCount, ringError, hardwareIdle}");
2461 parlioStreamValidate_fn.set("description", "Functional test of the PARLIO ISR-chunked streaming engine (#2548). Drives N back-to-back FastLED.show() calls through the production engine (which uses BF1+pipe4 on 16-lane Wave8 since #2559) and verifies all complete within timeout. Catches hangs/stalls.");
2462 functions.push_back(parlioStreamValidate_fn);
2463
2464 fl::json flexioRxBenchmark_fn = fl::json::object();
2465 flexioRxBenchmark_fn.set("name", "flexioRxBenchmark");
2466 flexioRxBenchmark_fn.set("phase", "Phase 4: Utility");
2467 flexioRxBenchmark_fn.set("args", "[{frequency_hz=1000, duration_ms=100, tx_pin=3, rx_pin=4}] (all optional)");
2468 flexioRxBenchmark_fn.set("returns", "{success, frequency_hz, duration_ms, tx_pin, rx_pin, edges_captured, periods, period_mean_ns, period_sigma_ns, period_min_ns, period_max_ns}");
2469 flexioRxBenchmark_fn.set("description", "Square-wave validation for the FlexIO RX backend (Teensy 4.x only, FastLED#2764 Phase 2). Drives tx_pin via analogWriteFrequency at 50%% duty, captures via RxBackend::FLEXIO on rx_pin, reports per-period statistics.");
2470 functions.push_back(flexioRxBenchmark_fn);
2471
2472 fl::json flexioObjectFledTest_fn = fl::json::object();
2473 flexioObjectFledTest_fn.set("name", "flexioObjectFledTest");
2474 flexioObjectFledTest_fn.set("phase", "Phase 4: Utility");
2475 flexioObjectFledTest_fn.set("args", "[{test_case=0..4, tx_pin=3, rx_pin=4, capture_ms=50}] (all optional)");
2476 flexioObjectFledTest_fn.set("returns", "{success, test_case, tx_pin, rx_pin, num_leds, expected_bytes, decoded_bytes, matched, mismatched, edges_captured}");
2477 flexioObjectFledTest_fn.set("description", "End-to-end ObjectFLED TX -> FlexIO RX loopback verification (Teensy 4.x only, FastLED#2764 Phase 3). Drives WS2812 patterns through Bus::OBJECT_FLED, captures via RxBackend::FLEXIO, decodes the bit stream, and reports byte-level match counts. Five fixed test patterns: 0=red, 1=RGB triple, 2=all zeros, 3=all ones, 4=100-LED alternating.");
2478 functions.push_back(flexioObjectFledTest_fn);
2479
2480 fl::json response = fl::json::object();
2481 response.set("success", true);
2482 response.set("totalFunctions", static_cast<int64_t>(28));
2483 response.set("functions", functions);
2484 return response;
2485 });
2486
2487 // Register "testSimd" function - run comprehensive SIMD test suite
2488 mRemote->bind("testSimd", [](const fl::json& args) -> fl::json {
2489 fl::json response = fl::json::object();
2490
2491 // Run the full test suite and collect per-test results
2493 const SimdTestEntry* tests = nullptr;
2494 int num_tests = 0;
2495 autoresearch::simd_check::getTests(&tests, &num_tests);
2496
2497 int passed_count = 0;
2498 int failed_count = 0;
2499 fl::json failures = fl::json::array();
2500
2501 for (int i = 0; i < num_tests; i++) {
2502 bool ok = tests[i].func();
2503 if (ok) {
2504 passed_count++;
2505 } else {
2506 failed_count++;
2507 failures.push_back(fl::string(tests[i].name));
2508 }
2509 }
2510
2511 response.set("success", true);
2512 response.set("passed", failed_count == 0);
2513 response.set("totalTests", static_cast<int64_t>(num_tests));
2514 response.set("passedTests", static_cast<int64_t>(passed_count));
2515 response.set("failedTests", static_cast<int64_t>(failed_count));
2516 if (failed_count > 0) {
2517 response.set("failures", failures);
2518 }
2519 return response;
2520 });
2521
2522 // Register "testSimdBenchmark" - multiply speed benchmark
2523 mRemote->bind("testSimdBenchmark", [](const fl::json& args) -> fl::json {
2524 fl::json response = fl::json::object();
2525
2526 int iters = 10000;
2527 fl::json config;
2528 if (args.is_object()) {
2529 config = args;
2530 } else if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
2531 config = args[0];
2532 }
2533 if (!config.is_null() && config.contains("iterations") && config["iterations"].is_int()) {
2534 iters = static_cast<int>(config["iterations"].as_int().value());
2535 if (iters < 1) iters = 1;
2536 if (iters > 1000000) iters = 1000000;
2537 }
2538
2540
2541 response.set("success", true);
2542 response.set("iterations", result.iterations);
2543
2544 fl::json add = fl::json::object();
2545 add.set("float_us", result.add_float_us);
2546 add.set("s8x8_us", result.add_s8x8_us);
2547 add.set("s16x16_us", result.add_s16x16_us);
2548 add.set("u16x16_us", result.add_u16x16_us);
2549 add.set("simd_us", result.add_simd_us);
2550 response.set("add", add);
2551
2552 fl::json sub = fl::json::object();
2553 sub.set("float_us", result.sub_float_us);
2554 sub.set("s8x8_us", result.sub_s8x8_us);
2555 sub.set("s16x16_us", result.sub_s16x16_us);
2556 sub.set("u16x16_us", result.sub_u16x16_us);
2557 sub.set("simd_us", result.sub_simd_us);
2558 response.set("sub", sub);
2559
2560 fl::json mul = fl::json::object();
2561 mul.set("float_us", result.mul_float_us);
2562 mul.set("s8x8_us", result.mul_s8x8_us);
2563 mul.set("s16x16_us", result.mul_s16x16_us);
2564 mul.set("u16x16_us", result.mul_u16x16_us);
2565 mul.set("simd_us", result.mul_simd_us);
2566 response.set("mul", mul);
2567
2568 fl::json div = fl::json::object();
2569 div.set("float_us", result.div_float_us);
2570 div.set("s8x8_us", result.div_s8x8_us);
2571 div.set("s16x16_us", result.div_s16x16_us);
2572 div.set("u16x16_us", result.div_u16x16_us);
2573 response.set("div", div);
2574
2575 return response;
2576 });
2577
2578 // Register "animartrixPerlinBench" - Animartrix-representative Perlin
2579 // noise bench: scalar float (fl::pnoise) vs s16x16 fixed-point
2580 // (fl::perlin_i16_optimized::pnoise2d). Same workload that drives every
2581 // Animartrix frame — one Perlin lookup per output pixel, 16x16 grid
2582 // per iteration. Args: {iterations} (optional, default 100).
2583 mRemote->bind("animartrixPerlinBench", [](const fl::json& args) -> fl::json {
2584 fl::json response = fl::json::object();
2585
2586 int iters = 100;
2587 fl::json config;
2588 if (args.is_object()) {
2589 config = args;
2590 } else if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
2591 config = args[0];
2592 }
2593 if (!config.is_null() && config.contains("iterations") && config["iterations"].is_int()) {
2594 iters = static_cast<int>(config["iterations"].as_int().value());
2595 if (iters < 1) iters = 1;
2596 if (iters > 10000) iters = 10000;
2597 }
2598
2600
2601 response.set("success", true);
2602 response.set("iterations", result.iterations);
2603 // Workload-per-iter is 16*16 = 256 pnoise calls. Surface this so
2604 // the client can compute per-call timings without hardcoding.
2605 response.set("pnoise_calls_per_iter", static_cast<int64_t>(256));
2606 response.set("pnoise_float_us", result.pnoise_float_us);
2607 response.set("pnoise_i16_us", result.pnoise_i16_us);
2608 // Speedup expressed as float / i16 (>1 means i16 wins). Computed
2609 // host-side too for cross-check; this is just convenience.
2610 if (result.pnoise_i16_us > 0) {
2611 double speedup = static_cast<double>(result.pnoise_float_us) /
2612 static_cast<double>(result.pnoise_i16_us);
2613 // Encode as basis-points (1000ths) to avoid float in the json wire fmt
2614 response.set("speedup_x1000", static_cast<int64_t>(speedup * 1000.0));
2615 }
2616 return response;
2617 });
2618
2619 // Register "wave8ExpandBenchmark" - PARLIO Wave8 expansion bench (#2526).
2620 // Compares nibble-LUT vs byte-LUT vs batched byte-LUT, and times the full
2621 // per-byte-position cost (expansion + 16-lane transpose) for both LUTs.
2622 // Args: {iterations} (optional, default 30000, max 200000)
2623 mRemote->bind("wave8ExpandBenchmark", [](const fl::json& args) -> fl::json {
2624 fl::json response = fl::json::object();
2625
2626 int iters = 30000;
2627 fl::json config;
2628 if (args.is_object()) {
2629 config = args;
2630 } else if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
2631 config = args[0];
2632 }
2633 if (!config.is_null() && config.contains("iterations") && config["iterations"].is_int()) {
2634 iters = static_cast<int>(config["iterations"].as_int().value());
2635 }
2636
2638
2639 response.set("success", true);
2640 response.set("iterations", static_cast<int64_t>(r.iters));
2641 response.set("expand_nibble_us", static_cast<int64_t>(r.expand_nibble_us));
2642 response.set("expand_byte_us", static_cast<int64_t>(r.expand_byte_us));
2643 response.set("expand_batched_us", static_cast<int64_t>(r.expand_batched_us));
2644 response.set("transpose16_nibble_us", static_cast<int64_t>(r.transpose16_nibble_us));
2645 response.set("transpose16_byte_us", static_cast<int64_t>(r.transpose16_byte_us));
2646 response.set("sink", static_cast<int64_t>(r.sink));
2647 return response;
2648 });
2649
2650 // Register "parlioStreamValidate" - functional test of the production
2651 // PARLIO ISR-chunked streaming engine (#2548). Exercises the 16-lane Wave8
2652 // path which dispatches to wave8Transpose_16x4_bf1_pipe4 (BF1, #2559).
2653 // Drives N back-to-back FastLED.show() cycles and verifies each completes
2654 // within timeout. Returns per-iter timing so the host can diagnose stalls.
2655 mRemote->bind("parlioStreamValidate", [this](const fl::json& args) -> fl::json {
2656 fl::json response = fl::json::object();
2657 auto invalidArgs = [](const char* message) -> fl::json {
2658 fl::json error = fl::json::object();
2659 error.set("success", false);
2660 error.set("error", "InvalidArgs");
2661 error.set("message", message);
2662 return error;
2663 };
2664
2665 int num_lanes = 16;
2666 int num_leds = 256;
2667 int iterations = 5;
2668 int timeout_ms = 200;
2669 int base_tx_pin = mState->pin_tx;
2671 bool has_tx_pins = false;
2672 bool num_lanes_provided = false;
2673 for (int i = 0; i < autoresearch::parlio_stream::kMaxLanes; ++i) {
2674 tx_pins[i] = -1;
2675 }
2676
2677 fl::json config;
2678 if (args.is_object()) {
2679 config = args;
2680 } else if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
2681 config = args[0];
2682 }
2683 if (!config.is_null()) {
2684 if (config.contains("baseTxPin") && config["baseTxPin"].is_int())
2685 base_tx_pin = static_cast<int>(config["baseTxPin"].as_int().value());
2686 if (config.contains("numLanes")) {
2687 if (!config["numLanes"].is_int()) {
2688 return invalidArgs("numLanes must be an integer");
2689 }
2690 num_lanes = static_cast<int>(config["numLanes"].as_int().value());
2691 num_lanes_provided = true;
2692 }
2693 if (config.contains("numLeds") && config["numLeds"].is_int())
2694 num_leds = static_cast<int>(config["numLeds"].as_int().value());
2695 if (config.contains("iterations") && config["iterations"].is_int())
2696 iterations = static_cast<int>(config["iterations"].as_int().value());
2697 if (config.contains("timeoutMs") && config["timeoutMs"].is_int())
2698 timeout_ms = static_cast<int>(config["timeoutMs"].as_int().value());
2699 if (config.contains("txPins")) {
2700 if (!config["txPins"].is_array()) {
2701 return invalidArgs("txPins must be an integer array");
2702 }
2703 if (!num_lanes_provided) {
2704 return invalidArgs("txPins requires numLanes");
2705 }
2706 if (num_lanes < 1 ||
2708 return invalidArgs("numLanes out of range for txPins");
2709 }
2710
2711 const fl::json pins = config["txPins"];
2712 if (pins.size() != static_cast<size_t>(num_lanes)) {
2713 return invalidArgs("txPins length must match numLanes");
2714 }
2715
2716 for (int i = 0; i < num_lanes; ++i) {
2717 if (!pins[i].is_int()) {
2718 return invalidArgs("txPins entries must be integers");
2719 }
2720 int64_t pin_value = pins[i].as_int().value();
2721 if (pin_value < 0 || pin_value >= 64) {
2722 return invalidArgs("txPins entries must be in range 0..63");
2723 }
2724 tx_pins[i] = static_cast<int>(pin_value);
2725 }
2726 has_tx_pins = true;
2727 base_tx_pin = tx_pins[0];
2728 }
2729 }
2730
2731 // Clamp inputs to safe ranges.
2732 if (base_tx_pin < 0) base_tx_pin = 0;
2733 if (num_lanes < 1) num_lanes = 1;
2734 if (num_lanes > 16) num_lanes = 16;
2735 if (num_leds < 1) num_leds = 1;
2736 if (num_leds > 256) num_leds = 256;
2737 if (iterations < 1) iterations = 1;
2740 }
2741 if (timeout_ms < 1) timeout_ms = 1;
2742 if (timeout_ms > 5000) timeout_ms = 5000;
2743
2745 base_tx_pin, num_lanes, num_leds, iterations,
2746 static_cast<uint32_t>(timeout_ms),
2747 has_tx_pins ? tx_pins : nullptr);
2748
2749 response.set("success", true);
2750 response.set("channelsOk", r.channels_ok);
2751 response.set("completed", r.completed);
2752 response.set("baseTxPin", static_cast<int64_t>(r.base_tx_pin));
2753 int last_tx_pin = r.base_tx_pin + r.lanes - 1;
2754 if (r.explicit_tx_pins) {
2755 for (int i = r.lanes - 1; i >= 0; --i) {
2756 if (r.tx_pins[i] >= 0) {
2757 last_tx_pin = r.tx_pins[i];
2758 break;
2759 }
2760 }
2761 }
2762 response.set("lastTxPin", static_cast<int64_t>(last_tx_pin));
2763 response.set("explicitTxPins", r.explicit_tx_pins);
2764 response.set("lanes", static_cast<int64_t>(r.lanes));
2765 response.set("ledsPerLane", static_cast<int64_t>(r.leds_per_lane));
2766 response.set("iterations", static_cast<int64_t>(r.iterations));
2767 response.set("steadyAvgUs", static_cast<int64_t>(r.steady_avg_us));
2768 response.set("steadyAvgShowUs", static_cast<int64_t>(r.steady_avg_show_us));
2769 response.set("steadyAvgWaitUs", static_cast<int64_t>(r.steady_avg_wait_us));
2770 response.set("failedIter", static_cast<int64_t>(r.failed_iter));
2771 response.set("timeoutMs", static_cast<int64_t>(r.timeout_ms));
2772 response.set("txDoneCount", static_cast<int64_t>(r.tx_done_count));
2773 response.set("workerIsrCount", static_cast<int64_t>(r.worker_isr_count));
2774 response.set("underrunCount", static_cast<int64_t>(r.underrun_count));
2775 response.set("ringCount", static_cast<int64_t>(r.ring_count));
2776 response.set("bytesTotal", static_cast<int64_t>(r.bytes_total));
2777 response.set("bytesTransmitted", static_cast<int64_t>(r.bytes_transmitted));
2778 response.set("ringError", r.ring_error);
2779 response.set("hardwareIdle", r.hardware_idle);
2780 fl::json response_tx_pins = fl::json::array();
2781 for (int i = 0; i < r.lanes; ++i) {
2782 response_tx_pins.push_back(static_cast<int64_t>(r.tx_pins[i]));
2783 }
2784 response.set("txPins", response_tx_pins);
2785 fl::json per_iter = fl::json::array();
2786 fl::json per_iter_show = fl::json::array();
2787 fl::json per_iter_wait = fl::json::array();
2788 for (int i = 0; i < r.iterations; ++i) {
2789 per_iter.push_back(static_cast<int64_t>(r.per_iter_us[i]));
2790 per_iter_show.push_back(static_cast<int64_t>(r.per_iter_show_us[i]));
2791 per_iter_wait.push_back(static_cast<int64_t>(r.per_iter_wait_us[i]));
2792 }
2793 response.set("perIterUs", per_iter);
2794 response.set("perIterShowUs", per_iter_show);
2795 response.set("perIterWaitUs", per_iter_wait);
2796 return response;
2797 });
2798
2799 // Register "parlioEncodeBenchmark" - full PARLIO encode hot-loop bench with
2800 // {scratch, output} in SRAM/PSRAM (4 combinations). Answers the PSRAM
2801 // hypothesis + ISR-streaming feasibility on the byte-LUT path (#2526
2802 // follow-up).
2803 mRemote->bind("parlioEncodeBenchmark", [](const fl::json& args) -> fl::json {
2804 fl::json response = fl::json::object();
2805
2806 int iters = 12000;
2807 fl::json config;
2808 if (args.is_object()) {
2809 config = args;
2810 } else if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
2811 config = args[0];
2812 }
2813 if (!config.is_null() && config.contains("iterations") && config["iterations"].is_int()) {
2814 iters = static_cast<int>(config["iterations"].as_int().value());
2815 }
2816
2818
2819 response.set("success", r.iters > 0);
2820 response.set("iters", static_cast<int64_t>(r.iters));
2821 response.set("lanes", static_cast<int64_t>(r.lanes));
2822 response.set("leds_per_lane", static_cast<int64_t>(r.leds_per_lane));
2823 response.set("scratchPsramOk", r.scratch_psram_ok);
2824 response.set("outputPsramOk", r.output_psram_ok);
2825 response.set("perpos_ss_us", static_cast<int64_t>(r.perpos_ss_us));
2826 response.set("perpos_sp_us", static_cast<int64_t>(r.perpos_sp_us));
2827 response.set("perpos_ps_us", static_cast<int64_t>(r.perpos_ps_us));
2828 response.set("perpos_pp_us", static_cast<int64_t>(r.perpos_pp_us));
2829 if (r.iters > 0) {
2830 constexpr fl::u32 kFrameBytePositions = 256 * 3;
2831 response.set("frame_ss_us", static_cast<int64_t>(
2832 static_cast<fl::u64>(r.perpos_ss_us) * kFrameBytePositions / r.iters));
2833 response.set("frame_sp_us", static_cast<int64_t>(
2834 static_cast<fl::u64>(r.perpos_sp_us) * kFrameBytePositions / r.iters));
2835 response.set("frame_ps_us", static_cast<int64_t>(
2836 static_cast<fl::u64>(r.perpos_ps_us) * kFrameBytePositions / r.iters));
2837 response.set("frame_pp_us", static_cast<int64_t>(
2838 static_cast<fl::u64>(r.perpos_pp_us) * kFrameBytePositions / r.iters));
2839 }
2840 response.set("sink", static_cast<int64_t>(r.sink));
2841 return response;
2842 });
2843
2844 // Register "testAsync" function - verify that show() returns before TX completes (async DMA)
2845 // This proves the SPI driver releases back to the main thread while draining.
2846 mRemote->bind("testAsync", [this](const fl::json& args) -> fl::json {
2847 fl::json response = fl::json::object();
2848
2849 // Parse optional parameters from args object
2850 int num_leds = 300;
2851 fl::string requested_driver;
2852 fl::json config;
2853 if (args.is_object()) {
2854 config = args;
2855 } else if (args.is_array() && args.size() >= 1 && args[0].is_object()) {
2856 config = args[0];
2857 }
2858 if (!config.is_null()) {
2859 if (config.contains("numLeds") && config["numLeds"].is_int()) {
2860 num_leds = static_cast<int>(config["numLeds"].as_int().value());
2861 }
2862 if (config.contains("driver") && config["driver"].is_string()) {
2863 requested_driver = config["driver"].as_string().value();
2864 }
2865 }
2866
2867 // Set exclusive driver if requested (by-name path: requested_driver from RPC)
2868 if (!requested_driver.empty()) {
2869 if (!autoResearchSetExclusiveDriverByName(requested_driver.c_str())) {
2870 response.set("success", false);
2871 response.set("error", "DriverSetupFailed");
2872 fl::sstream msg;
2873 msg << "Failed to set '" << requested_driver.c_str() << "' as exclusive driver";
2874 response.set("message", msg.str().c_str());
2875 return response;
2876 }
2877 }
2878
2879 if (num_leds < 10 || num_leds > 1000) {
2880 response.set("success", false);
2881 response.set("error", "InvalidNumLeds");
2882 response.set("message", "numLeds must be 10-1000");
2883 return response;
2884 }
2885
2886 // Set up a channel with the specified number of LEDs
2887 fl::vector<CRGB> leds(num_leds);
2888 for (int i = 0; i < num_leds; i++) {
2889 leds[i] = CRGB(0xFF, 0x00, 0x80); // Solid color pattern
2890 }
2891
2892 fl::ChannelConfig channel_config(
2893 mState->pin_tx,
2895 leds,
2896 RGB
2897 );
2898
2899 auto channel = FastLED.add(channel_config);
2900 if (!channel) {
2901 response.set("success", false);
2902 response.set("error", "ChannelCreationFailed");
2903 response.set("message", "Failed to create channel");
2904 return response;
2905 }
2906
2907 // === DRAW 1: May include one-time SPI hardware init overhead ===
2908 uint32_t t0 = micros();
2909 FastLED.show();
2910 uint32_t t1 = micros();
2911 FastLED.wait(5000); // 5 second timeout
2912 uint32_t t2 = micros();
2913
2914 uint32_t show1_us = t1 - t0;
2915 uint32_t wait1_us = t2 - t1;
2916 uint32_t total1_us = t2 - t0;
2917
2918 // === DRAW 2: Should be fast (no init overhead) ===
2919 uint32_t t3 = micros();
2920 FastLED.show();
2921 uint32_t t4 = micros();
2922 FastLED.wait(5000); // 5 second timeout
2923 uint32_t t5 = micros();
2924
2925 uint32_t show2_us = t4 - t3;
2926 uint32_t wait2_us = t5 - t4;
2927 uint32_t total2_us = t5 - t3;
2928
2929 // Clean up channel
2931
2932 // Determine if async behavior is working on draw 2 (no init overhead):
2933 // If async: show_us << total_us (show returns quickly, wait blocks for remainder)
2934 // If blocking: show_us ≈ total_us (show blocks for entire TX, wait returns instantly)
2935 // Pass criterion: show_us < 50% of total_us (on draw 2)
2936 bool passed = (total2_us > 0) && (show2_us < total2_us / 2);
2937
2938 // Determine driver name
2939 fl::string driver_name = "unknown";
2940 for (fl::size i = 0; i < mState->drivers_available.size(); i++) {
2941 if (mState->drivers_available[i].enabled) {
2942 driver_name = mState->drivers_available[i].name;
2943 break;
2944 }
2945 }
2946
2947 response.set("success", true);
2948 response.set("passed", passed);
2949 // Draw 1 (with possible init overhead)
2950 response.set("show1_us", static_cast<int64_t>(show1_us));
2951 response.set("wait1_us", static_cast<int64_t>(wait1_us));
2952 response.set("total1_us", static_cast<int64_t>(total1_us));
2953 // Draw 2 (steady-state, no init)
2954 response.set("show2_us", static_cast<int64_t>(show2_us));
2955 response.set("wait2_us", static_cast<int64_t>(wait2_us));
2956 response.set("total2_us", static_cast<int64_t>(total2_us));
2957 response.set("num_leds", static_cast<int64_t>(num_leds));
2958 response.set("driver", driver_name.c_str());
2959
2960 fl::sstream msg;
2961 if (passed) {
2962 msg << "Async OK: draw2 show()=" << show2_us
2963 << "us, wait()=" << wait2_us
2964 << "us, total=" << total2_us << "us"
2965 << " (draw1 show=" << show1_us << "us)";
2966 } else {
2967 msg << "Async FAIL: draw2 show()=" << show2_us
2968 << "us out of " << total2_us
2969 << "us total (expected <50%)"
2970 << " (draw1 show=" << show1_us << "us)";
2971 }
2972 response.set("message", msg.str().c_str());
2973
2974 return response;
2975 });
2976
2977 // ========================================================================
2978 // Network Validation RPC Functions
2979 // ========================================================================
2980
2981 // Register "startNetServer" - Start WiFi AP + HTTP server for net-server validation
2982 mRemote->bind("startNetServer", [this](const fl::json& args) -> fl::json {
2983 mState->net_server_active = true;
2984 return startNetServer();
2985 });
2986
2987 // Register "startNetClient" - Start WiFi AP only for net-client validation
2988 mRemote->bind("startNetClient", [this](const fl::json& args) -> fl::json {
2989 mState->net_client_active = true;
2990 return startNetClient();
2991 });
2992
2993 // Register "runNetClientTest" - ESP32 fetches from host HTTP server
2994 // Args: {host_ip: string, port: int}
2995 mRemote->bind("runNetClientTest", [](const fl::json& args) -> fl::json {
2996 fl::json response = fl::json::object();
2997
2998 // Parse arguments - args is the config object
2999 if (!args.is_object()) {
3000 response.set("success", false);
3001 response.set("error", "Expected object with host_ip and port");
3002 return response;
3003 }
3004
3005 fl::json host_ip_val = args[fl::string("host_ip")];
3006 fl::json port_val = args[fl::string("port")];
3007
3008 if (!host_ip_val.is_string() || !port_val.is_int()) {
3009 response.set("success", false);
3010 response.set("error", "Expected {host_ip: string, port: int}");
3011 return response;
3012 }
3013
3014 fl::string host_ip = host_ip_val.as_string().value();
3015 uint16_t port = static_cast<uint16_t>(port_val.as_int().value());
3016
3017 return runNetClientTest(host_ip.c_str(), port);
3018 });
3019
3020 // Register "runNetLoopback" - Self-contained loopback test (no WiFi needed)
3021 // Starts HTTP server on localhost, client GETs 127.0.0.1 endpoints
3022 mRemote->bind("runNetLoopback", [](const fl::json& args) -> fl::json {
3023 return runNetLoopback();
3024 });
3025
3026 // Register "stopNet" - Stop WiFi AP and HTTP server/client
3027 mRemote->bind("stopNet", [this](const fl::json& args) -> fl::json {
3028 mState->net_server_active = false;
3029 mState->net_client_active = false;
3030 return stopNet();
3031 });
3032
3033 // ========================================================================
3034 // OTA Validation RPC Functions
3035 // ========================================================================
3036
3037 // Register "startOta" - Start WiFi AP + OTA HTTP server for OTA validation
3038 mRemote->bind("startOta", [](const fl::json& args) -> fl::json {
3039 return startOta();
3040 });
3041
3042 // Register "stopOta" - Stop OTA server and WiFi AP
3043 mRemote->bind("stopOta", [](const fl::json& args) -> fl::json {
3044 return stopOta();
3045 });
3046
3047 // ========================================================================
3048 // BLE Validation RPC Functions
3049 // ========================================================================
3050
3051 // Register "startBle" - Start BLE GATT server + create BLE Remote
3052 mRemote->bind("startBle", [this](const fl::json& args) -> fl::json {
3053 return this->startBleRemote();
3054 });
3055
3056 // Register "stopBle" - Stop BLE GATT server + destroy BLE Remote
3057 mRemote->bind("stopBle", [this](const fl::json& args) -> fl::json {
3058 return this->stopBleRemote();
3059 });
3060
3061 // Register "decodeFile" - Decode a media file and return first 16 pixels
3062 // Args: [base64_data_string, extension_string] (base64 auto-decoded to fl::vector<fl::u8>)
3063 mRemote->bind("decodeFile", [](fl::vector<fl::u8> data, fl::string ext) -> fl::json {
3064 fl::json response = fl::json::object();
3065
3066 if (ext != ".mp4") {
3067 response.set("success", false);
3068 response.set("error", "Only .mp4 supported for device decode");
3069 return response;
3070 }
3071
3072 // Parse MP4 container metadata
3073 fl::string error;
3074 fl::H264Info info = fl::H264::parseH264Info(data, &error);
3075 if (!info.isValid) {
3076 response.set("success", false);
3077 response.set("error", error.c_str());
3078 return response;
3079 }
3080 response.set("width", static_cast<int64_t>(info.width));
3081 response.set("height", static_cast<int64_t>(info.height));
3082
3083 if (!fl::H264::isSupported()) {
3084 response.set("success", false);
3085 response.set("error", "H264 decoder not supported on this platform");
3086 return response;
3087 }
3088
3089 // Create decoder and decode first frame
3090 fl::string dec_error;
3091 auto decoder = fl::H264::createDecoder(fl::H264Config{}, &dec_error);
3092 if (!decoder) {
3093 response.set("success", false);
3094 response.set("error", dec_error.c_str());
3095 return response;
3096 }
3097
3098 auto stream = fl::make_shared<fl::memorybuf>(data.size());
3099 stream->write(data);
3100
3101 if (!decoder->begin(stream)) {
3102 fl::string msg;
3103 decoder->hasError(&msg);
3104 response.set("success", false);
3105 response.set("error", msg.empty() ? "Decoder begin() failed" : msg.c_str());
3106 return response;
3107 }
3108
3109 auto result = decoder->decode();
3110 if (result != fl::DecodeResult::Success) {
3111 response.set("success", false);
3112 response.set("error", "Decode returned non-success");
3113 response.set("decode_result", static_cast<int64_t>(static_cast<int>(result)));
3114 decoder->end();
3115 return response;
3116 }
3117
3118 fl::Frame frame = decoder->getCurrentFrame();
3119 decoder->end();
3120
3121 if (!frame.isValid()) {
3122 response.set("success", false);
3123 response.set("error", "Decoded frame is invalid");
3124 return response;
3125 }
3126
3127 response.set("success", true);
3128 response.set("frame_width", static_cast<int64_t>(frame.getWidth()));
3129 response.set("frame_height", static_cast<int64_t>(frame.getHeight()));
3130
3131 // Return first 16 pixels as [[r,g,b], ...]
3132 auto pixels = frame.rgb();
3133 fl::json pixel_array = fl::json::array();
3134 int count = pixels.size() < 16 ? static_cast<int>(pixels.size()) : 16;
3135 for (int i = 0; i < count; i++) {
3137 px.push_back(static_cast<int64_t>(pixels[i].r));
3138 px.push_back(static_cast<int64_t>(pixels[i].g));
3139 px.push_back(static_cast<int64_t>(pixels[i].b));
3140 pixel_array.push_back(px);
3141 }
3142 response.set("pixels", pixel_array);
3143
3144 return response;
3145 });
3146
3147 // Register "bleStatus" - Query BLE connection/subscription state
3148 mRemote->bind("bleStatus", [this](const fl::json& args) -> fl::json {
3149 fl::json response = fl::json::object();
3150 response.set("ble_active", mState->ble_server_active);
3152 response.set("connected", info.connected);
3153 response.set("connected_count", static_cast<int64_t>(info.connectedCount));
3154 response.set("tx_char_exists", info.txCharExists);
3155 response.set("tx_value_len", static_cast<int64_t>(info.txValueLen));
3156 response.set("ring_head", static_cast<int64_t>(info.ringHead));
3157 response.set("ring_tail", static_cast<int64_t>(info.ringTail));
3158 return response;
3159 });
3160
3161 // ========================================================================
3162 // Coroutine Tests - fl::task::coroutine() and fl::task::await()
3163 // ========================================================================
3164
3165 // Test: Basic coroutine creation and completion
3166 mRemote->bind("testCoroutineBasic", [](const fl::json& args) -> fl::json {
3167 (void)args;
3169
3170 fl::atomic<bool> task_ran(false);
3171 fl::atomic<bool> task_completed(false);
3172
3174 cfg.func = [&task_ran, &task_completed]() {
3175 task_ran.store(true);
3176 delay(50);
3177 task_completed.store(true);
3178 };
3179 cfg.name = "test_basic";
3180 auto t = fl::task::coroutine(cfg);
3181
3182 uint32_t start = millis();
3183 while (t.isRunning() && (millis() - start) < 2000) {
3184 delay(10);
3185 }
3186
3187 bool passed = task_ran.load() && task_completed.load() && !t.isRunning();
3188 r.set("success", passed);
3189 r.set("taskRan", task_ran.load());
3190 r.set("taskCompleted", task_completed.load());
3191 r.set("isRunning", t.isRunning());
3192 r.set("durationMs", static_cast<int64_t>(millis() - start));
3193 return r;
3194 });
3195
3196 // Test: Task stop() while running
3197 mRemote->bind("testCoroutineStop", [](const fl::json& args) -> fl::json {
3198 (void)args;
3200
3201 fl::atomic<bool> task_started(false);
3202
3204 cfg.func = [&task_started]() {
3205 task_started.store(true);
3206 while (true) {
3207 delay(10);
3208 }
3209 };
3210 cfg.name = "test_stop";
3211 auto t = fl::task::coroutine(cfg);
3212
3213 uint32_t start = millis();
3214 while (!task_started.load() && (millis() - start) < 2000) {
3215 delay(10);
3216 }
3217
3218 bool was_running = t.isRunning();
3219 t.stop();
3220 bool stopped = !t.isRunning();
3221
3222 bool passed = task_started.load() && was_running && stopped;
3223 r.set("success", passed);
3224 r.set("taskStarted", task_started.load());
3225 r.set("wasRunning", was_running);
3226 r.set("stopped", stopped);
3227 r.set("durationMs", static_cast<int64_t>(millis() - start));
3228 return r;
3229 });
3230
3231 // Test: Multiple concurrent coroutines
3232 mRemote->bind("testCoroutineConcurrent", [](const fl::json& args) -> fl::json {
3233 (void)args;
3235
3236 const int NUM_TASKS = 3;
3237 fl::atomic<int> completed_count(0);
3238 fl::atomic<bool> task_flags[NUM_TASKS];
3239 for (int i = 0; i < NUM_TASKS; i++) {
3240 task_flags[i].store(false);
3241 }
3242
3243 fl::task::Handle tasks[NUM_TASKS];
3244 for (int i = 0; i < NUM_TASKS; i++) {
3246 cfg.func = [i, &task_flags, &completed_count]() {
3247 delay(20 + i * 20);
3248 task_flags[i].store(true);
3249 completed_count.fetch_add(1);
3250 };
3251 cfg.name = "test_concurrent";
3252 tasks[i] = fl::task::coroutine(cfg);
3253 }
3254
3255 uint32_t start = millis();
3256 while (completed_count.load() < NUM_TASKS && (millis() - start) < 3000) {
3257 delay(10);
3258 }
3259
3260 bool all_completed = true;
3261 bool all_stopped = true;
3262 for (int i = 0; i < NUM_TASKS; i++) {
3263 if (!task_flags[i].load()) all_completed = false;
3264 if (tasks[i].isRunning()) all_stopped = false;
3265 }
3266
3267 bool passed = all_completed && all_stopped && (completed_count.load() == NUM_TASKS);
3268 r.set("success", passed);
3269 r.set("completedCount", static_cast<int64_t>(completed_count.load()));
3270 r.set("allCompleted", all_completed);
3271 r.set("allStopped", all_stopped);
3272 r.set("durationMs", static_cast<int64_t>(millis() - start));
3273 return r;
3274 });
3275
3276 // Test: Consumer coroutine awaits promise, producer coroutine fulfills it.
3277 // Verifies fl::task::await() truly blocks until the producer resolves the promise.
3278 // Main thread does NOT touch the promise — only the producer coroutine does.
3279 mRemote->bind("testCoroutineAwait", [](const fl::json& args) -> fl::json {
3280 (void)args;
3282
3284 fl::atomic<bool> consumer_started(false);
3285 fl::atomic<bool> consumer_finished(false);
3286 fl::atomic<int> consumer_value(0);
3287 fl::atomic<bool> consumer_ok(false);
3288 fl::atomic<bool> producer_started(false);
3289 fl::atomic<bool> producer_finished(false);
3290
3291 // Consumer coroutine: starts first, calls fl::task::await() which should block
3292 // until the producer coroutine resolves the promise
3293 fl::task::CoroutineConfig consumer_cfg;
3294 consumer_cfg.func = [promise_ptr, &consumer_started, &consumer_finished,
3295 &consumer_value, &consumer_ok]() {
3296 consumer_started.store(true);
3297 // This should block the coroutine until producer fulfills the promise
3298 auto result = fl::task::await(*promise_ptr);
3299 if (result.ok()) {
3300 consumer_ok.store(true);
3301 consumer_value.store(result.value());
3302 }
3303 consumer_finished.store(true);
3304 };
3305 consumer_cfg.name = "await_consumer";
3306 auto consumer = fl::task::coroutine(consumer_cfg);
3307
3308 // Small delay so consumer enters fl::task::await() before producer starts
3309 delay(50);
3310
3311 // Producer coroutine: simulates async work, then resolves the promise
3312 fl::task::CoroutineConfig producer_cfg;
3313 producer_cfg.func = [promise_ptr, &producer_started, &producer_finished]() {
3314 producer_started.store(true);
3315 delay(200); // simulate real async work (e.g. sensor read, network I/O)
3316 promise_ptr->complete_with_value(42);
3317 producer_finished.store(true);
3318 };
3319 producer_cfg.name = "await_producer";
3320 auto producer = fl::task::coroutine(producer_cfg);
3321
3322 // Main thread waits for both coroutines to finish (polling only)
3323 uint32_t start = millis();
3324 while ((!consumer_finished.load() || producer.isRunning()) && (millis() - start) < 5000) {
3325 delay(10);
3326 }
3327
3328 // Verify: consumer was truly blocked — it should not finish before producer
3329 bool passed = consumer_started.load() && consumer_finished.load()
3330 && consumer_ok.load() && (consumer_value.load() == 42)
3331 && producer_started.load() && producer_finished.load();
3332 r.set("success", passed);
3333 r.set("consumerStarted", consumer_started.load());
3334 r.set("consumerFinished", consumer_finished.load());
3335 r.set("consumerOk", consumer_ok.load());
3336 r.set("consumerValue", static_cast<int64_t>(consumer_value.load()));
3337 r.set("producerStarted", producer_started.load());
3338 r.set("producerFinished", producer_finished.load());
3339 r.set("durationMs", static_cast<int64_t>(millis() - start));
3340 return r;
3341 });
3342
3343 // Test: Consumer coroutine awaits promise, producer coroutine rejects it.
3344 // Verifies that fl::task::await() properly propagates errors from producer.
3345 mRemote->bind("testCoroutineAwaitError", [](const fl::json& args) -> fl::json {
3346 (void)args;
3348
3350 fl::atomic<bool> consumer_finished(false);
3351 fl::atomic<bool> got_error(false);
3352 fl::atomic<bool> producer_finished(false);
3353
3354 // Consumer coroutine: awaits promise, expects error
3355 fl::task::CoroutineConfig consumer_cfg;
3356 consumer_cfg.func = [promise_ptr, &consumer_finished, &got_error]() {
3357 auto result = fl::task::await(*promise_ptr);
3358 if (!result.ok()) {
3359 got_error.store(true);
3360 }
3361 consumer_finished.store(true);
3362 };
3363 consumer_cfg.name = "await_err_consumer";
3364 auto consumer = fl::task::coroutine(consumer_cfg);
3365
3366 delay(50); // let consumer enter await
3367
3368 // Producer coroutine: rejects the promise with error
3369 fl::task::CoroutineConfig producer_cfg;
3370 producer_cfg.func = [promise_ptr, &producer_finished]() {
3371 delay(100);
3372 promise_ptr->complete_with_error(fl::task::Error("test error"));
3373 producer_finished.store(true);
3374 };
3375 producer_cfg.name = "await_err_producer";
3376 auto producer = fl::task::coroutine(producer_cfg);
3377
3378 uint32_t start = millis();
3379 while ((!consumer_finished.load() || producer.isRunning()) && (millis() - start) < 5000) {
3380 delay(10);
3381 }
3382
3383 bool passed = consumer_finished.load() && got_error.load() && producer_finished.load();
3384 r.set("success", passed);
3385 r.set("consumerFinished", consumer_finished.load());
3386 r.set("gotError", got_error.load());
3387 r.set("producerFinished", producer_finished.load());
3388 r.set("durationMs", static_cast<int64_t>(millis() - start));
3389 return r;
3390 });
3391
3392 // Test: Promise then/catch_ callbacks fire correctly when fulfilled by a coroutine.
3393 // The promise is created with .then() and .catch_() callbacks attached BEFORE
3394 // the producer coroutine fulfills it, verifying callback dispatch works.
3395 mRemote->bind("testCoroutinePromiseCallbacks", [](const fl::json& args) -> fl::json {
3396 (void)args;
3398
3400 fl::atomic<bool> then_called(false);
3401 fl::atomic<int> then_value(0);
3402 fl::atomic<bool> catch_called(false);
3403 fl::atomic<bool> producer_finished(false);
3404
3405 // Attach callbacks to the promise BEFORE producer runs
3406 promise_ptr->then([&then_called, &then_value](const int& val) {
3407 then_called.store(true);
3408 then_value.store(val);
3409 });
3410 promise_ptr->catch_([&catch_called](const fl::task::Error&) {
3411 catch_called.store(true);
3412 });
3413
3414 // Producer coroutine fulfills the promise
3415 fl::task::CoroutineConfig producer_cfg;
3416 producer_cfg.func = [promise_ptr, &producer_finished]() {
3417 delay(100);
3418 promise_ptr->complete_with_value(99);
3419 producer_finished.store(true);
3420 };
3421 producer_cfg.name = "cb_producer";
3422 auto producer = fl::task::coroutine(producer_cfg);
3423
3424 uint32_t start = millis();
3425 while (producer.isRunning() && (millis() - start) < 5000) {
3426 delay(10);
3427 promise_ptr->update(); // pump callbacks
3428 }
3429 promise_ptr->update(); // final pump
3430
3431 // then() should fire, catch_() should NOT
3432 bool passed = then_called.load() && (then_value.load() == 99)
3433 && !catch_called.load() && producer_finished.load();
3434 r.set("success", passed);
3435 r.set("thenCalled", then_called.load());
3436 r.set("thenValue", static_cast<int64_t>(then_value.load()));
3437 r.set("catchCalled", catch_called.load());
3438 r.set("producerFinished", producer_finished.load());
3439 r.set("durationMs", static_cast<int64_t>(millis() - start));
3440 return r;
3441 });
3442
3443 // Test: Promise catch_ callback fires on rejection by a coroutine.
3444 mRemote->bind("testCoroutinePromiseCatchCallback", [](const fl::json& args) -> fl::json {
3445 (void)args;
3447
3449 fl::atomic<bool> then_called(false);
3450 fl::atomic<bool> catch_called(false);
3451 fl::atomic<bool> producer_finished(false);
3452
3453 promise_ptr->then([&then_called](const int&) {
3454 then_called.store(true);
3455 });
3456 promise_ptr->catch_([&catch_called](const fl::task::Error&) {
3457 catch_called.store(true);
3458 });
3459
3460 // Producer coroutine rejects the promise
3461 fl::task::CoroutineConfig producer_cfg;
3462 producer_cfg.func = [promise_ptr, &producer_finished]() {
3463 delay(100);
3464 promise_ptr->complete_with_error(fl::task::Error("rejection test"));
3465 producer_finished.store(true);
3466 };
3467 producer_cfg.name = "catch_producer";
3468 auto producer = fl::task::coroutine(producer_cfg);
3469
3470 uint32_t start = millis();
3471 while (producer.isRunning() && (millis() - start) < 5000) {
3472 delay(10);
3473 promise_ptr->update();
3474 }
3475 promise_ptr->update();
3476
3477 // catch_() should fire, then() should NOT
3478 bool passed = !then_called.load() && catch_called.load() && producer_finished.load();
3479 r.set("success", passed);
3480 r.set("thenCalled", then_called.load());
3481 r.set("catchCalled", catch_called.load());
3482 r.set("producerFinished", producer_finished.load());
3483 r.set("durationMs", static_cast<int64_t>(millis() - start));
3484 return r;
3485 });
3486
3487 // Test: Pipeline of 3 coroutines passing data through promises.
3488 // coroutine A produces value -> promise1 -> coroutine B transforms -> promise2 -> coroutine C consumes
3489 mRemote->bind("testCoroutineChainedAwait", [](const fl::json& args) -> fl::json {
3490 (void)args;
3492
3495 fl::atomic<int> final_value(0);
3496 fl::atomic<bool> chain_complete(false);
3497 fl::atomic<bool> a_done(false);
3498 fl::atomic<bool> b_done(false);
3499 fl::atomic<bool> c_done(false);
3500
3501 // Coroutine C: end of chain, awaits p2
3503 cfg_c.func = [p2, &final_value, &chain_complete, &c_done]() {
3504 auto result = fl::task::await(*p2);
3505 if (result.ok()) {
3506 final_value.store(result.value());
3507 }
3508 c_done.store(true);
3509 chain_complete.store(true);
3510 };
3511 cfg_c.name = "chain_c";
3512 auto tc = fl::task::coroutine(cfg_c);
3513
3514 // Coroutine B: middle of chain, awaits p1, transforms, fulfills p2
3516 cfg_b.func = [p1, p2, &b_done]() {
3517 auto result = fl::task::await(*p1);
3518 if (result.ok()) {
3519 // Transform: multiply by 10
3520 p2->complete_with_value(result.value() * 10);
3521 } else {
3522 p2->complete_with_error(result.error());
3523 }
3524 b_done.store(true);
3525 };
3526 cfg_b.name = "chain_b";
3527 auto tb = fl::task::coroutine(cfg_b);
3528
3529 // Coroutine A: head of chain, produces the initial value into p1
3531 cfg_a.func = [p1, &a_done]() {
3532 delay(100); // simulate work
3533 p1->complete_with_value(7);
3534 a_done.store(true);
3535 };
3536 cfg_a.name = "chain_a";
3537 auto ta = fl::task::coroutine(cfg_a);
3538
3539 uint32_t start = millis();
3540 while (!chain_complete.load() && (millis() - start) < 5000) {
3541 delay(10);
3542 }
3543
3544 // 7 * 10 = 70
3545 bool passed = chain_complete.load() && (final_value.load() == 70)
3546 && a_done.load() && b_done.load() && c_done.load();
3547 r.set("success", passed);
3548 r.set("finalValue", static_cast<int64_t>(final_value.load()));
3549 r.set("aDone", a_done.load());
3550 r.set("bDone", b_done.load());
3551 r.set("cDone", c_done.load());
3552 r.set("durationMs", static_cast<int64_t>(millis() - start));
3553 return r;
3554 });
3555
3556 // Run all coroutine tests sequentially
3557 mRemote->bind("testCoroutineAll", [this](const fl::json& args) -> fl::json {
3558 (void)args;
3560
3561 const char* test_names[] = {
3562 "testCoroutineBasic",
3563 "testCoroutineStop",
3564 "testCoroutineConcurrent",
3565 "testCoroutineAwait",
3566 "testCoroutineAwaitError",
3567 "testCoroutinePromiseCallbacks",
3568 "testCoroutinePromiseCatchCallback",
3569 "testCoroutineChainedAwait"
3570 };
3571 const int num_tests = 8;
3572
3573 int passed = 0;
3574 int failed = 0;
3575 fl::json results = fl::json::object();
3576
3577 for (int i = 0; i < num_tests; i++) {
3578 fl::json empty_args = fl::json::object();
3579 auto bound = mRemote->get<fl::json(const fl::json&)>(test_names[i]);
3580 if (bound.ok()) {
3581 fl::json test_result = bound.value()(empty_args);
3582 bool success = false;
3583 if (test_result.contains("success")) {
3584 auto val = test_result["success"].as_bool();
3585 success = val.has_value() && val.value();
3586 }
3587 if (success) {
3588 passed++;
3589 FL_PRINT("[COROUTINE] PASS: " << test_names[i]);
3590 } else {
3591 failed++;
3592 FL_PRINT("[COROUTINE] FAIL: " << test_names[i]);
3593 }
3594 results.set(test_names[i], test_result);
3595 } else {
3596 failed++;
3597 fl::json err = fl::json::object();
3598 err.set("success", false);
3599 err.set("error", "Method not found");
3600 results.set(test_names[i], err);
3601 FL_PRINT("[COROUTINE] FAIL: " << test_names[i] << " (not found)");
3602 }
3603 }
3604
3605 r.set("success", failed == 0);
3606 r.set("passed", static_cast<int64_t>(passed));
3607 r.set("failed", static_cast<int64_t>(failed));
3608 r.set("total", static_cast<int64_t>(num_tests));
3609 r.set("results", results);
3610 return r;
3611 });
3612}
3613
3614void AutoResearchRemoteControl::tick(uint32_t current_millis) {
3615 if (mRemote) {
3616 // Remote::update() does pull + tick + push
3617 mRemote->update(current_millis);
3618 }
3619 if (mBleRemote) {
3620 mBleRemote->update(current_millis);
3621 }
3622 // Deferred BLE teardown: stopBle RPC sets this flag so the response
3623 // is sent (via push() above) before we call ble::destroyTransport().
3624 if (mPendingBleStop) {
3625 mPendingBleStop = false;
3626 mBleRemote.reset(); // destroy lambdas before freeing state they capture
3628 mBleState = nullptr;
3629 mState->ble_server_active = false;
3631 FL_WARN("[BLE] Deferred teardown complete");
3632 }
3633}
3634
3636 // Register the core methods that BLE remote needs.
3637 // This registers a subset of methods — enough for ping/pong PoC.
3638
3639 // Register "ping" function - health check with timestamp
3640 remote->bind("ping", [this](const fl::json& args) -> fl::json {
3641 uint32_t now = millis();
3642 fl::json response = fl::json::object();
3643 response.set("success", true);
3644 response.set("message", "pong");
3645 response.set("timestamp", static_cast<int64_t>(now));
3646 response.set("uptimeMs", static_cast<int64_t>(now));
3647 response.set("transport", "ble");
3648 return response;
3649 });
3650
3651 // Register "status" function - device readiness check
3652 remote->bind("status", [this](const fl::json& args) -> fl::json {
3653 fl::json status = fl::json::object();
3654 status.set("ready", true);
3655 status.set("pinTx", static_cast<int64_t>(mState->pin_tx));
3656 status.set("pinRx", static_cast<int64_t>(mState->pin_rx));
3657 status.set("transport", "ble");
3658 return status;
3659 });
3660}
3661
3663 if (mBleRemote) {
3664 fl::json response = fl::json::object();
3665 response.set("success", true);
3666 response.set("message", "BLE remote already active");
3667 response.set("device_name", AUTORESEARCH_BLE_DEVICE_NAME);
3668 return response;
3669 }
3670
3671 // Create BLE GATT server (heap-allocates transport state)
3672 // On stub platforms, createTransport returns nullptr and logs FL_ERROR.
3674 if (!mBleState) {
3675 fl::json response = fl::json::object();
3676 response.set("success", false);
3677 response.set("error", "BLE not available on this platform");
3678 return response;
3679 }
3680
3681 // Get transport lambdas that capture mBleState
3683
3684 // Create BLE Remote instance with BLE transport
3685 mBleRemote = fl::make_unique<fl::Remote>(callbacks.first, callbacks.second);
3686
3687 // Register RPC methods on the BLE remote
3689
3690 mState->ble_server_active = true;
3692
3693 fl::json response = fl::json::object();
3694 response.set("success", true);
3695 response.set("device_name", AUTORESEARCH_BLE_DEVICE_NAME);
3696 response.set("service_uuid", FL_BLE_SERVICE_UUID);
3697 response.set("rx_uuid", FL_BLE_CHAR_RX_UUID);
3698 response.set("tx_uuid", FL_BLE_CHAR_TX_UUID);
3699 FL_WARN("[BLE] Remote created and advertising");
3700 return response;
3701}
3702
3704 // Defer actual BLE teardown to tick() so the RPC response is sent first.
3705 // BLEDevice::deinit(true) blocks long enough to prevent the response
3706 // from being transmitted over serial before the device resets BLE state.
3707 mPendingBleStop = true;
3708 fl::json response = fl::json::object();
3709 response.set("success", true);
3710 return response;
3711}
fl::CRGB leds[NUM_LEDS]
fl::UISlider brightness("Brightness", BRIGHTNESS, 0, 255)
AutoResearchBleState & getBleState()
Get current BLE autoresearch state.
#define AUTORESEARCH_BLE_DEVICE_NAME
bool autoResearchSetExclusiveDriverByName(const char *name)
AutoResearch-style helper: set an exclusive driver by name.
fl::json runNetLoopback()
Run self-contained loopback test: start HTTP server, client GETs localhost.
fl::json startNetClient()
Start WiFi Soft AP only (for net-client mode).
fl::json runNetClientTest(const char *host_ip, uint16_t port)
Run HTTP client tests against a host server.
fl::json startNetServer()
Start WiFi Soft AP and HTTP server with test endpoints.
fl::json stopNet()
Stop WiFi AP and HTTP server/client, release all resources.
fl::json stopOta()
Stop OTA server and WiFi AP, release all resources.
fl::json startOta()
Start WiFi Soft AP and OTA HTTP server.
Full PARLIO encode bench for ESP32-P4 (post-byte-LUT, #2526 landed).
PARLIO ISR-streaming functional validation (#2548 deep-dive follow-up).
void printStreamRaw(const char *messageType, const fl::json &data)
Print JSONL stream message directly to Serial, bypassing fl::println.
void printJsonRaw(const fl::json &json, const char *prefix)
Print JSON directly to Serial, bypassing fl::println and ScopedLogDisable This ensures RPC responses ...
fl::json makeResponse(bool success, ReturnCode returnCode, const char *message, const fl::json &data=fl::json())
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)
Wave8 expansion micro-benchmark for #2526.
TestState state
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
fl::unique_ptr< fl::Remote > remote
Definition RpcClient.ino:43
int passed_tests
Definition SIMD.ino:75
int total_tests
Definition SIMD.ino:74
int pins[]
Definition Spi.ino:11
#define FL_BLE_CHAR_TX_UUID
Definition ble.h:41
#define FL_BLE_CHAR_RX_UUID
Definition ble.h:40
#define FL_BLE_SERVICE_UUID
Definition ble.h:39
fl::net::ble — BLE GATT transport layer for JSON-RPC
SPI encoder configuration for clocked LED chipsets.
fl::json stopBleRemote()
Stop BLE remote (destroys BLE Remote + GATT server)
fl::json runParallelTestImpl(const fl::json &args)
void tick(uint32_t current_millis)
Process RPC system (pull + tick + push)
fl::json runSingleTestImpl(const fl::json &args)
fl::net::ble::TransportState * mBleState
fl::unique_ptr< fl::Remote > mBleRemote
fl::shared_ptr< AutoResearchState > mState
fl::json findConnectedPinsImpl(const fl::json &args)
void registerFunctions(fl::shared_ptr< AutoResearchState > state)
Register all RPC functions with shared autoresearch state.
~AutoResearchRemoteControl()
Destructor.
fl::json startBleRemote()
Start BLE remote (creates BLE GATT server + second Remote instance)
void registerAllMethods(fl::Remote *remote)
Register all RPC methods on a given Remote instance.
fl::unique_ptr< fl::Remote > mRemote
void store(T value, memory_order=memory_order_seq_cst) FL_NOEXCEPT
Definition atomic.h:65
T load(memory_order=memory_order_seq_cst) const FL_NOEXCEPT
Definition atomic.h:61
T fetch_add(T value) FL_NOEXCEPT
Definition atomic.h:157
fl::u16 getWidth() const
Definition frame.h:59
fl::span< CRGB > rgb()
Definition frame.h:42
fl::u16 getHeight() const
Definition frame.h:60
bool isValid() const
static IDecoderPtr createDecoder(const H264Config &config, fl::string *error_message=nullptr)
static H264Info parseH264Info(fl::span< const fl::u8 > mp4Data, fl::string *error_message=nullptr)
Definition h264.cpp.hpp:196
static bool isSupported()
JSON-RPC server with scheduling support.
Definition remote.h:40
static RxChannelPtr create(const RxChannelConfig &config) FL_NOEXCEPT
bool empty() const FL_NOEXCEPT
const char * c_str() const FL_NOEXCEPT
void push_back(const json &value) FL_NOEXCEPT
Definition json.h:745
fl::optional< i64 > as_int() const FL_NOEXCEPT
Definition json.h:255
bool is_array() const FL_NOEXCEPT
Definition json.h:246
bool is_null() const FL_NOEXCEPT
Definition json.h:238
fl::optional< bool > as_bool() const FL_NOEXCEPT
Definition json.h:254
bool is_int() const FL_NOEXCEPT
Definition json.h:240
bool is_object() const FL_NOEXCEPT
Definition json.h:248
size_t size() const FL_NOEXCEPT
Definition json.h:633
bool is_string() const FL_NOEXCEPT
Definition json.h:245
bool is_bool() const FL_NOEXCEPT
Definition json.h:239
fl::vector< fl::string > keys() const FL_NOEXCEPT
Definition json.h:522
bool contains(size_t idx) const FL_NOEXCEPT
Definition json.h:625
T value() const FL_NOEXCEPT
Definition json.h:336
fl::optional< fl::string > as_string() const FL_NOEXCEPT
Definition json.h:282
void set(const fl::string &key, const json &value) FL_NOEXCEPT
Definition json.h:701
static json object() FL_NOEXCEPT
Definition json.h:692
static json array() FL_NOEXCEPT
Definition json.h:688
string str() const FL_NOEXCEPT
Definition strstream.h:43
const char * c_str() const FL_NOEXCEPT
Definition strstream.h:44
Task Handle with fluent API (was class fl::task, renamed to avoid namespace collision)
Definition task.h:139
static Promise< T > create() FL_NOEXCEPT
Create a pending Promise.
Definition promise.h:61
fl::size size() const FL_NOEXCEPT
bool empty() const FL_NOEXCEPT
T * data() FL_NOEXCEPT
Definition vector.h:619
void reserve(fl::size n) FL_NOEXCEPT
Definition vector.h:591
void clear() FL_NOEXCEPT
Definition vector.h:634
void assign(InputIt first, InputIt last) FL_NOEXCEPT
Definition vector.h:638
void push_back(const T &value) FL_NOEXCEPT
Definition vector.h:624
void resize(fl::size n) FL_NOEXCEPT
Definition vector.h:593
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::size_t size_t
Definition cstddef.h:61
constexpr EOrder RGB
Definition eorder.h:17
static uint32_t t
Definition Luminova.h:55
Task executor — runs registered task runners and manages the run loop.
fl::CRGB CRGB
Definition crgb.h:25
Platform-abstracted heap memory query functions.
FastLED's Elegant JSON Library: fl::json
#define FL_WARN(X)
Definition log.h:276
#define FL_ERROR(X)
Definition log.h:219
#define FL_DBG
Definition log.h:388
#define FL_PRINT(X)
Print without prefix (like FL_WARN but without "WARN: " prefix) Uses sstream for dynamic formatting (...
Definition log.h:457
uint32_t maxLaneLeds(const fl::vector< fl::ChannelConfig > &tx_configs)
uint32_t expectedClocklessWireUs(const fl::ChipsetTimingConfig &timing, uint32_t max_leds)
fl::json measureTightTiming(const fl::string &driver_name, const fl::ChipsetTimingConfig &timing, const fl::vector< fl::ChannelConfig > &tx_configs, int iterations, uint32_t max_allowed_overhead_us, bool &out_passed)
PerlinBenchResult runPerlinBenchmark(int iters)
ParlioEncodeResult measureParlioEncode(int=12000)
ValidateResult validateParlioStreaming(int base_tx_pin, int num_lanes, int num_leds, int iterations, uint32_t timeout_ms, const int *tx_pins=nullptr)
Run the PARLIO streaming functional test.
void getTests(const SimdTestEntry **out_tests, int *out_count)
Get the static test table. Used by both runSimdTests() and the RPC handler.
BenchmarkResult runMultiplyBenchmark(int iters=10000)
Wave8ExpandResult measureWave8Expand(int=30000)
TransportState * createTransport(const char *) FL_NOEXCEPT
Create BLE GATT server, heap-allocate transport state.
Definition ble.cpp.hpp:21
StatusInfo queryStatus(const TransportState *) FL_NOEXCEPT
Query BLE connection/subscription diagnostics.
Definition ble.cpp.hpp:30
void destroyTransport(TransportState *) FL_NOEXCEPT
Deinitialize BLE stack and free heap state.
Definition ble.cpp.hpp:26
fl::pair< fl::function< fl::optional< fl::json >()>, fl::function< void(const fl::json &)> > getTransportCallbacks(TransportState *) FL_NOEXCEPT
Get RequestSource and ResponseSink lambdas for fl::Remote.
Definition ble.cpp.hpp:36
Platform-neutral BLE diagnostics (returned by queryStatus)
Definition ble.h:63
Handle coroutine(const CoroutineConfig &config)
Definition task.cpp.hpp:364
PromiseResult< T > await(Promise< T > p)
Await promise completion in a coroutine (Trampoline to platform implementation)
Definition executor.h:305
function< void()> func
Definition task.h:131
Configuration for OS-level coroutine tasks.
Definition task.h:129
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_WS2812
Default, no preamble (WS2812 and compatible)
const dec_t dec
Definition ios.cpp.hpp:7
AtomicFake< T > atomic
Definition atomic.h:26
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NOEXCEPT
Definition unique_ptr.h:261
constexpr ChipsetTiming to_runtime_timing() FL_NOEXCEPT
Convert enum-based timing type to runtime ChipsetTiming struct.
Definition led_timing.h:521
fl::string formatJsonResponse(const fl::json &response, const char *prefix)
Serialize JSON response to a string.
constexpr ChipsetTimingConfig makeTimingConfig() FL_NOEXCEPT
Convert compile-time CHIPSET type to runtime timing config.
@ FLEXIO
Teensy 4.x-only FlexIO capture backend (FLEXIO1 — FLEXIO2 is owned by the WS2812 TX driver)....
Definition types.h:13
const hex_t hex
Definition ios.cpp.hpp:6
@ LPUART
Teensy 4.x iMXRT1062 LPUART (inverted-TX + eDMA) clockless driver.
Definition bus.h:73
@ FLEX_IO
Teensy 4.x FlexIO2 driver.
Definition bus.h:71
@ I2S_SPI
Original ESP32 native I2S parallel SPI (true SPI chipsets).
Definition bus.h:66
@ SPI
Generic SPI clockless driver.
Definition bus.h:64
@ PARLIO
ESP32-P4/C6/H2/C5 parallel I/O peripheral.
Definition bus.h:63
@ OBJECT_FLED
Teensy 4.x ObjectFLED driver.
Definition bus.h:72
@ LCD_RGB
ESP32-P4 LCD RGB peripheral (parallel clockless).
Definition bus.h:67
@ LCD_CLOCKLESS
ESP32-S3 LCD_CAM clockless driver (replaces misnamed I2S).
Definition bus.h:69
@ I2S
ESP32-S3 LCD_CAM via legacy I80 bus (clockless).
Definition bus.h:65
@ BIT_BANG
Portable bit-bang fallback driver.
Definition bus.h:74
@ LCD_SPI
ESP32-S3 LCD_CAM SPI driver (true SPI chipsets).
Definition bus.h:68
@ RMT
ESP32 RMT peripheral (all ESP32 variants).
Definition bus.h:62
@ UART
ESP32 UART driver via wave8 framing.
Definition bus.h:70
@ STUB
Native/host/test stub driver.
Definition bus.h:75
shared_ptr< T > make_shared(Args &&... args) FL_NOEXCEPT
Definition shared_ptr.h:414
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
constexpr ClocklessEncoder encoder_for() FL_NOEXCEPT
Extract the encoder selector from a compile-time TIMING type.
@ RMT
RMT-based receiver (ESP32)
Definition rx.h:166
fl::u64 u64
Definition s16x16x4.h:221
void println(const char *str) FL_NOEXCEPT
bool flush(u32 timeoutMs)
Base definition for an LED controller.
Definition crgb.hpp:179
4-phase RX timing thresholds for chipset detection
Definition rx.h:87
Generic chipset timing entry Provides T1, T2, T3 timing parameters in nanoseconds for any LED protoco...
Definition led_timing.h:86
corkscrew_args args
Definition old.h:149
Promise-based fluent API for FastLED - standalone async primitives.
#define FL_NOEXCEPT
Umbrella header for SIMD subsystem.
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
Optional channel configuration parameters All fields have sensible defaults and can be overridden as ...
Definition options.h:43
u32 reset_us
Reset/latch time (microseconds)
constexpr u32 total_period_ns() const FL_NOEXCEPT
Get total bit period (T1 + T2 + T3)
Runtime bit-period timing for a clockless chipset.
Universal edge timing representation (platform-agnostic)
Definition rx.h:34
bool isValid
Definition h264.h:16
fl::u16 height
Definition h264.h:12
fl::u16 width
Definition h264.h:11
fl::ClocklessEncoder encoder
Definition Common.h:35
const char * name
Definition Common.h:36
fl::ChipsetTimingConfig timing
Definition Common.h:34
Chipset timing configuration with name for testing.
Definition Common.h:33
size_t edge_capacity
Definition config.h:17
SPI chipset configuration (data + clock pins)
Definition config.h:102
static SpiEncoder apa102(u32 clock_hz=6000000) FL_NOEXCEPT
Create APA102 encoder configuration.
Definition spi.h:44
Error type for promises.
Definition promise.h:39