diff --git a/Data/data/settings/system.properties b/Data/data/settings/system.properties index 0b905db83..71b8b7193 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/Los_Angeles \ No newline at end of file diff --git a/Devices/lilygo-tdeck/Source/Configuration.cpp b/Devices/lilygo-tdeck/Source/Configuration.cpp index 640f71145..6c44b1351 100644 --- a/Devices/lilygo-tdeck/Source/Configuration.cpp +++ b/Devices/lilygo-tdeck/Source/Configuration.cpp @@ -1,7 +1,9 @@ #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 @@ -15,6 +17,8 @@ static std::vector> createDevices() { createPower(), createDisplay(), std::make_shared(), + std::make_shared(), + std::make_shared(), createSdCard() }; } diff --git a/Devices/lilygo-tdeck/Source/Init.cpp b/Devices/lilygo-tdeck/Source/Init.cpp index 574496640..8c58d227e 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" @@ -59,5 +65,45 @@ 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. + // 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) { + TT_LOG_W(TAG, "Failed to set keyboard backlight brightness"); + } + + trackball::setEnabled(kbSettings.trackballEnabled); + }); + return true; } diff --git a/Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.cpp b/Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.cpp new file mode 100644 index 000000000..32f5939b4 --- /dev/null +++ b/Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.cpp @@ -0,0 +1,109 @@ +#include "KeyboardBacklight.h" +#include +#include + +static const char* TAG = "KeyboardBacklight"; + +namespace 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; + +// TODO: Umm...something. Calls xxxBrightness, ignores return values. +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 + 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; +} + +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() { + if (g_i2cPort >= I2C_NUM_MAX) { + ESP_LOGE(TAG, "Keyboard backlight not initialized"); + return 0; + } + + return g_currentBrightness; +} + +} diff --git a/Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.h b/Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.h new file mode 100644 index 000000000..dad27c2be --- /dev/null +++ b/Devices/lilygo-tdeck/Source/KeyboardBacklight/KeyboardBacklight.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +namespace 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/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp b/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp new file mode 100644 index 000000000..74c87aeb6 --- /dev/null +++ b/Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp @@ -0,0 +1,145 @@ +#include "Trackball.h" +#include + +static const char* TAG = "Trackball"; + +namespace 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}; + +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 += g_config.movementStep; + } + // Up pressed (rising edge) + if (currentStates[1] && !g_lastState[1]) { + diff -= g_config.movementStep; + } + // Left pressed (rising edge) + if (currentStates[2] && !g_lastState[2]) { + diff -= g_config.movementStep; + } + // Down pressed (rising edge) + if (currentStates[3] && !g_lastState[3]) { + diff += g_config.movementStep; + } + + // 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/Devices/lilygo-tdeck/Source/Trackball/Trackball.h b/Devices/lilygo-tdeck/Source/Trackball/Trackball.h new file mode 100644 index 000000000..acfe9c29f --- /dev/null +++ b/Devices/lilygo-tdeck/Source/Trackball/Trackball.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +namespace 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 Encoder steps 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/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp new file mode 100644 index 000000000..a91377798 --- /dev/null +++ b/Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp @@ -0,0 +1,37 @@ +#include "KeyboardBacklight.h" +#include // Driver +#include + +// TODO: Add Mutex and consider refactoring into a class +bool KeyboardBacklightDevice::start() { + if (initialized) { + return true; + } + + // T-Deck uses I2C_NUM_0 for internal peripherals + initialized = keyboardbacklight::init(I2C_NUM_0); + return initialized; +} + +bool KeyboardBacklightDevice::stop() { + if (initialized) { + // Turn off backlight on shutdown + keyboardbacklight::setBrightness(0); + initialized = false; + } + return true; +} + +bool KeyboardBacklightDevice::setBrightness(uint8_t brightness) { + if (!initialized) { + return false; + } + return keyboardbacklight::setBrightness(brightness); +} + +uint8_t KeyboardBacklightDevice::getBrightness() const { + if (!initialized) { + return 0; + } + return 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..31cf0173d 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,25 @@ 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); + + // 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) { + 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..16b65f265 --- /dev/null +++ b/Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp @@ -0,0 +1,36 @@ +#include "TrackballDevice.h" +#include // Driver + +bool TrackballDevice::start() { + if (initialized) { + return true; + } + + // T-Deck trackball GPIO configuration from LilyGo reference + 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 = 1 // pixels per movement + }; + + indev = trackball::init(config); + if (indev != nullptr) { + initialized = true; + return true; + } + + return false; +} + +bool TrackballDevice::stop() { + if (initialized) { + // LVGL will handle indev cleanup + trackball::deinit(); + 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..76e9d88a5 --- /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::Other; } + 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/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/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 ffc33e283..ee22228cf 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..a050765d0 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -51,6 +51,10 @@ 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; } +#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 @@ -83,6 +87,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; } @@ -124,6 +131,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); @@ -227,6 +237,10 @@ static void registerAndStartSecondaryServices() { addService(service::loader::manifest); 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 7479f11eb..3dbcd5b5e 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,60 @@ 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 + + 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); + } + } } 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..320ea6669 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 = 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 + 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,62 @@ 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(); + + 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; + } + + 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); + + 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()); + lock->unlock(); + break; + } + + 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(); + } + 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..075d18420 --- /dev/null +++ b/Tactility/Source/app/keyboard/KeyboardSettings.cpp @@ -0,0 +1,221 @@ +#ifdef ESP_PLATFORM + +#include + +#include +#include +#include + +#include + +// Forward declare driver functions +namespace keyboardbacklight { + bool setBrightness(uint8_t brightness); +} + +namespace trackball { + void setEnabled(bool enabled); +} + +namespace tt::app::keyboardsettings { + +constexpr auto* TAG = "KeyboardSettings"; + +static void applyKeyboardBacklight(bool enabled, uint8_t brightness) { + 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* switchTrackball = nullptr; + lv_obj_t* sliderBrightness = nullptr; + lv_obj_t* switchTimeoutEnable = nullptr; + lv_obj_t* timeoutDropdown = 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; + 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->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->kbSettings.backlightTimeoutMs = values_ms[idx]; + app->updated = true; + } + } + +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, "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) + 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 { + 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 +}; + +} + +#endif diff --git a/Tactility/Source/app/localesettings/LocaleSettings.cpp b/Tactility/Source/app/localesettings/LocaleSettings.cpp index e35775434..e31592374 100644 --- a/Tactility/Source/app/localesettings/LocaleSettings.cpp +++ b/Tactility/Source/app/localesettings/LocaleSettings.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -28,14 +29,9 @@ extern const AppManifest manifest; class LocaleSettingsApp final : public App { tt::i18n::TextResources textResources = tt::i18n::TextResources(TEXT_RESOURCE_PATH); RecursiveMutex mutex; - 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; @@ -68,9 +64,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())); @@ -86,6 +79,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 { @@ -108,42 +106,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()); @@ -151,18 +149,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/systeminfo/SystemInfo.cpp b/Tactility/Source/app/systeminfo/SystemInfo.cpp index e81e535bb..901fc11c0 100644 --- a/Tactility/Source/app/systeminfo/SystemInfo.cpp +++ b/Tactility/Source/app/systeminfo/SystemInfo.cpp @@ -1,16 +1,21 @@ #include #include +#include #include #include #include +#include +#include #include #include #include +#include #ifdef ESP_PLATFORM #include +#include #include #endif @@ -50,6 +55,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 +123,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 +143,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 +168,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 +204,47 @@ 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; - lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task)); + + // 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 addRtosTasks(lv_obj_t* parent) { +static void updateRtosTasks(lv_obj_t* parent, bool showCpuPercent) { + clearContainer(parent); + UBaseType_t count = uxTaskGetNumberOfTasks(); 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"); + 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 +270,311 @@ 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) { + auto lock = lvgl::getSyncLock()->asScopedLock(); + lock.lock(); + app->updateMemory(); + app->updatePsram(); + } + }); + + Timer tasksTimer = Timer(Timer::Type::Periodic, []() { + auto app = optApp(); + if (app) { + auto lock = lvgl::getSyncLock()->asScopedLock(); + lock.lock(); + 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* taskCountLabel = nullptr; // Shows active task count + lv_obj_t* uptimeLabel = nullptr; // Shows system uptime + + bool hasExternalMem = false; + bool hasDataStorage = false; + bool hasSdcardStorage = false; + bool hasSystemStorage = false; + + void updateMemory() { + 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 && taskCountLabel && uptimeLabel) { + UBaseType_t count = uxTaskGetNumberOfTasks(); + auto* 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(taskCountLabel, core_text.c_str()); + + // 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(uptimeLabel, uptime_text.c_str()); + } else { + lv_label_set_text(cpuSummaryLabel, "Overall CPU Usage: --.-%"); + lv_label_set_text(taskCountLabel, "Active Tasks: --"); + lv_label_set_text(uptimeLabel, "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 +586,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); + + taskCountLabel = lv_label_create(cpu_tab); + lv_label_set_text(taskCountLabel, "Active Tasks: --.-%"); + + 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); + 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 +680,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(); } }; @@ -300,4 +707,3 @@ extern const AppManifest manifest = { }; } // namespace - diff --git a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp index 3b3e13f45..bc521ebbe 100644 --- a/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp +++ b/Tactility/Source/app/timedatesettings/TimeDateSettings.cpp @@ -1,9 +1,12 @@ #include #include +#include #include #include +#include #include #include +#include #include @@ -16,6 +19,8 @@ extern const AppManifest manifest; class TimeDateSettingsApp final : public App { RecursiveMutex mutex; + 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); @@ -23,6 +28,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 { @@ -36,15 +59,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); @@ -54,6 +79,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(); + } + } + } } }; @@ -70,3 +163,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::function +#include +#include +#include +#include +#include +#include + +namespace tt::service::displayidle { + +constexpr auto* TAG = "DisplayIdle"; + +class DisplayIdleService final : public Service { + + std::unique_ptr timer; + bool displayDimmed = false; + settings::display::DisplaySettings cachedDisplaySettings; + + static std::shared_ptr getDisplay() { + return hal::findFirstDevice(hal::Device::Type::Display); + } + + 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; + } + } + } + } + +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(); + + // Note: Settings changes require service restart to take effect + // TODO: Add DisplaySettingsChanged 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; + } + } +}; + +extern const ServiceManifest manifest = { + .id = "DisplayIdle", + .createService = create +}; + +} diff --git a/Tactility/Source/service/espnow/EspNowWifi.cpp b/Tactility/Source/service/espnow/EspNowWifi.cpp index 46f6ba128..ea9414262 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,23 +97,19 @@ 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"); - } - - if (esp_wifi_deinit() != ESP_OK) { - TT_LOG_E(TAG, "Failed to deinit"); - } + // 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. + TT_LOG_I(TAG, "ESP-NOW WiFi deinitialized (WiFi service continues independently)"); return true; } 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 diff --git a/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp b/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp index 75d7897e3..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 = 1000U / portTICK_PERIOD_MS; +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; 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..2f7a26972 --- /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 = 60000 // 60 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 } }