Remove old HAL components and refactored GPS-related code (#583)

- Added generic GPS/GNSS support with device detection, configuration, and persistent settings.
- Improved device and module lifecycle management.
- Added flexible filesystem locking support for displays and storage.
- Improved display-idle and keyboard backlight handling.
- Updated architecture, driver, module, testing, and licensing documentation.
- Removed old HAL device and related code.
This commit is contained in:
Ken Van Hoeylandt
2026-07-25 17:20:17 +02:00
committed by GitHub
parent 29e80cfd65
commit 2a2558b29a
173 changed files with 3855 additions and 5445 deletions
+4 -3
View File
@@ -4,7 +4,6 @@
#include <Tactility/app/AppRegistration.h>
#include <Tactility/file/File.h>
#include <Tactility/file/FileLock.h>
#include <tactility/hal/Device.h>
#include <Tactility/Paths.h>
#include <cerrno>
@@ -118,8 +117,10 @@ bool install(const std::string& path) {
return false;
}
auto target_path_lock = file::getLock(app_parent_path)->asScopedLock();
auto source_path_lock = file::getLock(path)->asScopedLock();
auto target_path_lockable = file::getLock(app_parent_path);
auto source_path_lockable = file::getLock(path);
auto target_path_lock = target_path_lockable->asScopedLock();
auto source_path_lock = source_path_lockable->asScopedLock();
target_path_lock.lock();
source_path_lock.lock();
LOG_I(TAG, "Extracting app from %s to %s", path.c_str(), app_target_path.c_str());
+27 -31
View File
@@ -1,17 +1,19 @@
#include <Tactility/StringUtils.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/hal/gps/GpsDevice.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/gps/GpsService.h>
#include "tactility/drivers/uart_controller.h"
#include <cstring>
#include <lvgl.h>
#include <tactility/drivers/uart_controller.h>
#include <tactility/log.h>
#include <tactility/lvgl_icon_shared.h>
#include <gps/gps.h>
#include <gps/gps_settings.h>
#include <cstring>
#include <lvgl.h>
namespace tt::app::addgps {
constexpr auto* TAG = "AddGps";
@@ -29,6 +31,14 @@ class AddGpsApp final : public App {
std::array<uint32_t, 6> baudRates = { 9600, 19200, 28800, 38400, 57600, 115200 };
const char* baudRatesDropdownValues = "9600\n19200\n28800\n38400\n57600\n115200";
static std::vector<std::string> getModelNames() {
std::vector<std::string> result;
for (int model = GpsModel::GPS_MODEL_UNKNOWN; model <= GpsModel::GPS_MODEL_UC6580; model++) {
result.emplace_back(gps_model_to_string(static_cast<GpsModel>(model)));
}
return result;
}
static void onAddGpsCallback(lv_event_t* event) {
auto* app = (AddGpsApp*)lv_event_get_user_data(event);
app->onAddGps();
@@ -37,38 +47,24 @@ class AddGpsApp final : public App {
void onAddGps() {
auto selected_baud_index = lv_dropdown_get_selected(baudDropdown);
auto new_configuration = hal::gps::GpsConfiguration {
.uartName = { 0x00 },
.baudRate = baudRates[selected_baud_index],
GpsConfiguration new_configuration = {
.uart_name = { 0x00 },
.baud_rate = baudRates[selected_baud_index],
// Warning: This assumes that the enum is a regularly indexed one that starts at 0
.model = (hal::gps::GpsModel)lv_dropdown_get_selected(modelDropdown)
.model = (GpsModel)lv_dropdown_get_selected(modelDropdown)
};
lv_dropdown_get_selected_str(uartDropdown, new_configuration.uartName, sizeof(new_configuration.uartName));
if (new_configuration.uartName[0] == 0x00) {
lv_dropdown_get_selected_str(uartDropdown, new_configuration.uart_name, sizeof(new_configuration.uart_name));
if (new_configuration.uart_name[0] == 0x00) {
alertdialog::start("Error", "You must select a bus/uart.");
return;
}
LOG_I(TAG, "Saving: uart=%s, model=%d, baud=%u", new_configuration.uartName, (int)new_configuration.model, (unsigned)new_configuration.baudRate);
auto service = service::gps::findGpsService();
std::vector<tt::hal::gps::GpsConfiguration> configurations;
if (service != nullptr) {
service->getGpsConfigurations(configurations);
for (auto& stored_configuration: configurations) {
if (strcmp(stored_configuration.uartName, new_configuration.uartName) == 0) {
auto message = std::string("Bus \"{}\" is already in use in another configuration", (const char*)new_configuration.uartName);
app::alertdialog::start("Error", message.c_str());
return;
}
}
if (!service->addGpsConfiguration(new_configuration)) {
app::alertdialog::start("Error", "Failed to add configuration");
} else {
stop();
}
LOG_I(TAG, "Saving: uart=%s, model=%d, baud=%u", new_configuration.uart_name, (int)new_configuration.model, (unsigned)new_configuration.baud_rate);
if (gps_settings_add_configuration(&new_configuration) != ERROR_NONE) {
alertdialog::start("Error", "Failed to add configuration");
} else {
stop();
}
}
@@ -137,7 +133,7 @@ public:
modelDropdown = lv_dropdown_create(model_wrapper);
auto model_names = hal::gps::getModels();
auto model_names = getModelNames();
auto model_options = string::join(model_names, "\n");
lv_dropdown_set_options(modelDropdown, model_options.c_str());
lv_obj_align(modelDropdown, LV_ALIGN_TOP_RIGHT, 0, 0);
+4 -2
View File
@@ -58,7 +58,8 @@ class AppHubApp final : public App {
void onRefreshSuccess() {
LOG_I(TAG, "Request success");
auto lock = lvgl::getSyncLock()->asScopedLock();
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
lock.lock();
showApps();
@@ -66,7 +67,8 @@ class AppHubApp final : public App {
void onRefreshError(const char* error) {
LOG_E(TAG, "Request failed: %s", error);
auto lock = lvgl::getSyncLock()->asScopedLock();
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
lock.lock();
showRefreshFailedError("Cannot reach server");
+2 -1
View File
@@ -21,7 +21,8 @@ static bool parseEntry(const cJSON* object, AppHubEntry& entry) {
}
bool parseJson(const std::string& filePath, std::vector<AppHubEntry>& entries) {
auto lock = file::getLock(filePath)->asScopedLock();
auto lockable = file::getLock(filePath);
auto lock = lockable->asScopedLock();
lock.lock();
auto data = file::readString(filePath);
+3 -32
View File
@@ -9,7 +9,6 @@
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppPaths.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/hal/usb/Usb.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/service/loader/Loader.h>
@@ -36,10 +35,6 @@ constexpr auto* TAG = "Boot";
extern const AppManifest manifest;
static std::shared_ptr<hal::display::DisplayDevice> getHalDisplay() {
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
}
class BootApp : public App {
// Snapshot of hal::usb::isUsbBootMode(), taken before the boot thread starts and
@@ -58,31 +53,7 @@ class BootApp : public App {
getCpuAffinityConfiguration().system
);
static void setupHalDisplay() {
const auto hal_display = getHalDisplay();
if (hal_display == nullptr) {
return;
}
settings::display::DisplaySettings settings;
if (settings::display::load(settings)) {
if (hal_display->getGammaCurveCount() > 0) {
hal_display->setGammaCurve(settings.gammaCurve);
LOG_I(TAG, "Gamma curve %d", settings.gammaCurve);
}
} else {
settings = settings::display::getDefault();
}
if (hal_display->supportsBacklightDuty()) {
LOG_I(TAG, "Backlight %d", settings.backlightDuty);
hal_display->setBacklightDuty(settings.backlightDuty);
} else {
LOG_I(TAG, "No backlight");
}
}
static void setupKernelDisplay() {
static void setupDisplay() {
auto* display = device_find_first_by_type(&DISPLAY_TYPE);
// Boards not yet migrated to the kernel display driver register a placeholder device (so
// the devicetree node resolves) with a NULL api - nothing for this function to act on.
@@ -161,8 +132,8 @@ class BootApp : public App {
// TODO: Support for multiple displays
LOG_I(TAG, "Setup display");
setupHalDisplay();
setupKernelDisplay();
setupDisplay();
LOG_I(TAG, "Prepare file systems");
prepareFileSystems();
#ifdef CONFIG_TT_USER_DATA_LOCATION_SD
+6 -3
View File
@@ -84,7 +84,8 @@ void ChatApp::onReceive(const esp_now_recv_info_t* receiveInfo, const uint8_t* d
state.addMessage(msg);
{
auto lock = lvgl::getSyncLock()->asScopedLock();
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
lock.lock();
view.displayMessage(msg);
}
@@ -115,7 +116,8 @@ void ChatApp::sendMessage(const std::string& text) {
state.addMessage(msg);
{
auto lock = lvgl::getSyncLock()->asScopedLock();
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
lock.lock();
view.displayMessage(msg);
}
@@ -172,7 +174,8 @@ void ChatApp::switchChannel(const std::string& chatChannel) {
saveSettings(settings);
{
auto lock = lvgl::getSyncLock()->asScopedLock();
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
lock.lock();
view.refreshMessageList();
}
@@ -3,12 +3,12 @@
#include <Tactility/app/crashdiagnostics/QrHelpers.h>
#include <Tactility/app/crashdiagnostics/QrUrl.h>
#include <Tactility/app/launcher/Launcher.h>
#include <tactility/hal/Device.h>
#include <Tactility/lvgl/Statusbar.h>
#include <Tactility/service/loader/Loader.h>
#include <lvgl.h>
#include <qrcode.h>
#include <tactility/drivers/pointer.h>
#include <tactility/log.h>
namespace tt::app::crashdiagnostics {
@@ -36,7 +36,7 @@ public:
lv_obj_align(top_label, LV_ALIGN_TOP_MID, 0, 2);
auto* bottom_label = lv_label_create(parent);
if (hal::hasDevice(hal::Device::Type::Touch)) {
if (device_has_active_by_type(&POINTER_TYPE)) {
lv_label_set_text(bottom_label, "Tap screen to continue");
} else {
lv_label_set_text(bottom_label, "Reboot device to continue");
@@ -1,9 +1,8 @@
#ifdef ESP_PLATFORM
#include <Tactility/Timer.h>
#include <Tactility/Tactility.h>
#include <Tactility/Timer.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/lvgl/Lvgl.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Style.h>
#include <Tactility/lvgl/Toolbar.h>
@@ -14,6 +13,7 @@
#include <tactility/log.h>
#include <tactility/lvgl_icon_shared.h>
#include <tactility/lvgl_module.h>
#include <cstring>
#include <lvgl.h>
@@ -31,9 +31,10 @@ class DevelopmentApp final : public App {
std::shared_ptr<service::development::DevelopmentService> service;
Timer timer = Timer(Timer::Type::Periodic, pdMS_TO_TICKS(1000), [this] {
auto lock = lvgl::getSyncLock()->asScopedLock();
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
// TODO: There's a crash when this is called when the app is being destroyed
if (lock.lock(lvgl::defaultLockTime) && lvgl::isStarted()) {
if (lock.lock(lvgl::defaultLockTime) && module_is_started(&lvgl_module)) {
updateViewState();
}
});
@@ -157,7 +158,8 @@ public:
}
void onHide(AppContext& appContext) override {
auto lock = lvgl::getSyncLock()->asScopedLock();
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
// Ensure that the update isn't already happening
lock.lock();
timer.stop();
-310
View File
@@ -1,310 +0,0 @@
#include <Tactility/Tactility.h>
#include <tactility/lvgl_icon_shared.h>
#ifdef ESP_PLATFORM
#include <Tactility/service/displayidle/DisplayIdleService.h>
#endif
#include <Tactility/app/App.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/settings/DisplaySettings.h>
#include <lvgl.h>
#include <tactility/log.h>
#include <tactility/lvgl_module.h>
namespace tt::app::display {
constexpr auto* TAG = "Display";
static std::shared_ptr<hal::display::DisplayDevice> getHalDisplay() {
return hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
}
class HalDisplayApp final : public App {
settings::display::DisplaySettings displaySettings;
bool displaySettingsUpdated = false;
lv_obj_t* timeoutSwitch = nullptr;
lv_obj_t* timeoutDropdown = nullptr;
lv_obj_t* screensaverDropdown = nullptr;
static void onBacklightSliderEvent(lv_event_t* event) {
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
auto* app = static_cast<HalDisplayApp*>(lv_event_get_user_data(event));
auto hal_display = getHalDisplay();
assert(hal_display != nullptr);
if (hal_display->supportsBacklightDuty()) {
int32_t slider_value = lv_slider_get_value(slider);
app->displaySettings.backlightDuty = static_cast<uint8_t>(slider_value);
app->displaySettingsUpdated = true;
hal_display->setBacklightDuty(app->displaySettings.backlightDuty);
}
}
static void onGammaSliderEvent(lv_event_t* event) {
auto* slider = static_cast<lv_obj_t*>(lv_event_get_target(event));
auto hal_display = hal::findFirstDevice<hal::display::DisplayDevice>(hal::Device::Type::Display);
auto* app = static_cast<HalDisplayApp*>(lv_event_get_user_data(event));
assert(hal_display != nullptr);
if (hal_display->getGammaCurveCount() > 0) {
int32_t slider_value = lv_slider_get_value(slider);
app->displaySettings.gammaCurve = static_cast<uint8_t>(slider_value);
app->displaySettingsUpdated = true;
hal_display->setGammaCurve(app->displaySettings.gammaCurve);
}
}
static void onOrientationSet(lv_event_t* event) {
auto* app = static_cast<HalDisplayApp*>(lv_event_get_user_data(event));
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
uint32_t selected_index = lv_dropdown_get_selected(dropdown);
LOG_I(TAG, "Selected %u", (unsigned)selected_index);
auto selected_orientation = static_cast<settings::display::Orientation>(selected_index);
if (selected_orientation != app->displaySettings.orientation) {
app->displaySettings.orientation = selected_orientation;
app->displaySettingsUpdated = true;
lv_display_set_rotation(lv_display_get_default(), settings::display::toLvglDisplayRotation(selected_orientation));
}
}
static void onTimeoutSwitch(lv_event_t* event) {
auto* app = static_cast<HalDisplayApp*>(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);
if (app->screensaverDropdown) {
lv_obj_clear_state(app->screensaverDropdown, LV_STATE_DISABLED);
}
} else {
lv_obj_add_state(app->timeoutDropdown, LV_STATE_DISABLED);
if (app->screensaverDropdown) {
lv_obj_add_state(app->screensaverDropdown, LV_STATE_DISABLED);
}
}
}
}
static void onTimeoutChanged(lv_event_t* event) {
auto* app = static_cast<HalDisplayApp*>(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;
}
}
static void onScreensaverChanged(lv_event_t* event) {
auto* app = static_cast<HalDisplayApp*>(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);
// Validate index bounds before casting to enum
if (idx >= static_cast<uint32_t>(settings::display::ScreensaverType::Count)) {
return;
}
auto selected_type = static_cast<settings::display::ScreensaverType>(idx);
if (selected_type != app->displaySettings.screensaverType) {
app->displaySettings.screensaverType = selected_type;
app->displaySettingsUpdated = true;
}
}
public:
void onShow(AppContext& app, lv_obj_t* parent) override {
displaySettings = settings::display::loadOrGetDefault();
auto ui_density = lvgl_get_ui_density();
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(parent, 0, LV_STATE_DEFAULT);
auto hal_display = getHalDisplay();
assert(hal_display != nullptr);
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);
// Backlight slider
if (hal_display->supportsBacklightDuty()) {
auto* brightness_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(brightness_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_hor(brightness_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(brightness_wrapper, 0, LV_STATE_DEFAULT);
if (ui_density != LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_ver(brightness_wrapper, 4, LV_STATE_DEFAULT);
}
auto* brightness_label = lv_label_create(brightness_wrapper);
lv_label_set_text(brightness_label, "Brightness");
lv_obj_align(brightness_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* brightness_slider = lv_slider_create(brightness_wrapper);
lv_obj_set_width(brightness_slider, LV_PCT(50));
lv_obj_align(brightness_slider, LV_ALIGN_RIGHT_MID, 0, 0);
lv_slider_set_range(brightness_slider, 0, 255);
lv_obj_add_event_cb(brightness_slider, onBacklightSliderEvent, LV_EVENT_VALUE_CHANGED, this);
lv_slider_set_value(brightness_slider, displaySettings.backlightDuty, LV_ANIM_OFF);
}
// Gamma slider
if (hal_display->getGammaCurveCount() > 0) {
auto* gamma_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(gamma_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_hor(gamma_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(gamma_wrapper, 0, LV_STATE_DEFAULT);
if (ui_density != LVGL_UI_DENSITY_COMPACT) {
lv_obj_set_style_pad_ver(gamma_wrapper, 4, LV_STATE_DEFAULT);
}
auto* gamma_label = lv_label_create(gamma_wrapper);
lv_label_set_text(gamma_label, "Gamma");
lv_obj_align(gamma_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_y(gamma_label, 0);
auto* gamma_slider = lv_slider_create(gamma_wrapper);
lv_obj_set_width(gamma_slider, LV_PCT(50));
lv_obj_align(gamma_slider, LV_ALIGN_RIGHT_MID, 0, 0);
lv_slider_set_range(gamma_slider, 0, hal_display->getGammaCurveCount());
lv_obj_add_event_cb(gamma_slider, onGammaSliderEvent, LV_EVENT_VALUE_CHANGED, this);
uint8_t curve_index = displaySettings.gammaCurve;
lv_slider_set_value(gamma_slider, curve_index, LV_ANIM_OFF);
}
// Orientation
auto* orientation_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(orientation_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(orientation_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(orientation_wrapper, 0, LV_STATE_DEFAULT);
auto* orientation_label = lv_label_create(orientation_wrapper);
lv_label_set_text(orientation_label, "Orientation");
lv_obj_align(orientation_label, LV_ALIGN_LEFT_MID, 0, 0);
auto* orientation_dropdown = lv_dropdown_create(orientation_wrapper);
// Note: order correlates with settings::display::Orientation item order
lv_dropdown_set_options(orientation_dropdown, "Landscape\nPortrait Right\nLandscape Flipped\nPortrait Left");
lv_obj_align(orientation_dropdown, LV_ALIGN_RIGHT_MID, 0, 0);
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_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);
}
// Screensaver type
auto* screensaver_wrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(screensaver_wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(screensaver_wrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(screensaver_wrapper, 0, LV_STATE_DEFAULT);
auto* screensaver_label = lv_label_create(screensaver_wrapper);
lv_label_set_text(screensaver_label, "Screensaver");
lv_obj_align(screensaver_label, LV_ALIGN_LEFT_MID, 0, 0);
screensaverDropdown = lv_dropdown_create(screensaver_wrapper);
// Note: order correlates with settings::display::ScreensaverType enum order
lv_dropdown_set_options(screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan");
lv_obj_align(screensaverDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(screensaverDropdown, onScreensaverChanged, LV_EVENT_VALUE_CHANGED, this);
lv_dropdown_set_selected(screensaverDropdown, static_cast<uint16_t>(displaySettings.screensaverType));
if (!displaySettings.backlightTimeoutEnabled) {
lv_obj_add_state(screensaverDropdown, LV_STATE_DISABLED);
}
}
}
void onHide(AppContext& app) override {
if (displaySettingsUpdated) {
// Dispatch it, so file IO doesn't block the UI
const settings::display::DisplaySettings settings_to_save = displaySettings;
getMainDispatcher().dispatch([settings_to_save] {
settings::display::save(settings_to_save);
#ifdef ESP_PLATFORM
// Notify DisplayIdle service to reload settings
auto displayIdle = service::displayidle::findService();
if (displayIdle) {
displayIdle->reloadSettings();
}
#endif
});
}
}
};
extern const AppManifest manifest = {
.appId = "Display",
.appName = "Display",
.appIcon = LVGL_ICON_SHARED_DISPLAY_SETTINGS,
.appCategory = Category::Settings,
.createApp = create<HalDisplayApp>
};
} // namespace
+6 -3
View File
@@ -450,7 +450,8 @@ void View::onEjectPressed() {
void View::update(size_t start_index) {
const bool is_root = (state->getCurrentPath() == "/");
auto scoped_lockable = lvgl::getSyncLock()->asScopedLock();
auto sync_lockable = lvgl::getSyncLock();
auto scoped_lockable = sync_lockable->asScopedLock();
if (!scoped_lockable.lock(lvgl::defaultLockTime)) {
LOG_E(TAG, "Mutex acquisition timeout (%s)", "lvgl");
return;
@@ -550,14 +551,16 @@ void View::init(const AppContext& appContext, lv_obj_t* parent) {
}
void View::onDirEntryListScrollBegin() {
auto scoped_lockable = lvgl::getSyncLock()->asScopedLock();
auto sync_lockable = lvgl::getSyncLock();
auto scoped_lockable = sync_lockable->asScopedLock();
if (scoped_lockable.lock(lvgl::defaultLockTime)) {
lv_obj_add_flag(action_list, LV_OBJ_FLAG_HIDDEN);
}
}
void View::onNavigate() {
auto scoped_lockable = lvgl::getSyncLock()->asScopedLock();
auto sync_lockable = lvgl::getSyncLock();
auto scoped_lockable = sync_lockable->asScopedLock();
if (scoped_lockable.lock(lvgl::defaultLockTime)) {
lv_obj_add_flag(action_list, LV_OBJ_FLAG_HIDDEN);
}
+2 -1
View File
@@ -155,7 +155,8 @@ void View::onNavigateUpPressed() {
}
void View::update() {
auto scoped_lockable = lvgl::getSyncLock()->asScopedLock();
auto sync_lockable = lvgl::getSyncLock();
auto scoped_lockable = sync_lockable->asScopedLock();
if (scoped_lockable.lock(lvgl::defaultLockTime)) {
lv_obj_clean(dir_entry_list);
+223 -376
View File
@@ -1,20 +1,28 @@
#include "tactility/lvgl_module.h"
#include <Tactility/Tactility.h>
#include <Tactility/Timer.h>
#include <Tactility/app/AppManifest.h>
#include <Tactility/app/alertdialog/AlertDialog.h>
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/lvgl/Toolbar.h>
#include <Tactility/service/gps/GpsService.h>
#include <Tactility/service/gps/GpsState.h>
#include <Tactility/service/loader/Loader.h>
#include <tactility/log.h>
#include <tactility/device.h>
#include <tactility/lvgl_icon_shared.h>
#include <atomic>
#include <cstring>
#include <format>
#include <lvgl.h>
#include <string>
#include <vector>
#include <gps/gps.h>
#include <gps/gps_settings.h>
namespace tt::app::addgps {
extern AppManifest manifest;
@@ -26,40 +34,21 @@ extern const AppManifest manifest;
class GpsSettingsApp final : public App {
static constexpr auto* TAG = "GpsSettings";
struct DeviceRow {
Device* device;
lv_obj_t* button;
lv_obj_t* buttonLabel;
bool hasConfiguration = false;
size_t configurationIndex = 0;
};
std::unique_ptr<Timer> timer;
std::shared_ptr<GpsSettingsApp*> appReference = std::make_shared<GpsSettingsApp*>(this);
lv_obj_t* statusWrapper = nullptr;
lv_obj_t* statusLabelWidget = nullptr;
lv_obj_t* statusLatitudeValue = nullptr;
lv_obj_t* statusLongitudeValue = nullptr;
lv_obj_t* statusAltitudeValue = nullptr;
lv_obj_t* statusSpeedValue = nullptr;
lv_obj_t* statusHeadingValue = nullptr;
lv_obj_t* statusSatellitesValue = nullptr;
lv_obj_t* switchWidget = nullptr;
lv_obj_t* spinnerWidget = nullptr;
lv_obj_t* infoContainerWidget = nullptr;
lv_obj_t* gpsConfigWrapper = nullptr;
lv_obj_t* addGpsWrapper = nullptr;
bool hasSetInfo = false;
PubSub<service::gps::State>::SubscriptionHandle serviceStateSubscription = nullptr;
std::shared_ptr<service::gps::GpsService> service;
void onServiceStateChanged() {
auto lock = lvgl::getSyncLock()->asScopedLock();
if (lock.lock(100 / portTICK_PERIOD_MS)) {
if (!updateTimerState()) {
updateViews();
}
}
}
static void onGpsToggledCallback(lv_event_t* event) {
auto* app = (GpsSettingsApp*)lv_event_get_user_data(event);
app->onGpsToggled(event);
}
lv_obj_t* deviceListWrapper = nullptr;
std::vector<DeviceRow> deviceRows;
std::atomic<bool> isShown = false;
bool hasPendingDelete = false;
Device* pendingDeleteDevice = nullptr;
size_t pendingDeleteIndex = 0;
static void onAddGpsCallback(lv_event_t* event) {
auto* app = (GpsSettingsApp*)lv_event_get_user_data(event);
@@ -70,385 +59,243 @@ class GpsSettingsApp final : public App {
app::start(addgps::manifest.appId);
}
void startReceivingUpdates() {
timer->start();
updateViews();
}
void stopReceivingUpdates() {
timer->stop();
updateViews();
}
void createInfoView(hal::gps::GpsModel model) {
auto* label = lv_label_create(infoContainerWidget);
if (model == hal::gps::GpsModel::Unknown) {
lv_label_set_text(label, "Model: auto-detect");
} else {
lv_label_set_text_fmt(label, "Model: %s", toString(model));
}
}
static void onDeleteConfiguration(lv_event_t* event) {
auto* app = (GpsSettingsApp*)lv_event_get_user_data(event);
static void onDeviceButtonCallback(lv_event_t* event) {
auto* button = lv_event_get_target_obj(event);
auto index_as_voidptr = lv_obj_get_user_data(button); // config index
int index;
// TODO: Find a better way to cast void* to int, or find a different way to pass the index
memcpy(&index, &index_as_voidptr, sizeof(int));
auto* device = static_cast<Device*>(lv_obj_get_user_data(button));
std::vector<tt::hal::gps::GpsConfiguration> configurations;
auto gps_service = service::gps::findGpsService();
if (gps_service && gps_service->getGpsConfigurations(configurations)) {
LOG_I(TAG, "Found service and configs %d %d", index, (int)configurations.size());
if (index < configurations.size()) {
if (gps_service->removeGpsConfiguration(configurations[index])) {
app->updateViews();
} else {
alertdialog::start("Error", "Failed to remove configuration");
}
}
}
}
void createGpsView(const hal::gps::GpsConfiguration& configuration, int index) {
auto* wrapper = lv_obj_create(gpsConfigWrapper);
lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_style_margin_hor(wrapper, 0, 0);
lv_obj_set_style_margin_bottom(wrapper, 8, 0);
// Left wrapper
auto* left_wrapper = lv_obj_create(wrapper);
lv_obj_set_style_border_width(left_wrapper, 0, 0);
lv_obj_set_style_pad_all(left_wrapper, 0, 0);
lv_obj_set_size(left_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_flex_grow(left_wrapper, 1);
lv_obj_set_flex_flow(left_wrapper, LV_FLEX_FLOW_COLUMN);
auto* uart_label = lv_label_create(left_wrapper);
lv_label_set_text_fmt(uart_label, "UART: %s", configuration.uartName);
auto* baud_label = lv_label_create(left_wrapper);
lv_label_set_text_fmt(baud_label, "Baud: %lu", configuration.baudRate);
auto* model_label = lv_label_create(left_wrapper);
if (configuration.model == hal::gps::GpsModel::Unknown) {
lv_label_set_text(model_label, "Model: auto-detect");
} else {
lv_label_set_text_fmt(model_label, "Model: %s", toString(configuration.model));
}
// Right wrapper
auto* right_wrapper = lv_obj_create(wrapper);
lv_obj_set_style_border_width(right_wrapper, 0, 0);
lv_obj_set_style_pad_all(right_wrapper, 0, 0);
lv_obj_set_size(right_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(right_wrapper, LV_FLEX_FLOW_COLUMN);
auto* delete_button = lv_button_create(right_wrapper);
lv_obj_add_event_cb(delete_button, onDeleteConfiguration, LV_EVENT_SHORT_CLICKED, this);
lv_obj_set_user_data(delete_button, reinterpret_cast<void*>(index));
auto* delete_label = lv_label_create(delete_button);
lv_label_set_text_fmt(delete_label, LV_SYMBOL_TRASH);
}
void updateViews() {
auto lock = lvgl::getSyncLock()->asScopedLock();
if (lock.lock(100 / portTICK_PERIOD_MS)) {
auto state = service->getState();
// Update toolbar
switch (state) {
case service::gps::State::OnPending:
LOG_D(TAG, "OnPending");
lv_obj_remove_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_state(switchWidget, LV_STATE_CHECKED);
lv_obj_add_state(switchWidget, LV_STATE_DISABLED);
lv_obj_remove_flag(statusWrapper, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(addGpsWrapper, LV_OBJ_FLAG_HIDDEN);
break;
case service::gps::State::On:
LOG_D(TAG, "On");
lv_obj_add_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_state(switchWidget, LV_STATE_CHECKED);
lv_obj_remove_state(switchWidget, LV_STATE_DISABLED);
lv_obj_remove_flag(statusWrapper, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(addGpsWrapper, LV_OBJ_FLAG_HIDDEN);
break;
case service::gps::State::OffPending:
LOG_D(TAG, "OffPending");
lv_obj_remove_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_state(switchWidget, LV_STATE_CHECKED);
lv_obj_add_state(switchWidget, LV_STATE_DISABLED);
lv_obj_add_flag(statusWrapper, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(addGpsWrapper, LV_OBJ_FLAG_HIDDEN);
break;
case service::gps::State::Off:
LOG_D(TAG, "Off");
lv_obj_add_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_state(switchWidget, LV_STATE_CHECKED);
lv_obj_remove_state(switchWidget, LV_STATE_DISABLED);
lv_obj_add_flag(statusWrapper, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN);
lv_obj_remove_flag(addGpsWrapper, LV_OBJ_FLAG_HIDDEN);
break;
}
// Update status label and device info
if (state == service::gps::State::On) {
if (!hasSetInfo) {
auto devices = hal::findDevices<hal::gps::GpsDevice>(hal::Device::Type::Gps);
for (auto& device : devices) {
createInfoView(device->getModel());
hasSetInfo = true;
}
}
minmea_sentence_rmc rmc;
char buffer[64];
if (service->getCoordinates(rmc)) {
lv_label_set_text(statusLabelWidget, "Lock acquired");
lv_obj_set_style_text_color(statusLabelWidget, lv_color_hex(0x00ff00), 0);
minmea_float latitude = { rmc.latitude.value, rmc.latitude.scale };
minmea_float longitude = { rmc.longitude.value, rmc.longitude.scale };
double latCoord = minmea_tocoord(&latitude);
double lonCoord = minmea_tocoord(&longitude);
if (isnan(latCoord) || isnan(lonCoord)) {
lv_label_set_text(statusLatitudeValue, "--");
lv_label_set_text(statusLongitudeValue, "--");
} else {
const char* latDir = (latCoord >= 0) ? "N" : "S";
const char* lonDir = (lonCoord >= 0) ? "E" : "W";
snprintf(buffer, sizeof(buffer), "%.6f %s", std::abs(latCoord), latDir);
lv_label_set_text(statusLatitudeValue, buffer);
snprintf(buffer, sizeof(buffer), "%.6f %s", std::abs(lonCoord), lonDir);
lv_label_set_text(statusLongitudeValue, buffer);
}
float speedKnots = minmea_tofloat(&rmc.speed);
if (!isnan(speedKnots)) {
float speedKmh = speedKnots * 1.852f;
snprintf(buffer, sizeof(buffer), "%.1f km/h", speedKmh);
lv_label_set_text(statusSpeedValue, buffer);
} else {
lv_label_set_text(statusSpeedValue, "--");
}
float heading = minmea_tofloat(&rmc.course);
if (!isnan(heading)) {
// Normalize heading to [0, 360) range
heading = fmodf(heading, 360.0f);
if (heading < 0) heading += 360.0f;
const char* dirs[] = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
// Calculate cardinal direction index (0-7)
int idx = (int)((heading + 22.5f) / 45.0f) % 8;
snprintf(buffer, sizeof(buffer), "%.0f° %s", heading, dirs[idx]);
lv_label_set_text(statusHeadingValue, buffer);
} else {
lv_label_set_text(statusHeadingValue, "--");
}
} else {
lv_label_set_text(statusLabelWidget, "Acquiring lock...");
lv_obj_set_style_text_color(statusLabelWidget, lv_color_hex(0xffaa00), 0);
lv_label_set_text(statusLatitudeValue, "--");
lv_label_set_text(statusLongitudeValue, "--");
lv_label_set_text(statusSpeedValue, "--");
lv_label_set_text(statusHeadingValue, "--");
}
minmea_sentence_gga gga;
if (service->getGga(gga)) {
float altitude = minmea_tofloat(&gga.altitude);
if (!isnan(altitude)) {
snprintf(buffer, sizeof(buffer), "%.1f m", altitude);
lv_label_set_text(statusAltitudeValue, buffer);
} else {
lv_label_set_text(statusAltitudeValue, "--");
}
snprintf(buffer, sizeof(buffer), "%d", gga.satellites_tracked);
lv_label_set_text(statusSatellitesValue, buffer);
} else {
lv_label_set_text(statusAltitudeValue, "--");
lv_label_set_text(statusSatellitesValue, "--");
}
lv_obj_remove_flag(statusLabelWidget, LV_OBJ_FLAG_HIDDEN);
bool running = device_is_ready(device);
// device_start()/device_stop() are potentially blocking calls, so use a dispatcher to not block the UI
getMainDispatcher().dispatch([device, running] {
if (running) {
device_stop(device);
} else {
if (hasSetInfo) {
lv_obj_clean(infoContainerWidget);
hasSetInfo = false;
}
lv_obj_add_flag(statusLabelWidget, LV_OBJ_FLAG_HIDDEN);
device_start(device);
}
if (!lv_obj_has_flag(gpsConfigWrapper, LV_OBJ_FLAG_HIDDEN)) {
lv_obj_clean(gpsConfigWrapper);
std::vector<tt::hal::gps::GpsConfiguration> configurations;
auto gps_service = tt::service::gps::findGpsService();
if (gps_service && gps_service->getGpsConfigurations(configurations)) {
int index = 0;
for (auto& configuration : configurations) {
createGpsView(configuration, index++);
}
}
} else {
lv_obj_clean(gpsConfigWrapper);
}
}
});
}
/** @return true if the views were updated */
bool updateTimerState() {
bool is_on = service->getState() == service::gps::State::On;
if (is_on && !timer->isRunning()) {
startReceivingUpdates();
return true;
} else if (!is_on && timer->isRunning()) {
stopReceivingUpdates();
return true;
} else {
// Finds the persisted configuration backing `device` (matched by its parent UART's name)
// and returns its index into gps_settings_for_each_configuration()'s ordering - the handle
// gps_settings_remove_configuration_at() needs to delete exactly this entry, even if another
// entry happens to have identical field values.
// Devicetree-declared GPS_TYPE devices have no such configuration and never match.
static bool findConfigurationIndexForDevice(Device* device, size_t& outIndex) {
auto* parent = device_get_parent(device);
if (parent == nullptr) {
return false;
}
struct Context {
const char* uartName;
size_t* outIndex;
bool found;
} context = { parent->name, &outIndex, false };
gps_settings_for_each_configuration(&context, [](const GpsConfiguration* configuration, size_t index, void* untyped_context) {
auto* ctx = static_cast<Context*>(untyped_context);
if (!ctx->found && strcmp(configuration->uart_name, ctx->uartName) == 0) {
*ctx->outIndex = index;
ctx->found = true;
}
});
return context.found;
}
void onGpsToggled(lv_event_t* event) {
bool wants_on = lv_obj_has_state(switchWidget, LV_STATE_CHECKED);
auto state = service->getState();
bool is_on = (state == service::gps::State::On) || (state == service::gps::State::OnPending);
static void onDeleteButtonCallback(lv_event_t* event) {
auto* app = static_cast<GpsSettingsApp*>(lv_event_get_user_data(event));
auto* button = lv_event_get_target_obj(event);
auto* device = static_cast<Device*>(lv_obj_get_user_data(button));
app->onDeleteDevice(device);
}
if (wants_on != is_on) {
// start/stop are potentially blocking calls, so we use a dispatcher to not block the UI
if (wants_on) {
getMainDispatcher().dispatch([this] {
service->startReceiving();
});
} else {
getMainDispatcher().dispatch([this] {
service->stopReceiving();
});
void onDeleteDevice(Device* device) {
for (auto& row : deviceRows) {
if (row.device == device && row.hasConfiguration) {
pendingDeleteDevice = device;
pendingDeleteIndex = row.configurationIndex;
hasPendingDelete = true;
alertdialog::start("Confirmation", std::string("Do you want to delete ") + device->name + "?", std::vector<std::string> { "Yes", "No" });
return;
}
}
}
lv_obj_t* createInfoRow(lv_obj_t* parent, const char* labelText, lv_color_t color) {
lv_obj_t* row = lv_obj_create(parent);
lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START);
void createDeviceRow(Device* device) {
auto* wrapper = lv_obj_create(deviceListWrapper);
lv_obj_set_size(wrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(wrapper, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_border_width(wrapper, 0, 0);
lv_obj_set_style_pad_all(wrapper, 0, 0);
lv_obj_set_style_pad_all(row, 0, 0);
lv_obj_set_style_pad_right(row, 10, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
auto* name_label = lv_label_create(wrapper);
char model_name[64];
if (gps_get_model_name(device, model_name, sizeof(model_name)) == ERROR_NONE) {
lv_label_set_text(name_label, model_name);
} else {
lv_label_set_text(name_label, device->name);
}
lv_obj_t* label = lv_label_create(row);
lv_label_set_text(label, labelText);
lv_obj_set_style_text_color(label, lv_palette_lighten(LV_PALETTE_GREY, 5), 0);
auto* actions_wrapper = lv_obj_create(wrapper);
lv_obj_set_size(actions_wrapper, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(actions_wrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_style_border_width(actions_wrapper, 0, 0);
lv_obj_set_style_pad_all(actions_wrapper, 0, 0);
lv_obj_set_style_pad_column(actions_wrapper, 4, 0);
lv_obj_t* value = lv_label_create(row);
lv_label_set_text(value, "--");
lv_obj_set_style_text_color(value, color, 0);
auto* button = lv_button_create(actions_wrapper);
lv_obj_add_event_cb(button, onDeviceButtonCallback, LV_EVENT_SHORT_CLICKED, this);
lv_obj_set_user_data(button, device);
auto* button_label = lv_label_create(button);
lv_label_set_text(button_label, "Start");
return value;
DeviceRow row { .device = device, .button = button, .buttonLabel = button_label };
// Only devices backed by a persisted configuration (not devicetree-declared ones) can be deleted.
size_t configurationIndex;
if ((device->flags & DEVICE_FLAG_DYNAMIC) && findConfigurationIndexForDevice(device, configurationIndex)) {
auto* delete_button = lv_button_create(actions_wrapper);
lv_obj_add_event_cb(delete_button, onDeleteButtonCallback, LV_EVENT_SHORT_CLICKED, this);
lv_obj_set_user_data(delete_button, device);
auto* delete_label = lv_label_create(delete_button);
lv_label_set_text(delete_label, LVGL_ICON_SHARED_DELETE);
row.hasConfiguration = true;
row.configurationIndex = configurationIndex;
}
deviceRows.push_back(row);
}
// Rebuilds the device list. Only needs to run when the set of devices could've changed
// (on show, and after returning from AddGpsApp) - button state itself is refreshed by the timer.
void rebuildDeviceList() {
lv_obj_clean(deviceListWrapper);
deviceRows.clear();
device_for_each_of_type(&GPS_TYPE, this, [](Device* device, void* context) {
static_cast<GpsSettingsApp*>(context)->createDeviceRow(device);
return true;
});
}
void updateDeviceStates() {
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
if (lock.lock(100 / portTICK_PERIOD_MS)) {
for (auto& row : deviceRows) {
const char* text = "Start";
bool enabled = true;
if (device_is_ready(row.device)) {
switch (gps_get_state(row.device)) {
case GPS_STATE_PENDING_ON:
text = "Starting...";
enabled = false;
break;
case GPS_STATE_PENDING_OFF:
text = "Stopping...";
enabled = false;
break;
default:
text = "Stop";
enabled = true;
break;
}
}
lv_label_set_text(row.buttonLabel, text);
if (enabled) {
lv_obj_remove_state(row.button, LV_STATE_DISABLED);
} else {
lv_obj_add_state(row.button, LV_STATE_DISABLED);
}
}
}
}
public:
GpsSettingsApp() {
// Runs while the screen is shown - there's no push notification for GPS device state
// changes, so this is the only way this screen finds out about them.
timer = std::make_unique<Timer>(Timer::Type::Periodic, kernel::secondsToTicks(1), [this] {
updateViews();
updateDeviceStates();
});
service = service::gps::findGpsService();
}
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);
uint8_t margin = (lvgl_get_ui_density() == LVGL_UI_DENSITY_COMPACT) ? 2 : 8;
auto* toolbar = lvgl::toolbar_create(parent, app);
lvgl::toolbar_add_text_button_action(toolbar, LV_SYMBOL_PLUS, onAddGpsCallback, this);
lv_obj_set_style_margin_bottom(toolbar, margin, LV_STATE_DEFAULT);
spinnerWidget = lvgl::toolbar_add_spinner_action(toolbar);
lv_obj_add_flag(spinnerWidget, LV_OBJ_FLAG_HIDDEN);
deviceListWrapper = lv_obj_create(parent);
lv_obj_set_size(deviceListWrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(deviceListWrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_grow(deviceListWrapper, 1);
lv_obj_set_style_border_width(deviceListWrapper, 0, 0);
lv_obj_set_style_pad_hor(deviceListWrapper, margin, 0);
lv_obj_set_style_pad_top(deviceListWrapper, 0, 0);
lv_obj_set_style_pad_bottom(deviceListWrapper, margin, 0);
lv_obj_set_style_pad_row(deviceListWrapper, margin, 0);
switchWidget = lvgl::toolbar_add_switch_action(toolbar);
lv_obj_add_event_cb(switchWidget, onGpsToggledCallback, LV_EVENT_VALUE_CHANGED, this);
rebuildDeviceList();
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);
lv_obj_set_style_border_width(main_wrapper, 0, 0);
lv_obj_set_style_pad_all(main_wrapper, 0, 0);
timer->start();
updateDeviceStates();
statusWrapper = lv_obj_create(main_wrapper);
lv_obj_set_width(statusWrapper, LV_PCT(100));
lv_obj_set_height(statusWrapper, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(statusWrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(statusWrapper, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(statusWrapper, 0, 0);
lv_obj_set_style_pad_row(statusWrapper, 8, 0);
lv_obj_set_style_border_width(statusWrapper, 0, 0);
statusLabelWidget = lv_label_create(statusWrapper);
infoContainerWidget = lv_obj_create(statusWrapper);
lv_obj_set_size(infoContainerWidget, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(infoContainerWidget, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_border_width(infoContainerWidget, 0, 0);
lv_obj_set_style_pad_row(infoContainerWidget, 5, 0);
lv_obj_set_style_pad_hor(infoContainerWidget, 10, 0);
hasSetInfo = false;
statusLatitudeValue = createInfoRow(infoContainerWidget, "Latitude", lv_color_hex(0x00ff00));
statusLongitudeValue = createInfoRow(infoContainerWidget, "Longitude", lv_color_hex(0x00ff00));
statusAltitudeValue = createInfoRow(infoContainerWidget, "Altitude", lv_color_hex(0x00ffff));
statusSpeedValue = createInfoRow(infoContainerWidget, "Speed", lv_color_hex(0xffff00));
statusHeadingValue = createInfoRow(infoContainerWidget, "Heading", lv_color_hex(0xff88ff));
statusSatellitesValue = createInfoRow(infoContainerWidget, "Satellites", lv_color_hex(0xffffff));
serviceStateSubscription = service->getStatePubsub()->subscribe([this](auto) {
onServiceStateChanged();
});
gpsConfigWrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(gpsConfigWrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_border_width(gpsConfigWrapper, 0, 0);
lv_obj_set_style_margin_all(gpsConfigWrapper, 0, 0);
lv_obj_set_style_pad_bottom(gpsConfigWrapper, 0, 0);
addGpsWrapper = lv_obj_create(main_wrapper);
lv_obj_set_size(addGpsWrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_border_width(addGpsWrapper, 0, 0);
lv_obj_set_style_pad_all(addGpsWrapper, 0, 0);
lv_obj_set_style_margin_top(addGpsWrapper, 0, 0);
lv_obj_set_style_margin_bottom(addGpsWrapper, 8, 0);
auto* add_gps_button = lv_button_create(addGpsWrapper);
auto* add_gps_label = lv_label_create(add_gps_button);
lv_label_set_text(add_gps_label, "Add GPS");
lv_obj_add_event_cb(add_gps_button, onAddGpsCallback, LV_EVENT_SHORT_CLICKED, this);
lv_obj_align(add_gps_button, LV_ALIGN_TOP_MID, 0, 0);
updateTimerState();
updateViews();
// Only after deviceListWrapper is fully built: onResult() (Loader thread) checks
// this before touching it, since it can run before or after this onShow() call.
isShown = true;
}
void onHide(AppContext& app) override {
service->getStatePubsub()->unsubscribe(serviceStateSubscription);
serviceStateSubscription = nullptr;
isShown = false;
timer->stop();
}
void onResult(AppContext&, LaunchId, Result result, std::unique_ptr<Bundle> bundle) override {
if (!hasPendingDelete) {
return;
}
hasPendingDelete = false;
if (result != Result::Ok || bundle == nullptr || alertdialog::getResultIndex(*bundle) != 0) { // 0 = Yes
return;
}
// This runs on the Loader thread, concurrently with the periodic timer callback
// (updateDeviceStates(), timer daemon thread) and possibly with onShow() (GUI
// thread). Take the same lock updateDeviceStates() uses and hold it across the
// free below, so the timer can never observe pendingDeleteDevice as a dangling
// pointer in deviceRows.
lvgl_lock();
// Drop the stale row unconditionally (cheap vector op, no LVGL calls) - this is
// what keeps the timer safe regardless of whether onShow() has run yet this cycle.
std::erase_if(deviceRows, [this](const DeviceRow& row) {
return row.device == pendingDeleteDevice;
});
lvgl_unlock();
// gps_settings_remove_configuration_at() frees the underlying Device synchronously -
// do this only after the dangling pointer is already out of deviceRows.
gps_settings_remove_configuration_at(pendingDeleteIndex);
pendingDeleteDevice = nullptr;
// Only safe to touch deviceListWrapper if onShow() already built it for this show
// cycle - it may not have run yet, in which case it'll rebuild fresh (post-deletion,
// deviceRows already correct) when it does.
lvgl_lock();
if (isShown) {
rebuildDeviceList();
}
lvgl_unlock();
}
};
+2 -1
View File
@@ -87,7 +87,8 @@ class NotesApp final : public App {
file::getLock(path)->withLock([this, path] {
auto data = file::readString(path);
if (data != nullptr) {
auto lock = lvgl::getSyncLock()->asScopedLock();
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
lock.lock();
lv_textarea_set_text(uiNoteText, reinterpret_cast<const char*>(data.get()));
lv_label_set_text(uiCurrentFileName, path.c_str());
@@ -4,13 +4,11 @@
#include <Tactility/app/AppContext.h>
#include <Tactility/app/AppRegistration.h>
#include <Tactility/hal/display/DisplayDevice.h>
#include <Tactility/service/loader/Loader.h>
#include <lvgl.h>
#include <tactility/device.h>
#include <tactility/drivers/power_supply.h>
#include <tactility/hal/Device.h>
#include <tactility/lvgl_fonts.h>
#include <tactility/lvgl_icon_shared.h>
@@ -87,7 +87,8 @@ ScreenshotApp::~ScreenshotApp() {
}
void ScreenshotApp::onTimerTick() {
auto lock = lvgl::getSyncLock()->asScopedLock();
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
if (lock.lock(lvgl::defaultLockTime)) {
updateScreenshotMode();
}
@@ -247,7 +247,8 @@ class SystemInfoApp final : public App {
Timer memoryTimer = Timer(Timer::Type::Periodic, kernel::millisToTicks(10000), [] {
auto app = optApp();
if (app) {
auto lock = lvgl::getSyncLock()->asScopedLock();
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
lock.lock();
app->updateMemory();
}
@@ -256,7 +257,8 @@ class SystemInfoApp final : public App {
Timer tasksTimer = Timer(Timer::Type::Periodic, kernel::millisToTicks(15000), [] {
auto app = optApp();
if (app) {
auto lock = lvgl::getSyncLock()->asScopedLock();
auto lockable = lvgl::getSyncLock();
auto lock = lockable->asScopedLock();
lock.lock();
app->updateTasks();
}
@@ -1,4 +1,4 @@
#include "tactility/lvgl_module.h"
#include <tactility/lvgl_module.h>
#include <Tactility/RecursiveMutex.h>