From 28ea218a186065ea38b2fd4696836ea6b011deaf Mon Sep 17 00:00:00 2001 From: Torlando <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 04:56:07 +0000 Subject: [PATCH] refactor(settings): move status readouts to Status screen, reorganize by use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings held live status (GPS fix, storage/RAM/identity) that belongs on the Status screen, plus a per-second tick() doing SPI flash stat reads and label churn mid-scroll — the main cause of laggy scrolling. - GPS section (sats/location/altitude/HDOP/time) -> StatusScreen - System Info (firmware build, storage, RAM) -> StatusScreen, with storage/RAM stat reads throttled to ~5s and stack-buffer snprintfs instead of Arduino String concatenation - Settings gains a Status link row (trackball-reachable) that opens Route::STATUS; the per-second SettingsScreen tick/refresh is deleted - Reordered sections by frequency of use: General (name/brightness/ timeout/kb-light), Notifications, Network (now includes the TCP/Auto/BLE interface switches), Radio (LoRa + params), Delivery, Advanced, DANGER: Transport Mode (still final) - Identity/LXMF hashes shown in Settings were truncated duplicates of the Status screen's full display; removed - main.cpp publishes firmware build + GPS to the Status screen - Contract test for the storage readout follows the code to StatusScreen --- lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp | 345 ++++++------------ lib/tdeck_ui/UI/LXMF/SettingsScreen.h | 124 +++---- lib/tdeck_ui/UI/LXMF/StatusScreen.cpp | 148 +++++++- lib/tdeck_ui/UI/LXMF/StatusScreen.h | 32 ++ lib/tdeck_ui/UI/LXMF/UIManager.cpp | 18 +- src/main.cpp | 14 +- .../test_message_persistence_contract.py | 4 +- 7 files changed, 348 insertions(+), 337 deletions(-) diff --git a/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp b/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp index ce8a3b42..968ddba4 100644 --- a/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp @@ -8,10 +8,7 @@ #include #include "../LVGL/LVGLLock.h" -#include -#include #include -#include #include "../LVGL/LVGLInit.h" #include "../TextAreaHelper.h" @@ -60,9 +57,7 @@ SettingsScreen::SettingsScreen(lv_obj_t* parent) _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_gps_time(nullptr), - _label_identity_hash(nullptr), _label_lxmf_address(nullptr), _label_firmware(nullptr), - _label_storage(nullptr), _label_ram(nullptr), + _btn_status(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), @@ -72,7 +67,7 @@ SettingsScreen::SettingsScreen(lv_obj_t* parent) _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), _gps(nullptr) { + _save_state(0U), _apply_retry_at_ms(0U) { LVGL_LOCK(); // Create screen object @@ -171,15 +166,14 @@ void SettingsScreen::create_content() { 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 sections. Order is by frequency of use: the everyday controls + // come first, the rare/advanced ones last. Live status readouts no longer + // live here — the Status link at the top opens the Status screen (Route::STATUS). + create_general_section(_content); create_notifications_section(_content); - create_interfaces_section(_content); + create_network_section(_content); + create_radio_section(_content); create_delivery_section(_content); - create_gps_section(_content); - create_system_section(_content); create_advanced_section(_content); // This dangerous opt-in must remain the final Settings section. create_transport_mode_section(_content); @@ -293,17 +287,95 @@ void SettingsScreen::create_network_section(lv_obj_t* parent) { 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); + // 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); + + } -void SettingsScreen::create_identity_section(lv_obj_t* parent) { - create_section_header(parent, "== Identity =="); +void SettingsScreen::create_general_section(lv_obj_t* parent) { + // Status link: live readouts (GPS fix, system storage/RAM, network state) + // moved to the Status screen (Route::STATUS). Settings stays actionable-only, + // which also removes the per-second refresh work from this scroll view. + _btn_status = lv_btn_create(parent); + lv_obj_set_width(_btn_status, LV_PCT(100)); + lv_obj_set_height(_btn_status, 30); + lv_obj_set_style_bg_color(_btn_status, Theme::surfaceInput(), 0); + lv_obj_set_style_bg_color(_btn_status, Theme::surfaceElevated(), LV_STATE_PRESSED); + lv_obj_add_event_cb(_btn_status, on_status_clicked, LV_EVENT_CLICKED, this); + lv_obj_t* status_label = lv_label_create(_btn_status); + lv_label_set_text(status_label, "Status " LV_SYMBOL_RIGHT); + lv_obj_align(status_label, LV_ALIGN_LEFT_MID, 0, 0); + lv_obj_set_style_text_color(status_label, Theme::textPrimary(), 0); + { + lv_group_t* g = LVGL::LVGLInit::get_default_group(); + if (g) lv_group_add_obj(g, _btn_status); + } + + create_section_header(parent, "== General =="); 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); @@ -436,71 +508,8 @@ void SettingsScreen::create_notifications_section(lv_obj_t* parent) { 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); +void SettingsScreen::create_radio_section(lv_obj_t* parent) { + create_section_header(parent, "== Radio =="); // LoRa Enable row lv_obj_t* lora_row = lv_obj_create(parent); @@ -743,26 +752,6 @@ void SettingsScreen::create_delivery_section(lv_obj_t* parent) { 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: --"); - _label_gps_time = create_label_row(parent, "Time: --"); -} - -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: " FIRMWARE_VERSION); - _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 =="); @@ -1388,140 +1377,6 @@ void SettingsScreen::update_settings_from_ui() { } } -void SettingsScreen::update_gps_display() { - LVGL_LOCK(); - - // Current system clock (independent of the GPS object): verify the time-sync at - // a glance. A sane local date means a good fix synced; "not set" means unsynced; - // a far-future year would flag a GPS week-rollover slipping through. - { - time_t now = time(nullptr); - struct tm lt; - localtime_r(&now, <); - if (lt.tm_year + 1900 >= 2024) { // < 2024 == unsynced (1970 or the ESP32 ~2016 boot default) - char tbuf[48]; - strftime(tbuf, sizeof(tbuf), "Time: %Y-%m-%d %H:%M:%S", <); - lv_label_set_text(_label_gps_time, tbuf); - } else { - lv_label_set_text(_label_gps_time, "Time: not set (awaiting GPS)"); - } - } - - 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(); // hdop() already returns the true value (raw value()/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 = LittleFS.totalBytes(); - size_t used = LittleFS.usedBytes(); - String storage; - if (total == 0 || used > total) { - storage = "Storage: unavailable"; - } else { - size_t free = total - used; - 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_firmware_version(const String& version) { - if (_label_firmware) { - String text = "Firmware: " + version; - lv_label_set_text(_label_firmware, text.c_str()); - } -} - -void SettingsScreen::set_gps(TinyGPSPlus* gps) { - _gps = gps; -} - -void SettingsScreen::refresh() { - update_gps_display(); - update_system_info(); -} - -void SettingsScreen::tick() { - // Called every main-loop pass while Settings is the active screen. Throttled to - // ~1s, it refreshes the live readouts so the clock (and GPS/system info) updates - // on screen instead of being a static snapshot from when the screen was opened. - uint32_t now = millis(); - if (now - _last_tick_ms < 1000) return; - _last_tick_ms = now; - refresh(); -} - void SettingsScreen::set_back_callback(BackCallback callback) { _back_callback = callback; } @@ -1542,9 +1397,12 @@ void SettingsScreen::set_propagation_nodes_callback(PropagationNodesCallback cal _propagation_nodes_callback = callback; } +void SettingsScreen::set_status_callback(StatusScreenCallback callback) { + _status_callback = callback; +} + void SettingsScreen::show() { LVGL_LOCK(); - refresh(); lv_obj_clear_flag(_screen, LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(_screen); @@ -1681,6 +1539,13 @@ void SettingsScreen::on_transport_cancel_enable(lv_event_t* event) { screen->close_transport_warning(); } +void SettingsScreen::on_status_clicked(lv_event_t* event) { + SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event); + if (screen->_status_callback) { + screen->_status_callback(); + } +} + void SettingsScreen::on_propagation_nodes_clicked(lv_event_t* event) { SettingsScreen* screen = (SettingsScreen*)lv_event_get_user_data(event); if (screen->_propagation_nodes_callback) { diff --git a/lib/tdeck_ui/UI/LXMF/SettingsScreen.h b/lib/tdeck_ui/UI/LXMF/SettingsScreen.h index cfbce98e..0f71b105 100644 --- a/lib/tdeck_ui/UI/LXMF/SettingsScreen.h +++ b/lib/tdeck_ui/UI/LXMF/SettingsScreen.h @@ -10,11 +10,6 @@ #include #include #include -#include -#include - -// Forward declaration -class TinyGPSPlus; namespace UI { namespace LXMF { @@ -97,41 +92,49 @@ struct AppSettings { * Settings Screen * * Allows configuration of WiFi, TCP server, display, and other settings. - * Also shows GPS status and system info. + * Live GPS/system readouts live on the Status screen (Route::STATUS); + * Settings links there and holds only actionable controls. * * Layout: * +---------------------------------------+ * | [<] Settings [Save] | 36px * +---------------------------------------+ - * | == Network == | - * | WiFi SSID: [__________________] | - * | Password: [******************] | - * | TCP Server: [_________________] | - * | TCP Port: [____] [Reconnect] | + * | Status > (link) + * | == General == | + * | Display Name: [_______________] | + * | Brightness: [=======o------] 180 | + * | Keyboard Light: [OFF] | + * | Screen Timeout: [1 min v] | * | | - * | == Identity == | - * | Display Name: [_______________] | + * | == Notifications == | + * | Message Sound: [ON] | + * | Volume: [=====o--------] 10 | * | | - * | == Display == | - * | Brightness: [=======o------] 180 | - * | Timeout: [1 min v] | + * | == Network == | + * | WiFi SSID: [__________________] | + * | Password: [******************] | + * | TCP Server: [_________________] | + * | Port: [____] [Reconnect] | + * | TCP Interface: [ON] | + * | Auto Discovery: [OFF] | + * | BLE P2P: [OFF] | * | | - * | == GPS Status == | - * | Satellites: 8 | - * | Location: 40.7128, -74.0060 | - * | Altitude: 10.5m | - * | HDOP: 1.2 (Excellent) | + * | == Radio == | + * | LoRa Interface: [OFF] | + * | (params shown when enabled) | * | | - * | == System Info == | - * | Identity: a1b2c3d4e5f6... | - * | LXMF: f7e8d9c0b1a2... | - * | Firmware: v1.0.0 | - * | Storage: 1.2 MB free | - * | RAM: 145 KB free | + * | == Delivery == | + * | Propagation Nodes: [View] | + * | Fallback to Prop: [ON] | + * | Propagation Only: [OFF] | * | | - * | == Advanced == | - * | Announce: [60] seconds | - * | GPS Sync: [ON] | + * | == Advanced == | + * | Announce Interval (min): [240] | + * | Prop Sync Interval (hrs): [4] | + * | GPS Time Sync: [ON] | + * | | + * | == DANGER: Transport Mode == | + * | (warning + confirm switch) | * +---------------------------------------+ */ class SettingsScreen { @@ -143,6 +146,7 @@ public: using WifiReconnectCallback = std::function; using BrightnessChangeCallback = std::function; using PropagationNodesCallback = std::function; + using StatusScreenCallback = std::function; /** * Create settings screen @@ -171,30 +175,9 @@ public: */ const AppSettings& get_settings() const { return _settings; } - /** - * Set identity hash for display - */ - void set_identity_hash(const RNS::Bytes& hash); - - /** - * Set LXMF delivery address hash - */ - void set_lxmf_address(const RNS::Bytes& hash); - /** Set the exact running firmware build for post-reboot verification. */ void set_firmware_version(const String& version); - /** - * Set GPS pointer for status display - */ - void set_gps(TinyGPSPlus* gps); - - /** - * Refresh GPS and system info displays - */ - void refresh(); - void tick(); // periodic update while the screen is shown (ticks clock/GPS/system readouts) - /** * Set callback for back button */ @@ -220,6 +203,11 @@ public: */ void set_propagation_nodes_callback(PropagationNodesCallback callback); + /** + * Set callback for the Status link (opens Route::STATUS) + */ + void set_status_callback(StatusScreenCallback callback); + /** * Show the screen */ @@ -270,20 +258,8 @@ private: lv_obj_t* _slider_notification_volume; lv_obj_t* _label_notification_volume_value; - // GPS status labels (read-only) - lv_obj_t* _label_gps_sats; - lv_obj_t* _label_gps_coords; - lv_obj_t* _label_gps_alt; - lv_obj_t* _label_gps_hdop; - lv_obj_t* _label_gps_time; - uint32_t _last_tick_ms = 0; // throttle for tick() - - // System info labels (read-only) - lv_obj_t* _label_identity_hash; - lv_obj_t* _label_lxmf_address; - lv_obj_t* _label_firmware; - lv_obj_t* _label_storage; - lv_obj_t* _label_ram; + // Status link row (opens the Status screen with the live readouts) + lv_obj_t* _btn_status; // Interfaces section lv_obj_t* _switch_tcp_enabled; @@ -319,9 +295,6 @@ private: AppSettings _pending_save_settings; std::atomic _save_state; // 0 idle, 1 pending, 2 processing std::uint32_t _apply_retry_at_ms; - RNS::Bytes _identity_hash; - RNS::Bytes _lxmf_address; - TinyGPSPlus* _gps; // Callbacks BackCallback _back_callback; @@ -329,20 +302,18 @@ private: WifiReconnectCallback _wifi_reconnect_callback; BrightnessChangeCallback _brightness_change_callback; PropagationNodesCallback _propagation_nodes_callback; + StatusScreenCallback _status_callback; // UI construction void create_header(); void create_content(); - void create_network_section(lv_obj_t* parent); - void create_identity_section(lv_obj_t* parent); - void create_display_section(lv_obj_t* parent); + void create_general_section(lv_obj_t* parent); void create_notifications_section(lv_obj_t* parent); - void create_interfaces_section(lv_obj_t* parent); - void create_gps_section(lv_obj_t* parent); - void create_system_section(lv_obj_t* parent); + void create_network_section(lv_obj_t* parent); + void create_radio_section(lv_obj_t* parent); + void create_delivery_section(lv_obj_t* parent); void create_advanced_section(lv_obj_t* parent); void create_transport_mode_section(lv_obj_t* parent); - void create_delivery_section(lv_obj_t* parent); // Helpers lv_obj_t* create_section_header(lv_obj_t* parent, const char* title); @@ -353,12 +324,11 @@ private: // Update UI from settings void update_ui_from_settings(); void update_settings_from_ui(); - void update_gps_display(); - void update_system_info(); // Event handlers static void on_back_clicked(lv_event_t* event); static void on_save_clicked(lv_event_t* event); + static void on_status_clicked(lv_event_t* event); static void on_reconnect_clicked(lv_event_t* event); static void on_brightness_changed(lv_event_t* event); static void on_lora_enabled_changed(lv_event_t* event); diff --git a/lib/tdeck_ui/UI/LXMF/StatusScreen.cpp b/lib/tdeck_ui/UI/LXMF/StatusScreen.cpp index cb078295..58615a00 100644 --- a/lib/tdeck_ui/UI/LXMF/StatusScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/StatusScreen.cpp @@ -7,6 +7,8 @@ #ifdef ARDUINO #include +#include +#include #include #include "../LVGL/LVGLInit.h" #include "../LVGL/LVGLLock.h" @@ -22,6 +24,10 @@ StatusScreen::StatusScreen(lv_obj_t* parent) _label_identity_value(nullptr), _label_lxmf_value(nullptr), _label_wifi_status(nullptr), _label_wifi_ip(nullptr), _label_wifi_rssi(nullptr), _label_rns_status(nullptr), _label_prop_node(nullptr), _label_ble_header(nullptr), + _label_gps_sats(nullptr), _label_gps_coords(nullptr), _label_gps_alt(nullptr), + _label_gps_hdop(nullptr), _label_gps_time(nullptr), + _label_firmware(nullptr), _label_storage(nullptr), _label_ram(nullptr), + _gps(nullptr), _last_heavy_ms(0U), _rns_connected(false), _ble_peer_count(0) { // Initialize BLE peer labels array for (size_t i = 0; i < MAX_BLE_PEERS; i++) { @@ -191,6 +197,46 @@ void StatusScreen::create_content() { lv_label_set_long_mode(_label_prop_node, LV_LABEL_LONG_WRAP); lv_obj_set_style_pad_bottom(_label_prop_node, 8, 0); + // GPS section (read-only live readouts, previously on the Settings screen) + _label_gps_sats = lv_label_create(_content); + lv_label_set_text(_label_gps_sats, "Satellites: --"); + lv_obj_set_style_text_color(_label_gps_sats, Theme::textPrimary(), 0); + lv_obj_set_style_pad_bottom(_label_gps_sats, 2, 0); + + _label_gps_coords = lv_label_create(_content); + lv_label_set_text(_label_gps_coords, "Location: No fix"); + lv_obj_set_style_text_color(_label_gps_coords, Theme::textPrimary(), 0); + lv_obj_set_style_pad_bottom(_label_gps_coords, 2, 0); + + _label_gps_alt = lv_label_create(_content); + lv_label_set_text(_label_gps_alt, "Altitude: --"); + lv_obj_set_style_text_color(_label_gps_alt, Theme::textPrimary(), 0); + lv_obj_set_style_pad_bottom(_label_gps_alt, 2, 0); + + _label_gps_hdop = lv_label_create(_content); + lv_label_set_text(_label_gps_hdop, "HDOP: --"); + lv_obj_set_style_text_color(_label_gps_hdop, Theme::textPrimary(), 0); + lv_obj_set_style_pad_bottom(_label_gps_hdop, 2, 0); + + _label_gps_time = lv_label_create(_content); + lv_label_set_text(_label_gps_time, "Time: --"); + lv_obj_set_style_text_color(_label_gps_time, Theme::textPrimary(), 0); + lv_obj_set_style_pad_bottom(_label_gps_time, 8, 0); + + // System section (firmware build, storage, RAM) + _label_firmware = lv_label_create(_content); + lv_label_set_text(_label_firmware, "Firmware: " FIRMWARE_VERSION); + lv_obj_set_style_text_color(_label_firmware, Theme::textPrimary(), 0); + + _label_storage = lv_label_create(_content); + lv_label_set_text(_label_storage, "Storage: --"); + lv_obj_set_style_text_color(_label_storage, Theme::textPrimary(), 0); + + _label_ram = lv_label_create(_content); + lv_label_set_text(_label_ram, "RAM: --"); + lv_obj_set_style_text_color(_label_ram, Theme::textPrimary(), 0); + lv_obj_set_style_pad_bottom(_label_ram, 8, 0); + // BLE section header _label_ble_header = lv_label_create(_content); lv_label_set_text(_label_ble_header, "BLE: No peers"); @@ -205,6 +251,7 @@ void StatusScreen::create_content() { lv_obj_set_style_pad_left(_label_ble_peers[i], 8, 0); lv_obj_add_flag(_label_ble_peers[i], LV_OBJ_FLAG_HIDDEN); } + } void StatusScreen::set_identity_hash(const Bytes& hash) { @@ -242,6 +289,16 @@ void StatusScreen::set_propagation_node(const String& display) { update_labels(); } +void StatusScreen::set_gps(TinyGPSPlus* gps) { + _gps = gps; +} + +void StatusScreen::set_firmware_version(const String& version) { + LVGL_LOCK(); + _firmware_version = version; + update_system_labels(); +} + void StatusScreen::refresh() { LVGL_LOCK(); update_labels(); @@ -357,6 +414,95 @@ void StatusScreen::update_labels() { lv_obj_add_flag(_label_ble_peers[i], LV_OBJ_FLAG_HIDDEN); } } + + update_gps_labels(); + update_system_labels(); +} + +void StatusScreen::update_gps_labels() { + // Current system clock (independent of the GPS object): verify the time-sync at + // a glance. A sane local date means a good fix synced; "not set" means unsynced. + { + time_t now = time(nullptr); + struct tm lt; + localtime_r(&now, <); + if (lt.tm_year + 1900 >= 2024) { + char tbuf[48]; + strftime(tbuf, sizeof(tbuf), "Time: %Y-%m-%d %H:%M:%S", <); + lv_label_set_text(_label_gps_time, tbuf); + } else { + lv_label_set_text(_label_gps_time, "Time: not set (awaiting GPS)"); + } + } + + 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; + } + + char buf[64]; + snprintf(buf, sizeof(buf), "Satellites: %d", (int)_gps->satellites.value()); + lv_label_set_text(_label_gps_sats, buf); + + if (_gps->location.isValid()) { + snprintf(buf, sizeof(buf), "Location: %.4f, %.4f", + _gps->location.lat(), _gps->location.lng()); + lv_label_set_text(_label_gps_coords, buf); + 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); + } + + if (_gps->altitude.isValid()) { + snprintf(buf, sizeof(buf), "Altitude: %.1fm", _gps->altitude.meters()); + lv_label_set_text(_label_gps_alt, buf); + } else { + lv_label_set_text(_label_gps_alt, "Altitude: --"); + } + + if (_gps->hdop.isValid()) { + double hdop = _gps->hdop.hdop(); + const char* 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"; + snprintf(buf, sizeof(buf), "HDOP: %.1f (%s)", hdop, quality); + lv_label_set_text(_label_gps_hdop, buf); + } else { + lv_label_set_text(_label_gps_hdop, "HDOP: --"); + } +} + +void StatusScreen::update_system_labels() { + if (_label_firmware && _firmware_version.length() > 0) { + char fbuf[96]; + snprintf(fbuf, sizeof(fbuf), "Firmware: %s", _firmware_version.c_str()); + lv_label_set_text(_label_firmware, fbuf); + } + + // Storage/RAM stat reads hit the SPI flash and the allocator; throttle them + // to ~5s so the per-second refresh stays cheap. + uint32_t now = millis(); + if (now - _last_heavy_ms < 5000) return; + _last_heavy_ms = now; + + char buf[48]; + size_t total = LittleFS.totalBytes(); + size_t used = LittleFS.usedBytes(); + if (total == 0 || used > total) { + lv_label_set_text(_label_storage, "Storage: unavailable"); + } else { + snprintf(buf, sizeof(buf), "Storage: %d KB free", (int)((total - used) / 1024)); + lv_label_set_text(_label_storage, buf); + } + snprintf(buf, sizeof(buf), "RAM: %d KB free", (int)(ESP.getFreeHeap() / 1024)); + lv_label_set_text(_label_ram, buf); } void StatusScreen::set_back_callback(BackCallback callback) { @@ -373,7 +519,7 @@ void StatusScreen::set_radio_activity_callback(RadioActivityCallback callback) { void StatusScreen::show() { LVGL_LOCK(); - refresh(); // Update status when shown + update_labels(); // Update status when shown lv_obj_clear_flag(_screen, LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(_screen); diff --git a/lib/tdeck_ui/UI/LXMF/StatusScreen.h b/lib/tdeck_ui/UI/LXMF/StatusScreen.h index fc42b9e5..fc56a6cc 100644 --- a/lib/tdeck_ui/UI/LXMF/StatusScreen.h +++ b/lib/tdeck_ui/UI/LXMF/StatusScreen.h @@ -12,6 +12,9 @@ #include #include +// Forward declaration +class TinyGPSPlus; + namespace UI { namespace LXMF { @@ -23,6 +26,8 @@ namespace LXMF { * - LXMF delivery destination hash * - WiFi status and IP * - RNS connection status + * - GPS fix (satellites, location, altitude, HDOP, time) + * - System (firmware build, storage, RAM) * * Layout: * ┌─────────────────────────────────────┐ @@ -77,6 +82,16 @@ public: */ void set_rns_status(bool connected, const String& server_name = ""); + /** + * Set the GPS receiver for live fix readouts + */ + void set_gps(TinyGPSPlus* gps); + + /** + * Set the exact running firmware build string (e.g. "v0.3.7 [ota_0]") + */ + void set_firmware_version(const String& version); + /** * Set propagation node display string * @param display Formatted string like "Auto (NodeName)" or "None" @@ -153,6 +168,18 @@ private: lv_obj_t* _label_rns_status; lv_obj_t* _label_prop_node; + // GPS readout labels (read-only) + lv_obj_t* _label_gps_sats; + lv_obj_t* _label_gps_coords; + lv_obj_t* _label_gps_alt; + lv_obj_t* _label_gps_hdop; + lv_obj_t* _label_gps_time; + + // System readout labels (read-only) + lv_obj_t* _label_firmware; + lv_obj_t* _label_storage; + lv_obj_t* _label_ram; + // BLE peer labels (pre-allocated, hidden when unused) lv_obj_t* _label_ble_header; lv_obj_t* _label_ble_peers[MAX_BLE_PEERS]; // Each shows identity + rssi + mac @@ -162,6 +189,9 @@ private: bool _rns_connected; String _rns_server; String _prop_node_display; + String _firmware_version; + TinyGPSPlus* _gps; + uint32_t _last_heavy_ms; // throttles storage/RAM stat reads in update_labels() // BLE peer data (cached for display) BLEPeerInfo _ble_peers[MAX_BLE_PEERS]; @@ -174,6 +204,8 @@ private: void create_header(); void create_content(); void update_labels(); + void update_gps_labels(); + void update_system_labels(); static void on_back_clicked(lv_event_t* event); static void on_share_clicked(lv_event_t* event); diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.cpp b/lib/tdeck_ui/UI/LXMF/UIManager.cpp index 4221c552..ea0e3693 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.cpp +++ b/lib/tdeck_ui/UI/LXMF/UIManager.cpp @@ -594,6 +594,12 @@ bool UIManager::init() { [this]() { on_back_from_settings(); } ); + // Status link on the Settings screen opens the Status screen; the + // navigation stack carries the back button there. + _settings_screen->set_status_callback( + [this]() { show_status(); } + ); + // Set up callbacks for propagation nodes screen _propagation_nodes_screen->set_back_callback( @@ -646,10 +652,6 @@ bool UIManager::init() { } } - // Set identity and LXMF address on settings screen - _settings_screen->set_identity_hash(_router.identity().hash()); - _settings_screen->set_lxmf_address(_router.delivery_destination().hash()); - // Set identity hash and LXMF address on status screen _status_screen->set_identity_hash(_router.identity().hash()); @@ -813,9 +815,6 @@ void UIManager::update() { if (_navigation.current() == Route::ANNOUNCES && _announce_list_screen) { _announce_list_screen->tick(); } - if (_navigation.current() == Route::SETTINGS && _settings_screen) { - _settings_screen->tick(); // keep the live clock / GPS / system readouts ticking - } const uint32_t now = millis(); nomad_update_library(); nomad_update(); @@ -1149,7 +1148,6 @@ void UIManager::render_route(Route route) { _announce_list_screen->show(); break; case Route::STATUS: - _status_screen->refresh(); _status_screen->show(); break; case Route::RADIO_ACTIVITY: @@ -1165,7 +1163,6 @@ void UIManager::render_route(Route route) { _nomadnet_screen->show(); break; case Route::SETTINGS: - _settings_screen->refresh(); _settings_screen->show(); break; case Route::CALL: if (_call_screen) _call_screen->show(); break; @@ -1874,9 +1871,6 @@ void UIManager::refresh_current_screen() { case Route::STATUS: _status_screen->refresh(); break; - case Route::SETTINGS: - _settings_screen->refresh(); - break; case Route::PROPAGATION_NODES: _propagation_nodes_screen->refresh(); break; diff --git a/src/main.cpp b/src/main.cpp index b1cbcf64..b7a2d37a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1453,9 +1453,8 @@ void setup_ui_manager() { // Set GPS for satellite count display ui_manager->set_gps(&gps); - // Configure settings screen - UI::LXMF::SettingsScreen* settings = ui_manager->get_settings_screen(); - if (settings) { + // Publish the exact running build (with OTA slot) to the Status screen + if (ui_manager->get_status_screen()) { String firmware_info = FIRMWARE_VERSION; const esp_partition_t* running_partition = esp_ota_get_running_partition(); if (running_partition) { @@ -1463,10 +1462,13 @@ void setup_ui_manager() { firmware_info += running_partition->label; firmware_info += "]"; } - settings->set_firmware_version(firmware_info); - // Pass GPS for status display - settings->set_gps(&gps); + ui_manager->get_status_screen()->set_firmware_version(firmware_info); + ui_manager->get_status_screen()->set_gps(&gps); + } + // Configure settings screen + UI::LXMF::SettingsScreen* settings = ui_manager->get_settings_screen(); + if (settings) { // Set brightness change callback (immediate) settings->set_brightness_change_callback([](uint8_t brightness) { // Apply brightness immediately via display backlight diff --git a/tests/build_scripts/test_message_persistence_contract.py b/tests/build_scripts/test_message_persistence_contract.py index 2d628f46..6b3be09d 100644 --- a/tests/build_scripts/test_message_persistence_contract.py +++ b/tests/build_scripts/test_message_persistence_contract.py @@ -135,7 +135,9 @@ def test_successful_boot_cancels_ota_rollback_after_subsystems_initialize(): def test_system_info_reports_littlefs_not_unmounted_spiffs(): - source = (REPO_ROOT / "lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp").read_text() + # The live system readouts (firmware build, storage, RAM) moved from the + # Settings screen to the Status screen; the contract follows them there. + source = (REPO_ROOT / "lib/tdeck_ui/UI/LXMF/StatusScreen.cpp").read_text() assert '"Firmware: " FIRMWARE_VERSION' in source assert "LittleFS.totalBytes()" in source assert "LittleFS.usedBytes()" in source