Fixes and improvements

This commit is contained in:
Ken Van Hoeylandt 2026-08-08 16:44:58 +02:00
parent c628668e75
commit 1ae6e77083
48 changed files with 2315 additions and 740 deletions

View File

@ -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

View File

@ -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:

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### 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.

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### 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.

View File

@ -1,3 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus

View File

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

View File

@ -1,3 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif

View File

@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <tactility/error.h>
#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

View File

@ -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 <tactility/filesystem/file_mutex.h>
#include <cstring>
#include <dirent.h>
@ -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<std::string>& 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<std::string> 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);
}
}
}

View File

@ -8,7 +8,7 @@
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/task.h>
#include <cstdint>
#include <stdint.h>
#include <string>
#include <unordered_map>
@ -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 {

View File

@ -5,8 +5,6 @@
#include <tactility/error.h>
#include <cstdint>
/**
* 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

View File

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

View File

@ -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,

View File

@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/paths.h>
#include <tactility/paths.h>
#include <cstdio>
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"

View File

@ -9,10 +9,8 @@
#include <service/instance.h>
#include <service/manager.h>
#include <tactility/delay.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/time.h>
#include <cstdint>
#include <cstdio>
@ -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<unsigned long>(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;
}
}

View File

@ -1,5 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/event.h>
#include <app/install.h>
#include <app/manager.h>
#include <app/module.h>
#include <app/scheduler.h>
@ -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
};

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### 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.

View File

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

View File

@ -3,11 +3,14 @@
#include <lvgl/lvgl.h>
#include <tactility/check.h>
#include <tactility/concurrent/mutex.h>
#include <algorithm>
#include <vector>
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<WindowRecord> 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<TaskHandle_t> 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);

View File

@ -1,122 +0,0 @@
#pragma once
#include <tt_bundle.h>
#include <stdio.h>
#include <stdbool.h>
#include <lvgl.h>
#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

View File

@ -1,7 +1,6 @@
#pragma once
#include "tt_app.h"
#include "tt_bundle.h"
#include <app/manager.h>
#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
}

View File

@ -1,7 +1,6 @@
#pragma once
#include "tt_app.h"
#include "tt_bundle.h"
#include <app/manager.h>
#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
}

View File

@ -1,7 +1,6 @@
#pragma once
#include "tt_app.h"
#include "tt_bundle.h"
#include <app/manager.h>
#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
}

View File

@ -1,74 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
/** 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

View File

@ -1,83 +0,0 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
/**
* 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

View File

@ -1,133 +0,0 @@
#include "tt_app.h"
#include <Tactility/app/App.h>
#include <Tactility/app/AppPaths.h>
#include <Tactility/app/AppContext.h>
#include <Tactility/app/ElfApp.h>
#include <tactility/log.h>
#include <cstring>
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<tt::app::OnResult>(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<tt::Bundle>(static_cast<tt::Bundle*>(bundle));
HANDLE_AS_APP_CONTEXT(handle)->getApp()->setResult(static_cast<tt::app::Result>(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<tt::Bundle>(static_cast<tt::Bundle*>(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;
}
}

View File

@ -1,18 +1,16 @@
#include "tt_app_alertdialog.h"
#include <Tactility/app/alertdialog/AlertDialog.h>
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<std::string> 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);
}
}

View File

@ -1,28 +1,25 @@
#include "tt_app_fileselection.h"
#include <Tactility/app/App.h>
#include <Tactility/app/fileselection/FileSelection.h>
#include <Tactility/Bundle.h>
#include <cstring>
#include <string>
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;
}

View File

@ -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<std::string> 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);
}
}

View File

@ -1,53 +0,0 @@
#include "tt_bundle.h"
#include <Tactility/Bundle.h>
#include <cstring>
#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);
}
}

View File

@ -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),

View File

@ -1,53 +0,0 @@
#include "tt_preferences.h"
#include <Tactility/Preferences.h>
#include <cstring>
#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);
}
}

View File

@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#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

View File

@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#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

View File

@ -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 <stdbool.h>
#include <stddef.h>
#include <tactility/error.h>
#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

View File

@ -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

View File

@ -0,0 +1,151 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/bundle.h>
#include <cstring>
#include <new>
#include <string>
#include <unordered_map>
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<std::string, Value> 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"

View File

@ -0,0 +1,211 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/preferences.h>
#include <tactility/properties_file.h>
#include <tactility/paths.h>
#include <cinttypes>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <new>
#include <string>
#include <sys/stat.h>
#include <vector>
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<char> 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<int32_t>(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<int64_t>(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"

View File

@ -0,0 +1,167 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/properties_file.h>
#include <tactility/filesystem/file_mutex.h>
#include <tactility/log.h>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <new>
#include <string>
#include <unordered_map>
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<std::string, std::string> 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<char>(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"

View File

@ -1,3 +1,4 @@
#include <tactility/bundle.h>
#include <tactility/concurrent/dispatcher.h>
#include <tactility/concurrent/event_group.h>
#include <tactility/concurrent/thread.h>
@ -41,6 +42,8 @@
#include <tactility/filesystem/file_system.h>
#include <tactility/memory.h>
#include <tactility/module.h>
#include <tactility/preferences.h>
#include <tactility/properties_file.h>
#include <tactility/wifi_auto_scan.h>
#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
};

View File

@ -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;
}

View File

@ -0,0 +1,134 @@
#include "doctest.h"
#include <tactility/bundle.h>
#include <cstring>
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);
}

View File

@ -0,0 +1,153 @@
#include "doctest.h"
#include <tactility/preferences.h>
#include <cstdio>
#include <cstring>
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);
}

View File

@ -0,0 +1,180 @@
#include "doctest.h"
#include <tactility/properties_file.h>
#include <cstdio>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
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<std::pair<std::string, std::string>> seen;
properties_file_for_each(file, [](const char* key, const char* value, void* context) {
auto* out = static_cast<std::vector<std::pair<std::string, std::string>>*>(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);
}

View File

@ -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<uint8_t> data;
uint64_t timestamp;
};
@ -24,11 +26,11 @@ static std::vector<RecordedCall> calls_a;
static std::vector<RecordedCall> 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<uint8_t>(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<uint8_t>(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<const Payload*>(calls_a[0].data)->value, 42);
REQUIRE_EQ(calls_a[0].data.size(), sizeof(payload));
CHECK_EQ(reinterpret_cast<const Payload*>(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<uint8_t>(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<const NetworkConnectedEvent*>(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<uint8_t*>(&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<NetworkDisconnectedEvent*>(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);
}

View File

@ -10,6 +10,7 @@
#include <tactility/freertos/task.h>
#include <algorithm>
#include <atomic>
#include <cstdlib>
#include <cstring>
#include <string>
@ -30,12 +31,22 @@ error_t fake_load(AppLocation, void** out_runtime) {
int last_received_argc = -1;
std::vector<std::string> 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<bool> 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);

View File

@ -43,7 +43,10 @@ int main(int argc, char** argv) {
1,
nullptr
);
assert(task_result == pdPASS);
if (task_result != pdPASS) {
return 1;
}
vTaskStartScheduler();