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.
This commit is contained in:
DeFiDude committed 2026-06-26 15:13:12 -06:00
1 parent e497de63b3
commit a2aad03a39
5 files changed
+61 -30

No files matched your search

+1 -1
View File
@@ -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 ---
+1 -1
View File
@@ -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) {
+52 -28
View File
@@ -4,6 +4,7 @@
#include <Transport.h>
#include <time.h>
#include <algorithm>
#include <exception>
LXMFManager* LXMFManager::_instance = nullptr;
std::map<std::string, LXMFManager::PendingProof> 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) {
+6
View File
@@ -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)) {
+1
View File
@@ -56,6 +56,7 @@ public:
RNS::Destination& destination() { return _destination; }
LoRaInterface* loraInterface() { return _loraImpl; }
bool isLoRaNextHop(const RNS::Bytes& destHash) const;
private:
bool loadOrCreateIdentity();