Refactor app loading and window management (#609)

This commit is contained in:
Ken Van Hoeylandt
2026-08-11 23:40:59 +02:00
committed by GitHub
parent dc3f6104b8
commit 37c507544b
243 changed files with 16034 additions and 10865 deletions
+5 -3
View File
@@ -2,12 +2,12 @@ idf_component_register(
INCLUDE_DIRS
"Libraries/TactilityC/include"
"Libraries/TactilityKernel/include"
"Libraries/TactilityFreeRtos/include"
"Libraries/TactilityFreeRtos/Include"
"Libraries/lvgl/include"
"Libraries/minmea/include"
"Libraries/minitar/include"
"Modules/lvgl-module/include"
# DRIVER_INCLUDE_DIRS_PLACEHOLDER
REQUIRES esp_timer
REQUIRES esp_timer app-module crypt-module gps-module lvgl-module lvgl-window-manager-module service-module
)
# Regular and core features
@@ -15,8 +15,10 @@ add_prebuilt_library(TactilityC Libraries/TactilityC/binary/libTactilityC.a)
add_prebuilt_library(TactilityKernel Libraries/TactilityKernel/binary/libTactilityKernel.a)
add_prebuilt_library(lvgl Libraries/lvgl/binary/liblvgl.a)
add_prebuilt_library(minmea Libraries/minmea/binary/libminmea.a)
add_prebuilt_library(minitar Libraries/minitar/binary/libminitar.a)
target_link_libraries(${COMPONENT_LIB} INTERFACE TactilityC)
target_link_libraries(${COMPONENT_LIB} INTERFACE TactilityKernel)
target_link_libraries(${COMPONENT_LIB} INTERFACE lvgl)
target_link_libraries(${COMPONENT_LIB} INTERFACE minmea)
target_link_libraries(${COMPONENT_LIB} INTERFACE minitar)
+8 -4
View File
@@ -18,14 +18,18 @@ macro(tactility_project project_name)
endif()
set(EXTRA_COMPONENT_DIRS
"Libraries/TactilityFreeRtos"
"Modules"
"Drivers"
"${TACTILITY_SDK_PATH}/Libraries/TactilityFreeRtos"
"${TACTILITY_SDK_PATH}/Modules"
)
set(COMPONENTS
TactilityFreeRtos
# DRIVER_COMPONENTS_PLACEHOLDER
app-module
crypt-module
gps-module
lvgl-module
lvgl-window-manager-module
service-module
)
endmacro()
+15 -46
View File
@@ -111,43 +111,13 @@ def add_module(target_path, module_name):
cmakelists_content = create_module_cmakelists(module_name)
write_module_cmakelists(os.path.join(target_path, f"Modules/{module_name}/CMakeLists.txt"), cmakelists_content)
def discover_all_drivers():
"""
Discover all *-module directories under Drivers/ (not Modules/ - those are handled
separately via add_module). Sorted for deterministic output across OS/filesystem order.
"""
pattern = os.path.join('Drivers', '*-module')
return sorted(
os.path.basename(p) for p in glob.glob(pattern) if os.path.isdir(p)
)
def generate_tactility_sdk_cmake(target_path, available_drivers):
def generate_tactility_sdk_cmake(target_path):
src = os.path.join('Buildscripts', 'TactilitySDK', 'TactilitySDK.cmake')
with open(src) as f:
content = f.read()
placeholder = " # DRIVER_COMPONENTS_PLACEHOLDER"
assert placeholder in content, \
f"Placeholder '{placeholder.strip()}' not found in {src} - template drifted, generator needs updating"
components = "\n".join(f" {d}" for d in available_drivers)
new_content = content.replace(placeholder, components)
assert placeholder not in new_content, \
f"Placeholder '{placeholder.strip()}' still present after replacement in {src}"
with open(os.path.join(target_path, 'TactilitySDK.cmake'), 'w') as f:
f.write(new_content)
shutil.copy2(src, os.path.join(target_path, 'TactilitySDK.cmake'))
def generate_tactility_sdk_top_cmakelists(target_path, available_drivers):
def generate_tactility_sdk_top_cmakelists(target_path):
src = os.path.join('Buildscripts', 'TactilitySDK', 'CMakeLists.txt')
with open(src) as f:
content = f.read()
placeholder = " # DRIVER_INCLUDE_DIRS_PLACEHOLDER"
assert placeholder in content, \
f"Placeholder '{placeholder.strip()}' not found in {src} - template drifted, generator needs updating"
include_dirs = "\n".join(f' "Drivers/{d}/include"' for d in available_drivers)
new_content = content.replace(placeholder, include_dirs)
assert placeholder not in new_content, \
f"Placeholder '{placeholder.strip()}' still present after replacement in {src}"
with open(os.path.join(target_path, 'CMakeLists.txt'), 'w') as f:
f.write(new_content)
shutil.copy2(src, os.path.join(target_path, 'CMakeLists.txt'))
def main():
if len(sys.argv) < 2:
@@ -167,7 +137,7 @@ def main():
{'src': 'TactilityC/CMakeLists.txt', 'dst': 'Libraries/TactilityC/'},
{'src': 'TactilityC/LICENSE*.*', 'dst': 'Libraries/TactilityC/'},
# TactilityFreeRtos
{'src': 'TactilityFreeRtos/Include/**', 'dst': 'Libraries/TactilityFreeRtos/include/'},
{'src': 'TactilityFreeRtos/Include/**', 'dst': 'Libraries/TactilityFreeRtos/Include/'},
{'src': 'TactilityFreeRtos/CMakeLists.txt', 'dst': 'Libraries/TactilityFreeRtos/'},
{'src': 'TactilityFreeRtos/LICENSE*.*', 'dst': 'Libraries/TactilityFreeRtos/'},
# TactilityKernel
@@ -185,6 +155,10 @@ def main():
# elf_loader
{'src': 'Libraries/elf_loader/elf_loader.cmake', 'dst': 'Libraries/elf_loader/'},
{'src': 'Libraries/elf_loader/license.txt', 'dst': 'Libraries/elf_loader/'},
# minitar
{'src': 'build/esp-idf/minitar/libminitar.a', 'dst': 'Libraries/minitar/binary/'},
{'src': 'Libraries/minitar/minitar/minitar.h', 'dst': 'Libraries/minitar/include/'},
{'src': 'Libraries/minitar/minitar/LICENSE*', 'dst': 'Libraries/minitar/'},
# minmea
{'src': 'build/esp-idf/minmea/libminmea.a', 'dst': 'Libraries/minmea/binary/'},
{'src': 'Libraries/minmea/Include/**', 'dst': 'Libraries/minmea/include/'},
@@ -197,21 +171,16 @@ def main():
map_copy(mappings, target_path)
# Modules
add_module(target_path, "lvgl-module")
add_module(target_path, "app-module")
add_module(target_path, "crypt-module")
add_module(target_path, "gps-module")
add_module(target_path, "lvgl-module")
add_module(target_path, "lvgl-window-manager-module")
add_module(target_path, "service-module")
# Drivers - only ones actually built for this target (chip-restricted drivers like
# sc2356-module won't have a .a outside ESP32-P4)
available_drivers = [d for d in discover_all_drivers() if driver_is_available(d)]
for driver_name in available_drivers:
add_driver(target_path, driver_name)
# Final scripts - generated (not copied verbatim) so COMPONENTS/INCLUDE_DIRS only list
# drivers actually available for this target
generate_tactility_sdk_cmake(target_path, available_drivers)
generate_tactility_sdk_top_cmakelists(target_path, available_drivers)
# Final scripts - copied verbatim
generate_tactility_sdk_cmake(target_path)
generate_tactility_sdk_top_cmakelists(target_path)
# Output ESP-IDF SDK version to file
esp_idf_version = os.environ.get("ESP_IDF_VERSION", "")
+2
View File
@@ -100,6 +100,8 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Modules/crypt-module)
add_subdirectory(Modules/gps-module)
add_subdirectory(Modules/service-module)
add_subdirectory(Modules/app-module)
add_subdirectory(Modules/lvgl-window-manager-module)
add_subdirectory(Drivers/gps-generic-module)
add_subdirectory(Drivers/gps-meshtastic-module)
-7
View File
@@ -1,10 +1,3 @@
dependencies:
- Platforms/platform-esp32
# Add all driver modules because the generic devices are used to build the SDK
- Drivers/bm8563-module
- Drivers/bmi270-module
- Drivers/mpu6886-module
- Drivers/pi4ioe5v6408-module
- Drivers/qmi8658-module
- Drivers/rx8130ce-module
dts: generic,esp32.dts
-7
View File
@@ -1,10 +1,3 @@
dependencies:
- Platforms/platform-esp32
# Add all driver modules because the generic devices are used to build the SDK
- Drivers/bm8563-module
- Drivers/bmi270-module
- Drivers/mpu6886-module
- Drivers/pi4ioe5v6408-module
- Drivers/qmi8658-module
- Drivers/rx8130ce-module
dts: generic,esp32c6.dts
-8
View File
@@ -1,11 +1,3 @@
dependencies:
- Platforms/platform-esp32
# Add all driver modules because the generic devices are used to build the SDK
- Drivers/bm8563-module
- Drivers/bmi270-module
- Drivers/mpu6886-module
- Drivers/pi4ioe5v6408-module
- Drivers/qmi8658-module
- Drivers/rx8130ce-module
- Drivers/sc2356-module
dts: generic,esp32p4.dts
-7
View File
@@ -1,10 +1,3 @@
dependencies:
- Platforms/platform-esp32
# Add all driver modules because the generic devices are used to build the SDK
- Drivers/bm8563-module
- Drivers/bmi270-module
- Drivers/mpu6886-module
- Drivers/pi4ioe5v6408-module
- Drivers/qmi8658-module
- Drivers/rx8130ce-module
dts: generic,esp32s3.dts
@@ -23,3 +23,8 @@ cdn.infoMessage=To put the device into bootloader mode: <br/>1. Press the trackb
lvgl.colorDepth=16
sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y
# Fix error "PSRAM space not enough for the Flash instructions" on boot:
sdkconfig.CONFIG_SPIRAM_FETCH_INSTRUCTIONS=n
sdkconfig.CONFIG_SPIRAM_RODATA=n
sdkconfig.CONFIG_SPIRAM_XIP_FROM_PSRAM=n
+2 -2
View File
@@ -17,12 +17,12 @@ static void on_boot_completed(struct SystemEvent* /*event*/, void* /*context*/)
}
static error_t start() {
system_event_subscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr);
system_event_callback_add(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed, nullptr);
return ERROR_NONE;
}
static error_t stop() {
system_event_unsubscribe(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed);
system_event_callback_remove(KERNEL_EVENT_BOOT_COMPLETED, on_boot_completed);
return ERROR_NONE;
}
@@ -67,6 +67,11 @@ static void create_gt911_touch(Device* i2c0) {
// Reset is pulsed via io_expander0 (detect.cpp's pulse_display_reset_pins), not a direct SoC GPIO.
.pin_reset = GPIO_PIN_SPEC_NONE,
.pin_interrupt = GPIO_PIN_SPEC_NONE,
.reset_pulses = 0, // no-op: pin_reset is NONE, so reset_controller_pin() skips anyway
.x_offset = 0,
.y_offset = 0,
.x_scale = 1000,
.y_scale = 1000,
};
gt911_device.config = &gt911_config;
@@ -575,6 +575,7 @@ static error_t tab5_keyboard_read_key(Device* device, KeyboardKeyData* data) {
static const KeyboardApi tab5_keyboard_api = {
.read_key = tab5_keyboard_read_key,
.is_present = tab5_keyboard_is_attached,
};
// Defined in module.cpp - this driver is registered directly by m5stack-tab5's own module,
+22
View File
@@ -12,6 +12,22 @@
## Higher Priority
- Devices with a keyboard attached should always highlight the first widget (~Cardputer navigation issue), same for LV_INDEV_TYPE_ENCODER being present
- Make it more clear to end-users that an SD card is required to run Tactility
- Move "# Fix error "PSRAM space not enough for the Flash instructions" on boot:" fix from T-Deck and others to device.py
- Make it possible to override stack size for an app via config file (loaded at boot), and make it possible to set preferred memory location (e.g. internal/external)
- Put task stacks in PSRAM when possible.
- Wrap file operations like fopen/fclose with file_mutex
- Add bold fonts for e-ink readability improvement
- Split up Claude instructions: https://code.claude.com/docs/en/memory#import-additional-files
and add https://github.com/multica-ai/andrej-karpathy-skills/blob/main/CLAUDE.md
- Move test projects to their relevant subproject
- tt_alertdialog start() etc is broken as it can't fetch the app instance id. Fetch automatically via thread context?
- Migrate Tactility/Paths.cpp functions to TactilityKernel
- app_manager_find_manifest() should make a copy, not return a pointer.
- Httpd.cpp: warn if running on same CPU core (or task) as UI/LVGL/window manager.
- Improve Setup: Show "Step done" screen
- Improve Setup: Add keyboard/keypad navigation explanation
- display.h API: get_backlight does not change ref counting, but it should
- 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()
@@ -38,6 +54,9 @@
## Medium Priority
- Consider moving certain drivers into separate modules: audio, bt, wifi, etc
- 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:
@@ -53,6 +72,9 @@
## Lower Priority
- lvgl-module has a keyboard.cpp that creates a `keyboard_group`. This group is set as the default group, so it can also work with trackball(= LVGL "encoder").
Make a separate group that is the default group. The keyboard can then use it (or use its own).
The basic idea is to invert the ownership: now the keyboard group is made the default group, but it's probably more logical to have the default group used by the keyboard.
- lvgl-module's spinner relies on hard-coded spinner asset from Tactility main project.
- Localize all apps
- Support hot-plugging SD card (note: this is not possible if they require the CS pin hack)
+2
View File
@@ -96,6 +96,8 @@ else ()
Tactility
TactilityFreeRtos
lvgl-module
lvgl-window-manager-module
app-module
crypt-module
gps-module
gps-generic-module
+5
View File
@@ -25,4 +25,9 @@ else()
target_include_directories(QRCode
PUBLIC src
)
# qrcode.h polyfills bool/true/false for pre-C23 compilers - on a host compiler that
# defaults to C23 (where bool is a keyword), that polyfill itself fails to compile. Pin to
# C11 for the simulator build only; ESP-IDF's own toolchain default is unaffected.
set_target_properties(QRCode PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON)
endif()
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(app-esp32-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel app-module service-module elf_loader
)
@@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+4
View File
@@ -0,0 +1,4 @@
dependencies:
- TactilityKernel
- Modules/app-module
- Modules/service-module
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module app_esp32_module;
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: Apache-2.0
#ifdef ESP_PLATFORM
#include <sdkconfig.h>
#endif
#include <app/loader.h>
#include <app/location.h>
#include <tactility/error.h>
#include <tactility/check.h>
#include <tactility/filesystem/file_mutex.h>
#include <tactility/log.h>
#include <service/manager.h>
#include <esp_elf.h>
#include <esp_err.h>
#include <cstdio>
#include <new>
#include <string>
constexpr auto* TAG = "app_esp32_loader";
namespace {
/** load()-allocated state, passed back through run()/unload(). */
struct Esp32AppRuntime {
esp_elf_t elf {};
uint8_t* file_data = nullptr;
};
error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
FileMutex mutex;
file_mutex_get(&mutex, path);
file_mutex_lock(&mutex);
FILE* file = fopen(path, "rb");
if (file == nullptr) {
LOG_E(TAG, "Failed to open %s", path);
file_mutex_unlock(&mutex);
return ERROR_NOT_FOUND;
}
fseek(file, 0, SEEK_END);
long size = ftell(file);
fseek(file, 0, SEEK_SET);
if (size <= 0) {
fclose(file);
file_mutex_unlock(&mutex);
return ERROR_RESOURCE;
}
auto* data = static_cast<uint8_t*>(malloc(static_cast<size_t>(size)));
if (data == nullptr) {
fclose(file);
file_mutex_unlock(&mutex);
return ERROR_OUT_OF_MEMORY;
}
size_t read = fread(data, 1, static_cast<size_t>(size), file);
fclose(file);
file_mutex_unlock(&mutex);
if (read != static_cast<size_t>(size)) {
free(data);
return ERROR_RESOURCE;
}
*out_data = data;
*out_size = static_cast<size_t>(size);
return ERROR_NONE;
}
// location.location can be either an app's install directory or the .elf file directly; the
// former resolves to the per-target binary at {dir}/elf/{CONFIG_IDF_TARGET}.elf.
std::string resolve_elf_path(const std::string& path) {
if (path.ends_with(".elf")) {
return path;
}
return path + "/elf/" + CONFIG_IDF_TARGET + ".elf";
}
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
if (location.type != APP_LOCATION_PATH) {
LOG_E(TAG, "Out of memory");
return ERROR_NOT_SUPPORTED;
}
LOG_I(TAG, "Loading %s", static_cast<const char*>(location.location));
auto* runtime = new (std::nothrow) Esp32AppRuntime();
if (runtime == nullptr) {
LOG_E(TAG, "Out of memory");
return ERROR_OUT_OF_MEMORY;
}
auto elf_path = resolve_elf_path(static_cast<const char*>(location.location));
size_t size = 0;
error_t read_result = read_file(elf_path.c_str(), &runtime->file_data, &size);
if (read_result != ERROR_NONE) {
LOG_E(TAG, "Failed to read file");
delete runtime;
return read_result;
}
if (esp_elf_init(&runtime->elf) != ESP_OK) {
free(runtime->file_data);
delete runtime;
LOG_E(TAG, "Failed to init elf");
return ERROR_RESOURCE;
}
if (esp_elf_relocate(&runtime->elf, runtime->file_data) != 0) {
// esp_elf_relocate() already frees elf->pdata/ptext itself on a relocation failure
free(runtime->file_data);
delete runtime;
LOG_E(TAG, "Failed to map elf");
return ERROR_RESOURCE;
}
*out_runtime = runtime;
return ERROR_NONE;
}
int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc, char* argv[]) {
auto* runtime = static_cast<Esp32AppRuntime*>(runtime_ptr);
return esp_elf_request(&runtime->elf, 0, argc, argv);
}
void api_unload(AppRuntime runtime_ptr) {
auto* runtime = static_cast<Esp32AppRuntime*>(runtime_ptr);
esp_elf_deinit(&runtime->elf);
free(runtime->file_data);
delete runtime;
}
AppLoaderApi loader_api = {
.load = api_load,
.run = api_run,
.unload = api_unload,
};
void* create_service(const ServiceManifest*) {
return &loader_api;
}
void destroy_service(const ServiceManifest*, void*) {
}
} // namespace
ServiceManifest loader_service_manifest = {
.id = APP_LOADER_PATH_SERVICE_ID,
.create_service = create_service,
.destroy_service = destroy_service,
.on_start = nullptr,
.on_stop = nullptr,
};
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
#include <app_esp32/module.h>
#include <service/manager.h>
#include <tactility/error.h>
#include <tactility/module.h>
extern "C" {
extern ServiceManifest loader_service_manifest;
static error_t start() {
return service_manager_add(&loader_service_manifest, /*auto_start=*/true);
}
static error_t stop() {
return service_manager_remove(loader_service_manifest.id);
}
Module app_esp32_module = {
.name = "app-esp32",
.start = start,
.stop = stop,
.drivers = nullptr,
.symbols = nullptr,
.internal = nullptr
};
}
+12
View File
@@ -0,0 +1,12 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(app-module
SRCS ${SOURCE_FILES}
PRIV_INCLUDE_DIRS private/
INCLUDE_DIRS include/
REQUIRES TactilityKernel service-module minitar
)
+195
View File
@@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+3
View File
@@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
- Modules/service-module
+103
View File
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/task.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Identifies the kind of app-lifecycle event delivered through app_event_await(). */
enum AppEventType {
APP_EVENT_RESULT, // struct AppResultEventData
APP_EVENT_CLOSE, // no data - terminate now, permanently
};
/** Data for APP_EVENT_RESULT. */
struct AppResultEventData {
uint32_t launch_id;
/** The child app instance's own AppMainFn/AppLoaderApi::run() return value. By convention:
* 0 = Ok, 1 = Cancelled, 2 = Error. Apps that need to hand back more than this (e.g. picked
* text, a path) expose their own "get last result" getter instead - see e.g.
* tt::app::inputdialog::getLastText(). */
int32_t result;
};
struct AppEvent {
enum AppEventType type;
/** Stamped by app_event_emit(); any value passed in by the caller is ignored. */
uint64_t timestamp;
/** Valid only when type == APP_EVENT_RESULT. */
struct AppResultEventData result;
};
/**
* Number of events that can be queued per subscription before app_event_emit() starts
* returning ERROR_RESOURCE (dropping the newest event, preserving FIFO order of what's
* already queued). Deliberately generous: app-module's scheduler is the only emitter and it
* serializes app-lifecycle transitions, so a given app can't realistically receive events
* faster than the scheduler produces them one at a time.
*/
#define APP_EVENT_QUEUE_CAPACITY 4
/**
* Caller-owned subscription node. Unlike TactilityKernel's system_event poll subscription
* (which coalesces to the latest value), this queues events by value (FIFO) since dropping an
* APP_EVENT_RESULT would be unacceptable.
* @warning Fields other than `app_instance_id` are for internal use only; do not read or write
* them directly.
*/
struct AppEventSubscription {
/** The app instance this subscription receives events for; set by the caller before app_event_subscribe(). */
uint32_t app_instance_id;
TaskHandle_t task;
struct AppEvent queue[APP_EVENT_QUEUE_CAPACITY];
uint8_t head;
uint8_t count;
struct AppEventSubscription* next;
};
/**
* Register a subscription for events addressed to @a sub->app_instance_id.
* @warning Does not work in ISR context.
* @param[in,out] sub subscription to register; caller sets @a sub->app_instance_id beforehand,
* owns the storage, and must keep it alive (and stationary) until unsubscribed
* @return ERROR_NONE on success
*/
error_t app_event_subscribe(struct AppEventSubscription* sub);
/**
* Remove a previously registered subscription.
* @warning Does not work in ISR context.
* @return ERROR_NONE on success, ERROR_NOT_FOUND if no matching subscription exists
*/
error_t app_event_unsubscribe(struct AppEventSubscription* sub);
/**
* Deliver @a event to every subscription registered for @a app_instance_id (normally exactly one).
* @warning Does not work in ISR context.
* @retval ERROR_NONE delivered to at least one subscription
* @retval ERROR_NOT_FOUND no subscription is registered for @a app_instance_id
* @retval ERROR_RESOURCE at least one matching subscription's queue was full; the event was
* dropped for that subscription (still delivered to any other matching subscription)
*/
error_t app_event_emit(uint32_t app_instance_id, const struct AppEvent* event);
/**
* Pop the next event for @a sub, blocking up to @a timeout if the queue is currently empty.
* @retval ERROR_NONE @a out_event was filled
* @retval ERROR_TIMEOUT no event arrived before the timeout elapsed
*/
error_t app_event_await(struct AppEventSubscription* sub, struct AppEvent* out_event, TickType_t timeout);
#ifdef __cplusplus
}
#endif
+50
View File
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Computes the install directory for @a app_id (does not check whether anything is actually
* installed there).
* @param[out] path always NULL-terminated on return, even on failure (empty string if
* @a path_size == 0 - nothing is written in that case; otherwise at least "" is written)
* @retval ERROR_NONE on success
* @retval ERROR_BUFFER_OVERFLOW @a path_size is too small to hold the path (including the
* NULL terminator)
* @retval ERROR_NOT_FOUND the app install location isn't available (e.g. no SD card)
*/
error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
/**
* Installs an app from a tarball at @a source_path: extracts it into the app install directory,
* parses the extracted manifest.properties (see app/metadata.h) to determine its id, then
* registers it with app_manager_add() as an AppLocation{APP_LOCATION_PATH, <install dir>} app.
* If an app with the same id is already installed (via a previous app_install() call), it is
* uninstalled first - stopped if running, its old install directory removed - before the new
* one takes its place.
* @param[in] source_path path to a tar file containing the app (must have manifest.properties
* at its root)
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND @a source_path doesn't exist / can't be read
* @retval ERROR_INVALID_ARGUMENT the tarball has no valid manifest.properties at its root
*/
error_t app_install(const char* source_path);
/**
* Uninstalls a previously app_install()-ed app: stops it if currently running, deletes its
* install directory, and unregisters it (app_manager_remove()).
* @param[in] app_id the id the app was installed under (AppMetadata::app_id)
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND no such app was installed via app_install()
*/
error_t app_uninstall(const char* app_id);
#ifdef __cplusplus
}
#endif
+24
View File
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Identifies a running (or previously running) app instance. 0 is never a valid instance id. */
typedef uint32_t AppInstanceId;
/** Lifecycle state of a running (or previously running) app instance. Every app instance owns
* its own task for its entire lifetime - there is no "saved, task given up" state. */
typedef enum {
APP_INSTANCE_STATE_STARTING,
APP_INSTANCE_STATE_ACTIVE,
APP_INSTANCE_STATE_STOPPING,
APP_INSTANCE_STATE_STOPPED,
} AppInstanceState;
#ifdef __cplusplus
}
#endif
+59
View File
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manifest.h>
#include <tactility/error.h>
#include <stdint.h>
#include "location.h"
#ifdef __cplusplus
extern "C" {
#endif
/** service-module id the AppLoaderApi implementation for AppManifest::location.type ==
* APP_LOCATION_MEMORY must register under. Implemented by app-module itself (source/app_internal_loader.cpp). */
#define APP_LOADER_MEMORY_SERVICE_ID "app-loader-memory"
/** service-module id the AppLoaderApi implementation for AppManifest::location.type ==
* APP_LOCATION_PATH must register under. Implemented by a platform module (e.g. app-esp32-module). */
#define APP_LOADER_PATH_SERVICE_ID "app-loader-path"
/**
* Entry point signature for an APP_LOCATION_MEMORY app: a function linked directly into this
* firmware binary. Called on the dedicated task app-module's scheduler spawns for this instance,
* blocking for the app's whole lifetime - same contract as an external app's main(), plus
* @a app_instance_id identifying this running instance (use it with
* app_event_subscribe()/window_manager_create()/app_manager_finish()/etc.).
* AppManifest::location.location holds this cast to void*.
*/
typedef int32_t (*AppMainFn)(uint32_t app_instance_id, int argc, char* argv[]);
typedef void* AppRuntime;
/**
* Pluggable mechanism for loading and executing an app.
*/
struct AppLoaderApi {
/**
* Prepares an app instance for execution (e.g. read + relocate its binary).
* @param[in] location the location to load the elf from
* @param[out] out_runtime opaque handle to whatever load() allocated; passed back to run()/unload()
*/
error_t (*load)(struct AppLocation location, AppRuntime* out_runtime);
/**
* Blocking: runs the app to completion.
* @param[in] runtime handle produced by load()
* @param[in] app_instance_id the running instance's id
* @param[in] argc the amount of arguments in @a argv
* @param[in] argv the array of string pointers (can be NULL)
*/
int32_t (*run)(AppRuntime runtime, uint32_t app_instance_id, int argc, char* argv[]);
/** Releases whatever load() allocated. Called after run() returns. */
void (*unload)(AppRuntime runtime);
};
#ifdef __cplusplus
}
#endif
+21
View File
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
enum AppLocationType {
APP_LOCATION_MEMORY,
APP_LOCATION_PATH,
};
struct AppLocation {
enum AppLocationType type;
/** Meaning depends on `type`; see AppLocationType. */
void* location;
};
#ifdef __cplusplus
}
#endif
+153
View File
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/instance.h>
#include <app/manifest.h>
#include <tactility/error.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Register an app manifest.
* @retval ERROR_INVALID_ARGUMENT a manifest with the same id is already registered
* @retval ERROR_NONE on success
*/
error_t app_manager_add(const struct AppManifest* manifest);
/**
* Unregister a previously-added manifest.
* @retval ERROR_NOT_FOUND no manifest with this id is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_remove(const char* id);
/** @return the manifest, or NULL if not found. */
const struct AppManifest* app_manager_find_manifest(const char* id);
/**
* 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);
/**
* Starts a new instance of the app registered under @a id. Every app instance gets its own
* dedicated task for its entire lifetime - starting an app never asks any other app to give up
* its task, and multiple instances (of the same or different apps) can be Active at once.
* @param[in] id the manifest id to start
* @param[out] out_app_instance_id the id of the new app instance
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_start(const char* id, AppInstanceId* out_app_instance_id);
/**
* Same as app_manager_start(), but also passes @a argc/@a argv to the new instance's own main
* function (see app/loader.h's AppMainFn) - modelled on a C program's main(argc, argv). For
* regular (non-modal) navigations that need to pass data to the target app (e.g. "show details
* for this app id") without expecting a result back.
* @param[in] argv @a argc strings; app-module makes its own deep copy before returning, so
* @a argv and the strings it points to may be freed/go out of scope immediately after this call
* returns (e.g. safe to pass a stack-local array of a caller's own std::string::c_str()s).
*/
error_t app_manager_start_with_parameters(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
/**
* Starts @a id as a modal child of @a parent_instance_id, for the purpose of receiving a
* result. The parent keeps running (window_manager's own multi-window stack handles burying its
* window while the child is shown).
*
* When the child's task exits, an APP_EVENT_RESULT is delivered to @a parent_instance_id -
* result is whatever the child's AppMainFn/AppLoaderApi::run() returned - unless
* @a parent_instance_id is 0, in which case no result is delivered (fire-and-forget, for
* callers with no app_instance_id of their own). The parent is then responsible for calling
* app_manager_stop() on the child's instance id to fully reap it. Children that need to hand
* back more than an int32_t (e.g. picked text, a path) expose their own "get last result"
* getter for the parent to call after receiving the event - see e.g.
* tt::app::inputdialog::getLastText().
* @param[in] argv @a argc strings; app-module makes its own deep copy before returning (same as
* app_manager_start_with_parameters()), so @a argv and the strings it points to may be
* freed/go out of scope immediately after this call returns.
* @retval ERROR_NOT_FOUND no manifest with this id is registered, or no AppLoaderApi is registered
* @retval ERROR_NONE on success
*/
error_t app_manager_start_for_result(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
/**
* Stop an app instance permanently. Emits APP_EVENT_CLOSE and bound-waits for its task to exit
* if it was running.
* @warning Must not be called from the instance's own task (it bound-waits via thread_join(),
* which asserts against joining yourself) - an app closing itself must call app_manager_finish()
* instead, right before returning from its own AppMainFn/AppLoaderApi::run().
*/
error_t app_manager_stop(AppInstanceId app_instance_id);
/**
* Called by an app instance, from its own task, right before it returns in response to
* APP_EVENT_CLOSE - whether that close was self-initiated (e.g. its own back button) or came
* from someone else. Marks this instance Stopped immediately (rather than waiting for its task
* to actually exit) so app_manager_get_state()/app_manager_get_topmost_instance_id() reflect the
* closure as soon as the app has decided to close, not just once its task has fully unwound.
* @warning Does not join or free this instance's own task/ledger entry (can't - this runs on
* that very task); those are cleaned up on a later app_manager_stop() call, same as any
* self-terminating instance.
*/
error_t app_manager_finish(AppInstanceId app_instance_id);
/** @return the instance's current state, or APP_INSTANCE_STATE_STOPPED if the id is unknown. */
AppInstanceState app_manager_get_state(AppInstanceId app_instance_id);
/**
* @param[out] out_app_instance_id set to the instance id of the topmost currently-Active app -
* the most recently started of whichever instances are Active (a modal child launched via
* app_manager_start_for_result() stays Active alongside its parent while shown, so this
* correctly picks the child, not the parent, while a dialog is up).
* @retval ERROR_NOT_FOUND no app is Active
* @retval ERROR_NONE on success
*/
error_t app_manager_get_topmost_instance_id(AppInstanceId* out_app_instance_id);
/**
* Same as app_manager_get_topmost_instance_id(), but resolves straight to the topmost app's
* manifest id string.
* @param[out] buffer always NULL-terminated on return, even on failure (empty string if
* @a buffer_size == 0 - nothing is written in that case; otherwise at least "" is written)
* @retval ERROR_NOT_FOUND no app is Active
* @retval ERROR_BUFFER_OVERFLOW @a buffer_size is too small to hold the id (including the NULL
* terminator)
* @retval ERROR_NONE on success
*/
error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size);
/**
* Registers @a path as a directory to scan for app manifests - each direct subdirectory of
* @a path is expected to hold a manifest.properties (see app/metadata.h), matching the layout
* app_install() creates ({install dir}/{app_id}/manifest.properties), though this is not
* install/uninstall - it only ever adds/removes manifest registrations, never touches files on
* disk or running instances. No-op if @a path is already registered. Does not scan immediately -
* call app_manager_install_path_scan() to do that.
* @retval ERROR_NONE on success
*/
error_t app_manager_install_path_add(const char* path);
/**
* Scans every path registered via app_manager_install_path_add(): registers
* (app_manager_add()) any direct subdirectory with a valid manifest.properties that isn't
* already registered, and unregisters (app_manager_remove() only - does not stop it if running,
* does not delete anything) any manifest a previous scan registered whose directory has since
* disappeared. Safe to call repeatedly (e.g. after an SD card is mounted/unmounted).
*/
void app_manager_install_path_scan(void);
#ifdef __cplusplus
}
#endif
+41
View File
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include "location.h"
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/** Broad classification of an app, used for grouping/launcher presentation. */
enum AppCategory {
APP_CATEGORY_SYSTEM,
APP_CATEGORY_SETTINGS,
APP_CATEGORY_USER,
};
/** Bit flags for AppManifest::flags. */
enum AppManifestFlags {
/** Excluded from generic app-browsing UIs (AppList, Settings) - for apps only ever reached
* by direct navigation (modal dialogs, detail views that require parameters, wizard/
* bootstrap steps). */
APP_MANIFEST_FLAG_HIDDEN = 0b00000001,
};
/** Describes a registrable app. One manifest exists per app id. */
struct AppManifest {
/** Unique app identifier. Should never be NULL. */
const char* id;
/** Human-readable name. Should never be NULL. */
const char* name;
enum AppCategory category;
struct AppLocation location;
/** Bitmask of AppManifestFlags. Most apps should leave this 0. */
uint8_t flags;
};
#ifdef __cplusplus
}
#endif
+60
View File
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define APP_METADATA_TARGET_SDK_LENGTH 16
#define APP_METADATA_APP_ID_LENGTH 32
#define APP_METADATA_APP_NAME_LENGTH 32
#define APP_METADATA_APP_VERSION_NAME_LENGTH 16
struct AppMetadata {
/**
* The SDK version that was used to compile this app. (e.g. "0.6.0")
* Must be NULL-terminated.
*/
char target_sdk[APP_METADATA_TARGET_SDK_LENGTH + 1];
/**
* The identifier by which the app is launched by the system and other apps.
* Must be NULL-terminated.
*/
char app_id[APP_METADATA_APP_ID_LENGTH + 1];
/**
* The user-readable name of the app. Used in UI.
* Must be NULL-terminated.
*/
char app_name[APP_METADATA_APP_NAME_LENGTH + 1];
/**
* The version as it is displayed to the user (e.g. "1.2.0")
* Must be NULL-terminated.
*/
char app_version_name[APP_METADATA_APP_VERSION_NAME_LENGTH + 1];
/** The technical version (must be incremented with new releases of the app) */
uint64_t app_version_code;
};
/**
* Parses a manifest.properties file at @a path into @a out_metadata, auto-detecting the V1
* (sectioned, e.g. "[app]id=...") or V2 (flat dot-notation, e.g. "app.id=...") format from its
* first line.
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
* @a out_metadata's fixed-size buffers
*/
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata);
#ifdef __cplusplus
}
#endif
+14
View File
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module app_module;
#ifdef __cplusplus
}
#endif
+56
View File
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stddef.h>
#include <tactility/error.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Get the user data directory for an app. Survives OS upgrades. No trailing "/".
* @param[in] app_id non-null app id
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t app_paths_get_user_data_directory(const char* app_id, char* out_path, size_t out_path_size);
/**
* @brief Get a path within the user data directory for an app.
* @param[in] app_id non-null app id
* @param[in] child_path path without a "/" prefix
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t app_paths_get_user_data_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size);
/**
* @brief Get the assets directory for an app. Do not store configuration data here. No trailing "/".
* @param[in] app_id non-null app id
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_t out_path_size);
/**
* @brief Get a path within the assets directory for an app.
* @param[in] app_id non-null app id
* @param[in] child_path path without a "/" prefix
* @param[out] out_path buffer to store the path
* @param[in] out_path_size size of the output buffer
* @retval ERROR_BUFFER_OVERFLOW if out_path_size is too small
* @retval ERROR_NONE on success
*/
error_t app_paths_get_assets_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/instance.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @return the app_instance_id of whichever app instance's task is calling this (every app
* instance's task stashes it in its own thread-local storage when it starts), or 0 if called
* from a task that isn't a running app instance. An app's own main() typically calls this once,
* near the top, to learn its own instance id - see e.g. app_event_subscribe()/
* window_manager_create(), both of which need it.
*/
AppInstanceId app_scheduler_current_app_id(void);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
// Minimal filesystem helpers shared by app-module internals that need to look at on-disk app
// directories (app_install.cpp, manager.cpp's install-path scan) - app-module may not depend
// 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 <cstring>
#include <dirent.h>
#include <string>
#include <sys/stat.h>
#include <vector>
inline bool app_fs_is_directory(const std::string& path) {
struct stat result {};
FileMutex file_mutex;
file_mutex_get(&file_mutex, path.c_str());
file_mutex_lock(&file_mutex);
auto is_dir = stat(path.c_str(), &result) == 0 && S_ISDIR(result.st_mode);
file_mutex_unlock(&file_mutex);
return is_dir;
}
inline bool app_fs_is_file(const std::string& path) {
FileMutex file_mutex;
file_mutex_get(&file_mutex, path.c_str());
file_mutex_lock(&file_mutex);
struct stat result {};
auto retval = stat(path.c_str(), &result) == 0 && S_ISREG(result.st_mode);
file_mutex_unlock(&file_mutex);
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.
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);
DIR* dir = opendir(path.c_str());
if (dir == nullptr) {
file_mutex_unlock(&file_mutex);
return;
}
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) {
continue;
}
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);
}
}
}
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/instance.h>
#include <app/manifest.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/freertos/freertos.h>
#include <tactility/freertos/semphr.h>
#include <tactility/freertos/task.h>
#include <stdint.h>
#include <string>
#include <unordered_map>
/**
* A dedicated completion signal(1) for one app instance's task, given as the
* literal last action app_task_main() takes before vTaskDelete().
* Heap-allocated with its own refcount (protected by app_ledger().mutex, not atomic)
* rather than owned by the ledger entry, since app_task_main() always erases that entry -
* and may run its exit path entirely - before app_scheduler_stop() ever looks for it:
* Whichever side(2) finishes with it last is the one that deletes `semaphore` and frees this struct.
*
* (1) Not the task's shared default FreeRTOS notification, which app_event.cpp's
* AppEventSubscription also uses - an unrelated event delivered to the same task could
* otherwise unblock a waiter early.
* (2) The exiting task, or a concurrent app_scheduler_stop() that found the entry in time and is waiting on `semaphore`
*/
struct AppCompletionSignal {
SemaphoreHandle_t semaphore;
/** Starts at 1, owned by app_task_main() until its own exit. app_scheduler_stop() takes an
* additional reference for as long as it's waiting on `semaphore`, if it finds the instance
* still running. Reaching 0 means deletion. */
int refcount = 1;
};
/** A registered/running app instance, as tracked internally by app-module. */
struct AppInstanceRecord {
uint32_t id;
const AppManifest* manifest;
AppInstanceState state;
/** The FreeRTOS task currently executing AppLoaderApi::run() for this instance; NULL when not running. */
TaskHandle_t task;
/** 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;
/** This instance's completion signal - see AppCompletionSignal. Set once by
* app_scheduler_start(), never reassigned. */
AppCompletionSignal* completion = nullptr;
};
struct AppLedger {
std::unordered_map<std::string, const AppManifest*> manifests;
std::unordered_map<uint32_t, AppInstanceRecord> instances;
uint32_t next_instance_id = 1;
Mutex mutex {};
AppLedger() { mutex_construct(&mutex); }
~AppLedger() { mutex_destruct(&mutex); }
};
inline AppLedger& app_ledger() {
static AppLedger ledger;
return ledger;
}
/**
* Frees a deep-copied argv previously built by app_manager_start_with_parameters()/app_manager_start_for_result():
* each individually heap-allocated string, then the array itself. Safe to call with count == 0 values == nullptr (no-op).
*/
inline void app_ledger_free_arguments(int count, char** values) {
if (values == nullptr) {
return;
}
for (int i = 0; i < count; i++) {
delete[] values[i];
}
delete[] values;
}
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/metadata.h>
#include <map>
#include <string>
/** Shared helpers + per-format parsers for app_metadata_parse() (source/app_metadata_parsing.cpp)
* - split out like the old tt::app manifest parser (AppManifestParsing/V1/V2.cpp) that this is
* modelled on, one file per format plus a shared dispatcher. */
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value);
bool app_metadata_is_valid_format_version(const std::string& version);
bool app_metadata_is_valid_id(const std::string& id);
bool app_metadata_is_valid_name(const std::string& name);
bool app_metadata_is_valid_version_name(const std::string& version);
bool app_metadata_is_valid_version_code(const std::string& version);
/** Copies @a value into @a dest (a fixed-size buffer of @a dest_size bytes, including the NULL
* terminator) if it fits.
* @retval false @a value doesn't fit in @a dest_size bytes - @a dest is left untouched */
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value);
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map into @a out_metadata. */
bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map into @a out_metadata. */
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/manifest.h>
#include <tactility/error.h>
/**
* 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
* (not TactilityKernel's Thread wrapper). Every app instance gets its own dedicated task for its
* entire lifetime - no task is ever reused for a different instance.
*/
#ifdef __cplusplus
extern "C" {
#endif
/**
* Loads and starts an app instance: spawns a dedicated task that calls
* AppLoaderApi::load()/run(), marking the instance ACTIVE for the duration of run().
* @param[in] app_instance_id id already allocated in the ledger for this instance
* @param[in] location the location of the app
* @param[in] argc the amount of arguments to pass to the app's main function
* @param[in] argv the array of arguments to pass to the app's main function - ownership is
* taken by the scheduler regardless of outcome (freed once the spawned task's run() returns, or
* immediately on a failure to start it)
*/
error_t app_scheduler_start(AppInstanceId app_instance_id, struct AppLocation location, int argc, char* argv[]);
/**
* Permanently stops an app instance (APP_EVENT_CLOSE if it was running), bound-waits for its
* task to exit, and removes it from the ledger.
*/
error_t app_scheduler_stop(AppInstanceId app_instance_id, TickType_t join_timeout);
// app_scheduler_current_app_id() is public - see app/scheduler.h.
#ifdef __cplusplus
}
#endif
+413
View File
@@ -0,0 +1,413 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/install.h>
#include <app/manager.h>
#include <app/metadata.h>
#include <app/private/app_fs.h>
#include <app/private/app_ledger.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/filesystem/file_mutex.h>
#include <tactility/log.h>
#include <tactility/paths.h>
#include <minitar.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
constexpr auto* TAG = "app_install";
namespace {
// region Filesystem helpers (app-module may not depend upward on Tactility::file - see
// app_metadata_parsing.cpp for the same constraint applied to properties-file loading)
std::string last_path_segment(const std::string& path) {
auto index = path.find_last_of('/');
return index == std::string::npos ? path : path.substr(index + 1);
}
// mkdir -p.
bool ensure_directory(const std::string& path) {
if (path.empty() || app_fs_is_directory(path)) {
return true;
}
FileMutex mutex {};
file_mutex_get(&mutex, path.c_str());
file_mutex_lock(&mutex);
bool created = mkdir(path.c_str(), 0777) == 0 || errno == EEXIST;
file_mutex_unlock(&mutex);
if (!created) {
return false;
}
return app_fs_is_directory(path);
}
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);
}
bool delete_recursively(const std::string& path) {
LOG_D(TAG, "Deleting %s...", path.c_str());
if (path.empty() || path == "/" || path == "." || path == "..") {
return true;
}
if (app_fs_is_directory(path)) {
LOG_D(TAG, "Deleting dir %s", path.c_str());
FileMutex file_mutex;
file_mutex_get(&file_mutex, path.c_str());
file_mutex_lock(&file_mutex);
DIR* dir = opendir(path.c_str());
if (dir == nullptr) {
LOG_E(TAG, "Failed to scan directory %s", path.c_str());
file_mutex_unlock(&file_mutex);
return false;
}
bool success = true;
dirent* entry;
while (success && (entry = readdir(dir)) != nullptr) {
if (std::strcmp(entry->d_name, ".") == 0 || std::strcmp(entry->d_name, "..") == 0) {
continue;
}
success = delete_recursively(path + "/" + entry->d_name);
}
closedir(dir);
if (!success) {
file_mutex_unlock(&file_mutex);
return false;
}
bool result = rmdir(path.c_str()) == 0;
file_mutex_unlock(&file_mutex);
return result;
}
if (app_fs_is_file(path)) {
LOG_D(TAG, "Deleting file %s", path.c_str());
FileMutex mutex {};
file_mutex_get(&mutex, path.c_str());
file_mutex_lock(&mutex);
bool result = remove(path.c_str()) == 0;
file_mutex_unlock(&mutex);
return result;
}
LOG_D(TAG, "Deleting done");
return true;
}
bool get_app_install_directory(std::string& out_path) {
char root[192];
if (paths_get_user_data_path(root, sizeof(root)) != ERROR_NONE) {
return false;
}
out_path = std::string(root) + "/app";
return true;
}
// endregion
// region Tar extraction (ported from the old Tactility::app AppInstall.cpp)
bool untar_file(minitar* archive, const minitar_entry* entry, const std::string& destination_path) {
auto absolute_path = destination_path + "/" + entry->metadata.path;
if (!ensure_directory_recursive(destination_path)) {
LOG_E(TAG, "Can't find or create directory %s", destination_path.c_str());
return false;
}
if (!minitar_read_contents_to_file(archive, entry, absolute_path.c_str())) {
LOG_E(TAG, "Failed to write data to %s", absolute_path.c_str());
return false;
}
// Note: fchmod() doesn't exist on ESP-IDF and chmod() does nothing on that platform.
chmod(absolute_path.c_str(), entry->metadata.mode);
return true;
}
bool untar_directory(const minitar_entry* entry, const std::string& destination_path) {
return ensure_directory_recursive(destination_path + "/" + entry->metadata.path);
}
bool untar(const std::string& tar_path, const std::string& destination_path) {
minitar archive {};
if (minitar_open(tar_path.c_str(), &archive) != 0) {
LOG_E(TAG, "Failed to open %s", tar_path.c_str());
return false;
}
bool success = true;
minitar_entry entry {};
while (minitar_read_entry(&archive, &entry) == 0) {
LOG_I(TAG, "Extracting %s", entry.metadata.path);
if (entry.metadata.type == MTAR_DIRECTORY) {
if (std::strcmp(entry.metadata.name, ".") == 0 || std::strcmp(entry.metadata.name, "..") == 0 || std::strcmp(entry.metadata.name, "/") == 0) {
continue;
}
success = untar_directory(&entry, destination_path);
} else if (entry.metadata.type == MTAR_REGULAR) {
success = untar_file(&archive, &entry, destination_path);
} else {
LOG_E(TAG, "Unsupported entry type: %d", static_cast<int>(entry.metadata.type));
success = false;
}
if (!success) {
LOG_E(TAG, "Failed to extract %s", entry.metadata.path);
break;
}
}
minitar_close(&archive);
return success;
}
// endregion
// region Installed-app registry: owns the AppManifest (and its id/name/path strings) that
// app_manager's ledger only keeps a non-owning pointer to (see app_manager_add()'s contract).
struct InstalledAppRecord {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
};
struct InstallRegistry {
std::unordered_map<std::string, std::unique_ptr<InstalledAppRecord>> apps;
Mutex mutex {};
InstallRegistry() { mutex_construct(&mutex); }
};
InstallRegistry& install_registry() {
static InstallRegistry registry;
return registry;
}
// Registers @a app_dir_path (already confirmed to hold a valid manifest.properties, parsed into
// @a metadata) with app_manager_add(), taking ownership of its id/name/path strings.
// @warning Caller must hold install_registry().mutex, and must have already ensured
// @a metadata.app_id isn't already registered (app_manager_add() rejects duplicates, but the
// InstalledAppRecord for the earlier registration would leak since this always inserts fresh).
error_t register_installed_app_locked(const std::string& app_dir_path, const AppMetadata& metadata) {
auto& registry = install_registry();
auto record = std::make_unique<InstalledAppRecord>();
record->id = metadata.app_id;
record->name = metadata.app_name;
record->path = app_dir_path;
record->manifest = AppManifest {
.id = record->id.c_str(),
.name = record->name.c_str(),
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = 0,
};
// Belt-and-braces: app_install()'s earlier app_manager_remove() call is meant to have
// already cleared any stale registration for this id (e.g. left over from
// app_manager_install_path_scan()'s separate registry), but that call happens before the
// tarball is even extracted - remove once more, right before add, so a duplicate id can
// never turn a filesystem-level install success into a reported failure.
app_manager_remove(record->id.c_str());
error_t add_result = app_manager_add(&record->manifest);
if (add_result != ERROR_NONE) {
LOG_E(TAG, "Failed to register app '%s': %s", record->id.c_str(), error_to_string(add_result));
return add_result;
}
registry.apps[record->id] = std::move(record);
return ERROR_NONE;
}
// Stops every currently-running instance of @a manifest. Collects matching instance ids while
// holding the ledger lock, then calls app_manager_stop() on each after releasing it - that call
// bound-joins the instance's thread, which must not happen while the ledger mutex (also taken by
// the instance's own thread_main()) is held, or the two threads would deadlock each other.
void stop_all_instances_of(const AppManifest* manifest) {
std::vector<uint32_t> instance_ids;
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
for (const auto& [id, record]: ledger.instances) {
if (record.manifest == manifest) {
instance_ids.push_back(id);
}
}
mutex_unlock(&ledger.mutex);
for (uint32_t id: instance_ids) {
app_manager_stop(id);
}
}
// 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);
if (iterator == registry.apps.end()) {
return ERROR_NOT_FOUND;
}
stop_all_instances_of(&iterator->second->manifest);
app_manager_remove(app_id.c_str());
delete_recursively(iterator->second->path);
registry.apps.erase(iterator);
return ERROR_NONE;
}
// endregion
} // namespace
extern "C" {
error_t app_get_install_path(const char* app_id, char* path, size_t path_size) {
if (path_size == 0) {
return ERROR_BUFFER_OVERFLOW;
}
path[0] = '\0';
std::string app_parent_path;
if (!get_app_install_directory(app_parent_path)) {
return ERROR_NOT_FOUND;
}
int written = std::snprintf(path, path_size, "%s/%s", app_parent_path.c_str(), app_id);
if (written < 0 || static_cast<size_t>(written) >= path_size) {
path[0] = '\0';
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
error_t app_install(const char* source_path) {
LOG_I(TAG, "Installing app from %s", source_path);
std::string app_parent_path;
if (!get_app_install_directory(app_parent_path)) {
return ERROR_NOT_FOUND;
}
if (!ensure_directory_recursive(app_parent_path)) {
LOG_E(TAG, "Failed to create %s", app_parent_path.c_str());
return ERROR_NOT_FOUND;
}
auto staging_path = app_parent_path + "/" + last_path_segment(source_path);
delete_recursively(staging_path);
FileMutex target_mutex {};
file_mutex_get(&target_mutex, app_parent_path.c_str());
FileMutex source_mutex {};
file_mutex_get(&source_mutex, source_path);
file_mutex_lock(&target_mutex);
file_mutex_lock(&source_mutex);
bool untar_success = untar(source_path, staging_path);
file_mutex_unlock(&source_mutex);
file_mutex_unlock(&target_mutex);
if (!untar_success) {
LOG_E(TAG, "Failed to extract %s", source_path);
delete_recursively(staging_path);
return ERROR_NOT_FOUND;
}
auto manifest_path = staging_path + "/manifest.properties";
if (!app_fs_is_file(manifest_path)) {
LOG_E(TAG, "Manifest not found at %s", manifest_path.c_str());
delete_recursively(staging_path);
return ERROR_INVALID_ARGUMENT;
}
AppMetadata metadata {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
LOG_E(TAG, "Install failed: invalid manifest");
delete_recursively(staging_path);
return ERROR_INVALID_ARGUMENT;
}
auto& registry = install_registry();
mutex_lock(&registry.mutex);
// Replace any previous install of this app id (mirrors the old install()'s "already
// running/present" handling). uninstall_locked() only clears app_install.cpp's own
// registry - the same app id may instead be registered by app_manager_install_path_scan()
// (manager.cpp's separate registry, scanning this same directory tree), which
// uninstall_locked() doesn't know about. Clear the app-manager registration unconditionally
// too, or app_manager_add() below rejects the re-add as a duplicate.
uninstall_locked(metadata.app_id);
error_t remove_result = app_manager_remove(metadata.app_id);
if (remove_result != ERROR_NONE && remove_result != ERROR_NOT_FOUND) {
LOG_E(TAG, "Install failed: failed to remove existing installation");
mutex_unlock(&registry.mutex);
delete_recursively(staging_path);
return ERROR_RESOURCE;
}
auto final_path = app_parent_path + "/" + metadata.app_id;
delete_recursively(final_path);
file_mutex_lock(&target_mutex);
bool rename_success = rename(staging_path.c_str(), final_path.c_str()) == 0;
file_mutex_unlock(&target_mutex);
if (!rename_success) {
LOG_E(TAG, "Failed to rename \"%s\" to \"%s\"", staging_path.c_str(), final_path.c_str());
delete_recursively(staging_path);
mutex_unlock(&registry.mutex);
return ERROR_NOT_FOUND;
}
// Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above
// already removed any previous registration for this exact id.
error_t add_result = register_installed_app_locked(final_path, metadata);
mutex_unlock(&registry.mutex);
return add_result;
}
error_t app_uninstall(const char* app_id) {
LOG_I(TAG, "Uninstalling app %s", app_id);
auto& registry = install_registry();
mutex_lock(&registry.mutex);
error_t result = uninstall_locked(app_id);
mutex_unlock(&registry.mutex);
return result;
}
} // extern "C"
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/loader.h>
#include <app/manifest.h>
#include <service/instance.h>
#include <service/manager.h>
namespace {
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
if (location.type != APP_LOCATION_MEMORY) {
return ERROR_NOT_SUPPORTED;
}
*out_runtime = location.location;
return ERROR_NONE;
}
int32_t api_run(AppRuntime runtime, uint32_t app_instance_id, int argc, char* argv[]) {
auto entry = reinterpret_cast<AppMainFn>(runtime);
return entry(app_instance_id, argc, argv);
}
void api_unload(AppRuntime /*unused*/) {
}
AppLoaderApi memory_loader_api = {
.load = api_load,
.run = api_run,
.unload = api_unload,
};
void* create_service(const ServiceManifest*) {
return &memory_loader_api;
}
void destroy_service(const ServiceManifest*, void*) {
}
} // namespace
ServiceManifest app_internal_loader_service_manifest = {
.id = APP_LOADER_MEMORY_SERVICE_ID,
.create_service = create_service,
.destroy_service = destroy_service,
.on_start = nullptr,
.on_stop = nullptr,
};
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: Apache-2.0
#include "tactility/filesystem/file_mutex.h"
#include <app/metadata.h>
#include <app/private/app_metadata_parsing_internal.h>
#include <tactility/log.h>
#include <cctype>
#include <cstring>
#include <fstream>
#include <map>
#include <string>
constexpr auto* TAG = "app_metadata";
namespace {
std::string trim(const std::string& value) {
constexpr auto* whitespace = " \t\r\n";
auto start = value.find_first_not_of(whitespace);
if (start == std::string::npos) {
return "";
}
auto end = value.find_last_not_of(whitespace);
return value.substr(start, end - start + 1);
}
bool validate_string(const std::string& value, bool (*is_valid_char)(char)) {
for (char c: value) {
if (!is_valid_char(c)) {
return false;
}
}
return true;
}
/** manifest.properties format: "key=value" lines, "[section]" lines prefix every following key
* until the next section, "#" lines are comments, blank lines are skipped. Deliberately a local,
* minimal re-implementation rather than depending on Tactility's file::loadPropertiesFile() -
* app-module (like every other kernel module) may not depend upward on the Tactility layer. */
bool load_properties(const std::string& path, std::map<std::string, std::string>& out_properties, std::string& out_first_line) {
FileMutex mutex;
file_mutex_get(&mutex, path.c_str());
file_mutex_lock(&mutex);
std::ifstream file(path);
if (!file.is_open()) {
file_mutex_unlock(&mutex);
return false;
}
std::string line;
std::string section_prefix;
bool got_first_line = false;
while (std::getline(file, line)) {
auto trimmed_line = trim(line);
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
continue;
}
if (!got_first_line) {
out_first_line = trimmed_line;
got_first_line = true;
}
if (trimmed_line.starts_with("[")) {
section_prefix = trimmed_line;
continue;
}
auto separator_index = trimmed_line.find('=');
if (separator_index == std::string::npos) {
LOG_E(TAG, "Failed to parse manifest line (skipped): %s", trimmed_line.c_str());
continue;
}
auto key = section_prefix + trim(trimmed_line.substr(0, separator_index));
auto value = trim(trimmed_line.substr(separator_index + 1));
out_properties[key] = value;
}
file_mutex_unlock(&mutex);
return true;
}
} // namespace
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value) {
const auto iterator = properties.find(key);
if (iterator == properties.end()) {
LOG_E(TAG, "Failed to find %s in manifest", key.c_str());
return false;
}
out_value = iterator->second;
return true;
}
bool app_metadata_is_valid_format_version(const std::string& version) {
return !version.empty() && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_metadata_is_valid_id(const std::string& id) {
return id.size() >= 5 && id.size() <= APP_METADATA_APP_ID_LENGTH && validate_string(id, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_metadata_is_valid_name(const std::string& name) {
return name.size() >= 2 && name.size() <= APP_METADATA_APP_NAME_LENGTH && validate_string(name, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == ' ' || c == '-';
});
}
bool app_metadata_is_valid_version_name(const std::string& version) {
return !version.empty() && version.size() <= APP_METADATA_APP_VERSION_NAME_LENGTH && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.' || c == '-' || c == '_';
});
}
bool app_metadata_is_valid_version_code(const std::string& version) {
// 20 digits is the maximum decimal width of uint64_t.
return !version.empty() && version.size() <= 20 && validate_string(version, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value) {
if (value.size() >= dest_size) {
return false;
}
memcpy(dest, value.c_str(), value.size() + 1);
return true;
}
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata) {
LOG_I(TAG, "Parsing manifest %s", path);
std::map<std::string, std::string> properties;
std::string first_line;
if (!load_properties(path, properties, first_line)) {
LOG_E(TAG, "Failed to load manifest at %s", path);
return ERROR_NOT_FOUND;
}
// The V1 format's first line is always the literal "[manifest]" section header; V2 files are
// flat from the first line onward.
bool is_v1_format = first_line == "[manifest]";
bool success = is_v1_format
? app_metadata_parse_v1(properties, *out_metadata)
: app_metadata_parse_v2(properties, *out_metadata);
return success ? ERROR_NONE : ERROR_INVALID_ARGUMENT;
}
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/metadata.h>
#include <app/private/app_metadata_parsing_internal.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v1";
bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties, AppMetadata& out_metadata) {
// [manifest]
std::string format_version;
if (!app_metadata_get_value(properties, "[manifest]version", format_version)) {
return false;
}
if (!app_metadata_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return false;
}
// [app]
std::string id;
if (!app_metadata_get_value(properties, "[app]id", id)) {
return false;
}
if (!app_metadata_is_valid_id(id)) {
LOG_E(TAG, "Invalid app id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_id, sizeof(out_metadata.app_id), id)) {
LOG_E(TAG, "App id too long");
return false;
}
std::string name;
if (!app_metadata_get_value(properties, "[app]name", name)) {
return false;
}
if (!app_metadata_is_valid_name(name)) {
LOG_E(TAG, "Invalid app name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_name, sizeof(out_metadata.app_name), name)) {
LOG_E(TAG, "App name too long");
return false;
}
std::string version_name;
if (!app_metadata_get_value(properties, "[app]versionName", version_name)) {
return false;
}
if (!app_metadata_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_version_name, sizeof(out_metadata.app_version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return false;
}
std::string version_code_string;
if (!app_metadata_get_value(properties, "[app]versionCode", version_code_string)) {
return false;
}
if (!app_metadata_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return false;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return false;
}
out_metadata.app_version_code = version_code; // [target]
std::string target_sdk;
if (!app_metadata_get_value(properties, "[target]sdk", target_sdk)) {
return false;
}
if (!app_metadata_copy_bounded(out_metadata.target_sdk, sizeof(out_metadata.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return false;
}
return true;
}
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/metadata.h>
#include <app/private/app_metadata_parsing_internal.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v2";
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, AppMetadata& out_metadata) {
// manifest
std::string format_version;
if (!app_metadata_get_value(properties, "manifest.version", format_version)) {
return false;
}
if (!app_metadata_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return false;
}
// app
std::string id;
if (!app_metadata_get_value(properties, "app.id", id)) {
return false;
}
if (!app_metadata_is_valid_id(id)) {
LOG_E(TAG, "Invalid app id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_id, sizeof(out_metadata.app_id), id)) {
LOG_E(TAG, "App id too long");
return false;
}
std::string name;
if (!app_metadata_get_value(properties, "app.name", name)) {
return false;
}
if (!app_metadata_is_valid_name(name)) {
LOG_E(TAG, "Invalid app name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_name, sizeof(out_metadata.app_name), name)) {
LOG_E(TAG, "App name too long");
return false;
}
std::string version_name;
if (!app_metadata_get_value(properties, "app.version.name", version_name)) {
return false;
}
if (!app_metadata_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_version_name, sizeof(out_metadata.app_version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return false;
}
std::string version_code_string;
if (!app_metadata_get_value(properties, "app.version.code", version_code_string)) {
return false;
}
if (!app_metadata_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return false;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return false;
}
out_metadata.app_version_code = version_code; // [target]
// target
std::string target_sdk;
if (!app_metadata_get_value(properties, "target.sdk", target_sdk)) {
return false;
}
if (!app_metadata_copy_bounded(out_metadata.target_sdk, sizeof(out_metadata.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return false;
}
return true;
}
+62
View File
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/paths.h>
#include <tactility/paths.h>
#include <cstdio>
extern "C" {
error_t app_paths_get_user_data_directory(const char* app_id, char* out_path, size_t out_path_size) {
char root[192];
error_t error = paths_get_user_data_path(root, sizeof(root));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/app/%s", root, app_id);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
error_t app_paths_get_user_data_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size) {
char directory[224];
error_t error = app_paths_get_user_data_directory(app_id, directory, sizeof(directory));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_t out_path_size) {
char directory[224];
error_t error = app_paths_get_user_data_directory(app_id, directory, sizeof(directory));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/assets", directory);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
error_t app_paths_get_assets_path(const char* app_id, const char* child_path, char* out_path, size_t out_path_size) {
char directory[224];
error_t error = app_paths_get_assets_directory(app_id, directory, sizeof(directory));
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/%s", directory, child_path);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
return ERROR_NONE;
}
} // extern "C"
+299
View File
@@ -0,0 +1,299 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/private/app_ledger.h>
#include <app/private/app_scheduler.h>
#include <app/event.h>
#include <app/instance.h>
#include <app/loader.h>
#include <app/scheduler.h>
#include <service/instance.h>
#include <service/manager.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <cstdint>
#include <cstdio>
#include <new>
constexpr auto* TAG = "app_scheduler";
// Slot 0 is reserved by ESP-IDF's pthread API (see TactilityKernel's Thread wrapper for the
// same convention/comment) - app tasks use slot 1 to stash their own app_instance_id, so any
// code running on an app's own task can retrieve it via app_scheduler_current_app_id() without
// needing it threaded through as a parameter.
constexpr size_t APP_INSTANCE_ID_THREAD_SLOT_INDEX = 1;
// Matches TactilityKernel's Thread wrapper's THREAD_PRIORITY_NORMAL.
constexpr UBaseType_t APP_TASK_PRIORITY = 4;
namespace {
struct TaskContext {
const AppLoaderApi* loader;
void* runtime;
AppInstanceId app_instance_id;
int argc;
char** argv;
AppCompletionSignal* completion;
};
void set_state(AppInstanceId app_instance_id, AppInstanceState state) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator != ledger.instances.end()) {
iterator->second.state = state;
}
mutex_unlock(&ledger.mutex);
}
void set_task(AppInstanceId app_instance_id, TaskHandle_t task) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator != ledger.instances.end()) {
iterator->second.task = task;
}
mutex_unlock(&ledger.mutex);
}
void set_completion(AppInstanceId app_instance_id, AppCompletionSignal* completion) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator != ledger.instances.end()) {
iterator->second.completion = completion;
}
mutex_unlock(&ledger.mutex);
}
// Takes a reference on app_instance_id's completion signal (see AppCompletionSignal), for the
// caller to wait on. @return the signal to wait on, or NULL if the instance has already fully
// finished (its ledger entry - and so its reference to the signal - is already gone) and so
// there's nothing left to wait for, or if the instance is still starting up (start_internal()
// in manager.cpp inserts the ledger entry before app_scheduler_start() has gotten as far as
// set_completion() - `completion` is NULL for that whole window) and so there's nothing to
// take a reference on yet.
AppCompletionSignal* acquire_completion_signal(AppInstanceId app_instance_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
AppCompletionSignal* completion = nullptr;
if (iterator != ledger.instances.end() && iterator->second.completion != nullptr) {
completion = iterator->second.completion;
completion->refcount++;
}
mutex_unlock(&ledger.mutex);
return completion;
}
// Releases a reference taken by acquire_completion_signal(), deleting the signal (and its
// semaphore) if this was the last one.
void release_completion_signal(AppCompletionSignal* completion) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
bool should_delete = (--completion->refcount == 0);
mutex_unlock(&ledger.mutex);
if (should_delete) {
vSemaphoreDelete(completion->semaphore);
delete completion;
}
}
const char* loader_service_id_for(AppLocationType type) {
return (type == APP_LOCATION_MEMORY) ? APP_LOADER_MEMORY_SERVICE_ID : APP_LOADER_PATH_SERVICE_ID;
}
const AppLoaderApi* find_loader_api(AppLocationType type) {
ServiceInstance* instance = service_manager_find_instance(loader_service_id_for(type));
if (instance == nullptr) {
return nullptr;
}
return static_cast<const AppLoaderApi*>(service_instance_get_data(instance));
}
// If this instance was launched via app_manager_start_for_result(), delivers @a result (its
// own AppMainFn/AppLoaderApi::run() return value) to its parent. No-op for a top-level instance
// (parent_id == 0).
void deliver_result_to_parent_if_any(AppInstanceId app_instance_id, int32_t result) {
auto& ledger = app_ledger();
AppInstanceId parent_id;
AppEvent event { .type = APP_EVENT_RESULT, .timestamp = 0, .result = {} };
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator == ledger.instances.end()) {
mutex_unlock(&ledger.mutex);
return;
}
parent_id = iterator->second.parent_id;
event.result.launch_id = app_instance_id;
event.result.result = result;
mutex_unlock(&ledger.mutex);
if (parent_id != 0) {
app_event_emit(parent_id, &event);
}
}
void app_task_main(void* context) {
auto* ctx = static_cast<TaskContext*>(context);
check(pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX) == nullptr);
vTaskSetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX, reinterpret_cast<void*>(static_cast<uintptr_t>(ctx->app_instance_id)));
LOG_I(TAG, "Thread for %d started", ctx->app_instance_id);
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_ACTIVE);
int32_t result = ctx->loader->run(ctx->runtime, ctx->app_instance_id, ctx->argc, ctx->argv);
vTaskSetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX, nullptr);
ctx->loader->unload(ctx->runtime);
deliver_result_to_parent_if_any(ctx->app_instance_id, result);
// A safe default terminal marker for CLOSE (and any other exit): an app that calls
// app_manager_finish() already marked itself Stopped before returning, so this is a no-op
// for it - but it's still needed as the terminal marker for any other exit path.
set_state(ctx->app_instance_id, APP_INSTANCE_STATE_STOPPED);
app_ledger_free_arguments(ctx->argc, ctx->argv);
AppInstanceId app_instance_id = ctx->app_instance_id;
AppCompletionSignal* completion = ctx->completion;
delete ctx;
LOG_I(TAG, "Thread for %d finished", app_instance_id);
// Erase the ledger entry before self-deleting - see "Reap self-terminated app tasks":
// nothing else is guaranteed to ever call app_scheduler_stop() for this instance (the
// common case is the app just closing itself), so this can't wait for that to happen.
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
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 - unlike watching the ledger
// entry disappear, this can only happen once the task is truly done running. A dedicated
// semaphore rather than this task's default FreeRTOS notification, since app_event.cpp's
// AppEventSubscription also uses that shared slot - an unrelated event (e.g. a child's
// APP_EVENT_RESULT) delivered to this same task could otherwise unblock a concurrent
// app_scheduler_stop() early.
xSemaphoreGive(completion->semaphore);
release_completion_signal(completion); // releases app_task_main()'s own reference
vTaskDelete(nullptr);
}
} // namespace
extern "C" {
error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location, int argc, char* argv[]) {
const AppLoaderApi* loader = find_loader_api(location.type);
if (loader == nullptr) {
LOG_E(TAG, "No app loader is registered (service '%s' not found)", loader_service_id_for(location.type));
app_ledger_free_arguments(argc, argv);
return ERROR_NOT_FOUND;
}
void* runtime = nullptr;
error_t load_result = loader->load(location, &runtime);
if (load_result != ERROR_NONE) {
LOG_E(TAG, "Failed to load app: %s", error_to_string(load_result));
app_ledger_free_arguments(argc, argv);
return load_result;
}
auto* completion = new (std::nothrow) AppCompletionSignal();
if (completion == nullptr) {
LOG_E(TAG, "Failed to allocate app");
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
completion->semaphore = xSemaphoreCreateBinary();
if (completion->semaphore == nullptr) {
LOG_E(TAG, "Failed to allocate app");
delete completion;
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
auto* context = new (std::nothrow) TaskContext { loader, runtime, app_instance_id, argc, argv, completion };
if (context == nullptr) {
LOG_E(TAG, "Failed to allocate app");
vSemaphoreDelete(completion->semaphore);
delete completion;
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
char task_name[16];
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.
// 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;
vSemaphoreDelete(completion->semaphore);
delete completion;
loader->unload(runtime);
app_ledger_free_arguments(argc, argv);
return ERROR_OUT_OF_MEMORY;
}
vTaskSuspend(task_handle);
set_task(app_instance_id, task_handle);
set_completion(app_instance_id, completion);
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) {
AppCompletionSignal* completion = acquire_completion_signal(app_instance_id);
if (completion != nullptr) {
AppEvent event { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
app_event_emit(app_instance_id, &event);
// Blocks until app_task_main() gives this dedicated semaphore as the literal last thing it does before vTaskDelete().
// Uses aa dedicated semaphore rather than this task's default FreeRTOS notification because app_event.cpp's AppEventSubscription also uses that shared slot.
// An unrelated event (e.g. a different child's APP_EVENT_RESULT) delivered to this same task could otherwise unblock this early.
BaseType_t taken = xSemaphoreTake(completion->semaphore, join_timeout);
release_completion_signal(completion);
if (taken == pdFALSE) {
LOG_W(TAG, "App instance %u did not stop in time", app_instance_id);
return ERROR_TIMEOUT;
}
}
set_state(app_instance_id, APP_INSTANCE_STATE_STOPPED);
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
ledger.instances.erase(app_instance_id);
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
AppInstanceId app_scheduler_current_app_id(void) {
void* value = pvTaskGetThreadLocalStoragePointer(nullptr, APP_INSTANCE_ID_THREAD_SLOT_INDEX);
return reinterpret_cast<uintptr_t>(value);
}
} // extern "C"
+115
View File
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/event.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/time.h>
/**
* Intrusive singly-linked list of subscriptions, keyed by app_instance_id.
* Guarded by a single coarse-grained mutex, notifying a subscriber here never invokes caller code
* (just a struct copy and an xTaskNotifyGive), so there is no reentrancy concern requiring a snapshot-then-unlock dance.
*/
static AppEventSubscription* subscriptions = nullptr;
struct AppEventMutex {
Mutex handle {};
AppEventMutex() { mutex_construct(&handle); }
~AppEventMutex() { mutex_destruct(&handle); }
};
static AppEventMutex subscriptions_mutex;
extern "C" {
error_t app_event_subscribe(AppEventSubscription* sub) {
sub->task = xTaskGetCurrentTaskHandle();
sub->head = 0;
sub->count = 0;
mutex_lock(&subscriptions_mutex.handle);
sub->next = subscriptions;
subscriptions = sub;
mutex_unlock(&subscriptions_mutex.handle);
return ERROR_NONE;
}
error_t app_event_unsubscribe(AppEventSubscription* sub) {
error_t result = ERROR_NOT_FOUND;
mutex_lock(&subscriptions_mutex.handle);
for (AppEventSubscription** link = &subscriptions; *link != nullptr; link = &(*link)->next) {
if (*link == sub) {
*link = sub->next;
result = ERROR_NONE;
break;
}
}
mutex_unlock(&subscriptions_mutex.handle);
return result;
}
error_t app_event_emit(uint32_t app_instance_id, const AppEvent* event) {
AppEvent stamped_event = *event;
stamped_event.timestamp = get_micros_since_boot();
error_t result = ERROR_NOT_FOUND;
mutex_lock(&subscriptions_mutex.handle);
for (AppEventSubscription* sub = subscriptions; sub != nullptr; sub = sub->next) {
if (sub->app_instance_id != app_instance_id) {
continue;
}
if (sub->count >= APP_EVENT_QUEUE_CAPACITY) {
result = ERROR_RESOURCE;
continue;
}
uint8_t tail = (sub->head + sub->count) % APP_EVENT_QUEUE_CAPACITY;
sub->queue[tail] = stamped_event;
sub->count++;
if (result != ERROR_RESOURCE) {
result = ERROR_NONE;
}
xTaskNotifyGive(sub->task);
}
mutex_unlock(&subscriptions_mutex.handle);
return result;
}
static bool try_pop(AppEventSubscription* sub, AppEvent* out_event) {
mutex_lock(&subscriptions_mutex.handle);
bool has_event = sub->count > 0;
if (has_event) {
*out_event = sub->queue[sub->head];
sub->head = (sub->head + 1) % APP_EVENT_QUEUE_CAPACITY;
sub->count--;
}
mutex_unlock(&subscriptions_mutex.handle);
return has_event;
}
error_t app_event_await(AppEventSubscription* sub, AppEvent* out_event, TickType_t timeout) {
if (try_pop(sub, out_event)) {
// Drain any notification credit this (or an earlier) push accumulated on this task's
// FreeRTOS notification value: each app_event_emit() calls xTaskNotifyGive() regardless
// of whether the consumer takes this fast path or the blocking path below, so without
// this the credit would carry over and cause a future ulTaskNotifyTake() below to
// return immediately for a notification that was already accounted for here.
ulTaskNotifyTake(pdTRUE, 0);
return ERROR_NONE;
}
if (ulTaskNotifyTake(pdTRUE, timeout) == 0) {
return ERROR_TIMEOUT;
}
// Single-consumer by design (one task per subscription), so a wakeup implies the event
// this call was notified for is still there for us to pop.
return try_pop(sub, out_event) ? ERROR_NONE : ERROR_TIMEOUT;
}
} // extern "C"
+337
View File
@@ -0,0 +1,337 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manager.h>
#include <app/metadata.h>
#include <app/private/app_fs.h>
#include <app/private/app_ledger.h>
#include <app/private/app_scheduler.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/log.h>
#include <algorithm>
#include <cstring>
#include <memory>
#include <unordered_map>
#include <vector>
#define TAG "app_manager"
extern "C" {
error_t app_manager_add(const AppManifest* manifest) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
if (ledger.manifests.contains(manifest->id)) {
mutex_unlock(&ledger.mutex);
LOG_E(TAG, "Manifest with id '%s' is already registered", manifest->id);
return ERROR_INVALID_ARGUMENT;
}
ledger.manifests[manifest->id] = manifest;
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
error_t app_manager_remove(const char* id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.manifests.find(id);
if (iterator == ledger.manifests.end()) {
mutex_unlock(&ledger.mutex);
return ERROR_NOT_FOUND;
}
ledger.manifests.erase(iterator);
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
const AppManifest* app_manager_find_manifest(const char* id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.manifests.find(id);
const AppManifest* manifest = (iterator != ledger.manifests.end()) ? iterator->second : nullptr;
mutex_unlock(&ledger.mutex);
return manifest;
}
void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
for (auto& [id, manifest] : ledger.manifests) {
visitor(manifest, context);
}
mutex_unlock(&ledger.mutex);
}
namespace {
// Deep-copies argv (argc <= 0 => NULL, matching "no parameters"). Caller passes the result to
// app_scheduler_start(), which takes ownership regardless of outcome.
char** copy_arguments(int argc, const char* const argv[]) {
if (argc <= 0) {
return nullptr;
}
auto* copy = new char*[argc + 1];
for (int i = 0; i < argc; i++) {
size_t length = strlen(argv[i]);
copy[i] = new char[length + 1];
memcpy(copy[i], argv[i], length + 1);
}
copy[argc] = nullptr;
return copy;
}
// Takes ownership of argv (already a deep copy, or NULL/argc==0) regardless of outcome -
// app_scheduler_start() frees it on any failure path, and the spawned task frees it once its
// run() returns.
error_t start_internal(const char* id, AppInstanceId parent_instance_id, int argc, char* argv[], AppInstanceId* out_app_instance_id) {
const AppManifest* manifest = app_manager_find_manifest(id);
if (manifest == nullptr) {
app_ledger_free_arguments(argc, argv);
return ERROR_NOT_FOUND;
}
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
AppInstanceId target_id = ledger.next_instance_id++;
AppInstanceRecord record { target_id, manifest, APP_INSTANCE_STATE_STARTING, nullptr };
record.parent_id = parent_instance_id;
ledger.instances[target_id] = record;
mutex_unlock(&ledger.mutex);
error_t result = app_scheduler_start(target_id, manifest->location, argc, argv);
if (result != ERROR_NONE) {
mutex_lock(&ledger.mutex);
ledger.instances.erase(target_id);
mutex_unlock(&ledger.mutex);
return result;
}
*out_app_instance_id = target_id;
return ERROR_NONE;
}
} // namespace
error_t app_manager_start(const char* id, AppInstanceId* out_app_instance_id) {
return start_internal(id, 0, 0, nullptr, out_app_instance_id);
}
error_t app_manager_start_with_parameters(const char* id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) {
return start_internal(id, 0, argc, copy_arguments(argc, argv), out_app_instance_id);
}
error_t app_manager_start_for_result(const char* id, AppInstanceId parent_instance_id, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) {
return start_internal(id, parent_instance_id, argc, copy_arguments(argc, argv), out_app_instance_id);
}
error_t app_manager_stop(AppInstanceId app_instance_id) {
return app_scheduler_stop(app_instance_id, pdMS_TO_TICKS(2000));
}
error_t app_manager_finish(AppInstanceId app_instance_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
if (iterator != ledger.instances.end()) {
iterator->second.state = APP_INSTANCE_STATE_STOPPED;
}
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
AppInstanceState app_manager_get_state(AppInstanceId app_instance_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(app_instance_id);
AppInstanceState state = (iterator != ledger.instances.end()) ? iterator->second.state : APP_INSTANCE_STATE_STOPPED;
mutex_unlock(&ledger.mutex);
return state;
}
error_t app_manager_get_topmost_instance_id(AppInstanceId* out_app_instance_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
AppInstanceId topmost_id = 0;
for (auto& [instance_id, record] : ledger.instances) {
// Instance ids are handed out in increasing order (AppLedger::next_instance_id), so
// the highest Active id is also the most recently started one.
if (record.state == APP_INSTANCE_STATE_ACTIVE && instance_id > topmost_id) {
topmost_id = instance_id;
}
}
mutex_unlock(&ledger.mutex);
if (topmost_id == 0) {
return ERROR_NOT_FOUND;
}
*out_app_instance_id = topmost_id;
return ERROR_NONE;
}
error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) {
if (buffer_size == 0) {
return ERROR_BUFFER_OVERFLOW;
}
buffer[0] = '\0';
AppInstanceId topmost_id = 0;
error_t result = app_manager_get_topmost_instance_id(&topmost_id);
if (result != ERROR_NONE) {
return result;
}
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.instances.find(topmost_id);
const char* app_id = (iterator != ledger.instances.end()) ? iterator->second.manifest->id : nullptr;
mutex_unlock(&ledger.mutex);
if (app_id == nullptr) {
return ERROR_NOT_FOUND;
}
size_t length = strlen(app_id);
if (length >= buffer_size) {
buffer[0] = '\0';
return ERROR_BUFFER_OVERFLOW;
}
memcpy(buffer, app_id, length + 1);
return ERROR_NONE;
}
} // extern "C"
namespace {
// Owns the AppManifest (and its id/name/path strings) that app_manager_add() only keeps a
// non-owning pointer to (see app_manager_add()'s contract), for manifests registered by
// app_manager_install_path_scan() specifically - separate from app_install.cpp's own registry,
// since scanning only ever adds/removes manifest registrations and never touches files on disk
// or running instances (unlike app_install()/app_uninstall()).
struct ScannedAppManifest {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
};
struct InstallPathRegistry {
std::vector<std::string> paths;
std::unordered_map<std::string, std::unique_ptr<ScannedAppManifest>> scanned;
Mutex mutex {};
InstallPathRegistry() { mutex_construct(&mutex); }
};
InstallPathRegistry& install_path_registry() {
static InstallPathRegistry registry;
return registry;
}
} // namespace
extern "C" {
error_t app_manager_install_path_add(const char* path) {
auto& registry = install_path_registry();
mutex_lock(&registry.mutex);
if (std::ranges::find(registry.paths, path) == registry.paths.end()) {
registry.paths.emplace_back(path);
}
mutex_unlock(&registry.mutex);
return ERROR_NONE;
}
void app_manager_install_path_scan(void) {
auto& registry = install_path_registry();
mutex_lock(&registry.mutex);
auto paths_copy = registry.paths;
mutex_unlock(&registry.mutex);
std::vector<std::string> found_app_dirs;
for (const auto& root : paths_copy) {
app_fs_list_direct_subdirectories(root, found_app_dirs);
}
// Snapshot of what's already registered, taken once so the rest of this scan can run without holding registry.mutex
mutex_lock(&registry.mutex);
std::unordered_map<std::string, std::string> known_paths; // id -> path
for (const auto& [id, record] : registry.scanned) {
known_paths.emplace(id, record->path);
}
mutex_unlock(&registry.mutex);
// Stat each manifest and parse it entirely without registry.mutex held (due to filesystem IO being slow)
std::vector<std::unique_ptr<ScannedAppManifest>> new_records;
for (const auto& app_dir : found_app_dirs) {
auto manifest_path = app_dir + "/manifest.properties";
if (!app_fs_is_file(manifest_path)) {
continue;
}
AppMetadata metadata {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
LOG_W(TAG, "Invalid manifest at %s", manifest_path.c_str());
continue;
}
if (known_paths.contains(metadata.app_id)) {
continue; // already registered by an earlier scan
}
auto record = std::make_unique<ScannedAppManifest>();
record->id = metadata.app_id;
record->name = metadata.app_name;
record->path = app_dir;
record->manifest = AppManifest {
.id = record->id.c_str(),
.name = record->name.c_str(),
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = 0,
};
new_records.push_back(std::move(record));
}
// Anything a previous scan registered whose directory has since disappeared gets unregistered below.
std::vector<std::string> missing_ids;
for (const auto& [id, path] : known_paths) {
if (!app_fs_is_directory(path)) {
missing_ids.push_back(id);
}
}
// app_manager_add()/app_manager_remove() take app-module's own ledger mutex internally -
// calling them while holding registry.mutex would establish a registry.mutex -> ledger-
// mutex lock order that any future opposite-order path would deadlock against, so these
// also run with registry.mutex released. registry.mutex is taken only afterward, briefly,
// to publish the results (plain in-memory map updates, no I/O or other locks involved).
for (const auto& id : missing_ids) {
app_manager_remove(id.c_str());
}
std::vector<std::unique_ptr<ScannedAppManifest>> added_records;
for (auto& record : new_records) {
if (app_manager_add(&record->manifest) == ERROR_NONE) {
added_records.push_back(std::move(record));
} else {
LOG_E(TAG, "Failed to register app %s (duplicate id?)", record->id.c_str());
}
}
mutex_lock(&registry.mutex);
for (const auto& id : missing_ids) {
registry.scanned.erase(id);
}
for (auto& record : added_records) {
registry.scanned[record->id] = std::move(record);
}
mutex_unlock(&registry.mutex);
}
} // extern "C"
+73
View File
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/event.h>
#include <app/install.h>
#include <app/manager.h>
#include <app/metadata.h>
#include <app/paths.h>
#include <app/scheduler.h>
#include <service/manager.h>
#include <tactility/error.h>
#include <tactility/module.h>
extern "C" {
extern ServiceManifest app_internal_loader_service_manifest;
const ModuleSymbol app_module_symbols[] = {
// app/event
DEFINE_MODULE_SYMBOL(app_event_subscribe),
DEFINE_MODULE_SYMBOL(app_event_unsubscribe),
DEFINE_MODULE_SYMBOL(app_event_emit),
DEFINE_MODULE_SYMBOL(app_event_await),
// app/install
DEFINE_MODULE_SYMBOL(app_get_install_path),
DEFINE_MODULE_SYMBOL(app_install),
DEFINE_MODULE_SYMBOL(app_uninstall),
// app/manager
DEFINE_MODULE_SYMBOL(app_manager_start),
DEFINE_MODULE_SYMBOL(app_manager_start_with_parameters),
DEFINE_MODULE_SYMBOL(app_manager_start_for_result),
DEFINE_MODULE_SYMBOL(app_manager_stop),
DEFINE_MODULE_SYMBOL(app_manager_finish),
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/metadata
DEFINE_MODULE_SYMBOL(app_metadata_parse),
// app/paths
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_directory),
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_path),
DEFINE_MODULE_SYMBOL(app_paths_get_assets_directory),
DEFINE_MODULE_SYMBOL(app_paths_get_assets_path),
// app/scheduler
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id),
// terminator
MODULE_SYMBOL_TERMINATOR
};
static error_t start() {
return service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
}
static error_t stop() {
return service_manager_remove(app_internal_loader_service_manifest.id);
}
Module app_module = {
.name = "app",
.start = start,
.stop = stop,
.drivers = nullptr,
.symbols = app_module_symbols,
.internal = nullptr
};
}
@@ -50,11 +50,10 @@ void lvgl_keyboard_enable(lv_indev_t* indev);
void lvgl_keyboard_disable(lv_indev_t* indev);
/**
* @brief Wires a textarea up to the on-screen keyboard: shows it on focus, hides it on
* defocus/ready, and adds the textarea to the keyboard's navigation group.
*
* No-op if lvgl_software_keyboard_is_enabled() is false (i.e. a hardware keyboard is present).
*
* @brief Adds the textarea to the shared keyboard navigation group (so any keypad indev -
* hardware or on-screen - can type into it once it's focused), and, only when
* lvgl_software_keyboard_is_enabled() is true (i.e. no hardware keyboard is present), wires it
* up to show/hide the on-screen keyboard on focus/defocus/ready.
* @warning Caller must hold the LVGL lock.
* @param[in] keyboard the on-screen keyboard to associate with the textarea
* @param[in] textarea the lv_textarea_t object to wire up
@@ -19,6 +19,10 @@ void lvgl_keyboard_on_start_lvgl() {
lvgl_lock();
keyboard_group = lv_group_create();
check(keyboard_group);
// We currently set this group as the default, so it doesn't only get (manually added) textareas,
// but gets all widgets by default. This is a temporary work-around until a proper default group is
// created to fix the trackball issue (see trackball.cpp and ideas.md, search for "group")
lv_group_set_default(keyboard_group);
lvgl_unlock();
}
@@ -77,6 +81,8 @@ error_t lvgl_keyboard_add(struct Device* device, lv_display_t* display, lv_indev
lv_indev_set_display(indev, display);
}
lvgl_keyboard_enable(indev);
*out_indev = indev;
return ERROR_NONE;
}
@@ -106,8 +112,11 @@ bool lvgl_hardware_keyboard_is_available() {
return false;
}
// TODO: Refactor the driver subsystem to so it does proper probing/releasing of such devices
// This work-around exists for the Tab5 keyboard driver.
bool present = keyboard_is_present(keyboard_device);
device_put(keyboard_device);
return true;
return present;
}
void lvgl_hardware_keyboard_add_custom(lv_indev_t* indev) {
@@ -137,9 +146,15 @@ static void textarea_show_keyboard(lv_event_t* event) {
}
static void textarea_hide_keyboard(lv_event_t* event) {
if (last_software_keyboard.object != nullptr) {
lvgl_software_keyboard_hide(&last_software_keyboard);
if (last_software_keyboard.object == nullptr) {
return;
}
// Only hide if the keyboard is actually bound to the textarea that triggered this
lv_obj_t* target = lv_event_get_current_target_obj(event);
if (lv_keyboard_get_textarea(last_software_keyboard.object) != target) {
return;
}
lvgl_software_keyboard_hide(&last_software_keyboard);
}
void lvgl_software_keyboard_construct(LvglSoftwareKeyboard* keyboard, lv_obj_t* parent) {
@@ -177,16 +192,23 @@ LvglSoftwareKeyboard* lvgl_software_keyboard_get_last() {
}
void lvgl_keyboard_add_textarea(LvglSoftwareKeyboard* keyboard, lv_obj_t* textarea) {
// Only the on-screen keyboard's show/hide wiring is specific to "no hardware keyboard"
// mode. Group membership must NOT be gated on it: a hardware keypad indev (see
// lvgl_keyboard_enable()/lvgl_software_keyboard_activate()) is bound to keyboard_group
// regardless of whether a software keyboard is in use, so skipping lv_group_add_obj()
// here left every textarea unreachable from a hardware keyboard - it was never a member
// of the group its indev delivers key events through.
if (lvgl_software_keyboard_is_enabled()) {
lv_obj_add_event_cb(textarea, textarea_show_keyboard, LV_EVENT_FOCUSED, nullptr);
lv_obj_add_event_cb(textarea, textarea_hide_keyboard, LV_EVENT_DEFOCUSED, nullptr);
lv_obj_add_event_cb(textarea, textarea_hide_keyboard, LV_EVENT_READY, nullptr);
// lv_obj_t auto-remove themselves from the group when they are destroyed (last checked in LVGL 8.3)
lv_group_add_obj(keyboard_group, textarea);
lvgl_software_keyboard_activate(keyboard);
lv_obj_add_event_cb(textarea, textarea_hide_keyboard, LV_EVENT_DELETE, nullptr);
}
// lv_obj_t auto-remove themselves from the group when they are destroyed (last checked in LVGL 8.3)
lv_group_add_obj(keyboard_group, textarea);
lvgl_software_keyboard_activate(keyboard);
}
void lvgl_software_keyboard_activate(LvglSoftwareKeyboard* keyboard) {
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
#include <lvgl/devices/trackball.h>
#include <lvgl/devices/device_context.h>
#include <lvgl/devices/keyboard.h>
#include <lvgl/lvgl.h>
#include <tactility/drivers/trackball.h>
@@ -154,6 +155,13 @@ error_t lvgl_trackball_add(struct Device* device, lv_display_t* display, lv_inde
}
recenter_cursor(ctx, indev);
// Encoder indevs are useless without a group (LVGL dispatch bails out immediately if indev->group is NULL)
// Use the keyboard group for now, until it's refactored into a proper global/shared group. See ideas.md
// When refactoring this, you probably want to remove the calls to lvgl_keyboard_* below.
if (ctx->settings.mode == LVGL_TRACKBALL_MODE_ENCODER) {
lvgl_keyboard_enable(indev);
}
*out_indev = indev;
return ERROR_NONE;
}
@@ -196,12 +204,14 @@ error_t lvgl_trackball_set_settings(lv_indev_t* indev, const struct LvglTrackbal
if (mode_changed) {
if (settings->mode == LVGL_TRACKBALL_MODE_POINTER) {
lvgl_keyboard_disable(indev);
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
recenter_cursor(ctx, indev);
show_cursor(ctx, indev);
} else {
hide_cursor(ctx);
lv_indev_set_type(indev, LV_INDEV_TYPE_ENCODER);
lvgl_keyboard_enable(indev);
}
}
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(lvgl-window-manager-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel lvgl-module app-module
)
@@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,2 @@
dependencies:
- TactilityKernel
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module lvgl_window_manager_module;
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <app/instance.h>
#include <lvgl.h>
#include <tactility/error.h>
#include <tactility/freertos/freertos.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef uint32_t WindowId;
enum WindowState {
/** id is the current topmost window and has live widgets. */
WINDOW_STATE_GRANTED,
/** id is not currently topmost - either buried under a newer window (its widgets don't
* exist right now, but it may resurface and get rebuilt if everything above it is removed)
* or it no longer exists at all (removed). */
WINDOW_STATE_REVOKED,
};
/**
* Called once by window_manager_start(), given the real root widget (a raw, full-size
* container created directly under the default display's active screen). May add extra chrome
* (e.g. a statusbar) as children of @a root_widget.
* @param[in] root_widget the real root widget; owned by this module, deleted automatically
* (along with everything added under it) by window_manager_stop()
* @return the widget windows should actually be placed into - @a root_widget itself, or a
* child of it. Returning NULL falls back to @a root_widget.
* @warning Called on the LVGL task with the LVGL lock already held.
* @warning Also called with window-manager's internal lifecycle_mutex held (non-recursive) -
* do NOT call window_manager_start()/window_manager_stop()/window_manager_create()/
* window_manager_remove() or any other window-manager API from this callback, that would
* deadlock.
*/
typedef lv_obj_t* (*WindowManagerScreenInitFn)(lv_obj_t* root_widget);
/**
* Configures the screen-init callback window_manager_start() invokes to build the root/content
* widgets. Pass NULL to restore the default (no chrome - the raw root widget is used directly).
* @warning Must be called before window_manager_start(); has no effect once already started.
*/
void window_manager_configure(WindowManagerScreenInitFn screen_init);
/**
* Creates the root widget (under the default display's active screen) and, via the configured
* screen-init callback, whatever chrome/content widget it wants around it. Idempotent - a
* second call while already started is a no-op.
* @retval ERROR_RESOURCE no default display is active (lv_screen_active() returned NULL)
* @retval ERROR_NONE on success (including if already started)
*/
error_t window_manager_start(void);
/**
* Deletes the root widget created by window_manager_start() (and everything under it - any
* chrome plus whatever the topmost window had drawn), removing it from the display, and drops
* every tracked window. Idempotent - a second call while already stopped is a no-op.
*/
error_t window_manager_stop(void);
/**
* Called to populate a window's widgets: once by window_manager_create() when the window is
* first created, and again later by window_manager_remove() if this window resurfaces as the
* new topmost after whatever was above it is removed. Only the current topmost window ever has
* live widgets - everything below it in the stack exists as tracked state only.
* @param[in] root a fresh, full-size container created directly under the content widget for
* this window; deleted automatically once this window stops being topmost
* @param[in] user_data whatever was passed to window_manager_create() for this window
* @warning Called on the LVGL task with the LVGL lock already held.
* @warning May run on a different kernel thread than the one that called window_manager_create()
* for this window - the rebuild-on-remove path runs on whichever thread called
* window_manager_remove() for the window that used to be on top (e.g. a dialog's own thread as
* it closes). Do NOT rely on thread_local state set by this window's own app thread; use
* @a user_data instead.
* @warning Also called with window-manager's internal lifecycle_mutex held (non-recursive) -
* do NOT call window_manager_start()/window_manager_stop()/window_manager_create()/
* window_manager_remove() or any other window-manager API from this callback, that would
* deadlock.
*/
typedef void (*WindowCreateWidgetsFn)(lv_obj_t* root, void* user_data);
/**
* Creates a new window on top of the stack (last created = topmost). Deletes the previously
* topmost window's widgets (if any) and builds this window's widgets immediately via
* @a create_widgets - only the topmost window ever has live widgets.
* @param[in] app_instance_id the application instance this window belongs to, should not be 0
* @param[in] user_data opaque; passed back to @a create_widgets on every call, including a
* later rebuild triggered by window_manager_remove() - see its @warning about which thread that
* can run on. Typically the calling app's own Context*.
* @return the new window's id, or 0 if window_manager_start() hasn't been called
*/
WindowId window_manager_create(AppInstanceId app_instance_id, WindowCreateWidgetsFn create_widgets, void* user_data);
/**
* Removes a window, wherever it is in the stack - not necessarily the topmost one. If it was
* topmost, its widgets are deleted and whichever window is now on top (if any) has its
* create_widgets called again to rebuild its widgets.
*/
void window_manager_remove(WindowId id);
/** @return the current state of @a id; WINDOW_STATE_REVOKED if @a id is buried or doesn't exist. */
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);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: Apache-2.0
#include <lvgl_window_manager/module.h>
#include <lvgl_window_manager/window_manager.h>
#include <tactility/error.h>
#include <tactility/module.h>
extern "C" {
const ModuleSymbol lvgl_window_manager_module_symbols[] = {
DEFINE_MODULE_SYMBOL(window_manager_create),
DEFINE_MODULE_SYMBOL(window_manager_remove),
DEFINE_MODULE_SYMBOL(window_manager_get_state),
DEFINE_MODULE_SYMBOL(window_manager_await_state_change),
// terminator
MODULE_SYMBOL_TERMINATOR
};
Module lvgl_window_manager_module = {
.name = "lvgl-window-manager",
.start = window_manager_start,
.stop = window_manager_stop,
.drivers = nullptr,
.symbols = lvgl_window_manager_module_symbols,
.internal = nullptr
};
}
@@ -0,0 +1,483 @@
// SPDX-License-Identifier: Apache-2.0
#include <lvgl_window_manager/window_manager.h>
#include <app/instance.h>
#include <lvgl/lvgl.h>
#include <tactility/check.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/freertos/semphr.h>
#include <algorithm>
#include <new>
#include <vector>
constexpr auto* TAG = "window_manager";
namespace {
/**
* Completion signal for a single window_manager_await_state_change() call.
*
* Heap-allocated with its own refcount, protected by WindowManagerState::mutex (not atomic).
* It can't be owned solely by the WindowRecord: window_manager_create()/remove() claim
* (read + clear) a window's signal under the lock, then give it after releasing that lock.
* The refcount lets whichever side finishes last - the waiting task waking up, or the
* claimer after giving the semaphore - safely delete it.
*/
struct WindowWaitSignal {
SemaphoreHandle_t semaphore;
/** Starts at 1, owned by window_manager_await_state_change() until it's done waiting.
* Whoever claims this signal from a WindowRecord (see claim_waiter_locked()) takes an
* extra reference for as long as it takes to give the semaphore. Reaching 0 deletes it. */
int refcount = 1;
};
struct WindowRecord {
WindowId id;
uint32_t app_instance_id;
WindowCreateWidgetsFn create_widgets;
void* user_data;
/** Set by window_manager_await_state_change() when a task is blocked waiting on this
* window (see that function's @warning: at most one concurrent awaiter per window).
* Per-window rather than a single manager-wide slot, because a stacked window manager
* serving several app tasks can have more than one window with a live await() call
* outstanding, even though only one is ever topmost/GRANTED at a time. */
WindowWaitSignal* waiting_signal = nullptr;
};
struct WindowManagerState {
/** Mutex for read/write operations. Shortly held. */
Mutex mutex {};
/** Serializes the full start()/stop()/create()/remove() transitions against each other,
* including LVGL work done after `mutex` is released, such as a create_widgets() or
* screen_init() callback. Without it, window_manager_stop() could free
* real_root_widget/content_root_widget/top_widget out from under a concurrent create() or
* remove() that captured one of those pointers under `mutex` but only uses it afterward,
* via build_window_widget()/delete_widget(). */
Mutex lifecycle_mutex {};
bool started = false;
WindowManagerScreenInitFn screen_init = nullptr;
/** The raw, full-size container window_manager_start() creates; owns (and deletion
* cascades to) whatever the screen-init callback added under it. */
lv_obj_t* real_root_widget = nullptr;
/** The stable parent each window's own widget is created under. Normally
* real_root_widget itself, but the screen-init callback may return a nested content
* widget to use instead. */
lv_obj_t* content_root_widget = nullptr;
WindowId next_id = 1;
/** windows.back() is topmost; only it ever has a live widget (top_widget). */
std::vector<WindowRecord> windows;
lv_obj_t* top_widget = nullptr;
WindowManagerState() {
mutex_construct(&mutex);
mutex_construct(&lifecycle_mutex);
}
};
WindowManagerState& state() {
static WindowManagerState instance;
return instance;
}
lv_obj_t* build_window_widget(lv_obj_t* content, WindowCreateWidgetsFn create_widgets, void* user_data) {
if (content == nullptr) {
return nullptr;
}
lvgl_lock();
lv_obj_t* widget = lv_obj_create(content);
lv_obj_set_size(widget, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_pad_all(widget, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(widget, 0, LV_STATE_DEFAULT);
lv_obj_set_style_radius(widget, 0, LV_STATE_DEFAULT);
// Plain layout container, not meant to scroll on its own - every app already does this
// for its own root object. Without it, a sub-pixel flex-layout overflow here can show the
// theme's scrollbar styling as a thin line hugging this widget's edges.
lv_obj_remove_flag(widget, LV_OBJ_FLAG_SCROLLABLE);
if (create_widgets != nullptr) {
create_widgets(widget, user_data);
}
lvgl_unlock();
return widget;
}
void delete_widget(lv_obj_t* widget) {
if (widget == nullptr) {
return;
}
lvgl_lock();
lv_obj_delete(widget);
lvgl_unlock();
}
// Call while holding WindowManagerState::mutex. Transfers ownership of `window`'s waiting
// signal, if any, to the caller, taking an extra reference on the caller's behalf. The
// caller must pass the result to give_and_release() exactly once, outside the lock.
WindowWaitSignal* claim_waiter_locked(WindowRecord& window) {
WindowWaitSignal* signal = window.waiting_signal;
window.waiting_signal = nullptr;
if (signal != nullptr) {
signal->refcount++;
}
return signal;
}
// Gives `signal`'s semaphore, waking window_manager_await_state_change() if it's still
// waiting, then releases the caller's reference from claim_waiter_locked(). Deletes the
// signal if that was the last reference. No-op if `signal` is NULL.
void give_and_release(WindowWaitSignal* signal) {
if (signal == nullptr) {
return;
}
xSemaphoreGive(signal->semaphore);
auto& s = state();
mutex_lock(&s.mutex);
bool should_delete = (--signal->refcount == 0);
mutex_unlock(&s.mutex);
if (should_delete) {
vSemaphoreDelete(signal->semaphore);
delete signal;
}
}
} // namespace
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);
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) {
auto& s = state();
// Held for the whole transition, including the LVGL work below done with `mutex`
// released. Blocks a concurrent start() from also passing the `started` check and
// building its own root widget, and blocks a concurrent stop() from running while this
// start is still mid-flight.
mutex_lock(&s.lifecycle_mutex);
mutex_lock(&s.mutex);
if (s.started) {
mutex_unlock(&s.mutex);
mutex_unlock(&s.lifecycle_mutex);
return ERROR_NONE;
}
WindowManagerScreenInitFn screen_init = s.screen_init;
mutex_unlock(&s.mutex);
lv_obj_t* real_widget = nullptr;
lv_obj_t* content_widget = nullptr;
lvgl_lock();
lv_obj_t* screen = lv_screen_active();
if (screen != nullptr) {
real_widget = lv_obj_create(screen);
lv_obj_set_size(real_widget, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_pad_all(real_widget, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(real_widget, 0, LV_STATE_DEFAULT);
lv_obj_set_style_radius(real_widget, 0, LV_STATE_DEFAULT);
// See build_window_widget()'s identical flag removal for why.
lv_obj_remove_flag(real_widget, LV_OBJ_FLAG_SCROLLABLE);
content_widget = (screen_init != nullptr) ? screen_init(real_widget) : nullptr;
if (content_widget == nullptr) {
content_widget = real_widget;
}
}
lvgl_unlock();
if (real_widget == nullptr) {
mutex_unlock(&s.lifecycle_mutex);
return ERROR_RESOURCE;
}
// A previous stop() may have left window records behind for an app that's still running
// (see window_manager_stop()'s comment). Rebuild the topmost one now, the same way
// window_manager_remove() rebuilds when a buried window resurfaces. Otherwise that app's
// task stays blocked in its own event loop forever, with no window and no signal telling
// it to rebuild one.
WindowCreateWidgetsFn top_create_widgets = nullptr;
void* top_user_data = nullptr;
WindowId top_id = 0;
bool has_top = false;
mutex_lock(&s.mutex);
s.real_root_widget = real_widget;
s.content_root_widget = content_widget;
s.started = true;
if (!s.windows.empty()) {
top_create_widgets = s.windows.back().create_widgets;
top_user_data = s.windows.back().user_data;
top_id = s.windows.back().id;
has_top = true;
}
mutex_unlock(&s.mutex);
if (has_top) {
lv_obj_t* new_widget = build_window_widget(content_widget, top_create_widgets, top_user_data);
mutex_lock(&s.mutex);
bool still_topmost = !s.windows.empty() && s.windows.back().id == top_id;
if (still_topmost) {
s.top_widget = new_widget;
new_widget = nullptr; // consumed
}
mutex_unlock(&s.mutex);
// The window stack changed while we were building, e.g. a concurrent remove() -
// discard what we just made.
delete_widget(new_widget);
}
mutex_unlock(&s.lifecycle_mutex);
return ERROR_NONE;
}
error_t window_manager_stop(void) {
auto& s = state();
// See window_manager_start(): blocks until any in-flight start() has finished, or failed,
// before this stop observes or tears down state.
mutex_lock(&s.lifecycle_mutex);
mutex_lock(&s.mutex);
if (!s.started) {
mutex_unlock(&s.mutex);
mutex_unlock(&s.lifecycle_mutex);
return ERROR_NONE;
}
lv_obj_t* widget = s.real_root_widget;
// Claim every window's waiter before tearing down. Normally only the topmost window has
// one set, but every window's widget is torn down here, so every one is checked.
std::vector<WindowWaitSignal*> waiters;
for (auto& window : s.windows) {
if (auto* signal = claim_waiter_locked(window); signal != nullptr) {
waiters.push_back(signal);
}
}
s.real_root_widget = nullptr;
s.content_root_widget = nullptr;
s.top_widget = nullptr;
// Deliberately not s.windows.clear(): this tears down only the LVGL widget tree, not the
// window records. On a real full shutdown every app has already removed its own window via
// window_manager_remove(), so the list is empty anyway and this is a no-op. But a caller can
// also stop()/start() this module on its own, temporarily, while apps keep running
// underneath - for example one borrowing the display/touch hardware directly. Those apps'
// tasks stay alive, blocked in their own event loops, with no way to know they need to call
// window_manager_create() again. Keeping the records lets window_manager_start() rebuild the
// topmost one automatically instead of leaving that app stuck with no window forever.
s.started = false;
mutex_unlock(&s.mutex);
for (WindowWaitSignal* waiter : waiters) {
give_and_release(waiter);
}
// Deleting the real widget cascades to everything under it - chrome and top_widget alike.
delete_widget(widget);
mutex_unlock(&s.lifecycle_mutex);
return ERROR_NONE;
}
WindowId window_manager_create(AppInstanceId app_instance_id, WindowCreateWidgetsFn create_widgets, void* user_data) {
if (app_instance_id == 0) {
return 0;
}
auto& s = state();
// See lifecycle_mutex's comment: blocks a concurrent window_manager_stop() (or another
// create()/remove()) from touching real_root_widget/content_root_widget/top_widget while
// this call still holds pointers to them.
mutex_lock(&s.lifecycle_mutex);
mutex_lock(&s.mutex);
if (!s.started) {
mutex_unlock(&s.mutex);
mutex_unlock(&s.lifecycle_mutex);
return 0;
}
lv_obj_t* content = s.content_root_widget;
lv_obj_t* old_top_widget = s.top_widget;
// The current topmost window, if any, is about to be superseded - claim its waiter here
// so it gets notified below.
WindowWaitSignal* waiter = !s.windows.empty() ? claim_waiter_locked(s.windows.back()) : 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 });
mutex_unlock(&s.mutex);
give_and_release(waiter);
delete_widget(old_top_widget);
lv_obj_t* new_widget = build_window_widget(content, create_widgets, user_data);
mutex_lock(&s.mutex);
bool still_topmost = !s.windows.empty() && s.windows.back().id == new_id;
if (still_topmost) {
s.top_widget = new_widget;
new_widget = nullptr; // consumed
}
mutex_unlock(&s.mutex);
// Another window became topmost while we were building, e.g. a concurrent create() from
// another app thread - discard what we just made.
delete_widget(new_widget);
mutex_unlock(&s.lifecycle_mutex);
return new_id;
}
void window_manager_remove(WindowId id) {
auto& s = state();
// See lifecycle_mutex's comment: blocks a concurrent window_manager_stop() (or another
// create()/remove()) from touching real_root_widget/content_root_widget/top_widget while
// this call still holds pointers to them.
mutex_lock(&s.lifecycle_mutex);
mutex_lock(&s.mutex);
auto iterator = std::find_if(s.windows.begin(), s.windows.end(),
[id](const WindowRecord& window) { return window.id == id; });
if (iterator == s.windows.end()) {
mutex_unlock(&s.mutex);
mutex_unlock(&s.lifecycle_mutex);
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()); if this window had
// since stopped being topmost without being removed, window_manager_create() would already
// have claimed and cleared it. So a buried window's waiting_signal is always already null.
WindowWaitSignal* waiter = claim_waiter_locked(*iterator);
s.windows.erase(iterator);
lv_obj_t* content = s.content_root_widget;
lv_obj_t* old_widget = nullptr;
WindowCreateWidgetsFn next_create_widgets = nullptr;
void* next_user_data = nullptr;
WindowId next_id = 0;
bool has_next = false;
if (was_topmost) {
old_widget = s.top_widget;
s.top_widget = nullptr;
if (!s.windows.empty()) {
next_create_widgets = s.windows.back().create_widgets;
next_user_data = s.windows.back().user_data;
next_id = s.windows.back().id;
has_next = true;
}
}
mutex_unlock(&s.mutex);
give_and_release(waiter);
if (!was_topmost) {
// A buried window was removed; the topmost window's widgets are unaffected.
mutex_unlock(&s.lifecycle_mutex);
return;
}
delete_widget(old_widget);
lv_obj_t* new_widget = has_next ? build_window_widget(content, next_create_widgets, next_user_data) : nullptr;
mutex_lock(&s.mutex);
bool still_topmost = has_next && !s.windows.empty() && s.windows.back().id == next_id;
if (still_topmost) {
s.top_widget = new_widget;
new_widget = nullptr; // consumed
}
mutex_unlock(&s.mutex);
delete_widget(new_widget);
mutex_unlock(&s.lifecycle_mutex);
}
WindowState window_manager_get_state(WindowId id) {
auto& s = state();
mutex_lock(&s.mutex);
bool is_top = !s.windows.empty() && s.windows.back().id == id;
mutex_unlock(&s.mutex);
return is_top ? WINDOW_STATE_GRANTED : WINDOW_STATE_REVOKED;
}
WindowState window_manager_await_state_change(WindowId id, TickType_t timeout) {
auto& s = state();
// Uses a dedicated semaphore rather than this task's default FreeRTOS notification.
// Other subsystems, e.g. app_event.cpp's AppEventSubscription, share that same slot - an
// unrelated notification delivered to this task could otherwise wake this wait early.
auto* signal = new (std::nothrow) WindowWaitSignal();
if (signal == nullptr) {
return window_manager_get_state(id);
}
signal->semaphore = xSemaphoreCreateBinary();
if (signal->semaphore == nullptr) {
delete signal;
return window_manager_get_state(id);
}
mutex_lock(&s.mutex);
bool is_top = !s.windows.empty() && s.windows.back().id == id;
if (!is_top) {
mutex_unlock(&s.mutex);
vSemaphoreDelete(signal->semaphore);
delete signal;
return WINDOW_STATE_REVOKED;
}
// At most one concurrent awaiter per window; see this function's @warning.
check(s.windows.back().waiting_signal == nullptr);
s.windows.back().waiting_signal = signal;
mutex_unlock(&s.mutex);
xSemaphoreTake(signal->semaphore, timeout);
// Deregister ourselves if a create()/remove() hasn't already claimed us. This is the
// ordinary, intended wakeup path; without it, a later create()/remove() could read a
// signal that's already been given away here. Re-locate the record by id, since it may
// have been erased by window_manager_remove() while we waited. Either way, release our
// own reference - whichever side finishes last, us or a claimer, is the one that deletes
// it.
mutex_lock(&s.mutex);
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_signal == signal) {
iterator->waiting_signal = nullptr;
}
bool should_delete = (--signal->refcount == 0);
mutex_unlock(&s.mutex);
if (should_delete) {
vSemaphoreDelete(signal->semaphore);
delete signal;
}
return window_manager_get_state(id);
}
} // extern "C"
@@ -16,6 +16,7 @@
#include <tactility/drivers/wifi.h>
#include <tactility/error_esp32.h>
#include <tactility/log.h>
#include <tactility/time.h>
#if defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
#include <tactility/drivers/esp32_esp_hosted_ota.h>
@@ -56,6 +57,15 @@ struct Esp32WifiCtx {
esp_event_handler_instance_t wifiEventHandler = nullptr;
esp_event_handler_instance_t ipEventHandler = nullptr;
// Dedup for WIFI_EVENT/IP_EVENT notifications: on the esp_hosted/Wi-Fi Remote transport
// (e.g. Tab5's P4 host + C6 co-processor), the RPC layer has been observed delivering the
// exact same event twice in a row (same base, same event_id, same millisecond - not two
// genuinely separate occurrences). Native WiFi doesn't exhibit this, but the handler is
// shared, so the guard applies unconditionally; it's a no-op for well-separated real events.
esp_event_base_t lastEventBase = nullptr;
int32_t lastEventId = -1;
TickType_t lastEventTick = 0;
Mutex callbackMutex{};
WifiCallbackEntry callbacks[WIFI_MAX_CALLBACKS] = {};
size_t callbackCount = 0;
@@ -100,6 +110,20 @@ void fire_event(Esp32WifiCtx* ctx, WifiEvent event) {
void on_wifi_or_ip_event(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
auto* ctx = static_cast<Esp32WifiCtx*>(arg);
// See Esp32WifiCtx::lastEventBase/lastEventId/lastEventTick - collapse an immediate duplicate
// delivery of the same event (observed on the esp_hosted/Wi-Fi Remote transport) into one.
constexpr uint32_t DEDUP_WINDOW_MS = 50; // well under any real re-occurrence of the same event
TickType_t now = get_ticks();
bool is_duplicate = event_base == ctx->lastEventBase && event_id == ctx->lastEventId &&
(now - ctx->lastEventTick) <= millis_to_ticks(DEDUP_WINDOW_MS);
ctx->lastEventBase = event_base;
ctx->lastEventId = event_id;
ctx->lastEventTick = now;
if (is_duplicate) {
LOG_D(TAG, "Ignoring duplicate WiFi event %d", (int)event_id);
return;
}
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
mutex_lock(&ctx->mutex);
bool was_pending = ctx->stationState == WIFI_STATION_STATE_CONNECTION_PENDING;
+3
View File
@@ -8,6 +8,8 @@ list(APPEND REQUIRES_LIST
TactilityKernel
TactilityFreeRtos
lvgl-module
lvgl-window-manager-module
app-module
crypt-module
gps-module
gps-generic-module
@@ -20,6 +22,7 @@ list(APPEND REQUIRES_LIST
if (DEFINED ENV{ESP_IDF_VERSION})
list(APPEND REQUIRES_LIST
app-esp32-module
platform-esp32
driver
elf_loader
-68
View File
@@ -1,68 +0,0 @@
/**
* @brief key-value storage for general purpose.
* Maps strings on a fixed set of data types.
*/
#pragma once
#include <cstdint>
#include <string>
#include <unordered_map>
namespace tt {
/**
* A dictionary that maps keys (strings) onto several atomary types.
*/
class Bundle final {
typedef uint32_t Hash;
enum class Type {
Bool,
Int32,
Int64,
String,
};
typedef struct {
Type type;
union {
bool value_bool;
int32_t value_int32;
int64_t value_int64;
};
std::string value_string;
} Value;
std::unordered_map<std::string, Value> entries;
public:
Bundle() = default;
Bundle(const Bundle& bundle) {
this->entries = bundle.entries;
}
bool getBool(const std::string& key) const;
int32_t getInt32(const std::string& key) const;
int64_t getInt64(const std::string& key) const;
std::string getString(const std::string& key) const;
bool hasBool(const std::string& key) const;
bool hasInt32(const std::string& key) const;
bool hasInt64(const std::string& key) const;
bool hasString(const std::string& key) const;
bool optBool(const std::string& key, bool& out) const;
bool optInt32(const std::string& key, int32_t& out) const;
bool optInt64(const std::string& key, int64_t& out) const;
bool optString(const std::string& key, std::string& out) const;
void putBool(const std::string& key, bool value);
void putInt32(const std::string& key, int32_t value);
void putInt64(const std::string& key, int64_t value);
void putString(const std::string& key, const std::string& value);
};
} // namespace
@@ -1,10 +1,12 @@
/**
* DEPRECATED: Use TactilityKernels' tactility/paths.h
*/
#pragma once
#include <string>
#include <tactility/filesystem/file_system.h>
namespace tt {
bool findFirstMountedSdCardPath(std::string& path);
-41
View File
@@ -1,41 +0,0 @@
#pragma once
#include <cstdint>
#include <string>
namespace tt {
/**
* Settings that persist on NVS flash for ESP32.
* On simulator, the settings are only in-memory.
*
* 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)
*/
class Preferences {
const char* namespace_;
public:
explicit Preferences(const char* namespace_) {
this->namespace_ = namespace_;
}
bool hasBool(const std::string& key) const;
bool hasInt32(const std::string& key) const;
bool hasInt64(const std::string& key) const;
bool hasString(const std::string& key) const;
bool optBool(const std::string& key, bool& out) const;
bool optInt32(const std::string& key, int32_t& out) const;
bool optInt64(const std::string& key, int64_t& out) const;
bool optString(const std::string& key, std::string& out) const;
void putBool(const std::string& key, bool value);
void putInt32(const std::string& key, int32_t value);
void putInt64(const std::string& key, int64_t value);
void putString(const std::string& key, const std::string& value);
};
} // namespace
-1
View File
@@ -3,7 +3,6 @@
#include <tactility/concurrent/dispatcher.h>
#include <tactility/device.h>
#include <tactility/module.h>
#include <Tactility/app/AppManifest.h>
#include <functional>
-116
View File
@@ -1,116 +0,0 @@
#pragma once
#include "Tactility/app/AppContext.h"
#include <Tactility/Bundle.h>
#include <Tactility/Mutex.h>
#include <string>
// Forward declarations
typedef struct _lv_obj_t lv_obj_t;
namespace tt::app {
// Forward declarations
class AppContext;
enum class Result;
typedef unsigned int LaunchId;
class App {
Mutex mutex;
struct ResultHolder {
Result result;
std::unique_ptr<Bundle> resultData;
explicit ResultHolder(Result result) : result(result), resultData(nullptr) {}
ResultHolder(Result result, std::unique_ptr<Bundle> resultData) :
result(result),
resultData(std::move(resultData)) {}
};
std::unique_ptr<ResultHolder> resultHolder;
public:
App() = default;
virtual ~App() = default;
virtual void onCreate(AppContext& appContext) {}
virtual void onDestroy(AppContext& appContext) {}
virtual void onShow(AppContext& appContext, lv_obj_t* parent) {}
virtual void onHide(AppContext& appContext) {}
/** resultData could be null */
virtual void onResult(AppContext& appContext, LaunchId launchId, Result result, std::unique_ptr<Bundle> resultData) {}
Mutex& getMutex() { return mutex; }
bool hasResult() const { return resultHolder != nullptr; }
void setResult(Result result, std::unique_ptr<Bundle> resultData = nullptr) {
auto lock = getMutex().asScopedLock();
lock.lock();
resultHolder = std::make_unique<ResultHolder>(result, std::move(resultData));
}
/**
* Used by system to extract the result data when this application is finished.
* Note that this removes the data from the class!
*/
bool moveResult(Result& outResult, std::unique_ptr<Bundle>& outBundle) {
auto lock = getMutex().asScopedLock();
lock.lock();
if (resultHolder == nullptr) {
return false;
}
outResult = resultHolder->result;
outBundle = std::move(resultHolder->resultData);
resultHolder = nullptr;
return true;
}
};
template<typename T>
std::shared_ptr<App> create() { return std::shared_ptr<T>(new T); }
/**
* @brief Start an app
* @param[in] id application name or id
* @param[in] parameters optional parameters to pass onto the application. can be nullptr.
*/
LaunchId start(const std::string& id, std::shared_ptr<const Bundle> parameters = nullptr);
/** @brief Stop the currently showing app. Show the previous app if any app was still running. */
void stop();
/** @brief Stop a specific app and any apps it might have launched on the stack.
* @param[in] id the app id
*/
void stop(const std::string& id);
/** @brief Stop all app instances that match with this identifier and also stop the apps they started.
* @warning onResult() will only be called for the resulting app that gets shown (if any)
* @param[in] id the id of the app to stop
*/
void stopAll(const std::string& id);
/** @return true if the app is running somewhere in the app stack (doesn't have to be the top-most app) */
bool isRunning(const std::string& id);
/** @return the currently running app context (it is only ever null before the splash screen is shown) */
std::shared_ptr<AppContext> getCurrentAppContext();
/** @return the currently running app (it is only ever null before the splash screen is shown) */
std::shared_ptr<App> getCurrentApp();
bool install(const std::string& path);
bool uninstall(const std::string& appId);
}
@@ -1,41 +0,0 @@
#pragma once
#include <Tactility/Bundle.h>
#include <memory>
namespace tt::app {
// Forward declarations
class App;
class AppPaths;
struct AppManifest;
enum class Result;
typedef union {
struct {
bool hideStatusbar : 1;
};
unsigned char flags;
} Flags;
/**
* The public representation of an application instance.
* @warning Do not store references or pointers to these! You can retrieve them via the service registry.
*/
class AppContext {
protected:
virtual ~AppContext() = default;
public:
virtual const AppManifest& getManifest() const = 0;
virtual std::shared_ptr<const Bundle> getParameters() const = 0;
virtual std::unique_ptr<AppPaths> getPaths() const = 0;
virtual std::shared_ptr<App> getApp() const = 0;
};
}
@@ -1,104 +0,0 @@
#pragma once
#include <Tactility/app/AppRegistration.h>
#include <string>
namespace tt::app {
class App;
class AppContext;
/** Application types */
enum class Category {
/** Standard apps, provided by the system. */
System,
/** The apps that are launched/shown by the Settings app. The Settings app itself is of type AppTypeSystem. */
Settings,
/** User-provided apps. */
User
};
/** Result status code for application result callback. */
enum class Result {
Ok = 0U,
Cancelled = 1U,
Error = 2U
};
class Location {
std::string path;
Location() = default;
explicit Location(const std::string& path) : path(path) {}
public:
static Location internal() { return {}; }
static Location external(const std::string& path) {
return Location(path);
}
/** Internal apps are all apps that are part of the firmware release. */
bool isInternal() const { return path.empty(); }
/**
* External apps are all apps that are not part of the firmware release.
* e.g. an application on the sd card or one that is installed in /data
*/
bool isExternal() const { return !path.empty(); }
const std::string& getPath() const { return path; }
};
typedef std::shared_ptr<App>(*CreateApp)();
struct AppManifest {
struct Flags {
constexpr static uint32_t None = 0;
/** Don't show the statusbar */
constexpr static uint32_t HideStatusBar = 1 << 0;
/** Hint to other systems to not show this app (e.g. in launcher or settings) */
constexpr static uint32_t Hidden = 1 << 1;
};
/** The SDK version that was used to compile this app. (e.g. "0.6.0") */
std::string targetSdk = {};
/** Comma-separated list of platforms, e.g. "esp32,esp32s3" */
std::string targetPlatforms = {};
/** The identifier by which the app is launched by the system and other apps. */
std::string appId = {};
/** The user-readable name of the app. Used in UI. */
std::string appName = {};
/** Optional icon. */
std::string appIcon = {};
/** The version as it is displayed to the user (e.g. "1.2.0") */
std::string appVersionName = {};
/** The technical version (must be incremented with new releases of the app */
uint64_t appVersionCode = 0;
/** App category helps with listing apps in Launcher, app list or settings apps. */
Category appCategory = Category::User;
/** Where the app is located */
Location appLocation = Location::internal();
/** Controls various settings */
uint16_t appFlags = Flags::None;
/** Create the instance of the app */
CreateApp createApp = nullptr;
};
struct {
bool operator()(const std::shared_ptr<AppManifest>& left, const std::shared_ptr<AppManifest>& right) const { return left->appName < right->appName; }
} SortAppManifestByName;
} // namespace
@@ -1,47 +0,0 @@
#pragma once
#include <string>
#include <memory>
namespace tt::app {
// Forward declarations
class AppManifest;
class AppPaths {
const AppManifest& manifest;
public:
explicit AppPaths(const AppManifest& manifest) : manifest(manifest) {}
/**
* The user data directory is intended to survive OS upgrades.
* The path will not end with a "/".
*/
std::string getUserDataPath() const;
/**
* The user data directory is intended to survive OS upgrades.
* Configuration data should be stored here.
* @param[in] childPath the path without a "/" prefix
*/
std::string getUserDataPath(const std::string& childPath) const;
/**
* You should not store configuration data here.
* The path will not end with a "/".
* This is mainly used for core apps (system/boot/settings type).
*/
std::string getAssetsPath() const;
/**
* You should not store configuration data here.
* This is mainly used for core apps (system/boot/settings type).
* @param[in] childPath the path without a "/" prefix
*/
std::string getAssetsPath(const std::string& childPath) const;
};
}
@@ -1,26 +0,0 @@
#pragma once
#include "App.h"
#include <string>
#include <vector>
namespace tt::app {
struct AppManifest;
/** Register an application with its manifest */
void addAppManifest(const AppManifest& manifest);
/** Remove an app from the registry */
bool removeAppManifest(const std::string& id);
/** Find an application manifest by its id
* @param[in] id the manifest id
* @return the application manifest if it was found
*/
std::shared_ptr<AppManifest> findAppManifestById(const std::string& id);
/** @return a list of all registered apps. This includes user and system apps. */
std::vector<std::shared_ptr<AppManifest>> getAppManifests();
} // namespace
-36
View File
@@ -1,36 +0,0 @@
#pragma once
#include "AppManifest.h"
#ifdef ESP_PLATFORM
namespace tt::app {
typedef void* (*CreateData)();
typedef void (*DestroyData)(void* data);
/** data is nullable */
typedef void (*OnCreate)(void* appContext, void* data);
/** data is nullable */
typedef void (*OnDestroy)(void* appContext, void* data);
/** data is nullable */
typedef void (*OnShow)(void* appContext, void* data, lv_obj_t* parent);
/** data is nullable */
typedef void (*OnHide)(void* appContext, void* data);
/** data is nullable, resultData is nullable. */
typedef void (*OnResult)(void* appContext, void* data, LaunchId launchId, Result result, Bundle* resultData);
/** All fields are nullable */
void setElfAppParameters(
CreateData createData,
DestroyData destroyData,
OnCreate onCreate,
OnDestroy onDestroy,
OnShow onShow,
OnHide onHide,
OnResult onResult
);
std::shared_ptr<App> createElfApp(const std::shared_ptr<AppManifest>& manifest);
}
#endif // ESP_PLATFORM
@@ -1,49 +1,27 @@
#pragma once
#include <Tactility/Bundle.h>
#include <cstdint>
#include <string>
#include <vector>
#include <Tactility/app/App.h>
/**
* Start the app by its ID and provide:
* - a title
* - a text
* - 0, 1 or more buttons
* Show a dialog with a title, a message and 0, 1 or more buttons.
*/
namespace tt::app::alertdialog {
/**
* Show a dialog with the provided title, message and 0, 1 or more buttons.
* @param[in] title the title to show in the toolbar
* @param[in] message the message to display
* @param[in] buttonLabels the buttons to show
* @return the launch id
* Show a dialog with the provided title, message and buttons, as a modal child of
* @a callerAppInstanceId (a new-model app - see app/manager.h). The caller receives the
* result as an APP_EVENT_RESULT in its own event loop: result is the pressed button's index
* (>= 0), or a value not matching any button (currently always 1) if the dialog was dismissed
* without a button press. No result_bundle. The caller is responsible for calling
* app_manager_stop() on the returned instance id once it has handled the result.
* @return the new dialog's app instance id
*/
LaunchId start(const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels);
/**
* Show a dialog with the provided title, message and 0, 1 or more buttons.
* @param[in] title the title to show in the toolbar
* @param[in] message the message to display
* @param[in] buttonLabels the buttons to show
* @return the launch id
*/
LaunchId start(const std::string& title, const std::string& message, const std::vector<const char*>& buttonLabels);
uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::vector<std::string>& buttonLabels);
/**
* Show a dialog with the provided title, message and an OK button
* @param[in] title the title to show in the toolbar
* @param[in] message the message to display
* @return the launch id
*/
LaunchId start(const std::string& title, const std::string& message);
/**
* Get the index of the button that the user selected.
*
* @return a value greater than 0 when a selection was done, or -1 when the app was closed clicking one of the selection buttons.
*/
int32_t getResultIndex(const Bundle& bundle);
/** @copydoc start(uint32_t, const std::string&, const std::string&, const std::vector<std::string>&)
* Shows a single "OK" button. */
uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message);
}
@@ -1,9 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
#include <cstdint>
namespace tt::app::btmanage {
LaunchId start();
uint32_t start();
} // namespace tt::app::btmanage
@@ -1,28 +1,29 @@
#pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h>
#include <cstdint>
#include <string>
namespace tt::app::fileselection {
/**
* Show a file selection dialog that allows the user to select an existing file.
* This app returns the absolute file path as a result.
* Show a file selection dialog that allows the user to select an existing file, as a modal
* child of @a callerAppInstanceId (see app_manager_start_for_result()). Result (0 = Ok,
* 1 = Cancelled) is delivered back via APP_EVENT_RESULT once this app's thread exits - call
* getLastPath() right after receiving it, on result == 0. The caller must call
* app_manager_stop() on the returned instance id once that event arrives, to fully reap this
* instance.
* @return the new app instance id
*/
LaunchId startForExistingFile();
uint32_t startForExistingFile(uint32_t callerAppInstanceId);
/**
* Show a file selection dialog that allows the user to select a new or existing file.
* This app returns the absolute file path as a result.
* Same as startForExistingFile(), but also allows picking a path that doesn't exist yet (for
* "save as"-style flows).
*/
LaunchId startForExistingOrNewFile();
uint32_t startForExistingOrNewFile(uint32_t callerAppInstanceId);
/**
* @param bundle the result bundle of an app
* @return the path from the bundle, or empty string if none is present
*/
std::string getResultPath(const Bundle& bundle);
/** @return the path picked by the last FileSelection dialog that closed with result == Ok. Only
* one dialog is expected to be open at a time. */
std::string getLastPath();
} // namespace
@@ -1,9 +1,14 @@
#pragma once
#include <Tactility/app/App.h>
#include <string>
namespace tt::app::imageviewer {
LaunchId start(const std::string& file);
/**
* Show a full-screen viewer for a single image file. Fire-and-forget: doesn't report any result
* back to the caller.
* @param file the path to the image file to display
*/
void start(const std::string& file);
}
@@ -1,21 +1,28 @@
#pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h>
#include <cstdint>
#include <string>
/**
* Start the app by its ID and provide:
* - a title
* - a text
* Show a dialog with a title, a message and a text field.
*/
namespace tt::app::inputdialog {
LaunchId start(const std::string& title, const std::string& message, const std::string& prefilled = "");
/**
* Show a dialog with the provided title, message and prefilled text, as a modal child of
* @a callerAppInstanceId (a new-model app - see app/manager.h). The caller receives the result
* as an APP_EVENT_RESULT in its own event loop: 0 = OK (call getLastText() for the entered
* text), 1 = Cancelled or dismissed without a press. The caller is responsible for calling
* app_manager_stop() on the returned instance id once it has handled the result.
* @return the new dialog's app instance id
*/
uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message, const std::string& prefilled = "");
/**
* @return the text that was in the field when OK was pressed, or otherwise empty string
* @return the text entered the last time any InputDialog instance was closed with OK. Only one
* dialog is expected to be open at a time - call this right after receiving its
* APP_EVENT_RESULT with result == 0.
*/
std::string getResult(const Bundle& bundle);
std::string getLastText();
}
@@ -1,14 +1,13 @@
#pragma once
#include <Tactility/app/App.h>
#include <string>
namespace tt::app::notes {
/**
* Start the notes app with the specified text file.
* @param[in] filePath the path to the text file to open
* @return the launch id
*/
LaunchId start(const std::string& filePath);
void start(const std::string& filePath);
}
@@ -1,28 +1,28 @@
#pragma once
#include <Tactility/app/App.h>
#include <Tactility/Bundle.h>
#include "app/instance.h"
#include <cstdint>
#include <string>
#include <vector>
/**
* Start the app by its ID and provide:
* - an optional title
* - 2 or more items
*
* If you provide 0 items, the app will auto-close.
* If you provide 1 item, the app will auto-close with result index 0
* Show a dialog with a title and a list of selectable items.
*/
namespace tt::app::selectiondialog {
LaunchId start(const std::string& title, const std::vector<std::string>& items);
/**
* Get the index of the item that the user selected.
*
* @return a value greater than 0 when a selection was done, or -1 when the app was closed without selecting an item.
* Show a selection dialog with the provided title and items, as a modal child of
* @a callerAppInstanceId (a new-model app - see app/manager.h). The caller receives the
* result as an APP_EVENT_RESULT in its own event loop: result is the selected item's index
* (>= 0), -1 if 0 items were provided (an error - the dialog auto-closes without showing
* anything), or a value not matching any item (currently always 1) if the dialog was
* dismissed without a selection. No result_bundle. If exactly 1 item is provided, the dialog
* auto-closes with result index 0 without showing anything. The caller is responsible for
* calling app_manager_stop() on the returned instance id once it has handled the result.
* @return the new dialog's app instance id
*/
int32_t getResultIndex(const Bundle& bundle);
AppInstanceId start(AppInstanceId callerAppInstanceId, const std::string& title, const std::vector<std::string>& items);
}
@@ -6,11 +6,16 @@
#if defined(CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED)
#include <Tactility/app/App.h>
#include <cstdint>
namespace tt::app::touchcalibration {
LaunchId start();
/**
* Starts calibration as a modal child of @a callerAppInstanceId. Result (Ok=0/Error=2, no
* bundle) is delivered as APP_EVENT_RESULT once the user dismisses the outcome screen.
* @return the new app instance id
*/
uint32_t start(uint32_t callerAppInstanceId);
} // namespace tt::app::touchcalibration
@@ -1,9 +1,16 @@
#pragma once
#include <Tactility/app/App.h>
#include <cstdint>
namespace tt::app::wifimanage {
LaunchId start();
/**
* Starts as a modal child of @a callerAppInstanceId (see app_manager_start_for_result()) - an
* APP_EVENT_RESULT is delivered back once the user closes this screen (default Cancelled/no
* bundle if never explicitly set - callers that just want a "the wifi step is done" signal, like
* Setup, can ignore the actual result value).
* @return the new app instance id
*/
uint32_t start(uint32_t callerAppInstanceId);
} // namespace
+2 -2
View File
@@ -1,9 +1,9 @@
#pragma once
#include <Tactility/app/AppContext.h>
#include <lvgl.h>
#include <string>
namespace tt::lvgl {
constexpr auto STATUSBAR_ICON_LIMIT = 8;
@@ -1,12 +1,7 @@
#pragma once
#include "../app/AppContext.h"
#include <lvgl/widgets/toolbar.h>
namespace tt::lvgl {
/** Create a toolbar widget that shows the app name as title */
lv_obj_t* toolbar_create(lv_obj_t* parent, const app::AppContext& app);
} // namespace
@@ -1,101 +0,0 @@
#pragma once
#include <Tactility/DispatcherThread.h>
#include <Tactility/Bundle.h>
#include <Tactility/PubSub.h>
#include <Tactility/RecursiveMutex.h>
#include <Tactility/app/AppInstance.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/service/Service.h>
#include <memory>
namespace tt::service::loader {
class LoaderService final : public Service {
public:
enum class Event {
ApplicationStarted,
ApplicationShowing,
ApplicationHiding,
ApplicationStopped
};
private:
std::shared_ptr<PubSub<Event>> pubsubExternal = std::make_shared<PubSub<Event>>();
RecursiveMutex mutex;
std::vector<std::shared_ptr<app::AppInstance>> appStack;
app::LaunchId nextLaunchId = 0;
/** The dispatcher thread needs a callstack large enough to accommodate all the dispatched methods.
* This includes full LVGL redraw via Gui::redraw()
*/
std::unique_ptr<DispatcherThread> dispatcherThread = std::make_unique<DispatcherThread>("loader_dispatcher", 6144); // Files app requires ~5k
void onStartAppMessage(const std::string& id, app::LaunchId launchId, std::shared_ptr<const Bundle> parameters);
void onStopTopAppMessage(const std::string& id);
void onStopAllAppMessage(const std::string& id);
void transitionAppToState(const std::shared_ptr<app::AppInstance>& app, app::State state);
int findAppInStack(const std::string& id) const;
bool onStart(ServiceContext& service) override {
dispatcherThread->start();
return true;
}
void onStop(ServiceContext& service) override {
// Send stop signal to thread and wait for thread to finish
mutex.withLock([this] {
dispatcherThread->stop();
});
}
public:
/**
* @brief Start an app given an app id and an optional bundle with parameters
* @param id the app identifier
* @param parameters optional parameter bundle (nullable)
* @return the launch id
*/
app::LaunchId start(const std::string& id, std::shared_ptr<const Bundle> parameters);
/**
* @brief Stops the top-most app (the one that is currently active shown to the user
* @warning Avoid calling this directly and use stopTop(id) instead
*/
void stopTop();
/**
* @brief Stops the top-most app if the id is still matching by the time the stop event arrives.
* @param id the id of the app to stop
*/
void stopTop(const std::string& id);
/**
* @brief Stops all apps with the provided id and any apps that were pushed on top of the stack after the original app was started.
* @param id the id of the app to stop
*/
void stopAll(const std::string& id);
/** @return the AppContext of the top-most application, or nullptr if no app is running. */
std::shared_ptr<app::AppContext> getCurrentAppContext();
/** @return true if the app is running anywhere in the app stack (the app does not have to be the top-most one for this to return true) */
bool isRunning(const std::string& id) const;
/** @return the PubSub object that is responsible for event publishing */
std::shared_ptr<PubSub<Event>> getPubsub() const { return pubsubExternal; }
};
/** return the service or nullptr if it's not running */
std::shared_ptr<LoaderService> findLoaderService();
} // namespace
@@ -1,98 +0,0 @@
#pragma once
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/ElfApp.h>
#include <Tactility/Bundle.h>
#include <tactility/check.h>
#include <tactility/log.h>
#include <Tactility/Mutex.h>
#include <memory>
#include <utility>
namespace tt::app {
enum class State {
Initial, // AppInstance was created, but the state hasn't advanced yet
Created, // App was placed into memory
Showing, // App view was created
Hiding, // App view was destroyed
Destroyed // App was removed from memory
};
/**
* Thread-safe app instance.
*/
class AppInstance : public AppContext {
Mutex mutex;
const std::shared_ptr<AppManifest> manifest;
State state = State::Initial;
LaunchId launchId;
Flags flags = { .hideStatusbar = true };
/** @brief Optional parameters to start the app with
* When these are stored in the app struct, the struct takes ownership.
* Do not mutate after app creation.
*/
std::shared_ptr<const Bundle> parameters;
std::shared_ptr<App> app;
static std::shared_ptr<App> createApp(
const std::shared_ptr<AppManifest>& manifest
) {
if (manifest->appLocation.isInternal()) {
assert(manifest->createApp != nullptr);
return manifest->createApp();
} else if (manifest->appLocation.isExternal()) {
if (manifest->createApp != nullptr) {
LOG_W("AppInstance", "Manifest specifies createApp, but this is not used with external apps");
}
#ifdef ESP_PLATFORM
return createElfApp(manifest);
#else
check(false, "not supported");
#endif
} else {
check(false, "not implemented");
}
}
public:
explicit AppInstance(const std::shared_ptr<AppManifest>& manifest, LaunchId launchId) :
manifest(manifest),
launchId(launchId),
app(createApp(manifest))
{}
AppInstance(const std::shared_ptr<AppManifest>& manifest, LaunchId launchId, std::shared_ptr<const Bundle> parameters) :
manifest(manifest),
launchId(launchId),
parameters(std::move(parameters)),
app(createApp(manifest))
{}
~AppInstance() override = default;
LaunchId getLaunchId() const { return launchId; }
void setState(State state);
State getState() const;
const AppManifest& getManifest() const override;
Flags getFlags() const;
void setFlags(Flags flags);
Flags& mutableFlags() { return flags; } // TODO: locking mechanism
std::shared_ptr<const Bundle> getParameters() const override;
std::unique_ptr<AppPaths> getPaths() const override;
std::shared_ptr<App> getApp() const override { return app; }
};
} // namespace
@@ -1,14 +0,0 @@
#pragma once
#include <Tactility/app/AppManifest.h>
#include <string>
namespace tt::app {
bool isValidId(const std::string& id);
/** Parses a manifest.properties file, auto-detecting the V1 (sectioned) or V2 (flat) format from its first line. */
bool parseManifest(const std::string& filePath, AppManifest& manifest);
}
@@ -1,23 +0,0 @@
#pragma once
#include <Tactility/app/AppManifest.h>
#include <map>
#include <string>
namespace tt::app {
bool getValueFromManifest(const std::map<std::string, std::string>& map, const std::string& key, std::string& output);
bool isValidManifestVersion(const std::string& version);
bool isValidAppVersionName(const std::string& version);
bool isValidAppVersionCode(const std::string& version);
bool isValidName(const std::string& name);
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map. */
bool parseManifestV1(const std::map<std::string, std::string>& map, AppManifest& manifest);
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map. */
bool parseManifestV2(const std::map<std::string, std::string>& map, AppManifest& manifest);
}
@@ -5,11 +5,13 @@
namespace tt::app::btmanage {
typedef void (*OnBtToggled)(bool enable);
typedef void (*OnScanToggled)(bool enable);
// `context` is this app instance's Context* (see BtManagePrivate.h) - the new app-module has no
// global "current app" accessor, so callbacks need it threaded through explicitly.
typedef void (*OnBtToggled)(void* context, bool enable);
typedef void (*OnScanToggled)(void* context, bool enable);
typedef void (*OnConnectPeer)(const std::array<uint8_t, 6>& addr, int profileId);
typedef void (*OnDisconnectPeer)(const std::array<uint8_t, 6>& addr, int profileId);
typedef void (*OnPairPeer)(const std::array<uint8_t, 6>& addr);
typedef void (*OnPairPeer)(void* context, const std::array<uint8_t, 6>& addr);
typedef void (*OnForgetPeer)(const std::array<uint8_t, 6>& addr);
struct Bindings {
@@ -3,7 +3,6 @@
#include "./View.h"
#include "./State.h"
#include <Tactility/app/App.h>
#include <Tactility/Mutex.h>
#include <Tactility/bluetooth/Bluetooth.h>
#include <tactility/drivers/bluetooth.h>
@@ -13,54 +12,37 @@
namespace tt::app::btmanage {
class BtManage final : public App {
struct Context {
uint32_t appInstanceId;
Mutex mutex;
Bindings bindings = { };
Bindings bindings {};
State state;
View view = View(&bindings, &state);
bool isViewEnabled = false;
Device* btDevice = nullptr;
bool callbackRegistered = false;
// Bumped by onHide() to invalidate any BT event already dispatched to the main
// task for this show/hide session (BtManage is reused across hide/show cycles -
// e.g. launching BtPeerSettings pushes it on top and hides this instance without
// destroying it). Kept in its own heap allocation, independent of BtManage's
// lifetime, so a dispatched callback can check it without touching a possibly
// already-destroyed `this`.
// Bumped right before the BT event callback is unregistered at the end of appMain(), to
// invalidate any BT event already dispatched to the main task for this instance. Kept in
// its own heap allocation, independent of Context's (stack-local) lifetime, so a dispatched
// callback can check it without touching a possibly already-destroyed Context.
std::shared_ptr<std::atomic<int>> generation = std::make_shared<std::atomic<int>>(0);
public:
void onBtEvent(const struct BtEvent& event);
BtManage();
void lock();
void unlock();
void onShow(AppContext& app, lv_obj_t* parent) override;
void onHide(AppContext& app) override;
Bindings& getBindings() { return bindings; }
State& getState() { return state; }
void requestViewUpdate();
std::shared_ptr<std::atomic<int>> getGeneration() const { return generation; }
// Re-attempts registering the device event callback. Needed because the BLE driver
// only allocates its callback list while the device is started/on: a registration
// attempted while the radio is off silently no-ops, so this must be called again
// right after a successful bluetooth::start(). Idempotent: no-ops if already
// registered for this device, so it's safe to call from both onShow() and here.
void registerDeviceCallback(Device* dev);
// Call after bluetooth::stop(): the driver frees its callback list on stop, so the
// registration state must be cleared here too, without touching the (now-dangling)
// driver-side list.
void forgetCallbackRegistration();
void lock() { mutex.lock(); }
void unlock() { mutex.unlock(); }
};
void onBtEvent(Context* ctx, const struct BtEvent& event);
void requestViewUpdate(Context* ctx);
// Re-attempts registering the device event callback. Needed because the BLE driver only
// allocates its callback list while the device is started/on: a registration attempted while
// the radio is off silently no-ops, so this must be called again right after a successful
// bluetooth::start(). Idempotent: no-ops if already registered for this device.
void registerDeviceCallback(Context* ctx, Device* dev);
// Call after bluetooth::stop(): the driver frees its callback list on stop, so the
// registration state must be cleared here too, without touching the (now-dangling) driver-side
// list.
void forgetCallbackRegistration(Context* ctx);
} // namespace tt::app::btmanage
@@ -3,9 +3,7 @@
#include "./Bindings.h"
#include "./State.h"
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppPaths.h>
#include <cstdint>
#include <lvgl.h>
namespace tt::app::btmanage {
@@ -14,7 +12,9 @@ class View final {
Bindings* bindings;
State* state;
std::unique_ptr<AppPaths> paths;
// Passed through to onBtToggled/onScanToggled/onPairPeer via lv_obj user_data - see
// Bindings.h. Set in init(), before any callback can fire.
void* context = nullptr;
lv_obj_t* root = nullptr;
lv_obj_t* enable_switch = nullptr;
lv_obj_t* enable_on_boot_switch = nullptr;
@@ -34,7 +34,7 @@ public:
View(Bindings* bindings, State* state) : bindings(bindings), state(state) {}
void init(const AppContext& app, lv_obj_t* parent);
void init(void* context, lv_obj_t* parent);
void update();
};
@@ -10,37 +10,32 @@
#include "ChatView.h"
#include "ChatSettings.h"
#include <Tactility/app/App.h>
#include <Tactility/service/espnow/EspNow.h>
#include <cstdint>
#include <string>
namespace tt::app::chat {
class ChatApp final : public App {
// Replaces the old ChatApp (tt::app::App subclass) under the thread-per-app model. Declared
// here (rather than local to ChatApp.cpp's anonymous namespace, as most converted apps do)
// because ChatView - a separate translation unit - also needs to reference it.
struct Context {
uint32_t appInstanceId;
ChatState state;
ChatView view = ChatView(this, &state);
service::espnow::ReceiverSubscription receiveSubscription = -1;
ChatSettingsData settings;
bool isFirstLaunch = false;
void onReceive(const esp_now_recv_info_t* receiveInfo, const uint8_t* data, int length);
void enableEspNow();
void disableEspNow();
public:
void onCreate(AppContext& appContext) override;
void onDestroy(AppContext& appContext) override;
void onShow(AppContext& context, lv_obj_t* parent) override;
void sendMessage(const std::string& text);
void applySettings(const std::string& nickname, const std::string& keyHex);
void switchChannel(const std::string& chatChannel);
const ChatSettingsData& getSettings() const { return settings; }
~ChatApp() override = default;
};
void enableEspNow(Context* ctx);
void disableEspNow(Context* ctx);
void sendMessage(Context* ctx, const std::string& text);
void applySettings(Context* ctx, const std::string& nickname, const std::string& keyHex);
void switchChannel(Context* ctx, const std::string& chatChannel);
} // namespace tt::app::chat
#endif // CONFIG_SOC_WIFI_SUPPORTED || CONFIG_SLAVE_SOC_WIFI_SUPPORTED
@@ -9,18 +9,16 @@
#include "ChatState.h"
#include "ChatSettings.h"
#include <Tactility/app/AppContext.h>
#include <esp_now.h>
#include <lvgl.h>
namespace tt::app::chat {
class ChatApp;
struct Context;
class ChatView {
ChatApp* app;
Context* app;
ChatState* state;
lv_obj_t* toolbar = nullptr;
@@ -45,6 +43,7 @@ class ChatView {
static void addMessageToList(lv_obj_t* msgList, const StoredMessage& msg);
static void onBackPressed(lv_event_t* e);
static void onSendClicked(lv_event_t* e);
static void onSettingsClicked(lv_event_t* e);
static void onSettingsSave(lv_event_t* e);
@@ -54,7 +53,7 @@ class ChatView {
static void onChannelCancel(lv_event_t* e);
public:
ChatView(ChatApp* app, ChatState* state) : app(app), state(state) {}
ChatView(Context* app, ChatState* state) : app(app), state(state) {}
~ChatView() = default;
ChatView(const ChatView&) = delete;
@@ -62,7 +61,7 @@ public:
ChatView(ChatView&&) = delete;
ChatView& operator=(ChatView&&) = delete;
void init(AppContext& appContext, lv_obj_t* parent);
void init(lv_obj_t* parent);
void displayMessage(const StoredMessage& msg);
void refreshMessageList();
@@ -1,11 +0,0 @@
#pragma once
#ifdef ESP_PLATFORM
namespace tt::app::development {
void start();
}
#endif // ESP_PLATFORM
+7 -6
View File
@@ -2,8 +2,7 @@
#include "./State.h"
#include <Tactility/app/AppManifest.h>
#include <cstdint>
#include <lvgl.h>
#include <memory>
@@ -11,6 +10,7 @@ namespace tt::app::files {
class View final {
std::shared_ptr<State> state;
uint32_t appInstanceId = 0;
size_t current_start_index = 0;
size_t last_loaded_index = 0;
@@ -24,7 +24,7 @@ class View final {
lv_obj_t* paste_button = nullptr;
std::string installAppPath = { 0 };
LaunchId installAppLaunchId = 0;
uint32_t installDialogId = 0;
void showActions();
void showActionsForDirectory();
@@ -39,9 +39,10 @@ public:
explicit View(const std::shared_ptr<State>& state) : state(state) {}
void init(const AppContext& appContext, lv_obj_t* parent);
void init(uint32_t appInstanceId, lv_obj_t* parent);
void update(size_t start_index = 0);
void onBackPressed();
void onNavigateUpPressed();
void onDirEntryPressed(uint32_t index);
void onDirEntryLongPressed(int32_t index);
@@ -54,8 +55,8 @@ public:
void onPastePressed();
void onEjectPressed();
void onDirEntryListScrollBegin();
void onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle);
void deinit(const AppContext& appContext);
void onResult(uint32_t launchId, int32_t result);
void deinit();
private:
@@ -1,7 +1,5 @@
#pragma once
#include <Tactility/Bundle.h>
namespace tt::app::fileselection {
enum class Mode {
@@ -9,6 +7,4 @@ enum class Mode {
ExistingOrNew = 1
};
Mode getMode(const Bundle& bundle);
}
@@ -3,14 +3,13 @@
#include "./State.h"
#include "./FileSelectionPrivate.h"
#include <Tactility/app/AppManifest.h>
#include <lvgl.h>
#include <memory>
namespace tt::app::fileselection {
class View final {
uint32_t appInstanceId;
std::shared_ptr<State> state;
lv_obj_t* dir_entry_list = nullptr;
@@ -22,11 +21,15 @@ class View final {
void onTapFile(const std::string&path, const std::string&filename);
static void onSelectButtonPressed(lv_event_t* event);
static void onPathTextChanged(lv_event_t* event);
/** Emits an async APP_EVENT_CLOSE for appInstanceId - see FileSelection.cpp's appMain() for
* why this indirection (rather than calling app_manager_stop() here) is required. */
static void onBackPressedCallback(lv_event_t* event);
void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry);
public:
explicit View(const std::shared_ptr<State>& state, std::function<void(const std::string& path)> onFileSelected) :
explicit View(uint32_t appInstanceId, const std::shared_ptr<State>& state, std::function<void(const std::string& path)> onFileSelected) :
appInstanceId(appInstanceId),
state(state),
on_file_selected(std::move(onFileSelected))
{}
@@ -1,9 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
#include <cstdint>
namespace tt::app::i2cscanner {
LaunchId start();
uint32_t start();
}
@@ -1,9 +1,7 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::launcher {
LaunchId start();
uint32_t start();
}
@@ -1,9 +1,5 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::localesettings {
LaunchId start();
}
// Intentionally empty: LocaleSettings.cpp has no external callers (verified via repo-wide
// grep during its thread-per-app conversion), so it no longer exposes a start() wrapper. This
// header is kept as a placeholder in case that changes; nothing currently includes it.
@@ -1,10 +1,8 @@
#pragma once
#include <Tactility/app/App.h>
namespace tt::app::setup {
LaunchId start();
void start();
/** @return true if the setup wizard has already run to completion */
bool isCompleted();
@@ -1,9 +1,9 @@
#pragma once
#include <Tactility/app/App.h>
#include <cstdint>
namespace tt::app::timedatesettings {
LaunchId start();
uint32_t start();
}

Some files were not shown because too many files have changed in this diff Show More