mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-08-17 23:55:04 +00:00
Fixes and improvements
This commit is contained in:
parent
c628668e75
commit
1ae6e77083
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
195
Modules/app-esp32-module/LICENSE-Apache-2.0.md
Normal file
195
Modules/app-esp32-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### 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.
|
||||
|
||||
195
Modules/app-module/LICENSE-Apache-2.0.md
Normal file
195
Modules/app-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### 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.
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
56
Modules/app-module/include/app/paths.h
Normal file
56
Modules/app-module/include/app/paths.h
Normal 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
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
62
Modules/app-module/source/app_paths.cpp
Normal file
62
Modules/app-module/source/app_paths.cpp
Normal 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"
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
195
Modules/lvgl-window-manager-module/LICENSE-Apache-2.0.md
Normal file
195
Modules/lvgl-window-manager-module/LICENSE-Apache-2.0.md
Normal file
@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### 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.
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
64
TactilityKernel/include/tactility/bundle.h
Normal file
64
TactilityKernel/include/tactility/bundle.h
Normal 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
|
||||
61
TactilityKernel/include/tactility/preferences.h
Normal file
61
TactilityKernel/include/tactility/preferences.h
Normal 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
|
||||
57
TactilityKernel/include/tactility/properties_file.h
Normal file
57
TactilityKernel/include/tactility/properties_file.h
Normal 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
|
||||
@ -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
|
||||
|
||||
151
TactilityKernel/source/bundle.cpp
Normal file
151
TactilityKernel/source/bundle.cpp
Normal 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"
|
||||
211
TactilityKernel/source/preferences.cpp
Normal file
211
TactilityKernel/source/preferences.cpp
Normal 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"
|
||||
167
TactilityKernel/source/properties_file.cpp
Normal file
167
TactilityKernel/source/properties_file.cpp
Normal 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"
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
134
Tests/TactilityKernel/Source/BundleTest.cpp
Normal file
134
Tests/TactilityKernel/Source/BundleTest.cpp
Normal 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);
|
||||
}
|
||||
153
Tests/TactilityKernel/Source/PreferencesTest.cpp
Normal file
153
Tests/TactilityKernel/Source/PreferencesTest.cpp
Normal 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);
|
||||
}
|
||||
180
Tests/TactilityKernel/Source/PropertiesFileTest.cpp
Normal file
180
Tests/TactilityKernel/Source/PropertiesFileTest.cpp
Normal 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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -43,7 +43,10 @@ int main(int argc, char** argv) {
|
||||
1,
|
||||
nullptr
|
||||
);
|
||||
assert(task_result == pdPASS);
|
||||
|
||||
if (task_result != pdPASS) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
vTaskStartScheduler();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user