Files
ratdeck/src/transport/LoRaInterface.cpp
T
DeFiDude a83739c1cf Fix LXMF link delivery and improve LoRa reliability
Link proof signature, TCP proof routing, and split-packet handling
were causing intermittent message failures with Python LXMF clients.
Route >254B messages through link delivery, add proof retry for LoRa.
2026-04-02 22:48:24 -06:00

296 lines
10 KiB
C++

#include "LoRaInterface.h"
#include "config/BoardConfig.h"
#include <algorithm>
// RNode on-air framing constants (from RNode_Firmware Framing.h / Config.h)
// Every LoRa packet has a 1-byte header: upper nibble = random sequence, lower nibble = flags
#define RNODE_HEADER_L 1
#define RNODE_FLAG_SPLIT 0x01
#define RNODE_NIBBLE_SEQ 0xF0
#define RNODE_SINGLE_MTU (MAX_PACKET_SIZE - RNODE_HEADER_L) // 254 bytes payload per frame
LoRaInterface::LoRaInterface(SX1262* radio, const char* name)
: RNS::InterfaceImpl(name), _radio(radio)
{
_IN = true;
_OUT = true;
_bitrate = 2000;
// Reticulum MTU (500 bytes) — split-packet framing allows up to 2x254 = 508 bytes
_HW_MTU = RNS::Type::Reticulum::MTU;
}
LoRaInterface::~LoRaInterface() {
stop();
}
bool LoRaInterface::start() {
if (!_radio || !_radio->isRadioOnline()) {
Serial.println("[LORA_IF] Radio not available");
_online = false;
return false;
}
_online = true;
_radio->receive();
Serial.println("[LORA_IF] Interface started (split-packet enabled, MTU=500)");
return true;
}
void LoRaInterface::stop() {
_online = false;
Serial.println("[LORA_IF] Interface stopped");
}
void LoRaInterface::send_outgoing(const RNS::Bytes& data) {
if (!_online || !_radio) return;
// Reject packets exceeding Reticulum MTU (500 bytes)
if (data.size() > RNS::Type::Reticulum::MTU) {
Serial.printf("[LORA_IF] TX DROPPED: exceeds Reticulum MTU (%d > %d)\n",
(int)data.size(), (int)RNS::Type::Reticulum::MTU);
return;
}
// 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);
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();
_txQueue.push_back(data);
}
return;
}
transmitNow(data);
}
void LoRaInterface::transmitNow(const RNS::Bytes& data) {
uint8_t header = (uint8_t)(random(256)) & RNODE_NIBBLE_SEQ;
bool needsSplit = (data.size() > RNODE_SINGLE_MTU);
if (needsSplit) {
header |= RNODE_FLAG_SPLIT;
// First frame: header + first 254 bytes of payload
size_t firstLen = RNODE_SINGLE_MTU;
Serial.printf("[LORA_IF] TX SPLIT: %d bytes in 2 frames (seq=0x%02X)\n",
(int)data.size(), header & RNODE_NIBBLE_SEQ);
_radio->beginPacket();
_radio->write(header);
_radio->write(data.data(), firstLen);
_radio->endPacket(true);
// Save remaining data for second frame
_splitTxPending = true;
_splitTxRemaining = RNS::Bytes(data.data() + firstLen, data.size() - firstLen);
_splitTxHeader = header;
Serial.printf("[LORA_IF] TX SPLIT frame 1: %d+1 bytes (remaining: %d)\n",
(int)firstLen, (int)_splitTxRemaining.size());
} else {
// Single frame: fits in one LoRa packet
_radio->beginPacket();
_radio->write(header);
_radio->write(data.data(), data.size());
_radio->endPacket(true);
Serial.printf("[LORA_IF] TX %d+1 bytes (hdr=0x%02X)\n", (int)data.size(), header);
}
_txPending = true;
_txData = data;
InterfaceImpl::handle_outgoing(data);
// Track airtime
size_t airBytes = needsSplit ? (RNODE_SINGLE_MTU + RNODE_HEADER_L) : (data.size() + RNODE_HEADER_L);
float airtimeMs = _radio->getAirtime(airBytes);
unsigned long txNow = millis();
if (txNow - _airtimeWindowStart >= AIRTIME_WINDOW_MS) {
_airtimeAccumMs = 0;
_airtimeWindowStart = txNow;
} else {
float elapsed = (float)(txNow - _airtimeWindowStart);
float remaining = 1.0f - (elapsed / AIRTIME_WINDOW_MS);
if (remaining < 0) remaining = 0;
_airtimeAccumMs *= remaining;
_airtimeWindowStart = txNow;
}
_airtimeAccumMs += airtimeMs;
}
void LoRaInterface::loop() {
if (!_online || !_radio) return;
// Handle async TX completion
if (_txPending) {
if (!_radio->isTxBusy()) {
_txPending = false;
// If split TX pending, send the second frame immediately
if (_splitTxPending) {
_splitTxPending = false;
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(), frame2Size);
_radio->endPacket(true);
_txPending = true;
_splitTxRemaining = RNS::Bytes();
// Track airtime for second frame (must use saved size before clear)
float airtimeMs = _radio->getAirtime(frame2Size + RNODE_HEADER_L);
_airtimeAccumMs += airtimeMs;
return;
}
_txData = RNS::Bytes();
if (!_txQueue.empty()) {
RNS::Bytes next = _txQueue.front();
_txQueue.pop_front();
transmitNow(next);
} else {
_radio->receive();
}
}
return;
}
// 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
static unsigned long lastRxDebug = 0;
if (millis() - lastRxDebug > 30000) {
lastRxDebug = millis();
int rssi = _radio->currentRssi();
uint8_t status = _radio->getStatus();
uint8_t chipMode = (status >> 4) & 0x07;
Serial.printf("[LORA_IF] RX: RSSI=%d dBm, status=0x%02X(mode=%d)\n",
rssi, status, chipMode);
}
if (!_radio->packetAvailable) return;
_radio->packetAvailable = false;
int packetSize = _radio->parsePacket();
if (packetSize <= RNODE_HEADER_L) {
if (packetSize > 0) {
Serial.printf("[LORA_IF] RX runt packet (%d bytes), discarding\n", packetSize);
}
_radio->receive();
return;
}
uint8_t raw[MAX_PACKET_SIZE];
memcpy(raw, _radio->packetBuffer(), packetSize);
// Capture signal quality before any further processing
_lastRxRssi = _radio->packetRssi();
_lastRxSnr = _radio->packetSnr();
uint8_t header = raw[0];
int payloadSize = packetSize - RNODE_HEADER_L;
uint8_t seq = header & RNODE_NIBBLE_SEQ;
bool isSplit = (header & RNODE_FLAG_SPLIT) != 0;
if (isSplit) {
// Split packet handling
if (!_splitRxPending) {
// First frame of a split packet
_splitRxPending = true;
_splitRxSeq = seq;
_splitRxBuffer = RNS::Bytes(raw + RNODE_HEADER_L, payloadSize);
_splitRxTimestamp = millis();
Serial.printf("[LORA_IF] RX SPLIT frame 1: %d bytes (seq=0x%02X), RSSI=%d, SNR=%.1f\n",
payloadSize, seq, _lastRxRssi, _lastRxSnr);
_radio->receive();
return;
} else if (seq == _splitRxSeq) {
// Second frame matches — reassemble
Serial.printf("[LORA_IF] RX SPLIT frame 2: %d bytes (seq=0x%02X), RSSI=%d, SNR=%.1f\n",
payloadSize, seq, _lastRxRssi, _lastRxSnr);
_splitRxBuffer.append(raw + RNODE_HEADER_L, payloadSize);
int totalSize = _splitRxBuffer.size();
_splitRxPending = false;
Serial.printf("[LORA_IF] RX SPLIT reassembled: %d bytes total\n", totalSize);
InterfaceImpl::handle_incoming(_splitRxBuffer);
_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);
} else if (!_txPending) {
_radio->receive();
}
return;
} else {
// 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);
_splitRxTimestamp = millis();
_radio->receive();
return;
}
}
// 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.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",
packetSize, header, payloadSize,
_lastRxRssi, _lastRxSnr);
RNS::Bytes buf(payloadSize);
memcpy(buf.writable(payloadSize), raw + RNODE_HEADER_L, payloadSize);
InterfaceImpl::handle_incoming(buf);
if (!_txPending) {
_radio->receive();
}
}
float LoRaInterface::airtimeUtilization() const {
if (_airtimeAccumMs <= 0) return 0;
unsigned long elapsed = millis() - _airtimeWindowStart;
if (elapsed == 0) elapsed = 1;
float windowMs = std::min((float)elapsed, (float)AIRTIME_WINDOW_MS);
return _airtimeAccumMs / windowMs;
}