Files
MeshCore/src/Dispatcher.h
T

722 lines
30 KiB
C++

#pragma once
#include <MeshCore.h>
#include <Identity.h>
#include <Packet.h>
#include <Utils.h>
#include <RadioProfiles.h>
#include <helpers/MeshtextConfig.h>
#include <helpers/MorseTx.h>
#include <helpers/MorseRx.h>
#include <helpers/AGCResetScheduler.h>
#include <helpers/BeaconConfig.h>
#ifndef RADIO_LIVENESS_SOFT_ONLY
#include <helpers/RadioLivenessTracker.h>
#endif
#include <string.h>
namespace mesh {
enum class RadioParamApplyResult : uint8_t {
APPLIED,
BUSY,
FAILED
};
/**
* \brief Abstraction of local/volatile clock with Millisecond granularity.
*/
class MillisecondClock {
public:
virtual unsigned long getMillis() = 0;
};
/**
* \brief Abstraction of this device's packet radio.
*/
class Radio {
public:
static constexpr uint32_t CarrierWaveDefaultMillis = 10000;
static constexpr uint32_t CarrierWaveMaxMillis = 60000;
virtual bool supportsCarrierWave() const { return false; }
// duration_ms == 0 stops either carrier; there is one physical transmitter.
virtual RadioParamApplyResult setCarrierWave(uint8_t profile, uint32_t duration_ms) {
return RadioParamApplyResult::FAILED;
}
virtual bool isCarrierWaveActive() const { return false; }
virtual uint8_t carrierWaveProfile() const { return 0; }
virtual uint32_t carrierWaveRemainingMillis() const { return 0; }
virtual bool supportsMorseTx() const { return false; }
virtual MorseTxResult startMorseTx(float frequency_mhz, uint8_t wpm, const char* text) {
(void)frequency_mhz; (void)wpm; (void)text;
return MorseTxResult::Unsupported;
}
virtual MorseTxResult stopMorseTx() { return MorseTxResult::Unsupported; }
virtual bool isMorseTxActive() const { return false; }
virtual float morseTxFrequency() const { return MorseTx::DefaultFrequencyMHz; }
virtual uint8_t morseTxWpm() const { return MorseTx::DefaultWpm; }
virtual uint32_t morseTxRemainingMillis() const { return 0; }
virtual bool supportsMorseRx() const { return false; }
virtual MorseRxResult startMorseRx(float frequency_mhz, uint8_t wpm, uint32_t duration_ms) {
(void)frequency_mhz; (void)wpm; (void)duration_ms;
return MorseRxResult::Unsupported;
}
virtual MorseRxResult stopMorseRx() { return MorseRxResult::Unsupported; }
virtual bool isMorseRxActive() const { return false; }
virtual bool morseRxRecovering() const { return false; }
virtual bool morseRxLearning() const { return false; }
virtual float morseRxFrequency() const { return MorseTx::DefaultFrequencyMHz; }
virtual float morseRxBandwidthKHz() const { return 0; }
virtual uint32_t morseRxRemainingMillis() const { return 0; }
virtual uint32_t morseCaptureGeneration() const { return 0; }
virtual bool morseRxDiagnostics(MorseRxDiagnostics& out) const { out = {}; return false; }
virtual size_t morseCaptureLength() const { return 0; }
virtual bool morseCaptureTruncated() const { return false; }
virtual const char* morseCaptureText() const { return ""; }
virtual size_t readMorseCapture(size_t offset, char* out, size_t capacity) const {
(void)offset;
if (out && capacity) out[0] = 0;
return 0;
}
virtual void clearMorseCapture() { }
#if defined(MESH_MORSE_HIL) && MESH_MORSE_HIL
virtual size_t morseTraceLength() const { return 0; }
virtual uint32_t morseTraceTotal() const { return 0; }
virtual bool morseTraceTruncated() const { return false; }
virtual bool readMorseTrace(size_t offset, MorseRssiTraceEntry& out) const {
(void)offset; (void)out; return false;
}
#endif
virtual void begin() { }
virtual RadioProfiles* profiles() { return nullptr; }
virtual const RadioProfiles* profiles() const { return nullptr; }
virtual bool validateProfile(const RadioProfileParams&) const { return false; }
virtual bool supportsMeshtext() const { return false; }
virtual bool supportsEthernetTunnel() const { return false; }
#if MESH_ENABLE_TX_BEACONS
// Check the requested antenna power at the prospective beacon frequency.
// Validation must not retune the radio or alter its current power setting.
virtual bool validateBeaconPower(float, int8_t) const { return false; }
virtual RadioParamApplyResult prepareBeaconTransmit(const RadioProfileParams&, int8_t) {
return RadioParamApplyResult::FAILED;
}
virtual RadioParamApplyResult restoreBeaconReceive() { return RadioParamApplyResult::APPLIED; }
virtual bool isBeaconRadioOwned() const { return false; }
virtual bool isBeaconChannelBusy() { return true; }
virtual uint32_t getBeaconAirtime(const RadioProfileParams&, int) { return 0; }
#endif
virtual uint8_t receiveProfile() const { return 0; }
virtual uint32_t receiveProfileGeneration() const {
return profiles() ? profiles()->generation[receiveProfile()] : 0;
}
virtual RadioParamApplyResult prepareTransmitProfile(uint8_t profile) {
return profile == 0 ? RadioParamApplyResult::APPLIED : RadioParamApplyResult::FAILED;
}
virtual RadioParamApplyResult prepareTransmitProfile(uint8_t profile, bool reply_rx_override) {
(void)reply_rx_override;
return prepareTransmitProfile(profile);
}
virtual uint16_t profilePreamble(uint8_t) const { return 0; }
virtual uint32_t getProfileAirtime(uint8_t, int len_bytes, uint8_t = 0) {
return getEstAirtimeFor(len_bytes);
}
virtual RadioParamApplyResult trySetPrimaryParams(const RadioProfileParams& p,
bool temporary, const uint32_t* timings = nullptr) {
(void)temporary;
return trySetParams(p.freq, p.bw, p.sf, p.cr, timings);
}
/**
* \brief polls for incoming raw packet.
* \param bytes destination to store incoming raw packet.
* \param sz maximum packet size allowed.
* \returns 0 if no incoming data, otherwise length of complete packet received.
*/
virtual int recvRaw(uint8_t* bytes, int sz) = 0;
/** Called after the received packet and its radio metadata have been processed. */
virtual void onReceiveProcessed() { }
/**
* \returns estimated transmit air-time needed for packet of 'len_bytes', in milliseconds.
*/
virtual uint32_t getEstAirtimeFor(int len_bytes) = 0;
virtual float packetScore(float snr, int packet_len) = 0;
/**
* \brief starts the raw packet send. (no wait)
* \param bytes the raw packet data
* \param len the length in bytes
* \returns true if successfully started
*/
virtual bool startSendRaw(const uint8_t* bytes, int len) = 0;
/**
* \brief Sets LoRa coding rate for subsequent transmits/receives.
* \returns true if the radio accepted the coding rate.
*/
virtual bool setCodingRate(uint8_t cr) { return false; }
virtual RadioParamApplyResult tryRestoreCodingRate(uint8_t cr) {
return setCodingRate(cr) ? RadioParamApplyResult::APPLIED : RadioParamApplyResult::FAILED;
}
/**
* \returns true if the previous 'startSendRaw()' completed successfully.
*/
virtual bool isSendComplete() = 0;
/**
* \brief a hook for doing any necessary clean up after transmit.
*/
virtual void onSendFinished() = 0;
/**
* \brief do any processing needed on each loop cycle
*/
virtual void loop() { }
virtual void idle() { }
virtual void startRecv() { }
// A tracker may stop RF reception between explicit check-in windows. The
// dispatcher must be made unavailable while asleep so watchdog recovery
// cannot restart RX. Unsupported transports decline without changing state.
virtual bool setTrackerSleep(bool sleeping) { (void)sleeping; return false; }
// Recover a radio whose MCU driver is still running but whose RF state has
// stopped making progress. hard=false is a safe receive/AGC re-arm;
// hard=true may reset only the radio peripheral. The default remains useful
// for simple radios that do not expose a dedicated reset.
virtual bool recoverRadio(bool hard) {
(void)hard;
idle();
startRecv();
return true;
}
virtual int getNoiseFloor() const { return 0; }
virtual float getNoiseFloorDbm() const {
return static_cast<float>(getNoiseFloor());
}
// Profile zero is the primary radio. Radios that scan a second profile can
// expose its independent floor without changing the legacy stats API.
virtual float getNoiseFloorDbm(uint8_t profile) const {
(void)profile;
return getNoiseFloorDbm();
}
// A zero result means there is no active sampled calibration window (or
// that this transport does not expose one). The estimate itself may remain
// visible while a later refresh is being collected.
virtual float getNoiseFloorCalibrationSecondsRemaining() const { return 0.0f; }
virtual float getNoiseFloorCalibrationSecondsRemaining(uint8_t profile) const {
(void)profile;
return getNoiseFloorCalibrationSecondsRemaining();
}
virtual bool isCalibratingNoiseFloor() const { return false; }
// MCU service deadline only: RF calibration can continue in continuous RX
// while the CPU idles between readings. Unknown implementations retain the
// conservative whole-batch veto rather than inventing a safe sample gap.
virtual bool getNoiseFloorWakeDelay(uint32_t now, uint32_t& delay_ms) const {
(void)now;
if (!isCalibratingNoiseFloor()) return false;
delay_ms = 0;
return true;
}
// A failed receive start may need a later retry without keeping the MCU
// awake. Transports without a deferred receive retry retain their cadence.
virtual bool getReceiveRetryDelay(uint32_t now, uint32_t& delay_ms) const {
(void)now; (void)delay_ms; return false;
}
// A supported profile scanner may idle the MCU while RF keeps receiving.
// Return a conservative whole-millisecond service budget, not an RF sleep
// permission. False (unsupported/not ready) or zero retains fast polling.
// The caller must preserve every other work deadline and the validated
// board wake contract, then recheck admission immediately before waiting.
virtual bool getProfileIdleDelay(uint32_t& delay_millis) const {
(void)delay_millis; return false;
}
virtual void triggerNoiseFloorCalibrate(int threshold) { }
// Discard the current estimate and establish a new baseline. Use this after
// changing receive-path gain, where the old floor is no longer a valid
// filter reference for normal periodic calibration.
virtual void recalibrateNoiseFloor() { }
virtual void setCADEnabled(bool enable) { }
// LoRa wrappers shadow these with their configurable CAD timeout. Other
// transports (such as ESP-NOW) support only the automatic/default value.
bool setCADScanTimeoutMillis(uint32_t timeout_ms) { return timeout_ms == 0; }
uint32_t getCADScanTimeoutMillis() const { return 0; }
virtual void resetAGC() { }
// True means the maintenance sequence was admitted, not that receiver
// sensitivity has been measured. Busy implementations may defer it.
virtual bool tryResetAGC() { resetAGC(); return true; }
virtual uint32_t getRecoveryCount() const { return 0; }
virtual uint8_t getRadioState() const { return 0; }
virtual unsigned long getLastRecvMillis() const { return 0; }
virtual unsigned long getLastRadioInterruptMillis() const { return 0; }
virtual bool isInRecvMode() const = 0;
virtual bool supportsRxPowerSaving() const { return false; }
virtual bool setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) { return !enabled; }
virtual bool isRxPowerSavingContinuousFallback() const { return false; }
virtual bool supportsRxPowerSavingRfRxDisable() const { return false; }
virtual bool setRxPowerSavingRfRxDisabled(bool disabled) {
(void)disabled;
return false;
}
virtual bool isRxPowerSavingRfRxDisabled() const { return false; }
/**
* \brief Attempts to change the radio modulation parameters without waiting.
*
* BUSY means the request is valid but the radio is temporarily unable to
* leave RX/TX. Callers may retry it from their main loop. FAILED means the
* radio attempted or rejected the change and retrying the same tuple is not
* expected to help.
*/
virtual RadioParamApplyResult trySetParams(float freq, float bw, uint8_t sf, uint8_t cr,
const uint32_t* rx_ps_timings = nullptr) {
(void)freq;
(void)bw;
(void)sf;
(void)cr;
(void)rx_ps_timings;
return RadioParamApplyResult::FAILED;
}
/**
* \returns true if the radio is currently mid-receive of a packet.
*/
virtual bool isReceiving() { return false; }
/**
* \brief Non-invasive channel check for a queued retry.
*
* Implementations should avoid CAD or any operation that restarts RX, because
* doing so can hide the downstream forwarding echo that would cancel the
* retry. Radios without a passive implementation retain the legacy check.
*/
virtual bool isReceivingPassive(int interference_margin_db) {
(void)interference_margin_db;
return isReceiving();
}
virtual float getLastRSSI() const { return 0; }
virtual float getLastSNR() const { return 0; }
/**
* \returns number of receive errors (e.g. CRC failures) since last reset; 0 if not tracked.
*/
virtual uint32_t getPacketsRecvErrors() const { return 0; }
};
/**
* \brief An abstraction for managing instances of Packets (eg. in a static pool),
* and for managing the outbound packet queue.
*/
class PacketManager {
public:
virtual Packet* allocNew() = 0;
virtual void free(Packet* packet) = 0;
virtual bool queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0;
virtual Packet* getNextOutbound(uint32_t now) = 0; // by priority
virtual Packet* peekNextOutbound(uint32_t now) {
(void)now;
return NULL;
}
// Managers that shed queued outbound packets return them here instead of
// freeing them silently. Dispatcher will run the normal send-failure
// lifecycle hook before returning each packet to the pool.
virtual Packet* getNextDroppedOutbound() { return NULL; }
virtual bool deferOutbound(Packet*, uint32_t) { return false; }
// Deliberate local pacing is not congestion. Managers with stale-packet
// expiry can pause that age for this interval, preserving any prior backlog.
virtual bool deferOutboundForPacing(Packet* packet, uint32_t now, uint32_t scheduled_for) {
(void)now;
return deferOutbound(packet, scheduled_for);
}
virtual int getOutboundCount(uint32_t now) const = 0;
virtual int getOutboundTotal() const = 0;
// Returns the earliest runnable time in the queue. A queue with any overdue
// entry reports `now`, which keeps rollover-safe timer comparisons local to
// the queue implementation.
virtual bool getNextOutboundTime(uint32_t now, uint32_t& scheduled_for) const {
(void)now;
(void)scheduled_for;
return false;
}
virtual int getFreeCount() const = 0;
virtual Packet* getOutboundByIdx(int i) = 0;
virtual Packet* removeOutboundByIdx(int i) = 0;
virtual void queueInbound(Packet* packet, uint32_t scheduled_for) = 0;
virtual Packet* getNextInbound(uint32_t now) = 0;
virtual bool getNextInboundTime(uint32_t now, uint32_t& scheduled_for) const {
(void)now;
(void)scheduled_for;
return false;
}
};
typedef uint32_t DispatcherAction;
#define ACTION_RELEASE (0)
#define ACTION_MANUAL_HOLD (1)
#define ACTION_RETRANSMIT(pri) (((uint32_t)1 + (pri))<<24)
#define ACTION_RETRANSMIT_DELAYED(pri, _delay) ((((uint32_t)1 + (pri))<<24) | (_delay))
#define ERR_EVENT_FULL (1 << 0)
#define ERR_EVENT_CAD_TIMEOUT (1 << 1)
#define ERR_EVENT_STARTRX_TIMEOUT (1 << 2)
#define ERR_EVENT_RADIO_WATCHDOG (1 << 3)
#ifndef RADIO_WATCHDOG_MS
#define RADIO_WATCHDOG_MS 300000 // 5 minutes
#endif
/**
* \brief The low-level task that manages detecting incoming Packets, and the queueing
* and scheduling of outbound Packets.
*/
class Dispatcher {
friend class FleetChannel; // bounded deferred receiver and exact reply retirement
enum class OutboundCancellation : uint8_t { None, Cancelled, Delivered };
Packet* outbound; // current outbound packet
unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time;
unsigned long outbound_radio_retry_at;
uint32_t outbound_radio_prepare_deadline = 0;
#ifdef RADIO_LIVENESS_SOFT_ONLY
unsigned long rx_watchdog_window_start;
#else
RadioLivenessTracker radio_liveness;
#endif
uint8_t outbound_restore_cr;
unsigned long next_tx_time;
uint32_t ota_tx_finished_at = 0;
uint32_t ota_tx_airtime = 0;
unsigned long cad_busy_start;
unsigned long radio_nonrx_start;
unsigned long next_floor_calib_time;
bool radio_nonrx_timer_armed;
bool prev_isrecv_mode;
bool radio_available;
bool dispatcher_started;
bool outbound_radio_retry_pending;
bool outbound_radio_retry_used;
OutboundCancellation outbound_cancellation = OutboundCancellation::None;
#ifndef RADIO_LIVENESS_SOFT_ONLY
bool nonrx_soft_recovery_attempted;
#endif
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
uint32_t last_meshcore_recv_millis;
unsigned long tx_budget_ms;
unsigned long last_budget_update;
unsigned long duty_cycle_window_ms;
#if MESH_ENABLE_FOREIGN_RADIO
// One bounded user-send slot; never placed in the MeshCore packet pool.
uint8_t foreign_radio_bytes[MAX_TRANS_UNIT] = {};
uint16_t foreign_radio_length = 0;
uint8_t foreign_radio_profile = 0;
RadioProfileProtocol foreign_radio_protocol = RadioProfileProtocol::MeshCore;
uint32_t foreign_radio_generation = 0;
#if MESH_ENABLE_OTA_KISS_DIRECT
uint32_t foreign_radio_ota_epoch = 0;
#endif
uint32_t foreign_radio_retry_at = 0, foreign_radio_prepare_deadline = 0;
uint32_t foreign_radio_start = 0, foreign_radio_expiry = 0;
bool foreign_radio_sending = false;
bool foreign_radio_yield_meshcore = false;
bool isForeignRadioCurrent() const;
bool supportsForeignRadioProtocol(RadioProfileProtocol protocol) const;
bool serviceForeignRadioFrame();
bool checkForeignRadioSend();
void finishForeignRadioFrame(bool success, bool account_airtime);
#endif
void processRecvPacket(Packet* pkt);
void releaseDroppedOutbound();
bool startOutboundTransmit();
bool scheduleOutboundRadioRetry();
bool isTransmitChannelReady(const Packet* packet, uint32_t& retry_delay);
void failOutboundTransmit();
bool restoreOutboundTxOverrides();
uint32_t outbound_restore_retry_at = 0;
uint32_t outbound_restore_recovery_at = 0;
void updateTxBudget();
protected:
// Preserve RX affinity while an authenticated command is handled outside
// the receive call stack. Replies inherit this profile and session.
class ReceiveProfileScope {
Dispatcher& owner;
uint8_t old_profile;
uint32_t old_generation;
bool old_active;
public:
ReceiveProfileScope(Dispatcher& dispatcher, uint8_t profile, uint32_t generation)
: owner(dispatcher), old_profile(owner.receive_context_profile),
old_generation(owner.receive_context_generation), old_active(owner.receive_context_active) {
owner.receive_context_profile = profile;
owner.receive_context_generation = generation;
owner.receive_context_active = true;
}
~ReceiveProfileScope() {
owner.receive_context_profile = old_profile;
owner.receive_context_generation = old_generation;
owner.receive_context_active = old_active;
}
ReceiveProfileScope(const ReceiveProfileScope&) = delete;
ReceiveProfileScope& operator=(const ReceiveProfileScope&) = delete;
};
PacketManager* _mgr;
Radio* _radio;
MillisecondClock* _ms;
uint16_t _err_flags;
Dispatcher(Radio& radio, MillisecondClock& ms, PacketManager& mgr)
: _radio(&radio), _ms(&ms), _mgr(&mgr)
{
outbound = NULL;
outbound_radio_retry_at = 0;
outbound_radio_retry_pending = false;
outbound_radio_retry_used = false;
outbound_restore_cr = 0;
total_air_time = rx_air_time = 0;
last_meshcore_recv_millis = 0;
next_tx_time = ms.getMillis();
cad_busy_start = 0;
next_floor_calib_time = 0;
_err_flags = 0;
radio_nonrx_start = 0;
radio_nonrx_timer_armed = false;
prev_isrecv_mode = true;
radio_available = true;
dispatcher_started = false;
tx_budget_ms = 0;
last_budget_update = 0;
duty_cycle_window_ms = 3600000;
#ifdef RADIO_LIVENESS_SOFT_ONLY
rx_watchdog_window_start = 0;
#else
nonrx_soft_recovery_attempted = false;
#endif
}
virtual DispatcherAction onRecvPacket(Packet* pkt) = 0;
virtual void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { } // custom hook
virtual void logRx(Packet* packet, int len, float score) { } // hooks for custom logging
virtual void logTx(Packet* packet, int len) { }
virtual void logTxFail(Packet* packet, int len) { }
virtual void onTracePacketQueuedForSend(Packet* packet) { }
virtual void onSendComplete(Packet* packet) { }
virtual void onSendFail(Packet* packet) { }
#if MESH_ENABLE_FOREIGN_RADIO
// Foreign RX is consumed here before MeshCore parsing or any raw bridge.
// Returning false drops the frame; it never falls through to MeshCore.
virtual bool onForeignRadioFrame(const uint8_t*, size_t, uint8_t, uint32_t, float, float) { return false; }
virtual void onForeignRadioSendComplete(bool) { }
virtual bool allowForeignRadioTransmit() const { return true; }
#if MESH_ENABLE_OTA_KISS_DIRECT
// The KISS host framing is off-air. These are bounded, zero-hop OTA radio
// frames, never Packet objects and never a second physical TX owner.
virtual bool onDirectOtaRadioFrame(const uint8_t*, size_t, uint8_t, uint32_t) { return false; }
virtual bool allowDirectOtaTransmit(uint8_t, uint32_t) const { return false; }
// Captured when queued: abort, fallback and workspace replacement retire
// this token even when the physical temporary profile remains unchanged.
virtual uint32_t directOtaEpoch() const { return 0; }
bool queueDirectOtaRadioFrame(const uint8_t* message, uint16_t length,
uint8_t profile, uint32_t generation);
#endif
bool queueForeignRadioFrame(const uint8_t* bytes, size_t length, uint8_t profile, uint32_t generation,
RadioProfileProtocol protocol = RadioProfileProtocol::Meshtext);
RadioProfileProtocol queuedForeignRadioProtocol() const { return foreign_radio_protocol; }
// Cancel host work that has not reached RF. An in-flight frame cannot be
// unsent and retains normal completion/airtime accounting.
bool discardQueuedForeignRadioFrame(RadioProfileProtocol protocol);
bool hasForeignRadioFrame() const { return foreign_radio_length != 0; }
#endif
virtual void onRadioProfileCopyQueued(Packet* packet, const Packet* original, uint8_t priority) { }
// Policy gate for the other profile only; same-profile TX remains eligible.
// Called before admitting copies (including bound retry packets) to a queue.
virtual bool allowRadioProfileCross(const Packet* packet) { return true; }
virtual const char* getLogDateTime() { return ""; }
virtual float getAirtimeBudgetFactor() const;
virtual float getOtaSpeedFactor() const { return 1.0f; }
virtual float getOtaPacketSpeedFactor() const { return getOtaSpeedFactor(); }
virtual int calcRxDelay(float score, uint32_t air_time) const;
virtual bool shouldBypassRxDelay(const Packet* packet) {
(void)packet;
return false;
}
virtual int calcRxDelayForPacket(const Packet* packet, float score,
uint32_t air_time) {
return shouldBypassRxDelay(packet) ? 0 : calcRxDelay(score, air_time);
}
virtual uint32_t getCADFailRetryDelay() const;
virtual uint32_t getCADFailMaxDuration() const;
virtual uint8_t getDefaultTxCodingRate() const { return 0; }
virtual bool allowPacketTransmit(const Packet* packet) const { return true; }
virtual bool usePassiveChannelCheck(const Packet* packet) const {
(void)packet;
return false;
}
virtual int getRetryInterferenceMargin() const { return 12; }
virtual int getInterferenceThreshold() const { return 0; } // disabled by default
virtual bool getCADEnabled() const { return false; } // hardware CAD disabled by default
virtual int getAGCResetInterval() const { return 0; } // disabled by default
virtual bool useAdaptiveAGC() const { return false; }
virtual unsigned long getDutyCycleWindowMs() const { return 3600000; }
#ifdef WITH_MQTT_BRIDGE
virtual uint32_t getRadioWatchdogMillis() const; // observer-only radio recovery
#endif
const Packet* getOutboundInFlight() const { return outbound; }
// A retry may be retained outside the manager during radio recovery. Mark
// it here and let loop() release it after RX callbacks finish. An on-air
// transmission completes normally, but cannot retry after a later timeout.
void cancelOutboundRadioRetry(const Packet* packet, bool delivered = false) {
if (packet != NULL && packet == outbound) {
outbound_cancellation = delivered ? OutboundCancellation::Delivered : OutboundCancellation::Cancelled;
}
}
// Milliseconds until Dispatcher can next make progress on a queued packet.
// This includes queue schedules, delayed inbound processing, airtime-budget
// waits, and short channel-busy deferrals.
bool getNextQueueWakeDelay(uint32_t& delay_millis) const;
// Timed MCU wakes are not RF leases: an idle tracker can suspend the radio
// across AGC maintenance and a future beacon, but not actual packet work.
bool hasPendingRadioWork() const {
if (hasOutbound() || outbound_restore_cr || _mgr->getOutboundTotal() != 0
|| _radio->isCarrierWaveActive() || _radio->isMorseTxActive()
|| _radio->isMorseRxActive()) return true;
uint32_t ignored;
return _mgr->getNextInboundTime(_ms->getMillis(), ignored)
|| (radio_available && _radio->getReceiveRetryDelay(_ms->getMillis(), ignored));
}
#if MESH_ENABLE_TX_BEACONS
// A deliberately suspended tracker must still wake before the beacon CLI
// consumes its one admission attempt. Other unavailable radios retain the
// existing getNextQueueWakeDelay() policy.
bool getNextBeaconWakeDelay(uint32_t& delay_millis) const {
if (!beacon_wake_enabled) return false;
const int32_t wait = int32_t(beacon_wake_at - uint32_t(_ms->getMillis()));
delay_millis = wait > 0 ? uint32_t(wait) : 0;
return true;
}
#endif
bool hasQueuedWorkDue() const {
uint32_t delay_millis;
return getNextQueueWakeDelay(delay_millis) && delay_millis == 0;
}
bool queueOutboundPacket(Packet* packet, uint8_t priority, uint32_t delay_millis);
bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len);
void setRadioAvailable(bool available);
bool isRadioAvailable() const { return radio_available; }
bool isPacketRadioCurrent(const Packet* packet) const;
// Planned fan-out before transport filters; also bounds OTA queue admission.
uint8_t getTransmitProfileMask(const Packet* packet) const;
uint32_t getTransmitAirtime(const Packet* packet) const;
// TRACE callers supply final DIRECT length; createTrace()'s unset route
// bits would otherwise add four transport-code bytes before sendDirect().
uint32_t getTransmitAirtime(const Packet* packet, int wire_length) const;
public:
// Kept separate from whole-second queue sleep policy: a 50 ms calibration
// deadline needs a bounded millisecond wake, not a zero-second queue veto.
bool getNextNoiseFloorWakeDelay(uint32_t& delay_millis) const;
void begin();
void loop();
#if MESH_ENABLE_TX_BEACONS
~Dispatcher();
bool queueBeaconRadioFrame(const uint8_t* bytes, size_t length,
const RadioProfileParams& params, int8_t power);
void cancelBeaconRadioFrame();
bool hasBeaconRadioFrame() const { return beacon_tx != nullptr; }
void setBeaconWakeDelay(uint32_t now, uint32_t delay, bool enabled) {
beacon_wake_at = now + delay;
beacon_wake_enabled = enabled;
}
#endif
Radio* getProfileRadio() { return _radio; }
bool isDualRadioActive() const { return _radio->profiles() && _radio->profiles()->enabled(); }
bool canIdleDualRadio() const {
if (!isDualRadioActive()) return true;
uint32_t delay_millis = 0;
return radio_available && _radio->getProfileIdleDelay(delay_millis)
&& delay_millis > 0;
}
Packet* obtainNewPacket();
void releasePacket(Packet* packet);
bool sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0);
unsigned long getTotalAirTime() const { return total_air_time; }
unsigned long getReceiveAirTime() const {return rx_air_time; }
unsigned long getRemainingTxBudget() const { return tx_budget_ms; }
uint32_t getNumSentFlood() const { return n_sent_flood; }
uint32_t getNumSentDirect() const { return n_sent_direct; }
uint32_t getNumRecvFlood() const { return n_recv_flood; }
uint32_t getNumRecvDirect() const { return n_recv_direct; }
// Successful MeshCore packet parsing, before receive delays and routing filters.
// Raw radio reads, local sends, and statistics resets do not update this clock.
uint32_t getLastMeshCoreRecvMillis() const { return last_meshcore_recv_millis; }
uint16_t getErrFlags() const { return _err_flags; } // Get error flags
bool hasOutbound() const {
return outbound != NULL
#if MESH_ENABLE_TX_BEACONS
|| hasBeaconRadioFrame()
#endif
#if MESH_ENABLE_FOREIGN_RADIO
|| hasForeignRadioFrame()
#endif
;
}
bool isCurrentOutbound(const Packet* packet) const { return outbound == packet; }
void resetStats() {
n_sent_flood = n_sent_direct = n_recv_flood = n_recv_direct = 0;
_err_flags = 0;
}
// helper methods
bool millisHasNowPassed(unsigned long timestamp) const;
unsigned long futureMillis(int millis_from_now) const;
private:
// Keep the frequently accessed core fields at compact ARM load offsets.
AGCResetScheduler agc_reset;
uint32_t receive_context_generation = 0;
uint32_t profile_cad_busy[2] = {};
uint8_t receive_context_profile = 0;
bool receive_context_active = false;
#if MESH_ENABLE_TX_BEACONS
static constexpr uint32_t BEACON_START_GUARD_MS = 3600000UL;
struct BeaconTransmit;
BeaconTransmit* beacon_tx = nullptr;
uint32_t beacon_wake_at = 0, beacon_last_start = 0;
bool beacon_wake_enabled = false, beacon_has_started = false;
bool serviceBeaconRadioFrame();
bool checkBeaconRadioSend();
void accountBeaconAirtime();
#endif
#if MESH_PACKET_LOGGING
void logPacketLine(const char* direction, const Packet* packet, int len,
bool include_rx_metrics, float score,
uint32_t air_time) __attribute__((noinline));
#endif
void checkRecv();
void checkSend();
};
}