FastLED 3.10.6
Loading...
Searching...
No Matches
AutoResearchRemoteAsyncMethods.cpp
Go to the documentation of this file.
1// Async primitive RPC bindings — exercise fl::task::coroutine() / await()
2// and fl::Promise semantics: basic create/complete, stop, concurrent,
3// await, await-error, promise callbacks, promise-catch callbacks, chained
4// await, and an "all" aggregator.
5// Extracted from AutoResearchRemote.cpp as part of #3132 / meta #3127.
6
8#if FL_PLATFORM_HAS_LARGE_MEMORY
9
10// Legacy debug macros (no-ops, kept for debugTest RPC function)
11#define DEBUG_PRINT(x) do {} while(0)
12#define DEBUG_PRINTLN(x) do {} while(0)
13
14#include "AutoResearchRemote.h"
15#include "AutoResearchBle.h"
16#include "AutoResearchNet.h"
17#include "AutoResearchOta.h"
19#include "fl/system/heap.h"
20#include "Common.h"
21#include "AutoResearchTest.h"
22#include "AutoResearchHelpers.h"
23#include "fl/stl/sstream.h"
24#include "fl/stl/unique_ptr.h"
25#include "fl/stl/optional.h"
26#include "fl/stl/json.h"
27#include "fl/task/task.h"
28#include "fl/task/executor.h"
30#include "fl/stl/atomic.h"
31#include "fl/task/promise.h"
32#include "fl/math/simd.h"
33#include "AutoResearchSimd.h"
39#include "fl/chipsets/spi.h"
40#include "fl/channels/config.h"
41#include <Arduino.h>
42
43#include "fl/net/ble.h"
44
45#include "fl/codec/h264.h"
46#include "fl/codec/mp4_parser.h"
48#include "fl/fx/frame.h"
49
50
52 // ========================================================================
53 // Coroutine Tests - fl::task::coroutine() and fl::task::await()
54 // ========================================================================
55
56 // Test: Basic coroutine creation and completion
57// Decline the await-based coroutine tests where no coroutine can run.
58//
59// `fl::platforms::await()` (platforms/await.h) polls
60// `while (!promise.is_completed()) { promise.update(); suspendMainthread(); }`
61// with no bound. On RP the coroutine backend is null, so the producer
62// coroutine that would resolve the promise never executes and that loop never
63// exits: the sketch stops answering RPC entirely and only comes back when the
64// watchdog resets the board. Confirmed on an RP2350W, each of the three
65// tested *in isolation* with a `ping` after it: the call returns no response,
66// the following `ping` returns nothing either, and the board re-enumerates
67// reporting `lastResetCause: WATCHDOG`.
68//
69// Only the three tests that actually call `fl::task::await()` are guarded --
70// Await and AwaitError have one call site each, ChainedAwait two. The two
71// promise-callback tests call it zero times: they wait on
72// `while (producer.isRunning() && millis() - start < 5000)`, which is bounded
73// and exits immediately on RP because a coroutine that never starts is never
74// running. They measure 0 ms and 1 ms. They first looked like hangs only
75// because they were called *after* `testCoroutineAwait` had already wedged
76// the board -- "returned null" and "was called after something else died"
77// are indistinguishable from a transcript.
78//
79// `testCoroutineAll` already declines on FL_IS_RP, but these binds are
80// unconditional and `rpc.discover` advertises them, so the RPC surface invites
81// exactly the call that wedges the board. The three non-await tests
82// (Basic/Stop/Concurrent) are deliberately left callable: they return
83// `taskRan: false` promptly and are the evidence that the backend is null.
84//
85// This guards the symptom. The underlying hazard is that an unbounded
86// `await()` hangs wherever a promise cannot be resolved; see FastLED#3832.
87#if defined(FL_IS_RP)
88#define AUTORESEARCH_DECLINE_AWAIT_ON_RP(name) \
89 do { \
90 fl::json r = fl::json::object(); \
91 r.set("success", false); \
92 r.set("supported", false); \
93 r.set("backend", "null"); \
94 r.set("reason", \
95 "RP2xxx selects the null coroutine backend; " name \
96 " would block forever in await() and wedge the board " \
97 "until the watchdog resets it"); \
98 return r; \
99 } while (0)
100#else
101#define AUTORESEARCH_DECLINE_AWAIT_ON_RP(name) do { } while (0)
102#endif
103
104 remote.bind("testCoroutineBasic", [](const fl::json& args) -> fl::json {
105 (void)args;
106 fl::json r = fl::json::object();
107
108 fl::atomic<bool> task_ran(false);
109 fl::atomic<bool> task_completed(false);
110
111 fl::task::CoroutineConfig cfg;
112 cfg.func = [&task_ran, &task_completed]() {
113 task_ran.store(true);
114 delay(50);
115 task_completed.store(true);
116 };
117 cfg.name = "test_basic";
118 auto t = fl::task::coroutine(cfg);
119
120 uint32_t start = millis();
121 while (t.isRunning() && (millis() - start) < 2000) {
122 delay(10);
123 }
124
125 bool passed = task_ran.load() && task_completed.load() && !t.isRunning();
126 r.set("success", passed);
127 r.set("taskRan", task_ran.load());
128 r.set("taskCompleted", task_completed.load());
129 r.set("isRunning", t.isRunning());
130 r.set("durationMs", static_cast<int64_t>(millis() - start));
131 return r;
132 });
133
134 // Test: Task stop() while running
135 remote.bind("testCoroutineStop", [](const fl::json& args) -> fl::json {
136 (void)args;
137 fl::json r = fl::json::object();
138
139 fl::atomic<bool> task_started(false);
140
141 fl::task::CoroutineConfig cfg;
142 cfg.func = [&task_started]() {
143 task_started.store(true);
144 while (true) {
145 delay(10);
146 }
147 };
148 cfg.name = "test_stop";
149 auto t = fl::task::coroutine(cfg);
150
151 uint32_t start = millis();
152 while (!task_started.load() && (millis() - start) < 2000) {
153 delay(10);
154 }
155
156 bool was_running = t.isRunning();
157 t.stop();
158 bool stopped = !t.isRunning();
159
160 bool passed = task_started.load() && was_running && stopped;
161 r.set("success", passed);
162 r.set("taskStarted", task_started.load());
163 r.set("wasRunning", was_running);
164 r.set("stopped", stopped);
165 r.set("durationMs", static_cast<int64_t>(millis() - start));
166 return r;
167 });
168
169 // Test: Multiple concurrent coroutines
170 remote.bind("testCoroutineConcurrent", [](const fl::json& args) -> fl::json {
171 (void)args;
172 fl::json r = fl::json::object();
173
174 const int NUM_TASKS = 3;
175 fl::atomic<int> completed_count(0);
176 fl::atomic<bool> task_flags[NUM_TASKS];
177 for (int i = 0; i < NUM_TASKS; i++) {
178 task_flags[i].store(false);
179 }
180
181 fl::task::Handle tasks[NUM_TASKS];
182 for (int i = 0; i < NUM_TASKS; i++) {
183 fl::task::CoroutineConfig cfg;
184 cfg.func = [i, &task_flags, &completed_count]() {
185 delay(20 + i * 20);
186 task_flags[i].store(true);
187 completed_count.fetch_add(1);
188 };
189 cfg.name = "test_concurrent";
190 tasks[i] = fl::task::coroutine(cfg);
191 }
192
193 uint32_t start = millis();
194 while (completed_count.load() < NUM_TASKS && (millis() - start) < 3000) {
195 delay(10);
196 }
197
198 bool all_completed = true;
199 bool all_stopped = true;
200 for (int i = 0; i < NUM_TASKS; i++) {
201 if (!task_flags[i].load()) all_completed = false;
202 if (tasks[i].isRunning()) all_stopped = false;
203 }
204
205 bool passed = all_completed && all_stopped && (completed_count.load() == NUM_TASKS);
206 r.set("success", passed);
207 r.set("completedCount", static_cast<int64_t>(completed_count.load()));
208 r.set("allCompleted", all_completed);
209 r.set("allStopped", all_stopped);
210 r.set("durationMs", static_cast<int64_t>(millis() - start));
211 return r;
212 });
213
214 // Test: Consumer coroutine awaits promise, producer coroutine fulfills it.
215 // Verifies fl::task::await() truly blocks until the producer resolves the promise.
216 // Main thread does NOT touch the promise — only the producer coroutine does.
217 remote.bind("testCoroutineAwait", [](const fl::json& args) -> fl::json {
218 (void)args;
219 AUTORESEARCH_DECLINE_AWAIT_ON_RP("testCoroutineAwait");
220 fl::json r = fl::json::object();
221
223 fl::atomic<bool> consumer_started(false);
224 fl::atomic<bool> consumer_finished(false);
225 fl::atomic<int> consumer_value(0);
226 fl::atomic<bool> consumer_ok(false);
227 fl::atomic<bool> producer_started(false);
228 fl::atomic<bool> producer_finished(false);
229
230 // Consumer coroutine: starts first, calls fl::task::await() which should block
231 // until the producer coroutine resolves the promise
232 fl::task::CoroutineConfig consumer_cfg;
233 consumer_cfg.func = [promise_ptr, &consumer_started, &consumer_finished,
234 &consumer_value, &consumer_ok]() {
235 consumer_started.store(true);
236 // This should block the coroutine until producer fulfills the promise
237 auto result = fl::task::await(*promise_ptr);
238 if (result.ok()) {
239 consumer_ok.store(true);
240 consumer_value.store(result.value());
241 }
242 consumer_finished.store(true);
243 };
244 consumer_cfg.name = "await_consumer";
245 auto consumer = fl::task::coroutine(consumer_cfg);
246
247 // Small delay so consumer enters fl::task::await() before producer starts
248 delay(50);
249
250 // Producer coroutine: simulates async work, then resolves the promise
251 fl::task::CoroutineConfig producer_cfg;
252 producer_cfg.func = [promise_ptr, &producer_started, &producer_finished]() {
253 producer_started.store(true);
254 delay(200); // simulate real async work (e.g. sensor read, network I/O)
255 promise_ptr->complete_with_value(42);
256 producer_finished.store(true);
257 };
258 producer_cfg.name = "await_producer";
259 auto producer = fl::task::coroutine(producer_cfg);
260
261 // Main thread waits for both coroutines to finish (polling only)
262 uint32_t start = millis();
263 while ((!consumer_finished.load() || producer.isRunning()) && (millis() - start) < 5000) {
264 delay(10);
265 }
266
267 // Verify: consumer was truly blocked — it should not finish before producer
268 bool passed = consumer_started.load() && consumer_finished.load()
269 && consumer_ok.load() && (consumer_value.load() == 42)
270 && producer_started.load() && producer_finished.load();
271 r.set("success", passed);
272 r.set("consumerStarted", consumer_started.load());
273 r.set("consumerFinished", consumer_finished.load());
274 r.set("consumerOk", consumer_ok.load());
275 r.set("consumerValue", static_cast<int64_t>(consumer_value.load()));
276 r.set("producerStarted", producer_started.load());
277 r.set("producerFinished", producer_finished.load());
278 r.set("durationMs", static_cast<int64_t>(millis() - start));
279 return r;
280 });
281
282 // Test: Consumer coroutine awaits promise, producer coroutine rejects it.
283 // Verifies that fl::task::await() properly propagates errors from producer.
284 remote.bind("testCoroutineAwaitError", [](const fl::json& args) -> fl::json {
285 (void)args;
286 AUTORESEARCH_DECLINE_AWAIT_ON_RP("testCoroutineAwaitError");
287 fl::json r = fl::json::object();
288
290 fl::atomic<bool> consumer_finished(false);
291 fl::atomic<bool> got_error(false);
292 fl::atomic<bool> producer_finished(false);
293
294 // Consumer coroutine: awaits promise, expects error
295 fl::task::CoroutineConfig consumer_cfg;
296 consumer_cfg.func = [promise_ptr, &consumer_finished, &got_error]() {
297 auto result = fl::task::await(*promise_ptr);
298 if (!result.ok()) {
299 got_error.store(true);
300 }
301 consumer_finished.store(true);
302 };
303 consumer_cfg.name = "await_err_consumer";
304 auto consumer = fl::task::coroutine(consumer_cfg);
305
306 delay(50); // let consumer enter await
307
308 // Producer coroutine: rejects the promise with error
309 fl::task::CoroutineConfig producer_cfg;
310 producer_cfg.func = [promise_ptr, &producer_finished]() {
311 delay(100);
312 promise_ptr->complete_with_error(fl::task::Error("test error"));
313 producer_finished.store(true);
314 };
315 producer_cfg.name = "await_err_producer";
316 auto producer = fl::task::coroutine(producer_cfg);
317
318 uint32_t start = millis();
319 while ((!consumer_finished.load() || producer.isRunning()) && (millis() - start) < 5000) {
320 delay(10);
321 }
322
323 bool passed = consumer_finished.load() && got_error.load() && producer_finished.load();
324 r.set("success", passed);
325 r.set("consumerFinished", consumer_finished.load());
326 r.set("gotError", got_error.load());
327 r.set("producerFinished", producer_finished.load());
328 r.set("durationMs", static_cast<int64_t>(millis() - start));
329 return r;
330 });
331
332 // Test: Promise then/catch_ callbacks fire correctly when fulfilled by a coroutine.
333 // The promise is created with .then() and .catch_() callbacks attached BEFORE
334 // the producer coroutine fulfills it, verifying callback dispatch works.
335 remote.bind("testCoroutinePromiseCallbacks", [](const fl::json& args) -> fl::json {
336 (void)args;
337 fl::json r = fl::json::object();
338
340 fl::atomic<bool> then_called(false);
341 fl::atomic<int> then_value(0);
342 fl::atomic<bool> catch_called(false);
343 fl::atomic<bool> producer_finished(false);
344
345 // Attach callbacks to the promise BEFORE producer runs
346 promise_ptr->then([&then_called, &then_value](const int& val) {
347 then_called.store(true);
348 then_value.store(val);
349 });
350 promise_ptr->catch_([&catch_called](const fl::task::Error&) {
351 catch_called.store(true);
352 });
353
354 // Producer coroutine fulfills the promise
355 fl::task::CoroutineConfig producer_cfg;
356 producer_cfg.func = [promise_ptr, &producer_finished]() {
357 delay(100);
358 promise_ptr->complete_with_value(99);
359 producer_finished.store(true);
360 };
361 producer_cfg.name = "cb_producer";
362 auto producer = fl::task::coroutine(producer_cfg);
363
364 uint32_t start = millis();
365 while (producer.isRunning() && (millis() - start) < 5000) {
366 delay(10);
367 promise_ptr->update(); // pump callbacks
368 }
369 promise_ptr->update(); // final pump
370
371 // then() should fire, catch_() should NOT
372 bool passed = then_called.load() && (then_value.load() == 99)
373 && !catch_called.load() && producer_finished.load();
374 r.set("success", passed);
375 r.set("thenCalled", then_called.load());
376 r.set("thenValue", static_cast<int64_t>(then_value.load()));
377 r.set("catchCalled", catch_called.load());
378 r.set("producerFinished", producer_finished.load());
379 r.set("durationMs", static_cast<int64_t>(millis() - start));
380 return r;
381 });
382
383 // Test: Promise catch_ callback fires on rejection by a coroutine.
384 remote.bind("testCoroutinePromiseCatchCallback", [](const fl::json& args) -> fl::json {
385 (void)args;
386 fl::json r = fl::json::object();
387
389 fl::atomic<bool> then_called(false);
390 fl::atomic<bool> catch_called(false);
391 fl::atomic<bool> producer_finished(false);
392
393 promise_ptr->then([&then_called](const int&) {
394 then_called.store(true);
395 });
396 promise_ptr->catch_([&catch_called](const fl::task::Error&) {
397 catch_called.store(true);
398 });
399
400 // Producer coroutine rejects the promise
401 fl::task::CoroutineConfig producer_cfg;
402 producer_cfg.func = [promise_ptr, &producer_finished]() {
403 delay(100);
404 promise_ptr->complete_with_error(fl::task::Error("rejection test"));
405 producer_finished.store(true);
406 };
407 producer_cfg.name = "catch_producer";
408 auto producer = fl::task::coroutine(producer_cfg);
409
410 uint32_t start = millis();
411 while (producer.isRunning() && (millis() - start) < 5000) {
412 delay(10);
413 promise_ptr->update();
414 }
415 promise_ptr->update();
416
417 // catch_() should fire, then() should NOT
418 bool passed = !then_called.load() && catch_called.load() && producer_finished.load();
419 r.set("success", passed);
420 r.set("thenCalled", then_called.load());
421 r.set("catchCalled", catch_called.load());
422 r.set("producerFinished", producer_finished.load());
423 r.set("durationMs", static_cast<int64_t>(millis() - start));
424 return r;
425 });
426
427 // Test: Pipeline of 3 coroutines passing data through promises.
428 // coroutine A produces value -> promise1 -> coroutine B transforms -> promise2 -> coroutine C consumes
429 remote.bind("testCoroutineChainedAwait", [](const fl::json& args) -> fl::json {
430 (void)args;
431 AUTORESEARCH_DECLINE_AWAIT_ON_RP("testCoroutineChainedAwait");
432 fl::json r = fl::json::object();
433
436 fl::atomic<int> final_value(0);
437 fl::atomic<bool> chain_complete(false);
438 fl::atomic<bool> a_done(false);
439 fl::atomic<bool> b_done(false);
440 fl::atomic<bool> c_done(false);
441
442 // Coroutine C: end of chain, awaits p2
443 fl::task::CoroutineConfig cfg_c;
444 cfg_c.func = [p2, &final_value, &chain_complete, &c_done]() {
445 auto result = fl::task::await(*p2);
446 if (result.ok()) {
447 final_value.store(result.value());
448 }
449 c_done.store(true);
450 chain_complete.store(true);
451 };
452 cfg_c.name = "chain_c";
453 auto tc = fl::task::coroutine(cfg_c);
454
455 // Coroutine B: middle of chain, awaits p1, transforms, fulfills p2
456 fl::task::CoroutineConfig cfg_b;
457 cfg_b.func = [p1, p2, &b_done]() {
458 auto result = fl::task::await(*p1);
459 if (result.ok()) {
460 // Transform: multiply by 10
461 p2->complete_with_value(result.value() * 10);
462 } else {
463 p2->complete_with_error(result.error());
464 }
465 b_done.store(true);
466 };
467 cfg_b.name = "chain_b";
468 auto tb = fl::task::coroutine(cfg_b);
469
470 // Coroutine A: head of chain, produces the initial value into p1
471 fl::task::CoroutineConfig cfg_a;
472 cfg_a.func = [p1, &a_done]() {
473 delay(100); // simulate work
474 p1->complete_with_value(7);
475 a_done.store(true);
476 };
477 cfg_a.name = "chain_a";
478 auto ta = fl::task::coroutine(cfg_a);
479
480 uint32_t start = millis();
481 while (!chain_complete.load() && (millis() - start) < 5000) {
482 delay(10);
483 }
484
485 // 7 * 10 = 70
486 bool passed = chain_complete.load() && (final_value.load() == 70)
487 && a_done.load() && b_done.load() && c_done.load();
488 r.set("success", passed);
489 r.set("finalValue", static_cast<int64_t>(final_value.load()));
490 r.set("aDone", a_done.load());
491 r.set("bDone", b_done.load());
492 r.set("cDone", c_done.load());
493 r.set("durationMs", static_cast<int64_t>(millis() - start));
494 return r;
495 });
496
497 // Run all coroutine tests sequentially
498 remote.bind("testCoroutineAll", [this](const fl::json& args) -> fl::json {
499 (void)args;
500 fl::json r = fl::json::object();
501
502#if defined(FL_IS_RP)
503 r.set("success", false);
504 r.set("supported", false);
505 r.set("backend", "null");
506 r.set("reason", "RP2xxx currently selects the generic Arduino null coroutine backend");
507 r.set("passed", static_cast<int64_t>(0));
508 r.set("failed", static_cast<int64_t>(0));
509 r.set("total", static_cast<int64_t>(0));
510 r.set("results", fl::json::object());
511 return r;
512#else
513
514 const char* test_names[] = {
515 "testCoroutineBasic",
516 "testCoroutineStop",
517 "testCoroutineConcurrent",
518 "testCoroutineAwait",
519 "testCoroutineAwaitError",
520 "testCoroutinePromiseCallbacks",
521 "testCoroutinePromiseCatchCallback",
522 "testCoroutineChainedAwait"
523 };
524 const int num_tests = 8;
525
526 int passed = 0;
527 int failed = 0;
528 fl::json results = fl::json::object();
529
530 for (int i = 0; i < num_tests; i++) {
531 fl::json empty_args = fl::json::object();
532 auto bound = mRemote->get<fl::json(const fl::json&)>(test_names[i]);
533 if (bound.ok()) {
534 fl::json test_result = bound.value()(empty_args);
535 bool success = false;
536 if (test_result.contains("success")) {
537 auto val = test_result["success"].as_bool();
538 success = val.has_value() && val.value();
539 }
540 if (success) {
541 passed++;
542 } else {
543 failed++;
544 }
545 results.set(test_names[i], test_result);
546 } else {
547 failed++;
548 fl::json err = fl::json::object();
549 err.set("success", false);
550 err.set("error", "Method not found");
551 results.set(test_names[i], err);
552 }
553 }
554
555 r.set("success", failed == 0);
556 r.set("passed", static_cast<int64_t>(passed));
557 r.set("failed", static_cast<int64_t>(failed));
558 r.set("total", static_cast<int64_t>(num_tests));
559 r.set("results", results);
560 return r;
561#endif
562 });
563}
564
565#endif // FL_PLATFORM_HAS_LARGE_MEMORY
Full PARLIO encode bench for ESP32-P4 (post-byte-LUT, #2526 landed).
PARLIO ISR-streaming functional validation (#2548 deep-dive follow-up).
Issue #2994 repro: compounded per-sequence timing drift vs 3.10.3.
Wave8 expansion micro-benchmark for #2526.
fl::unique_ptr< fl::Remote > remote
Definition RpcClient.ino:43
fl::net::ble — BLE GATT transport layer for JSON-RPC
SPI encoder configuration for clocked LED chipsets.
void bindAsyncMethods(fl::Remote &remote)
fl::unique_ptr< fl::Remote > mRemote
void store(T value, memory_order=memory_order_seq_cst) FL_NO_EXCEPT
Definition atomic.h:65
JSON-RPC server with scheduling support.
Definition remote.h:42
void set(const fl::string &key, const json &value) FL_NO_EXCEPT
Definition json.h:866
fl::optional< bool > as_bool() const FL_NO_EXCEPT
Definition json.h:397
bool contains(size_t idx) const FL_NO_EXCEPT
Definition json.h:790
static json object() FL_NO_EXCEPT
Definition json.h:857
static Promise< T > create() FL_NO_EXCEPT
Create a pending Promise.
Definition promise.h:61
static uint32_t t
Definition Luminova.h:55
Task executor — runs registered task runners and manages the run loop.
Platform-abstracted heap memory query functions.
FastLED's Elegant JSON Library: fl::json
Handle coroutine(const CoroutineConfig &config)
Creates and starts an OS-level coroutine task.
Definition task.cpp.hpp:364
PromiseResult< T > await(Promise< T > p, fl::u32 timeout_ms=fl::platforms::kAwaitDefaultTimeoutMs) FL_NO_EXCEPT
Await promise completion in a coroutine (Trampoline to platform implementation)
Definition executor.h:307
function< void()> func
Function executed by the coroutine, copied at creation time.
Definition task.h:138
string name
Diagnostic name reported by the platform. Defaults to "task".
Definition task.h:140
void delay(u32 ms, bool run_async=true) FL_NO_EXCEPT
Public delay wrapper that keeps bare Arduino delay() preferred after using fl::delay; while still all...
Definition delay.h:98
AtomicFake< T > atomic
Definition atomic.h:26
fl::u32 millis()
Universal millisecond timer - returns milliseconds since system startup.
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:414
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
corkscrew_args args
Definition old.h:149
Promise-based fluent API for FastLED - standalone async primitives.
Umbrella header for SIMD subsystem.
fl::u32 uint32_t
Definition stdint.h:218