From 48c4f1791ff1c4e8d6fa8e240ebac70a06c1cb89 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 24 Dec 2025 09:59:04 +1000 Subject: [PATCH 01/22] feature additions KeyboardBacklight driver Trackball driver Display sleep/wake Files - create file/folder Keyboard settings Time & Date settings tweaks Locale settings tweaks Maybe missed some things in the list. Maybe more to come. A few notes: Something weird is happening with the language dropdown menu. It appears to have ghost entries. Touch wake could do with some kinda of block on wake first touch to prevent UI elements being hit when waking device with touch. Same with encoder/trackball press i guess. --- Data/data/settings/system.properties | 5 +- Devices/lilygo-tdeck/CMakeLists.txt | 2 +- Devices/lilygo-tdeck/Source/Configuration.cpp | 60 ++++++ .../Source/devices/KeyboardBacklight.cpp | 36 ++++ .../Source/devices/KeyboardBacklight.h | 32 +++ .../Source/devices/TdeckKeyboard.cpp | 26 +++ .../Source/devices/TrackballDevice.cpp | 35 ++++ .../Source/devices/TrackballDevice.h | 21 ++ Drivers/KeyboardBacklight/CMakeLists.txt | 5 + .../Source/KeyboardBacklight.cpp | 96 +++++++++ .../Source/KeyboardBacklight.h | 36 ++++ Drivers/Trackball/CMakeLists.txt | 5 + Drivers/Trackball/Source/Trackball.cpp | 147 +++++++++++++ Drivers/Trackball/Source/Trackball.h | 44 ++++ Tactility/CMakeLists.txt | 1 + .../Include/Tactility/hal/Configuration.h | 7 + .../Tactility/settings/DisplaySettings.h | 2 + .../Tactility/settings/KeyboardSettings.h | 23 ++ .../Tactility/settings/SystemSettings.h | 2 + Tactility/Private/Tactility/app/files/State.h | 4 +- Tactility/Private/Tactility/app/files/View.h | 4 + Tactility/Source/Tactility.cpp | 6 + Tactility/Source/app/display/Display.cpp | 71 +++++++ Tactility/Source/app/files/View.cpp | 95 ++++++++- .../Source/app/keyboard/KeyboardSettings.cpp | 197 ++++++++++++++++++ .../app/localesettings/LocaleSettings.cpp | 80 ++++--- .../app/timedatesettings/TimeDateSettings.cpp | 100 ++++++++- Tactility/Source/file/PropertiesFile.cpp | 11 +- Tactility/Source/hal/Hal.cpp | 12 ++ .../service/displayidle/DisplayIdle.cpp | 137 ++++++++++++ .../service/keyboardinit/KeyboardInit.cpp | 48 +++++ Tactility/Source/settings/DisplaySettings.cpp | 22 +- .../Source/settings/KeyboardSettings.cpp | 65 ++++++ Tactility/Source/settings/SystemSettings.cpp | 22 ++ Tactility/Source/settings/Time.cpp | 4 +- 35 files changed, 1406 insertions(+), 57 deletions(-) create mode 100644 Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp create mode 100644 Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.h create mode 100644 Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp create mode 100644 Devices/lilygo-tdeck/Source/devices/TrackballDevice.h create mode 100644 Drivers/KeyboardBacklight/CMakeLists.txt create mode 100644 Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp create mode 100644 Drivers/KeyboardBacklight/Source/KeyboardBacklight.h create mode 100644 Drivers/Trackball/CMakeLists.txt create mode 100644 Drivers/Trackball/Source/Trackball.cpp create mode 100644 Drivers/Trackball/Source/Trackball.h create mode 100644 Tactility/Include/Tactility/settings/KeyboardSettings.h create mode 100644 Tactility/Source/app/keyboard/KeyboardSettings.cpp create mode 100644 Tactility/Source/service/displayidle/DisplayIdle.cpp create mode 100644 Tactility/Source/service/keyboardinit/KeyboardInit.cpp create mode 100644 Tactility/Source/settings/KeyboardSettings.cpp diff --git a/Data/data/settings/system.properties b/Data/data/settings/system.properties index 0b905db83..1e299ddf4 100644 --- a/Data/data/settings/system.properties +++ b/Data/data/settings/system.properties @@ -1,2 +1,5 @@ language=en-US -timeFormat24h=true \ No newline at end of file +timeFormat24h=true +dateFormat=MM/DD/YYYY +region=US +timezone=America/New_York \ No newline at end of file diff --git a/Devices/lilygo-tdeck/CMakeLists.txt b/Devices/lilygo-tdeck/CMakeLists.txt index 6b073058a..df27972df 100644 --- a/Devices/lilygo-tdeck/CMakeLists.txt +++ b/Devices/lilygo-tdeck/CMakeLists.txt @@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "Source" - REQUIRES Tactility EspLcdCompat ST7789 GT911 PwmBacklight EstimatedPower driver + REQUIRES Tactility EspLcdCompat ST7789 GT911 PwmBacklight EstimatedPower KeyboardBacklight Trackball driver ) diff --git a/Devices/lilygo-tdeck/Source/Configuration.cpp b/Devices/lilygo-tdeck/Source/Configuration.cpp index 640f71145..48b1f05a6 100644 --- a/Devices/lilygo-tdeck/Source/Configuration.cpp +++ b/Devices/lilygo-tdeck/Source/Configuration.cpp @@ -1,26 +1,86 @@ #include "devices/Display.h" +#include "devices/KeyboardBacklight.h" #include "devices/Power.h" #include "devices/Sdcard.h" #include "devices/TdeckKeyboard.h" +#include "devices/TrackballDevice.h" #include +#include #include +#include +#include +#include +#include bool initBoot(); +void initI2cDevices(); using namespace tt::hal; +constexpr auto* TAG = "TDeckConfig"; + static std::vector> createDevices() { return { createPower(), createDisplay(), std::make_shared(), + std::make_shared(), + std::make_shared(), createSdCard() }; } +void initI2cDevices() { + // Defer I2C device startup to avoid heap corruption during early boot + // Use a one-shot FreeRTOS timer to delay initialization + static TimerHandle_t initTimer = xTimerCreate( + "I2CInit", + pdMS_TO_TICKS(500), // 500ms delay + pdFALSE, // One-shot + nullptr, + [](TimerHandle_t timer) { + TT_LOG_I(TAG, "Starting deferred I2C devices"); + + // Start keyboard backlight device + auto kbBacklight = tt::hal::findDevice("Keyboard Backlight"); + if (kbBacklight) { + TT_LOG_I(TAG, "%s starting", kbBacklight->getName().c_str()); + auto kbDevice = std::static_pointer_cast(kbBacklight); + if (kbDevice->start()) { + TT_LOG_I(TAG, "%s started", kbBacklight->getName().c_str()); + } else { + TT_LOG_E(TAG, "%s start failed", kbBacklight->getName().c_str()); + } + } + + // Small delay between I2C device inits to avoid concurrent transactions + vTaskDelay(pdMS_TO_TICKS(50)); + + // Start trackball device + auto trackball = tt::hal::findDevice("Trackball"); + if (trackball) { + TT_LOG_I(TAG, "%s starting", trackball->getName().c_str()); + auto tbDevice = std::static_pointer_cast(trackball); + if (tbDevice->start()) { + TT_LOG_I(TAG, "%s started", trackball->getName().c_str()); + } else { + TT_LOG_E(TAG, "%s start failed", trackball->getName().c_str()); + } + } + + TT_LOG_I(TAG, "Deferred I2C devices completed"); + } + ); + + if (initTimer != nullptr) { + xTimerStart(initTimer, 0); + } +} + extern const Configuration hardwareConfiguration = { .initBoot = initBoot, + .initI2cDevices = initI2cDevices, .createDevices = createDevices, .i2c = { i2c::Configuration { diff --git a/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp new file mode 100644 index 000000000..d14d4083f --- /dev/null +++ b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp @@ -0,0 +1,36 @@ +#include "KeyboardBacklight.h" +#include // Driver +#include + +bool KeyboardBacklightDevice::start() { + if (initialized) { + return true; + } + + // T-Deck uses I2C_NUM_0 for internal peripherals + initialized = driver::keyboardbacklight::init(I2C_NUM_0); + return initialized; +} + +bool KeyboardBacklightDevice::stop() { + if (initialized) { + // Turn off backlight on shutdown + driver::keyboardbacklight::setBrightness(0); + initialized = false; + } + return true; +} + +bool KeyboardBacklightDevice::setBrightness(uint8_t brightness) { + if (!initialized) { + return false; + } + return driver::keyboardbacklight::setBrightness(brightness); +} + +uint8_t KeyboardBacklightDevice::getBrightness() const { + if (!initialized) { + return 0; + } + return driver::keyboardbacklight::getBrightness(); +} diff --git a/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.h b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.h new file mode 100644 index 000000000..0827e93d8 --- /dev/null +++ b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +class KeyboardBacklightDevice final : public tt::hal::Device { + + bool initialized = false; + +public: + + tt::hal::Device::Type getType() const override { return tt::hal::Device::Type::I2c; } + std::string getName() const override { return "Keyboard Backlight"; } + std::string getDescription() const override { return "T-Deck keyboard backlight control"; } + + bool start(); + bool stop(); + bool isAttached() const { return initialized; } + + /** + * Set keyboard backlight brightness + * @param brightness 0-255 (0=off, 255=max) + */ + bool setBrightness(uint8_t brightness); + + /** + * Get current brightness + * @return 0-255 + */ + uint8_t getBrightness() const; +}; + diff --git a/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp b/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp index 92afdfee4..30b404fc8 100644 --- a/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp +++ b/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp @@ -1,6 +1,14 @@ #include "TdeckKeyboard.h" #include #include +#include +#include +#include +#include +#include +#include + +using tt::hal::findFirstDevice; constexpr auto* TAG = "TdeckKeyboard"; constexpr auto TDECK_KEYBOARD_I2C_BUS_HANDLE = I2C_NUM_0; @@ -36,6 +44,24 @@ static void keyboard_read_callback(TT_UNUSED lv_indev_t* indev, lv_indev_data_t* TT_LOG_D(TAG, "Pressed %d", read_buffer); data->key = read_buffer; data->state = LV_INDEV_STATE_PRESSED; + // Ensure LVGL activity is triggered so idle services can wake the display + lv_disp_trig_activity(nullptr); + + // Actively wake display/backlights immediately on key press (independent of idle tick) + // Restore display backlight if off (we assume duty 0 means dimmed) + auto display = findFirstDevice(tt::hal::Device::Type::Display); + if (display && display->supportsBacklightDuty()) { + // Load display settings for target duty + auto dsettings = tt::settings::display::loadOrGetDefault(); + // Always set duty, harmless if already on + display->setBacklightDuty(dsettings.backlightDuty); + } + + // Restore keyboard backlight if enabled in settings + auto ksettings = tt::settings::keyboard::loadOrGetDefault(); + if (ksettings.backlightEnabled) { + driver::keyboardbacklight::setBrightness(ksettings.backlightBrightness); + } } } diff --git a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp new file mode 100644 index 000000000..abd428e97 --- /dev/null +++ b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp @@ -0,0 +1,35 @@ +#include "TrackballDevice.h" +#include // Driver + +bool TrackballDevice::start() { + if (initialized) { + return true; + } + + // T-Deck trackball GPIO configuration from LilyGo reference + driver::trackball::TrackballConfig config = { + .pinRight = GPIO_NUM_2, // BOARD_TBOX_G02 + .pinUp = GPIO_NUM_3, // BOARD_TBOX_G01 + .pinLeft = GPIO_NUM_1, // BOARD_TBOX_G04 + .pinDown = GPIO_NUM_15, // BOARD_TBOX_G03 + .pinClick = GPIO_NUM_0, // BOARD_BOOT_PIN + .movementStep = 10 // pixels per movement + }; + + indev = driver::trackball::init(config); + if (indev != nullptr) { + initialized = true; + return true; + } + + return false; +} + +bool TrackballDevice::stop() { + if (initialized) { + // LVGL will handle indev cleanup + indev = nullptr; + initialized = false; + } + return true; +} diff --git a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.h b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.h new file mode 100644 index 000000000..d72df497c --- /dev/null +++ b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +class TrackballDevice : public tt::hal::Device { +public: + tt::hal::Device::Type getType() const override { return tt::hal::Device::Type::I2c; } + std::string getName() const override { return "Trackball"; } + std::string getDescription() const override { return "5-way GPIO trackball navigation"; } + + bool start(); + bool stop(); + bool isAttached() const { return initialized; } + + lv_indev_t* getLvglIndev() const { return indev; } + +private: + lv_indev_t* indev = nullptr; + bool initialized = false; +}; diff --git a/Drivers/KeyboardBacklight/CMakeLists.txt b/Drivers/KeyboardBacklight/CMakeLists.txt new file mode 100644 index 000000000..48a859134 --- /dev/null +++ b/Drivers/KeyboardBacklight/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES TactilityCore driver +) diff --git a/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp b/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp new file mode 100644 index 000000000..ee845cba6 --- /dev/null +++ b/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp @@ -0,0 +1,96 @@ +#include "KeyboardBacklight.h" +#include +#include + +static const char* TAG = "KeyboardBacklight"; + +namespace driver::keyboardbacklight { + +static const uint8_t CMD_BRIGHTNESS = 0x01; +static const uint8_t CMD_DEFAULT_BRIGHTNESS = 0x02; + +static i2c_port_t g_i2cPort = I2C_NUM_MAX; +static uint8_t g_slaveAddress = 0x55; +static uint8_t g_currentBrightness = 127; + +bool init(i2c_port_t i2cPort, uint8_t slaveAddress) { + g_i2cPort = i2cPort; + g_slaveAddress = slaveAddress; + + ESP_LOGI(TAG, "Keyboard backlight initialized on I2C port %d, address 0x%02X", g_i2cPort, g_slaveAddress); + + // Set a reasonable default brightness + setDefaultBrightness(127); + setBrightness(127); + + return true; +} + +bool setBrightness(uint8_t brightness) { + if (g_i2cPort >= I2C_NUM_MAX) { + ESP_LOGE(TAG, "Keyboard backlight not initialized"); + return false; + } + + // Skip if brightness is already at target value (avoid I2C spam on every keypress) + if (brightness == g_currentBrightness) { + return true; + } + + ESP_LOGI(TAG, "Setting brightness to %d on I2C port %d, address 0x%02X", brightness, g_i2cPort, g_slaveAddress); + + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (g_slaveAddress << 1) | I2C_MASTER_WRITE, true); + i2c_master_write_byte(cmd, CMD_BRIGHTNESS, true); + i2c_master_write_byte(cmd, brightness, true); + i2c_master_stop(cmd); + + esp_err_t ret = i2c_master_cmd_begin(g_i2cPort, cmd, pdMS_TO_TICKS(100)); + i2c_cmd_link_delete(cmd); + + if (ret == ESP_OK) { + g_currentBrightness = brightness; + ESP_LOGI(TAG, "Successfully set brightness to %d", brightness); + return true; + } else { + ESP_LOGE(TAG, "Failed to set brightness: %s (0x%x)", esp_err_to_name(ret), ret); + return false; + } +} + +bool setDefaultBrightness(uint8_t brightness) { + if (g_i2cPort >= I2C_NUM_MAX) { + ESP_LOGE(TAG, "Keyboard backlight not initialized"); + return false; + } + + // Clamp to valid range for default brightness + if (brightness < 30) { + brightness = 30; + } + + i2c_cmd_handle_t cmd = i2c_cmd_link_create(); + i2c_master_start(cmd); + i2c_master_write_byte(cmd, (g_slaveAddress << 1) | I2C_MASTER_WRITE, true); + i2c_master_write_byte(cmd, CMD_DEFAULT_BRIGHTNESS, true); + i2c_master_write_byte(cmd, brightness, true); + i2c_master_stop(cmd); + + esp_err_t ret = i2c_master_cmd_begin(g_i2cPort, cmd, pdMS_TO_TICKS(100)); + i2c_cmd_link_delete(cmd); + + if (ret == ESP_OK) { + ESP_LOGD(TAG, "Set default brightness to %d", brightness); + return true; + } else { + ESP_LOGE(TAG, "Failed to set default brightness: %s", esp_err_to_name(ret)); + return false; + } +} + +uint8_t getBrightness() { + return g_currentBrightness; +} + +} diff --git a/Drivers/KeyboardBacklight/Source/KeyboardBacklight.h b/Drivers/KeyboardBacklight/Source/KeyboardBacklight.h new file mode 100644 index 000000000..2cb0f8dd7 --- /dev/null +++ b/Drivers/KeyboardBacklight/Source/KeyboardBacklight.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +namespace driver::keyboardbacklight { + +/** + * @brief Initialize keyboard backlight control + * @param i2cPort I2C port number (I2C_NUM_0 or I2C_NUM_1) + * @param slaveAddress I2C slave address (default 0x55 for T-Deck keyboard) + * @return true if initialization succeeded + */ +bool init(i2c_port_t i2cPort, uint8_t slaveAddress = 0x55); + +/** + * @brief Set keyboard backlight brightness + * @param brightness Brightness level (0-255, 0=off, 255=max) + * @return true if command succeeded + */ +bool setBrightness(uint8_t brightness); + +/** + * @brief Set default keyboard backlight brightness for ALT+B toggle + * @param brightness Default brightness level (30-255) + * @return true if command succeeded + */ +bool setDefaultBrightness(uint8_t brightness); + +/** + * @brief Get current keyboard backlight brightness + * @return Current brightness level (0-255) + */ +uint8_t getBrightness(); + +} diff --git a/Drivers/Trackball/CMakeLists.txt b/Drivers/Trackball/CMakeLists.txt new file mode 100644 index 000000000..1a37664b5 --- /dev/null +++ b/Drivers/Trackball/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES TactilityCore driver lvgl +) diff --git a/Drivers/Trackball/Source/Trackball.cpp b/Drivers/Trackball/Source/Trackball.cpp new file mode 100644 index 000000000..5a64a806f --- /dev/null +++ b/Drivers/Trackball/Source/Trackball.cpp @@ -0,0 +1,147 @@ +#include "Trackball.h" +#include + +static const char* TAG = "Trackball"; + +namespace driver::trackball { + +static TrackballConfig g_config; +static lv_indev_t* g_indev = nullptr; +static bool g_initialized = false; +static bool g_enabled = true; + +// Track last GPIO states for edge detection +static bool g_lastState[5] = {false, false, false, false, false}; +// Encoder diff accumulator for navigation +static int16_t g_encDiff = 0; + +static void read_cb(lv_indev_t* indev, lv_indev_data_t* data) { + if (!g_initialized || !g_enabled) { + data->state = LV_INDEV_STATE_RELEASED; + data->enc_diff = 0; + return; + } + + const gpio_num_t pins[5] = { + g_config.pinRight, + g_config.pinUp, + g_config.pinLeft, + g_config.pinDown, + g_config.pinClick + }; + + // Read GPIO states and detect changes (active low with pull-up) + bool currentStates[5]; + for (int i = 0; i < 5; i++) { + currentStates[i] = gpio_get_level(pins[i]) == 0; + } + + // Process directional inputs as encoder steps + // Right/Down = positive diff (next item), Left/Up = negative diff (prev item) + int16_t diff = 0; + + // Right pressed (rising edge) + if (currentStates[0] && !g_lastState[0]) { + diff += 1; + } + // Up pressed (rising edge) + if (currentStates[1] && !g_lastState[1]) { + diff -= 1; + } + // Left pressed (rising edge) + if (currentStates[2] && !g_lastState[2]) { + diff -= 1; + } + // Down pressed (rising edge) + if (currentStates[3] && !g_lastState[3]) { + diff += 1; + } + + // Update last states + for (int i = 0; i < 5; i++) { + g_lastState[i] = currentStates[i]; + } + + // Update encoder diff and button state + data->enc_diff = diff; + data->state = currentStates[4] ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + + // Trigger activity for wake-on-trackball + if (diff != 0 || currentStates[4]) { + lv_disp_trig_activity(nullptr); + } +} + +lv_indev_t* init(const TrackballConfig& config) { + if (g_initialized) { + ESP_LOGW(TAG, "Trackball already initialized"); + return g_indev; + } + + g_config = config; + + // Set default movement step if not specified + if (g_config.movementStep == 0) { + g_config.movementStep = 10; + } + + // Configure all GPIO pins as inputs with pull-ups (active low) + const gpio_num_t pins[5] = { + config.pinRight, + config.pinUp, + config.pinLeft, + config.pinDown, + config.pinClick + }; + + gpio_config_t io_conf = {}; + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + + for (int i = 0; i < 5; i++) { + io_conf.pin_bit_mask = (1ULL << pins[i]); + gpio_config(&io_conf); + g_lastState[i] = gpio_get_level(pins[i]) == 0; + } + + // Register as LVGL encoder input device for group navigation + g_indev = lv_indev_create(); + lv_indev_set_type(g_indev, LV_INDEV_TYPE_ENCODER); + lv_indev_set_read_cb(g_indev, read_cb); + + if (g_indev) { + g_initialized = true; + ESP_LOGI(TAG, "Trackball initialized as encoder (R:%d U:%d L:%d D:%d Click:%d)", + config.pinRight, config.pinUp, config.pinLeft, config.pinDown, + config.pinClick); + return g_indev; + } else { + ESP_LOGE(TAG, "Failed to register LVGL input device"); + return nullptr; + } +} + +void deinit() { + if (g_indev) { + lv_indev_delete(g_indev); + g_indev = nullptr; + } + g_initialized = false; + ESP_LOGI(TAG, "Trackball deinitialized"); +} + +void setMovementStep(uint8_t step) { + if (step > 0) { + g_config.movementStep = step; + ESP_LOGD(TAG, "Movement step set to %d", step); + } +} + +void setEnabled(bool enabled) { + g_enabled = enabled; + ESP_LOGI(TAG, "Trackball %s", enabled ? "enabled" : "disabled"); +} + +} diff --git a/Drivers/Trackball/Source/Trackball.h b/Drivers/Trackball/Source/Trackball.h new file mode 100644 index 000000000..593c70d94 --- /dev/null +++ b/Drivers/Trackball/Source/Trackball.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +namespace driver::trackball { + +/** + * @brief Trackball configuration structure + */ +struct TrackballConfig { + gpio_num_t pinRight; // Right direction GPIO + gpio_num_t pinUp; // Up direction GPIO + gpio_num_t pinLeft; // Left direction GPIO + gpio_num_t pinDown; // Down direction GPIO + gpio_num_t pinClick; // Click/select button GPIO + uint8_t movementStep; // Pixels to move per trackball event (default: 10) +}; + +/** + * @brief Initialize trackball as LVGL input device + * @param config Trackball GPIO configuration + * @return LVGL input device pointer, or nullptr on failure + */ +lv_indev_t* init(const TrackballConfig& config); + +/** + * @brief Deinitialize trackball + */ +void deinit(); + +/** + * @brief Set movement step size + * @param step Pixels to move per trackball event + */ +void setMovementStep(uint8_t step); + +/** + * @brief Enable or disable trackball input processing + * @param enabled Boolean value to enable or disable + */ +void setEnabled(bool enabled); + +} diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index a1e6e9bba..431a2d292 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -10,6 +10,7 @@ if (DEFINED ENV{ESP_IDF_VERSION}) TactilityCore lvgl driver + KeyboardBacklight elf_loader lv_screenshot QRCode diff --git a/Tactility/Include/Tactility/hal/Configuration.h b/Tactility/Include/Tactility/hal/Configuration.h index ee2353a9f..4faa787b0 100644 --- a/Tactility/Include/Tactility/hal/Configuration.h +++ b/Tactility/Include/Tactility/hal/Configuration.h @@ -8,6 +8,7 @@ namespace tt::hal { typedef bool (*InitBoot)(); +typedef void (*InitI2cDevices) (); typedef std::vector> DeviceVector; @@ -33,6 +34,12 @@ struct Configuration { */ const InitBoot _Nullable initBoot = nullptr; + /** + * Called after I2C bus is initialized and devices are registered. + * Used for starting I2C peripheral devices (e.g., keyboard backlight, trackball). + */ + const InitI2cDevices _Nullable initI2cDevices = nullptr; + /** Init behaviour: default (esp_lvgl_port for ESP32, nothing for PC) or None (nothing on any platform). Only used in Tactility, not in TactilityHeadless. */ const LvglInit lvglInit = LvglInit::Default; diff --git a/Tactility/Include/Tactility/settings/DisplaySettings.h b/Tactility/Include/Tactility/settings/DisplaySettings.h index be25e9737..926dd6cc1 100644 --- a/Tactility/Include/Tactility/settings/DisplaySettings.h +++ b/Tactility/Include/Tactility/settings/DisplaySettings.h @@ -16,6 +16,8 @@ struct DisplaySettings { Orientation orientation; uint8_t gammaCurve; uint8_t backlightDuty; + bool backlightTimeoutEnabled; + uint32_t backlightTimeoutMs; // 0 = Never }; /** Compares default settings with the function parameter to return the difference */ diff --git a/Tactility/Include/Tactility/settings/KeyboardSettings.h b/Tactility/Include/Tactility/settings/KeyboardSettings.h new file mode 100644 index 000000000..c65dbafd1 --- /dev/null +++ b/Tactility/Include/Tactility/settings/KeyboardSettings.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace tt::settings::keyboard { + +struct KeyboardSettings { + bool backlightEnabled; + uint8_t backlightBrightness; // 0-255 + bool trackballEnabled; + bool backlightTimeoutEnabled; + uint32_t backlightTimeoutMs; // Timeout in milliseconds +}; + +bool load(KeyboardSettings& settings); + +KeyboardSettings loadOrGetDefault(); + +KeyboardSettings getDefault(); + +bool save(const KeyboardSettings& settings); + +} diff --git a/Tactility/Include/Tactility/settings/SystemSettings.h b/Tactility/Include/Tactility/settings/SystemSettings.h index 8d0019b4f..b74b3a79e 100644 --- a/Tactility/Include/Tactility/settings/SystemSettings.h +++ b/Tactility/Include/Tactility/settings/SystemSettings.h @@ -7,6 +7,8 @@ namespace tt::settings { struct SystemSettings { Language language; bool timeFormat24h; + std::string dateFormat; // MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD, YYYY/MM/DD + std::string region; // (US, EU, JP, etc.) }; bool loadSystemSettings(SystemSettings& properties); diff --git a/Tactility/Private/Tactility/app/files/State.h b/Tactility/Private/Tactility/app/files/State.h index 5f2deab50..60fd990c8 100644 --- a/Tactility/Private/Tactility/app/files/State.h +++ b/Tactility/Private/Tactility/app/files/State.h @@ -15,7 +15,9 @@ class State final { enum PendingAction { ActionNone, ActionDelete, - ActionRename + ActionRename, + ActionCreateFile, + ActionCreateFolder }; private: diff --git a/Tactility/Private/Tactility/app/files/View.h b/Tactility/Private/Tactility/app/files/View.h index 92fd5ffad..c6b969df5 100644 --- a/Tactility/Private/Tactility/app/files/View.h +++ b/Tactility/Private/Tactility/app/files/View.h @@ -15,6 +15,8 @@ class View final { lv_obj_t* dir_entry_list = nullptr; lv_obj_t* action_list = nullptr; lv_obj_t* navigate_up_button = nullptr; + lv_obj_t* new_file_button = nullptr; + lv_obj_t* new_folder_button = nullptr; std::string installAppPath = { 0 }; LaunchId installAppLaunchId = 0; @@ -38,6 +40,8 @@ class View final { void onDirEntryLongPressed(int32_t index); void onRenamePressed(); void onDeletePressed(); + void onNewFilePressed(); + void onNewFolderPressed(); void onDirEntryListScrollBegin(); void onResult(LaunchId launchId, Result result, std::unique_ptr bundle); }; diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 38e346036..d3fa8278a 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -51,6 +51,8 @@ namespace service { namespace loader { extern const ServiceManifest manifest; } namespace memorychecker { extern const ServiceManifest manifest; } namespace statusbar { extern const ServiceManifest manifest; } + namespace displayidle { extern const ServiceManifest manifest; } + namespace keyboardinit { extern const ServiceManifest manifest; } #if TT_FEATURE_SCREENSHOT_ENABLED namespace screenshot { extern const ServiceManifest manifest; } #endif @@ -83,6 +85,7 @@ namespace app { namespace imageviewer { extern const AppManifest manifest; } namespace inputdialog { extern const AppManifest manifest; } namespace launcher { extern const AppManifest manifest; } + namespace keyboardsettings { extern const AppManifest manifest; } namespace localesettings { extern const AppManifest manifest; } namespace notes { extern const AppManifest manifest; } namespace power { extern const AppManifest manifest; } @@ -124,6 +127,7 @@ static void registerInternalApps() { addAppManifest(app::imageviewer::manifest); addAppManifest(app::inputdialog::manifest); addAppManifest(app::launcher::manifest); + addAppManifest(app::keyboardsettings::manifest); addAppManifest(app::localesettings::manifest); addAppManifest(app::notes::manifest); addAppManifest(app::settings::manifest); @@ -227,6 +231,8 @@ static void registerAndStartSecondaryServices() { addService(service::loader::manifest); addService(service::gui::manifest); addService(service::statusbar::manifest); + addService(service::displayidle::manifest); + addService(service::keyboardinit::manifest); addService(service::memorychecker::manifest); #if TT_FEATURE_SCREENSHOT_ENABLED addService(service::screenshot::manifest); diff --git a/Tactility/Source/app/display/Display.cpp b/Tactility/Source/app/display/Display.cpp index 7479f11eb..3b3090ff4 100644 --- a/Tactility/Source/app/display/Display.cpp +++ b/Tactility/Source/app/display/Display.cpp @@ -19,6 +19,8 @@ class DisplayApp final : public App { settings::display::DisplaySettings displaySettings; bool displaySettingsUpdated = false; + lv_obj_t* timeoutSwitch = nullptr; + lv_obj_t* timeoutDropdown = nullptr; static void onBacklightSliderEvent(lv_event_t* event) { auto* slider = static_cast(lv_event_get_target(event)); @@ -61,6 +63,33 @@ class DisplayApp final : public App { } } + static void onTimeoutSwitch(lv_event_t* event) { + auto* app = static_cast(lv_event_get_user_data(event)); + auto* sw = static_cast(lv_event_get_target(event)); + bool enabled = lv_obj_has_state(sw, LV_STATE_CHECKED); + app->displaySettings.backlightTimeoutEnabled = enabled; + app->displaySettingsUpdated = true; + if (app->timeoutDropdown) { + if (enabled) { + lv_obj_clear_state(app->timeoutDropdown, LV_STATE_DISABLED); + } else { + lv_obj_add_state(app->timeoutDropdown, LV_STATE_DISABLED); + } + } + } + + static void onTimeoutChanged(lv_event_t* event) { + auto* app = static_cast(lv_event_get_user_data(event)); + auto* dropdown = static_cast(lv_event_get_target(event)); + uint32_t idx = lv_dropdown_get_selected(dropdown); + // Map dropdown index to ms: 0=15s,1=30s,2=1m,3=2m,4=5m,5=Never + static const uint32_t values_ms[] = {15000, 30000, 60000, 120000, 300000, 0}; + if (idx < (sizeof(values_ms)/sizeof(values_ms[0]))) { + app->displaySettings.backlightTimeoutMs = values_ms[idx]; + app->displaySettingsUpdated = true; + } + } + public: void onShow(AppContext& app, lv_obj_t* parent) override { @@ -150,6 +179,48 @@ class DisplayApp final : public App { lv_obj_add_event_cb(orientation_dropdown, onOrientationSet, LV_EVENT_VALUE_CHANGED, this); // Set the dropdown to match current orientation enum lv_dropdown_set_selected(orientation_dropdown, static_cast(displaySettings.orientation)); + + // Screen timeout + + auto* timeout_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(timeout_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(timeout_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(timeout_wrapper, 0, LV_STATE_DEFAULT); + + auto* timeout_label = lv_label_create(timeout_wrapper); + lv_label_set_text(timeout_label, "Auto screen off"); + lv_obj_align(timeout_label, LV_ALIGN_LEFT_MID, 0, 0); + + timeoutSwitch = lv_switch_create(timeout_wrapper); + if (displaySettings.backlightTimeoutEnabled) { + lv_obj_add_state(timeoutSwitch, LV_STATE_CHECKED); + } + lv_obj_align(timeoutSwitch, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(timeoutSwitch, onTimeoutSwitch, LV_EVENT_VALUE_CHANGED, this); + + auto* timeout_select_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(timeout_select_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(timeout_select_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(timeout_select_wrapper, 0, LV_STATE_DEFAULT); + + auto* timeout_value_label = lv_label_create(timeout_select_wrapper); + lv_label_set_text(timeout_value_label, "Timeout"); + lv_obj_align(timeout_value_label, LV_ALIGN_LEFT_MID, 0, 0); + + timeoutDropdown = lv_dropdown_create(timeout_select_wrapper); + lv_dropdown_set_options(timeoutDropdown, "15 seconds\n30 seconds\n1 minute\n2 minutes\n5 minutes\nNever"); + lv_obj_align(timeoutDropdown, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_set_style_border_color(timeoutDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN); + lv_obj_set_style_border_width(timeoutDropdown, 1, LV_PART_MAIN); + lv_obj_add_event_cb(timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, this); + // Initialize dropdown selection from settings + uint32_t ms = displaySettings.backlightTimeoutMs; + uint32_t idx = 2; // default 1 minute + if (ms == 15000) idx = 0; else if (ms == 30000) idx = 1; else if (ms == 60000) idx = 2; else if (ms == 120000) idx = 3; else if (ms == 300000) idx = 4; else if (ms == 0) idx = 5; + lv_dropdown_set_selected(timeoutDropdown, idx); + if (!displaySettings.backlightTimeoutEnabled) { + lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED); + } } void onHide(TT_UNUSED AppContext& app) override { diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index 1387882f0..a0eef8816 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -15,7 +15,9 @@ #include #include +#include #include +#include #include #ifdef ESP_PLATFORM @@ -62,6 +64,16 @@ static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) { view->onNavigateUpPressed(); } +static void onNewFilePressedCallback(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + view->onNewFilePressed(); +} + +static void onNewFolderPressedCallback(lv_event_t* event) { + auto* view = static_cast(lv_event_get_user_data(event)); + view->onNewFolderPressed(); +} + // endregion void View::viewFile(const std::string& path, const std::string& filename) { @@ -179,7 +191,38 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) { } else { symbol = LV_SYMBOL_FILE; } - lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry.d_name); + + // Get file size for regular files + std::string label_text = dir_entry.d_name; + if (dir_entry.d_type == file::TT_DT_REG) { + std::string file_path = state->getCurrentPath() + "/" + dir_entry.d_name; + struct stat st; + if (stat(file_path.c_str(), &st) == 0) { + // Format file size in human-readable format + const char* size_suffix; + double size; + if (st.st_size < 1024) { + size = st.st_size; + size_suffix = " B"; + } else if (st.st_size < 1024 * 1024) { + size = st.st_size / 1024.0; + size_suffix = " KB"; + } else { + size = st.st_size / (1024.0 * 1024.0); + size_suffix = " MB"; + } + + char size_str[32]; + if (st.st_size < 1024) { + snprintf(size_str, sizeof(size_str), " (%d%s)", (int)size, size_suffix); + } else { + snprintf(size_str, sizeof(size_str), " (%.1f%s)", size, size_suffix); + } + label_text += size_str; + } + } + + lv_obj_t* button = lv_list_add_button(list, symbol, label_text.c_str()); lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this); lv_obj_add_event_cb(button, &onDirEntryLongPressedCallback, LV_EVENT_LONG_PRESSED, this); } @@ -212,6 +255,18 @@ void View::onDeletePressed() { alertdialog::start("Are you sure?", message, choices); } +void View::onNewFilePressed() { + TT_LOG_I(TAG, "Creating new file"); + state->setPendingAction(State::ActionCreateFile); + inputdialog::start("New File", "Enter filename:", ""); +} + +void View::onNewFolderPressed() { + TT_LOG_I(TAG, "Creating new folder"); + state->setPendingAction(State::ActionCreateFolder); + inputdialog::start("New Folder", "Enter folder name:", ""); +} + void View::showActionsForDirectory() { lv_obj_clean(action_list); @@ -262,6 +317,8 @@ void View::init(const AppContext& appContext, lv_obj_t* parent) { auto* toolbar = lvgl::toolbar_create(parent, appContext); navigate_up_button = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this); + new_file_button = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_FILE, &onNewFilePressedCallback, this); + new_folder_button = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_DIRECTORY, &onNewFolderPressedCallback, this); auto* wrapper = lv_obj_create(parent); lv_obj_set_width(wrapper, LV_PCT(100)); @@ -354,6 +411,42 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr bu } break; } + case State::ActionCreateFile: { + auto filename = inputdialog::getResult(*bundle); + if (!filename.empty()) { + std::string new_file_path = file::getChildPath(state->getCurrentPath(), filename); + auto lock = file::getLock(new_file_path); + lock->lock(); + + FILE* new_file = fopen(new_file_path.c_str(), "w"); + if (new_file) { + fclose(new_file); + TT_LOG_I(TAG, "Created file \"%s\"", new_file_path.c_str()); + } else { + TT_LOG_E(TAG, "Failed to create file \"%s\"", new_file_path.c_str()); + } + lock->unlock(); + + state->setEntriesForPath(state->getCurrentPath()); + update(); + } + break; + } + case State::ActionCreateFolder: { + auto foldername = inputdialog::getResult(*bundle); + if (!foldername.empty()) { + std::string new_folder_path = file::getChildPath(state->getCurrentPath(), foldername); + if (mkdir(new_folder_path.c_str(), 0755) == 0) { + TT_LOG_I(TAG, "Created folder \"%s\"", new_folder_path.c_str()); + } else { + TT_LOG_E(TAG, "Failed to create folder \"%s\"", new_folder_path.c_str()); + } + + state->setEntriesForPath(state->getCurrentPath()); + update(); + } + break; + } default: break; } diff --git a/Tactility/Source/app/keyboard/KeyboardSettings.cpp b/Tactility/Source/app/keyboard/KeyboardSettings.cpp new file mode 100644 index 000000000..c298b95e0 --- /dev/null +++ b/Tactility/Source/app/keyboard/KeyboardSettings.cpp @@ -0,0 +1,197 @@ +#include + +#include +#include +#include + +#include + +// Forward declare driver functions +namespace driver::keyboardbacklight { + bool setBrightness(uint8_t brightness); +} + +namespace driver::trackball { + void setEnabled(bool enabled); +} + +namespace tt::app::keyboardsettings { + +constexpr auto* TAG = "KeyboardSettings"; + +static void applyKeyboardBacklight(bool enabled, uint8_t brightness) { + driver::keyboardbacklight::setBrightness(enabled ? brightness : 0); +} + +class KeyboardSettingsApp final : public App { + + settings::keyboard::KeyboardSettings kbSettings; + bool updated = false; + lv_obj_t* switchBacklight = nullptr; + lv_obj_t* sliderBrightness = nullptr; + lv_obj_t* switchTrackball = nullptr; + lv_obj_t* switchTimeoutEnable = nullptr; + lv_obj_t* sliderTimeoutSeconds = nullptr; + lv_obj_t* labelTimeoutValue = nullptr; + + static void onBacklightSwitch(lv_event_t* e) { + auto* app = static_cast(lv_event_get_user_data(e)); + bool enabled = lv_obj_has_state(app->switchBacklight, LV_STATE_CHECKED); + app->kbSettings.backlightEnabled = enabled; + app->updated = true; + if (app->sliderBrightness) { + if (enabled) lv_obj_clear_state(app->sliderBrightness, LV_STATE_DISABLED); + else lv_obj_add_state(app->sliderBrightness, LV_STATE_DISABLED); + } + applyKeyboardBacklight(enabled, app->kbSettings.backlightBrightness); + } + + static void onBrightnessChanged(lv_event_t* e) { + auto* app = static_cast(lv_event_get_user_data(e)); + int32_t v = lv_slider_get_value(app->sliderBrightness); + app->kbSettings.backlightBrightness = static_cast(v); + app->updated = true; + if (app->kbSettings.backlightEnabled) { + applyKeyboardBacklight(true, app->kbSettings.backlightBrightness); + } + } + + static void onTrackballSwitch(lv_event_t* e) { + auto* app = static_cast(lv_event_get_user_data(e)); + bool enabled = lv_obj_has_state(app->switchTrackball, LV_STATE_CHECKED); + app->kbSettings.trackballEnabled = enabled; + app->updated = true; + driver::trackball::setEnabled(enabled); + } + + static void onTimeoutEnableSwitch(lv_event_t* e) { + auto* app = static_cast(lv_event_get_user_data(e)); + bool enabled = lv_obj_has_state(app->switchTimeoutEnable, LV_STATE_CHECKED); + app->kbSettings.backlightTimeoutEnabled = enabled; + app->updated = true; + if (app->sliderTimeoutSeconds) { + if (enabled) lv_obj_clear_state(app->sliderTimeoutSeconds, LV_STATE_DISABLED); + else lv_obj_add_state(app->sliderTimeoutSeconds, LV_STATE_DISABLED); + } + } + + static void onTimeoutSliderChanged(lv_event_t* e) { + auto* app = static_cast(lv_event_get_user_data(e)); + if (!app->sliderTimeoutSeconds) return; + int32_t seconds = lv_slider_get_value(app->sliderTimeoutSeconds); + app->kbSettings.backlightTimeoutMs = static_cast(seconds) * 1000; + app->updated = true; + if (app->labelTimeoutValue) { + lv_label_set_text_fmt(app->labelTimeoutValue, "%ld s", seconds); + } + } + +public: + void onShow(AppContext& app, lv_obj_t* parent) override { + kbSettings = settings::keyboard::loadOrGetDefault(); + + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); + + lvgl::toolbar_create(parent, app); + + auto* main_wrapper = lv_obj_create(parent); + lv_obj_set_flex_flow(main_wrapper, LV_FLEX_FLOW_COLUMN); + lv_obj_set_width(main_wrapper, LV_PCT(100)); + lv_obj_set_flex_grow(main_wrapper, 1); + + // Keyboard backlight toggle + auto* bl_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(bl_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(bl_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(bl_wrapper, 0, LV_STATE_DEFAULT); + auto* bl_label = lv_label_create(bl_wrapper); + lv_label_set_text(bl_label, "Keyboard backlight"); + lv_obj_align(bl_label, LV_ALIGN_LEFT_MID, 0, 0); + switchBacklight = lv_switch_create(bl_wrapper); + if (kbSettings.backlightEnabled) lv_obj_add_state(switchBacklight, LV_STATE_CHECKED); + lv_obj_align(switchBacklight, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(switchBacklight, onBacklightSwitch, LV_EVENT_VALUE_CHANGED, this); + + // Brightness slider + auto* br_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(br_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(br_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(br_wrapper, 0, LV_STATE_DEFAULT); + auto* br_label = lv_label_create(br_wrapper); + lv_label_set_text(br_label, "Brightness"); + lv_obj_align(br_label, LV_ALIGN_LEFT_MID, 0, 0); + sliderBrightness = lv_slider_create(br_wrapper); + lv_obj_set_width(sliderBrightness, LV_PCT(50)); + lv_obj_align(sliderBrightness, LV_ALIGN_RIGHT_MID, 0, 0); + lv_slider_set_range(sliderBrightness, 0, 255); + lv_slider_set_value(sliderBrightness, kbSettings.backlightBrightness, LV_ANIM_OFF); + if (!kbSettings.backlightEnabled) lv_obj_add_state(sliderBrightness, LV_STATE_DISABLED); + lv_obj_add_event_cb(sliderBrightness, onBrightnessChanged, LV_EVENT_VALUE_CHANGED, this); + + // Trackball toggle + auto* tb_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(tb_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(tb_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(tb_wrapper, 0, LV_STATE_DEFAULT); + auto* tb_label = lv_label_create(tb_wrapper); + lv_label_set_text(tb_label, "Trackball"); + lv_obj_align(tb_label, LV_ALIGN_LEFT_MID, 0, 0); + switchTrackball = lv_switch_create(tb_wrapper); + if (kbSettings.trackballEnabled) lv_obj_add_state(switchTrackball, LV_STATE_CHECKED); + lv_obj_align(switchTrackball, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(switchTrackball, onTrackballSwitch, LV_EVENT_VALUE_CHANGED, this); + + // Backlight timeout enable + auto* to_enable_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(to_enable_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(to_enable_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(to_enable_wrapper, 0, LV_STATE_DEFAULT); + auto* to_enable_label = lv_label_create(to_enable_wrapper); + lv_label_set_text(to_enable_label, "Backlight timeout"); + lv_obj_align(to_enable_label, LV_ALIGN_LEFT_MID, 0, 0); + switchTimeoutEnable = lv_switch_create(to_enable_wrapper); + if (kbSettings.backlightTimeoutEnabled) lv_obj_add_state(switchTimeoutEnable, LV_STATE_CHECKED); + lv_obj_align(switchTimeoutEnable, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(switchTimeoutEnable, onTimeoutEnableSwitch, LV_EVENT_VALUE_CHANGED, this); + + // Backlight timeout value (seconds) + auto* to_value_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(to_value_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(to_value_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(to_value_wrapper, 0, LV_STATE_DEFAULT); + auto* to_value_label = lv_label_create(to_value_wrapper); + lv_label_set_text(to_value_label, "Timeout (s)"); + lv_obj_align(to_value_label, LV_ALIGN_LEFT_MID, 0, 0); + labelTimeoutValue = lv_label_create(to_value_wrapper); + uint32_t timeoutSeconds = kbSettings.backlightTimeoutMs / 1000; + lv_label_set_text_fmt(labelTimeoutValue, "%lu s", (unsigned long)timeoutSeconds); + lv_obj_align(labelTimeoutValue, LV_ALIGN_RIGHT_MID, -60, 0); // leave room for slider + sliderTimeoutSeconds = lv_slider_create(to_value_wrapper); + lv_obj_set_width(sliderTimeoutSeconds, 120); + lv_obj_align(sliderTimeoutSeconds, LV_ALIGN_RIGHT_MID, 0, 0); + lv_slider_set_range(sliderTimeoutSeconds, 5, 600); // 5s to 10 minutes + if (timeoutSeconds < 5) timeoutSeconds = 5; + if (timeoutSeconds > 600) timeoutSeconds = 600; + lv_slider_set_value(sliderTimeoutSeconds, (int32_t)timeoutSeconds, LV_ANIM_OFF); + if (!kbSettings.backlightTimeoutEnabled) lv_obj_add_state(sliderTimeoutSeconds, LV_STATE_DISABLED); + lv_obj_add_event_cb(sliderTimeoutSeconds, onTimeoutSliderChanged, LV_EVENT_VALUE_CHANGED, this); + } + + void onHide(TT_UNUSED AppContext& app) override { + if (updated) { + const auto copy = kbSettings; + getMainDispatcher().dispatch([copy]{ settings::keyboard::save(copy); }); + } + } +}; + +extern const AppManifest manifest = { + .appId = "KeyboardSettings", + .appName = "Keyboard", + .appIcon = TT_ASSETS_APP_ICON_SETTINGS, + .appCategory = Category::Settings, + .createApp = create +}; + +} diff --git a/Tactility/Source/app/localesettings/LocaleSettings.cpp b/Tactility/Source/app/localesettings/LocaleSettings.cpp index 8a01bf533..ea08e24b7 100644 --- a/Tactility/Source/app/localesettings/LocaleSettings.cpp +++ b/Tactility/Source/app/localesettings/LocaleSettings.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -27,14 +28,9 @@ extern const AppManifest manifest; class LocaleSettingsApp final : public App { tt::i18n::TextResources textResources = tt::i18n::TextResources(TEXT_RESOURCE_PATH); Mutex mutex = Mutex(Mutex::Type::Recursive); - lv_obj_t* timeZoneLabel = nullptr; - lv_obj_t* regionLabel = nullptr; + lv_obj_t* regionTextArea = nullptr; lv_obj_t* languageDropdown = nullptr; - lv_obj_t* languageLabel = nullptr; - - static void onConfigureTimeZonePressed(TT_UNUSED lv_event_t* event) { - timezone::start(); - } + bool settingsUpdated = false; std::map languageMap; @@ -67,9 +63,6 @@ class LocaleSettingsApp final : public App { void updateViews() { textResources.load(); - lv_label_set_text(regionLabel , textResources[i18n::Text::REGION].c_str()); - lv_label_set_text(languageLabel, textResources[i18n::Text::LANGUAGE].c_str()); - std::string language_options = getLanguageOptions(); lv_dropdown_set_options(languageDropdown, language_options.c_str()); lv_dropdown_set_selected(languageDropdown, static_cast(settings::getLanguage())); @@ -85,6 +78,11 @@ class LocaleSettingsApp final : public App { self->updateViews(); } + static void onRegionChanged(lv_event_t* event) { + auto* self = static_cast(lv_event_get_user_data(event)); + self->settingsUpdated = true; + } + public: void onShow(AppContext& app, lv_obj_t* parent) override { @@ -107,42 +105,42 @@ class LocaleSettingsApp final : public App { auto* region_wrapper = lv_obj_create(main_wrapper); lv_obj_set_width(region_wrapper, LV_PCT(100)); lv_obj_set_height(region_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(region_wrapper, 0, 0); + lv_obj_set_style_pad_all(region_wrapper, 8, 0); lv_obj_set_style_border_width(region_wrapper, 0, 0); - regionLabel = lv_label_create(region_wrapper); - lv_label_set_text(regionLabel , textResources[i18n::Text::REGION].c_str()); - lv_obj_align(regionLabel , LV_ALIGN_LEFT_MID, 0, 0); - - auto* region_button = lv_button_create(region_wrapper); - lv_obj_align(region_button, LV_ALIGN_RIGHT_MID, 0, 0); - auto* region_button_image = lv_image_create(region_button); - lv_obj_add_event_cb(region_button, onConfigureTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr); - lv_image_set_src(region_button_image, LV_SYMBOL_SETTINGS); - - timeZoneLabel = lv_label_create(region_wrapper); - std::string timeZoneName = settings::getTimeZoneName(); - if (timeZoneName.empty()) { - timeZoneName = "not set"; + auto* region_label = lv_label_create(region_wrapper); + lv_label_set_text(region_label, textResources[i18n::Text::REGION].c_str()); + lv_obj_align(region_label, LV_ALIGN_LEFT_MID, 4, 0); + + // Region text area for user input (e.g., US, EU, JP) + regionTextArea = lv_textarea_create(region_wrapper); + lv_obj_set_width(regionTextArea, 120); + lv_textarea_set_one_line(regionTextArea, true); + lv_textarea_set_max_length(regionTextArea, 50); + lv_textarea_set_placeholder_text(regionTextArea, "e.g. US, EU"); + + // Load current region from settings + settings::SystemSettings sysSettings; + if (settings::loadSystemSettings(sysSettings)) { + lv_textarea_set_text(regionTextArea, sysSettings.region.c_str()); } - - lv_label_set_text(timeZoneLabel, timeZoneName.c_str()); - const int offset = ui_scale == hal::UiScale::Smallest ? -2 : -10; - lv_obj_align_to(timeZoneLabel, region_button, LV_ALIGN_OUT_LEFT_MID, offset, 0); + lv_obj_add_event_cb(regionTextArea, onRegionChanged, LV_EVENT_VALUE_CHANGED, this); + lv_obj_align(regionTextArea, LV_ALIGN_RIGHT_MID, 0, 0); // Language auto* language_wrapper = lv_obj_create(main_wrapper); lv_obj_set_width(language_wrapper, LV_PCT(100)); lv_obj_set_height(language_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(language_wrapper, 0, 0); + lv_obj_set_style_pad_all(language_wrapper, 8, 0); lv_obj_set_style_border_width(language_wrapper, 0, 0); - languageLabel = lv_label_create(language_wrapper); + auto* languageLabel = lv_label_create(language_wrapper); lv_label_set_text(languageLabel, textResources[i18n::Text::LANGUAGE].c_str()); - lv_obj_align(languageLabel, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_align(languageLabel, LV_ALIGN_LEFT_MID, 4, 0); languageDropdown = lv_dropdown_create(language_wrapper); + lv_obj_set_width(languageDropdown, 150); lv_obj_align(languageDropdown, LV_ALIGN_RIGHT_MID, 0, 0); std::string language_options = getLanguageOptions(); lv_dropdown_set_options(languageDropdown, language_options.c_str()); @@ -150,18 +148,12 @@ class LocaleSettingsApp final : public App { lv_obj_add_event_cb(languageDropdown, onLanguageSet, LV_EVENT_VALUE_CHANGED, this); } - void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr bundle) override { - if (result == Result::Ok && bundle != nullptr) { - const auto name = timezone::getResultName(*bundle); - const auto code = timezone::getResultCode(*bundle); - TT_LOG_I(TAG, "Result name=%s code=%s", name.c_str(), code.c_str()); - settings::setTimeZone(name, code); - - if (!name.empty()) { - if (lvgl::lock(100 / portTICK_PERIOD_MS)) { - lv_label_set_text(timeZoneLabel, name.c_str()); - lvgl::unlock(); - } + void onHide(TT_UNUSED AppContext& app) override { + if (settingsUpdated && regionTextArea) { + settings::SystemSettings sysSettings; + if (settings::loadSystemSettings(sysSettings)) { + sysSettings.region = lv_textarea_get_text(regionTextArea); + settings::saveSystemSettings(sysSettings); } } } diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index 1c4117e13..229d423e1 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -1,8 +1,11 @@ #include #include +#include #include +#include #include #include +#include #include @@ -15,6 +18,8 @@ extern const AppManifest manifest; class TimeDateSettingsApp final : public App { Mutex mutex = Mutex(Mutex::Type::Recursive); + lv_obj_t* timeZoneLabel = nullptr; + lv_obj_t* dateFormatDropdown = nullptr; static void onTimeFormatChanged(lv_event_t* event) { auto* widget = lv_event_get_target_obj(event); @@ -22,6 +27,24 @@ class TimeDateSettingsApp final : public App { settings::setTimeFormat24Hour(show_24); } + static void onTimeZonePressed(lv_event_t* event) { + timezone::start(); + } + + static void onDateFormatChanged(lv_event_t* event) { + auto* dropdown = static_cast(lv_event_get_target(event)); + auto index = lv_dropdown_get_selected(dropdown); + + const char* dateFormats[] = {"MM/DD/YYYY", "DD/MM/YYYY", "YYYY-MM-DD", "YYYY/MM/DD"}; + std::string selected_format = dateFormats[index]; + + settings::SystemSettings sysSettings; + if (settings::loadSystemSettings(sysSettings)) { + sysSettings.dateFormat = selected_format; + settings::saveSystemSettings(sysSettings); + } + } + public: void onShow(AppContext& app, lv_obj_t* parent) override { @@ -35,15 +58,17 @@ class TimeDateSettingsApp final : public App { lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_flex_grow(main_wrapper, 1); + // 24-hour format toggle + auto* time_format_wrapper = lv_obj_create(main_wrapper); lv_obj_set_width(time_format_wrapper, LV_PCT(100)); lv_obj_set_height(time_format_wrapper, LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(time_format_wrapper, 0, 0); + lv_obj_set_style_pad_all(time_format_wrapper, 8, 0); lv_obj_set_style_border_width(time_format_wrapper, 0, 0); auto* time_24h_label = lv_label_create(time_format_wrapper); - lv_label_set_text(time_24h_label, "24-hour clock"); - lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_label_set_text(time_24h_label, "24-hour format"); + lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 4, 0); auto* time_24h_switch = lv_switch_create(time_format_wrapper); lv_obj_align(time_24h_switch, LV_ALIGN_RIGHT_MID, 0, 0); @@ -53,6 +78,74 @@ class TimeDateSettingsApp final : public App { } else { lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED); } + + // Date format dropdown + + auto* date_format_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_width(date_format_wrapper, LV_PCT(100)); + lv_obj_set_height(date_format_wrapper, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(date_format_wrapper, 8, 0); + lv_obj_set_style_border_width(date_format_wrapper, 0, 0); + + auto* date_format_label = lv_label_create(date_format_wrapper); + lv_label_set_text(date_format_label, "Date format"); + lv_obj_align(date_format_label, LV_ALIGN_LEFT_MID, 4, 0); + + dateFormatDropdown = lv_dropdown_create(date_format_wrapper); + lv_obj_set_width(dateFormatDropdown, 150); + lv_obj_align(dateFormatDropdown, LV_ALIGN_RIGHT_MID, 0, 0); + lv_dropdown_set_options(dateFormatDropdown, "MM/DD/YYYY\nDD/MM/YYYY\nYYYY-MM-DD\nYYYY/MM/DD"); + + settings::SystemSettings sysSettings; + if (settings::loadSystemSettings(sysSettings)) { + int index = 0; + if (sysSettings.dateFormat == "DD/MM/YYYY") index = 1; + else if (sysSettings.dateFormat == "YYYY-MM-DD") index = 2; + else if (sysSettings.dateFormat == "YYYY/MM/DD") index = 3; + lv_dropdown_set_selected(dateFormatDropdown, index); + } + lv_obj_add_event_cb(dateFormatDropdown, onDateFormatChanged, LV_EVENT_VALUE_CHANGED, nullptr); + + // Timezone selector + + auto* timezone_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_width(timezone_wrapper, LV_PCT(100)); + lv_obj_set_height(timezone_wrapper, LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(timezone_wrapper, 8, 0); + lv_obj_set_style_border_width(timezone_wrapper, 0, 0); + + auto* timezone_label = lv_label_create(timezone_wrapper); + lv_label_set_text(timezone_label, "Timezone"); + lv_obj_align(timezone_label, LV_ALIGN_LEFT_MID, 4, 0); + + auto* timezone_button = lv_button_create(timezone_wrapper); + lv_obj_set_width(timezone_button, 150); + lv_obj_align(timezone_button, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(timezone_button, onTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr); + + timeZoneLabel = lv_label_create(timezone_button); + std::string timeZoneName = settings::getTimeZoneName(); + if (timeZoneName.empty()) { + timeZoneName = "not set"; + } + lv_obj_center(timeZoneLabel); + lv_label_set_text(timeZoneLabel, timeZoneName.c_str()); + } + + void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr bundle) override { + if (result == Result::Ok && bundle != nullptr) { + const auto name = timezone::getResultName(*bundle); + const auto code = timezone::getResultCode(*bundle); + TT_LOG_I(TAG, "Result name=%s code=%s", name.c_str(), code.c_str()); + settings::setTimeZone(name, code); + + if (!name.empty()) { + if (lvgl::lock(100 / portTICK_PERIOD_MS)) { + lv_label_set_text(timeZoneLabel, name.c_str()); + lvgl::unlock(); + } + } + } } }; @@ -69,3 +162,4 @@ LaunchId start() { } } // namespace + diff --git a/Tactility/Source/file/PropertiesFile.cpp b/Tactility/Source/file/PropertiesFile.cpp index 5335a8bef..e6bc3a31b 100644 --- a/Tactility/Source/file/PropertiesFile.cpp +++ b/Tactility/Source/file/PropertiesFile.cpp @@ -19,13 +19,17 @@ bool getKeyValuePair(const std::string& input, std::string& key, std::string& va } bool loadPropertiesFile(const std::string& filePath, std::function callback) { - TT_LOG_I(TAG, "Reading properties file %s", filePath.c_str()); + // Reading properties is a common operation; make this debug-level to avoid + // flooding the serial console under frequent polling. + TT_LOG_D(TAG, "Reading properties file %s", filePath.c_str()); uint16_t line_count = 0; std::string key_prefix = ""; + // Malformed lines are skipped, valid lines are loaded and callback is called return readLines(filePath, true, [&key_prefix, &line_count, &filePath, &callback](const std::string& line) { line_count++; std::string key, value; - auto trimmed_line = string::trim(line, " \t"); + // Trim all whitespace including \r\n (Windows line endings) + auto trimmed_line = string::trim(line, " \t\r\n"); if (!trimmed_line.starts_with("#") && !trimmed_line.empty()) { if (trimmed_line.starts_with("[")) { key_prefix = trimmed_line; @@ -35,7 +39,8 @@ bool loadPropertiesFile(const std::string& filePath, std::functioninitI2cDevices) { + TT_LOG_I(TAG, "Starting I2C devices"); + config->initI2cDevices(); + } +} + void init(const Configuration& configuration) { kernel::publishSystemEvent(kernel::SystemEvent::BootInitHalBegin); @@ -89,6 +99,8 @@ void init(const Configuration& configuration) { startDisplays(); // Warning: SPI displays need to start after SPI SD cards are mounted + startI2cDevices(); // Start I2C peripheral devices after displays (they might depend on display being ready) + kernel::publishSystemEvent(kernel::SystemEvent::BootInitHalEnd); } diff --git a/Tactility/Source/service/displayidle/DisplayIdle.cpp b/Tactility/Source/service/displayidle/DisplayIdle.cpp new file mode 100644 index 000000000..9c554049b --- /dev/null +++ b/Tactility/Source/service/displayidle/DisplayIdle.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef ESP_PLATFORM +#include +#endif + +// Forward declare driver functions +namespace driver::keyboardbacklight { + bool setBrightness(uint8_t brightness); +} + +namespace tt::service::displayidle { + +constexpr auto* TAG = "DisplayIdle"; + +class DisplayIdleService final : public Service { + + std::unique_ptr timer; + bool displayDimmed = false; + bool keyboardDimmed = false; + settings::display::DisplaySettings cachedDisplaySettings; + settings::keyboard::KeyboardSettings cachedKeyboardSettings; + + static std::shared_ptr getDisplay() { + return hal::findFirstDevice(hal::Device::Type::Display); + } + + static std::shared_ptr getKeyboard() { + return hal::findFirstDevice(hal::Device::Type::Keyboard); + } + + void reloadSettings() { + cachedDisplaySettings = settings::display::loadOrGetDefault(); + cachedKeyboardSettings = settings::keyboard::loadOrGetDefault(); + } + + void tick() { + // Settings are now cached and event-driven (no file I/O in timer callback!) + // This prevents watchdog timeout from blocking the Timer Service task + + // Query LVGL inactivity once for both checks + uint32_t inactive_ms = 0; + if (lvgl::lock(100)) { + inactive_ms = lv_disp_get_inactive_time(nullptr); + lvgl::unlock(); + } + + // Handle display backlight + auto display = getDisplay(); + if (display != nullptr && display->supportsBacklightDuty()) { + // If timeout disabled, ensure backlight restored if we had dimmed it + if (!cachedDisplaySettings.backlightTimeoutEnabled || cachedDisplaySettings.backlightTimeoutMs == 0) { + if (displayDimmed) { + display->setBacklightDuty(cachedDisplaySettings.backlightDuty); + displayDimmed = false; + } + } else { + if (!displayDimmed && inactive_ms >= cachedDisplaySettings.backlightTimeoutMs) { + display->setBacklightDuty(0); + displayDimmed = true; + } else if (displayDimmed && inactive_ms < 100) { + display->setBacklightDuty(cachedDisplaySettings.backlightDuty); + displayDimmed = false; + } + } + } + + // Handle keyboard backlight + auto keyboard = getKeyboard(); + if (keyboard != nullptr && keyboard->isAttached()) { + if (!cachedKeyboardSettings.backlightTimeoutEnabled || cachedKeyboardSettings.backlightTimeoutMs == 0) { + if (keyboardDimmed) { + driver::keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0); + keyboardDimmed = false; + } + } else { + if (!keyboardDimmed && inactive_ms >= cachedKeyboardSettings.backlightTimeoutMs) { + driver::keyboardbacklight::setBrightness(0); + keyboardDimmed = true; + } else if (keyboardDimmed && inactive_ms < 100) { + driver::keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0); + keyboardDimmed = false; + } + } + } + } + +public: + bool onStart(TT_UNUSED ServiceContext& service) override { + // Load settings once at startup and cache them + // This eliminates file I/O from timer callback (prevents watchdog timeout) + cachedDisplaySettings = settings::display::loadOrGetDefault(); + cachedKeyboardSettings = settings::keyboard::loadOrGetDefault(); + + // Note: Settings changes require service restart to take effect + // TODO: Add DisplaySettingsChanged/KeyboardSettingsChanged events for dynamic updates + + timer = std::make_unique(Timer::Type::Periodic, [this]{ this->tick(); }); + timer->setThreadPriority(Thread::Priority::Lower); + timer->start(250); // check 4x per second for snappy restore + return true; + } + + void onStop(TT_UNUSED ServiceContext& service) override { + if (timer) { + timer->stop(); + timer = nullptr; + } + // Ensure display restored on stop + auto display = getDisplay(); + if (display && displayDimmed) { + display->setBacklightDuty(cachedDisplaySettings.backlightDuty); + displayDimmed = false; + } + // Ensure keyboard backlight restored on stop + auto keyboard = getKeyboard(); + if (keyboard && keyboardDimmed) { + driver::keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0); + keyboardDimmed = false; + } + } +}; + +extern const ServiceManifest manifest = { + .id = "DisplayIdle", + .createService = create +}; + +} diff --git a/Tactility/Source/service/keyboardinit/KeyboardInit.cpp b/Tactility/Source/service/keyboardinit/KeyboardInit.cpp new file mode 100644 index 000000000..423222a99 --- /dev/null +++ b/Tactility/Source/service/keyboardinit/KeyboardInit.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include +#include +#include + +// Forward declare driver functions +namespace driver::keyboardbacklight { + bool setBrightness(uint8_t brightness); +} + +namespace driver::trackball { + void setEnabled(bool enabled); +} + +namespace tt::service::keyboardinit { + +constexpr auto* TAG = "KeyboardInit"; + +class KeyboardInitService final : public Service { +public: + bool onStart(TT_UNUSED ServiceContext& service) override { + auto settings = settings::keyboard::loadOrGetDefault(); + + // Apply keyboard backlight setting + bool result = driver::keyboardbacklight::setBrightness(settings.backlightEnabled ? settings.backlightBrightness : 0); + if (!result) { + TT_LOG_W(TAG, "Failed to set keyboard backlight brightness"); + } + + // Apply trackball enabled setting + driver::trackball::setEnabled(settings.trackballEnabled); + + return true; + } + + void onStop(TT_UNUSED ServiceContext& service) override { + // Nothing to clean up + } +}; + +extern const ServiceManifest manifest = { + .id = "KeyboardInit", + .createService = create +}; + +} diff --git a/Tactility/Source/settings/DisplaySettings.cpp b/Tactility/Source/settings/DisplaySettings.cpp index 4c8bd77c5..33a1e9fce 100644 --- a/Tactility/Source/settings/DisplaySettings.cpp +++ b/Tactility/Source/settings/DisplaySettings.cpp @@ -15,6 +15,8 @@ constexpr auto* SETTINGS_FILE = "/data/settings/display.properties"; constexpr auto* SETTINGS_KEY_ORIENTATION = "orientation"; constexpr auto* SETTINGS_KEY_GAMMA_CURVE = "gammaCurve"; constexpr auto* SETTINGS_KEY_BACKLIGHT_DUTY = "backlightDuty"; +constexpr auto* SETTINGS_KEY_TIMEOUT_ENABLED = "backlightTimeoutEnabled"; +constexpr auto* SETTINGS_KEY_TIMEOUT_MS = "backlightTimeoutMs"; static Orientation getDefaultOrientation() { auto* display = lv_display_get_default(); @@ -90,9 +92,23 @@ bool load(DisplaySettings& settings) { } } + bool timeout_enabled = true; + auto timeout_enabled_entry = map.find(SETTINGS_KEY_TIMEOUT_ENABLED); + if (timeout_enabled_entry != map.end()) { + timeout_enabled = (timeout_enabled_entry->second == "1" || timeout_enabled_entry->second == "true" || timeout_enabled_entry->second == "True"); + } + + uint32_t timeout_ms = 60000; // default 60s + auto timeout_ms_entry = map.find(SETTINGS_KEY_TIMEOUT_MS); + if (timeout_ms_entry != map.end()) { + timeout_ms = static_cast(std::strtoul(timeout_ms_entry->second.c_str(), nullptr, 10)); + } + settings.orientation = orientation; settings.gammaCurve = gamma_curve; settings.backlightDuty = backlight_duty; + settings.backlightTimeoutEnabled = timeout_enabled; + settings.backlightTimeoutMs = timeout_ms; return true; } @@ -101,7 +117,9 @@ DisplaySettings getDefault() { return DisplaySettings { .orientation = getDefaultOrientation(), .gammaCurve = 1, - .backlightDuty = 200 + .backlightDuty = 200, + .backlightTimeoutEnabled = true, + .backlightTimeoutMs = 60000 }; } @@ -118,6 +136,8 @@ bool save(const DisplaySettings& settings) { map[SETTINGS_KEY_BACKLIGHT_DUTY] = std::to_string(settings.backlightDuty); map[SETTINGS_KEY_GAMMA_CURVE] = std::to_string(settings.gammaCurve); map[SETTINGS_KEY_ORIENTATION] = toString(settings.orientation); + map[SETTINGS_KEY_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0"; + map[SETTINGS_KEY_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs); return file::savePropertiesFile(SETTINGS_FILE, map); } diff --git a/Tactility/Source/settings/KeyboardSettings.cpp b/Tactility/Source/settings/KeyboardSettings.cpp new file mode 100644 index 000000000..e2462bbe2 --- /dev/null +++ b/Tactility/Source/settings/KeyboardSettings.cpp @@ -0,0 +1,65 @@ +#include +#include + +#include +#include + +namespace tt::settings::keyboard { + +constexpr auto* SETTINGS_FILE = "/data/settings/keyboard.properties"; +constexpr auto* KEY_BACKLIGHT_ENABLED = "backlightEnabled"; +constexpr auto* KEY_BACKLIGHT_BRIGHTNESS = "backlightBrightness"; +constexpr auto* KEY_TRACKBALL_ENABLED = "trackballEnabled"; +constexpr auto* KEY_BACKLIGHT_TIMEOUT_ENABLED = "backlightTimeoutEnabled"; +constexpr auto* KEY_BACKLIGHT_TIMEOUT_MS = "backlightTimeoutMs"; + +bool load(KeyboardSettings& settings) { + std::map map; + if (!file::loadPropertiesFile(SETTINGS_FILE, map)) { + return false; + } + + auto bl_enabled = map.find(KEY_BACKLIGHT_ENABLED); + auto bl_brightness = map.find(KEY_BACKLIGHT_BRIGHTNESS); + auto tb_enabled = map.find(KEY_TRACKBALL_ENABLED); + auto bl_timeout_enabled = map.find(KEY_BACKLIGHT_TIMEOUT_ENABLED); + auto bl_timeout_ms = map.find(KEY_BACKLIGHT_TIMEOUT_MS); + + settings.backlightEnabled = (bl_enabled != map.end()) ? (bl_enabled->second == "1" || bl_enabled->second == "true" || bl_enabled->second == "True") : true; + settings.backlightBrightness = (bl_brightness != map.end()) ? static_cast(std::stoi(bl_brightness->second)) : 127; + settings.trackballEnabled = (tb_enabled != map.end()) ? (tb_enabled->second == "1" || tb_enabled->second == "true" || tb_enabled->second == "True") : true; + settings.backlightTimeoutEnabled = (bl_timeout_enabled != map.end()) ? (bl_timeout_enabled->second == "1" || bl_timeout_enabled->second == "true" || bl_timeout_enabled->second == "True") : true; + settings.backlightTimeoutMs = (bl_timeout_ms != map.end()) ? static_cast(std::stoul(bl_timeout_ms->second)) : 30000; // Default 30 seconds + + return true; +} + +KeyboardSettings getDefault() { + return KeyboardSettings{ + .backlightEnabled = true, + .backlightBrightness = 127, + .trackballEnabled = true, + .backlightTimeoutEnabled = true, + .backlightTimeoutMs = 30000 // 30 seconds default + }; +} + +KeyboardSettings loadOrGetDefault() { + KeyboardSettings s; + if (!load(s)) { + s = getDefault(); + } + return s; +} + +bool save(const KeyboardSettings& settings) { + std::map map; + map[KEY_BACKLIGHT_ENABLED] = settings.backlightEnabled ? "1" : "0"; + map[KEY_BACKLIGHT_BRIGHTNESS] = std::to_string(settings.backlightBrightness); + map[KEY_TRACKBALL_ENABLED] = settings.trackballEnabled ? "1" : "0"; + map[KEY_BACKLIGHT_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0"; + map[KEY_BACKLIGHT_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs); + return file::savePropertiesFile(SETTINGS_FILE, map); +} + +} diff --git a/Tactility/Source/settings/SystemSettings.cpp b/Tactility/Source/settings/SystemSettings.cpp index 2a06f17c2..9ce01faed 100644 --- a/Tactility/Source/settings/SystemSettings.cpp +++ b/Tactility/Source/settings/SystemSettings.cpp @@ -40,6 +40,25 @@ static bool loadSystemSettingsFromFile(SystemSettings& properties) { bool time_format_24h = time_format_entry == map.end() ? true : (time_format_entry->second == "true"); properties.timeFormat24h = time_format_24h; + // Load date format + // Default to MM/DD/YYYY if missing (backward compat with older system.properties) + auto date_format_entry = map.find("dateFormat"); + if (date_format_entry != map.end() && !date_format_entry->second.empty()) { + properties.dateFormat = date_format_entry->second; + } else { + TT_LOG_I(TAG, "dateFormat missing or empty, using default MM/DD/YYYY (likely from older system.properties)"); + properties.dateFormat = "MM/DD/YYYY"; + } + + // Load region + auto region_entry = map.find("region"); + if (region_entry != map.end() && !region_entry->second.empty()) { + properties.region = region_entry->second; + } else { + TT_LOG_I(TAG, "region missing or empty, using default US"); + properties.region = "US"; + } + TT_LOG_I(TAG, "System settings loaded"); return true; } @@ -61,12 +80,15 @@ bool saveSystemSettings(const SystemSettings& properties) { std::map map; map["language"] = toString(properties.language); map["timeFormat24h"] = properties.timeFormat24h ? "true" : "false"; + map["dateFormat"] = properties.dateFormat; + map["region"] = properties.region; if (!file::savePropertiesFile(file_path, map)) { TT_LOG_E(TAG, "Failed to save %s", file_path.c_str()); return false; } + // Update local cache cachedSettings = properties; cached = true; return true; diff --git a/Tactility/Source/settings/Time.cpp b/Tactility/Source/settings/Time.cpp index a9cfa48ef..c00517179 100644 --- a/Tactility/Source/settings/Time.cpp +++ b/Tactility/Source/settings/Time.cpp @@ -45,7 +45,7 @@ std::string getTimeZoneName() { if (preferences.optString(TIMEZONE_PREFERENCES_KEY_NAME, result)) { return result; } else { - return {}; + return "America/Los_Angeles"; // Default: Pacific Time (PST/PDT) } } @@ -55,7 +55,7 @@ std::string getTimeZoneCode() { if (preferences.optString(TIMEZONE_PREFERENCES_KEY_CODE, result)) { return result; } else { - return {}; + return "PST8PDT,M3.2.0,M11.1.0"; // Default: Pacific Time POSIX string } } From 55c431e7fcf21b0510b40aa72ca640e3bc383efd Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 24 Dec 2025 14:53:34 +1000 Subject: [PATCH 02/22] Systeminfo additions --- .../Source/app/systeminfo/SystemInfo.cpp | 491 ++++++++++++++++-- 1 file changed, 448 insertions(+), 43 deletions(-) diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index e81e535bb..58a7abbbd 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -11,6 +12,7 @@ #ifdef ESP_PLATFORM #include +#include #include #endif @@ -50,6 +52,22 @@ static size_t getSpiTotal() { #endif } +static size_t getPsramMinFree() { +#ifdef ESP_PLATFORM + return heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM); +#else + return 4096 * 1024; +#endif +} + +static size_t getPsramLargestBlock() { +#ifdef ESP_PLATFORM + return heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM); +#else + return 4096 * 1024; +#endif +} + enum class StorageUnit { Bytes, Kilobytes, @@ -102,8 +120,12 @@ static std::string getStorageValue(StorageUnit unit, uint64_t bytes) { } } -static void addMemoryBar(lv_obj_t* parent, const char* label, uint64_t free, uint64_t total) { - uint64_t used = total - free; +struct MemoryBarWidgets { + lv_obj_t* bar = nullptr; + lv_obj_t* label = nullptr; +}; + +static MemoryBarWidgets createMemoryBar(lv_obj_t* parent, const char* label) { auto* container = lv_obj_create(parent); lv_obj_set_size(container, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(container, 0, LV_STATE_DEFAULT); @@ -118,6 +140,22 @@ static void addMemoryBar(lv_obj_t* parent, const char* label, uint64_t free, uin auto* bar = lv_bar_create(container); lv_obj_set_flex_grow(bar, 1); + auto* bottom_label = lv_label_create(parent); + lv_obj_set_width(bottom_label, LV_PCT(100)); + lv_obj_set_style_text_align(bottom_label, LV_TEXT_ALIGN_RIGHT, 0); + + if (hal::getConfiguration()->uiScale == hal::UiScale::Smallest) { + lv_obj_set_style_pad_bottom(bottom_label, 2, LV_STATE_DEFAULT); + } else { + lv_obj_set_style_pad_bottom(bottom_label, 12, LV_STATE_DEFAULT); + } + + return {bar, bottom_label}; +} + +static void updateMemoryBar(const MemoryBarWidgets& widgets, uint64_t free, uint64_t total) { + uint64_t used = total - free; + // Scale down the uint64_t until it fits int32_t for the lv_bar uint64_t free_scaled = free; uint64_t total_scaled = total; @@ -127,27 +165,20 @@ static void addMemoryBar(lv_obj_t* parent, const char* label, uint64_t free, uin } if (total > 0) { - lv_bar_set_range(bar, 0, total_scaled); + lv_bar_set_range(widgets.bar, 0, total_scaled); } else { - lv_bar_set_range(bar, 0, 1); + lv_bar_set_range(widgets.bar, 0, 1); } - lv_bar_set_value(bar, (total_scaled - free_scaled), LV_ANIM_OFF); + lv_bar_set_value(widgets.bar, (total_scaled - free_scaled), LV_ANIM_OFF); - auto* bottom_label = lv_label_create(parent); const auto unit = getStorageUnit(total); const auto unit_label = getStorageUnitString(unit); - const auto used_converted = getStorageValue(unit, used); + const auto free_converted = getStorageValue(unit, free); const auto total_converted = getStorageValue(unit, total); - lv_label_set_text_fmt(bottom_label, "%s / %s %s used", used_converted.c_str(), total_converted.c_str(), unit_label.c_str()); - lv_obj_set_width(bottom_label, LV_PCT(100)); - lv_obj_set_style_text_align(bottom_label, LV_TEXT_ALIGN_RIGHT, 0); - - if (hal::getConfiguration()->uiScale == hal::UiScale::Smallest) { - lv_obj_set_style_pad_bottom(bottom_label, 2, LV_STATE_DEFAULT); - } else { - lv_obj_set_style_pad_bottom(bottom_label, 12, LV_STATE_DEFAULT); - } + lv_label_set_text_fmt(widgets.label, "%s / %s %s free (%llu / %llu bytes)", + free_converted.c_str(), total_converted.c_str(), unit_label.c_str(), + (unsigned long long)free, (unsigned long long)total); } #if configUSE_TRACE_FACILITY @@ -170,22 +201,52 @@ static const char* getTaskState(const TaskStatus_t& task) { } } -static void addRtosTask(lv_obj_t* parent, const TaskStatus_t& task) { +static void clearContainer(lv_obj_t* container) { + lv_obj_clean(container); +} + +static void addRtosTask(lv_obj_t* parent, const TaskStatus_t& task, uint32_t totalRuntime) { auto* label = lv_label_create(parent); const char* name = (task.pcTaskName == nullptr || task.pcTaskName[0] == 0) ? "(unnamed)" : task.pcTaskName; + + // If totalRuntime provided, show CPU percentage; otherwise just show state + if (totalRuntime > 0) { + float cpu_percent = (task.ulRunTimeCounter * 100.0f) / totalRuntime; + lv_label_set_text_fmt(label, "%s: %.1f%%", name, cpu_percent); + } else { lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task)); } +} + +static void updateRtosTasks(lv_obj_t* parent, bool showCpuPercent) { + clearContainer(parent); -static void addRtosTasks(lv_obj_t* parent) { UBaseType_t count = uxTaskGetNumberOfTasks(); - auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); + // Allocate task list in PSRAM to save internal RAM + auto* tasks = (TaskStatus_t*)heap_caps_malloc(sizeof(TaskStatus_t) * count, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!tasks) { + // Fallback to internal RAM if PSRAM allocation fails + tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); + } + if (!tasks) { + auto* error_label = lv_label_create(parent); + lv_label_set_text(error_label, "Failed to allocate memory for task list"); + return; + } uint32_t totalRuntime = 0; UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime); + // Sort by CPU usage if showing percentages, otherwise keep original order + if (showCpuPercent) { + std::sort(tasks, tasks + actual, [](const TaskStatus_t& a, const TaskStatus_t& b) { + return a.ulRunTimeCounter > b.ulRunTimeCounter; + }); + } + for (int i = 0; i < actual; ++i) { - const TaskStatus_t& task = tasks[i]; - addRtosTask(parent, task); + addRtosTask(parent, tasks[i], showCpuPercent ? totalRuntime : 0); } + free(tasks); } @@ -211,14 +272,308 @@ static lv_obj_t* createTab(lv_obj_t* tabview, const char* name) { return tab; } +extern const AppManifest manifest; + +class SystemInfoApp; + +static std::shared_ptr _Nullable optApp() { + auto appContext = getCurrentAppContext(); + if (appContext != nullptr && appContext->getManifest().appId == manifest.appId) { + return std::static_pointer_cast(appContext->getApp()); + } + return nullptr; +} + class SystemInfoApp final : public App { + Timer memoryTimer = Timer(Timer::Type::Periodic, []() { + auto app = optApp(); + if (app) { + app->updateMemory(); + app->updatePsram(); + } + }); + + Timer tasksTimer = Timer(Timer::Type::Periodic, []() { + auto app = optApp(); + if (app) app->updateTasks(); + }); + + MemoryBarWidgets internalMemBar; + MemoryBarWidgets externalMemBar; + MemoryBarWidgets dataStorageBar; + MemoryBarWidgets sdcardStorageBar; + MemoryBarWidgets systemStorageBar; + + lv_obj_t* tasksContainer = nullptr; + lv_obj_t* cpuContainer = nullptr; + lv_obj_t* psramContainer = nullptr; + lv_obj_t* cpuSummaryLabel = nullptr; // Shows overall CPU utilization + lv_obj_t* cpuCore0Label = nullptr; // Shows Core 0 tasks + lv_obj_t* cpuCore1Label = nullptr; // Shows Core 1 tasks + + bool hasExternalMem = false; + bool hasDataStorage = false; + bool hasSdcardStorage = false; + bool hasSystemStorage = false; + + void updateMemory() { + // Timer callbacks run in a task context (not ISR), so we can use LVGL lock + // Don't use kernel::lock() from timer callbacks - causes scheduler assertion + updateMemoryBar(internalMemBar, getHeapFree(), getHeapTotal()); + + if (hasExternalMem) { + updateMemoryBar(externalMemBar, getSpiFree(), getSpiTotal()); + } + } + + void updateStorage() { +#ifdef ESP_PLATFORM + uint64_t storage_total = 0; + uint64_t storage_free = 0; + + if (hasDataStorage) { + if (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK) { + updateMemoryBar(dataStorageBar, storage_free, storage_total); + } + } + + if (hasSdcardStorage) { + const auto sdcard_devices = hal::findDevices(hal::Device::Type::SdCard); + for (const auto& sdcard : sdcard_devices) { + if (sdcard->isMounted() && esp_vfs_fat_info(sdcard->getMountPath().c_str(), &storage_total, &storage_free) == ESP_OK) { + updateMemoryBar(sdcardStorageBar, storage_free, storage_total); + break; // Only update first SD card + } + } + } + + if (hasSystemStorage) { + if (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK) { + updateMemoryBar(systemStorageBar, storage_free, storage_total); + } + } +#endif + } + + void updateTasks() { +#if configUSE_TRACE_FACILITY + if (tasksContainer) { + updateRtosTasks(tasksContainer, false); // Tasks tab: show state + } + + if (cpuContainer) { + updateRtosTasks(cpuContainer, true); // CPU tab: show percentages + + // Update CPU summary at top of tab + // Note: FreeRTOS runtime stats accumulate since boot, so percentages + // are averages over entire uptime, not instantaneous usage + if (cpuSummaryLabel && cpuCore0Label && cpuCore1Label) { + UBaseType_t count = uxTaskGetNumberOfTasks(); + auto* tasks = (TaskStatus_t*)heap_caps_malloc(sizeof(TaskStatus_t) * count, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!tasks) tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); + if (tasks) { + uint32_t totalRuntime = 0; + UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime); + + if (totalRuntime > 0 && actual > 0) { + // Calculate total CPU usage (100% - idle = usage) + uint32_t idleTime = 0; + for (int i = 0; i < actual; ++i) { + const char* name = tasks[i].pcTaskName; + if (name && (strcmp(name, "IDLE0") == 0 || strcmp(name, "IDLE1") == 0)) { + idleTime += tasks[i].ulRunTimeCounter; + } + } + + float cpuUsage = ((totalRuntime - idleTime) * 100.0f) / totalRuntime; + auto summary_text = std::format("Overall CPU Usage: {:.1f}% (avg since boot)", cpuUsage); + lv_label_set_text(cpuSummaryLabel, summary_text.c_str()); + + // Show total task count + auto core_text = std::format("Active Tasks: {} total", actual); + lv_label_set_text(cpuCore0Label, core_text.c_str()); + + // Show uptime estimate (runtime is in ticks, typically 1 tick = 10us) + // totalRuntime accumulates across all cores/tasks + float uptime_sec = totalRuntime / (configTICK_RATE_HZ * 100000.0f); + auto uptime_text = std::format("System Uptime: {:.1f} min", uptime_sec / 60.0f); + lv_label_set_text(cpuCore1Label, uptime_text.c_str()); + } else { + lv_label_set_text(cpuSummaryLabel, "Overall CPU Usage: --.-%"); + lv_label_set_text(cpuCore0Label, "Active Tasks: --"); + lv_label_set_text(cpuCore1Label, "System Uptime: --"); + } + + free(tasks); + } + } + } +#endif + } + + void updatePsram() { +#ifdef ESP_PLATFORM + if (!psramContainer || !hasExternalMem) return; + + clearContainer(psramContainer); + + size_t free_mem = getSpiFree(); + size_t total = getSpiTotal(); + size_t used = total - free_mem; + size_t min_free = getPsramMinFree(); + size_t largest_block = getPsramLargestBlock(); + size_t peak_usage = total - min_free; + + // Safety check - if no PSRAM, show error + if (total == 0) { + auto* error_label = lv_label_create(psramContainer); + lv_label_set_text(error_label, "No PSRAM detected"); + return; + } + + // Summary + auto* summary_label = lv_label_create(psramContainer); + lv_label_set_text(summary_label, "PSRAM Usage Summary"); + lv_obj_set_style_text_font(summary_label, &lv_font_montserrat_14, 0); + lv_obj_set_style_pad_bottom(summary_label, 8, 0); + + // Current usage + auto* usage_label = lv_label_create(psramContainer); + float used_mb = used / (1024.0f * 1024.0f); + float total_mb = total / (1024.0f * 1024.0f); + float used_percent = (used * 100.0f) / total; + auto usage_text = std::format("Current: {:.2f} / {:.2f} MB ({:.1f}% used)", + used_mb, total_mb, used_percent); + lv_label_set_text(usage_label, usage_text.c_str()); + + // Peak usage + auto* peak_label = lv_label_create(psramContainer); + float peak_mb = peak_usage / (1024.0f * 1024.0f); + float peak_percent = (peak_usage * 100.0f) / total; + auto peak_text = std::format("Peak: {:.2f} MB ({:.1f}% of total)", + peak_mb, peak_percent); + lv_label_set_text(peak_label, peak_text.c_str()); + + // Minimum free (lowest point) + auto* min_free_label = lv_label_create(psramContainer); + float min_free_mb = min_free / (1024.0f * 1024.0f); + auto min_free_text = std::format("Min Free: {:.2f} MB", min_free_mb); + lv_label_set_text(min_free_label, min_free_text.c_str()); + + // Largest contiguous block + auto* largest_label = lv_label_create(psramContainer); + float largest_mb = largest_block / (1024.0f * 1024.0f); + auto largest_text = std::format("Largest Block: {:.2f} MB", largest_mb); + lv_label_set_text(largest_label, largest_text.c_str()); + + // Spacer + auto* spacer = lv_obj_create(psramContainer); + lv_obj_set_size(spacer, LV_PCT(100), 16); + lv_obj_set_style_bg_opa(spacer, 0, 0); + lv_obj_set_style_border_width(spacer, 0, 0); + + // PSRAM Configuration section + auto* config_header = lv_label_create(psramContainer); + lv_label_set_text(config_header, "PSRAM Configuration"); + lv_obj_set_style_text_font(config_header, &lv_font_montserrat_14, 0); + lv_obj_set_style_pad_bottom(config_header, 8, 0); + + // Get threshold from sdkconfig +#ifdef CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL + const int threshold = CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL; +#else + const int threshold = 16384; // Default ESP-IDF value +#endif + + // Display threshold configuration + auto* threshold_info = lv_label_create(psramContainer); + if (threshold >= 1024) { + lv_label_set_text_fmt(threshold_info, "• Threshold: >=%d KB -> PSRAM", threshold / 1024); + } else { + lv_label_set_text_fmt(threshold_info, "• Threshold: >=%d bytes -> PSRAM", threshold); + } + + auto* internal_info = lv_label_create(psramContainer); + if (threshold >= 1024) { + lv_label_set_text_fmt(internal_info, "• Allocations <%d KB -> Internal RAM", threshold / 1024); + } else { + lv_label_set_text_fmt(internal_info, "• Allocations <%d bytes -> Internal RAM", threshold); + } + + auto* note_label = lv_label_create(psramContainer); + lv_label_set_text(note_label, "• DMA buffers always use Internal RAM"); + + // Spacer after config + auto* spacer_config = lv_obj_create(psramContainer); + lv_obj_set_size(spacer_config, LV_PCT(100), 16); + lv_obj_set_style_bg_opa(spacer_config, 0, 0); + lv_obj_set_style_border_width(spacer_config, 0, 0); + + // Known PSRAM consumers header + auto* consumers_label = lv_label_create(psramContainer); + lv_label_set_text(consumers_label, "PSRAM Allocation Strategy"); + lv_obj_set_style_text_font(consumers_label, &lv_font_montserrat_14, 0); + lv_obj_set_style_pad_bottom(consumers_label, 8, 0); + + // Explain what's in PSRAM + auto* strategy_note = lv_label_create(psramContainer); + lv_label_set_text(strategy_note, "Apps don't pre-allocate to PSRAM.\nThey use LVGL dynamic allocation:"); + lv_obj_set_style_text_color(strategy_note, lv_palette_main(LV_PALETTE_GREY), 0); + + // List what automatically goes to PSRAM + auto* lvgl_label = lv_label_create(psramContainer); + lv_label_set_text(lvgl_label, "• All LVGL widgets (buttons, labels, etc.)"); + + auto* framebuffer_label = lv_label_create(psramContainer); + lv_label_set_text(framebuffer_label, "• Display framebuffers"); + + auto* wifi_label = lv_label_create(psramContainer); + lv_label_set_text(wifi_label, "• WiFi/Network buffers"); + + auto* file_label = lv_label_create(psramContainer); + lv_label_set_text(file_label, "• File I/O buffers"); + + auto* task_label = lv_label_create(psramContainer); + lv_label_set_text(task_label, "• Task stacks (when enabled)"); + + auto* general_label = lv_label_create(psramContainer); + if (threshold >= 1024) { + lv_label_set_text_fmt(general_label, "• All allocations >=%d KB", threshold / 1024); + } else { + lv_label_set_text_fmt(general_label, "• All allocations >=%d bytes", threshold); + } + + // Spacer + auto* spacer_apps = lv_obj_create(psramContainer); + lv_obj_set_size(spacer_apps, LV_PCT(100), 16); + lv_obj_set_style_bg_opa(spacer_apps, 0, 0); + lv_obj_set_style_border_width(spacer_apps, 0, 0); + + // App behavior explanation + auto* app_behavior_label = lv_label_create(psramContainer); + lv_label_set_text(app_behavior_label, "App Memory Behavior"); + lv_obj_set_style_text_font(app_behavior_label, &lv_font_montserrat_14, 0); + lv_obj_set_style_pad_bottom(app_behavior_label, 8, 0); + + auto* app_note1 = lv_label_create(psramContainer); + lv_label_set_text(app_note1, "• Apps allocate UI when opened (10-50 KB)"); + + auto* app_note2 = lv_label_create(psramContainer); + lv_label_set_text(app_note2, "• All app UI goes to PSRAM automatically"); + + auto* app_note3 = lv_label_create(psramContainer); + lv_label_set_text(app_note3, "• Apps deallocate when closed (no caching)"); + + auto* app_note4 = lv_label_create(psramContainer); + lv_label_set_text(app_note4, "• One app open at a time = 10-50 KB in PSRAM"); +#endif + } void onShow(AppContext& app, lv_obj_t* parent) override { lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lvgl::toolbar_create(parent, app); - // This wrapper automatically has its children added vertically underneath eachother auto* wrapper = lv_obj_create(parent); lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); @@ -230,53 +585,89 @@ class SystemInfoApp final : public App { lv_tabview_set_tab_bar_position(tabview, LV_DIR_LEFT); lv_tabview_set_tab_bar_size(tabview, 80); - // Tabs - + // Create tabs auto* memory_tab = createTab(tabview, "Memory"); + auto* psram_tab = createTab(tabview, "PSRAM"); + auto* cpu_tab = createTab(tabview, "CPU"); auto* storage_tab = createTab(tabview, "Storage"); auto* tasks_tab = createTab(tabview, "Tasks"); auto* devices_tab = createTab(tabview, "Devices"); auto* about_tab = createTab(tabview, "About"); // Memory tab content + internalMemBar = createMemoryBar(memory_tab, "Internal"); + + hasExternalMem = getSpiTotal() > 0; + if (hasExternalMem) { + externalMemBar = createMemoryBar(memory_tab, "External"); + } - addMemoryBar(memory_tab, "Internal", getHeapFree(), getHeapTotal()); - if (getSpiTotal() > 0) { - addMemoryBar(memory_tab, "External", getSpiFree(), getSpiTotal()); + // PSRAM tab content (only if PSRAM exists) + if (hasExternalMem) { + psramContainer = lv_obj_create(psram_tab); + lv_obj_set_size(psramContainer, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(psramContainer, 8, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(psramContainer, 0, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(psramContainer, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_opa(psramContainer, 0, LV_STATE_DEFAULT); } #ifdef ESP_PLATFORM - // Wrapper for the memory usage bars + // Storage tab content uint64_t storage_total = 0; uint64_t storage_free = 0; - - if (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK) { - addMemoryBar(storage_tab, file::MOUNT_POINT_DATA, storage_free, storage_total); + hasDataStorage = (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK); + if (hasDataStorage) { + dataStorageBar = createMemoryBar(storage_tab, file::MOUNT_POINT_DATA); } const auto sdcard_devices = hal::findDevices(hal::Device::Type::SdCard); for (const auto& sdcard : sdcard_devices) { if (sdcard->isMounted() && esp_vfs_fat_info(sdcard->getMountPath().c_str(), &storage_total, &storage_free) == ESP_OK) { - addMemoryBar( - storage_tab, - sdcard->getMountPath().c_str(), - storage_free, - storage_total - ); + hasSdcardStorage = true; + sdcardStorageBar = createMemoryBar(storage_tab, sdcard->getMountPath().c_str()); + break; // Only show first SD card } } if (config::SHOW_SYSTEM_PARTITION) { - if (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK) { - addMemoryBar(storage_tab, file::MOUNT_POINT_SYSTEM, storage_free, storage_total); + hasSystemStorage = (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK); + if (hasSystemStorage) { + systemStorageBar = createMemoryBar(storage_tab, file::MOUNT_POINT_SYSTEM); } } - #endif #if configUSE_TRACE_FACILITY - addRtosTasks(tasks_tab); + // CPU tab - summary at top + cpuSummaryLabel = lv_label_create(cpu_tab); + lv_label_set_text(cpuSummaryLabel, "Overall CPU Usage: --.-%"); + lv_obj_set_style_text_font(cpuSummaryLabel, &lv_font_montserrat_14, 0); + lv_obj_set_style_pad_bottom(cpuSummaryLabel, 4, 0); + + cpuCore0Label = lv_label_create(cpu_tab); + lv_label_set_text(cpuCore0Label, "Core 0: --.-%"); + + cpuCore1Label = lv_label_create(cpu_tab); + lv_label_set_text(cpuCore1Label, "Core 1: --.-%"); + lv_obj_set_style_pad_bottom(cpuCore1Label, 8, 0); + + // CPU tab - container for task list (dynamic updates) + cpuContainer = lv_obj_create(cpu_tab); + lv_obj_set_size(cpuContainer, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(cpuContainer, 8, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(cpuContainer, 0, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(cpuContainer, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_opa(cpuContainer, 0, LV_STATE_DEFAULT); + + // Tasks tab - container for dynamic updates + tasksContainer = lv_obj_create(tasks_tab); + lv_obj_set_size(tasksContainer, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(tasksContainer, 8, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(tasksContainer, 0, LV_STATE_DEFAULT); + lv_obj_set_flex_flow(tasksContainer, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_bg_opa(tasksContainer, 0, LV_STATE_DEFAULT); #endif addDevices(devices_tab); @@ -288,6 +679,21 @@ class SystemInfoApp final : public App { auto* esp_idf_version = lv_label_create(about_tab); lv_label_set_text_fmt(esp_idf_version, "ESP-IDF v%d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH); #endif + + // Initial updates + updateMemory(); + updateStorage(); // Storage: one-time update on show (doesn't change frequently) + updateTasks(); + updatePsram(); // PSRAM: detailed breakdown + + // Start timers (only run while app is visible, stopped in onHide) + memoryTimer.start(kernel::millisToTicks(10000)); // Memory & PSRAM: every 10s + tasksTimer.start(kernel::millisToTicks(15000)); // Tasks/CPU: every 15s + } + + void onHide(TT_UNUSED AppContext& app) override { + memoryTimer.stop(); + tasksTimer.stop(); } }; @@ -295,9 +701,8 @@ extern const AppManifest manifest = { .appId = "SystemInfo", .appName = "System Info", .appIcon = TT_ASSETS_APP_ICON_SYSTEM_INFO, - .appCategory = Category::System, + .appCategory = Category::Settings, // Show in Settings submenu for easy access to memory/tasks/devices .createApp = create }; } // namespace - From cea0b79c14216bc2d02eaff56641688680e11adf Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 24 Dec 2025 15:17:49 +1000 Subject: [PATCH 03/22] Espnow wifi coexist --- .../Source/service/espnow/EspNowWifi.cpp | 30 ++++++++++++------- Tactility/Source/service/loader/Loader.cpp | 7 +++++ .../memorychecker/MemoryCheckerService.cpp | 2 +- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Tactility/Source/service/espnow/EspNowWifi.cpp b/Tactility/Source/service/espnow/EspNowWifi.cpp index 46f6ba128..10a19094d 100644 --- a/Tactility/Source/service/espnow/EspNowWifi.cpp +++ b/Tactility/Source/service/espnow/EspNowWifi.cpp @@ -34,18 +34,29 @@ static bool disableWifiService() { } bool initWifi(const EspNowConfig& config) { + // ESP-NOW can coexist with WiFi STA mode; only preserve WiFi state if already connected + auto wifi_state = wifi::getRadioState(); + bool wifi_was_connected = (wifi_state == wifi::RadioState::ConnectionActive); + + // If WiFi is off or in other states, temporarily disable it to initialize ESP-NOW + // If WiFi is already connected, keep it running and just add ESP-NOW on top + if (!wifi_was_connected && wifi_state != wifi::RadioState::Off && wifi_state != wifi::RadioState::OffPending) { if (!disableWifiService()) { TT_LOG_E(TAG, "Failed to disable wifi"); return false; } + } wifi_mode_t mode; if (config.mode == Mode::Station) { + // Use STA mode to allow coexistence with normal WiFi connection mode = wifi_mode_t::WIFI_MODE_STA; } else { mode = wifi_mode_t::WIFI_MODE_AP; } + // Only reinitialize WiFi if it's not already running + if (wifi::getRadioState() == wifi::RadioState::Off) { wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); if (esp_wifi_init(&cfg) != ESP_OK) { TT_LOG_E(TAG, "esp_wifi_init() failed"); @@ -66,6 +77,7 @@ bool initWifi(const EspNowConfig& config) { TT_LOG_E(TAG, "esp_wifi_start() failed"); return false; } + } if (esp_wifi_set_channel(config.channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) { TT_LOG_E(TAG, "esp_wifi_set_channel() failed"); @@ -85,22 +97,20 @@ bool initWifi(const EspNowConfig& config) { } } + TT_LOG_I(TAG, "WiFi initialized for ESP-NOW (preserved existing connection: %s)", wifi_was_connected ? "yes" : "no"); return true; } bool deinitWifi() { - if (esp_wifi_stop() != ESP_OK) { - TT_LOG_E(TAG, "Failed to stop radio"); - return false; - } + // Don't deinitialize WiFi completely - just disable ESP-NOW + // This allows normal WiFi connection to continue + // Only stop/deinit if WiFi was originally off - if (esp_wifi_set_mode(WIFI_MODE_NULL) != ESP_OK) { - TT_LOG_E(TAG, "Failed to unset mode"); - } + // Since we're only using WiFi for ESP-NOW, we can safely keep it in a minimal state + // or shut it down. For now, keep it running to support STA + ESP-NOW coexistence. - if (esp_wifi_deinit() != ESP_OK) { - TT_LOG_E(TAG, "Failed to deinit"); - } + TT_LOG_I(TAG, "ESP-NOW WiFi deinitialized (WiFi service continues independently)"); + return true; return true; } diff --git a/Tactility/Source/service/loader/Loader.cpp b/Tactility/Source/service/loader/Loader.cpp index bca45bc2e..0fa5d7ea1 100644 --- a/Tactility/Source/service/loader/Loader.cpp +++ b/Tactility/Source/service/loader/Loader.cpp @@ -8,6 +8,7 @@ #include #include +#include // For malloc_trim() to release heap fragmentation after app destruction #ifdef ESP_PLATFORM #include @@ -263,6 +264,12 @@ void LoaderService::transitionAppToState(const std::shared_ptr case Destroyed: app->getApp()->onDestroy(*app); pubsubExternal->publish(Event::ApplicationStopped); + + // Consolidate and release heap fragmentation after app destruction + // This ensures internal memory is returned to the free pool + // malloc_trim(0) releases empty heap pages and compacts the heap + TT_LOG_I(TAG, "Releasing heap fragmentation after app destruction"); + malloc_trim(0); break; } diff --git a/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp b/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp index 75d7897e3..3b3a4ae9e 100644 --- a/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp +++ b/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp @@ -8,7 +8,7 @@ namespace tt::service::memorychecker { constexpr const char* TAG = "MemoryChecker"; -constexpr TickType_t TIMER_UPDATE_INTERVAL = 1000U / portTICK_PERIOD_MS; +constexpr TickType_t TIMER_UPDATE_INTERVAL = 10000U / portTICK_PERIOD_MS; // 10 seconds (reduced from 1s for CPU savings) // Total memory (in bytes) that should be free before warnings occur constexpr auto TOTAL_FREE_THRESHOLD = 10'000; From 2e7625df11cc299f2cdd99631f2e501b30ece939 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 24 Dec 2025 15:25:33 +1000 Subject: [PATCH 04/22] Move systeminfo back --- Tactility/Source/app/systeminfo/SystemInfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index 58a7abbbd..80a5011d8 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -701,7 +701,7 @@ extern const AppManifest manifest = { .appId = "SystemInfo", .appName = "System Info", .appIcon = TT_ASSETS_APP_ICON_SYSTEM_INFO, - .appCategory = Category::Settings, // Show in Settings submenu for easy access to memory/tasks/devices + .appCategory = Category::System, .createApp = create }; From cbf052c9a7b50ffbdce4396d829df3d91fe3109f Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 24 Dec 2025 22:36:03 +1000 Subject: [PATCH 05/22] Updates initI2cDevices - moved to T-deck init.cpp / initBoot KeyboardInitService - removed, moved to T-deck init.cpp / initBoot Adjusted TIMER_UPDATE_INTERVAL to 2 seconds. Added lock to ActionCreateFolder --- Devices/lilygo-tdeck/Source/Configuration.cpp | 56 --------------- Devices/lilygo-tdeck/Source/Init.cpp | 71 +++++++++++++++++++ .../Include/Tactility/hal/Configuration.h | 7 -- Tactility/Source/Tactility.cpp | 2 - Tactility/Source/app/files/View.cpp | 3 + Tactility/Source/hal/Hal.cpp | 12 ---- .../service/keyboardinit/KeyboardInit.cpp | 48 ------------- .../memorychecker/MemoryCheckerService.cpp | 2 +- 8 files changed, 75 insertions(+), 126 deletions(-) delete mode 100644 Tactility/Source/service/keyboardinit/KeyboardInit.cpp diff --git a/Devices/lilygo-tdeck/Source/Configuration.cpp b/Devices/lilygo-tdeck/Source/Configuration.cpp index 48b1f05a6..6c44b1351 100644 --- a/Devices/lilygo-tdeck/Source/Configuration.cpp +++ b/Devices/lilygo-tdeck/Source/Configuration.cpp @@ -6,20 +6,12 @@ #include "devices/TrackballDevice.h" #include -#include #include -#include -#include -#include -#include bool initBoot(); -void initI2cDevices(); using namespace tt::hal; -constexpr auto* TAG = "TDeckConfig"; - static std::vector> createDevices() { return { createPower(), @@ -31,56 +23,8 @@ static std::vector> createDevices() { }; } -void initI2cDevices() { - // Defer I2C device startup to avoid heap corruption during early boot - // Use a one-shot FreeRTOS timer to delay initialization - static TimerHandle_t initTimer = xTimerCreate( - "I2CInit", - pdMS_TO_TICKS(500), // 500ms delay - pdFALSE, // One-shot - nullptr, - [](TimerHandle_t timer) { - TT_LOG_I(TAG, "Starting deferred I2C devices"); - - // Start keyboard backlight device - auto kbBacklight = tt::hal::findDevice("Keyboard Backlight"); - if (kbBacklight) { - TT_LOG_I(TAG, "%s starting", kbBacklight->getName().c_str()); - auto kbDevice = std::static_pointer_cast(kbBacklight); - if (kbDevice->start()) { - TT_LOG_I(TAG, "%s started", kbBacklight->getName().c_str()); - } else { - TT_LOG_E(TAG, "%s start failed", kbBacklight->getName().c_str()); - } - } - - // Small delay between I2C device inits to avoid concurrent transactions - vTaskDelay(pdMS_TO_TICKS(50)); - - // Start trackball device - auto trackball = tt::hal::findDevice("Trackball"); - if (trackball) { - TT_LOG_I(TAG, "%s starting", trackball->getName().c_str()); - auto tbDevice = std::static_pointer_cast(trackball); - if (tbDevice->start()) { - TT_LOG_I(TAG, "%s started", trackball->getName().c_str()); - } else { - TT_LOG_E(TAG, "%s start failed", trackball->getName().c_str()); - } - } - - TT_LOG_I(TAG, "Deferred I2C devices completed"); - } - ); - - if (initTimer != nullptr) { - xTimerStart(initTimer, 0); - } -} - extern const Configuration hardwareConfiguration = { .initBoot = initBoot, - .initI2cDevices = initI2cDevices, .createDevices = createDevices, .i2c = { i2c::Configuration { diff --git a/Devices/lilygo-tdeck/Source/Init.cpp b/Devices/lilygo-tdeck/Source/Init.cpp index 574496640..8b7fda7e2 100644 --- a/Devices/lilygo-tdeck/Source/Init.cpp +++ b/Devices/lilygo-tdeck/Source/Init.cpp @@ -4,6 +4,12 @@ #include #include +#include + +#include "devices/KeyboardBacklight.h" +#include "devices/TrackballDevice.h" +#include +#include #define TAG "tdeck" @@ -30,6 +36,68 @@ static bool powerOn() { return true; } +static void initI2cDevices() { + // Defer I2C device startup to avoid heap corruption during early boot + // Use a one-shot FreeRTOS timer to delay initialization + static TimerHandle_t initTimer = xTimerCreate( + "I2CInit", + pdMS_TO_TICKS(500), // 500ms delay + pdFALSE, // One-shot + nullptr, + [](TimerHandle_t timer) { + TT_LOG_I(TAG, "Starting deferred I2C devices"); + + // Start keyboard backlight device + auto kbBacklight = tt::hal::findDevice("Keyboard Backlight"); + if (kbBacklight) { + TT_LOG_I(TAG, "%s starting", kbBacklight->getName().c_str()); + auto kbDevice = std::static_pointer_cast(kbBacklight); + if (kbDevice->start()) { + TT_LOG_I(TAG, "%s started", kbBacklight->getName().c_str()); + } else { + TT_LOG_E(TAG, "%s start failed", kbBacklight->getName().c_str()); + } + } + + // Small delay between I2C device inits to avoid concurrent transactions + vTaskDelay(pdMS_TO_TICKS(50)); + + // Start trackball device + auto trackball = tt::hal::findDevice("Trackball"); + if (trackball) { + TT_LOG_I(TAG, "%s starting", trackball->getName().c_str()); + auto tbDevice = std::static_pointer_cast(trackball); + if (tbDevice->start()) { + TT_LOG_I(TAG, "%s started", trackball->getName().c_str()); + } else { + TT_LOG_E(TAG, "%s start failed", trackball->getName().c_str()); + } + } + + TT_LOG_I(TAG, "Deferred I2C devices completed"); + } + ); + + if (initTimer != nullptr) { + xTimerStart(initTimer, 0); + } +} + +static bool keyboardInit() { + auto kbSettings = tt::settings::keyboard::loadOrGetDefault(); + + auto kbBacklight = tt::hal::findDevice("Keyboard Backlight"); + bool result = driver::keyboardbacklight::setBrightness(kbSettings.backlightEnabled ? kbSettings.backlightBrightness : 0); + + if (!result) { + TT_LOG_W(TAG, "Failed to set keyboard backlight brightness"); + } + + driver::trackball::setEnabled(kbSettings.trackballEnabled); + + return true; +} + bool initBoot() { ESP_LOGI(TAG, LOG_MESSAGE_POWER_ON_START); if (!powerOn()) { @@ -59,5 +127,8 @@ bool initBoot() { } } }); + + initI2cDevices(); + keyboardInit(); return true; } diff --git a/Tactility/Include/Tactility/hal/Configuration.h b/Tactility/Include/Tactility/hal/Configuration.h index 4faa787b0..ee2353a9f 100644 --- a/Tactility/Include/Tactility/hal/Configuration.h +++ b/Tactility/Include/Tactility/hal/Configuration.h @@ -8,7 +8,6 @@ namespace tt::hal { typedef bool (*InitBoot)(); -typedef void (*InitI2cDevices) (); typedef std::vector> DeviceVector; @@ -34,12 +33,6 @@ struct Configuration { */ const InitBoot _Nullable initBoot = nullptr; - /** - * Called after I2C bus is initialized and devices are registered. - * Used for starting I2C peripheral devices (e.g., keyboard backlight, trackball). - */ - const InitI2cDevices _Nullable initI2cDevices = nullptr; - /** Init behaviour: default (esp_lvgl_port for ESP32, nothing for PC) or None (nothing on any platform). Only used in Tactility, not in TactilityHeadless. */ const LvglInit lvglInit = LvglInit::Default; diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index d3fa8278a..fba21be44 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -52,7 +52,6 @@ namespace service { namespace memorychecker { extern const ServiceManifest manifest; } namespace statusbar { extern const ServiceManifest manifest; } namespace displayidle { extern const ServiceManifest manifest; } - namespace keyboardinit { extern const ServiceManifest manifest; } #if TT_FEATURE_SCREENSHOT_ENABLED namespace screenshot { extern const ServiceManifest manifest; } #endif @@ -232,7 +231,6 @@ static void registerAndStartSecondaryServices() { addService(service::gui::manifest); addService(service::statusbar::manifest); addService(service::displayidle::manifest); - addService(service::keyboardinit::manifest); addService(service::memorychecker::manifest); #if TT_FEATURE_SCREENSHOT_ENABLED addService(service::screenshot::manifest); diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index a0eef8816..073d0bbe2 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -436,11 +436,14 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr bu auto foldername = inputdialog::getResult(*bundle); if (!foldername.empty()) { std::string new_folder_path = file::getChildPath(state->getCurrentPath(), foldername); + auto lock = file::getLock(new_folder_path); + lock->lock(); if (mkdir(new_folder_path.c_str(), 0755) == 0) { TT_LOG_I(TAG, "Created folder \"%s\"", new_folder_path.c_str()); } else { TT_LOG_E(TAG, "Failed to create folder \"%s\"", new_folder_path.c_str()); } + lock->unlock(); state->setEntriesForPath(state->getCurrentPath()); update(); diff --git a/Tactility/Source/hal/Hal.cpp b/Tactility/Source/hal/Hal.cpp index a5822cb93..4cb315f9c 100644 --- a/Tactility/Source/hal/Hal.cpp +++ b/Tactility/Source/hal/Hal.cpp @@ -63,16 +63,6 @@ static void startDisplays() { } } -static void startI2cDevices() { - // Start I2C peripheral devices by calling a callback if provided - // This allows device-specific initialization without coupling to device implementations - const auto* config = getConfiguration(); - if (config && config->initI2cDevices) { - TT_LOG_I(TAG, "Starting I2C devices"); - config->initI2cDevices(); - } -} - void init(const Configuration& configuration) { kernel::publishSystemEvent(kernel::SystemEvent::BootInitHalBegin); @@ -99,8 +89,6 @@ void init(const Configuration& configuration) { startDisplays(); // Warning: SPI displays need to start after SPI SD cards are mounted - startI2cDevices(); // Start I2C peripheral devices after displays (they might depend on display being ready) - kernel::publishSystemEvent(kernel::SystemEvent::BootInitHalEnd); } diff --git a/Tactility/Source/service/keyboardinit/KeyboardInit.cpp b/Tactility/Source/service/keyboardinit/KeyboardInit.cpp deleted file mode 100644 index 423222a99..000000000 --- a/Tactility/Source/service/keyboardinit/KeyboardInit.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include -#include -#include -#include -#include -#include - -// Forward declare driver functions -namespace driver::keyboardbacklight { - bool setBrightness(uint8_t brightness); -} - -namespace driver::trackball { - void setEnabled(bool enabled); -} - -namespace tt::service::keyboardinit { - -constexpr auto* TAG = "KeyboardInit"; - -class KeyboardInitService final : public Service { -public: - bool onStart(TT_UNUSED ServiceContext& service) override { - auto settings = settings::keyboard::loadOrGetDefault(); - - // Apply keyboard backlight setting - bool result = driver::keyboardbacklight::setBrightness(settings.backlightEnabled ? settings.backlightBrightness : 0); - if (!result) { - TT_LOG_W(TAG, "Failed to set keyboard backlight brightness"); - } - - // Apply trackball enabled setting - driver::trackball::setEnabled(settings.trackballEnabled); - - return true; - } - - void onStop(TT_UNUSED ServiceContext& service) override { - // Nothing to clean up - } -}; - -extern const ServiceManifest manifest = { - .id = "KeyboardInit", - .createService = create -}; - -} diff --git a/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp b/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp index 3b3a4ae9e..462f9bcd6 100644 --- a/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp +++ b/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp @@ -8,7 +8,7 @@ namespace tt::service::memorychecker { constexpr const char* TAG = "MemoryChecker"; -constexpr TickType_t TIMER_UPDATE_INTERVAL = 10000U / portTICK_PERIOD_MS; // 10 seconds (reduced from 1s for CPU savings) +constexpr TickType_t TIMER_UPDATE_INTERVAL = 2000U / portTICK_PERIOD_MS; // Total memory (in bytes) that should be free before warnings occur constexpr auto TOTAL_FREE_THRESHOLD = 10'000; From 6f6ae5df3dc09f655f1f27079e2d629afdec8479 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 25 Dec 2025 17:42:24 +1000 Subject: [PATCH 06/22] Minor cleanup Keyboard settings more in line with display settings. --- Data/data/settings/system.properties | 2 +- Devices/lilygo-tdeck/Source/Init.cpp | 5 ++ .../Source/devices/TrackballDevice.cpp | 1 + .../Source/app/keyboard/KeyboardSettings.cpp | 76 +++++++++++-------- .../service/displayidle/DisplayIdle.cpp | 9 --- .../Source/service/espnow/EspNowWifi.cpp | 2 - 6 files changed, 50 insertions(+), 45 deletions(-) diff --git a/Data/data/settings/system.properties b/Data/data/settings/system.properties index 1e299ddf4..71b8b7193 100644 --- a/Data/data/settings/system.properties +++ b/Data/data/settings/system.properties @@ -2,4 +2,4 @@ language=en-US timeFormat24h=true dateFormat=MM/DD/YYYY region=US -timezone=America/New_York \ No newline at end of file +timezone=America/Los_Angeles \ No newline at end of file diff --git a/Devices/lilygo-tdeck/Source/Init.cpp b/Devices/lilygo-tdeck/Source/Init.cpp index 8b7fda7e2..eb3d26dc8 100644 --- a/Devices/lilygo-tdeck/Source/Init.cpp +++ b/Devices/lilygo-tdeck/Source/Init.cpp @@ -75,11 +75,16 @@ static void initI2cDevices() { } TT_LOG_I(TAG, "Deferred I2C devices completed"); + + // Clean up the one-shot timer + xTimerDelete(timer, 0); } ); if (initTimer != nullptr) { xTimerStart(initTimer, 0); + } else { + TT_LOG_E(TAG, "Failed to create I2C init timer"); } } diff --git a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp index abd428e97..43af582b6 100644 --- a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp +++ b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp @@ -28,6 +28,7 @@ bool TrackballDevice::start() { bool TrackballDevice::stop() { if (initialized) { // LVGL will handle indev cleanup + driver::trackball::deinit(); indev = nullptr; initialized = false; } diff --git a/Tactility/Source/app/keyboard/KeyboardSettings.cpp b/Tactility/Source/app/keyboard/KeyboardSettings.cpp index c298b95e0..7744df0c7 100644 --- a/Tactility/Source/app/keyboard/KeyboardSettings.cpp +++ b/Tactility/Source/app/keyboard/KeyboardSettings.cpp @@ -31,8 +31,7 @@ class KeyboardSettingsApp final : public App { lv_obj_t* sliderBrightness = nullptr; lv_obj_t* switchTrackball = nullptr; lv_obj_t* switchTimeoutEnable = nullptr; - lv_obj_t* sliderTimeoutSeconds = nullptr; - lv_obj_t* labelTimeoutValue = nullptr; + lv_obj_t* timeoutDropdown = nullptr; static void onBacklightSwitch(lv_event_t* e) { auto* app = static_cast(lv_event_get_user_data(e)); @@ -69,20 +68,24 @@ class KeyboardSettingsApp final : public App { bool enabled = lv_obj_has_state(app->switchTimeoutEnable, LV_STATE_CHECKED); app->kbSettings.backlightTimeoutEnabled = enabled; app->updated = true; - if (app->sliderTimeoutSeconds) { - if (enabled) lv_obj_clear_state(app->sliderTimeoutSeconds, LV_STATE_DISABLED); - else lv_obj_add_state(app->sliderTimeoutSeconds, LV_STATE_DISABLED); + if (app->timeoutDropdown) { + if (enabled) { + lv_obj_clear_state(app->timeoutDropdown, LV_STATE_DISABLED); + } else { + lv_obj_add_state(app->timeoutDropdown, LV_STATE_DISABLED); + } } } - static void onTimeoutSliderChanged(lv_event_t* e) { - auto* app = static_cast(lv_event_get_user_data(e)); - if (!app->sliderTimeoutSeconds) return; - int32_t seconds = lv_slider_get_value(app->sliderTimeoutSeconds); - app->kbSettings.backlightTimeoutMs = static_cast(seconds) * 1000; + static void onTimeoutChanged(lv_event_t* event) { + auto* app = static_cast(lv_event_get_user_data(event)); + auto* dropdown = static_cast(lv_event_get_target(event)); + uint32_t idx = lv_dropdown_get_selected(dropdown); + // Map dropdown index to ms: 0=15s,1=30s,2=1m,3=2m,4=5m,5=Never + static const uint32_t values_ms[] = {15000, 30000, 60000, 120000, 300000, 0}; + if (idx < (sizeof(values_ms)/sizeof(values_ms[0]))) { + app->kbSettings.backlightTimeoutMs = values_ms[idx]; app->updated = true; - if (app->labelTimeoutValue) { - lv_label_set_text_fmt(app->labelTimeoutValue, "%ld s", seconds); } } @@ -105,6 +108,7 @@ class KeyboardSettingsApp final : public App { lv_obj_set_size(bl_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(bl_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(bl_wrapper, 0, LV_STATE_DEFAULT); + auto* bl_label = lv_label_create(bl_wrapper); lv_label_set_text(bl_label, "Keyboard backlight"); lv_obj_align(bl_label, LV_ALIGN_LEFT_MID, 0, 0); @@ -118,6 +122,7 @@ class KeyboardSettingsApp final : public App { lv_obj_set_size(br_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(br_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(br_wrapper, 0, LV_STATE_DEFAULT); + auto* br_label = lv_label_create(br_wrapper); lv_label_set_text(br_label, "Brightness"); lv_obj_align(br_label, LV_ALIGN_LEFT_MID, 0, 0); @@ -134,6 +139,7 @@ class KeyboardSettingsApp final : public App { lv_obj_set_size(tb_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(tb_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(tb_wrapper, 0, LV_STATE_DEFAULT); + auto* tb_label = lv_label_create(tb_wrapper); lv_label_set_text(tb_label, "Trackball"); lv_obj_align(tb_label, LV_ALIGN_LEFT_MID, 0, 0); @@ -147,35 +153,39 @@ class KeyboardSettingsApp final : public App { lv_obj_set_size(to_enable_wrapper, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_style_pad_all(to_enable_wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(to_enable_wrapper, 0, LV_STATE_DEFAULT); + auto* to_enable_label = lv_label_create(to_enable_wrapper); - lv_label_set_text(to_enable_label, "Backlight timeout"); + lv_label_set_text(to_enable_label, "Auto backlight off"); lv_obj_align(to_enable_label, LV_ALIGN_LEFT_MID, 0, 0); switchTimeoutEnable = lv_switch_create(to_enable_wrapper); if (kbSettings.backlightTimeoutEnabled) lv_obj_add_state(switchTimeoutEnable, LV_STATE_CHECKED); lv_obj_align(switchTimeoutEnable, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(switchTimeoutEnable, onTimeoutEnableSwitch, LV_EVENT_VALUE_CHANGED, this); + auto* timeout_select_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(timeout_select_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(timeout_select_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(timeout_select_wrapper, 0, LV_STATE_DEFAULT); + + auto* timeout_value_label = lv_label_create(timeout_select_wrapper); + lv_label_set_text(timeout_value_label, "Timeout"); + lv_obj_align(timeout_value_label, LV_ALIGN_LEFT_MID, 0, 0); + // Backlight timeout value (seconds) - auto* to_value_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(to_value_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(to_value_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(to_value_wrapper, 0, LV_STATE_DEFAULT); - auto* to_value_label = lv_label_create(to_value_wrapper); - lv_label_set_text(to_value_label, "Timeout (s)"); - lv_obj_align(to_value_label, LV_ALIGN_LEFT_MID, 0, 0); - labelTimeoutValue = lv_label_create(to_value_wrapper); - uint32_t timeoutSeconds = kbSettings.backlightTimeoutMs / 1000; - lv_label_set_text_fmt(labelTimeoutValue, "%lu s", (unsigned long)timeoutSeconds); - lv_obj_align(labelTimeoutValue, LV_ALIGN_RIGHT_MID, -60, 0); // leave room for slider - sliderTimeoutSeconds = lv_slider_create(to_value_wrapper); - lv_obj_set_width(sliderTimeoutSeconds, 120); - lv_obj_align(sliderTimeoutSeconds, LV_ALIGN_RIGHT_MID, 0, 0); - lv_slider_set_range(sliderTimeoutSeconds, 5, 600); // 5s to 10 minutes - if (timeoutSeconds < 5) timeoutSeconds = 5; - if (timeoutSeconds > 600) timeoutSeconds = 600; - lv_slider_set_value(sliderTimeoutSeconds, (int32_t)timeoutSeconds, LV_ANIM_OFF); - if (!kbSettings.backlightTimeoutEnabled) lv_obj_add_state(sliderTimeoutSeconds, LV_STATE_DISABLED); - lv_obj_add_event_cb(sliderTimeoutSeconds, onTimeoutSliderChanged, LV_EVENT_VALUE_CHANGED, this); + timeoutDropdown = lv_dropdown_create(timeout_select_wrapper); + lv_dropdown_set_options(timeoutDropdown, "15 seconds\n30 seconds\n1 minute\n2 minutes\n5 minutes\nNever"); + lv_obj_align(timeoutDropdown, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_set_style_border_color(timeoutDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN); + lv_obj_set_style_border_width(timeoutDropdown, 1, LV_PART_MAIN); + lv_obj_add_event_cb(timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, this); + // Initialize dropdown selection from settings + uint32_t ms = kbSettings.backlightTimeoutMs; + uint32_t idx = 2; // default 1 minute + if (ms == 15000) idx = 0; else if (ms == 30000) idx = 1; else if (ms == 60000) idx = 2; else if (ms == 120000) idx = 3; else if (ms == 300000) idx = 4; else if (ms == 0) idx = 5; + lv_dropdown_set_selected(timeoutDropdown, idx); + if (!kbSettings.backlightTimeoutEnabled) { + lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED); + } } void onHide(TT_UNUSED AppContext& app) override { diff --git a/Tactility/Source/service/displayidle/DisplayIdle.cpp b/Tactility/Source/service/displayidle/DisplayIdle.cpp index 9c554049b..2ce2d491a 100644 --- a/Tactility/Source/service/displayidle/DisplayIdle.cpp +++ b/Tactility/Source/service/displayidle/DisplayIdle.cpp @@ -8,10 +8,6 @@ #include #include -#ifdef ESP_PLATFORM -#include -#endif - // Forward declare driver functions namespace driver::keyboardbacklight { bool setBrightness(uint8_t brightness); @@ -37,11 +33,6 @@ class DisplayIdleService final : public Service { return hal::findFirstDevice(hal::Device::Type::Keyboard); } - void reloadSettings() { - cachedDisplaySettings = settings::display::loadOrGetDefault(); - cachedKeyboardSettings = settings::keyboard::loadOrGetDefault(); - } - void tick() { // Settings are now cached and event-driven (no file I/O in timer callback!) // This prevents watchdog timeout from blocking the Timer Service task diff --git a/Tactility/Source/service/espnow/EspNowWifi.cpp b/Tactility/Source/service/espnow/EspNowWifi.cpp index 10a19094d..ea9414262 100644 --- a/Tactility/Source/service/espnow/EspNowWifi.cpp +++ b/Tactility/Source/service/espnow/EspNowWifi.cpp @@ -111,8 +111,6 @@ bool deinitWifi() { TT_LOG_I(TAG, "ESP-NOW WiFi deinitialized (WiFi service continues independently)"); return true; - - return true; } } // namespace tt::service::espnow From c8050413c9e1ae2dd695753852f66f9d679379cd Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 25 Dec 2025 22:55:44 +1000 Subject: [PATCH 07/22] some fixes --- .../Source/devices/KeyboardBacklight.cpp | 1 + .../Source/devices/TrackballDevice.h | 2 +- .../Source/KeyboardBacklight.cpp | 1 + Tactility/Include/Tactility/hal/Device.h | 3 +- .../Source/app/systeminfo/SystemInfo.cpp | 34 +++++++++---------- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp index d14d4083f..00149e315 100644 --- a/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp +++ b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp @@ -2,6 +2,7 @@ #include // Driver #include +// TODO: Add Mutex and consider refactoring into a class bool KeyboardBacklightDevice::start() { if (initialized) { return true; diff --git a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.h b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.h index d72df497c..76e9d88a5 100644 --- a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.h +++ b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.h @@ -5,7 +5,7 @@ class TrackballDevice : public tt::hal::Device { public: - tt::hal::Device::Type getType() const override { return tt::hal::Device::Type::I2c; } + tt::hal::Device::Type getType() const override { return tt::hal::Device::Type::Other; } std::string getName() const override { return "Trackball"; } std::string getDescription() const override { return "5-way GPIO trackball navigation"; } diff --git a/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp b/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp index ee845cba6..0f5c84f78 100644 --- a/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp +++ b/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp @@ -13,6 +13,7 @@ static i2c_port_t g_i2cPort = I2C_NUM_MAX; static uint8_t g_slaveAddress = 0x55; static uint8_t g_currentBrightness = 127; +// TODO: Umm...something. Calls xxxBrightness, ignores return values. bool init(i2c_port_t i2cPort, uint8_t slaveAddress) { g_i2cPort = i2cPort; g_slaveAddress = slaveAddress; diff --git a/Tactility/Include/Tactility/hal/Device.h b/Tactility/Include/Tactility/hal/Device.h index e323c9652..4504d062e 100644 --- a/Tactility/Include/Tactility/hal/Device.h +++ b/Tactility/Include/Tactility/hal/Device.h @@ -21,7 +21,8 @@ class Device { Keyboard, Encoder, Power, - Gps + Gps, + Other }; typedef uint32_t Id; diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index 80a5011d8..95c7b7222 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -214,8 +214,8 @@ static void addRtosTask(lv_obj_t* parent, const TaskStatus_t& task, uint32_t tot float cpu_percent = (task.ulRunTimeCounter * 100.0f) / totalRuntime; lv_label_set_text_fmt(label, "%s: %.1f%%", name, cpu_percent); } else { - lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task)); -} + lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task)); + } } static void updateRtosTasks(lv_obj_t* parent, bool showCpuPercent) { @@ -308,8 +308,8 @@ class SystemInfoApp final : public App { lv_obj_t* cpuContainer = nullptr; lv_obj_t* psramContainer = nullptr; lv_obj_t* cpuSummaryLabel = nullptr; // Shows overall CPU utilization - lv_obj_t* cpuCore0Label = nullptr; // Shows Core 0 tasks - lv_obj_t* cpuCore1Label = nullptr; // Shows Core 1 tasks + lv_obj_t* taskCountLabel = nullptr; // Shows active task count + lv_obj_t* uptimeLabel = nullptr; // Shows system uptime bool hasExternalMem = false; bool hasDataStorage = false; @@ -367,7 +367,7 @@ class SystemInfoApp final : public App { // Update CPU summary at top of tab // Note: FreeRTOS runtime stats accumulate since boot, so percentages // are averages over entire uptime, not instantaneous usage - if (cpuSummaryLabel && cpuCore0Label && cpuCore1Label) { + if (cpuSummaryLabel && taskCountLabel && uptimeLabel) { UBaseType_t count = uxTaskGetNumberOfTasks(); auto* tasks = (TaskStatus_t*)heap_caps_malloc(sizeof(TaskStatus_t) * count, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); if (!tasks) tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); @@ -391,17 +391,17 @@ class SystemInfoApp final : public App { // Show total task count auto core_text = std::format("Active Tasks: {} total", actual); - lv_label_set_text(cpuCore0Label, core_text.c_str()); + lv_label_set_text(taskCountLabel, core_text.c_str()); - // Show uptime estimate (runtime is in ticks, typically 1 tick = 10us) - // totalRuntime accumulates across all cores/tasks - float uptime_sec = totalRuntime / (configTICK_RATE_HZ * 100000.0f); + // Use actual system tick count for uptime + TickType_t ticks = xTaskGetTickCount(); + float uptime_sec = static_cast(ticks) / configTICK_RATE_HZ; auto uptime_text = std::format("System Uptime: {:.1f} min", uptime_sec / 60.0f); - lv_label_set_text(cpuCore1Label, uptime_text.c_str()); + lv_label_set_text(uptimeLabel, uptime_text.c_str()); } else { lv_label_set_text(cpuSummaryLabel, "Overall CPU Usage: --.-%"); - lv_label_set_text(cpuCore0Label, "Active Tasks: --"); - lv_label_set_text(cpuCore1Label, "System Uptime: --"); + lv_label_set_text(taskCountLabel, "Active Tasks: --"); + lv_label_set_text(uptimeLabel, "System Uptime: --"); } free(tasks); @@ -646,12 +646,12 @@ class SystemInfoApp final : public App { lv_obj_set_style_text_font(cpuSummaryLabel, &lv_font_montserrat_14, 0); lv_obj_set_style_pad_bottom(cpuSummaryLabel, 4, 0); - cpuCore0Label = lv_label_create(cpu_tab); - lv_label_set_text(cpuCore0Label, "Core 0: --.-%"); + taskCountLabel = lv_label_create(cpu_tab); + lv_label_set_text(taskCountLabel, "Active Tasks: --.-%"); - cpuCore1Label = lv_label_create(cpu_tab); - lv_label_set_text(cpuCore1Label, "Core 1: --.-%"); - lv_obj_set_style_pad_bottom(cpuCore1Label, 8, 0); + uptimeLabel = lv_label_create(cpu_tab); + lv_label_set_text(uptimeLabel, "System Uptime: --.-%"); + lv_obj_set_style_pad_bottom(uptimeLabel, 8, 0); // CPU tab - container for task list (dynamic updates) cpuContainer = lv_obj_create(cpu_tab); From 65212921eddde78a212a3e9699a78c7d4ba3a3c0 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 26 Dec 2025 02:50:17 +1000 Subject: [PATCH 08/22] More fixes... --- Devices/lilygo-tdeck/Source/Init.cpp | 104 ++++++------------ .../Source/KeyboardBacklight.cpp | 16 ++- Drivers/Trackball/Source/Trackball.cpp | 8 +- Tactility/Source/app/files/View.cpp | 6 + .../Source/app/systeminfo/SystemInfo.cpp | 3 + Tactility/Source/service/loader/Loader.cpp | 7 -- .../Source/settings/KeyboardSettings.cpp | 2 +- 7 files changed, 63 insertions(+), 83 deletions(-) diff --git a/Devices/lilygo-tdeck/Source/Init.cpp b/Devices/lilygo-tdeck/Source/Init.cpp index eb3d26dc8..2e029fec0 100644 --- a/Devices/lilygo-tdeck/Source/Init.cpp +++ b/Devices/lilygo-tdeck/Source/Init.cpp @@ -36,73 +36,6 @@ static bool powerOn() { return true; } -static void initI2cDevices() { - // Defer I2C device startup to avoid heap corruption during early boot - // Use a one-shot FreeRTOS timer to delay initialization - static TimerHandle_t initTimer = xTimerCreate( - "I2CInit", - pdMS_TO_TICKS(500), // 500ms delay - pdFALSE, // One-shot - nullptr, - [](TimerHandle_t timer) { - TT_LOG_I(TAG, "Starting deferred I2C devices"); - - // Start keyboard backlight device - auto kbBacklight = tt::hal::findDevice("Keyboard Backlight"); - if (kbBacklight) { - TT_LOG_I(TAG, "%s starting", kbBacklight->getName().c_str()); - auto kbDevice = std::static_pointer_cast(kbBacklight); - if (kbDevice->start()) { - TT_LOG_I(TAG, "%s started", kbBacklight->getName().c_str()); - } else { - TT_LOG_E(TAG, "%s start failed", kbBacklight->getName().c_str()); - } - } - - // Small delay between I2C device inits to avoid concurrent transactions - vTaskDelay(pdMS_TO_TICKS(50)); - - // Start trackball device - auto trackball = tt::hal::findDevice("Trackball"); - if (trackball) { - TT_LOG_I(TAG, "%s starting", trackball->getName().c_str()); - auto tbDevice = std::static_pointer_cast(trackball); - if (tbDevice->start()) { - TT_LOG_I(TAG, "%s started", trackball->getName().c_str()); - } else { - TT_LOG_E(TAG, "%s start failed", trackball->getName().c_str()); - } - } - - TT_LOG_I(TAG, "Deferred I2C devices completed"); - - // Clean up the one-shot timer - xTimerDelete(timer, 0); - } - ); - - if (initTimer != nullptr) { - xTimerStart(initTimer, 0); - } else { - TT_LOG_E(TAG, "Failed to create I2C init timer"); - } -} - -static bool keyboardInit() { - auto kbSettings = tt::settings::keyboard::loadOrGetDefault(); - - auto kbBacklight = tt::hal::findDevice("Keyboard Backlight"); - bool result = driver::keyboardbacklight::setBrightness(kbSettings.backlightEnabled ? kbSettings.backlightBrightness : 0); - - if (!result) { - TT_LOG_W(TAG, "Failed to set keyboard backlight brightness"); - } - - driver::trackball::setEnabled(kbSettings.trackballEnabled); - - return true; -} - bool initBoot() { ESP_LOGI(TAG, LOG_MESSAGE_POWER_ON_START); if (!powerOn()) { @@ -132,8 +65,41 @@ bool initBoot() { } } }); + + tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) { + auto kbBacklight = tt::hal::findDevice("Keyboard Backlight"); + if (kbBacklight != nullptr) { + TT_LOG_I(TAG, "%s starting", kbBacklight->getName().c_str()); + auto kbDevice = std::static_pointer_cast(kbBacklight); + if (kbDevice->start()) { + TT_LOG_I(TAG, "%s started", kbBacklight->getName().c_str()); + } else { + TT_LOG_E(TAG, "%s start failed", kbBacklight->getName().c_str()); + } + } + + auto trackball = tt::hal::findDevice("Trackball"); + if (trackball != nullptr) { + TT_LOG_I(TAG, "%s starting", trackball->getName().c_str()); + auto tbDevice = std::static_pointer_cast(trackball); + if (tbDevice->start()) { + TT_LOG_I(TAG, "%s started", trackball->getName().c_str()); + } else { + TT_LOG_E(TAG, "%s start failed", trackball->getName().c_str()); + } + } + + //Backlight doesn't seem to turn on until toggled on and off from keyboard settings... + // Or let the display and backlight sleep then wake it up. + //Then it works fine...until reboot, then you need to toggle again. + auto kbSettings = tt::settings::keyboard::loadOrGetDefault(); + bool result = driver::keyboardbacklight::setBrightness(kbSettings.backlightEnabled ? kbSettings.backlightBrightness : 0); + if (!result) { + TT_LOG_W(TAG, "Failed to set keyboard backlight brightness"); + } + + driver::trackball::setEnabled(kbSettings.trackballEnabled); + }); - initI2cDevices(); - keyboardInit(); return true; } diff --git a/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp b/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp index 0f5c84f78..62d32ccef 100644 --- a/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp +++ b/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp @@ -21,8 +21,15 @@ bool init(i2c_port_t i2cPort, uint8_t slaveAddress) { ESP_LOGI(TAG, "Keyboard backlight initialized on I2C port %d, address 0x%02X", g_i2cPort, g_slaveAddress); // Set a reasonable default brightness - setDefaultBrightness(127); - setBrightness(127); + if (!setDefaultBrightness(127)) { + ESP_LOGE(TAG, "Failed to set default brightness"); + return false; + } + + if (!setBrightness(127)) { + ESP_LOGE(TAG, "Failed to set brightness"); + return false; + } return true; } @@ -91,6 +98,11 @@ bool setDefaultBrightness(uint8_t brightness) { } uint8_t getBrightness() { + if (g_i2cPort >= I2C_NUM_MAX) { + ESP_LOGE(TAG, "Keyboard backlight not initialized"); + return 0; + } + return g_currentBrightness; } diff --git a/Drivers/Trackball/Source/Trackball.cpp b/Drivers/Trackball/Source/Trackball.cpp index 5a64a806f..be088575f 100644 --- a/Drivers/Trackball/Source/Trackball.cpp +++ b/Drivers/Trackball/Source/Trackball.cpp @@ -42,19 +42,19 @@ static void read_cb(lv_indev_t* indev, lv_indev_data_t* data) { // Right pressed (rising edge) if (currentStates[0] && !g_lastState[0]) { - diff += 1; + diff += g_config.movementStep; } // Up pressed (rising edge) if (currentStates[1] && !g_lastState[1]) { - diff -= 1; + diff -= g_config.movementStep; } // Left pressed (rising edge) if (currentStates[2] && !g_lastState[2]) { - diff -= 1; + diff -= g_config.movementStep; } // Down pressed (rising edge) if (currentStates[3] && !g_lastState[3]) { - diff += 1; + diff += g_config.movementStep; } // Update last states diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index 073d0bbe2..ee7523bc8 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -415,6 +415,12 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr bu auto filename = inputdialog::getResult(*bundle); if (!filename.empty()) { std::string new_file_path = file::getChildPath(state->getCurrentPath(), filename); + struct stat st; + if (stat(new_file_path.c_str(), &st) == 0) { + TT_LOG_W(TAG, "File already exists: \"%s\"", new_file_path.c_str()); + break; + } + auto lock = file::getLock(new_file_path); lock->lock(); diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index 95c7b7222..25f0fc103 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -322,7 +323,9 @@ class SystemInfoApp final : public App { updateMemoryBar(internalMemBar, getHeapFree(), getHeapTotal()); if (hasExternalMem) { + lvgl::getSyncLock()->lock(); updateMemoryBar(externalMemBar, getSpiFree(), getSpiTotal()); + lvgl::getSyncLock()->unlock(); } } diff --git a/Tactility/Source/service/loader/Loader.cpp b/Tactility/Source/service/loader/Loader.cpp index 0fa5d7ea1..bca45bc2e 100644 --- a/Tactility/Source/service/loader/Loader.cpp +++ b/Tactility/Source/service/loader/Loader.cpp @@ -8,7 +8,6 @@ #include #include -#include // For malloc_trim() to release heap fragmentation after app destruction #ifdef ESP_PLATFORM #include @@ -264,12 +263,6 @@ void LoaderService::transitionAppToState(const std::shared_ptr case Destroyed: app->getApp()->onDestroy(*app); pubsubExternal->publish(Event::ApplicationStopped); - - // Consolidate and release heap fragmentation after app destruction - // This ensures internal memory is returned to the free pool - // malloc_trim(0) releases empty heap pages and compacts the heap - TT_LOG_I(TAG, "Releasing heap fragmentation after app destruction"); - malloc_trim(0); break; } diff --git a/Tactility/Source/settings/KeyboardSettings.cpp b/Tactility/Source/settings/KeyboardSettings.cpp index e2462bbe2..2f7a26972 100644 --- a/Tactility/Source/settings/KeyboardSettings.cpp +++ b/Tactility/Source/settings/KeyboardSettings.cpp @@ -40,7 +40,7 @@ KeyboardSettings getDefault() { .backlightBrightness = 127, .trackballEnabled = true, .backlightTimeoutEnabled = true, - .backlightTimeoutMs = 30000 // 30 seconds default + .backlightTimeoutMs = 60000 // 60 seconds default }; } From 47b2a4cc64cd30684aead2ec8751bf6d26fe5e59 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 26 Dec 2025 03:15:51 +1000 Subject: [PATCH 09/22] Update View.cpp --- Tactility/Source/app/files/View.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index ee7523bc8..76cf0578b 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -415,15 +415,16 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr bu auto filename = inputdialog::getResult(*bundle); if (!filename.empty()) { std::string new_file_path = file::getChildPath(state->getCurrentPath(), filename); + + auto lock = file::getLock(new_file_path); + lock->lock(); + struct stat st; if (stat(new_file_path.c_str(), &st) == 0) { TT_LOG_W(TAG, "File already exists: \"%s\"", new_file_path.c_str()); break; } - auto lock = file::getLock(new_file_path); - lock->lock(); - FILE* new_file = fopen(new_file_path.c_str(), "w"); if (new_file) { fclose(new_file); @@ -442,8 +443,16 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr bu auto foldername = inputdialog::getResult(*bundle); if (!foldername.empty()) { std::string new_folder_path = file::getChildPath(state->getCurrentPath(), foldername); + auto lock = file::getLock(new_folder_path); lock->lock(); + + struct stat st; + if (stat(new_folder_path.c_str(), &st) == 0) { + TT_LOG_W(TAG, "Folder already exists: \"%s\"", new_folder_path.c_str()); + break; + } + if (mkdir(new_folder_path.c_str(), 0755) == 0) { TT_LOG_I(TAG, "Created folder \"%s\"", new_folder_path.c_str()); } else { From 82218608f4dd228d969f2c84f0ae623215001ea2 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 26 Dec 2025 13:49:11 +1000 Subject: [PATCH 10/22] are we there yet... correct locks? i don't know at this point. I'm trying! --- Tactility/Source/app/files/View.cpp | 2 +- Tactility/Source/app/systeminfo/SystemInfo.cpp | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index 76cf0578b..0958ec146 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -195,7 +195,7 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) { // Get file size for regular files std::string label_text = dir_entry.d_name; if (dir_entry.d_type == file::TT_DT_REG) { - std::string file_path = state->getCurrentPath() + "/" + dir_entry.d_name; + std::string file_path = file::getChildPath(state->getCurrentPath(), dir_entry.d_name); struct stat st; if (stat(file_path.c_str(), &st) == 0) { // Format file size in human-readable format diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index 25f0fc103..98c73d993 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -289,14 +289,20 @@ class SystemInfoApp final : public App { Timer memoryTimer = Timer(Timer::Type::Periodic, []() { auto app = optApp(); if (app) { + lvgl::getSyncLock()->lock(); app->updateMemory(); app->updatePsram(); + lvgl::getSyncLock()->unlock(); } }); Timer tasksTimer = Timer(Timer::Type::Periodic, []() { auto app = optApp(); - if (app) app->updateTasks(); + if (app) { + lvgl::getSyncLock()->lock(); + app->updateTasks(); + lvgl::getSyncLock()->unlock(); + } }); MemoryBarWidgets internalMemBar; From f47d4ac6da3741e4a8da17c944d2ca4c0c86645f Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 26 Dec 2025 14:00:27 +1000 Subject: [PATCH 11/22] are we thereeeeeee yet... --- Tactility/Source/app/files/View.cpp | 2 ++ Tactility/Source/app/systeminfo/SystemInfo.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Tactility/Source/app/files/View.cpp b/Tactility/Source/app/files/View.cpp index 0958ec146..320ea6669 100644 --- a/Tactility/Source/app/files/View.cpp +++ b/Tactility/Source/app/files/View.cpp @@ -422,6 +422,7 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr bu struct stat st; if (stat(new_file_path.c_str(), &st) == 0) { TT_LOG_W(TAG, "File already exists: \"%s\"", new_file_path.c_str()); + lock->unlock(); break; } @@ -450,6 +451,7 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr bu struct stat st; if (stat(new_folder_path.c_str(), &st) == 0) { TT_LOG_W(TAG, "Folder already exists: \"%s\"", new_folder_path.c_str()); + lock->unlock(); break; } diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index 98c73d993..9d639a6ca 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -289,19 +289,19 @@ class SystemInfoApp final : public App { Timer memoryTimer = Timer(Timer::Type::Periodic, []() { auto app = optApp(); if (app) { - lvgl::getSyncLock()->lock(); + auto lock = lvgl::getSyncLock()->asScopedLock(); + lock.lock(); app->updateMemory(); app->updatePsram(); - lvgl::getSyncLock()->unlock(); } }); Timer tasksTimer = Timer(Timer::Type::Periodic, []() { auto app = optApp(); if (app) { - lvgl::getSyncLock()->lock(); + auto lock = lvgl::getSyncLock()->asScopedLock(); + lock.lock(); app->updateTasks(); - lvgl::getSyncLock()->unlock(); } }); From dd521318c1352640ae6ba332300ac177d2a6f612 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 26 Dec 2025 14:16:06 +1000 Subject: [PATCH 12/22] Update SystemInfo.cpp --- Tactility/Source/app/systeminfo/SystemInfo.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index 9d639a6ca..7acdd9e1b 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -324,14 +325,10 @@ class SystemInfoApp final : public App { bool hasSystemStorage = false; void updateMemory() { - // Timer callbacks run in a task context (not ISR), so we can use LVGL lock - // Don't use kernel::lock() from timer callbacks - causes scheduler assertion updateMemoryBar(internalMemBar, getHeapFree(), getHeapTotal()); if (hasExternalMem) { - lvgl::getSyncLock()->lock(); updateMemoryBar(externalMemBar, getSpiFree(), getSpiTotal()); - lvgl::getSyncLock()->unlock(); } } From ef2d47399d269318022ab4e4c5b9cae5825c463c Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 26 Dec 2025 18:05:22 +1000 Subject: [PATCH 13/22] reduce trackball sensitivity very usable now. --- Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp index 43af582b6..cbcbda7da 100644 --- a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp +++ b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp @@ -13,7 +13,7 @@ bool TrackballDevice::start() { .pinLeft = GPIO_NUM_1, // BOARD_TBOX_G04 .pinDown = GPIO_NUM_15, // BOARD_TBOX_G03 .pinClick = GPIO_NUM_0, // BOARD_BOOT_PIN - .movementStep = 10 // pixels per movement + .movementStep = 1 // pixels per movement }; indev = driver::trackball::init(config); From c4c1123b7ba8c6773a04f4ffc162496628d1d46d Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 31 Dec 2025 06:07:39 +1000 Subject: [PATCH 14/22] maybe it builds now? hmm --- Tactility/CMakeLists.txt | 1 + Tactility/Source/app/systeminfo/SystemInfo.cpp | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index 431a2d292..7730a9f34 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -11,6 +11,7 @@ if (DEFINED ENV{ESP_IDF_VERSION}) lvgl driver KeyboardBacklight + Trackball elf_loader lv_screenshot QRCode diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index 7acdd9e1b..438071b78 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #ifdef ESP_PLATFORM #include @@ -224,8 +225,7 @@ static void updateRtosTasks(lv_obj_t* parent, bool showCpuPercent) { clearContainer(parent); UBaseType_t count = uxTaskGetNumberOfTasks(); - // Allocate task list in PSRAM to save internal RAM - auto* tasks = (TaskStatus_t*)heap_caps_malloc(sizeof(TaskStatus_t) * count, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + auto* tasks = malloc(sizeof(TaskStatus_t) * count); if (!tasks) { // Fallback to internal RAM if PSRAM allocation fails tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); @@ -375,7 +375,7 @@ class SystemInfoApp final : public App { // are averages over entire uptime, not instantaneous usage if (cpuSummaryLabel && taskCountLabel && uptimeLabel) { UBaseType_t count = uxTaskGetNumberOfTasks(); - auto* tasks = (TaskStatus_t*)heap_caps_malloc(sizeof(TaskStatus_t) * count, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + auto* tasks = malloc(sizeof(TaskStatus_t) * count); if (!tasks) tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); if (tasks) { uint32_t totalRuntime = 0; From 7aab8e2c54224f36b3ef13b90437f333a202cecd Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 31 Dec 2025 06:22:00 +1000 Subject: [PATCH 15/22] maybe now? --- Tactility/Source/app/localesettings/LocaleSettings.cpp | 3 ++- Tactility/Source/app/systeminfo/SystemInfo.cpp | 4 ---- Tactility/Source/app/timedatesettings/TimeDateSettings.cpp | 3 ++- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Tactility/Source/app/localesettings/LocaleSettings.cpp b/Tactility/Source/app/localesettings/LocaleSettings.cpp index ea08e24b7..e31592374 100644 --- a/Tactility/Source/app/localesettings/LocaleSettings.cpp +++ b/Tactility/Source/app/localesettings/LocaleSettings.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -27,7 +28,7 @@ extern const AppManifest manifest; class LocaleSettingsApp final : public App { tt::i18n::TextResources textResources = tt::i18n::TextResources(TEXT_RESOURCE_PATH); - Mutex mutex = Mutex(Mutex::Type::Recursive); + RecursiveMutex mutex; lv_obj_t* regionTextArea = nullptr; lv_obj_t* languageDropdown = nullptr; bool settingsUpdated = false; diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index 438071b78..dc54ac54d 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -226,10 +226,6 @@ static void updateRtosTasks(lv_obj_t* parent, bool showCpuPercent) { UBaseType_t count = uxTaskGetNumberOfTasks(); auto* tasks = malloc(sizeof(TaskStatus_t) * count); - if (!tasks) { - // Fallback to internal RAM if PSRAM allocation fails - tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); - } if (!tasks) { auto* error_label = lv_label_create(parent); lv_label_set_text(error_label, "Failed to allocate memory for task list"); diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index 229d423e1..bc521ebbe 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -17,7 +18,7 @@ extern const AppManifest manifest; class TimeDateSettingsApp final : public App { - Mutex mutex = Mutex(Mutex::Type::Recursive); + RecursiveMutex mutex; lv_obj_t* timeZoneLabel = nullptr; lv_obj_t* dateFormatDropdown = nullptr; From f717ee001a610d2a5dfdbe49580e4393e8e910cb Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 31 Dec 2025 06:53:59 +1000 Subject: [PATCH 16/22] maybe now? yes? ok --- Tactility/Source/app/systeminfo/SystemInfo.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index dc54ac54d..e2d601a11 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -225,7 +225,7 @@ static void updateRtosTasks(lv_obj_t* parent, bool showCpuPercent) { clearContainer(parent); UBaseType_t count = uxTaskGetNumberOfTasks(); - auto* tasks = malloc(sizeof(TaskStatus_t) * count); + auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); if (!tasks) { auto* error_label = lv_label_create(parent); lv_label_set_text(error_label, "Failed to allocate memory for task list"); @@ -371,7 +371,7 @@ class SystemInfoApp final : public App { // are averages over entire uptime, not instantaneous usage if (cpuSummaryLabel && taskCountLabel && uptimeLabel) { UBaseType_t count = uxTaskGetNumberOfTasks(); - auto* tasks = malloc(sizeof(TaskStatus_t) * count); + auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); if (!tasks) tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); if (tasks) { uint32_t totalRuntime = 0; From 1bae8ea261163b83d0eb3712ba592b4cf254f280 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 31 Dec 2025 07:24:17 +1000 Subject: [PATCH 17/22] Simulator's stop being mad! --- Drivers/KeyboardBacklight/CMakeLists.txt | 29 ++++++++++++++++++++---- Drivers/Trackball/CMakeLists.txt | 29 ++++++++++++++++++++---- Tactility/CMakeLists.txt | 2 ++ 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/Drivers/KeyboardBacklight/CMakeLists.txt b/Drivers/KeyboardBacklight/CMakeLists.txt index 48a859134..d85af2519 100644 --- a/Drivers/KeyboardBacklight/CMakeLists.txt +++ b/Drivers/KeyboardBacklight/CMakeLists.txt @@ -1,5 +1,24 @@ -idf_component_register( - SRC_DIRS "Source" - INCLUDE_DIRS "Source" - REQUIRES TactilityCore driver -) +cmake_minimum_required(VERSION 3.20) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +if (DEFINED ENV{ESP_IDF_VERSION}) + idf_component_register( + SRC_DIRS "Source/" + INCLUDE_DIRS "Source/" + REQUIRES TactilityCore driver + ) + +else() + file(GLOB SOURCES "Source/*.c*") + file(GLOB HEADERS "Source/*.h*") + + add_library(KeyboardBacklight STATIC) + + target_sources(KeyboardBacklight PRIVATE ${SOURCES}) + include_directories(KeyboardBacklight "Source/") + target_include_directories(KeyboardBacklight PUBLIC "Source/") + +endif() diff --git a/Drivers/Trackball/CMakeLists.txt b/Drivers/Trackball/CMakeLists.txt index 1a37664b5..d9b38a069 100644 --- a/Drivers/Trackball/CMakeLists.txt +++ b/Drivers/Trackball/CMakeLists.txt @@ -1,5 +1,24 @@ -idf_component_register( - SRC_DIRS "Source" - INCLUDE_DIRS "Source" - REQUIRES TactilityCore driver lvgl -) +cmake_minimum_required(VERSION 3.20) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +if (DEFINED ENV{ESP_IDF_VERSION}) + idf_component_register( + SRC_DIRS "Source/" + INCLUDE_DIRS "Source/" + REQUIRES TactilityCore driver lvgl + ) + +else() + file(GLOB SOURCES "Source/*.c*") + file(GLOB HEADERS "Source/*.h*") + + add_library(Trackball STATIC) + + target_sources(Trackball PRIVATE ${SOURCES}) + include_directories(Trackball "Source/") + target_include_directories(Trackball PUBLIC "Source/") + +endif() diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index 7730a9f34..27c099f8e 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -79,6 +79,8 @@ else() PUBLIC lv_screenshot PUBLIC minmea PUBLIC minitar + PUBLIC KeyboardBacklight + PUBLIC Trackball ) endif() From a152fa3aca8620b3010ddb59e3bac992b2d770e9 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Wed, 31 Dec 2025 07:27:07 +1000 Subject: [PATCH 18/22] Update SystemInfo.cpp --- Tactility/Source/app/systeminfo/SystemInfo.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Tactility/Source/app/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index e2d601a11..901fc11c0 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -372,7 +372,6 @@ class SystemInfoApp final : public App { if (cpuSummaryLabel && taskCountLabel && uptimeLabel) { UBaseType_t count = uxTaskGetNumberOfTasks(); auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); - if (!tasks) tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); if (tasks) { uint32_t totalRuntime = 0; UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime); From 0bccf0c32d0b0add696c88e71f5369ee0f42ff4e Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 1 Jan 2026 02:04:04 +1000 Subject: [PATCH 19/22] Fix simulator build hopefully.... --- Devices/lilygo-tdeck/CMakeLists.txt | 2 +- Devices/lilygo-tdeck/Source/Init.cpp | 54 +++++++++---------- .../KeyboardBacklight}/KeyboardBacklight.cpp | 2 +- .../KeyboardBacklight}/KeyboardBacklight.h | 2 +- .../Source/Trackball}/Trackball.cpp | 2 +- .../Source/Trackball}/Trackball.h | 2 +- .../Source/devices/KeyboardBacklight.cpp | 10 ++-- .../Source/devices/TdeckKeyboard.cpp | 4 +- .../Source/devices/TrackballDevice.cpp | 8 +-- Drivers/KeyboardBacklight/CMakeLists.txt | 24 --------- Drivers/Trackball/CMakeLists.txt | 24 --------- Tactility/CMakeLists.txt | 4 -- Tactility/Source/Tactility.cpp | 4 ++ .../Source/app/keyboard/KeyboardSettings.cpp | 24 ++++++--- .../service/displayidle/DisplayIdle.cpp | 28 +++++----- 15 files changed, 78 insertions(+), 116 deletions(-) rename {Drivers/KeyboardBacklight/Source => Devices/lilygo-tdeck/Source/KeyboardBacklight}/KeyboardBacklight.cpp (98%) rename {Drivers/KeyboardBacklight/Source => Devices/lilygo-tdeck/Source/KeyboardBacklight}/KeyboardBacklight.h (95%) rename {Drivers/Trackball/Source => Devices/lilygo-tdeck/Source/Trackball}/Trackball.cpp (99%) rename {Drivers/Trackball/Source => Devices/lilygo-tdeck/Source/Trackball}/Trackball.h (97%) delete mode 100644 Drivers/KeyboardBacklight/CMakeLists.txt delete mode 100644 Drivers/Trackball/CMakeLists.txt diff --git a/Devices/lilygo-tdeck/CMakeLists.txt b/Devices/lilygo-tdeck/CMakeLists.txt index df27972df..6b073058a 100644 --- a/Devices/lilygo-tdeck/CMakeLists.txt +++ b/Devices/lilygo-tdeck/CMakeLists.txt @@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "Source" - REQUIRES Tactility EspLcdCompat ST7789 GT911 PwmBacklight EstimatedPower KeyboardBacklight Trackball driver + REQUIRES Tactility EspLcdCompat ST7789 GT911 PwmBacklight EstimatedPower driver ) diff --git a/Devices/lilygo-tdeck/Source/Init.cpp b/Devices/lilygo-tdeck/Source/Init.cpp index 2e029fec0..40ca3a51a 100644 --- a/Devices/lilygo-tdeck/Source/Init.cpp +++ b/Devices/lilygo-tdeck/Source/Init.cpp @@ -8,8 +8,8 @@ #include "devices/KeyboardBacklight.h" #include "devices/TrackballDevice.h" -#include -#include +#include +#include #define TAG "tdeck" @@ -65,40 +65,40 @@ bool initBoot() { } } }); - + tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) { - auto kbBacklight = tt::hal::findDevice("Keyboard Backlight"); + auto kbBacklight = tt::hal::findDevice("Keyboard Backlight"); if (kbBacklight != nullptr) { - TT_LOG_I(TAG, "%s starting", kbBacklight->getName().c_str()); - auto kbDevice = std::static_pointer_cast(kbBacklight); - if (kbDevice->start()) { - TT_LOG_I(TAG, "%s started", kbBacklight->getName().c_str()); - } else { - TT_LOG_E(TAG, "%s start failed", kbBacklight->getName().c_str()); - } + TT_LOG_I(TAG, "%s starting", kbBacklight->getName().c_str()); + auto kbDevice = std::static_pointer_cast(kbBacklight); + if (kbDevice->start()) { + TT_LOG_I(TAG, "%s started", kbBacklight->getName().c_str()); + } else { + TT_LOG_E(TAG, "%s start failed", kbBacklight->getName().c_str()); } - - auto trackball = tt::hal::findDevice("Trackball"); + } + + auto trackball = tt::hal::findDevice("Trackball"); if (trackball != nullptr) { - TT_LOG_I(TAG, "%s starting", trackball->getName().c_str()); - auto tbDevice = std::static_pointer_cast(trackball); - if (tbDevice->start()) { - TT_LOG_I(TAG, "%s started", trackball->getName().c_str()); - } else { - TT_LOG_E(TAG, "%s start failed", trackball->getName().c_str()); - } + TT_LOG_I(TAG, "%s starting", trackball->getName().c_str()); + auto tbDevice = std::static_pointer_cast(trackball); + if (tbDevice->start()) { + TT_LOG_I(TAG, "%s started", trackball->getName().c_str()); + } else { + TT_LOG_E(TAG, "%s start failed", trackball->getName().c_str()); } - + } + //Backlight doesn't seem to turn on until toggled on and off from keyboard settings... // Or let the display and backlight sleep then wake it up. //Then it works fine...until reboot, then you need to toggle again. - auto kbSettings = tt::settings::keyboard::loadOrGetDefault(); - bool result = driver::keyboardbacklight::setBrightness(kbSettings.backlightEnabled ? kbSettings.backlightBrightness : 0); - if (!result) { - TT_LOG_W(TAG, "Failed to set keyboard backlight brightness"); - } + auto kbSettings = tt::settings::keyboard::loadOrGetDefault(); + bool result = keyboardbacklight::setBrightness(kbSettings.backlightEnabled ? kbSettings.backlightBrightness : 0); + if (!result) { + TT_LOG_W(TAG, "Failed to set keyboard backlight brightness"); + } - driver::trackball::setEnabled(kbSettings.trackballEnabled); + trackball::setEnabled(kbSettings.trackballEnabled); }); return true; diff --git a/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp b/Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.cpp similarity index 98% rename from Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp rename to Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.cpp index 62d32ccef..32f5939b4 100644 --- a/Drivers/KeyboardBacklight/Source/KeyboardBacklight.cpp +++ b/Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.cpp @@ -4,7 +4,7 @@ static const char* TAG = "KeyboardBacklight"; -namespace driver::keyboardbacklight { +namespace keyboardbacklight { static const uint8_t CMD_BRIGHTNESS = 0x01; static const uint8_t CMD_DEFAULT_BRIGHTNESS = 0x02; diff --git a/Drivers/KeyboardBacklight/Source/KeyboardBacklight.h b/Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.h similarity index 95% rename from Drivers/KeyboardBacklight/Source/KeyboardBacklight.h rename to Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.h index 2cb0f8dd7..dad27c2be 100644 --- a/Drivers/KeyboardBacklight/Source/KeyboardBacklight.h +++ b/Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.h @@ -3,7 +3,7 @@ #include #include -namespace driver::keyboardbacklight { +namespace keyboardbacklight { /** * @brief Initialize keyboard backlight control diff --git a/Drivers/Trackball/Source/Trackball.cpp b/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp similarity index 99% rename from Drivers/Trackball/Source/Trackball.cpp rename to Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp index be088575f..68b38d697 100644 --- a/Drivers/Trackball/Source/Trackball.cpp +++ b/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp @@ -3,7 +3,7 @@ static const char* TAG = "Trackball"; -namespace driver::trackball { +namespace trackball { static TrackballConfig g_config; static lv_indev_t* g_indev = nullptr; diff --git a/Drivers/Trackball/Source/Trackball.h b/Devices/lilygo-tdeck/Source/Trackball/Trackball.h similarity index 97% rename from Drivers/Trackball/Source/Trackball.h rename to Devices/lilygo-tdeck/Source/Trackball/Trackball.h index 593c70d94..5e103b37b 100644 --- a/Drivers/Trackball/Source/Trackball.h +++ b/Devices/lilygo-tdeck/Source/Trackball/Trackball.h @@ -3,7 +3,7 @@ #include #include -namespace driver::trackball { +namespace trackball { /** * @brief Trackball configuration structure diff --git a/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp index 00149e315..a91377798 100644 --- a/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp +++ b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp @@ -1,5 +1,5 @@ #include "KeyboardBacklight.h" -#include // Driver +#include // Driver #include // TODO: Add Mutex and consider refactoring into a class @@ -9,14 +9,14 @@ bool KeyboardBacklightDevice::start() { } // T-Deck uses I2C_NUM_0 for internal peripherals - initialized = driver::keyboardbacklight::init(I2C_NUM_0); + initialized = keyboardbacklight::init(I2C_NUM_0); return initialized; } bool KeyboardBacklightDevice::stop() { if (initialized) { // Turn off backlight on shutdown - driver::keyboardbacklight::setBrightness(0); + keyboardbacklight::setBrightness(0); initialized = false; } return true; @@ -26,12 +26,12 @@ bool KeyboardBacklightDevice::setBrightness(uint8_t brightness) { if (!initialized) { return false; } - return driver::keyboardbacklight::setBrightness(brightness); + return keyboardbacklight::setBrightness(brightness); } uint8_t KeyboardBacklightDevice::getBrightness() const { if (!initialized) { return 0; } - return driver::keyboardbacklight::getBrightness(); + return keyboardbacklight::getBrightness(); } diff --git a/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp b/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp index 30b404fc8..063d3a825 100644 --- a/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp +++ b/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include using tt::hal::findFirstDevice; @@ -60,7 +60,7 @@ static void keyboard_read_callback(TT_UNUSED lv_indev_t* indev, lv_indev_data_t* // Restore keyboard backlight if enabled in settings auto ksettings = tt::settings::keyboard::loadOrGetDefault(); if (ksettings.backlightEnabled) { - driver::keyboardbacklight::setBrightness(ksettings.backlightBrightness); + keyboardbacklight::setBrightness(ksettings.backlightBrightness); } } } diff --git a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp index cbcbda7da..16b65f265 100644 --- a/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp +++ b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp @@ -1,5 +1,5 @@ #include "TrackballDevice.h" -#include // Driver +#include // Driver bool TrackballDevice::start() { if (initialized) { @@ -7,7 +7,7 @@ bool TrackballDevice::start() { } // T-Deck trackball GPIO configuration from LilyGo reference - driver::trackball::TrackballConfig config = { + trackball::TrackballConfig config = { .pinRight = GPIO_NUM_2, // BOARD_TBOX_G02 .pinUp = GPIO_NUM_3, // BOARD_TBOX_G01 .pinLeft = GPIO_NUM_1, // BOARD_TBOX_G04 @@ -16,7 +16,7 @@ bool TrackballDevice::start() { .movementStep = 1 // pixels per movement }; - indev = driver::trackball::init(config); + indev = trackball::init(config); if (indev != nullptr) { initialized = true; return true; @@ -28,7 +28,7 @@ bool TrackballDevice::start() { bool TrackballDevice::stop() { if (initialized) { // LVGL will handle indev cleanup - driver::trackball::deinit(); + trackball::deinit(); indev = nullptr; initialized = false; } diff --git a/Drivers/KeyboardBacklight/CMakeLists.txt b/Drivers/KeyboardBacklight/CMakeLists.txt deleted file mode 100644 index d85af2519..000000000 --- a/Drivers/KeyboardBacklight/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -cmake_minimum_required(VERSION 3.20) - -set(CMAKE_CXX_STANDARD 23) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -if (DEFINED ENV{ESP_IDF_VERSION}) - idf_component_register( - SRC_DIRS "Source/" - INCLUDE_DIRS "Source/" - REQUIRES TactilityCore driver - ) - -else() - file(GLOB SOURCES "Source/*.c*") - file(GLOB HEADERS "Source/*.h*") - - add_library(KeyboardBacklight STATIC) - - target_sources(KeyboardBacklight PRIVATE ${SOURCES}) - include_directories(KeyboardBacklight "Source/") - target_include_directories(KeyboardBacklight PUBLIC "Source/") - -endif() diff --git a/Drivers/Trackball/CMakeLists.txt b/Drivers/Trackball/CMakeLists.txt deleted file mode 100644 index d9b38a069..000000000 --- a/Drivers/Trackball/CMakeLists.txt +++ /dev/null @@ -1,24 +0,0 @@ -cmake_minimum_required(VERSION 3.20) - -set(CMAKE_CXX_STANDARD 23) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -if (DEFINED ENV{ESP_IDF_VERSION}) - idf_component_register( - SRC_DIRS "Source/" - INCLUDE_DIRS "Source/" - REQUIRES TactilityCore driver lvgl - ) - -else() - file(GLOB SOURCES "Source/*.c*") - file(GLOB HEADERS "Source/*.h*") - - add_library(Trackball STATIC) - - target_sources(Trackball PRIVATE ${SOURCES}) - include_directories(Trackball "Source/") - target_include_directories(Trackball PUBLIC "Source/") - -endif() diff --git a/Tactility/CMakeLists.txt b/Tactility/CMakeLists.txt index 27c099f8e..a1e6e9bba 100644 --- a/Tactility/CMakeLists.txt +++ b/Tactility/CMakeLists.txt @@ -10,8 +10,6 @@ if (DEFINED ENV{ESP_IDF_VERSION}) TactilityCore lvgl driver - KeyboardBacklight - Trackball elf_loader lv_screenshot QRCode @@ -79,8 +77,6 @@ else() PUBLIC lv_screenshot PUBLIC minmea PUBLIC minitar - PUBLIC KeyboardBacklight - PUBLIC Trackball ) endif() diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index fba21be44..09d030e1b 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -84,7 +84,9 @@ namespace app { namespace imageviewer { extern const AppManifest manifest; } namespace inputdialog { extern const AppManifest manifest; } namespace launcher { extern const AppManifest manifest; } +#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK) namespace keyboardsettings { extern const AppManifest manifest; } +#endif namespace localesettings { extern const AppManifest manifest; } namespace notes { extern const AppManifest manifest; } namespace power { extern const AppManifest manifest; } @@ -126,7 +128,9 @@ static void registerInternalApps() { addAppManifest(app::imageviewer::manifest); addAppManifest(app::inputdialog::manifest); addAppManifest(app::launcher::manifest); +#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK) addAppManifest(app::keyboardsettings::manifest); +#endif addAppManifest(app::localesettings::manifest); addAppManifest(app::notes::manifest); addAppManifest(app::settings::manifest); diff --git a/Tactility/Source/app/keyboard/KeyboardSettings.cpp b/Tactility/Source/app/keyboard/KeyboardSettings.cpp index 7744df0c7..31b9cfe71 100644 --- a/Tactility/Source/app/keyboard/KeyboardSettings.cpp +++ b/Tactility/Source/app/keyboard/KeyboardSettings.cpp @@ -1,3 +1,5 @@ +#ifdef ESP_PLATFORM + #include #include @@ -7,11 +9,11 @@ #include // Forward declare driver functions -namespace driver::keyboardbacklight { +namespace keyboardbacklight { bool setBrightness(uint8_t brightness); } -namespace driver::trackball { +namespace trackball { void setEnabled(bool enabled); } @@ -20,7 +22,7 @@ namespace tt::app::keyboardsettings { constexpr auto* TAG = "KeyboardSettings"; static void applyKeyboardBacklight(bool enabled, uint8_t brightness) { - driver::keyboardbacklight::setBrightness(enabled ? brightness : 0); + keyboardbacklight::setBrightness(enabled ? brightness : 0); } class KeyboardSettingsApp final : public App { @@ -28,8 +30,8 @@ class KeyboardSettingsApp final : public App { settings::keyboard::KeyboardSettings kbSettings; bool updated = false; lv_obj_t* switchBacklight = nullptr; - lv_obj_t* sliderBrightness = nullptr; lv_obj_t* switchTrackball = nullptr; + lv_obj_t* sliderBrightness = nullptr; lv_obj_t* switchTimeoutEnable = nullptr; lv_obj_t* timeoutDropdown = nullptr; @@ -60,7 +62,7 @@ class KeyboardSettingsApp final : public App { bool enabled = lv_obj_has_state(app->switchTrackball, LV_STATE_CHECKED); app->kbSettings.trackballEnabled = enabled; app->updated = true; - driver::trackball::setEnabled(enabled); + trackball::setEnabled(enabled); } static void onTimeoutEnableSwitch(lv_event_t* e) { @@ -161,6 +163,8 @@ class KeyboardSettingsApp final : public App { if (kbSettings.backlightTimeoutEnabled) lv_obj_add_state(switchTimeoutEnable, LV_STATE_CHECKED); lv_obj_align(switchTimeoutEnable, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(switchTimeoutEnable, onTimeoutEnableSwitch, LV_EVENT_VALUE_CHANGED, this); + // Remove this state once implemented properly to sync with display backlight + lv_obj_add_state(switchTimeoutEnable, LV_STATE_DISABLED); auto* timeout_select_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(timeout_select_wrapper, LV_PCT(100), LV_SIZE_CONTENT); @@ -183,9 +187,11 @@ class KeyboardSettingsApp final : public App { uint32_t idx = 2; // default 1 minute if (ms == 15000) idx = 0; else if (ms == 30000) idx = 1; else if (ms == 60000) idx = 2; else if (ms == 120000) idx = 3; else if (ms == 300000) idx = 4; else if (ms == 0) idx = 5; lv_dropdown_set_selected(timeoutDropdown, idx); - if (!kbSettings.backlightTimeoutEnabled) { - lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED); - } + //if (!kbSettings.backlightTimeoutEnabled) { + //lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED); + //} + // Remove this state and uncomment above once implemented properly to sync with display backlight + lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED); } void onHide(TT_UNUSED AppContext& app) override { @@ -205,3 +211,5 @@ extern const AppManifest manifest = { }; } + +#endif diff --git a/Tactility/Source/service/displayidle/DisplayIdle.cpp b/Tactility/Source/service/displayidle/DisplayIdle.cpp index 2ce2d491a..51a25a415 100644 --- a/Tactility/Source/service/displayidle/DisplayIdle.cpp +++ b/Tactility/Source/service/displayidle/DisplayIdle.cpp @@ -4,14 +4,16 @@ #include #include #include -#include +//#include #include -#include +//#include + +// TODO: KEYBOARD STUFF COMMENTED OUT FOR FUTURE REFERENCE!! // Forward declare driver functions -namespace driver::keyboardbacklight { +/* namespace driver::keyboardbacklight { bool setBrightness(uint8_t brightness); -} +} */ namespace tt::service::displayidle { @@ -21,17 +23,17 @@ class DisplayIdleService final : public Service { std::unique_ptr timer; bool displayDimmed = false; - bool keyboardDimmed = false; + //bool keyboardDimmed = false; settings::display::DisplaySettings cachedDisplaySettings; - settings::keyboard::KeyboardSettings cachedKeyboardSettings; + //settings::keyboard::KeyboardSettings cachedKeyboardSettings; static std::shared_ptr getDisplay() { return hal::findFirstDevice(hal::Device::Type::Display); } - static std::shared_ptr getKeyboard() { +/* static std::shared_ptr getKeyboard() { return hal::findFirstDevice(hal::Device::Type::Keyboard); - } + } */ void tick() { // Settings are now cached and event-driven (no file I/O in timer callback!) @@ -65,7 +67,7 @@ class DisplayIdleService final : public Service { } // Handle keyboard backlight - auto keyboard = getKeyboard(); +/* auto keyboard = getKeyboard(); if (keyboard != nullptr && keyboard->isAttached()) { if (!cachedKeyboardSettings.backlightTimeoutEnabled || cachedKeyboardSettings.backlightTimeoutMs == 0) { if (keyboardDimmed) { @@ -81,7 +83,7 @@ class DisplayIdleService final : public Service { keyboardDimmed = false; } } - } + } */ } public: @@ -89,7 +91,7 @@ class DisplayIdleService final : public Service { // Load settings once at startup and cache them // This eliminates file I/O from timer callback (prevents watchdog timeout) cachedDisplaySettings = settings::display::loadOrGetDefault(); - cachedKeyboardSettings = settings::keyboard::loadOrGetDefault(); + //cachedKeyboardSettings = settings::keyboard::loadOrGetDefault(); // Note: Settings changes require service restart to take effect // TODO: Add DisplaySettingsChanged/KeyboardSettingsChanged events for dynamic updates @@ -112,11 +114,11 @@ class DisplayIdleService final : public Service { displayDimmed = false; } // Ensure keyboard backlight restored on stop - auto keyboard = getKeyboard(); +/* auto keyboard = getKeyboard(); if (keyboard && keyboardDimmed) { driver::keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0); keyboardDimmed = false; - } + } */ } }; From de137e567257803ae33895b2f85c75c189ba2115 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 1 Jan 2026 02:27:53 +1000 Subject: [PATCH 20/22] minor cleanup --- Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp | 2 -- Devices/lilygo-tdeck/Source/Trackball/Trackball.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp b/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp index 68b38d697..74c87aeb6 100644 --- a/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp +++ b/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp @@ -12,8 +12,6 @@ static bool g_enabled = true; // Track last GPIO states for edge detection static bool g_lastState[5] = {false, false, false, false, false}; -// Encoder diff accumulator for navigation -static int16_t g_encDiff = 0; static void read_cb(lv_indev_t* indev, lv_indev_data_t* data) { if (!g_initialized || !g_enabled) { diff --git a/Devices/lilygo-tdeck/Source/Trackball/Trackball.h b/Devices/lilygo-tdeck/Source/Trackball/Trackball.h index 5e103b37b..acfe9c29f 100644 --- a/Devices/lilygo-tdeck/Source/Trackball/Trackball.h +++ b/Devices/lilygo-tdeck/Source/Trackball/Trackball.h @@ -31,7 +31,7 @@ void deinit(); /** * @brief Set movement step size - * @param step Pixels to move per trackball event + * @param step Encoder steps per trackball event */ void setMovementStep(uint8_t step); From b93e9186018bb0da4ec92add4bfd972a0d7ea40c Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 1 Jan 2026 18:22:28 +1000 Subject: [PATCH 21/22] keyboardidle service and some fixes and notes. --- Devices/lilygo-tdeck/Source/Init.cpp | 8 +- Tactility/Source/Tactility.cpp | 6 ++ Tactility/Source/app/display/Display.cpp | 88 +++++++++-------- .../Source/app/keyboard/KeyboardSettings.cpp | 22 +++-- .../service/displayidle/DisplayIdle.cpp | 43 +------- .../service/keyboardidle/KeyboardIdle.cpp | 97 +++++++++++++++++++ 6 files changed, 174 insertions(+), 90 deletions(-) create mode 100644 Tactility/Source/service/keyboardidle/KeyboardIdle.cpp diff --git a/Devices/lilygo-tdeck/Source/Init.cpp b/Devices/lilygo-tdeck/Source/Init.cpp index 40ca3a51a..8c58d227e 100644 --- a/Devices/lilygo-tdeck/Source/Init.cpp +++ b/Devices/lilygo-tdeck/Source/Init.cpp @@ -89,9 +89,13 @@ bool initBoot() { } } - //Backlight doesn't seem to turn on until toggled on and off from keyboard settings... + // Backlight doesn't seem to turn on until toggled on and off from keyboard settings... // Or let the display and backlight sleep then wake it up. - //Then it works fine...until reboot, then you need to toggle again. + // Then it works fine...until reboot, then you need to toggle again. + // The current keyboard firmware sets backlight duty to 0 on boot. + // https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/firmware/T-Keyboard_Keyboard_ESP32C3_250620.bin + // https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/Keyboard_ESP32C3/Keyboard_ESP32C3.ino#L25 + // https://github.com/Xinyuan-LilyGO/T-Deck/blob/master/examples/Keyboard_ESP32C3/Keyboard_ESP32C3.ino#L217 auto kbSettings = tt::settings::keyboard::loadOrGetDefault(); bool result = keyboardbacklight::setBrightness(kbSettings.backlightEnabled ? kbSettings.backlightBrightness : 0); if (!result) { diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 09d030e1b..a050765d0 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -52,6 +52,9 @@ namespace service { namespace memorychecker { extern const ServiceManifest manifest; } namespace statusbar { extern const ServiceManifest manifest; } namespace displayidle { extern const ServiceManifest manifest; } +#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK) + namespace keyboardidle { extern const ServiceManifest manifest; } +#endif #if TT_FEATURE_SCREENSHOT_ENABLED namespace screenshot { extern const ServiceManifest manifest; } #endif @@ -235,6 +238,9 @@ static void registerAndStartSecondaryServices() { addService(service::gui::manifest); addService(service::statusbar::manifest); addService(service::displayidle::manifest); +#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK) + addService(service::keyboardidle::manifest); +#endif addService(service::memorychecker::manifest); #if TT_FEATURE_SCREENSHOT_ENABLED addService(service::screenshot::manifest); diff --git a/Tactility/Source/app/display/Display.cpp b/Tactility/Source/app/display/Display.cpp index 3b3090ff4..3dbcd5b5e 100644 --- a/Tactility/Source/app/display/Display.cpp +++ b/Tactility/Source/app/display/Display.cpp @@ -182,44 +182,56 @@ class DisplayApp final : public App { // Screen timeout - auto* timeout_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(timeout_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(timeout_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(timeout_wrapper, 0, LV_STATE_DEFAULT); - - auto* timeout_label = lv_label_create(timeout_wrapper); - lv_label_set_text(timeout_label, "Auto screen off"); - lv_obj_align(timeout_label, LV_ALIGN_LEFT_MID, 0, 0); - - timeoutSwitch = lv_switch_create(timeout_wrapper); - if (displaySettings.backlightTimeoutEnabled) { - lv_obj_add_state(timeoutSwitch, LV_STATE_CHECKED); - } - lv_obj_align(timeoutSwitch, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_add_event_cb(timeoutSwitch, onTimeoutSwitch, LV_EVENT_VALUE_CHANGED, this); - - auto* timeout_select_wrapper = lv_obj_create(main_wrapper); - lv_obj_set_size(timeout_select_wrapper, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_style_pad_all(timeout_select_wrapper, 0, LV_STATE_DEFAULT); - lv_obj_set_style_border_width(timeout_select_wrapper, 0, LV_STATE_DEFAULT); - - auto* timeout_value_label = lv_label_create(timeout_select_wrapper); - lv_label_set_text(timeout_value_label, "Timeout"); - lv_obj_align(timeout_value_label, LV_ALIGN_LEFT_MID, 0, 0); - - timeoutDropdown = lv_dropdown_create(timeout_select_wrapper); - lv_dropdown_set_options(timeoutDropdown, "15 seconds\n30 seconds\n1 minute\n2 minutes\n5 minutes\nNever"); - lv_obj_align(timeoutDropdown, LV_ALIGN_RIGHT_MID, 0, 0); - lv_obj_set_style_border_color(timeoutDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN); - lv_obj_set_style_border_width(timeoutDropdown, 1, LV_PART_MAIN); - lv_obj_add_event_cb(timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, this); - // Initialize dropdown selection from settings - uint32_t ms = displaySettings.backlightTimeoutMs; - uint32_t idx = 2; // default 1 minute - if (ms == 15000) idx = 0; else if (ms == 30000) idx = 1; else if (ms == 60000) idx = 2; else if (ms == 120000) idx = 3; else if (ms == 300000) idx = 4; else if (ms == 0) idx = 5; - lv_dropdown_set_selected(timeoutDropdown, idx); - if (!displaySettings.backlightTimeoutEnabled) { - lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED); + if (hal_display->supportsBacklightDuty()) { + auto* timeout_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(timeout_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(timeout_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(timeout_wrapper, 0, LV_STATE_DEFAULT); + + auto* timeout_label = lv_label_create(timeout_wrapper); + lv_label_set_text(timeout_label, "Auto screen off"); + lv_obj_align(timeout_label, LV_ALIGN_LEFT_MID, 0, 0); + + timeoutSwitch = lv_switch_create(timeout_wrapper); + if (displaySettings.backlightTimeoutEnabled) { + lv_obj_add_state(timeoutSwitch, LV_STATE_CHECKED); + } + lv_obj_align(timeoutSwitch, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(timeoutSwitch, onTimeoutSwitch, LV_EVENT_VALUE_CHANGED, this); + + auto* timeout_select_wrapper = lv_obj_create(main_wrapper); + lv_obj_set_size(timeout_select_wrapper, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_pad_all(timeout_select_wrapper, 0, LV_STATE_DEFAULT); + lv_obj_set_style_border_width(timeout_select_wrapper, 0, LV_STATE_DEFAULT); + + auto* timeout_value_label = lv_label_create(timeout_select_wrapper); + lv_label_set_text(timeout_value_label, "Timeout"); + lv_obj_align(timeout_value_label, LV_ALIGN_LEFT_MID, 0, 0); + + timeoutDropdown = lv_dropdown_create(timeout_select_wrapper); + lv_dropdown_set_options(timeoutDropdown, "15 seconds\n30 seconds\n1 minute\n2 minutes\n5 minutes\nNever"); + lv_obj_align(timeoutDropdown, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_set_style_border_color(timeoutDropdown, lv_color_hex(0xFAFAFA), LV_PART_MAIN); + lv_obj_set_style_border_width(timeoutDropdown, 1, LV_PART_MAIN); + lv_obj_add_event_cb(timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, this); + // Initialize dropdown selection from settings + uint32_t ms = displaySettings.backlightTimeoutMs; + uint32_t idx = 2; // default 1 minute + if (ms == 15000) idx = 0; + else if (ms == 30000) + idx = 1; + else if (ms == 60000) + idx = 2; + else if (ms == 120000) + idx = 3; + else if (ms == 300000) + idx = 4; + else if (ms == 0) + idx = 5; + lv_dropdown_set_selected(timeoutDropdown, idx); + if (!displaySettings.backlightTimeoutEnabled) { + lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED); + } } } diff --git a/Tactility/Source/app/keyboard/KeyboardSettings.cpp b/Tactility/Source/app/keyboard/KeyboardSettings.cpp index 31b9cfe71..075d18420 100644 --- a/Tactility/Source/app/keyboard/KeyboardSettings.cpp +++ b/Tactility/Source/app/keyboard/KeyboardSettings.cpp @@ -163,8 +163,6 @@ class KeyboardSettingsApp final : public App { if (kbSettings.backlightTimeoutEnabled) lv_obj_add_state(switchTimeoutEnable, LV_STATE_CHECKED); lv_obj_align(switchTimeoutEnable, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_add_event_cb(switchTimeoutEnable, onTimeoutEnableSwitch, LV_EVENT_VALUE_CHANGED, this); - // Remove this state once implemented properly to sync with display backlight - lv_obj_add_state(switchTimeoutEnable, LV_STATE_DISABLED); auto* timeout_select_wrapper = lv_obj_create(main_wrapper); lv_obj_set_size(timeout_select_wrapper, LV_PCT(100), LV_SIZE_CONTENT); @@ -185,13 +183,21 @@ class KeyboardSettingsApp final : public App { // Initialize dropdown selection from settings uint32_t ms = kbSettings.backlightTimeoutMs; uint32_t idx = 2; // default 1 minute - if (ms == 15000) idx = 0; else if (ms == 30000) idx = 1; else if (ms == 60000) idx = 2; else if (ms == 120000) idx = 3; else if (ms == 300000) idx = 4; else if (ms == 0) idx = 5; + if (ms == 15000) idx = 0; + else if (ms == 30000) + idx = 1; + else if (ms == 60000) + idx = 2; + else if (ms == 120000) + idx = 3; + else if (ms == 300000) + idx = 4; + else if (ms == 0) + idx = 5; lv_dropdown_set_selected(timeoutDropdown, idx); - //if (!kbSettings.backlightTimeoutEnabled) { - //lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED); - //} - // Remove this state and uncomment above once implemented properly to sync with display backlight - lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED); + if (!kbSettings.backlightTimeoutEnabled) { + lv_obj_add_state(timeoutDropdown, LV_STATE_DISABLED); + } } void onHide(TT_UNUSED AppContext& app) override { diff --git a/Tactility/Source/service/displayidle/DisplayIdle.cpp b/Tactility/Source/service/displayidle/DisplayIdle.cpp index 51a25a415..d2f9dcbd4 100644 --- a/Tactility/Source/service/displayidle/DisplayIdle.cpp +++ b/Tactility/Source/service/displayidle/DisplayIdle.cpp @@ -4,16 +4,7 @@ #include #include #include -//#include #include -//#include - -// TODO: KEYBOARD STUFF COMMENTED OUT FOR FUTURE REFERENCE!! - -// Forward declare driver functions -/* namespace driver::keyboardbacklight { - bool setBrightness(uint8_t brightness); -} */ namespace tt::service::displayidle { @@ -23,18 +14,12 @@ class DisplayIdleService final : public Service { std::unique_ptr timer; bool displayDimmed = false; - //bool keyboardDimmed = false; settings::display::DisplaySettings cachedDisplaySettings; - //settings::keyboard::KeyboardSettings cachedKeyboardSettings; static std::shared_ptr getDisplay() { return hal::findFirstDevice(hal::Device::Type::Display); } -/* static std::shared_ptr getKeyboard() { - return hal::findFirstDevice(hal::Device::Type::Keyboard); - } */ - void tick() { // Settings are now cached and event-driven (no file I/O in timer callback!) // This prevents watchdog timeout from blocking the Timer Service task @@ -65,25 +50,6 @@ class DisplayIdleService final : public Service { } } } - - // Handle keyboard backlight -/* auto keyboard = getKeyboard(); - if (keyboard != nullptr && keyboard->isAttached()) { - if (!cachedKeyboardSettings.backlightTimeoutEnabled || cachedKeyboardSettings.backlightTimeoutMs == 0) { - if (keyboardDimmed) { - driver::keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0); - keyboardDimmed = false; - } - } else { - if (!keyboardDimmed && inactive_ms >= cachedKeyboardSettings.backlightTimeoutMs) { - driver::keyboardbacklight::setBrightness(0); - keyboardDimmed = true; - } else if (keyboardDimmed && inactive_ms < 100) { - driver::keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0); - keyboardDimmed = false; - } - } - } */ } public: @@ -91,10 +57,9 @@ class DisplayIdleService final : public Service { // Load settings once at startup and cache them // This eliminates file I/O from timer callback (prevents watchdog timeout) cachedDisplaySettings = settings::display::loadOrGetDefault(); - //cachedKeyboardSettings = settings::keyboard::loadOrGetDefault(); // Note: Settings changes require service restart to take effect - // TODO: Add DisplaySettingsChanged/KeyboardSettingsChanged events for dynamic updates + // TODO: Add DisplaySettingsChanged events for dynamic updates timer = std::make_unique(Timer::Type::Periodic, [this]{ this->tick(); }); timer->setThreadPriority(Thread::Priority::Lower); @@ -113,12 +78,6 @@ class DisplayIdleService final : public Service { display->setBacklightDuty(cachedDisplaySettings.backlightDuty); displayDimmed = false; } - // Ensure keyboard backlight restored on stop -/* auto keyboard = getKeyboard(); - if (keyboard && keyboardDimmed) { - driver::keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0); - keyboardDimmed = false; - } */ } }; diff --git a/Tactility/Source/service/keyboardidle/KeyboardIdle.cpp b/Tactility/Source/service/keyboardidle/KeyboardIdle.cpp new file mode 100644 index 000000000..d722c496c --- /dev/null +++ b/Tactility/Source/service/keyboardidle/KeyboardIdle.cpp @@ -0,0 +1,97 @@ +#ifdef ESP_PLATFORM + +#include +#include +#include +#include +#include +#include +#include + +namespace keyboardbacklight { + bool setBrightness(uint8_t brightness); +} + +namespace tt::service::keyboardidle { + +constexpr auto* TAG = "KeyboardIdle"; + +class KeyboardIdleService final : public Service { + + std::unique_ptr timer; + bool keyboardDimmed = false; + settings::keyboard::KeyboardSettings cachedKeyboardSettings; + + static std::shared_ptr getKeyboard() { + return hal::findFirstDevice(hal::Device::Type::Keyboard); + } + + void tick() { + // Settings are now cached and event-driven (no file I/O in timer callback!) + // This prevents watchdog timeout from blocking the Timer Service task + + // Query LVGL inactivity once for both checks + uint32_t inactive_ms = 0; + if (lvgl::lock(100)) { + inactive_ms = lv_disp_get_inactive_time(nullptr); + lvgl::unlock(); + } + + // Handle keyboard backlight + auto keyboard = getKeyboard(); + if (keyboard != nullptr && keyboard->isAttached()) { + // If timeout disabled, ensure backlight restored if we had dimmed it + if (!cachedKeyboardSettings.backlightTimeoutEnabled || cachedKeyboardSettings.backlightTimeoutMs == 0) { + if (keyboardDimmed) { + keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0); + keyboardDimmed = false; + } + } else { + if (!keyboardDimmed && inactive_ms >= cachedKeyboardSettings.backlightTimeoutMs) { + keyboardbacklight::setBrightness(0); + keyboardDimmed = true; + } else if (keyboardDimmed && inactive_ms < 100) { + keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0); + keyboardDimmed = false; + } + } + } + } + +public: + bool onStart(TT_UNUSED ServiceContext& service) override { + // Load settings once at startup and cache them + // This eliminates file I/O from timer callback (prevents watchdog timeout) + cachedKeyboardSettings = settings::keyboard::loadOrGetDefault(); + + // Note: Settings changes require service restart to take effect + // TODO: Add KeyboardSettingsChanged events for dynamic updates + + timer = std::make_unique(Timer::Type::Periodic, [this]{ this->tick(); }); + timer->setThreadPriority(Thread::Priority::Lower); + timer->start(250); // check 4x per second for snappy restore + return true; + } + + void onStop(TT_UNUSED ServiceContext& service) override { + if (timer) { + timer->stop(); + timer = nullptr; + } + // Ensure keyboard restored on stop + auto keyboard = getKeyboard(); + if (keyboard && keyboardDimmed) { + keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0); + keyboardDimmed = false; + } + } +}; + +extern const ServiceManifest manifest = { + .id = "KeyboardIdle", + .createService = create +}; + +} + +#endif From 1c3b98e8b4b8ab03db2909f84ccc8bdb5c574288 Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Fri, 2 Jan 2026 21:03:15 +1000 Subject: [PATCH 22/22] Update TdeckKeyboard.cpp --- Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp b/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp index 063d3a825..31cf0173d 100644 --- a/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp +++ b/Devices/lilygo-tdeck/Source/devices/TdeckKeyboard.cpp @@ -44,6 +44,7 @@ static void keyboard_read_callback(TT_UNUSED lv_indev_t* indev, lv_indev_data_t* TT_LOG_D(TAG, "Pressed %d", read_buffer); data->key = read_buffer; data->state = LV_INDEV_STATE_PRESSED; + // TODO: Avoid performance hit by calling loadOrGetDefault() on each key press // Ensure LVGL activity is triggered so idle services can wake the display lv_disp_trig_activity(nullptr);