mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-19 16:44:22 +00:00
Commit 22eb9b87 ("Revert 'Merge remote-tracking branch origin/dev...'")
reverted an entire upstream merge to escape a bad merge state, dropping
860 lines across 66 files. Among the collateral never reconciled on a
later re-merge was eb4fa032's token-bucket duty-cycle enforcement — the
mechanism that keeps nodes under a configured airtime budget (and EU
868 MHz nodes under the legally-mandated duty cycle). The fork had fallen
back to fixed per-packet spacing (getAirtimeBudgetFactor reverted to 2.0),
losing the windowed enforcement.
This was never an intentional design choice, so restoring it re-aligns the
fork with upstream and REDUCES the merge-conflict surface: Dispatcher.{h,cpp}
now diverge from upstream by watchdog additions only (77 insertions, 0
deletions) instead of rewriting checkSend()/loop().
Restored from upstream: updateTxBudget/tx_budget_ms/duty_cycle_window_ms/
getRemainingTxBudget/getDutyCycleWindowMs and the windowed budget logic in
Dispatcher; getOutboundTotal() and the 0xFFFFFFFF count-all sentinel in
StaticPoolPacketManager; the getOutboundTotal() call in StatsFormatHelper.
Re-applied the fork's MQTT radio-watchdog on top as pure additions
(#ifdef WITH_MQTT_BRIDGE), keeping formatRadioDiag.
Stored airtime_factor settings keep their meaning: fork's t*factor spacing
and upstream's 1/(1+factor) windowed budget yield the same steady-state
duty cycle; upstream additionally allows short bursts within the window.
Phase 2 (CAD / radio_fem_rxgain, which touch NodePrefs persistence and
per-board FEM wiring) is documented in RESTORE_UPSTREAM_NOTES.md, not done
here. Builds: Heltec_v3 observer + plain repeater. NEEDS ON-DEVICE
duty-cycle validation before merge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
233 lines
7.2 KiB
C++
233 lines
7.2 KiB
C++
#pragma once
|
|
|
|
#include <MeshCore.h>
|
|
#include <Identity.h>
|
|
#include <Packet.h>
|
|
#include <Utils.h>
|
|
#include <string.h>
|
|
|
|
namespace mesh {
|
|
|
|
/**
|
|
* \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:
|
|
virtual void begin() { }
|
|
|
|
/**
|
|
* \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;
|
|
|
|
/**
|
|
* \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;
|
|
|
|
/**
|
|
* \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() { }
|
|
|
|
virtual int getNoiseFloor() const { return 0; }
|
|
|
|
virtual void triggerNoiseFloorCalibrate(int threshold) { }
|
|
|
|
virtual void setCADEnabled(bool enable) { }
|
|
|
|
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;
|
|
|
|
/**
|
|
* \returns true if the radio is currently mid-receive of a packet.
|
|
*/
|
|
virtual bool isReceiving() { return false; }
|
|
|
|
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 void queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0;
|
|
virtual Packet* getNextOutbound(uint32_t now) = 0; // by priority
|
|
virtual int getOutboundCount(uint32_t now) const = 0;
|
|
virtual int getOutboundTotal() const = 0;
|
|
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;
|
|
};
|
|
|
|
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 {
|
|
Packet* outbound; // current outbound packet
|
|
unsigned long outbound_expiry, outbound_start, total_air_time, rx_air_time;
|
|
unsigned long last_watchdog_recovery;
|
|
unsigned long last_radio_active_ms; // updated on any TX or RX event; used by watchdog
|
|
unsigned long next_tx_time;
|
|
unsigned long cad_busy_start;
|
|
unsigned long radio_nonrx_start;
|
|
unsigned long next_floor_calib_time, next_agc_reset_time;
|
|
bool prev_isrecv_mode;
|
|
uint32_t n_sent_flood, n_sent_direct;
|
|
uint32_t n_recv_flood, n_recv_direct;
|
|
unsigned long tx_budget_ms;
|
|
unsigned long last_budget_update;
|
|
unsigned long duty_cycle_window_ms;
|
|
|
|
void processRecvPacket(Packet* pkt);
|
|
void updateTxBudget();
|
|
|
|
protected:
|
|
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;
|
|
total_air_time = rx_air_time = 0;
|
|
next_tx_time = ms.getMillis();
|
|
cad_busy_start = 0;
|
|
next_floor_calib_time = next_agc_reset_time = 0;
|
|
_err_flags = 0;
|
|
radio_nonrx_start = 0;
|
|
prev_isrecv_mode = true;
|
|
tx_budget_ms = 0;
|
|
last_budget_update = 0;
|
|
duty_cycle_window_ms = 3600000;
|
|
last_watchdog_recovery = 0;
|
|
last_radio_active_ms = 0;
|
|
}
|
|
|
|
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 const char* getLogDateTime() { return ""; }
|
|
|
|
virtual float getAirtimeBudgetFactor() const;
|
|
virtual int calcRxDelay(float score, uint32_t air_time) const;
|
|
virtual uint32_t getCADFailRetryDelay() const;
|
|
virtual uint32_t getCADFailMaxDuration() const;
|
|
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
|
|
|
|
public:
|
|
void begin();
|
|
void loop();
|
|
|
|
Packet* obtainNewPacket();
|
|
void releasePacket(Packet* packet);
|
|
void 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; }
|
|
uint16_t getErrFlags() const { return _err_flags; } // Get error flags
|
|
bool hasOutbound() const { return outbound != NULL; }
|
|
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;
|
|
|
|
bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len);
|
|
|
|
private:
|
|
void checkRecv();
|
|
void checkSend();
|
|
};
|
|
|
|
}
|