mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-08-28 05:24:32 +00:00
Merge branch 'main' into touch
This commit is contained in:
+13
-1
@@ -133,6 +133,9 @@ constexpr unsigned long TCP_GLOBAL_BUDGET_MS = 35; // Max cumulative TCP ti
|
||||
bool wifiDeferredAnnounce = false;
|
||||
unsigned long wifiConnectedAt = 0;
|
||||
|
||||
// LXMF diagnostic counters (reset each heartbeat)
|
||||
static uint32_t diagTcpSkipEvents = 0;
|
||||
|
||||
// =============================================================================
|
||||
// Timezone helper — returns POSIX TZ string for current config
|
||||
// =============================================================================
|
||||
@@ -1062,6 +1065,7 @@ void loop() {
|
||||
// 8. WiFi + TCP loops (with global budget) — skip only if RNS severely overloaded
|
||||
{
|
||||
bool skipTcp = (rnsDuration > 500);
|
||||
if (skipTcp) diagTcpSkipEvents++;
|
||||
if (!skipTcp && wifiImpl) wifiImpl->loop();
|
||||
if (!skipTcp) {
|
||||
unsigned long tcpBudgetStart = millis();
|
||||
@@ -1157,10 +1161,18 @@ void loop() {
|
||||
{
|
||||
auto& ifaces = RNS::Transport::get_interfaces();
|
||||
int tcpUp = 0;
|
||||
for (auto* tcp : tcpClients) { if (tcp && tcp->isConnected()) tcpUp++; }
|
||||
int tcpRx = 0;
|
||||
for (auto* tcp : tcpClients) {
|
||||
if (tcp && tcp->isConnected()) tcpUp++;
|
||||
if (tcp) tcpRx += tcp->hubRxCount();
|
||||
}
|
||||
Serial.printf("[HEART-DIAG] ifaces=%d tcp=%d/%d wifi=%s\n",
|
||||
(int)ifaces.size(), tcpUp, (int)tcpClients.size(),
|
||||
wifiSTAConnected ? "STA" : (wifiImpl ? "AP" : "OFF"));
|
||||
Serial.printf("[LXMF-DIAG] tcp_rx=%d tcp_skip=%lu ann_filt=%lu\n",
|
||||
tcpRx, (unsigned long)diagTcpSkipEvents,
|
||||
(unsigned long)rns.announceFilterCount());
|
||||
diagTcpSkipEvents = 0;
|
||||
}
|
||||
#if HAS_GPS
|
||||
if (userConfig.settings().gpsTimeEnabled) {
|
||||
|
||||
@@ -189,17 +189,22 @@ bool LXMFManager::sendDirect(LXMFMessage& msg) {
|
||||
// Fallback: opportunistic or queue for link-based resource transfer
|
||||
if (!sent) {
|
||||
RNS::Bytes payloadBytes(payload.data(), payload.size());
|
||||
if (payloadBytes.size() <= RNS::Type::Reticulum::MDU) {
|
||||
// Small enough for single opportunistic packet
|
||||
// Use opportunistic only for packets that fit in a single LoRa frame (254 bytes).
|
||||
// Larger packets require split-frame over LoRa, which is unreliable — any single
|
||||
// frame loss (CRC error, collision, half-duplex timing) kills the entire transfer
|
||||
// with no recovery. Link-based delivery handles retransmission at the protocol level.
|
||||
static constexpr size_t SINGLE_FRAME_MAX = 254;
|
||||
if (payloadBytes.size() <= SINGLE_FRAME_MAX) {
|
||||
// Fits in single LoRa frame — send opportunistic
|
||||
Serial.printf("[LXMF] sending opportunistic: %d bytes to %s\n",
|
||||
(int)payloadBytes.size(), outDest.hash().toHex().substr(0, 12).c_str());
|
||||
RNS::Packet packet(outDest, payloadBytes);
|
||||
RNS::PacketReceipt receipt = packet.send();
|
||||
if (receipt) { sent = true; }
|
||||
} else {
|
||||
// Too large for opportunistic — need link + resource transfer
|
||||
Serial.printf("[LXMF] Message too large for opportunistic (%d bytes > MDU), needs link (retry %d)\n",
|
||||
(int)payloadBytes.size(), msg.retries);
|
||||
// Too large for single frame — need link + resource transfer
|
||||
Serial.printf("[LXMF] Message needs link delivery (%d bytes > %d single-frame), retry %d\n",
|
||||
(int)payloadBytes.size(), (int)SINGLE_FRAME_MAX, msg.retries);
|
||||
if (msg.retries % 3 == 0 && (!_outLink || _outLinkDestHash != msg.destHash
|
||||
|| _outLink.status() != RNS::Type::Link::ACTIVE)) {
|
||||
_outLinkPendingHash = msg.destHash;
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
uint32_t ReticulumManager::_announceFilterCount = 0;
|
||||
|
||||
bool LittleFSFileSystem::init() { return true; }
|
||||
bool LittleFSFileSystem::file_exists(const char* p) { return LittleFS.exists(p); }
|
||||
|
||||
@@ -116,7 +118,7 @@ bool ReticulumManager::begin(SX1262* radio, FlashStore* flash) {
|
||||
|
||||
// Adaptive rate: tighter during first 60s boot flood, then normal
|
||||
unsigned int maxRate = (now < 60000) ? 3 : RATDECK_MAX_ANNOUNCES_PER_SEC;
|
||||
if (++count > maxRate) return false;
|
||||
if (++count > maxRate) { ReticulumManager::_announceFilterCount++; return false; }
|
||||
|
||||
// Skip re-validation of known paths (saves ~100ms Ed25519 per announce)
|
||||
// Allow through if better path discovered, or once per 5 min for name/ratchet updates.
|
||||
@@ -131,7 +133,10 @@ bool ReticulumManager::begin(SX1262* radio, FlashStore* flash) {
|
||||
packet.destination_hash().size());
|
||||
|
||||
auto it = lastRevalidate.find(key);
|
||||
if (it != lastRevalidate.end() && (now - it->second) < 300000) return false;
|
||||
if (it != lastRevalidate.end() && (now - it->second) < 300000) {
|
||||
ReticulumManager::_announceFilterCount++;
|
||||
return false;
|
||||
}
|
||||
lastRevalidate[key] = now;
|
||||
|
||||
if (lastRevalidate.size() > 300) lastRevalidate.clear();
|
||||
@@ -288,7 +293,11 @@ void ReticulumManager::persistData() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
Serial.printf("[PERSIST] Cycle %d done (%lums)\n", _persistCycle, millis() - start);
|
||||
unsigned long dur = millis() - start;
|
||||
Serial.printf("[PERSIST] Cycle %d done (%lums)\n", _persistCycle, dur);
|
||||
if (dur > 500) {
|
||||
Serial.printf("[PERSIST] WARNING: Cycle %d blocked for %lums!\n", _persistCycle, dur);
|
||||
}
|
||||
_persistCycle = (_persistCycle + 1) % 3;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ public:
|
||||
|
||||
void announce(const RNS::Bytes& appData = {});
|
||||
unsigned long lastAnnounceTime() const { return _lastAnnounceTime; }
|
||||
static uint32_t announceFilterCount() { return _announceFilterCount; }
|
||||
|
||||
RNS::Destination& destination() { return _destination; }
|
||||
LoRaInterface* loraInterface() { return _loraImpl; }
|
||||
@@ -71,4 +72,5 @@ private:
|
||||
unsigned long _lastPersist = 0;
|
||||
unsigned long _lastAnnounceTime = 0;
|
||||
uint8_t _persistCycle = 0; // Rotating: 0=Transport, 1=Identity, 2=SD backup
|
||||
static uint32_t _announceFilterCount;
|
||||
};
|
||||
|
||||
@@ -50,10 +50,17 @@ void LoRaInterface::send_outgoing(const RNS::Bytes& data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_txPending || _splitTxPending) {
|
||||
// Queue TX when radio is busy OR when we're waiting for split frame 2.
|
||||
// Transmitting during split RX would put the radio in TX mode, causing
|
||||
// frame 2 to be lost (LoRa is half-duplex).
|
||||
if (_txPending || _splitTxPending || _splitRxPending) {
|
||||
if ((int)_txQueue.size() < TX_QUEUE_MAX) {
|
||||
_txQueue.push_back(data);
|
||||
Serial.printf("[LORA_IF] TX queued (%d in queue)\n", (int)_txQueue.size());
|
||||
if (_splitRxPending) {
|
||||
Serial.printf("[LORA_IF] TX deferred (split RX pending, %d in queue)\n", (int)_txQueue.size());
|
||||
} else {
|
||||
Serial.printf("[LORA_IF] TX queued (%d in queue)\n", (int)_txQueue.size());
|
||||
}
|
||||
} else {
|
||||
Serial.println("[LORA_IF] TX queue full, dropping oldest");
|
||||
_txQueue.pop_front();
|
||||
@@ -132,19 +139,19 @@ void LoRaInterface::loop() {
|
||||
if (_splitTxPending) {
|
||||
_splitTxPending = false;
|
||||
|
||||
Serial.printf("[LORA_IF] TX SPLIT frame 2: %d+1 bytes\n",
|
||||
(int)_splitTxRemaining.size());
|
||||
size_t frame2Size = _splitTxRemaining.size();
|
||||
Serial.printf("[LORA_IF] TX SPLIT frame 2: %d+1 bytes\n", (int)frame2Size);
|
||||
|
||||
_radio->beginPacket();
|
||||
_radio->write(_splitTxHeader);
|
||||
_radio->write(_splitTxRemaining.data(), _splitTxRemaining.size());
|
||||
_radio->write(_splitTxRemaining.data(), frame2Size);
|
||||
_radio->endPacket(true);
|
||||
|
||||
_txPending = true;
|
||||
_splitTxRemaining = RNS::Bytes();
|
||||
|
||||
// Track airtime for second frame
|
||||
float airtimeMs = _radio->getAirtime(_splitTxRemaining.size() + RNODE_HEADER_L);
|
||||
// Track airtime for second frame (must use saved size before clear)
|
||||
float airtimeMs = _radio->getAirtime(frame2Size + RNODE_HEADER_L);
|
||||
_airtimeAccumMs += airtimeMs;
|
||||
return;
|
||||
}
|
||||
@@ -162,11 +169,17 @@ void LoRaInterface::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Split RX timeout: discard stale partial packets
|
||||
// Split RX timeout: discard stale partial packets and drain deferred TX
|
||||
if (_splitRxPending && (millis() - _splitRxTimestamp > SPLIT_RX_TIMEOUT_MS)) {
|
||||
Serial.println("[LORA_IF] RX SPLIT timeout, discarding partial");
|
||||
_splitRxPending = false;
|
||||
_splitRxBuffer = RNS::Bytes();
|
||||
// Drain any TX that was deferred during split RX hold
|
||||
if (!_txQueue.empty() && !_txPending) {
|
||||
RNS::Bytes next = _txQueue.front();
|
||||
_txQueue.pop_front();
|
||||
transmitNow(next);
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic RX debug
|
||||
@@ -231,13 +244,19 @@ void LoRaInterface::loop() {
|
||||
InterfaceImpl::handle_incoming(_splitRxBuffer);
|
||||
_splitRxBuffer = RNS::Bytes();
|
||||
|
||||
if (!_txPending) {
|
||||
// Drain any TX that was deferred during split RX hold
|
||||
if (!_txQueue.empty() && !_txPending) {
|
||||
RNS::Bytes next = _txQueue.front();
|
||||
_txQueue.pop_front();
|
||||
transmitNow(next);
|
||||
} else if (!_txPending) {
|
||||
_radio->receive();
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// Sequence mismatch — discard old, start new
|
||||
Serial.printf("[LORA_IF] RX SPLIT seq mismatch (had 0x%02X, got 0x%02X), restarting\n",
|
||||
// Different split packet's frame 1 arrived — the previous split is lost.
|
||||
// This happens when frame 2 was missed (radio was busy, collision, etc.)
|
||||
Serial.printf("[LORA_IF] RX SPLIT new seq (had 0x%02X, got 0x%02X), previous frame 2 lost\n",
|
||||
_splitRxSeq, seq);
|
||||
_splitRxSeq = seq;
|
||||
_splitRxBuffer = RNS::Bytes(raw + RNODE_HEADER_L, payloadSize);
|
||||
@@ -247,11 +266,11 @@ void LoRaInterface::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Non-split packet — if we were waiting for split frame 2, discard the partial
|
||||
// Non-split packet while waiting for split frame 2:
|
||||
// Process the non-split packet normally but KEEP the split buffer.
|
||||
// Frame 2 may still arrive after this interleaving packet.
|
||||
if (_splitRxPending) {
|
||||
Serial.println("[LORA_IF] RX non-split while waiting for split frame 2, discarding partial");
|
||||
_splitRxPending = false;
|
||||
_splitRxBuffer = RNS::Bytes();
|
||||
Serial.printf("[LORA_IF] RX non-split %d bytes while awaiting split frame 2 (kept)\n", payloadSize);
|
||||
}
|
||||
|
||||
Serial.printf("[LORA_IF] RX %d bytes (hdr=0x%02X, payload=%d), RSSI=%d, SNR=%.1f\n",
|
||||
|
||||
@@ -83,9 +83,9 @@ void TCPClientInterface::loop() {
|
||||
return; // Will reconnect on next loop iteration
|
||||
}
|
||||
|
||||
// Drain incoming frames per loop (up to 10, time-boxed)
|
||||
// Drain incoming frames per loop (up to 15, 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 < 15 && _client.available() && (millis() - tcpStart < TCP_LOOP_BUDGET_MS); i++) {
|
||||
unsigned long rxStart = millis();
|
||||
int len = readFrame();
|
||||
if (len > 0) {
|
||||
@@ -151,7 +151,7 @@ void TCPClientInterface::send_outgoing(const RNS::Bytes& data) {
|
||||
Serial.printf("[TCP-DIAG] *** PROOF packet being sent via TCP! ***\n");
|
||||
}
|
||||
|
||||
if (packet_type != 0x01) { // Not ANNOUNCE
|
||||
if (packet_type != 0x01 && packet_type != 0x03) { // Not ANNOUNCE, not PROOF
|
||||
if (header_type == 0) {
|
||||
// Header1 → wrap as Header2 (handles hops==1, hops==0, unknown path)
|
||||
uint8_t new_flags = flags | 0x50; // Set Header2 (bit 6) + Transport (bit 4)
|
||||
|
||||
@@ -65,4 +65,5 @@ private:
|
||||
|
||||
public:
|
||||
unsigned long lastRxTime() const { return _lastRxTime; }
|
||||
unsigned long hubRxCount() const { return _hubRxCount; }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user