mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-10-23 01:41:17 +00:00
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:
1 parent
e497de63b3
commit
a2aad03a39
5 files changed
+61
-30
No files matched your search
+1
-1
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -56,6 +56,7 @@ public:
|
||||
|
||||
RNS::Destination& destination() { return _destination; }
|
||||
LoRaInterface* loraInterface() { return _loraImpl; }
|
||||
bool isLoRaNextHop(const RNS::Bytes& destHash) const;
|
||||
|
||||
private:
|
||||
bool loadOrCreateIdentity();
|
||||
|
||||
Reference in new issue
Block a user