FastLED 3.9.15
Loading...
Searching...
No Matches

◆ registerFunctions()

void AutoResearchRemoteControl::registerFunctions ( fl::shared_ptr< AutoResearchState > state)

Register all RPC functions with shared autoresearch state.

Parameters
stateShared pointer to autoresearch runtime state

Definition at line 1301 of file AutoResearchRemote.cpp.

1301 {
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.
1482 fl::RxChannelConfig rx_cfg(rx_pin, fl::RxBackend::FLEXIO);
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.
1515 fl::vector<fl::EdgeTime> edges;
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
1684 fl::RxChannelConfig rx_cfg(rx_pin, fl::RxBackend::FLEXIO);
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.
1764 fl::vector<fl::EdgeTime> edges;
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.
1849 fl::RxChannelConfig rx_cfg(rx_pin, fl::RxBackend::FLEXIO);
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) {
1924 fl::json e = fl::json::object();
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
2492 using autoresearch::simd_check::SimdTestEntry;
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++) {
3136 fl::json px = fl::json::array();
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);
3151 fl::net::ble::StatusInfo info = fl::net::ble::queryStatus(mBleState);
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;
3168 fl::json r = fl::json::object();
3169
3170 fl::atomic<bool> task_ran(false);
3171 fl::atomic<bool> task_completed(false);
3172
3173 fl::task::CoroutineConfig cfg;
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;
3199 fl::json r = fl::json::object();
3200
3201 fl::atomic<bool> task_started(false);
3202
3203 fl::task::CoroutineConfig cfg;
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;
3234 fl::json r = fl::json::object();
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++) {
3245 fl::task::CoroutineConfig cfg;
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;
3281 fl::json r = fl::json::object();
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;
3347 fl::json r = fl::json::object();
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;
3397 fl::json r = fl::json::object();
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;
3446 fl::json r = fl::json::object();
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;
3491 fl::json r = fl::json::object();
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
3502 fl::task::CoroutineConfig cfg_c;
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
3515 fl::task::CoroutineConfig cfg_b;
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
3530 fl::task::CoroutineConfig cfg_a;
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;
3559 fl::json r = fl::json::object();
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}
fl::CRGB leds[NUM_LEDS]
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.
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
int pins[]
Definition Spi.ino:11
fl::json stopBleRemote()
Stop BLE remote (destroys BLE Remote + GATT server)
fl::json runParallelTestImpl(const fl::json &args)
fl::json runSingleTestImpl(const fl::json &args)
fl::net::ble::TransportState * mBleState
fl::shared_ptr< AutoResearchState > mState
fl::json findConnectedPinsImpl(const fl::json &args)
fl::json startBleRemote()
Start BLE remote (creates BLE GATT server + second Remote instance)
fl::unique_ptr< fl::Remote > mRemote
void show(fl::u8 scale)
Update all our controllers with the current led colors, using the passed in brightness.
static void add(fl::ChannelPtr channel)
Add a Channel-based LED controller (from ChannelPtr)
void wait()
Wait for all channel bus transmissions to complete.
void clear(bool writeData=false)
Clear the leds, wiping the local array of data.
void store(T value, memory_order=memory_order_seq_cst) FL_NOEXCEPT
Definition atomic.h:65
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()
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_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_string() const FL_NOEXCEPT
Definition json.h:245
bool contains(size_t idx) const FL_NOEXCEPT
Definition json.h:625
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
static Promise< T > create() FL_NOEXCEPT
Create a pending Promise.
Definition promise.h:61
fl::size size() const FL_NOEXCEPT
void reserve(fl::size n) FL_NOEXCEPT
Definition vector.h:591
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
fl::size_t size_t
Definition cstddef.h:61
constexpr EOrder RGB
Definition eorder.h:17
static uint32_t t
Definition Luminova.h:55
fl::CRGB CRGB
Definition crgb.h:25
#define FL_WARN(X)
Definition log.h:276
#define FL_ERROR(X)
Definition log.h:219
#define FL_PRINT(X)
Print without prefix (like FL_WARN but without "WARN: " prefix) Uses sstream for dynamic formatting (...
Definition log.h:457
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)
StatusInfo queryStatus(const TransportState *) FL_NOEXCEPT
Query BLE connection/subscription diagnostics.
Definition ble.cpp.hpp:30
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
const dec_t dec
Definition ios.cpp.hpp:7
fl::u32 uint32_t
Definition s16x16x4.h:219
AtomicFake< T > atomic
Definition atomic.h:26
fl::u16 uint16_t
Definition s16x16x4.h:214
fl::u32 millis()
Universal millisecond timer - returns milliseconds since system startup.
constexpr ChipsetTiming to_runtime_timing() FL_NOEXCEPT
Convert enum-based timing type to runtime ChipsetTiming struct.
Definition led_timing.h:521
void delay(u32 ms, bool run_async=true) FL_NOEXCEPT
Public delay wrapper that keeps bare Arduino delay() preferred after using fl::delay; while still all...
Definition delay.h:98
constexpr ChipsetTimingConfig makeTimingConfig() FL_NOEXCEPT
Convert compile-time CHIPSET type to runtime timing config.
fl::uptr uintptr_t
Definition s16x16x4.h:224
@ 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
@ OBJECT_FLED
Teensy 4.x ObjectFLED driver.
Definition bus.h:72
shared_ptr< T > make_shared(Args &&... args) FL_NOEXCEPT
Definition shared_ptr.h:414
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
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
fl::u32 micros()
Universal microsecond timer - returns microseconds since system startup.
void pinMode(int pin, PinMode mode)
Set pin mode (input, output, pull-up, pull-down)
Definition pin.cpp.hpp:378
PinValue digitalRead(int pin)
Read digital value from pin.
Definition pin.cpp.hpp:55
float add(float &a, float &b)
void analogWrite(int pin, u16 val)
Write analog value to pin (PWM)
Definition pin.cpp.hpp:227
void digitalWrite(int pin, PinValue val)
Write digital value to pin.
Definition pin.cpp.hpp:51
constexpr ClocklessEncoder encoder_for() FL_NOEXCEPT
Extract the encoder selector from a compile-time TIMING type.
fl::u64 u64
Definition s16x16x4.h:221
void delayMicroseconds(u32 us)
Delay for a given number of microseconds.
corkscrew_args args
Definition old.h:149
@ 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
bool isValid
Definition h264.h:16
fl::u16 height
Definition h264.h:12
fl::u16 width
Definition h264.h:11

References args, fl::json::array(), fl::json::as_bool(), fl::json::as_int(), fl::json::as_string(), fl::vector< T >::assign(), fl::ASYNC, autoResearchSetExclusiveDriverByName(), fl::task::await(), fl::CRGB::Black, fl::CRGB::Blue, fl::basic_string::c_str(), CHANNELS, fl::net::ble::StatusInfo::connected, fl::net::ble::StatusInfo::connectedCount, fl::json::contains(), fl::task::coroutine(), fl::RxChannel::create(), fl::task::Promise< T >::create(), fl::H264::createDecoder(), fl::dec, fl::RxChannelConfig::edge_capacity, fl::basic_string::empty(), fl::encoder_for(), FastLED, fl::AtomicFake< T >::fetch_add(), findConnectedPinsImpl(), FL_ERROR, FL_PRINT, FL_WARN, fl::FLEXIO, fl::task::CoroutineConfig::func, fl::Frame::getHeight(), autoresearch::simd_check::getTests(), fl::Frame::getWidth(), fl::CRGB::Green, fl::H264Info::height, fl::hex, fl::json::is_int(), fl::json::is_null(), fl::json::is_string(), fl::H264::isSupported(), fl::Frame::isValid(), fl::H264Info::isValid, autoresearch::parlio_stream::kMaxIterations, autoresearch::parlio_stream::kMaxLanes, leds, fl::AtomicFake< T >::load(), fl::make4PhaseTiming(), fl::make_shared(), fl::makeTimingConfig(), mBleState, fl::ChannelOptions::mBus, autoresearch::parlio_bench::measureParlioEncode(), autoresearch::wave8_bench::measureWave8Expand(), mRemote, mState, fl::task::CoroutineConfig::name, fl::json::object(), fl::OBJECT_FLED, fl::H264::parseH264Info(), pins, fl::json::push_back(), fl::vector< T >::push_back(), fl::net::ble::queryStatus(), fl::CRGB::Red, fl::vector< T >::reserve(), fl::vector< T >::resize(), RGB, fl::Frame::rgb(), fl::net::ble::StatusInfo::ringHead, fl::net::ble::StatusInfo::ringTail, autoresearch::simd_check::runMultiplyBenchmark(), runNetClientTest(), runNetLoopback(), runParallelTestImpl(), autoresearch::animartrix_check::runPerlinBenchmark(), runSingleTestImpl(), fl::json::set(), fl::vector_basic::size(), fl::RxChannelConfig::start_low, startBleRemote(), startNetClient(), startNetServer(), startOta(), state, stopBleRemote(), stopNet(), stopOta(), fl::AtomicFake< T >::store(), fl::sstream::str(), fl::Success, t, fl::NamedTimingConfig::timing, fl::to_runtime_timing(), fl::net::ble::StatusInfo::txCharExists, fl::net::ble::StatusInfo::txValueLen, autoresearch::parlio_stream::validateParlioStreaming(), fl::json::value(), and fl::H264Info::width.

Referenced by setup().

+ Here is the caller graph for this function: