Refactor app loading and window management (#609)
This commit is contained in:
committed by
GitHub
parent
dc3f6104b8
commit
37c507544b
@@ -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)
|
||||
|
||||
@@ -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
@@ -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", "")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = >911_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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -96,6 +96,8 @@ else ()
|
||||
Tactility
|
||||
TactilityFreeRtos
|
||||
lvgl-module
|
||||
lvgl-window-manager-module
|
||||
app-module
|
||||
crypt-module
|
||||
gps-module
|
||||
gps-generic-module
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
- Modules/service-module
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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(®istry.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(®istry.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(®istry.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(®istry.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(®istry.mutex);
|
||||
error_t result = uninstall_locked(app_id);
|
||||
mutex_unlock(®istry.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;
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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(®istry.mutex);
|
||||
if (std::ranges::find(registry.paths, path) == registry.paths.end()) {
|
||||
registry.paths.emplace_back(path);
|
||||
}
|
||||
mutex_unlock(®istry.mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
void app_manager_install_path_scan(void) {
|
||||
auto& registry = install_path_registry();
|
||||
|
||||
mutex_lock(®istry.mutex);
|
||||
auto paths_copy = registry.paths;
|
||||
mutex_unlock(®istry.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(®istry.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(®istry.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(®istry.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(®istry.mutex);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -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_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <tactility/concurrent/dispatcher.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/module.h>
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user