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
|
INCLUDE_DIRS
|
||||||
"Libraries/TactilityC/include"
|
"Libraries/TactilityC/include"
|
||||||
"Libraries/TactilityKernel/include"
|
"Libraries/TactilityKernel/include"
|
||||||
"Libraries/TactilityFreeRtos/include"
|
"Libraries/TactilityFreeRtos/Include"
|
||||||
"Libraries/lvgl/include"
|
"Libraries/lvgl/include"
|
||||||
"Libraries/minmea/include"
|
"Libraries/minmea/include"
|
||||||
|
"Libraries/minitar/include"
|
||||||
"Modules/lvgl-module/include"
|
"Modules/lvgl-module/include"
|
||||||
# DRIVER_INCLUDE_DIRS_PLACEHOLDER
|
REQUIRES esp_timer app-module crypt-module gps-module lvgl-module lvgl-window-manager-module service-module
|
||||||
REQUIRES esp_timer
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Regular and core features
|
# 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(TactilityKernel Libraries/TactilityKernel/binary/libTactilityKernel.a)
|
||||||
add_prebuilt_library(lvgl Libraries/lvgl/binary/liblvgl.a)
|
add_prebuilt_library(lvgl Libraries/lvgl/binary/liblvgl.a)
|
||||||
add_prebuilt_library(minmea Libraries/minmea/binary/libminmea.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 TactilityC)
|
||||||
target_link_libraries(${COMPONENT_LIB} INTERFACE TactilityKernel)
|
target_link_libraries(${COMPONENT_LIB} INTERFACE TactilityKernel)
|
||||||
target_link_libraries(${COMPONENT_LIB} INTERFACE lvgl)
|
target_link_libraries(${COMPONENT_LIB} INTERFACE lvgl)
|
||||||
target_link_libraries(${COMPONENT_LIB} INTERFACE minmea)
|
target_link_libraries(${COMPONENT_LIB} INTERFACE minmea)
|
||||||
|
target_link_libraries(${COMPONENT_LIB} INTERFACE minitar)
|
||||||
|
|||||||
@@ -18,14 +18,18 @@ macro(tactility_project project_name)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(EXTRA_COMPONENT_DIRS
|
set(EXTRA_COMPONENT_DIRS
|
||||||
"Libraries/TactilityFreeRtos"
|
"${TACTILITY_SDK_PATH}/Libraries/TactilityFreeRtos"
|
||||||
"Modules"
|
"${TACTILITY_SDK_PATH}/Modules"
|
||||||
"Drivers"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set(COMPONENTS
|
set(COMPONENTS
|
||||||
TactilityFreeRtos
|
TactilityFreeRtos
|
||||||
# DRIVER_COMPONENTS_PLACEHOLDER
|
app-module
|
||||||
|
crypt-module
|
||||||
|
gps-module
|
||||||
|
lvgl-module
|
||||||
|
lvgl-window-manager-module
|
||||||
|
service-module
|
||||||
)
|
)
|
||||||
|
|
||||||
endmacro()
|
endmacro()
|
||||||
|
|||||||
+15
-46
@@ -111,43 +111,13 @@ def add_module(target_path, module_name):
|
|||||||
cmakelists_content = create_module_cmakelists(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)
|
write_module_cmakelists(os.path.join(target_path, f"Modules/{module_name}/CMakeLists.txt"), cmakelists_content)
|
||||||
|
|
||||||
def discover_all_drivers():
|
def generate_tactility_sdk_cmake(target_path):
|
||||||
"""
|
|
||||||
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):
|
|
||||||
src = os.path.join('Buildscripts', 'TactilitySDK', 'TactilitySDK.cmake')
|
src = os.path.join('Buildscripts', 'TactilitySDK', 'TactilitySDK.cmake')
|
||||||
with open(src) as f:
|
shutil.copy2(src, os.path.join(target_path, 'TactilitySDK.cmake'))
|
||||||
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)
|
|
||||||
|
|
||||||
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')
|
src = os.path.join('Buildscripts', 'TactilitySDK', 'CMakeLists.txt')
|
||||||
with open(src) as f:
|
shutil.copy2(src, os.path.join(target_path, 'CMakeLists.txt'))
|
||||||
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)
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
@@ -167,7 +137,7 @@ def main():
|
|||||||
{'src': 'TactilityC/CMakeLists.txt', 'dst': 'Libraries/TactilityC/'},
|
{'src': 'TactilityC/CMakeLists.txt', 'dst': 'Libraries/TactilityC/'},
|
||||||
{'src': 'TactilityC/LICENSE*.*', 'dst': 'Libraries/TactilityC/'},
|
{'src': 'TactilityC/LICENSE*.*', 'dst': 'Libraries/TactilityC/'},
|
||||||
# TactilityFreeRtos
|
# TactilityFreeRtos
|
||||||
{'src': 'TactilityFreeRtos/Include/**', 'dst': 'Libraries/TactilityFreeRtos/include/'},
|
{'src': 'TactilityFreeRtos/Include/**', 'dst': 'Libraries/TactilityFreeRtos/Include/'},
|
||||||
{'src': 'TactilityFreeRtos/CMakeLists.txt', 'dst': 'Libraries/TactilityFreeRtos/'},
|
{'src': 'TactilityFreeRtos/CMakeLists.txt', 'dst': 'Libraries/TactilityFreeRtos/'},
|
||||||
{'src': 'TactilityFreeRtos/LICENSE*.*', 'dst': 'Libraries/TactilityFreeRtos/'},
|
{'src': 'TactilityFreeRtos/LICENSE*.*', 'dst': 'Libraries/TactilityFreeRtos/'},
|
||||||
# TactilityKernel
|
# TactilityKernel
|
||||||
@@ -185,6 +155,10 @@ def main():
|
|||||||
# elf_loader
|
# elf_loader
|
||||||
{'src': 'Libraries/elf_loader/elf_loader.cmake', 'dst': 'Libraries/elf_loader/'},
|
{'src': 'Libraries/elf_loader/elf_loader.cmake', 'dst': 'Libraries/elf_loader/'},
|
||||||
{'src': 'Libraries/elf_loader/license.txt', '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
|
# minmea
|
||||||
{'src': 'build/esp-idf/minmea/libminmea.a', 'dst': 'Libraries/minmea/binary/'},
|
{'src': 'build/esp-idf/minmea/libminmea.a', 'dst': 'Libraries/minmea/binary/'},
|
||||||
{'src': 'Libraries/minmea/Include/**', 'dst': 'Libraries/minmea/include/'},
|
{'src': 'Libraries/minmea/Include/**', 'dst': 'Libraries/minmea/include/'},
|
||||||
@@ -197,21 +171,16 @@ def main():
|
|||||||
map_copy(mappings, target_path)
|
map_copy(mappings, target_path)
|
||||||
|
|
||||||
# Modules
|
# Modules
|
||||||
add_module(target_path, "lvgl-module")
|
add_module(target_path, "app-module")
|
||||||
add_module(target_path, "crypt-module")
|
add_module(target_path, "crypt-module")
|
||||||
add_module(target_path, "gps-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")
|
add_module(target_path, "service-module")
|
||||||
|
|
||||||
# Drivers - only ones actually built for this target (chip-restricted drivers like
|
# Final scripts - copied verbatim
|
||||||
# sc2356-module won't have a .a outside ESP32-P4)
|
generate_tactility_sdk_cmake(target_path)
|
||||||
available_drivers = [d for d in discover_all_drivers() if driver_is_available(d)]
|
generate_tactility_sdk_top_cmakelists(target_path)
|
||||||
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)
|
|
||||||
|
|
||||||
# Output ESP-IDF SDK version to file
|
# Output ESP-IDF SDK version to file
|
||||||
esp_idf_version = os.environ.get("ESP_IDF_VERSION", "")
|
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/crypt-module)
|
||||||
add_subdirectory(Modules/gps-module)
|
add_subdirectory(Modules/gps-module)
|
||||||
add_subdirectory(Modules/service-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-generic-module)
|
||||||
add_subdirectory(Drivers/gps-meshtastic-module)
|
add_subdirectory(Drivers/gps-meshtastic-module)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
- Platforms/platform-esp32
|
- 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
|
dts: generic,esp32.dts
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
- Platforms/platform-esp32
|
- 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
|
dts: generic,esp32c6.dts
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
- Platforms/platform-esp32
|
- 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
|
dts: generic,esp32p4.dts
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
- Platforms/platform-esp32
|
- 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
|
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
|
lvgl.colorDepth=16
|
||||||
|
|
||||||
sdkconfig.CONFIG_CODEC_DUMMY_SUPPORT=y
|
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() {
|
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;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static error_t stop() {
|
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;
|
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.
|
// 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_reset = GPIO_PIN_SPEC_NONE,
|
||||||
.pin_interrupt = 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;
|
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 = {
|
static const KeyboardApi tab5_keyboard_api = {
|
||||||
.read_key = tab5_keyboard_read_key,
|
.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,
|
// Defined in module.cpp - this driver is registered directly by m5stack-tab5's own module,
|
||||||
|
|||||||
@@ -12,6 +12,22 @@
|
|||||||
|
|
||||||
## Higher Priority
|
## 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
|
- 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
|
- 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()
|
- Improve kernel_init.cpp (and other modules): create driver_ensure_added() and driver_ensure_destructed()
|
||||||
@@ -38,6 +54,9 @@
|
|||||||
|
|
||||||
## Medium Priority
|
## 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)
|
- 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
|
- Make USB host driver disabled by default, so it doesn't consume memory
|
||||||
- Filtering for apps in App Hub:
|
- Filtering for apps in App Hub:
|
||||||
@@ -53,6 +72,9 @@
|
|||||||
|
|
||||||
## Lower Priority
|
## 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.
|
- lvgl-module's spinner relies on hard-coded spinner asset from Tactility main project.
|
||||||
- Localize all apps
|
- Localize all apps
|
||||||
- Support hot-plugging SD card (note: this is not possible if they require the CS pin hack)
|
- Support hot-plugging SD card (note: this is not possible if they require the CS pin hack)
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ else ()
|
|||||||
Tactility
|
Tactility
|
||||||
TactilityFreeRtos
|
TactilityFreeRtos
|
||||||
lvgl-module
|
lvgl-module
|
||||||
|
lvgl-window-manager-module
|
||||||
|
app-module
|
||||||
crypt-module
|
crypt-module
|
||||||
gps-module
|
gps-module
|
||||||
gps-generic-module
|
gps-generic-module
|
||||||
|
|||||||
@@ -25,4 +25,9 @@ else()
|
|||||||
target_include_directories(QRCode
|
target_include_directories(QRCode
|
||||||
PUBLIC src
|
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()
|
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);
|
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
|
* @brief Adds the textarea to the shared keyboard navigation group (so any keypad indev -
|
||||||
* defocus/ready, and adds the textarea to the keyboard's navigation group.
|
* 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
|
||||||
* No-op if lvgl_software_keyboard_is_enabled() is false (i.e. a hardware keyboard is present).
|
* up to show/hide the on-screen keyboard on focus/defocus/ready.
|
||||||
*
|
|
||||||
* @warning Caller must hold the LVGL lock.
|
* @warning Caller must hold the LVGL lock.
|
||||||
* @param[in] keyboard the on-screen keyboard to associate with the textarea
|
* @param[in] keyboard the on-screen keyboard to associate with the textarea
|
||||||
* @param[in] textarea the lv_textarea_t object to wire up
|
* @param[in] textarea the lv_textarea_t object to wire up
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ void lvgl_keyboard_on_start_lvgl() {
|
|||||||
lvgl_lock();
|
lvgl_lock();
|
||||||
keyboard_group = lv_group_create();
|
keyboard_group = lv_group_create();
|
||||||
check(keyboard_group);
|
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();
|
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);
|
lv_indev_set_display(indev, display);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lvgl_keyboard_enable(indev);
|
||||||
|
|
||||||
*out_indev = indev;
|
*out_indev = indev;
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
@@ -106,8 +112,11 @@ bool lvgl_hardware_keyboard_is_available() {
|
|||||||
return false;
|
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);
|
device_put(keyboard_device);
|
||||||
return true;
|
return present;
|
||||||
}
|
}
|
||||||
|
|
||||||
void lvgl_hardware_keyboard_add_custom(lv_indev_t* indev) {
|
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) {
|
static void textarea_hide_keyboard(lv_event_t* event) {
|
||||||
if (last_software_keyboard.object != nullptr) {
|
if (last_software_keyboard.object == nullptr) {
|
||||||
lvgl_software_keyboard_hide(&last_software_keyboard);
|
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) {
|
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) {
|
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()) {
|
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_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_DEFOCUSED, nullptr);
|
||||||
lv_obj_add_event_cb(textarea, textarea_hide_keyboard, LV_EVENT_READY, nullptr);
|
lv_obj_add_event_cb(textarea, textarea_hide_keyboard, LV_EVENT_READY, nullptr);
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
void lvgl_software_keyboard_activate(LvglSoftwareKeyboard* keyboard) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
#include <lvgl/devices/trackball.h>
|
#include <lvgl/devices/trackball.h>
|
||||||
#include <lvgl/devices/device_context.h>
|
#include <lvgl/devices/device_context.h>
|
||||||
|
#include <lvgl/devices/keyboard.h>
|
||||||
#include <lvgl/lvgl.h>
|
#include <lvgl/lvgl.h>
|
||||||
|
|
||||||
#include <tactility/drivers/trackball.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);
|
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;
|
*out_indev = indev;
|
||||||
return ERROR_NONE;
|
return ERROR_NONE;
|
||||||
}
|
}
|
||||||
@@ -196,12 +204,14 @@ error_t lvgl_trackball_set_settings(lv_indev_t* indev, const struct LvglTrackbal
|
|||||||
|
|
||||||
if (mode_changed) {
|
if (mode_changed) {
|
||||||
if (settings->mode == LVGL_TRACKBALL_MODE_POINTER) {
|
if (settings->mode == LVGL_TRACKBALL_MODE_POINTER) {
|
||||||
|
lvgl_keyboard_disable(indev);
|
||||||
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
|
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
|
||||||
recenter_cursor(ctx, indev);
|
recenter_cursor(ctx, indev);
|
||||||
show_cursor(ctx, indev);
|
show_cursor(ctx, indev);
|
||||||
} else {
|
} else {
|
||||||
hide_cursor(ctx);
|
hide_cursor(ctx);
|
||||||
lv_indev_set_type(indev, LV_INDEV_TYPE_ENCODER);
|
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/drivers/wifi.h>
|
||||||
#include <tactility/error_esp32.h>
|
#include <tactility/error_esp32.h>
|
||||||
#include <tactility/log.h>
|
#include <tactility/log.h>
|
||||||
|
#include <tactility/time.h>
|
||||||
|
|
||||||
#if defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
#if defined(CONFIG_SLAVE_SOC_WIFI_SUPPORTED)
|
||||||
#include <tactility/drivers/esp32_esp_hosted_ota.h>
|
#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 wifiEventHandler = nullptr;
|
||||||
esp_event_handler_instance_t ipEventHandler = 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{};
|
Mutex callbackMutex{};
|
||||||
WifiCallbackEntry callbacks[WIFI_MAX_CALLBACKS] = {};
|
WifiCallbackEntry callbacks[WIFI_MAX_CALLBACKS] = {};
|
||||||
size_t callbackCount = 0;
|
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) {
|
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);
|
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) {
|
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||||
mutex_lock(&ctx->mutex);
|
mutex_lock(&ctx->mutex);
|
||||||
bool was_pending = ctx->stationState == WIFI_STATION_STATE_CONNECTION_PENDING;
|
bool was_pending = ctx->stationState == WIFI_STATION_STATE_CONNECTION_PENDING;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ list(APPEND REQUIRES_LIST
|
|||||||
TactilityKernel
|
TactilityKernel
|
||||||
TactilityFreeRtos
|
TactilityFreeRtos
|
||||||
lvgl-module
|
lvgl-module
|
||||||
|
lvgl-window-manager-module
|
||||||
|
app-module
|
||||||
crypt-module
|
crypt-module
|
||||||
gps-module
|
gps-module
|
||||||
gps-generic-module
|
gps-generic-module
|
||||||
@@ -20,6 +22,7 @@ list(APPEND REQUIRES_LIST
|
|||||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||||
|
|
||||||
list(APPEND REQUIRES_LIST
|
list(APPEND REQUIRES_LIST
|
||||||
|
app-esp32-module
|
||||||
platform-esp32
|
platform-esp32
|
||||||
driver
|
driver
|
||||||
elf_loader
|
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
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <tactility/filesystem/file_system.h>
|
#include <tactility/filesystem/file_system.h>
|
||||||
|
|
||||||
|
|
||||||
namespace tt {
|
namespace tt {
|
||||||
|
|
||||||
bool findFirstMountedSdCardPath(std::string& path);
|
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/concurrent/dispatcher.h>
|
||||||
#include <tactility/device.h>
|
#include <tactility/device.h>
|
||||||
#include <tactility/module.h>
|
#include <tactility/module.h>
|
||||||
#include <Tactility/app/AppManifest.h>
|
|
||||||
|
|
||||||
#include <functional>
|
#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
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/Bundle.h>
|
#include <cstdint>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <Tactility/app/App.h>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the app by its ID and provide:
|
* Show a dialog with a title, a message and 0, 1 or more buttons.
|
||||||
* - a title
|
|
||||||
* - a text
|
|
||||||
* - 0, 1 or more buttons
|
|
||||||
*/
|
*/
|
||||||
namespace tt::app::alertdialog {
|
namespace tt::app::alertdialog {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a dialog with the provided title, message and 0, 1 or more buttons.
|
* Show a dialog with the provided title, message and buttons, as a modal child of
|
||||||
* @param[in] title the title to show in the toolbar
|
* @a callerAppInstanceId (a new-model app - see app/manager.h). The caller receives the
|
||||||
* @param[in] message the message to display
|
* result as an APP_EVENT_RESULT in its own event loop: result is the pressed button's index
|
||||||
* @param[in] buttonLabels the buttons to show
|
* (>= 0), or a value not matching any button (currently always 1) if the dialog was dismissed
|
||||||
* @return the launch id
|
* 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);
|
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 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);
|
|
||||||
|
|
||||||
/**
|
/** @copydoc start(uint32_t, const std::string&, const std::string&, const std::vector<std::string>&)
|
||||||
* Show a dialog with the provided title, message and an OK button
|
* Shows a single "OK" button. */
|
||||||
* @param[in] title the title to show in the toolbar
|
uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std::string& message);
|
||||||
* @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);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace tt::app::btmanage {
|
namespace tt::app::btmanage {
|
||||||
|
|
||||||
LaunchId start();
|
uint32_t start();
|
||||||
|
|
||||||
} // namespace tt::app::btmanage
|
} // namespace tt::app::btmanage
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
#include <cstdint>
|
||||||
#include <Tactility/Bundle.h>
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace tt::app::fileselection {
|
namespace tt::app::fileselection {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a file selection dialog that allows the user to select an existing file.
|
* Show a file selection dialog that allows the user to select an existing file, as a modal
|
||||||
* This app returns the absolute file path as a result.
|
* 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.
|
* Same as startForExistingFile(), but also allows picking a path that doesn't exist yet (for
|
||||||
* This app returns the absolute file path as a result.
|
* "save as"-style flows).
|
||||||
*/
|
*/
|
||||||
LaunchId startForExistingOrNewFile();
|
uint32_t startForExistingOrNewFile(uint32_t callerAppInstanceId);
|
||||||
|
|
||||||
/**
|
/** @return the path picked by the last FileSelection dialog that closed with result == Ok. Only
|
||||||
* @param bundle the result bundle of an app
|
* one dialog is expected to be open at a time. */
|
||||||
* @return the path from the bundle, or empty string if none is present
|
std::string getLastPath();
|
||||||
*/
|
|
||||||
std::string getResultPath(const Bundle& bundle);
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
#include <string>
|
||||||
|
|
||||||
namespace tt::app::imageviewer {
|
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
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
#include <cstdint>
|
||||||
#include <Tactility/Bundle.h>
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the app by its ID and provide:
|
* Show a dialog with a title, a message and a text field.
|
||||||
* - a title
|
|
||||||
* - a text
|
|
||||||
*/
|
*/
|
||||||
namespace tt::app::inputdialog {
|
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
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
#include <string>
|
||||||
|
|
||||||
namespace tt::app::notes {
|
namespace tt::app::notes {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the notes app with the specified text file.
|
* Start the notes app with the specified text file.
|
||||||
* @param[in] filePath the path to the text file to open
|
* @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
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
#include "app/instance.h"
|
||||||
#include <Tactility/Bundle.h>
|
|
||||||
|
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the app by its ID and provide:
|
* Show a dialog with a title and a list of selectable items.
|
||||||
* - 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
|
|
||||||
*/
|
*/
|
||||||
namespace tt::app::selectiondialog {
|
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.
|
* 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
|
||||||
* @return a value greater than 0 when a selection was done, or -1 when the app was closed without selecting an item.
|
* 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)
|
#if defined(CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED)
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace tt::app::touchcalibration {
|
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
|
} // namespace tt::app::touchcalibration
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace tt::app::wifimanage {
|
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
|
} // namespace
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/AppContext.h>
|
|
||||||
|
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace tt::lvgl {
|
namespace tt::lvgl {
|
||||||
|
|
||||||
constexpr auto STATUSBAR_ICON_LIMIT = 8;
|
constexpr auto STATUSBAR_ICON_LIMIT = 8;
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../app/AppContext.h"
|
|
||||||
|
|
||||||
#include <lvgl/widgets/toolbar.h>
|
#include <lvgl/widgets/toolbar.h>
|
||||||
|
|
||||||
namespace tt::lvgl {
|
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
|
} // 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 {
|
namespace tt::app::btmanage {
|
||||||
|
|
||||||
typedef void (*OnBtToggled)(bool enable);
|
// `context` is this app instance's Context* (see BtManagePrivate.h) - the new app-module has no
|
||||||
typedef void (*OnScanToggled)(bool enable);
|
// 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 (*OnConnectPeer)(const std::array<uint8_t, 6>& addr, int profileId);
|
||||||
typedef void (*OnDisconnectPeer)(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);
|
typedef void (*OnForgetPeer)(const std::array<uint8_t, 6>& addr);
|
||||||
|
|
||||||
struct Bindings {
|
struct Bindings {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include "./View.h"
|
#include "./View.h"
|
||||||
#include "./State.h"
|
#include "./State.h"
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
|
||||||
#include <Tactility/Mutex.h>
|
#include <Tactility/Mutex.h>
|
||||||
#include <Tactility/bluetooth/Bluetooth.h>
|
#include <Tactility/bluetooth/Bluetooth.h>
|
||||||
#include <tactility/drivers/bluetooth.h>
|
#include <tactility/drivers/bluetooth.h>
|
||||||
@@ -13,54 +12,37 @@
|
|||||||
|
|
||||||
namespace tt::app::btmanage {
|
namespace tt::app::btmanage {
|
||||||
|
|
||||||
class BtManage final : public App {
|
struct Context {
|
||||||
|
uint32_t appInstanceId;
|
||||||
Mutex mutex;
|
Mutex mutex;
|
||||||
Bindings bindings = { };
|
Bindings bindings {};
|
||||||
State state;
|
State state;
|
||||||
View view = View(&bindings, &state);
|
View view = View(&bindings, &state);
|
||||||
bool isViewEnabled = false;
|
|
||||||
Device* btDevice = nullptr;
|
Device* btDevice = nullptr;
|
||||||
bool callbackRegistered = false;
|
bool callbackRegistered = false;
|
||||||
|
|
||||||
// Bumped by onHide() to invalidate any BT event already dispatched to the main
|
// Bumped right before the BT event callback is unregistered at the end of appMain(), to
|
||||||
// task for this show/hide session (BtManage is reused across hide/show cycles -
|
// invalidate any BT event already dispatched to the main task for this instance. Kept in
|
||||||
// e.g. launching BtPeerSettings pushes it on top and hides this instance without
|
// its own heap allocation, independent of Context's (stack-local) lifetime, so a dispatched
|
||||||
// destroying it). Kept in its own heap allocation, independent of BtManage's
|
// callback can check it without touching a possibly already-destroyed Context.
|
||||||
// lifetime, so a dispatched callback can check it without touching a possibly
|
|
||||||
// already-destroyed `this`.
|
|
||||||
std::shared_ptr<std::atomic<int>> generation = std::make_shared<std::atomic<int>>(0);
|
std::shared_ptr<std::atomic<int>> generation = std::make_shared<std::atomic<int>>(0);
|
||||||
|
|
||||||
public:
|
void lock() { mutex.lock(); }
|
||||||
|
void unlock() { mutex.unlock(); }
|
||||||
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 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
|
} // namespace tt::app::btmanage
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
#include "./Bindings.h"
|
#include "./Bindings.h"
|
||||||
#include "./State.h"
|
#include "./State.h"
|
||||||
|
|
||||||
#include <Tactility/app/AppContext.h>
|
#include <cstdint>
|
||||||
#include <Tactility/app/AppPaths.h>
|
|
||||||
|
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
|
|
||||||
namespace tt::app::btmanage {
|
namespace tt::app::btmanage {
|
||||||
@@ -14,7 +12,9 @@ class View final {
|
|||||||
|
|
||||||
Bindings* bindings;
|
Bindings* bindings;
|
||||||
State* state;
|
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* root = nullptr;
|
||||||
lv_obj_t* enable_switch = nullptr;
|
lv_obj_t* enable_switch = nullptr;
|
||||||
lv_obj_t* enable_on_boot_switch = nullptr;
|
lv_obj_t* enable_on_boot_switch = nullptr;
|
||||||
@@ -34,7 +34,7 @@ public:
|
|||||||
|
|
||||||
View(Bindings* bindings, State* state) : bindings(bindings), state(state) {}
|
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();
|
void update();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,37 +10,32 @@
|
|||||||
#include "ChatView.h"
|
#include "ChatView.h"
|
||||||
#include "ChatSettings.h"
|
#include "ChatSettings.h"
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
|
||||||
#include <Tactility/service/espnow/EspNow.h>
|
#include <Tactility/service/espnow/EspNow.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace tt::app::chat {
|
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;
|
ChatState state;
|
||||||
ChatView view = ChatView(this, &state);
|
ChatView view = ChatView(this, &state);
|
||||||
service::espnow::ReceiverSubscription receiveSubscription = -1;
|
service::espnow::ReceiverSubscription receiveSubscription = -1;
|
||||||
ChatSettingsData settings;
|
ChatSettingsData settings;
|
||||||
bool isFirstLaunch = false;
|
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
|
} // namespace tt::app::chat
|
||||||
|
|
||||||
#endif // CONFIG_SOC_WIFI_SUPPORTED || CONFIG_SLAVE_SOC_WIFI_SUPPORTED
|
#endif // CONFIG_SOC_WIFI_SUPPORTED || CONFIG_SLAVE_SOC_WIFI_SUPPORTED
|
||||||
|
|||||||
@@ -9,18 +9,16 @@
|
|||||||
#include "ChatState.h"
|
#include "ChatState.h"
|
||||||
#include "ChatSettings.h"
|
#include "ChatSettings.h"
|
||||||
|
|
||||||
#include <Tactility/app/AppContext.h>
|
|
||||||
|
|
||||||
#include <esp_now.h>
|
#include <esp_now.h>
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
|
|
||||||
namespace tt::app::chat {
|
namespace tt::app::chat {
|
||||||
|
|
||||||
class ChatApp;
|
struct Context;
|
||||||
|
|
||||||
class ChatView {
|
class ChatView {
|
||||||
|
|
||||||
ChatApp* app;
|
Context* app;
|
||||||
ChatState* state;
|
ChatState* state;
|
||||||
|
|
||||||
lv_obj_t* toolbar = nullptr;
|
lv_obj_t* toolbar = nullptr;
|
||||||
@@ -45,6 +43,7 @@ class ChatView {
|
|||||||
|
|
||||||
static void addMessageToList(lv_obj_t* msgList, const StoredMessage& msg);
|
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 onSendClicked(lv_event_t* e);
|
||||||
static void onSettingsClicked(lv_event_t* e);
|
static void onSettingsClicked(lv_event_t* e);
|
||||||
static void onSettingsSave(lv_event_t* e);
|
static void onSettingsSave(lv_event_t* e);
|
||||||
@@ -54,7 +53,7 @@ class ChatView {
|
|||||||
static void onChannelCancel(lv_event_t* e);
|
static void onChannelCancel(lv_event_t* e);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ChatView(ChatApp* app, ChatState* state) : app(app), state(state) {}
|
ChatView(Context* app, ChatState* state) : app(app), state(state) {}
|
||||||
~ChatView() = default;
|
~ChatView() = default;
|
||||||
|
|
||||||
ChatView(const ChatView&) = delete;
|
ChatView(const ChatView&) = delete;
|
||||||
@@ -62,7 +61,7 @@ public:
|
|||||||
ChatView(ChatView&&) = delete;
|
ChatView(ChatView&&) = delete;
|
||||||
ChatView& operator=(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 displayMessage(const StoredMessage& msg);
|
||||||
void refreshMessageList();
|
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 "./State.h"
|
||||||
|
|
||||||
#include <Tactility/app/AppManifest.h>
|
#include <cstdint>
|
||||||
|
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@@ -11,6 +10,7 @@ namespace tt::app::files {
|
|||||||
|
|
||||||
class View final {
|
class View final {
|
||||||
std::shared_ptr<State> state;
|
std::shared_ptr<State> state;
|
||||||
|
uint32_t appInstanceId = 0;
|
||||||
|
|
||||||
size_t current_start_index = 0;
|
size_t current_start_index = 0;
|
||||||
size_t last_loaded_index = 0;
|
size_t last_loaded_index = 0;
|
||||||
@@ -24,7 +24,7 @@ class View final {
|
|||||||
lv_obj_t* paste_button = nullptr;
|
lv_obj_t* paste_button = nullptr;
|
||||||
|
|
||||||
std::string installAppPath = { 0 };
|
std::string installAppPath = { 0 };
|
||||||
LaunchId installAppLaunchId = 0;
|
uint32_t installDialogId = 0;
|
||||||
|
|
||||||
void showActions();
|
void showActions();
|
||||||
void showActionsForDirectory();
|
void showActionsForDirectory();
|
||||||
@@ -39,9 +39,10 @@ public:
|
|||||||
|
|
||||||
explicit View(const std::shared_ptr<State>& state) : state(state) {}
|
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 update(size_t start_index = 0);
|
||||||
|
|
||||||
|
void onBackPressed();
|
||||||
void onNavigateUpPressed();
|
void onNavigateUpPressed();
|
||||||
void onDirEntryPressed(uint32_t index);
|
void onDirEntryPressed(uint32_t index);
|
||||||
void onDirEntryLongPressed(int32_t index);
|
void onDirEntryLongPressed(int32_t index);
|
||||||
@@ -54,8 +55,8 @@ public:
|
|||||||
void onPastePressed();
|
void onPastePressed();
|
||||||
void onEjectPressed();
|
void onEjectPressed();
|
||||||
void onDirEntryListScrollBegin();
|
void onDirEntryListScrollBegin();
|
||||||
void onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle);
|
void onResult(uint32_t launchId, int32_t result);
|
||||||
void deinit(const AppContext& appContext);
|
void deinit();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/Bundle.h>
|
|
||||||
|
|
||||||
namespace tt::app::fileselection {
|
namespace tt::app::fileselection {
|
||||||
|
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
@@ -9,6 +7,4 @@ enum class Mode {
|
|||||||
ExistingOrNew = 1
|
ExistingOrNew = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
Mode getMode(const Bundle& bundle);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
#include "./State.h"
|
#include "./State.h"
|
||||||
#include "./FileSelectionPrivate.h"
|
#include "./FileSelectionPrivate.h"
|
||||||
|
|
||||||
#include <Tactility/app/AppManifest.h>
|
|
||||||
|
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
namespace tt::app::fileselection {
|
namespace tt::app::fileselection {
|
||||||
|
|
||||||
class View final {
|
class View final {
|
||||||
|
uint32_t appInstanceId;
|
||||||
std::shared_ptr<State> state;
|
std::shared_ptr<State> state;
|
||||||
|
|
||||||
lv_obj_t* dir_entry_list = nullptr;
|
lv_obj_t* dir_entry_list = nullptr;
|
||||||
@@ -22,11 +21,15 @@ class View final {
|
|||||||
void onTapFile(const std::string&path, const std::string&filename);
|
void onTapFile(const std::string&path, const std::string&filename);
|
||||||
static void onSelectButtonPressed(lv_event_t* event);
|
static void onSelectButtonPressed(lv_event_t* event);
|
||||||
static void onPathTextChanged(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);
|
void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry);
|
||||||
|
|
||||||
public:
|
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),
|
state(state),
|
||||||
on_file_selected(std::move(onFileSelected))
|
on_file_selected(std::move(onFileSelected))
|
||||||
{}
|
{}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace tt::app::i2cscanner {
|
namespace tt::app::i2cscanner {
|
||||||
|
|
||||||
LaunchId start();
|
uint32_t start();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
|
||||||
|
|
||||||
namespace tt::app::launcher {
|
namespace tt::app::launcher {
|
||||||
|
|
||||||
LaunchId start();
|
uint32_t start();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
// 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
|
||||||
namespace tt::app::localesettings {
|
// header is kept as a placeholder in case that changes; nothing currently includes it.
|
||||||
|
|
||||||
LaunchId start();
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
|
||||||
|
|
||||||
namespace tt::app::setup {
|
namespace tt::app::setup {
|
||||||
|
|
||||||
LaunchId start();
|
void start();
|
||||||
|
|
||||||
/** @return true if the setup wizard has already run to completion */
|
/** @return true if the setup wizard has already run to completion */
|
||||||
bool isCompleted();
|
bool isCompleted();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Tactility/app/App.h>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace tt::app::timedatesettings {
|
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