Files
firmware/src/mesh/RadioInterface.h
T
Jonathan BennettandClaude Opus 5 e5c559a1c8 Make the frame-end anchor atomic
The radio worker writes lastFrameEndMs from onNotify() while a draw for a
packet being queued reads it on whatever thread called send(), so the access
was an unsynchronized read of a plain uint32_t. std::atomic with relaxed
ordering is free on every target here and matches MeshService's fromNum.

Relaxed is the right ordering: the anchor publishes nothing but itself, both
writers are the radio worker so the compare-then-store races with no other
write, and losing the race against a reader only anchors that one draw to the
frame before last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkrqYBzhAxyybmoezb1TJV
2026-10-10 15:24:32 +00:00

404 lines
17 KiB
C++

#pragma once
#include "MemoryPool.h"
#include "MeshTypes.h"
#include "Observer.h"
#include "PointerQueue.h"
#include "airtime.h"
#include "error.h"
#include <atomic>
#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();
/** millis() when the last frame this node sent or heard left the air, 0 before the first.
* Only read by a draw that was asked for a slot parity; the driver keeps it up to date so any
* module can ask at any time.
*
* Atomic because the radio worker writes it from onNotify() while a draw for a packet being
* queued reads it on whatever thread called send(). Relaxed is enough: it publishes nothing
* but itself, and losing the race just anchors that one draw to the frame before last. */
std::atomic<uint32_t> lastFrameEndMs{0};
/** "tx" or "rx" for the frame lastFrameEndMs came from, for the trace line */
const char *lastFrameEndWhat = "none";
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.
* p is the packet about to go out (NULL when there is none yet): its slot_parity, when set,
* puts the draw on that parity's slots of the grid anchored to the last frame's air end. */
[[nodiscard]] uint32_t getTxDelayMsec(const meshtastic_MeshPacket *p = nullptr);
/** Note when a frame this node sent or heard left the air, for the anchored backoff grid.
* A stamp older than the one already held is a frame delivered late, and is ignored. */
void noteFrameEnd(uint32_t endMs, const char *what);
/** 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);
/** A backoff of up to `slots` slots on one parity of the grid anchored to the last frame's air
* end, returned as a delay from now. Two nodes on opposite parities that redraw after the same
* frame are then always at least one whole slot apart, whatever their handling delay was. */
[[nodiscard]] uint32_t getAnchoredSlotDelayMsec(uint32_t slots, meshtastic_MeshPacket_SlotParity parity);
/** The grid arithmetic behind it, with the anchor and the draw passed in: the delay that lands
* on the `pairsDrawn`-th slot of `parity`, counting from the first slot of that parity not yet
* started `sinceEndMs` after the anchor. */
[[nodiscard]] static uint32_t anchoredSlotDelayMsec(uint32_t sinceEndMs, uint32_t slotMsec, uint32_t pairsDrawn,
meshtastic_MeshPacket_SlotParity parity);
/** 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, Rearm };
// RX_DONE clears every flag, so one still latched a max packet after first sight has no frame behind it.
// A header may mean a wedged demodulator (SX1280 DS 16.2), so re-arm; a bare preamble is only cleared.
constexpr StaleRxFlagAction staleRxFlagAction(bool headerSeen, uint32_t sinceFirstSeenMsec, uint32_t maxPacketTimeMsec)
{
return sinceFirstSeenMsec < maxPacketTimeMsec ? StaleRxFlagAction::Keep
: headerSeen ? StaleRxFlagAction::Rearm
: StaleRxFlagAction::ClearPreamble;
}
/// Debug printing for packets
void printPacket(const char *prefix, const meshtastic_MeshPacket *p);