From 00782336f35132bfffb6b44e6aa8527342ae988e Mon Sep 17 00:00:00 2001 From: DeFiDude <59237470+DeFiDude@users.noreply.github.com> Date: Mon, 4 May 2026 13:14:13 -0600 Subject: [PATCH] fix msg status, home button(s), active msg improvement --- src/config/UserConfig.cpp | 6 ++ src/config/UserConfig.h | 2 + src/main.cpp | 110 +++++++++++++++++++++++++- src/reticulum/ReticulumManager.cpp | 18 +++-- src/reticulum/ReticulumManager.h | 2 +- src/transport/LoRaInterface.h | 1 + src/ui/screens/LvHomeScreen.cpp | 115 ++++++++++++++++++++++++---- src/ui/screens/LvHomeScreen.h | 16 ++++ src/ui/screens/LvMessageView.cpp | 50 ++++++------ src/ui/screens/LvMessageView.h | 1 + src/ui/screens/LvMessagesScreen.cpp | 7 +- src/ui/screens/LvSettingsScreen.cpp | 47 ++++++++++-- src/ui/screens/LvSettingsScreen.h | 3 + 13 files changed, 317 insertions(+), 61 deletions(-) diff --git a/src/config/UserConfig.cpp b/src/config/UserConfig.cpp index 3a11390..9ac3aac 100644 --- a/src/config/UserConfig.cpp +++ b/src/config/UserConfig.cpp @@ -18,6 +18,7 @@ bool UserConfig::parseJson(const String& json) { _settings.loraCR = doc["lora_cr"] | (int)LORA_DEFAULT_CR; _settings.loraTxPower = doc["lora_txp"] | (int)LORA_DEFAULT_TX_POWER; _settings.loraPreamble = doc["lora_pre"] | (long)LORA_DEFAULT_PREAMBLE; + _settings.loraEnabled = doc["lora_on"] | true; // WiFi mode — migrate from legacy wifi_enabled bool int mode = doc["wifi_mode"] | -1; @@ -26,6 +27,9 @@ bool UserConfig::parseJson(const String& json) { } else { _settings.wifiMode = (doc["wifi_enabled"] | true) ? RAT_WIFI_AP : RAT_WIFI_OFF; } + int restoreMode = doc["wifi_restore_mode"] | (int)(_settings.wifiMode == RAT_WIFI_OFF ? RAT_WIFI_STA : _settings.wifiMode); + _settings.wifiRestoreMode = (RatWiFiMode)constrain(restoreMode, 1, 2); + if (_settings.wifiMode != RAT_WIFI_OFF) _settings.wifiRestoreMode = _settings.wifiMode; _settings.wifiAPSSID = doc["wifi_ap_ssid"] | ""; _settings.wifiAPPassword = doc["wifi_ap_pass"] | WIFI_AP_PASSWORD; _settings.wifiSTASelected = constrain((int)(doc["wifi_sta_selected"] | 0), 0, (int)WIFI_STA_MAX_NETWORKS - 1); @@ -111,8 +115,10 @@ String UserConfig::serializeToJson() const { doc["lora_cr"] = _settings.loraCR; doc["lora_txp"] = _settings.loraTxPower; doc["lora_pre"] = _settings.loraPreamble; + doc["lora_on"] = _settings.loraEnabled; doc["wifi_mode"] = (int)_settings.wifiMode; + doc["wifi_restore_mode"] = (int)_settings.wifiRestoreMode; doc["wifi_ap_ssid"] = _settings.wifiAPSSID; doc["wifi_ap_pass"] = _settings.wifiAPPassword; doc["wifi_sta_selected"] = (int)constrain((int)_settings.wifiSTASelected, 0, (int)WIFI_STA_MAX_NETWORKS - 1); diff --git a/src/config/UserConfig.h b/src/config/UserConfig.h index fcceee3..00ad06e 100644 --- a/src/config/UserConfig.h +++ b/src/config/UserConfig.h @@ -32,9 +32,11 @@ struct UserSettings { uint8_t loraCR = LORA_DEFAULT_CR; int8_t loraTxPower = LORA_DEFAULT_TX_POWER; long loraPreamble = LORA_DEFAULT_PREAMBLE; + bool loraEnabled = true; // WiFi RatWiFiMode wifiMode = RAT_WIFI_STA; + RatWiFiMode wifiRestoreMode = RAT_WIFI_STA; String wifiAPSSID; String wifiAPPassword = WIFI_AP_PASSWORD; std::vector wifiSTANetworks; diff --git a/src/main.cpp b/src/main.cpp index 43a6b20..e02d384 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -643,7 +643,7 @@ void setup() { lvBootScreen.setProgress(0.65f, "Starting Reticulum..."); // (LVGL boot renders via lv_timer_handler in setProgress) rns.setSDStore(&sdStore); - if (rns.begin(&radio, &flash)) { + if (rns.begin(&radio, &flash, userConfig.settings().loraEnabled)) { Serial.printf("[BOOT] Identity: %s\n", rns.identityHash().c_str()); lvBootScreen.setProgress(0.72f, "Reticulum active"); } else { @@ -720,7 +720,7 @@ void setup() { // (LVGL boot renders via lv_timer_handler in setProgress) // Step 21: Apply radio config - if (radioOnline) { + if (radioOnline && userConfig.settings().loraEnabled) { auto& s = userConfig.settings(); radio.setFrequency(s.loraFrequency); radio.setSpreadingFactor(s.loraSF); @@ -732,6 +732,11 @@ void setup() { Serial.printf("[BOOT] Radio: %lu Hz, SF%d, BW%lu, CR4/%d, %d dBm, pre=%ld\n", (unsigned long)s.loraFrequency, s.loraSF, (unsigned long)s.loraBW, s.loraCR, s.loraTxPower, s.loraPreamble); + ui.lvStatusBar().setLoRaOnline(true); + } else if (radioOnline) { + radio.sleep(); + ui.lvStatusBar().setLoRaOnline(false); + Serial.println("[BOOT] LoRa disabled by config"); } lvBootScreen.setProgress(0.84f, "Radio configured"); // (LVGL boot renders via lv_timer_handler in setProgress) @@ -894,6 +899,107 @@ void setup() { userConfig.settings().audioEnabled ? "ON" : "OFF", ok ? "OK" : "FAILED"); }); + lvHomeScreen.setLoraToggleCallback([]() { + auto& s = userConfig.settings(); + s.loraEnabled = !s.loraEnabled; + bool ok = userConfig.save(sdStore, flash); + ui.lvStatusBar().showToast( + ok ? "LoRa saved; reboot to apply" : "Save failed", + ok ? 3000 : 2000); + Serial.printf("[LORA] Saved %s (save %s, reboot required)\n", + s.loraEnabled ? "ON" : "OFF", + ok ? "OK" : "FAILED"); + }); + lvHomeScreen.setTCPToggleCallback([]() { + auto& s = userConfig.settings(); + bool enabled = false; + bool hasSavedRelay = false; + for (const auto& ep : s.tcpConnections) { + if (!ep.host.isEmpty()) hasSavedRelay = true; + if (!ep.host.isEmpty() && ep.autoConnect) { enabled = true; break; } + } + if (enabled) { + for (auto& ep : s.tcpConnections) ep.autoConnect = false; + } else if (hasSavedRelay) { + for (auto& ep : s.tcpConnections) { + if (!ep.host.isEmpty()) ep.autoConnect = true; + } + } else { + s.tcpConnections.clear(); + TCPEndpoint ep; + ep.host = "rns.ratspeak.org"; + ep.port = TCP_DEFAULT_PORT; + ep.autoConnect = true; + s.tcpConnections.push_back(ep); + } + bool ok = userConfig.save(sdStore, flash); + ui.lvStatusBar().showToast( + ok ? "TCP relay saved; reboot to apply" : "Save failed", + ok ? 3000 : 2000); + Serial.printf("[TCP] Saved relay %s (save %s, reboot required)\n", + enabled ? "OFF" : "ON", + ok ? "OK" : "FAILED"); + }); + lvHomeScreen.setWiFiToggleCallback([]() { + auto& s = userConfig.settings(); + if (s.wifiMode == RAT_WIFI_OFF) { + RatWiFiMode restoreMode = s.wifiRestoreMode == RAT_WIFI_OFF ? RAT_WIFI_STA : s.wifiRestoreMode; + if (restoreMode == RAT_WIFI_STA) { + size_t slot = s.wifiSTASelected < s.wifiSTANetworks.size() ? s.wifiSTASelected : 0; + if (slot >= s.wifiSTANetworks.size() || s.wifiSTANetworks[slot].ssid.isEmpty()) { + ui.lvStatusBar().showToast("Add WiFi in Settings", 2000); + return; + } + } else if (restoreMode != RAT_WIFI_AP) { + ui.lvStatusBar().showToast("Add WiFi in Settings", 2000); + return; + } + s.wifiMode = restoreMode; + } else { + s.wifiRestoreMode = s.wifiMode; + s.wifiMode = RAT_WIFI_OFF; + } + bool ok = userConfig.save(sdStore, flash); + ui.lvStatusBar().showToast( + ok ? "WiFi saved; reboot to apply" : "Save failed", + ok ? 3000 : 2000); + Serial.printf("[WIFI] Saved mode %d (save %s, reboot required)\n", + (int)s.wifiMode, ok ? "OK" : "FAILED"); + }); +#if HAS_GPS + lvHomeScreen.setGPSToggleCallback([]() { + auto& s = userConfig.settings(); + bool oldTime = s.gpsTimeEnabled; + s.gpsTimeEnabled = !s.gpsTimeEnabled; + bool ok = userConfig.save(sdStore, flash); + if (!ok) { + s.gpsTimeEnabled = oldTime; + ui.lvStatusBar().showToast("Save failed", 2000); + Serial.println("[GPS] Toggle save failed"); + return; + } + if (s.gpsTimeEnabled) { + gps.setPosixTZ(currentPosixTZ()); + gps.setLocationEnabled(s.gpsLocationEnabled); + gps.begin(); + ui.lvStatusBar().showToast("GPS ON", 1000); + Serial.println("[GPS] Enabled via Home"); + } else { + gps.stop(); + ui.lvStatusBar().setGPSFix(false); + ui.lvStatusBar().showToast("GPS OFF", 1000); + Serial.println("[GPS] Disabled via Home"); + } + }); +#else + lvHomeScreen.setGPSToggleCallback([]() { + ui.lvStatusBar().showToast("GPS unavailable", 1500); + }); +#endif + lvHomeScreen.setPeersCallback([]() { + ui.lvTabBar().setActiveTab(LvTabBar::TAB_NODES); + ui.setScreen(&lvNodesScreen); + }); lvContactsScreen.setAnnounceManager(announceManager); lvContactsScreen.setUIManager(&ui); diff --git a/src/reticulum/ReticulumManager.cpp b/src/reticulum/ReticulumManager.cpp index 30cfea2..5e973aa 100644 --- a/src/reticulum/ReticulumManager.cpp +++ b/src/reticulum/ReticulumManager.cpp @@ -175,7 +175,7 @@ std::list LittleFSFileSystem::list_directory(const char* p, Callbac size_t LittleFSFileSystem::storage_size() { return LittleFS.totalBytes(); } size_t LittleFSFileSystem::storage_available() { return LittleFS.totalBytes() - LittleFS.usedBytes(); } -bool ReticulumManager::begin(SX1262* radio, FlashStore* flash) { +bool ReticulumManager::begin(SX1262* radio, FlashStore* flash, bool loraEnabled) { _flash = flash; LittleFSFileSystem* fsImpl = new LittleFSFileSystem(); @@ -207,12 +207,16 @@ bool ReticulumManager::begin(SX1262* radio, FlashStore* flash) { } } - _loraImpl = new LoRaInterface(radio, "LoRa.915"); - _loraIface = _loraImpl; - _loraIface.mode(RNS::Type::Interface::MODE_GATEWAY); - RNS::Transport::register_interface(_loraIface); - if (!_loraImpl->start()) { - Serial.println("[RNS] WARNING: LoRa interface failed to start"); + if (loraEnabled) { + _loraImpl = new LoRaInterface(radio, "LoRa.915"); + _loraIface = _loraImpl; + _loraIface.mode(RNS::Type::Interface::MODE_GATEWAY); + RNS::Transport::register_interface(_loraIface); + if (!_loraImpl->start()) { + Serial.println("[RNS] WARNING: LoRa interface failed to start"); + } + } else { + Serial.println("[RNS] LoRa interface disabled by config"); } _reticulum = RNS::Reticulum(); diff --git a/src/reticulum/ReticulumManager.h b/src/reticulum/ReticulumManager.h index fd8a310..780a117 100644 --- a/src/reticulum/ReticulumManager.h +++ b/src/reticulum/ReticulumManager.h @@ -36,7 +36,7 @@ public: _destination({RNS::Type::NONE}), _loraIface({RNS::Type::NONE}) {} - bool begin(SX1262* radio, FlashStore* flash); + bool begin(SX1262* radio, FlashStore* flash, bool loraEnabled = true); void setSDStore(SDStore* sd) { _sd = sd; } void loop(); void persistData(); diff --git a/src/transport/LoRaInterface.h b/src/transport/LoRaInterface.h index 032700f..0211962 100644 --- a/src/transport/LoRaInterface.h +++ b/src/transport/LoRaInterface.h @@ -22,6 +22,7 @@ public: // Last received packet signal quality int lastRxRssi() const { return _lastRxRssi; } float lastRxSnr() const { return _lastRxSnr; } + bool isOnline() const { return _online; } protected: virtual void send_outgoing(const RNS::Bytes& data) override; diff --git a/src/ui/screens/LvHomeScreen.cpp b/src/ui/screens/LvHomeScreen.cpp index a919d30..e78eb9f 100644 --- a/src/ui/screens/LvHomeScreen.cpp +++ b/src/ui/screens/LvHomeScreen.cpp @@ -114,6 +114,20 @@ void setChipState(lv_obj_t* chip, lv_obj_t* label, const char* text, bool active } } +void makeClickable(lv_obj_t* obj, void (*cb)(lv_event_t*), void* userData) { + if (!obj) return; + lv_obj_add_flag(obj, LV_OBJ_FLAG_CLICKABLE); + lv_group_add_obj(LvInput::group(), obj); + lv_obj_add_event_cb(obj, cb, LV_EVENT_CLICKED, userData); +} + +bool tcpConfigured(const UserSettings& settings) { + for (const auto& ep : settings.tcpConnections) { + if (!ep.host.isEmpty() && ep.autoConnect) return true; + } + return false; +} + void formatAge(unsigned long elapsedMs, char* out, size_t len) { unsigned long sec = elapsedMs / 1000; if (sec < 60) { @@ -178,19 +192,37 @@ void LvHomeScreen::createUI(lv_obj_t* parent) { "ID ----", LV_TEXT_ALIGN_RIGHT); _chipLora = makeChip(parent, kPad, "LORA", &_lblLoraState); + makeClickable(_chipLora, [](lv_event_t* e) { + auto* self = (LvHomeScreen*)lv_event_get_user_data(e); + self->toggleLora(); + }, this); _chipTcp = makeChip(parent, kPad + kCellW + kGap, "TCP", &_lblTcpState); + makeClickable(_chipTcp, [](lv_event_t* e) { + auto* self = (LvHomeScreen*)lv_event_get_user_data(e); + self->toggleTcp(); + }, this); _chipWifi = makeChip(parent, kPad + (kCellW + kGap) * 2, "WIFI", &_lblWifiState); + makeClickable(_chipWifi, [](lv_event_t* e) { + auto* self = (LvHomeScreen*)lv_event_get_user_data(e); + self->toggleWiFi(); + }, this); _statNodes = makeStat(parent, kPad, "PEERS", &_lblNodes); + makeClickable(_statNodes, [](lv_event_t* e) { + auto* self = (LvHomeScreen*)lv_event_get_user_data(e); + self->openPeers(); + }, this); _statPaths = makeStat(parent, kPad + kCellW + kGap, "AUDIO", &_lblPaths); - lv_obj_add_flag(_statPaths, LV_OBJ_FLAG_CLICKABLE); - lv_group_add_obj(LvInput::group(), _statPaths); - lv_obj_add_event_cb(_statPaths, [](lv_event_t* e) { + makeClickable(_statPaths, [](lv_event_t* e) { auto* self = (LvHomeScreen*)lv_event_get_user_data(e); self->toggleAudio(); - }, LV_EVENT_CLICKED, this); + }, this); _statLinks = makeStat(parent, kPad + (kCellW + kGap) * 2, "GPS", &_lblLinks); + makeClickable(_statLinks, [](lv_event_t* e) { + auto* self = (LvHomeScreen*)lv_event_get_user_data(e); + self->toggleGPS(); + }, this); lv_obj_t* footer = makePanel(parent, kPad, kFooterY, 198, kFooterH, Theme::BG_ELEVATED, Theme::BORDER); @@ -280,23 +312,31 @@ void LvHomeScreen::refreshUI() { } renderAvatar(avatarSeed); - bool loraUp = _radioOnline && _radio && _radio->isRadioOnline(); + auto* loraIf = _rns ? _rns->loraInterface() : nullptr; + bool loraRuntimeUp = _radioOnline && _radio && _radio->isRadioOnline() + && loraIf && loraIf->isOnline(); + bool loraEnabled = _cfg ? _cfg->settings().loraEnabled : loraRuntimeUp; bool tcpUp = false; - int tcpConfigured = 0; + int tcpClientCount = 0; if (_tcpClients) { for (auto* tcp : *_tcpClients) { - if (tcp) tcpConfigured++; + if (tcp) tcpClientCount++; if (tcp && tcp->isConnected()) { tcpUp = true; break; } } } bool wifiUp = WiFi.status() == WL_CONNECTED; + bool wifiEnabled = _cfg ? _cfg->settings().wifiMode != RAT_WIFI_OFF : wifiUp; + bool wifiRuntimeActive = wifiUp || (_cfg && _cfg->settings().wifiMode == RAT_WIFI_AP); + bool tcpEnabled = _cfg ? tcpConfigured(_cfg->settings()) : tcpClientCount > 0; bool transportActive = _rns && _rns->isTransportActive(); - bool reachable = loraUp || tcpUp; + bool reachable = loraRuntimeUp || tcpUp; - setChipState(_chipLora, _lblLoraState, loraUp ? "ON" : "OFF", loraUp); - setChipState(_chipTcp, _lblTcpState, tcpUp ? "ON" : "OFF", - tcpUp, tcpConfigured > 0 && wifiUp); - setChipState(_chipWifi, _lblWifiState, wifiUp ? "ON" : "OFF", wifiUp); + setChipState(_chipLora, _lblLoraState, loraEnabled ? "ON" : "OFF", + loraEnabled && loraRuntimeUp, loraEnabled != loraRuntimeUp); + setChipState(_chipTcp, _lblTcpState, tcpEnabled ? "ON" : "OFF", + tcpUp, tcpEnabled && !tcpUp); + setChipState(_chipWifi, _lblWifiState, wifiEnabled ? "ON" : "OFF", + wifiRuntimeActive, wifiEnabled && !wifiRuntimeActive); if (reachable) { lv_label_set_text(_lblStatus, "ONLINE"); @@ -330,7 +370,7 @@ void LvHomeScreen::refreshUI() { audioOn ? Theme::PRIMARY : Theme::BORDER); #if HAS_GPS - bool gpsOn = _cfg && (_cfg->settings().gpsTimeEnabled || _cfg->settings().gpsLocationEnabled); + bool gpsOn = _cfg && _cfg->settings().gpsTimeEnabled; #else bool gpsOn = false; #endif @@ -376,6 +416,26 @@ bool LvHomeScreen::handleKey(const KeyEvent& event) { toggleAudio(); return true; } + if (focused == _chipLora) { + toggleLora(); + return true; + } + if (focused == _chipTcp) { + toggleTcp(); + return true; + } + if (focused == _chipWifi) { + toggleWiFi(); + return true; + } + if (focused == _statLinks) { + toggleGPS(); + return true; + } + if (focused == _statNodes) { + openPeers(); + return true; + } if (_announceCb) _announceCb(); return true; } @@ -384,8 +444,37 @@ bool LvHomeScreen::handleKey(const KeyEvent& event) { void LvHomeScreen::toggleAudio() { if (_audioToggleCb) _audioToggleCb(); + forceRefresh(); +} + +void LvHomeScreen::toggleLora() { + if (_loraToggleCb) _loraToggleCb(); + forceRefresh(); +} + +void LvHomeScreen::toggleTcp() { + if (_tcpToggleCb) _tcpToggleCb(); + forceRefresh(); +} + +void LvHomeScreen::toggleWiFi() { + if (_wifiToggleCb) _wifiToggleCb(); + forceRefresh(); +} + +void LvHomeScreen::toggleGPS() { + if (_gpsToggleCb) _gpsToggleCb(); + forceRefresh(); +} + +void LvHomeScreen::openPeers() { + if (_peersCb) _peersCb(); +} + +void LvHomeScreen::forceRefresh() { _lastRefreshMs = 0; _lastUptime = ULONG_MAX; + _lastHeap = UINT32_MAX; refreshUI(); } diff --git a/src/ui/screens/LvHomeScreen.h b/src/ui/screens/LvHomeScreen.h index e5e8777..1ac9a58 100644 --- a/src/ui/screens/LvHomeScreen.h +++ b/src/ui/screens/LvHomeScreen.h @@ -28,6 +28,11 @@ public: void setTCPClients(std::vector* clients) { _tcpClients = clients; } void setAnnounceCallback(std::function cb) { _announceCb = cb; } void setAudioToggleCallback(std::function cb) { _audioToggleCb = cb; } + void setLoraToggleCallback(std::function cb) { _loraToggleCb = cb; } + void setTCPToggleCallback(std::function cb) { _tcpToggleCb = cb; } + void setWiFiToggleCallback(std::function cb) { _wifiToggleCb = cb; } + void setGPSToggleCallback(std::function cb) { _gpsToggleCb = cb; } + void setPeersCallback(std::function cb) { _peersCb = cb; } const char* title() const override { return "Home"; } @@ -41,6 +46,11 @@ private: bool _radioOnline = false; std::function _announceCb; std::function _audioToggleCb; + std::function _loraToggleCb; + std::function _tcpToggleCb; + std::function _wifiToggleCb; + std::function _gpsToggleCb; + std::function _peersCb; unsigned long _lastRefreshMs = 0; unsigned long _lastUptime = 0; uint32_t _lastHeap = 0; @@ -74,5 +84,11 @@ private: lv_obj_t* _lblAnnounceAction = nullptr; void toggleAudio(); + void toggleLora(); + void toggleTcp(); + void toggleWiFi(); + void toggleGPS(); + void openPeers(); + void forceRefresh(); void renderAvatar(const String& seed); }; diff --git a/src/ui/screens/LvMessageView.cpp b/src/ui/screens/LvMessageView.cpp index 57468b6..6578741 100644 --- a/src/ui/screens/LvMessageView.cpp +++ b/src/ui/screens/LvMessageView.cpp @@ -66,11 +66,6 @@ int textWidthForBubble(const std::string& content) { return width; } -std::string shortHex(const std::string& peerHex) { - if (peerHex.size() <= 12) return peerHex; - return peerHex.substr(0, 6) + ".." + peerHex.substr(peerHex.size() - 4); -} - void makeTransparent(lv_obj_t* obj) { lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(obj, 0, 0); @@ -96,31 +91,17 @@ void LvMessageView::updateHeader() { lv_label_set_text(_lblHeader, name.c_str()); const DiscoveredNode* node = _am ? _am->findNodeByHex(_peerHex) : nullptr; - const char* identity = "unknown"; const char* state = "UNKNOWN"; uint32_t stateColor = Theme::TEXT_MUTED; - char meta[72]; if (node) { bool fresh = node->lastSeen != 0 && (millis() - node->lastSeen) <= kFreshNodeMs; - identity = node->saved ? "friend" : "heard"; state = fresh ? "ONLINE" : (node->saved ? "FRIEND" : "STALE"); stateColor = fresh ? Theme::SUCCESS : (node->saved ? Theme::PRIMARY : Theme::TEXT_SECONDARY); - if (node->rssi != 0 || node->snr != 0.0f) { - snprintf(meta, sizeof(meta), "%s | %s | %ddBm %.1fdB %uH", - identity, shortHex(_peerHex).c_str(), node->rssi, node->snr, (unsigned)node->hops); - } else if (node->hops > 0) { - snprintf(meta, sizeof(meta), "%s | %s | %uH", - identity, shortHex(_peerHex).c_str(), (unsigned)node->hops); - } else { - snprintf(meta, sizeof(meta), "%s | %s", identity, shortHex(_peerHex).c_str()); - } - } else { - snprintf(meta, sizeof(meta), "%s | %s", identity, shortHex(_peerHex).c_str()); } if (_lblHeaderMeta) { - lv_label_set_text(_lblHeaderMeta, meta); + lv_label_set_text(_lblHeaderMeta, _peerHex.c_str()); } if (_lblHeaderState) { lv_label_set_text(_lblHeaderState, state); @@ -144,6 +125,23 @@ void LvMessageView::refreshComposerPlaceholder() { bool focused = lv_obj_has_state(_textarea, LV_STATE_FOCUSED); lv_textarea_set_placeholder_text(_textarea, (_inputText.empty() && !focused) ? kComposerPlaceholder : ""); + updateComposerText(); +} + +void LvMessageView::updateComposerText() { + if (!_textarea) return; + + bool focused = lv_obj_has_state(_textarea, LV_STATE_FOCUSED); + bool showCaret = focused || !_inputText.empty(); + if (!showCaret) { + lv_textarea_set_text(_textarea, ""); + return; + } + + std::string display = _inputText; + display += "_"; + lv_textarea_set_text(_textarea, display.c_str()); + lv_textarea_set_cursor_pos(_textarea, LV_TEXTAREA_CURSOR_LAST); } void LvMessageView::createUI(lv_obj_t* parent) { @@ -188,7 +186,7 @@ void LvMessageView::createUI(lv_obj_t* parent) { lv_obj_set_style_text_font(_lblHeaderMeta, &lv_font_ratdeck_10, 0); lv_obj_set_style_text_color(_lblHeaderMeta, lv_color_hex(Theme::TEXT_SECONDARY), 0); lv_label_set_long_mode(_lblHeaderMeta, LV_LABEL_LONG_DOT); - lv_obj_set_width(_lblHeaderMeta, 214); + lv_obj_set_width(_lblHeaderMeta, Theme::CONTENT_W - 30); lv_obj_set_pos(_lblHeaderMeta, 22, 21); _lblHeaderState = lv_label_create(_header); @@ -234,7 +232,7 @@ void LvMessageView::createUI(lv_obj_t* parent) { lv_obj_set_size(_textarea, Theme::CONTENT_W - kComposerButtonW - 12, 23); lv_obj_align(_textarea, LV_ALIGN_LEFT_MID, 0, 0); lv_textarea_set_one_line(_textarea, true); - lv_textarea_set_max_length(_textarea, MAX_COMPOSER_CHARS); + lv_textarea_set_max_length(_textarea, MAX_COMPOSER_CHARS + 1); lv_textarea_set_placeholder_text(_textarea, kComposerPlaceholder); lv_obj_add_style(_textarea, LvTheme::styleTextarea(), 0); lv_obj_add_style(_textarea, LvTheme::styleTextareaFocused(), LV_STATE_FOCUSED); @@ -245,7 +243,7 @@ void LvMessageView::createUI(lv_obj_t* parent) { auto* self = (LvMessageView*)lv_event_get_user_data(e); lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_FOCUSED || code == LV_EVENT_CLICKED || code == LV_EVENT_PRESSED) { - lv_textarea_set_placeholder_text(self->_textarea, ""); + self->refreshComposerPlaceholder(); } else if (code == LV_EVENT_DEFOCUSED) { self->refreshComposerPlaceholder(); } @@ -326,8 +324,7 @@ void LvMessageView::onEnter() { hideSendModeMenu(); if (_textarea) { - lv_textarea_set_text(_textarea, ""); - refreshComposerPlaceholder(); + updateComposerText(); } updateHeader(); updateComposerState(); @@ -629,7 +626,6 @@ void LvMessageView::sendCurrentMessage(bool viaLink) { if (viaLink && _ui) _ui->lvStatusBar().showToast("Link send queued", 1200); _inputText.clear(); - if (_textarea) lv_textarea_set_text(_textarea, ""); updateComposerState(); _cachedMsgs.clear(); // Force fresh load in rebuildMessages rebuildMessages(); @@ -666,7 +662,6 @@ bool LvMessageView::handleKey(const KeyEvent& event) { if (event.del || event.character == 0x08) { if (!_inputText.empty()) { _inputText.pop_back(); - if (_textarea) lv_textarea_set_text(_textarea, _inputText.c_str()); updateComposerState(); } else { if (_onBack) _onBack(); @@ -703,7 +698,6 @@ bool LvMessageView::handleKey(const KeyEvent& event) { return true; } _inputText += (char)event.character; - if (_textarea) lv_textarea_set_text(_textarea, _inputText.c_str()); updateComposerState(); return true; } diff --git a/src/ui/screens/LvMessageView.h b/src/ui/screens/LvMessageView.h index 502a8e6..dcbc574 100644 --- a/src/ui/screens/LvMessageView.h +++ b/src/ui/screens/LvMessageView.h @@ -37,6 +37,7 @@ private: void updateHeader(); void updateComposerState(); void refreshComposerPlaceholder(); + void updateComposerText(); void showSendModeMenu(); void hideSendModeMenu(); void updateSendModeMenu(); diff --git a/src/ui/screens/LvMessagesScreen.cpp b/src/ui/screens/LvMessagesScreen.cpp index da240ac..fdbead4 100644 --- a/src/ui/screens/LvMessagesScreen.cpp +++ b/src/ui/screens/LvMessagesScreen.cpp @@ -246,6 +246,7 @@ void LvMessagesScreen::rebuildList() { for (int i = 0; i < count; i++) { const auto& ci = _sortedConvs[i]; + bool latestOutgoingFailed = ci.hasOutgoing && ci.lastOutgoingStatus == LXMFStatus::FAILED; lv_obj_t* row = lv_obj_create(_list); lv_obj_set_size(row, Theme::CONTENT_W, kRowH); @@ -280,7 +281,7 @@ void LvMessagesScreen::rebuildList() { }, LV_EVENT_FOCUSED, nullptr); uint32_t railColor = Theme::BORDER; - if (ci.hasFailed) railColor = Theme::ERROR_CLR; + if (latestOutgoingFailed) railColor = Theme::ERROR_CLR; else if (ci.hasPending) railColor = Theme::WARNING_CLR; else if (ci.hasUnread) railColor = Theme::PRIMARY; else if (ci.savedNode) railColor = Theme::TEXT_SECONDARY; @@ -328,7 +329,7 @@ void LvMessagesScreen::rebuildList() { lv_obj_t* prevLbl = lv_label_create(row); lv_obj_set_style_text_font(prevLbl, smallFont, 0); uint32_t previewColor = Theme::TEXT_SECONDARY; - if (ci.hasFailed) previewColor = Theme::ERROR_CLR; + if (latestOutgoingFailed) previewColor = Theme::ERROR_CLR; else if (ci.hasPending) previewColor = Theme::WARNING_CLR; else if (ci.hasUnread) previewColor = Theme::TEXT_PRIMARY; lv_obj_set_style_text_color(prevLbl, lv_color_hex(previewColor), 0); @@ -339,7 +340,7 @@ void LvMessagesScreen::rebuildList() { char chipBuf[16] = {0}; uint32_t chipColor = Theme::TEXT_MUTED; - if (ci.hasFailed) { + if (latestOutgoingFailed) { snprintf(chipBuf, sizeof(chipBuf), "FAILED"); chipColor = Theme::ERROR_CLR; } else if (ci.hasPending) { diff --git a/src/ui/screens/LvSettingsScreen.cpp b/src/ui/screens/LvSettingsScreen.cpp index 3bfd60d..883c0c2 100644 --- a/src/ui/screens/LvSettingsScreen.cpp +++ b/src/ui/screens/LvSettingsScreen.cpp @@ -175,6 +175,7 @@ bool LvSettingsScreen::settingNeedsReboot(const SettingItem& item) const { if (!_cfg) return false; const auto& s = _cfg->settings(); if (labelEq(item.label, "WiFi Mode")) return s.wifiMode != _rebootSnap.wifiMode; + if (labelEq(item.label, "LoRa Radio")) return loraSettingsChanged(); if (labelEq(item.label, "Active WiFi")) return s.wifiSTASelected != _rebootSnap.wifiSTASelected; if (isWiFiSSIDLabel(item.label) || isWiFiPasswordLabel(item.label)) return interfaceSettingsChanged(); if (labelEq(item.label, "WiFi Scan") || labelEq(item.label, "Forget WiFi")) return interfaceSettingsChanged(); @@ -187,6 +188,9 @@ bool LvSettingsScreen::settingNeedsReboot(const SettingItem& item) const { bool LvSettingsScreen::categoryNeedsReboot(int catIdx) const { if (catIdx < 0 || catIdx >= (int)_categories.size()) return false; + if (labelEq(_categories[catIdx].name, "LoRa")) { + return loraSettingsChanged(); + } if (labelEq(_categories[catIdx].name, "Interfaces")) { return interfaceSettingsChanged() || tcpSettingsChanged(); } @@ -512,6 +516,11 @@ void LvSettingsScreen::buildItems() { // LoRa link int radioStart = idx; + _items.push_back({"LoRa Radio", SettingType::TOGGLE, + [&s]() { return s.loraEnabled ? 1 : 0; }, + [&s](int v) { s.loraEnabled = (v != 0); }, + [](int v) { return String(onOff(v != 0)); }}); + idx++; { // Region picker - always visible SettingItem regionItem; @@ -627,8 +636,12 @@ void LvSettingsScreen::buildItems() { } _categories.push_back({"LoRa", radioStart, idx - radioStart, [this]() { + if (loraSettingsChanged()) { + return String("Saved - reboot to apply"); + } int p = detectPreset(); auto& s = _cfg->settings(); + if (!s.loraEnabled) return String("Off"); String label = (p >= 0) ? String(LV_PRESETS[p].name) : String("Custom"); label += " "; label += REGION_LABELS[constrain(s.radioRegion, 0, REGION_COUNT - 1)]; @@ -640,7 +653,10 @@ void LvSettingsScreen::buildItems() { int netStart = idx; _items.push_back({"WiFi Mode", SettingType::ENUM_CHOICE, [&s]() { return (int)s.wifiMode; }, - [&s](int v) { s.wifiMode = (RatWiFiMode)v; }, + [&s](int v) { + s.wifiMode = (RatWiFiMode)v; + if (s.wifiMode != RAT_WIFI_OFF) s.wifiRestoreMode = s.wifiMode; + }, nullptr, 0, 2, 1, {"Off", "Hotspot", "Client"}}); idx++; _items.push_back({"Active WiFi", SettingType::INTEGER, @@ -725,16 +741,24 @@ void LvSettingsScreen::buildItems() { tcpPreset.label = "TCP Relay"; tcpPreset.type = SettingType::ENUM_CHOICE; tcpPreset.getter = [&s]() { - for (auto& ep : s.tcpConnections) { if (ep.host == "rns.ratspeak.org") return 1; } - if (!s.tcpConnections.empty()) return 2; + for (auto& ep : s.tcpConnections) { + if (!ep.autoConnect || ep.host.isEmpty()) continue; + if (ep.host == "rns.ratspeak.org") return 1; + return 2; + } return 0; }; tcpPreset.setter = [&s](int v) { - if (v == 0) { s.tcpConnections.clear(); } + if (v == 0) { + for (auto& ep : s.tcpConnections) ep.autoConnect = false; + } else if (v == 1) { s.tcpConnections.clear(); TCPEndpoint ep; ep.host = "rns.ratspeak.org"; ep.port = TCP_DEFAULT_PORT; ep.autoConnect = true; s.tcpConnections.push_back(ep); + } else if (v == 2 && s.tcpConnections.empty()) { + TCPEndpoint ep; ep.port = TCP_DEFAULT_PORT; ep.autoConnect = false; + s.tcpConnections.push_back(ep); } }; tcpPreset.minVal = 0; tcpPreset.maxVal = 2; tcpPreset.step = 1; @@ -774,7 +798,7 @@ void LvSettingsScreen::buildItems() { idx++; _categories.push_back({"Interfaces", netStart, idx - netStart, [this, &s]() { - if (rebootSettingsChanged()) return String("Saved - reboot to apply"); + if (interfaceSettingsChanged() || tcpSettingsChanged()) return String("Saved - reboot to apply"); String summary = wifiModeLabel(s.wifiMode); if (s.wifiMode == RAT_WIFI_STA) { String ssid = WiFi.status() == WL_CONNECTED ? WiFi.SSID() : selectedWiFiSSID(s); @@ -1896,11 +1920,18 @@ void LvSettingsScreen::snapshotRebootSettings() { _rebootSnap.wifiSTASelected = s.wifiSTASelected; _rebootSnap.autoIfaceEnabled = s.autoIfaceEnabled; _rebootSnap.sdStorageEnabled = s.sdStorageEnabled; + _rebootSnap.loraEnabled = s.loraEnabled; _gpsSnapEnabled = s.gpsTimeEnabled; } bool LvSettingsScreen::rebootSettingsChanged() const { - return interfaceSettingsChanged() || storageSettingsChanged() || tcpSettingsChanged(); + return loraSettingsChanged() || interfaceSettingsChanged() + || storageSettingsChanged() || tcpSettingsChanged(); +} + +bool LvSettingsScreen::loraSettingsChanged() const { + if (!_cfg) return false; + return _cfg->settings().loraEnabled != _rebootSnap.loraEnabled; } bool LvSettingsScreen::interfaceSettingsChanged() const { @@ -1927,6 +1958,7 @@ void LvSettingsScreen::snapshotTCPSettings() { auto& s = _cfg->settings(); _tcpSnapHost = s.tcpConnections.empty() ? "" : s.tcpConnections[0].host; _tcpSnapPort = s.tcpConnections.empty() ? 0 : s.tcpConnections[0].port; + _tcpSnapAuto = !s.tcpConnections.empty() && s.tcpConnections[0].autoConnect; } bool LvSettingsScreen::tcpSettingsChanged() const { @@ -1934,7 +1966,8 @@ bool LvSettingsScreen::tcpSettingsChanged() const { auto& s = _cfg->settings(); String curHost = s.tcpConnections.empty() ? "" : s.tcpConnections[0].host; uint16_t curPort = s.tcpConnections.empty() ? 0 : s.tcpConnections[0].port; - return curHost != _tcpSnapHost || curPort != _tcpSnapPort; + bool curAuto = !s.tcpConnections.empty() && s.tcpConnections[0].autoConnect; + return curHost != _tcpSnapHost || curPort != _tcpSnapPort || curAuto != _tcpSnapAuto; } // --- Frequency digit-cursor editor helpers --- diff --git a/src/ui/screens/LvSettingsScreen.h b/src/ui/screens/LvSettingsScreen.h index 22dfb8b..f1ba549 100644 --- a/src/ui/screens/LvSettingsScreen.h +++ b/src/ui/screens/LvSettingsScreen.h @@ -191,9 +191,11 @@ private: uint8_t wifiSTASelected; bool autoIfaceEnabled; bool sdStorageEnabled; + bool loraEnabled; }; RebootSnapshot _rebootSnap; void snapshotRebootSettings(); + bool loraSettingsChanged() const; bool interfaceSettingsChanged() const; bool storageSettingsChanged() const; bool rebootSettingsChanged() const; @@ -201,6 +203,7 @@ private: // TCP change detection String _tcpSnapHost; uint16_t _tcpSnapPort = 0; + bool _tcpSnapAuto = false; void snapshotTCPSettings(); bool tcpSettingsChanged() const;