mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-16 20:52:40 +00:00
get rid of housekeeping tick and AGC reset
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user