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();