#pragma once #include #include #include #include #include #ifndef RADIO_LIVENESS_SOFT_ONLY #include #endif #include 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 void begin() { } virtual RadioProfiles* profiles() { return nullptr; } virtual const RadioProfiles* profiles() const { return nullptr; } virtual bool validateProfile(const RadioProfileParams&) const { return false; } 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() { } // 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(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; } 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() { } 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 { 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, next_agc_reset_time; int armed_agc_reset_interval; bool agc_reset_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; uint8_t receive_context_profile = 0; uint32_t receive_context_generation = 0; bool receive_context_active = false; uint32_t profile_cad_busy[2] = {}; 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 = next_agc_reset_time = 0; armed_agc_reset_interval = 0; agc_reset_armed = false; _err_flags = 0; radio_nonrx_start = 0; 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) { } 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 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; 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; public: void begin(); void loop(); Radio* getProfileRadio() { return _radio; } bool isDualRadioActive() const { return _radio->profiles() && _radio->profiles()->enabled(); } 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; } 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: #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(); }; }