Feature additions (#434)
Lots of things "ported" over from the "enhanced" fork. With some adjustments here and there. KeyboardBacklight driver (for T-Deck only currently) Trackball driver (for T-Deck only currently) Keyboard backlight sleep/wake (for T-Deck only currently...also requires keyboard firmware update) Display sleep/wake Files - create file/folder Keyboard settings (for T-Deck only currently) Time & Date settings tweaks Locale settings tweaks Systeminfo additions Espnow wifi coexist initI2cDevices - moved to T-deck init.cpp / initBoot KeyboardInitService - removed, moved to T-deck init.cpp / initBoot Adjusted TIMER_UPDATE_INTERVAL to 2 seconds. Added lock to ActionCreateFolder Maybe missed some things in the list. Display wake could do with some kind of block on wake first touch to prevent UI elements being hit when waking device with touch. Same with encoder/trackball/keyboard press i guess. The original code was written by @cscott0108 at https://github.com/cscott0108/tactility-enhanced-t-deck
This commit is contained in:
@@ -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 */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
#include <Tactility/StringUtils.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <Tactility/file/FileLock.h>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
@@ -62,6 +64,16 @@ static void onNavigateUpPressedCallback(TT_UNUSED lv_event_t* event) {
|
||||
view->onNavigateUpPressed();
|
||||
}
|
||||
|
||||
static void onNewFilePressedCallback(lv_event_t* event) {
|
||||
auto* view = static_cast<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 +191,38 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
|
||||
} else {
|
||||
symbol = LV_SYMBOL_FILE;
|
||||
}
|
||||
lv_obj_t* button = lv_list_add_button(list, symbol, dir_entry.d_name);
|
||||
|
||||
// Get file size for regular files
|
||||
std::string label_text = dir_entry.d_name;
|
||||
if (dir_entry.d_type == file::TT_DT_REG) {
|
||||
std::string file_path = file::getChildPath(state->getCurrentPath(), dir_entry.d_name);
|
||||
struct stat st;
|
||||
if (stat(file_path.c_str(), &st) == 0) {
|
||||
// Format file size in human-readable format
|
||||
const char* size_suffix;
|
||||
double size;
|
||||
if (st.st_size < 1024) {
|
||||
size = st.st_size;
|
||||
size_suffix = " B";
|
||||
} else if (st.st_size < 1024 * 1024) {
|
||||
size = st.st_size / 1024.0;
|
||||
size_suffix = " KB";
|
||||
} else {
|
||||
size = st.st_size / (1024.0 * 1024.0);
|
||||
size_suffix = " MB";
|
||||
}
|
||||
|
||||
char size_str[32];
|
||||
if (st.st_size < 1024) {
|
||||
snprintf(size_str, sizeof(size_str), " (%d%s)", (int)size, size_suffix);
|
||||
} else {
|
||||
snprintf(size_str, sizeof(size_str), " (%.1f%s)", size, size_suffix);
|
||||
}
|
||||
label_text += size_str;
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t* button = lv_list_add_button(list, symbol, label_text.c_str());
|
||||
lv_obj_add_event_cb(button, &onDirEntryPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
lv_obj_add_event_cb(button, &onDirEntryLongPressedCallback, LV_EVENT_LONG_PRESSED, this);
|
||||
}
|
||||
@@ -212,6 +255,18 @@ void View::onDeletePressed() {
|
||||
alertdialog::start("Are you sure?", message, choices);
|
||||
}
|
||||
|
||||
void View::onNewFilePressed() {
|
||||
TT_LOG_I(TAG, "Creating new file");
|
||||
state->setPendingAction(State::ActionCreateFile);
|
||||
inputdialog::start("New File", "Enter filename:", "");
|
||||
}
|
||||
|
||||
void View::onNewFolderPressed() {
|
||||
TT_LOG_I(TAG, "Creating new folder");
|
||||
state->setPendingAction(State::ActionCreateFolder);
|
||||
inputdialog::start("New Folder", "Enter folder name:", "");
|
||||
}
|
||||
|
||||
void View::showActionsForDirectory() {
|
||||
lv_obj_clean(action_list);
|
||||
|
||||
@@ -262,6 +317,8 @@ void View::init(const AppContext& appContext, lv_obj_t* parent) {
|
||||
|
||||
auto* toolbar = lvgl::toolbar_create(parent, appContext);
|
||||
navigate_up_button = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_UP, &onNavigateUpPressedCallback, this);
|
||||
new_file_button = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_FILE, &onNewFilePressedCallback, this);
|
||||
new_folder_button = lvgl::toolbar_add_image_button_action(toolbar, LV_SYMBOL_DIRECTORY, &onNewFolderPressedCallback, this);
|
||||
|
||||
auto* wrapper = lv_obj_create(parent);
|
||||
lv_obj_set_width(wrapper, LV_PCT(100));
|
||||
@@ -354,6 +411,62 @@ void View::onResult(LaunchId launchId, Result result, std::unique_ptr<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;
|
||||
}
|
||||
|
||||
@@ -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, []() {
|
||||
auto app = optApp();
|
||||
if (app) {
|
||||
auto lock = lvgl::getSyncLock()->asScopedLock();
|
||||
lock.lock();
|
||||
app->updateMemory();
|
||||
app->updatePsram();
|
||||
}
|
||||
});
|
||||
|
||||
Timer tasksTimer = Timer(Timer::Type::Periodic, []() {
|
||||
auto app = optApp();
|
||||
if (app) {
|
||||
auto lock = lvgl::getSyncLock()->asScopedLock();
|
||||
lock.lock();
|
||||
app->updateTasks();
|
||||
}
|
||||
});
|
||||
|
||||
MemoryBarWidgets internalMemBar;
|
||||
MemoryBarWidgets externalMemBar;
|
||||
MemoryBarWidgets dataStorageBar;
|
||||
MemoryBarWidgets sdcardStorageBar;
|
||||
MemoryBarWidgets systemStorageBar;
|
||||
|
||||
lv_obj_t* tasksContainer = nullptr;
|
||||
lv_obj_t* cpuContainer = nullptr;
|
||||
lv_obj_t* psramContainer = nullptr;
|
||||
lv_obj_t* cpuSummaryLabel = nullptr; // Shows overall CPU utilization
|
||||
lv_obj_t* taskCountLabel = nullptr; // Shows active task count
|
||||
lv_obj_t* uptimeLabel = nullptr; // Shows system uptime
|
||||
|
||||
bool hasExternalMem = false;
|
||||
bool hasDataStorage = false;
|
||||
bool hasSdcardStorage = false;
|
||||
bool hasSystemStorage = false;
|
||||
|
||||
void updateMemory() {
|
||||
updateMemoryBar(internalMemBar, getHeapFree(), getHeapTotal());
|
||||
|
||||
if (hasExternalMem) {
|
||||
updateMemoryBar(externalMemBar, getSpiFree(), getSpiTotal());
|
||||
}
|
||||
}
|
||||
|
||||
void updateStorage() {
|
||||
#ifdef ESP_PLATFORM
|
||||
uint64_t storage_total = 0;
|
||||
uint64_t storage_free = 0;
|
||||
|
||||
if (hasDataStorage) {
|
||||
if (esp_vfs_fat_info(file::MOUNT_POINT_DATA, &storage_total, &storage_free) == ESP_OK) {
|
||||
updateMemoryBar(dataStorageBar, storage_free, storage_total);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSdcardStorage) {
|
||||
const auto sdcard_devices = hal::findDevices<hal::sdcard::SdCardDevice>(hal::Device::Type::SdCard);
|
||||
for (const auto& sdcard : sdcard_devices) {
|
||||
if (sdcard->isMounted() && esp_vfs_fat_info(sdcard->getMountPath().c_str(), &storage_total, &storage_free) == ESP_OK) {
|
||||
updateMemoryBar(sdcardStorageBar, storage_free, storage_total);
|
||||
break; // Only update first SD card
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasSystemStorage) {
|
||||
if (esp_vfs_fat_info(file::MOUNT_POINT_SYSTEM, &storage_total, &storage_free) == ESP_OK) {
|
||||
updateMemoryBar(systemStorageBar, storage_free, storage_total);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void updateTasks() {
|
||||
#if configUSE_TRACE_FACILITY
|
||||
if (tasksContainer) {
|
||||
updateRtosTasks(tasksContainer, false); // Tasks tab: show state
|
||||
}
|
||||
|
||||
if (cpuContainer) {
|
||||
updateRtosTasks(cpuContainer, true); // CPU tab: show percentages
|
||||
|
||||
// Update CPU summary at top of tab
|
||||
// Note: FreeRTOS runtime stats accumulate since boot, so percentages
|
||||
// are averages over entire uptime, not instantaneous usage
|
||||
if (cpuSummaryLabel && taskCountLabel && uptimeLabel) {
|
||||
UBaseType_t count = uxTaskGetNumberOfTasks();
|
||||
auto* tasks = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * count);
|
||||
if (tasks) {
|
||||
uint32_t totalRuntime = 0;
|
||||
UBaseType_t actual = uxTaskGetSystemState(tasks, count, &totalRuntime);
|
||||
|
||||
if (totalRuntime > 0 && actual > 0) {
|
||||
// Calculate total CPU usage (100% - idle = usage)
|
||||
uint32_t idleTime = 0;
|
||||
for (int i = 0; i < actual; ++i) {
|
||||
const char* name = tasks[i].pcTaskName;
|
||||
if (name && (strcmp(name, "IDLE0") == 0 || strcmp(name, "IDLE1") == 0)) {
|
||||
idleTime += tasks[i].ulRunTimeCounter;
|
||||
}
|
||||
}
|
||||
|
||||
float cpuUsage = ((totalRuntime - idleTime) * 100.0f) / totalRuntime;
|
||||
auto summary_text = std::format("Overall CPU Usage: {:.1f}% (avg since boot)", cpuUsage);
|
||||
lv_label_set_text(cpuSummaryLabel, summary_text.c_str());
|
||||
|
||||
// Show total task count
|
||||
auto core_text = std::format("Active Tasks: {} total", actual);
|
||||
lv_label_set_text(taskCountLabel, core_text.c_str());
|
||||
|
||||
// Use actual system tick count for uptime
|
||||
TickType_t ticks = xTaskGetTickCount();
|
||||
float uptime_sec = static_cast<float>(ticks) / configTICK_RATE_HZ;
|
||||
auto uptime_text = std::format("System Uptime: {:.1f} min", uptime_sec / 60.0f);
|
||||
lv_label_set_text(uptimeLabel, uptime_text.c_str());
|
||||
} else {
|
||||
lv_label_set_text(cpuSummaryLabel, "Overall CPU Usage: --.-%");
|
||||
lv_label_set_text(taskCountLabel, "Active Tasks: --");
|
||||
lv_label_set_text(uptimeLabel, "System Uptime: --");
|
||||
}
|
||||
|
||||
free(tasks);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void updatePsram() {
|
||||
#ifdef ESP_PLATFORM
|
||||
if (!psramContainer || !hasExternalMem) return;
|
||||
|
||||
clearContainer(psramContainer);
|
||||
|
||||
size_t free_mem = getSpiFree();
|
||||
size_t total = getSpiTotal();
|
||||
size_t used = total - free_mem;
|
||||
size_t min_free = getPsramMinFree();
|
||||
size_t largest_block = getPsramLargestBlock();
|
||||
size_t peak_usage = total - min_free;
|
||||
|
||||
// Safety check - if no PSRAM, show error
|
||||
if (total == 0) {
|
||||
auto* error_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(error_label, "No PSRAM detected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Summary
|
||||
auto* summary_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(summary_label, "PSRAM Usage Summary");
|
||||
lv_obj_set_style_text_font(summary_label, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_pad_bottom(summary_label, 8, 0);
|
||||
|
||||
// Current usage
|
||||
auto* usage_label = lv_label_create(psramContainer);
|
||||
float used_mb = used / (1024.0f * 1024.0f);
|
||||
float total_mb = total / (1024.0f * 1024.0f);
|
||||
float used_percent = (used * 100.0f) / total;
|
||||
auto usage_text = std::format("Current: {:.2f} / {:.2f} MB ({:.1f}% used)",
|
||||
used_mb, total_mb, used_percent);
|
||||
lv_label_set_text(usage_label, usage_text.c_str());
|
||||
|
||||
// Peak usage
|
||||
auto* peak_label = lv_label_create(psramContainer);
|
||||
float peak_mb = peak_usage / (1024.0f * 1024.0f);
|
||||
float peak_percent = (peak_usage * 100.0f) / total;
|
||||
auto peak_text = std::format("Peak: {:.2f} MB ({:.1f}% of total)",
|
||||
peak_mb, peak_percent);
|
||||
lv_label_set_text(peak_label, peak_text.c_str());
|
||||
|
||||
// Minimum free (lowest point)
|
||||
auto* min_free_label = lv_label_create(psramContainer);
|
||||
float min_free_mb = min_free / (1024.0f * 1024.0f);
|
||||
auto min_free_text = std::format("Min Free: {:.2f} MB", min_free_mb);
|
||||
lv_label_set_text(min_free_label, min_free_text.c_str());
|
||||
|
||||
// Largest contiguous block
|
||||
auto* largest_label = lv_label_create(psramContainer);
|
||||
float largest_mb = largest_block / (1024.0f * 1024.0f);
|
||||
auto largest_text = std::format("Largest Block: {:.2f} MB", largest_mb);
|
||||
lv_label_set_text(largest_label, largest_text.c_str());
|
||||
|
||||
// Spacer
|
||||
auto* spacer = lv_obj_create(psramContainer);
|
||||
lv_obj_set_size(spacer, LV_PCT(100), 16);
|
||||
lv_obj_set_style_bg_opa(spacer, 0, 0);
|
||||
lv_obj_set_style_border_width(spacer, 0, 0);
|
||||
|
||||
// PSRAM Configuration section
|
||||
auto* config_header = lv_label_create(psramContainer);
|
||||
lv_label_set_text(config_header, "PSRAM Configuration");
|
||||
lv_obj_set_style_text_font(config_header, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_pad_bottom(config_header, 8, 0);
|
||||
|
||||
// Get threshold from sdkconfig
|
||||
#ifdef CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL
|
||||
const int threshold = CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL;
|
||||
#else
|
||||
const int threshold = 16384; // Default ESP-IDF value
|
||||
#endif
|
||||
|
||||
// Display threshold configuration
|
||||
auto* threshold_info = lv_label_create(psramContainer);
|
||||
if (threshold >= 1024) {
|
||||
lv_label_set_text_fmt(threshold_info, "• Threshold: >=%d KB -> PSRAM", threshold / 1024);
|
||||
} else {
|
||||
lv_label_set_text_fmt(threshold_info, "• Threshold: >=%d bytes -> PSRAM", threshold);
|
||||
}
|
||||
|
||||
auto* internal_info = lv_label_create(psramContainer);
|
||||
if (threshold >= 1024) {
|
||||
lv_label_set_text_fmt(internal_info, "• Allocations <%d KB -> Internal RAM", threshold / 1024);
|
||||
} else {
|
||||
lv_label_set_text_fmt(internal_info, "• Allocations <%d bytes -> Internal RAM", threshold);
|
||||
}
|
||||
|
||||
auto* note_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(note_label, "• DMA buffers always use Internal RAM");
|
||||
|
||||
// Spacer after config
|
||||
auto* spacer_config = lv_obj_create(psramContainer);
|
||||
lv_obj_set_size(spacer_config, LV_PCT(100), 16);
|
||||
lv_obj_set_style_bg_opa(spacer_config, 0, 0);
|
||||
lv_obj_set_style_border_width(spacer_config, 0, 0);
|
||||
|
||||
// Known PSRAM consumers header
|
||||
auto* consumers_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(consumers_label, "PSRAM Allocation Strategy");
|
||||
lv_obj_set_style_text_font(consumers_label, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_pad_bottom(consumers_label, 8, 0);
|
||||
|
||||
// Explain what's in PSRAM
|
||||
auto* strategy_note = lv_label_create(psramContainer);
|
||||
lv_label_set_text(strategy_note, "Apps don't pre-allocate to PSRAM.\nThey use LVGL dynamic allocation:");
|
||||
lv_obj_set_style_text_color(strategy_note, lv_palette_main(LV_PALETTE_GREY), 0);
|
||||
|
||||
// List what automatically goes to PSRAM
|
||||
auto* lvgl_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(lvgl_label, "• All LVGL widgets (buttons, labels, etc.)");
|
||||
|
||||
auto* framebuffer_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(framebuffer_label, "• Display framebuffers");
|
||||
|
||||
auto* wifi_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(wifi_label, "• WiFi/Network buffers");
|
||||
|
||||
auto* file_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(file_label, "• File I/O buffers");
|
||||
|
||||
auto* task_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(task_label, "• Task stacks (when enabled)");
|
||||
|
||||
auto* general_label = lv_label_create(psramContainer);
|
||||
if (threshold >= 1024) {
|
||||
lv_label_set_text_fmt(general_label, "• All allocations >=%d KB", threshold / 1024);
|
||||
} else {
|
||||
lv_label_set_text_fmt(general_label, "• All allocations >=%d bytes", threshold);
|
||||
}
|
||||
|
||||
// Spacer
|
||||
auto* spacer_apps = lv_obj_create(psramContainer);
|
||||
lv_obj_set_size(spacer_apps, LV_PCT(100), 16);
|
||||
lv_obj_set_style_bg_opa(spacer_apps, 0, 0);
|
||||
lv_obj_set_style_border_width(spacer_apps, 0, 0);
|
||||
|
||||
// App behavior explanation
|
||||
auto* app_behavior_label = lv_label_create(psramContainer);
|
||||
lv_label_set_text(app_behavior_label, "App Memory Behavior");
|
||||
lv_obj_set_style_text_font(app_behavior_label, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_pad_bottom(app_behavior_label, 8, 0);
|
||||
|
||||
auto* app_note1 = lv_label_create(psramContainer);
|
||||
lv_label_set_text(app_note1, "• Apps allocate UI when opened (10-50 KB)");
|
||||
|
||||
auto* app_note2 = lv_label_create(psramContainer);
|
||||
lv_label_set_text(app_note2, "• All app UI goes to PSRAM automatically");
|
||||
|
||||
auto* app_note3 = lv_label_create(psramContainer);
|
||||
lv_label_set_text(app_note3, "• Apps deallocate when closed (no caching)");
|
||||
|
||||
auto* app_note4 = lv_label_create(psramContainer);
|
||||
lv_label_set_text(app_note4, "• One app open at a time = 10-50 KB in PSRAM");
|
||||
#endif
|
||||
}
|
||||
|
||||
void onShow(AppContext& app, lv_obj_t* parent) override {
|
||||
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(kernel::millisToTicks(10000)); // Memory & PSRAM: every 10s
|
||||
tasksTimer.start(kernel::millisToTicks(15000)); // Tasks/CPU: every 15s
|
||||
}
|
||||
|
||||
void onHide(TT_UNUSED AppContext& app) override {
|
||||
memoryTimer.stop();
|
||||
tasksTimer.stop();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -300,4 +707,3 @@ extern const AppManifest manifest = {
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
#include <Tactility/service/ServiceManifest.h>
|
||||
#include <Tactility/service/ServiceContext.h>
|
||||
#include <Tactility/Timer.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/settings/DisplaySettings.h>
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
|
||||
namespace tt::service::displayidle {
|
||||
|
||||
constexpr auto* TAG = "DisplayIdle";
|
||||
|
||||
class DisplayIdleService final : public Service {
|
||||
|
||||
std::unique_ptr<Timer> timer;
|
||||
bool displayDimmed = false;
|
||||
settings::display::DisplaySettings cachedDisplaySettings;
|
||||
|
||||
static std::shared_ptr<hal::display::DisplayDevice> getDisplay() {
|
||||
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
|
||||
}
|
||||
|
||||
void tick() {
|
||||
// Settings are now cached and event-driven (no file I/O in timer callback!)
|
||||
// This prevents watchdog timeout from blocking the Timer Service task
|
||||
|
||||
// Query LVGL inactivity once for both checks
|
||||
uint32_t inactive_ms = 0;
|
||||
if (lvgl::lock(100)) {
|
||||
inactive_ms = lv_disp_get_inactive_time(nullptr);
|
||||
lvgl::unlock();
|
||||
}
|
||||
|
||||
// Handle display backlight
|
||||
auto display = getDisplay();
|
||||
if (display != nullptr && display->supportsBacklightDuty()) {
|
||||
// If timeout disabled, ensure backlight restored if we had dimmed it
|
||||
if (!cachedDisplaySettings.backlightTimeoutEnabled || cachedDisplaySettings.backlightTimeoutMs == 0) {
|
||||
if (displayDimmed) {
|
||||
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
||||
displayDimmed = false;
|
||||
}
|
||||
} else {
|
||||
if (!displayDimmed && inactive_ms >= cachedDisplaySettings.backlightTimeoutMs) {
|
||||
display->setBacklightDuty(0);
|
||||
displayDimmed = true;
|
||||
} else if (displayDimmed && inactive_ms < 100) {
|
||||
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
||||
displayDimmed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
bool onStart(TT_UNUSED ServiceContext& service) override {
|
||||
// Load settings once at startup and cache them
|
||||
// This eliminates file I/O from timer callback (prevents watchdog timeout)
|
||||
cachedDisplaySettings = settings::display::loadOrGetDefault();
|
||||
|
||||
// Note: Settings changes require service restart to take effect
|
||||
// TODO: Add DisplaySettingsChanged events for dynamic updates
|
||||
|
||||
timer = std::make_unique<Timer>(Timer::Type::Periodic, [this]{ this->tick(); });
|
||||
timer->setThreadPriority(Thread::Priority::Lower);
|
||||
timer->start(250); // check 4x per second for snappy restore
|
||||
return true;
|
||||
}
|
||||
|
||||
void onStop(TT_UNUSED ServiceContext& service) override {
|
||||
if (timer) {
|
||||
timer->stop();
|
||||
timer = nullptr;
|
||||
}
|
||||
// Ensure display restored on stop
|
||||
auto display = getDisplay();
|
||||
if (display && displayDimmed) {
|
||||
display->setBacklightDuty(cachedDisplaySettings.backlightDuty);
|
||||
displayDimmed = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const ServiceManifest manifest = {
|
||||
.id = "DisplayIdle",
|
||||
.createService = create<DisplayIdleService>
|
||||
};
|
||||
|
||||
}
|
||||
@@ -34,18 +34,29 @@ static bool disableWifiService() {
|
||||
}
|
||||
|
||||
bool initWifi(const EspNowConfig& config) {
|
||||
// ESP-NOW can coexist with WiFi STA mode; only preserve WiFi state if already connected
|
||||
auto wifi_state = wifi::getRadioState();
|
||||
bool wifi_was_connected = (wifi_state == wifi::RadioState::ConnectionActive);
|
||||
|
||||
// If WiFi is off or in other states, temporarily disable it to initialize ESP-NOW
|
||||
// If WiFi is already connected, keep it running and just add ESP-NOW on top
|
||||
if (!wifi_was_connected && wifi_state != wifi::RadioState::Off && wifi_state != wifi::RadioState::OffPending) {
|
||||
if (!disableWifiService()) {
|
||||
TT_LOG_E(TAG, "Failed to disable wifi");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
wifi_mode_t mode;
|
||||
if (config.mode == Mode::Station) {
|
||||
// Use STA mode to allow coexistence with normal WiFi connection
|
||||
mode = wifi_mode_t::WIFI_MODE_STA;
|
||||
} else {
|
||||
mode = wifi_mode_t::WIFI_MODE_AP;
|
||||
}
|
||||
|
||||
// Only reinitialize WiFi if it's not already running
|
||||
if (wifi::getRadioState() == wifi::RadioState::Off) {
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
if (esp_wifi_init(&cfg) != ESP_OK) {
|
||||
TT_LOG_E(TAG, "esp_wifi_init() failed");
|
||||
@@ -66,6 +77,7 @@ bool initWifi(const EspNowConfig& config) {
|
||||
TT_LOG_E(TAG, "esp_wifi_start() failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (esp_wifi_set_channel(config.channel, WIFI_SECOND_CHAN_NONE) != ESP_OK) {
|
||||
TT_LOG_E(TAG, "esp_wifi_set_channel() failed");
|
||||
@@ -85,23 +97,19 @@ bool initWifi(const EspNowConfig& config) {
|
||||
}
|
||||
}
|
||||
|
||||
TT_LOG_I(TAG, "WiFi initialized for ESP-NOW (preserved existing connection: %s)", wifi_was_connected ? "yes" : "no");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool deinitWifi() {
|
||||
if (esp_wifi_stop() != ESP_OK) {
|
||||
TT_LOG_E(TAG, "Failed to stop radio");
|
||||
return false;
|
||||
}
|
||||
// Don't deinitialize WiFi completely - just disable ESP-NOW
|
||||
// This allows normal WiFi connection to continue
|
||||
// Only stop/deinit if WiFi was originally off
|
||||
|
||||
if (esp_wifi_set_mode(WIFI_MODE_NULL) != ESP_OK) {
|
||||
TT_LOG_E(TAG, "Failed to unset mode");
|
||||
}
|
||||
|
||||
if (esp_wifi_deinit() != ESP_OK) {
|
||||
TT_LOG_E(TAG, "Failed to deinit");
|
||||
}
|
||||
// Since we're only using WiFi for ESP-NOW, we can safely keep it in a minimal state
|
||||
// or shut it down. For now, keep it running to support STA + ESP-NOW coexistence.
|
||||
|
||||
TT_LOG_I(TAG, "ESP-NOW WiFi deinitialized (WiFi service continues independently)");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#ifdef ESP_PLATFORM
|
||||
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
#include <Tactility/service/ServiceManifest.h>
|
||||
#include <Tactility/service/ServiceContext.h>
|
||||
#include <Tactility/Timer.h>
|
||||
#include <Tactility/lvgl/LvglSync.h>
|
||||
#include <Tactility/settings/KeyboardSettings.h>
|
||||
#include <Tactility/hal/keyboard/KeyboardDevice.h>
|
||||
|
||||
namespace keyboardbacklight {
|
||||
bool setBrightness(uint8_t brightness);
|
||||
}
|
||||
|
||||
namespace tt::service::keyboardidle {
|
||||
|
||||
constexpr auto* TAG = "KeyboardIdle";
|
||||
|
||||
class KeyboardIdleService final : public Service {
|
||||
|
||||
std::unique_ptr<Timer> timer;
|
||||
bool keyboardDimmed = false;
|
||||
settings::keyboard::KeyboardSettings cachedKeyboardSettings;
|
||||
|
||||
static std::shared_ptr<hal::keyboard::KeyboardDevice> getKeyboard() {
|
||||
return hal::findFirstDevice<hal::keyboard::KeyboardDevice>(hal::Device::Type::Keyboard);
|
||||
}
|
||||
|
||||
void tick() {
|
||||
// Settings are now cached and event-driven (no file I/O in timer callback!)
|
||||
// This prevents watchdog timeout from blocking the Timer Service task
|
||||
|
||||
// Query LVGL inactivity once for both checks
|
||||
uint32_t inactive_ms = 0;
|
||||
if (lvgl::lock(100)) {
|
||||
inactive_ms = lv_disp_get_inactive_time(nullptr);
|
||||
lvgl::unlock();
|
||||
}
|
||||
|
||||
// Handle keyboard backlight
|
||||
auto keyboard = getKeyboard();
|
||||
if (keyboard != nullptr && keyboard->isAttached()) {
|
||||
// If timeout disabled, ensure backlight restored if we had dimmed it
|
||||
if (!cachedKeyboardSettings.backlightTimeoutEnabled || cachedKeyboardSettings.backlightTimeoutMs == 0) {
|
||||
if (keyboardDimmed) {
|
||||
keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0);
|
||||
keyboardDimmed = false;
|
||||
}
|
||||
} else {
|
||||
if (!keyboardDimmed && inactive_ms >= cachedKeyboardSettings.backlightTimeoutMs) {
|
||||
keyboardbacklight::setBrightness(0);
|
||||
keyboardDimmed = true;
|
||||
} else if (keyboardDimmed && inactive_ms < 100) {
|
||||
keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0);
|
||||
keyboardDimmed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
bool onStart(TT_UNUSED ServiceContext& service) override {
|
||||
// Load settings once at startup and cache them
|
||||
// This eliminates file I/O from timer callback (prevents watchdog timeout)
|
||||
cachedKeyboardSettings = settings::keyboard::loadOrGetDefault();
|
||||
|
||||
// Note: Settings changes require service restart to take effect
|
||||
// TODO: Add KeyboardSettingsChanged events for dynamic updates
|
||||
|
||||
timer = std::make_unique<Timer>(Timer::Type::Periodic, [this]{ this->tick(); });
|
||||
timer->setThreadPriority(Thread::Priority::Lower);
|
||||
timer->start(250); // check 4x per second for snappy restore
|
||||
return true;
|
||||
}
|
||||
|
||||
void onStop(TT_UNUSED ServiceContext& service) override {
|
||||
if (timer) {
|
||||
timer->stop();
|
||||
timer = nullptr;
|
||||
}
|
||||
// Ensure keyboard restored on stop
|
||||
auto keyboard = getKeyboard();
|
||||
if (keyboard && keyboardDimmed) {
|
||||
keyboardbacklight::setBrightness(cachedKeyboardSettings.backlightEnabled ? cachedKeyboardSettings.backlightBrightness : 0);
|
||||
keyboardDimmed = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
extern const ServiceManifest manifest = {
|
||||
.id = "KeyboardIdle",
|
||||
.createService = create<KeyboardIdleService>
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -8,7 +8,7 @@
|
||||
namespace tt::service::memorychecker {
|
||||
|
||||
constexpr const char* TAG = "MemoryChecker";
|
||||
constexpr TickType_t TIMER_UPDATE_INTERVAL = 1000U / portTICK_PERIOD_MS;
|
||||
constexpr TickType_t TIMER_UPDATE_INTERVAL = 2000U / portTICK_PERIOD_MS;
|
||||
|
||||
// Total memory (in bytes) that should be free before warnings occur
|
||||
constexpr auto TOTAL_FREE_THRESHOLD = 10'000;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user