Files
firmware/src/mesh/RadioInterface.h
T
Jonathan BennettandClaude Opus 5 08e5811deb Don't let a stale HEADER_ERR re-arm RX over a live frame
Before: a header that fails on noise, a collision or a foreign frame
latches HEADER_ERR and no RX_DONE follows it. `checkStaleRxFlags()`
counts HEADER_VALID and HEADER_ERR as one boolean, so the error starts
the max-packet window that re-arms RX. A good frame arriving inside that
window raises HEADER_VALID beside the error, which does not restart the
window - a header was already up. One max packet after the error was
first seen, the poll re-arms, and `startReceive()`'s standby aborts the
frame still on the air.

After: the window is timed from the valid header alone. A HEADER_ERR
beside a HEADER_VALID is retired at once and decides nothing, because one
header raises one flag or the other (SX1262 DS rev 2.1 table 13-29), so
the two together are two frames and the error is the older one.

A lone stale HEADER_ERR past its window is now cleared rather than
re-armed on every chip but SX128x. The re-arm exists for the SX1280 wedge
of DS rev 3.3 16.2, which a clear does not unwedge; on SX126x, LR11x0 and
LR20x0 the demodulator is already hunting for a preamble again, and a
clear never aborts a frame that may be in one. `headerErrWedgesRx()` is
the per-chip hook, true only on SX128x. SX126x already clears HEADER_ERR
unconditionally in `resumeRunningReceive()` for the same reason.

The busy decision is unaffected: `receiveDetected()` reads only
HEADER_VALID and PREAMBLE_DETECTED, so the cost of the bug was a dropped
reception, never a talk-over.

How: `staleRxFlagAction()` takes HEADER_VALID and HEADER_ERR separately
plus the per-chip wedge flag, and gains a ClearHeaderErr verdict.
`checkStaleRxFlags()` keeps a second stamp bit for "the window was
started by a valid header", so a valid header restarts a failed one's
window the way develop already has a header restart a bare preamble's.
Five native cases cover it, two of them the regression itself.

Found and analysed by Tom (NomDeTom), who also wrote the commit the
behaviour came from, 45262f825 in #11137.

Claude-Session: https://claude.ai/code/session_01DLRPmdJQTu6MDG7o2QLceR

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-12 23:48:49 +00:00

381 lines
16 KiB
C++

#pragma once
#include "MemoryPool.h"
#include "MeshTypes.h"
#include "Observer.h"
#include "PointerQueue.h"
#include "airtime.h"
#include "error.h"
#include <memory>
#if HAS_LORA_FEM
#include "LoRaFEMInterface.h"
#endif
// Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file.
typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig;
#define MAX_TX_QUEUE 16 // max number of packets which can be waiting for transmission
#define MAX_LORA_PAYLOAD_LEN 255 // max length of 255 per Semtech's datasheets on SX12xx
#define MESHTASTIC_HEADER_LENGTH 16
#define MESHTASTIC_PKC_OVERHEAD 12
#define MESHTASTIC_AEAD_OVERHEAD 12
#define PACKET_FLAGS_HOP_LIMIT_MASK 0x07
#define PACKET_FLAGS_WANT_ACK_MASK 0x08
#define PACKET_FLAGS_VIA_MQTT_MASK 0x10
#define PACKET_FLAGS_HOP_START_MASK 0xE0
#define PACKET_FLAGS_HOP_START_SHIFT 5
/**
* This structure has to exactly match the wire layout when sent over the radio link. Used to keep compatibility
* with the old radiohead implementation.
*/
typedef struct {
NodeNum to, from; // can be 1 byte or four bytes
PacketId id; // can be 1 byte or 4 bytes
/**
* Usage of flags:
*
* The bottom three bits of flags are use to store hop_limit when sent over the wire.
**/
uint8_t flags;
/** The channel hash - used as a hint for the decoder to limit which channels we consider */
uint8_t channel;
// Last byte of the NodeNum of the next-hop for this packet
uint8_t next_hop;
// Last byte of the NodeNum of the node that will relay/relayed this packet
uint8_t relay_node;
} PacketHeader;
/**
* This structure represent the structured buffer : a PacketHeader then the payload. The whole is
* MAX_LORA_PAYLOAD_LEN + 1 length
* It makes the use of its data easier, and avoids manipulating pointers (and potential non aligned accesses)
*/
typedef struct {
/** The header, as defined just before */
PacketHeader header;
/** The payload, of maximum length minus the header, aligned just to be sure */
uint8_t payload[MAX_LORA_PAYLOAD_LEN + 1 - sizeof(PacketHeader)] __attribute__((__aligned__));
} RadioBuffer;
/// On-air ceiling for MeshPacket.encrypted. RadioBuffer holds one byte more, but the PHY caps a whole
/// frame at MAX_LORA_PAYLOAD_LEN, so the header comes out of the same budget (matches perhapsEncode).
constexpr size_t MAX_RADIO_PAYLOAD_LEN = MAX_LORA_PAYLOAD_LEN - sizeof(PacketHeader);
static_assert(MAX_RADIO_PAYLOAD_LEN < sizeof(RadioBuffer::payload), "payload ceiling must fit the buffer");
/**
* Basic operations all radio chipsets must implement.
*
* This defines the SOLE API for talking to radios (because soon we will have alternate radio implementations)
*/
class RadioInterface
{
friend class MeshRadio; // for debugging we let that class touch pool
CallbackObserver<RadioInterface, void *> configChangedObserver =
CallbackObserver<RadioInterface, void *>(this, &RadioInterface::reloadConfig);
CallbackObserver<RadioInterface, void *> preflightSleepObserver =
CallbackObserver<RadioInterface, void *>(this, &RadioInterface::preflightSleepCb);
CallbackObserver<RadioInterface, void *> notifyDeepSleepObserver =
CallbackObserver<RadioInterface, void *>(this, &RadioInterface::notifyDeepSleepCb);
protected:
bool disabled = false;
float bw = 125;
uint8_t sf = 9;
uint8_t cr = 5;
// Fallback per-band counts for a driver that states nothing of its own. Every driver in the tree
// states its own, and they do NOT all agree: the slot has to match the scan each part really runs,
// so a mesh mixing RF95 (2.25 symbols, fixed in hardware) with SX126x/LR (4) runs two slot lengths.
// Uncited on purpose - no single vendor note covers every part they cover. AN1200.77 backs the
// 2.4 GHz figure for SX1280: a longer window moves P(Detection) away from P(False Detection).
static constexpr uint8_t NUM_SYM_CAD = 4;
static constexpr uint8_t NUM_SYM_CAD_24GHZ = 8;
// Runs during RadioInterface's own construction, so it uses the fallbacks above, never a driver
// override. applyModemConfig() recomputes it before anything reads it, and is authoritative.
uint32_t slotTimeMsec = computeSlotTimeMsec();
uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving
static constexpr uint16_t preambleLengthDefault =
16; // 8 is default, but we use longer to increase the amount of sleep time when receiving
static constexpr uint16_t wideLoraPreambleLengthDefault = 12; // 12 is default for wide Lora
uint32_t preambleTimeMsec = 165; // calculated on startup, this is the default for LongFast
static constexpr uint32_t PROCESSING_TIME_MSEC =
4500; // time to construct, process and construct a packet again (empirically determined)
static constexpr uint8_t CWmin = 3; // minimum CWsize
static constexpr uint8_t CWmax = 8; // maximum CWsize
meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending
uint32_t lastTxStart = 0L;
uint32_t computeSlotTimeMsec();
// Symbols this part scans, declared per band: a part that works in both need not scan the same
// length in each, and the parts sharing a band do not all agree. Each driver states its own, and
// must pass a matching symNum - these are what size the CW slot, so a mismatch mis-scales backoff.
virtual uint8_t getCadSymbolCountSubGhz() const { return NUM_SYM_CAD; }
virtual uint8_t getCadSymbolCountWideLora() const { return NUM_SYM_CAD_24GHZ; }
/** Whichever of the two applies to the band actually in use. */
uint8_t getCadSymbolCount() const;
/**
* A temporary buffer used for sending/receiving packets, sized to hold the biggest buffer we might need
* */
RadioBuffer radioBuffer __attribute__((__aligned__));
/**
* Enqueue a received packet for the registered receiver
*/
void deliverToReceiver(meshtastic_MeshPacket *p);
public:
/** pool is the pool we will alloc our rx packets from
*/
RadioInterface();
virtual ~RadioInterface() {}
/// Fires once per valid received LoRa packet (arg = sender NodeNum). Used e.g. to flash LED_LORA.
static Observable<uint32_t> loraRxPacketObservable;
/**
* Coerce LoRa config fields (bandwidth/spread_factor) derived from presets.
* This is used during early bootstrapping so UIs that display these fields directly remain consistent.
*/
// static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig); // maybe superseded?
/**
* Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving)
*
* This method must be used before putting the CPU into deep or light sleep.
*
* @param deepSleep true when the radio itself is about to be powered down (deep sleep or
* shutdown) - an in-flight transmission then vetoes sleep, since it would be truncated on
* air. false for a light sleep where the radio stays powered and finishes the TX on its own.
*/
virtual bool canSleep(bool deepSleep = false) { return true; }
virtual bool wideLora() { return false; }
/// Whether the radio can tune sub-GHz bands. False for 2.4 GHz-only chips (SX128x);
/// multiband chips like the LR1121 keep the default.
virtual bool supportsSubGhz() { return true; }
/// Prepare hardware for sleep. Call this _only_ for deep sleep, not needed for light sleep.
virtual bool sleep() { return true; }
/// Disable this interface (while disabled, no packets can be sent or received)
void disable()
{
disabled = true;
sleep();
}
/**
* Send a packet (possibly by enquing in a private fifo). This routine will
* later free() the packet to pool. This routine is not allowed to stall.
* If the txmit queue is full it might return an error
*/
virtual ErrorCode send(meshtastic_MeshPacket *p) = 0;
/** Return TX queue status */
[[nodiscard]] virtual meshtastic_QueueStatus getQueueStatus()
{
meshtastic_QueueStatus qs;
qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0;
return qs;
}
/** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */
virtual bool cancelSending(NodeNum from, PacketId id) { return false; }
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
virtual bool findInTxQueue(NodeNum from, PacketId id) { return false; }
// methods from radiohead
/// Initialise the Driver transport hardware and software.
/// Make sure the Driver is properly configured before calling init().
/// \return true if initialisation succeeded.
virtual bool init();
/// Apply any radio provisioning changes
/// Make sure the Driver is properly configured before calling init().
/// \return true if initialisation succeeded.
virtual bool reconfigure();
/** The delay to use for retransmitting dropped packets */
[[nodiscard]] uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p);
/** The delay to use when we want to send something */
[[nodiscard]] uint32_t getTxDelayMsec();
/** The CW to use when calculating SNR_based delays */
[[nodiscard]] uint8_t getCWsize(float snr);
/** The worst-case SNR_based packet delay */
[[nodiscard]] uint32_t getTxDelayMsecWeightedWorst(float snr);
/** Returns true if we should rebroadcast early like a ROUTER */
[[nodiscard]] bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p);
/** The delay to use when we want to flood a message. Use a weighted scale based on SNR */
[[nodiscard]] uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p);
/** If the packet is not already in the late rebroadcast window, move it there */
virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; }
/**
* If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version
* @return Whether a pending packet was removed
*/
virtual bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) { return false; }
/**
* Calculate airtime per
* https://www.rs-online.com/designspark/rel-assets/ds-assets/uploads/knowledge-items/application-notes-for-the-internet-of-things/LoRa%20Design%20Guide.pdf
* section 4
*
* @return num msecs for the packet
*/
[[nodiscard]] uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false);
[[nodiscard]] virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0;
/**
* Get the channel we saved.
*/
[[nodiscard]] uint32_t getChannelNum();
/**
* Get the frequency we saved.
*/
[[nodiscard]] virtual float getFreq();
/// Some boards (1st gen Pinetab Lora module) have broken IRQ wires, so we need to poll via i2c registers
virtual bool isIRQPending() { return false; }
// Whether we use the default frequency slot given our LoRa config (region and modem preset)
static bool uses_default_frequency_slot;
// Whether we have a custom channel name
static bool uses_custom_channel_name;
static bool checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp);
// Check if a candidate region is compatible and valid, with no side effects (safe for
// speculative UI checks). prospectiveLicensedOwner is for a UI flow that requires
// confirmation before it sets the owner licensed. errBuf, if given, receives the failure reason.
static bool checkConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig, char *errBuf = nullptr, size_t errLen = 0,
bool prospectiveLicensedOwner = false);
// Check if a candidate region is compatible and valid. On failure, logs at ERROR,
// records a critical error, and sends a client notification.
static bool validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig);
// Check if a candidate radio configuration is valid.
static bool validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig);
// Make a candidate radio configuration valid, even if it isn't.
static void clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig);
// If preset is locked to a sibling of currentRegion among the swappable EU regions
// (EU_868/EU_866/EU_N_868), return the sibling region owning the preset, else nullptr.
static const RegionInfo *regionSwapForPreset(meshtastic_Config_LoRaConfig_RegionCode currentRegion,
meshtastic_Config_LoRaConfig_ModemPreset preset);
protected:
int8_t power = 17; // Set by applyModemConfig()
float savedFreq;
uint32_t savedChannelNum;
/***
* given a packet set sendingPacket and decode the protobufs into radiobuf. Returns # of bytes to send (including the
* PacketHeader & payload).
*
* Used as the first step of
*/
[[nodiscard]] size_t beginSending(meshtastic_MeshPacket *p);
/** beginSending()'s encoding into radioBuffer, without claiming sendingPacket; returns the bytes to send */
[[nodiscard]] size_t encodeRadioBuffer(meshtastic_MeshPacket *p);
/**
* Some regulatory regions limit xmit power.
* This function should be called by subclasses after setting their desired power. It might lower it
*/
void limitPower(int8_t MAX_POWER);
/**
* Save the frequency we selected for later reuse.
*/
virtual void saveFreq(float savedFreq);
/**
* Save the channel we selected for later reuse.
*/
virtual void saveChannelNum(uint32_t savedChannelNum);
/**
* Get current RSSI reading from the radio.
* Returns 0 if not available.
*/
virtual int16_t getCurrentRSSI() { return 0; }
private:
/**
* Convert our modemConfig enum into wf, sf, etc...
*
* These parameters will be pull from the channelSettings global
*/
void applyModemConfig();
/// Return 0 if sleep is okay. A non-NULL argument means the radio is about to be powered
/// down (deep sleep / shutdown), see doPreflightSleep()
int preflightSleepCb(void *deepSleep = NULL) { return canSleep(deepSleep != NULL) ? 0 : 1; }
int notifyDeepSleepCb(void *unused = NULL);
int reloadConfig(void *unused)
{
reconfigure();
return 0;
}
};
std::unique_ptr<RadioInterface> initLoRa();
// What the plain-RX poll does with PREAMBLE/HEADER flags that no RX_DONE has consumed.
enum class StaleRxFlagAction : uint8_t { Keep, ClearPreamble, ClearHeaderErr, Rearm };
// RX_DONE clears every flag, so one still latched a max packet after first sight has no frame behind it.
// One header raises HEADER_VALID or HEADER_ERR and never both (SX1262 DS rev 2.1 table 13-29), so an error
// beside a valid header is an earlier frame's and is retired without touching the valid header's window.
// Only where a header error wedges RX (SX1280 DS rev 3.3 16.2) is a stale one worth the standby in a re-arm.
constexpr StaleRxFlagAction staleRxFlagAction(bool validSeen, bool errSeen, bool errWedgesRx, uint32_t sinceFirstSeenMsec,
uint32_t maxPacketTimeMsec)
{
return validSeen && errSeen ? StaleRxFlagAction::ClearHeaderErr
: sinceFirstSeenMsec < maxPacketTimeMsec ? StaleRxFlagAction::Keep
: validSeen ? StaleRxFlagAction::Rearm
: !errSeen ? StaleRxFlagAction::ClearPreamble
: errWedgesRx ? StaleRxFlagAction::Rearm
: StaleRxFlagAction::ClearHeaderErr;
}
/// Debug printing for packets
void printPacket(const char *prefix, const meshtastic_MeshPacket *p);