get rid of housekeeping tick and AGC reset

This commit is contained in:
liquidraver
2026-07-28 14:12:40 +02:00
parent 394eefec51
commit fe6e585eb6
30 changed files with 524 additions and 166 deletions
+65 -4
View File
@@ -11,7 +11,8 @@ namespace mesh {
ContentionTracker::ContentionTracker()
: _next_idx(0), _ema_x256(0), _finalized_count(0),
_last_retransmit_ms(0), _backoff_multiplier(DEFAULT_BACKOFF_MULT)
_last_retransmit_ms(0), _last_decay_ms(0),
_backoff_multiplier(DEFAULT_BACKOFF_MULT)
{
memset(_ring, 0, sizeof(_ring));
}
@@ -144,12 +145,72 @@ void ContentionTracker::tick(uint32_t now_ms)
}
}
/* Decay EMA toward 0 if no retransmit in STALE_MS */
if (_last_retransmit_ms != 0 && now_ms - _last_retransmit_ms > STALE_MS) {
if (_ema_x256 > 0) {
/* Decay EMA toward 0 if no retransmit in STALE_MS. Paced by wall clock
* (one 1/8 step per DECAY_PERIOD_MS) rather than one step per call, so
* the decay rate no longer depends on how often the event loop happens
* to call us — see DECAY_PERIOD_MS. */
if (_last_retransmit_ms != 0 && now_ms - _last_retransmit_ms > STALE_MS &&
_ema_x256 > 0) {
if (_last_decay_ms == 0) {
/* Arm one full period in the PAST, so the loop below
* applies a step on this very call. The old code decayed
* immediately on the first tick that observed staleness;
* arming at now_ms instead would make the first check
* (0 < DECAY_PERIOD_MS) break without decaying, deferring
* onset by a whole period on every stale transition — and
* would let the reset branch below starve decay entirely
* for traffic that goes stale and un-stale repeatedly.
* Unsigned wraparound makes this exact even near zero. */
_last_decay_ms = now_ms - DECAY_PERIOD_MS;
}
for (int n = 0; n < MAX_DECAY_CATCHUP; n++) {
if (now_ms - _last_decay_ms < DECAY_PERIOD_MS ||
_ema_x256 == 0) {
break;
}
_ema_x256 -= _ema_x256 >> EMA_SHIFT;
_last_decay_ms += DECAY_PERIOD_MS;
}
} else {
/* Not decaying — restart the phase next time we are. */
_last_decay_ms = 0;
}
}
uint32_t ContentionTracker::msUntilNextTick(uint32_t now_ms) const
{
uint32_t next = MAINTENANCE_IDLE;
/* Soonest ring entry to fall out of the observation window. */
for (int i = 0; i < RING_SIZE; i++) {
if (!_ring[i].active) {
continue;
}
next = maintenanceSooner(
next, maintenanceUntil(now_ms,
_ring[i].first_seen_ms + WINDOW_MS + 1));
}
/* Stale-decay step, only while there is EMA left to decay. */
if (_last_retransmit_ms != 0 && _ema_x256 > 0) {
uint32_t stale_at = _last_retransmit_ms + STALE_MS + 1;
if (now_ms - _last_retransmit_ms > STALE_MS) {
/* Already stale: next step is one period after the last
* one (or immediately, if we have not started yet). */
next = maintenanceSooner(
next, _last_decay_ms == 0
? 0
: maintenanceUntil(now_ms,
_last_decay_ms +
DECAY_PERIOD_MS));
} else {
next = maintenanceSooner(next,
maintenanceUntil(now_ms, stale_at));
}
}
return next;
}
float ContentionTracker::getContentionEstimate() const
+33 -17
View File
@@ -34,7 +34,6 @@ Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
tx_budget_ms = 0;
last_budget_update = 0;
duty_cycle_window_ms = 0;
next_agc_reset_time = 0;
_err_flags = 0;
radio_nonrx_start = 0;
prev_isrecv_mode = true;
@@ -148,15 +147,8 @@ void Dispatcher::loop()
} else {
return;
}
next_agc_reset_time = futureMillis(getAGCResetInterval());
}
{
Packet *pkt = _mgr->getNextInbound((uint32_t)_ms->getMillis());
if (pkt) {
processRecvPacket(pkt);
}
}
checkRecv();
checkSend();
}
@@ -165,8 +157,12 @@ void Dispatcher::maintenanceLoop()
{
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
/* RX mode watchdog: TX counts as "active" to avoid false triggers
* when the 5s housekeeping timer misses brief RX windows. */
/* RX mode watchdog: TX counts as "active" to avoid false triggers when
* a maintenance pass lands between brief RX windows. Diagnostic only —
* it raises a status bit and recovers nothing — but that bit is surfaced
* on every role: the repeater/room-server "stats" CLI reply and binary
* telemetry read _err_flags directly, the MQTT uplink publishes it, and
* the companion returns it in its BLE device-status response. */
bool is_active = _radio->isInRecvMode() || !_radio->isSendComplete();
if (is_active != prev_isrecv_mode) {
prev_isrecv_mode = is_active;
@@ -174,16 +170,11 @@ void Dispatcher::maintenanceLoop()
radio_nonrx_start = (uint32_t)_ms->getMillis();
}
}
if (!is_active && (uint32_t)_ms->getMillis() - radio_nonrx_start > 8000) { /* 8s stall threshold */
if (!is_active &&
(uint32_t)_ms->getMillis() - radio_nonrx_start > RADIO_STALL_THRESHOLD_MS) {
_err_flags |= ERR_EVENT_STARTRX_TIMEOUT;
}
/* Periodic AGC recalibration */
if (getAGCResetInterval() > 0 && millisHasNowPassed(next_agc_reset_time)) {
_radio->resetAGC();
next_agc_reset_time = futureMillis(getAGCResetInterval());
}
/* Adaptive CAD: probe scheduling + staircase live in the radio;
* we only surface offset changes so the app layer can persist them. */
_radio->cadMaintenance();
@@ -199,6 +190,31 @@ void Dispatcher::maintenanceLoop()
}
}
uint32_t Dispatcher::msUntilNextMaintenance()
{
uint32_t now = (uint32_t)_ms->getMillis();
/* Noise floor sampling + CAD probing/decay both live in the radio and
* carry their own deadlines. */
uint32_t next = _radio->msUntilNextMaintenance();
/* Radio stall watchdog. Only pending while the radio is known to be
* neither receiving nor transmitting as of the last pass — the
* transition into that state is itself event-driven (TX start, RX done,
* CAD), so there is nothing to poll for while the radio is active.
*
* The already-flagged check is load-bearing: the verdict is a latched
* status bit, so once raised its deadline sits permanently in the past.
* Without this the query would return 0 on every call and the event loop
* would re-arm at its minimum interval forever. */
if (!prev_isrecv_mode && !(_err_flags & ERR_EVENT_STARTRX_TIMEOUT)) {
next = maintenanceSooner(
next, maintenanceUntil(now, radio_nonrx_start +
RADIO_STALL_THRESHOLD_MS));
}
return next;
}
bool Dispatcher::tryParsePacket(Packet *pkt, const uint8_t *raw, int len)
{
int i = 0;
+8
View File
@@ -34,6 +34,14 @@ void Mesh::maintenanceLoop()
_contention.tick(now);
}
uint32_t Mesh::msUntilNextMaintenance()
{
uint32_t now = (uint32_t)_ms->getMillis();
return maintenanceSooner(Dispatcher::msUntilNextMaintenance(),
_contention.msUntilNextTick(now));
}
void Mesh::extendPendingRetransmit(uint32_t hash32)
{
uint32_t now = (uint32_t)_ms->getMillis();
-15
View File
@@ -113,7 +113,6 @@ struct PacketQueue {
static Packet _packet_pool[POOL_SIZE];
static PacketQueue _unused;
static PacketQueue _send_queue;
static PacketQueue _rx_queue;
static bool _initialized = false;
static void init_pool() {
@@ -202,18 +201,4 @@ uint8_t StaticPoolPacketManager::peekNextOutboundPriority(uint32_t now) const
return _send_queue.peekPriority(now);
}
void StaticPoolPacketManager::queueInbound(Packet *packet, uint32_t scheduled_for)
{
if (!_rx_queue.add(packet, 0, scheduled_for)) {
LOG_WRN("queueInbound: FULL (%d entries) — dropping type=%d",
_rx_queue.count(), packet->getPayloadType());
free(packet);
}
}
Packet *StaticPoolPacketManager::getNextInbound(uint32_t now)
{
return _rx_queue.get(now);
}
} /* namespace mesh */
+82 -16
View File
@@ -57,6 +57,19 @@ extern "C" void bt_ctlr_assert_handle(char *file, uint32_t line)
/* UI subsystem (display, buttons, buzzer) */
#include "ui_task.h"
/* Headless repeaters link the weak no-op ui_* stubs (ui_headless_stubs.c), so
* the periodic UI refresh in the maintenance pass is pure work for nothing on
* them. Guard the hot path on a real ui_* implementation being linked; the
* one-shot calls at init and in the CLI reply path stay unguarded, matching
* the rest of the file.
*
* This MUST mirror the CMake condition that selects the stubs, not the display
* devicetree node: ZEPHCORE_UI_DESIGN_BUTTON is enabled by BUTTONS *or*
* DISPLAY *or* BUZZER, so a board with buttons/buzzer and no panel still links
* the real UI and still needs these updates. */
#define ZEPHCORE_HAS_UI (IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_BUTTON) || \
IS_ENABLED(CONFIG_ZEPHCORE_UI_DESIGN_JOYSTICK))
/* Radio + mesh includes (shared header selects LR1110 or SX126x) */
#include <mesh/RadioIncludes.h>
@@ -85,15 +98,26 @@ static const struct gpio_dt_spec led1 = GPIO_DT_SPEC_GET(LED1_NODE, gpios);
#define MESH_EVENT_LORA_RX BIT(0) /* LoRa packet received */
#define MESH_EVENT_LORA_TX_DONE BIT(1) /* LoRa TX complete */
#define MESH_EVENT_CLI_RX BIT(2) /* CLI command received */
#define MESH_EVENT_HOUSEKEEPING BIT(3) /* Periodic housekeeping (noise floor, etc.) */
#define MESH_EVENT_MAINTENANCE BIT(3) /* A maintenance deadline came due */
#define MESH_EVENT_GPS_ACTION BIT(4) /* GPS state change (must run on main thread!) */
#define MESH_EVENT_TX_DRAIN BIT(5) /* Outbound packet delay expired, run checkSend */
#define MESH_EVENT_RTC_SAVE BIT(6) /* Hardware-RTC write requested off-main */
#define MESH_EVENT_INIT_ADVERT BIT(7) /* Deferred boot advert — send on main thread */
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_HOUSEKEEPING | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_RTC_SAVE | MESH_EVENT_INIT_ADVERT)
#define MESH_EVENT_ALL (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE | MESH_EVENT_CLI_RX | MESH_EVENT_MAINTENANCE | MESH_EVENT_GPS_ACTION | MESH_EVENT_TX_DRAIN | MESH_EVENT_RTC_SAVE | MESH_EVENT_INIT_ADVERT)
/* Housekeeping interval - infrequent to preserve power savings */
#define HOUSEKEEPING_INTERVAL_MS CONFIG_ZEPHCORE_HOUSEKEEPING_INTERVAL_MS
/* Maintenance is deadline-driven, not periodic: after every pass the loop asks
* the mesh when its soonest pending deadline is (msUntilNextMaintenance) and
* arms a single one-shot wake for exactly that moment. An idle repeater with
* nothing scheduled therefore sleeps until its next real deadline instead of
* waking on a fixed cadence.
*
* MAINTENANCE_BACKSTOP_MS bounds that: it caps how long we will go without a
* pass even when everything reports idle, so a deadline that is missed or
* mis-reported degrades to the old behaviour instead of wedging.
* MAINTENANCE_MIN_MS floors it, so an item that is due-but-blocked (radio
* mid-packet, duty-cycle sleep window) re-arms shortly rather than spinning. */
#define MAINTENANCE_BACKSTOP_MS CONFIG_ZEPHCORE_MAINTENANCE_BACKSTOP_MS
#define MAINTENANCE_MIN_MS 50
/* Event object for mesh loop */
static struct k_event mesh_events;
@@ -128,15 +152,16 @@ K_MSGQ_DEFINE(cli_cmd_queue, sizeof(struct cli_cmd_line), 4, 4);
/* Work items for event-driven processing */
static void cli_rx_work_fn(struct k_work *work);
static void housekeeping_timer_fn(struct k_timer *timer);
static void maintenance_timer_fn(struct k_timer *timer);
static void tx_drain_work_fn(struct k_work *work);
static void initial_advert_work_fn(struct k_work *work);
K_WORK_DEFINE(cli_rx_work, cli_rx_work_fn);
K_WORK_DELAYABLE_DEFINE(tx_drain_work, tx_drain_work_fn);
K_WORK_DELAYABLE_DEFINE(initial_advert_work, initial_advert_work_fn);
/* Housekeeping timer for periodic tasks (noise floor calibration, etc.) */
K_TIMER_DEFINE(housekeeping_timer, housekeeping_timer_fn, NULL);
/* One-shot maintenance wake, re-armed after every loop pass (see
* arm_maintenance_wake). Not periodic the period is the deadline. */
K_TIMER_DEFINE(maintenance_timer, maintenance_timer_fn, NULL);
/* Forward declarations */
#ifdef ZEPHCORE_LORA
@@ -244,11 +269,43 @@ static void process_cli_commands(void)
}
#endif
/* Housekeeping timer callback - signals event to wake mesh loop periodically */
static void housekeeping_timer_fn(struct k_timer *timer)
/* Maintenance deadline expired — wake the mesh loop to run the pass. */
static void maintenance_timer_fn(struct k_timer *timer)
{
ARG_UNUSED(timer);
k_event_post(&mesh_events, MESH_EVENT_HOUSEKEEPING);
k_event_post(&mesh_events, MESH_EVENT_MAINTENANCE);
}
/* Ask the mesh for its soonest pending deadline and arm the one-shot for it.
* Called after every loop pass, whatever woke us: any event may have created
* or cleared a deadline (a queued advert, a CLI tempradio command, a CAD probe
* that just ran), so the schedule is recomputed from scratch each time rather
* than tracked incrementally.
*
* The backstop below is an unconditional CEILING on the wait, not a fallback
* used only when everything reports idle. That means it must stay well above
* the shortest legitimate recurring deadline (the noise-floor sampler, and the
* CAD probe) or it becomes the effective period and the deadline scheduling
* buys nothing see ZEPHCORE_MAINTENANCE_BACKSTOP_MS. */
static void arm_maintenance_wake(void)
{
uint32_t delay = MAINTENANCE_BACKSTOP_MS;
#ifdef ZEPHCORE_LORA
if (repeater_mesh_ptr) {
uint32_t next = repeater_mesh_ptr->msUntilNextMaintenance();
if (next < delay) {
delay = next;
}
}
#endif
if (delay < MAINTENANCE_MIN_MS) {
delay = MAINTENANCE_MIN_MS;
}
k_timer_start(&maintenance_timer, K_MSEC(delay), K_NO_WAIT);
}
#ifdef ZEPHCORE_LORA
@@ -398,9 +455,8 @@ static void repeater_event_loop(void)
/* Print startup banner (no prompt - Arduino style) */
cli_print("\r\n=== ZephCore Repeater ===\r\n");
/* Start housekeeping timer for periodic maintenance tasks */
k_timer_start(&housekeeping_timer, K_MSEC(HOUSEKEEPING_INTERVAL_MS),
K_MSEC(HOUSEKEEPING_INTERVAL_MS));
/* Arm the first maintenance wake; every pass below re-arms it. */
arm_maintenance_wake();
for (;;) {
/* Wait for any mesh event - blocks until signaled */
@@ -436,8 +492,8 @@ static void repeater_event_loop(void)
}
#endif
/* Periodic housekeeping — maintenance + display refresh */
if (events & MESH_EVENT_HOUSEKEEPING) {
/* A maintenance deadline came due — run the pass. */
if (events & MESH_EVENT_MAINTENANCE) {
#ifdef ZEPHCORE_LORA
/* Radio maintenance: noise floor calibration, AGC reset,
* RX watchdog. Separated from loop() so these never run
@@ -452,16 +508,21 @@ static void repeater_event_loop(void)
}
#endif
#if ZEPHCORE_HAS_UI
ui_set_clock(rtc_clock.getCurrentTime());
#ifdef ZEPHCORE_LORA
/* Refresh live radio state (noise floor, TX power
* reduction, RX/TX mode, packet counters). */
* reduction, RX/TX mode, packet counters). Headless
* repeaters compile this out entirely every ui_set_*
* below it is a weak no-op there (ui_headless_stubs.c),
* so the whole block was pure work for nothing. */
refresh_repeater_ui_radio_state();
/* Battery is now refreshed lazily from ui_pages_render() with
* a 30 s freshness guard no periodic ADC fire here. */
#endif
#endif /* ZEPHCORE_HAS_UI */
}
/* Off-main RTC write request (gps_fix_callback runs in modem_chat
@@ -476,6 +537,11 @@ static void repeater_event_loop(void)
}
#endif
}
/* Re-arm for the soonest deadline this pass left behind. Done for
* every wake, not just maintenance ones: a CLI command, an inbound
* packet or a GPS fix can all create or clear a deadline. */
arm_maintenance_wake();
}
}