Feature additions (#434)

Lots of things "ported" over from the "enhanced" fork. With some adjustments here and there.

KeyboardBacklight driver (for T-Deck only currently)
Trackball driver (for T-Deck only currently)
Keyboard backlight sleep/wake (for T-Deck only currently...also requires keyboard firmware update)
Display sleep/wake
Files - create file/folder
Keyboard settings (for T-Deck only currently)
Time & Date settings tweaks
Locale settings tweaks
Systeminfo additions
Espnow wifi coexist

initI2cDevices - moved to T-deck init.cpp / initBoot
KeyboardInitService - removed,  moved to T-deck init.cpp / initBoot
Adjusted TIMER_UPDATE_INTERVAL to 2 seconds.
Added lock to ActionCreateFolder

Maybe missed some things in the list.

Display wake could do with some kind of block on wake first touch to prevent UI elements being hit when waking device with touch. Same with encoder/trackball/keyboard press i guess.

The original code was written by @cscott0108 at https://github.com/cscott0108/tactility-enhanced-t-deck
This commit is contained in:
Shadowtrance 2026-01-02 21:14:55 +10:00 committed by GitHub
parent feaeb11e49
commit a4dc633063
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1916 additions and 113 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

@ -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

@ -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

@ -15,7 +15,9 @@
#include <Tactility/StringUtils.h> #include <Tactility/StringUtils.h>
#include <cstring> #include <cstring>
#include <cstdio>
#include <unistd.h> #include <unistd.h>
#include <sys/stat.h>
#include <Tactility/file/FileLock.h> #include <Tactility/file/FileLock.h>
#ifdef ESP_PLATFORM #ifdef ESP_PLATFORM
@ -62,6 +64,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 +191,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 +255,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 +317,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 +411,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);
std::string timeZoneName = settings::getTimeZoneName(); // Load current region from settings
if (timeZoneName.empty()) { settings::SystemSettings sysSettings;
timeZoneName = "not set"; if (settings::loadSystemSettings(sysSettings)) {
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, []() {
auto app = optApp();
if (app) {
auto lock = lvgl::getSyncLock()->asScopedLock();
lock.lock();
app->updateMemory();
app->updatePsram();
}
});
Timer tasksTimer = Timer(Timer::Type::Periodic, []() {
auto app = optApp();
if (app) {
auto lock = lvgl::getSyncLock()->asScopedLock();
lock.lock();
app->updateTasks();
}
});
MemoryBarWidgets internalMemBar;
MemoryBarWidgets externalMemBar;
MemoryBarWidgets dataStorageBar;
MemoryBarWidgets sdcardStorageBar;
MemoryBarWidgets systemStorageBar;
lv_obj_t* tasksContainer = nullptr;
lv_obj_t* cpuContainer = nullptr;
lv_obj_t* psramContainer = nullptr;
lv_obj_t* cpuSummaryLabel = nullptr; // Shows overall CPU utilization
lv_obj_t* taskCountLabel = nullptr; // Shows active task count
lv_obj_t* uptimeLabel = nullptr; // Shows system uptime
bool hasExternalMem = false;
bool hasDataStorage = false;
bool hasSdcardStorage = false;
bool hasSystemStorage = false;
void updateMemory() {
updateMemoryBar(internalMemBar, getHeapFree(), getHeapTotal());
if (hasExternalMem) {
updateMemoryBar(externalMemBar, getSpiFree(), getSpiTotal());
}
}
void updateStorage() {
#ifdef ESP_PLATFORM
uint64_t storage_total = 0;
uint64_t storage_free = 0;
if (hasDataStorage) {
if (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK) {
updateMemoryBar(dataStorageBar, storage_free, storage_total);
}
}
if (hasSdcardStorage) {
const auto sdcard_devices = hal::findDevices<hal::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(kernel::millisToTicks(10000)); // Memory & PSRAM: every 10s
tasksTimer.start(kernel::millisToTicks(15000)); // Tasks/CPU: every 15s
}
void onHide(TT_UNUSED AppContext& app) override {
memoryTimer.stop();
tasksTimer.stop();
} }
}; };
@ -300,4 +707,3 @@ extern const AppManifest manifest = {
}; };
} // namespace } // 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,89 @@
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceContext.h>
#include <Tactility/Timer.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/settings/DisplaySettings.h>
#include <Tactility/hal/display/DisplayDevice.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, [this]{ this->tick(); });
timer->setThreadPriority(Thread::Priority::Lower);
timer->start(250); // check 4x per second for snappy restore
return true;
}
void onStop(TT_UNUSED ServiceContext& service) override {
if (timer) {
timer->stop();
timer = nullptr;
}
// Ensure display restored on stop
auto display = getDisplay();
if (display && displayDimmed) {
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
displayDimmed = false;
}
}
};
extern const ServiceManifest manifest = {
.id = "DisplayIdle",
.createService = create<DisplayIdleService>
};
}

View File

@ -34,18 +34,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");
@ -66,6 +77,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");
@ -85,23 +97,19 @@ bool initWifi(const EspNowConfig& config) {
} }
} }
TT_LOG_I(TAG, "WiFi initialized for ESP-NOW (preserved existing connection: %s)", wifi_was_connected ? "yes" : "no");
return true; 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,97 @@
#ifdef ESP_PLATFORM
#include <Tactility/service/ServiceRegistration.h>
#include <Tactility/service/ServiceManifest.h>
#include <Tactility/service/ServiceContext.h>
#include <Tactility/Timer.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/settings/KeyboardSettings.h>
#include <Tactility/hal/keyboard/KeyboardDevice.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, [this]{ this->tick(); });
timer->setThreadPriority(Thread::Priority::Lower);
timer->start(250); // check 4x per second for snappy restore
return true;
}
void onStop(TT_UNUSED ServiceContext& service) override {
if (timer) {
timer->stop();
timer = nullptr;
}
// Ensure keyboard restored on stop
auto keyboard = getKeyboard();
if (keyboard && keyboardDimmed) {
keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0);
keyboardDimmed = false;
}
}
};
extern const ServiceManifest manifest = {
.id = "KeyboardIdle",
.createService = create<KeyboardIdleService>
};
}
#endif

View File

@ -8,7 +8,7 @@
namespace tt::service::memorychecker { namespace tt::service::memorychecker {
constexpr const char* TAG = "MemoryChecker"; constexpr const char* TAG = "MemoryChecker";
constexpr TickType_t TIMER_UPDATE_INTERVAL = 1000U / portTICK_PERIOD_MS; constexpr TickType_t TIMER_UPDATE_INTERVAL = 2000U / portTICK_PERIOD_MS;
// Total memory (in bytes) that should be free before warnings occur // Total memory (in bytes) that should be free before warnings occur
constexpr auto TOTAL_FREE_THRESHOLD = 10'000; constexpr auto TOTAL_FREE_THRESHOLD = 10'000;

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
} }
} }