From 609a3bc62b74b91e2276b07466e7fadb6b3bca79 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Mon, 2 Mar 2026 23:03:32 -0500 Subject: [PATCH] LXMF propagation sync, manual node entry, and status improvements Propagation sync (microReticulum submodule): - Fix msgpack interop: send nil (not 0) for per_transfer_limit so Python server doesn't reject all messages as exceeding "0 KB limit" - Fix Resource response routing: extract request_id from packed data when not present in Resource advertisement, route to pending request callback instead of generic concluded handler - Fix Link::request() to manually build packed arrays, avoiding Bytes::to_msgpack() BIN-wrapping that breaks protocol interop UI enhancements: - PropagationNodesScreen: manual node entry via 32-char hex hash in search field, with paste support and radio button selection - StatusScreen: display stamp cost from propagation node - UIManager: NVS persistence for selected propagation node, proactive path request on node selection, sync state machine with timeout handling Co-Authored-By: Claude Opus 4.6 --- deps/microReticulum | 2 +- .../UI/LXMF/PropagationNodesScreen.cpp | 212 +++++++++++++++--- lib/tdeck_ui/UI/LXMF/PropagationNodesScreen.h | 12 +- lib/tdeck_ui/UI/LXMF/StatusScreen.cpp | 25 ++- lib/tdeck_ui/UI/LXMF/StatusScreen.h | 8 + lib/tdeck_ui/UI/LXMF/UIManager.cpp | 87 ++++++- src/main.cpp | 2 +- 7 files changed, 313 insertions(+), 35 deletions(-) diff --git a/deps/microReticulum b/deps/microReticulum index 8532c0df..ca355e54 160000 --- a/deps/microReticulum +++ b/deps/microReticulum @@ -1 +1 @@ -Subproject commit 8532c0df1a52922bcf12a97c57b89eb613f62b56 +Subproject commit ca355e54a0565782444e66a531326e57ae9f8435 diff --git a/lib/tdeck_ui/UI/LXMF/PropagationNodesScreen.cpp b/lib/tdeck_ui/UI/LXMF/PropagationNodesScreen.cpp index d957e7ad..2f22c523 100644 --- a/lib/tdeck_ui/UI/LXMF/PropagationNodesScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/PropagationNodesScreen.cpp @@ -11,6 +11,7 @@ #include "Utilities/OS.h" #include "../LVGL/LVGLInit.h" #include "../LVGL/LVGLLock.h" +#include "../TextAreaHelper.h" using namespace RNS; @@ -21,7 +22,7 @@ PropagationNodesScreen::PropagationNodesScreen(lv_obj_t* parent) : _screen(nullptr), _header(nullptr), _list(nullptr), _btn_back(nullptr), _btn_sync(nullptr), _auto_select_row(nullptr), _auto_select_checkbox(nullptr), _empty_label(nullptr), - _auto_select_enabled(true) { + _search_input(nullptr), _auto_select_enabled(true) { // Create screen object if (parent) { @@ -40,6 +41,7 @@ PropagationNodesScreen::PropagationNodesScreen(lv_obj_t* parent) // Create UI components create_header(); + create_search_row(); create_auto_select_row(); create_list(); @@ -100,10 +102,39 @@ void PropagationNodesScreen::create_header() { lv_obj_set_style_text_font(label_sync, &lv_font_montserrat_12, 0); } +void PropagationNodesScreen::create_search_row() { + lv_obj_t* row = lv_obj_create(_screen); + lv_obj_set_size(row, LV_PCT(100), 32); + lv_obj_align(row, LV_ALIGN_TOP_MID, 0, 36); + lv_obj_set_style_bg_color(row, Theme::surface(), 0); + lv_obj_set_style_border_width(row, 0, 0); + lv_obj_set_style_radius(row, 0, 0); + lv_obj_set_style_pad_all(row, 2, 0); + lv_obj_set_style_pad_left(row, 8, 0); + lv_obj_set_style_pad_right(row, 8, 0); + + _search_input = lv_textarea_create(row); + lv_obj_set_size(_search_input, LV_PCT(100), 28); + lv_obj_align(_search_input, LV_ALIGN_CENTER, 0, 0); + lv_textarea_set_placeholder_text(_search_input, "Filter nodes..."); + lv_textarea_set_one_line(_search_input, true); + lv_textarea_set_max_length(_search_input, 32); + lv_obj_set_style_bg_color(_search_input, Theme::surfaceInput(), 0); + lv_obj_set_style_text_color(_search_input, Theme::textPrimary(), 0); + lv_obj_set_style_border_color(_search_input, Theme::border(), 0); + lv_obj_set_style_border_width(_search_input, 1, 0); + lv_obj_set_style_radius(_search_input, 4, 0); + lv_obj_set_style_text_font(_search_input, &lv_font_montserrat_12, 0); + lv_obj_add_event_cb(_search_input, on_search_changed, LV_EVENT_VALUE_CHANGED, this); + + // Enable paste on long-press + TextAreaHelper::enable_paste(_search_input); +} + void PropagationNodesScreen::create_auto_select_row() { _auto_select_row = lv_obj_create(_screen); lv_obj_set_size(_auto_select_row, LV_PCT(100), 36); - lv_obj_align(_auto_select_row, LV_ALIGN_TOP_MID, 0, 36); + lv_obj_align(_auto_select_row, LV_ALIGN_TOP_MID, 0, 68); lv_obj_set_style_bg_color(_auto_select_row, lv_color_hex(0x1e1e1e), 0); lv_obj_set_style_border_width(_auto_select_row, 0, 0); lv_obj_set_style_border_side(_auto_select_row, LV_BORDER_SIDE_BOTTOM, 0); @@ -133,8 +164,8 @@ void PropagationNodesScreen::create_auto_select_row() { void PropagationNodesScreen::create_list() { _list = lv_obj_create(_screen); - lv_obj_set_size(_list, LV_PCT(100), 168); // 240 - 36 (header) - 36 (auto-select row) - lv_obj_align(_list, LV_ALIGN_TOP_MID, 0, 72); + lv_obj_set_size(_list, LV_PCT(100), 136); // 240 - 36 (header) - 32 (search) - 36 (auto-select) + lv_obj_align(_list, LV_ALIGN_TOP_MID, 0, 104); lv_obj_set_style_pad_all(_list, 4, 0); lv_obj_set_style_pad_gap(_list, 4, 0); lv_obj_set_style_bg_color(_list, Theme::surface(), 0); @@ -161,7 +192,6 @@ void PropagationNodesScreen::load_nodes(::LXMF::PropagationNodeManager& manager, } // Clear existing items - lv_obj_clean(_list); _nodes.clear(); _empty_label = nullptr; @@ -172,8 +202,9 @@ void PropagationNodesScreen::load_nodes(::LXMF::PropagationNodeManager& manager, NodeItem item; item.node_hash = node_info.node_hash; item.name = node_info.name.c_str(); - item.hash_display = truncate_hash(node_info.node_hash); + item.hash_display = format_hash(node_info.node_hash); item.hops = node_info.hops; + item.stamp_cost = node_info.stamp_cost; item.enabled = node_info.enabled; item.is_selected = (!_auto_select_enabled && node_info.node_hash == _selected_hash); @@ -183,13 +214,7 @@ void PropagationNodesScreen::load_nodes(::LXMF::PropagationNodeManager& manager, std::string count_msg = " Found " + std::to_string(_nodes.size()) + " propagation nodes"; INFO(count_msg.c_str()); - if (_nodes.empty()) { - show_empty_state(); - } else { - for (size_t i = 0; i < _nodes.size(); i++) { - create_node_item(_nodes[i], i); - } - } + apply_filter(); } void PropagationNodesScreen::refresh() { @@ -199,7 +224,7 @@ void PropagationNodesScreen::refresh() { void PropagationNodesScreen::show_empty_state() { _empty_label = lv_label_create(_list); - lv_label_set_text(_empty_label, "No propagation nodes\n\nWaiting for nodes\nto announce..."); + lv_label_set_text(_empty_label, "No propagation nodes\n\nWaiting for announces\nor enter 32-char hash"); lv_obj_set_style_text_color(_empty_label, Theme::textMuted(), 0); lv_obj_set_style_text_align(_empty_label, LV_TEXT_ALIGN_CENTER, 0); lv_obj_align(_empty_label, LV_ALIGN_CENTER, 0, 0); @@ -263,22 +288,83 @@ void PropagationNodesScreen::create_node_item(const NodeItem& item, size_t index lv_obj_align(label_status, LV_ALIGN_BOTTOM_RIGHT, -4, -2); lv_obj_set_style_text_color(label_status, Theme::error(), 0); lv_obj_set_style_text_font(label_status, &lv_font_montserrat_12, 0); + } else if (item.stamp_cost > 0) { + lv_obj_t* label_cost = lv_label_create(container); + lv_label_set_text_fmt(label_cost, "stamp:%d", item.stamp_cost); + lv_obj_align(label_cost, LV_ALIGN_BOTTOM_RIGHT, -4, -2); + lv_obj_set_style_text_color(label_cost, Theme::textTertiary(), 0); + lv_obj_set_style_text_font(label_cost, &lv_font_montserrat_12, 0); + } +} + +void PropagationNodesScreen::create_manual_entry_item(const Bytes& hash, const String& hex_display) { + lv_obj_t* container = lv_obj_create(_list); + lv_obj_set_size(container, LV_PCT(100), 44); + lv_obj_set_style_bg_color(container, lv_color_hex(0x1e1e1e), 0); + lv_obj_set_style_bg_color(container, Theme::surfaceInput(), LV_STATE_PRESSED); + lv_obj_set_style_border_width(container, 1, 0); + lv_obj_set_style_border_color(container, Theme::info(), 0); + lv_obj_set_style_radius(container, 6, 0); + lv_obj_set_style_pad_all(container, 4, 0); + lv_obj_add_flag(container, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(container, on_manual_node_clicked, LV_EVENT_CLICKED, this); + + // Radio indicator + bool is_selected = (!_auto_select_enabled && hash == _selected_hash); + lv_obj_t* radio = lv_obj_create(container); + lv_obj_set_size(radio, 16, 16); + lv_obj_align(radio, LV_ALIGN_LEFT_MID, 2, 0); + lv_obj_set_style_radius(radio, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_width(radio, 2, 0); + lv_obj_clear_flag(radio, LV_OBJ_FLAG_CLICKABLE); + if (is_selected) { + lv_obj_set_style_bg_color(radio, Theme::info(), 0); + lv_obj_set_style_border_color(radio, Theme::info(), 0); + } else { + lv_obj_set_style_bg_color(radio, lv_color_hex(0x1e1e1e), 0); + lv_obj_set_style_border_color(radio, Theme::textMuted(), 0); + } + + // Row 1: "Use manual node" + lv_obj_t* label_name = lv_label_create(container); + lv_label_set_text(label_name, "Use manual node"); + lv_obj_align(label_name, LV_ALIGN_TOP_LEFT, 24, 2); + lv_obj_set_style_text_color(label_name, Theme::info(), 0); + lv_obj_set_style_text_font(label_name, &lv_font_montserrat_14, 0); + + // Row 2: Full hex hash + lv_obj_t* label_hash = lv_label_create(container); + lv_label_set_text(label_hash, hex_display.c_str()); + lv_obj_align(label_hash, LV_ALIGN_BOTTOM_LEFT, 24, -2); + lv_obj_set_style_text_color(label_hash, Theme::textMuted(), 0); + lv_obj_set_style_text_font(label_hash, &lv_font_montserrat_12, 0); +} + +void PropagationNodesScreen::on_manual_node_clicked(lv_event_t* event) { + PropagationNodesScreen* screen = static_cast(lv_event_get_user_data(event)); + + if (screen->_manual_hash.size() == 0) return; + + INFO("Manual propagation node selected"); + + // Disable auto-select + screen->_auto_select_enabled = false; + lv_obj_clear_state(screen->_auto_select_checkbox, LV_STATE_CHECKED); + + screen->_selected_hash = screen->_manual_hash; + screen->update_selection_ui(); + + if (screen->_auto_select_changed_callback) { + screen->_auto_select_changed_callback(false); + } + + if (screen->_node_selected_callback) { + screen->_node_selected_callback(screen->_manual_hash); } } void PropagationNodesScreen::update_selection_ui() { - // Clear and recreate list to update selection state - lv_obj_clean(_list); - _empty_label = nullptr; - - if (_nodes.empty()) { - show_empty_state(); - } else { - for (size_t i = 0; i < _nodes.size(); i++) { - _nodes[i].is_selected = (!_auto_select_enabled && _nodes[i].node_hash == _selected_hash); - create_node_item(_nodes[i], i); - } - } + apply_filter(); } // Event handlers @@ -347,6 +433,60 @@ void PropagationNodesScreen::on_auto_select_changed(lv_event_t* event) { } } +void PropagationNodesScreen::on_search_changed(lv_event_t* event) { + PropagationNodesScreen* screen = static_cast(lv_event_get_user_data(event)); + const char* text = lv_textarea_get_text(screen->_search_input); + screen->_search_filter = text; + screen->_search_filter.toLowerCase(); + screen->apply_filter(); +} + +void PropagationNodesScreen::apply_filter() { + lv_obj_clean(_list); + _empty_label = nullptr; + _manual_hash = {}; + + // Check if search is a valid 32-char hex hash with no matching discovered node + bool show_manual = false; + if (is_valid_hex(_search_filter)) { + Bytes parsed_hash; + parsed_hash.assignHex(_search_filter.c_str()); + bool found_in_nodes = false; + for (size_t i = 0; i < _nodes.size(); i++) { + if (_nodes[i].node_hash == parsed_hash) { + found_in_nodes = true; + break; + } + } + if (!found_in_nodes) { + show_manual = true; + _manual_hash = parsed_hash; + create_manual_entry_item(parsed_hash, _search_filter); + } + } + + size_t shown = 0; + for (size_t i = 0; i < _nodes.size(); i++) { + if (_search_filter.length() > 0) { + String name_lower = _nodes[i].name; + name_lower.toLowerCase(); + String hash_lower = _nodes[i].hash_display; + hash_lower.toLowerCase(); + if (name_lower.indexOf(_search_filter) < 0 && + hash_lower.indexOf(_search_filter) < 0) { + continue; + } + } + _nodes[i].is_selected = (!_auto_select_enabled && _nodes[i].node_hash == _selected_hash); + create_node_item(_nodes[i], i); + shown++; + } + + if (shown == 0 && !show_manual) { + show_empty_state(); + } +} + // Callback setters void PropagationNodesScreen::set_node_selected_callback(NodeSelectedCallback callback) { _node_selected_callback = callback; @@ -374,6 +514,7 @@ void PropagationNodesScreen::show() { lv_group_t* group = LVGL::LVGLInit::get_default_group(); if (group) { if (_btn_back) lv_group_add_obj(group, _btn_back); + if (_search_input) lv_group_add_obj(group, _search_input); if (_btn_sync) lv_group_add_obj(group, _btn_sync); // Focus on back button @@ -389,6 +530,7 @@ void PropagationNodesScreen::hide() { lv_group_t* group = LVGL::LVGLInit::get_default_group(); if (group) { if (_btn_back) lv_group_remove_obj(_btn_back); + if (_search_input) lv_group_remove_obj(_search_input); if (_btn_sync) lv_group_remove_obj(_btn_sync); } @@ -400,10 +542,9 @@ lv_obj_t* PropagationNodesScreen::get_object() { } // Utility functions -String PropagationNodesScreen::truncate_hash(const Bytes& hash) { - if (hash.size() < 8) return "???"; - String hex = hash.toHex().c_str(); - return hex.substring(0, 8) + "..."; +String PropagationNodesScreen::format_hash(const Bytes& hash) { + if (hash.size() == 0) return "???"; + return String(hash.toHex().c_str()); } String PropagationNodesScreen::format_hops(uint8_t hops) { @@ -412,6 +553,17 @@ String PropagationNodesScreen::format_hops(uint8_t hops) { return String(hops) + " hop" + (hops == 1 ? "" : "s"); } +bool PropagationNodesScreen::is_valid_hex(const String& str) { + if (str.length() != 32) return false; + for (size_t i = 0; i < 32; i++) { + char c = str.charAt(i); + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + return false; + } + } + return true; +} + } // namespace LXMF } // namespace UI diff --git a/lib/tdeck_ui/UI/LXMF/PropagationNodesScreen.h b/lib/tdeck_ui/UI/LXMF/PropagationNodesScreen.h index f80659d8..15a34ee9 100644 --- a/lib/tdeck_ui/UI/LXMF/PropagationNodesScreen.h +++ b/lib/tdeck_ui/UI/LXMF/PropagationNodesScreen.h @@ -50,6 +50,7 @@ public: String name; String hash_display; uint8_t hops; + uint8_t stamp_cost; bool enabled; bool is_selected; }; @@ -137,10 +138,13 @@ private: lv_obj_t* _auto_select_row; lv_obj_t* _auto_select_checkbox; lv_obj_t* _empty_label; + lv_obj_t* _search_input; std::vector _nodes; RNS::Bytes _selected_hash; + RNS::Bytes _manual_hash; bool _auto_select_enabled; + String _search_filter; // Callbacks NodeSelectedCallback _node_selected_callback; @@ -150,21 +154,27 @@ private: // UI construction void create_header(); + void create_search_row(); void create_auto_select_row(); void create_list(); void create_node_item(const NodeItem& item, size_t index); + void create_manual_entry_item(const RNS::Bytes& hash, const String& hex_display); void show_empty_state(); void update_selection_ui(); + void apply_filter(); // Event handlers static void on_node_clicked(lv_event_t* event); + static void on_manual_node_clicked(lv_event_t* event); static void on_back_clicked(lv_event_t* event); static void on_sync_clicked(lv_event_t* event); static void on_auto_select_changed(lv_event_t* event); + static void on_search_changed(lv_event_t* event); // Utility - static String truncate_hash(const RNS::Bytes& hash); + static String format_hash(const RNS::Bytes& hash); static String format_hops(uint8_t hops); + static bool is_valid_hex(const String& str); }; } // namespace LXMF diff --git a/lib/tdeck_ui/UI/LXMF/StatusScreen.cpp b/lib/tdeck_ui/UI/LXMF/StatusScreen.cpp index a2f9cf8c..718ad621 100644 --- a/lib/tdeck_ui/UI/LXMF/StatusScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/StatusScreen.cpp @@ -21,7 +21,7 @@ StatusScreen::StatusScreen(lv_obj_t* parent) _btn_share(nullptr), _label_uptime(nullptr), _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_ble_header(nullptr), + _label_rns_status(nullptr), _label_prop_node(nullptr), _label_ble_header(nullptr), _rns_connected(false), _ble_peer_count(0) { // Initialize BLE peer labels array for (size_t i = 0; i < MAX_BLE_PEERS; i++) { @@ -172,6 +172,14 @@ void StatusScreen::create_content() { lv_label_set_long_mode(_label_rns_status, LV_LABEL_LONG_WRAP); lv_obj_set_style_pad_bottom(_label_rns_status, 8, 0); + // Propagation node section + _label_prop_node = lv_label_create(_content); + lv_label_set_text(_label_prop_node, "Prop Node: None"); + lv_obj_set_style_text_color(_label_prop_node, Theme::textPrimary(), 0); + lv_obj_set_width(_label_prop_node, lv_pct(100)); + lv_label_set_long_mode(_label_prop_node, LV_LABEL_LONG_WRAP); + lv_obj_set_style_pad_bottom(_label_prop_node, 8, 0); + // BLE section header _label_ble_header = lv_label_create(_content); lv_label_set_text(_label_ble_header, "BLE: No peers"); @@ -217,6 +225,12 @@ void StatusScreen::set_ble_info(const BLEPeerInfo* peers, size_t count) { update_labels(); } +void StatusScreen::set_propagation_node(const String& display) { + LVGL_LOCK(); + _prop_node_display = display; + update_labels(); +} + void StatusScreen::refresh() { LVGL_LOCK(); update_labels(); @@ -289,6 +303,15 @@ void StatusScreen::update_labels() { lv_obj_set_style_text_color(_label_rns_status, Theme::error(), 0); } + // Update propagation node + if (_prop_node_display.length() > 0) { + char prop_text[80]; + snprintf(prop_text, sizeof(prop_text), "Prop Node: %s", _prop_node_display.c_str()); + lv_label_set_text(_label_prop_node, prop_text); + } else { + lv_label_set_text(_label_prop_node, "Prop Node: None"); + } + // Update BLE peer info if (_ble_peer_count > 0) { char ble_header[32]; diff --git a/lib/tdeck_ui/UI/LXMF/StatusScreen.h b/lib/tdeck_ui/UI/LXMF/StatusScreen.h index 98155c39..aea416fa 100644 --- a/lib/tdeck_ui/UI/LXMF/StatusScreen.h +++ b/lib/tdeck_ui/UI/LXMF/StatusScreen.h @@ -76,6 +76,12 @@ public: */ void set_rns_status(bool connected, const String& server_name = ""); + /** + * Set propagation node display string + * @param display Formatted string like "Auto (NodeName)" or "None" + */ + void set_propagation_node(const String& display); + /** * BLE peer summary for display (matches BLEInterface::PeerSummary) */ @@ -140,6 +146,7 @@ private: lv_obj_t* _label_wifi_ip; lv_obj_t* _label_wifi_rssi; lv_obj_t* _label_rns_status; + lv_obj_t* _label_prop_node; // BLE peer labels (pre-allocated, hidden when unused) lv_obj_t* _label_ble_header; @@ -149,6 +156,7 @@ private: RNS::Bytes _lxmf_address; bool _rns_connected; String _rns_server; + String _prop_node_display; // BLE peer data (cached for display) BLEPeerInfo _ble_peers[MAX_BLE_PEERS]; diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.cpp b/lib/tdeck_ui/UI/LXMF/UIManager.cpp index 3e3efaea..0ad45b48 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.cpp +++ b/lib/tdeck_ui/UI/LXMF/UIManager.cpp @@ -21,6 +21,7 @@ using namespace RNS; static const char* NVS_NAMESPACE = "propagation"; static const char* KEY_AUTO_SELECT = "auto_select"; static const char* KEY_NODE_HASH = "node_hash"; +static const char* KEY_STAMP_COST = "stamp_cost"; namespace UI { namespace LXMF { @@ -223,6 +224,28 @@ bool UIManager::init() { // Load settings from NVS _settings_screen->load_settings(); + // Restore propagation node selection from NVS + { + Preferences prefs; + prefs.begin(NVS_NAMESPACE, true); + bool auto_select = prefs.getBool(KEY_AUTO_SELECT, true); + uint8_t stamp_cost = prefs.getUChar(KEY_STAMP_COST, 0); + Bytes saved_hash; + size_t hash_len = prefs.getBytesLength(KEY_NODE_HASH); + if (hash_len > 0 && hash_len <= 32) { + uint8_t buf[32]; + prefs.getBytes(KEY_NODE_HASH, buf, hash_len); + saved_hash = Bytes(buf, hash_len); + } + prefs.end(); + + if (!auto_select && saved_hash.size() > 0) { + _router.set_outbound_propagation_node(saved_hash); + _router.set_outbound_propagation_stamp_cost(stamp_cost); + INFO(("Restored propagation node from NVS: " + saved_hash.toHex().substr(0, 16) + "...").c_str()); + } + } + // 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()); @@ -376,6 +399,50 @@ void UIManager::show_status() { LVGL_LOCK(); INFO("Showing status screen"); + // Build propagation node display string + if (_propagation_manager) { + Preferences prefs; + prefs.begin(NVS_NAMESPACE, true); + bool auto_select = prefs.getBool(KEY_AUTO_SELECT, true); + + Bytes saved_hash; + size_t hash_len = prefs.getBytesLength(KEY_NODE_HASH); + if (hash_len > 0 && hash_len <= 32) { + uint8_t buf[32]; + prefs.getBytes(KEY_NODE_HASH, buf, hash_len); + saved_hash = Bytes(buf, hash_len); + } + prefs.end(); + + Bytes effective = auto_select ? _propagation_manager->get_effective_node() : saved_hash; + + String display; + if (auto_select) { + if (effective.size() > 0) { + auto info = _propagation_manager->get_node(effective); + if (info && !info.name.empty()) { + display = "Auto (" + String(info.name.c_str()) + ")"; + } else { + display = "Auto (" + String(effective.toHex().substr(0, 12).c_str()) + "...)"; + } + } else { + display = "Auto"; + } + } else { + if (effective.size() > 0) { + auto info = _propagation_manager->get_node(effective); + if (info && !info.name.empty()) { + display = String(info.name.c_str()); + } else { + display = String(effective.toHex().substr(0, 12).c_str()) + "..."; + } + } else { + display = "None"; + } + } + _status_screen->set_propagation_node(display); + } + _status_screen->refresh(); _status_screen->show(); _conversation_list_screen->hide(); @@ -549,11 +616,28 @@ void UIManager::on_propagation_node_selected(const Bytes& node_hash) { // Set the node in the router _router.set_outbound_propagation_node(node_hash); + // Set stamp cost from node info if available + uint8_t stamp_cost = 0; + if (_propagation_manager) { + auto node_info = _propagation_manager->get_node(node_hash); + if (node_info) { + stamp_cost = node_info.stamp_cost; + } + } + _router.set_outbound_propagation_stamp_cost(stamp_cost); + + // Proactively request path if we don't have one + if (!Transport::has_path(node_hash)) { + DEBUG("Requesting path for propagation node"); + Transport::request_path(node_hash); + } + // Save to NVS Preferences prefs; prefs.begin(NVS_NAMESPACE, false); prefs.putBool(KEY_AUTO_SELECT, false); prefs.putBytes(KEY_NODE_HASH, node_hash.data(), node_hash.size()); + prefs.putUChar(KEY_STAMP_COST, stamp_cost); prefs.end(); DEBUG("Propagation node saved to NVS"); } @@ -573,7 +657,8 @@ void UIManager::on_propagation_auto_select_changed(bool enabled) { prefs.begin(NVS_NAMESPACE, false); prefs.putBool(KEY_AUTO_SELECT, enabled); if (enabled) { - prefs.remove(KEY_NODE_HASH); // Clear saved node when auto-select enabled + prefs.remove(KEY_NODE_HASH); + prefs.remove(KEY_STAMP_COST); } prefs.end(); DEBUG("Propagation auto-select saved to NVS"); diff --git a/src/main.cpp b/src/main.cpp index 9d0563d2..3cbad586 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -839,7 +839,6 @@ void setup_lxmf() { INFO("Propagation node manager registered"); // Configure propagation settings - router->set_propagation_node_manager(propagation_manager); router->set_fallback_to_propagation(app_settings.prop_fallback_enabled); router->set_propagation_only(app_settings.prop_only); if (!app_settings.prop_selected_node.isEmpty()) { @@ -1462,6 +1461,7 @@ void loop() { if (router) { router->process_outbound(); router->process_inbound(); + router->process_sync(); } // Update UI manager (processes LXMF messages)