mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-02-20 07:25:06 +00:00
Merge branch 'main' into develop
This commit is contained in:
commit
2d867ac17a
@ -1,2 +1,5 @@
|
||||
language=en-US
|
||||
timeFormat24h=true
|
||||
timeFormat24h=true
|
||||
dateFormat=MM/DD/YYYY
|
||||
region=US
|
||||
timezone=America/Los_Angeles
|
||||
@ -11,9 +11,9 @@ spiRamSpeed=120M
|
||||
esptoolFlashFreq=120M
|
||||
|
||||
[display]
|
||||
size=2.4"
|
||||
size=5"
|
||||
shape=rectangle
|
||||
dpi=139
|
||||
dpi=187
|
||||
|
||||
[lvgl]
|
||||
colorDepth=16
|
||||
@ -1,7 +1,9 @@
|
||||
#include "devices/Display.h"
|
||||
#include "devices/KeyboardBacklight.h"
|
||||
#include "devices/Power.h"
|
||||
#include "devices/Sdcard.h"
|
||||
#include "devices/TdeckKeyboard.h"
|
||||
#include "devices/TrackballDevice.h"
|
||||
|
||||
#include <Tactility/hal/Configuration.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
@ -15,6 +17,8 @@ static std::vector<std::shared_ptr<Device>> createDevices() {
|
||||
createPower(),
|
||||
createDisplay(),
|
||||
std::make_shared<TdeckKeyboard>(),
|
||||
std::make_shared<KeyboardBacklightDevice>(),
|
||||
std::make_shared<TrackballDevice>(),
|
||||
createSdCard()
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,6 +4,12 @@
|
||||
|
||||
#include <Tactility/TactilityCore.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"
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
|
||||
}
|
||||
145
Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp
Normal file
145
Devices/lilygo-tdeck/Source/Trackball/Trackball.cpp
Normal 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");
|
||||
}
|
||||
|
||||
}
|
||||
44
Devices/lilygo-tdeck/Source/Trackball/Trackball.h
Normal file
44
Devices/lilygo-tdeck/Source/Trackball/Trackball.h
Normal 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);
|
||||
|
||||
}
|
||||
37
Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp
Normal file
37
Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.cpp
Normal 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();
|
||||
}
|
||||
32
Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.h
Normal file
32
Devices/lilygo-tdeck/Source/devices/KeyboardBacklight.h
Normal 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;
|
||||
};
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
#include "TdeckKeyboard.h"
|
||||
#include <Tactility/hal/i2c/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 TDECK_KEYBOARD_I2C_BUS_HANDLE = I2C_NUM_0;
|
||||
@ -36,6 +44,25 @@ static void keyboard_read_callback(TT_UNUSED lv_indev_t* indev, lv_indev_data_t*
|
||||
TT_LOG_D(TAG, "Pressed %d", read_buffer);
|
||||
data->key = read_buffer;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
// TODO: Avoid performance hit by calling loadOrGetDefault() on each key press
|
||||
// Ensure LVGL activity is triggered so idle services can wake the display
|
||||
lv_disp_trig_activity(nullptr);
|
||||
|
||||
// Actively wake display/backlights immediately on key press (independent of idle tick)
|
||||
// Restore display backlight if off (we assume duty 0 means dimmed)
|
||||
auto display = findFirstDevice<tt::hal::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
36
Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp
Normal file
36
Devices/lilygo-tdeck/Source/devices/TrackballDevice.cpp
Normal 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;
|
||||
}
|
||||
21
Devices/lilygo-tdeck/Source/devices/TrackballDevice.h
Normal file
21
Devices/lilygo-tdeck/Source/devices/TrackballDevice.h
Normal 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;
|
||||
};
|
||||
@ -14,7 +14,7 @@ esptoolFlashFreq=80M
|
||||
[display]
|
||||
size=3.5"
|
||||
shape=rectangle
|
||||
dpi=139
|
||||
dpi=165
|
||||
|
||||
[lvgl]
|
||||
colorDepth=16
|
||||
|
||||
@ -21,7 +21,8 @@ public:
|
||||
Keyboard,
|
||||
Encoder,
|
||||
Power,
|
||||
Gps
|
||||
Gps,
|
||||
Other
|
||||
};
|
||||
|
||||
typedef uint32_t Id;
|
||||
|
||||
@ -16,6 +16,8 @@ struct DisplaySettings {
|
||||
Orientation orientation;
|
||||
uint8_t gammaCurve;
|
||||
uint8_t backlightDuty;
|
||||
bool backlightTimeoutEnabled;
|
||||
uint32_t backlightTimeoutMs; // 0 = Never
|
||||
};
|
||||
|
||||
/** Compares default settings with the function parameter to return the difference */
|
||||
|
||||
23
Tactility/Include/Tactility/settings/KeyboardSettings.h
Normal file
23
Tactility/Include/Tactility/settings/KeyboardSettings.h
Normal 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);
|
||||
|
||||
}
|
||||
@ -7,6 +7,8 @@ namespace tt::settings {
|
||||
struct SystemSettings {
|
||||
Language language;
|
||||
bool timeFormat24h;
|
||||
std::string dateFormat; // MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD, YYYY/MM/DD
|
||||
std::string region; // (US, EU, JP, etc.)
|
||||
};
|
||||
|
||||
bool loadSystemSettings(SystemSettings& properties);
|
||||
|
||||
@ -15,7 +15,9 @@ public:
|
||||
enum PendingAction {
|
||||
ActionNone,
|
||||
ActionDelete,
|
||||
ActionRename
|
||||
ActionRename,
|
||||
ActionCreateFile,
|
||||
ActionCreateFolder
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
@ -15,6 +15,8 @@ class View final {
|
||||
lv_obj_t* dir_entry_list = nullptr;
|
||||
lv_obj_t* action_list = nullptr;
|
||||
lv_obj_t* navigate_up_button = nullptr;
|
||||
lv_obj_t* new_file_button = nullptr;
|
||||
lv_obj_t* new_folder_button = nullptr;
|
||||
|
||||
std::string installAppPath = { 0 };
|
||||
LaunchId installAppLaunchId = 0;
|
||||
@ -38,6 +40,8 @@ public:
|
||||
void onDirEntryLongPressed(int32_t index);
|
||||
void onRenamePressed();
|
||||
void onDeletePressed();
|
||||
void onNewFilePressed();
|
||||
void onNewFolderPressed();
|
||||
void onDirEntryListScrollBegin();
|
||||
void onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bundle);
|
||||
};
|
||||
|
||||
@ -51,6 +51,10 @@ namespace service {
|
||||
namespace loader { extern const ServiceManifest manifest; }
|
||||
namespace memorychecker { extern const ServiceManifest manifest; }
|
||||
namespace statusbar { extern const ServiceManifest manifest; }
|
||||
namespace displayidle { extern const ServiceManifest manifest; }
|
||||
#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK)
|
||||
namespace keyboardidle { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
namespace screenshot { extern const ServiceManifest manifest; }
|
||||
#endif
|
||||
@ -83,6 +87,9 @@ namespace app {
|
||||
namespace imageviewer { extern const AppManifest manifest; }
|
||||
namespace inputdialog { extern const AppManifest manifest; }
|
||||
namespace launcher { extern const AppManifest manifest; }
|
||||
#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK)
|
||||
namespace keyboardsettings { extern const AppManifest manifest; }
|
||||
#endif
|
||||
namespace localesettings { extern const AppManifest manifest; }
|
||||
namespace notes { extern const AppManifest manifest; }
|
||||
namespace power { extern const AppManifest manifest; }
|
||||
@ -124,6 +131,9 @@ static void registerInternalApps() {
|
||||
addAppManifest(app::imageviewer::manifest);
|
||||
addAppManifest(app::inputdialog::manifest);
|
||||
addAppManifest(app::launcher::manifest);
|
||||
#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK)
|
||||
addAppManifest(app::keyboardsettings::manifest);
|
||||
#endif
|
||||
addAppManifest(app::localesettings::manifest);
|
||||
addAppManifest(app::notes::manifest);
|
||||
addAppManifest(app::settings::manifest);
|
||||
@ -227,6 +237,10 @@ static void registerAndStartSecondaryServices() {
|
||||
addService(service::loader::manifest);
|
||||
addService(service::gui::manifest);
|
||||
addService(service::statusbar::manifest);
|
||||
addService(service::displayidle::manifest);
|
||||
#if defined(ESP_PLATFORM) && defined(CONFIG_TT_DEVICE_LILYGO_TDECK)
|
||||
addService(service::keyboardidle::manifest);
|
||||
#endif
|
||||
addService(service::memorychecker::manifest);
|
||||
#if TT_FEATURE_SCREENSHOT_ENABLED
|
||||
addService(service::screenshot::manifest);
|
||||
|
||||
@ -19,6 +19,8 @@ class DisplayApp final : public App {
|
||||
|
||||
settings::display::DisplaySettings displaySettings;
|
||||
bool displaySettingsUpdated = false;
|
||||
lv_obj_t* timeoutSwitch = nullptr;
|
||||
lv_obj_t* timeoutDropdown = nullptr;
|
||||
|
||||
static void onBacklightSliderEvent(lv_event_t* event) {
|
||||
auto* slider = static_cast<lv_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:
|
||||
|
||||
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);
|
||||
// Set the dropdown to match current orientation enum
|
||||
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 {
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
#include <Tactility/Tactility.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
@ -62,6 +63,16 @@ static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) {
|
||||
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
|
||||
|
||||
void View::viewFile(const std::string& path, const std::string& filename) {
|
||||
@ -179,7 +190,38 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
|
||||
} else {
|
||||
symbol = LV_SYMBOL_FILE;
|
||||
}
|
||||
lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry.d_name);
|
||||
|
||||
// Get file size for regular files
|
||||
std::string label_text = dir_entry.d_name;
|
||||
if (dir_entry.d_type == file::TT_DT_REG) {
|
||||
std::string file_path = file::getChildPath(state->getCurrentPath(), dir_entry.d_name);
|
||||
struct stat st;
|
||||
if (stat(file_path.c_str(), &st) == 0) {
|
||||
// Format file size in human-readable format
|
||||
const char* size_suffix;
|
||||
double size;
|
||||
if (st.st_size < 1024) {
|
||||
size = st.st_size;
|
||||
size_suffix = " B";
|
||||
} else if (st.st_size < 1024 * 1024) {
|
||||
size = st.st_size / 1024.0;
|
||||
size_suffix = " KB";
|
||||
} else {
|
||||
size = st.st_size / (1024.0 * 1024.0);
|
||||
size_suffix = " MB";
|
||||
}
|
||||
|
||||
char size_str[32];
|
||||
if (st.st_size < 1024) {
|
||||
snprintf(size_str, sizeof(size_str), " (%d%s)", (int)size, size_suffix);
|
||||
} else {
|
||||
snprintf(size_str, sizeof(size_str), " (%.1f%s)", size, size_suffix);
|
||||
}
|
||||
label_text += size_str;
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t* button = lv_list_add_button(list, symbol, label_text.c_str());
|
||||
lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
lv_obj_add_event_cb(button, &onDirEntryLongPressedCallback, LV_EVENT_LONG_PRESSED, this);
|
||||
}
|
||||
@ -212,6 +254,18 @@ void View::onDeletePressed() {
|
||||
alertdialog::start("Are you sure?", message, choices);
|
||||
}
|
||||
|
||||
void View::onNewFilePressed() {
|
||||
TT_LOG_I(TAG, "Creating new file");
|
||||
state->setPendingAction(State::ActionCreateFile);
|
||||
inputdialog::start("New File", "Enter filename:", "");
|
||||
}
|
||||
|
||||
void View::onNewFolderPressed() {
|
||||
TT_LOG_I(TAG, "Creating new folder");
|
||||
state->setPendingAction(State::ActionCreateFolder);
|
||||
inputdialog::start("New Folder", "Enter folder name:", "");
|
||||
}
|
||||
|
||||
void View::showActionsForDirectory() {
|
||||
lv_obj_clean(action_list);
|
||||
|
||||
@ -262,6 +316,8 @@ void View::init(const AppContext& appContext, lv_obj_t* parent) {
|
||||
|
||||
auto* toolbar = lvgl::toolbar_create(parent, appContext);
|
||||
navigate_up_button = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this);
|
||||
new_file_button = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_FILE, &onNewFilePressedCallback, this);
|
||||
new_folder_button = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_DIRECTORY, &onNewFolderPressedCallback, this);
|
||||
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
@ -354,6 +410,62 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr<Bundle> bu
|
||||
}
|
||||
break;
|
||||
}
|
||||
case State::ActionCreateFile: {
|
||||
auto filename = inputdialog::getResult(*bundle);
|
||||
if (!filename.empty()) {
|
||||
std::string new_file_path = file::getChildPath(state->getCurrentPath(), filename);
|
||||
|
||||
auto lock = file::getLock(new_file_path);
|
||||
lock->lock();
|
||||
|
||||
struct stat st;
|
||||
if (stat(new_file_path.c_str(), &st) == 0) {
|
||||
TT_LOG_W(TAG, "File already exists: \"%s\"", new_file_path.c_str());
|
||||
lock->unlock();
|
||||
break;
|
||||
}
|
||||
|
||||
FILE* new_file = fopen(new_file_path.c_str(), "w");
|
||||
if (new_file) {
|
||||
fclose(new_file);
|
||||
TT_LOG_I(TAG, "Created file \"%s\"", new_file_path.c_str());
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to create file \"%s\"", new_file_path.c_str());
|
||||
}
|
||||
lock->unlock();
|
||||
|
||||
state->setEntriesForPath(state->getCurrentPath());
|
||||
update();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case State::ActionCreateFolder: {
|
||||
auto foldername = inputdialog::getResult(*bundle);
|
||||
if (!foldername.empty()) {
|
||||
std::string new_folder_path = file::getChildPath(state->getCurrentPath(), foldername);
|
||||
|
||||
auto lock = file::getLock(new_folder_path);
|
||||
lock->lock();
|
||||
|
||||
struct stat st;
|
||||
if (stat(new_folder_path.c_str(), &st) == 0) {
|
||||
TT_LOG_W(TAG, "Folder already exists: \"%s\"", new_folder_path.c_str());
|
||||
lock->unlock();
|
||||
break;
|
||||
}
|
||||
|
||||
if (mkdir(new_folder_path.c_str(), 0755) == 0) {
|
||||
TT_LOG_I(TAG, "Created folder \"%s\"", new_folder_path.c_str());
|
||||
} else {
|
||||
TT_LOG_E(TAG, "Failed to create folder \"%s\"", new_folder_path.c_str());
|
||||
}
|
||||
lock->unlock();
|
||||
|
||||
state->setEntriesForPath(state->getCurrentPath());
|
||||
update();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
221
Tactility/Source/app/keyboard/KeyboardSettings.cpp
Normal file
221
Tactility/Source/app/keyboard/KeyboardSettings.cpp
Normal 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
|
||||
@ -8,6 +8,7 @@
|
||||
#include <Tactility/settings/Time.h>
|
||||
#include <Tactility/StringUtils.h>
|
||||
#include <Tactility/settings/Language.h>
|
||||
#include <Tactility/settings/SystemSettings.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
#include <map>
|
||||
@ -28,14 +29,9 @@ extern const AppManifest manifest;
|
||||
class LocaleSettingsApp final : public App {
|
||||
tt::i18n::TextResources textResources = tt::i18n::TextResources(TEXT_RESOURCE_PATH);
|
||||
RecursiveMutex mutex;
|
||||
lv_obj_t* timeZoneLabel = nullptr;
|
||||
lv_obj_t* regionLabel = nullptr;
|
||||
lv_obj_t* regionTextArea = nullptr;
|
||||
lv_obj_t* languageDropdown = nullptr;
|
||||
lv_obj_t* languageLabel = nullptr;
|
||||
|
||||
static void onConfigureTimeZonePressed(TT_UNUSED lv_event_t* event) {
|
||||
timezone::start();
|
||||
}
|
||||
bool settingsUpdated = false;
|
||||
|
||||
std::map<settings::Language, std::string> languageMap;
|
||||
|
||||
@ -68,9 +64,6 @@ class LocaleSettingsApp final : public App {
|
||||
void updateViews() {
|
||||
textResources.load();
|
||||
|
||||
lv_label_set_text(regionLabel , textResources[i18n::Text::REGION].c_str());
|
||||
lv_label_set_text(languageLabel, textResources[i18n::Text::LANGUAGE].c_str());
|
||||
|
||||
std::string language_options = getLanguageOptions();
|
||||
lv_dropdown_set_options(languageDropdown, language_options.c_str());
|
||||
lv_dropdown_set_selected(languageDropdown, static_cast<uint32_t>(settings::getLanguage()));
|
||||
@ -86,6 +79,11 @@ class LocaleSettingsApp final : public App {
|
||||
self->updateViews();
|
||||
}
|
||||
|
||||
static void onRegionChanged(lv_event_t* event) {
|
||||
auto* self = static_cast<LocaleSettingsApp*>(lv_event_get_user_data(event));
|
||||
self->settingsUpdated = true;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
@ -108,42 +106,42 @@ public:
|
||||
auto* region_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(region_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(region_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(region_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(region_wrapper, 8, 0);
|
||||
lv_obj_set_style_border_width(region_wrapper, 0, 0);
|
||||
|
||||
regionLabel = lv_label_create(region_wrapper);
|
||||
lv_label_set_text(regionLabel , textResources[i18n::Text::REGION].c_str());
|
||||
lv_obj_align(regionLabel , LV_ALIGN_LEFT_MID, 0, 0);
|
||||
auto* region_label = lv_label_create(region_wrapper);
|
||||
lv_label_set_text(region_label, textResources[i18n::Text::REGION].c_str());
|
||||
lv_obj_align(region_label, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
|
||||
auto* region_button = lv_button_create(region_wrapper);
|
||||
lv_obj_align(region_button, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
auto* region_button_image = lv_image_create(region_button);
|
||||
lv_obj_add_event_cb(region_button, onConfigureTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
lv_image_set_src(region_button_image, LV_SYMBOL_SETTINGS);
|
||||
|
||||
timeZoneLabel = lv_label_create(region_wrapper);
|
||||
std::string timeZoneName = settings::getTimeZoneName();
|
||||
if (timeZoneName.empty()) {
|
||||
timeZoneName = "not set";
|
||||
// Region text area for user input (e.g., US, EU, JP)
|
||||
regionTextArea = lv_textarea_create(region_wrapper);
|
||||
lv_obj_set_width(regionTextArea, 120);
|
||||
lv_textarea_set_one_line(regionTextArea, true);
|
||||
lv_textarea_set_max_length(regionTextArea, 50);
|
||||
lv_textarea_set_placeholder_text(regionTextArea, "e.g. US, EU");
|
||||
|
||||
// Load current region from settings
|
||||
settings::SystemSettings sysSettings;
|
||||
if (settings::loadSystemSettings(sysSettings)) {
|
||||
lv_textarea_set_text(regionTextArea, sysSettings.region.c_str());
|
||||
}
|
||||
|
||||
lv_label_set_text(timeZoneLabel, timeZoneName.c_str());
|
||||
const int offset = ui_scale == hal::UiScale::Smallest ? -2 : -10;
|
||||
lv_obj_align_to(timeZoneLabel, region_button, LV_ALIGN_OUT_LEFT_MID, offset, 0);
|
||||
lv_obj_add_event_cb(regionTextArea, onRegionChanged, LV_EVENT_VALUE_CHANGED, this);
|
||||
lv_obj_align(regionTextArea, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
|
||||
// Language
|
||||
|
||||
auto* language_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(language_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(language_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(language_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(language_wrapper, 8, 0);
|
||||
lv_obj_set_style_border_width(language_wrapper, 0, 0);
|
||||
|
||||
languageLabel = lv_label_create(language_wrapper);
|
||||
auto* languageLabel = lv_label_create(language_wrapper);
|
||||
lv_label_set_text(languageLabel, textResources[i18n::Text::LANGUAGE].c_str());
|
||||
lv_obj_align(languageLabel, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_obj_align(languageLabel, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
|
||||
languageDropdown = lv_dropdown_create(language_wrapper);
|
||||
lv_obj_set_width(languageDropdown, 150);
|
||||
lv_obj_align(languageDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
std::string language_options = getLanguageOptions();
|
||||
lv_dropdown_set_options(languageDropdown, language_options.c_str());
|
||||
@ -151,18 +149,12 @@ public:
|
||||
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 {
|
||||
if (result == Result::Ok && bundle != nullptr) {
|
||||
const auto name = timezone::getResultName(*bundle);
|
||||
const auto code = timezone::getResultCode(*bundle);
|
||||
TT_LOG_I(TAG, "Result name=%s code=%s", name.c_str(), code.c_str());
|
||||
settings::setTimeZone(name, code);
|
||||
|
||||
if (!name.empty()) {
|
||||
if (lvgl::lock(100 / portTICK_PERIOD_MS)) {
|
||||
lv_label_set_text(timeZoneLabel, name.c_str());
|
||||
lvgl::unlock();
|
||||
}
|
||||
void onHide(TT_UNUSED AppContext& app) override {
|
||||
if (settingsUpdated && regionTextArea) {
|
||||
settings::SystemSettings sysSettings;
|
||||
if (settings::loadSystemSettings(sysSettings)) {
|
||||
sysSettings.region = lv_textarea_get_text(regionTextArea);
|
||||
settings::saveSystemSettings(sysSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
#include <Tactility/TactilityConfig.h>
|
||||
#include <Tactility/lvgl/Toolbar.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
|
||||
#include <Tactility/Assets.h>
|
||||
#include <Tactility/hal/Device.h>
|
||||
#include <Tactility/Tactility.h>
|
||||
#include <Tactility/Timer.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <lvgl.h>
|
||||
#include <utility>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <esp_vfs_fat.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <Tactility/MountPoints.h>
|
||||
#endif
|
||||
|
||||
@ -50,6 +55,22 @@ static size_t getSpiTotal() {
|
||||
#endif
|
||||
}
|
||||
|
||||
static size_t getPsramMinFree() {
|
||||
#ifdef ESP_PLATFORM
|
||||
return heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM);
|
||||
#else
|
||||
return 4096 * 1024;
|
||||
#endif
|
||||
}
|
||||
|
||||
static size_t getPsramLargestBlock() {
|
||||
#ifdef ESP_PLATFORM
|
||||
return heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM);
|
||||
#else
|
||||
return 4096 * 1024;
|
||||
#endif
|
||||
}
|
||||
|
||||
enum class StorageUnit {
|
||||
Bytes,
|
||||
Kilobytes,
|
||||
@ -102,8 +123,12 @@ static std::string getStorageValue(StorageUnit unit, uint64_t bytes) {
|
||||
}
|
||||
}
|
||||
|
||||
static void addMemoryBar(lv_obj_t* parent, const char* label, uint64_t free, uint64_t total) {
|
||||
uint64_t used = total - free;
|
||||
struct MemoryBarWidgets {
|
||||
lv_obj_t* bar = nullptr;
|
||||
lv_obj_t* label = nullptr;
|
||||
};
|
||||
|
||||
static MemoryBarWidgets createMemoryBar(lv_obj_t* parent, const char* label) {
|
||||
auto* container = lv_obj_create(parent);
|
||||
lv_obj_set_size(container, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(container, 0, LV_STATE_DEFAULT);
|
||||
@ -118,6 +143,22 @@ static void addMemoryBar(lv_obj_t* parent, const char* label, uint64_t free, uin
|
||||
auto* bar = lv_bar_create(container);
|
||||
lv_obj_set_flex_grow(bar, 1);
|
||||
|
||||
auto* bottom_label = lv_label_create(parent);
|
||||
lv_obj_set_width(bottom_label, LV_PCT(100));
|
||||
lv_obj_set_style_text_align(bottom_label, LV_TEXT_ALIGN_RIGHT, 0);
|
||||
|
||||
if (hal::getConfiguration()->uiScale == hal::UiScale::Smallest) {
|
||||
lv_obj_set_style_pad_bottom(bottom_label, 2, LV_STATE_DEFAULT);
|
||||
} else {
|
||||
lv_obj_set_style_pad_bottom(bottom_label, 12, LV_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
return {bar, bottom_label};
|
||||
}
|
||||
|
||||
static void updateMemoryBar(const MemoryBarWidgets& widgets, uint64_t free, uint64_t total) {
|
||||
uint64_t used = total - free;
|
||||
|
||||
// Scale down the uint64_t until it fits int32_t for the lv_bar
|
||||
uint64_t free_scaled = free;
|
||||
uint64_t total_scaled = total;
|
||||
@ -127,27 +168,20 @@ static void addMemoryBar(lv_obj_t* parent, const char* label, uint64_t free, uin
|
||||
}
|
||||
|
||||
if (total > 0) {
|
||||
lv_bar_set_range(bar, 0, total_scaled);
|
||||
lv_bar_set_range(widgets.bar, 0, total_scaled);
|
||||
} else {
|
||||
lv_bar_set_range(bar, 0, 1);
|
||||
lv_bar_set_range(widgets.bar, 0, 1);
|
||||
}
|
||||
|
||||
lv_bar_set_value(bar, (total_scaled - free_scaled), LV_ANIM_OFF);
|
||||
lv_bar_set_value(widgets.bar, (total_scaled - free_scaled), LV_ANIM_OFF);
|
||||
|
||||
auto* bottom_label = lv_label_create(parent);
|
||||
const auto unit = getStorageUnit(total);
|
||||
const auto unit_label = getStorageUnitString(unit);
|
||||
const auto used_converted = getStorageValue(unit, used);
|
||||
const auto free_converted = getStorageValue(unit, free);
|
||||
const auto total_converted = getStorageValue(unit, total);
|
||||
lv_label_set_text_fmt(bottom_label, "%s / %s %s used", used_converted.c_str(), total_converted.c_str(), unit_label.c_str());
|
||||
lv_obj_set_width(bottom_label, LV_PCT(100));
|
||||
lv_obj_set_style_text_align(bottom_label, LV_TEXT_ALIGN_RIGHT, 0);
|
||||
|
||||
if (hal::getConfiguration()->uiScale == hal::UiScale::Smallest) {
|
||||
lv_obj_set_style_pad_bottom(bottom_label, 2, LV_STATE_DEFAULT);
|
||||
} else {
|
||||
lv_obj_set_style_pad_bottom(bottom_label, 12, LV_STATE_DEFAULT);
|
||||
}
|
||||
lv_label_set_text_fmt(widgets.label, "%s / %s %s free (%llu / %llu bytes)",
|
||||
free_converted.c_str(), total_converted.c_str(), unit_label.c_str(),
|
||||
(unsigned long long)free, (unsigned long long)total);
|
||||
}
|
||||
|
||||
#if configUSE_TRACE_FACILITY
|
||||
@ -170,22 +204,47 @@ static const char* getTaskState(const TaskStatus_t& task) {
|
||||
}
|
||||
}
|
||||
|
||||
static void addRtosTask(lv_obj_t* parent, const TaskStatus_t& task) {
|
||||
auto* label = lv_label_create(parent);
|
||||
const char* name = (task.pcTaskName == nullptr || task.pcTaskName[0] == 0) ? "(unnamed)" : task.pcTaskName;
|
||||
lv_label_set_text_fmt(label, "%s (%s)", name, getTaskState(task));
|
||||
static void clearContainer(lv_obj_t* container) {
|
||||
lv_obj_clean(container);
|
||||
}
|
||||
|
||||
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();
|
||||
auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count);
|
||||
if (!tasks) {
|
||||
auto* error_label = lv_label_create(parent);
|
||||
lv_label_set_text(error_label, "Failed to allocate memory for task list");
|
||||
return;
|
||||
}
|
||||
uint32_t totalRuntime = 0;
|
||||
UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime);
|
||||
|
||||
for (int i = 0; i < actual; ++i) {
|
||||
const TaskStatus_t& task = tasks[i];
|
||||
addRtosTask(parent, task);
|
||||
// Sort by CPU usage if showing percentages, otherwise keep original order
|
||||
if (showCpuPercent) {
|
||||
std::sort(tasks, tasks + actual, [](const TaskStatus_t& a, const TaskStatus_t& b) {
|
||||
return a.ulRunTimeCounter > b.ulRunTimeCounter;
|
||||
});
|
||||
}
|
||||
|
||||
for (int i = 0; i < actual; ++i) {
|
||||
addRtosTask(parent, tasks[i], showCpuPercent ? totalRuntime : 0);
|
||||
}
|
||||
|
||||
free(tasks);
|
||||
}
|
||||
|
||||
@ -211,14 +270,311 @@ static lv_obj_t* createTab(lv_obj_t* tabview, const char* name) {
|
||||
return tab;
|
||||
}
|
||||
|
||||
extern const AppManifest manifest;
|
||||
|
||||
class SystemInfoApp;
|
||||
|
||||
static std::shared_ptr<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 {
|
||||
Timer memoryTimer = Timer(Timer::Type::Periodic, kernel::millisToTicks(10000), [] {
|
||||
auto app = optApp();
|
||||
if (app) {
|
||||
auto lock = lvgl::getSyncLock()->asScopedLock();
|
||||
lock.lock();
|
||||
app->updateMemory();
|
||||
app->updatePsram();
|
||||
}
|
||||
});
|
||||
|
||||
Timer tasksTimer = Timer(Timer::Type::Periodic, kernel::millisToTicks(15000), [] {
|
||||
auto app = optApp();
|
||||
if (app) {
|
||||
auto lock = lvgl::getSyncLock()->asScopedLock();
|
||||
lock.lock();
|
||||
app->updateTasks();
|
||||
}
|
||||
});
|
||||
|
||||
MemoryBarWidgets internalMemBar;
|
||||
MemoryBarWidgets externalMemBar;
|
||||
MemoryBarWidgets dataStorageBar;
|
||||
MemoryBarWidgets sdcardStorageBar;
|
||||
MemoryBarWidgets systemStorageBar;
|
||||
|
||||
lv_obj_t* tasksContainer = nullptr;
|
||||
lv_obj_t* cpuContainer = nullptr;
|
||||
lv_obj_t* psramContainer = nullptr;
|
||||
lv_obj_t* cpuSummaryLabel = nullptr; // Shows overall CPU utilization
|
||||
lv_obj_t* taskCountLabel = nullptr; // Shows active task count
|
||||
lv_obj_t* uptimeLabel = nullptr; // Shows system uptime
|
||||
|
||||
bool hasExternalMem = false;
|
||||
bool hasDataStorage = false;
|
||||
bool hasSdcardStorage = false;
|
||||
bool hasSystemStorage = false;
|
||||
|
||||
void updateMemory() {
|
||||
updateMemoryBar(internalMemBar, getHeapFree(), getHeapTotal());
|
||||
|
||||
if (hasExternalMem) {
|
||||
updateMemoryBar(externalMemBar, getSpiFree(), getSpiTotal());
|
||||
}
|
||||
}
|
||||
|
||||
void updateStorage() {
|
||||
#ifdef ESP_PLATFORM
|
||||
uint64_t storage_total = 0;
|
||||
uint64_t storage_free = 0;
|
||||
|
||||
if (hasDataStorage) {
|
||||
if (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK) {
|
||||
updateMemoryBar(dataStorageBar, storage_free, storage_total);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSdcardStorage) {
|
||||
const auto sdcard_devices = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
|
||||
for (const auto& sdcard : sdcard_devices) {
|
||||
if (sdcard->isMounted() && esp_vfs_fat_info(sdcard->getMountPath().c_str(), &storage_total, &storage_free) == ESP_OK) {
|
||||
updateMemoryBar(sdcardStorageBar, storage_free, storage_total);
|
||||
break; // Only update first SD card
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSystemStorage) {
|
||||
if (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK) {
|
||||
updateMemoryBar(systemStorageBar, storage_free, storage_total);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void updateTasks() {
|
||||
#if configUSE_TRACE_FACILITY
|
||||
if (tasksContainer) {
|
||||
updateRtosTasks(tasksContainer, false); // Tasks tab: show state
|
||||
}
|
||||
|
||||
if (cpuContainer) {
|
||||
updateRtosTasks(cpuContainer, true); // CPU tab: show percentages
|
||||
|
||||
// Update CPU summary at top of tab
|
||||
// Note: FreeRTOS runtime stats accumulate since boot, so percentages
|
||||
// are averages over entire uptime, not instantaneous usage
|
||||
if (cpuSummaryLabel && taskCountLabel && uptimeLabel) {
|
||||
UBaseType_t count = uxTaskGetNumberOfTasks();
|
||||
auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count);
|
||||
if (tasks) {
|
||||
uint32_t totalRuntime = 0;
|
||||
UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime);
|
||||
|
||||
if (totalRuntime > 0 && actual > 0) {
|
||||
// Calculate total CPU usage (100% - idle = usage)
|
||||
uint32_t idleTime = 0;
|
||||
for (int i = 0; i < actual; ++i) {
|
||||
const char* name = tasks[i].pcTaskName;
|
||||
if (name && (strcmp(name, "IDLE0") == 0 || strcmp(name, "IDLE1") == 0)) {
|
||||
idleTime += tasks[i].ulRunTimeCounter;
|
||||
}
|
||||
}
|
||||
|
||||
float cpuUsage = ((totalRuntime - idleTime) * 100.0f) / totalRuntime;
|
||||
auto summary_text = std::format("Overall CPU Usage: {:.1f}% (avg since boot)", cpuUsage);
|
||||
lv_label_set_text(cpuSummaryLabel, summary_text.c_str());
|
||||
|
||||
// Show total task count
|
||||
auto core_text = std::format("Active Tasks: {} total", actual);
|
||||
lv_label_set_text(taskCountLabel, core_text.c_str());
|
||||
|
||||
// Use actual system tick count for uptime
|
||||
TickType_t ticks = xTaskGetTickCount();
|
||||
float uptime_sec = static_cast<float>(ticks) / configTICK_RATE_HZ;
|
||||
auto uptime_text = std::format("System Uptime: {:.1f} min", uptime_sec / 60.0f);
|
||||
lv_label_set_text(uptimeLabel, uptime_text.c_str());
|
||||
} else {
|
||||
lv_label_set_text(cpuSummaryLabel, "Overall CPU Usage: --.-%");
|
||||
lv_label_set_text(taskCountLabel, "Active Tasks: --");
|
||||
lv_label_set_text(uptimeLabel, "System Uptime: --");
|
||||
}
|
||||
|
||||
free(tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void updatePsram() {
|
||||
#ifdef ESP_PLATFORM
|
||||
if (!psramContainer || !hasExternalMem) return;
|
||||
|
||||
clearContainer(psramContainer);
|
||||
|
||||
size_t free_mem = getSpiFree();
|
||||
size_t total = getSpiTotal();
|
||||
size_t used = total - free_mem;
|
||||
size_t min_free = getPsramMinFree();
|
||||
size_t largest_block = getPsramLargestBlock();
|
||||
size_t peak_usage = total - min_free;
|
||||
|
||||
// Safety check - if no PSRAM, show error
|
||||
if (total == 0) {
|
||||
auto* error_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(error_label, "No PSRAM detected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Summary
|
||||
auto* summary_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(summary_label, "PSRAM Usage Summary");
|
||||
lv_obj_set_style_text_font(summary_label, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_pad_bottom(summary_label, 8, 0);
|
||||
|
||||
// Current usage
|
||||
auto* usage_label = lv_label_create(psramContainer);
|
||||
float used_mb = used / (1024.0f * 1024.0f);
|
||||
float total_mb = total / (1024.0f * 1024.0f);
|
||||
float used_percent = (used * 100.0f) / total;
|
||||
auto usage_text = std::format("Current: {:.2f} / {:.2f} MB ({:.1f}% used)",
|
||||
used_mb, total_mb, used_percent);
|
||||
lv_label_set_text(usage_label, usage_text.c_str());
|
||||
|
||||
// Peak usage
|
||||
auto* peak_label = lv_label_create(psramContainer);
|
||||
float peak_mb = peak_usage / (1024.0f * 1024.0f);
|
||||
float peak_percent = (peak_usage * 100.0f) / total;
|
||||
auto peak_text = std::format("Peak: {:.2f} MB ({:.1f}% of total)",
|
||||
peak_mb, peak_percent);
|
||||
lv_label_set_text(peak_label, peak_text.c_str());
|
||||
|
||||
// Minimum free (lowest point)
|
||||
auto* min_free_label = lv_label_create(psramContainer);
|
||||
float min_free_mb = min_free / (1024.0f * 1024.0f);
|
||||
auto min_free_text = std::format("Min Free: {:.2f} MB", min_free_mb);
|
||||
lv_label_set_text(min_free_label, min_free_text.c_str());
|
||||
|
||||
// Largest contiguous block
|
||||
auto* largest_label = lv_label_create(psramContainer);
|
||||
float largest_mb = largest_block / (1024.0f * 1024.0f);
|
||||
auto largest_text = std::format("Largest Block: {:.2f} MB", largest_mb);
|
||||
lv_label_set_text(largest_label, largest_text.c_str());
|
||||
|
||||
// Spacer
|
||||
auto* spacer = lv_obj_create(psramContainer);
|
||||
lv_obj_set_size(spacer, LV_PCT(100), 16);
|
||||
lv_obj_set_style_bg_opa(spacer, 0, 0);
|
||||
lv_obj_set_style_border_width(spacer, 0, 0);
|
||||
|
||||
// PSRAM Configuration section
|
||||
auto* config_header = lv_label_create(psramContainer);
|
||||
lv_label_set_text(config_header, "PSRAM Configuration");
|
||||
lv_obj_set_style_text_font(config_header, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_pad_bottom(config_header, 8, 0);
|
||||
|
||||
// Get threshold from sdkconfig
|
||||
#ifdef CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL
|
||||
const int threshold = CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL;
|
||||
#else
|
||||
const int threshold = 16384; // Default ESP-IDF value
|
||||
#endif
|
||||
|
||||
// Display threshold configuration
|
||||
auto* threshold_info = lv_label_create(psramContainer);
|
||||
if (threshold >= 1024) {
|
||||
lv_label_set_text_fmt(threshold_info, "• Threshold: >=%d KB -> PSRAM", threshold / 1024);
|
||||
} else {
|
||||
lv_label_set_text_fmt(threshold_info, "• Threshold: >=%d bytes -> PSRAM", threshold);
|
||||
}
|
||||
|
||||
auto* internal_info = lv_label_create(psramContainer);
|
||||
if (threshold >= 1024) {
|
||||
lv_label_set_text_fmt(internal_info, "• Allocations <%d KB -> Internal RAM", threshold / 1024);
|
||||
} else {
|
||||
lv_label_set_text_fmt(internal_info, "• Allocations <%d bytes -> Internal RAM", threshold);
|
||||
}
|
||||
|
||||
auto* note_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(note_label, "• DMA buffers always use Internal RAM");
|
||||
|
||||
// Spacer after config
|
||||
auto* spacer_config = lv_obj_create(psramContainer);
|
||||
lv_obj_set_size(spacer_config, LV_PCT(100), 16);
|
||||
lv_obj_set_style_bg_opa(spacer_config, 0, 0);
|
||||
lv_obj_set_style_border_width(spacer_config, 0, 0);
|
||||
|
||||
// Known PSRAM consumers header
|
||||
auto* consumers_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(consumers_label, "PSRAM Allocation Strategy");
|
||||
lv_obj_set_style_text_font(consumers_label, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_pad_bottom(consumers_label, 8, 0);
|
||||
|
||||
// Explain what's in PSRAM
|
||||
auto* strategy_note = lv_label_create(psramContainer);
|
||||
lv_label_set_text(strategy_note, "Apps don't pre-allocate to PSRAM.\nThey use LVGL dynamic allocation:");
|
||||
lv_obj_set_style_text_color(strategy_note, lv_palette_main(LV_PALETTE_GREY), 0);
|
||||
|
||||
// List what automatically goes to PSRAM
|
||||
auto* lvgl_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(lvgl_label, "• All LVGL widgets (buttons, labels, etc.)");
|
||||
|
||||
auto* framebuffer_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(framebuffer_label, "• Display framebuffers");
|
||||
|
||||
auto* wifi_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(wifi_label, "• WiFi/Network buffers");
|
||||
|
||||
auto* file_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(file_label, "• File I/O buffers");
|
||||
|
||||
auto* task_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(task_label, "• Task stacks (when enabled)");
|
||||
|
||||
auto* general_label = lv_label_create(psramContainer);
|
||||
if (threshold >= 1024) {
|
||||
lv_label_set_text_fmt(general_label, "• All allocations >=%d KB", threshold / 1024);
|
||||
} else {
|
||||
lv_label_set_text_fmt(general_label, "• All allocations >=%d bytes", threshold);
|
||||
}
|
||||
|
||||
// Spacer
|
||||
auto* spacer_apps = lv_obj_create(psramContainer);
|
||||
lv_obj_set_size(spacer_apps, LV_PCT(100), 16);
|
||||
lv_obj_set_style_bg_opa(spacer_apps, 0, 0);
|
||||
lv_obj_set_style_border_width(spacer_apps, 0, 0);
|
||||
|
||||
// App behavior explanation
|
||||
auto* app_behavior_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(app_behavior_label, "App Memory Behavior");
|
||||
lv_obj_set_style_text_font(app_behavior_label, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_pad_bottom(app_behavior_label, 8, 0);
|
||||
|
||||
auto* app_note1 = lv_label_create(psramContainer);
|
||||
lv_label_set_text(app_note1, "• Apps allocate UI when opened (10-50 KB)");
|
||||
|
||||
auto* app_note2 = lv_label_create(psramContainer);
|
||||
lv_label_set_text(app_note2, "• All app UI goes to PSRAM automatically");
|
||||
|
||||
auto* app_note3 = lv_label_create(psramContainer);
|
||||
lv_label_set_text(app_note3, "• Apps deallocate when closed (no caching)");
|
||||
|
||||
auto* app_note4 = lv_label_create(psramContainer);
|
||||
lv_label_set_text(app_note4, "• One app open at a time = 10-50 KB in PSRAM");
|
||||
#endif
|
||||
}
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
|
||||
lvgl::toolbar_create(parent, app);
|
||||
|
||||
// This wrapper automatically has its children added vertically underneath eachother
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_style_border_width(wrapper, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_COLUMN);
|
||||
@ -230,53 +586,89 @@ class SystemInfoApp final : public App {
|
||||
lv_tabview_set_tab_bar_position(tabview, LV_DIR_LEFT);
|
||||
lv_tabview_set_tab_bar_size(tabview, 80);
|
||||
|
||||
// Tabs
|
||||
|
||||
// Create tabs
|
||||
auto* memory_tab = createTab(tabview, "Memory");
|
||||
auto* psram_tab = createTab(tabview, "PSRAM");
|
||||
auto* cpu_tab = createTab(tabview, "CPU");
|
||||
auto* storage_tab = createTab(tabview, "Storage");
|
||||
auto* tasks_tab = createTab(tabview, "Tasks");
|
||||
auto* devices_tab = createTab(tabview, "Devices");
|
||||
auto* about_tab = createTab(tabview, "About");
|
||||
|
||||
// Memory tab content
|
||||
internalMemBar = createMemoryBar(memory_tab, "Internal");
|
||||
|
||||
addMemoryBar(memory_tab, "Internal", getHeapFree(), getHeapTotal());
|
||||
if (getSpiTotal() > 0) {
|
||||
addMemoryBar(memory_tab, "External", getSpiFree(), getSpiTotal());
|
||||
hasExternalMem = getSpiTotal() > 0;
|
||||
if (hasExternalMem) {
|
||||
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
|
||||
// Wrapper for the memory usage bars
|
||||
// Storage tab content
|
||||
uint64_t storage_total = 0;
|
||||
uint64_t storage_free = 0;
|
||||
|
||||
|
||||
if (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK) {
|
||||
addMemoryBar(storage_tab, file::MOUNT_POINT_DATA, storage_free, storage_total);
|
||||
hasDataStorage = (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK);
|
||||
if (hasDataStorage) {
|
||||
dataStorageBar = createMemoryBar(storage_tab, file::MOUNT_POINT_DATA);
|
||||
}
|
||||
|
||||
const auto sdcard_devices = hal::findDevices<hal::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) {
|
||||
addMemoryBar(
|
||||
storage_tab,
|
||||
sdcard->getMountPath().c_str(),
|
||||
storage_free,
|
||||
storage_total
|
||||
);
|
||||
hasSdcardStorage = true;
|
||||
sdcardStorageBar = createMemoryBar(storage_tab, sdcard->getMountPath().c_str());
|
||||
break; // Only show first SD card
|
||||
}
|
||||
}
|
||||
|
||||
if (config::SHOW_SYSTEM_PARTITION) {
|
||||
if (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK) {
|
||||
addMemoryBar(storage_tab, file::MOUNT_POINT_SYSTEM, storage_free, storage_total);
|
||||
hasSystemStorage = (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK);
|
||||
if (hasSystemStorage) {
|
||||
systemStorageBar = createMemoryBar(storage_tab, file::MOUNT_POINT_SYSTEM);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if configUSE_TRACE_FACILITY
|
||||
addRtosTasks(tasks_tab);
|
||||
// CPU tab - summary at top
|
||||
cpuSummaryLabel = lv_label_create(cpu_tab);
|
||||
lv_label_set_text(cpuSummaryLabel, "Overall CPU Usage: --.-%");
|
||||
lv_obj_set_style_text_font(cpuSummaryLabel, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_pad_bottom(cpuSummaryLabel, 4, 0);
|
||||
|
||||
taskCountLabel = lv_label_create(cpu_tab);
|
||||
lv_label_set_text(taskCountLabel, "Active Tasks: --.-%");
|
||||
|
||||
uptimeLabel = lv_label_create(cpu_tab);
|
||||
lv_label_set_text(uptimeLabel, "System Uptime: --.-%");
|
||||
lv_obj_set_style_pad_bottom(uptimeLabel, 8, 0);
|
||||
|
||||
// CPU tab - container for task list (dynamic updates)
|
||||
cpuContainer = lv_obj_create(cpu_tab);
|
||||
lv_obj_set_size(cpuContainer, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(cpuContainer, 8, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(cpuContainer, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(cpuContainer, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_bg_opa(cpuContainer, 0, LV_STATE_DEFAULT);
|
||||
|
||||
// Tasks tab - container for dynamic updates
|
||||
tasksContainer = lv_obj_create(tasks_tab);
|
||||
lv_obj_set_size(tasksContainer, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(tasksContainer, 8, LV_STATE_DEFAULT);
|
||||
lv_obj_set_style_border_width(tasksContainer, 0, LV_STATE_DEFAULT);
|
||||
lv_obj_set_flex_flow(tasksContainer, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_style_bg_opa(tasksContainer, 0, LV_STATE_DEFAULT);
|
||||
#endif
|
||||
|
||||
addDevices(devices_tab);
|
||||
@ -288,6 +680,21 @@ class SystemInfoApp final : public App {
|
||||
auto* esp_idf_version = lv_label_create(about_tab);
|
||||
lv_label_set_text_fmt(esp_idf_version, "ESP-IDF v%d.%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR, ESP_IDF_VERSION_PATCH);
|
||||
#endif
|
||||
|
||||
// Initial updates
|
||||
updateMemory();
|
||||
updateStorage(); // Storage: one-time update on show (doesn't change frequently)
|
||||
updateTasks();
|
||||
updatePsram(); // PSRAM: detailed breakdown
|
||||
|
||||
// Start timers (only run while app is visible, stopped in onHide)
|
||||
memoryTimer.start(); // Memory & PSRAM: every 10s
|
||||
tasksTimer.start(); // Tasks/CPU: every 15s
|
||||
}
|
||||
|
||||
void onHide(TT_UNUSED AppContext& app) override {
|
||||
memoryTimer.stop();
|
||||
tasksTimer.stop();
|
||||
}
|
||||
};
|
||||
|
||||
@ -300,4 +707,3 @@ extern const AppManifest manifest = {
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
#include <Tactility/Assets.h>
|
||||
#include <Tactility/app/AppManifest.h>
|
||||
#include <Tactility/app/timezone/TimeZone.h>
|
||||
#include <Tactility/lvgl/Toolbar.h>
|
||||
#include <Tactility/RecursiveMutex.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/service/loader/Loader.h>
|
||||
#include <Tactility/settings/Time.h>
|
||||
#include <Tactility/settings/SystemSettings.h>
|
||||
|
||||
#include <lvgl.h>
|
||||
|
||||
@ -16,6 +19,8 @@ extern const AppManifest manifest;
|
||||
class TimeDateSettingsApp final : public App {
|
||||
|
||||
RecursiveMutex mutex;
|
||||
lv_obj_t* timeZoneLabel = nullptr;
|
||||
lv_obj_t* dateFormatDropdown = nullptr;
|
||||
|
||||
static void onTimeFormatChanged(lv_event_t* event) {
|
||||
auto* widget = lv_event_get_target_obj(event);
|
||||
@ -23,6 +28,24 @@ class TimeDateSettingsApp final : public App {
|
||||
settings::setTimeFormat24Hour(show_24);
|
||||
}
|
||||
|
||||
static void onTimeZonePressed(lv_event_t* event) {
|
||||
timezone::start();
|
||||
}
|
||||
|
||||
static void onDateFormatChanged(lv_event_t* event) {
|
||||
auto* dropdown = static_cast<lv_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:
|
||||
|
||||
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_flex_grow(main_wrapper, 1);
|
||||
|
||||
// 24-hour format toggle
|
||||
|
||||
auto* time_format_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(time_format_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(time_format_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(time_format_wrapper, 0, 0);
|
||||
lv_obj_set_style_pad_all(time_format_wrapper, 8, 0);
|
||||
lv_obj_set_style_border_width(time_format_wrapper, 0, 0);
|
||||
|
||||
auto* time_24h_label = lv_label_create(time_format_wrapper);
|
||||
lv_label_set_text(time_24h_label, "24-hour clock");
|
||||
lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
lv_label_set_text(time_24h_label, "24-hour format");
|
||||
lv_obj_align(time_24h_label, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
|
||||
auto* time_24h_switch = lv_switch_create(time_format_wrapper);
|
||||
lv_obj_align(time_24h_switch, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
@ -54,6 +79,74 @@ public:
|
||||
} else {
|
||||
lv_obj_remove_state(time_24h_switch, LV_STATE_CHECKED);
|
||||
}
|
||||
|
||||
// Date format dropdown
|
||||
|
||||
auto* date_format_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(date_format_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(date_format_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(date_format_wrapper, 8, 0);
|
||||
lv_obj_set_style_border_width(date_format_wrapper, 0, 0);
|
||||
|
||||
auto* date_format_label = lv_label_create(date_format_wrapper);
|
||||
lv_label_set_text(date_format_label, "Date format");
|
||||
lv_obj_align(date_format_label, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
|
||||
dateFormatDropdown = lv_dropdown_create(date_format_wrapper);
|
||||
lv_obj_set_width(dateFormatDropdown, 150);
|
||||
lv_obj_align(dateFormatDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_dropdown_set_options(dateFormatDropdown, "MM/DD/YYYY\nDD/MM/YYYY\nYYYY-MM-DD\nYYYY/MM/DD");
|
||||
|
||||
settings::SystemSettings sysSettings;
|
||||
if (settings::loadSystemSettings(sysSettings)) {
|
||||
int index = 0;
|
||||
if (sysSettings.dateFormat == "DD/MM/YYYY") index = 1;
|
||||
else if (sysSettings.dateFormat == "YYYY-MM-DD") index = 2;
|
||||
else if (sysSettings.dateFormat == "YYYY/MM/DD") index = 3;
|
||||
lv_dropdown_set_selected(dateFormatDropdown, index);
|
||||
}
|
||||
lv_obj_add_event_cb(dateFormatDropdown, onDateFormatChanged, LV_EVENT_VALUE_CHANGED, nullptr);
|
||||
|
||||
// Timezone selector
|
||||
|
||||
auto* timezone_wrapper = lv_obj_create(main_wrapper);
|
||||
lv_obj_set_width(timezone_wrapper, LV_PCT(100));
|
||||
lv_obj_set_height(timezone_wrapper, LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_pad_all(timezone_wrapper, 8, 0);
|
||||
lv_obj_set_style_border_width(timezone_wrapper, 0, 0);
|
||||
|
||||
auto* timezone_label = lv_label_create(timezone_wrapper);
|
||||
lv_label_set_text(timezone_label, "Timezone");
|
||||
lv_obj_align(timezone_label, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
|
||||
auto* timezone_button = lv_button_create(timezone_wrapper);
|
||||
lv_obj_set_width(timezone_button, 150);
|
||||
lv_obj_align(timezone_button, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(timezone_button, onTimeZonePressed, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
|
||||
timeZoneLabel = lv_label_create(timezone_button);
|
||||
std::string timeZoneName = settings::getTimeZoneName();
|
||||
if (timeZoneName.empty()) {
|
||||
timeZoneName = "not set";
|
||||
}
|
||||
lv_obj_center(timeZoneLabel);
|
||||
lv_label_set_text(timeZoneLabel, timeZoneName.c_str());
|
||||
}
|
||||
|
||||
void onResult(AppContext& app, TT_UNUSED LaunchId launchId, Result result, std::unique_ptr<Bundle> 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
|
||||
|
||||
|
||||
@ -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) {
|
||||
TT_LOG_I(TAG, "Reading properties file %s", filePath.c_str());
|
||||
// Reading properties is a common operation; make this debug-level to avoid
|
||||
// flooding the serial console under frequent polling.
|
||||
TT_LOG_D(TAG, "Reading properties file %s", filePath.c_str());
|
||||
uint16_t line_count = 0;
|
||||
std::string key_prefix = "";
|
||||
// Malformed lines are skipped, valid lines are loaded and callback is called
|
||||
return readLines(filePath, true, [&key_prefix, &line_count, &filePath, &callback](const std::string& line) {
|
||||
line_count++;
|
||||
std::string key, value;
|
||||
auto trimmed_line = string::trim(line, " \t");
|
||||
// Trim all whitespace including \r\n (Windows line endings)
|
||||
auto trimmed_line = string::trim(line, " \t\r\n");
|
||||
if (!trimmed_line.starts_with("#") && !trimmed_line.empty()) {
|
||||
if (trimmed_line.starts_with("[")) {
|
||||
key_prefix = trimmed_line;
|
||||
@ -35,7 +39,8 @@ bool loadPropertiesFile(const std::string& filePath, std::function<void(const st
|
||||
std::string trimmed_value = string::trim(value, " \t");
|
||||
callback(trimmed_key, trimmed_value);
|
||||
} 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
90
Tactility/Source/service/displayidle/DisplayIdle.cpp
Normal file
90
Tactility/Source/service/displayidle/DisplayIdle.cpp
Normal file
@ -0,0 +1,90 @@
|
||||
#include <Tactility/CoreDefines.h>
|
||||
#include <Tactility/Timer.h>
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/service/ServiceContext.h>
|
||||
#include <Tactility/service/ServiceManifest.h>
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
#include <Tactility/settings/DisplaySettings.h>
|
||||
|
||||
namespace tt::service::displayidle {
|
||||
|
||||
constexpr auto* TAG = "DisplayIdle";
|
||||
|
||||
class DisplayIdleService final : public Service {
|
||||
|
||||
std::unique_ptr<Timer> timer;
|
||||
bool displayDimmed = false;
|
||||
settings::display::DisplaySettings cachedDisplaySettings;
|
||||
|
||||
static std::shared_ptr<hal::display::DisplayDevice> getDisplay() {
|
||||
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
|
||||
}
|
||||
|
||||
void tick() {
|
||||
// Settings are now cached and event-driven (no file I/O in timer callback!)
|
||||
// This prevents watchdog timeout from blocking the Timer Service task
|
||||
|
||||
// Query LVGL inactivity once for both checks
|
||||
uint32_t inactive_ms = 0;
|
||||
if (lvgl::lock(100)) {
|
||||
inactive_ms = lv_disp_get_inactive_time(nullptr);
|
||||
lvgl::unlock();
|
||||
}
|
||||
|
||||
// Handle display backlight
|
||||
auto display = getDisplay();
|
||||
if (display != nullptr && display->supportsBacklightDuty()) {
|
||||
// If timeout disabled, ensure backlight restored if we had dimmed it
|
||||
if (!cachedDisplaySettings.backlightTimeoutEnabled || cachedDisplaySettings.backlightTimeoutMs == 0) {
|
||||
if (displayDimmed) {
|
||||
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
||||
displayDimmed = false;
|
||||
}
|
||||
} else {
|
||||
if (!displayDimmed && inactive_ms >= cachedDisplaySettings.backlightTimeoutMs) {
|
||||
display->setBacklightDuty(0);
|
||||
displayDimmed = true;
|
||||
} else if (displayDimmed && inactive_ms < 100) {
|
||||
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
||||
displayDimmed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
bool onStart(TT_UNUSED ServiceContext& service) override {
|
||||
// Load settings once at startup and cache them
|
||||
// This eliminates file I/O from timer callback (prevents watchdog timeout)
|
||||
cachedDisplaySettings = settings::display::loadOrGetDefault();
|
||||
|
||||
// Note: Settings changes require service restart to take effect
|
||||
// TODO: Add DisplaySettingsChanged events for dynamic updates
|
||||
|
||||
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::millisToTicks(250), [this]{ this->tick(); });
|
||||
timer->setCallbackPriority(Thread::Priority::Lower);
|
||||
timer->start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void onStop(TT_UNUSED ServiceContext& service) override {
|
||||
if (timer) {
|
||||
timer->stop();
|
||||
timer = nullptr;
|
||||
}
|
||||
// Ensure display restored on stop
|
||||
auto display = getDisplay();
|
||||
if (display && displayDimmed) {
|
||||
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
||||
displayDimmed = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const ServiceManifest manifest = {
|
||||
.id = "DisplayIdle",
|
||||
.createService = create<DisplayIdleService>
|
||||
};
|
||||
|
||||
}
|
||||
@ -35,18 +35,29 @@ static bool disableWifiService() {
|
||||
}
|
||||
|
||||
bool initWifi(const EspNowConfig& config) {
|
||||
// ESP-NOW can coexist with WiFi STA mode; only preserve WiFi state if already connected
|
||||
auto wifi_state = wifi::getRadioState();
|
||||
bool wifi_was_connected = (wifi_state == wifi::RadioState::ConnectionActive);
|
||||
|
||||
// If WiFi is off or in other states, temporarily disable it to initialize ESP-NOW
|
||||
// If WiFi is already connected, keep it running and just add ESP-NOW on top
|
||||
if (!wifi_was_connected && wifi_state != wifi::RadioState::Off && wifi_state != wifi::RadioState::OffPending) {
|
||||
if (!disableWifiService()) {
|
||||
TT_LOG_E(TAG, "Failed to disable wifi");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
wifi_mode_t mode;
|
||||
if (config.mode == Mode::Station) {
|
||||
// Use STA mode to allow coexistence with normal WiFi connection
|
||||
mode = wifi_mode_t::WIFI_MODE_STA;
|
||||
} else {
|
||||
mode = wifi_mode_t::WIFI_MODE_AP;
|
||||
}
|
||||
|
||||
// Only reinitialize WiFi if it's not already running
|
||||
if (wifi::getRadioState() == wifi::RadioState::Off) {
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
if (esp_wifi_init(&cfg) != ESP_OK) {
|
||||
TT_LOG_E(TAG, "esp_wifi_init() failed");
|
||||
@ -67,6 +78,7 @@ bool initWifi(const EspNowConfig& config) {
|
||||
TT_LOG_E(TAG, "esp_wifi_start() failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (esp_wifi_set_channel(config.channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
|
||||
TT_LOG_E(TAG, "esp_wifi_set_channel() failed");
|
||||
@ -86,23 +98,19 @@ bool initWifi(const EspNowConfig& config) {
|
||||
}
|
||||
}
|
||||
|
||||
TT_LOG_I(TAG, "WiFi initialized for ESP-NOW (preserved existing connection: %s)", wifi_was_connected ? "yes" : "no");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool deinitWifi() {
|
||||
if (esp_wifi_stop() != ESP_OK) {
|
||||
TT_LOG_E(TAG, "Failed to stop radio");
|
||||
return false;
|
||||
}
|
||||
// Don't deinitialize WiFi completely - just disable ESP-NOW
|
||||
// This allows normal WiFi connection to continue
|
||||
// Only stop/deinit if WiFi was originally off
|
||||
|
||||
if (esp_wifi_set_mode(WIFI_MODE_NULL) != ESP_OK) {
|
||||
TT_LOG_E(TAG, "Failed to unset mode");
|
||||
}
|
||||
|
||||
if (esp_wifi_deinit() != ESP_OK) {
|
||||
TT_LOG_E(TAG, "Failed to deinit");
|
||||
}
|
||||
// Since we're only using WiFi for ESP-NOW, we can safely keep it in a minimal state
|
||||
// or shut it down. For now, keep it running to support STA + ESP-NOW coexistence.
|
||||
|
||||
TT_LOG_I(TAG, "ESP-NOW WiFi deinitialized (WiFi service continues independently)");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
98
Tactility/Source/service/keyboardidle/KeyboardIdle.cpp
Normal file
98
Tactility/Source/service/keyboardidle/KeyboardIdle.cpp
Normal file
@ -0,0 +1,98 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/CoreDefines.h>
|
||||
#include <Tactility/hal/keyboard/KeyboardDevice.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/service/ServiceContext.h>
|
||||
#include <Tactility/service/ServiceManifest.h>
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
#include <Tactility/settings/KeyboardSettings.h>
|
||||
#include <Tactility/Timer.h>
|
||||
|
||||
namespace keyboardbacklight {
|
||||
bool setBrightness(uint8_t brightness);
|
||||
}
|
||||
|
||||
namespace tt::service::keyboardidle {
|
||||
|
||||
constexpr auto* TAG = "KeyboardIdle";
|
||||
|
||||
class KeyboardIdleService final : public Service {
|
||||
|
||||
std::unique_ptr<Timer> timer;
|
||||
bool keyboardDimmed = false;
|
||||
settings::keyboard::KeyboardSettings cachedKeyboardSettings;
|
||||
|
||||
static std::shared_ptr<hal::keyboard::KeyboardDevice> getKeyboard() {
|
||||
return hal::findFirstDevice<hal::keyboard::KeyboardDevice>(hal::Device::Type::Keyboard);
|
||||
}
|
||||
|
||||
void tick() {
|
||||
// Settings are now cached and event-driven (no file I/O in timer callback!)
|
||||
// This prevents watchdog timeout from blocking the Timer Service task
|
||||
|
||||
// Query LVGL inactivity once for both checks
|
||||
uint32_t inactive_ms = 0;
|
||||
if (lvgl::lock(100)) {
|
||||
inactive_ms = lv_disp_get_inactive_time(nullptr);
|
||||
lvgl::unlock();
|
||||
}
|
||||
|
||||
// Handle keyboard backlight
|
||||
auto keyboard = getKeyboard();
|
||||
if (keyboard != nullptr && keyboard->isAttached()) {
|
||||
// If timeout disabled, ensure backlight restored if we had dimmed it
|
||||
if (!cachedKeyboardSettings.backlightTimeoutEnabled || cachedKeyboardSettings.backlightTimeoutMs == 0) {
|
||||
if (keyboardDimmed) {
|
||||
keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0);
|
||||
keyboardDimmed = false;
|
||||
}
|
||||
} else {
|
||||
if (!keyboardDimmed && inactive_ms >= cachedKeyboardSettings.backlightTimeoutMs) {
|
||||
keyboardbacklight::setBrightness(0);
|
||||
keyboardDimmed = true;
|
||||
} else if (keyboardDimmed && inactive_ms < 100) {
|
||||
keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0);
|
||||
keyboardDimmed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
bool onStart(TT_UNUSED ServiceContext& service) override {
|
||||
// Load settings once at startup and cache them
|
||||
// This eliminates file I/O from timer callback (prevents watchdog timeout)
|
||||
cachedKeyboardSettings = settings::keyboard::loadOrGetDefault();
|
||||
|
||||
// Note: Settings changes require service restart to take effect
|
||||
// TODO: Add KeyboardSettingsChanged events for dynamic updates
|
||||
|
||||
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::millisToTicks(250), [this]{ this->tick(); });
|
||||
timer->setCallbackPriority(Thread::Priority::Lower);
|
||||
timer->start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void onStop(TT_UNUSED ServiceContext& service) override {
|
||||
if (timer) {
|
||||
timer->stop();
|
||||
timer = nullptr;
|
||||
}
|
||||
// Ensure keyboard restored on stop
|
||||
auto keyboard = getKeyboard();
|
||||
if (keyboard && keyboardDimmed) {
|
||||
keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0);
|
||||
keyboardDimmed = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const ServiceManifest manifest = {
|
||||
.id = "KeyboardIdle",
|
||||
.createService = create<KeyboardIdleService>
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@ -15,6 +15,8 @@ constexpr auto* SETTINGS_FILE = "/data/settings/display.properties";
|
||||
constexpr auto* SETTINGS_KEY_ORIENTATION = "orientation";
|
||||
constexpr auto* SETTINGS_KEY_GAMMA_CURVE = "gammaCurve";
|
||||
constexpr auto* SETTINGS_KEY_BACKLIGHT_DUTY = "backlightDuty";
|
||||
constexpr auto* SETTINGS_KEY_TIMEOUT_ENABLED = "backlightTimeoutEnabled";
|
||||
constexpr auto* SETTINGS_KEY_TIMEOUT_MS = "backlightTimeoutMs";
|
||||
|
||||
static Orientation getDefaultOrientation() {
|
||||
auto* display = lv_display_get_default();
|
||||
@ -90,9 +92,23 @@ bool load(DisplaySettings& settings) {
|
||||
}
|
||||
}
|
||||
|
||||
bool timeout_enabled = true;
|
||||
auto timeout_enabled_entry = map.find(SETTINGS_KEY_TIMEOUT_ENABLED);
|
||||
if (timeout_enabled_entry != map.end()) {
|
||||
timeout_enabled = (timeout_enabled_entry->second == "1" || timeout_enabled_entry->second == "true" || timeout_enabled_entry->second == "True");
|
||||
}
|
||||
|
||||
uint32_t timeout_ms = 60000; // default 60s
|
||||
auto timeout_ms_entry = map.find(SETTINGS_KEY_TIMEOUT_MS);
|
||||
if (timeout_ms_entry != map.end()) {
|
||||
timeout_ms = static_cast<uint32_t>(std::strtoul(timeout_ms_entry->second.c_str(), nullptr, 10));
|
||||
}
|
||||
|
||||
settings.orientation = orientation;
|
||||
settings.gammaCurve = gamma_curve;
|
||||
settings.backlightDuty = backlight_duty;
|
||||
settings.backlightTimeoutEnabled = timeout_enabled;
|
||||
settings.backlightTimeoutMs = timeout_ms;
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -101,7 +117,9 @@ DisplaySettings getDefault() {
|
||||
return DisplaySettings {
|
||||
.orientation = getDefaultOrientation(),
|
||||
.gammaCurve = 1,
|
||||
.backlightDuty = 200
|
||||
.backlightDuty = 200,
|
||||
.backlightTimeoutEnabled = true,
|
||||
.backlightTimeoutMs = 60000
|
||||
};
|
||||
}
|
||||
|
||||
@ -118,6 +136,8 @@ bool save(const DisplaySettings& settings) {
|
||||
map[SETTINGS_KEY_BACKLIGHT_DUTY] = std::to_string(settings.backlightDuty);
|
||||
map[SETTINGS_KEY_GAMMA_CURVE] = std::to_string(settings.gammaCurve);
|
||||
map[SETTINGS_KEY_ORIENTATION] = toString(settings.orientation);
|
||||
map[SETTINGS_KEY_TIMEOUT_ENABLED] = settings.backlightTimeoutEnabled ? "1" : "0";
|
||||
map[SETTINGS_KEY_TIMEOUT_MS] = std::to_string(settings.backlightTimeoutMs);
|
||||
return file::savePropertiesFile(SETTINGS_FILE, map);
|
||||
}
|
||||
|
||||
|
||||
65
Tactility/Source/settings/KeyboardSettings.cpp
Normal file
65
Tactility/Source/settings/KeyboardSettings.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -40,6 +40,25 @@ static bool loadSystemSettingsFromFile(SystemSettings& properties) {
|
||||
bool time_format_24h = time_format_entry == map.end() ? true : (time_format_entry->second == "true");
|
||||
properties.timeFormat24h = time_format_24h;
|
||||
|
||||
// Load date format
|
||||
// Default to MM/DD/YYYY if missing (backward compat with older system.properties)
|
||||
auto date_format_entry = map.find("dateFormat");
|
||||
if (date_format_entry != map.end() && !date_format_entry->second.empty()) {
|
||||
properties.dateFormat = date_format_entry->second;
|
||||
} else {
|
||||
TT_LOG_I(TAG, "dateFormat missing or empty, using default MM/DD/YYYY (likely from older system.properties)");
|
||||
properties.dateFormat = "MM/DD/YYYY";
|
||||
}
|
||||
|
||||
// Load region
|
||||
auto region_entry = map.find("region");
|
||||
if (region_entry != map.end() && !region_entry->second.empty()) {
|
||||
properties.region = region_entry->second;
|
||||
} else {
|
||||
TT_LOG_I(TAG, "region missing or empty, using default US");
|
||||
properties.region = "US";
|
||||
}
|
||||
|
||||
TT_LOG_I(TAG, "System settings loaded");
|
||||
return true;
|
||||
}
|
||||
@ -61,12 +80,15 @@ bool saveSystemSettings(const SystemSettings& properties) {
|
||||
std::map<std::string, std::string> map;
|
||||
map["language"] = toString(properties.language);
|
||||
map["timeFormat24h"] = properties.timeFormat24h ? "true" : "false";
|
||||
map["dateFormat"] = properties.dateFormat;
|
||||
map["region"] = properties.region;
|
||||
|
||||
if (!file::savePropertiesFile(file_path, map)) {
|
||||
TT_LOG_E(TAG, "Failed to save %s", file_path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update local cache
|
||||
cachedSettings = properties;
|
||||
cached = true;
|
||||
return true;
|
||||
|
||||
@ -45,7 +45,7 @@ std::string getTimeZoneName() {
|
||||
if (preferences.optString(TIMEZONE_PREFERENCES_KEY_NAME, result)) {
|
||||
return result;
|
||||
} else {
|
||||
return {};
|
||||
return "America/Los_Angeles"; // Default: Pacific Time (PST/PDT)
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ std::string getTimeZoneCode() {
|
||||
if (preferences.optString(TIMEZONE_PREFERENCES_KEY_CODE, result)) {
|
||||
return result;
|
||||
} else {
|
||||
return {};
|
||||
return "PST8PDT,M3.2.0,M11.1.0"; // Default: Pacific Time POSIX string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -351,6 +351,7 @@ const esp_elfsym main_symbols[] {
|
||||
ESP_ELFSYM_EXPORT(lv_event_get_target_obj),
|
||||
ESP_ELFSYM_EXPORT(lv_event_get_target),
|
||||
ESP_ELFSYM_EXPORT(lv_event_get_current_target_obj),
|
||||
ESP_ELFSYM_EXPORT(lv_event_get_draw_task),
|
||||
// lv_obj
|
||||
ESP_ELFSYM_EXPORT(lv_color_hex),
|
||||
ESP_ELFSYM_EXPORT(lv_color_make),
|
||||
@ -448,6 +449,11 @@ const esp_elfsym main_symbols[] {
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_size),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_width),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_height),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_send_event),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_outline_color),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_outline_width),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_outline_pad),
|
||||
ESP_ELFSYM_EXPORT(lv_obj_set_style_outline_opa),
|
||||
// lv_font
|
||||
ESP_ELFSYM_EXPORT(lv_font_get_default),
|
||||
// lv_theme
|
||||
@ -552,6 +558,7 @@ const esp_elfsym main_symbols[] {
|
||||
ESP_ELFSYM_EXPORT(lv_display_get_vertical_resolution),
|
||||
ESP_ELFSYM_EXPORT(lv_display_get_physical_horizontal_resolution),
|
||||
ESP_ELFSYM_EXPORT(lv_display_get_physical_vertical_resolution),
|
||||
ESP_ELFSYM_EXPORT(lv_display_dpx),
|
||||
// lv_pct
|
||||
ESP_ELFSYM_EXPORT(lv_pct),
|
||||
ESP_ELFSYM_EXPORT(lv_pct_to_px),
|
||||
@ -578,6 +585,7 @@ const esp_elfsym main_symbols[] {
|
||||
ESP_ELFSYM_EXPORT(lv_indev_get_key),
|
||||
ESP_ELFSYM_EXPORT(lv_indev_get_gesture_dir),
|
||||
ESP_ELFSYM_EXPORT(lv_indev_get_state),
|
||||
ESP_ELFSYM_EXPORT(lv_indev_active),
|
||||
// lv_timer
|
||||
ESP_ELFSYM_EXPORT(lv_timer_handler),
|
||||
ESP_ELFSYM_EXPORT(lv_timer_handler_run_in_period),
|
||||
@ -606,6 +614,18 @@ const esp_elfsym main_symbols[] {
|
||||
ESP_ELFSYM_EXPORT(lv_line_create),
|
||||
ESP_ELFSYM_EXPORT(lv_line_set_points),
|
||||
ESP_ELFSYM_EXPORT(lv_line_set_points_mutable),
|
||||
// lv_group
|
||||
ESP_ELFSYM_EXPORT(lv_group_remove_obj),
|
||||
// lv_mem
|
||||
ESP_ELFSYM_EXPORT(lv_free),
|
||||
ESP_ELFSYM_EXPORT(lv_malloc),
|
||||
// lv_draw
|
||||
ESP_ELFSYM_EXPORT(lv_draw_task_get_draw_dsc),
|
||||
ESP_ELFSYM_EXPORT(lv_draw_task_get_label_dsc),
|
||||
ESP_ELFSYM_EXPORT(lv_draw_task_get_fill_dsc),
|
||||
// lv_image
|
||||
ESP_ELFSYM_EXPORT(lv_image_create),
|
||||
ESP_ELFSYM_EXPORT(lv_image_set_src),
|
||||
// stdio.h
|
||||
ESP_ELFSYM_EXPORT(rename),
|
||||
// dirent.h
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user