Fixes and improvements (#616)

This commit is contained in:
Ken Van Hoeylandt 2026-08-18 20:43:07 +02:00 committed by GitHub
parent f943c4dd69
commit b03759a111
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
141 changed files with 899 additions and 221 deletions

View File

@ -41,6 +41,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
"Modules"
"Platforms/platform-esp32"
"TactilityKernel"
"TactilityKernelCpp"
"Tactility"
"TactilityC"
"TactilityFreeRtos"
@ -77,7 +78,7 @@ else ()
add_compile_definitions(CONFIG_TT_DEVICE_NAME="Simulator")
add_compile_definitions(CONFIG_TT_DEVICE_VENDOR="")
add_compile_definitions(CONFIG_TT_DEVICE_NAME_SIMPLE="Simulator")
add_compile_definitions(CONFIG_TT_LAUNCHER_APP_ID="Launcher")
add_compile_definitions(CONFIG_TT_LAUNCHER_APP_ID="tactility.launcher")
add_compile_definitions(CONFIG_TT_AUTO_START_APP_ID="")
add_compile_definitions(CONFIG_TT_USER_DATA_LOCATION_INTERNAL)
endif ()
@ -89,6 +90,7 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Tactility)
add_subdirectory(TactilityFreeRtos)
add_subdirectory(TactilityKernel)
add_subdirectory(TactilityKernelCpp)
add_subdirectory(Platforms/platform-posix)
add_subdirectory(Devices/simulator)
add_subdirectory(Libraries/cJSON)

View File

@ -1,7 +1,7 @@
general.vendor=BigTreeTech
general.name=Panda Touch,K Touch
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=CL-32
general.name=CL-32
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=8MB

View File

@ -1,7 +1,7 @@
general.vendor=CYD
general.name=2432S024C
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=CYD
general.name=2432S024R
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=CYD
general.name=2432S028R
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=CYD
general.name=2432S028R v3
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=CYD
general.name=2432S032C
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=CYD
general.name=3248S035C
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=CYD
general.name=4848S040C
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -2,7 +2,7 @@ general.vendor=CYD
general.name=8048S043C
general.incubating=false
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=CYD
general.name=E32R28T
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=CYD
general.name=E32R32P
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=Elecrow
general.name=CrowPanel Advance 2.8"
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=Elecrow
general.name=CrowPanel Advance 3.5"
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=Elecrow
general.name=CrowPanel Advance 5.0"
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=Elecrow
general.name=CrowPanel Basic 2.8"
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=Elecrow
general.name=CrowPanel Basic 3.5"
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=Elecrow
general.name=CrowPanel Basic 5.0"
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=CYD
general.name=ES3C28P
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=Generic
general.name=ESP32
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=8MB

View File

@ -1,7 +1,7 @@
general.vendor=Generic
general.name=ESP32-C6
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32C6
hardware.flashSize=8MB

View File

@ -1,7 +1,7 @@
general.vendor=Generic
general.name=ESP32-P4
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32P4
hardware.flashSize=8MB

View File

@ -1,7 +1,7 @@
general.vendor=Generic
general.name=ESP32-S3
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=8MB

View File

@ -1,7 +1,7 @@
general.vendor=Guition
general.name=JC1060P470C-I-W-Y
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32P4
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=Guition
general.name=JC2432W328C
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=4MB

View File

@ -1,7 +1,7 @@
general.vendor=Guition
general.name=JC8048W550C
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -2,8 +2,8 @@ general.vendor=Heltec
general.name=WiFi LoRa 32 v3
general.incubating=true
apps.launcherAppId=Launcher
apps.autoStartAppId=ApWebServer
apps.launcherAppId=tactility.launcher
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32S3
hardware.flashSize=8MB

View File

@ -3,7 +3,7 @@ general.name=T-Deck Max
# SD card support is not yet working, display driver is untested since refactoring; remove once it is functional.
general.incubating=true
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=LilyGO
general.name=T-Deck Plus
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -3,7 +3,7 @@ general.name=T-Deck Pro
# The display driver is not reliable yet
general.incubating=true
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=LilyGO
general.name=T-Deck
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,8 +1,8 @@
general.vendor=LilyGO
general.name=T-Display S3
apps.launcherAppId=Launcher
apps.autoStartAppId=ApWebServer
apps.launcherAppId=tactility.launcher
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -2,8 +2,8 @@ general.vendor=LilyGO
general.name=T-Display
general.incubating=true
apps.launcherAppId=Launcher
apps.autoStartAppId=ApWebServer
apps.launcherAppId=tactility.launcher
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32
hardware.flashSize=16MB

View File

@ -2,8 +2,8 @@ general.vendor=LilyGO
general.name=T-Dongle S3
general.incubating=true
apps.launcherAppId=Launcher
apps.autoStartAppId=ApWebServer
apps.launcherAppId=tactility.launcher
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=LilyGO
general.name=T-HMI
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=LilyGO
general.name=T-Lora Pager
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=M5Stack
general.name=Cardputer Adv
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=8MB

View File

@ -1,7 +1,7 @@
general.vendor=M5Stack
general.name=Cardputer,Cardputer v1.1
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=8MB

View File

@ -1,7 +1,7 @@
general.vendor=M5Stack
general.name=Core2
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=M5Stack
general.name=CoreS3
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -2,7 +2,7 @@ general.vendor=M5Stack
general.name=PaperS3
general.incubating=true
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=esp32s3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=M5Stack
general.name=StackChan
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=M5Stack
general.name=StickC Plus2
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32
hardware.flashSize=8MB

View File

@ -1,7 +1,7 @@
general.vendor=M5Stack
general.name=StickS3
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=8MB

View File

@ -1,7 +1,7 @@
general.vendor=M5Stack
general.name=Tab5
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32P4
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=Simulator
general.name=Tab5Simulator
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=POSIX

View File

@ -1,7 +1,7 @@
general.vendor=Tulip
general.name=Tulip Creative Computer (4r11)
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=32MB

View File

@ -1,7 +1,7 @@
general.vendor=unPhone
general.name=unPhone
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=8MB

View File

@ -2,8 +2,8 @@ general.vendor=Waveshare
general.name=ESP32 S3 GEEK
general.incubating=true
apps.launcherAppId=Launcher
apps.autoStartAppId=ApWebServer
apps.launcherAppId=tactility.launcher
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -2,8 +2,8 @@ general.vendor=WaveShare
general.name=S3 LCD 1.3"
general.incubating=true
apps.launcherAppId=Launcher
apps.autoStartAppId=ApWebServer
apps.launcherAppId=tactility.launcher
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -2,8 +2,8 @@ general.vendor=WaveShare
general.name=S3 Touch LCD 1.28"
general.incubating=true
apps.launcherAppId=Launcher
apps.autoStartAppId=ApWebServer
apps.launcherAppId=tactility.launcher
apps.autoStartAppId=tactility.apwebserver
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -2,7 +2,7 @@ general.vendor=WaveShare
general.name=S3 Touch LCD 1.47"
general.incubating=true
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -1,7 +1,7 @@
general.vendor=WaveShare
general.name=S3 Touch LCD 4.3"
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
# Some models have 16MB flash, but some have 8MB. We only support the latter to avoid making more builds.

View File

@ -1,7 +1,7 @@
general.vendor=Wireless Tag
general.name=WT32 SC01 Plus
apps.launcherAppId=Launcher
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB

View File

@ -11,6 +11,7 @@
## Higher Priority
- AppHubApp: Prevent download callbacks from accessing a destroyed view.
- Move USB host task stacks to SPIRAM when available: esp32_usbhost*.cpp
- wifi: wifi_add_event_callback() and wifi_remove_event_callback() should be replaced by a subscribe/await pattern like system events.
When that's changed reduce LVGL callstack size in Tactility.cpp run()

View File

@ -1,11 +1,6 @@
# Audio codec module
Defines codec-agnostic `AudioCodecApi` (open/close/read/write/volume/mute/native-rate/native-channels/capabilities)
that every audio codec driver in this repo implements (ES8311, ES8388, ES7210, AW88298, the dummy I2S amps, the PDM mic).
This is intentionally low-level: no resampling or mixing, one codec device per chip. Most app code should go through
`audio_stream` (see `Drivers/audio-stream-module`) instead of talking to a codec device directly.
Also provides `audio_codec_adapters.h`: glue that lets a codec driver build an `esp_codec_dev` control/data/GPIO
Provides `audio_codec_adapters.h`: glue that lets a codec driver build an `esp_codec_dev` control/data/GPIO
interface (`audio_codec_ctrl_if_t` / `audio_codec_data_if_t` / `audio_codec_gpio_if_t`) directly from Tactility's
own I2C controller, I2S controller, and GPIO pin devices, so a codec driver only ever needs
`device_get_parent()`/`device_find_by_name()` plus a handful of devicetree properties -- it never touches the platform

View File

@ -23,7 +23,7 @@ menu "Tactility App"
See https://docs.tactilityproject.org for formatting guidance.
config TT_LAUNCHER_APP_ID
string "Launcher App ID"
default "Launcher"
default "tactility.launcher"
help
The applications that gives access to other application.
This is the first thing that starts after the boot screen.

View File

@ -13,6 +13,7 @@ extern "C" {
#define APP_METADATA_APP_ID_LENGTH 32
#define APP_METADATA_APP_NAME_LENGTH 32
#define APP_METADATA_APP_VERSION_NAME_LENGTH 16
#define APP_METADATA_REQUIRES_DEVICE_ID_LENGTH 64
struct AppMetadata {
@ -42,6 +43,13 @@ struct AppMetadata {
/** The technical version (must be incremented with new releases of the app) */
uint64_t app_version_code;
/**
* Comma-separated list of device ids the app is restricted to (e.g. "m5stack-tab5"), matching
* the folder names under Devices/. Empty means unrestricted.
* Must be NULL-terminated.
*/
char requires_device_id[APP_METADATA_REQUIRES_DEVICE_ID_LENGTH + 1];
};
/**

View File

@ -18,6 +18,9 @@ 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);
/** Validates a comma-separated list of device ids (alphanumeric + '-' items, matching Devices/<id> folder names). */
bool app_metadata_is_valid_device_id_list(const std::string& value);
/** 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 */

View File

@ -37,6 +37,26 @@ bool validate_string(const std::string& value, bool (*is_valid_char)(char)) {
return true;
}
/** Validates a comma-separated list: non-empty, no leading/trailing/double commas (which would
* produce an empty item), and every item passing @a is_valid_item. */
bool validate_csv_list(const std::string& value, bool (*is_valid_item)(const std::string&)) {
if (value.empty()) {
return false;
}
size_t start = 0;
while (true) {
auto comma = value.find(',', start);
auto end = comma == std::string::npos ? value.size() : comma;
if (end == start || !is_valid_item(value.substr(start, end - start))) {
return false;
}
if (comma == std::string::npos) {
return true;
}
start = comma + 1;
}
}
/** 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() -
@ -130,6 +150,20 @@ bool app_metadata_is_valid_version_code(const std::string& version) {
});
}
bool app_metadata_is_valid_device_id_list(const std::string& value) {
return validate_csv_list(value, [](const std::string& item) {
bool has_alnum = false;
for (char c: item) {
if (std::isalnum(static_cast<unsigned char>(c)) != 0) {
has_alnum = true;
} else if (c != '-') {
return false;
}
}
return has_alnum;
});
}
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value) {
if (value.size() >= dest_size) {
return false;
@ -141,6 +175,10 @@ bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string&
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata) {
LOG_I(TAG, "Parsing manifest %s", path);
// requires_device_id is optional in V2 and unwritten by V1; zeroing here (rather than relying
// on the caller) guarantees it reads back as empty ("unrestricted") either way.
*out_metadata = {};
std::map<std::string, std::string> properties;
std::string first_line;
if (!load_properties(path, properties, first_line)) {

View File

@ -99,5 +99,20 @@ bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties,
return false;
}
// requires.device.id (optional; if present, must be a non-empty comma-separated list)
auto device_id_iterator = properties.find("requires.device.id");
if (device_id_iterator != properties.end()) {
const std::string& device_id = device_id_iterator->second;
if (!app_metadata_is_valid_device_id_list(device_id)) {
LOG_E(TAG, "Invalid requires.device.id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.requires_device_id, sizeof(out_metadata.requires_device_id), device_id)) {
LOG_E(TAG, "requires.device.id too long");
return false;
}
}
return true;
}

View File

@ -13,7 +13,7 @@ error_t app_paths_get_user_data_directory(const char* app_id, char* out_path, si
if (error != ERROR_NONE) {
return error;
}
int written = std::snprintf(out_path, out_path_size, "%s/app/%s", root, app_id);
int written = std::snprintf(out_path, out_path_size, "%s/user/app/%s", root, app_id);
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}

View File

@ -17,7 +17,7 @@ constexpr auto* TAG = "gps_settings";
// Storage key for the persisted configuration file (services would use their own service ID for
// this; gps_settings has no service backing it, so it defines its own).
constexpr auto* GPS_SETTINGS_STORAGE_ID = "gps";
constexpr auto* GPS_SETTINGS_STORAGE_ID = "tactility.gps";
// region Configuration persistence

View File

@ -4,9 +4,12 @@
#include <lvgl/lvgl.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/log.h>
#include <vector>
constexpr auto* TAG = "lvgl_keyboard";
static LvglSoftwareKeyboard last_software_keyboard = {
.object = nullptr
};
@ -106,16 +109,23 @@ void lvgl_keyboard_disable(lv_indev_t* indev) {
lv_indev_set_group(indev, nullptr);
}
bool lvgl_hardware_keyboard_is_available() {
Device* keyboard_device;
if (device_get_first_active_by_type(&KEYBOARD_TYPE, &keyboard_device) != ERROR_NONE) {
return false;
static bool lvgl_hardware_keyboard_check_present(Device* device, void* context) {
bool ready = device_is_ready(device);
bool present = ready && keyboard_is_present(device);
LOG_D(TAG, "keyboard device %s: ready=%d present=%d", device->name, (int)ready, (int)present);
if (!present) {
return true; // keep looking
}
*static_cast<bool*>(context) = true;
return false; // found one, stop iterating
}
bool lvgl_hardware_keyboard_is_available() {
// 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);
bool present = false;
device_for_each_of_type(&KEYBOARD_TYPE, &present, lvgl_hardware_keyboard_check_present);
LOG_D(TAG, "lvgl_hardware_keyboard_is_available() -> %d", (int)present);
return present;
}
@ -138,6 +148,11 @@ void lvgl_hardware_keyboard_remove_custom(lv_indev_t* indev) {
}
static void textarea_show_keyboard(lv_event_t* event) {
// Re-checked here rather than gated once at lvgl_keyboard_add_textarea() time, so a hardware
// keyboard that connects/disconnects after the textarea was created is honored immediately.
if (!lvgl_software_keyboard_is_enabled()) {
return;
}
lv_obj_t* target = lv_event_get_current_target_obj(event);
if (last_software_keyboard.object != nullptr) {
lvgl_software_keyboard_show(&last_software_keyboard, target);
@ -192,18 +207,10 @@ LvglSoftwareKeyboard* lvgl_software_keyboard_get_last() {
}
void lvgl_keyboard_add_textarea(LvglSoftwareKeyboard* keyboard, lv_obj_t* textarea) {
// Only the on-screen keyboard's show/hide wiring is specific to "no hardware keyboard"
// mode. Group membership must NOT be gated on it: a hardware keypad indev (see
// lvgl_keyboard_enable()/lvgl_software_keyboard_activate()) is bound to keyboard_group
// regardless of whether a software keyboard is in use, so skipping lv_group_add_obj()
// here left every textarea unreachable from a hardware keyboard - it was never a member
// of the group its indev delivers key events through.
if (lvgl_software_keyboard_is_enabled()) {
lv_obj_add_event_cb(textarea, textarea_show_keyboard, LV_EVENT_FOCUSED, nullptr);
lv_obj_add_event_cb(textarea, textarea_hide_keyboard, LV_EVENT_DEFOCUSED, nullptr);
lv_obj_add_event_cb(textarea, textarea_hide_keyboard, LV_EVENT_READY, nullptr);
lv_obj_add_event_cb(textarea, textarea_hide_keyboard, LV_EVENT_DELETE, 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_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);

View File

@ -35,11 +35,10 @@ static constexpr size_t COMPOSITE_STRING_DESCRIPTOR_MAX = 6;
// ---- Controller state ----
// One TinyUSB device-mode slot, shared by every USB device class (each a separate child device
// under usbdevice0 in the devicetree - see esp32_usb_hid_device.cpp / esp32_usb_device_msc.cpp /
// esp32_usb_midi_device.cpp). Only one primary class may be installed at a time - see
// UsbDeviceControllerApi::claim(). CDC (esp32_usb_cdc_device.cpp) is a separate, orthogonal
// devicetree-presence addon composited into whichever primary is active (HID or MIDI; MSC is
// deliberately excluded, see claim()'s cdc_enabled computation) - see claim() below.
// under usbdevice0 in the devicetree; \see esp32_usb_hid_device.cpp, esp32_usb_device_msc.cpp,
// esp32_usb_midi_device.cpp). Only one primary class may be installed at a time, enforced in
// claim(). CDC (esp32_usb_cdc_device.cpp) is a separate, orthogonal devicetree-presence addon
// composited into whichever primary is active; \see claim()'s cdc_enabled computation.
struct UsbDeviceControllerCtx {
enum UsbDeviceClass active_class = USB_DEVICE_CLASS_NONE;
@ -53,10 +52,10 @@ struct UsbDeviceControllerCtx {
uint8_t next_in_endpoint = 1; // EP0 reserved
uint8_t next_out_endpoint = 1;
// Composite descriptor scratch, rebuilt on every claim() - can't be `static const` per-class
// Composite descriptor scratch, rebuilt on every claim(); can't be `static const` per-class
// anymore now that CDC's presence is a runtime devicetree fact, not compile-time. Two buffers
// since a primary (MSC) may need different bytes per speed (see UsbInterfaceContribution's
// hs_descriptor_bytes) - under !TUD_OPT_HIGH_SPEED only the fs buffer is ever used.
// since a primary (MSC) may need different bytes per speed (\see UsbInterfaceContribution's
// hs_descriptor_bytes). Under !TUD_OPT_HIGH_SPEED only the fs buffer is ever used.
uint8_t composite_fs_config_descriptor[COMPOSITE_CONFIG_DESCRIPTOR_MAX];
#if (TUD_OPT_HIGH_SPEED)
uint8_t composite_hs_config_descriptor[COMPOSITE_CONFIG_DESCRIPTOR_MAX];
@ -101,7 +100,7 @@ static bool find_cdc_child(struct Device* child, void* context) {
}
// Devicetree children are constructed/added/started strictly after their parent
// (kernel_init.cpp's dts_devices[] loop starts each device in list order, parent first) - so the
// (kernel_init.cpp's dts_devices[] loop starts each device in list order, parent first), so the
// usbdevicecdc0 child does not exist yet when this controller's own start_device() runs.
// Discovering it lazily here (called from is_cdc_enabled(), well after all devicetree devices
// have finished starting) instead of once at start_device() time is the only way to actually see
@ -168,14 +167,7 @@ static error_t claim(struct Device* device, enum UsbDeviceClass usb_class, const
return ERROR_INVALID_STATE;
}
// MSC is deliberately excluded from CDC compositing - a MSC-presenting device is meant to
// look/behave like plain mass storage to the host's storage stack, and there's no real use
// case for a console interface while acting as a flash drive, unlike HID/MIDI which
// plausibly want a console alongside. (A Windows safe-eject hang was seen during development
// of this compositing code but traced to a stale Windows-side driver/device-cache state,
// cleared by a host reboot - not caused by CDC compositing or this exclusion. Kept the
// exclusion anyway since it matches MSC's actual intended behavior.)
const bool cdc_enabled = usb_class != USB_DEVICE_CLASS_MSC && is_cdc_enabled(device);
const bool cdc_enabled = is_cdc_enabled(device);
// ---- String table: primary's table, plus CDC's own interface string appended last.
size_t string_count = config->string_descriptor_count;
@ -207,7 +199,7 @@ static error_t claim(struct Device* device, enum UsbDeviceClass usb_class, const
// ---- Descriptor byte assembly: primary's bytes, then CDC's (if enabled), behind one config
// header. Built twice (fs/hs) under TUD_OPT_HIGH_SPEED if the primary supplied distinct
// high-speed bytes (see UsbInterfaceContribution::hs_descriptor_bytes) - CDC never varies by
// high-speed bytes (\see UsbInterfaceContribution::hs_descriptor_bytes). CDC never varies by
// speed, so its bytes are reused verbatim in both buffers.
auto assemble = [&](uint8_t* dest, size_t dest_capacity, const uint8_t* primary_bytes, size_t primary_len) -> error_t {
const size_t total_len = TUD_CONFIG_DESC_LEN + primary_len + (cdc_enabled ? cdc_contribution.descriptor_bytes_len : 0);
@ -250,13 +242,6 @@ static error_t claim(struct Device* device, enum UsbDeviceClass usb_class, const
ctx->allocation_open = false;
// ---- Device descriptor: primary's metadata (idVendor/idProduct/strings), controller decides
// the class triad since CDC's presence (not the primary's identity) is what needs IAD in the
// general case - except MSC, which always got MISC/IAD unconditionally even standalone in
// the pre-refactor code (see esp32_usb_device_msc.cpp history). Kept that behavior verbatim
// for MSC (regardless of cdc_enabled, which is always false for MSC anyway - see the
// cdc_enabled computation above) simply to match what shipped before rather than introduce
// an unrelated behavior change while separating CDC out of HID's descriptor.
ctx->composite_device_descriptor = *static_cast<const tusb_desc_device_t*>(config->device_descriptor);
if (cdc_enabled || usb_class == USB_DEVICE_CLASS_MSC) {
ctx->composite_device_descriptor.bDeviceClass = TUSB_CLASS_MISC;
@ -277,7 +262,7 @@ static error_t claim(struct Device* device, enum UsbDeviceClass usb_class, const
ctx->composite_tusb_cfg.fs_configuration_descriptor = ctx->composite_fs_config_descriptor;
ctx->composite_tusb_cfg.hs_configuration_descriptor = ctx->composite_hs_config_descriptor;
// The qualifier descriptor's class triad must mirror the device descriptor's (same IAD
// reasoning) - built here rather than supplied per-primary since its content is boilerplate
// reasoning). Built here rather than supplied per-primary since its content is boilerplate
// (same bMaxPacketSize0/bNumConfigurations for every class) and the controller already knows
// the class triad.
ctx->composite_device_qualifier = {
@ -366,7 +351,7 @@ extern const UsbDeviceControllerApi usb_device_controller_api = {
};
// ---- Driver lifecycle ----
// This device is defined in each board's .dts (usbdevice0), same pattern as usbhost0 - its
// This device is defined in each board's .dts (usbdevice0), same pattern as usbhost0. Its
// child classes (usbdevicehid0, usbdevicemsc0, usbdevicemidi0, usbdevicecdc0) are separate .dts
// nodes with their own drivers, wired to this device as their parent by the devicetree compiler.
@ -377,19 +362,19 @@ static error_t start_device(struct Device* device) {
auto* ctx = new UsbDeviceControllerCtx();
device_set_driver_data(device, ctx);
// cdc_child is NOT resolved here: usbdevicecdc0 (a child of this device in the devicetree)
// hasn't been constructed/started yet at this point - kernel_init.cpp starts devicetree
// devices in list order, parent before children. Resolved lazily instead, see
// find_cdc_child_lazy() / is_cdc_enabled().
// hasn't been constructed/started yet at this point; kernel_init.cpp starts devicetree
// devices in list order, parent before children.
// \see find_cdc_child_lazy(), is_cdc_enabled() for the lazy resolution.
return ERROR_NONE;
}
static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbDeviceControllerCtx*>(device_get_driver_data(device));
// Safety cleanup: if a class still holds the slot (e.g. this device is stopped while HID/
// MSC/MIDI is still active), tear TinyUSB down and restore the PHY route before freeing ctx -
// MSC/MIDI is still active), tear TinyUSB down and restore the PHY route before freeing ctx;
// otherwise the installed TinyUSB driver and mis-routed PHY would outlive the context that
// tracks them. Mirrors the same safety-release pattern each child driver's own stop_device
// uses (see esp32_usb_hid_device.cpp / esp32_usb_device_msc.cpp / esp32_usb_midi_device.cpp).
// tracks them. Mirrors the same safety-release pattern each child driver's own stop_device uses.
// \see esp32_usb_hid_device.cpp, esp32_usb_device_msc.cpp, esp32_usb_midi_device.cpp
if (ctx->active_class != USB_DEVICE_CLASS_NONE) {
release(device, ctx->active_class);
}

View File

@ -3,6 +3,7 @@
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/keyboard.h>
#include <tactility/drivers/usb_host_hid.h>
#include <tactility/log.h>
@ -26,6 +27,7 @@ constexpr auto HID_PROC_TASK_STACK = 4096;
constexpr auto HID_PROC_TASK_PRIORITY = 5;
constexpr auto HID_STOP_TIMEOUT_MS = 2000;
constexpr auto MAX_SUBSCRIBERS = 4;
constexpr auto USB_HID_KB_QUEUE_SIZE = 16;
typedef struct {
hid_host_device_handle_t handle;
@ -53,8 +55,20 @@ struct UsbHidContext {
QueueHandle_t subscribers[MAX_SUBSCRIBERS] = {};
SemaphoreHandle_t sub_mutex = nullptr;
// The controller Device itself (set in start_device()), used as the parent for kb_device.
Device* controller_device = nullptr;
// Dynamic KEYBOARD_TYPE child device, constructed while a physical USB keyboard is connected.
Device kb_device{};
bool kb_device_active = false;
};
extern "C" {
static void usb_hid_keyboard_device_construct(UsbHidContext* ctx);
static void usb_hid_keyboard_device_destruct(UsbHidContext* ctx);
static void usb_hid_keyboard_publish_key(UsbHidContext* ctx, uint32_t lv_key, bool pressed, bool ctrl, bool alt, uint8_t hid_keycode, uint8_t hid_modifier);
}
static const uint8_t keycode2ascii[57][2] = {
{0, 0}, {0, 0}, {0, 0}, {0, 0},
{'a', 'A'}, {'b', 'B'}, {'c', 'C'}, {'d', 'D'}, {'e', 'E'},
@ -182,8 +196,7 @@ static void hid_interface_callback(hid_host_device_handle_t handle,
uint32_t lv_key = ctx->pressed_lv_keys[prev_hid];
ctx->pressed_lv_keys[prev_hid] = 0;
if (lv_key) {
UsbHidEvent evt = { .type = USB_HID_EVENT_KEY, .key = { lv_key, false, with_ctrl, with_alt } };
publish_event(ctx, &evt);
usb_hid_keyboard_publish_key(ctx, lv_key, false, with_ctrl, with_alt, prev_hid, kb->modifier.val);
}
}
}
@ -221,11 +234,7 @@ static void hid_interface_callback(hid_host_device_handle_t handle,
uint32_t lv_key = hid_keycode_to_key(kb->modifier.val, hid_code,
ctx->caps_lock_active, ctx->num_lock_active);
if (lv_key) {
UsbHidEvent evt = {
.type = USB_HID_EVENT_KEY,
.key = { lv_key, true, with_ctrl, with_alt }
};
publish_event(ctx, &evt);
usb_hid_keyboard_publish_key(ctx, lv_key, true, with_ctrl, with_alt, hid_code, kb->modifier.val);
ctx->pressed_lv_keys[hid_code] = lv_key;
}
}
@ -271,6 +280,7 @@ static void hid_interface_callback(hid_host_device_handle_t handle,
memset(ctx->pressed_lv_keys, 0, sizeof(ctx->pressed_lv_keys));
ctx->kb_handle.store(nullptr);
ctx->kb_led_pending.store(false);
usb_hid_keyboard_device_destruct(ctx);
} else if (params.proto == HID_PROTOCOL_MOUSE) {
ctx->mouse_connected = false;
}
@ -349,6 +359,7 @@ static void hid_proc_task(void* arg) {
| (ctx->caps_lock_active ? 0x02 : 0)
| (ctx->scroll_lock_active ? 0x04 : 0);
hid_class_request_set_report(dev_evt.handle, HID_REPORT_TYPE_OUTPUT, 0, &leds, 1);
usb_hid_keyboard_device_construct(ctx);
} else if (params.proto == HID_PROTOCOL_MOUSE) {
ctx->mouse_connected = true;
}
@ -419,8 +430,122 @@ static const UsbHidApi hid_api = {
extern "C" {
// region Dynamic KEYBOARD_TYPE device
//
// While a physical USB keyboard is connected, a KEYBOARD_TYPE child device is constructed so the
// rest of the system (lvgl_hardware_keyboard_is_available(), Tactility's KeyboardDeviceListener)
// sees a real hardware keyboard through the same generic device model as any other keyboard, e.g.
// Devices/m5stack-tab5/Source/devices/tab5_keyboard.cpp. Real key events are delivered exclusively
// through this device (kb_handle's hid_interface_callback pushes into its queue below); the
// generic UsbHidEvent publish/subscribe channel above is unaffected for mouse move/button/scroll
// and the right-click-as-ESC synthesis it already does.
static error_t usb_hid_kb_device_start(Device* device) {
auto* queue = xQueueCreate(USB_HID_KB_QUEUE_SIZE, sizeof(KeyboardKeyData));
if (queue == nullptr) {
return ERROR_RESOURCE;
}
device_set_driver_data(device, queue);
return ERROR_NONE;
}
static error_t usb_hid_kb_device_stop(Device* device) {
auto* queue = static_cast<QueueHandle_t>(device_get_driver_data(device));
vQueueDelete(queue);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
static error_t usb_hid_kb_device_read_key(Device* device, KeyboardKeyData* data) {
auto* queue = static_cast<QueueHandle_t>(device_get_driver_data(device));
if (queue == nullptr) {
*data = {};
return ERROR_NONE;
}
if (xQueueReceive(queue, data, 0) != pdTRUE) {
*data = {};
return ERROR_NONE;
}
data->continue_reading = uxQueueMessagesWaiting(queue) > 0;
return ERROR_NONE;
}
static const KeyboardApi esp32_usbhost_hid_keyboard_api = {
.read_key = usb_hid_kb_device_read_key,
.get_backlight = nullptr,
.is_present = nullptr,
};
Driver esp32_usbhost_hid_keyboard_driver = {
.name = "esp32_usbhost_hid_keyboard",
.compatible = (const char*[]) { nullptr },
.start_device = usb_hid_kb_device_start,
.stop_device = usb_hid_kb_device_stop,
.api = &esp32_usbhost_hid_keyboard_api,
.device_type = &KEYBOARD_TYPE,
.owner = nullptr,
.internal = nullptr,
};
static void usb_hid_keyboard_device_construct(UsbHidContext* ctx) {
if (ctx->kb_device_active) {
return;
}
ctx->kb_device = Device {
.address = 0,
.name = "usb_keyboard0",
.config = nullptr,
.parent = nullptr,
.internal = nullptr,
};
if (device_construct(&ctx->kb_device) != ERROR_NONE) {
LOG_E(TAG, "failed to construct USB keyboard device");
return;
}
device_set_parent(&ctx->kb_device, ctx->controller_device);
device_set_driver(&ctx->kb_device, &esp32_usbhost_hid_keyboard_driver);
if (device_add(&ctx->kb_device) != ERROR_NONE) {
LOG_E(TAG, "failed to add USB keyboard device");
device_destruct(&ctx->kb_device);
return;
}
if (device_start(&ctx->kb_device) != ERROR_NONE) {
LOG_E(TAG, "failed to start USB keyboard device");
device_remove(&ctx->kb_device);
device_destruct(&ctx->kb_device);
return;
}
ctx->kb_device_active = true;
}
static void usb_hid_keyboard_device_destruct(UsbHidContext* ctx) {
if (!ctx->kb_device_active) {
return;
}
ctx->kb_device_active = false;
device_stop(&ctx->kb_device);
device_remove(&ctx->kb_device);
device_destruct(&ctx->kb_device);
}
static void usb_hid_keyboard_publish_key(UsbHidContext* ctx, uint32_t lv_key, bool pressed, bool ctrl, bool alt, uint8_t hid_keycode, uint8_t hid_modifier) {
if (!ctx->kb_device_active) {
return;
}
auto* queue = static_cast<QueueHandle_t>(device_get_driver_data(&ctx->kb_device));
KeyboardKeyData data = { lv_key, pressed, false, ctrl, alt, hid_keycode, hid_modifier };
xQueueSend(queue, &data, 0);
}
// endregion
static error_t start_device(struct Device* device) {
auto* ctx = new UsbHidContext();
ctx->controller_device = device;
ctx->sub_mutex = xSemaphoreCreateMutex();
if (!ctx->sub_mutex) {
@ -486,6 +611,13 @@ static error_t stop_device(struct Device* device) {
auto* ctx = static_cast<UsbHidContext*>(device_get_driver_data(device));
if (!ctx) return ERROR_NONE;
// hid_host_device_close() halts and flushes the pending IN transfer before returning, so no
// hid_interface_callback() for this handle can race the queue delete below.
if (auto kb_handle = ctx->kb_handle.load()) {
hid_host_device_close(kb_handle);
}
usb_hid_keyboard_device_destruct(ctx);
ctx->hid_proc_running = false;
if (xSemaphoreTake(ctx->hid_proc_task_done, pdMS_TO_TICKS(HID_STOP_TIMEOUT_MS)) != pdTRUE) {

View File

@ -40,6 +40,7 @@ extern Driver esp32_ble_hid_device_driver;
#if SOC_USB_OTG_SUPPORTED
extern Driver esp32_usbhost_driver;
extern Driver esp32_usbhost_hid_driver;
extern Driver esp32_usbhost_hid_keyboard_driver;
extern Driver esp32_usbhost_midi_driver;
extern Driver esp32_usbhost_msc_driver;
#endif
@ -91,6 +92,7 @@ static error_t start() {
#if SOC_USB_OTG_SUPPORTED
check(driver_construct_add(&esp32_usbhost_driver) == ERROR_NONE);
check(driver_construct_add(&esp32_usbhost_hid_driver) == ERROR_NONE);
check(driver_construct_add(&esp32_usbhost_hid_keyboard_driver) == ERROR_NONE);
check(driver_construct_add(&esp32_usbhost_midi_driver) == ERROR_NONE);
check(driver_construct_add(&esp32_usbhost_msc_driver) == ERROR_NONE);
#endif
@ -141,6 +143,7 @@ static error_t stop() {
#if SOC_USB_OTG_SUPPORTED
check(driver_remove_destruct(&esp32_usbhost_msc_driver) == ERROR_NONE);
check(driver_remove_destruct(&esp32_usbhost_midi_driver) == ERROR_NONE);
check(driver_remove_destruct(&esp32_usbhost_hid_keyboard_driver) == ERROR_NONE);
check(driver_remove_destruct(&esp32_usbhost_hid_driver) == ERROR_NONE);
check(driver_remove_destruct(&esp32_usbhost_driver) == ERROR_NONE);
#endif

View File

@ -6,6 +6,7 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
list(APPEND REQUIRES_LIST
TactilityKernel
TactilityKernelCpp
TactilityFreeRtos
lvgl-module
lvgl-window-manager-module

View File

@ -0,0 +1,21 @@
#pragma once
namespace tt::lvgl {
/**
* @brief Starts listening for KEYBOARD_TYPE kernel devices being started/stopped at runtime
* (e.g. a USB HID keyboard being plugged in/out) and binds/unbinds each one to LVGL via
* lvgl_keyboard_add()/lvgl_keyboard_remove().
*
* Devices already started before this is called are picked up by lvgl-module's own boot-time
* scan instead (see Modules/lvgl-module/source/devices/devices.cpp's lvgl_devices_attach()) -
* this only reacts to devices that start/stop afterwards.
*/
void startKeyboardDeviceListener();
/**
* @brief Stops listening and unbinds every KEYBOARD_TYPE device this listener bound to LVGL.
*/
void stopKeyboardDeviceListener();
} // namespace tt::lvgl

View File

@ -1,5 +1,7 @@
#pragma once
#include <TactilityCpp/Allocator.h>
#include <string>
#include <vector>
@ -16,6 +18,10 @@ struct AppHubEntry {
std::string file;
};
bool parseJson(const std::string& filePath, std::vector<AppHubEntry>& entries);
// The top-level entries buffer prefers PSRAM/SPIRAM via OptExternalAllocator; individual
// AppHubEntry string/vector members still use the default (internal-RAM) allocator.
using AppHubEntryList = std::vector<AppHubEntry, OptExternalAllocator<AppHubEntry>>;
bool parseJson(const std::string& filePath, AppHubEntryList& entries);
}

View File

@ -25,6 +25,7 @@
#include <Tactility/bluetooth/Bluetooth.h>
#include <Tactility/file/File.h>
#include <Tactility/hal/SdCard.h>
#include <Tactility/lvgl/KeyboardDeviceListener.h>
#include <Tactility/lvgl/Statusbar.h>
#include <Tactility/lvgl/TrackballInit.h>
#include <Tactility/lvgl/UsbHidInput.h>
@ -441,6 +442,7 @@ static void onLvglStarted() {
#endif
lvgl::startUsbHidInput();
lvgl::startKeyboardDeviceListener();
lvgl::initTrackball();
#ifdef CONFIG_TT_TOUCH_CALIBRATION_SUPPORTED
@ -457,6 +459,7 @@ static void onLvglStopped() {
module_stop(&lvgl_window_manager_module);
lvgl::stopKeyboardDeviceListener();
lvgl::stopUsbHidInput();
#if TT_FEATURE_SCREENSHOT_ENABLED

View File

@ -237,7 +237,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "AddGps",
.id = "tactility.addgps",
.name = "Add GPS",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -152,7 +152,7 @@ uint32_t start(uint32_t callerAppInstanceId, const std::string& title, const std
}
extern const ::AppManifest manifest = {
.id = "AlertDialog",
.id = "tactility.alertdialog",
.name = "Alert Dialog",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -162,7 +162,7 @@ void start(const std::string& appId) {
}
extern const ::AppManifest manifest = {
.id = "AppDetails",
.id = "tactility.appdetails",
.name = "App Details",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -36,8 +36,11 @@ struct Context {
lv_obj_t* contentWrapper = nullptr;
lv_obj_t* refreshButton = nullptr;
std::string cachedAppsJsonFile = std::format("{}/app_hub.json", getTempPath());
std::vector<AppHubEntry> entries;
AppHubEntryList entries;
Mutex mutex;
// Survives across a bury/resurface cycle (e.g. opening AppHubDetailsApp and returning),
int32_t scrollY = 0;
};
@ -46,10 +49,6 @@ void refresh(Context* ctx);
void onBackPressed(lv_event_t* event) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
// deadlock against itself.
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
app_event_emit(ctx->appInstanceId, &closeEvent);
}
@ -86,9 +85,25 @@ void showNoInternet(Context* ctx) {
}
void showApps(Context* ctx) {
// Refresh rebuilds the list from scratch (cached copy, then again once the network fetch
// lands), which would otherwise reset the user's scroll position each time.
auto scrollY = lv_obj_get_scroll_y(ctx->contentWrapper);
lv_obj_clean(ctx->contentWrapper);
ctx->mutex.lock();
if (parseJson(ctx->cachedAppsJsonFile, ctx->entries)) {
// An empty targetPlatforms list means the entry runs everywhere; otherwise it must name
// this build's own target to be installable here. The simulator isn't a real MCU target,
// so it has nothing to match against and skips this filter entirely.
std::erase_if(ctx->entries, [](const AppHubEntry& entry) {
#ifdef ESP_PLATFORM
return !entry.targetPlatforms.empty() &&
std::ranges::find(entry.targetPlatforms, CONFIG_IDF_TARGET) == entry.targetPlatforms.end();
#else
(void)entry;
return false;
#endif
});
std::ranges::sort(ctx->entries, [](auto left, auto right) {
return left.appName < right.appName;
});
@ -106,6 +121,8 @@ void showApps(Context* ctx) {
lv_obj_set_user_data(entry_button, int_as_voidptr);
lv_obj_add_event_cb(entry_button, onAppPressed, LV_EVENT_SHORT_CLICKED, ctx);
}
lv_obj_scroll_to_y(ctx->contentWrapper, scrollY, LV_ANIM_OFF);
} else {
showRefreshFailedError(ctx, "Failed to load content");
}
@ -169,6 +186,15 @@ void createWidgets(lv_obj_t* parent, void* userData) {
lv_obj_set_style_pad_ver(ctx->contentWrapper, 0, LV_STATE_DEFAULT);
refresh(ctx);
lv_obj_scroll_to_y(ctx->contentWrapper, ctx->scrollY, LV_ANIM_OFF);
}
void destroyWidgets(void* userData) {
auto* ctx = static_cast<Context*>(userData);
ctx->scrollY = lv_obj_get_scroll_y(ctx->contentWrapper);
ctx->contentWrapper = nullptr;
ctx->refreshButton = nullptr;
}
int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
@ -179,7 +205,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
sub.app_instance_id = appInstanceId;
app_event_subscribe(&sub);
WindowId window = window_manager_create(appInstanceId, createWidgets, &ctx);
WindowId window = window_manager_create_ext(appInstanceId, createWidgets, destroyWidgets, &ctx);
bool shouldClose = false;
while (!shouldClose) {
@ -206,7 +232,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "AppHub",
.id = "tactility.apphub",
.name = "App Hub",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

View File

@ -20,7 +20,7 @@ static bool parseEntry(const cJSON* object, AppHubEntry& entry) {
reader.readStringArray("targetPlatforms", entry.targetPlatforms);
}
bool parseJson(const std::string& filePath, std::vector<AppHubEntry>& entries) {
bool parseJson(const std::string& filePath, AppHubEntryList& entries) {
file::FileMutexGuard guard(filePath);
auto data = file::readString(filePath);

View File

@ -1,7 +1,3 @@
#include "../../../../Modules/app-module/private/app/private/app_ledger.h"
#include "app/metadata.h"
#include <Tactility/DeprecatedPaths.h>
#include <Tactility/StringUtils.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
@ -12,6 +8,7 @@
#include <app/event.h>
#include <app/install.h>
#include <app/metadata.h>
#include <app/manager.h>
#include <app/manifest.h>
@ -64,10 +61,6 @@ uint32_t showConfirmDialog(Context* ctx, const char* action) {
void onBackPressed(lv_event_t* e) {
auto* ctx = static_cast<Context*>(lv_event_get_user_data(e));
// Async, non-blocking - must NOT call app_manager_stop() directly here: that bound-waits
// (thread_join) for this app's own thread to finish, which needs the LVGL lock
// (window_manager_remove()) - but this callback runs ON the LVGL task, which would
// deadlock against itself.
AppEvent closeEvent { .type = APP_EVENT_CLOSE, .timestamp = 0, .result = {} };
app_event_emit(ctx->appInstanceId, &closeEvent);
}
@ -308,7 +301,7 @@ void start(const apphub::AppHubEntry& entry) {
}
extern const ::AppManifest manifest = {
.id = "AppHubDetails",
.id = "tactility.apphubdetails",
.name = "App Details",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -106,7 +106,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "AppList",
.id = "tactility.applist",
.name = "Apps",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -121,7 +121,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "AppSettings",
.id = "tactility.appsettings",
.name = "Apps",
.category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

View File

@ -176,7 +176,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "ApWebServer",
.id = "tactility.apwebserver",
.name = "AP Web Server",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -251,7 +251,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "AudioSettings",
.id = "tactility.audiosettings",
.name = "Audio",
.category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

View File

@ -326,7 +326,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "Boot",
.id = "tactility.boot",
.name = "Boot",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -283,7 +283,7 @@ uint32_t start() {
}
extern const ::AppManifest manifest = {
.id = "BtManage",
.id = "tactility.btmanage",
.name = "Bluetooth",
.category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

View File

@ -256,7 +256,7 @@ void start(const std::string& addrHex) {
}
extern const ::AppManifest manifest = {
.id = "BtPeerSettings",
.id = "tactility.btpeersettings",
.name = "BT Device Settings",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -219,7 +219,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "Chat",
.id = "tactility.chat",
.name = "Chat",
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

View File

@ -7,10 +7,10 @@
#include <Tactility/app/chat/ChatSettings.h>
#include <Tactility/app/chat/ChatProtocol.h>
#include <Tactility/DeprecatedPaths.h>
#include <Tactility/file/File.h>
#include <Tactility/file/PropertiesFile.h>
#include <app/paths.h>
#include <crypt/crypt.h>
#include <esp_random.h>
@ -28,7 +28,11 @@ namespace tt::app::chat {
constexpr auto* TAG = "ChatSettings";
static std::string getSettingsFilePath() {
return getUserDataPath() + "/settings/chat.properties";
char path[256];
if (app_paths_get_user_data_path("tactility.chat", "chat.properties", path, sizeof(path)) != ERROR_NONE) {
return "";
}
return path;
}
constexpr auto* KEY_SENDER_ID = "senderId";

View File

@ -199,7 +199,7 @@ void start() {
}
extern const ::AppManifest manifest = {
.id = "CrashDiagnostics",
.id = "tactility.crashdiagnostics",
.name = "Crash Diagnostics",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -226,7 +226,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "Development",
.id = "tactility.development",
.name = "Development",
.category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

View File

@ -66,7 +66,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "Files",
.id = "tactility.files",
.name = "Files",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -106,7 +106,7 @@ uint32_t startForExistingOrNewFile(uint32_t callerAppInstanceId) {
}
extern const ::AppManifest manifest = {
.id = "FileSelection",
.id = "tactility.fileselection",
.name = "File Selection",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -320,7 +320,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "GpsSettings",
.id = "tactility.gpssettings",
.name = "GPS",
.category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

View File

@ -124,7 +124,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "GroveSettings",
.id = "tactility.grovesettings",
.name = "Grove",
.category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

View File

@ -419,7 +419,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "I2cScanner",
.id = "tactility.i2cscanner",
.name = "I2C Scanner",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

View File

@ -126,7 +126,7 @@ void start(const std::string& file) {
}
extern const ::AppManifest manifest = {
.id = "ImageViewer",
.id = "tactility.imageviewer",
.name = "Image Viewer",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -148,7 +148,7 @@ std::string getLastText() {
}
extern const ::AppManifest manifest = {
.id = "InputDialog",
.id = "tactility.inputdialog",
.name = "Input Dialog",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -340,7 +340,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "Display",
.id = "tactility.display",
.name = "Display",
.category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

View File

@ -241,7 +241,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "KeyboardSettings",
.id = "tactility.keyboardsettings",
.name = "Keyboard",
.category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

View File

@ -168,9 +168,9 @@ void createWidgets(lv_obj_t* parent, void*) {
? computeButtonMargin(lv_display_get_horizontal_resolution(display), total_button_size)
: computeButtonMargin(lv_display_get_vertical_resolution(display), total_button_size);
auto* app_list_button = createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_APPS, "AppList", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_FOLDER, "Files", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_SETTINGS, "Settings", margin, is_landscape_display);
auto* app_list_button = createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_APPS, "tactility.applist", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_FOLDER, "tactility.files", margin, is_landscape_display);
createAppButton(buttons_wrapper, ui_density, LVGL_ICON_LAUNCHER_SETTINGS, "tactility.settings", margin, is_landscape_display);
// The launcher's container is several levels below the screen, and LVGL only sends
// LV_EVENT_SIZE_CHANGED to the screen object itself on a resolution change - so the
@ -184,7 +184,7 @@ void createWidgets(lv_obj_t* parent, void*) {
auto* power_button = lv_button_create(parent);
lv_obj_set_style_pad_all(power_button, 8, 0);
lv_obj_align(power_button, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_add_event_cb(power_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)"PowerOff");
lv_obj_add_event_cb(power_button, onAppPressed, LV_EVENT_SHORT_CLICKED, (void*)"tactility.poweroff");
lv_obj_set_style_shadow_width(power_button, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(power_button, 0, LV_PART_MAIN);
@ -261,7 +261,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "Launcher",
.id = "tactility.launcher",
.name = "Launcher",
.category = APP_CATEGORY_SYSTEM,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) },

View File

@ -176,7 +176,7 @@ int32_t appMain(uint32_t appInstanceId, int argc, char* argv[]) {
} // namespace
extern const ::AppManifest manifest = {
.id = "LocaleSettings",
.id = "tactility.localesettings",
.name = "Region & Language",
.category = APP_CATEGORY_SETTINGS,
.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(appMain) }

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