Merge branch 'main' into develop

This commit is contained in:
Ken Van Hoeylandt 2026-01-02 19:56:54 +01:00
commit 2d867ac17a
36 changed files with 1939 additions and 115 deletions

View File

@ -1,2 +1,5 @@
language=en-US language=en-US
timeFormat24h=true timeFormat24h=true
dateFormat=MM/DD/YYYY
region=US
timezone=America/Los_Angeles

View File

@ -11,9 +11,9 @@ spiRamSpeed=120M
esptoolFlashFreq=120M esptoolFlashFreq=120M
[display] [display]
size=2.4" size=5"
shape=rectangle shape=rectangle
dpi=139 dpi=187
[lvgl] [lvgl]
colorDepth=16 colorDepth=16

View File

@ -1,7 +1,9 @@
#include "devices/Display.h" #include "devices/Display.h"
#include "devices/KeyboardBacklight.h"
#include "devices/Power.h" #include "devices/Power.h"
#include "devices/Sdcard.h" #include "devices/Sdcard.h"
#include "devices/TdeckKeyboard.h" #include "devices/TdeckKeyboard.h"
#include "devices/TrackballDevice.h"
#include <Tactility/hal/Configuration.h> #include <Tactility/hal/Configuration.h>
#include <Tactility/lvgl/LvglSync.h> #include <Tactility/lvgl/LvglSync.h>
@ -15,6 +17,8 @@ static std::vector<std::shared_ptr<Device>> createDevices() {
createPower(), createPower(),
createDisplay(), createDisplay(),
std::make_shared<TdeckKeyboard>(), std::make_shared<TdeckKeyboard>(),
std::make_shared<KeyboardBacklightDevice>(),
std::make_shared<TrackballDevice>(),
createSdCard() createSdCard()
}; };
} }

View File

@ -4,6 +4,12 @@
#include <Tactility/TactilityCore.h> #include <Tactility/TactilityCore.h>
#include <Tactility/hal/gps/GpsConfiguration.h> #include <Tactility/hal/gps/GpsConfiguration.h>
#include <Tactility/settings/KeyboardSettings.h>
#include "devices/KeyboardBacklight.h"
#include "devices/TrackballDevice.h"
#include <KeyboardBacklight/KeyboardBacklight.h>
#include <Trackball/Trackball.h>
#define TAG "tdeck" #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<KeyboardBacklightDevice>(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<TrackballDevice>(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; return true;
} }

View File

@ -0,0 +1,109 @@
#include "KeyboardBacklight.h"
#include <esp_log.h>
#include <cstring>
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;
}
}

View File

@ -0,0 +1,36 @@
#pragma once
#include <driver/i2c.h>
#include <cstdint>
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();
}

View File

@ -0,0 +1,145 @@
#include "Trackball.h"
#include <esp_log.h>
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");
}
}

View File

@ -0,0 +1,44 @@
#pragma once
#include <driver/gpio.h>
#include <lvgl.h>
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);
}

View File

@ -0,0 +1,37 @@
#include "KeyboardBacklight.h"
#include <KeyboardBacklight/KeyboardBacklight.h> // Driver
#include <Tactility/hal/i2c/I2c.h>
// 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();
}

View File

@ -0,0 +1,32 @@
#pragma once
#include <Tactility/hal/Device.h>
#include <Tactility/TactilityCore.h>
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;
};

View File

@ -1,6 +1,14 @@
#include "TdeckKeyboard.h" #include "TdeckKeyboard.h"
#include <Tactility/hal/i2c/I2c.h> #include <Tactility/hal/i2c/I2c.h>
#include <driver/i2c.h> #include <driver/i2c.h>
#include <lvgl.h>
#include <Tactility/settings/KeyboardSettings.h>
#include <Tactility/settings/DisplaySettings.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/Device.h>
#include <KeyboardBacklight/KeyboardBacklight.h>
using tt::hal::findFirstDevice;
constexpr auto* TAG = "TdeckKeyboard"; constexpr auto* TAG = "TdeckKeyboard";
constexpr auto TDECK_KEYBOARD_I2C_BUS_HANDLE = I2C_NUM_0; 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); TT_LOG_D(TAG, "Pressed %d", read_buffer);
data->key = read_buffer; data->key = read_buffer;
data->state = LV_INDEV_STATE_PRESSED; 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::display::DisplayDevice>(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);
}
} }
} }

View File

@ -0,0 +1,36 @@
#include "TrackballDevice.h"
#include <Trackball/Trackball.h> // 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;
}

View File

@ -0,0 +1,21 @@
#pragma once
#include <Tactility/hal/Device.h>
#include <lvgl.h>
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;
};

View File

@ -14,7 +14,7 @@ esptoolFlashFreq=80M
[display] [display]
size=3.5" size=3.5"
shape=rectangle shape=rectangle
dpi=139 dpi=165
[lvgl] [lvgl]
colorDepth=16 colorDepth=16

View File

@ -21,7 +21,8 @@ public:
Keyboard, Keyboard,
Encoder, Encoder,
Power, Power,
Gps Gps,
Other
}; };
typedef uint32_t Id; typedef uint32_t Id;

View File

@ -16,6 +16,8 @@ struct DisplaySettings {
Orientation orientation; Orientation orientation;
uint8_t gammaCurve; uint8_t gammaCurve;
uint8_t backlightDuty; uint8_t backlightDuty;
bool backlightTimeoutEnabled;
uint32_t backlightTimeoutMs; // 0 = Never
}; };
/** Compares default settings with the function parameter to return the difference */ /** Compares default settings with the function parameter to return the difference */

View File

@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
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);
}

View File

@ -7,6 +7,8 @@ namespace tt::settings {
struct SystemSettings { struct SystemSettings {
Language language; Language language;
bool timeFormat24h; 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); bool loadSystemSettings(SystemSettings& properties);

View File

@ -15,7 +15,9 @@ public:
enum PendingAction { enum PendingAction {
ActionNone, ActionNone,
ActionDelete, ActionDelete,
ActionRename ActionRename,
ActionCreateFile,
ActionCreateFolder
}; };
private: private:

View File

@ -15,6 +15,8 @@ class View final {
lv_obj_t* dir_entry_list = nullptr; lv_obj_t* dir_entry_list = nullptr;
lv_obj_t* action_list = nullptr; lv_obj_t* action_list = nullptr;
lv_obj_t* navigate_up_button = 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 }; std::string installAppPath = { 0 };
LaunchId installAppLaunchId = 0; LaunchId installAppLaunchId = 0;
@ -38,6 +40,8 @@ public:
void onDirEntryLongPressed(int32_t index); void onDirEntryLongPressed(int32_t index);
void onRenamePressed(); void onRenamePressed();
void onDeletePressed(); void onDeletePressed();
void onNewFilePressed();
void onNewFolderPressed();
void onDirEntryListScrollBegin(); void onDirEntryListScrollBegin();
void onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle); void onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle);
}; };

View File

@ -51,6 +51,10 @@ namespace service {
namespace loader { extern const ServiceManifest manifest; } namespace loader { extern const ServiceManifest manifest; }
namespace memorychecker { extern const ServiceManifest manifest; } namespace memorychecker { extern const ServiceManifest manifest; }
namespace statusbar { 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 #if TT_FEATURE_SCREENSHOT_ENABLED
namespace screenshot { extern const ServiceManifest manifest; } namespace screenshot { extern const ServiceManifest manifest; }
#endif #endif
@ -83,6 +87,9 @@ namespace app {
namespace imageviewer { extern const AppManifest manifest; } namespace imageviewer { extern const AppManifest manifest; }
namespace inputdialog { extern const AppManifest manifest; } namespace inputdialog { extern const AppManifest manifest; }
namespace launcher { 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 localesettings { extern const AppManifest manifest; }
namespace notes { extern const AppManifest manifest; } namespace notes { extern const AppManifest manifest; }
namespace power { extern const AppManifest manifest; } namespace power { extern const AppManifest manifest; }
@ -124,6 +131,9 @@ static void registerInternalApps() {
addAppManifest(app::imageviewer::manifest); addAppManifest(app::imageviewer::manifest);
addAppManifest(app::inputdialog::manifest); addAppManifest(app::inputdialog::manifest);
addAppManifest(app::launcher::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::localesettings::manifest);
addAppManifest(app::notes::manifest); addAppManifest(app::notes::manifest);
addAppManifest(app::settings::manifest); addAppManifest(app::settings::manifest);
@ -227,6 +237,10 @@ static void registerAndStartSecondaryServices() {
addService(service::loader::manifest); addService(service::loader::manifest);
addService(service::gui::manifest); addService(service::gui::manifest);
addService(service::statusbar::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); addService(service::memorychecker::manifest);
#if TT_FEATURE_SCREENSHOT_ENABLED #if TT_FEATURE_SCREENSHOT_ENABLED
addService(service::screenshot::manifest); addService(service::screenshot::manifest);

View File

@ -19,6 +19,8 @@ class DisplayApp final : public App {
settings::display::DisplaySettings displaySettings; settings::display::DisplaySettings displaySettings;
bool displaySettingsUpdated = false; bool displaySettingsUpdated = false;
lv_obj_t* timeoutSwitch = nullptr;
lv_obj_t* timeoutDropdown = nullptr;
static void onBacklightSliderEvent(lv_event_t* event) { static void onBacklightSliderEvent(lv_event_t* event) {
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event)); auto* slider = static_cast<lv_obj_t*>(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<DisplayApp*>(lv_event_get_user_data(event));
auto* sw = static_cast<lv_obj_t*>(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<DisplayApp*>(lv_event_get_user_data(event));
auto* dropdown = static_cast<lv_obj_t*>(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: public:
void onShow(AppContext& app, lv_obj_t* parent) override { void onShow(AppContext& app, lv_obj_t* parent) override {
@ -150,6 +179,60 @@ public:
lv_obj_add_event_cb(orientation_dropdown, onOrientationSet, LV_EVENT_VALUE_CHANGED, this); lv_obj_add_event_cb(orientation_dropdown, onOrientationSet, LV_EVENT_VALUE_CHANGED, this);
// Set the dropdown to match current orientation enum // Set the dropdown to match current orientation enum
lv_dropdown_set_selected(orientation_dropdown, static_cast<uint16_t>(displaySettings.orientation)); lv_dropdown_set_selected(orientation_dropdown, static_cast<uint16_t>(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 { void onHide(TT_UNUSED AppContext& app) override {

View File

@ -16,6 +16,7 @@
#include <Tactility/Tactility.h> #include <Tactility/Tactility.h>
#include <cstring> #include <cstring>
#include <cstdio>
#include <unistd.h> #include <unistd.h>
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
@ -62,6 +63,16 @@ static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) {
view->onNavigateUpPressed(); view->onNavigateUpPressed();
} }
static void onNewFilePressedCallback(lv_event_t* event) {
auto* view = static_cast<View*>(lv_event_get_user_data(event));
view->onNewFilePressed();
}
static void onNewFolderPressedCallback(lv_event_t* event) {
auto* view = static_cast<View*>(lv_event_get_user_data(event));
view->onNewFolderPressed();
}
// endregion // endregion
void View::viewFile(const std::string& path, const std::string& filename) { void View::viewFile(const std::string& path, const std::string& filename) {
@ -179,7 +190,38 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
} else { } else {
symbol = LV_SYMBOL_FILE; 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, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this);
lv_obj_add_event_cb(button, &onDirEntryLongPressedCallback, LV_EVENT_LONG_PRESSED, this); lv_obj_add_event_cb(button, &onDirEntryLongPressedCallback, LV_EVENT_LONG_PRESSED, this);
} }
@ -212,6 +254,18 @@ void View::onDeletePressed() {
alertdialog::start("Are you sure?", message, choices); 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() { void View::showActionsForDirectory() {
lv_obj_clean(action_list); lv_obj_clean(action_list);
@ -262,6 +316,8 @@ void View::init(const AppContext& appContext, lv_obj_t* parent) {
auto* toolbar = lvgl::toolbar_create(parent, appContext); auto* toolbar = lvgl::toolbar_create(parent, appContext);
navigate_up_button = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this); 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); auto* wrapper = lv_obj_create(parent);
lv_obj_set_width(wrapper, LV_PCT(100)); lv_obj_set_width(wrapper, LV_PCT(100));
@ -354,6 +410,62 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bu
} }
break; 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: default:
break; break;
} }

View File

@ -0,0 +1,221 @@
#ifdef ESP_PLATFORM
#include <Tactility/Tactility.h>
#include <Tactility/settings/KeyboardSettings.h>
#include <Tactility/Assets.h>
#include <Tactility/lvgl/Toolbar.h>
#include <lvgl.h>
// 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<KeyboardSettingsApp*>(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<KeyboardSettingsApp*>(lv_event_get_user_data(e));
int32_t v = lv_slider_get_value(app->sliderBrightness);
app->kbSettings.backlightBrightness = static_cast<uint8_t>(v);
app->updated = true;
if (app->kbSettings.backlightEnabled) {
applyKeyboardBacklight(true, app->kbSettings.backlightBrightness);
}
}
static void onTrackballSwitch(lv_event_t* e) {
auto* app = static_cast<KeyboardSettingsApp*>(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<KeyboardSettingsApp*>(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<KeyboardSettingsApp*>(lv_event_get_user_data(event));
auto* dropdown = static_cast<lv_obj_t*>(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<KeyboardSettingsApp>
};
}
#endif

View File

@ -8,6 +8,7 @@
#include <Tactility/settings/Time.h> #include <Tactility/settings/Time.h>
#include <Tactility/StringUtils.h> #include <Tactility/StringUtils.h>
#include <Tactility/settings/Language.h> #include <Tactility/settings/Language.h>
#include <Tactility/settings/SystemSettings.h>
#include <lvgl.h> #include <lvgl.h>
#include <map> #include <map>
@ -28,14 +29,9 @@ extern const AppManifest manifest;
class LocaleSettingsApp final : public App { class LocaleSettingsApp final : public App {
tt::i18n::TextResources textResources = tt::i18n::TextResources(TEXT_RESOURCE_PATH); tt::i18n::TextResources textResources = tt::i18n::TextResources(TEXT_RESOURCE_PATH);
RecursiveMutex mutex; RecursiveMutex mutex;
lv_obj_t* timeZoneLabel = nullptr; lv_obj_t* regionTextArea = nullptr;
lv_obj_t* regionLabel = nullptr;
lv_obj_t* languageDropdown = nullptr; lv_obj_t* languageDropdown = nullptr;
lv_obj_t* languageLabel = nullptr; bool settingsUpdated = false;
static void onConfigureTimeZonePressed(TT_UNUSED lv_event_t* event) {
timezone::start();
}
std::map<settings::Language, std::string> languageMap; std::map<settings::Language, std::string> languageMap;
@ -68,9 +64,6 @@ class LocaleSettingsApp final : public App {
void updateViews() { void updateViews() {
textResources.load(); 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(); std::string language_options = getLanguageOptions();
lv_dropdown_set_options(languageDropdown, language_options.c_str()); lv_dropdown_set_options(languageDropdown, language_options.c_str());
lv_dropdown_set_selected(languageDropdown, static_cast<uint32_t>(settings::getLanguage())); lv_dropdown_set_selected(languageDropdown, static_cast<uint32_t>(settings::getLanguage()));
@ -86,6 +79,11 @@ class LocaleSettingsApp final : public App {
self->updateViews(); self->updateViews();
} }
static void onRegionChanged(lv_event_t* event) {
auto* self = static_cast<LocaleSettingsApp*>(lv_event_get_user_data(event));
self->settingsUpdated = true;
}
public: public:
void onShow(AppContext& app, lv_obj_t* parent) override { void onShow(AppContext& app, lv_obj_t* parent) override {
@ -108,42 +106,42 @@ public:
auto* region_wrapper = lv_obj_create(main_wrapper); auto* region_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_width(region_wrapper, LV_PCT(100)); lv_obj_set_width(region_wrapper, LV_PCT(100));
lv_obj_set_height(region_wrapper, LV_SIZE_CONTENT); 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); lv_obj_set_style_border_width(region_wrapper, 0, 0);
regionLabel = lv_label_create(region_wrapper); auto* region_label = lv_label_create(region_wrapper);
lv_label_set_text(regionLabel , textResources[i18n::Text::REGION].c_str()); lv_label_set_text(region_label, textResources[i18n::Text::REGION].c_str());
lv_obj_align(regionLabel , LV_ALIGN_LEFT_MID, 0, 0); lv_obj_align(region_label, LV_ALIGN_LEFT_MID, 4, 0);
auto* region_button = lv_button_create(region_wrapper); // Region text area for user input (e.g., US, EU, JP)
lv_obj_align(region_button, LV_ALIGN_RIGHT_MID, 0, 0); regionTextArea = lv_textarea_create(region_wrapper);
auto* region_button_image = lv_image_create(region_button); lv_obj_set_width(regionTextArea, 120);
lv_obj_add_event_cb(region_button, onConfigureTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr); lv_textarea_set_one_line(regionTextArea, true);
lv_image_set_src(region_button_image, LV_SYMBOL_SETTINGS); lv_textarea_set_max_length(regionTextArea, 50);
lv_textarea_set_placeholder_text(regionTextArea, "e.g. US, EU");
timeZoneLabel = lv_label_create(region_wrapper); // Load current region from settings
std::string timeZoneName = settings::getTimeZoneName(); settings::SystemSettings sysSettings;
if (timeZoneName.empty()) { if (settings::loadSystemSettings(sysSettings)) {
timeZoneName = "not set"; lv_textarea_set_text(regionTextArea, sysSettings.region.c_str());
} }
lv_obj_add_event_cb(regionTextArea, onRegionChanged, LV_EVENT_VALUE_CHANGED, this);
lv_label_set_text(timeZoneLabel, timeZoneName.c_str()); lv_obj_align(regionTextArea, LV_ALIGN_RIGHT_MID, 0, 0);
const int offset = ui_scale == hal::UiScale::Smallest ? -2 : -10;
lv_obj_align_to(timeZoneLabel, region_button, LV_ALIGN_OUT_LEFT_MID, offset, 0);
// Language // Language
auto* language_wrapper = lv_obj_create(main_wrapper); auto* language_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_width(language_wrapper, LV_PCT(100)); lv_obj_set_width(language_wrapper, LV_PCT(100));
lv_obj_set_height(language_wrapper, LV_SIZE_CONTENT); 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); 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_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); languageDropdown = lv_dropdown_create(language_wrapper);
lv_obj_set_width(languageDropdown, 150);
lv_obj_align(languageDropdown, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_align(languageDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
std::string language_options = getLanguageOptions(); std::string language_options = getLanguageOptions();
lv_dropdown_set_options(languageDropdown, language_options.c_str()); lv_dropdown_set_options(languageDropdown, language_options.c_str());
@ -151,18 +149,12 @@ public:
lv_obj_add_event_cb(languageDropdown, onLanguageSet, LV_EVENT_VALUE_CHANGED, this); 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> bundle) override { void onHide(TT_UNUSED AppContext& app) override {
if (result == Result::Ok && bundle != nullptr) { if (settingsUpdated && regionTextArea) {
const auto name = timezone::getResultName(*bundle); settings::SystemSettings sysSettings;
const auto code = timezone::getResultCode(*bundle); if (settings::loadSystemSettings(sysSettings)) {
TT_LOG_I(TAG, "Result name=%s code=%s", name.c_str(), code.c_str()); sysSettings.region = lv_textarea_get_text(regionTextArea);
settings::setTimeZone(name, code); settings::saveSystemSettings(sysSettings);
if (!name.empty()) {
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
lv_label_set_text(timeZoneLabel, name.c_str());
lvgl::unlock();
}
} }
} }
} }

View File

@ -1,16 +1,21 @@
#include <Tactility/TactilityConfig.h> #include <Tactility/TactilityConfig.h>
#include <Tactility/lvgl/Toolbar.h> #include <Tactility/lvgl/Toolbar.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/Assets.h> #include <Tactility/Assets.h>
#include <Tactility/hal/Device.h> #include <Tactility/hal/Device.h>
#include <Tactility/Tactility.h> #include <Tactility/Tactility.h>
#include <Tactility/Timer.h>
#include <algorithm>
#include <format> #include <format>
#include <lvgl.h> #include <lvgl.h>
#include <utility> #include <utility>
#include <cstring>
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
#include <esp_vfs_fat.h> #include <esp_vfs_fat.h>
#include <esp_heap_caps.h>
#include <Tactility/MountPoints.h> #include <Tactility/MountPoints.h>
#endif #endif
@ -50,6 +55,22 @@ static size_t getSpiTotal() {
#endif #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 { enum class StorageUnit {
Bytes, Bytes,
Kilobytes, 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) { struct MemoryBarWidgets {
uint64_t used = total - free; 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); auto* container = lv_obj_create(parent);
lv_obj_set_size(container, LV_PCT(100), LV_SIZE_CONTENT); lv_obj_set_size(container, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(container, 0, LV_STATE_DEFAULT); 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); auto* bar = lv_bar_create(container);
lv_obj_set_flex_grow(bar, 1); 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 // Scale down the uint64_t until it fits int32_t for the lv_bar
uint64_t free_scaled = free; uint64_t free_scaled = free;
uint64_t total_scaled = total; 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) { if (total > 0) {
lv_bar_set_range(bar, 0, total_scaled); lv_bar_set_range(widgets.bar, 0, total_scaled);
} else { } 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 = getStorageUnit(total);
const auto unit_label = getStorageUnitString(unit); 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); 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_label_set_text_fmt(widgets.label, "%s / %s %s free (%llu / %llu bytes)",
lv_obj_set_width(bottom_label, LV_PCT(100)); free_converted.c_str(), total_converted.c_str(), unit_label.c_str(),
lv_obj_set_style_text_align(bottom_label, LV_TEXT_ALIGN_RIGHT, 0); (unsigned long long)free, (unsigned long long)total);
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);
}
} }
#if configUSE_TRACE_FACILITY #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) {
auto* label = lv_label_create(parent); lv_obj_clean(container);
const char* name = (task.pcTaskName == nullptr || task.pcTaskName[0] == 0) ? "(unnamed)" : task.pcTaskName;
lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task));
} }
static void addRtosTasks(lv_obj_t* parent) { static void addRtosTask(lv_obj_t* parent, const TaskStatus_t& task, uint32_t totalRuntime) {
auto* label = lv_label_create(parent);
const char* name = (task.pcTaskName == nullptr || task.pcTaskName[0] == 0) ? "(unnamed)" : task.pcTaskName;
// If totalRuntime provided, show CPU percentage; otherwise just show state
if (totalRuntime > 0) {
float cpu_percent = (task.ulRunTimeCounter * 100.0f) / totalRuntime;
lv_label_set_text_fmt(label, "%s: %.1f%%", name, cpu_percent);
} else {
lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task));
}
}
static void updateRtosTasks(lv_obj_t* parent, bool showCpuPercent) {
clearContainer(parent);
UBaseType_t count = uxTaskGetNumberOfTasks(); UBaseType_t count = uxTaskGetNumberOfTasks();
auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count); auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count);
if (!tasks) {
auto* error_label = lv_label_create(parent);
lv_label_set_text(error_label, "Failed to allocate memory for task list");
return;
}
uint32_t totalRuntime = 0; uint32_t totalRuntime = 0;
UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime); UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime);
for (int i = 0; i < actual; ++i) { // Sort by CPU usage if showing percentages, otherwise keep original order
const TaskStatus_t& task = tasks[i]; if (showCpuPercent) {
addRtosTask(parent, task); 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) {
addRtosTask(parent, tasks[i], showCpuPercent ? totalRuntime : 0);
}
free(tasks); free(tasks);
} }
@ -211,14 +270,311 @@ static lv_obj_t* createTab(lv_obj_t* tabview, const char* name) {
return tab; return tab;
} }
extern const AppManifest manifest;
class SystemInfoApp;
static std::shared_ptr<SystemInfoApp> _Nullable optApp() {
auto appContext = getCurrentAppContext();
if (appContext != nullptr && appContext->getManifest().appId == manifest.appId) {
return std::static_pointer_cast<SystemInfoApp>(appContext->getApp());
}
return nullptr;
}
class SystemInfoApp final : public App { class SystemInfoApp final : public App {
Timer memoryTimer = Timer(Timer::Type::Periodic, kernel::millisToTicks(10000), [] {
auto app = optApp();
if (app) {
auto lock = lvgl::getSyncLock()->asScopedLock();
lock.lock();
app->updateMemory();
app->updatePsram();
}
});
Timer tasksTimer = Timer(Timer::Type::Periodic, kernel::millisToTicks(15000), [] {
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::sdcard::SdCardDevice>(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<float>(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 { void onShow(AppContext& app, lv_obj_t* parent) override {
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT); lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
lvgl::toolbar_create(parent, app); lvgl::toolbar_create(parent, app);
// This wrapper automatically has its children added vertically underneath eachother
auto* wrapper = lv_obj_create(parent); auto* wrapper = lv_obj_create(parent);
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT); lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN); 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_position(tabview, LV_DIR_LEFT);
lv_tabview_set_tab_bar_size(tabview, 80); lv_tabview_set_tab_bar_size(tabview, 80);
// Tabs // Create tabs
auto* memory_tab = createTab(tabview, "Memory"); 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* storage_tab = createTab(tabview, "Storage");
auto* tasks_tab = createTab(tabview, "Tasks"); auto* tasks_tab = createTab(tabview, "Tasks");
auto* devices_tab = createTab(tabview, "Devices"); auto* devices_tab = createTab(tabview, "Devices");
auto* about_tab = createTab(tabview, "About"); auto* about_tab = createTab(tabview, "About");
// Memory tab content // Memory tab content
internalMemBar = createMemoryBar(memory_tab, "Internal");
addMemoryBar(memory_tab, "Internal", getHeapFree(), getHeapTotal()); hasExternalMem = getSpiTotal() > 0;
if (getSpiTotal() > 0) { if (hasExternalMem) {
addMemoryBar(memory_tab, "External", getSpiFree(), getSpiTotal()); externalMemBar = createMemoryBar(memory_tab, "External");
}
// 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 #ifdef ESP_PLATFORM
// Wrapper for the memory usage bars // Storage tab content
uint64_t storage_total = 0; uint64_t storage_total = 0;
uint64_t storage_free = 0; uint64_t storage_free = 0;
hasDataStorage = (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK);
if (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK) { if (hasDataStorage) {
addMemoryBar(storage_tab, file::MOUNT_POINT_DATA, storage_free, storage_total); dataStorageBar = createMemoryBar(storage_tab, file::MOUNT_POINT_DATA);
} }
const auto sdcard_devices = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard); const auto sdcard_devices = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
for (const auto& sdcard : sdcard_devices) { for (const auto& sdcard : sdcard_devices) {
if (sdcard->isMounted() && esp_vfs_fat_info(sdcard->getMountPath().c_str(), &storage_total, &storage_free) == ESP_OK) { if (sdcard->isMounted() && esp_vfs_fat_info(sdcard->getMountPath().c_str(), &storage_total, &storage_free) == ESP_OK) {
addMemoryBar( hasSdcardStorage = true;
storage_tab, sdcardStorageBar = createMemoryBar(storage_tab, sdcard->getMountPath().c_str());
sdcard->getMountPath().c_str(), break; // Only show first SD card
storage_free,
storage_total
);
} }
} }
if (config::SHOW_SYSTEM_PARTITION) { if (config::SHOW_SYSTEM_PARTITION) {
if (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK) { hasSystemStorage = (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); if (hasSystemStorage) {
systemStorageBar = createMemoryBar(storage_tab, file::MOUNT_POINT_SYSTEM);
} }
} }
#endif #endif
#if configUSE_TRACE_FACILITY #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 #endif
addDevices(devices_tab); addDevices(devices_tab);
@ -288,6 +680,21 @@ class SystemInfoApp final : public App {
auto* esp_idf_version = lv_label_create(about_tab); 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); 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 #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(); // Memory & PSRAM: every 10s
tasksTimer.start(); // Tasks/CPU: every 15s
}
void onHide(TT_UNUSED AppContext& app) override {
memoryTimer.stop();
tasksTimer.stop();
} }
}; };
@ -300,4 +707,3 @@ extern const AppManifest manifest = {
}; };
} // namespace } // namespace

View File

@ -1,9 +1,12 @@
#include <Tactility/Assets.h> #include <Tactility/Assets.h>
#include <Tactility/app/AppManifest.h> #include <Tactility/app/AppManifest.h>
#include <Tactility/app/timezone/TimeZone.h>
#include <Tactility/lvgl/Toolbar.h> #include <Tactility/lvgl/Toolbar.h>
#include <Tactility/RecursiveMutex.h> #include <Tactility/RecursiveMutex.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/loader/Loader.h> #include <Tactility/service/loader/Loader.h>
#include <Tactility/settings/Time.h> #include <Tactility/settings/Time.h>
#include <Tactility/settings/SystemSettings.h>
#include <lvgl.h> #include <lvgl.h>
@ -16,6 +19,8 @@ extern const AppManifest manifest;
class TimeDateSettingsApp final : public App { class TimeDateSettingsApp final : public App {
RecursiveMutex mutex; RecursiveMutex mutex;
lv_obj_t* timeZoneLabel = nullptr;
lv_obj_t* dateFormatDropdown = nullptr;
static void onTimeFormatChanged(lv_event_t* event) { static void onTimeFormatChanged(lv_event_t* event) {
auto* widget = lv_event_get_target_obj(event); auto* widget = lv_event_get_target_obj(event);
@ -23,6 +28,24 @@ class TimeDateSettingsApp final : public App {
settings::setTimeFormat24Hour(show_24); 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_obj_t*>(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: public:
void onShow(AppContext& app, lv_obj_t* parent) override { void onShow(AppContext& app, lv_obj_t* parent) override {
@ -36,15 +59,17 @@ public:
lv_obj_set_width(main_wrapper, LV_PCT(100)); lv_obj_set_width(main_wrapper, LV_PCT(100));
lv_obj_set_flex_grow(main_wrapper, 1); lv_obj_set_flex_grow(main_wrapper, 1);
// 24-hour format toggle
auto* time_format_wrapper = lv_obj_create(main_wrapper); auto* time_format_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_width(time_format_wrapper, LV_PCT(100)); lv_obj_set_width(time_format_wrapper, LV_PCT(100));
lv_obj_set_height(time_format_wrapper, LV_SIZE_CONTENT); 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); lv_obj_set_style_border_width(time_format_wrapper, 0, 0);
auto* time_24h_label = lv_label_create(time_format_wrapper); auto* time_24h_label = lv_label_create(time_format_wrapper);
lv_label_set_text(time_24h_label, "24-hour clock"); lv_label_set_text(time_24h_label, "24-hour format");
lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 0, 0); lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 4, 0);
auto* time_24h_switch = lv_switch_create(time_format_wrapper); auto* time_24h_switch = lv_switch_create(time_format_wrapper);
lv_obj_align(time_24h_switch, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_align(time_24h_switch, LV_ALIGN_RIGHT_MID, 0, 0);
@ -54,6 +79,74 @@ public:
} else { } else {
lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED); 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> 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 } // namespace

View File

@ -19,13 +19,17 @@ bool getKeyValuePair(const std::string& input, std::string& key, std::string& va
} }
bool loadPropertiesFile(const std::string& filePath, std::function<void(const std::string& key, const std::string& value)> callback) { bool loadPropertiesFile(const std::string& filePath, std::function<void(const std::string& key, const std::string& value)> 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; uint16_t line_count = 0;
std::string key_prefix = ""; 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) { return readLines(filePath, true, [&key_prefix, &line_count, &filePath, &callback](const std::string& line) {
line_count++; line_count++;
std::string key, value; 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("#") && !trimmed_line.empty()) {
if (trimmed_line.starts_with("[")) { if (trimmed_line.starts_with("[")) {
key_prefix = trimmed_line; key_prefix = trimmed_line;
@ -35,7 +39,8 @@ bool loadPropertiesFile(const std::string& filePath, std::function<void(const st
std::string trimmed_value = string::trim(value, " \t"); std::string trimmed_value = string::trim(value, " \t");
callback(trimmed_key, trimmed_value); callback(trimmed_key, trimmed_value);
} else { } else {
TT_LOG_E(TAG, "Failed to parse line %d of %s", line_count, filePath.c_str()); TT_LOG_E(TAG, "Failed to parse line %d of %s (skipped)", line_count, filePath.c_str());
// Continue loading other lines
} }
} }
} }

View File

@ -0,0 +1,90 @@
#include <Tactility/CoreDefines.h>
#include <Tactility/Timer.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/settings/DisplaySettings.h>
namespace tt::service::displayidle {
constexpr auto* TAG = "DisplayIdle";
class DisplayIdleService final : public Service {
std::unique_ptr<Timer> timer;
bool displayDimmed = false;
settings::display::DisplaySettings cachedDisplaySettings;
static std::shared_ptr<hal::display::DisplayDevice> getDisplay() {
return hal::findFirstDevice<hal::display::DisplayDevice>(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>(Timer::Type::Periodic, kernel::millisToTicks(250), [this]{ this->tick(); });
timer->setCallbackPriority(Thread::Priority::Lower);
timer->start();
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<DisplayIdleService>
};
}

View File

@ -35,18 +35,29 @@ static bool disableWifiService() {
} }
bool initWifi(const EspNowConfig& config) { 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()) { if (!disableWifiService()) {
TT_LOG_E(TAG, "Failed to disable wifi"); TT_LOG_E(TAG, "Failed to disable wifi");
return false; return false;
} }
}
wifi_mode_t mode; wifi_mode_t mode;
if (config.mode == Mode::Station) { if (config.mode == Mode::Station) {
// Use STA mode to allow coexistence with normal WiFi connection
mode = wifi_mode_t::WIFI_MODE_STA; mode = wifi_mode_t::WIFI_MODE_STA;
} else { } else {
mode = wifi_mode_t::WIFI_MODE_AP; 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(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
if (esp_wifi_init(&cfg) != ESP_OK) { if (esp_wifi_init(&cfg) != ESP_OK) {
TT_LOG_E(TAG, "esp_wifi_init() failed"); TT_LOG_E(TAG, "esp_wifi_init() failed");
@ -67,6 +78,7 @@ bool initWifi(const EspNowConfig& config) {
TT_LOG_E(TAG, "esp_wifi_start() failed"); TT_LOG_E(TAG, "esp_wifi_start() failed");
return false; return false;
} }
}
if (esp_wifi_set_channel(config.channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) { if (esp_wifi_set_channel(config.channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
TT_LOG_E(TAG, "esp_wifi_set_channel() failed"); TT_LOG_E(TAG, "esp_wifi_set_channel() failed");
@ -86,23 +98,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; return true;
} }
bool deinitWifi() { bool deinitWifi() {
if (esp_wifi_stop() != ESP_OK) { // Don't deinitialize WiFi completely - just disable ESP-NOW
TT_LOG_E(TAG, "Failed to stop radio"); // This allows normal WiFi connection to continue
return false; // Only stop/deinit if WiFi was originally off
}
if (esp_wifi_set_mode(WIFI_MODE_NULL) != ESP_OK) { // Since we're only using WiFi for ESP-NOW, we can safely keep it in a minimal state
TT_LOG_E(TAG, "Failed to unset mode"); // or shut it down. For now, keep it running to support STA + ESP-NOW coexistence.
}
if (esp_wifi_deinit() != ESP_OK) {
TT_LOG_E(TAG, "Failed to deinit");
}
TT_LOG_I(TAG, "ESP-NOW WiFi deinitialized (WiFi service continues independently)");
return true; return true;
} }

View File

@ -0,0 +1,98 @@
#ifdef ESP_PLATFORM
#include <Tactility/CoreDefines.h>
#include <Tactility/hal/keyboard/KeyboardDevice.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/service/ServiceContext.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/settings/KeyboardSettings.h>
#include <Tactility/Timer.h>
namespace keyboardbacklight {
bool setBrightness(uint8_t brightness);
}
namespace tt::service::keyboardidle {
constexpr auto* TAG = "KeyboardIdle";
class KeyboardIdleService final : public Service {
std::unique_ptr<Timer> timer;
bool keyboardDimmed = false;
settings::keyboard::KeyboardSettings cachedKeyboardSettings;
static std::shared_ptr<hal::keyboard::KeyboardDevice> getKeyboard() {
return hal::findFirstDevice<hal::keyboard::KeyboardDevice>(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>(Timer::Type::Periodic, kernel::millisToTicks(250), [this]{ this->tick(); });
timer->setCallbackPriority(Thread::Priority::Lower);
timer->start();
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<KeyboardIdleService>
};
}
#endif

View File

@ -15,6 +15,8 @@ constexpr auto* SETTINGS_FILE = "/data/settings/display.properties";
constexpr auto* SETTINGS_KEY_ORIENTATION = "orientation"; constexpr auto* SETTINGS_KEY_ORIENTATION = "orientation";
constexpr auto* SETTINGS_KEY_GAMMA_CURVE = "gammaCurve"; constexpr auto* SETTINGS_KEY_GAMMA_CURVE = "gammaCurve";
constexpr auto* SETTINGS_KEY_BACKLIGHT_DUTY = "backlightDuty"; constexpr auto* SETTINGS_KEY_BACKLIGHT_DUTY = "backlightDuty";
constexpr auto* SETTINGS_KEY_TIMEOUT_ENABLED = "backlightTimeoutEnabled";
constexpr auto* SETTINGS_KEY_TIMEOUT_MS = "backlightTimeoutMs";
static Orientation getDefaultOrientation() { static Orientation getDefaultOrientation() {
auto* display = lv_display_get_default(); 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<uint32_t>(std::strtoul(timeout_ms_entry->second.c_str(), nullptr, 10));
}
settings.orientation = orientation; settings.orientation = orientation;
settings.gammaCurve = gamma_curve; settings.gammaCurve = gamma_curve;
settings.backlightDuty = backlight_duty; settings.backlightDuty = backlight_duty;
settings.backlightTimeoutEnabled = timeout_enabled;
settings.backlightTimeoutMs = timeout_ms;
return true; return true;
} }
@ -101,7 +117,9 @@ DisplaySettings getDefault() {
return DisplaySettings { return DisplaySettings {
.orientation = getDefaultOrientation(), .orientation = getDefaultOrientation(),
.gammaCurve = 1, .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_BACKLIGHT_DUTY] = std::to_string(settings.backlightDuty);
map[SETTINGS_KEY_GAMMA_CURVE] = std::to_string(settings.gammaCurve); map[SETTINGS_KEY_GAMMA_CURVE] = std::to_string(settings.gammaCurve);
map[SETTINGS_KEY_ORIENTATION] = toString(settings.orientation); 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); return file::savePropertiesFile(SETTINGS_FILE, map);
} }

View File

@ -0,0 +1,65 @@
#include <Tactility/settings/KeyboardSettings.h>
#include <Tactility/file/PropertiesFile.h>
#include <map>
#include <string>
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<std::string, std::string> 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<uint8_t>(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<uint32_t>(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<std::string, std::string> 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);
}
}

View File

@ -40,6 +40,25 @@ static bool loadSystemSettingsFromFile(SystemSettings& properties) {
bool time_format_24h = time_format_entry == map.end() ? true : (time_format_entry->second == "true"); bool time_format_24h = time_format_entry == map.end() ? true : (time_format_entry->second == "true");
properties.timeFormat24h = time_format_24h; 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"); TT_LOG_I(TAG, "System settings loaded");
return true; return true;
} }
@ -61,12 +80,15 @@ bool saveSystemSettings(const SystemSettings& properties) {
std::map<std::string, std::string> map; std::map<std::string, std::string> map;
map["language"] = toString(properties.language); map["language"] = toString(properties.language);
map["timeFormat24h"] = properties.timeFormat24h ? "true" : "false"; map["timeFormat24h"] = properties.timeFormat24h ? "true" : "false";
map["dateFormat"] = properties.dateFormat;
map["region"] = properties.region;
if (!file::savePropertiesFile(file_path, map)) { if (!file::savePropertiesFile(file_path, map)) {
TT_LOG_E(TAG, "Failed to save %s", file_path.c_str()); TT_LOG_E(TAG, "Failed to save %s", file_path.c_str());
return false; return false;
} }
// Update local cache
cachedSettings = properties; cachedSettings = properties;
cached = true; cached = true;
return true; return true;

View File

@ -45,7 +45,7 @@ std::string getTimeZoneName() {
if (preferences.optString(TIMEZONE_PREFERENCES_KEY_NAME, result)) { if (preferences.optString(TIMEZONE_PREFERENCES_KEY_NAME, result)) {
return result; return result;
} else { } 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)) { if (preferences.optString(TIMEZONE_PREFERENCES_KEY_CODE, result)) {
return result; return result;
} else { } else {
return {}; return "PST8PDT,M3.2.0,M11.1.0"; // Default: Pacific Time POSIX string
} }
} }

View File

@ -351,6 +351,7 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(lv_event_get_target_obj), ESP_ELFSYM_EXPORT(lv_event_get_target_obj),
ESP_ELFSYM_EXPORT(lv_event_get_target), ESP_ELFSYM_EXPORT(lv_event_get_target),
ESP_ELFSYM_EXPORT(lv_event_get_current_target_obj), ESP_ELFSYM_EXPORT(lv_event_get_current_target_obj),
ESP_ELFSYM_EXPORT(lv_event_get_draw_task),
// lv_obj // lv_obj
ESP_ELFSYM_EXPORT(lv_color_hex), ESP_ELFSYM_EXPORT(lv_color_hex),
ESP_ELFSYM_EXPORT(lv_color_make), ESP_ELFSYM_EXPORT(lv_color_make),
@ -448,6 +449,11 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(lv_obj_set_size), ESP_ELFSYM_EXPORT(lv_obj_set_size),
ESP_ELFSYM_EXPORT(lv_obj_set_width), ESP_ELFSYM_EXPORT(lv_obj_set_width),
ESP_ELFSYM_EXPORT(lv_obj_set_height), ESP_ELFSYM_EXPORT(lv_obj_set_height),
ESP_ELFSYM_EXPORT(lv_obj_send_event),
ESP_ELFSYM_EXPORT(lv_obj_set_style_outline_color),
ESP_ELFSYM_EXPORT(lv_obj_set_style_outline_width),
ESP_ELFSYM_EXPORT(lv_obj_set_style_outline_pad),
ESP_ELFSYM_EXPORT(lv_obj_set_style_outline_opa),
// lv_font // lv_font
ESP_ELFSYM_EXPORT(lv_font_get_default), ESP_ELFSYM_EXPORT(lv_font_get_default),
// lv_theme // lv_theme
@ -552,6 +558,7 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(lv_display_get_vertical_resolution), ESP_ELFSYM_EXPORT(lv_display_get_vertical_resolution),
ESP_ELFSYM_EXPORT(lv_display_get_physical_horizontal_resolution), ESP_ELFSYM_EXPORT(lv_display_get_physical_horizontal_resolution),
ESP_ELFSYM_EXPORT(lv_display_get_physical_vertical_resolution), ESP_ELFSYM_EXPORT(lv_display_get_physical_vertical_resolution),
ESP_ELFSYM_EXPORT(lv_display_dpx),
// lv_pct // lv_pct
ESP_ELFSYM_EXPORT(lv_pct), ESP_ELFSYM_EXPORT(lv_pct),
ESP_ELFSYM_EXPORT(lv_pct_to_px), ESP_ELFSYM_EXPORT(lv_pct_to_px),
@ -578,6 +585,7 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(lv_indev_get_key), ESP_ELFSYM_EXPORT(lv_indev_get_key),
ESP_ELFSYM_EXPORT(lv_indev_get_gesture_dir), ESP_ELFSYM_EXPORT(lv_indev_get_gesture_dir),
ESP_ELFSYM_EXPORT(lv_indev_get_state), ESP_ELFSYM_EXPORT(lv_indev_get_state),
ESP_ELFSYM_EXPORT(lv_indev_active),
// lv_timer // lv_timer
ESP_ELFSYM_EXPORT(lv_timer_handler), ESP_ELFSYM_EXPORT(lv_timer_handler),
ESP_ELFSYM_EXPORT(lv_timer_handler_run_in_period), ESP_ELFSYM_EXPORT(lv_timer_handler_run_in_period),
@ -606,6 +614,18 @@ const esp_elfsym main_symbols[] {
ESP_ELFSYM_EXPORT(lv_line_create), ESP_ELFSYM_EXPORT(lv_line_create),
ESP_ELFSYM_EXPORT(lv_line_set_points), ESP_ELFSYM_EXPORT(lv_line_set_points),
ESP_ELFSYM_EXPORT(lv_line_set_points_mutable), ESP_ELFSYM_EXPORT(lv_line_set_points_mutable),
// lv_group
ESP_ELFSYM_EXPORT(lv_group_remove_obj),
// lv_mem
ESP_ELFSYM_EXPORT(lv_free),
ESP_ELFSYM_EXPORT(lv_malloc),
// lv_draw
ESP_ELFSYM_EXPORT(lv_draw_task_get_draw_dsc),
ESP_ELFSYM_EXPORT(lv_draw_task_get_label_dsc),
ESP_ELFSYM_EXPORT(lv_draw_task_get_fill_dsc),
// lv_image
ESP_ELFSYM_EXPORT(lv_image_create),
ESP_ELFSYM_EXPORT(lv_image_set_src),
// stdio.h // stdio.h
ESP_ELFSYM_EXPORT(rename), ESP_ELFSYM_EXPORT(rename),
// dirent.h // dirent.h