diff --git a/Buildscripts/TactilitySDK/CMakeLists.txt b/Buildscripts/TactilitySDK/CMakeLists.txt index ffa734c57..67107519e 100644 --- a/Buildscripts/TactilitySDK/CMakeLists.txt +++ b/Buildscripts/TactilitySDK/CMakeLists.txt @@ -7,7 +7,7 @@ idf_component_register( "Libraries/minmea/include" "Modules/lvgl-module/include" # DRIVER_INCLUDE_DIRS_PLACEHOLDER - REQUIRES esp_timer app-module crypt-module gps-module lvgl-module lvgl-window-manager-module service-module + REQUIRES esp_timer minitar app-module crypt-module gps-module lvgl-module lvgl-window-manager-module service-module ) # Regular and core features diff --git a/Documentation/ideas.md b/Documentation/ideas.md index 963ffabaa..c1e06e060 100644 --- a/Documentation/ideas.md +++ b/Documentation/ideas.md @@ -30,7 +30,7 @@ - bluetooth: various getters for child devices do not change ref counting, but they should - Improve kernel_init.cpp (and other modules): create driver_ensure_added() and driver_ensure_destructed() - Remove and migrate `Include/Tactility/kernel/Kernel.h` into `tactility/delay.h` -- Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual modulej. +- Drivers/audio-codec-module is not a module. Move it somewhere else. Or make it an actual module. - LilyGO T-Dongle S3: 1 button control, stop auto-launching web server - Core2: support power off via software - Create `#define` for empty module (for modules that fully rely on device.properties and don't define drivers or have start/stop logic) @@ -53,6 +53,7 @@ ## Medium Priority - Consider using https://github.com/Graphify-Labs/graphify +- Consider implementing LVGL gridnav in apps https://lvgl.io/docs/open/9.3/details/auxiliary-modules/gridnav.html - Implement a LED kernel driver (single colour and RGB, plain GPIO and PWM) - Make USB host driver disabled by default, so it doesn't consume memory - Filtering for apps in App Hub: diff --git a/Modules/app-esp32-module/LICENSE-Apache-2.0.md b/Modules/app-esp32-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Modules/app-esp32-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Modules/app-module/LICENSE-Apache-2.0.md b/Modules/app-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Modules/app-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Modules/app-module/include/app/location.h b/Modules/app-module/include/app/location.h index 721c07772..eb8703ca0 100644 --- a/Modules/app-module/include/app/location.h +++ b/Modules/app-module/include/app/location.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 #pragma once #ifdef __cplusplus diff --git a/Modules/app-module/include/app/manager.h b/Modules/app-module/include/app/manager.h index 55f6c7943..e35ba5db2 100644 --- a/Modules/app-module/include/app/manager.h +++ b/Modules/app-module/include/app/manager.h @@ -31,9 +31,10 @@ error_t app_manager_remove(const char* id); const struct AppManifest* app_manager_find_manifest(const char* id); /** - * Calls @a visitor once for every registered manifest (e.g. for AppList/Settings to enumerate - * apps to show). Iteration order is unspecified. Safe to call app_manager_add()/_remove() from - * within @a visitor is NOT guaranteed - do not mutate the registry from inside the callback. + * Calls `@a` visitor once for every registered manifest. Iteration order is unspecified. + * `@warning` `@a` visitor runs with app-module's internal registry lock held. Do not call any + * app_manager_*() function from inside `@a` visitor - copy out what you need and act on it after + * this call returns. */ typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* context); void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context); diff --git a/Modules/app-module/include/app/metadata.h b/Modules/app-module/include/app/metadata.h index 4b759f4ae..6cd039ec8 100644 --- a/Modules/app-module/include/app/metadata.h +++ b/Modules/app-module/include/app/metadata.h @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 #pragma once #include diff --git a/Modules/app-module/include/app/module.h b/Modules/app-module/include/app/module.h index 5bfd3ee67..d54dba82f 100644 --- a/Modules/app-module/include/app/module.h +++ b/Modules/app-module/include/app/module.h @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #pragma once +#include + #ifdef __cplusplus extern "C" { #endif diff --git a/Modules/app-module/include/app/paths.h b/Modules/app-module/include/app/paths.h new file mode 100644 index 000000000..0042c956b --- /dev/null +++ b/Modules/app-module/include/app/paths.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Get the user data directory for an app. Survives OS upgrades. No trailing "/". + * @param[in] app_id non-null app id + * @param[out] out_path buffer to store the path + * @param[in] out_path_size size of the output buffer + * @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small + * @retval ERROR_NONE on success + */ +error_t app_paths_get_user_data_directory(const char* app_id, char* out_path, size_t out_path_size); + +/** + * @brief Get a path within the user data directory for an app. + * @param[in] app_id non-null app id + * @param[in] child_path path without a "/" prefix + * @param[out] out_path buffer to store the path + * @param[in] out_path_size size of the output buffer + * @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small + * @retval ERROR_NONE on success + */ +error_t app_paths_get_user_data_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size); + +/** + * @brief Get the assets directory for an app. Do not store configuration data here. No trailing "/". + * @param[in] app_id non-null app id + * @param[out] out_path buffer to store the path + * @param[in] out_path_size size of the output buffer + * @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small + * @retval ERROR_NONE on success + */ +error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_t out_path_size); + +/** + * @brief Get a path within the assets directory for an app. + * @param[in] app_id non-null app id + * @param[in] child_path path without a "/" prefix + * @param[out] out_path buffer to store the path + * @param[in] out_path_size size of the output buffer + * @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small + * @retval ERROR_NONE on success + */ +error_t app_paths_get_assets_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/app-module/private/app/private/app_fs.h b/Modules/app-module/private/app/private/app_fs.h index 03b4d77c5..d4dff2a14 100644 --- a/Modules/app-module/private/app/private/app_fs.h +++ b/Modules/app-module/private/app/private/app_fs.h @@ -6,8 +6,7 @@ // upward on Tactility::file, so this is a small local re-implementation (see // app_metadata_parsing.cpp for the same constraint applied to properties-file loading). -#include "tactility/filesystem/file_mutex.h" - +#include #include #include @@ -35,9 +34,16 @@ inline bool app_fs_is_file(const std::string& path) { return retval; } -// Appends the full path of every direct subdirectory of @a path to @a out. No-op (not an error) -// if @a path can't be opened. +// Appends the full path of every direct subdirectory of @a path to @a out. +// No-op (not an error) if @a path can't be opened. inline void app_fs_list_direct_subdirectories(const std::string& path, std::vector& out) { + // Collect child names while the directory lock is held, then release it before classifying + // each one with app_fs_is_directory() - that function looks up and locks a FileMutex too, + // and file_mutex_get() resolves a child path to the same registered mutex as its parent + // mount. Calling it while still holding the directory's own lock would be a nested + // acquisition of that same (possibly non-recursive) mutex, and could self-deadlock. + std::vector children; + FileMutex file_mutex; file_mutex_get(&file_mutex, path.c_str()); file_mutex_lock(&file_mutex); @@ -52,12 +58,15 @@ inline void app_fs_list_direct_subdirectories(const std::string& path, std::vect if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) { continue; } - auto child_path = path + "/" + entry->d_name; - if (app_fs_is_directory(child_path)) { - out.push_back(child_path); - } + children.push_back(path + "/" + entry->d_name); } closedir(dir); file_mutex_unlock(&file_mutex); + + for (const auto& child_path : children) { + if (app_fs_is_directory(child_path)) { + out.push_back(child_path); + } + } } diff --git a/Modules/app-module/private/app/private/app_ledger.h b/Modules/app-module/private/app/private/app_ledger.h index fdac66287..c7907cb13 100644 --- a/Modules/app-module/private/app/private/app_ledger.h +++ b/Modules/app-module/private/app/private/app_ledger.h @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include @@ -24,6 +24,12 @@ struct AppInstanceRecord { /** 0 for a top-level launch (app_manager_start()). Non-zero for a modal child launched via * app_manager_start_for_result() - the instance that receives this child's APP_EVENT_RESULT. */ uint32_t parent_id = 0; + + /** The task currently blocked in app_scheduler_stop() for this instance, if any - notified + * (via xTaskNotifyGive()) as the literal last action app_task_main() takes before + * vTaskDelete(), so app_scheduler_stop() can't observe completion before the task has + * actually finished running. See app_scheduler.cpp. */ + TaskHandle_t stop_waiter = nullptr; }; struct AppLedger { diff --git a/Modules/app-module/private/app/private/app_scheduler.h b/Modules/app-module/private/app/private/app_scheduler.h index 6f9069a77..d323360c3 100644 --- a/Modules/app-module/private/app/private/app_scheduler.h +++ b/Modules/app-module/private/app/private/app_scheduler.h @@ -5,8 +5,6 @@ #include -#include - /** * Owns per-app task lifecycle on behalf of app_manager_*(). AppLoaderApi implementations * stay task-agnostic; all of xTaskCreate()/vTaskDelete() happens here, as a plain FreeRTOS task diff --git a/Modules/app-module/source/app_install.cpp b/Modules/app-module/source/app_install.cpp index 3586a4034..bcbd6c8db 100644 --- a/Modules/app-module/source/app_install.cpp +++ b/Modules/app-module/source/app_install.cpp @@ -261,7 +261,7 @@ void stop_all_instances_of(const AppManifest* manifest) { } } -// Takes install_registry().mutex - caller must not already hold it. +// Caller must already hold install_registry().mutex error_t uninstall_locked(const std::string& app_id) { auto& registry = install_registry(); auto iterator = registry.apps.find(app_id); diff --git a/Modules/app-module/source/app_internal_loader.cpp b/Modules/app-module/source/app_internal_loader.cpp index d9175f188..4c447d3fe 100644 --- a/Modules/app-module/source/app_internal_loader.cpp +++ b/Modules/app-module/source/app_internal_loader.cpp @@ -39,7 +39,7 @@ void destroy_service(const ServiceManifest*, void*) { } // namespace -extern ServiceManifest app_internal_loader_service_manifest = { +ServiceManifest app_internal_loader_service_manifest = { .id = APP_LOADER_MEMORY_SERVICE_ID, .create_service = create_service, .destroy_service = destroy_service, diff --git a/Modules/app-module/source/app_paths.cpp b/Modules/app-module/source/app_paths.cpp new file mode 100644 index 000000000..eb7ac418a --- /dev/null +++ b/Modules/app-module/source/app_paths.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include +#include + +#include + +extern "C" { + +error_t app_paths_get_user_data_directory(const char* app_id, char* out_path, size_t out_path_size) { + char root[192]; + error_t error = paths_get_user_data_path(root, sizeof(root)); + if (error != ERROR_NONE) { + return error; + } + int written = std::snprintf(out_path, out_path_size, "%s/app/%s", root, app_id); + if (written < 0 || (size_t)written >= out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + return ERROR_NONE; +} + +error_t app_paths_get_user_data_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size) { + char directory[224]; + error_t error = app_paths_get_user_data_directory(app_id, directory, sizeof(directory)); + if (error != ERROR_NONE) { + return error; + } + int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path); + if (written < 0 || (size_t)written >= out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + return ERROR_NONE; +} + +error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_t out_path_size) { + char directory[224]; + error_t error = app_paths_get_user_data_directory(app_id, directory, sizeof(directory)); + if (error != ERROR_NONE) { + return error; + } + int written = std::snprintf(out_path, out_path_size, "%s/assets", directory); + if (written < 0 || (size_t)written >= out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + return ERROR_NONE; +} + +error_t app_paths_get_assets_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size) { + char directory[224]; + error_t error = app_paths_get_assets_directory(app_id, directory, sizeof(directory)); + if (error != ERROR_NONE) { + return error; + } + int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path); + if (written < 0 || (size_t)written >= out_path_size) { + return ERROR_BUFFER_OVERFLOW; + } + return ERROR_NONE; +} + +} // extern "C" diff --git a/Modules/app-module/source/app_scheduler.cpp b/Modules/app-module/source/app_scheduler.cpp index 91f3d94b8..70ce18eaf 100644 --- a/Modules/app-module/source/app_scheduler.cpp +++ b/Modules/app-module/source/app_scheduler.cpp @@ -9,10 +9,8 @@ #include #include -#include #include #include -#include #include #include @@ -49,15 +47,6 @@ void set_state(AppInstanceId app_instance_id, AppInstanceState state) { mutex_unlock(&ledger.mutex); } -TaskHandle_t get_task(AppInstanceId app_instance_id) { - auto& ledger = app_ledger(); - mutex_lock(&ledger.mutex); - auto iterator = ledger.instances.find(app_instance_id); - TaskHandle_t task = (iterator != ledger.instances.end()) ? iterator->second.task : nullptr; - mutex_unlock(&ledger.mutex); - return task; -} - void set_task(AppInstanceId app_instance_id, TaskHandle_t task) { auto& ledger = app_ledger(); mutex_lock(&ledger.mutex); @@ -68,6 +57,23 @@ void set_task(AppInstanceId app_instance_id, TaskHandle_t task) { mutex_unlock(&ledger.mutex); } +// Registers the calling task to be notified when app_instance_id's task actually finishes +// running (see app_task_main()'s exit path), and reports whether there's anything to wait for. +// @return true if the instance's ledger entry still exists (a wait was registered); false if +// the task has already fully finished (and already given any notification it would have) - +// there is nothing left to wait for. +bool register_stop_waiter(AppInstanceId app_instance_id) { + auto& ledger = app_ledger(); + mutex_lock(&ledger.mutex); + auto iterator = ledger.instances.find(app_instance_id); + bool exists = iterator != ledger.instances.end(); + if (exists) { + iterator->second.stop_waiter = xTaskGetCurrentTaskHandle(); + } + mutex_unlock(&ledger.mutex); + return exists; +} + const char* loader_service_id_for(AppLocationType type) { return (type == APP_LOCATION_MEMORY) ? APP_LOADER_MEMORY_SERVICE_ID : APP_LOADER_PATH_SERVICE_ID; } @@ -135,12 +141,23 @@ void app_task_main(void* context) { LOG_I(TAG, "Thread for %d finished", app_instance_id); - // Erase the ledger entry before self-deleting + // Erase the ledger entry before self-deleting, capturing whoever's blocked in + // app_scheduler_stop() for this instance (if anyone) so they can be notified afterward. auto& ledger = app_ledger(); mutex_lock(&ledger.mutex); + auto iterator = ledger.instances.find(app_instance_id); + TaskHandle_t stop_waiter = (iterator != ledger.instances.end()) ? iterator->second.stop_waiter : nullptr; ledger.instances.erase(app_instance_id); mutex_unlock(&ledger.mutex); + // Signal completion as the literal last action before this task ceases to exist, so + // app_scheduler_stop() can't observe "stopped" one step early (see its own comment) - + // unlike watching the ledger entry disappear, this can only happen once the task is truly + // done running. + if (stop_waiter != nullptr) { + xTaskNotifyGive(stop_waiter); + } + vTaskDelete(nullptr); } @@ -176,36 +193,45 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, snprintf(task_name, sizeof(task_name), "app_%lu", static_cast(app_instance_id)); TaskHandle_t task_handle = nullptr; - // 8192 bytes -> stack depth in words, matching what TactilityKernel's Thread wrapper does - // with the stack size it's given. - BaseType_t create_result = xTaskCreate(app_task_main, task_name, 8192 / sizeof(StackType_t), context, APP_TASK_PRIORITY, &task_handle); + // 8192 bytes -> stack depth in words, matching what TactilityKernel's Thread wrapper does with the stack size it's given. + // Created at idle priority so it can't preempt us before vTaskSuspend() below runs, then suspended immediately - + // the ledger must record the handle (set_task()) before the task can possibly observe or erase its own entry. + // (see app_scheduler_stop()'s liveness check and app_task_main()'s exit path) + BaseType_t create_result = xTaskCreate(app_task_main, task_name, 8192 / sizeof(StackType_t), context, tskIDLE_PRIORITY, &task_handle); if (create_result != pdPASS) { delete context; loader->unload(runtime); app_ledger_free_arguments(argc, argv); return ERROR_OUT_OF_MEMORY; } + vTaskSuspend(task_handle); set_task(app_instance_id, task_handle); + vTaskPrioritySet(task_handle, APP_TASK_PRIORITY); + vTaskResume(task_handle); return ERROR_NONE; } error_t app_scheduler_stop(AppInstanceId app_instance_id, TickType_t join_timeout) { - TaskHandle_t task = get_task(app_instance_id); - if (task != nullptr) { + // Drain any stale notification credit before registering as the waiter - otherwise a + // leftover give from an unrelated earlier wait on this same task (e.g. a previous + // app_scheduler_stop() call that timed out and only got notified afterward) could make the + // take below return immediately for the wrong event. Mirrors app_event_await()'s same + // defensive drain. + ulTaskNotifyTake(pdTRUE, 0); + + if (register_stop_waiter(app_instance_id)) { AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} }; app_event_emit(app_instance_id, &event); - // Poll for the task to clear its own ledger entry (see app_task_main()) - plain - // FreeRTOS has no built-in task-join primitive. - TickType_t start_ticks = get_ticks(); - while (get_task(app_instance_id) != nullptr) { - delay_ticks(pdMS_TO_TICKS(10)); - if (get_ticks() - start_ticks > join_timeout) { - LOG_W(TAG, "App instance %u did not stop in time", app_instance_id); - return ERROR_TIMEOUT; - } + // Blocks until app_task_main() gives this notification as the literal last thing it + // does before vTaskDelete() - unlike polling the ledger for the task handle to clear, + // this can't observe "stopped" while the task is still mid-exit (still running its own + // cleanup/vTaskDelete()). + if (ulTaskNotifyTake(pdTRUE, join_timeout) == 0) { + LOG_W(TAG, "App instance %u did not stop in time", app_instance_id); + return ERROR_TIMEOUT; } } diff --git a/Modules/app-module/source/symbols.cpp b/Modules/app-module/source/symbols.cpp index 7a1b38a3a..a6d00b2b6 100644 --- a/Modules/app-module/source/symbols.cpp +++ b/Modules/app-module/source/symbols.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include #include #include @@ -30,6 +31,16 @@ const ModuleSymbol app_module_symbols[] = { DEFINE_MODULE_SYMBOL(app_manager_get_state), DEFINE_MODULE_SYMBOL(app_manager_find_manifest), DEFINE_MODULE_SYMBOL(app_manager_for_each_manifest), + DEFINE_MODULE_SYMBOL(app_manager_add), + DEFINE_MODULE_SYMBOL(app_manager_remove), + DEFINE_MODULE_SYMBOL(app_manager_get_topmost_instance_id), + DEFINE_MODULE_SYMBOL(app_manager_get_topmost_app_id), + DEFINE_MODULE_SYMBOL(app_manager_install_path_add), + DEFINE_MODULE_SYMBOL(app_manager_install_path_scan), + // app/install + DEFINE_MODULE_SYMBOL(app_get_install_path), + DEFINE_MODULE_SYMBOL(app_install), + DEFINE_MODULE_SYMBOL(app_uninstall), // terminator MODULE_SYMBOL_TERMINATOR }; diff --git a/Modules/lvgl-window-manager-module/LICENSE-Apache-2.0.md b/Modules/lvgl-window-manager-module/LICENSE-Apache-2.0.md new file mode 100644 index 000000000..f5f4b8b5e --- /dev/null +++ b/Modules/lvgl-window-manager-module/LICENSE-Apache-2.0.md @@ -0,0 +1,195 @@ +Apache License +============== + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +* **(a)** You must give any other recipients of the Work or Derivative Works a copy of +this License; and +* **(b)** You must cause any modified files to carry prominent notices stating that You +changed the files; and +* **(c)** You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets `[]` replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same “printed page” as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/Modules/lvgl-window-manager-module/include/lvgl_window_manager/window_manager.h b/Modules/lvgl-window-manager-module/include/lvgl_window_manager/window_manager.h index 71c7022ba..7a4ba6044 100644 --- a/Modules/lvgl-window-manager-module/include/lvgl_window_manager/window_manager.h +++ b/Modules/lvgl-window-manager-module/include/lvgl_window_manager/window_manager.h @@ -101,6 +101,10 @@ enum WindowState window_manager_get_state(WindowId id); * Blocks the calling task until @a id's state changes away from WINDOW_STATE_GRANTED, or * @a timeout elapses. Returns immediately with WINDOW_STATE_REVOKED if @a id isn't currently * topmost (nothing to wait for). + * @warning At most one task may have an outstanding await() call per window at a time (each + * window tracks a single waiter). A second concurrent call for the same @a id asserts. Calls + * for different windows (e.g. from different app tasks in a stacked window manager) don't + * conflict with each other. * @return the state after waking (or immediately, if there was nothing to wait for) */ enum WindowState window_manager_await_state_change(WindowId id, TickType_t timeout); diff --git a/Modules/lvgl-window-manager-module/source/window_manager.cpp b/Modules/lvgl-window-manager-module/source/window_manager.cpp index dda814abe..7b1d8df31 100644 --- a/Modules/lvgl-window-manager-module/source/window_manager.cpp +++ b/Modules/lvgl-window-manager-module/source/window_manager.cpp @@ -3,11 +3,14 @@ #include +#include #include #include #include +constexpr auto* TAG = "window_manager"; + namespace { struct WindowRecord { @@ -15,6 +18,13 @@ struct WindowRecord { uint32_t app_instance_id; WindowCreateWidgetsFn create_widgets; void* user_data; + + /** Task blocked in window_manager_await_state_change() for this specific window, if any - + * see that function's @warning on at most one concurrent awaiter per window. Per-window + * rather than a single manager-wide slot, since a stacked window manager serving several + * app tasks can have more than one window (though only ever one of them topmost/GRANTED at + * a time) with a live await() call outstanding. */ + TaskHandle_t waiting_task = nullptr; }; struct WindowManagerState { @@ -44,9 +54,6 @@ struct WindowManagerState { std::vector windows; lv_obj_t* top_widget = nullptr; - /** Task blocked in window_manager_await_state_change(), if any. */ - TaskHandle_t waiting_task = nullptr; - WindowManagerState() { mutex_construct(&mutex); mutex_construct(&lifecycle_mutex); @@ -90,9 +97,19 @@ extern "C" { void window_manager_configure(WindowManagerScreenInitFn screen_init) { auto& s = state(); + + // Serializes against window_manager_start()/stop() + mutex_lock(&s.lifecycle_mutex); + mutex_lock(&s.mutex); - s.screen_init = screen_init; + if (!s.started) { + s.screen_init = screen_init; + } else { + LOG_W(TAG, "Ignoring window_manager_configure: module is already started"); + } mutex_unlock(&s.mutex); + + mutex_unlock(&s.lifecycle_mutex); } error_t window_manager_start(void) { @@ -161,16 +178,22 @@ error_t window_manager_stop(void) { return ERROR_NONE; } lv_obj_t* widget = s.real_root_widget; - TaskHandle_t waiter = s.waiting_task; + // Collect every window's waiter before clearing - normally at most the topmost window's is + // ever set, but every window is being torn down here, so every one is checked. + std::vector waiters; + for (const auto& window : s.windows) { + if (window.waiting_task != nullptr) { + waiters.push_back(window.waiting_task); + } + } s.real_root_widget = nullptr; s.content_root_widget = nullptr; s.top_widget = nullptr; s.windows.clear(); s.started = false; - s.waiting_task = nullptr; mutex_unlock(&s.mutex); - if (waiter != nullptr) { + for (TaskHandle_t waiter : waiters) { xTaskNotifyGive(waiter); } @@ -191,8 +214,13 @@ WindowId window_manager_create(uint32_t app_instance_id, WindowCreateWidgetsFn c } lv_obj_t* content = s.content_root_widget; lv_obj_t* old_top_widget = s.top_widget; - TaskHandle_t waiter = s.waiting_task; - s.waiting_task = nullptr; + // The current topmost window (if any) is about to be superseded - transfer its waiter (if + // any) here so it gets notified below, since it's no longer topmost after this. + TaskHandle_t waiter = nullptr; + if (!s.windows.empty()) { + waiter = s.windows.back().waiting_task; + s.windows.back().waiting_task = nullptr; + } s.top_widget = nullptr; WindowId new_id = s.next_id++; s.windows.push_back(WindowRecord { new_id, app_instance_id, create_widgets, user_data }); @@ -231,6 +259,11 @@ void window_manager_remove(WindowId id) { return; } bool was_topmost = (iterator + 1 == s.windows.end()); + // The window being removed owns its own waiter (if any) - a waiter is only ever registered + // while its window is topmost (see window_manager_await_state_change()), and if this window + // later stopped being topmost without being removed, window_manager_create() would already + // have transferred/cleared it - so a buried window's waiting_task is always already null. + TaskHandle_t waiter = iterator->waiting_task; s.windows.erase(iterator); lv_obj_t* content = s.content_root_widget; @@ -239,7 +272,6 @@ void window_manager_remove(WindowId id) { void* next_user_data = nullptr; WindowId next_id = 0; bool has_next = false; - TaskHandle_t waiter = nullptr; if (was_topmost) { old_widget = s.top_widget; @@ -250,11 +282,6 @@ void window_manager_remove(WindowId id) { next_id = s.windows.back().id; has_next = true; } - // Only the topmost window's state can actually change here - a waiter blocked in - // window_manager_await_state_change() is always waiting on the current top window (see - // that function), so removing a buried window never affects what it's waiting for. - waiter = s.waiting_task; - s.waiting_task = nullptr; } mutex_unlock(&s.mutex); @@ -298,17 +325,22 @@ WindowState window_manager_await_state_change(WindowId id, TickType_t timeout) { mutex_unlock(&s.mutex); return WINDOW_STATE_REVOKED; } - s.waiting_task = xTaskGetCurrentTaskHandle(); + // At most one concurrent awaiter per window - see the @warning on this function. + check(s.windows.back().waiting_task == nullptr); + s.windows.back().waiting_task = xTaskGetCurrentTaskHandle(); mutex_unlock(&s.mutex); ulTaskNotifyTake(pdTRUE, timeout); /* Deregister ourselves if a create()/remove() hasn't already claimed us (the ordinary, intended wakeup) * Otherwise a later create()/remove() could notify a task that's no longer waiting here: - * a use-after-exit on the handle if this task is gone, or a stale wakeup the next time it waits. */ + * a use-after-exit on the handle if this task is gone, or a stale wakeup the next time it waits. + * Re-locate the record by id - it may have been erased (window_manager_remove()) while we waited. */ mutex_lock(&s.mutex); - if (s.waiting_task == xTaskGetCurrentTaskHandle()) { - s.waiting_task = nullptr; + auto iterator = std::find_if(s.windows.begin(), s.windows.end(), + [id](const WindowRecord& window) { return window.id == id; }); + if (iterator != s.windows.end() && iterator->waiting_task == xTaskGetCurrentTaskHandle()) { + iterator->waiting_task = nullptr; } mutex_unlock(&s.mutex); diff --git a/TactilityC/Include/tt_app.h b/TactilityC/Include/tt_app.h deleted file mode 100644 index 874cf4d6b..000000000 --- a/TactilityC/Include/tt_app.h +++ /dev/null @@ -1,122 +0,0 @@ -#pragma once - -#include - -#include -#include - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef void* AppHandle; - -/** Important: These values must map to tt::app::Result values exactly */ -typedef enum { - APP_RESULT_OK = 0, - APP_RESULT_CANCELLED = 1, - APP_RESULT_ERROR = 2 -} AppResult; - -typedef unsigned int AppLaunchId; - -/** Important: These function types must map to t::app types exactly. All void* data is nullable. */ -typedef void* (*AppCreateData)(); -typedef void (*AppDestroyData)(void* data); -typedef void (*AppOnCreate)(AppHandle app, void* data); -typedef void (*AppOnDestroy)(AppHandle app, void* data); -typedef void (*AppOnShow)(AppHandle app, void* data, lv_obj_t* parent); -typedef void (*AppOnHide)(AppHandle app, void* data); -typedef void (*AppOnResult)(AppHandle app, void* data, AppLaunchId launchId, AppResult result, BundleHandle resultData); - -/** All callback types are nullable */ -typedef struct { - /** The application can allocate data to re-use later (e.g. struct with state) */ - AppCreateData createData; - /** If createData is specified, this one must be specified too */ - AppDestroyData destroyData; - /** Called when the app is launched (started) */ - AppOnCreate onCreate; - /** Called when the app is exited (stopped) */ - AppOnDestroy onDestroy; - /** Called when the app is about to be shown to the user (app becomes visible) */ - AppOnShow onShow; - /** Called when the app is about to be invisible to the user (e.g. other app was launched by this app, and this app goes to the background) */ - AppOnHide onHide; - /** Called when the app receives a result after launching another app */ - AppOnResult onResult; -} AppRegistration; - -/** This is used to register the manifest of an external app. */ -void tt_app_register(const AppRegistration app); - -/** @return the bundle that belongs to this application, or null if it wasn't started with parameters. */ -BundleHandle tt_app_get_parameters(AppHandle handle); - -/** - * Set the result before closing an app. - * The result and bundle are passed along to the app that launched this app, when this app is closed. - * @param[in] handle the app handle to set the result for - * @param[in] result the result state to set - * @param[in] bundle the result bundle to set (can be null) - */ -void tt_app_set_result(AppHandle handle, AppResult result, BundleHandle bundle); - -/** @return true if a result was set for this app context */ -bool tt_app_has_result(AppHandle handle); - -/** Get the path to the user data directory for this app. - * The app can store user-specific (mutable) data in there such as app settings. - * @param[in] handle the app handle - * @param[out] buffer the output buffer (recommended size is 256 bytes) - * @param[inout] size used as input for maximum buffer size (including null terminator) and is set with the path string length by this function - */ -void tt_app_get_user_data_path(AppHandle handle, char* buffer, size_t* size); - -/** Resolve a child path in the user directory of this app. - * The app can store user-specific (mutable) data in there such as app settings. - * @param[in] handle the app handle - * @param[in] childPath the child path to resolve - * @param[out] buffer the output buffer (recommended size is 256 bytes) - * @param[inout] size used as input for maximum buffer size (including null terminator) and is set with the path string length by this function - */ -void tt_app_get_user_data_child_path(AppHandle handle, const char* childPath, char* buffer, size_t* size); - -/** Get the path to the assets directory of this app. - * The content in this path should be treated as read-only. - * @param[in] handle the app handle - * @param[out] buffer the output buffer (recommended size is 256 bytes) - * @param[inout] size used as input for maximum buffer size (including null terminator) and is set with the path string length by this function - */ -void tt_app_get_assets_path(AppHandle handle, char* buffer, size_t* size); - -/** Resolve a child path in the assets directory of this app. - * The content in this path should be treated as read-only. - * @param[in] handle the app handle - * @param[in] childPath the child path to resolve - * @param[out] buffer the output buffer (recommended size is 256 bytes) - * @param[inout] size used as input for maximum buffer size (including null terminator) and is set with the path string length by this function - */ -void tt_app_get_assets_child_path(AppHandle handle, const char* childPath, char* buffer, size_t* size); - -/** - * Start an app by id. - * @param[in] appId the app manifest id - */ -void tt_app_start(const char* appId); - -/** Stop the currently running app */ -void tt_app_stop(); - -/** - * Start an app by id and bundle. - * @param[in] appId the app manifest id - * @param[in] parameters the parameters to pass onto the starting app - */ -void tt_app_start_with_bundle(const char* appId, BundleHandle parameters); - -#ifdef __cplusplus -} -#endif \ No newline at end of file diff --git a/TactilityC/Include/tt_app_alertdialog.h b/TactilityC/Include/tt_app_alertdialog.h index ff57c2eb3..6f476bdb1 100644 --- a/TactilityC/Include/tt_app_alertdialog.h +++ b/TactilityC/Include/tt_app_alertdialog.h @@ -1,7 +1,6 @@ #pragma once -#include "tt_app.h" -#include "tt_bundle.h" +#include #ifdef __cplusplus extern "C" { @@ -11,18 +10,18 @@ extern "C" { /** * Show a dialog with the provided title, message and 0, 1 or more buttons. + * @warning AlertDialog is now a new-model app (see Modules/app-module); it delivers its result + * via APP_EVENT_RESULT to a caller's app_instance_id, which side-loaded ELF apps don't have. + * The dialog will show, but this app's onResult callback will NOT be invoked with the button + * that was pressed - there is currently no bridge back to the old ELF app result mechanism. + * @param[in] parent_id parent app ID or 0 * @param[in] title the title to show in the toolbar * @param[in] message the message to display * @param[in] buttonLabels the buttons to show, or null when there are none to show * @param[in] buttonLabelCount the amount of buttons (0 or more) - * @return the launch ID of the dialog, which can be compared in onResult to identify the source + * @return the launch ID of the dialog (kept for source compatibility; no onResult will follow) */ -AppLaunchId tt_app_alertdialog_start(const char* title, const char* message, const char* buttonLabels[], uint32_t buttonLabelCount); - -/** - * @return the index of the button that was clicked (the index in the array when start() was called) - */ -int32_t tt_app_alertdialog_get_result_index(BundleHandle handle); +AppInstanceId tt_app_alertdialog_start(AppInstanceId parent_id, const char* title, const char* message, const char* buttonLabels[], uint32_t buttonLabelCount); #ifdef __cplusplus } diff --git a/TactilityC/Include/tt_app_fileselection.h b/TactilityC/Include/tt_app_fileselection.h index 453347324..659a06d28 100644 --- a/TactilityC/Include/tt_app_fileselection.h +++ b/TactilityC/Include/tt_app_fileselection.h @@ -1,7 +1,6 @@ #pragma once -#include "tt_app.h" -#include "tt_bundle.h" +#include #ifdef __cplusplus extern "C" { @@ -11,21 +10,23 @@ extern "C" { * Show a file selection dialog that allows the user to select an existing file. * @return the launch ID of the dialog, which can be compared in onResult to identify the source */ -AppLaunchId tt_app_fileselection_start_for_existing_file(); +AppInstanceId tt_app_fileselection_start_for_existing_file(AppInstanceId app_id); /** * Show a file selection dialog that allows the user to select a new or existing file. * @return the launch ID of the dialog, which can be compared in onResult to identify the source */ -AppLaunchId tt_app_fileselection_start_for_existing_or_new_file(); +AppInstanceId tt_app_fileselection_start_for_existing_or_new_file(AppInstanceId app_id); /** - * @param[in] handle the result bundle passed to onResult + * @return the path picked by the last FileSelection dialog that closed with result == Ok (see + * tt::app::fileselection::getLastPath()). Only one dialog is expected to be open at a time. * @param[out] buffer the buffer to store the selected path in * @param[in] bufferSize the size of the buffer (must include room for the null terminator) - * @return true if a path was selected and written to buffer, false otherwise + * @retval false @a bufferSize was too small - @a buffer is left untouched + * @retval true @a buffer was filled */ -bool tt_app_fileselection_get_result_path(BundleHandle handle, char* buffer, uint32_t bufferSize); +bool tt_app_fileselection_get_result_path(char* buffer, uint32_t bufferSize); #ifdef __cplusplus } diff --git a/TactilityC/Include/tt_app_selectiondialog.h b/TactilityC/Include/tt_app_selectiondialog.h index 795772cdb..005409cab 100644 --- a/TactilityC/Include/tt_app_selectiondialog.h +++ b/TactilityC/Include/tt_app_selectiondialog.h @@ -1,7 +1,6 @@ #pragma once -#include "tt_app.h" -#include "tt_bundle.h" +#include #ifdef __cplusplus extern "C" { @@ -9,15 +8,13 @@ extern "C" { /** * Start an app that displays a list of items and allows the user to select one. + * @param[in] parent_id parent app ID or 0 * @param[in] title the title to show in the toolbar * @param[in] argc the amount of items that the list contains * @param[in] argv the labels of the items in the list - * @return the launch ID of the dialog, which can be compared in onResult to identify the source + * @return the app instance ID of the dialog, which can be compared in onResult to identify the source */ -AppLaunchId tt_app_selectiondialog_start(const char* title, int argc, const char* argv[]); - -/** @return the index of the item that was clicked by the user, or -1 when the user didn't select anything */ -int32_t tt_app_selectiondialog_get_result_index(BundleHandle handle); +AppInstanceId tt_app_selectiondialog_start(AppInstanceId parent_id, const char* title, int argc, const char* argv[]); #ifdef __cplusplus } diff --git a/TactilityC/Include/tt_bundle.h b/TactilityC/Include/tt_bundle.h deleted file mode 100644 index 3c285071b..000000000 --- a/TactilityC/Include/tt_bundle.h +++ /dev/null @@ -1,74 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include - -/** The handle that represents a bundle instance */ -typedef void* BundleHandle; - -/** @return a new bundle instance */ -BundleHandle tt_bundle_alloc(); - -/** Dealloc an existing bundle instance */ -void tt_bundle_free(BundleHandle handle); - -/** - * Try to get a boolean value from a Bundle - * @param[in] handle the handle that represents the bundle - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[out] out the output value (only set when return value is set to true) - * @return true if "out" was set - */ -bool tt_bundle_opt_bool(BundleHandle handle, const char* key, bool* out); - -/** - * Try to get an int32_t value from a Bundle - * @param[in] handle the handle that represents the bundle - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[out] out the output value (only set when return value is set to true) - * @return true if "out" was set - */ -bool tt_bundle_opt_int32(BundleHandle handle, const char* key, int32_t* out); - -/** - * Try to get a string from a Bundle - * @warning outSize must be large enough to include null terminator. This means that your string has to be the expected text length + 1 extra character. - * @param[in] handle the handle that represents the bundle - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[out] out the buffer to store the string in - * @param[in] outSize the size of the buffer - * @return true if "out" was set - */ -bool tt_bundle_opt_string(BundleHandle handle, const char* key, char* out, uint32_t outSize); - -/** - * Store a boolean value in a Bundle - * @param[in] handle the handle that represents the bundle - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[in] value the value to store - */ -void tt_bundle_put_bool(BundleHandle handle, const char* key, bool value); - -/** - * Store an int32_t value in a Bundle - * @param[in] handle the handle that represents the bundle - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[in] value the value to store - */ -void tt_bundle_put_int32(BundleHandle handle, const char* key, int32_t value); - -/** - * Store a string value in a Bundle - * @param[in] handle the handle that represents the bundle - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[in] value the value to store - */ -void tt_bundle_put_string(BundleHandle handle, const char* key, const char* value); - -#ifdef __cplusplus -} -#endif \ No newline at end of file diff --git a/TactilityC/Include/tt_preferences.h b/TactilityC/Include/tt_preferences.h deleted file mode 100644 index 735982b35..000000000 --- a/TactilityC/Include/tt_preferences.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include - -/** - * Note that on ESP32, there are limitations: - * - namespace name is limited by NVS_NS_NAME_MAX_SIZE (generally 16 characters) - * - key is limited by NVS_KEY_NAME_MAX_SIZE (generally 16 characters) - */ - -/** The handle that represents a Preferences instance */ -typedef void* PreferencesHandle; - -/** - * @param[in] identifier the name of the preferences. This determines the NVS namespace on ESP. - * @return a new preferences instance - */ -PreferencesHandle tt_preferences_alloc(const char* identifier); - -/** Dealloc an existing preferences instance */ -void tt_preferences_free(PreferencesHandle handle); - -/** - * Try to get a boolean value - * @param[in] handle the handle that represents the preferences - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[out] out the output value (only set when return value is set to true) - * @return true if "out" was set - */ -bool tt_preferences_opt_bool(PreferencesHandle handle, const char* key, bool* out); - -/** - * Try to get an int32_t value - * @param[in] handle the handle that represents the preferences - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[out] out the output value (only set when return value is set to true) - * @return true if "out" was set - */ -bool tt_preferences_opt_int32(PreferencesHandle handle, const char* key, int32_t* out); - -/** - * Try to get a string - * @warning outSize must be large enough to include null terminator. This means that your string has to be the expected text length + 1 extra character. - * @param[in] handle the handle that represents the preferences - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[out] out the buffer to store the string in - * @param[in] outSize the size of the buffer - * @return true if "out" was set - */ -bool tt_preferences_opt_string(PreferencesHandle handle, const char* key, char* out, uint32_t outSize); - -/** - * Store a boolean value - * @param[in] handle the handle that represents the preferences - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[in] value the value to store - */ -void tt_preferences_put_bool(PreferencesHandle handle, const char* key, bool value); - -/** - * Store an int32_t value - * @param[in] handle the handle that represents the preferences - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[in] value the value to store - */ -void tt_preferences_put_int32(PreferencesHandle handle, const char* key, int32_t value); - -/** - * Store a string value - * @param[in] handle the handle that represents the preferences - * @param[in] key the identifier that represents the stored value (~variable name) - * @param[in] value the value to store - */ -void tt_preferences_put_string(PreferencesHandle handle, const char* key, const char* value); - -#ifdef __cplusplus -} -#endif \ No newline at end of file diff --git a/TactilityC/Source/tt_app.cpp b/TactilityC/Source/tt_app.cpp deleted file mode 100644 index ad703b497..000000000 --- a/TactilityC/Source/tt_app.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#include "tt_app.h" -#include -#include -#include -#include -#include - -#include - -constexpr auto* TAG = "tt_app"; - -extern "C" { - -#define HANDLE_AS_APP_CONTEXT(handle) ((tt::app::AppContext*)(handle)) - -void tt_app_register( - const AppRegistration appRegistration -) { -#ifdef ESP_PLATFORM - assert((appRegistration.createData == nullptr) == (appRegistration.destroyData == nullptr)); - tt::app::setElfAppParameters( - appRegistration.createData, - appRegistration.destroyData, - appRegistration.onCreate, - appRegistration.onDestroy, - appRegistration.onShow, - appRegistration.onHide, - reinterpret_cast(appRegistration.onResult) - ); -#else - check(false, "TactilityC is not intended for PC/Simulator"); -#endif -} - -BundleHandle tt_app_get_parameters(AppHandle handle) { - return (BundleHandle)HANDLE_AS_APP_CONTEXT(handle)->getParameters().get(); -} - -void tt_app_set_result(AppHandle handle, AppResult result, BundleHandle bundle) { - auto shared_bundle = std::unique_ptr(static_cast(bundle)); - HANDLE_AS_APP_CONTEXT(handle)->getApp()->setResult(static_cast(result), std::move(shared_bundle)); -} - -bool tt_app_has_result(AppHandle handle) { - return HANDLE_AS_APP_CONTEXT(handle)->getApp()->hasResult(); -} - -void tt_app_start(const char* appId) { - tt::app::start(appId); -} - -void tt_app_start_with_bundle(const char* appId, BundleHandle parameters) { - tt::app::start(appId, std::shared_ptr(static_cast(parameters))); -} - -void tt_app_stop() { - tt::app::stop(); -} - -void tt_app_get_user_data_path(AppHandle handle, char* buffer, size_t* size) { - assert(buffer != nullptr); - assert(size != nullptr); - assert(*size > 0); - const auto paths = HANDLE_AS_APP_CONTEXT(handle)->getPaths(); - const auto data_path = paths->getUserDataPath(); - const auto expected_length = data_path.length() + 1; - if (*size < expected_length) { - LOG_E(TAG, "Path buffer not large enough (%u < %u)", (unsigned)*size, (unsigned)expected_length); - *size = 0; - buffer[0] = 0; - return; - } - - strcpy(buffer, data_path.c_str()); - *size = data_path.length(); -} - -void tt_app_get_user_data_child_path(AppHandle handle, const char* childPath, char* buffer, size_t* size) { - assert(buffer != nullptr); - assert(size != nullptr); - assert(*size > 0); - const auto paths = HANDLE_AS_APP_CONTEXT(handle)->getPaths(); - const auto resolved_path = paths->getUserDataPath(childPath); - const auto resolved_path_length = resolved_path.length(); - if (*size < (resolved_path_length + 1)) { - LOG_E(TAG, "Path buffer not large enough (%u < %u)", (unsigned)*size, (unsigned)(resolved_path_length + 1)); - *size = 0; - buffer[0] = 0; - return; - } - - strcpy(buffer, resolved_path.c_str()); - *size = resolved_path_length; -} - -void tt_app_get_assets_path(AppHandle handle, char* buffer, size_t* size) { - assert(buffer != nullptr); - assert(size != nullptr); - assert(*size > 0); - const auto paths = HANDLE_AS_APP_CONTEXT(handle)->getPaths(); - const auto assets_path = paths->getAssetsPath(); - const auto expected_length = assets_path.length() + 1; - if (*size < expected_length) { - LOG_E(TAG, "Path buffer not large enough (%u < %u)", (unsigned)*size, (unsigned)expected_length); - *size = 0; - buffer[0] = 0; - return; - } - - strcpy(buffer, assets_path.c_str()); - *size = assets_path.length(); -} - -void tt_app_get_assets_child_path(AppHandle handle, const char* childPath, char* buffer, size_t* size) { - assert(buffer != nullptr); - assert(size != nullptr); - assert(*size > 0); - const auto paths = HANDLE_AS_APP_CONTEXT(handle)->getPaths(); - const auto resolved_path = paths->getAssetsPath(childPath); - const auto resolved_path_length = resolved_path.length(); - if (*size < (resolved_path_length + 1)) { - LOG_E(TAG, "Path buffer not large enough (%u < %u)", (unsigned)*size, (unsigned)(resolved_path_length + 1)); - *size = 0; - buffer[0] = 0; - return; - } - - strcpy(buffer, resolved_path.c_str()); - *size = resolved_path_length; - -} - -} \ No newline at end of file diff --git a/TactilityC/Source/tt_app_alertdialog.cpp b/TactilityC/Source/tt_app_alertdialog.cpp index dd8d86dec..63cb6084b 100644 --- a/TactilityC/Source/tt_app_alertdialog.cpp +++ b/TactilityC/Source/tt_app_alertdialog.cpp @@ -1,18 +1,16 @@ #include "tt_app_alertdialog.h" + #include extern "C" { -AppLaunchId tt_app_alertdialog_start(const char* title, const char* message, const char* buttonLabels[], uint32_t buttonLabelCount) { +AppInstanceId tt_app_alertdialog_start(AppInstanceId parent_id, const char* title, const char* message, const char* buttonLabels[], uint32_t buttonLabelCount) { std::vector list; for (int i = 0; i < buttonLabelCount; i++) { list.emplace_back(buttonLabels[i]); } - return tt::app::alertdialog::start(title, message, list); -} - -int32_t tt_app_alertdialog_get_result_index(BundleHandle handle) { - return tt::app::alertdialog::getResultIndex(*(tt::Bundle*)handle); + // TODO: Get caller app instance id from task context? + return tt::app::alertdialog::start(parent_id, title, message, list); } } diff --git a/TactilityC/Source/tt_app_fileselection.cpp b/TactilityC/Source/tt_app_fileselection.cpp index acdbf1e4b..7a8e64c43 100644 --- a/TactilityC/Source/tt_app_fileselection.cpp +++ b/TactilityC/Source/tt_app_fileselection.cpp @@ -1,28 +1,25 @@ #include "tt_app_fileselection.h" -#include #include -#include #include #include extern "C" { -AppLaunchId tt_app_fileselection_start_for_existing_file() { - return tt::app::fileselection::startForExistingFile(); +AppInstanceId tt_app_fileselection_start_for_existing_file(AppInstanceId app_id) { + return tt::app::fileselection::startForExistingFile(app_id); } -AppLaunchId tt_app_fileselection_start_for_existing_or_new_file() { - return tt::app::fileselection::startForExistingOrNewFile(); +AppInstanceId tt_app_fileselection_start_for_existing_or_new_file(AppInstanceId app_id) { + return tt::app::fileselection::startForExistingOrNewFile(app_id); } -bool tt_app_fileselection_get_result_path(BundleHandle handle, char* buffer, uint32_t bufferSize) { - auto path = tt::app::fileselection::getResultPath(*(tt::Bundle*)handle); - if (path.empty() || bufferSize == 0) { +bool tt_app_fileselection_get_result_path(char* buffer, uint32_t bufferSize) { + const std::string path = tt::app::fileselection::getLastPath(); + if (path.length() + 1 > bufferSize) { return false; } - strncpy(buffer, path.c_str(), bufferSize - 1); - buffer[bufferSize - 1] = '\0'; + std::strcpy(buffer, path.c_str()); return true; } diff --git a/TactilityC/Source/tt_app_selectiondialog.cpp b/TactilityC/Source/tt_app_selectiondialog.cpp index a67df0867..73b91d883 100644 --- a/TactilityC/Source/tt_app_selectiondialog.cpp +++ b/TactilityC/Source/tt_app_selectiondialog.cpp @@ -3,16 +3,12 @@ extern "C" { -AppLaunchId tt_app_selectiondialog_start(const char* title, int argc, const char* argv[]) { +AppInstanceId tt_app_selectiondialog_start(AppInstanceId parent_id, const char* title, int argc, const char* argv[]) { std::vector list; for (int i = 0; i < argc; i++) { list.emplace_back(argv[i]); } - return tt::app::selectiondialog::start(title, list); -} - -int32_t tt_app_selectiondialog_get_result_index(BundleHandle handle) { - return tt::app::selectiondialog::getResultIndex(*(tt::Bundle*)handle); + return tt::app::selectiondialog::start(parent_id, title, list); } } diff --git a/TactilityC/Source/tt_bundle.cpp b/TactilityC/Source/tt_bundle.cpp deleted file mode 100644 index 4172bf2c0..000000000 --- a/TactilityC/Source/tt_bundle.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "tt_bundle.h" -#include -#include - -#define HANDLE_AS_BUNDLE(handle) ((tt::Bundle*)(handle)) - -extern "C" { - -BundleHandle tt_bundle_alloc() { - return new tt::Bundle(); -} - -void tt_bundle_free(BundleHandle handle) { - delete HANDLE_AS_BUNDLE(handle); -} - -bool tt_bundle_opt_bool(BundleHandle handle, const char* key, bool* out) { - return HANDLE_AS_BUNDLE(handle)->optBool(key, *out); -} - -bool tt_bundle_opt_int32(BundleHandle handle, const char* key, int32_t* out) { - return HANDLE_AS_BUNDLE(handle)->optInt32(key, *out); -} -bool tt_bundle_opt_string(BundleHandle handle, const char* key, char* out, uint32_t outSize) { - std::string out_string; - - if (!HANDLE_AS_BUNDLE(handle)->optString(key, out_string)) { - return false; - } - - if (out_string.length() >= outSize) { - // Need 1 byte to add 0 at the end - return false; - } - - memcpy(out, out_string.c_str(), out_string.length()); - out[out_string.length()] = 0x00; - return true; -} - -void tt_bundle_put_bool(BundleHandle handle, const char* key, bool value) { - HANDLE_AS_BUNDLE(handle)->putBool(key, value); -} - -void tt_bundle_put_int32(BundleHandle handle, const char* key, int32_t value) { - HANDLE_AS_BUNDLE(handle)->putInt32(key, value); -} - -void tt_bundle_put_string(BundleHandle handle, const char* key, const char* value) { - HANDLE_AS_BUNDLE(handle)->putString(key, value); -} - -} \ No newline at end of file diff --git a/TactilityC/Source/tt_init.cpp b/TactilityC/Source/tt_init.cpp index 296b9f68b..74f8bea41 100644 --- a/TactilityC/Source/tt_init.cpp +++ b/TactilityC/Source/tt_init.cpp @@ -1,11 +1,8 @@ #ifdef ESP_PLATFORM -#include "tt_app.h" #include "tt_app_alertdialog.h" #include "tt_app_fileselection.h" #include "tt_app_selectiondialog.h" -#include "tt_bundle.h" -#include "tt_preferences.h" #include "tt_time.h" #include "symbols/cplusplus.h" @@ -260,40 +257,11 @@ const esp_elfsym main_symbols[] { ESP_ELFSYM_EXPORT(esp_log_timestamp), ESP_ELFSYM_EXPORT(esp_err_to_name), // Tactility - ESP_ELFSYM_EXPORT(tt_app_start), - ESP_ELFSYM_EXPORT(tt_app_start_with_bundle), - ESP_ELFSYM_EXPORT(tt_app_stop), - ESP_ELFSYM_EXPORT(tt_app_register), - ESP_ELFSYM_EXPORT(tt_app_get_parameters), - ESP_ELFSYM_EXPORT(tt_app_set_result), - ESP_ELFSYM_EXPORT(tt_app_has_result), ESP_ELFSYM_EXPORT(tt_app_fileselection_start_for_existing_file), ESP_ELFSYM_EXPORT(tt_app_fileselection_start_for_existing_or_new_file), ESP_ELFSYM_EXPORT(tt_app_fileselection_get_result_path), ESP_ELFSYM_EXPORT(tt_app_selectiondialog_start), - ESP_ELFSYM_EXPORT(tt_app_selectiondialog_get_result_index), ESP_ELFSYM_EXPORT(tt_app_alertdialog_start), - ESP_ELFSYM_EXPORT(tt_app_alertdialog_get_result_index), - ESP_ELFSYM_EXPORT(tt_app_get_user_data_path), - ESP_ELFSYM_EXPORT(tt_app_get_user_data_child_path), - ESP_ELFSYM_EXPORT(tt_app_get_assets_path), - ESP_ELFSYM_EXPORT(tt_app_get_assets_child_path), - ESP_ELFSYM_EXPORT(tt_bundle_alloc), - ESP_ELFSYM_EXPORT(tt_bundle_free), - ESP_ELFSYM_EXPORT(tt_bundle_opt_bool), - ESP_ELFSYM_EXPORT(tt_bundle_opt_int32), - ESP_ELFSYM_EXPORT(tt_bundle_opt_string), - ESP_ELFSYM_EXPORT(tt_bundle_put_bool), - ESP_ELFSYM_EXPORT(tt_bundle_put_int32), - ESP_ELFSYM_EXPORT(tt_bundle_put_string), - ESP_ELFSYM_EXPORT(tt_preferences_alloc), - ESP_ELFSYM_EXPORT(tt_preferences_free), - ESP_ELFSYM_EXPORT(tt_preferences_opt_bool), - ESP_ELFSYM_EXPORT(tt_preferences_opt_int32), - ESP_ELFSYM_EXPORT(tt_preferences_opt_string), - ESP_ELFSYM_EXPORT(tt_preferences_put_bool), - ESP_ELFSYM_EXPORT(tt_preferences_put_int32), - ESP_ELFSYM_EXPORT(tt_preferences_put_string), ESP_ELFSYM_EXPORT(tt_timezone_set), ESP_ELFSYM_EXPORT(tt_timezone_get_name), ESP_ELFSYM_EXPORT(tt_timezone_get_code), diff --git a/TactilityC/Source/tt_preferences.cpp b/TactilityC/Source/tt_preferences.cpp deleted file mode 100644 index 5aeb6d1d7..000000000 --- a/TactilityC/Source/tt_preferences.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "tt_preferences.h" -#include -#include - -#define HANDLE_AS_PREFERENCES(handle) ((tt::Preferences*)(handle)) - -extern "C" { - -PreferencesHandle tt_preferences_alloc(const char* identifier) { - return new tt::Preferences(identifier); -} - -void tt_preferences_free(PreferencesHandle handle) { - delete HANDLE_AS_PREFERENCES(handle); -} - -bool tt_preferences_opt_bool(PreferencesHandle handle, const char* key, bool* out) { - return HANDLE_AS_PREFERENCES(handle)->optBool(key, *out); -} - -bool tt_preferences_opt_int32(PreferencesHandle handle, const char* key, int32_t* out) { - return HANDLE_AS_PREFERENCES(handle)->optInt32(key, *out); -} -bool tt_preferences_opt_string(PreferencesHandle handle, const char* key, char* out, uint32_t outSize) { - std::string out_string; - - if (!HANDLE_AS_PREFERENCES(handle)->optString(key, out_string)) { - return false; - } - - if (out_string.length() >= outSize) { - // Need 1 byte to add 0 at the end - return false; - } - - memcpy(out, out_string.c_str(), out_string.length()); - out[out_string.length()] = 0x00; - return true; -} - -void tt_preferences_put_bool(PreferencesHandle handle, const char* key, bool value) { - HANDLE_AS_PREFERENCES(handle)->putBool(key, value); -} - -void tt_preferences_put_int32(PreferencesHandle handle, const char* key, int32_t value) { - HANDLE_AS_PREFERENCES(handle)->putInt32(key, value); -} - -void tt_preferences_put_string(PreferencesHandle handle, const char* key, const char* value) { - HANDLE_AS_PREFERENCES(handle)->putString(key, value); -} - -} \ No newline at end of file diff --git a/TactilityKernel/include/tactility/bundle.h b/TactilityKernel/include/tactility/bundle.h new file mode 100644 index 000000000..553ad8517 --- /dev/null +++ b/TactilityKernel/include/tactility/bundle.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 + +/** + * @brief key-value storage for general purpose. + * Maps strings on a fixed set of data types. + */ +#pragma once + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * A dictionary that maps keys (strings) onto several atomary types. + * Opaque handle - allocate with bundle_alloc(), release with bundle_free(). + */ +typedef struct Bundle Bundle; + +Bundle* bundle_alloc(void); +Bundle* bundle_clone(const Bundle* bundle); +void bundle_free(Bundle* bundle); + +/** @warning Undefined if @a key is absent or not a bool - check with bundle_has_bool()/bundle_opt_bool() first. */ +bool bundle_get_bool(const Bundle* bundle, const char* key); +/** @warning Undefined if @a key is absent or not an int32 - check with bundle_has_int32()/bundle_opt_int32() first. */ +int32_t bundle_get_int32(const Bundle* bundle, const char* key); +/** @warning Undefined if @a key is absent or not an int64 - check with bundle_has_int64()/bundle_opt_int64() first. */ +int64_t bundle_get_int64(const Bundle* bundle, const char* key); +/** + * @warning Undefined if @a key is absent or not a string - check with bundle_has_string()/bundle_opt_string() first. + * @retval ERROR_BUFFER_OVERFLOW out_value_size is too small + * @retval ERROR_NONE on success + */ +error_t bundle_get_string(const Bundle* bundle, const char* key, char* out_value, size_t out_value_size); + +bool bundle_has_bool(const Bundle* bundle, const char* key); +bool bundle_has_int32(const Bundle* bundle, const char* key); +bool bundle_has_int64(const Bundle* bundle, const char* key); +bool bundle_has_string(const Bundle* bundle, const char* key); + +bool bundle_opt_bool(const Bundle* bundle, const char* key, bool* out_value); +bool bundle_opt_int32(const Bundle* bundle, const char* key, int32_t* out_value); +bool bundle_opt_int64(const Bundle* bundle, const char* key, int64_t* out_value); +/** + * @retval ERROR_NOT_FOUND @a key is absent or not a string - @a out_value is left untouched + * @retval ERROR_BUFFER_OVERFLOW out_value_size is too small - @a out_value is left untouched + * @retval ERROR_NONE on success + */ +error_t bundle_opt_string(const Bundle* bundle, const char* key, char* out_value, size_t out_value_size); + +void bundle_put_bool(Bundle* bundle, const char* key, bool value); +void bundle_put_int32(Bundle* bundle, const char* key, int32_t value); +void bundle_put_int64(Bundle* bundle, const char* key, int64_t value); +void bundle_put_string(Bundle* bundle, const char* key, const char* value); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/preferences.h b/TactilityKernel/include/tactility/preferences.h new file mode 100644 index 000000000..908086bf4 --- /dev/null +++ b/TactilityKernel/include/tactility/preferences.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 + +/** + * @brief Key-value settings, persisted as a .properties file on disk (instead of NVS/in-memory). + */ +#pragma once + +#include +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Opaque handle - open with preferences_open(), release with preferences_close(). + */ +typedef struct Preferences Preferences; + +/** + * Open (or create) a preferences store backed by the properties file at @a path. The file is + * read into memory now; changes made with preferences_put_*() are only written back to disk by + * preferences_close(). + * @param[in] path absolute or relative file path (e.g. "/data/settings.properties") - the + * parent directory must already exist + * @return the new instance, or NULL on allocation failure + */ +Preferences* preferences_open(const char* path); + +/** Writes any pending preferences_put_*() changes to the backing file, then releases the + * instance. */ +void preferences_close(Preferences* preferences); + +bool preferences_has_bool(const Preferences* preferences, const char* key); +bool preferences_has_int32(const Preferences* preferences, const char* key); +bool preferences_has_int64(const Preferences* preferences, const char* key); +bool preferences_has_string(const Preferences* preferences, const char* key); + +bool preferences_opt_bool(const Preferences* preferences, const char* key, bool* out_value); +bool preferences_opt_int32(const Preferences* preferences, const char* key, int32_t* out_value); +bool preferences_opt_int64(const Preferences* preferences, const char* key, int64_t* out_value); +/** + * @retval ERROR_NOT_FOUND @a key is absent or not a string - @a out_value is left untouched + * @retval ERROR_BUFFER_OVERFLOW out_value_size is too small - @a out_value is left untouched + * @retval ERROR_NONE on success + */ +error_t preferences_opt_string(const Preferences* preferences, const char* key, char* out_value, size_t out_value_size); + +/** Sets the value in the in-memory cache; only persisted to the backing file by + * preferences_close(). */ +void preferences_put_bool(Preferences* preferences, const char* key, bool value); +void preferences_put_int32(Preferences* preferences, const char* key, int32_t value); +void preferences_put_int64(Preferences* preferences, const char* key, int64_t value); +void preferences_put_string(Preferences* preferences, const char* key, const char* value); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/properties_file.h b/TactilityKernel/include/tactility/properties_file.h new file mode 100644 index 000000000..a08499c03 --- /dev/null +++ b/TactilityKernel/include/tactility/properties_file.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 + +/** + * @brief Generic string key-value ".properties" file. + * @note Safely acquires/releases the filesystem mutex registered for the file's path (see + * tactility/filesystem/file_mutex.h) - manual locking isn't needed. + */ +#pragma once + +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Opaque handle - open with properties_file_open(), release with properties_file_close(). + */ +typedef struct PropertiesFile PropertiesFile; + +/** + * Open (or create) a properties file at @a path. The file is read into memory now; changes + * made with properties_file_set() are only written back to disk by properties_file_close(). + * @param[in] path absolute or relative file path (e.g. "/data/settings.properties") - the + * parent directory must already exist + * @return the new instance, or NULL on allocation failure + */ +PropertiesFile* properties_file_open(const char* path); + +/** Writes any pending properties_file_set() changes to the backing file, then releases the + * instance. */ +void properties_file_close(PropertiesFile* file); + +bool properties_file_has(const PropertiesFile* file, const char* key); + +/** + * @retval ERROR_NOT_FOUND @a key is absent - @a out_value is left untouched + * @retval ERROR_BUFFER_OVERFLOW out_value_size is too small - @a out_value is left untouched + * @retval ERROR_NONE on success + */ +error_t properties_file_get(const PropertiesFile* file, const char* key, char* out_value, size_t out_value_size); + +/** Sets the value in the in-memory cache; only persisted to the backing file by + * properties_file_close(). */ +void properties_file_set(PropertiesFile* file, const char* key, const char* value); + +typedef void (*PropertiesFileVisitorFn)(const char* key, const char* value, void* context); + +/** Invokes @a visitor for every key currently cached, in unspecified order. */ +void properties_file_for_each(const PropertiesFile* file, PropertiesFileVisitorFn visitor, void* context); + +#ifdef __cplusplus +} +#endif diff --git a/TactilityKernel/include/tactility/system_event.h b/TactilityKernel/include/tactility/system_event.h index 9b4cb967b..826da6542 100644 --- a/TactilityKernel/include/tactility/system_event.h +++ b/TactilityKernel/include/tactility/system_event.h @@ -28,20 +28,6 @@ enum SystemEventType { KERNEL_EVENT_TIME_CHANGED, // No data - fired whenever system time is set (NTP sync, RTC restore, manual change) }; -/** - * A system-wide event as delivered to a system_event_callback_t. - * `data` points at the type-specific struct documented next to `type`'s enum value - * in SystemEventType (or is NULL when none is documented). - * It is only valid for the duration of the callback. - */ -struct SystemEvent { - enum SystemEventType type; - /** Microseconds since boot, from get_micros_since_boot(). */ - uint64_t timestamp; - const void *data; - size_t data_len; -}; - /** Data for KERNEL_EVENT_NETWORK_CONNECTED. */ struct NetworkConnectedEvent { struct Device* device; @@ -74,6 +60,26 @@ struct ServiceStoppedEvent { const char* id; }; +/** Size of the largest type-specific event struct documented in SystemEventType, i.e. the + * embedded buffer size needed by SystemEvent/SystemEventSubscription to hold any event's + * payload by value. */ +#define SYSTEM_EVENT_MAX_DATA_SIZE (sizeof(struct NetworkConnectedEvent)) + +/** + * A system-wide event as delivered to a system_event_callback_t. + * `data` (up to `data_len` bytes, `SYSTEM_EVENT_MAX_DATA_SIZE` max) is a by-value copy of the + * type-specific struct documented next to `type`'s enum value in SystemEventType (or unused, + * with `data_len` 0, when none is documented) - like SystemEventSubscription's `data`, but only + * valid for the duration of the callback rather than for the subscription's lifetime. + */ +struct SystemEvent { + enum SystemEventType type; + /** Microseconds since boot, from get_micros_since_boot(). */ + uint64_t timestamp; + uint8_t data[SYSTEM_EVENT_MAX_DATA_SIZE]; + size_t data_len; +}; + /** * @param[in] event the event being delivered; only valid for the duration of the call * @param[in] context the context pointer passed to system_event_callback_add() @@ -133,31 +139,30 @@ error_t system_event_emit( #define SYSTEM_EVENT_MAX_DATA_SIZE (sizeof(struct NetworkConnectedEvent)) /** - * gps.h-style poll subscription: caller-owned node, registered with system_event_subscribe() - * and polled with system_event_await(). Unlike system_event_callback_t, the payload is copied - * by value into @a data (up to SYSTEM_EVENT_MAX_DATA_SIZE bytes) so it remains valid after - * system_event_emit() returns. - * @warning Fields other than `type` are for internal use only; do not read or write them - * directly. + * 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 + * copy that remains valid for the subscription's lifetime (until the next matching event + * overwrites it), not just for the duration of a callback. */ struct SystemEventSubscription { - /** Event type to subscribe to; set by the caller before system_event_subscribe(). */ - enum SystemEventType type; + /** `event.type` is the event type to subscribe to; set by the caller before + * system_event_subscribe(). The rest of `event` (timestamp/data/data_len) is populated by + * each matching system_event_emit() - see the @warning above. */ + struct SystemEvent event; - /** 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; + /** 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; - uint64_t timestamp; - uint8_t data[SYSTEM_EVENT_MAX_DATA_SIZE]; - size_t data_len; + uint32_t sequence; + uint32_t consumed_sequence; - uint32_t sequence; - uint32_t consumed_sequence; - - struct SystemEventSubscription* next; + struct SystemEventSubscription* next; + } internal; }; /** @@ -188,6 +193,18 @@ error_t system_event_unsubscribe(struct SystemEventSubscription* sub); */ error_t system_event_await(struct SystemEventSubscription* sub, TickType_t timeout); +/** + * Copies @a sub's current event payload (the data from the most recent system_event_emit() + * that reached it) into @a data. + * @param[in] sub subscription to read the payload from, as passed to system_event_subscribe() + * @param[out] data buffer to copy the payload into + * @param[in] data_len size of @a data + * @retval ERROR_NONE on success + * @retval ERROR_BUFFER_OVERFLOW @a data_len is smaller than the stored payload - @a data is + * left untouched + */ +error_t system_event_get_data(struct SystemEventSubscription* sub, uint8_t* data, size_t data_len); + #ifdef __cplusplus } #endif diff --git a/TactilityKernel/source/bundle.cpp b/TactilityKernel/source/bundle.cpp new file mode 100644 index 000000000..92b49984f --- /dev/null +++ b/TactilityKernel/source/bundle.cpp @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include + +namespace { + +enum class Type { + Bool, + Int32, + Int64, + String, +}; + +struct Value { + Type type; + union { + bool value_bool; + int32_t value_int32; + int64_t value_int64; + }; + std::string value_string; +}; + +} // namespace + +// Definition of the opaque handle declared in tactility/bundle.h - C callers only ever see it +// through a Bundle* pointer, never its members. +struct Bundle { + std::unordered_map entries; +}; + +extern "C" { + +Bundle* bundle_alloc(void) { + return new (std::nothrow) Bundle(); +} + +Bundle* bundle_clone(const Bundle* bundle) { + auto* clone = new (std::nothrow) Bundle(); + if (clone != nullptr) { + clone->entries = bundle->entries; + } + return clone; +} + +void bundle_free(Bundle* bundle) { + delete bundle; +} + +bool bundle_get_bool(const Bundle* bundle, const char* key) { + return bundle->entries.find(key)->second.value_bool; +} + +int32_t bundle_get_int32(const Bundle* bundle, const char* key) { + return bundle->entries.find(key)->second.value_int32; +} + +int64_t bundle_get_int64(const Bundle* bundle, const char* key) { + return bundle->entries.find(key)->second.value_int64; +} + +error_t bundle_get_string(const Bundle* bundle, const char* key, char* out_value, size_t out_value_size) { + const std::string& value = bundle->entries.find(key)->second.value_string; + if (value.size() + 1 > out_value_size) { + return ERROR_BUFFER_OVERFLOW; + } + std::memcpy(out_value, value.c_str(), value.size() + 1); + return ERROR_NONE; +} + +bool bundle_has_bool(const Bundle* bundle, const char* key) { + auto entry = bundle->entries.find(key); + return entry != bundle->entries.end() && entry->second.type == Type::Bool; +} + +bool bundle_has_int32(const Bundle* bundle, const char* key) { + auto entry = bundle->entries.find(key); + return entry != bundle->entries.end() && entry->second.type == Type::Int32; +} + +bool bundle_has_int64(const Bundle* bundle, const char* key) { + auto entry = bundle->entries.find(key); + return entry != bundle->entries.end() && entry->second.type == Type::Int64; +} + +bool bundle_has_string(const Bundle* bundle, const char* key) { + auto entry = bundle->entries.find(key); + return entry != bundle->entries.end() && entry->second.type == Type::String; +} + +bool bundle_opt_bool(const Bundle* bundle, const char* key, bool* out_value) { + auto entry = bundle->entries.find(key); + if (entry != bundle->entries.end() && entry->second.type == Type::Bool) { + *out_value = entry->second.value_bool; + return true; + } + return false; +} + +bool bundle_opt_int32(const Bundle* bundle, const char* key, int32_t* out_value) { + auto entry = bundle->entries.find(key); + if (entry != bundle->entries.end() && entry->second.type == Type::Int32) { + *out_value = entry->second.value_int32; + return true; + } + return false; +} + +bool bundle_opt_int64(const Bundle* bundle, const char* key, int64_t* out_value) { + auto entry = bundle->entries.find(key); + if (entry != bundle->entries.end() && entry->second.type == Type::Int64) { + *out_value = entry->second.value_int64; + return true; + } + return false; +} + +error_t bundle_opt_string(const Bundle* bundle, const char* key, char* out_value, size_t out_value_size) { + auto entry = bundle->entries.find(key); + if (entry == bundle->entries.end() || entry->second.type != Type::String) { + return ERROR_NOT_FOUND; + } + const std::string& value = entry->second.value_string; + if (value.size() + 1 > out_value_size) { + return ERROR_BUFFER_OVERFLOW; + } + std::memcpy(out_value, value.c_str(), value.size() + 1); + return ERROR_NONE; +} + +void bundle_put_bool(Bundle* bundle, const char* key, bool value) { + bundle->entries[key] = Value { .type = Type::Bool, .value_bool = value, .value_string = "" }; +} + +void bundle_put_int32(Bundle* bundle, const char* key, int32_t value) { + bundle->entries[key] = Value { .type = Type::Int32, .value_int32 = value, .value_string = "" }; +} + +void bundle_put_int64(Bundle* bundle, const char* key, int64_t value) { + bundle->entries[key] = Value { .type = Type::Int64, .value_int64 = value, .value_string = "" }; +} + +void bundle_put_string(Bundle* bundle, const char* key, const char* value) { + bundle->entries[key] = Value { .type = Type::String, .value_bool = false, .value_string = value }; +} + +} // extern "C" diff --git a/TactilityKernel/source/preferences.cpp b/TactilityKernel/source/preferences.cpp new file mode 100644 index 000000000..ee77e436b --- /dev/null +++ b/TactilityKernel/source/preferences.cpp @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// Escapes '\\' and '\n' so a string value can never break properties_file's one-entry-per-line +// on-disk format, regardless of its content. +std::string escape(const std::string& value) { + std::string result; + result.reserve(value.size()); + for (char c : value) { + if (c == '\\') { + result += "\\\\"; + } else if (c == '\n') { + result += "\\n"; + } else { + result += c; + } + } + return result; +} + +std::string unescape(const std::string& value) { + std::string result; + result.reserve(value.size()); + for (size_t i = 0; i < value.size(); i++) { + if (value[i] == '\\' && i + 1 < value.size()) { + i++; + result += (value[i] == 'n') ? '\n' : value[i]; + } else { + result += value[i]; + } + } + return result; +} + +// Splits a tagged value ("b:1", "i32:42", "i64:123", "s:escaped text") into its type tag and +// raw payload. Returns false if there's no ':' separator (malformed/missing). +bool split_tag(const std::string& tagged_value, std::string& tag, std::string& raw_value) { + size_t colon = tagged_value.find(':'); + if (colon == std::string::npos) { + return false; + } + tag = tagged_value.substr(0, colon); + raw_value = tagged_value.substr(colon + 1); + return true; +} + +bool ensure_directory(const std::string& path) { + struct stat info {}; + if (stat(path.c_str(), &info) == 0) { + return (info.st_mode & S_IFMT) == S_IFDIR; + } + return mkdir(path.c_str(), 0777) == 0; +} + +// mkdir -p. +bool ensure_directory_recursive(const std::string& path) { + for (size_t index = path.find('/', 1); index != std::string::npos; index = path.find('/', index + 1)) { + if (!ensure_directory(path.substr(0, index))) { + return false; + } + } + return ensure_directory(path); +} + +} // namespace + +// Definition of the opaque handle declared in tactility/preferences.h - C callers only ever +// see it through a Preferences* pointer, never its members. Backed by a PropertiesFile +// (tactility/properties_file.h) rather than its own file I/O - each value is stored as a +// tagged string ("b:1", "i32:42", "i64:123", "s:escaped text") since PropertiesFile only knows +// about plain strings. +struct Preferences { + PropertiesFile* file; +}; + +namespace { + +// Grow-and-retry: properties_file_get() needs a bounded buffer, and a string preference's +// value (unlike bool/int32/int64's short encodings) can be arbitrarily long. +bool try_get_tagged(const PropertiesFile* file, const char* key, std::string& tag, std::string& raw_value) { + std::vector buffer(32); + while (true) { + error_t error = properties_file_get(file, key, buffer.data(), buffer.size()); + if (error == ERROR_NONE) { + return split_tag(std::string(buffer.data()), tag, raw_value); + } + if (error == ERROR_NOT_FOUND) { + return false; + } + buffer.resize(buffer.size() * 2); + } +} + +} // namespace + +extern "C" { + +Preferences* preferences_open(const char* path) { + PropertiesFile* file = properties_file_open(path); + if (file == nullptr) { + return nullptr; + } + + auto* preferences = new (std::nothrow) Preferences { file }; + if (preferences == nullptr) { + properties_file_close(file); + return nullptr; + } + return preferences; +} + +void preferences_close(Preferences* preferences) { + properties_file_close(preferences->file); + delete preferences; +} + +bool preferences_has_bool(const Preferences* preferences, const char* key) { + std::string tag, raw_value; + return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "b"; +} + +bool preferences_has_int32(const Preferences* preferences, const char* key) { + std::string tag, raw_value; + return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "i32"; +} + +bool preferences_has_int64(const Preferences* preferences, const char* key) { + std::string tag, raw_value; + return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "i64"; +} + +bool preferences_has_string(const Preferences* preferences, const char* key) { + std::string tag, raw_value; + return try_get_tagged(preferences->file, key, tag, raw_value) && tag == "s"; +} + +bool preferences_opt_bool(const Preferences* preferences, const char* key, bool* out_value) { + std::string tag, raw_value; + if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "b") { + return false; + } + *out_value = (raw_value == "1"); + return true; +} + +bool preferences_opt_int32(const Preferences* preferences, const char* key, int32_t* out_value) { + std::string tag, raw_value; + if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "i32") { + return false; + } + *out_value = static_cast(std::strtol(raw_value.c_str(), nullptr, 10)); + return true; +} + +bool preferences_opt_int64(const Preferences* preferences, const char* key, int64_t* out_value) { + std::string tag, raw_value; + if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "i64") { + return false; + } + *out_value = static_cast(std::strtoll(raw_value.c_str(), nullptr, 10)); + return true; +} + +error_t preferences_opt_string(const Preferences* preferences, const char* key, char* out_value, size_t out_value_size) { + std::string tag, raw_value; + if (!try_get_tagged(preferences->file, key, tag, raw_value) || tag != "s") { + return ERROR_NOT_FOUND; + } + std::string value = unescape(raw_value); + if (value.size() + 1 > out_value_size) { + return ERROR_BUFFER_OVERFLOW; + } + std::memcpy(out_value, value.c_str(), value.size() + 1); + return ERROR_NONE; +} + +void preferences_put_bool(Preferences* preferences, const char* key, bool value) { + properties_file_set(preferences->file, key, value ? "b:1" : "b:0"); +} + +void preferences_put_int32(Preferences* preferences, const char* key, int32_t value) { + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "i32:%" PRId32, value); + properties_file_set(preferences->file, key, buffer); +} + +void preferences_put_int64(Preferences* preferences, const char* key, int64_t value) { + char buffer[40]; + std::snprintf(buffer, sizeof(buffer), "i64:%" PRId64, value); + properties_file_set(preferences->file, key, buffer); +} + +void preferences_put_string(Preferences* preferences, const char* key, const char* value) { + std::string tagged = "s:" + escape(value); + properties_file_set(preferences->file, key, tagged.c_str()); +} + +} // extern "C" diff --git a/TactilityKernel/source/properties_file.cpp b/TactilityKernel/source/properties_file.cpp new file mode 100644 index 000000000..418bb191b --- /dev/null +++ b/TactilityKernel/source/properties_file.cpp @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache-2.0 +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +constexpr auto* TAG = "properties_file"; + +namespace { + +std::string trim(const std::string& value, const char* chars) { + size_t start = value.find_first_not_of(chars); + if (start == std::string::npos) { + return ""; + } + size_t end = value.find_last_not_of(chars); + return value.substr(start, end - start + 1); +} + +bool split_key_value(const std::string& line, std::string& key, std::string& value) { + size_t index = line.find('='); + if (index == std::string::npos) { + return false; + } + key = line.substr(0, index); + value = line.substr(index + 1); + return true; +} + +} // namespace + +// Definition of the opaque handle declared in tactility/properties_file.h - C callers only +// ever see it through a PropertiesFile* pointer, never its members. +struct PropertiesFile { + std::string path; + std::unordered_map entries; +}; + +namespace { + +// Missing file is not an error - a fresh instance just starts out empty and gets created on +// close(). Mirrors Tactility's loadPropertiesFile(): "#"-prefixed and blank lines are skipped; +// a "[section]" line becomes a literal prefix (verbatim, brackets included) prepended to every +// subsequent key, until the next "[section]" line replaces it. +void load_from_file(PropertiesFile* file) { + FileMutex mutex {}; + file_mutex_get(&mutex, file->path.c_str()); + file_mutex_lock(&mutex); + + FILE* handle = std::fopen(file->path.c_str(), "r"); + if (handle == nullptr) { + file_mutex_unlock(&mutex); + return; + } + + std::string key_prefix; + std::string raw_line; + uint32_t line_number = 0; + + auto flush_line = [&]() { + line_number++; + std::string trimmed_line = trim(raw_line, " \t\r\n"); + raw_line.clear(); + + if (trimmed_line.empty() || trimmed_line.starts_with("#")) { + return; + } + if (trimmed_line.starts_with("[")) { + key_prefix = trimmed_line; + return; + } + + std::string key, value; + if (!split_key_value(trimmed_line, key, value)) { + LOG_E(TAG, "Failed to parse line %u of %s (skipped)", line_number, file->path.c_str()); + return; + } + file->entries[key_prefix + trim(key, " \t")] = trim(value, " \t"); + }; + + int c; + while ((c = std::fgetc(handle)) != EOF) { + if (c == '\n') { + flush_line(); + } else { + raw_line += static_cast(c); + } + } + flush_line(); + + std::fclose(handle); + file_mutex_unlock(&mutex); +} + +void save_to_file(const PropertiesFile* file) { + FileMutex mutex {}; + file_mutex_get(&mutex, file->path.c_str()); + file_mutex_lock(&mutex); + + FILE* handle = std::fopen(file->path.c_str(), "w"); + if (handle == nullptr) { + LOG_E(TAG, "Failed to open %s", file->path.c_str()); + file_mutex_unlock(&mutex); + return; + } + + for (const auto& [key, value] : file->entries) { + std::fprintf(handle, "%s=%s\n", key.c_str(), value.c_str()); + } + + std::fclose(handle); + file_mutex_unlock(&mutex); +} + +} // namespace + +extern "C" { + +PropertiesFile* properties_file_open(const char* path) { + auto* file = new (std::nothrow) PropertiesFile(); + if (file == nullptr) { + return nullptr; + } + file->path = path; + load_from_file(file); + return file; +} + +void properties_file_close(PropertiesFile* file) { + save_to_file(file); + delete file; +} + +bool properties_file_has(const PropertiesFile* file, const char* key) { + return file->entries.contains(key); +} + +error_t properties_file_get(const PropertiesFile* file, const char* key, char* out_value, size_t out_value_size) { + auto entry = file->entries.find(key); + if (entry == file->entries.end()) { + return ERROR_NOT_FOUND; + } + const std::string& value = entry->second; + if (value.size() + 1 > out_value_size) { + return ERROR_BUFFER_OVERFLOW; + } + std::memcpy(out_value, value.c_str(), value.size() + 1); + return ERROR_NONE; +} + +void properties_file_set(PropertiesFile* file, const char* key, const char* value) { + file->entries[key] = value; +} + +void properties_file_for_each(const PropertiesFile* file, PropertiesFileVisitorFn visitor, void* context) { + for (const auto& [key, value] : file->entries) { + visitor(key.c_str(), value.c_str(), context); + } +} + +} // extern "C" diff --git a/TactilityKernel/source/symbols.c b/TactilityKernel/source/symbols.c index 48c500737..6928bd486 100644 --- a/TactilityKernel/source/symbols.c +++ b/TactilityKernel/source/symbols.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -41,6 +42,8 @@ #include #include #include +#include +#include #include #ifndef ESP_PLATFORM @@ -470,6 +473,48 @@ const struct ModuleSymbol KERNEL_SYMBOLS[] = { DEFINE_MODULE_SYMBOL(module_is_started), DEFINE_MODULE_SYMBOL(module_resolve_symbol), DEFINE_MODULE_SYMBOL(module_resolve_symbol_global), + // bundle + DEFINE_MODULE_SYMBOL(bundle_alloc), + DEFINE_MODULE_SYMBOL(bundle_clone), + DEFINE_MODULE_SYMBOL(bundle_free), + DEFINE_MODULE_SYMBOL(bundle_get_bool), + DEFINE_MODULE_SYMBOL(bundle_get_int32), + DEFINE_MODULE_SYMBOL(bundle_get_int64), + DEFINE_MODULE_SYMBOL(bundle_get_string), + DEFINE_MODULE_SYMBOL(bundle_has_bool), + DEFINE_MODULE_SYMBOL(bundle_has_int32), + DEFINE_MODULE_SYMBOL(bundle_has_int64), + DEFINE_MODULE_SYMBOL(bundle_has_string), + DEFINE_MODULE_SYMBOL(bundle_opt_bool), + DEFINE_MODULE_SYMBOL(bundle_opt_int32), + DEFINE_MODULE_SYMBOL(bundle_opt_int64), + DEFINE_MODULE_SYMBOL(bundle_opt_string), + DEFINE_MODULE_SYMBOL(bundle_put_bool), + DEFINE_MODULE_SYMBOL(bundle_put_int32), + DEFINE_MODULE_SYMBOL(bundle_put_int64), + DEFINE_MODULE_SYMBOL(bundle_put_string), + // preferences + DEFINE_MODULE_SYMBOL(preferences_open), + DEFINE_MODULE_SYMBOL(preferences_close), + DEFINE_MODULE_SYMBOL(preferences_has_bool), + DEFINE_MODULE_SYMBOL(preferences_has_int32), + DEFINE_MODULE_SYMBOL(preferences_has_int64), + DEFINE_MODULE_SYMBOL(preferences_has_string), + DEFINE_MODULE_SYMBOL(preferences_opt_bool), + DEFINE_MODULE_SYMBOL(preferences_opt_int32), + DEFINE_MODULE_SYMBOL(preferences_opt_int64), + DEFINE_MODULE_SYMBOL(preferences_opt_string), + DEFINE_MODULE_SYMBOL(preferences_put_bool), + DEFINE_MODULE_SYMBOL(preferences_put_int32), + DEFINE_MODULE_SYMBOL(preferences_put_int64), + DEFINE_MODULE_SYMBOL(preferences_put_string), + // properties_file + DEFINE_MODULE_SYMBOL(properties_file_open), + DEFINE_MODULE_SYMBOL(properties_file_close), + DEFINE_MODULE_SYMBOL(properties_file_has), + DEFINE_MODULE_SYMBOL(properties_file_get), + DEFINE_MODULE_SYMBOL(properties_file_set), + DEFINE_MODULE_SYMBOL(properties_file_for_each), // terminator MODULE_SYMBOL_TERMINATOR }; diff --git a/TactilityKernel/source/system_event.cpp b/TactilityKernel/source/system_event.cpp index 679d5e3e1..403845b7a 100644 --- a/TactilityKernel/source/system_event.cpp +++ b/TactilityKernel/source/system_event.cpp @@ -78,16 +78,16 @@ static void notify_poll_subscribers( ) { mutex_lock(&poll_subscriptions_mutex.handle); - for (SystemEventSubscription* sub = poll_subscriptions; sub != nullptr; sub = sub->next) { - if (sub->type == type) { - sub->timestamp = timestamp; + for (SystemEventSubscription* sub = poll_subscriptions; sub != nullptr; sub = sub->internal.next) { + if (sub->event.type == type) { + sub->event.timestamp = timestamp; const size_t copied_len = std::min(data_len, SYSTEM_EVENT_MAX_DATA_SIZE); if (copied_len > 0) { - std::memcpy(sub->data, data, copied_len); + std::memcpy(sub->event.data, data, copied_len); } - sub->data_len = copied_len; - sub->sequence++; - xSemaphoreGive(sub->semaphore); + sub->event.data_len = copied_len; + sub->internal.sequence++; + xSemaphoreGive(sub->internal.semaphore); } } @@ -148,12 +148,14 @@ error_t system_event_emit( const void* data, size_t data_len ) { - SystemEvent event = { - .type = type, - .timestamp = get_micros_since_boot(), - .data = data, - .data_len = data_len, - }; + SystemEvent event {}; + event.type = type; + event.timestamp = get_micros_since_boot(); + const size_t copied_len = std::min(data_len, SYSTEM_EVENT_MAX_DATA_SIZE); + if (copied_len > 0) { + std::memcpy(event.data, data, copied_len); + } + event.data_len = copied_len; notify_poll_subscribers(type, event.timestamp, data, data_len); auto error = notify_listeners(event); @@ -173,7 +175,7 @@ error_t system_event_subscribe(SystemEventSubscription* sub) { // 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->next) { + for (SystemEventSubscription* existing = poll_subscriptions; existing != nullptr; existing = existing->internal.next) { if (existing == sub) { mutex_unlock(&poll_subscriptions_mutex.handle); vSemaphoreDelete(semaphore); @@ -181,11 +183,11 @@ error_t system_event_subscribe(SystemEventSubscription* sub) { } } - sub->semaphore = semaphore; - sub->sequence = 0; - sub->consumed_sequence = 0; - sub->data_len = 0; - sub->next = poll_subscriptions; + sub->internal.semaphore = semaphore; + sub->internal.sequence = 0; + sub->internal.consumed_sequence = 0; + sub->event.data_len = 0; + sub->internal.next = poll_subscriptions; poll_subscriptions = sub; mutex_unlock(&poll_subscriptions_mutex.handle); @@ -197,9 +199,9 @@ error_t system_event_unsubscribe(SystemEventSubscription* sub) { error_t result = ERROR_NOT_FOUND; mutex_lock(&poll_subscriptions_mutex.handle); - for (SystemEventSubscription** link = &poll_subscriptions; *link != nullptr; link = &(*link)->next) { + for (SystemEventSubscription** link = &poll_subscriptions; *link != nullptr; link = &(*link)->internal.next) { if (*link == sub) { - *link = sub->next; + *link = sub->internal.next; result = ERROR_NONE; break; } @@ -209,23 +211,31 @@ error_t system_event_unsubscribe(SystemEventSubscription* sub) { if (result == ERROR_NONE) { // Unlinked first, so notify_poll_subscribers() can no longer reach this semaphore // before it's deleted. - vSemaphoreDelete(sub->semaphore); - sub->semaphore = nullptr; + vSemaphoreDelete(sub->internal.semaphore); + sub->internal.semaphore = nullptr; } return result; } error_t system_event_await(SystemEventSubscription* sub, TickType_t timeout) { - uint32_t old_sequence = sub->sequence; + uint32_t old_sequence = sub->internal.sequence; - while (sub->sequence == old_sequence) { - if (xSemaphoreTake(sub->semaphore, timeout) == pdFALSE) { + while (sub->internal.sequence == old_sequence) { + if (xSemaphoreTake(sub->internal.semaphore, timeout) == pdFALSE) { return ERROR_TIMEOUT; } } - sub->consumed_sequence = sub->sequence; + sub->internal.consumed_sequence = sub->internal.sequence; + return ERROR_NONE; +} + +error_t system_event_get_data(SystemEventSubscription* sub, uint8_t* data, size_t data_len) { + if (data_len < sub->event.data_len) { + return ERROR_BUFFER_OVERFLOW; + } + std::memcpy(data, sub->event.data, sub->event.data_len); return ERROR_NONE; } diff --git a/Tests/TactilityKernel/Source/BundleTest.cpp b/Tests/TactilityKernel/Source/BundleTest.cpp new file mode 100644 index 000000000..f1195a345 --- /dev/null +++ b/Tests/TactilityKernel/Source/BundleTest.cpp @@ -0,0 +1,134 @@ +#include "doctest.h" +#include + +#include + +TEST_CASE("bundle_alloc/bundle_free round-trip") { + Bundle* bundle = bundle_alloc(); + CHECK_NE(bundle, nullptr); + bundle_free(bundle); +} + +TEST_CASE("bool can be stored and retrieved") { + Bundle* bundle = bundle_alloc(); + bundle_put_bool(bundle, "key", true); + + CHECK(bundle_has_bool(bundle, "key")); + CHECK_EQ(bundle_get_bool(bundle, "key"), true); + + bool out = false; + CHECK(bundle_opt_bool(bundle, "key", &out)); + CHECK_EQ(out, true); + + bundle_free(bundle); +} + +TEST_CASE("int32 can be stored and retrieved") { + Bundle* bundle = bundle_alloc(); + bundle_put_int32(bundle, "key", -42); + + CHECK(bundle_has_int32(bundle, "key")); + CHECK_EQ(bundle_get_int32(bundle, "key"), -42); + + int32_t out = 0; + CHECK(bundle_opt_int32(bundle, "key", &out)); + CHECK_EQ(out, -42); + + bundle_free(bundle); +} + +TEST_CASE("int64 can be stored and retrieved") { + Bundle* bundle = bundle_alloc(); + bundle_put_int64(bundle, "key", 123456789012345LL); + + CHECK(bundle_has_int64(bundle, "key")); + CHECK_EQ(bundle_get_int64(bundle, "key"), 123456789012345LL); + + int64_t out = 0; + CHECK(bundle_opt_int64(bundle, "key", &out)); + CHECK_EQ(out, 123456789012345LL); + + bundle_free(bundle); +} + +TEST_CASE("string can be stored and retrieved") { + Bundle* bundle = bundle_alloc(); + bundle_put_string(bundle, "key", "hello world"); + + CHECK(bundle_has_string(bundle, "key")); + + char buffer[32]; + CHECK_EQ(bundle_get_string(bundle, "key", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "hello world"), 0); + + char tiny[4]; + CHECK_EQ(bundle_get_string(bundle, "key", tiny, sizeof(tiny)), ERROR_BUFFER_OVERFLOW); + + char out[32]; + CHECK_EQ(bundle_opt_string(bundle, "key", out, sizeof(out)), ERROR_NONE); + CHECK_EQ(std::strcmp(out, "hello world"), 0); + + bundle_free(bundle); +} + +TEST_CASE("has_*/opt_* reject a key stored with a different type") { + Bundle* bundle = bundle_alloc(); + bundle_put_bool(bundle, "key", true); + + CHECK_FALSE(bundle_has_int32(bundle, "key")); + CHECK_FALSE(bundle_has_int64(bundle, "key")); + CHECK_FALSE(bundle_has_string(bundle, "key")); + + int32_t out_int32 = 0; + CHECK_FALSE(bundle_opt_int32(bundle, "key", &out_int32)); + + char out_string[8]; + CHECK_EQ(bundle_opt_string(bundle, "key", out_string, sizeof(out_string)), ERROR_NOT_FOUND); + + bundle_free(bundle); +} + +TEST_CASE("opt_string reports ERROR_NOT_FOUND for a missing key") { + Bundle* bundle = bundle_alloc(); + char out[8]; + CHECK_EQ(bundle_opt_string(bundle, "missing", out, sizeof(out)), ERROR_NOT_FOUND); + bundle_free(bundle); +} + +TEST_CASE("bundle_clone makes an independent deep copy") { + Bundle* original = bundle_alloc(); + bundle_put_bool(original, "bool", true); + bundle_put_int32(original, "int32", 123); + bundle_put_string(original, "string", "text"); + + Bundle* clone = bundle_clone(original); + bundle_free(original); // clone must not be affected + + CHECK_EQ(bundle_get_bool(clone, "bool"), true); + CHECK_EQ(bundle_get_int32(clone, "int32"), 123); + + char buffer[16]; + CHECK_EQ(bundle_get_string(clone, "string", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "text"), 0); + + // Mutating the clone must not affect a re-clone of the (already-freed) original's data. + bundle_put_int32(clone, "int32", 456); + CHECK_EQ(bundle_get_int32(clone, "int32"), 456); + + bundle_free(clone); +} + +TEST_CASE("put overwrites a previously stored value, including across types") { + Bundle* bundle = bundle_alloc(); + bundle_put_int32(bundle, "key", 1); + bundle_put_string(bundle, "key", "now a string"); + + CHECK_FALSE(bundle_has_int32(bundle, "key")); + CHECK(bundle_has_string(bundle, "key")); + + char buffer[32]; + CHECK_EQ(bundle_get_string(bundle, "key", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "now a string"), 0); + + bundle_free(bundle); +} diff --git a/Tests/TactilityKernel/Source/PreferencesTest.cpp b/Tests/TactilityKernel/Source/PreferencesTest.cpp new file mode 100644 index 000000000..2c1d28987 --- /dev/null +++ b/Tests/TactilityKernel/Source/PreferencesTest.cpp @@ -0,0 +1,153 @@ +#include "doctest.h" +#include + +#include +#include + +namespace { + +const char* TEST_PATH = "/tmp/tactility_kernel_preferences_test.properties"; + +struct ScratchFile { + ScratchFile() { std::remove(TEST_PATH); } + ~ScratchFile() { std::remove(TEST_PATH); } +}; + +bool file_exists(const char* path) { + FILE* file = std::fopen(path, "r"); + if (file == nullptr) { + return false; + } + std::fclose(file); + return true; +} + +} // namespace + +TEST_CASE("preferences_open_path on a missing file starts out empty, without creating it") { + ScratchFile scratch; + + Preferences* preferences = preferences_open(TEST_PATH); + CHECK_NE(preferences, nullptr); + CHECK_FALSE(preferences_has_bool(preferences, "key")); + CHECK_FALSE(file_exists(TEST_PATH)); + + preferences_close(preferences); +} + +TEST_CASE("put_*/has_*/opt_* round-trip all four types") { + ScratchFile scratch; + + Preferences* preferences = preferences_open(TEST_PATH); + preferences_put_bool(preferences, "flag", true); + preferences_put_int32(preferences, "count", -42); + preferences_put_int64(preferences, "big", 123456789012345LL); + preferences_put_string(preferences, "text", "hello world"); + + CHECK(preferences_has_bool(preferences, "flag")); + bool bool_out = false; + CHECK(preferences_opt_bool(preferences, "flag", &bool_out)); + CHECK_EQ(bool_out, true); + + CHECK(preferences_has_int32(preferences, "count")); + int32_t int32_out = 0; + CHECK(preferences_opt_int32(preferences, "count", &int32_out)); + CHECK_EQ(int32_out, -42); + + CHECK(preferences_has_int64(preferences, "big")); + int64_t int64_out = 0; + CHECK(preferences_opt_int64(preferences, "big", &int64_out)); + CHECK_EQ(int64_out, 123456789012345LL); + + CHECK(preferences_has_string(preferences, "text")); + char buffer[32]; + CHECK_EQ(preferences_opt_string(preferences, "text", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "hello world"), 0); + + preferences_close(preferences); +} + +TEST_CASE("opt_string reports ERROR_BUFFER_OVERFLOW and ERROR_NOT_FOUND") { + ScratchFile scratch; + + Preferences* preferences = preferences_open(TEST_PATH); + preferences_put_string(preferences, "text", "hello world"); + + char tiny[4]; + CHECK_EQ(preferences_opt_string(preferences, "text", tiny, sizeof(tiny)), ERROR_BUFFER_OVERFLOW); + + char buffer[32]; + CHECK_EQ(preferences_opt_string(preferences, "missing", buffer, sizeof(buffer)), ERROR_NOT_FOUND); + + preferences_close(preferences); +} + +TEST_CASE("has_*/opt_* reject a key stored with a different type") { + ScratchFile scratch; + + Preferences* preferences = preferences_open(TEST_PATH); + preferences_put_bool(preferences, "key", true); + + CHECK_FALSE(preferences_has_int32(preferences, "key")); + CHECK_FALSE(preferences_has_int64(preferences, "key")); + CHECK_FALSE(preferences_has_string(preferences, "key")); + + int32_t out = 0; + CHECK_FALSE(preferences_opt_int32(preferences, "key", &out)); + + preferences_close(preferences); +} + +TEST_CASE("a string value with embedded newlines and backslashes survives a reopen") { + ScratchFile scratch; + + { + Preferences* preferences = preferences_open(TEST_PATH); + preferences_put_string(preferences, "text", "line1\nline2 with \\ backslash"); + preferences_close(preferences); + } + { + Preferences* preferences = preferences_open(TEST_PATH); + char buffer[64]; + CHECK_EQ(preferences_opt_string(preferences, "text", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "line1\nline2 with \\ backslash"), 0); + preferences_close(preferences); + } +} + +TEST_CASE("preferences_close persists changes, and only close persists them") { + ScratchFile scratch; + + Preferences* preferences = preferences_open(TEST_PATH); + preferences_put_bool(preferences, "flag", true); + + // Not persisted yet - only preferences_close() writes to disk. + CHECK_FALSE(file_exists(TEST_PATH)); + + preferences_close(preferences); + CHECK(file_exists(TEST_PATH)); + + Preferences* reopened = preferences_open(TEST_PATH); + CHECK(preferences_has_bool(reopened, "flag")); + preferences_close(reopened); +} + +TEST_CASE("put_* on an already-closed value is visible without reopening") { + ScratchFile scratch; + + Preferences* preferences = preferences_open(TEST_PATH); + preferences_put_int32(preferences, "count", 1); + preferences_close(preferences); + + Preferences* reopened = preferences_open(TEST_PATH); + preferences_put_int32(reopened, "count", 2); + int32_t out = 0; + CHECK(preferences_opt_int32(reopened, "count", &out)); + CHECK_EQ(out, 2); + preferences_close(reopened); + + Preferences* final_instance = preferences_open(TEST_PATH); + CHECK(preferences_opt_int32(final_instance, "count", &out)); + CHECK_EQ(out, 2); + preferences_close(final_instance); +} diff --git a/Tests/TactilityKernel/Source/PropertiesFileTest.cpp b/Tests/TactilityKernel/Source/PropertiesFileTest.cpp new file mode 100644 index 000000000..6f7a4b579 --- /dev/null +++ b/Tests/TactilityKernel/Source/PropertiesFileTest.cpp @@ -0,0 +1,180 @@ +#include "doctest.h" +#include + +#include +#include +#include +#include +#include + +namespace { + +const char* TEST_PATH = "/tmp/tactility_kernel_properties_file_test.properties"; + +struct ScratchFile { + ScratchFile() { std::remove(TEST_PATH); } + ~ScratchFile() { std::remove(TEST_PATH); } +}; + +bool file_exists(const char* path) { + FILE* file = std::fopen(path, "r"); + if (file == nullptr) { + return false; + } + std::fclose(file); + return true; +} + +void write_raw(const char* path, const char* content) { + FILE* file = std::fopen(path, "w"); + std::fputs(content, file); + std::fclose(file); +} + +} // namespace + +TEST_CASE("properties_file_open on a missing file starts out empty, without creating it") { + ScratchFile scratch; + + PropertiesFile* file = properties_file_open(TEST_PATH); + CHECK_NE(file, nullptr); + CHECK_FALSE(properties_file_has(file, "key")); + CHECK_FALSE(file_exists(TEST_PATH)); + + properties_file_close(file); +} + +TEST_CASE("set/has/get round-trip, and close persists while unclosed changes don't") { + ScratchFile scratch; + + PropertiesFile* file = properties_file_open(TEST_PATH); + properties_file_set(file, "key", "value"); + + CHECK(properties_file_has(file, "key")); + char buffer[32]; + CHECK_EQ(properties_file_get(file, "key", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "value"), 0); + + // Not persisted yet - only properties_file_close() writes to disk. + CHECK_FALSE(file_exists(TEST_PATH)); + + properties_file_close(file); + CHECK(file_exists(TEST_PATH)); + + PropertiesFile* reopened = properties_file_open(TEST_PATH); + CHECK(properties_file_has(reopened, "key")); + properties_file_close(reopened); +} + +TEST_CASE("properties_file_get reports ERROR_BUFFER_OVERFLOW and ERROR_NOT_FOUND") { + ScratchFile scratch; + + PropertiesFile* file = properties_file_open(TEST_PATH); + properties_file_set(file, "key", "value"); + + char tiny[3]; + CHECK_EQ(properties_file_get(file, "key", tiny, sizeof(tiny)), ERROR_BUFFER_OVERFLOW); + + char buffer[32]; + CHECK_EQ(properties_file_get(file, "missing", buffer, sizeof(buffer)), ERROR_NOT_FOUND); + + properties_file_close(file); +} + +TEST_CASE("set overwrites a previously stored value") { + ScratchFile scratch; + + PropertiesFile* file = properties_file_open(TEST_PATH); + properties_file_set(file, "key", "first"); + properties_file_set(file, "key", "second"); + + char buffer[32]; + CHECK_EQ(properties_file_get(file, "key", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "second"), 0); + + properties_file_close(file); +} + +TEST_CASE("comments and blank lines are skipped, keys and values are trimmed") { + ScratchFile scratch; + write_raw(TEST_PATH, + "# Comment\n" + " \t# Indented comment\n" + "\n" + "key1=value1\n" + " \tkey 2\t = \tvalue 2\t \n"); + + PropertiesFile* file = properties_file_open(TEST_PATH); + + char buffer[32]; + CHECK_EQ(properties_file_get(file, "key1", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "value1"), 0); + + // Only leading/trailing whitespace is trimmed - the internal space in "key 2"/"value 2" + // survives. + CHECK_EQ(properties_file_get(file, "key 2", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "value 2"), 0); + + properties_file_close(file); +} + +TEST_CASE("a malformed line (no '=') is skipped without aborting the rest of the file") { + ScratchFile scratch; + write_raw(TEST_PATH, "not_a_key_value_pair\nkey=value\n"); + + PropertiesFile* file = properties_file_open(TEST_PATH); + CHECK(properties_file_has(file, "key")); + CHECK_FALSE(properties_file_has(file, "not_a_key_value_pair")); + properties_file_close(file); +} + +TEST_CASE("a [section] line prefixes every following key until the next section") { + ScratchFile scratch; + write_raw(TEST_PATH, + "[app]\n" + "id=one.tactility.helloworld\n" + "name=Hello\n" + "[other]\n" + "id=x\n"); + + PropertiesFile* file = properties_file_open(TEST_PATH); + + char buffer[64]; + CHECK_EQ(properties_file_get(file, "[app]id", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "one.tactility.helloworld"), 0); + + CHECK_EQ(properties_file_get(file, "[app]name", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "Hello"), 0); + + CHECK_EQ(properties_file_get(file, "[other]id", buffer, sizeof(buffer)), ERROR_NONE); + CHECK_EQ(std::strcmp(buffer, "x"), 0); + + CHECK_FALSE(properties_file_has(file, "id")); + + properties_file_close(file); +} + +TEST_CASE("properties_file_for_each visits every key exactly once") { + ScratchFile scratch; + + PropertiesFile* file = properties_file_open(TEST_PATH); + properties_file_set(file, "a", "1"); + properties_file_set(file, "b", "2"); + properties_file_set(file, "c", "3"); + + std::vector> seen; + properties_file_for_each(file, [](const char* key, const char* value, void* context) { + auto* out = static_cast>*>(context); + out->emplace_back(key, value); + }, &seen); + + CHECK_EQ(seen.size(), 3); + for (const auto& [key, value] : seen) { + if (key == "a") CHECK_EQ(value, "1"); + else if (key == "b") CHECK_EQ(value, "2"); + else if (key == "c") CHECK_EQ(value, "3"); + else FAIL("unexpected key: " << key); + } + + properties_file_close(file); +} diff --git a/Tests/TactilityKernel/Source/SystemEventTest.cpp b/Tests/TactilityKernel/Source/SystemEventTest.cpp index 0ea1b7157..fa7c8e09d 100644 --- a/Tests/TactilityKernel/Source/SystemEventTest.cpp +++ b/Tests/TactilityKernel/Source/SystemEventTest.cpp @@ -15,8 +15,10 @@ struct RecordedCall { void* context; SystemEventType type; - const void* data; - size_t data_len; + // Copied out of event->data during the callback - event->data is only valid for the + // duration of the callback (it lives in system_event_emit()'s own stack frame), so a bare + // pointer/length pair recorded here would dangle by the time a TEST_CASE inspects it. + std::vector data; uint64_t timestamp; }; @@ -24,11 +26,11 @@ static std::vector calls_a; static std::vector calls_b; static void listener_a(SystemEvent* event, void* context) { - calls_a.push_back({ context, event->type, event->data, event->data_len, event->timestamp }); + calls_a.push_back({ context, event->type, std::vector(event->data, event->data + event->data_len), event->timestamp }); } static void listener_b(SystemEvent* event, void* context) { - calls_b.push_back({ context, event->type, event->data, event->data_len, event->timestamp }); + calls_b.push_back({ context, event->type, std::vector(event->data, event->data + event->data_len), event->timestamp }); } static void reset_calls() { @@ -71,7 +73,7 @@ TEST_CASE("system_event_emit only invokes subscribers registered for the emitted system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a); } -TEST_CASE("system_event_emit passes the data pointer and length through unchanged") { +TEST_CASE("system_event_emit copies the data into the delivered event") { reset_calls(); int context_a = 1; struct Payload { int value; } payload { 42 }; @@ -80,14 +82,13 @@ TEST_CASE("system_event_emit passes the data pointer and length through unchange system_event_emit(KERNEL_EVENT_TIME_CHANGED, &payload, sizeof(payload)); REQUIRE_EQ(calls_a.size(), 1); - CHECK_EQ(calls_a[0].data, &payload); - CHECK_EQ(calls_a[0].data_len, sizeof(payload)); - CHECK_EQ(static_cast(calls_a[0].data)->value, 42); + REQUIRE_EQ(calls_a[0].data.size(), sizeof(payload)); + CHECK_EQ(reinterpret_cast(calls_a[0].data.data())->value, 42); system_event_callback_remove(KERNEL_EVENT_TIME_CHANGED, listener_a); } -TEST_CASE("system_event_emit with no data passes a null pointer and zero length") { +TEST_CASE("system_event_emit with no data delivers an empty payload") { reset_calls(); int context_a = 1; system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, listener_a, &context_a); @@ -95,8 +96,7 @@ TEST_CASE("system_event_emit with no data passes a null pointer and zero length" system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0); REQUIRE_EQ(calls_a.size(), 1); - CHECK_EQ(calls_a[0].data, nullptr); - CHECK_EQ(calls_a[0].data_len, 0); + CHECK(calls_a[0].data.empty()); system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, listener_a); } @@ -175,7 +175,7 @@ TEST_CASE("system_event_emit stamps the event with the current boot-relative tim static bool reentrant_add_triggered = false; static void reentrant_listener(SystemEvent* event, void* context) { - calls_a.push_back({ context, event->type, event->data, event->data_len, event->timestamp }); + calls_a.push_back({ context, event->type, std::vector(event->data, event->data + event->data_len), event->timestamp }); if (!reentrant_add_triggered) { reentrant_add_triggered = true; // Subscribing from within a notification must not deadlock: emit() releases the @@ -225,7 +225,7 @@ TEST_CASE("system_event_emit is safe when a callback subscribes, unsubscribes an TEST_CASE("system_event_subscribe/_await deliver the event payload by value") { SystemEventSubscription sub {}; - sub.type = KERNEL_EVENT_NETWORK_CONNECTED; + sub.event.type = KERNEL_EVENT_NETWORK_CONNECTED; CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE); NetworkConnectedEvent connected { .device = nullptr, .ipv4_addr = 0x0A000001, .gateway = 0x0A0000FE }; @@ -245,10 +245,10 @@ TEST_CASE("system_event_subscribe/_await deliver the event payload by value") { CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE); - const auto* received = reinterpret_cast(sub.data); - CHECK_EQ(received->ipv4_addr, connected.ipv4_addr); - CHECK_EQ(received->gateway, connected.gateway); - CHECK_EQ(sub.data_len, sizeof(connected)); + 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); CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); thread_free(thread); @@ -259,7 +259,7 @@ TEST_CASE("system_event_subscribe/_await deliver the event payload by value") { TEST_CASE("system_event_await times out when no matching event has arrived") { SystemEventSubscription sub {}; - sub.type = KERNEL_EVENT_TIME_CHANGED; + sub.event.type = KERNEL_EVENT_TIME_CHANGED; system_event_subscribe(&sub); CHECK_EQ(system_event_await(&sub, 0), ERROR_TIMEOUT); @@ -269,7 +269,7 @@ TEST_CASE("system_event_await times out when no matching event has arrived") { TEST_CASE("system_event_emit does not notify a poll subscriber of a different type") { SystemEventSubscription sub {}; - sub.type = KERNEL_EVENT_BOOT_COMPLETED; + sub.event.type = KERNEL_EVENT_BOOT_COMPLETED; system_event_subscribe(&sub); system_event_emit(KERNEL_EVENT_TIME_CHANGED, nullptr, 0); @@ -277,3 +277,67 @@ TEST_CASE("system_event_emit does not notify a poll subscriber of a different ty system_event_unsubscribe(&sub); } + +TEST_CASE("system_event_get_data reports ERROR_BUFFER_OVERFLOW and leaves the buffer untouched") { + SystemEventSubscription sub {}; + sub.event.type = KERNEL_EVENT_NETWORK_DISCONNECTED; + CHECK_EQ(system_event_subscribe(&sub), 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. + NetworkDisconnectedEvent disconnected { .device = nullptr }; + auto* thread = thread_alloc_full( + "system-event-emitter", + 4096, + [](void* context) { + delay_millis(20); + auto* disconnected_ptr = static_cast(context); + system_event_emit(KERNEL_EVENT_NETWORK_DISCONNECTED, disconnected_ptr, sizeof(*disconnected_ptr)); + return 0; + }, + &disconnected, + -1 + ); + CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE); + CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); + thread_free(thread); + + uint8_t tiny[1] = { 0xAA }; + CHECK_EQ(system_event_get_data(&sub, tiny, sizeof(tiny)), ERROR_BUFFER_OVERFLOW); + CHECK_EQ(tiny[0], 0xAA); + + uint8_t exact[sizeof(NetworkDisconnectedEvent)]; + CHECK_EQ(system_event_get_data(&sub, exact, sizeof(exact)), ERROR_NONE); + + system_event_unsubscribe(&sub); +} + +TEST_CASE("system_event_get_data on a subscription with no payload copies nothing and succeeds") { + SystemEventSubscription sub {}; + sub.event.type = KERNEL_EVENT_BOOT_COMPLETED; + CHECK_EQ(system_event_subscribe(&sub), ERROR_NONE); + + auto* thread = thread_alloc_full( + "system-event-emitter", + 4096, + [](void*) { + delay_millis(20); + system_event_emit(KERNEL_EVENT_BOOT_COMPLETED, nullptr, 0); + return 0; + }, + nullptr, + -1 + ); + CHECK_EQ(thread_start(thread), ERROR_NONE); + CHECK_EQ(system_event_await(&sub, pdMS_TO_TICKS(2000)), ERROR_NONE); + CHECK_EQ(thread_join(thread, 2, 1), ERROR_NONE); + thread_free(thread); + + uint8_t buffer[1] = { 0x42 }; + CHECK_EQ(system_event_get_data(&sub, buffer, 0), ERROR_NONE); + CHECK_EQ(buffer[0], 0x42); // untouched - nothing to copy + + system_event_unsubscribe(&sub); +} diff --git a/Tests/app-module/Source/AppManagerTest.cpp b/Tests/app-module/Source/AppManagerTest.cpp index 43f999dd1..5cd439fa1 100644 --- a/Tests/app-module/Source/AppManagerTest.cpp +++ b/Tests/app-module/Source/AppManagerTest.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -30,12 +31,22 @@ error_t fake_load(AppLocation, void** out_runtime) { int last_received_argc = -1; std::vector last_received_argv; +// fake_run() runs on the app's own task; stash_received_arguments() writes +// last_received_argc/last_received_argv there while a test thread reads them - wait_for_state() +// only establishes that the instance reached APP_INSTANCE_STATE_ACTIVE (set before +// AppLoaderApi::run() is even called, i.e. before fake_run() runs at all), not that +// stash_received_arguments() has finished writing. This flag is the actual ordering: reset +// before starting the app, set (release) as the last step of stash_received_arguments(), waited +// on (acquire) before a test reads the stashed values. +std::atomic arguments_stashed { false }; + void stash_received_arguments(int argc, char* argv[]) { last_received_argc = argc; last_received_argv.clear(); for (int i = 0; i < argc; i++) { last_received_argv.emplace_back(argv[i]); } + arguments_stashed.store(true, std::memory_order_release); } // A minimal stand-in for a real app's main(): subscribes to its own app_event stream and exits @@ -141,6 +152,18 @@ bool wait_for_state(uint32_t instance_id, AppInstanceState target, uint32_t time return app_manager_get_state(instance_id) == target; } +bool wait_for_arguments_stashed(uint32_t timeout_ms) { + uint32_t waited = 0; + while (waited < timeout_ms) { + if (arguments_stashed.load(std::memory_order_acquire)) { + return true; + } + delay_millis(10); + waited += 10; + } + return arguments_stashed.load(std::memory_order_acquire); +} + } // namespace TEST_CASE("app_manager_start activates an app instance, app_manager_stop terminates it") { @@ -217,6 +240,7 @@ TEST_CASE("app_manager_start_with_parameters deep-copies argv before the app ins REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE); uint32_t instance_id = 0; + arguments_stashed.store(false, std::memory_order_relaxed); { // Caller's argv is stack-local and goes out of scope immediately after this block - // proves app-module made its own copy rather than aliasing the caller's strings. @@ -226,6 +250,7 @@ TEST_CASE("app_manager_start_with_parameters deep-copies argv before the app ins REQUIRE_EQ(app_manager_start_with_parameters("test.app.args", 2, argv, &instance_id), ERROR_NONE); } CHECK(wait_for_state(instance_id, APP_INSTANCE_STATE_ACTIVE, 1000)); + REQUIRE(wait_for_arguments_stashed(1000)); REQUIRE_EQ(last_received_argc, 2); REQUIRE_EQ(last_received_argv.size(), 2u); diff --git a/Tests/app-module/Source/Main.cpp b/Tests/app-module/Source/Main.cpp index acd1df905..731b8be76 100644 --- a/Tests/app-module/Source/Main.cpp +++ b/Tests/app-module/Source/Main.cpp @@ -43,7 +43,10 @@ int main(int argc, char** argv) { 1, nullptr ); - assert(task_result == pdPASS); + + if (task_result != pdPASS) { + return 1; + } vTaskStartScheduler();