diff --git a/.claude/rules/app-framework.md b/.claude/rules/app-framework.md index cd66dc27e..d9b36568f 100644 --- a/.claude/rules/app-framework.md +++ b/.claude/rules/app-framework.md @@ -1,10 +1,10 @@ # Architecture: App Framework -Apps are event-driven, C API (`app-module`, ``), not a C++ class. Each app has an `AppManifest` (`id`, `name`, `category`, `location`, `flags`) and a `main(app_instance_id, argc, argv)` entry point (`AppMainFn`), modelled on a C program's `main()`. Every app instance gets its own dedicated task for its whole lifetime, and blocks in that task until it returns. +Apps are event-driven, C API (`app-module`, ``), not a C++ class. Each app has an `AppManifest` (`id`, `name`, `category`, `location`, `flags`) and a `main(argc, argv)` entry point (`AppMainFn`), modelled on a C program's `main()`. Every app instance gets its own dedicated task for its whole lifetime, and blocks in that task until it returns. Use `app_scheduler_current_app_id()` (`app/scheduler.h`) to identify the running instance - it's not passed as a parameter. Lifecycle and inter-app communication go through `app_manager_*()` (`app/manager.h`) and `app_event_*()` (`app/event.h`): - `app_manager_start()`/`app_manager_start_with_parameters()` launch a plain instance; `app_manager_start_for_result()` launches a modal child that reports back to a parent instance. -- An app subscribes with `app_event_subscribe()`/`app_event_await()` and reacts to `APP_EVENT_CLOSE` (terminate now) and `APP_EVENT_RESULT` (a child it started reported back). +- An app subscribes with `app_event_subscribe()` (registers for its own instance's events, no id argument needed), blocks via `task_event_group_wait()`/`task_event_group_wait_any()`, and drains with `app_event_poll()`, reacting to `APP_EVENT_CLOSE` (terminate now) and `APP_EVENT_RESULT` (a child it started reported back). - An app closes itself by calling `app_manager_finish()` right before returning from `main()`; another instance is closed via `app_manager_stop()`. Apps are registered at startup via `app_manager_add()`. External apps can be loaded from SD card via `manifest.properties` files, or side-loaded as ELF binaries on ESP32 (see `app/loader.h`'s `AppLoaderApi`). diff --git a/Devices/btt-panda-touch/bigtreetech,panda-touch.dts b/Devices/btt-panda-touch/bigtreetech,panda-touch.dts index d106f1ce4..e6995484d 100644 --- a/Devices/btt-panda-touch/bigtreetech,panda-touch.dts +++ b/Devices/btt-panda-touch/bigtreetech,panda-touch.dts @@ -18,7 +18,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/cyd-2432s024c/cyd,2432s024c.dts b/Devices/cyd-2432s024c/cyd,2432s024c.dts index dadaf0bc6..eafea7ac4 100644 --- a/Devices/cyd-2432s024c/cyd,2432s024c.dts +++ b/Devices/cyd-2432s024c/cyd,2432s024c.dts @@ -18,7 +18,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/cyd-2432s024r/cyd,2432s024r.dts b/Devices/cyd-2432s024r/cyd,2432s024r.dts index 06f7c9741..179628783 100644 --- a/Devices/cyd-2432s024r/cyd,2432s024r.dts +++ b/Devices/cyd-2432s024r/cyd,2432s024r.dts @@ -18,7 +18,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/cyd-2432s028r/cyd,2432s028r.dts b/Devices/cyd-2432s028r/cyd,2432s028r.dts index 9aa1dcefb..443a3f42e 100644 --- a/Devices/cyd-2432s028r/cyd,2432s028r.dts +++ b/Devices/cyd-2432s028r/cyd,2432s028r.dts @@ -19,7 +19,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/cyd-2432s028rv3/cyd,2432s028rv3.dts b/Devices/cyd-2432s028rv3/cyd,2432s028rv3.dts index 2b1efc462..93aaf1731 100644 --- a/Devices/cyd-2432s028rv3/cyd,2432s028rv3.dts +++ b/Devices/cyd-2432s028rv3/cyd,2432s028rv3.dts @@ -19,7 +19,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/cyd-2432s032c/cyd,2432s032c.dts b/Devices/cyd-2432s032c/cyd,2432s032c.dts index 940b9ac50..563e73726 100644 --- a/Devices/cyd-2432s032c/cyd,2432s032c.dts +++ b/Devices/cyd-2432s032c/cyd,2432s032c.dts @@ -18,7 +18,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/cyd-3248s035c/cyd,3248s035c.dts b/Devices/cyd-3248s035c/cyd,3248s035c.dts index dd8e8647c..8718e54b3 100644 --- a/Devices/cyd-3248s035c/cyd,3248s035c.dts +++ b/Devices/cyd-3248s035c/cyd,3248s035c.dts @@ -19,7 +19,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/cyd-4848s040c/cyd,4848s040c.dts b/Devices/cyd-4848s040c/cyd,4848s040c.dts index 6a5471b2f..b8198b5a2 100644 --- a/Devices/cyd-4848s040c/cyd,4848s040c.dts +++ b/Devices/cyd-4848s040c/cyd,4848s040c.dts @@ -18,7 +18,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/cyd-8048s043c/cyd,8048s043c.dts b/Devices/cyd-8048s043c/cyd,8048s043c.dts index 71b477695..eecd8949b 100644 --- a/Devices/cyd-8048s043c/cyd,8048s043c.dts +++ b/Devices/cyd-8048s043c/cyd,8048s043c.dts @@ -19,7 +19,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/cyd-e32r28t/cyd,e32r28t.dts b/Devices/cyd-e32r28t/cyd,e32r28t.dts index e5e111e41..53457ad16 100644 --- a/Devices/cyd-e32r28t/cyd,e32r28t.dts +++ b/Devices/cyd-e32r28t/cyd,e32r28t.dts @@ -17,7 +17,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/cyd-e32r32p/cyd,e32r32p.dts b/Devices/cyd-e32r32p/cyd,e32r32p.dts index 2953bf9d3..51a8fa564 100644 --- a/Devices/cyd-e32r32p/cyd,e32r32p.dts +++ b/Devices/cyd-e32r32p/cyd,e32r32p.dts @@ -20,7 +20,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/elecrow-crowpanel-advance-28/elecrow,crowpanel-advance-28.dts b/Devices/elecrow-crowpanel-advance-28/elecrow,crowpanel-advance-28.dts index 78db1398e..919c72db3 100644 --- a/Devices/elecrow-crowpanel-advance-28/elecrow,crowpanel-advance-28.dts +++ b/Devices/elecrow-crowpanel-advance-28/elecrow,crowpanel-advance-28.dts @@ -20,7 +20,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/elecrow-crowpanel-advance-35/elecrow,crowpanel-advance-35.dts b/Devices/elecrow-crowpanel-advance-35/elecrow,crowpanel-advance-35.dts index a687b01c6..810a5209d 100644 --- a/Devices/elecrow-crowpanel-advance-35/elecrow,crowpanel-advance-35.dts +++ b/Devices/elecrow-crowpanel-advance-35/elecrow,crowpanel-advance-35.dts @@ -20,7 +20,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/elecrow-crowpanel-advance-50/elecrow,crowpanel-advance-50.dts b/Devices/elecrow-crowpanel-advance-50/elecrow,crowpanel-advance-50.dts index 1a0f93ba8..a9e168832 100644 --- a/Devices/elecrow-crowpanel-advance-50/elecrow,crowpanel-advance-50.dts +++ b/Devices/elecrow-crowpanel-advance-50/elecrow,crowpanel-advance-50.dts @@ -20,7 +20,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/elecrow-crowpanel-basic-28/elecrow,crowpanel-basic-28.dts b/Devices/elecrow-crowpanel-basic-28/elecrow,crowpanel-basic-28.dts index 5c04fcab3..77e07953a 100644 --- a/Devices/elecrow-crowpanel-basic-28/elecrow,crowpanel-basic-28.dts +++ b/Devices/elecrow-crowpanel-basic-28/elecrow,crowpanel-basic-28.dts @@ -18,7 +18,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/elecrow-crowpanel-basic-35/elecrow,crowpanel-basic-35.dts b/Devices/elecrow-crowpanel-basic-35/elecrow,crowpanel-basic-35.dts index de27790aa..f5ab1aeca 100644 --- a/Devices/elecrow-crowpanel-basic-35/elecrow,crowpanel-basic-35.dts +++ b/Devices/elecrow-crowpanel-basic-35/elecrow,crowpanel-basic-35.dts @@ -18,7 +18,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/elecrow-crowpanel-basic-50/elecrow,crowpanel-basic-50.dts b/Devices/elecrow-crowpanel-basic-50/elecrow,crowpanel-basic-50.dts index ebb9e0b84..085d8ed6c 100644 --- a/Devices/elecrow-crowpanel-basic-50/elecrow,crowpanel-basic-50.dts +++ b/Devices/elecrow-crowpanel-basic-50/elecrow,crowpanel-basic-50.dts @@ -20,7 +20,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/es3c28p/es3c28p.dts b/Devices/es3c28p/es3c28p.dts index 75d5ac1dc..6aca63899 100644 --- a/Devices/es3c28p/es3c28p.dts +++ b/Devices/es3c28p/es3c28p.dts @@ -24,7 +24,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/generic-esp32/generic,esp32.dts b/Devices/generic-esp32/generic,esp32.dts index 3e49b6c50..84fc39d4f 100644 --- a/Devices/generic-esp32/generic,esp32.dts +++ b/Devices/generic-esp32/generic,esp32.dts @@ -11,7 +11,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/generic-esp32c6/generic,esp32c6.dts b/Devices/generic-esp32c6/generic,esp32c6.dts index fef95675b..7c5254acb 100644 --- a/Devices/generic-esp32c6/generic,esp32c6.dts +++ b/Devices/generic-esp32c6/generic,esp32c6.dts @@ -11,7 +11,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/generic-esp32s3/generic,esp32s3.dts b/Devices/generic-esp32s3/generic,esp32s3.dts index 8443fcf47..eba13b006 100644 --- a/Devices/generic-esp32s3/generic,esp32s3.dts +++ b/Devices/generic-esp32s3/generic,esp32s3.dts @@ -11,7 +11,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/guition-jc2432w328c/guition,jc2432w328c.dts b/Devices/guition-jc2432w328c/guition,jc2432w328c.dts index 138df637c..3fa69997c 100644 --- a/Devices/guition-jc2432w328c/guition,jc2432w328c.dts +++ b/Devices/guition-jc2432w328c/guition,jc2432w328c.dts @@ -19,7 +19,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/guition-jc8048w550c/guition,jc8048w550c.dts b/Devices/guition-jc8048w550c/guition,jc8048w550c.dts index 3be39b67a..df755b847 100644 --- a/Devices/guition-jc8048w550c/guition,jc8048w550c.dts +++ b/Devices/guition-jc8048w550c/guition,jc8048w550c.dts @@ -21,7 +21,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts b/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts index db8be2bf2..9d8a983e9 100644 --- a/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts +++ b/Devices/heltec-wifi-lora-32-v3/heltec,wifi-lora-32-v3.dts @@ -18,7 +18,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts b/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts index d8ca0f0ab..64bd0d874 100644 --- a/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts +++ b/Devices/lilygo-tdeck-max/lilygo,tdeck-max.dts @@ -26,7 +26,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts b/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts index 5a0476c92..52354d01a 100644 --- a/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts +++ b/Devices/lilygo-tdeck-plus/lilygo,tdeck-plus.dts @@ -48,7 +48,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts b/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts index a8db46f48..eed3ea5d8 100644 --- a/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts +++ b/Devices/lilygo-tdeck-pro/lilygo,tdeck-pro.dts @@ -23,7 +23,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/lilygo-tdeck/lilygo,tdeck.dts b/Devices/lilygo-tdeck/lilygo,tdeck.dts index 9c4bd34dd..be3d027a1 100644 --- a/Devices/lilygo-tdeck/lilygo,tdeck.dts +++ b/Devices/lilygo-tdeck/lilygo,tdeck.dts @@ -46,7 +46,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts b/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts index 2b742a3c2..9f436f89b 100644 --- a/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts +++ b/Devices/lilygo-tdisplay-s3/lilygo,tdisplay-s3.dts @@ -19,7 +19,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/lilygo-tdisplay/lilygo,tdisplay.dts b/Devices/lilygo-tdisplay/lilygo,tdisplay.dts index 877efc869..fc8248fd4 100644 --- a/Devices/lilygo-tdisplay/lilygo,tdisplay.dts +++ b/Devices/lilygo-tdisplay/lilygo,tdisplay.dts @@ -15,7 +15,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts b/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts index 519bade15..2c7a7bcd6 100644 --- a/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts +++ b/Devices/lilygo-tdongle-s3/lilygo,tdongle-s3.dts @@ -19,7 +19,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/lilygo-thmi/lilygo,thmi.dts b/Devices/lilygo-thmi/lilygo,thmi.dts index 3811c0c6c..fe3d027ce 100644 --- a/Devices/lilygo-thmi/lilygo,thmi.dts +++ b/Devices/lilygo-thmi/lilygo,thmi.dts @@ -23,7 +23,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts index d42846f31..553213840 100644 --- a/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts +++ b/Devices/lilygo-tlora-pager/lilygo,tlora-pager.dts @@ -27,7 +27,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts b/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts index bdfa69ea2..9306ff6f6 100644 --- a/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts +++ b/Devices/m5stack-cardputer-adv/m5stack,cardputer-adv.dts @@ -27,7 +27,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/m5stack-cardputer/m5stack,cardputer.dts b/Devices/m5stack-cardputer/m5stack,cardputer.dts index 8af8b42e4..bf97ffe67 100644 --- a/Devices/m5stack-cardputer/m5stack,cardputer.dts +++ b/Devices/m5stack-cardputer/m5stack,cardputer.dts @@ -28,7 +28,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/m5stack-core2/m5stack,core2.dts b/Devices/m5stack-core2/m5stack,core2.dts index 499df6d1f..87ceca1eb 100644 --- a/Devices/m5stack-core2/m5stack,core2.dts +++ b/Devices/m5stack-core2/m5stack,core2.dts @@ -24,7 +24,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/m5stack-cores3/m5stack,cores3.dts b/Devices/m5stack-cores3/m5stack,cores3.dts index 130a2c41a..0b6c098ef 100644 --- a/Devices/m5stack-cores3/m5stack,cores3.dts +++ b/Devices/m5stack-cores3/m5stack,cores3.dts @@ -31,7 +31,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/m5stack-papers3/m5stack,papers3.dts b/Devices/m5stack-papers3/m5stack,papers3.dts index b2bc3ccec..b51fa9d73 100644 --- a/Devices/m5stack-papers3/m5stack,papers3.dts +++ b/Devices/m5stack-papers3/m5stack,papers3.dts @@ -23,7 +23,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/m5stack-stackchan/m5stack,stackchan.dts b/Devices/m5stack-stackchan/m5stack,stackchan.dts index 9cbb0f4f6..078d491cc 100644 --- a/Devices/m5stack-stackchan/m5stack,stackchan.dts +++ b/Devices/m5stack-stackchan/m5stack,stackchan.dts @@ -31,7 +31,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/m5stack-stickc-plus2/m5stack,stickc-plus2.dts b/Devices/m5stack-stickc-plus2/m5stack,stickc-plus2.dts index 701040f98..dff6ff412 100644 --- a/Devices/m5stack-stickc-plus2/m5stack,stickc-plus2.dts +++ b/Devices/m5stack-stickc-plus2/m5stack,stickc-plus2.dts @@ -19,7 +19,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; gpio0 { diff --git a/Devices/m5stack-sticks3/m5stack,sticks3.dts b/Devices/m5stack-sticks3/m5stack,sticks3.dts index 0011b7e3e..139d12f9e 100644 --- a/Devices/m5stack-sticks3/m5stack,sticks3.dts +++ b/Devices/m5stack-sticks3/m5stack,sticks3.dts @@ -24,7 +24,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/tulip-4r11/tulip-4r11.dts b/Devices/tulip-4r11/tulip-4r11.dts index fd206859a..2781ce45c 100644 --- a/Devices/tulip-4r11/tulip-4r11.dts +++ b/Devices/tulip-4r11/tulip-4r11.dts @@ -24,7 +24,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/unphone/unphone.dts b/Devices/unphone/unphone.dts index f0c3c3999..f98fdd8b8 100644 --- a/Devices/unphone/unphone.dts +++ b/Devices/unphone/unphone.dts @@ -22,7 +22,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts b/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts index b159666c5..da0a0ba5b 100644 --- a/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts +++ b/Devices/waveshare-esp32-s3-geek/waveshare,esp32-s3-geek.dts @@ -21,7 +21,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts b/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts index 388d0c20f..f59ba1bca 100644 --- a/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts +++ b/Devices/waveshare-s3-lcd-13/waveshare,s3-lcd-13.dts @@ -20,7 +20,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts b/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts index 09e23375a..50dc811e6 100644 --- a/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts +++ b/Devices/waveshare-s3-touch-lcd-128/waveshare,s3-touch-lcd-128.dts @@ -21,7 +21,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts b/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts index 737a9e478..32ea31df8 100644 --- a/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts +++ b/Devices/waveshare-s3-touch-lcd-147/waveshare,s3-touch-lcd-147.dts @@ -21,7 +21,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts b/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts index 3bb7edb1b..44c66423a 100644 --- a/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts +++ b/Devices/waveshare-s3-touch-lcd-43/waveshare,s3-touch-lcd-43.dts @@ -20,7 +20,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts b/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts index 385683d00..0fb9663a4 100644 --- a/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts +++ b/Devices/wireless-tag-wt32-sc01-plus/wireless-tag,wt32-sc01-plus.dts @@ -20,7 +20,6 @@ wifi0 { compatible = "espressif,esp32-wifi-pinned"; - status = "disabled"; }; ble0 { diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 75151dbd9..e004d7911 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -11,11 +11,15 @@ ## Higher Priority -- AppEventSubscription, SystemEventSubscription: should use TaskHandle_t notification. That way, there can be a single wait event for a task instead of X separate ones with each their timeout. +- lvgl file lock won't work with display vs sdcard when lvgl is stopped (external app bug risk) +- Apps should be able to specify stack size in their manifest +- Apps currently have a `Context` object with an `appInstanceId` in it, purely for being able to close the app. + Change it so that the app has its own termination signal that it waits for in the loop, it should subscribe to the event group. +- stopAppFromToolbar() in Tactility.cpp stops the top-most app. Change it so the toolbar knows for which app id it is created, so it can rely on that. +- Warn if file operations are done from prohibited tasks (e.g. lvgl task) - AppHubApp: Prevent download callbacks from accessing a destroyed view. - Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp -- wifi: wifi_add_event_callback() and wifi_remove_event_callback() should be replaced by a subscribe/await pattern like system events. - When that's changed reduce LVGL callstack size in Tactility.cpp run() +- Get rid of WiFi service (Wifi.cpp/h) in Tactility.cpp - Make it more clear to end-users that an SD card is required to run Tactility - Make it possible to override stack size for an app via config file (loaded at boot), and make it possible to set preferred memory location (e.g. internal/external) - Wrap file operations like fopen/fclose with file_mutex diff --git a/Modules/app-module/include/app/event.h b/Modules/app-module/include/app/event.h index e5af8f119..f6b77f903 100644 --- a/Modules/app-module/include/app/event.h +++ b/Modules/app-module/include/app/event.h @@ -6,15 +6,14 @@ #include #include +#include #include -#include -#include #ifdef __cplusplus extern "C" { #endif -/** Identifies the kind of app-lifecycle event delivered through app_event_await(). */ +/** Identifies the kind of app-lifecycle event delivered through app_event_poll(). */ enum AppEventType { APP_EVENT_RESULT, // struct AppResultEventData APP_EVENT_CLOSE, // no data - terminate now, permanently @@ -51,30 +50,62 @@ struct AppEvent { * Caller-owned subscription node. Unlike TactilityKernel's system_event poll subscription * (which coalesces to the latest value), this queues events by value (FIFO) since dropping an * APP_EVENT_RESULT would be unacceptable. - * @warning Fields other than `app_instance_id` are for internal use only; do not read or write - * them directly. + * @warning Fields other than `bit` are for internal use only; do not read or write them + * directly. */ struct AppEventSubscription { - /** The app instance this subscription receives events for; set by the caller before app_event_subscribe(). */ - AppInstanceId app_instance_id; - - TaskHandle_t task; - - struct AppEvent queue[APP_EVENT_QUEUE_CAPACITY]; - uint8_t head; - uint8_t count; - - struct AppEventSubscription* next; + /** Set by app_event_subscribe()/app_event_subscribe_with_app_id(). Read-only for the + * caller: OR it into a task_event_group_wait() mask (alongside other subscriptions sharing + * the same `event_group`) to block on this subscription and other event sources with one + * call. */ + uint32_t bit; + + struct { + /** The app instance this subscription receives events for; set by + * app_event_subscribe()/app_event_subscribe_with_app_id(). */ + AppInstanceId app_instance_id; + + /** Caller-owned, borrowed; set by app_event_subscribe(). */ + struct TaskEventGroup* event_group; + + struct AppEvent queue[APP_EVENT_QUEUE_CAPACITY]; + uint8_t head; + uint8_t count; + + struct AppEventSubscription* next; + } internal; }; /** - * Register a subscription for events addressed to @a sub->app_instance_id. + * Register a subscription for events addressed to the calling app's own instance (identified via + * app_scheduler_current_app_id()). + * @warning Does not work in ISR context. Must be called from the app's own task. + * @param[in,out] sub subscription to register; owns the storage, must stay alive (and + * stationary) until unsubscribed + * @param[in] event_group caller-owned group to wait on; must outlive @a sub (i.e. be + * destructed only after app_event_unsubscribe()). To block for an event, call + * task_event_group_wait()/task_event_group_wait_any() on this group (OR sub->bit into the mask, + * or use _wait_any() to include every subscription sharing it), then drain with app_event_poll(). + * @retval ERROR_NONE on success + * @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered + * @retval ERROR_INVALID_STATE @a sub is already registered + */ +error_t app_event_subscribe(struct AppEventSubscription* sub, struct TaskEventGroup* event_group); + +/** + * Same as app_event_subscribe(), but for a caller that isn't running on @a app_instance_id's own + * task (e.g. test code simulating multiple distinct app instances from one thread). Production + * app code should use app_event_subscribe() instead. * @warning Does not work in ISR context. - * @param[in,out] sub subscription to register; caller sets @a sub->app_instance_id beforehand, - * owns the storage, and must keep it alive (and stationary) until unsubscribed - * @return ERROR_NONE on success + * @param[in,out] sub subscription to register; owns the storage, must stay alive (and + * stationary) until unsubscribed + * @param[in] event_group caller-owned group to wait on; same contract as app_event_subscribe() + * @param[in] app_instance_id the app instance this subscription receives events for + * @retval ERROR_NONE on success + * @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered + * @retval ERROR_INVALID_STATE @a sub is already registered */ -error_t app_event_subscribe(struct AppEventSubscription* sub); +error_t app_event_subscribe_with_app_id(struct AppEventSubscription* sub, struct TaskEventGroup* event_group, AppInstanceId app_instance_id); /** * Remove a previously registered subscription. @@ -94,11 +125,14 @@ error_t app_event_unsubscribe(struct AppEventSubscription* sub); error_t app_event_emit(AppInstanceId app_instance_id, const struct AppEvent* event); /** - * Pop the next event for @a sub, blocking up to @a timeout if the queue is currently empty. + * Non-blocking: pop the next event for @a sub if one is already queued. + * @warning Never blocks. To wait for an event, block in task_event_group_wait()/ + * task_event_group_wait_any() on @a sub's event group first (see app_event_subscribe()), then + * drain with this in a loop. * @retval ERROR_NONE @a out_event was filled - * @retval ERROR_TIMEOUT no event arrived before the timeout elapsed + * @retval ERROR_TIMEOUT nothing queued right now */ -error_t app_event_await(struct AppEventSubscription* sub, struct AppEvent* out_event, TickType_t timeout); +error_t app_event_poll(struct AppEventSubscription* sub, struct AppEvent* out_event); #ifdef __cplusplus } diff --git a/Modules/app-module/include/app/loader.h b/Modules/app-module/include/app/loader.h index 0d606184a..db2a12704 100644 --- a/Modules/app-module/include/app/loader.h +++ b/Modules/app-module/include/app/loader.h @@ -21,13 +21,13 @@ extern "C" { /** * Entry point signature for an APP_LOCATION_MEMORY app: a function linked directly into this * firmware binary. Called on the dedicated task app-module's scheduler spawns for this instance, - * blocking for the app's whole lifetime - same contract as an external app's main(), plus - * @a app_instance_id identifying this running instance (use it with + * blocking for the app's whole lifetime - same contract as an external app's main(). Use + * app_scheduler_current_app_id() to identify this running instance (e.g. with * app_event_subscribe()/window_manager_create()/etc.). The instance closes when this function * returns - no separate call is needed. * AppManifest::location.location holds this cast to void*. */ -typedef int32_t (*AppMainFn)(uint32_t app_instance_id, int argc, char* argv[]); +typedef int32_t (*AppMainFn)(int argc, char* argv[]); typedef void* AppRuntime; diff --git a/Modules/app-module/source/app_internal_loader.cpp b/Modules/app-module/source/app_internal_loader.cpp index 4c447d3fe..70d0e179d 100644 --- a/Modules/app-module/source/app_internal_loader.cpp +++ b/Modules/app-module/source/app_internal_loader.cpp @@ -16,9 +16,9 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) { return ERROR_NONE; } -int32_t api_run(AppRuntime runtime, uint32_t app_instance_id, int argc, char* argv[]) { +int32_t api_run(AppRuntime runtime, uint32_t /*app_instance_id*/, int argc, char* argv[]) { auto entry = reinterpret_cast(runtime); - return entry(app_instance_id, argc, argv); + return entry(argc, argv); } void api_unload(AppRuntime /*unused*/) { diff --git a/Modules/app-module/source/event.cpp b/Modules/app-module/source/event.cpp index da24885c0..957668758 100644 --- a/Modules/app-module/source/event.cpp +++ b/Modules/app-module/source/event.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include @@ -7,7 +8,7 @@ /** * Intrusive singly-linked list of subscriptions, keyed by app_instance_id. * Guarded by a single coarse-grained mutex, notifying a subscriber here never invokes caller code - * (just a struct copy and an xTaskNotifyGive), so there is no reentrancy concern requiring a snapshot-then-unlock dance. + * (just a struct copy and a task_event_group_signal), so there is no reentrancy concern requiring a snapshot-then-unlock dance. */ static AppEventSubscription* subscriptions = nullptr; @@ -21,32 +22,55 @@ static AppEventMutex subscriptions_mutex; extern "C" { -error_t app_event_subscribe(AppEventSubscription* sub) { - sub->task = xTaskGetCurrentTaskHandle(); - sub->head = 0; - sub->count = 0; +error_t app_event_subscribe_with_app_id(AppEventSubscription* sub, TaskEventGroup* event_group, AppInstanceId app_instance_id) { + uint32_t bit; + error_t claim_result = task_event_group_claim_bit(event_group, &bit); + if (claim_result != ERROR_NONE) { + return claim_result; + } mutex_lock(&subscriptions_mutex.handle); - sub->next = subscriptions; + + // Avoid cyclic subscription list that would loop forever + if (subscriptions == sub) { + mutex_unlock(&subscriptions_mutex.handle); + task_event_group_release_bit(event_group, bit); + return ERROR_INVALID_STATE; + } + + sub->bit = bit; + sub->internal.app_instance_id = app_instance_id; + sub->internal.event_group = event_group; + sub->internal.head = 0; + sub->internal.count = 0; + sub->internal.next = subscriptions; subscriptions = sub; mutex_unlock(&subscriptions_mutex.handle); return ERROR_NONE; } +error_t app_event_subscribe(AppEventSubscription* sub, TaskEventGroup* event_group) { + return app_event_subscribe_with_app_id(sub, event_group, app_scheduler_current_app_id()); +} + error_t app_event_unsubscribe(AppEventSubscription* sub) { error_t result = ERROR_NOT_FOUND; mutex_lock(&subscriptions_mutex.handle); - for (AppEventSubscription** link = &subscriptions; *link != nullptr; link = &(*link)->next) { + for (AppEventSubscription** link = &subscriptions; *link != nullptr; link = &(*link)->internal.next) { if (*link == sub) { - *link = sub->next; + *link = sub->internal.next; result = ERROR_NONE; break; } } mutex_unlock(&subscriptions_mutex.handle); + if (result == ERROR_NONE) { + task_event_group_release_bit(sub->internal.event_group, sub->bit); + } + return result; } @@ -57,23 +81,23 @@ error_t app_event_emit(AppInstanceId app_instance_id, const AppEvent* event) { error_t result = ERROR_NOT_FOUND; mutex_lock(&subscriptions_mutex.handle); - for (AppEventSubscription* sub = subscriptions; sub != nullptr; sub = sub->next) { - if (sub->app_instance_id != app_instance_id) { + for (AppEventSubscription* sub = subscriptions; sub != nullptr; sub = sub->internal.next) { + if (sub->internal.app_instance_id != app_instance_id) { continue; } - if (sub->count >= APP_EVENT_QUEUE_CAPACITY) { + if (sub->internal.count >= APP_EVENT_QUEUE_CAPACITY) { result = ERROR_RESOURCE; continue; } - uint8_t tail = (sub->head + sub->count) % APP_EVENT_QUEUE_CAPACITY; - sub->queue[tail] = stamped_event; - sub->count++; + uint8_t tail = (sub->internal.head + sub->internal.count) % APP_EVENT_QUEUE_CAPACITY; + sub->internal.queue[tail] = stamped_event; + sub->internal.count++; if (result != ERROR_RESOURCE) { result = ERROR_NONE; } - xTaskNotifyGive(sub->task); + task_event_group_signal(sub->internal.event_group, sub->bit); } mutex_unlock(&subscriptions_mutex.handle); @@ -82,33 +106,17 @@ error_t app_event_emit(AppInstanceId app_instance_id, const AppEvent* event) { static bool try_pop(AppEventSubscription* sub, AppEvent* out_event) { mutex_lock(&subscriptions_mutex.handle); - bool has_event = sub->count > 0; + bool has_event = sub->internal.count > 0; if (has_event) { - *out_event = sub->queue[sub->head]; - sub->head = (sub->head + 1) % APP_EVENT_QUEUE_CAPACITY; - sub->count--; + *out_event = sub->internal.queue[sub->internal.head]; + sub->internal.head = (sub->internal.head + 1) % APP_EVENT_QUEUE_CAPACITY; + sub->internal.count--; } mutex_unlock(&subscriptions_mutex.handle); return has_event; } -error_t app_event_await(AppEventSubscription* sub, AppEvent* out_event, TickType_t timeout) { - if (try_pop(sub, out_event)) { - // Drain any notification credit this (or an earlier) push accumulated on this task's - // FreeRTOS notification value: each app_event_emit() calls xTaskNotifyGive() regardless - // of whether the consumer takes this fast path or the blocking path below, so without - // this the credit would carry over and cause a future ulTaskNotifyTake() below to - // return immediately for a notification that was already accounted for here. - ulTaskNotifyTake(pdTRUE, 0); - return ERROR_NONE; - } - - if (ulTaskNotifyTake(pdTRUE, timeout) == 0) { - return ERROR_TIMEOUT; - } - - // Single-consumer by design (one task per subscription), so a wakeup implies the event - // this call was notified for is still there for us to pop. +error_t app_event_poll(AppEventSubscription* sub, AppEvent* out_event) { return try_pop(sub, out_event) ? ERROR_NONE : ERROR_TIMEOUT; } diff --git a/Modules/app-module/source/symbols.cpp b/Modules/app-module/source/symbols.cpp index 0a7311255..28711df61 100644 --- a/Modules/app-module/source/symbols.cpp +++ b/Modules/app-module/source/symbols.cpp @@ -8,6 +8,7 @@ #include +#include #include #include @@ -18,9 +19,10 @@ extern ServiceManifest app_internal_loader_service_manifest; const ModuleSymbol app_module_symbols[] = { // app/event DEFINE_MODULE_SYMBOL(app_event_subscribe), + DEFINE_MODULE_SYMBOL(app_event_subscribe_with_app_id), DEFINE_MODULE_SYMBOL(app_event_unsubscribe), DEFINE_MODULE_SYMBOL(app_event_emit), - DEFINE_MODULE_SYMBOL(app_event_await), + DEFINE_MODULE_SYMBOL(app_event_poll), // app/install DEFINE_MODULE_SYMBOL(app_get_install_path), DEFINE_MODULE_SYMBOL(app_install), diff --git a/Modules/app-module/tests/source/app_event_test.cpp b/Modules/app-module/tests/source/app_event_test.cpp index e12a51296..43795aeff 100644 --- a/Modules/app-module/tests/source/app_event_test.cpp +++ b/Modules/app-module/tests/source/app_event_test.cpp @@ -6,10 +6,12 @@ #include #include -TEST_CASE("app_event_subscribe/_await deliver events in FIFO order") { +TEST_CASE("app_event_subscribe/_poll deliver events in FIFO order") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = 1; - CHECK_EQ(app_event_subscribe(&sub), ERROR_NONE); + CHECK_EQ(app_event_subscribe_with_app_id(&sub, &event_group, 1), ERROR_NONE); for (uint32_t i = 0; i < 3; i++) { AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = i, .result = 0 } }; @@ -18,32 +20,38 @@ TEST_CASE("app_event_subscribe/_await deliver events in FIFO order") { for (uint32_t i = 0; i < 3; i++) { AppEvent out {}; - CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_NONE); + CHECK_EQ(app_event_poll(&sub, &out), ERROR_NONE); CHECK_EQ(out.type, APP_EVENT_RESULT); CHECK_EQ(out.result.launch_id, i); } app_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); } TEST_CASE("app_event_emit only delivers to subscriptions for that app_instance_id") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = 10; - app_event_subscribe(&sub); + app_event_subscribe_with_app_id(&sub, &event_group, 10); AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; CHECK_EQ(app_event_emit(11, &event), ERROR_NOT_FOUND); AppEvent out {}; - CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT); + CHECK_EQ(app_event_poll(&sub, &out), ERROR_TIMEOUT); app_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); } TEST_CASE("app_event_emit returns ERROR_RESOURCE and drops the newest event once a subscription's queue is full") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = 20; - app_event_subscribe(&sub); + app_event_subscribe_with_app_id(&sub, &event_group, 20); for (uint32_t i = 0; i < APP_EVENT_QUEUE_CAPACITY; i++) { AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = { .launch_id = i, .result = 0 } }; @@ -57,42 +65,52 @@ TEST_CASE("app_event_emit returns ERROR_RESOURCE and drops the newest event once // The already-queued events survive, in order, and the dropped one never arrives. for (uint32_t i = 0; i < APP_EVENT_QUEUE_CAPACITY; i++) { AppEvent out {}; - CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_NONE); + CHECK_EQ(app_event_poll(&sub, &out), ERROR_NONE); CHECK_EQ(out.result.launch_id, i); } AppEvent out {}; - CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT); + CHECK_EQ(app_event_poll(&sub, &out), ERROR_TIMEOUT); app_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); } TEST_CASE("app_event_unsubscribe stops further delivery") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = 30; - app_event_subscribe(&sub); + app_event_subscribe_with_app_id(&sub, &event_group, 30); CHECK_EQ(app_event_unsubscribe(&sub), ERROR_NONE); CHECK_EQ(app_event_unsubscribe(&sub), ERROR_NOT_FOUND); AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; CHECK_EQ(app_event_emit(30, &event), ERROR_NOT_FOUND); + + task_event_group_destruct(&event_group); } -TEST_CASE("app_event_await times out when no event has arrived") { +TEST_CASE("app_event_poll times out when no event has arrived") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = 40; - app_event_subscribe(&sub); + app_event_subscribe_with_app_id(&sub, &event_group, 40); AppEvent out {}; - CHECK_EQ(app_event_await(&sub, &out, 0), ERROR_TIMEOUT); + CHECK_EQ(app_event_poll(&sub, &out), ERROR_TIMEOUT); app_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); } TEST_CASE("app_event_emit stamps the event with the current boot-relative time") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = 50; - app_event_subscribe(&sub); + app_event_subscribe_with_app_id(&sub, &event_group, 50); auto before = static_cast(get_micros_since_boot()); AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; @@ -100,17 +118,20 @@ TEST_CASE("app_event_emit stamps the event with the current boot-relative time") auto after = static_cast(get_micros_since_boot()); AppEvent out {}; - REQUIRE_EQ(app_event_await(&sub, &out, 0), ERROR_NONE); + REQUIRE_EQ(app_event_poll(&sub, &out), ERROR_NONE); CHECK_GE(out.timestamp, before); CHECK_LE(out.timestamp, after); app_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); } -TEST_CASE("app_event_await wakes when the event is emitted from another task") { +TEST_CASE("task_event_group_wait wakes when the event is emitted from another task") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = 60; - CHECK_EQ(app_event_subscribe(&sub), ERROR_NONE); + CHECK_EQ(app_event_subscribe_with_app_id(&sub, &event_group, 60), ERROR_NONE); auto* thread = thread_alloc_full( "app-event-emitter", @@ -126,12 +147,14 @@ TEST_CASE("app_event_await wakes when the event is emitted from another task") { ); CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); AppEvent out {}; - CHECK_EQ(app_event_await(&sub, &out, pdMS_TO_TICKS(2000)), ERROR_NONE); + CHECK_EQ(app_event_poll(&sub, &out), ERROR_NONE); CHECK_EQ(out.type, APP_EVENT_CLOSE); CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), 1), ERROR_NONE); thread_free(thread); app_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); } diff --git a/Modules/app-module/tests/source/app_manager_test.cpp b/Modules/app-module/tests/source/app_manager_test.cpp index 101a10481..85a894102 100644 --- a/Modules/app-module/tests/source/app_manager_test.cpp +++ b/Modules/app-module/tests/source/app_manager_test.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -55,7 +56,7 @@ void stash_received_arguments(int argc, char* argv[]) { // parameter (app_manager_start_for_result()), acts as a modal dialog instead: returns the // requested result (argv[0], parsed as an int) immediately (the app's own return value IS the // delivered APP_EVENT_RESULT.result - see app_scheduler.cpp's thread_main()). -int32_t fake_run(void*, uint32_t app_instance_id, int argc, char* argv[]) { +int32_t fake_run(void*, uint32_t /*app_instance_id*/, int argc, char* argv[]) { stash_received_arguments(argc, argv); if (argc == 1) { @@ -66,21 +67,30 @@ int32_t fake_run(void*, uint32_t app_instance_id, int argc, char* argv[]) { return static_cast(strtol(argv[0], nullptr, 10)); } + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = app_instance_id; - app_event_subscribe(&sub); + app_event_subscribe(&sub, &event_group); while (true) { - AppEvent event {}; - if (app_event_await(&sub, &event, pdMS_TO_TICKS(5000)) != ERROR_NONE) { + if (task_event_group_wait_any(&event_group, nullptr, pdMS_TO_TICKS(5000)) != ERROR_NONE) { break; // safety net so a bug here can't hang the test suite } - if (event.type == APP_EVENT_CLOSE) { - break; + + bool done = false; + AppEvent event {}; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + done = true; + break; + } } + if (done) break; } app_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); return 0; } @@ -129,8 +139,8 @@ void ensure_memory_loader_registered() { // Same subscribe-until-close contract as fake_run() above, but called directly as an AppMainFn - // this is what a real internal app's entry point looks like. -int32_t fake_app_main(uint32_t app_instance_id, int argc, char* argv[]) { - return fake_run(nullptr, app_instance_id, argc, argv); +int32_t fake_app_main(int argc, char* argv[]) { + return fake_run(nullptr, app_scheduler_current_app_id(), argc, argv); } // Wraps app_manager_get_topmost_instance_id() for terse assertions: 0 if no app is Active. @@ -329,9 +339,11 @@ TEST_CASE("app_manager_start_for_result delivers APP_EVENT_RESULT to the parent, REQUIRE_EQ(app_manager_start("test.app.parent", &parent_id), ERROR_NONE); CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000)); + TaskEventGroup parent_event_group {}; + task_event_group_construct(&parent_event_group); + AppEventSubscription parent_sub {}; - parent_sub.app_instance_id = parent_id; - REQUIRE_EQ(app_event_subscribe(&parent_sub), ERROR_NONE); + REQUIRE_EQ(app_event_subscribe_with_app_id(&parent_sub, &parent_event_group, parent_id), ERROR_NONE); const char* argv[] = { "42" }; uint32_t child_id = 0; @@ -340,13 +352,15 @@ TEST_CASE("app_manager_start_for_result delivers APP_EVENT_RESULT to the parent, // Launching a modal child never touches the parent's own task/state. CHECK_EQ(app_manager_get_state(parent_id), APP_INSTANCE_STATE_ACTIVE); + REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); AppEvent event {}; - REQUIRE_EQ(app_event_await(&parent_sub, &event, pdMS_TO_TICKS(2000)), ERROR_NONE); + REQUIRE_EQ(app_event_poll(&parent_sub, &event), ERROR_NONE); CHECK_EQ(event.type, APP_EVENT_RESULT); CHECK_EQ(event.result.launch_id, child_id); CHECK_EQ(event.result.result, 42); app_event_unsubscribe(&parent_sub); + task_event_group_destruct(&parent_event_group); app_manager_stop(child_id); app_manager_stop(parent_id); app_manager_remove("test.app.parent"); @@ -365,9 +379,11 @@ TEST_CASE("app_manager_start_for_result delivers the child's own return value as REQUIRE_EQ(app_manager_start("test.app.parent2", &parent_id), ERROR_NONE); CHECK(wait_for_state(parent_id, APP_INSTANCE_STATE_ACTIVE, 1000)); + TaskEventGroup parent_event_group {}; + task_event_group_construct(&parent_event_group); + AppEventSubscription parent_sub {}; - parent_sub.app_instance_id = parent_id; - REQUIRE_EQ(app_event_subscribe(&parent_sub), ERROR_NONE); + REQUIRE_EQ(app_event_subscribe_with_app_id(&parent_sub, &parent_event_group, parent_id), ERROR_NONE); uint32_t child_id = 0; // No parameters - fake_run falls through to its normal CLOSE loop instead of acting as a @@ -377,13 +393,15 @@ TEST_CASE("app_manager_start_for_result delivers the child's own return value as app_manager_stop(child_id); // force-close + REQUIRE_EQ(task_event_group_wait(&parent_event_group, parent_sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); AppEvent event {}; - REQUIRE_EQ(app_event_await(&parent_sub, &event, pdMS_TO_TICKS(2000)), ERROR_NONE); + REQUIRE_EQ(app_event_poll(&parent_sub, &event), ERROR_NONE); CHECK_EQ(event.type, APP_EVENT_RESULT); CHECK_EQ(event.result.launch_id, child_id); CHECK_EQ(event.result.result, 0); // fake_run's CLOSE loop always returns 0 app_event_unsubscribe(&parent_sub); + task_event_group_destruct(&parent_event_group); app_manager_stop(parent_id); app_manager_remove("test.app.parent2"); app_manager_remove("test.app.child2"); diff --git a/Modules/app-module/tests/source/combined_event_test.cpp b/Modules/app-module/tests/source/combined_event_test.cpp new file mode 100644 index 000000000..88ead21b6 --- /dev/null +++ b/Modules/app-module/tests/source/combined_event_test.cpp @@ -0,0 +1,71 @@ +#include "doctest.h" + +#include + +#include + +// Regression coverage for the primary motivation behind TaskEventGroup: a task subscribed to +// both an app_event and a system_event must be able to block once and wake for either, without +// losing an event or regressing either subsystem's own delivery semantics (FIFO for app_event, +// coalescing for system_event). + +TEST_CASE("a task can wait on app_event and system_event together via one TaskEventGroup") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + + AppEventSubscription app_sub {}; + CHECK_EQ(app_event_subscribe_with_app_id(&app_sub, &event_group, 100), ERROR_NONE); + + SystemEventSubscription sys_sub {}; + sys_sub.event.type = KERNEL_EVENT_BOOT_COMPLETED; + CHECK_EQ(system_event_subscribe(&sys_sub, &event_group), ERROR_NONE); + + // Distinct bits, so a combined wait can tell (via out_flags) which source(s) fired. + CHECK_NE(app_sub.bit, sys_sub.bit); + + uint32_t both_bits = app_sub.bit | sys_sub.bit; + + // Only app_event fires: combined wait matches just that bit, and only app_event_poll() + // finds something to pop. + AppEvent emitted { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; + CHECK_EQ(app_event_emit(100, &emitted), ERROR_NONE); + + uint32_t out_flags = 0; + CHECK_EQ(task_event_group_wait(&event_group, both_bits, false, &out_flags, 0), ERROR_NONE); + CHECK_EQ(out_flags, app_sub.bit); + + AppEvent app_out {}; + CHECK_EQ(app_event_poll(&app_sub, &app_out), ERROR_NONE); + CHECK_EQ(app_out.type, APP_EVENT_CLOSE); + CHECK_EQ(system_event_poll(&sys_sub), ERROR_TIMEOUT); + + // Only system_event fires: combined wait matches just that bit, and only + // system_event_poll() finds something pending. + CHECK_EQ(system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0), ERROR_NONE); + + out_flags = 0; + CHECK_EQ(task_event_group_wait(&event_group, both_bits, false, &out_flags, 0), ERROR_NONE); + CHECK_EQ(out_flags, sys_sub.bit); + + CHECK_EQ(system_event_poll(&sys_sub), ERROR_NONE); + AppEvent app_out2 {}; + CHECK_EQ(app_event_poll(&app_sub, &app_out2), ERROR_TIMEOUT); + + // Both fire before the wait: combined wait matches both bits, and both subsystems' own + // polls (which re-check their own state rather than trusting the bit) still deliver. + CHECK_EQ(app_event_emit(100, &emitted), ERROR_NONE); + CHECK_EQ(system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0), ERROR_NONE); + + out_flags = 0; + CHECK_EQ(task_event_group_wait(&event_group, both_bits, false, &out_flags, 0), ERROR_NONE); + CHECK_EQ(out_flags, both_bits); + + CHECK_EQ(app_event_poll(&app_sub, &app_out), ERROR_NONE); + CHECK_EQ(system_event_poll(&sys_sub), ERROR_NONE); + + // Unsubscribe order (system_event before app_event) must not matter - the group outlives + // both and is destructed last. + system_event_unsubscribe(&sys_sub); + app_event_unsubscribe(&app_sub); + task_event_group_destruct(&event_group); +} diff --git a/Platforms/platform-esp32/CMakeLists.txt b/Platforms/platform-esp32/CMakeLists.txt index 419e0801a..4295d9c58 100644 --- a/Platforms/platform-esp32/CMakeLists.txt +++ b/Platforms/platform-esp32/CMakeLists.txt @@ -14,7 +14,7 @@ idf_component_register( SRCS ${SOURCES} INCLUDE_DIRS "include/" PRIV_INCLUDE_DIRS "private/" - REQUIRES TactilityKernel driver esp_adc esp_driver_i2c esp_lcd vfs fatfs esp_wifi esp_netif esp_event + REQUIRES TactilityKernel TactilityKernelCpp driver esp_adc esp_driver_i2c esp_lcd vfs fatfs esp_wifi esp_netif esp_event PRIV_REQUIRES ${PRIV_REQUIRES_LIST} ) diff --git a/Platforms/platform-esp32/source/drivers/esp32_wifi.cpp b/Platforms/platform-esp32/source/drivers/esp32_wifi.cpp index 3f9f22035..afae6d75a 100644 --- a/Platforms/platform-esp32/source/drivers/esp32_wifi.cpp +++ b/Platforms/platform-esp32/source/drivers/esp32_wifi.cpp @@ -18,6 +18,8 @@ #include #include +#include + #if defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED) #include #endif @@ -33,14 +35,8 @@ namespace { -constexpr size_t WIFI_MAX_CALLBACKS = 4; constexpr uint16_t WIFI_SCAN_RECORD_LIMIT = 32; -struct WifiCallbackEntry { - WifiEventCallback fn = nullptr; - void* ctx = nullptr; -}; - struct Esp32WifiCtx { Device* device = nullptr; @@ -66,9 +62,8 @@ struct Esp32WifiCtx { int32_t lastEventId = -1; TickType_t lastEventTick = 0; - Mutex callbackMutex{}; - WifiCallbackEntry callbacks[WIFI_MAX_CALLBACKS] = {}; - size_t callbackCount = 0; + Mutex subscriptionsMutex{}; + WifiEventSubscription* subscriptions = nullptr; }; #define GET_CTX(device) (static_cast(device_get_driver_data(device))) @@ -93,16 +88,18 @@ WifiAuthenticationType to_wifi_authentication_type(wifi_auth_mode_t mode) { } void fire_event(Esp32WifiCtx* ctx, WifiEvent event) { - WifiCallbackEntry local[WIFI_MAX_CALLBACKS]; - size_t count; - mutex_lock(&ctx->callbackMutex); - count = ctx->callbackCount; - memcpy(local, ctx->callbacks, count * sizeof(WifiCallbackEntry)); - mutex_unlock(&ctx->callbackMutex); - - for (size_t i = 0; i < count; i++) { - local[i].fn(ctx->device, local[i].ctx, event); + mutex_lock(&ctx->subscriptionsMutex); + for (WifiEventSubscription* sub = ctx->subscriptions; sub != nullptr; sub = sub->internal.next) { + mutex_lock(&sub->internal.ring_mutex); + if (sub->internal.count < WIFI_EVENT_QUEUE_CAPACITY) { + uint8_t tail = (sub->internal.head + sub->internal.count) % WIFI_EVENT_QUEUE_CAPACITY; + sub->internal.queue[tail] = event; + sub->internal.count++; + } + mutex_unlock(&sub->internal.ring_mutex); + task_event_group_signal(sub->internal.event_group, sub->bit); } + mutex_unlock(&ctx->subscriptionsMutex); } // ---- ESP-IDF event handling (runs on the esp_event task) ---- @@ -257,6 +254,11 @@ error_t bring_up_wifi(Esp32WifiCtx* ctx) { ctx->radioState = WIFI_RADIO_STATE_ON; mutex_unlock(&ctx->mutex); + WifiEvent radio_event = {}; + radio_event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED; + radio_event.radio_state = WIFI_RADIO_STATE_ON; + fire_event(ctx, radio_event); + LOG_I(TAG, "WiFi radio on"); return ERROR_NONE; } @@ -306,6 +308,11 @@ void bring_down_wifi(Esp32WifiCtx* ctx) { ctx->radioState = WIFI_RADIO_STATE_OFF; mutex_unlock(&ctx->mutex); + WifiEvent radio_event = {}; + radio_event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED; + radio_event.radio_state = WIFI_RADIO_STATE_OFF; + fire_event(ctx, radio_event); + LOG_I(TAG, "WiFi radio off"); } @@ -497,38 +504,77 @@ error_t api_station_get_rssi(Device* device, int32_t* rssi) { return ERROR_NONE; } -error_t api_add_event_callback(Device* device, void* callback_context, WifiEventCallback callback) { +error_t api_set_radio_on(Device* device) { + auto* ctx = GET_CTX(device); + if (ctx == nullptr) return ERROR_INVALID_STATE; + + mutex_lock(&ctx->mutex); + bool already_on = ctx->radioState == WIFI_RADIO_STATE_ON; + mutex_unlock(&ctx->mutex); + if (already_on) return ERROR_NONE; + + return bring_up_wifi(ctx); +} + +error_t api_set_radio_off(Device* device) { + auto* ctx = GET_CTX(device); + if (ctx == nullptr) return ERROR_INVALID_STATE; + + mutex_lock(&ctx->mutex); + bool already_off = ctx->radioState == WIFI_RADIO_STATE_OFF; + mutex_unlock(&ctx->mutex); + if (already_off) return ERROR_NONE; + + bring_down_wifi(ctx); + return ERROR_NONE; +} + +error_t api_event_subscribe(Device* device, WifiEventSubscription* sub, TaskEventGroup* event_group) { auto* ctx = GET_CTX(device); - if (ctx == nullptr || callback == nullptr) return ERROR_INVALID_ARGUMENT; + if (ctx == nullptr || sub == nullptr || event_group == nullptr) return ERROR_INVALID_ARGUMENT; + + uint32_t bit; + error_t claim_result = task_event_group_claim_bit(event_group, &bit); + if (claim_result != ERROR_NONE) { + return claim_result; + } - mutex_lock(&ctx->callbackMutex); - if (ctx->callbackCount >= WIFI_MAX_CALLBACKS) { - mutex_unlock(&ctx->callbackMutex); - return ERROR_OUT_OF_MEMORY; + mutex_lock(&ctx->subscriptionsMutex); + + // Avoid cyclic subscription list that would loop forever + if (ctx->subscriptions == sub) { + mutex_unlock(&ctx->subscriptionsMutex); + task_event_group_release_bit(event_group, bit); + return ERROR_INVALID_STATE; } - ctx->callbacks[ctx->callbackCount] = { .fn = callback, .ctx = callback_context }; - ctx->callbackCount++; - mutex_unlock(&ctx->callbackMutex); + + sub->internal.event_group = event_group; + sub->bit = bit; + sub->internal.next = ctx->subscriptions; + ctx->subscriptions = sub; + mutex_unlock(&ctx->subscriptionsMutex); return ERROR_NONE; } -error_t api_remove_event_callback(Device* device, WifiEventCallback callback) { +error_t api_event_unsubscribe(Device* device, WifiEventSubscription* sub) { auto* ctx = GET_CTX(device); - if (ctx == nullptr || callback == nullptr) return ERROR_INVALID_ARGUMENT; - - mutex_lock(&ctx->callbackMutex); - for (size_t i = 0; i < ctx->callbackCount; i++) { - if (ctx->callbacks[i].fn == callback) { - for (size_t j = i; j + 1 < ctx->callbackCount; j++) { - ctx->callbacks[j] = ctx->callbacks[j + 1]; - } - ctx->callbackCount--; - mutex_unlock(&ctx->callbackMutex); - return ERROR_NONE; + if (ctx == nullptr || sub == nullptr) return ERROR_INVALID_ARGUMENT; + + error_t result = ERROR_NOT_FOUND; + mutex_lock(&ctx->subscriptionsMutex); + for (WifiEventSubscription** link = &ctx->subscriptions; *link != nullptr; link = &(*link)->internal.next) { + if (*link == sub) { + *link = sub->internal.next; + result = ERROR_NONE; + break; } } - mutex_unlock(&ctx->callbackMutex); - return ERROR_NOT_FOUND; + mutex_unlock(&ctx->subscriptionsMutex); + + if (result == ERROR_NONE) { + task_event_group_release_bit(sub->internal.event_group, sub->bit); + } + return result; } error_t api_get_firmware_ops(Device* /*device*/, const FirmwareOps** ops, void** ctx) { @@ -550,6 +596,8 @@ error_t api_get_firmware_ops(Device* /*device*/, const FirmwareOps** ops, void** } const WifiApi esp32_wifi_api = { + .set_radio_on = api_set_radio_on, + .set_radio_off = api_set_radio_off, .get_radio_state = api_get_radio_state, .get_station_state = api_get_station_state, .get_access_point_state = api_get_access_point_state, @@ -561,34 +609,23 @@ const WifiApi esp32_wifi_api = { .station_connect = api_station_connect, .station_disconnect = api_station_disconnect, .station_get_rssi = api_station_get_rssi, - .add_event_callback = api_add_event_callback, - .remove_event_callback = api_remove_event_callback, + .event_subscribe = api_event_subscribe, + .event_unsubscribe = api_event_unsubscribe, .get_firmware_ops = api_get_firmware_ops }; -// ---- Driver lifecycle ---- -// The ESP-IDF WiFi stack isn't touched until the device is actually started: -// registering this driver (module start()) only makes it available for -// binding, it doesn't spin up any resources. - error_t start_device(Device* device) { - auto* ctx = new(std::nothrow) Esp32WifiCtx(); - if (ctx == nullptr) return ERROR_OUT_OF_MEMORY; + // Prefers PSRAM/SPIRAM, falling back to internal RAM when unavailable - aborts on true OOM + // (see OptExternalAllocator's own doc), so no null check here. + tt::OptExternalAllocator allocator; + auto* ctx = allocator.allocate(1); + new (ctx) Esp32WifiCtx(); ctx->device = device; mutex_construct(&ctx->mutex); - mutex_construct(&ctx->callbackMutex); + mutex_construct(&ctx->subscriptionsMutex); device_set_driver_data(device, ctx); - error_t result = bring_up_wifi(ctx); - if (result != ERROR_NONE) { - device_set_driver_data(device, nullptr); - mutex_destruct(&ctx->callbackMutex); - mutex_destruct(&ctx->mutex); - delete ctx; - return result; - } - return ERROR_NONE; } @@ -596,12 +633,28 @@ error_t stop_device(Device* device) { auto* ctx = GET_CTX(device); if (ctx == nullptr) return ERROR_NONE; - bring_down_wifi(ctx); + if (ctx->radioState == WIFI_RADIO_STATE_ON) { + bring_down_wifi(ctx); + } + + // Release any subscribers that never unsubscribed: device_stop() doesn't wait for apps still + // using this device, so a later wifi_event_unsubscribe() would find no ctx and skip releasing + // the bit and destructing sub->internal.ring_mutex. + mutex_lock(&ctx->subscriptionsMutex); + for (WifiEventSubscription* sub = ctx->subscriptions; sub != nullptr;) { + WifiEventSubscription* next = sub->internal.next; + task_event_group_release_bit(sub->internal.event_group, sub->bit); + mutex_destruct(&sub->internal.ring_mutex); + sub = next; + } + ctx->subscriptions = nullptr; + mutex_unlock(&ctx->subscriptionsMutex); device_set_driver_data(device, nullptr); - mutex_destruct(&ctx->callbackMutex); + mutex_destruct(&ctx->subscriptionsMutex); mutex_destruct(&ctx->mutex); - delete ctx; + ctx->~Esp32WifiCtx(); + tt::OptExternalAllocator().deallocate(ctx, 1); return ERROR_NONE; } diff --git a/Platforms/platform-esp32/source/drivers/esp32_wifi_pinned.cpp b/Platforms/platform-esp32/source/drivers/esp32_wifi_pinned.cpp index f83c86691..340c55910 100644 --- a/Platforms/platform-esp32/source/drivers/esp32_wifi_pinned.cpp +++ b/Platforms/platform-esp32/source/drivers/esp32_wifi_pinned.cpp @@ -41,6 +41,12 @@ struct Esp32WifiPinnedCtx { }; #define GET_CTX(device) (static_cast(device_get_driver_data(device))) +// Only for event_subscribe()/event_unsubscribe(): unlike the rest of WifiApi, have +// construct/destruct side effects on sub->ring_mutex at the wifi_event_subscribe()/ +// wifi_event_unsubscribe() kernel-wrapper layer (see wifi.cpp) - calling that wrapper AGAIN here +// (instead of the child driver's raw vtable entry) double-runs those side effects, corrupting the +// mutex. Every other WifiApi call below is a plain pass-through. +#define CHILD_WIFI_API(child) ((const struct WifiApi*)device_get_driver(child)->api) struct MarshalledCall { const std::function* work; @@ -176,20 +182,44 @@ error_t api_station_get_rssi(Device* device, int32_t* rssi) { return wifi_station_get_rssi(ctx->child, rssi); } -error_t api_add_event_callback(Device* device, void* callback_context, WifiEventCallback callback) { +error_t api_event_subscribe(Device* device, WifiEventSubscription* sub, TaskEventGroup* event_group) { auto* ctx = GET_CTX(device); if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE; - return wifi_add_event_callback(ctx->child, callback_context, callback); + return CHILD_WIFI_API(ctx->child)->event_subscribe(ctx->child, sub, event_group); } -error_t api_remove_event_callback(Device* device, WifiEventCallback callback) { +error_t api_event_unsubscribe(Device* device, WifiEventSubscription* sub) { auto* ctx = GET_CTX(device); if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE; - return wifi_remove_event_callback(ctx->child, callback); + return CHILD_WIFI_API(ctx->child)->event_unsubscribe(ctx->child, sub); } // ---- WifiApi: state-changing calls are marshalled onto the pinned WiFi task ---- +error_t api_set_radio_on(Device* device) { + auto* ctx = GET_CTX(device); + if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE; + + error_t result = ERROR_NONE; + Device* child = ctx->child; + error_t err = run_on_pinned_thread(ctx, [child, &result]() { + result = wifi_set_radio_on(child); + }); + return err != ERROR_NONE ? err : result; +} + +error_t api_set_radio_off(Device* device) { + auto* ctx = GET_CTX(device); + if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE; + + error_t result = ERROR_NONE; + Device* child = ctx->child; + error_t err = run_on_pinned_thread(ctx, [child, &result]() { + result = wifi_set_radio_off(child); + }); + return err != ERROR_NONE ? err : result; +} + error_t api_scan(Device* device) { auto* ctx = GET_CTX(device); if (ctx == nullptr || ctx->child == nullptr) return ERROR_INVALID_STATE; @@ -227,6 +257,8 @@ error_t api_station_disconnect(Device* device) { } const WifiApi esp32_wifi_pinned_api = { + .set_radio_on = api_set_radio_on, + .set_radio_off = api_set_radio_off, .get_radio_state = api_get_radio_state, .get_station_state = api_get_station_state, .get_access_point_state = api_get_access_point_state, @@ -238,15 +270,15 @@ const WifiApi esp32_wifi_pinned_api = { .station_connect = api_station_connect, .station_disconnect = api_station_disconnect, .station_get_rssi = api_station_get_rssi, - .add_event_callback = api_add_event_callback, - .remove_event_callback = api_remove_event_callback + .event_subscribe = api_event_subscribe, + .event_unsubscribe = api_event_unsubscribe }; // ---- Driver lifecycle ---- -// Starting/stopping the child (which brings the ESP-IDF WiFi stack up/down) -// also happens on the pinned thread, since esp_wifi_init/start/stop/deinit -// are subject to the same core-affinity requirement as the state-changing -// WifiApi calls above. +// Starting/stopping the child (allocating/freeing its bookkeeping - see esp32_wifi.cpp's own +// start_device()/stop_device() comment) happens on the pinned thread for the same core-affinity +// reason as the state-changing WifiApi calls above; enable()/disable() (which actually bring the +// ESP-IDF WiFi stack up/down) are marshalled the same way. error_t start_device(Device* device) { auto* ctx = new(std::nothrow) Esp32WifiPinnedCtx(); diff --git a/Platforms/platform-posix/source/drivers/mock_wifi.cpp b/Platforms/platform-posix/source/drivers/mock_wifi.cpp index 35855a8d6..42c85e4dc 100644 --- a/Platforms/platform-posix/source/drivers/mock_wifi.cpp +++ b/Platforms/platform-posix/source/drivers/mock_wifi.cpp @@ -19,8 +19,6 @@ namespace { -constexpr size_t WIFI_MAX_CALLBACKS = 4; - struct MockApRecord { const char* ssid; int8_t rssi; @@ -40,11 +38,6 @@ constexpr size_t MOCK_SCAN_RESULT_COUNT = sizeof(MOCK_SCAN_RESULTS) / sizeof(MOC constexpr int8_t MOCK_CONNECTED_RSSI = -30; constexpr const char* MOCK_IPV4_ADDRESS = "192.168.1.2"; -struct WifiCallbackEntry { - WifiEventCallback fn = nullptr; - void* ctx = nullptr; -}; - struct PosixWifiCtx { Device* device = nullptr; @@ -54,24 +47,25 @@ struct PosixWifiCtx { bool scanning = false; char targetSsid[33] = {}; - Mutex callbackMutex {}; - WifiCallbackEntry callbacks[WIFI_MAX_CALLBACKS] = {}; - size_t callbackCount = 0; + Mutex subscriptionsMutex {}; + WifiEventSubscription* subscriptions = nullptr; }; #define GET_CTX(device) (static_cast(device_get_driver_data(device))) void fireEvent(PosixWifiCtx* ctx, WifiEvent event) { - WifiCallbackEntry local[WIFI_MAX_CALLBACKS]; - size_t count; - mutex_lock(&ctx->callbackMutex); - count = ctx->callbackCount; - memcpy(local, ctx->callbacks, count * sizeof(WifiCallbackEntry)); - mutex_unlock(&ctx->callbackMutex); - - for (size_t i = 0; i < count; i++) { - local[i].fn(ctx->device, local[i].ctx, event); + mutex_lock(&ctx->subscriptionsMutex); + for (WifiEventSubscription* sub = ctx->subscriptions; sub != nullptr; sub = sub->internal.next) { + mutex_lock(&sub->internal.ring_mutex); + if (sub->internal.count < WIFI_EVENT_QUEUE_CAPACITY) { + uint8_t tail = (sub->internal.head + sub->internal.count) % WIFI_EVENT_QUEUE_CAPACITY; + sub->internal.queue[tail] = event; + sub->internal.count++; + } + mutex_unlock(&sub->internal.ring_mutex); + task_event_group_signal(sub->internal.event_group, sub->bit); } + mutex_unlock(&ctx->subscriptionsMutex); } // ---- WifiApi ---- @@ -247,41 +241,105 @@ error_t apiStationGetRssi(Device* device, int32_t* rssi) { return ERROR_NONE; } -error_t apiAddEventCallback(Device* device, void* callback_context, WifiEventCallback callback) { +error_t apiSetRadioOn(Device* device) { + auto* ctx = GET_CTX(device); + if (ctx == nullptr) return ERROR_INVALID_STATE; + + mutex_lock(&ctx->mutex); + bool already_on = ctx->radioState == WIFI_RADIO_STATE_ON; + ctx->radioState = WIFI_RADIO_STATE_ON; + mutex_unlock(&ctx->mutex); + if (already_on) return ERROR_NONE; + + WifiEvent radio_event = {}; + radio_event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED; + radio_event.radio_state = WIFI_RADIO_STATE_ON; + fireEvent(ctx, radio_event); + + LOG_I(TAG, "WiFi radio on (mock)"); + return ERROR_NONE; +} + +error_t apiSetRadioOff(Device* device) { + auto* ctx = GET_CTX(device); + if (ctx == nullptr) return ERROR_INVALID_STATE; + + mutex_lock(&ctx->mutex); + bool already_off = ctx->radioState == WIFI_RADIO_STATE_OFF; + bool was_connected = ctx->stationState != WIFI_STATION_STATE_DISCONNECTED; + ctx->radioState = WIFI_RADIO_STATE_OFF; + ctx->stationState = WIFI_STATION_STATE_DISCONNECTED; + ctx->scanning = false; + mutex_unlock(&ctx->mutex); + if (already_off) return ERROR_NONE; + + if (was_connected) { + WifiEvent station_event = {}; + station_event.type = WIFI_EVENT_TYPE_STATION_STATE_CHANGED; + station_event.station_state = WIFI_STATION_STATE_DISCONNECTED; + fireEvent(ctx, station_event); + } + + WifiEvent radio_event = {}; + radio_event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED; + radio_event.radio_state = WIFI_RADIO_STATE_OFF; + fireEvent(ctx, radio_event); + + LOG_I(TAG, "WiFi radio off (mock)"); + return ERROR_NONE; +} + +error_t apiEventSubscribe(Device* device, WifiEventSubscription* sub, TaskEventGroup* event_group) { auto* ctx = GET_CTX(device); - if (ctx == nullptr || callback == nullptr) return ERROR_INVALID_ARGUMENT; + if (ctx == nullptr || sub == nullptr || event_group == nullptr) return ERROR_INVALID_ARGUMENT; - mutex_lock(&ctx->callbackMutex); - if (ctx->callbackCount >= WIFI_MAX_CALLBACKS) { - mutex_unlock(&ctx->callbackMutex); - return ERROR_OUT_OF_MEMORY; + uint32_t bit; + error_t claim_result = task_event_group_claim_bit(event_group, &bit); + if (claim_result != ERROR_NONE) { + return claim_result; } - ctx->callbacks[ctx->callbackCount] = { .fn = callback, .ctx = callback_context }; - ctx->callbackCount++; - mutex_unlock(&ctx->callbackMutex); + + mutex_lock(&ctx->subscriptionsMutex); + + // Avoid cyclic subscription list that would loop forever + if (ctx->subscriptions == sub) { + mutex_unlock(&ctx->subscriptionsMutex); + task_event_group_release_bit(event_group, bit); + return ERROR_INVALID_STATE; + } + + sub->internal.event_group = event_group; + sub->bit = bit; + sub->internal.next = ctx->subscriptions; + ctx->subscriptions = sub; + mutex_unlock(&ctx->subscriptionsMutex); return ERROR_NONE; } -error_t apiRemoveEventCallback(Device* device, WifiEventCallback callback) { +error_t apiEventUnsubscribe(Device* device, WifiEventSubscription* sub) { auto* ctx = GET_CTX(device); - if (ctx == nullptr || callback == nullptr) return ERROR_INVALID_ARGUMENT; - - mutex_lock(&ctx->callbackMutex); - for (size_t i = 0; i < ctx->callbackCount; i++) { - if (ctx->callbacks[i].fn == callback) { - for (size_t j = i; j + 1 < ctx->callbackCount; j++) { - ctx->callbacks[j] = ctx->callbacks[j + 1]; - } - ctx->callbackCount--; - mutex_unlock(&ctx->callbackMutex); - return ERROR_NONE; + if (ctx == nullptr || sub == nullptr) return ERROR_INVALID_ARGUMENT; + + error_t result = ERROR_NOT_FOUND; + mutex_lock(&ctx->subscriptionsMutex); + for (WifiEventSubscription** link = &ctx->subscriptions; *link != nullptr; link = &(*link)->internal.next) { + if (*link == sub) { + *link = sub->internal.next; + result = ERROR_NONE; + break; } } - mutex_unlock(&ctx->callbackMutex); - return ERROR_NOT_FOUND; + mutex_unlock(&ctx->subscriptionsMutex); + + if (result == ERROR_NONE) { + task_event_group_release_bit(sub->internal.event_group, sub->bit); + } + return result; } const WifiApi posix_wifi_api = { + .set_radio_on = apiSetRadioOn, + .set_radio_off = apiSetRadioOff, .get_radio_state = apiGetRadioState, .get_station_state = apiGetStationState, .get_access_point_state = apiGetAccessPointState, @@ -293,13 +351,14 @@ const WifiApi posix_wifi_api = { .station_connect = apiStationConnect, .station_disconnect = apiStationDisconnect, .station_get_rssi = apiStationGetRssi, - .add_event_callback = apiAddEventCallback, - .remove_event_callback = apiRemoveEventCallback + .event_subscribe = apiEventSubscribe, + .event_unsubscribe = apiEventUnsubscribe }; // ---- Driver lifecycle ---- -// Unlike the real esp32 driver, there's no hardware to bring up: the radio -// simply reports itself as ON as soon as the device is started. +// startDevice()/stopDevice() only allocate/free this driver's bookkeeping (subscriber list +// included), so event subscribers can stay subscribed across radio on/off toggles - the radio +// itself is only touched by apiSetRadioOn()/apiSetRadioOff(). error_t startDevice(Device* device) { auto* ctx = new(std::nothrow) PosixWifiCtx(); @@ -307,12 +366,9 @@ error_t startDevice(Device* device) { ctx->device = device; mutex_construct(&ctx->mutex); - mutex_construct(&ctx->callbackMutex); - ctx->radioState = WIFI_RADIO_STATE_ON; + mutex_construct(&ctx->subscriptionsMutex); device_set_driver_data(device, ctx); - - LOG_I(TAG, "WiFi radio on (mock)"); return ERROR_NONE; } @@ -320,12 +376,28 @@ error_t stopDevice(Device* device) { auto* ctx = GET_CTX(device); if (ctx == nullptr) return ERROR_NONE; + if (ctx->radioState == WIFI_RADIO_STATE_ON) { + apiSetRadioOff(device); + } + + // Release any subscribers that never unsubscribed: device_stop() doesn't wait for apps still + // using this device, so a later wifi_event_unsubscribe() would find no ctx and skip releasing + // the bit and destructing sub->internal.ring_mutex. + mutex_lock(&ctx->subscriptionsMutex); + for (WifiEventSubscription* sub = ctx->subscriptions; sub != nullptr;) { + WifiEventSubscription* next = sub->internal.next; + task_event_group_release_bit(sub->internal.event_group, sub->bit); + mutex_destruct(&sub->internal.ring_mutex); + sub = next; + } + ctx->subscriptions = nullptr; + mutex_unlock(&ctx->subscriptionsMutex); + device_set_driver_data(device, nullptr); - mutex_destruct(&ctx->callbackMutex); + mutex_destruct(&ctx->subscriptionsMutex); mutex_destruct(&ctx->mutex); delete ctx; - LOG_I(TAG, "WiFi radio off (mock)"); return ERROR_NONE; } diff --git a/Tactility/Include/Tactility/service/wifi/Wifi.h b/Tactility/Include/Tactility/service/wifi/Wifi.h index a51e4f72f..5a23a8af5 100644 --- a/Tactility/Include/Tactility/service/wifi/Wifi.h +++ b/Tactility/Include/Tactility/service/wifi/Wifi.h @@ -26,12 +26,6 @@ enum class RadioState { Off, }; -/** - * @brief Get wifi pubsub that broadcasts Event objects - * @return PubSub - */ -std::shared_ptr> getPubsub(); - /** @return Get the current radio state */ RadioState getRadioState(); diff --git a/Tactility/Private/Tactility/app/chat/ChatAppPrivate.h b/Tactility/Private/Tactility/app/chat/ChatAppPrivate.h index 7abd67cc3..d5eece322 100644 --- a/Tactility/Private/Tactility/app/chat/ChatAppPrivate.h +++ b/Tactility/Private/Tactility/app/chat/ChatAppPrivate.h @@ -24,7 +24,6 @@ struct Context { uint32_t appInstanceId; ChatState state; ChatView view = ChatView(this, &state); - service::espnow::ReceiverSubscription receiveSubscription = -1; ChatSettingsData settings; bool isFirstLaunch = false; }; diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 7d8041ac0..74f151409 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -141,7 +141,6 @@ namespace app { namespace boot { extern const ::AppManifest manifest; } namespace development { extern const ::AppManifest manifest; } namespace display { extern const ::AppManifest manifest; } - namespace kerneldisplay { extern const ::AppManifest manifest; } namespace files { extern const ::AppManifest manifest; } namespace fileselection { extern const ::AppManifest manifest; } namespace gpssettings { extern const ::AppManifest manifest; } @@ -206,7 +205,7 @@ static void registerInternalApps() { app_manager_add(&app::audiosettings::manifest); } if (device_exists_of_type(&DISPLAY_TYPE)) { - app_manager_add(&app::kerneldisplay::manifest); + app_manager_add(&app::display::manifest); } app_manager_add(&app::files::manifest); app_manager_add(&app::fileselection::manifest); @@ -519,7 +518,6 @@ void run(Module* const dtsModules[], const DtsDevice dtsDevices[]) { .on_start = onLvglStarted, .on_stop = onLvglStopped, .task_priority = THREAD_PRIORITY_HIGHER, - // TODO: Remove Wi-Fi driver callback mechanism and use subscribe/await from wifi app to be able to reduce callstack .task_stack_size = 9120, #ifdef ESP_PLATFORM .task_affinity = getCpuAffinityConfiguration().graphics diff --git a/Tactility/Source/app/addgps/AddGps.cpp b/Tactility/Source/app/addgps/AddGps.cpp index 1f1aa5462..03448dd9f 100644 --- a/Tactility/Source/app/addgps/AddGps.cpp +++ b/Tactility/Source/app/addgps/AddGps.cpp @@ -5,11 +5,13 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -199,36 +201,42 @@ void createWidgets(lv_obj_t* parent, void* userData) { // endregion } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - case APP_EVENT_RESULT: - app_manager_stop(event.result.launch_id); - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + case APP_EVENT_RESULT: + app_manager_stop(event.result.launch_id); + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/alertdialog/AlertDialog.cpp b/Tactility/Source/app/alertdialog/AlertDialog.cpp index ce9b8a3a3..88c31a4cd 100644 --- a/Tactility/Source/app/alertdialog/AlertDialog.cpp +++ b/Tactility/Source/app/alertdialog/AlertDialog.cpp @@ -3,9 +3,11 @@ #include #include #include +#include #include +#include #include #include @@ -97,29 +99,37 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx { appInstanceId }; ctx.argc = argc; ctx.argv = argv; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool shouldClose = false; AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - if (event.type == APP_EVENT_CLOSE) { - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } } + if (shouldClose) break; } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return ctx.result; } diff --git a/Tactility/Source/app/appdetails/AppDetails.cpp b/Tactility/Source/app/appdetails/AppDetails.cpp index 562ae8433..c919d1305 100644 --- a/Tactility/Source/app/appdetails/AppDetails.cpp +++ b/Tactility/Source/app/appdetails/AppDetails.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include @@ -15,6 +16,7 @@ #include #include +#include #include constexpr auto* TAG = "AppDetails"; @@ -104,7 +106,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; @@ -114,38 +117,43 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { return 0; } + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - case APP_EVENT_RESULT: - if (event.result.launch_id == ctx.pendingUninstallDialogId) { - if (event.result.result == 0) { // 0 = Yes - app_uninstall(ctx.targetManifest.id); - shouldClose = true; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + case APP_EVENT_RESULT: + if (event.result.launch_id == ctx.pendingUninstallDialogId) { + if (event.result.result == 0) { // 0 = Yes + app_uninstall(ctx.targetManifest.id); + shouldClose = true; + } + app_manager_stop(event.result.launch_id); } - app_manager_stop(event.result.launch_id); - } - break; - default: - break; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/apphub/AppHubApp.cpp b/Tactility/Source/app/apphub/AppHubApp.cpp index d3cf6c6e1..5f1910f43 100644 --- a/Tactility/Source/app/apphub/AppHubApp.cpp +++ b/Tactility/Source/app/apphub/AppHubApp.cpp @@ -10,9 +10,11 @@ #include #include #include +#include #include +#include #include #include @@ -49,7 +51,7 @@ void refresh(Context* ctx); void onBackPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); - AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; + AppEvent closeEvent {.type = APP_EVENT_CLOSE, .timestamp = 0, .result = {}}; app_event_emit(ctx->appInstanceId, &closeEvent); } @@ -197,33 +199,39 @@ void destroyWidgets(void* userData) { ctx->refreshButton = nullptr; } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } @@ -234,7 +242,7 @@ extern const ::AppManifest manifest = { .id = "tactility.apphub", .name = "App Hub", .category = APP_CATEGORY_SYSTEM, - .location = { APP_LOCATION_MEMORY, reinterpret_cast(appMain) } + .location = {APP_LOCATION_MEMORY, reinterpret_cast(appMain)} }; -} // namespace +} // namespace tt::app::apphub diff --git a/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp b/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp index 4cb0bf823..495281228 100644 --- a/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp +++ b/Tactility/Source/app/apphubdetails/AppHubDetailsApp.cpp @@ -11,12 +11,14 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -211,7 +213,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { updateViews(ctx); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); // argv layout: [0]=appId, [1]=appVersionName, [2]=appVersionCode, [3]=appName, // [4]=appDescription, [5]=targetSdk, [6]=file, [7..argc)=targetPlatforms. @@ -230,41 +233,46 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { } } + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - case APP_EVENT_RESULT: { - bool confirmed = event.result.result == CONFIRMATION_BUTTON_INDEX; - if (event.result.launch_id == ctx.installDialogId && confirmed) { - installApp(&ctx); - } else if (event.result.launch_id == ctx.uninstallDialogId && confirmed) { - uninstallApp(&ctx); - } else if (event.result.launch_id == ctx.updateDialogId && confirmed) { - updateApp(&ctx); + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + case APP_EVENT_RESULT: { + bool confirmed = event.result.result == CONFIRMATION_BUTTON_INDEX; + if (event.result.launch_id == ctx.installDialogId && confirmed) { + installApp(&ctx); + } else if (event.result.launch_id == ctx.uninstallDialogId && confirmed) { + uninstallApp(&ctx); + } else if (event.result.launch_id == ctx.updateDialogId && confirmed) { + updateApp(&ctx); + } + app_manager_stop(event.result.launch_id); + break; } - app_manager_stop(event.result.launch_id); - break; + default: + break; } - default: - break; + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/applist/AppList.cpp b/Tactility/Source/app/applist/AppList.cpp index 119992651..cdab48a42 100644 --- a/Tactility/Source/app/applist/AppList.cpp +++ b/Tactility/Source/app/applist/AppList.cpp @@ -1,9 +1,12 @@ #include #include #include +#include #include +#include + #include #include #include @@ -78,27 +81,35 @@ void createWidgets(lv_obj_t* parent, void*) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); appListInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr); while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool shouldClose = false; AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - if (event.type == APP_EVENT_CLOSE) { - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } } + if (shouldClose) break; } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/appsettings/AppSettings.cpp b/Tactility/Source/app/appsettings/AppSettings.cpp index 6a57b3cbc..7bfc21553 100644 --- a/Tactility/Source/app/appsettings/AppSettings.cpp +++ b/Tactility/Source/app/appsettings/AppSettings.cpp @@ -6,9 +6,12 @@ #include #include #include +#include #include +#include + #include #include #include @@ -87,32 +90,38 @@ void createWidgets(lv_obj_t* parent, void*) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); appSettingsInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/apwebserver/ApWebServer.cpp b/Tactility/Source/app/apwebserver/ApWebServer.cpp index d50c11c5d..8ede703dd 100644 --- a/Tactility/Source/app/apwebserver/ApWebServer.cpp +++ b/Tactility/Source/app/apwebserver/ApWebServer.cpp @@ -7,11 +7,13 @@ #include #include #include +#include #include #include #include +#include #include namespace tt::app::apwebserver { @@ -121,29 +123,34 @@ void createWidgets(lv_obj_t* parent, void* userData) { ctx->webServerEnabledChanged = true; } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; ctx.wsSettings = settings::webserver::loadOrGetDefault(); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } @@ -167,7 +174,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { }); window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/audiosettings/AudioSettings.cpp b/Tactility/Source/app/audiosettings/AudioSettings.cpp index 4bab6d63c..0d37cd25e 100644 --- a/Tactility/Source/app/audiosettings/AudioSettings.cpp +++ b/Tactility/Source/app/audiosettings/AudioSettings.cpp @@ -5,9 +5,12 @@ #include #include #include +#include #include +#include + #include #include #include @@ -211,28 +214,33 @@ void createWidgets(lv_obj_t* parent, void* userData) { }); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } @@ -242,7 +250,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/boot/Boot.cpp b/Tactility/Source/app/boot/Boot.cpp index 4b5631494..33381c3a7 100644 --- a/Tactility/Source/app/boot/Boot.cpp +++ b/Tactility/Source/app/boot/Boot.cpp @@ -1,5 +1,6 @@ #include "tactility/system_event.h" +#include #include #include #include @@ -9,6 +10,7 @@ #include #include #include +#include #include @@ -284,7 +286,8 @@ void runBootSequence(TickType_t startTime) { system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); bootAppInstanceId = appInstanceId; const auto start_time = get_ticks(); @@ -292,9 +295,11 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { isUsbBootSplash = hal::usb::isUsbBootMode(); sdCardMissing = false; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); bootWindowId = window_manager_create(appInstanceId, createSplashWidgets, nullptr); @@ -304,19 +309,24 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { // startNextApp() above is what triggers that, via app-module's "save the previously active // app" policy, unless sdCardMissing halted before it. while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool shouldClose = false; AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - if (event.type == APP_EVENT_CLOSE) { - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } } + if (shouldClose) break; } if (bootWindowId != 0) { window_manager_remove(bootWindowId); } - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/btmanage/BtManage.cpp b/Tactility/Source/app/btmanage/BtManage.cpp index f561038ea..8aae722db 100644 --- a/Tactility/Source/app/btmanage/BtManage.cpp +++ b/Tactility/Source/app/btmanage/BtManage.cpp @@ -8,9 +8,11 @@ #include #include #include +#include #include +#include #include namespace tt::app::btmanage { @@ -199,7 +201,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { ctx->unlock(); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx; ctx.appInstanceId = appInstanceId; ctx.bindings = (Bindings) { @@ -220,9 +223,11 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { ctx.state.updateScanResults(); ctx.state.updatePairedPeers(); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); @@ -243,16 +248,18 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } @@ -270,7 +277,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp b/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp index 24a47449c..75b8a3bb8 100644 --- a/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp +++ b/Tactility/Source/app/btpeersettings/BtPeerSettings.cpp @@ -11,9 +11,11 @@ #include #include #include +#include #include +#include #include #include #include @@ -178,7 +180,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { updateViews(ctx); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; @@ -199,38 +202,42 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { device_put(btDevice); } + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - case APP_EVENT_RESULT: - if (event.result.result == 0) { // 0 = Yes - if (isCurrentlyConnected(&ctx)) { - if (ctx.profileId == BT_PROFILE_HID_HOST) { - bluetooth::hidHostDisconnect(); - } else { - bluetooth::disconnect(ctx.addr, ctx.profileId); + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + case APP_EVENT_RESULT: + if (event.result.result == 0) { // 0 = Yes + if (isCurrentlyConnected(&ctx)) { + if (ctx.profileId == BT_PROFILE_HID_HOST) { + bluetooth::hidHostDisconnect(); + } else { + bluetooth::disconnect(ctx.addr, ctx.profileId); + } } + bluetooth::unpair(ctx.addr); + shouldClose = true; } - bluetooth::unpair(ctx.addr); - shouldClose = true; - } - app_manager_stop(event.result.launch_id); - break; - default: - break; + app_manager_stop(event.result.launch_id); + break; + default: + break; + } + if (shouldClose) break; } } @@ -240,7 +247,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/chat/ChatApp.cpp b/Tactility/Source/app/chat/ChatApp.cpp index 9c1800654..3556abc47 100644 --- a/Tactility/Source/app/chat/ChatApp.cpp +++ b/Tactility/Source/app/chat/ChatApp.cpp @@ -10,9 +10,11 @@ #include #include #include +#include #include +#include #include #include @@ -167,7 +169,8 @@ void switchChannel(Context* ctx, const std::string& chatChannel) { namespace { -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; ctx.isFirstLaunch = !settingsFileExists(); @@ -178,40 +181,44 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { } enableEspNow(&ctx); - ctx.receiveSubscription = service::espnow::subscribeReceiver( - [&ctx](const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length) { - onReceive(&ctx, receiveInfo, data, length); - } - ); - + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); + auto espnow_subscription = service::espnow::subscribeReceiver( + [&ctx](const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length) { + onReceive(&ctx, receiveInfo, data, length); + } + ); + bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } - window_manager_remove(window); - app_event_unsubscribe(&sub); - - service::espnow::unsubscribeReceiver(ctx.receiveSubscription); + service::espnow::unsubscribeReceiver(espnow_subscription); disableEspNow(&ctx); + window_manager_remove(window); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); + return 0; } diff --git a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp index 0e681b4a8..bd73cfc57 100644 --- a/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp +++ b/Tactility/Source/app/crashdiagnostics/CrashDiagnostics.cpp @@ -8,11 +8,13 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -149,35 +151,41 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); if (!ctx.hasFatalError) { bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); bool continuePressed = ctx.continuePressed; diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp index 892988eec..6f69dc677 100644 --- a/Tactility/Source/app/development/Development.cpp +++ b/Tactility/Source/app/development/Development.cpp @@ -10,9 +10,11 @@ #include #include #include +#include #include +#include #include #include @@ -162,7 +164,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { updateViewState(ctx); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; ctx.service = service::development::findService(); @@ -174,9 +177,11 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { return 0; } + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); @@ -197,16 +202,18 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } @@ -216,7 +223,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { lvgl_unlock(); window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/kerneldisplay/KernelDisplay.cpp b/Tactility/Source/app/display/Display.cpp similarity index 93% rename from Tactility/Source/app/kerneldisplay/KernelDisplay.cpp rename to Tactility/Source/app/display/Display.cpp index e3e8f3662..51fb52bbb 100644 --- a/Tactility/Source/app/kerneldisplay/KernelDisplay.cpp +++ b/Tactility/Source/app/display/Display.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -12,8 +13,8 @@ #include #include -#include #include +#include #include @@ -25,11 +26,11 @@ #include #endif -namespace tt::app::kerneldisplay { +namespace tt::app::display { extern const ::AppManifest manifest; -constexpr auto* TAG = "KernelDisplay"; +constexpr auto* TAG = "Display"; namespace { @@ -304,34 +305,40 @@ void persistIfUpdated(Context& ctx) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - persistIfUpdated(ctx); - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + persistIfUpdated(ctx); + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } @@ -345,4 +352,4 @@ extern const ::AppManifest manifest = { .location = { APP_LOCATION_MEMORY, reinterpret_cast(appMain) } }; -} // namespace tt::app::kerneldisplay +} // namespace tt::app::display diff --git a/Tactility/Source/app/files/FilesApp.cpp b/Tactility/Source/app/files/FilesApp.cpp index defc2cf61..c08d48423 100644 --- a/Tactility/Source/app/files/FilesApp.cpp +++ b/Tactility/Source/app/files/FilesApp.cpp @@ -4,9 +4,12 @@ #include #include #include +#include #include +#include + #include namespace tt::app::files { @@ -25,39 +28,45 @@ void createWidgets(lv_obj_t* parent, void* userData) { ctx->view->init(ctx->appInstanceId, parent); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); auto state = std::make_shared(); View view(state); CreateContext createContext { &view, appInstanceId }; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &createContext); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - case APP_EVENT_RESULT: - view.onResult(event.result.launch_id, event.result.result); - app_manager_stop(event.result.launch_id); - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + case APP_EVENT_RESULT: + view.onResult(event.result.launch_id, event.result.result); + app_manager_stop(event.result.launch_id); + break; + default: + break; + } + if (shouldClose) break; } } view.deinit(); window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/fileselection/FileSelection.cpp b/Tactility/Source/app/fileselection/FileSelection.cpp index 7c1e5796b..96f4f31bf 100644 --- a/Tactility/Source/app/fileselection/FileSelection.cpp +++ b/Tactility/Source/app/fileselection/FileSelection.cpp @@ -5,9 +5,12 @@ #include #include #include +#include #include +#include + #include #include @@ -43,7 +46,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { ctx->view->init(parent, ctx->mode); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); // argv layout: [0]="existing" or "existing_or_new". Context ctx {}; @@ -62,24 +66,31 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { app_event_emit(appInstanceId, &closeEvent); }); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool shouldClose = false; AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - if (event.type == APP_EVENT_CLOSE) { - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } } + if (shouldClose) break; } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return ctx.result; } diff --git a/Tactility/Source/app/gpssettings/GpsSettings.cpp b/Tactility/Source/app/gpssettings/GpsSettings.cpp index 863346fda..8bb66e2b1 100644 --- a/Tactility/Source/app/gpssettings/GpsSettings.cpp +++ b/Tactility/Source/app/gpssettings/GpsSettings.cpp @@ -9,9 +9,11 @@ #include #include #include +#include #include +#include #include #include @@ -257,7 +259,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { updateDeviceStates(ctx); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; @@ -267,51 +270,56 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { updateDeviceStates(&ctx); }); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); ctx.timer->start(); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - case APP_EVENT_RESULT: - if (ctx.hasPendingDelete) { - ctx.hasPendingDelete = false; - if (event.result.result == 0) { // 0 = Yes - lvgl_lock(); - std::erase_if(ctx.deviceRows, [&ctx](const DeviceRow& row) { - return row.device == ctx.pendingDeleteDevice; - }); - lvgl_unlock(); - - gps_settings_remove_configuration_at(ctx.pendingDeleteIndex); - ctx.pendingDeleteDevice = nullptr; - - lvgl_lock(); - rebuildDeviceList(&ctx); - lvgl_unlock(); + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + case APP_EVENT_RESULT: + if (ctx.hasPendingDelete) { + ctx.hasPendingDelete = false; + if (event.result.result == 0) { // 0 = Yes + lvgl_lock(); + std::erase_if(ctx.deviceRows, [&ctx](const DeviceRow& row) { + return row.device == ctx.pendingDeleteDevice; + }); + lvgl_unlock(); + + gps_settings_remove_configuration_at(ctx.pendingDeleteIndex); + ctx.pendingDeleteDevice = nullptr; + + lvgl_lock(); + rebuildDeviceList(&ctx); + lvgl_unlock(); + } } - } - app_manager_stop(event.result.launch_id); - break; - default: - break; + app_manager_stop(event.result.launch_id); + break; + default: + break; + } + if (shouldClose) break; } } ctx.timer->stop(); window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/grovesettings/GroveSettings.cpp b/Tactility/Source/app/grovesettings/GroveSettings.cpp index 5d04a7593..846143571 100644 --- a/Tactility/Source/app/grovesettings/GroveSettings.cpp +++ b/Tactility/Source/app/grovesettings/GroveSettings.cpp @@ -2,12 +2,14 @@ #include +#include #include #include #include #include #include +#include #include @@ -89,33 +91,39 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/i2cscanner/I2cScanner.cpp b/Tactility/Source/app/i2cscanner/I2cScanner.cpp index af2116e6d..7f7f76b17 100644 --- a/Tactility/Source/app/i2cscanner/I2cScanner.cpp +++ b/Tactility/Source/app/i2cscanner/I2cScanner.cpp @@ -9,9 +9,11 @@ #include #include #include +#include #include +#include #include #include #include @@ -384,34 +386,40 @@ void stopScanningIfRunning(Context* ctx) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - stopScanningIfRunning(&ctx); - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + stopScanningIfRunning(&ctx); + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/imageviewer/ImageViewer.cpp b/Tactility/Source/app/imageviewer/ImageViewer.cpp index e69a48be7..bf47bda6d 100644 --- a/Tactility/Source/app/imageviewer/ImageViewer.cpp +++ b/Tactility/Source/app/imageviewer/ImageViewer.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -82,36 +83,42 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); check(argc > 0, "Parameters not set"); Context ctx {}; ctx.appInstanceId = appInstanceId; ctx.filePath = argv[0]; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/inputdialog/InputDialog.cpp b/Tactility/Source/app/inputdialog/InputDialog.cpp index ae5a9ba90..459dc0996 100644 --- a/Tactility/Source/app/inputdialog/InputDialog.cpp +++ b/Tactility/Source/app/inputdialog/InputDialog.cpp @@ -3,10 +3,12 @@ #include #include #include +#include #include #include +#include #include #include @@ -106,29 +108,37 @@ void createWidgets(lv_obj_t* parent, void* userData) { createButton(ctx, button_wrapper, "Cancel", nullptr); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx { appInstanceId }; ctx.argc = argc; ctx.argv = argv; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool shouldClose = false; AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - if (event.type == APP_EVENT_CLOSE) { - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } } + if (shouldClose) break; } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return ctx.result; } diff --git a/Tactility/Source/app/keyboard/KeyboardSettings.cpp b/Tactility/Source/app/keyboard/KeyboardSettings.cpp index 5f57f5c18..8738bbbf6 100644 --- a/Tactility/Source/app/keyboard/KeyboardSettings.cpp +++ b/Tactility/Source/app/keyboard/KeyboardSettings.cpp @@ -7,9 +7,11 @@ #include #include #include +#include #include +#include #include #include @@ -205,34 +207,40 @@ void persistIfUpdated(Context& ctx) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - persistIfUpdated(ctx); - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + persistIfUpdated(ctx); + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/launcher/Launcher.cpp b/Tactility/Source/app/launcher/Launcher.cpp index 4e4a9f3c4..995538033 100644 --- a/Tactility/Source/app/launcher/Launcher.cpp +++ b/Tactility/Source/app/launcher/Launcher.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -14,6 +15,7 @@ #include +#include #include #include #include @@ -231,29 +233,37 @@ void runAutoStart() { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); runAutoStart(); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr); // The launcher is meant to stay resident (it's the home screen) - it only gives up its // thread when app-module's scheduler asks it to (e.g. another new-model app is started). while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool shouldClose = false; AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - if (event.type == APP_EVENT_CLOSE) { - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } } + if (shouldClose) break; } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/localesettings/LocaleSettings.cpp b/Tactility/Source/app/localesettings/LocaleSettings.cpp index 013e0d38c..186eee5a0 100644 --- a/Tactility/Source/app/localesettings/LocaleSettings.cpp +++ b/Tactility/Source/app/localesettings/LocaleSettings.cpp @@ -9,9 +9,12 @@ #include #include #include +#include #include +#include + #include #include @@ -141,33 +144,39 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_add_event_cb(ctx->languageDropdown, onLanguageSet, LV_EVENT_VALUE_CHANGED, ctx); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/notes/Notes.cpp b/Tactility/Source/app/notes/Notes.cpp index 8e71019b0..94d6b5d2c 100644 --- a/Tactility/Source/app/notes/Notes.cpp +++ b/Tactility/Source/app/notes/Notes.cpp @@ -5,12 +5,14 @@ #include #include #include +#include #include #include #include #include +#include #include namespace tt::app::notes { @@ -182,7 +184,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; @@ -190,51 +193,56 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { ctx.filePath = argv[0]; } + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - case APP_EVENT_RESULT: - LOG_I(TAG, "Result for launch id %u", event.result.launch_id); - if (event.result.launch_id == ctx.loadFileLaunchId) { - ctx.loadFileLaunchId = 0; - if (event.result.result == 0 /* Ok */) { - auto path = fileselection::getLastPath(); - if (!path.empty()) { - openFile(&ctx, path); + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + case APP_EVENT_RESULT: + LOG_I(TAG, "Result for launch id %u", event.result.launch_id); + if (event.result.launch_id == ctx.loadFileLaunchId) { + ctx.loadFileLaunchId = 0; + if (event.result.result == 0 /* Ok */) { + auto path = fileselection::getLastPath(); + if (!path.empty()) { + openFile(&ctx, path); + } } - } - } else if (event.result.launch_id == ctx.saveFileLaunchId) { - ctx.saveFileLaunchId = 0; - if (event.result.result == 0 /* Ok */) { - auto path = fileselection::getLastPath(); - // Must re-open file, because the UI was cleared after opening the dialog. - if (!path.empty() && saveFile(&ctx, path)) { - openFile(&ctx, path); + } else if (event.result.launch_id == ctx.saveFileLaunchId) { + ctx.saveFileLaunchId = 0; + if (event.result.result == 0 /* Ok */) { + auto path = fileselection::getLastPath(); + // Must re-open file, because the UI was cleared after opening the dialog. + if (!path.empty() && saveFile(&ctx, path)) { + openFile(&ctx, path); + } } } - } - app_manager_stop(event.result.launch_id); - break; - default: - break; + app_manager_stop(event.result.launch_id); + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/power/Power.cpp b/Tactility/Source/app/power/Power.cpp index 93f378ddf..6b1e28af5 100644 --- a/Tactility/Source/app/power/Power.cpp +++ b/Tactility/Source/app/power/Power.cpp @@ -4,9 +4,11 @@ #include #include #include +#include #include +#include #include #include #include @@ -229,7 +231,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; @@ -240,31 +243,36 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { updateUi(&ctx); }); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); ctx.timer->start(); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } ctx.timer->stop(); window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/poweroff/PowerOff.cpp b/Tactility/Source/app/poweroff/PowerOff.cpp index edc638af3..81ec9451c 100644 --- a/Tactility/Source/app/poweroff/PowerOff.cpp +++ b/Tactility/Source/app/poweroff/PowerOff.cpp @@ -4,11 +4,13 @@ #include #include #include +#include #include #include #include +#include #include #include @@ -132,22 +134,25 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_obj_add_event_cb(no_button, onNoPressed, LV_EVENT_SHORT_CLICKED, ctx); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } + while (app_event_poll(&sub, &event) == ERROR_NONE) { switch (event.type) { case APP_EVENT_CLOSE: shouldClose = true; @@ -155,10 +160,13 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { default: break; } + if (shouldClose) break; + } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/screenshot/Screenshot.cpp b/Tactility/Source/app/screenshot/Screenshot.cpp index 86ecccc70..93fe098f0 100644 --- a/Tactility/Source/app/screenshot/Screenshot.cpp +++ b/Tactility/Source/app/screenshot/Screenshot.cpp @@ -12,9 +12,11 @@ #include #include #include +#include #include +#include #include #include @@ -240,7 +242,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; ctx.updateTimer = std::make_unique(Timer::Type::Periodic, 500 / portTICK_PERIOD_MS, [&ctx] { @@ -250,24 +253,28 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { } }); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } @@ -276,7 +283,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp index 801783c37..c0b5d7b50 100644 --- a/Tactility/Source/app/selectiondialog/SelectionDialog.cpp +++ b/Tactility/Source/app/selectiondialog/SelectionDialog.cpp @@ -3,9 +3,11 @@ #include #include #include +#include #include +#include #include #include @@ -99,29 +101,37 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } -int32_t appMain(AppInstanceId appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + AppInstanceId appInstanceId = app_scheduler_current_app_id(); Context ctx { appInstanceId }; ctx.argc = argc; ctx.argv = argv; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool shouldClose = false; AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - if (event.type == APP_EVENT_CLOSE) { - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } } + if (shouldClose) break; } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return ctx.result; } diff --git a/Tactility/Source/app/settings/Settings.cpp b/Tactility/Source/app/settings/Settings.cpp index f2623a52d..22ea8513c 100644 --- a/Tactility/Source/app/settings/Settings.cpp +++ b/Tactility/Source/app/settings/Settings.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include @@ -76,27 +77,35 @@ void createWidgets(lv_obj_t* parent, void*) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); settingsInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, nullptr); while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool shouldClose = false; AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - if (event.type == APP_EVENT_CLOSE) { - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } } + if (shouldClose) break; } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/setup/Setup.cpp b/Tactility/Source/app/setup/Setup.cpp index 66551182e..6382df639 100644 --- a/Tactility/Source/app/setup/Setup.cpp +++ b/Tactility/Source/app/setup/Setup.cpp @@ -11,9 +11,11 @@ #include #include #include +#include #include +#include #include #include @@ -159,7 +161,7 @@ void onContinueClicked(lv_event_t* event) { markCompleted(); // Async, non-blocking - must NOT call app_manager_stop() directly here: this // callback runs ON the LVGL task, and app-lifecycle transitions must happen on this - // app's own thread (woken via app_event_await()), which closes by returning. + // app's own thread (woken via app_event_poll()), which closes by returning. AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; app_event_emit(ctx->appInstanceId, &closeEvent); break; @@ -203,7 +205,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { renderCurrent(ctx); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; ctx.steps = { @@ -229,36 +232,41 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { } }; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - case APP_EVENT_RESULT: - if (event.result.launch_id == ctx.pendingStepDialogId) { - ctx.pendingStepDialogId = 0; - advanceTo(&ctx, ctx.stepIndex + 1); - } - app_manager_stop(event.result.launch_id); - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + case APP_EVENT_RESULT: + if (event.result.launch_id == ctx.pendingStepDialogId) { + ctx.pendingStepDialogId = 0; + advanceTo(&ctx, ctx.stepIndex + 1); + } + app_manager_stop(event.result.launch_id); + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index 24c8ca021..912b261f6 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -1,3 +1,4 @@ +#include #include "tactility/time.h" #include @@ -8,6 +9,7 @@ #include #include #include +#include #include @@ -399,7 +401,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { updateTasks(ctx); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; @@ -417,9 +420,11 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { lvgl_unlock(); }); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); ctx.memoryTimer->start(); // Memory: every 10s @@ -427,23 +432,26 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } ctx.memoryTimer->stop(); ctx.tasksTimer->stop(); window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index 9d22f7a27..e706460b5 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -6,9 +6,11 @@ #include #include #include +#include #include +#include #include #include @@ -153,48 +155,54 @@ void createWidgets(lv_obj_t* parent, void* userData) { lv_label_set_text(ctx->timeZoneLabel, timeZoneName.c_str()); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - case APP_EVENT_RESULT: - if (event.result.launch_id == ctx.pendingTimeZoneDialogId) { - ctx.pendingTimeZoneDialogId = 0; - if (event.result.result == 0 /* Ok */) { - const auto name = timezone::getLastName(); - LOG_I(TAG, "Result name=%s code=%s", name.c_str(), timezone::getLastCode().c_str()); - if (!name.empty()) { - lvgl_lock(); - lv_label_set_text(ctx.timeZoneLabel, name.c_str()); - lvgl_unlock(); + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + case APP_EVENT_RESULT: + if (event.result.launch_id == ctx.pendingTimeZoneDialogId) { + ctx.pendingTimeZoneDialogId = 0; + if (event.result.result == 0 /* Ok */) { + const auto name = timezone::getLastName(); + LOG_I(TAG, "Result name=%s code=%s", name.c_str(), timezone::getLastCode().c_str()); + if (!name.empty()) { + lvgl_lock(); + lv_label_set_text(ctx.timeZoneLabel, name.c_str()); + lvgl_unlock(); + } } } - } - app_manager_stop(event.result.launch_id); - break; - default: - break; + app_manager_stop(event.result.launch_id); + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/timezone/TimeZone.cpp b/Tactility/Source/app/timezone/TimeZone.cpp index c745d1482..77318c578 100644 --- a/Tactility/Source/app/timezone/TimeZone.cpp +++ b/Tactility/Source/app/timezone/TimeZone.cpp @@ -9,9 +9,11 @@ #include #include #include +#include #include +#include #include #include @@ -229,7 +231,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { ctx->listWidget = list; } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); // argv layout: [0]="1"/"0" (saveTimeZone). Context ctx; @@ -240,26 +243,33 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { updateList(&ctx); }); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); ctx.updateTimer->start(); while (true) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + + bool shouldClose = false; AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - if (event.type == APP_EVENT_CLOSE) { - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + shouldClose = true; + break; + } } + if (shouldClose) break; } ctx.updateTimer->stop(); window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return ctx.result; } diff --git a/Tactility/Source/app/touchcalibration/TouchCalibration.cpp b/Tactility/Source/app/touchcalibration/TouchCalibration.cpp index e799d3297..dd0f0047d 100644 --- a/Tactility/Source/app/touchcalibration/TouchCalibration.cpp +++ b/Tactility/Source/app/touchcalibration/TouchCalibration.cpp @@ -8,9 +8,11 @@ #include #include #include +#include #include +#include #include #include #include @@ -174,7 +176,7 @@ void onPress(lv_event_t* event) { // Async, non-blocking - must NOT call app_manager_stop() directly here: this callback runs // ON the LVGL task, and app-lifecycle transitions must happen on this app's own thread - // (woken up via app_event_await() below), which closes by returning. The result (Ok/Error) + // (woken up via app_event_poll() below), which closes by returning. The result (Ok/Error) // is reported by appMain() itself when it returns, based on ctx.calibrationApplied. AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; app_event_emit(ctx->appInstanceId, &closeEvent); @@ -221,7 +223,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { updateUi(ctx); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; @@ -233,29 +236,34 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { } lvgl_unlock(); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); // finishCalibration() already applied a new calibration on success. On cancel/failure, // restore whatever calibration was on disk before the block above cleared it. diff --git a/Tactility/Source/app/trackball/TrackballSettings.cpp b/Tactility/Source/app/trackball/TrackballSettings.cpp index bd2d9e326..90ef53bb9 100644 --- a/Tactility/Source/app/trackball/TrackballSettings.cpp +++ b/Tactility/Source/app/trackball/TrackballSettings.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #include @@ -268,34 +270,40 @@ void persistIfUpdated(Context& ctx) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - persistIfUpdated(ctx); - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + persistIfUpdated(ctx); + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/usbsettings/UsbSettings.cpp b/Tactility/Source/app/usbsettings/UsbSettings.cpp index 865fecc58..c517a314a 100644 --- a/Tactility/Source/app/usbsettings/UsbSettings.cpp +++ b/Tactility/Source/app/usbsettings/UsbSettings.cpp @@ -3,9 +3,12 @@ #include #include #include +#include #include +#include + #include #include @@ -81,33 +84,39 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/webserversettings/WebServerSettings.cpp b/Tactility/Source/app/webserversettings/WebServerSettings.cpp index b14afad6e..c5efd2583 100644 --- a/Tactility/Source/app/webserversettings/WebServerSettings.cpp +++ b/Tactility/Source/app/webserversettings/WebServerSettings.cpp @@ -8,9 +8,11 @@ #include #include #include +#include #include +#include #include #include @@ -354,7 +356,8 @@ void createWidgets(lv_obj_t* parent, void* userData) { "AP mode uses the password configured above."); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; ctx.wsSettings = settings::webserver::loadOrGetDefault(); @@ -362,24 +365,28 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { ctx.wsSettings.webServerEnabled = service::webserver::isWebServerEnabled(); ctx.originalSettings = ctx.wsSettings; + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + task_event_group_wait_any(&event_group, nullptr, portMAX_DELAY); + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } } @@ -422,7 +429,8 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp index 6b8b02831..13cc95ad0 100644 --- a/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp +++ b/Tactility/Source/app/wifiapsettings/WifiApSettings.cpp @@ -6,12 +6,15 @@ #include #include #include +#include #include #include #include +#include +#include #include namespace tt::app::wifiapsettings { @@ -29,9 +32,14 @@ struct Context { lv_obj_t* busySpinner = nullptr; lv_obj_t* connectButton = nullptr; lv_obj_t* disconnectButton = nullptr; + lv_obj_t* autoConnectSwitch = nullptr; + lv_obj_t* forgetButton = nullptr; + lv_obj_t* autoConnectWrapper = nullptr; uint32_t forgetDialogId = 0; - PubSub::SubscriptionHandle wifiSubscription = nullptr; + // Set once in appMain() before subscribing, left null if this device has no WiFi driver + Device* wifiDevice = nullptr; + WifiEventSubscription wifiEventSub {}; }; @@ -41,8 +49,7 @@ void onBackPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); // Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits // (thread_join) for this app's own thread to finish, which needs the LVGL lock - // (window_manager_remove()) - but this callback runs ON the LVGL task, which would - // deadlock against itself. + // but this callback runs ON the LVGL task, which would deadlock against itself. AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; app_event_emit(ctx->appInstanceId, &closeEvent); } @@ -104,13 +111,34 @@ void updateBusySpinner(Context* ctx) { } } +// Touches the filesystem (service::wifi::settings::load()) - callers must not run this on the +// LVGL task (see updateViews()'s callers). +void updateAutoConnectSection(Context* ctx) { + service::wifi::settings::WifiApSettings settings; + if (service::wifi::settings::load(ctx->ssid.c_str(), settings)) { + if (settings.autoConnect) { + lv_obj_add_state(ctx->autoConnectSwitch, LV_STATE_CHECKED); + } else { + lv_obj_remove_state(ctx->autoConnectSwitch, LV_STATE_CHECKED); + } + lv_obj_remove_flag(ctx->forgetButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(ctx->autoConnectWrapper, LV_OBJ_FLAG_HIDDEN); + } else { + LOG_W(TAG, "No settings found"); + lv_obj_add_flag(ctx->forgetButton, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(ctx->autoConnectWrapper, LV_OBJ_FLAG_HIDDEN); + } +} + +// Runs on whichever thread calls it, not necessarily the LVGL task void updateViews(Context* ctx) { if (ctx->connectButton == nullptr) { - // Buried (e.g. the forget confirmation dialog opened on top) - see destroyWidgets(). + // Buried (e.g. the forget confirmation dialog opened on top) return; } updateConnectButton(ctx); updateBusySpinner(ctx); + updateAutoConnectSection(ctx); } void requestViewUpdate(Context* ctx) { @@ -119,13 +147,14 @@ void requestViewUpdate(Context* ctx) { lvgl_unlock(); } -// Runs with the LVGL lock already held, possibly on another app's thread - see -// WindowDestroyWidgetsFn's warnings. Must stay lock-free: only nulls pointers. void destroyWidgets(void* userData) { auto* ctx = static_cast(userData); ctx->busySpinner = nullptr; ctx->connectButton = nullptr; ctx->disconnectButton = nullptr; + ctx->autoConnectSwitch = nullptr; + ctx->forgetButton = nullptr; + ctx->autoConnectWrapper = nullptr; } void createWidgets(lv_obj_t* parent, void* userData) { @@ -162,101 +191,127 @@ void createWidgets(lv_obj_t* parent, void* userData) { // Forget - auto* forget_button = lv_button_create(wrapper); - lv_obj_set_width(forget_button, LV_PCT(100)); - lv_obj_add_event_cb(forget_button, onPressForget, LV_EVENT_SHORT_CLICKED, ctx); - auto* forget_button_label = lv_label_create(forget_button); + ctx->forgetButton = lv_button_create(wrapper); + lv_obj_set_width(ctx->forgetButton, LV_PCT(100)); + lv_obj_add_event_cb(ctx->forgetButton, onPressForget, LV_EVENT_SHORT_CLICKED, ctx); + auto* forget_button_label = lv_label_create(ctx->forgetButton); lv_obj_align(forget_button_label, LV_ALIGN_CENTER, 0, 0); lv_label_set_text(forget_button_label, "Forget"); + lv_obj_add_flag(ctx->forgetButton, LV_OBJ_FLAG_HIDDEN); // shown by updateAutoConnectSection() // Auto-connect - auto* auto_connect_wrapper = lv_obj_create(wrapper); - lv_obj_set_size(auto_connect_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lvgl::obj_set_style_bg_invisible(auto_connect_wrapper); - lv_obj_set_style_pad_all(auto_connect_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(auto_connect_wrapper, 0, LV_STATE_DEFAULT); + ctx->autoConnectWrapper = lv_obj_create(wrapper); + lv_obj_set_size(ctx->autoConnectWrapper, LV_PCT(100), LV_SIZE_CONTENT); + lvgl::obj_set_style_bg_invisible(ctx->autoConnectWrapper); + lv_obj_set_style_pad_all(ctx->autoConnectWrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(ctx->autoConnectWrapper, 0, LV_STATE_DEFAULT); + lv_obj_add_flag(ctx->autoConnectWrapper, LV_OBJ_FLAG_HIDDEN); // shown by updateAutoConnectSection() - auto* auto_connect_label = lv_label_create(auto_connect_wrapper); + auto* auto_connect_label = lv_label_create(ctx->autoConnectWrapper); lv_label_set_text(auto_connect_label, "Auto-connect"); lv_obj_align(auto_connect_label, LV_ALIGN_LEFT_MID, 0, 0); - auto* auto_connect_switch = lv_switch_create(auto_connect_wrapper); - lv_obj_add_event_cb(auto_connect_switch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, ctx); - lv_obj_align(auto_connect_switch, LV_ALIGN_RIGHT_MID, 0, 0); - - service::wifi::settings::WifiApSettings settings; - if (service::wifi::settings::load(ctx->ssid.c_str(), settings)) { - if (settings.autoConnect) { - lv_obj_add_state(auto_connect_switch, LV_STATE_CHECKED); - } else { - lv_obj_remove_state(auto_connect_switch, LV_STATE_CHECKED); - } - } else { - LOG_W(TAG, "No settings found"); - lv_obj_add_flag(forget_button, LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(auto_connect_wrapper, LV_OBJ_FLAG_HIDDEN); - } - - updateViews(ctx); + ctx->autoConnectSwitch = lv_switch_create(ctx->autoConnectWrapper); + lv_obj_add_event_cb(ctx->autoConnectSwitch, onToggleAutoConnect, LV_EVENT_VALUE_CHANGED, ctx); + lv_obj_align(ctx->autoConnectSwitch, LV_ALIGN_RIGHT_MID, 0, 0); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; ctx.ssid = (argc > 0) ? argv[0] : std::string(); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); - // Subscribed once here, not in createWidgets(): that callback re-runs on every - // burial/resurface rebuild, and re-subscribing there would leak the previous subscription - // (and its captured ctx pointer) every time, only the last of which shutdown ever cleans up. - ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto) { - requestViewUpdate(&ctx); - }); + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) { + if (wifi_event_subscribe(wifi_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) { + ctx.wifiDevice = wifi_device; + } else { + LOG_W(TAG, "Failed to subscribe to WiFi events"); + device_put(wifi_device); + } + } else { + LOG_W(TAG, "No WiFi device found"); + } WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); + // The file-I/O-touching part of the view (updateAutoConnectSection()'s settings::load()) + // runs here, on this app's own task, not the LVGL task createWidgets() + requestViewUpdate(&ctx); + bool shouldClose = false; while (!shouldClose) { - AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; + TickType_t wait_timeout = (ctx.wifiDevice == nullptr) ? pdMS_TO_TICKS(500) : portMAX_DELAY; + task_event_group_wait_any(&event_group, nullptr, wait_timeout); + + if (ctx.wifiDevice == nullptr) { + Device* retry_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &retry_device) == ERROR_NONE) { + if (wifi_event_subscribe(retry_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) { + ctx.wifiDevice = retry_device; + } else { + device_put(retry_device); + } + } } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - case APP_EVENT_RESULT: - if (event.result.launch_id == ctx.forgetDialogId && event.result.result == 0) { // 0 = Yes - if (!service::wifi::settings::remove(ctx.ssid.c_str())) { - LOG_E(TAG, "Failed to remove SSID"); - } else { - LOG_I(TAG, "Removed SSID"); - if ( - service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive && - service::wifi::getConnectionTarget() == ctx.ssid - ) { - service::wifi::disconnect(); + + AppEvent event {}; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + case APP_EVENT_RESULT: + if (event.result.launch_id == ctx.forgetDialogId && event.result.result == 0) { // 0 = Yes + if (!service::wifi::settings::remove(ctx.ssid.c_str())) { + LOG_E(TAG, "Failed to remove SSID"); + } else { + LOG_I(TAG, "Removed SSID"); + if ( + service::wifi::getRadioState() == service::wifi::RadioState::ConnectionActive && + service::wifi::getConnectionTarget() == ctx.ssid + ) { + service::wifi::disconnect(); + } + shouldClose = true; } - shouldClose = true; } - } - app_manager_stop(event.result.launch_id); - break; - default: - break; + app_manager_stop(event.result.launch_id); + break; + default: + break; + } + if (shouldClose) break; + } + + if (ctx.wifiDevice != nullptr) { + WifiEvent wifi_event {}; + bool wifi_event_received = false; + while (wifi_event_poll(&ctx.wifiEventSub, &wifi_event) == ERROR_NONE) { + wifi_event_received = true; + } + if (wifi_event_received) { + requestViewUpdate(&ctx); + } } } - if (ctx.wifiSubscription != nullptr) { - service::wifi::getPubsub()->unsubscribe(ctx.wifiSubscription); + if (ctx.wifiDevice != nullptr) { + wifi_event_unsubscribe(ctx.wifiDevice, &ctx.wifiEventSub); + device_put(ctx.wifiDevice); } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/wificonnect/WifiConnect.cpp b/Tactility/Source/app/wificonnect/WifiConnect.cpp index 5701cc751..70b7160c8 100644 --- a/Tactility/Source/app/wificonnect/WifiConnect.cpp +++ b/Tactility/Source/app/wificonnect/WifiConnect.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -14,6 +15,8 @@ #include #include +#include +#include #include #include @@ -33,10 +36,6 @@ struct Context { std::string initialSsid; std::string initialPassword; - // Touched only from the LVGL task: directly by onConnectPressed() (an LVGL event callback, - // which already runs with the LVGL lock held), and by onWifiEvent() (a wifi-pubsub - // callback running on some other thread) which explicitly wraps its touches in - // lvgl_lock()/lvgl_unlock() - see WifiApSettings.cpp for the same convention. bool connecting = false; bool connectionError = false; @@ -49,7 +48,9 @@ struct Context { lv_obj_t* connecting_spinner = nullptr; lv_obj_t* connection_error = nullptr; - PubSub::SubscriptionHandle wifiSubscription = nullptr; + // Set once in appMain() before subscribing, left null if this device has no WiFi driver + Device* wifiDevice = nullptr; + WifiEventSubscription wifiEventSub {}; }; @@ -61,14 +62,12 @@ void onBackPressed(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); // Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits // (thread_join) for this app's own thread to finish, which needs the LVGL lock - // (window_manager_remove()) - but this callback runs ON the LVGL task, which would - // deadlock against itself. + // but this callback runs ON the LVGL task, which would deadlock against itself. AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; app_event_emit(ctx->appInstanceId, &closeEvent); } -// Runs on the wifi service's pubsub thread, not the LVGL task or this app's own thread. -void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) { +void onWifiEvent(Context* ctx, WifiEvent event) { bool shouldClose = false; lvgl_lock(); @@ -90,8 +89,6 @@ void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) { lvgl_unlock(); if (shouldClose) { - // Async, non-blocking - same reasoning as onBackPressed() (must not call - // app_manager_stop() on ourselves); safe to call from any thread. AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; app_event_emit(ctx->appInstanceId, &closeEvent); } @@ -121,8 +118,7 @@ void setLoading(Context* ctx, bool loading) { void updateView(Context* ctx) { if (ctx->connect_button == nullptr) { - // Buried (e.g. this window's own connecting state closed it, or a future dialog opens - // on top) - see destroyWidgets(). + // Buried (e.g. this window's own connecting state closed it, or a future dialog opens on top) return; } if (ctx->connectionError) { @@ -306,46 +302,77 @@ void createWidgets(lv_obj_t* parent, void* userData) { } } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx {}; ctx.appInstanceId = appInstanceId; ctx.initialSsid = (argc > 0) ? argv[0] : std::string(); ctx.initialPassword = (argc > 1) ? argv[1] : std::string(); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); - // Subscribed once here, not in createWidgets(): that callback re-runs on every - // burial/resurface rebuild, and re-subscribing there would leak the previous subscription - // (and its captured ctx pointer) every time, only the last of which shutdown ever cleans up. - ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto event) { - onWifiEvent(&ctx, event); - }); + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) { + if (wifi_event_subscribe(wifi_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) { + ctx.wifiDevice = wifi_device; + } else { + LOG_W(TAG, "Failed to subscribe to WiFi events"); + device_put(wifi_device); + } + } else { + LOG_W(TAG, "No WiFi device found"); + } WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); bool shouldClose = false; while (!shouldClose) { + TickType_t wait_timeout = (ctx.wifiDevice == nullptr) ? pdMS_TO_TICKS(500) : portMAX_DELAY; + task_event_group_wait_any(&event_group, nullptr, wait_timeout); + + if (ctx.wifiDevice == nullptr) { + Device* retry_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &retry_device) == ERROR_NONE) { + if (wifi_event_subscribe(retry_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) { + ctx.wifiDevice = retry_device; + } else { + device_put(retry_device); + } + } + } + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + + if (ctx.wifiDevice != nullptr) { + WifiEvent wifi_event {}; + while (wifi_event_poll(&ctx.wifiEventSub, &wifi_event) == ERROR_NONE) { + onWifiEvent(&ctx, wifi_event); + } } } - if (ctx.wifiSubscription != nullptr) { - service::wifi::getPubsub()->unsubscribe(ctx.wifiSubscription); + if (ctx.wifiDevice != nullptr) { + wifi_event_unsubscribe(ctx.wifiDevice, &ctx.wifiEventSub); + device_put(ctx.wifiDevice); } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/app/wifimanage/WifiManage.cpp b/Tactility/Source/app/wifimanage/WifiManage.cpp index a138cc23d..935f8b400 100644 --- a/Tactility/Source/app/wifimanage/WifiManage.cpp +++ b/Tactility/Source/app/wifimanage/WifiManage.cpp @@ -7,13 +7,18 @@ #include #include #include +#include #include +#include +#include #include #include +#include + namespace tt::app::wifimanage { constexpr auto* TAG = "WifiManage"; @@ -24,12 +29,18 @@ namespace { struct Context { uint32_t appInstanceId; - PubSub::SubscriptionHandle wifiSubscription = nullptr; + // Set once in appMain() before subscribing, left null if this device has no WiFi driver + Device* wifiDevice = nullptr; + WifiEventSubscription wifiEventSub {}; Mutex mutex; Bindings bindings {}; State state; View view = View(&bindings, &state); + TaskEventGroup* eventGroup = nullptr; + uint32_t refreshBit = 0; + std::atomic needsRefresh {false}; + void lock() { mutex.lock(); } void unlock() { mutex.unlock(); } }; @@ -62,17 +73,18 @@ static void onConnectToHidden() { wificonnect::start(); } -void requestViewUpdate(Context* ctx) { - ctx->lock(); +void updateView(Context* ctx) { + // Same lock order as createWidgets() (called with the LVGL lock already held, per the + // window-manager's WindowCreateWidgetsFn contract, then acquiring ctx->mutex) - acquiring + // these in the opposite order here would deadlock against a concurrent createWidgets() call. lvgl_lock(); - // Safe even while buried (e.g. WifiApSettings/WifiConnect opened on top): destroyWidgets() - // nulls the view's widget pointers before they're deleted, and update() no-ops on that. + ctx->lock(); ctx->view.update(); - lvgl_unlock(); ctx->unlock(); + lvgl_unlock(); } -void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) { +void onWifiEvent(Context* ctx, WifiEvent event) { auto radio_state = service::wifi::getRadioState(); LOG_I(TAG, "Update with state %s", service::wifi::radioStateToString(radio_state)); ctx->state.setRadioState(radio_state); @@ -93,7 +105,7 @@ void onWifiEvent(Context* ctx, service::wifi::WifiEvent event) { break; } - requestViewUpdate(ctx); + updateView(ctx); } void createWidgets(lv_obj_t* parent, void* userData) { @@ -101,18 +113,18 @@ void createWidgets(lv_obj_t* parent, void* userData) { ctx->lock(); ctx->state.setConnectSsid("Connected"); // TODO update with proper SSID ctx->view.init(ctx->appInstanceId, parent); - ctx->view.update(); ctx->unlock(); + ctx->needsRefresh = true; + task_event_group_signal(ctx->eventGroup, ctx->refreshBit); } -// Runs with the LVGL lock already held, possibly on another app's thread - see -// WindowDestroyWidgetsFn's warnings. Must stay lock-free: View::reset() only nulls pointers. void destroyWidgets(void* userData) { auto* ctx = static_cast(userData); ctx->view.reset(); } -int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { +int32_t appMain(int argc, char* argv[]) { + uint32_t appInstanceId = app_scheduler_current_app_id(); Context ctx; ctx.appInstanceId = appInstanceId; ctx.bindings = (Bindings) { @@ -123,18 +135,32 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { .onConnectToHidden = onConnectToHidden }; - ctx.wifiSubscription = service::wifi::getPubsub()->subscribe([&ctx](auto event) { - onWifiEvent(&ctx, event); - }); - // State update (it has its own locking) ctx.state.setRadioState(service::wifi::getRadioState()); ctx.state.setScanning(service::wifi::isScanning()); ctx.state.updateApRecords(); + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + ctx.eventGroup = &event_group; + if (task_event_group_claim_bit(&event_group, &ctx.refreshBit) != ERROR_NONE) { + LOG_W(TAG, "Failed to claim a refresh bit; resurfacing after burial won't repopulate the view"); + } + AppEventSubscription sub {}; - sub.app_instance_id = appInstanceId; - app_event_subscribe(&sub); + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); + + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) == ERROR_NONE) { + if (wifi_event_subscribe(wifi_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) { + ctx.wifiDevice = wifi_device; + } else { + LOG_W(TAG, "Failed to subscribe to WiFi events"); + device_put(wifi_device); + } + } else { + LOG_W(TAG, "No WiFi device found"); + } WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx); @@ -154,26 +180,58 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) { bool shouldClose = false; while (!shouldClose) { + // If the wifi device wasn't started yet when this app opened (boot-order race), + // ctx.wifiDevice is still null and there's no wifi-driven wake source to learn + // "it's ready now" from, so poll for it on a bounded timeout instead of blocking + // indefinitely. Once subscribed, this reverts to + // portMAX_DELAY - task_event_group_wait_any() still returns immediately for app_event + // and (once live) wifi_event, this timeout only matters while neither has fired yet. + TickType_t wait_timeout = (ctx.wifiDevice == nullptr) ? pdMS_TO_TICKS(500) : portMAX_DELAY; + task_event_group_wait_any(&event_group, nullptr, wait_timeout); + + if (ctx.wifiDevice == nullptr) { + Device* retry_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &retry_device) == ERROR_NONE) { + if (wifi_event_subscribe(retry_device, &ctx.wifiEventSub, &event_group) == ERROR_NONE) { + ctx.wifiDevice = retry_device; + } else { + device_put(retry_device); + } + } + } + AppEvent event {}; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + switch (event.type) { + case APP_EVENT_CLOSE: + shouldClose = true; + break; + default: + break; + } + if (shouldClose) break; } - switch (event.type) { - case APP_EVENT_CLOSE: - shouldClose = true; - break; - default: - break; + + if (ctx.wifiDevice != nullptr) { + WifiEvent wifi_event {}; + while (wifi_event_poll(&ctx.wifiEventSub, &wifi_event) == ERROR_NONE) { + onWifiEvent(&ctx, wifi_event); + } + } + + if (ctx.needsRefresh.exchange(false)) { + updateView(&ctx); } } - ctx.lock(); - service::wifi::getPubsub()->unsubscribe(ctx.wifiSubscription); - ctx.wifiSubscription = nullptr; - ctx.unlock(); + if (ctx.wifiDevice != nullptr) { + wifi_event_unsubscribe(ctx.wifiDevice, &ctx.wifiEventSub); + device_put(ctx.wifiDevice); + } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; } diff --git a/Tactility/Source/service/espnow/EspNowHostedTransport.cpp b/Tactility/Source/service/espnow/EspNowHostedTransport.cpp index 73bb4c9d7..751ffe11e 100644 --- a/Tactility/Source/service/espnow/EspNowHostedTransport.cpp +++ b/Tactility/Source/service/espnow/EspNowHostedTransport.cpp @@ -6,6 +6,7 @@ #include +#include #include namespace tt::service::espnow::backend { @@ -16,16 +17,19 @@ namespace tt::service::espnow::backend { // truth shared with any other caller (e.g. an external OTA app), instead of being duplicated // here. bool waitForHostedTransport(uint32_t timeoutMs) { - Device* device = wifi_find_first_registered_device(); - if (device == nullptr) { + Device* device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &device) != ERROR_NONE) { return false; } const FirmwareOps* ops = nullptr; void* ctx = nullptr; if (wifi_get_firmware_ops(device, &ops, &ctx) != ERROR_NONE) { + device_put(device); return false; } - return ops->wait_ready(ctx, timeoutMs); + bool ready = ops->wait_ready(ctx, timeoutMs); + device_put(device); + return ready; } } diff --git a/Tactility/Source/service/wifi/Wifi.cpp b/Tactility/Source/service/wifi/Wifi.cpp index 394f92c71..2f8cc1146 100644 --- a/Tactility/Source/service/wifi/Wifi.cpp +++ b/Tactility/Source/service/wifi/Wifi.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -62,7 +63,6 @@ namespace { /** State lives for the entire process; only ever (re)initialized by onStart(). */ struct WifiServiceState { Device* device = nullptr; - std::shared_ptr> pubsub = std::make_shared>(); RecursiveMutex mutex; bool secureConnection = false; // Internal: set by connect()/disconnect() while a manual attempt is in flight, cleared on @@ -79,24 +79,32 @@ struct WifiServiceState { TickType_t lastScanTime = MAX_TICKS; std::unique_ptr autoConnectTimer; bool bootEventSubscribed = false; + + // Dedicated consumer for WifiEvents, alive for the service's whole lifetime (started in + // onStart(), stopped in onStop() - see dispatchSetEnabled()'s comment on why this outlives + // radio on/off toggles): runs onWifiDeviceEvent() on its own stack instead of the ESP-IDF + // esp_event task's, by blocking in task_event_group_wait_any() rather than being called back + // directly from fire_event(). + TaskEventGroup wifiEventGroup {}; + WifiEventSubscription wifiEventSub {}; + Thread* wifiEventThread = nullptr; + std::atomic wifiEventThreadRunning {false}; }; WifiServiceState state; bool started = false; -void onWifiDeviceEvent(Device* device, void* context, ::WifiEvent event); +void onWifiDeviceEvent(Device* device, ::WifiEvent event); // ---- Helpers ---- -void publish(WifiEvent event) { - state.pubsub->publish(event); -} - -void publishRadioState(WifiRadioState radio_state) { - WifiEvent event = {}; - event.type = WIFI_EVENT_TYPE_RADIO_STATE_CHANGED; - event.radio_state = radio_state; - publish(event); +// state.device is started (bookkeeping allocated) for the service's entire lifetime now - see +// dispatchSetEnabled()'s comment - so device_is_ready() no longer tracks radio-on state; query +// the driver directly instead. +bool isRadioOn() { + if (state.device == nullptr) return false; + WifiRadioState radio = WIFI_RADIO_STATE_OFF; + return wifi_get_radio_state(state.device, &radio) == ERROR_NONE && radio == WIFI_RADIO_STATE_ON; } RadioState combineRadioState(WifiRadioState radio, WifiStationState station) { @@ -114,56 +122,88 @@ RadioState combineRadioState(WifiRadioState radio, WifiStationState station) { return RadioState::Off; } +// ---- WifiEvent consumer thread ---- +// Runs onWifiDeviceEvent() on its own stack (see WifiServiceState::wifiEventGroup's comment). + +constexpr configSTACK_DEPTH_TYPE WIFI_EVENT_THREAD_STACK_SIZE = 4096; + +int32_t wifiEventThreadMain() { + // The 250ms timeout only bounds how promptly a stop request (wifiEventThreadRunning going + // false) is noticed; a real event still wakes this immediately regardless, since + // task_event_group_wait_any() returns as soon as the bit is signalled, whichever comes first. + while (state.wifiEventThreadRunning.load()) { + task_event_group_wait_any(&state.wifiEventGroup, nullptr, pdMS_TO_TICKS(250)); + + WifiEvent event {}; + while (wifi_event_poll(&state.wifiEventSub, &event) == ERROR_NONE) { + onWifiDeviceEvent(state.device, event); + } + } + return 0; +} + +bool startWifiEventThread() { + task_event_group_construct(&state.wifiEventGroup); + if (wifi_event_subscribe(state.device, &state.wifiEventSub, &state.wifiEventGroup) != ERROR_NONE) { + task_event_group_destruct(&state.wifiEventGroup); + return false; + } + + state.wifiEventThreadRunning = true; + state.wifiEventThread = new Thread("wifi-events", WIFI_EVENT_THREAD_STACK_SIZE, [] { return wifiEventThreadMain(); }); + state.wifiEventThread->start(); + return true; +} + +void stopWifiEventThread() { + if (state.wifiEventThread == nullptr) return; + + state.wifiEventThreadRunning = false; + state.wifiEventThread->join(); + delete state.wifiEventThread; + state.wifiEventThread = nullptr; + + wifi_event_unsubscribe(state.device, &state.wifiEventSub); + task_event_group_destruct(&state.wifiEventGroup); +} + // ---- Dispatched work (runs on the main task) ---- +// state.device is started (device_start()) once, in onStart(), and never stopped until onStop() - +// this only toggles the radio itself, so the wifi-events thread (and any app subscribed directly +// to the driver) stays subscribed across on/off toggles instead of having to resubscribe. void dispatchSetEnabled(bool enabled) { LOG_I(TAG, "dispatchSetEnabled(%d)", (int)enabled); if (!started || state.device == nullptr) return; - bool ready = device_is_ready(state.device); - if (enabled == ready) { + if (enabled == isRadioOn()) { LOG_W(TAG, "Can't enable/disable from current state"); return; } if (enabled) { - publishRadioState(WIFI_RADIO_STATE_ON_PENDING); - - if (device_start(state.device) != ERROR_NONE) { - LOG_E(TAG, "Failed to start WiFi device"); - publishRadioState(WIFI_RADIO_STATE_OFF); - return; - } - if (wifi_add_event_callback(state.device, nullptr, onWifiDeviceEvent) != ERROR_NONE) { - LOG_E(TAG, "Failed to register WiFi event callback"); - device_stop(state.device); - publishRadioState(WIFI_RADIO_STATE_OFF); + if (wifi_set_radio_on(state.device) != ERROR_NONE) { + LOG_E(TAG, "Failed to enable WiFi radio"); return; } state.pauseAutoConnect = false; state.lastScanTime = 0; - publishRadioState(WIFI_RADIO_STATE_ON); } else { - publishRadioState(WIFI_RADIO_STATE_OFF_PENDING); - if (device_stop(state.device) != ERROR_NONE) { - LOG_E(TAG, "Failed to stop WiFi device"); - publishRadioState(WIFI_RADIO_STATE_ON); + if (wifi_set_radio_off(state.device) != ERROR_NONE) { + LOG_E(TAG, "Failed to disable WiFi radio"); return; } - wifi_remove_event_callback(state.device, onWifiDeviceEvent); - state.secureConnection = false; - publishRadioState(WIFI_RADIO_STATE_OFF); } } void dispatchScan() { LOG_I(TAG, "dispatchScan()"); - if (!started || state.device == nullptr || !device_is_ready(state.device)) return; + if (!started || state.device == nullptr || !isRadioOn()) return; state.lastScanTime = get_ticks(); @@ -189,18 +229,7 @@ void dispatchConnect() { LOG_I(TAG, "Connecting to %s", target.ssid.c_str()); - error_t result = wifi_station_connect(state.device, target.ssid.c_str(), target.password.c_str(), target.channel); - if (result != ERROR_NONE) { - LOG_E(TAG, "Failed to connect to %s (%s)", target.ssid.c_str(), error_to_string(result)); - WifiEvent event = {}; - event.type = WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT; - // The driver couldn't even initiate the connection attempt; there's no - // more specific WifiStationConnectionError for that. - event.connection_error = WIFI_STATION_CONNECTION_ERROR_TIMEOUT; - publish(event); - } - // On success, WIFI_EVENT_TYPE_STATION_STATE_CHANGED / _CONNECTION_RESULT arrive - // asynchronously via onWifiDeviceEvent(). + wifi_station_connect(state.device, target.ssid.c_str(), target.password.c_str(), target.channel); } void dispatchDisconnect() { @@ -233,12 +262,14 @@ bool findAutoConnectAp(settings::WifiApSettings& out) { void dispatchAutoConnect() { LOG_I(TAG, "dispatchAutoConnect()"); - if (state.pauseAutoConnect || state.externalScanPause.load()) { + if (state.pauseAutoConnect || state.externalScanPause.load() || !isRadioOn()) { // A manual disconnect() or an in-progress manual connect() has paused // auto-connect, or a caller (e.g. AutoScanPauseGuard) has externally paused it. // This is called on every SCAN_FINISHED, not just the auto-connect timer's own // scans (e.g. WifiManage re-scans on show), so it must honor the pause instead of - // reconnecting unconditionally. + // reconnecting unconditionally. The radio-off check matters because a scan that was + // already in flight can finish after the user turns the radio off. Without it, + // connect() would call dispatchSetEnabled(true) and turn the radio back on. return; } RadioState radio_state = getRadioState(); @@ -278,7 +309,7 @@ void onAutoConnectTimer() { // ---- Kernel driver event bridge ---- -void onWifiDeviceEvent(Device* device, void* /*context*/, ::WifiEvent event) { +void onWifiDeviceEvent(Device* device, ::WifiEvent event) { switch (event.type) { case WIFI_EVENT_TYPE_SCAN_FINISHED: getMainDispatcher().dispatch([] { dispatchAutoConnect(); }); @@ -333,10 +364,6 @@ void onWifiDeviceEvent(Device* device, void* /*context*/, ::WifiEvent event) { default: break; } - - // Forward the event as-is: subscribers inspect event.type and the - // relevant union field directly, same as this function does. - publish(event); } void autoScanSetPaused(bool paused) { @@ -348,12 +375,8 @@ void autoScanSetPaused(bool paused) { // region Public functions -std::shared_ptr> getPubsub() { - return state.pubsub; -} - RadioState getRadioState() { - if (!started || state.device == nullptr || !device_is_ready(state.device)) { + if (!started || state.device == nullptr) { return RadioState::Off; } @@ -403,7 +426,7 @@ void connect(const settings::WifiApSettings& ap, bool remember) { state.pauseAutoConnect = true; state.connectionTarget = ap; state.connectionTargetRemember = remember; - radio_off = !device_is_ready(state.device); + radio_off = !isRadioOn(); } getMainDispatcher().dispatch([radio_off] { @@ -505,9 +528,17 @@ class WifiService final : public Service { wifi_auto_scan_set_paused_function(autoScanSetPaused); - state.device = wifi_find_first_registered_device(); - if (state.device == nullptr) { + Device* wifi_device = nullptr; + if (device_get_first_by_type(&WIFI_TYPE, &wifi_device) != ERROR_NONE) { LOG_W(TAG, "No WiFi device found"); + } else if (device_start(wifi_device) != ERROR_NONE) { + LOG_E(TAG, "Failed to start WiFi device"); + device_put(wifi_device); + } else { + state.device = wifi_device; + if (!startWifiEventThread()) { + LOG_E(TAG, "Failed to subscribe to WiFi events"); + } } if (system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, onBootCompleted, nullptr) == ERROR_NONE) { @@ -535,9 +566,13 @@ class WifiService final : public Service { state.bootEventSubscribed = false; } - if (state.device != nullptr && device_is_ready(state.device)) { - wifi_remove_event_callback(state.device, onWifiDeviceEvent); + if (state.device != nullptr) { + if (isRadioOn()) { + wifi_set_radio_off(state.device); + } + stopWifiEventThread(); device_stop(state.device); + device_put(state.device); } state.secureConnection = false; diff --git a/TactilityKernel/include/tactility/concurrent/task_event_group.h b/TactilityKernel/include/tactility/concurrent/task_event_group.h new file mode 100644 index 000000000..e0fd2ff6f --- /dev/null +++ b/TactilityKernel/include/tactility/concurrent/task_event_group.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Usable bits per FreeRTOS event group when configUSE_16_BIT_TICKS is 0 + * (This project's setting on every target. See Devices/simulator/Source/FreeRTOSConfig.h). */ +#define TASK_EVENT_GROUP_MAX_BITS 24 + +/** + * Lets a task block on several independent event sources (e.g. app_event and system_event) with + * a single wait call, instead of each source claiming the task's own FreeRTOS notification value + * or its own semaphore - primitives that can't be combined into one blocking call. + * + * Caller-owned storage, constructed/destructed like struct Mutex/EventGroupHandle_t - no heap + * allocation. Each event source claims its own bit at subscribe time via + * task_event_group_claim_bit(), so sources at any layer (kernel, firmware, app) can share one + * group without knowing about each other's bit assignments. + * + * @warning `bit_mutex`/`claimed_bits` are implementation-only; do not read or write directly. + */ +struct TaskEventGroup { + uint32_t claimed_bits; + struct { + EventGroupHandle_t handle; + struct Mutex bit_mutex; + } internal; +}; + +/** @warning Does not work in ISR context. */ +void task_event_group_construct(struct TaskEventGroup* group); + +/** @warning Does not work in ISR context. */ +void task_event_group_destruct(struct TaskEventGroup* group); + +/** + * Claim an unused bit in @a group for exclusive use by one event source. Always starts clear + * (unsignalled), whether this bit has ever been claimed before or not. + * @param[out] out_bit set to the claimed bit (a single-bit mask, not a bit index) on success + * @retval ERROR_NONE on success + * @retval ERROR_RESOURCE all TASK_EVENT_GROUP_MAX_BITS bits are already claimed + */ +error_t task_event_group_claim_bit(struct TaskEventGroup* group, uint32_t* out_bit); + +/** + * Release a bit previously returned by task_event_group_claim_bit(), making it available for + * reuse. Also clears it, so a signal nobody drained before releasing can't be handed to the next + * claimant as a false "already fired" wake. + */ +void task_event_group_release_bit(struct TaskEventGroup* group, uint32_t bit); + +/** Signal @a bit, waking any task blocked in task_event_group_wait() for it. ISR-safe. */ +error_t task_event_group_signal(struct TaskEventGroup* group, uint32_t bit); + +/** + * Block until one or more bits in @a bits_mask are signalled, or @a timeout elapses. Matched + * bits are cleared on exit. + * @param[in] await_all if true, wait for every bit in @a bits_mask; otherwise wait for any of them + * @param[out] out_flags if non-NULL, set to the matched bits on ERROR_NONE + */ +error_t task_event_group_wait( + struct TaskEventGroup* group, + uint32_t bits_mask, + bool await_all, + uint32_t* out_flags, + TickType_t timeout +); + +/** + * Like task_event_group_wait(), but waits on every bit currently claimed in @a group (snapshotted + * at call time) instead of a caller-supplied mask - so the caller doesn't need to track/OR + * together each subscription's bit by hand. A bit claimed by a new subscription *during* the wait + * isn't included until the next call. + * @warning Meant for the common case where every subscription sharing @a group is set up before + * the wait loop starts (matching app_event/system_event/wifi_event's own usage patterns) - not + * for a group a subscriber can join mid-wait and expect to wake immediately. + * @retval ERROR_TIMEOUT returned immediately, without blocking, if @a group currently has no + * claimed bits (FreeRTOS asserts on a zero-bit wait mask, so this is handled before it gets there) + */ +error_t task_event_group_wait_any(struct TaskEventGroup* group, uint32_t* out_flags, TickType_t timeout); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/drivers/wifi.h b/TactilityKernel/include/tactility/drivers/wifi.h index 0a5b10308..78c7cff6a 100644 --- a/TactilityKernel/include/tactility/drivers/wifi.h +++ b/TactilityKernel/include/tactility/drivers/wifi.h @@ -4,6 +4,8 @@ #include #include +#include +#include #include #include @@ -90,9 +92,59 @@ struct WifiEvent { }; }; -typedef void (*WifiEventCallback)(struct Device* device, void* callback_context, struct WifiEvent event); +/** Number of events a WifiEventSubscription can hold before wifi_event_emit() starts dropping + * the newest event for it (still delivered to any other matching subscription). Generous: a + * device fires these serially, one at a time, not in genuinely concurrent bursts. */ +#define WIFI_EVENT_QUEUE_CAPACITY 4 + +/** + * Caller-owned subscription node, registered with wifi_event_subscribe() and polled with + * wifi_event_poll(). Like app_event, this queues events by value (FIFO) rather than + * coalescing to the latest one: a burst of distinct WifiEventTypes (e.g. + * WIFI_EVENT_TYPE_STATION_STATE_CHANGED immediately followed by + * WIFI_EVENT_TYPE_STATION_CONNECTION_RESULT) must each be delivered, not just "something + * changed." + * @warning Fields other than `bit` are for internal use only; do not read or write them + * directly. + */ +struct WifiEventSubscription { + /** Set by wifi_event_subscribe(). Read-only for the caller: OR it into a + * task_event_group_wait() mask (alongside other subscriptions sharing the same + * `event_group`) to block on this subscription and other event sources with one call. */ + uint32_t bit; + + struct { + /** Caller-owned, borrowed; set by wifi_event_subscribe(). */ + struct TaskEventGroup* event_group; + /** Guards `queue`/`head`/`count` between wifi_event_emit() (driver thread) and + * wifi_event_poll() (caller's thread) - the two live in different translation units with + * no other shared lock. */ + struct Mutex ring_mutex; + struct WifiEvent queue[WIFI_EVENT_QUEUE_CAPACITY]; + uint8_t head; + uint8_t count; + + struct WifiEventSubscription* next; + } internal; +}; struct WifiApi { + /** + * Turn the radio on. Unlike start_device()/stop_device() (which only allocate/free the + * driver's bookkeeping, so event subscribers can stay subscribed across radio toggles), + * this is what actually brings the hardware up. + * @param[in] device the wifi device + * @return ERROR_NONE on success, or if the radio is already on + */ + error_t (*set_radio_on)(struct Device* device); + + /** + * Turn the radio off. See set_radio_on(). + * @param[in] device the wifi device + * @return ERROR_NONE on success, or if the radio is already off + */ + error_t (*set_radio_off)(struct Device* device); + /** * Get the radio state of the device. * @param[in] device the wifi device @@ -182,21 +234,30 @@ struct WifiApi { error_t (*station_get_rssi)(struct Device* device, int32_t* rssi); /** - * Add a WifiEvent callback. + * Register a subscription for this device's WifiEvents. + * @warning Does not work in ISR context. * @param[in] device the wifi device - * @param[in] callback_context the context to pass to the callback - * @param[in] callback the callback function - * @return ERROR_NONE on success + * @param[in,out] sub subscription to register; owns the storage, must stay alive (and + * stationary) until unsubscribed + * @param[in] event_group caller-owned group to wait on; must outlive @a sub (i.e. be + * destructed only after event_unsubscribe()). To block for an event, call + * task_event_group_wait()/task_event_group_wait_any() on this group (OR sub->bit into the + * mask, or use _wait_any() to include every subscription sharing it), then drain with + * wifi_event_poll(). + * @retval ERROR_NONE on success + * @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered + * @retval ERROR_INVALID_STATE @a sub is already registered */ - error_t (*add_event_callback)(struct Device* device, void* callback_context, WifiEventCallback callback); + error_t (*event_subscribe)(struct Device* device, struct WifiEventSubscription* sub, struct TaskEventGroup* event_group); /** - * Remove a WifiEvent callback. + * Remove a previously registered subscription. + * @warning Does not work in ISR context. * @param[in] device the wifi device - * @param[in] callback the callback function - * @return ERROR_NONE on success + * @param[in] sub subscription to remove, as passed to event_subscribe() + * @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists */ - error_t (*remove_event_callback)(struct Device* device, WifiEventCallback callback); + error_t (*event_unsubscribe)(struct Device* device, struct WifiEventSubscription* sub); /** * Get this device's co-processor firmware update interface, if it has one. @@ -210,8 +271,10 @@ struct WifiApi { extern const struct DeviceType WIFI_TYPE; -/** @return the first registered WiFi device, regardless of started state, or NULL if none exists */ -struct Device* wifi_find_first_registered_device(void); +/** Turn the radio on. See WifiApi::set_radio_on(). Requires the device to be started (device_start()). */ +error_t wifi_set_radio_on(struct Device* device); +/** Turn the radio off. See WifiApi::set_radio_off(). Requires the device to be started (device_start()). */ +error_t wifi_set_radio_off(struct Device* device); error_t wifi_get_radio_state(struct Device* device, enum WifiRadioState* state); error_t wifi_get_station_state(struct Device* device, enum WifiStationState* state); @@ -224,8 +287,40 @@ error_t wifi_station_get_target_ssid(struct Device* device, char* ssid); error_t wifi_station_connect(struct Device* device, const char* ssid, const char* password, int32_t channel); error_t wifi_station_disconnect(struct Device* device); error_t wifi_station_get_rssi(struct Device* device, int32_t* rssi); -error_t wifi_add_event_callback(struct Device* device, void* callback_context, WifiEventCallback callback); -error_t wifi_remove_event_callback(struct Device* device, WifiEventCallback callback); + +/** + * Register a subscription for @a device's WifiEvents. + * @warning Does not work in ISR context. + * @param[in] device the wifi device + * @param[in,out] sub subscription to register; owns the storage, must stay alive (and + * stationary) until unsubscribed + * @param[in] event_group caller-owned group to wait on; must outlive @a sub. To block for an + * event, call task_event_group_wait()/task_event_group_wait_any() on this group (OR sub->bit + * into the mask, or use _wait_any() to include every subscription sharing it), then drain with + * wifi_event_poll(). + * @retval ERROR_NONE on success + * @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered + * @retval ERROR_INVALID_STATE @a sub is already registered + */ +error_t wifi_event_subscribe(struct Device* device, struct WifiEventSubscription* sub, struct TaskEventGroup* event_group); + +/** + * Remove a previously registered subscription. + * @warning Does not work in ISR context. + * @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists + */ +error_t wifi_event_unsubscribe(struct Device* device, struct WifiEventSubscription* sub); + +/** + * Non-blocking: pop the next event for @a sub if one is already queued. + * @warning Never blocks. To wait for an event, block in task_event_group_wait()/ + * task_event_group_wait_any() on @a sub's event group first (see wifi_event_subscribe()), then + * drain with this in a loop. + * @retval ERROR_NONE @a out_event was filled + * @retval ERROR_TIMEOUT nothing queued right now + */ +error_t wifi_event_poll(struct WifiEventSubscription* sub, struct WifiEvent* out_event); + error_t wifi_get_firmware_ops(struct Device* device, const struct FirmwareOps** ops, void** ctx); #ifdef __cplusplus diff --git a/TactilityKernel/include/tactility/system_event.h b/TactilityKernel/include/tactility/system_event.h index 71a97dc68..7ebd6a176 100644 --- a/TactilityKernel/include/tactility/system_event.h +++ b/TactilityKernel/include/tactility/system_event.h @@ -5,10 +5,8 @@ #include #include +#include #include -#include -#include -#include #ifdef __cplusplus extern "C" { @@ -141,17 +139,9 @@ error_t system_event_emit( /** * Poll subscription: caller-owned node, registered with system_event_subscribe() - * and polled with system_event_await(). Unlike system_event_callback_t, `event` is a by-value + * and polled with system_event_poll(). Unlike system_event_callback_t, `event` is a by-value * copy that remains valid for the subscription's lifetime (until the next matching event * overwrites it), not just for the duration of a callback. - * @warning Must be zero-initialized before the first system_event_subscribe() call (e.g. - * `SystemEventSubscription sub = {};` in C++, `SystemEventSubscription sub = {0};` in C, or - * static/global storage) - system_event_subscribe() reads `internal.unsubscribe_in_progress` - * before it writes it, to detect reuse of a node still being torn down by a concurrent - * system_event_unsubscribe() call; on indeterminate (non-zeroed) storage that read is undefined - * behavior. Not required again for a later system_event_subscribe() reusing the same node after - * system_event_unsubscribe() - the fields it depends on are fully owned/maintained by this API - * from the first successful registration onward. */ struct SystemEventSubscription { /** `event.type` is the event type to subscribe to; set by the caller before @@ -159,36 +149,26 @@ struct SystemEventSubscription { * each matching system_event_emit() - see the @warning above. */ struct SystemEvent event; + /** Set by system_event_subscribe(). Read-only for the caller: OR it into a + * task_event_group_wait() mask (alongside other subscriptions sharing the same + * `internal.event_group`) to block on this subscription and other event sources with one + * call. */ + uint32_t bit; + /** Implementation-only bookkeeping; do not read or write directly. */ struct { - /** Own wakeup signal, not the subscribing task's shared default notification value - a - * task with more than one poll subscription would otherwise have events for one - * subscription wake (and consume the notification meant for) system_event_await() - * calls on another. */ - SemaphoreHandle_t semaphore; + /** Caller-owned, borrowed; set by system_event_subscribe(). A task with more than one + * poll subscription shares one group across them - each subscription claims its own + * bit, so an event for one can't wake (and consume the signal meant for) another. */ + struct TaskEventGroup* event_group; uint32_t sequence; uint32_t consumed_sequence; - /** Number of tasks currently blocked in system_event_await() on `semaphore` - - * system_event_unsubscribe() waits for this to reach 0 before deleting it, since - * FreeRTOS requires no task be blocked on a semaphore when it's deleted. */ - int waiter_count; - /** Set by system_event_unsubscribe() before it gives `semaphore` and waits, so a task - * already blocked in system_event_await() bails out (ERROR_INVALID_STATE) instead of - * waiting out its full timeout. Reset once system_event_unsubscribe() finishes - * draining old awaiters (see unsubscribe_in_progress) - not simply "on the next - * system_event_subscribe()", so a fresh registration can never observe a stale `true` - * left over from an unsubscribe that hasn't returned yet. */ + /** Set by system_event_unsubscribe(). Diagnostic only: lets a subsequent + * system_event_poll() call report ERROR_INVALID_STATE instead of ERROR_TIMEOUT. Not a + * safety mechanism - see system_event_unsubscribe()'s @warning. */ bool cancelled; - /** True from the moment system_event_unsubscribe() unlinks `sub` until it has finished - * draining old awaiters and deleted the old semaphore. system_event_subscribe() spins - * until this clears before reusing `sub` - otherwise a new registration could reset - * waiter_count/cancelled (both shared with the old registration, there being only one - * `sub`) out from under the old system_event_unsubscribe() call still relying on them, - * or hand out a new semaphore for that same call to then promptly delete instead of the - * old one, while an old awaiter is still blocked on the real old semaphore. */ - bool unsubscribe_in_progress; struct SystemEventSubscription* next; } internal; @@ -197,52 +177,50 @@ struct SystemEventSubscription { /** * Register a poll subscription for events of @a sub->type. * @warning Does not work in ISR context. - * @warning On its very first call for a given @a sub, @a sub must have been zero-initialized - - * see SystemEventSubscription's @warning. - * @warning If @a sub was just passed to system_event_unsubscribe() (e.g. reusing a node for a - * new registration) and that call hasn't returned yet on another task, this call blocks - * (briefly - not for the full duration of anyone's timeout) until it does, before registering - - * see SystemEventSubscription::internal.unsubscribe_in_progress. * @param[in,out] sub subscription to register; caller sets @a sub->type beforehand, owns the * storage, and must keep it alive (and stationary) until unsubscribed + * @param[in] event_group caller-owned group to wait on; must outlive @a sub (i.e. be + * destructed only after system_event_unsubscribe()). To block for an event, call + * task_event_group_wait()/task_event_group_wait_any() on this group (OR sub->bit into the mask, + * or use _wait_any() to include every subscription sharing it), then poll with + * system_event_poll(). * @retval ERROR_NONE on success - * @retval ERROR_OUT_OF_MEMORY failed to allocate the subscription's wakeup semaphore; @a sub - * was not registered + * @retval ERROR_RESOURCE @a event_group has no free bits left to claim; @a sub was not registered * @retval ERROR_INVALID_STATE @a sub is already registered */ -error_t system_event_subscribe(struct SystemEventSubscription* sub); +error_t system_event_subscribe(struct SystemEventSubscription* sub, struct TaskEventGroup* event_group); /** * Remove a previously registered poll subscription. * @warning Does not work in ISR context. - * @warning Blocks (briefly - not for the full duration of anyone's timeout) until any task - * currently blocked in system_event_await() on @a sub has woken up and left, so it's safe to - * delete the subscription's semaphore before this call returns. A blocked awaiter is woken - * (with ERROR_INVALID_STATE) as part of this call rather than left to time out on its own. - * A concurrent system_event_subscribe() reusing the same @a sub waits out this same window - * (see system_event_subscribe()'s @warning) rather than racing it. + * @warning Does not wait for a task concurrently blocked in task_event_group_wait()/ + * task_event_group_wait_any() on @a sub's bit to leave before releasing that bit - the caller + * must ensure no other task is still waiting on @a sub before unsubscribing it. (A blocked task + * is still nudged awake as a best-effort courtesy - its subsequent system_event_poll() call will + * report ERROR_INVALID_STATE - but this is diagnostic, not a guarantee.) * @param[in] sub subscription to remove, as passed to system_event_subscribe() * @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists */ error_t system_event_unsubscribe(struct SystemEventSubscription* sub); /** - * Blocks the calling task until a new event arrives for @a sub, or timeout elapses. + * Non-blocking: check whether a new event has arrived for @a sub since the last call. + * @warning Never blocks. To wait, block in task_event_group_wait()/task_event_group_wait_any() + * on @a sub's bit first (see system_event_subscribe()), then call this. * @warning Poll subscriptions coalesce to the latest event, they are not a queue: if * system_event_emit() is called more than once for @a sub->event.type between two - * system_event_await() calls, only the most recent event's data/timestamp is visible via + * system_event_poll() calls, only the most recent event's data/timestamp is visible via * system_event_get_data()/system_event_get_timestamp() afterward - intermediate events are * silently overwritten, never delivered. Use system_event_callback_add() instead if every * individual event matters. - * @warning Cannot be called concurrently from different tasks. Each tasks must have its own subscription. - * @param[in,out] sub subscription to wait on, as passed to system_event_subscribe() - * @param[in] timeout max ticks to wait - * @retval ERROR_NONE an event arrived - * @retval ERROR_TIMEOUT @a timeout elapsed first - * @retval ERROR_INVALID_STATE another task called system_event_unsubscribe() on @a sub while - * this call was blocked + * @warning Cannot be called concurrently from different tasks. Each task must have its own subscription. + * @param[in,out] sub subscription to poll, as passed to system_event_subscribe() + * @retval ERROR_NONE a new event arrived - read it via @a sub->event or system_event_get_data() + * @retval ERROR_TIMEOUT no new event since the last call + * @retval ERROR_INVALID_STATE @a sub was unsubscribed (best-effort diagnostic, not guaranteed - + * see system_event_unsubscribe()'s @warning) */ -error_t system_event_await(struct SystemEventSubscription* sub, TickType_t timeout); +error_t system_event_poll(struct SystemEventSubscription* sub); /** * Copies @a sub's current event payload (the data from the most recent system_event_emit() diff --git a/TactilityKernel/source/concurrent/task_event_group.cpp b/TactilityKernel/source/concurrent/task_event_group.cpp new file mode 100644 index 000000000..383ef904f --- /dev/null +++ b/TactilityKernel/source/concurrent/task_event_group.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +extern "C" { + +void task_event_group_construct(TaskEventGroup* group) { + event_group_construct(&group->internal.handle); + mutex_construct(&group->internal.bit_mutex); + group->claimed_bits = 0; +} + +void task_event_group_destruct(TaskEventGroup* group) { + event_group_destruct(&group->internal.handle); + mutex_destruct(&group->internal.bit_mutex); + group->claimed_bits = 0; +} + +error_t task_event_group_claim_bit(TaskEventGroup* group, uint32_t* out_bit) { + mutex_lock(&group->internal.bit_mutex); + + error_t result = ERROR_RESOURCE; + for (uint32_t i = 0; i < TASK_EVENT_GROUP_MAX_BITS; i++) { + uint32_t bit = 1u << i; + if ((group->claimed_bits & bit) == 0) { + group->claimed_bits |= bit; + *out_bit = bit; + result = ERROR_NONE; + break; + } + } + + mutex_unlock(&group->internal.bit_mutex); + return result; +} + +void task_event_group_release_bit(TaskEventGroup* group, uint32_t bit) { + // Clear the real flag, not just the claim bookkeeping: a signal nobody drained before + // releasing (e.g. a courtesy nudge on unsubscribe with no one currently waiting) would + // otherwise sit set in the underlying event group and hand the next claimant of this same + // bit a false "already fired" wake. + event_group_clear(group->internal.handle, bit); + + mutex_lock(&group->internal.bit_mutex); + group->claimed_bits &= ~bit; + mutex_unlock(&group->internal.bit_mutex); +} + +error_t task_event_group_signal(TaskEventGroup* group, uint32_t bit) { + return event_group_set(group->internal.handle, bit); +} + +error_t task_event_group_wait( + TaskEventGroup* group, + uint32_t bits_mask, + bool await_all, + uint32_t* out_flags, + TickType_t timeout +) { + return event_group_wait(group->internal.handle, bits_mask, await_all, true, out_flags, timeout); +} + +error_t task_event_group_wait_any(TaskEventGroup* group, uint32_t* out_flags, TickType_t timeout) { + mutex_lock(&group->internal.bit_mutex); + uint32_t mask = group->claimed_bits; + mutex_unlock(&group->internal.bit_mutex); + + if (mask == 0) { + // xEventGroupWaitBits() asserts on a zero mask - nothing claimed means nothing to wait + // for, so this is the correct outcome anyway. + return ERROR_TIMEOUT; + } + + return task_event_group_wait(group, mask, false, out_flags, timeout); +} + +} // extern "C" diff --git a/TactilityKernel/source/drivers/wifi.cpp b/TactilityKernel/source/drivers/wifi.cpp index 8ac182d11..5c6c917ec 100644 --- a/TactilityKernel/source/drivers/wifi.cpp +++ b/TactilityKernel/source/drivers/wifi.cpp @@ -7,13 +7,12 @@ extern "C" { -struct Device* wifi_find_first_registered_device() { - struct Device* found = nullptr; - device_for_each_of_type(&WIFI_TYPE, &found, [](struct Device* dev, void* ctx) -> bool { - *static_cast(ctx) = dev; - return false; - }); - return found; +error_t wifi_set_radio_on(struct Device* device) { + return WIFI_API(device)->set_radio_on(device); +} + +error_t wifi_set_radio_off(struct Device* device) { + return WIFI_API(device)->set_radio_off(device); } error_t wifi_get_radio_state(struct Device* device, enum WifiRadioState* state) { @@ -60,12 +59,36 @@ error_t wifi_station_get_rssi(struct Device* device, int32_t* rssi) { return WIFI_API(device)->station_get_rssi(device, rssi); } -error_t wifi_add_event_callback(struct Device* device, void* callback_context, WifiEventCallback callback) { - return WIFI_API(device)->add_event_callback(device, callback_context, callback); +error_t wifi_event_subscribe(struct Device* device, struct WifiEventSubscription* sub, struct TaskEventGroup* event_group) { + mutex_construct(&sub->internal.ring_mutex); + sub->internal.head = 0; + sub->internal.count = 0; + + error_t result = WIFI_API(device)->event_subscribe(device, sub, event_group); + if (result != ERROR_NONE) { + mutex_destruct(&sub->internal.ring_mutex); + } + return result; +} + +error_t wifi_event_unsubscribe(struct Device* device, struct WifiEventSubscription* sub) { + error_t result = WIFI_API(device)->event_unsubscribe(device, sub); + if (result == ERROR_NONE) { + mutex_destruct(&sub->internal.ring_mutex); + } + return result; } -error_t wifi_remove_event_callback(struct Device* device, WifiEventCallback callback) { - return WIFI_API(device)->remove_event_callback(device, callback); +error_t wifi_event_poll(struct WifiEventSubscription* sub, struct WifiEvent* out_event) { + mutex_lock(&sub->internal.ring_mutex); + bool has_event = sub->internal.count > 0; + if (has_event) { + *out_event = sub->internal.queue[sub->internal.head]; + sub->internal.head = (sub->internal.head + 1) % WIFI_EVENT_QUEUE_CAPACITY; + sub->internal.count--; + } + mutex_unlock(&sub->internal.ring_mutex); + return has_event ? ERROR_NONE : ERROR_TIMEOUT; } error_t wifi_get_firmware_ops(struct Device* device, const struct FirmwareOps** ops, void** ctx) { diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index d55076948..accedeada 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -1,6 +1,8 @@ #include +#include #include #include +#include #include #include #include @@ -379,7 +381,8 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(camera_capture_jpeg), DEFINE_MODULE_SYMBOL(CAMERA_TYPE), // drivers/wifi - DEFINE_MODULE_SYMBOL(wifi_find_first_registered_device), + DEFINE_MODULE_SYMBOL(wifi_set_radio_on), + DEFINE_MODULE_SYMBOL(wifi_set_radio_off), DEFINE_MODULE_SYMBOL(wifi_get_radio_state), DEFINE_MODULE_SYMBOL(wifi_get_station_state), DEFINE_MODULE_SYMBOL(wifi_get_access_point_state), @@ -391,8 +394,9 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(wifi_station_connect), DEFINE_MODULE_SYMBOL(wifi_station_disconnect), DEFINE_MODULE_SYMBOL(wifi_station_get_rssi), - DEFINE_MODULE_SYMBOL(wifi_add_event_callback), - DEFINE_MODULE_SYMBOL(wifi_remove_event_callback), + DEFINE_MODULE_SYMBOL(wifi_event_subscribe), + DEFINE_MODULE_SYMBOL(wifi_event_unsubscribe), + DEFINE_MODULE_SYMBOL(wifi_event_poll), DEFINE_MODULE_SYMBOL(wifi_get_firmware_ops), DEFINE_MODULE_SYMBOL(WIFI_TYPE), // wifi_auto_scan @@ -471,6 +475,14 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(event_group_clear), DEFINE_MODULE_SYMBOL(event_group_get), DEFINE_MODULE_SYMBOL(event_group_wait), + // concurrent/task_event_group + DEFINE_MODULE_SYMBOL(task_event_group_construct), + DEFINE_MODULE_SYMBOL(task_event_group_destruct), + DEFINE_MODULE_SYMBOL(task_event_group_claim_bit), + DEFINE_MODULE_SYMBOL(task_event_group_release_bit), + DEFINE_MODULE_SYMBOL(task_event_group_signal), + DEFINE_MODULE_SYMBOL(task_event_group_wait), + DEFINE_MODULE_SYMBOL(task_event_group_wait_any), // concurrent/thread DEFINE_MODULE_SYMBOL(thread_alloc), DEFINE_MODULE_SYMBOL(thread_alloc_full), @@ -501,6 +513,8 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(timer_set_callback_priority), // error DEFINE_MODULE_SYMBOL(error_to_string), + // check + DEFINE_MODULE_SYMBOL(__crash), // log #ifndef ESP_PLATFORM DEFINE_MODULE_SYMBOL(log_generic), diff --git a/TactilityKernel/source/system_event.cpp b/TactilityKernel/source/system_event.cpp index 73c07ffac..d77602a6f 100644 --- a/TactilityKernel/source/system_event.cpp +++ b/TactilityKernel/source/system_event.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include @@ -30,9 +29,9 @@ struct KernelEventMutex { static KernelEventMutex subscriptions_mutex; // Intrusive singly-linked list of poll subscriptions (system_event_subscribe()/_unsubscribe()/ -// _await()), separate from the callback-based `subscriptions` vector above. Guarded by its own -// mutex since notifying a poll subscriber never invokes caller code (just a memcpy and an -// xTaskNotifyGive), so there is no reentrancy concern requiring a snapshot-then-unlock dance. +// _poll()), separate from the callback-based `subscriptions` vector above. Guarded by its own +// mutex since notifying a poll subscriber never invokes caller code (just a memcpy and a +// task_event_group_signal), so there is no reentrancy concern requiring a snapshot-then-unlock dance. static SystemEventSubscription* poll_subscriptions = nullptr; static KernelEventMutex poll_subscriptions_mutex; @@ -68,9 +67,9 @@ error_t system_event_callback_remove( return result; } -// Copies `data` into every current poll subscriber of `type` and signals its wakeup semaphore. +// Copies `data` into every current poll subscriber of `type` and signals its wakeup bit. // Held entirely under the lock: unlike the callback path, this never invokes caller code -// (just a memcpy and a semaphore give), so there is nothing that could reenter and deadlock. +// (just a memcpy and a signal), so there is nothing that could reenter and deadlock. static void notify_poll_subscribers( SystemEventType type, uint64_t timestamp, @@ -88,7 +87,7 @@ static void notify_poll_subscribers( } sub->event.data_len = copied_len; sub->internal.sequence++; - xSemaphoreGive(sub->internal.semaphore); + task_event_group_signal(sub->internal.event_group, sub->bit); } } @@ -165,46 +164,26 @@ error_t system_event_emit( return ERROR_NONE; } -error_t system_event_subscribe(SystemEventSubscription* sub) { - // Wait out any system_event_unsubscribe() call still draining old awaiters for this same - // `sub` on another task (see internal.unsubscribe_in_progress). waiter_count/cancelled - // belong to `sub` itself, not to a given registration - reusing `sub` before that call - // finishes would reset them out from under it, and could hand out a fresh semaphore for it - // to then promptly delete instead of the old one, while an old awaiter is still blocked on - // the real old semaphore. - while (true) { - mutex_lock(&poll_subscriptions_mutex.handle); - bool busy = sub->internal.unsubscribe_in_progress; - mutex_unlock(&poll_subscriptions_mutex.handle); - - if (!busy) { - break; - } - delay_ticks(pdMS_TO_TICKS(10)); - } - - SemaphoreHandle_t semaphore = xSemaphoreCreateBinary(); - if (semaphore == nullptr) { - return ERROR_OUT_OF_MEMORY; +error_t system_event_subscribe(SystemEventSubscription* sub, TaskEventGroup* event_group) { + uint32_t bit; + error_t claim_result = task_event_group_claim_bit(event_group, &bit); + if (claim_result != ERROR_NONE) { + return claim_result; } mutex_lock(&poll_subscriptions_mutex.handle); - // Check-and-insert in one critical section: registering the same `sub` twice would link - // it into a list that already contains it, creating a cycle that notify_poll_subscribers() - // would then traverse forever while holding this same mutex. - for (SystemEventSubscription* existing = poll_subscriptions; existing != nullptr; existing = existing->internal.next) { - if (existing == sub) { - mutex_unlock(&poll_subscriptions_mutex.handle); - vSemaphoreDelete(semaphore); - return ERROR_INVALID_STATE; - } + // Avoid cyclic subscription list that would loop forever + if (poll_subscriptions == sub) { + mutex_unlock(&poll_subscriptions_mutex.handle); + task_event_group_release_bit(event_group, bit); + return ERROR_INVALID_STATE; } - sub->internal.semaphore = semaphore; + sub->internal.event_group = event_group; + sub->bit = bit; sub->internal.sequence = 0; sub->internal.consumed_sequence = 0; - sub->internal.waiter_count = 0; sub->internal.cancelled = false; sub->event.data_len = 0; sub->internal.next = poll_subscriptions; @@ -217,7 +196,6 @@ error_t system_event_subscribe(SystemEventSubscription* sub) { error_t system_event_unsubscribe(SystemEventSubscription* sub) { error_t result = ERROR_NOT_FOUND; - SemaphoreHandle_t semaphore_to_delete = nullptr; mutex_lock(&poll_subscriptions_mutex.handle); for (SystemEventSubscription** link = &poll_subscriptions; *link != nullptr; link = &(*link)->internal.next) { @@ -228,99 +206,34 @@ error_t system_event_unsubscribe(SystemEventSubscription* sub) { } } if (result == ERROR_NONE) { - // Unlinked first, so notify_poll_subscribers() can no longer reach this subscription. - // Mark it cancelled (checked by system_event_await()'s loop) and capture the semaphore - // handle into a local variable rather than deleting it via sub->internal.semaphore - // directly - a concurrent system_event_subscribe() re-registering this same `sub` after - // this point would overwrite that field with a freshly created semaphore, and we must - // not delete the wrong (newly active) one. + // Diagnostic only now (see @warning) - not relied on for safety. Best-effort nudge for a + // task that might still be blocked in task_event_group_wait() on this bit; unlike before, + // this does not wait for it to leave before the bit is released. sub->internal.cancelled = true; - // Blocks a concurrent system_event_subscribe() from reusing `sub` until this whole - // call returns - see internal.unsubscribe_in_progress and system_event_subscribe(). - sub->internal.unsubscribe_in_progress = true; - semaphore_to_delete = sub->internal.semaphore; - sub->internal.semaphore = nullptr; - } - mutex_unlock(&poll_subscriptions_mutex.handle); - - if (result != ERROR_NONE) { - return result; + task_event_group_signal(sub->internal.event_group, sub->bit); + task_event_group_release_bit(sub->internal.event_group, sub->bit); } - - // Nudge any task already blocked in system_event_await() (it captured its own local copy - // of this same semaphore handle before this point, so it's unaffected by the field having - // just been cleared above) so it re-checks `cancelled` and bails out now instead of waiting - // out its full timeout, then wait for it to actually leave the semaphore before deleting it - // - FreeRTOS requires no task be blocked on a semaphore when it's deleted. - xSemaphoreGive(semaphore_to_delete); - while (true) { - mutex_lock(&poll_subscriptions_mutex.handle); - bool still_waiting = sub->internal.waiter_count > 0; - mutex_unlock(&poll_subscriptions_mutex.handle); - - if (!still_waiting) { - break; - } - delay_ticks(pdMS_TO_TICKS(10)); - } - - vSemaphoreDelete(semaphore_to_delete); - - // Reset under the lock, together, as the last step - only past this point is `sub` safe - // for system_event_subscribe() to reuse (see internal.unsubscribe_in_progress and the - // busy-wait at the top of system_event_subscribe()). - mutex_lock(&poll_subscriptions_mutex.handle); - sub->internal.cancelled = false; - sub->internal.unsubscribe_in_progress = false; mutex_unlock(&poll_subscriptions_mutex.handle); - return ERROR_NONE; + return result; } -error_t system_event_await(SystemEventSubscription* sub, TickType_t timeout) { +error_t system_event_poll(SystemEventSubscription* sub) { mutex_lock(&poll_subscriptions_mutex.handle); - SemaphoreHandle_t semaphore = sub->internal.semaphore; - sub->internal.waiter_count++; - mutex_unlock(&poll_subscriptions_mutex.handle); - - error_t result = ERROR_NONE; - - // sequence/consumed_sequence are written by notify_poll_subscribers() under - // poll_subscriptions_mutex - read (and, on a match, updated) under the same lock each - // iteration, rather than compared lock-free, so a concurrent emit can't land between an - // unlocked read and this loop acting on it. - // - // Compare against consumed_sequence, not a sequence snapshot taken now - an emit that - // landed between system_event_subscribe() and this call already incremented sequence and - // gave the semaphore, so that event is pending but unconsumed. Snapshotting "now" would - // make the loop wait for yet another event instead of returning this already-pending one. - while (true) { - mutex_lock(&poll_subscriptions_mutex.handle); - bool pending = sub->internal.sequence != sub->internal.consumed_sequence; - bool cancelled = sub->internal.cancelled; - if (pending) { - sub->internal.consumed_sequence = sub->internal.sequence; - } - mutex_unlock(&poll_subscriptions_mutex.handle); - - if (pending) { - break; - } - if (cancelled) { - result = ERROR_INVALID_STATE; - break; - } - if (xSemaphoreTake(semaphore, timeout) == pdFALSE) { - result = ERROR_TIMEOUT; - break; - } + bool pending = sub->internal.sequence != sub->internal.consumed_sequence; + bool cancelled = sub->internal.cancelled; + if (pending) { + sub->internal.consumed_sequence = sub->internal.sequence; } - - mutex_lock(&poll_subscriptions_mutex.handle); - sub->internal.waiter_count--; mutex_unlock(&poll_subscriptions_mutex.handle); - return result; + if (pending) { + return ERROR_NONE; + } + if (cancelled) { + return ERROR_INVALID_STATE; + } + return ERROR_TIMEOUT; } error_t system_event_get_data(SystemEventSubscription* sub, uint8_t* data, size_t data_len) { diff --git a/TactilityKernel/tests/source/system_event_test.cpp b/TactilityKernel/tests/source/system_event_test.cpp index 698a7d855..4a46dca79 100644 --- a/TactilityKernel/tests/source/system_event_test.cpp +++ b/TactilityKernel/tests/source/system_event_test.cpp @@ -216,17 +216,20 @@ TEST_CASE("system_event_emit is safe when a callback subscribes, unsubscribes an system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, listener_b); } -// gps.h-style poll subscription: system_event_subscribe()/_await()/_unsubscribe(). +// gps.h-style poll subscription: system_event_subscribe()/_poll()/_unsubscribe(). // -// system_event_await() only detects sequence increments that happen *after* it starts -// waiting (same as gps_api_event_await()), so the emit must be started from another task -// while this one is already blocked in await() - emitting first and awaiting after would -// race the notification the same way it would with any FreeRTOS task-notify consumer. +// system_event_poll() only detects sequence increments that happened before it's called (same +// as gps_api_event_await()), so tests that need to observe an emit from another task block via +// task_event_group_wait() first - emitting before that wait started would race the notification +// the same way it would with any FreeRTOS task-notify consumer. + +TEST_CASE("system_event_subscribe/_poll deliver the event payload by value") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); -TEST_CASE("system_event_subscribe/_await deliver the event payload by value") { SystemEventSubscription sub {}; sub.event.type = KERNEL_EVENT_NETWORK_CONNECTED; - CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE); + CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE); NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE }; auto* thread = thread_alloc_full( @@ -243,7 +246,8 @@ TEST_CASE("system_event_subscribe/_await deliver the event payload by value") { ); CHECK_EQ(thread_start(thread), ERROR_NONE); - CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE); + CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); + CHECK_EQ(system_event_poll(&sub), ERROR_NONE); NetworkConnectedEvent received {}; CHECK_EQ(system_event_get_data(&sub, reinterpret_cast(&received), sizeof(received)), ERROR_NONE); @@ -255,62 +259,78 @@ TEST_CASE("system_event_subscribe/_await deliver the event payload by value") { CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE); CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NOT_FOUND); + + task_event_group_destruct(&event_group); } -TEST_CASE("system_event_await returns a matching event that arrived before it started waiting") { +TEST_CASE("system_event_poll returns a matching event that arrived before it was called") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + SystemEventSubscription sub {}; sub.event.type = KERNEL_EVENT_NETWORK_CONNECTED; - CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE); + CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE); - // Same-thread emit, no background thread needed: unlike the "detects a change after it - // starts waiting" tests above, this is exactly the case system_event_await() must handle - - // sequence already moved ahead of consumed_sequence before await() is even called. + // Same-thread emit, no background thread or wait needed: unlike the "detects a change after + // it starts waiting" tests above, this is exactly the case system_event_poll() must handle - + // sequence already moved ahead of consumed_sequence before poll() is even called. NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE }; CHECK_EQ(system_event_emit(KERNEL_EVENT_NETWORK_CONNECTED, &connected, sizeof(connected)), ERROR_NONE); - CHECK_EQ(system_event_await(&sub, 0), ERROR_NONE); + CHECK_EQ(system_event_poll(&sub), ERROR_NONE); NetworkConnectedEvent received {}; CHECK_EQ(system_event_get_data(&sub, reinterpret_cast(&received), sizeof(received)), ERROR_NONE); CHECK_EQ(received.ipv4_addr, connected.ipv4_addr); CHECK_EQ(received.gateway, connected.gateway); - // The pending event was consumed by the call above - a second await() with no further - // emit must time out rather than returning the same event again. - CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT); + // The pending event was consumed by the call above - a second poll() with no further emit + // must time out rather than returning the same event again. + CHECK_EQ(system_event_poll(&sub), ERROR_TIMEOUT); system_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); } -TEST_CASE("system_event_await times out when no matching event has arrived") { +TEST_CASE("system_event_poll times out when no matching event has arrived") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + SystemEventSubscription sub {}; sub.event.type = KERNEL_EVENT_TIME_CHANGED; - system_event_subscribe(&sub); + system_event_subscribe(&sub, &event_group); - CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT); + CHECK_EQ(system_event_poll(&sub), ERROR_TIMEOUT); system_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); } TEST_CASE("system_event_emit does not notify a poll subscriber of a different type") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + SystemEventSubscription sub {}; sub.event.type = KERNEL_EVENT_BOOT_COMPLETED; - system_event_subscribe(&sub); + system_event_subscribe(&sub, &event_group); system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0); - CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT); + CHECK_EQ(system_event_poll(&sub), ERROR_TIMEOUT); system_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); } TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the buffer untouched") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + SystemEventSubscription sub {}; sub.event.type = KERNEL_EVENT_NETWORK_DISCONNECTED; - CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE); + CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE); - // system_event_await() only detects sequence increments that happen *after* it starts - // waiting (see the comment above), so the emit must come from another task while this one - // is already blocked in await() - same pattern as the payload-delivery test above. + // Background emit + wait, same pattern as the payload-delivery test above - see the comment + // near the top of the file. NetworkDisconnectedEvent disconnected { .device = nullptr }; auto* thread = thread_alloc_full( "system-event-emitter", @@ -325,7 +345,8 @@ TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the bu -1 ); CHECK_EQ(thread_start(thread), ERROR_NONE); - CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE); + CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); + CHECK_EQ(system_event_poll(&sub), ERROR_NONE); CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE); thread_free(thread); @@ -337,12 +358,16 @@ TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the bu CHECK_EQ(system_event_get_data(&sub, exact, sizeof(exact)), ERROR_NONE); system_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); } TEST_CASE("system_event_get_data on a subscription with no payload copies nothing and succeeds") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + SystemEventSubscription sub {}; sub.event.type = KERNEL_EVENT_BOOT_COMPLETED; - CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE); + CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE); auto* thread = thread_alloc_full( "system-event-emitter", @@ -356,7 +381,8 @@ TEST_CASE("system_event_get_data on a subscription with no payload copies nothin -1 ); CHECK_EQ(thread_start(thread), ERROR_NONE); - CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE); + CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); + CHECK_EQ(system_event_poll(&sub), ERROR_NONE); CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE); thread_free(thread); @@ -365,59 +391,44 @@ TEST_CASE("system_event_get_data on a subscription with no payload copies nothin CHECK_EQ(buffer[0], 0x42); // untouched - nothing to copy system_event_unsubscribe(&sub); + task_event_group_destruct(&event_group); } -// Regression coverage for system_event_unsubscribe() racing a task blocked in -// system_event_await() on the same subscription, and for reusing a subscription node after -// unsubscribing it - see the @warning on system_event_unsubscribe() in system_event.h. +// Regression coverage for system_event_unsubscribe()'s (now best-effort, not guaranteed - see +// its @warning in system_event.h) diagnostic signal, and for reusing a subscription node after +// unsubscribing it. + +TEST_CASE("system_event_poll returns ERROR_INVALID_STATE after unsubscribe (diagnostic, best-effort)") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); -TEST_CASE("system_event_unsubscribe wakes a task blocked in system_event_await with ERROR_INVALID_STATE") { SystemEventSubscription sub {}; sub.event.type = KERNEL_EVENT_SERVICE_STARTED; - CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE); - - auto* thread = thread_alloc_full( - "system-event-awaiter", - 4096, - [](void* context) { - auto* awaited_sub = static_cast(context); - // Long timeout - the point is that unsubscribe() wakes this early, not that it - // eventually times out on its own. - return static_cast(system_event_await(awaited_sub, pdMS_TO_TICKS(5000))); - }, - &sub, - -1 - ); - CHECK_EQ(thread_start(thread), ERROR_NONE); - - // Give the awaiter task a moment to actually reach xSemaphoreTake() before unsubscribing - - // otherwise this test wouldn't exercise the "already blocked" race at all. - delay_millis(20); - - // Must return promptly (nudging the blocked awaiter awake), not by waiting out its timeout. - TickType_t before = get_ticks(); + CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE); CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE); - CHECK_LT(get_ticks() - before, pdMS_TO_TICKS(1000)); - CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE); - CHECK_EQ(thread_get_return_code(thread), ERROR_INVALID_STATE); - thread_free(thread); + // `sub` is still caller-owned storage after unsubscribe - polling it directly (rather than + // still being blocked in task_event_group_wait() on it, which system_event_unsubscribe()'s + // @warning now says not to do) is the one remaining diagnostic case `cancelled` covers. + CHECK_EQ(system_event_poll(&sub), ERROR_INVALID_STATE); - // A second unsubscribe() has nothing left to do. - CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NOT_FOUND); + task_event_group_destruct(&event_group); } TEST_CASE("a subscription node can be re-subscribed after system_event_unsubscribe") { + TaskEventGroup event_group {}; + task_event_group_construct(&event_group); + SystemEventSubscription sub {}; sub.event.type = KERNEL_EVENT_SERVICE_STOPPED; - CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE); + CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE); CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE); // Re-registering the same node (same storage, not a fresh SystemEventSubscription) must - // work as if it were new - a fresh semaphore, and no leftover `cancelled` state from the + // work as if it were new - a fresh bit, and no leftover `cancelled` state from the // unsubscribe() above causing an immediate spurious ERROR_INVALID_STATE below. - CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE); + CHECK_EQ(system_event_subscribe(&sub, &event_group), ERROR_NONE); auto* thread = thread_alloc_full( "system-event-emitter", @@ -431,9 +442,12 @@ TEST_CASE("a subscription node can be re-subscribed after system_event_unsubscri -1 ); CHECK_EQ(thread_start(thread), ERROR_NONE); - CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE); + CHECK_EQ(task_event_group_wait(&event_group, sub.bit, false, nullptr, pdMS_TO_TICKS(2000)), ERROR_NONE); + CHECK_EQ(system_event_poll(&sub), ERROR_NONE); CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(2000), pdMS_TO_TICKS(1)), ERROR_NONE); thread_free(thread); CHECK_EQ(system_event_unsubscribe(&sub), ERROR_NONE); + + task_event_group_destruct(&event_group); } diff --git a/TactilityKernel/tests/source/task_event_group_test.cpp b/TactilityKernel/tests/source/task_event_group_test.cpp new file mode 100644 index 000000000..b26159bef --- /dev/null +++ b/TactilityKernel/tests/source/task_event_group_test.cpp @@ -0,0 +1,42 @@ +#include "doctest.h" + +#include + +TEST_CASE("task_event_group_wait_any wakes on any bit currently claimed in the group") { + TaskEventGroup group {}; + task_event_group_construct(&group); + + uint32_t bit_a, bit_b; + CHECK_EQ(task_event_group_claim_bit(&group, &bit_a), ERROR_NONE); + CHECK_EQ(task_event_group_claim_bit(&group, &bit_b), ERROR_NONE); + CHECK_NE(bit_a, bit_b); + + CHECK_EQ(task_event_group_signal(&group, bit_b), ERROR_NONE); + + uint32_t out_flags = 0; + CHECK_EQ(task_event_group_wait_any(&group, &out_flags, 0), ERROR_NONE); + CHECK_EQ(out_flags, bit_b); + + task_event_group_destruct(&group); +} + +TEST_CASE("task_event_group_wait_any times out immediately when the group has no claimed bits") { + TaskEventGroup group {}; + task_event_group_construct(&group); + + CHECK_EQ(task_event_group_wait_any(&group, nullptr, 0), ERROR_TIMEOUT); + + task_event_group_destruct(&group); +} + +TEST_CASE("task_event_group_wait_any times out when claimed bits exist but none are signalled") { + TaskEventGroup group {}; + task_event_group_construct(&group); + + uint32_t bit; + CHECK_EQ(task_event_group_claim_bit(&group, &bit), ERROR_NONE); + + CHECK_EQ(task_event_group_wait_any(&group, nullptr, 0), ERROR_TIMEOUT); + + task_event_group_destruct(&group); +} diff --git a/Tests/SdkIntegration/main/Source/main.c b/Tests/SdkIntegration/main/Source/main.c index 93c49bc75..54db9bc63 100644 --- a/Tests/SdkIntegration/main/Source/main.c +++ b/Tests/SdkIntegration/main/Source/main.c @@ -4,6 +4,8 @@ #include +#include + #include #include @@ -21,24 +23,30 @@ static void create_widgets(lv_obj_t* parent, void* userData) { int main(int argc, char* argv[]) { AppInstanceId app_instance_id = app_scheduler_current_app_id(); - struct AppEventSubscription sub = { .app_instance_id = app_instance_id }; - app_event_subscribe(&sub); + struct TaskEventGroup event_group = {0}; + task_event_group_construct(&event_group); + + struct AppEventSubscription sub = {0}; + check(app_event_subscribe(&sub, &event_group) == ERROR_NONE); WindowId window = window_manager_create(app_instance_id, create_widgets, NULL); bool should_close = false; while (!should_close) { + task_event_group_wait_any(&event_group, NULL, portMAX_DELAY); + struct AppEvent event; - if (app_event_await(&sub, &event, portMAX_DELAY) != ERROR_NONE) { - break; - } - if (event.type == APP_EVENT_CLOSE) { - should_close = true; + while (app_event_poll(&sub, &event) == ERROR_NONE) { + if (event.type == APP_EVENT_CLOSE) { + should_close = true; + break; + } } } window_manager_remove(window); - app_event_unsubscribe(&sub); + check(app_event_unsubscribe(&sub) == ERROR_NONE); + task_event_group_destruct(&event_group); return 0; }