Files
pyxis/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp
torlando-tech ac6ceca9f8 Initial commit: standalone Pyxis T-Deck firmware
Split T-Deck firmware from microReticulum examples/lxmf_tdeck/ into its
own repo. microReticulum is consumed as a git submodule dependency pinned
to feat/t-deck. All include paths updated from relative symlinks to bare
includes resolved via library build flags.

Both tdeck (NimBLE) and tdeck-bluedroid environments compile successfully.
Licensed under AGPLv3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 19:48:33 -05:00

1440 lines
60 KiB
C++

// Copyright (c) 2024 microReticulum contributors
// SPDX-License-Identifier: MIT
#include "SettingsScreen.h"
#include "Theme.h"
#ifdef ARDUINO
#include <WiFi.h>
#include "../LVGL/LVGLLock.h"
#include <SPIFFS.h>
#include <TinyGPSPlus.h>
#include "Log.h"
#include "Utilities/OS.h"
#include "../LVGL/LVGLInit.h"
#include "../TextAreaHelper.h"
using namespace RNS;
namespace UI {
namespace LXMF {
// NVS keys
static const char* NVS_NAMESPACE = "settings";
static const char* KEY_WIFI_SSID = "wifi_ssid";
static const char* KEY_WIFI_PASS = "wifi_pass";
static const char* KEY_TCP_HOST = "tcp_host";
static const char* KEY_TCP_PORT = "tcp_port";
static const char* KEY_DISPLAY_NAME = "disp_name";
static const char* KEY_BRIGHTNESS = "brightness";
static const char* KEY_KB_LIGHT = "kb_light";
static const char* KEY_TIMEOUT = "timeout";
static const char* KEY_ANNOUNCE_INT = "announce";
static const char* KEY_SYNC_INT = "sync_int";
static const char* KEY_GPS_SYNC = "gps_sync";
// Notification settings
static const char* KEY_NOTIF_SND = "notif_snd";
static const char* KEY_NOTIF_VOL = "notif_vol";
// Interface settings
static const char* KEY_TCP_ENABLED = "tcp_en";
static const char* KEY_LORA_ENABLED = "lora_en";
static const char* KEY_LORA_FREQ = "lora_freq";
static const char* KEY_LORA_BW = "lora_bw";
static const char* KEY_LORA_SF = "lora_sf";
static const char* KEY_LORA_CR = "lora_cr";
static const char* KEY_LORA_POWER = "lora_pwr";
static const char* KEY_AUTO_ENABLED = "auto_en";
static const char* KEY_BLE_ENABLED = "ble_en";
// Propagation settings
static const char* KEY_PROP_AUTO = "prop_auto";
static const char* KEY_PROP_NODE = "prop_node";
static const char* KEY_PROP_FALLBACK = "prop_fall";
static const char* KEY_PROP_ONLY = "prop_only";
SettingsScreen::SettingsScreen(lv_obj_t* parent)
: _screen(nullptr), _header(nullptr), _content(nullptr),
_btn_back(nullptr), _btn_save(nullptr),
_ta_wifi_ssid(nullptr), _ta_wifi_password(nullptr),
_ta_tcp_host(nullptr), _ta_tcp_port(nullptr), _btn_reconnect(nullptr),
_ta_display_name(nullptr),
_slider_brightness(nullptr), _label_brightness_value(nullptr), _switch_kb_light(nullptr), _dropdown_timeout(nullptr),
_label_gps_sats(nullptr), _label_gps_coords(nullptr), _label_gps_alt(nullptr), _label_gps_hdop(nullptr),
_label_identity_hash(nullptr), _label_lxmf_address(nullptr), _label_firmware(nullptr),
_label_storage(nullptr), _label_ram(nullptr),
_switch_tcp_enabled(nullptr), _switch_lora_enabled(nullptr),
_ta_lora_frequency(nullptr), _dropdown_lora_bandwidth(nullptr),
_dropdown_lora_sf(nullptr), _dropdown_lora_cr(nullptr),
_slider_lora_power(nullptr), _label_lora_power_value(nullptr),
_lora_params_container(nullptr), _switch_auto_enabled(nullptr), _switch_ble_enabled(nullptr),
_ta_announce_interval(nullptr), _ta_sync_interval(nullptr), _switch_gps_sync(nullptr),
_btn_propagation_nodes(nullptr), _switch_prop_fallback(nullptr), _switch_prop_only(nullptr),
_gps(nullptr) {
LVGL_LOCK();
// Create screen object
if (parent) {
_screen = lv_obj_create(parent);
} else {
_screen = lv_obj_create(lv_scr_act());
}
lv_obj_set_size(_screen, LV_PCT(100), LV_PCT(100));
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_bg_color(_screen, Theme::surface(), 0);
lv_obj_set_style_bg_opa(_screen, LV_OPA_COVER, 0);
lv_obj_set_style_pad_all(_screen, 0, 0);
lv_obj_set_style_border_width(_screen, 0, 0);
lv_obj_set_style_radius(_screen, 0, 0);
// Load settings from NVS
load_settings();
// Create UI components
create_header();
create_content();
// Update UI with loaded settings
update_ui_from_settings();
// Hide by default
hide();
TRACE("SettingsScreen created");
}
SettingsScreen::~SettingsScreen() {
LVGL_LOCK();
if (_screen) {
lv_obj_del(_screen);
}
}
void SettingsScreen::create_header() {
_header = lv_obj_create(_screen);
lv_obj_set_size(_header, LV_PCT(100), 36);
lv_obj_align(_header, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_set_style_bg_color(_header, Theme::surfaceHeader(), 0);
lv_obj_set_style_border_width(_header, 0, 0);
lv_obj_set_style_radius(_header, 0, 0);
lv_obj_set_style_pad_all(_header, 0, 0);
// Back button
_btn_back = lv_btn_create(_header);
lv_obj_set_size(_btn_back, 50, 28);
lv_obj_align(_btn_back, LV_ALIGN_LEFT_MID, 2, 0);
lv_obj_set_style_bg_color(_btn_back, Theme::btnSecondary(), 0);
lv_obj_set_style_bg_color(_btn_back, Theme::btnSecondaryPressed(), LV_STATE_PRESSED);
lv_obj_add_event_cb(_btn_back, on_back_clicked, LV_EVENT_CLICKED, this);
lv_obj_t* label_back = lv_label_create(_btn_back);
lv_label_set_text(label_back, LV_SYMBOL_LEFT);
lv_obj_center(label_back);
lv_obj_set_style_text_color(label_back, Theme::textSecondary(), 0);
// Title
lv_obj_t* title = lv_label_create(_header);
lv_label_set_text(title, "Settings");
lv_obj_align(title, LV_ALIGN_LEFT_MID, 60, 0);
lv_obj_set_style_text_color(title, Theme::textPrimary(), 0);
lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0);
// Save button
_btn_save = lv_btn_create(_header);
lv_obj_set_size(_btn_save, 55, 28);
lv_obj_align(_btn_save, LV_ALIGN_RIGHT_MID, -4, 0);
lv_obj_set_style_bg_color(_btn_save, Theme::primary(), 0);
lv_obj_set_style_bg_color(_btn_save, Theme::primaryPressed(), LV_STATE_PRESSED);
lv_obj_add_event_cb(_btn_save, on_save_clicked, LV_EVENT_CLICKED, this);
lv_obj_t* label_save = lv_label_create(_btn_save);
lv_label_set_text(label_save, "Save");
lv_obj_center(label_save);
lv_obj_set_style_text_color(label_save, Theme::textPrimary(), 0);
}
void SettingsScreen::create_content() {
_content = lv_obj_create(_screen);
lv_obj_set_size(_content, LV_PCT(100), 204); // 240 - 36 header
lv_obj_align(_content, LV_ALIGN_TOP_MID, 0, 36);
lv_obj_set_style_pad_all(_content, 4, 0);
lv_obj_set_style_pad_gap(_content, 2, 0);
lv_obj_set_style_bg_color(_content, Theme::surface(), 0);
lv_obj_set_style_border_width(_content, 0, 0);
lv_obj_set_style_radius(_content, 0, 0);
// Enable vertical scrolling with flex layout
lv_obj_set_flex_flow(_content, LV_FLEX_FLOW_COLUMN);
lv_obj_set_scroll_dir(_content, LV_DIR_VER);
lv_obj_set_scrollbar_mode(_content, LV_SCROLLBAR_MODE_AUTO);
// Create sections
create_network_section(_content);
create_identity_section(_content);
create_display_section(_content);
create_notifications_section(_content);
create_interfaces_section(_content);
create_delivery_section(_content);
create_gps_section(_content);
create_system_section(_content);
create_advanced_section(_content);
}
lv_obj_t* SettingsScreen::create_section_header(lv_obj_t* parent, const char* title) {
lv_obj_t* header = lv_label_create(parent);
lv_label_set_text(header, title);
lv_obj_set_width(header, LV_PCT(100));
lv_obj_set_style_text_color(header, Theme::info(), 0);
lv_obj_set_style_text_font(header, &lv_font_montserrat_12, 0);
lv_obj_set_style_pad_top(header, 6, 0);
lv_obj_set_style_pad_bottom(header, 2, 0);
return header;
}
lv_obj_t* SettingsScreen::create_label_row(lv_obj_t* parent, const char* text) {
lv_obj_t* label = lv_label_create(parent);
lv_label_set_text(label, text);
lv_obj_set_width(label, LV_PCT(100));
lv_obj_set_style_text_color(label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
return label;
}
lv_obj_t* SettingsScreen::create_text_input(lv_obj_t* parent, const char* placeholder,
bool password, int max_len) {
lv_obj_t* ta = lv_textarea_create(parent);
lv_obj_set_width(ta, LV_PCT(100));
lv_obj_set_height(ta, 28);
lv_textarea_set_placeholder_text(ta, placeholder);
lv_textarea_set_one_line(ta, true);
lv_textarea_set_max_length(ta, max_len);
if (password) {
lv_textarea_set_password_mode(ta, true);
}
lv_obj_set_style_bg_color(ta, Theme::surfaceInput(), 0);
lv_obj_set_style_text_color(ta, Theme::textPrimary(), 0);
lv_obj_set_style_border_color(ta, Theme::border(), 0);
lv_obj_set_style_border_width(ta, 1, 0);
lv_obj_set_style_radius(ta, 4, 0);
lv_obj_set_style_pad_all(ta, 4, 0);
lv_obj_set_style_text_font(ta, &lv_font_montserrat_14, 0);
// Add to input group for keyboard navigation
lv_group_t* group = LVGL::LVGLInit::get_default_group();
if (group) {
lv_group_add_obj(group, ta);
}
// Enable paste on long-press
TextAreaHelper::enable_paste(ta);
return ta;
}
void SettingsScreen::create_network_section(lv_obj_t* parent) {
create_section_header(parent, "== Network ==");
create_label_row(parent, "WiFi SSID:");
_ta_wifi_ssid = create_text_input(parent, "Enter SSID", false, 32);
create_label_row(parent, "WiFi Password:");
_ta_wifi_password = create_text_input(parent, "Enter password", true, 64);
create_label_row(parent, "TCP Server:");
_ta_tcp_host = create_text_input(parent, "IP or hostname", false, 64);
// Port and reconnect row
lv_obj_t* port_row = lv_obj_create(parent);
lv_obj_set_width(port_row, LV_PCT(100));
lv_obj_set_height(port_row, 32);
lv_obj_set_style_bg_opa(port_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(port_row, 0, 0);
lv_obj_set_style_pad_all(port_row, 0, 0);
lv_obj_clear_flag(port_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* port_label = lv_label_create(port_row);
lv_label_set_text(port_label, "Port:");
lv_obj_align(port_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(port_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(port_label, &lv_font_montserrat_14, 0);
_ta_tcp_port = lv_textarea_create(port_row);
lv_obj_set_size(_ta_tcp_port, 60, 26);
lv_obj_align(_ta_tcp_port, LV_ALIGN_LEFT_MID, 35, 0);
lv_textarea_set_one_line(_ta_tcp_port, true);
lv_textarea_set_max_length(_ta_tcp_port, 5);
lv_textarea_set_accepted_chars(_ta_tcp_port, "0123456789");
lv_obj_set_style_bg_color(_ta_tcp_port, Theme::surfaceInput(), 0);
lv_obj_set_style_text_color(_ta_tcp_port, Theme::textPrimary(), 0);
lv_obj_set_style_border_color(_ta_tcp_port, Theme::border(), 0);
lv_obj_set_style_border_width(_ta_tcp_port, 1, 0);
lv_obj_set_style_radius(_ta_tcp_port, 4, 0);
lv_obj_set_style_pad_all(_ta_tcp_port, 4, 0);
lv_obj_set_style_text_font(_ta_tcp_port, &lv_font_montserrat_14, 0);
// Add to input group for keyboard navigation
lv_group_t* group = LVGL::LVGLInit::get_default_group();
if (group) {
lv_group_add_obj(group, _ta_tcp_port);
}
// Enable paste on long-press
TextAreaHelper::enable_paste(_ta_tcp_port);
_btn_reconnect = lv_btn_create(port_row);
lv_obj_set_size(_btn_reconnect, 80, 26);
lv_obj_align(_btn_reconnect, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_btn_reconnect, Theme::primary(), 0);
lv_obj_set_style_bg_color(_btn_reconnect, Theme::primaryPressed(), LV_STATE_PRESSED);
lv_obj_add_event_cb(_btn_reconnect, on_reconnect_clicked, LV_EVENT_CLICKED, this);
lv_obj_t* label_reconnect = lv_label_create(_btn_reconnect);
lv_label_set_text(label_reconnect, "Reconnect");
lv_obj_center(label_reconnect);
lv_obj_set_style_text_color(label_reconnect, Theme::textPrimary(), 0);
lv_obj_set_style_text_font(label_reconnect, &lv_font_montserrat_14, 0);
}
void SettingsScreen::create_identity_section(lv_obj_t* parent) {
create_section_header(parent, "== Identity ==");
create_label_row(parent, "Display Name:");
_ta_display_name = create_text_input(parent, "Your name", false, 32);
}
void SettingsScreen::create_display_section(lv_obj_t* parent) {
create_section_header(parent, "== Display ==");
// Brightness row
lv_obj_t* brightness_row = lv_obj_create(parent);
lv_obj_set_width(brightness_row, LV_PCT(100));
lv_obj_set_height(brightness_row, 28);
lv_obj_set_style_bg_opa(brightness_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(brightness_row, 0, 0);
lv_obj_set_style_pad_all(brightness_row, 0, 0);
lv_obj_clear_flag(brightness_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* bright_label = lv_label_create(brightness_row);
lv_label_set_text(bright_label, "Brightness:");
lv_obj_align(bright_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(bright_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(bright_label, &lv_font_montserrat_14, 0);
_slider_brightness = lv_slider_create(brightness_row);
lv_obj_set_size(_slider_brightness, 120, 10);
lv_obj_align(_slider_brightness, LV_ALIGN_LEFT_MID, 95, 0);
lv_slider_set_range(_slider_brightness, 10, 255);
lv_obj_set_style_bg_color(_slider_brightness, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_slider_brightness, Theme::info(), LV_PART_INDICATOR);
lv_obj_set_style_bg_color(_slider_brightness, Theme::textPrimary(), LV_PART_KNOB);
lv_obj_add_event_cb(_slider_brightness, on_brightness_changed, LV_EVENT_VALUE_CHANGED, this);
_label_brightness_value = lv_label_create(brightness_row);
lv_label_set_text(_label_brightness_value, "180");
lv_obj_align(_label_brightness_value, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_text_color(_label_brightness_value, Theme::textPrimary(), 0);
lv_obj_set_style_text_font(_label_brightness_value, &lv_font_montserrat_14, 0);
// Keyboard light row
lv_obj_t* kb_light_row = lv_obj_create(parent);
lv_obj_set_width(kb_light_row, LV_PCT(100));
lv_obj_set_height(kb_light_row, 28);
lv_obj_set_style_bg_opa(kb_light_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(kb_light_row, 0, 0);
lv_obj_set_style_pad_all(kb_light_row, 0, 0);
lv_obj_clear_flag(kb_light_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* kb_light_label = lv_label_create(kb_light_row);
lv_label_set_text(kb_light_label, "Keyboard Light:");
lv_obj_align(kb_light_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(kb_light_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(kb_light_label, &lv_font_montserrat_14, 0);
_switch_kb_light = lv_switch_create(kb_light_row);
lv_obj_set_size(_switch_kb_light, 40, 20);
lv_obj_align(_switch_kb_light, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_switch_kb_light, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_switch_kb_light, Theme::primary(), LV_PART_INDICATOR | LV_STATE_CHECKED);
// Timeout row
lv_obj_t* timeout_row = lv_obj_create(parent);
lv_obj_set_width(timeout_row, LV_PCT(100));
lv_obj_set_height(timeout_row, 28);
lv_obj_set_style_bg_opa(timeout_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(timeout_row, 0, 0);
lv_obj_set_style_pad_all(timeout_row, 0, 0);
lv_obj_clear_flag(timeout_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* timeout_label = lv_label_create(timeout_row);
lv_label_set_text(timeout_label, "Screen Timeout:");
lv_obj_align(timeout_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(timeout_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(timeout_label, &lv_font_montserrat_14, 0);
_dropdown_timeout = lv_dropdown_create(timeout_row);
lv_dropdown_set_options(_dropdown_timeout, "30 sec\n1 min\n5 min\nNever");
lv_obj_set_size(_dropdown_timeout, 90, 28);
lv_obj_align(_dropdown_timeout, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_dropdown_timeout, Theme::surfaceInput(), 0);
lv_obj_set_style_text_color(_dropdown_timeout, Theme::textPrimary(), 0);
lv_obj_set_style_border_color(_dropdown_timeout, Theme::border(), 0);
lv_obj_set_style_text_font(_dropdown_timeout, &lv_font_montserrat_14, 0);
}
void SettingsScreen::create_notifications_section(lv_obj_t* parent) {
create_section_header(parent, "== Notifications ==");
// Sound enabled row
lv_obj_t* sound_row = lv_obj_create(parent);
lv_obj_set_width(sound_row, LV_PCT(100));
lv_obj_set_height(sound_row, 28);
lv_obj_set_style_bg_opa(sound_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(sound_row, 0, 0);
lv_obj_set_style_pad_all(sound_row, 0, 0);
lv_obj_clear_flag(sound_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* sound_label = lv_label_create(sound_row);
lv_label_set_text(sound_label, "Message Sound:");
lv_obj_align(sound_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(sound_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(sound_label, &lv_font_montserrat_14, 0);
_switch_notification_sound = lv_switch_create(sound_row);
lv_obj_set_size(_switch_notification_sound, 40, 20);
lv_obj_align(_switch_notification_sound, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_switch_notification_sound, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_switch_notification_sound, Theme::primary(), LV_PART_INDICATOR | LV_STATE_CHECKED);
// Volume row
lv_obj_t* volume_row = lv_obj_create(parent);
lv_obj_set_width(volume_row, LV_PCT(100));
lv_obj_set_height(volume_row, 28);
lv_obj_set_style_bg_opa(volume_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(volume_row, 0, 0);
lv_obj_set_style_pad_all(volume_row, 0, 0);
lv_obj_clear_flag(volume_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* volume_label = lv_label_create(volume_row);
lv_label_set_text(volume_label, "Volume:");
lv_obj_align(volume_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(volume_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(volume_label, &lv_font_montserrat_14, 0);
_slider_notification_volume = lv_slider_create(volume_row);
lv_obj_set_size(_slider_notification_volume, 120, 10);
lv_obj_align(_slider_notification_volume, LV_ALIGN_LEFT_MID, 65, 0);
lv_slider_set_range(_slider_notification_volume, 0, 100);
lv_obj_set_style_bg_color(_slider_notification_volume, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_slider_notification_volume, Theme::info(), LV_PART_INDICATOR);
lv_obj_set_style_bg_color(_slider_notification_volume, Theme::textPrimary(), LV_PART_KNOB);
lv_obj_add_event_cb(_slider_notification_volume, on_notification_volume_changed, LV_EVENT_VALUE_CHANGED, this);
_label_notification_volume_value = lv_label_create(volume_row);
lv_label_set_text(_label_notification_volume_value, "50");
lv_obj_align(_label_notification_volume_value, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_text_color(_label_notification_volume_value, Theme::textPrimary(), 0);
lv_obj_set_style_text_font(_label_notification_volume_value, &lv_font_montserrat_14, 0);
}
void SettingsScreen::create_interfaces_section(lv_obj_t* parent) {
create_section_header(parent, "== Interfaces ==");
// Auto Discovery row
lv_obj_t* auto_row = lv_obj_create(parent);
lv_obj_set_width(auto_row, LV_PCT(100));
lv_obj_set_height(auto_row, 28);
lv_obj_set_style_bg_opa(auto_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(auto_row, 0, 0);
lv_obj_set_style_pad_all(auto_row, 0, 0);
lv_obj_clear_flag(auto_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* auto_label = lv_label_create(auto_row);
lv_label_set_text(auto_label, "Auto Discovery:");
lv_obj_align(auto_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(auto_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(auto_label, &lv_font_montserrat_14, 0);
_switch_auto_enabled = lv_switch_create(auto_row);
lv_obj_set_size(_switch_auto_enabled, 40, 20);
lv_obj_align(_switch_auto_enabled, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_switch_auto_enabled, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_switch_auto_enabled, Theme::primary(), LV_PART_INDICATOR | LV_STATE_CHECKED);
// BLE P2P row
lv_obj_t* ble_row = lv_obj_create(parent);
lv_obj_set_width(ble_row, LV_PCT(100));
lv_obj_set_height(ble_row, 28);
lv_obj_set_style_bg_opa(ble_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(ble_row, 0, 0);
lv_obj_set_style_pad_all(ble_row, 0, 0);
lv_obj_clear_flag(ble_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* ble_label = lv_label_create(ble_row);
lv_label_set_text(ble_label, "BLE P2P:");
lv_obj_align(ble_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(ble_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(ble_label, &lv_font_montserrat_14, 0);
_switch_ble_enabled = lv_switch_create(ble_row);
lv_obj_set_size(_switch_ble_enabled, 40, 20);
lv_obj_align(_switch_ble_enabled, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_switch_ble_enabled, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_switch_ble_enabled, Theme::primary(), LV_PART_INDICATOR | LV_STATE_CHECKED);
// TCP Enable row
lv_obj_t* tcp_row = lv_obj_create(parent);
lv_obj_set_width(tcp_row, LV_PCT(100));
lv_obj_set_height(tcp_row, 28);
lv_obj_set_style_bg_opa(tcp_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(tcp_row, 0, 0);
lv_obj_set_style_pad_all(tcp_row, 0, 0);
lv_obj_clear_flag(tcp_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* tcp_label = lv_label_create(tcp_row);
lv_label_set_text(tcp_label, "TCP Interface:");
lv_obj_align(tcp_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(tcp_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(tcp_label, &lv_font_montserrat_14, 0);
_switch_tcp_enabled = lv_switch_create(tcp_row);
lv_obj_set_size(_switch_tcp_enabled, 40, 20);
lv_obj_align(_switch_tcp_enabled, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_switch_tcp_enabled, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_switch_tcp_enabled, Theme::primary(), LV_PART_INDICATOR | LV_STATE_CHECKED);
// LoRa Enable row
lv_obj_t* lora_row = lv_obj_create(parent);
lv_obj_set_width(lora_row, LV_PCT(100));
lv_obj_set_height(lora_row, 28);
lv_obj_set_style_bg_opa(lora_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(lora_row, 0, 0);
lv_obj_set_style_pad_all(lora_row, 0, 0);
lv_obj_clear_flag(lora_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* lora_label = lv_label_create(lora_row);
lv_label_set_text(lora_label, "LoRa Interface:");
lv_obj_align(lora_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(lora_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(lora_label, &lv_font_montserrat_14, 0);
_switch_lora_enabled = lv_switch_create(lora_row);
lv_obj_set_size(_switch_lora_enabled, 40, 20);
lv_obj_align(_switch_lora_enabled, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_switch_lora_enabled, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_switch_lora_enabled, Theme::primary(), LV_PART_INDICATOR | LV_STATE_CHECKED);
lv_obj_add_event_cb(_switch_lora_enabled, on_lora_enabled_changed, LV_EVENT_VALUE_CHANGED, this);
// LoRa parameters container (shown/hidden based on LoRa enabled)
_lora_params_container = lv_obj_create(parent);
lv_obj_set_width(_lora_params_container, LV_PCT(100));
lv_obj_set_height(_lora_params_container, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(_lora_params_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(_lora_params_container, 0, 0);
lv_obj_set_style_pad_all(_lora_params_container, 0, 0);
lv_obj_set_style_pad_gap(_lora_params_container, 2, 0);
lv_obj_set_flex_flow(_lora_params_container, LV_FLEX_FLOW_COLUMN);
lv_obj_clear_flag(_lora_params_container, LV_OBJ_FLAG_SCROLLABLE);
// Frequency row
lv_obj_t* freq_row = lv_obj_create(_lora_params_container);
lv_obj_set_width(freq_row, LV_PCT(100));
lv_obj_set_height(freq_row, 28);
lv_obj_set_style_bg_opa(freq_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(freq_row, 0, 0);
lv_obj_set_style_pad_all(freq_row, 0, 0);
lv_obj_clear_flag(freq_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* freq_label = lv_label_create(freq_row);
lv_label_set_text(freq_label, " Frequency:");
lv_obj_align(freq_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(freq_label, lv_color_hex(0x909090), 0);
lv_obj_set_style_text_font(freq_label, &lv_font_montserrat_14, 0);
_ta_lora_frequency = lv_textarea_create(freq_row);
lv_obj_set_size(_ta_lora_frequency, 80, 24);
lv_obj_align(_ta_lora_frequency, LV_ALIGN_RIGHT_MID, -30, 0);
lv_textarea_set_one_line(_ta_lora_frequency, true);
lv_textarea_set_max_length(_ta_lora_frequency, 8);
lv_textarea_set_accepted_chars(_ta_lora_frequency, "0123456789.");
lv_obj_set_style_bg_color(_ta_lora_frequency, Theme::surfaceInput(), 0);
lv_obj_set_style_text_color(_ta_lora_frequency, Theme::textPrimary(), 0);
lv_obj_set_style_border_color(_ta_lora_frequency, Theme::border(), 0);
lv_obj_set_style_border_width(_ta_lora_frequency, 1, 0);
lv_obj_set_style_radius(_ta_lora_frequency, 4, 0);
lv_obj_set_style_pad_all(_ta_lora_frequency, 4, 0);
lv_obj_set_style_text_font(_ta_lora_frequency, &lv_font_montserrat_14, 0);
// Enable paste on long-press
TextAreaHelper::enable_paste(_ta_lora_frequency);
lv_obj_t* mhz_label = lv_label_create(freq_row);
lv_label_set_text(mhz_label, "MHz");
lv_obj_align(mhz_label, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_text_color(mhz_label, Theme::textMuted(), 0);
lv_obj_set_style_text_font(mhz_label, &lv_font_montserrat_14, 0);
// Bandwidth dropdown row
lv_obj_t* bw_row = lv_obj_create(_lora_params_container);
lv_obj_set_width(bw_row, LV_PCT(100));
lv_obj_set_height(bw_row, 28);
lv_obj_set_style_bg_opa(bw_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(bw_row, 0, 0);
lv_obj_set_style_pad_all(bw_row, 0, 0);
lv_obj_clear_flag(bw_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* bw_label = lv_label_create(bw_row);
lv_label_set_text(bw_label, " Bandwidth:");
lv_obj_align(bw_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(bw_label, lv_color_hex(0x909090), 0);
lv_obj_set_style_text_font(bw_label, &lv_font_montserrat_14, 0);
_dropdown_lora_bandwidth = lv_dropdown_create(bw_row);
lv_dropdown_set_options(_dropdown_lora_bandwidth, "62.5 kHz\n125 kHz\n250 kHz\n500 kHz");
lv_obj_set_size(_dropdown_lora_bandwidth, 100, 28);
lv_obj_align(_dropdown_lora_bandwidth, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_dropdown_lora_bandwidth, Theme::surfaceInput(), 0);
lv_obj_set_style_text_color(_dropdown_lora_bandwidth, Theme::textPrimary(), 0);
lv_obj_set_style_border_color(_dropdown_lora_bandwidth, Theme::border(), 0);
lv_obj_set_style_text_font(_dropdown_lora_bandwidth, &lv_font_montserrat_14, 0);
// SF/CR row
lv_obj_t* sfcr_row = lv_obj_create(_lora_params_container);
lv_obj_set_width(sfcr_row, LV_PCT(100));
lv_obj_set_height(sfcr_row, 28);
lv_obj_set_style_bg_opa(sfcr_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(sfcr_row, 0, 0);
lv_obj_set_style_pad_all(sfcr_row, 0, 0);
lv_obj_clear_flag(sfcr_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* sf_label = lv_label_create(sfcr_row);
lv_label_set_text(sf_label, " SF:");
lv_obj_align(sf_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(sf_label, lv_color_hex(0x909090), 0);
lv_obj_set_style_text_font(sf_label, &lv_font_montserrat_14, 0);
_dropdown_lora_sf = lv_dropdown_create(sfcr_row);
lv_dropdown_set_options(_dropdown_lora_sf, "7\n8\n9\n10\n11\n12");
lv_obj_set_size(_dropdown_lora_sf, 50, 28);
lv_obj_align(_dropdown_lora_sf, LV_ALIGN_LEFT_MID, 30, 0);
lv_obj_set_style_bg_color(_dropdown_lora_sf, Theme::surfaceInput(), 0);
lv_obj_set_style_text_color(_dropdown_lora_sf, Theme::textPrimary(), 0);
lv_obj_set_style_border_color(_dropdown_lora_sf, Theme::border(), 0);
lv_obj_set_style_text_font(_dropdown_lora_sf, &lv_font_montserrat_14, 0);
lv_obj_t* cr_label = lv_label_create(sfcr_row);
lv_label_set_text(cr_label, "CR:");
lv_obj_align(cr_label, LV_ALIGN_LEFT_MID, 90, 0);
lv_obj_set_style_text_color(cr_label, lv_color_hex(0x909090), 0);
lv_obj_set_style_text_font(cr_label, &lv_font_montserrat_14, 0);
_dropdown_lora_cr = lv_dropdown_create(sfcr_row);
lv_dropdown_set_options(_dropdown_lora_cr, "4/5\n4/6\n4/7\n4/8");
lv_obj_set_size(_dropdown_lora_cr, 55, 28);
lv_obj_align(_dropdown_lora_cr, LV_ALIGN_LEFT_MID, 115, 0);
lv_obj_set_style_bg_color(_dropdown_lora_cr, Theme::surfaceInput(), 0);
lv_obj_set_style_text_color(_dropdown_lora_cr, Theme::textPrimary(), 0);
lv_obj_set_style_border_color(_dropdown_lora_cr, Theme::border(), 0);
lv_obj_set_style_text_font(_dropdown_lora_cr, &lv_font_montserrat_14, 0);
// TX Power row
lv_obj_t* power_row = lv_obj_create(_lora_params_container);
lv_obj_set_width(power_row, LV_PCT(100));
lv_obj_set_height(power_row, 28);
lv_obj_set_style_bg_opa(power_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(power_row, 0, 0);
lv_obj_set_style_pad_all(power_row, 0, 0);
lv_obj_clear_flag(power_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* power_label = lv_label_create(power_row);
lv_label_set_text(power_label, " TX Power:");
lv_obj_align(power_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(power_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(power_label, &lv_font_montserrat_14, 0);
_slider_lora_power = lv_slider_create(power_row);
lv_obj_set_size(_slider_lora_power, 100, 10);
lv_obj_align(_slider_lora_power, LV_ALIGN_LEFT_MID, 75, 0);
lv_slider_set_range(_slider_lora_power, 2, 22);
lv_obj_set_style_bg_color(_slider_lora_power, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_slider_lora_power, Theme::primary(), LV_PART_INDICATOR);
lv_obj_set_style_bg_color(_slider_lora_power, Theme::textPrimary(), LV_PART_KNOB);
lv_obj_add_event_cb(_slider_lora_power, on_lora_power_changed, LV_EVENT_VALUE_CHANGED, this);
_label_lora_power_value = lv_label_create(power_row);
lv_label_set_text(_label_lora_power_value, "17 dBm");
lv_obj_align(_label_lora_power_value, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_text_color(_label_lora_power_value, Theme::textPrimary(), 0);
lv_obj_set_style_text_font(_label_lora_power_value, &lv_font_montserrat_14, 0);
// Initially hide LoRa params if not enabled
lv_obj_add_flag(_lora_params_container, LV_OBJ_FLAG_HIDDEN);
}
void SettingsScreen::create_delivery_section(lv_obj_t* parent) {
create_section_header(parent, "== Delivery ==");
// Propagation Nodes button row
lv_obj_t* prop_nodes_row = lv_obj_create(parent);
lv_obj_set_width(prop_nodes_row, LV_PCT(100));
lv_obj_set_height(prop_nodes_row, 32);
lv_obj_set_style_bg_opa(prop_nodes_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(prop_nodes_row, 0, 0);
lv_obj_set_style_pad_all(prop_nodes_row, 0, 0);
lv_obj_clear_flag(prop_nodes_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* prop_label = lv_label_create(prop_nodes_row);
lv_label_set_text(prop_label, "Propagation Nodes:");
lv_obj_align(prop_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(prop_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(prop_label, &lv_font_montserrat_14, 0);
_btn_propagation_nodes = lv_btn_create(prop_nodes_row);
lv_obj_set_size(_btn_propagation_nodes, 70, 26);
lv_obj_align(_btn_propagation_nodes, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_btn_propagation_nodes, Theme::primary(), 0);
lv_obj_set_style_bg_color(_btn_propagation_nodes, Theme::primaryPressed(), LV_STATE_PRESSED);
lv_obj_add_event_cb(_btn_propagation_nodes, on_propagation_nodes_clicked, LV_EVENT_CLICKED, this);
lv_obj_t* btn_label = lv_label_create(_btn_propagation_nodes);
lv_label_set_text(btn_label, "View");
lv_obj_center(btn_label);
lv_obj_set_style_text_color(btn_label, Theme::textPrimary(), 0);
lv_obj_set_style_text_font(btn_label, &lv_font_montserrat_14, 0);
// Fallback to Propagation switch row
lv_obj_t* fallback_row = lv_obj_create(parent);
lv_obj_set_width(fallback_row, LV_PCT(100));
lv_obj_set_height(fallback_row, 28);
lv_obj_set_style_bg_opa(fallback_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(fallback_row, 0, 0);
lv_obj_set_style_pad_all(fallback_row, 0, 0);
lv_obj_clear_flag(fallback_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* fallback_label = lv_label_create(fallback_row);
lv_label_set_text(fallback_label, "Fallback to Prop:");
lv_obj_align(fallback_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(fallback_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(fallback_label, &lv_font_montserrat_14, 0);
_switch_prop_fallback = lv_switch_create(fallback_row);
lv_obj_set_size(_switch_prop_fallback, 40, 20);
lv_obj_align(_switch_prop_fallback, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_switch_prop_fallback, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_switch_prop_fallback, Theme::primary(), LV_PART_INDICATOR | LV_STATE_CHECKED);
// Propagation Only switch row
lv_obj_t* prop_only_row = lv_obj_create(parent);
lv_obj_set_width(prop_only_row, LV_PCT(100));
lv_obj_set_height(prop_only_row, 28);
lv_obj_set_style_bg_opa(prop_only_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(prop_only_row, 0, 0);
lv_obj_set_style_pad_all(prop_only_row, 0, 0);
lv_obj_clear_flag(prop_only_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* prop_only_label = lv_label_create(prop_only_row);
lv_label_set_text(prop_only_label, "Propagation Only:");
lv_obj_align(prop_only_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(prop_only_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(prop_only_label, &lv_font_montserrat_14, 0);
_switch_prop_only = lv_switch_create(prop_only_row);
lv_obj_set_size(_switch_prop_only, 40, 20);
lv_obj_align(_switch_prop_only, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_switch_prop_only, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_switch_prop_only, Theme::primary(), LV_PART_INDICATOR | LV_STATE_CHECKED);
}
void SettingsScreen::create_gps_section(lv_obj_t* parent) {
create_section_header(parent, "== GPS Status ==");
_label_gps_sats = create_label_row(parent, "Satellites: --");
_label_gps_coords = create_label_row(parent, "Location: No fix");
_label_gps_alt = create_label_row(parent, "Altitude: --");
_label_gps_hdop = create_label_row(parent, "HDOP: --");
}
void SettingsScreen::create_system_section(lv_obj_t* parent) {
create_section_header(parent, "== System Info ==");
_label_identity_hash = create_label_row(parent, "Identity: --");
_label_lxmf_address = create_label_row(parent, "LXMF: --");
_label_firmware = create_label_row(parent, "Firmware: v1.0.0");
_label_storage = create_label_row(parent, "Storage: --");
_label_ram = create_label_row(parent, "RAM: --");
}
void SettingsScreen::create_advanced_section(lv_obj_t* parent) {
create_section_header(parent, "== Advanced ==");
// Announce interval row
lv_obj_t* announce_row = lv_obj_create(parent);
lv_obj_set_width(announce_row, LV_PCT(100));
lv_obj_set_height(announce_row, 28);
lv_obj_set_style_bg_opa(announce_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(announce_row, 0, 0);
lv_obj_set_style_pad_all(announce_row, 0, 0);
lv_obj_clear_flag(announce_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* announce_label = lv_label_create(announce_row);
lv_label_set_text(announce_label, "Announce Interval:");
lv_obj_align(announce_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(announce_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(announce_label, &lv_font_montserrat_14, 0);
_ta_announce_interval = lv_textarea_create(announce_row);
lv_obj_set_size(_ta_announce_interval, 50, 24);
lv_obj_align(_ta_announce_interval, LV_ALIGN_RIGHT_MID, -30, 0);
lv_textarea_set_one_line(_ta_announce_interval, true);
lv_textarea_set_max_length(_ta_announce_interval, 5);
lv_textarea_set_accepted_chars(_ta_announce_interval, "0123456789");
lv_obj_set_style_bg_color(_ta_announce_interval, Theme::surfaceInput(), 0);
lv_obj_set_style_text_color(_ta_announce_interval, Theme::textPrimary(), 0);
lv_obj_set_style_border_color(_ta_announce_interval, Theme::border(), 0);
lv_obj_set_style_border_width(_ta_announce_interval, 1, 0);
lv_obj_set_style_radius(_ta_announce_interval, 4, 0);
lv_obj_set_style_pad_all(_ta_announce_interval, 4, 0);
lv_obj_set_style_text_font(_ta_announce_interval, &lv_font_montserrat_14, 0);
// Add to input group for keyboard navigation
lv_group_t* grp = LVGL::LVGLInit::get_default_group();
if (grp) {
lv_group_add_obj(grp, _ta_announce_interval);
}
// Enable paste on long-press
TextAreaHelper::enable_paste(_ta_announce_interval);
lv_obj_t* sec_label = lv_label_create(announce_row);
lv_label_set_text(sec_label, "sec");
lv_obj_align(sec_label, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_text_color(sec_label, Theme::textMuted(), 0);
lv_obj_set_style_text_font(sec_label, &lv_font_montserrat_14, 0);
// Sync interval row (for propagation node sync)
lv_obj_t* sync_row = lv_obj_create(parent);
lv_obj_set_width(sync_row, LV_PCT(100));
lv_obj_set_height(sync_row, 28);
lv_obj_set_style_bg_opa(sync_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(sync_row, 0, 0);
lv_obj_set_style_pad_all(sync_row, 0, 0);
lv_obj_clear_flag(sync_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* sync_label = lv_label_create(sync_row);
lv_label_set_text(sync_label, "Prop Sync Interval:");
lv_obj_align(sync_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(sync_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(sync_label, &lv_font_montserrat_14, 0);
_ta_sync_interval = lv_textarea_create(sync_row);
lv_obj_set_size(_ta_sync_interval, 50, 24);
lv_obj_align(_ta_sync_interval, LV_ALIGN_RIGHT_MID, -30, 0);
lv_textarea_set_one_line(_ta_sync_interval, true);
lv_textarea_set_max_length(_ta_sync_interval, 5);
lv_textarea_set_accepted_chars(_ta_sync_interval, "0123456789");
lv_obj_set_style_bg_color(_ta_sync_interval, Theme::surfaceInput(), 0);
lv_obj_set_style_text_color(_ta_sync_interval, Theme::textPrimary(), 0);
lv_obj_set_style_border_color(_ta_sync_interval, Theme::border(), 0);
lv_obj_set_style_border_width(_ta_sync_interval, 1, 0);
lv_obj_set_style_radius(_ta_sync_interval, 4, 0);
lv_obj_set_style_pad_all(_ta_sync_interval, 4, 0);
lv_obj_set_style_text_font(_ta_sync_interval, &lv_font_montserrat_14, 0);
if (grp) {
lv_group_add_obj(grp, _ta_sync_interval);
}
TextAreaHelper::enable_paste(_ta_sync_interval);
lv_obj_t* min_label = lv_label_create(sync_row);
lv_label_set_text(min_label, "min");
lv_obj_align(min_label, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_text_color(min_label, Theme::textMuted(), 0);
lv_obj_set_style_text_font(min_label, &lv_font_montserrat_14, 0);
// GPS sync row
lv_obj_t* gps_sync_row = lv_obj_create(parent);
lv_obj_set_width(gps_sync_row, LV_PCT(100));
lv_obj_set_height(gps_sync_row, 28);
lv_obj_set_style_bg_opa(gps_sync_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(gps_sync_row, 0, 0);
lv_obj_set_style_pad_all(gps_sync_row, 0, 0);
lv_obj_clear_flag(gps_sync_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* gps_sync_label = lv_label_create(gps_sync_row);
lv_label_set_text(gps_sync_label, "GPS Time Sync:");
lv_obj_align(gps_sync_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(gps_sync_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(gps_sync_label, &lv_font_montserrat_14, 0);
_switch_gps_sync = lv_switch_create(gps_sync_row);
lv_obj_set_size(_switch_gps_sync, 40, 20);
lv_obj_align(_switch_gps_sync, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_switch_gps_sync, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_switch_gps_sync, Theme::primary(), LV_PART_INDICATOR | LV_STATE_CHECKED);
}
void SettingsScreen::load_settings() {
Preferences prefs;
prefs.begin(NVS_NAMESPACE, true); // read-only
_settings.wifi_ssid = prefs.getString(KEY_WIFI_SSID, "");
_settings.wifi_password = prefs.getString(KEY_WIFI_PASS, "");
_settings.tcp_host = prefs.getString(KEY_TCP_HOST, "sideband.connect.reticulum.network");
_settings.tcp_port = prefs.getUShort(KEY_TCP_PORT, 4965);
_settings.display_name = prefs.getString(KEY_DISPLAY_NAME, "");
_settings.brightness = prefs.getUChar(KEY_BRIGHTNESS, 180);
_settings.keyboard_light = prefs.getBool(KEY_KB_LIGHT, false);
_settings.screen_timeout = prefs.getUShort(KEY_TIMEOUT, 60);
_settings.announce_interval = prefs.getUInt(KEY_ANNOUNCE_INT, 60);
_settings.sync_interval = prefs.getUInt(KEY_SYNC_INT, 3600); // Default 60 minutes
_settings.gps_time_sync = prefs.getBool(KEY_GPS_SYNC, true);
// Notification settings
_settings.notification_sound = prefs.getBool(KEY_NOTIF_SND, true);
_settings.notification_volume = prefs.getUChar(KEY_NOTIF_VOL, 10);
// Interface settings
_settings.tcp_enabled = prefs.getBool(KEY_TCP_ENABLED, true);
_settings.lora_enabled = prefs.getBool(KEY_LORA_ENABLED, false);
_settings.lora_frequency = prefs.getFloat(KEY_LORA_FREQ, 927.25f);
_settings.lora_bandwidth = prefs.getFloat(KEY_LORA_BW, 62.5f);
// Validate bandwidth - SX1262 valid values: 7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125, 250, 500
// If saved value is invalid (like 50 kHz), correct to nearest valid value
if (_settings.lora_bandwidth < 60.0f) {
_settings.lora_bandwidth = 62.5f; // 50 kHz -> 62.5 kHz
}
_settings.lora_sf = prefs.getUChar(KEY_LORA_SF, 7);
_settings.lora_cr = prefs.getUChar(KEY_LORA_CR, 5);
_settings.lora_power = prefs.getChar(KEY_LORA_POWER, 17);
_settings.auto_enabled = prefs.getBool(KEY_AUTO_ENABLED, false);
_settings.ble_enabled = prefs.getBool(KEY_BLE_ENABLED, false);
// Propagation settings
_settings.prop_auto_select = prefs.getBool(KEY_PROP_AUTO, true);
_settings.prop_selected_node = prefs.getString(KEY_PROP_NODE, "");
_settings.prop_fallback_enabled = prefs.getBool(KEY_PROP_FALLBACK, true);
_settings.prop_only = prefs.getBool(KEY_PROP_ONLY, false);
prefs.end();
DEBUG("Settings loaded from NVS");
}
void SettingsScreen::save_settings() {
update_settings_from_ui();
Preferences prefs;
prefs.begin(NVS_NAMESPACE, false); // read-write
prefs.putString(KEY_WIFI_SSID, _settings.wifi_ssid);
prefs.putString(KEY_WIFI_PASS, _settings.wifi_password);
prefs.putString(KEY_TCP_HOST, _settings.tcp_host);
prefs.putUShort(KEY_TCP_PORT, _settings.tcp_port);
prefs.putString(KEY_DISPLAY_NAME, _settings.display_name);
prefs.putUChar(KEY_BRIGHTNESS, _settings.brightness);
prefs.putBool(KEY_KB_LIGHT, _settings.keyboard_light);
prefs.putUShort(KEY_TIMEOUT, _settings.screen_timeout);
prefs.putUInt(KEY_ANNOUNCE_INT, _settings.announce_interval);
prefs.putUInt(KEY_SYNC_INT, _settings.sync_interval);
prefs.putBool(KEY_GPS_SYNC, _settings.gps_time_sync);
// Notification settings
prefs.putBool(KEY_NOTIF_SND, _settings.notification_sound);
prefs.putUChar(KEY_NOTIF_VOL, _settings.notification_volume);
// Interface settings
prefs.putBool(KEY_TCP_ENABLED, _settings.tcp_enabled);
prefs.putBool(KEY_LORA_ENABLED, _settings.lora_enabled);
prefs.putFloat(KEY_LORA_FREQ, _settings.lora_frequency);
prefs.putFloat(KEY_LORA_BW, _settings.lora_bandwidth);
prefs.putUChar(KEY_LORA_SF, _settings.lora_sf);
prefs.putUChar(KEY_LORA_CR, _settings.lora_cr);
prefs.putChar(KEY_LORA_POWER, _settings.lora_power);
prefs.putBool(KEY_AUTO_ENABLED, _settings.auto_enabled);
prefs.putBool(KEY_BLE_ENABLED, _settings.ble_enabled);
// Propagation settings
prefs.putBool(KEY_PROP_AUTO, _settings.prop_auto_select);
prefs.putString(KEY_PROP_NODE, _settings.prop_selected_node);
prefs.putBool(KEY_PROP_FALLBACK, _settings.prop_fallback_enabled);
prefs.putBool(KEY_PROP_ONLY, _settings.prop_only);
prefs.end();
INFO("Settings saved to NVS");
if (_save_callback) {
_save_callback(_settings);
}
}
void SettingsScreen::update_ui_from_settings() {
LVGL_LOCK();
if (_ta_wifi_ssid) {
lv_textarea_set_text(_ta_wifi_ssid, _settings.wifi_ssid.c_str());
}
if (_ta_wifi_password) {
lv_textarea_set_text(_ta_wifi_password, _settings.wifi_password.c_str());
}
if (_ta_tcp_host) {
lv_textarea_set_text(_ta_tcp_host, _settings.tcp_host.c_str());
}
if (_ta_tcp_port) {
lv_textarea_set_text(_ta_tcp_port, String(_settings.tcp_port).c_str());
}
if (_ta_display_name) {
lv_textarea_set_text(_ta_display_name, _settings.display_name.c_str());
}
if (_slider_brightness) {
lv_slider_set_value(_slider_brightness, _settings.brightness, LV_ANIM_OFF);
if (_label_brightness_value) {
lv_label_set_text(_label_brightness_value, String(_settings.brightness).c_str());
}
}
if (_switch_kb_light) {
if (_settings.keyboard_light) {
lv_obj_add_state(_switch_kb_light, LV_STATE_CHECKED);
} else {
lv_obj_clear_state(_switch_kb_light, LV_STATE_CHECKED);
}
}
// Notification settings
if (_switch_notification_sound) {
if (_settings.notification_sound) {
lv_obj_add_state(_switch_notification_sound, LV_STATE_CHECKED);
} else {
lv_obj_clear_state(_switch_notification_sound, LV_STATE_CHECKED);
}
}
if (_slider_notification_volume) {
lv_slider_set_value(_slider_notification_volume, _settings.notification_volume, LV_ANIM_OFF);
if (_label_notification_volume_value) {
lv_label_set_text(_label_notification_volume_value, String(_settings.notification_volume).c_str());
}
}
if (_dropdown_timeout) {
// Map timeout to dropdown index
int idx = 1; // default 1 min
if (_settings.screen_timeout == 30) idx = 0;
else if (_settings.screen_timeout == 60) idx = 1;
else if (_settings.screen_timeout == 300) idx = 2;
else if (_settings.screen_timeout == 0) idx = 3;
lv_dropdown_set_selected(_dropdown_timeout, idx);
}
if (_ta_announce_interval) {
lv_textarea_set_text(_ta_announce_interval, String(_settings.announce_interval).c_str());
}
if (_ta_sync_interval) {
// Display in minutes (stored in seconds)
lv_textarea_set_text(_ta_sync_interval, String(_settings.sync_interval / 60).c_str());
}
if (_switch_gps_sync) {
if (_settings.gps_time_sync) {
lv_obj_add_state(_switch_gps_sync, LV_STATE_CHECKED);
} else {
lv_obj_clear_state(_switch_gps_sync, LV_STATE_CHECKED);
}
}
// Interface settings
if (_switch_tcp_enabled) {
if (_settings.tcp_enabled) {
lv_obj_add_state(_switch_tcp_enabled, LV_STATE_CHECKED);
} else {
lv_obj_clear_state(_switch_tcp_enabled, LV_STATE_CHECKED);
}
}
if (_switch_lora_enabled) {
if (_settings.lora_enabled) {
lv_obj_add_state(_switch_lora_enabled, LV_STATE_CHECKED);
if (_lora_params_container) {
lv_obj_clear_flag(_lora_params_container, LV_OBJ_FLAG_HIDDEN);
}
} else {
lv_obj_clear_state(_switch_lora_enabled, LV_STATE_CHECKED);
if (_lora_params_container) {
lv_obj_add_flag(_lora_params_container, LV_OBJ_FLAG_HIDDEN);
}
}
}
if (_ta_lora_frequency) {
char freq_str[16];
snprintf(freq_str, sizeof(freq_str), "%.2f", _settings.lora_frequency);
lv_textarea_set_text(_ta_lora_frequency, freq_str);
}
if (_dropdown_lora_bandwidth) {
// Map bandwidth to index: 62.5=0, 125=1, 250=2, 500=3
int idx = 0;
if (_settings.lora_bandwidth < 100.0f) idx = 0; // 62.5 kHz
else if (_settings.lora_bandwidth < 200.0f) idx = 1; // 125 kHz
else if (_settings.lora_bandwidth < 400.0f) idx = 2; // 250 kHz
else idx = 3; // 500 kHz
lv_dropdown_set_selected(_dropdown_lora_bandwidth, idx);
}
if (_dropdown_lora_sf) {
// SF 7-12 maps to index 0-5
lv_dropdown_set_selected(_dropdown_lora_sf, _settings.lora_sf - 7);
}
if (_dropdown_lora_cr) {
// CR 5-8 maps to index 0-3
lv_dropdown_set_selected(_dropdown_lora_cr, _settings.lora_cr - 5);
}
if (_slider_lora_power) {
lv_slider_set_value(_slider_lora_power, _settings.lora_power, LV_ANIM_OFF);
if (_label_lora_power_value) {
char pwr_str[16];
snprintf(pwr_str, sizeof(pwr_str), "%d dBm", _settings.lora_power);
lv_label_set_text(_label_lora_power_value, pwr_str);
}
}
if (_switch_auto_enabled) {
if (_settings.auto_enabled) {
lv_obj_add_state(_switch_auto_enabled, LV_STATE_CHECKED);
} else {
lv_obj_clear_state(_switch_auto_enabled, LV_STATE_CHECKED);
}
}
if (_switch_ble_enabled) {
if (_settings.ble_enabled) {
lv_obj_add_state(_switch_ble_enabled, LV_STATE_CHECKED);
} else {
lv_obj_clear_state(_switch_ble_enabled, LV_STATE_CHECKED);
}
}
// Propagation settings
if (_switch_prop_fallback) {
if (_settings.prop_fallback_enabled) {
lv_obj_add_state(_switch_prop_fallback, LV_STATE_CHECKED);
} else {
lv_obj_clear_state(_switch_prop_fallback, LV_STATE_CHECKED);
}
}
if (_switch_prop_only) {
if (_settings.prop_only) {
lv_obj_add_state(_switch_prop_only, LV_STATE_CHECKED);
} else {
lv_obj_clear_state(_switch_prop_only, LV_STATE_CHECKED);
}
}
}
void SettingsScreen::update_settings_from_ui() {
LVGL_LOCK();
if (_ta_wifi_ssid) {
_settings.wifi_ssid = lv_textarea_get_text(_ta_wifi_ssid);
}
if (_ta_wifi_password) {
_settings.wifi_password = lv_textarea_get_text(_ta_wifi_password);
}
if (_ta_tcp_host) {
_settings.tcp_host = lv_textarea_get_text(_ta_tcp_host);
}
if (_ta_tcp_port) {
_settings.tcp_port = String(lv_textarea_get_text(_ta_tcp_port)).toInt();
}
if (_ta_display_name) {
_settings.display_name = lv_textarea_get_text(_ta_display_name);
}
if (_slider_brightness) {
_settings.brightness = lv_slider_get_value(_slider_brightness);
}
if (_switch_kb_light) {
_settings.keyboard_light = lv_obj_has_state(_switch_kb_light, LV_STATE_CHECKED);
}
// Notification settings
if (_switch_notification_sound) {
_settings.notification_sound = lv_obj_has_state(_switch_notification_sound, LV_STATE_CHECKED);
}
if (_slider_notification_volume) {
_settings.notification_volume = lv_slider_get_value(_slider_notification_volume);
}
if (_dropdown_timeout) {
int idx = lv_dropdown_get_selected(_dropdown_timeout);
switch (idx) {
case 0: _settings.screen_timeout = 30; break;
case 1: _settings.screen_timeout = 60; break;
case 2: _settings.screen_timeout = 300; break;
case 3: _settings.screen_timeout = 0; break;
}
}
if (_ta_announce_interval) {
_settings.announce_interval = String(lv_textarea_get_text(_ta_announce_interval)).toInt();
}
if (_ta_sync_interval) {
// UI shows minutes, store as seconds
_settings.sync_interval = String(lv_textarea_get_text(_ta_sync_interval)).toInt() * 60;
}
if (_switch_gps_sync) {
_settings.gps_time_sync = lv_obj_has_state(_switch_gps_sync, LV_STATE_CHECKED);
}
// Interface settings
if (_switch_tcp_enabled) {
_settings.tcp_enabled = lv_obj_has_state(_switch_tcp_enabled, LV_STATE_CHECKED);
}
if (_switch_lora_enabled) {
_settings.lora_enabled = lv_obj_has_state(_switch_lora_enabled, LV_STATE_CHECKED);
}
if (_ta_lora_frequency) {
_settings.lora_frequency = String(lv_textarea_get_text(_ta_lora_frequency)).toFloat();
}
if (_dropdown_lora_bandwidth) {
// Map index to bandwidth: 0=62.5, 1=125, 2=250, 3=500
static const float bw_values[] = {62.5f, 125.0f, 250.0f, 500.0f};
int idx = lv_dropdown_get_selected(_dropdown_lora_bandwidth);
if (idx >= 0 && idx < 4) {
_settings.lora_bandwidth = bw_values[idx];
}
}
if (_dropdown_lora_sf) {
// Index 0-5 maps to SF 7-12
_settings.lora_sf = lv_dropdown_get_selected(_dropdown_lora_sf) + 7;
}
if (_dropdown_lora_cr) {
// Index 0-3 maps to CR 5-8
_settings.lora_cr = lv_dropdown_get_selected(_dropdown_lora_cr) + 5;
}
if (_slider_lora_power) {
_settings.lora_power = lv_slider_get_value(_slider_lora_power);
}
if (_switch_auto_enabled) {
_settings.auto_enabled = lv_obj_has_state(_switch_auto_enabled, LV_STATE_CHECKED);
}
if (_switch_ble_enabled) {
_settings.ble_enabled = lv_obj_has_state(_switch_ble_enabled, LV_STATE_CHECKED);
}
// Propagation settings
if (_switch_prop_fallback) {
_settings.prop_fallback_enabled = lv_obj_has_state(_switch_prop_fallback, LV_STATE_CHECKED);
}
if (_switch_prop_only) {
_settings.prop_only = lv_obj_has_state(_switch_prop_only, LV_STATE_CHECKED);
}
}
void SettingsScreen::update_gps_display() {
LVGL_LOCK();
if (!_gps) {
lv_label_set_text(_label_gps_sats, "Satellites: N/A");
lv_label_set_text(_label_gps_coords, "Location: GPS not available");
lv_label_set_text(_label_gps_alt, "Altitude: --");
lv_label_set_text(_label_gps_hdop, "HDOP: --");
return;
}
// Satellites
String sats = "Satellites: " + String(_gps->satellites.value());
lv_label_set_text(_label_gps_sats, sats.c_str());
// Coordinates
if (_gps->location.isValid()) {
String coords = "Location: " +
String(_gps->location.lat(), 4) + ", " +
String(_gps->location.lng(), 4);
lv_label_set_text(_label_gps_coords, coords.c_str());
lv_obj_set_style_text_color(_label_gps_coords, Theme::success(), 0);
} else {
lv_label_set_text(_label_gps_coords, "Location: No fix");
lv_obj_set_style_text_color(_label_gps_coords, Theme::error(), 0);
}
// Altitude
if (_gps->altitude.isValid()) {
String alt = "Altitude: " + String(_gps->altitude.meters(), 1) + "m";
lv_label_set_text(_label_gps_alt, alt.c_str());
} else {
lv_label_set_text(_label_gps_alt, "Altitude: --");
}
// HDOP (fix quality)
if (_gps->hdop.isValid()) {
double hdop = _gps->hdop.hdop() / 100.0; // TinyGPSPlus returns hdop * 100
String quality;
if (hdop < 1.0) quality = "Ideal";
else if (hdop < 2.0) quality = "Excellent";
else if (hdop < 5.0) quality = "Good";
else if (hdop < 10.0) quality = "Moderate";
else quality = "Poor";
String hdop_str = "HDOP: " + String(hdop, 1) + " (" + quality + ")";
lv_label_set_text(_label_gps_hdop, hdop_str.c_str());
} else {
lv_label_set_text(_label_gps_hdop, "HDOP: --");
}
}
void SettingsScreen::update_system_info() {
LVGL_LOCK();
// Identity hash
if (_identity_hash.size() > 0) {
String hash = "Identity: " + String(_identity_hash.toHex().substr(0, 16).c_str()) + "...";
lv_label_set_text(_label_identity_hash, hash.c_str());
}
// LXMF address
if (_lxmf_address.size() > 0) {
String addr = "LXMF: " + String(_lxmf_address.toHex().substr(0, 16).c_str()) + "...";
lv_label_set_text(_label_lxmf_address, addr.c_str());
}
// Storage
size_t total = SPIFFS.totalBytes();
size_t used = SPIFFS.usedBytes();
size_t free = total - used;
String storage = "Storage: " + String(free / 1024) + " KB free";
lv_label_set_text(_label_storage, storage.c_str());
// RAM
size_t free_heap = ESP.getFreeHeap();
String ram = "RAM: " + String(free_heap / 1024) + " KB free";
lv_label_set_text(_label_ram, ram.c_str());
}
void SettingsScreen::set_identity_hash(const Bytes& hash) {
_identity_hash = hash;
}
void SettingsScreen::set_lxmf_address(const Bytes& hash) {
_lxmf_address = hash;
}
void SettingsScreen::set_gps(TinyGPSPlus* gps) {
_gps = gps;
}
void SettingsScreen::refresh() {
update_gps_display();
update_system_info();
}
void SettingsScreen::set_back_callback(BackCallback callback) {
_back_callback = callback;
}
void SettingsScreen::set_save_callback(SaveCallback callback) {
_save_callback = callback;
}
void SettingsScreen::set_wifi_reconnect_callback(WifiReconnectCallback callback) {
_wifi_reconnect_callback = callback;
}
void SettingsScreen::set_brightness_change_callback(BrightnessChangeCallback callback) {
_brightness_change_callback = callback;
}
void SettingsScreen::set_propagation_nodes_callback(PropagationNodesCallback callback) {
_propagation_nodes_callback = callback;
}
void SettingsScreen::show() {
LVGL_LOCK();
refresh();
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_HIDDEN);
lv_obj_move_foreground(_screen);
// Add back/save buttons to focus group for trackball navigation
lv_group_t* group = LVGL::LVGLInit::get_default_group();
if (group) {
if (_btn_back) lv_group_add_obj(group, _btn_back);
if (_btn_save) lv_group_add_obj(group, _btn_save);
// Focus on back button
if (_btn_back) {
lv_group_focus_obj(_btn_back);
}
}
}
void SettingsScreen::hide() {
LVGL_LOCK();
// Remove from focus group when hiding
lv_group_t* group = LVGL::LVGLInit::get_default_group();
if (group) {
if (_btn_back) lv_group_remove_obj(_btn_back);
if (_btn_save) lv_group_remove_obj(_btn_save);
}
lv_obj_add_flag(_screen, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t* SettingsScreen::get_object() {
return _screen;
}
void SettingsScreen::on_back_clicked(lv_event_t* event) {
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
if (screen->_back_callback) {
screen->_back_callback();
}
}
void SettingsScreen::on_save_clicked(lv_event_t* event) {
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
screen->save_settings();
}
void SettingsScreen::on_reconnect_clicked(lv_event_t* event) {
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
const char* ssid = lv_textarea_get_text(screen->_ta_wifi_ssid);
const char* pass = lv_textarea_get_text(screen->_ta_wifi_password);
if (screen->_wifi_reconnect_callback) {
screen->_wifi_reconnect_callback(String(ssid), String(pass));
}
}
void SettingsScreen::on_brightness_changed(lv_event_t* event) {
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
uint8_t brightness = lv_slider_get_value(screen->_slider_brightness);
// Update label
lv_label_set_text(screen->_label_brightness_value, String(brightness).c_str());
// Apply immediately
if (screen->_brightness_change_callback) {
screen->_brightness_change_callback(brightness);
}
}
void SettingsScreen::on_lora_enabled_changed(lv_event_t* event) {
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
bool enabled = lv_obj_has_state(screen->_switch_lora_enabled, LV_STATE_CHECKED);
// Show/hide LoRa parameters
if (screen->_lora_params_container) {
if (enabled) {
lv_obj_clear_flag(screen->_lora_params_container, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(screen->_lora_params_container, LV_OBJ_FLAG_HIDDEN);
}
}
}
void SettingsScreen::on_lora_power_changed(lv_event_t* event) {
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
int8_t power = lv_slider_get_value(screen->_slider_lora_power);
// Update label
char pwr_str[16];
snprintf(pwr_str, sizeof(pwr_str), "%d dBm", power);
lv_label_set_text(screen->_label_lora_power_value, pwr_str);
}
void SettingsScreen::on_notification_volume_changed(lv_event_t* event) {
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
uint8_t volume = lv_slider_get_value(screen->_slider_notification_volume);
// Update label
lv_label_set_text(screen->_label_notification_volume_value, String(volume).c_str());
}
void SettingsScreen::on_propagation_nodes_clicked(lv_event_t* event) {
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
if (screen->_propagation_nodes_callback) {
screen->_propagation_nodes_callback();
}
}
} // namespace LXMF
} // namespace UI
#endif // ARDUINO