diff --git a/src/config/Config.h b/src/config/Config.h index a721b06..bd69ab5 100644 --- a/src/config/Config.h +++ b/src/config/Config.h @@ -6,8 +6,8 @@ #define RATDECK_VERSION_MAJOR 1 #define RATDECK_VERSION_MINOR 5 -#define RATDECK_VERSION_PATCH 8 -#define RATDECK_VERSION_STRING "1.5.8" +#define RATDECK_VERSION_PATCH 9 +#define RATDECK_VERSION_STRING "1.5.9" // --- Feature Flags --- #define HAS_DISPLAY true @@ -47,7 +47,7 @@ #define TCP_CONNECT_TIMEOUT_MS 5000 // --- Announce Flood Defense --- -#define RATDECK_MAX_ANNOUNCES_PER_SEC 5 // Transport-level rate limit (before Ed25519 verify) +#define RATDECK_MAX_ANNOUNCES_PER_SEC 2 // Transport-level rate limit (before Ed25519 verify) // --- Limits --- #define RATDECK_MAX_NODES 200 // PSRAM allows more diff --git a/src/main.cpp b/src/main.cpp index 946bc53..5904b49 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -120,6 +120,11 @@ unsigned long lastHeartbeat = 0; constexpr unsigned long HEARTBEAT_INTERVAL_MS = 5000; unsigned long loopCycleStart = 0; unsigned long maxLoopTime = 0; +unsigned long lastLvglTime = 0; +constexpr unsigned long LVGL_INTERVAL_MS = 33; // ~30 FPS +constexpr unsigned long TCP_GLOBAL_BUDGET_MS = 12; // Max cumulative TCP time per loop +bool wifiDeferredAnnounce = false; +unsigned long wifiConnectedAt = 0; // ============================================================================= // Announce with display name (MessagePack-encoded app_data) @@ -888,16 +893,20 @@ void loop() { } } - // 3. LVGL timer handler — run FIRST after input for responsive UI - if (powerMgr.isScreenOn()) { - lv_timer_handler(); + // 3. LVGL timer handler — throttled to ~30 FPS + { + unsigned long now = millis(); + if (powerMgr.isScreenOn() && now - lastLvglTime >= LVGL_INTERVAL_MS) { + lastLvglTime = now; + lv_timer_handler(); + } } - // 4. Reticulum loop (radio RX via LoRaInterface) — throttle to ~200Hz + // 4. Reticulum loop (radio RX via LoRaInterface) — throttle to ~100Hz { static unsigned long lastRNS = 0; unsigned long now = millis(); - if (now - lastRNS >= 5) { + if (now - lastRNS >= 10) { lastRNS = now; rns.loop(); } @@ -932,20 +941,9 @@ void loop() { // Recreate TCP clients on every WiFi connect (old clients may have stale sockets) reloadTCPClients(); - // Announce over TCP now that it's available — delay to let VPS register the connection - bool anyTcpConnected = false; - for (auto* tcp : tcpClients) { - if (tcp->isConnected()) { anyTcpConnected = true; break; } - } - if (anyTcpConnected) { - delay(1500); // Let VPS Reticulum register the new interface - Serial.println("[TCP] Sending announce over new TCP connection..."); - RNS::Bytes appData = encodeAnnounceName(userConfig.settings().displayName); - rns.announce(appData); - lastAutoAnnounce = millis(); - } else { - Serial.println("[TCP] No TCP clients connected, skipping announce"); - } + // Defer announce to let VPS register the connection (non-blocking) + wifiDeferredAnnounce = true; + wifiConnectedAt = millis(); } else if (!connected && wifiSTAConnected) { wifiSTAConnected = false; ui.statusBar().setWiFiActive(false); @@ -962,11 +960,32 @@ void loop() { } } - // 8. WiFi + TCP loops + // 7.5. Deferred WiFi announce (non-blocking replacement for delay(1500)) + if (wifiDeferredAnnounce && millis() - wifiConnectedAt >= 1500) { + wifiDeferredAnnounce = false; + bool anyTcpConnected = false; + for (auto* tcp : tcpClients) { + if (tcp->isConnected()) { anyTcpConnected = true; break; } + } + if (anyTcpConnected) { + Serial.println("[TCP] Sending announce over new TCP connection..."); + RNS::Bytes appData = encodeAnnounceName(userConfig.settings().displayName); + rns.announce(appData); + lastAutoAnnounce = millis(); + } else { + Serial.println("[TCP] No TCP clients connected, skipping announce"); + } + } + + // 8. WiFi + TCP loops (with global budget) if (wifiImpl) wifiImpl->loop(); - for (auto* tcp : tcpClients) { - tcp->loop(); - yield(); + { + unsigned long tcpBudgetStart = millis(); + for (auto* tcp : tcpClients) { + if (millis() - tcpBudgetStart >= TCP_GLOBAL_BUDGET_MS) break; + tcp->loop(); + yield(); + } } // 9. BLE loops diff --git a/src/reticulum/ReticulumManager.cpp b/src/reticulum/ReticulumManager.cpp index 37555d6..4fdc7d4 100644 --- a/src/reticulum/ReticulumManager.cpp +++ b/src/reticulum/ReticulumManager.cpp @@ -232,28 +232,38 @@ void ReticulumManager::loop() { } void ReticulumManager::persistData() { - RNS::Transport::persist_data(); - RNS::Identity::persist_data(); - // Backup routing tables and known destinations to SD - if (_sd && _sd->isReady()) { - static const char* files[] = {"/destination_table", "/packet_hashlist", "/known_destinations"}; - for (const char* name : files) { - File f = LittleFS.open(name, "r"); - if (f && f.size() > 0) { - size_t sz = f.size(); - uint8_t* buf = (uint8_t*)malloc(sz); - if (buf) { - f.readBytes((char*)buf, sz); - char sdPath[64]; - snprintf(sdPath, sizeof(sdPath), "/ratputer/transport%s", name); - _sd->ensureDir("/ratputer/transport"); - _sd->writeSimple(sdPath, buf, sz); - free(buf); + // Rotate through persist steps to spread file I/O across cycles + switch (_persistCycle) { + case 0: + RNS::Transport::persist_data(); + break; + case 1: + RNS::Identity::persist_data(); + break; + case 2: + // Backup routing tables and known destinations to SD + if (_sd && _sd->isReady()) { + static const char* files[] = {"/destination_table", "/packet_hashlist", "/known_destinations"}; + for (const char* name : files) { + File f = LittleFS.open(name, "r"); + if (f && f.size() > 0) { + size_t sz = f.size(); + uint8_t* buf = (uint8_t*)malloc(sz); + if (buf) { + f.readBytes((char*)buf, sz); + char sdPath[64]; + snprintf(sdPath, sizeof(sdPath), "/ratputer/transport%s", name); + _sd->ensureDir("/ratputer/transport"); + _sd->writeSimple(sdPath, buf, sz); + free(buf); + } + } + if (f) f.close(); } } - if (f) f.close(); - } + break; } + _persistCycle = (_persistCycle + 1) % 3; } String ReticulumManager::identityHash() const { diff --git a/src/reticulum/ReticulumManager.h b/src/reticulum/ReticulumManager.h index dd25c4b..f5b9f6e 100644 --- a/src/reticulum/ReticulumManager.h +++ b/src/reticulum/ReticulumManager.h @@ -73,4 +73,5 @@ private: bool _transportActive = false; unsigned long _lastPersist = 0; unsigned long _lastAnnounceTime = 0; + uint8_t _persistCycle = 0; // Rotating: 0=Transport, 1=Identity, 2=SD backup }; diff --git a/src/transport/TCPClientInterface.cpp b/src/transport/TCPClientInterface.cpp index ccbe17a..34225c4 100644 --- a/src/transport/TCPClientInterface.cpp +++ b/src/transport/TCPClientInterface.cpp @@ -71,9 +71,9 @@ void TCPClientInterface::loop() { return; // Will reconnect on next loop iteration } - // Drain multiple incoming frames per loop (up to 10, time-boxed) + // Drain multiple incoming frames per loop (up to 3, time-boxed) unsigned long tcpStart = millis(); - for (int i = 0; i < 10 && _client.available() && (millis() - tcpStart < TCP_LOOP_BUDGET_MS); i++) { + for (int i = 0; i < 3 && _client.available() && (millis() - tcpStart < TCP_LOOP_BUDGET_MS); i++) { unsigned long rxStart = millis(); int len = readFrame(); if (len > 0) { diff --git a/src/transport/TCPClientInterface.h b/src/transport/TCPClientInterface.h index 7e13bf9..a60f63b 100644 --- a/src/transport/TCPClientInterface.h +++ b/src/transport/TCPClientInterface.h @@ -45,7 +45,7 @@ private: static constexpr uint8_t FRAME_ESC = 0x7D; static constexpr uint8_t FRAME_XOR = 0x20; static constexpr unsigned long TCP_KEEPALIVE_TIMEOUT_MS = 300000; // 5 min - static constexpr unsigned long TCP_LOOP_BUDGET_MS = 15; + static constexpr unsigned long TCP_LOOP_BUDGET_MS = 8; public: unsigned long lastRxTime() const { return _lastRxTime; } diff --git a/src/ui/screens/LvNodesScreen.cpp b/src/ui/screens/LvNodesScreen.cpp index 2f01bf3..648606f 100644 --- a/src/ui/screens/LvNodesScreen.cpp +++ b/src/ui/screens/LvNodesScreen.cpp @@ -50,9 +50,15 @@ void LvNodesScreen::refreshUI() { if (now - _lastRebuild < REBUILD_INTERVAL_MS) return; int contacts = 0; for (const auto& n : _am->nodes()) { if (n.saved) contacts++; } - if (_am->nodeCount() != _lastNodeCount || contacts != _lastContactCount) { + int countDelta = abs(_am->nodeCount() - _lastNodeCount); + int contactDelta = abs(contacts - _lastContactCount); + if (countDelta > 0 || contactDelta > 0) { _lastRebuild = now; - rebuildList(); + // Skip rebuild for tiny transient count changes (≤3 nodes, no contact changes) + // They'll be picked up on the next interval + if (countDelta > 3 || contactDelta > 0) { + rebuildList(); + } } } diff --git a/src/ui/screens/LvNodesScreen.h b/src/ui/screens/LvNodesScreen.h index efae3c5..cf817c0 100644 --- a/src/ui/screens/LvNodesScreen.h +++ b/src/ui/screens/LvNodesScreen.h @@ -44,7 +44,7 @@ private: std::vector _rowToNodeIdx; // Maps row index -> node index in _am->nodes(), -1 for headers unsigned long _lastRebuild = 0; - static constexpr unsigned long REBUILD_INTERVAL_MS = 2000; + static constexpr unsigned long REBUILD_INTERVAL_MS = 5000; lv_obj_t* _list = nullptr; lv_obj_t* _lblEmpty = nullptr;