refactor(settings): move status readouts to Status screen, reorganize by use

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
This commit is contained in:
Torlando committed 2026-09-04 04:56:07 +00:00
1 parent 8c0d38ee2e
commit 28ea218a18
7 files changed
+348 -337

No files matched your search

+105 -240
View File
@@ -8,10 +8,7 @@
#include <WiFi.h>
#include "../LVGL/LVGLLock.h"
#include <LittleFS.h>
#include <TinyGPSPlus.h>
#include <microReticulum/Log.h>
#include <microReticulum/Utilities/OS.h>
#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, &lt);
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", &lt);
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) {
+47 -77
View File
@@ -10,11 +10,6 @@
#include <Preferences.h>
#include <functional>
#include <atomic>
#include <microReticulum/Bytes.h>
#include <microReticulum/Identity.h>
// 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<void(const String&, const String&)>;
using BrightnessChangeCallback = std::function<void(uint8_t)>;
using PropagationNodesCallback = std::function<void()>;
using StatusScreenCallback = std::function<void()>;
/**
* 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<std::uint8_t> _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);
+147 -1
View File
@@ -7,6 +7,8 @@
#ifdef ARDUINO
#include <WiFi.h>
#include <LittleFS.h>
#include <TinyGPSPlus.h>
#include <microReticulum/Log.h>
#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, &lt);
if (lt.tm_year + 1900 >= 2024) {
char tbuf[48];
strftime(tbuf, sizeof(tbuf), "Time: %Y-%m-%d %H:%M:%S", &lt);
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);
+32
View File
@@ -12,6 +12,9 @@
#include <microReticulum/Bytes.h>
#include <microReticulum/Identity.h>
// 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);
+6 -12
View File
@@ -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;
+8 -6
View File
@@ -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
@@ -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