mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-22 20:11:33 +00:00
port vanilla duty cycling logic
This commit is contained in:
1 parent
b57df41753
commit
1092c49a7d
8 files changed
+227
-188
No files matched your search
@@ -391,10 +391,6 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs)
|
||||
size_t off = 0;
|
||||
memcpy(&prefs.airtime_factor, &buf[off], sizeof(float));
|
||||
off += 4;
|
||||
/* Migrate old AF multiplier (0-9) to duty cycle percentage (0-99) */
|
||||
if (prefs.airtime_factor > 0.0f && prefs.airtime_factor <= 9.0f) {
|
||||
prefs.airtime_factor *= 10.0f;
|
||||
}
|
||||
memcpy(prefs.node_name, &buf[off], 32);
|
||||
off += 36; /* 32 name + 4 pad */
|
||||
memcpy(&prefs.node_lat, &buf[off], sizeof(double));
|
||||
|
||||
@@ -1228,7 +1228,8 @@ uint32_t CompanionMesh::getInitialFloodJitter(const mesh::Packet *packet)
|
||||
|
||||
uint8_t CompanionMesh::getDutyCyclePercent() const
|
||||
{
|
||||
return (uint8_t)prefs.airtime_factor;
|
||||
/* Arduino formula: duty% = 100 / (af + 1). af=0 → 100%, af=9 → 10%. */
|
||||
return (uint8_t)(100.0f / (prefs.airtime_factor + 1.0f) + 0.5f);
|
||||
}
|
||||
|
||||
uint8_t CompanionMesh::getExtraAckTransmitCount() const
|
||||
|
||||
@@ -134,7 +134,8 @@ class RepeaterMesh : public mesh::Mesh, public CommonCLICallbacks {
|
||||
|
||||
protected:
|
||||
uint8_t getDutyCyclePercent() const override {
|
||||
return (uint8_t)_prefs.airtime_factor;
|
||||
/* Arduino formula: duty% = 100 / (af + 1). af=0 → 100%, af=9 → 10%. */
|
||||
return (uint8_t)(100.0f / (_prefs.airtime_factor + 1.0f) + 0.5f);
|
||||
}
|
||||
|
||||
bool allowPacketForward(const mesh::Packet* packet) override;
|
||||
|
||||
@@ -132,11 +132,10 @@ void CommonCLI::loadPrefs(const char* path) {
|
||||
_prefs->backoff_multiplier = 0.2f;
|
||||
}
|
||||
_prefs->backoff_multiplier = constrain(_prefs->backoff_multiplier, 0.0f, 2.0f);
|
||||
/* Migrate old AF multiplier (0-9) to duty cycle percentage (0-99) */
|
||||
if (_prefs->airtime_factor > 0.0f && _prefs->airtime_factor <= 9.0f) {
|
||||
_prefs->airtime_factor *= 10.0f;
|
||||
}
|
||||
_prefs->airtime_factor = constrain(_prefs->airtime_factor, 0.0f, 99.0f);
|
||||
/* af is the Arduino airtime budget factor: duty% = 100 / (af + 1).
|
||||
* Range matches upstream (0..9). Values >9 (from a previous build that
|
||||
* stored af as a percentage) get clamped to 9 → 10% effective. */
|
||||
_prefs->airtime_factor = constrain(_prefs->airtime_factor, 0.0f, 9.0f);
|
||||
_prefs->freq = constrain(_prefs->freq, 150.0f, 2500.0f);
|
||||
_prefs->bw = constrain(_prefs->bw, 7.8f, 500.0f);
|
||||
_prefs->sf = constrain(_prefs->sf, (uint8_t)5, (uint8_t)12);
|
||||
|
||||
@@ -83,7 +83,7 @@ struct NodePrefs {
|
||||
/* Default prefs -- must match LoRaConfig.h defaults for radio interop. */
|
||||
static inline void initNodePrefs(NodePrefs* prefs) {
|
||||
memset(prefs, 0, sizeof(NodePrefs));
|
||||
prefs->airtime_factor = 10.0f; /* 10% duty cycle */
|
||||
prefs->airtime_factor = 9.0f; /* Arduino formula: duty% = 100 / (af + 1) → 10% */
|
||||
prefs->node_lat = 0.0;
|
||||
prefs->node_lon = 0.0;
|
||||
#ifdef CONFIG_ZEPHCORE_ADMIN_PASSWORD
|
||||
|
||||
+127
-158
@@ -1,158 +1,127 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* ZephCore Dispatcher - packet queue and radio scheduling
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/MeshCore.h>
|
||||
#include <mesh/Identity.h>
|
||||
#include <mesh/Packet.h>
|
||||
#include <mesh/Utils.h>
|
||||
#include <mesh/Radio.h>
|
||||
#include <mesh/Clock.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
/* EU ETSI EN 300 220 duty cycle tracker.
|
||||
* Fixed 1-hour window, tracks cumulative TX airtime in ms.
|
||||
* duty_pct=0 disables all tracking (zero overhead). */
|
||||
struct DutyCycleTracker {
|
||||
uint32_t window_start;
|
||||
uint32_t window_airtime_ms;
|
||||
uint8_t duty_pct; /* 0=disabled, 1-99=percentage */
|
||||
|
||||
void init(uint8_t pct) {
|
||||
window_start = 0;
|
||||
window_airtime_ms = 0;
|
||||
duty_pct = pct;
|
||||
}
|
||||
|
||||
void recordTx(uint32_t duration_ms, uint32_t now) {
|
||||
if (duty_pct == 0) return;
|
||||
if (now - window_start > 3600000UL) { /* 1 hour window */
|
||||
window_start = now;
|
||||
window_airtime_ms = 0;
|
||||
}
|
||||
window_airtime_ms += duration_ms;
|
||||
}
|
||||
|
||||
bool isExceeded(uint32_t now) const {
|
||||
if (duty_pct == 0) return false;
|
||||
if (now - window_start > 3600000UL) return false; /* 1h window expired */
|
||||
uint32_t budget_ms = (3600000UL / 100) * (uint32_t)duty_pct; /* ms per 1% of 1h */
|
||||
return window_airtime_ms >= budget_ms;
|
||||
}
|
||||
|
||||
uint32_t budgetMs() const {
|
||||
if (duty_pct == 0) return 0;
|
||||
return (3600000UL / 100) * (uint32_t)duty_pct; /* ms per 1% of 1h */
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
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 uint32_t getOutboundSchedule(int i) const = 0;
|
||||
virtual bool rescheduleOutbound(int i, uint32_t new_scheduled_for) = 0;
|
||||
virtual void queueInbound(Packet *packet, uint32_t scheduled_for) = 0;
|
||||
virtual Packet *getNextInbound(uint32_t now) = 0;
|
||||
};
|
||||
|
||||
/* Notifies event loop of pending TX so it can schedule a wake. */
|
||||
typedef void (*tx_queued_callback_t)(uint32_t delay_ms, void *user_data);
|
||||
|
||||
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)
|
||||
|
||||
class Dispatcher {
|
||||
Packet *outbound;
|
||||
uint32_t outbound_expiry, outbound_start, total_air_time, rx_air_time;
|
||||
uint32_t next_tx_time;
|
||||
uint32_t cad_busy_start;
|
||||
DutyCycleTracker _duty_cycle;
|
||||
uint32_t radio_nonrx_start;
|
||||
uint32_t next_agc_reset_time;
|
||||
bool prev_isrecv_mode;
|
||||
uint32_t n_sent_flood, n_sent_direct;
|
||||
uint32_t n_recv_flood, n_recv_direct;
|
||||
tx_queued_callback_t _tx_queued_cb;
|
||||
void *_tx_queued_user_data;
|
||||
|
||||
void processRecvPacket(Packet *pkt);
|
||||
|
||||
protected:
|
||||
Radio *_radio;
|
||||
MillisecondClock *_ms;
|
||||
PacketManager *_mgr;
|
||||
uint16_t _err_flags;
|
||||
|
||||
Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr);
|
||||
void notifyTxQueued(uint32_t delay_ms) {
|
||||
if (_tx_queued_cb) _tx_queued_cb(delay_ms, _tx_queued_user_data);
|
||||
}
|
||||
virtual DispatcherAction onRecvPacket(Packet *pkt) = 0;
|
||||
virtual void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { (void)snr; (void)rssi; (void)raw; (void)len; }
|
||||
virtual void logRx(Packet *packet, int len, float score) { (void)packet; (void)len; (void)score; }
|
||||
virtual void logTx(Packet *packet, int len) { (void)packet; (void)len; }
|
||||
virtual void logTxFail(Packet *packet, int len) { (void)packet; (void)len; }
|
||||
virtual const char *getLogDateTime() { return ""; }
|
||||
virtual uint8_t getDutyCyclePercent() const;
|
||||
static bool isAdminPacket(const Packet *pkt);
|
||||
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; }
|
||||
virtual int getAGCResetInterval() const { return 0; }
|
||||
|
||||
public:
|
||||
void begin();
|
||||
void loop();
|
||||
void maintenanceLoop();
|
||||
Packet *obtainNewPacket();
|
||||
void releasePacket(Packet *packet);
|
||||
void sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis = 0);
|
||||
|
||||
uint32_t getTotalAirTime() const { return total_air_time; }
|
||||
uint32_t getReceiveAirTime() const { return rx_air_time; }
|
||||
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; }
|
||||
void resetStats() {
|
||||
n_sent_flood = n_sent_direct = 0;
|
||||
n_recv_flood = n_recv_direct = 0;
|
||||
_err_flags = 0;
|
||||
}
|
||||
void setTxQueuedCallback(tx_queued_callback_t cb, void *user_data) {
|
||||
_tx_queued_cb = cb;
|
||||
_tx_queued_user_data = user_data;
|
||||
}
|
||||
bool millisHasNowPassed(uint32_t timestamp) const;
|
||||
uint32_t futureMillis(int millis_from_now) const;
|
||||
|
||||
private:
|
||||
bool tryParsePacket(Packet *pkt, const uint8_t *raw, int len);
|
||||
void checkRecv();
|
||||
void checkSend();
|
||||
};
|
||||
|
||||
} /* namespace mesh */
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* ZephCore Dispatcher - packet queue and radio scheduling
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/MeshCore.h>
|
||||
#include <mesh/Identity.h>
|
||||
#include <mesh/Packet.h>
|
||||
#include <mesh/Utils.h>
|
||||
#include <mesh/Radio.h>
|
||||
#include <mesh/Clock.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
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;
|
||||
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 uint32_t getOutboundSchedule(int i) const = 0;
|
||||
virtual bool rescheduleOutbound(int i, uint32_t new_scheduled_for) = 0;
|
||||
virtual void queueInbound(Packet *packet, uint32_t scheduled_for) = 0;
|
||||
virtual Packet *getNextInbound(uint32_t now) = 0;
|
||||
};
|
||||
|
||||
/* Notifies event loop of pending TX so it can schedule a wake. */
|
||||
typedef void (*tx_queued_callback_t)(uint32_t delay_ms, void *user_data);
|
||||
|
||||
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)
|
||||
|
||||
class Dispatcher {
|
||||
Packet *outbound;
|
||||
uint32_t outbound_expiry, outbound_start, total_air_time, rx_air_time;
|
||||
uint32_t next_tx_time;
|
||||
uint32_t cad_busy_start;
|
||||
uint32_t tx_budget_ms;
|
||||
uint32_t last_budget_update;
|
||||
uint32_t duty_cycle_window_ms;
|
||||
uint32_t radio_nonrx_start;
|
||||
uint32_t next_agc_reset_time;
|
||||
bool prev_isrecv_mode;
|
||||
uint32_t n_sent_flood, n_sent_direct;
|
||||
uint32_t n_recv_flood, n_recv_direct;
|
||||
tx_queued_callback_t _tx_queued_cb;
|
||||
void *_tx_queued_user_data;
|
||||
|
||||
void processRecvPacket(Packet *pkt);
|
||||
|
||||
protected:
|
||||
Radio *_radio;
|
||||
MillisecondClock *_ms;
|
||||
PacketManager *_mgr;
|
||||
uint16_t _err_flags;
|
||||
|
||||
Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr);
|
||||
void notifyTxQueued(uint32_t delay_ms) {
|
||||
if (_tx_queued_cb) _tx_queued_cb(delay_ms, _tx_queued_user_data);
|
||||
}
|
||||
virtual DispatcherAction onRecvPacket(Packet *pkt) = 0;
|
||||
virtual void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { (void)snr; (void)rssi; (void)raw; (void)len; }
|
||||
virtual void logRx(Packet *packet, int len, float score) { (void)packet; (void)len; (void)score; }
|
||||
virtual void logTx(Packet *packet, int len) { (void)packet; (void)len; }
|
||||
virtual void logTxFail(Packet *packet, int len) { (void)packet; (void)len; }
|
||||
virtual const char *getLogDateTime() { return ""; }
|
||||
virtual uint8_t getDutyCyclePercent() const;
|
||||
static bool isAdminPacket(const Packet *pkt);
|
||||
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; }
|
||||
virtual int getAGCResetInterval() const { return 0; }
|
||||
virtual uint32_t getDutyCycleWindowMs() const { return 3600000UL; } /* 1h default */
|
||||
|
||||
public:
|
||||
void begin();
|
||||
void loop();
|
||||
void maintenanceLoop();
|
||||
Packet *obtainNewPacket();
|
||||
void releasePacket(Packet *packet);
|
||||
void sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis = 0);
|
||||
|
||||
uint32_t getTotalAirTime() const { return total_air_time; }
|
||||
uint32_t getReceiveAirTime() const { return rx_air_time; }
|
||||
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; }
|
||||
void resetStats() {
|
||||
n_sent_flood = n_sent_direct = 0;
|
||||
n_recv_flood = n_recv_direct = 0;
|
||||
_err_flags = 0;
|
||||
}
|
||||
void setTxQueuedCallback(tx_queued_callback_t cb, void *user_data) {
|
||||
_tx_queued_cb = cb;
|
||||
_tx_queued_user_data = user_data;
|
||||
}
|
||||
bool millisHasNowPassed(uint32_t timestamp) const;
|
||||
uint32_t futureMillis(int millis_from_now) const;
|
||||
|
||||
private:
|
||||
void updateTxBudget();
|
||||
uint32_t getMaxTxBudgetMs() const;
|
||||
bool tryParsePacket(Packet *pkt, const uint8_t *raw, int len);
|
||||
void checkRecv();
|
||||
void checkSend();
|
||||
};
|
||||
|
||||
} /* namespace mesh */
|
||||
+90
-17
@@ -21,7 +21,8 @@ LOG_MODULE_REGISTER(zephcore_dispatcher, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
|
||||
|
||||
namespace mesh {
|
||||
|
||||
#define MAX_RX_DELAY_MILLIS 32000 /* upper bound for score-based RX delay */
|
||||
#define MAX_RX_DELAY_MILLIS 32000 /* upper bound for score-based RX delay */
|
||||
#define MIN_TX_BUDGET_AIRTIME_DIV 2 /* require at least 1/N MTU airtime as budget before TX */
|
||||
|
||||
Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
|
||||
: _radio(&radio), _ms(&ms), _mgr(&mgr)
|
||||
@@ -30,9 +31,11 @@ Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
|
||||
total_air_time = rx_air_time = 0;
|
||||
next_tx_time = 0;
|
||||
cad_busy_start = 0;
|
||||
tx_budget_ms = 0;
|
||||
last_budget_update = 0;
|
||||
duty_cycle_window_ms = 0;
|
||||
next_agc_reset_time = 0;
|
||||
_err_flags = 0;
|
||||
_duty_cycle.init(0);
|
||||
radio_nonrx_start = 0;
|
||||
prev_isrecv_mode = true;
|
||||
n_sent_flood = n_sent_direct = 0;
|
||||
@@ -46,10 +49,14 @@ void Dispatcher::begin()
|
||||
n_sent_flood = n_sent_direct = 0;
|
||||
n_recv_flood = n_recv_direct = 0;
|
||||
_err_flags = 0;
|
||||
radio_nonrx_start = (uint32_t)_ms->getMillis();
|
||||
uint32_t now = (uint32_t)_ms->getMillis();
|
||||
radio_nonrx_start = now;
|
||||
duty_cycle_window_ms = getDutyCycleWindowMs();
|
||||
tx_budget_ms = getMaxTxBudgetMs();
|
||||
last_budget_update = now;
|
||||
next_tx_time = now;
|
||||
_radio->begin();
|
||||
prev_isrecv_mode = _radio->isInRecvMode();
|
||||
_duty_cycle.init(getDutyCyclePercent());
|
||||
}
|
||||
|
||||
uint8_t Dispatcher::getDutyCyclePercent() const
|
||||
@@ -57,6 +64,39 @@ uint8_t Dispatcher::getDutyCyclePercent() const
|
||||
return 10; /* EU 868 default: 10% duty cycle */
|
||||
}
|
||||
|
||||
uint32_t Dispatcher::getMaxTxBudgetMs() const
|
||||
{
|
||||
uint8_t duty_pct = getDutyCyclePercent();
|
||||
if (duty_pct == 0 || duty_cycle_window_ms == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (duty_cycle_window_ms * (uint32_t)duty_pct) / 100U;
|
||||
}
|
||||
|
||||
void Dispatcher::updateTxBudget()
|
||||
{
|
||||
uint8_t duty_pct = getDutyCyclePercent();
|
||||
if (duty_pct == 0 || duty_cycle_window_ms == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t now = (uint32_t)_ms->getMillis();
|
||||
uint32_t elapsed = now - last_budget_update;
|
||||
if (elapsed == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t refill = (elapsed * (uint32_t)duty_pct) / 100U;
|
||||
if (refill > 0) {
|
||||
uint32_t max_budget = getMaxTxBudgetMs();
|
||||
tx_budget_ms += refill;
|
||||
if (tx_budget_ms > max_budget) {
|
||||
tx_budget_ms = max_budget;
|
||||
}
|
||||
last_budget_update = now;
|
||||
}
|
||||
}
|
||||
|
||||
bool Dispatcher::isAdminPacket(const Packet *pkt)
|
||||
{
|
||||
uint8_t t = pkt->getPayloadType();
|
||||
@@ -99,7 +139,12 @@ void Dispatcher::loop()
|
||||
if (_radio->isSendComplete()) {
|
||||
uint32_t t = (uint32_t)_ms->getMillis() - outbound_start;
|
||||
total_air_time += t;
|
||||
_duty_cycle.recordTx(t, (uint32_t)_ms->getMillis());
|
||||
updateTxBudget();
|
||||
if (t >= tx_budget_ms) {
|
||||
tx_budget_ms = 0;
|
||||
} else {
|
||||
tx_budget_ms -= t;
|
||||
}
|
||||
_radio->onSendFinished();
|
||||
logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len);
|
||||
if (outbound->isRouteFlood()) {
|
||||
@@ -291,6 +336,46 @@ void Dispatcher::checkSend()
|
||||
return;
|
||||
}
|
||||
|
||||
/* Duty-cycle budget gate. Matches Arduino MeshCore: defer when remaining
|
||||
* budget < est_airtime / MIN_TX_BUDGET_AIRTIME_DIV (i.e. half an MTU's airtime).
|
||||
*
|
||||
* Divergence from upstream: we exempt admin packets from the gate so that
|
||||
* remote management (admin requests, login, etc.) keeps working when a node
|
||||
* has burned its budget. Strictly out-of-spec for EN 300 220 — admin floods
|
||||
* still consume airtime — but a managed node that can't be reached to be
|
||||
* disabled is worse than the marginal extra airtime. Scan is O(N) over
|
||||
* the small (24-32) packet pool so the cost is negligible. */
|
||||
updateTxBudget();
|
||||
uint8_t duty_pct = getDutyCyclePercent();
|
||||
if (duty_pct > 0) {
|
||||
bool due_admin_queued = false;
|
||||
int total = _mgr->getOutboundTotal();
|
||||
for (int i = 0; i < total; i++) {
|
||||
Packet *pkt = _mgr->getOutboundByIdx(i);
|
||||
if (!pkt) {
|
||||
continue;
|
||||
}
|
||||
if ((int32_t)(_mgr->getOutboundSchedule(i) - now) > 0) {
|
||||
continue;
|
||||
}
|
||||
if (isAdminPacket(pkt)) {
|
||||
due_admin_queued = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t est_airtime = _radio->getEstAirtimeFor(MAX_TRANS_UNIT);
|
||||
uint32_t threshold = est_airtime / MIN_TX_BUDGET_AIRTIME_DIV;
|
||||
if (!due_admin_queued && tx_budget_ms < threshold) {
|
||||
uint32_t needed = threshold - tx_budget_ms;
|
||||
uint32_t delay_ms = (needed * 100U + (uint32_t)duty_pct - 1U) / (uint32_t)duty_pct;
|
||||
if (_tx_queued_cb) {
|
||||
_tx_queued_cb(delay_ms + 1U, _tx_queued_user_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_radio->isReceiving()) {
|
||||
/* Channel busy — enforce retry timer so we don't hammer the check */
|
||||
if (!millisHasNowPassed(next_tx_time)) {
|
||||
@@ -319,18 +404,6 @@ void Dispatcher::checkSend()
|
||||
|
||||
outbound = _mgr->getNextOutbound(now);
|
||||
if (outbound) {
|
||||
/* Duty cycle enforcement — exempt admin packets */
|
||||
if (!isAdminPacket(outbound) && _duty_cycle.isExceeded(now)) {
|
||||
LOG_WRN("checkSend: duty cycle exceeded (%u/%u ms), re-queuing type=%d",
|
||||
_duty_cycle.window_airtime_ms, _duty_cycle.budgetMs(),
|
||||
outbound->getPayloadType());
|
||||
_mgr->queueOutbound(outbound, 0, futureMillis(5000));
|
||||
outbound = nullptr;
|
||||
if (_tx_queued_cb) {
|
||||
_tx_queued_cb(5000, _tx_queued_user_data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
uint8_t raw[MAX_TRANS_UNIT];
|
||||
int len = 0;
|
||||
raw[len++] = outbound->header;
|
||||
|
||||
@@ -532,7 +532,7 @@ int main(void)
|
||||
companion_mesh.prefs.cr = 8;
|
||||
companion_mesh.prefs.tx_power_dbm = 22;
|
||||
companion_mesh.prefs.rx_delay_base = 0.0f; /* Disabled for companion */
|
||||
companion_mesh.prefs.airtime_factor = 10.0f; /* 10% duty cycle (EU 868 default) */
|
||||
companion_mesh.prefs.airtime_factor = 9.0f; /* Arduino formula: 100/(af+1) → 10% (EU 868 default) */
|
||||
companion_mesh.prefs.rx_duty_cycle = 1; /* Companions: duty cycle ON by default (power save) */
|
||||
companion_mesh.prefs.rx_boost = 1; /* Default: boosted RX (+3dB sensitivity, +2mA) */
|
||||
companion_mesh.prefs.apc_enabled = 0; /* Default: APC off */
|
||||
|
||||
Reference in new issue
Block a user