mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-09-25 14:13:36 +00:00
Boot-loop regression from the hub-and-spoke rewrite: lv_label_set_text() on a plain lv_obj object wrote label internals into a non-label and freed a bogus pointer (heap_caps_free assert 'outside heap areas') during UIManager::init -> SettingsScreen::create_advanced_view, crashing every boot at 'Initializing UIManager'. Verified by decoding the on-device backtrace (ELF 62cf1d0e...) against the exact flashed firmware.
1800 lines
80 KiB
C++
1800 lines
80 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 <microReticulum/Log.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";
|
|
static const char* KEY_TRANSPORT_ENABLED = "transport";
|
|
// 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), _title(nullptr),
|
|
_btn_back(nullptr), _btn_save(nullptr),
|
|
_hub(nullptr),
|
|
_ta_wifi_ssid(nullptr), _ta_wifi_password(nullptr),
|
|
_ta_tcp_host(nullptr), _ta_tcp_port(nullptr), _btn_reconnect(nullptr),
|
|
_ta_display_name(nullptr), _btn_view_identity(nullptr),
|
|
_slider_brightness(nullptr), _label_brightness_value(nullptr),
|
|
_switch_kb_light(nullptr), _dropdown_timeout(nullptr),
|
|
_switch_notification_sound(nullptr), _slider_notification_volume(nullptr),
|
|
_label_notification_volume_value(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_tcp_enabled(nullptr), _switch_auto_enabled(nullptr),
|
|
_switch_ble_enabled(nullptr), _switch_lora_interface(nullptr),
|
|
_ta_announce_interval(nullptr), _ta_sync_interval(nullptr), _switch_gps_sync(nullptr),
|
|
_switch_transport_enabled(nullptr), _transport_warning_modal(nullptr),
|
|
_transport_modal_group(nullptr), _transport_enable_confirmed(false),
|
|
_btn_propagation_nodes(nullptr), _switch_prop_fallback(nullptr), _switch_prop_only(nullptr),
|
|
_save_state(0U), _apply_retry_at_ms(0U), _view(VIEW_HUB) {
|
|
for (int i = 0; i < VIEW_COUNT; ++i) _pages[i] = nullptr;
|
|
for (int i = 0; i < VIEW_COUNT - 1; ++i) _cards[i] = 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 (leaves a sub-view first; leaves Settings on the hub)
|
|
_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
|
|
_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 (visible only on the form sub-views)
|
|
_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);
|
|
}
|
|
|
|
lv_obj_t* SettingsScreen::create_page(View view) {
|
|
lv_obj_t* page = lv_obj_create(_screen);
|
|
lv_obj_set_size(page, LV_PCT(100), 204); // 240 - 36 header
|
|
lv_obj_align(page, LV_ALIGN_TOP_MID, 0, 36);
|
|
lv_obj_set_style_pad_all(page, 4, 0);
|
|
lv_obj_set_style_pad_gap(page, 2, 0);
|
|
lv_obj_set_style_bg_color(page, Theme::surface(), 0);
|
|
lv_obj_set_style_border_width(page, 0, 0);
|
|
lv_obj_set_style_radius(page, 0, 0);
|
|
|
|
// Vertical scrolling
|
|
lv_obj_set_flex_flow(page, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_scroll_dir(page, LV_DIR_VER);
|
|
lv_obj_set_scrollbar_mode(page, LV_SCROLLBAR_MODE_AUTO);
|
|
_pages[view] = page;
|
|
return page;
|
|
}
|
|
|
|
lv_obj_t* SettingsScreen::create_card(lv_obj_t* parent, const char* symbol,
|
|
const char* title, const char* detail,
|
|
View target) {
|
|
// Same card widget as the Network screen: 58px, icon + title + detail.
|
|
lv_obj_t* card = lv_btn_create(parent);
|
|
lv_obj_set_size(card, LV_PCT(100), 58);
|
|
lv_obj_set_style_bg_color(card, Theme::surfaceContainer(), 0);
|
|
lv_obj_set_style_bg_color(card, Theme::primaryPressed(), LV_STATE_FOCUSED);
|
|
lv_obj_set_style_border_width(card, 0, 0);
|
|
lv_obj_set_style_outline_color(card, Theme::primaryLight(), LV_STATE_FOCUSED);
|
|
lv_obj_set_style_outline_width(card, 1, LV_STATE_FOCUSED);
|
|
lv_obj_set_style_outline_pad(card, 1, LV_STATE_FOCUSED);
|
|
lv_obj_set_style_radius(card, 8, 0);
|
|
lv_obj_set_style_pad_all(card, 4, 0);
|
|
lv_obj_add_event_cb(card, on_card_clicked, LV_EVENT_CLICKED, this);
|
|
|
|
lv_obj_t* icon = lv_label_create(card);
|
|
lv_label_set_text(icon, symbol);
|
|
lv_obj_set_style_text_font(icon, &lv_font_montserrat_16, 0);
|
|
lv_obj_set_style_text_color(icon, Theme::primaryLight(), 0);
|
|
lv_obj_align(icon, LV_ALIGN_LEFT_MID, 4, 0);
|
|
|
|
lv_obj_t* title_label = lv_label_create(card);
|
|
lv_label_set_text(title_label, title);
|
|
lv_obj_set_style_text_font(title_label, &lv_font_montserrat_12, 0);
|
|
lv_obj_set_style_text_color(title_label, Theme::textPrimary(), 0);
|
|
lv_obj_align(title_label, LV_ALIGN_LEFT_MID, 32, -7);
|
|
|
|
lv_obj_t* detail_label = lv_label_create(card);
|
|
lv_label_set_text(detail_label, detail);
|
|
lv_obj_set_style_text_font(detail_label, &lv_font_montserrat_8, 0);
|
|
lv_obj_set_style_text_color(detail_label, Theme::textTertiary(), 0);
|
|
lv_obj_align(detail_label, LV_ALIGN_LEFT_MID, 32, 10);
|
|
|
|
lv_obj_set_user_data(card, (void*)(intptr_t)target);
|
|
return card;
|
|
}
|
|
|
|
lv_obj_t* SettingsScreen::create_label_row(lv_obj_t* parent, const char* label) {
|
|
// Section label above a control: Montserrat 14, tertiary color.
|
|
lv_obj_t* label_obj = lv_label_create(parent);
|
|
lv_label_set_text(label_obj, label);
|
|
lv_obj_set_width(label_obj, LV_PCT(100));
|
|
lv_obj_set_style_text_color(label_obj, Theme::textTertiary(), 0);
|
|
lv_obj_set_style_text_font(label_obj, &lv_font_montserrat_14, 0);
|
|
return label_obj;
|
|
}
|
|
|
|
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);
|
|
TextAreaHelper::enable_paste(ta);
|
|
return ta;
|
|
}
|
|
|
|
void SettingsScreen::create_content() {
|
|
// Hub: the navigation card list. Order mirrors Columba; the Transport
|
|
// card must remain the final entry.
|
|
_hub = create_page(VIEW_HUB);
|
|
lv_obj_set_style_pad_gap(_hub, 8, 0);
|
|
_cards[0] = create_card(_hub, LV_SYMBOL_BATTERY_FULL, "Status", "System, GPS & storage", VIEW_HUB);
|
|
_cards[1] = create_card(_hub, LV_SYMBOL_WIFI, "Network", "WiFi, TCP & interfaces", VIEW_NETWORK);
|
|
_cards[2] = create_card(_hub, LV_SYMBOL_EDIT, "Identity", "Display name & QR code", VIEW_IDENTITY);
|
|
_cards[3] = create_card(_hub, LV_SYMBOL_UPLOAD, "Radio", "LoRa configuration", VIEW_RADIO);
|
|
_cards[4] = create_card(_hub, LV_SYMBOL_LIST, "Delivery", "Propagation & delivery", VIEW_DELIVERY);
|
|
_cards[5] = create_card(_hub, LV_SYMBOL_EYE_OPEN, "Appearance", "Display & keyboard light", VIEW_APPEARANCE);
|
|
_cards[6] = create_card(_hub, LV_SYMBOL_SETTINGS, "Advanced", "Intervals & time sync", VIEW_ADVANCED);
|
|
_cards[7] = create_card(_hub, LV_SYMBOL_WARNING, "Transport", "Transport mode", VIEW_TRANSPORT);
|
|
|
|
create_identity_view(create_page(VIEW_IDENTITY));
|
|
create_network_view(create_page(VIEW_NETWORK));
|
|
create_radio_view(create_page(VIEW_RADIO));
|
|
create_delivery_view(create_page(VIEW_DELIVERY));
|
|
create_appearance_view(create_page(VIEW_APPEARANCE));
|
|
create_advanced_view(create_page(VIEW_ADVANCED));
|
|
// This dangerous opt-in must remain the final settings destination.
|
|
create_transport_mode_view(create_page(VIEW_TRANSPORT));
|
|
|
|
// Start on the hub.
|
|
switch_view(VIEW_HUB);
|
|
}
|
|
|
|
void SettingsScreen::create_identity_view(lv_obj_t* parent) {
|
|
create_label_row(parent, "Display Name:");
|
|
_ta_display_name = create_text_input(parent, "Your name", false, 32);
|
|
|
|
// View Identity: opens the QR screen (lxma:// share code)
|
|
_btn_view_identity = lv_btn_create(parent);
|
|
lv_obj_set_size(_btn_view_identity, LV_PCT(100), 30);
|
|
lv_obj_set_style_bg_color(_btn_view_identity, Theme::surfaceInput(), 0);
|
|
lv_obj_set_style_bg_color(_btn_view_identity, Theme::surfaceElevated(), LV_STATE_PRESSED);
|
|
lv_obj_add_event_cb(_btn_view_identity, on_view_identity_clicked, LV_EVENT_CLICKED, this);
|
|
lv_obj_t* identity_label = lv_label_create(_btn_view_identity);
|
|
lv_label_set_text(identity_label, LV_SYMBOL_IMAGE " View Identity " LV_SYMBOL_RIGHT);
|
|
lv_obj_align(identity_label, LV_ALIGN_LEFT_MID, 0, 0);
|
|
lv_obj_set_style_text_color(identity_label, Theme::textPrimary(), 0);
|
|
{
|
|
lv_group_t* g = LVGL::LVGLInit::get_default_group();
|
|
if (g) lv_group_add_obj(g, _btn_view_identity);
|
|
}
|
|
}
|
|
|
|
void SettingsScreen::create_network_view(lv_obj_t* parent) {
|
|
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 Host:");
|
|
_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);
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
lv_group_add_obj(group, _ta_tcp_port);
|
|
}
|
|
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);
|
|
|
|
// Interface row: how this device reaches peers. LoRa lives here next to
|
|
// TCP/Auto/BLE (it is one interface); the Radio page tunes the RF.
|
|
create_label_row(parent, "Interfaces:");
|
|
|
|
lv_obj_t* iface_row = lv_obj_create(parent);
|
|
lv_obj_set_width(iface_row, LV_PCT(100));
|
|
lv_obj_set_height(iface_row, 28);
|
|
lv_obj_set_style_bg_opa(iface_row, LV_OPA_TRANSP, 0);
|
|
lv_obj_set_style_border_width(iface_row, 0, 0);
|
|
lv_obj_set_style_pad_all(iface_row, 0, 0);
|
|
lv_obj_clear_flag(iface_row, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
lv_obj_t* tcp_label = lv_label_create(iface_row);
|
|
lv_label_set_text(tcp_label, "TCP");
|
|
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_12, 0);
|
|
_switch_tcp_enabled = lv_switch_create(iface_row);
|
|
lv_obj_set_size(_switch_tcp_enabled, 40, 20);
|
|
lv_obj_align(_switch_tcp_enabled, LV_ALIGN_LEFT_MID, 30, 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);
|
|
lv_obj_add_event_cb(_switch_tcp_enabled, on_interface_switch_changed, LV_EVENT_VALUE_CHANGED, this);
|
|
|
|
lv_obj_t* auto_label = lv_label_create(iface_row);
|
|
lv_label_set_text(auto_label, "AUTO");
|
|
lv_obj_align(auto_label, LV_ALIGN_LEFT_MID, 78, 0);
|
|
lv_obj_set_style_text_color(auto_label, Theme::textTertiary(), 0);
|
|
lv_obj_set_style_text_font(auto_label, &lv_font_montserrat_12, 0);
|
|
_switch_auto_enabled = lv_switch_create(iface_row);
|
|
lv_obj_set_size(_switch_auto_enabled, 40, 20);
|
|
lv_obj_align(_switch_auto_enabled, LV_ALIGN_LEFT_MID, 112, 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);
|
|
lv_obj_add_event_cb(_switch_auto_enabled, on_interface_switch_changed, LV_EVENT_VALUE_CHANGED, this);
|
|
|
|
lv_obj_t* lora_label = lv_label_create(iface_row);
|
|
lv_label_set_text(lora_label, "LoRa");
|
|
lv_obj_align(lora_label, LV_ALIGN_LEFT_MID, 160, 0);
|
|
lv_obj_set_style_text_color(lora_label, Theme::textTertiary(), 0);
|
|
lv_obj_set_style_text_font(lora_label, &lv_font_montserrat_12, 0);
|
|
_switch_lora_interface = lv_switch_create(iface_row);
|
|
lv_obj_set_size(_switch_lora_interface, 40, 20);
|
|
lv_obj_align(_switch_lora_interface, LV_ALIGN_LEFT_MID, 196, 0);
|
|
lv_obj_set_style_bg_color(_switch_lora_interface, Theme::border(), LV_PART_MAIN);
|
|
lv_obj_set_style_bg_color(_switch_lora_interface, Theme::primary(), LV_PART_INDICATOR | LV_STATE_CHECKED);
|
|
lv_obj_add_event_cb(_switch_lora_interface, on_interface_switch_changed, LV_EVENT_VALUE_CHANGED, this);
|
|
|
|
lv_obj_t* ble_label = lv_label_create(iface_row);
|
|
lv_label_set_text(ble_label, "BLE");
|
|
lv_obj_align(ble_label, LV_ALIGN_LEFT_MID, 244, 0);
|
|
lv_obj_set_style_text_color(ble_label, Theme::textTertiary(), 0);
|
|
lv_obj_set_style_text_font(ble_label, &lv_font_montserrat_12, 0);
|
|
_switch_ble_enabled = lv_switch_create(iface_row);
|
|
lv_obj_set_size(_switch_ble_enabled, 40, 20);
|
|
lv_obj_align(_switch_ble_enabled, LV_ALIGN_LEFT_MID, 272, 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);
|
|
lv_obj_add_event_cb(_switch_ble_enabled, on_interface_switch_changed, LV_EVENT_VALUE_CHANGED, this);
|
|
}
|
|
|
|
void SettingsScreen::create_radio_view(lv_obj_t* parent) {
|
|
// LoRa RF tuning. The on/off switch is the Radio-page handle for the same
|
|
// lora_enabled flag the Network interface row uses; both stay in sync
|
|
// through the settings snapshot.
|
|
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 Radio:");
|
|
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);
|
|
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, "5\n6\n7\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_view(lv_obj_t* parent) {
|
|
// 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);
|
|
lv_obj_add_event_cb(_switch_prop_fallback, on_prop_switch_changed, LV_EVENT_VALUE_CHANGED, this);
|
|
|
|
// 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);
|
|
lv_obj_add_event_cb(_switch_prop_only, on_prop_switch_changed, LV_EVENT_VALUE_CHANGED, this);
|
|
}
|
|
|
|
void SettingsScreen::create_appearance_view(lv_obj_t* parent) {
|
|
// 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 (immediate)
|
|
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);
|
|
lv_obj_add_event_cb(_switch_kb_light, on_kb_light_changed, LV_EVENT_VALUE_CHANGED, this);
|
|
|
|
// Timeout row (immediate)
|
|
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);
|
|
lv_obj_add_event_cb(_dropdown_timeout, on_timeout_changed, LV_EVENT_VALUE_CHANGED, this);
|
|
|
|
// Message sound row (immediate)
|
|
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);
|
|
lv_obj_add_event_cb(_switch_notification_sound, on_notif_sound_changed, LV_EVENT_VALUE_CHANGED, this);
|
|
|
|
// Volume row (immediate)
|
|
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_advanced_view(lv_obj_t* parent) {
|
|
// 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 (min):");
|
|
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);
|
|
}
|
|
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 (hrs):");
|
|
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 (immediate)
|
|
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);
|
|
lv_obj_add_event_cb(_switch_gps_sync, on_gps_sync_changed, LV_EVENT_VALUE_CHANGED, this);
|
|
}
|
|
|
|
void SettingsScreen::create_transport_mode_view(lv_obj_t* parent) {
|
|
lv_obj_t* warning = lv_label_create(parent);
|
|
lv_obj_set_width(warning, LV_PCT(100));
|
|
lv_label_set_long_mode(warning, LV_LABEL_LONG_WRAP);
|
|
lv_label_set_text(
|
|
warning,
|
|
"NOT RECOMMENDED. Transport mode routes other nodes' Reticulum traffic "
|
|
"across every enabled interface. It can saturate LoRa airtime, drain the battery, "
|
|
"and turn this handheld into network infrastructure. Enable only if you understand "
|
|
"Reticulum routing and intend this device to be a transport node. Takes effect after reboot.");
|
|
lv_obj_set_style_text_color(warning, Theme::error(), 0);
|
|
lv_obj_set_style_text_font(warning, &lv_font_montserrat_12, 0);
|
|
lv_obj_set_style_pad_bottom(warning, 4, 0);
|
|
|
|
// Keep this row as the final object in Settings so the opt-in cannot be
|
|
// mistaken for an ordinary interface switch higher in the screen.
|
|
lv_obj_t* transport_row = lv_obj_create(parent);
|
|
lv_obj_set_width(transport_row, LV_PCT(100));
|
|
lv_obj_set_height(transport_row, 32);
|
|
lv_obj_set_style_bg_opa(transport_row, LV_OPA_TRANSP, 0);
|
|
lv_obj_set_style_border_width(transport_row, 0, 0);
|
|
lv_obj_set_style_pad_all(transport_row, 0, 0);
|
|
lv_obj_clear_flag(transport_row, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
lv_obj_t* label = lv_label_create(transport_row);
|
|
lv_label_set_text(label, "Enable Transport Node:");
|
|
lv_obj_align(label, LV_ALIGN_LEFT_MID, 0, 0);
|
|
lv_obj_set_style_text_color(label, Theme::error(), 0);
|
|
lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
|
|
|
|
_switch_transport_enabled = lv_switch_create(transport_row);
|
|
lv_obj_set_size(_switch_transport_enabled, 40, 20);
|
|
lv_obj_align(_switch_transport_enabled, LV_ALIGN_RIGHT_MID, 0, 0);
|
|
lv_obj_set_style_bg_color(_switch_transport_enabled, Theme::border(), LV_PART_MAIN);
|
|
lv_obj_set_style_bg_color(_switch_transport_enabled, Theme::error(), LV_PART_INDICATOR | LV_STATE_CHECKED);
|
|
lv_obj_add_event_cb(_switch_transport_enabled, on_transport_enabled_changed, LV_EVENT_VALUE_CHANGED, this);
|
|
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (group) {
|
|
lv_group_add_obj(group, _switch_transport_enabled);
|
|
}
|
|
}
|
|
|
|
void SettingsScreen::show_transport_warning() {
|
|
if (_transport_warning_modal) return;
|
|
|
|
_transport_warning_modal = lv_obj_create(_screen);
|
|
lv_obj_set_size(_transport_warning_modal, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_center(_transport_warning_modal);
|
|
lv_obj_set_style_bg_color(_transport_warning_modal, lv_color_black(), 0);
|
|
lv_obj_set_style_bg_opa(_transport_warning_modal, LV_OPA_80, 0);
|
|
lv_obj_set_style_border_width(_transport_warning_modal, 0, 0);
|
|
lv_obj_set_style_radius(_transport_warning_modal, 0, 0);
|
|
lv_obj_set_style_pad_all(_transport_warning_modal, 8, 0);
|
|
lv_obj_clear_flag(_transport_warning_modal, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_add_flag(_transport_warning_modal, LV_OBJ_FLAG_CLICKABLE);
|
|
|
|
lv_obj_t* panel = lv_obj_create(_transport_warning_modal);
|
|
lv_obj_set_size(panel, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_center(panel);
|
|
lv_obj_set_style_bg_color(panel, Theme::surface(), 0);
|
|
lv_obj_set_style_border_color(panel, Theme::error(), 0);
|
|
lv_obj_set_style_border_width(panel, 2, 0);
|
|
lv_obj_set_style_radius(panel, 6, 0);
|
|
lv_obj_set_style_pad_all(panel, 8, 0);
|
|
lv_obj_clear_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
lv_obj_t* title = lv_label_create(panel);
|
|
lv_label_set_text(title, "DANGER: Transport Mode");
|
|
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 0);
|
|
lv_obj_set_style_text_color(title, Theme::error(), 0);
|
|
lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0);
|
|
|
|
lv_obj_t* text = lv_label_create(panel);
|
|
lv_obj_set_width(text, LV_PCT(100));
|
|
lv_label_set_long_mode(text, LV_LABEL_LONG_WRAP);
|
|
lv_label_set_text(
|
|
text,
|
|
"This is NOT RECOMMENDED. The device will relay other nodes' traffic and may "
|
|
"saturate LoRa airtime, drain the battery, and disrupt nearby Reticulum users.\n\n"
|
|
"Enable only if you understand the consequences and deliberately want this T-Deck "
|
|
"to operate as network infrastructure. Takes effect after reboot.");
|
|
lv_obj_align(text, LV_ALIGN_TOP_LEFT, 0, 28);
|
|
lv_obj_set_style_text_color(text, Theme::textPrimary(), 0);
|
|
lv_obj_set_style_text_font(text, &lv_font_montserrat_12, 0);
|
|
|
|
lv_obj_t* cancel = lv_btn_create(panel);
|
|
lv_obj_set_size(cancel, 100, 32);
|
|
lv_obj_align(cancel, LV_ALIGN_BOTTOM_LEFT, 0, 0);
|
|
lv_obj_set_style_bg_color(cancel, Theme::btnSecondary(), 0);
|
|
lv_obj_add_event_cb(cancel, on_transport_cancel_enable, LV_EVENT_CLICKED, this);
|
|
lv_obj_t* cancel_label = lv_label_create(cancel);
|
|
lv_label_set_text(cancel_label, "Cancel");
|
|
lv_obj_center(cancel_label);
|
|
|
|
lv_obj_t* confirm = lv_btn_create(panel);
|
|
lv_obj_set_size(confirm, 145, 32);
|
|
lv_obj_align(confirm, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
|
|
lv_obj_set_style_bg_color(confirm, Theme::error(), 0);
|
|
lv_obj_add_event_cb(confirm, on_transport_confirm_enable, LV_EVENT_CLICKED, this);
|
|
lv_obj_t* confirm_label = lv_label_create(confirm);
|
|
lv_label_set_text(confirm_label, "ENABLE ANYWAY");
|
|
lv_obj_center(confirm_label);
|
|
|
|
_transport_modal_group = lv_group_create();
|
|
if (_transport_modal_group) {
|
|
lv_group_add_obj(_transport_modal_group, cancel);
|
|
lv_group_add_obj(_transport_modal_group, confirm);
|
|
lv_group_focus_obj(cancel);
|
|
|
|
// Isolate keyboard and trackball focus inside the warning. Otherwise
|
|
// users can navigate into and activate Settings controls behind the
|
|
// still-visible modal.
|
|
lv_indev_t* keyboard = LVGL::LVGLInit::get_keyboard();
|
|
lv_indev_t* trackball = LVGL::LVGLInit::get_trackball();
|
|
if (keyboard) lv_indev_set_group(keyboard, _transport_modal_group);
|
|
if (trackball) lv_indev_set_group(trackball, _transport_modal_group);
|
|
}
|
|
}
|
|
|
|
void SettingsScreen::close_transport_warning() {
|
|
if (!_transport_warning_modal) return;
|
|
|
|
lv_group_t* default_group = LVGL::LVGLInit::get_default_group();
|
|
lv_indev_t* keyboard = LVGL::LVGLInit::get_keyboard();
|
|
lv_indev_t* trackball = LVGL::LVGLInit::get_trackball();
|
|
if (keyboard) lv_indev_set_group(keyboard, default_group);
|
|
if (trackball) lv_indev_set_group(trackball, default_group);
|
|
|
|
if (_transport_modal_group) {
|
|
lv_group_del(_transport_modal_group);
|
|
_transport_modal_group = nullptr;
|
|
}
|
|
|
|
lv_obj_t* modal = _transport_warning_modal;
|
|
_transport_warning_modal = nullptr;
|
|
lv_obj_del_async(modal);
|
|
|
|
if (default_group && _switch_transport_enabled) {
|
|
lv_group_focus_obj(_switch_transport_enabled);
|
|
}
|
|
}
|
|
|
|
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, 14400); // Default 14400s = 4 hours
|
|
_settings.sync_interval = prefs.getUInt(KEY_SYNC_INT, 14400); // Default 14400s = 4 hours
|
|
_settings.gps_time_sync = prefs.getBool(KEY_GPS_SYNC, true);
|
|
_settings.transport_enabled = prefs.getBool(KEY_TRANSPORT_ENABLED, false);
|
|
|
|
// 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();
|
|
std::uint8_t expected = SAVE_IDLE;
|
|
if (!_save_state.compare_exchange_strong(
|
|
expected, SAVE_PROCESSING, std::memory_order_acq_rel)) {
|
|
WARNING("Settings save already pending");
|
|
return;
|
|
}
|
|
_pending_save_settings = _settings;
|
|
_save_state.store(SAVE_PENDING, std::memory_order_release);
|
|
}
|
|
|
|
void SettingsScreen::service_pending_save() {
|
|
std::uint8_t expected = _save_state.load(std::memory_order_acquire);
|
|
if (expected != SAVE_PENDING && expected != SAVE_APPLY_RETRY) return;
|
|
if (expected == SAVE_APPLY_RETRY &&
|
|
static_cast<std::int32_t>(millis() - _apply_retry_at_ms) < 0) return;
|
|
const bool needs_persistence = expected == SAVE_PENDING;
|
|
if (!_save_state.compare_exchange_strong(
|
|
expected, SAVE_PROCESSING, std::memory_order_acq_rel)) return;
|
|
const AppSettings settings = _pending_save_settings;
|
|
|
|
if (needs_persistence) {
|
|
Preferences prefs;
|
|
prefs.begin(NVS_NAMESPACE, false);
|
|
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);
|
|
prefs.putBool(KEY_TRANSPORT_ENABLED, settings.transport_enabled);
|
|
prefs.putBool(KEY_NOTIF_SND, settings.notification_sound);
|
|
prefs.putUChar(KEY_NOTIF_VOL, settings.notification_volume);
|
|
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);
|
|
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");
|
|
}
|
|
|
|
const bool applied = !_save_callback || _save_callback(settings);
|
|
if (!applied) _apply_retry_at_ms = millis() + 1000U;
|
|
_save_state.store(applied ? SAVE_IDLE : SAVE_APPLY_RETRY,
|
|
std::memory_order_release);
|
|
}
|
|
|
|
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 / 60).c_str()); // stored seconds -> shown minutes
|
|
}
|
|
if (_ta_sync_interval) {
|
|
// Display in hours (stored in seconds)
|
|
lv_textarea_set_text(_ta_sync_interval, String(_settings.sync_interval / 3600).c_str()); // stored seconds -> shown hours
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
if (_switch_transport_enabled) {
|
|
// Bypass the user-confirmation handler while reflecting persisted state.
|
|
_transport_enable_confirmed = true;
|
|
if (_settings.transport_enabled) {
|
|
lv_obj_add_state(_switch_transport_enabled, LV_STATE_CHECKED);
|
|
} else {
|
|
lv_obj_clear_state(_switch_transport_enabled, LV_STATE_CHECKED);
|
|
}
|
|
_transport_enable_confirmed = false;
|
|
}
|
|
|
|
// Interface settings (TCP / Auto / LoRa / BLE)
|
|
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 (_switch_lora_interface) {
|
|
// The Network interface row mirrors the Radio page's LoRa switch.
|
|
if (_settings.lora_enabled) {
|
|
lv_obj_add_state(_switch_lora_interface, LV_STATE_CHECKED);
|
|
} else {
|
|
lv_obj_clear_state(_switch_lora_interface, LV_STATE_CHECKED);
|
|
}
|
|
}
|
|
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 5-12 maps to index 0-7
|
|
lv_dropdown_set_selected(_dropdown_lora_sf, _settings.lora_sf - 5);
|
|
}
|
|
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() * 60; // minutes -> seconds
|
|
}
|
|
if (_ta_sync_interval) {
|
|
// UI shows hours, store as seconds
|
|
_settings.sync_interval = String(lv_textarea_get_text(_ta_sync_interval)).toInt() * 3600; // hours -> seconds
|
|
}
|
|
if (_switch_gps_sync) {
|
|
_settings.gps_time_sync = lv_obj_has_state(_switch_gps_sync, LV_STATE_CHECKED);
|
|
}
|
|
if (_switch_transport_enabled) {
|
|
_settings.transport_enabled = lv_obj_has_state(_switch_transport_enabled, 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) {
|
|
// The Radio page's switch is the canonical lora_enabled handle.
|
|
// The Network interface row mirrors it (kept in sync below), so
|
|
// reading both would let a stale mirror clobber the last action.
|
|
_settings.lora_enabled = lv_obj_has_state(_switch_lora_enabled, LV_STATE_CHECKED);
|
|
}
|
|
if (_switch_lora_interface) {
|
|
if (_settings.lora_enabled) {
|
|
lv_obj_add_state(_switch_lora_interface, LV_STATE_CHECKED);
|
|
} else {
|
|
lv_obj_clear_state(_switch_lora_interface, 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-7 maps to SF 5-12
|
|
_settings.lora_sf = lv_dropdown_get_selected(_dropdown_lora_sf) + 5;
|
|
}
|
|
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::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::set_status_callback(StatusScreenCallback callback) {
|
|
_status_callback = callback;
|
|
}
|
|
|
|
void SettingsScreen::set_identity_callback(IdentityScreenCallback callback) {
|
|
_identity_callback = callback;
|
|
}
|
|
|
|
void SettingsScreen::show() {
|
|
LVGL_LOCK();
|
|
lv_obj_clear_flag(_screen, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_move_foreground(_screen);
|
|
|
|
// Entering Settings always lands on the hub.
|
|
switch_view(VIEW_HUB);
|
|
}
|
|
|
|
void SettingsScreen::hide() {
|
|
LVGL_LOCK();
|
|
close_transport_warning();
|
|
// 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);
|
|
for (int i = 0; i < VIEW_COUNT - 1; ++i) {
|
|
if (_cards[i]) lv_group_remove_obj(_cards[i]);
|
|
}
|
|
}
|
|
|
|
lv_obj_add_flag(_screen, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
lv_obj_t* SettingsScreen::get_object() {
|
|
return _screen;
|
|
}
|
|
|
|
const char* SettingsScreen::view_title(View view) {
|
|
switch (view) {
|
|
case VIEW_NETWORK: return "Network";
|
|
case VIEW_IDENTITY: return "Identity";
|
|
case VIEW_RADIO: return "Radio";
|
|
case VIEW_DELIVERY: return "Delivery";
|
|
case VIEW_APPEARANCE: return "Appearance";
|
|
case VIEW_ADVANCED: return "Advanced";
|
|
case VIEW_TRANSPORT: return "Transport Mode";
|
|
case VIEW_HUB:
|
|
default: return "Settings";
|
|
}
|
|
}
|
|
|
|
void SettingsScreen::switch_view(View view) {
|
|
close_transport_warning();
|
|
_view = view;
|
|
|
|
// Title + Save visibility (Save only on the form sub-views)
|
|
lv_label_set_text(_title, view_title(view));
|
|
const bool show_save = (view == VIEW_NETWORK || view == VIEW_RADIO || view == VIEW_IDENTITY);
|
|
if (show_save) {
|
|
lv_obj_clear_flag(_btn_save, LV_OBJ_FLAG_HIDDEN);
|
|
} else {
|
|
lv_obj_add_flag(_btn_save, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
// Show the requested page, hide the rest.
|
|
for (int i = 0; i < VIEW_COUNT; ++i) {
|
|
if (!_pages[i]) continue;
|
|
if (i == view) {
|
|
lv_obj_clear_flag(_pages[i], LV_OBJ_FLAG_HIDDEN);
|
|
// Hidden LVGL objects keep their scroll position; each entry
|
|
// starts at the top.
|
|
lv_obj_scroll_to_y(_pages[i], 0, LV_ANIM_OFF);
|
|
} else {
|
|
lv_obj_add_flag(_pages[i], LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
}
|
|
|
|
focus_group_for(view);
|
|
}
|
|
|
|
void SettingsScreen::focus_group_for(View view) {
|
|
lv_group_t* group = LVGL::LVGLInit::get_default_group();
|
|
if (!group) return;
|
|
|
|
// Remove everything settings-related, then add exactly the objects the
|
|
// current view contains. Only-visible-object focus avoids the
|
|
// auto-scroll-to-the-last-object behavior on entry.
|
|
lv_obj_t* all[] = {
|
|
_btn_back, _btn_save,
|
|
_cards[0], _cards[1], _cards[2], _cards[3], _cards[4], _cards[5], _cards[6], _cards[7],
|
|
_btn_view_identity, _btn_reconnect, _btn_propagation_nodes,
|
|
_ta_wifi_ssid, _ta_wifi_password, _ta_tcp_host, _ta_tcp_port,
|
|
_ta_display_name, _ta_lora_frequency, _ta_announce_interval, _ta_sync_interval,
|
|
_slider_brightness, _slider_notification_volume, _slider_lora_power,
|
|
_switch_kb_light, _dropdown_timeout,
|
|
_switch_notification_sound,
|
|
_switch_lora_enabled,
|
|
_switch_tcp_enabled, _switch_auto_enabled, _switch_ble_enabled, _switch_lora_interface,
|
|
_switch_gps_sync,
|
|
_switch_prop_fallback, _switch_prop_only,
|
|
_switch_transport_enabled,
|
|
};
|
|
for (lv_obj_t* obj : all) {
|
|
if (obj) lv_group_remove_obj(obj);
|
|
}
|
|
|
|
lv_group_focus_freeze(group, true);
|
|
lv_group_add_obj(group, _btn_back);
|
|
if (_btn_save && !lv_obj_has_flag(_btn_save, LV_OBJ_FLAG_HIDDEN)) {
|
|
lv_group_add_obj(group, _btn_save);
|
|
}
|
|
if (view == VIEW_HUB) {
|
|
for (int i = 0; i < VIEW_COUNT - 1; ++i) {
|
|
if (_cards[i]) lv_group_add_obj(group, _cards[i]);
|
|
}
|
|
} else if (view == VIEW_NETWORK) {
|
|
lv_obj_t* objs[] = {
|
|
_ta_wifi_ssid, _ta_wifi_password, _ta_tcp_host, _ta_tcp_port, _btn_reconnect,
|
|
_switch_tcp_enabled, _switch_auto_enabled, _switch_lora_interface, _switch_ble_enabled,
|
|
};
|
|
for (lv_obj_t* obj : objs) if (obj) lv_group_add_obj(group, obj);
|
|
} else if (view == VIEW_IDENTITY) {
|
|
if (_ta_display_name) lv_group_add_obj(group, _ta_display_name);
|
|
if (_btn_view_identity) lv_group_add_obj(group, _btn_view_identity);
|
|
} else if (view == VIEW_RADIO) {
|
|
// LoRa params are only focusable while visible (LoRa enabled);
|
|
// a hidden container would trap focus on invisible controls.
|
|
lv_obj_t* objs[] = {
|
|
_switch_lora_enabled, _ta_lora_frequency, _dropdown_lora_bandwidth,
|
|
_dropdown_lora_sf, _dropdown_lora_cr, _slider_lora_power,
|
|
};
|
|
for (lv_obj_t* obj : objs) {
|
|
if (!obj) continue;
|
|
if (obj != _switch_lora_enabled &&
|
|
lv_obj_has_flag(_lora_params_container, LV_OBJ_FLAG_HIDDEN)) {
|
|
continue;
|
|
}
|
|
lv_group_add_obj(group, obj);
|
|
}
|
|
} else if (view == VIEW_DELIVERY) {
|
|
if (_btn_propagation_nodes) lv_group_add_obj(group, _btn_propagation_nodes);
|
|
if (_switch_prop_fallback) lv_group_add_obj(group, _switch_prop_fallback);
|
|
if (_switch_prop_only) lv_group_add_obj(group, _switch_prop_only);
|
|
} else if (view == VIEW_APPEARANCE) {
|
|
lv_obj_t* objs[] = {
|
|
_slider_brightness, _switch_kb_light, _dropdown_timeout,
|
|
_switch_notification_sound, _slider_notification_volume,
|
|
};
|
|
for (lv_obj_t* obj : objs) if (obj) lv_group_add_obj(group, obj);
|
|
} else if (view == VIEW_ADVANCED) {
|
|
lv_obj_t* objs[] = {_ta_announce_interval, _ta_sync_interval, _switch_gps_sync};
|
|
for (lv_obj_t* obj : objs) if (obj) lv_group_add_obj(group, obj);
|
|
} else if (view == VIEW_TRANSPORT) {
|
|
if (_switch_transport_enabled) lv_group_add_obj(group, _switch_transport_enabled);
|
|
}
|
|
lv_group_focus_freeze(group, false);
|
|
|
|
lv_group_focus_obj(_btn_back);
|
|
}
|
|
|
|
void SettingsScreen::on_back_clicked(lv_event_t* event) {
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
if (screen->_view != VIEW_HUB) {
|
|
// Leave the sub-view first; stay on the hub.
|
|
screen->switch_view(VIEW_HUB);
|
|
return;
|
|
}
|
|
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_card_clicked(lv_event_t* event) {
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
lv_obj_t* card = lv_event_get_target(event);
|
|
View target = (View)(intptr_t)lv_obj_get_user_data(card);
|
|
|
|
// The Status card links to the Status screen (Route::STATUS); the
|
|
// navigation stack carries the back button there.
|
|
if (card == screen->_cards[0]) {
|
|
if (screen->_status_callback) {
|
|
screen->_status_callback();
|
|
}
|
|
return;
|
|
}
|
|
screen->switch_view(target);
|
|
}
|
|
|
|
void SettingsScreen::on_view_identity_clicked(lv_event_t* event) {
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
if (screen->_identity_callback) {
|
|
screen->_identity_callback();
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
screen->save_settings();
|
|
}
|
|
|
|
void SettingsScreen::on_kb_light_changed(lv_event_t* event) {
|
|
// Immediate: the main loop reads keyboard_light every frame.
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
screen->save_settings();
|
|
}
|
|
|
|
void SettingsScreen::on_timeout_changed(lv_event_t* event) {
|
|
// Immediate: the main loop reads screen_timeout for display sleep.
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
screen->save_settings();
|
|
}
|
|
|
|
void SettingsScreen::on_notif_sound_changed(lv_event_t* event) {
|
|
// Immediate: sound/volume are read at delivery time.
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
screen->save_settings();
|
|
}
|
|
|
|
void SettingsScreen::on_gps_sync_changed(lv_event_t* event) {
|
|
// Immediate: the GPS time-sync path reads gps_time_sync continuously.
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
screen->save_settings();
|
|
}
|
|
|
|
void SettingsScreen::on_interface_switch_changed(lv_event_t* event) {
|
|
// Immediate: interface start/stop is handled under RouterLock by the
|
|
// save callback on the main loop.
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
screen->save_settings();
|
|
}
|
|
|
|
void SettingsScreen::on_prop_switch_changed(lv_event_t* event) {
|
|
// Immediate: router->set_fallback_to_propagation / set_propagation_only
|
|
// are applied by the save callback on the main loop.
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
screen->save_settings();
|
|
}
|
|
|
|
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 and mirror to the Network interface row.
|
|
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);
|
|
}
|
|
}
|
|
if (screen->_switch_lora_interface) {
|
|
if (enabled) {
|
|
lv_obj_add_state(screen->_switch_lora_interface, LV_STATE_CHECKED);
|
|
} else {
|
|
lv_obj_clear_state(screen->_switch_lora_interface, LV_STATE_CHECKED);
|
|
}
|
|
}
|
|
// Refresh the focus group so params are focusable exactly while visible.
|
|
if (screen->_view == VIEW_RADIO) {
|
|
screen->focus_group_for(VIEW_RADIO);
|
|
}
|
|
screen->save_settings();
|
|
}
|
|
|
|
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());
|
|
screen->save_settings();
|
|
}
|
|
|
|
void SettingsScreen::on_transport_enabled_changed(lv_event_t* event) {
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
bool enabled = lv_obj_has_state(screen->_switch_transport_enabled, LV_STATE_CHECKED);
|
|
|
|
if (!enabled) {
|
|
screen->_transport_enable_confirmed = false;
|
|
return;
|
|
}
|
|
|
|
if (screen->_transport_enable_confirmed) {
|
|
screen->save_settings();
|
|
return;
|
|
}
|
|
|
|
// First enable attempt: require explicit confirmation before the
|
|
// dangerous opt-in persists or applies.
|
|
screen->show_transport_warning();
|
|
}
|
|
|
|
void SettingsScreen::on_transport_confirm_enable(lv_event_t* event) {
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
screen->_transport_enable_confirmed = true;
|
|
screen->close_transport_warning();
|
|
screen->save_settings();
|
|
}
|
|
|
|
void SettingsScreen::on_transport_cancel_enable(lv_event_t* event) {
|
|
SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event);
|
|
screen->_transport_enable_confirmed = false;
|
|
if (screen->_switch_transport_enabled) {
|
|
lv_obj_clear_state(screen->_switch_transport_enabled, LV_STATE_CHECKED);
|
|
}
|
|
screen->close_transport_warning();
|
|
}
|
|
|
|
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
|