From a2aad03a39c03c246fc6abc35ba94928f632dc39 Mon Sep 17 00:00:00 2001 From: DeFiDude <59237470+DeFiDude@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:13:12 -0600 Subject: [PATCH] lxmf: avoid split LoRa opportunistic sends Gate rsDeck opportunistic LXMF sends by the final packed Reticulum raw size when the next hop is LoRa, instead of using the pre-encryption LXMF payload size as the 254-byte limit. Keep TCP paths on the normal Reticulum MDU, route LoRa packets that would exceed one RNode RF frame into link delivery, and log payload/raw/lora_raw sizing for field diagnosis. Tested with rsDeck/Ratdeck T-Deck and Heltec V3 RNode attached over USB to a MacBook Pro running Ratspeak, using the medium-fast LoRa preset. Short 1-5 character opportunistic sends worked bidirectionally. Direct link delivery worked from T-Deck to Ratspeak/Heltec; Ratspeak-to-T-Deck link delivery later queued during link establishment while opportunistic still worked. --- src/config/Config.h | 2 +- src/main.cpp | 2 +- src/reticulum/LXMFManager.cpp | 80 +++++++++++++++++++----------- src/reticulum/ReticulumManager.cpp | 6 +++ src/reticulum/ReticulumManager.h | 1 + 5 files changed, 61 insertions(+), 30 deletions(-) diff --git a/src/config/Config.h b/src/config/Config.h index 642fc6f..07c2cd3 100644 --- a/src/config/Config.h +++ b/src/config/Config.h @@ -68,7 +68,7 @@ #define RSDECK_MAX_MESSAGES_PER_CONV 100 #define FLASH_MSG_CACHE_LIMIT 20 #define RSDECK_MAX_OUTQUEUE 20 -#define RSDECK_LXMF_SINGLE_FRAME_MAX 254 // T-Deck-safe payload cap until resource transfers are fixed +#define RSDECK_RNODE_SINGLE_FRAME_RAW_MAX 254 // Raw Reticulum bytes per RNode LoRa RF frame #define PATH_PERSIST_INTERVAL_MS 60000 // --- Power Management --- diff --git a/src/main.cpp b/src/main.cpp index c92ab19..55227b6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -711,7 +711,7 @@ static constexpr uint8_t LITE_TRANSPORT_ID[16] = { 'r', 's', 'l', 'i', 't', 'e', '-', 'h', 'e', 'l', 't', 'e', 'c', '-', 'v', '3' }; -static constexpr size_t RNODE_DIAG_SINGLE_MTU = 254; +static constexpr size_t RNODE_DIAG_SINGLE_MTU = RSDECK_RNODE_SINGLE_FRAME_RAW_MAX; static RNS::Bytes diagnosticLiteLinkId; static bool sendDiagnosticRawReticulum(const RNS::Bytes& raw, const char* label) { diff --git a/src/reticulum/LXMFManager.cpp b/src/reticulum/LXMFManager.cpp index 3c5f1ee..2c2d9ef 100644 --- a/src/reticulum/LXMFManager.cpp +++ b/src/reticulum/LXMFManager.cpp @@ -4,6 +4,7 @@ #include #include #include +#include LXMFManager* LXMFManager::_instance = nullptr; std::map LXMFManager::_pendingProofs; @@ -131,13 +132,6 @@ bool LXMFManager::sendMessage(const RNS::Bytes& destHash, const std::string& con Serial.println("[LXMF] Message pack failed"); return false; } - if (payload.size() > RSDECK_LXMF_SINGLE_FRAME_MAX) { - msg.status = LXMFStatus::FAILED; - if (_store) _store->saveMessage(msg); - Serial.printf("[LXMF] Message too large for T-Deck safe path (%d > %d); resource transfer disabled\n", - (int)payload.size(), RSDECK_LXMF_SINGLE_FRAME_MAX); - return true; - } if (preference == DeliveryPreference::Link) { _linkRequiredIds.insert(msg.messageId.toHex()); Serial.printf("[LXMF] Message %s queued for link delivery\n", @@ -267,12 +261,6 @@ bool LXMFManager::sendDirect(LXMFMessage& msg) { if (payload.empty()) { Serial.println("[LXMF] packFull returned empty!"); msg.status = LXMFStatus::FAILED; return true; } const std::string msgIdHex = msg.messageId.toHex(); const bool requireLink = _linkRequiredIds.count(msgIdHex) > 0; - if (payload.size() > RSDECK_LXMF_SINGLE_FRAME_MAX) { - Serial.printf("[LXMF] Refusing resource-sized message (%d bytes); T-Deck safe cap is %d\n", - (int)payload.size(), RSDECK_LXMF_SINGLE_FRAME_MAX); - msg.status = LXMFStatus::FAILED; - return true; - } msg.status = LXMFStatus::SENDING; bool sent = false; @@ -334,24 +322,60 @@ bool LXMFManager::sendDirect(LXMFMessage& msg) { return false; } - // 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. - if (payloadBytes.size() <= RSDECK_LXMF_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; registerProofTracking(receipt, msg); } + // Use opportunistic only when the LXMF payload fits in a normal + // Reticulum packet and, on LoRa, the final raw packet fits in one + // RNode RF frame. The 254-byte RNode limit applies after encryption. + const bool opportunisticPayloadFits = payloadBytes.size() <= RNS::Type::Reticulum::MDU; + bool packetPacked = false; + size_t rawLen = 0; + size_t estimatedLoRaRawLen = 0; + const bool loraPath = _rns && _rns->isLoRaNextHop(msg.destHash); + const uint8_t hops = RNS::Transport::hops_to(msg.destHash); + + if (!opportunisticPayloadFits) { + Serial.printf("[LXMF] Opportunistic payload exceeds MDU: payload=%d mdu=%u\n", + (int)payloadBytes.size(), (unsigned)RNS::Type::Reticulum::MDU); } else { - // 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(), RSDECK_LXMF_SINGLE_FRAME_MAX, msg.retries); + RNS::Packet packet(outDest, payloadBytes); + const bool transportWrapsHeader2 = loraPath && hops > 1 + && packet.header_type() == RNS::Type::Packet::HEADER_1; + + try { + packet.pack(); + packetPacked = true; + rawLen = packet.raw().size(); + estimatedLoRaRawLen = rawLen + + (transportWrapsHeader2 ? RNS::Type::Reticulum::DESTINATION_LENGTH : 0); + } catch (const std::exception& e) { + Serial.printf("[LXMF] Opportunistic pack failed (%s); trying link delivery\n", e.what()); + } + + const bool singleFrameSafe = packetPacked && (!loraPath + || estimatedLoRaRawLen <= RSDECK_RNODE_SINGLE_FRAME_RAW_MAX); + + if (singleFrameSafe) { + Serial.printf("[LXMF] sending opportunistic: payload=%d raw=%u lora_raw=%u lora=%s hops=%u to %s\n", + (int)payloadBytes.size(), (unsigned)rawLen, + (unsigned)estimatedLoRaRawLen, + loraPath ? "yes" : "no", (unsigned)hops, + outDest.hash().toHex().substr(0, 12).c_str()); + RNS::PacketReceipt receipt = packet.send(); + if (receipt) { sent = true; registerProofTracking(receipt, msg); } + } + } + + if (!sent) { + // Too large for single-frame LoRa, or too large for an opportunistic + // Reticulum packet. Use link/resource delivery so packet loss is + // recoverable above the physical RNode split-frame layer. + Serial.printf("[LXMF] Message needs link delivery: payload=%d raw=%u lora_raw=%u limit=%u lora=%s hops=%u retry=%d\n", + (int)payloadBytes.size(), (unsigned)rawLen, + (unsigned)estimatedLoRaRawLen, + (unsigned)RSDECK_RNODE_SINGLE_FRAME_RAW_MAX, + loraPath ? "yes" : "no", (unsigned)hops, msg.retries); if (msg.retries % 3 == 0 && (!_outLink || _outLinkDestHash != msg.destHash || _outLink.status() != RNS::Type::Link::ACTIVE)) { - ensureOutboundLink(outDest, msg.destHash, "resource transfer"); + ensureOutboundLink(outDest, msg.destHash, loraPath ? "single-frame LoRa overflow" : "resource transfer"); } msg.retries++; if (msg.retries >= 30) { diff --git a/src/reticulum/ReticulumManager.cpp b/src/reticulum/ReticulumManager.cpp index 5c67b1c..7455191 100644 --- a/src/reticulum/ReticulumManager.cpp +++ b/src/reticulum/ReticulumManager.cpp @@ -274,6 +274,12 @@ bool ReticulumManager::begin(SX1262* radio, FlashStore* flash, bool loraEnabled) return true; } +bool ReticulumManager::isLoRaNextHop(const RNS::Bytes& destHash) const { + if (!_loraImpl) return false; + RNS::Interface nextHop = RNS::Transport::next_hop_interface(destHash); + return nextHop && nextHop.get() == _loraImpl; +} + bool ReticulumManager::loadOrCreateIdentity() { // Tier 1: Flash (LittleFS) if (_flash->exists(PATH_IDENTITY)) { diff --git a/src/reticulum/ReticulumManager.h b/src/reticulum/ReticulumManager.h index 780a117..9596c56 100644 --- a/src/reticulum/ReticulumManager.h +++ b/src/reticulum/ReticulumManager.h @@ -56,6 +56,7 @@ public: RNS::Destination& destination() { return _destination; } LoRaInterface* loraInterface() { return _loraImpl; } + bool isLoRaNextHop(const RNS::Bytes& destHash) const; private: bool loadOrCreateIdentity();