mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-25 20:13:37 +00:00
separate main events from housekeeping ones
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
#include "radio_common.h"
|
#include "radio_common.h"
|
||||||
#include <mesh/LoRaConfig.h>
|
#include <mesh/LoRaConfig.h>
|
||||||
#include <zephyr/kernel.h>
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/random/random.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
|
|
||||||
@@ -515,60 +516,93 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int16_t rssi = hwGetCurrentRSSI();
|
/* Random delay 0-500 ms before sampling. Breaks phase-lock with
|
||||||
|
* periodic interference that might be synchronized with our fixed
|
||||||
|
* 5-second housekeeping cadence. */
|
||||||
|
uint32_t jitter;
|
||||||
|
sys_rand_get(&jitter, sizeof(jitter));
|
||||||
|
k_sleep(K_MSEC(jitter % 500));
|
||||||
|
|
||||||
/* First sample after reset (DEFAULT_NOISE_FLOOR == 0): seed directly
|
/* Re-check after the delay — a packet may have arrived. */
|
||||||
* and start warmup window where all samples are accepted. */
|
if (isReceiving()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Take multiple RSSI reads and use the minimum. The noise floor is
|
||||||
|
* the lowest ambient energy — any higher sample contains signal or
|
||||||
|
* interference. Min of N reads (~200 us) naturally rejects
|
||||||
|
* interference-contaminated samples. */
|
||||||
|
int16_t rssi = hwGetCurrentRSSI();
|
||||||
|
for (int i = 1; i < NOISE_FLOOR_SAMPLES_PER_TICK; i++) {
|
||||||
|
int16_t s = hwGetCurrentRSSI();
|
||||||
|
if (s < rssi) {
|
||||||
|
rssi = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* First sample after reset (DEFAULT_NOISE_FLOOR == 0): seed directly. */
|
||||||
if (_noise_floor == DEFAULT_NOISE_FLOOR) {
|
if (_noise_floor == DEFAULT_NOISE_FLOOR) {
|
||||||
_noise_floor = rssi;
|
_noise_floor = rssi;
|
||||||
if (_noise_floor < -120) _noise_floor = -120;
|
if (_noise_floor < -120) _noise_floor = -120;
|
||||||
if (_noise_floor > -50) _noise_floor = -50;
|
if (_noise_floor > -50) _noise_floor = -50;
|
||||||
_ema_unguarded = (1 << NOISE_FLOOR_EMA_SHIFT); /* 8 ticks */
|
_ema_unguarded = 0;
|
||||||
LOG_DBG("noise_floor_cal: seed=%d", _noise_floor);
|
LOG_DBG("noise_floor_cal: seed=%d", _noise_floor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Threshold filter with periodic unguarded samples.
|
/* Threshold filter with warmup and periodic bypass.
|
||||||
* During warmup (after seed/reset), all samples are accepted so the
|
*
|
||||||
* EMA converges quickly. After warmup, every Nth tick (N = EMA window)
|
* _ema_unguarded counts up from 0 on every tick.
|
||||||
* one sample bypasses the filter so the floor can track sustained
|
* Ticks 0..N-1 (warmup): all samples accepted for fast convergence
|
||||||
* upward shifts (new interference source, antenna change, etc.).
|
* after seed/reset — prevents a bad seed from locking out the
|
||||||
* The EMA's 1/8 weight naturally dampens isolated spikes. */
|
* real noise floor via a too-tight threshold.
|
||||||
if (_ema_unguarded > 0) {
|
* Ticks N+: threshold filter active. Every Nth tick (when the low
|
||||||
_ema_unguarded--;
|
* bits are zero) one sample bypasses the filter so the floor can
|
||||||
} else if (rssi >= _noise_floor + NOISE_FLOOR_SAMPLING_THRESHOLD) {
|
* track sustained upward shifts (new interference, antenna change).
|
||||||
|
* The EMA's 1/8 weight naturally dampens isolated spikes. */
|
||||||
|
const int N = (1 << NOISE_FLOOR_EMA_SHIFT); /* 8 */
|
||||||
|
bool warmup = (_ema_unguarded < N);
|
||||||
|
bool periodic = (!warmup && (_ema_unguarded & (N - 1)) == 0);
|
||||||
|
_ema_unguarded++; /* wraps at 255 — harmless */
|
||||||
|
|
||||||
|
if (!warmup && !periodic &&
|
||||||
|
rssi >= _noise_floor + NOISE_FLOOR_SAMPLING_THRESHOLD) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* Reload: next unguarded sample in N ticks. */
|
|
||||||
if (_ema_unguarded == 0) {
|
|
||||||
_ema_unguarded = (1 << NOISE_FLOOR_EMA_SHIFT); /* 8 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* EMA: floor += round_nearest((sample - floor) / 8).
|
/* EMA: floor += round_nearest((sample - floor) / N).
|
||||||
* Plain >> has downward bias (-1>>3 == -1 but +1>>3 == 0).
|
* Plain >> has downward bias (-1>>3 == -1 but +1>>3 == 0).
|
||||||
* Plain / has a ±7 dead zone (small drifts ignored).
|
* Plain / has a ±7 dead zone (small drifts ignored).
|
||||||
* Round-to-nearest: add half the divisor before dividing,
|
* Round-to-nearest: add half the divisor before dividing,
|
||||||
* with sign-aware bias so both directions are symmetric. */
|
* with sign-aware bias so both directions are symmetric. */
|
||||||
int diff = rssi - _noise_floor;
|
int diff = rssi - _noise_floor;
|
||||||
int half = (1 << NOISE_FLOOR_EMA_SHIFT) / 2; /* 4 */
|
int half = N / 2; /* 4 */
|
||||||
int step = (diff + (diff > 0 ? half : -half)) / (1 << NOISE_FLOOR_EMA_SHIFT);
|
int step = (diff + (diff > 0 ? half : -half)) / N;
|
||||||
_noise_floor += step;
|
_noise_floor += step;
|
||||||
if (_noise_floor < -120) _noise_floor = -120;
|
if (_noise_floor < -120) _noise_floor = -120;
|
||||||
if (_noise_floor > -50) _noise_floor = -50;
|
if (_noise_floor > -50) _noise_floor = -50;
|
||||||
|
|
||||||
LOG_DBG("noise_floor_cal: rssi=%d, floor=%d", rssi, _noise_floor);
|
LOG_DBG("noise_floor_cal: rssi=%d, floor=%d, tick=%u",
|
||||||
|
rssi, _noise_floor, _ema_unguarded - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoRaRadioBase::resetAGC()
|
void LoRaRadioBase::resetAGC()
|
||||||
{
|
{
|
||||||
/* Don't reset AGC while actively receiving a packet — warm sleep would
|
/* Don't reset AGC while transmitting or receiving — warm sleep would
|
||||||
* corrupt it. The Dispatcher will retry next interval. */
|
* abort the TX or corrupt the incoming packet. maintenanceLoop()
|
||||||
if (isReceiving()) {
|
* will retry next housekeeping cycle. */
|
||||||
|
if (_tx_active || isReceiving()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hwResetAGC();
|
hwResetAGC();
|
||||||
|
|
||||||
|
/* Warm sleep + calibrate leaves the radio in STANDBY.
|
||||||
|
* Restart receive if we were in RX mode. */
|
||||||
|
if (_in_recv_mode) {
|
||||||
|
startReceive();
|
||||||
|
}
|
||||||
|
|
||||||
/* Reset noise floor so it reconverges from scratch (seed + warmup).
|
/* Reset noise floor so it reconverges from scratch (seed + warmup).
|
||||||
* Without this, a stuck _noise_floor of -120 makes the sampling threshold
|
* Without this, a stuck _noise_floor of -120 makes the sampling threshold
|
||||||
* too low to accept normal samples, self-reinforcing the stuck value. */
|
* too low to accept normal samples, self-reinforcing the stuck value. */
|
||||||
|
|||||||
@@ -12,11 +12,14 @@
|
|||||||
#include <zephyr/drivers/lora.h>
|
#include <zephyr/drivers/lora.h>
|
||||||
|
|
||||||
/* --- Noise floor calibration (EMA) ---
|
/* --- Noise floor calibration (EMA) ---
|
||||||
* Single RSSI sample per tick, smoothed with exponential moving average.
|
* Takes SAMPLES_PER_TICK RSSI reads (~100 us), feeds the minimum into an
|
||||||
* alpha = 1/8 (bit-shiftable): new_floor = floor + (sample - floor) / 8
|
* exponential moving average. Using min naturally rejects interference
|
||||||
|
* spikes — the noise floor is the lowest ambient energy in the band.
|
||||||
|
* alpha = 1/8: new_floor = floor + round((sample - floor) / 8)
|
||||||
* Convergence: ~8 ticks (~40s at 5s housekeeping) to track a step change.
|
* Convergence: ~8 ticks (~40s at 5s housekeeping) to track a step change.
|
||||||
* Samples above floor + SAMPLING_THRESHOLD are rejected (interference). */
|
* Samples above floor + SAMPLING_THRESHOLD are rejected (interference). */
|
||||||
#define NOISE_FLOOR_EMA_SHIFT 3 /* alpha = 1 / (1 << 3) = 1/8 */
|
#define NOISE_FLOOR_EMA_SHIFT 3 /* alpha = 1 / (1 << 3) = 1/8 */
|
||||||
|
#define NOISE_FLOOR_SAMPLES_PER_TICK 4 /* min of 4 RSSI reads per tick */
|
||||||
#define NOISE_FLOOR_SAMPLING_THRESHOLD 14 /* only sample if rssi < floor + threshold */
|
#define NOISE_FLOOR_SAMPLING_THRESHOLD 14 /* only sample if rssi < floor + threshold */
|
||||||
#define DEFAULT_NOISE_FLOOR 0 /* accept all samples until first update */
|
#define DEFAULT_NOISE_FLOOR 0 /* accept all samples until first update */
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class Dispatcher {
|
|||||||
uint32_t cad_busy_start;
|
uint32_t cad_busy_start;
|
||||||
DutyCycleTracker _duty_cycle;
|
DutyCycleTracker _duty_cycle;
|
||||||
uint32_t radio_nonrx_start;
|
uint32_t radio_nonrx_start;
|
||||||
uint32_t next_floor_calib_time, next_agc_reset_time;
|
uint32_t next_agc_reset_time;
|
||||||
bool prev_isrecv_mode;
|
bool prev_isrecv_mode;
|
||||||
uint32_t n_sent_flood, n_sent_direct;
|
uint32_t n_sent_flood, n_sent_direct;
|
||||||
uint32_t n_recv_flood, n_recv_direct;
|
uint32_t n_recv_flood, n_recv_direct;
|
||||||
@@ -120,6 +120,7 @@ protected:
|
|||||||
public:
|
public:
|
||||||
void begin();
|
void begin();
|
||||||
void loop();
|
void loop();
|
||||||
|
void maintenanceLoop();
|
||||||
Packet *obtainNewPacket();
|
Packet *obtainNewPacket();
|
||||||
void releasePacket(Packet *packet);
|
void releasePacket(Packet *packet);
|
||||||
void sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis = 0);
|
void sendPacket(Packet *packet, uint8_t priority, uint32_t delay_millis = 0);
|
||||||
|
|||||||
+25
-23
@@ -22,7 +22,6 @@ LOG_MODULE_REGISTER(zephcore_dispatcher, CONFIG_ZEPHCORE_LORA_LOG_LEVEL);
|
|||||||
namespace mesh {
|
namespace mesh {
|
||||||
|
|
||||||
#define MAX_RX_DELAY_MILLIS 32000
|
#define MAX_RX_DELAY_MILLIS 32000
|
||||||
#define NOISE_FLOOR_CALIB_INTERVAL 2000
|
|
||||||
|
|
||||||
Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
|
Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
|
||||||
: _radio(&radio), _ms(&ms), _mgr(&mgr)
|
: _radio(&radio), _ms(&ms), _mgr(&mgr)
|
||||||
@@ -31,7 +30,7 @@ Dispatcher::Dispatcher(Radio &radio, MillisecondClock &ms, PacketManager &mgr)
|
|||||||
total_air_time = rx_air_time = 0;
|
total_air_time = rx_air_time = 0;
|
||||||
next_tx_time = 0;
|
next_tx_time = 0;
|
||||||
cad_busy_start = 0;
|
cad_busy_start = 0;
|
||||||
next_floor_calib_time = next_agc_reset_time = 0;
|
next_agc_reset_time = 0;
|
||||||
_err_flags = 0;
|
_err_flags = 0;
|
||||||
_duty_cycle.init(0);
|
_duty_cycle.init(0);
|
||||||
radio_nonrx_start = 0;
|
radio_nonrx_start = 0;
|
||||||
@@ -94,22 +93,6 @@ uint32_t Dispatcher::getCADFailMaxDuration() const
|
|||||||
|
|
||||||
void Dispatcher::loop()
|
void Dispatcher::loop()
|
||||||
{
|
{
|
||||||
if (millisHasNowPassed(next_floor_calib_time)) {
|
|
||||||
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
|
|
||||||
next_floor_calib_time = futureMillis(NOISE_FLOOR_CALIB_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_recv = _radio->isInRecvMode();
|
|
||||||
if (is_recv != prev_isrecv_mode) {
|
|
||||||
prev_isrecv_mode = is_recv;
|
|
||||||
if (!is_recv) {
|
|
||||||
radio_nonrx_start = (uint32_t)_ms->getMillis();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!is_recv && (uint32_t)_ms->getMillis() - radio_nonrx_start > 8000) {
|
|
||||||
_err_flags |= ERR_EVENT_STARTRX_TIMEOUT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outbound) {
|
if (outbound) {
|
||||||
if (_radio->isSendComplete()) {
|
if (_radio->isSendComplete()) {
|
||||||
uint32_t t = (uint32_t)_ms->getMillis() - outbound_start;
|
uint32_t t = (uint32_t)_ms->getMillis() - outbound_start;
|
||||||
@@ -135,11 +118,6 @@ void Dispatcher::loop()
|
|||||||
next_agc_reset_time = futureMillis(getAGCResetInterval());
|
next_agc_reset_time = futureMillis(getAGCResetInterval());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getAGCResetInterval() > 0 && millisHasNowPassed(next_agc_reset_time)) {
|
|
||||||
_radio->resetAGC();
|
|
||||||
next_agc_reset_time = futureMillis(getAGCResetInterval());
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
Packet *pkt = _mgr->getNextInbound((uint32_t)_ms->getMillis());
|
Packet *pkt = _mgr->getNextInbound((uint32_t)_ms->getMillis());
|
||||||
if (pkt) {
|
if (pkt) {
|
||||||
@@ -150,6 +128,30 @@ void Dispatcher::loop()
|
|||||||
checkSend();
|
checkSend();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Dispatcher::maintenanceLoop()
|
||||||
|
{
|
||||||
|
/* Noise floor calibration — one EMA tick per housekeeping cycle */
|
||||||
|
_radio->triggerNoiseFloorCalibrate(getInterferenceThreshold());
|
||||||
|
|
||||||
|
/* RX mode watchdog — detect if radio is stuck outside RX */
|
||||||
|
bool is_recv = _radio->isInRecvMode();
|
||||||
|
if (is_recv != prev_isrecv_mode) {
|
||||||
|
prev_isrecv_mode = is_recv;
|
||||||
|
if (!is_recv) {
|
||||||
|
radio_nonrx_start = (uint32_t)_ms->getMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!is_recv && (uint32_t)_ms->getMillis() - radio_nonrx_start > 8000) {
|
||||||
|
_err_flags |= ERR_EVENT_STARTRX_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* AGC reset — periodic warm sleep + recalibration */
|
||||||
|
if (getAGCResetInterval() > 0 && millisHasNowPassed(next_agc_reset_time)) {
|
||||||
|
_radio->resetAGC();
|
||||||
|
next_agc_reset_time = futureMillis(getAGCResetInterval());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool Dispatcher::tryParsePacket(Packet *pkt, const uint8_t *raw, int len)
|
bool Dispatcher::tryParsePacket(Packet *pkt, const uint8_t *raw, int len)
|
||||||
{
|
{
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|||||||
@@ -288,18 +288,22 @@ static void mesh_event_loop(void)
|
|||||||
gps_process_event();
|
gps_process_event();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Run mesh loop - handles all pending work:
|
/* Packet processing — only on radio/BLE/TX events */
|
||||||
* - Process received LoRa packets
|
if (companion_mesh_ptr &&
|
||||||
* - Check TX completion
|
(events & (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE |
|
||||||
* - Timeout handling
|
MESH_EVENT_BLE_RX | MESH_EVENT_TX_DRAIN))) {
|
||||||
* - Noise floor calibration (runs when loop is called)
|
|
||||||
*/
|
|
||||||
if (companion_mesh_ptr) {
|
|
||||||
companion_mesh_ptr->loop();
|
companion_mesh_ptr->loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Periodic UI refresh (every housekeeping cycle = 5s) */
|
/* Periodic housekeeping — maintenance + UI refresh */
|
||||||
if (events & MESH_EVENT_HOUSEKEEPING) {
|
if (events & MESH_EVENT_HOUSEKEEPING) {
|
||||||
|
/* Radio maintenance: noise floor calibration, AGC reset,
|
||||||
|
* RX watchdog. Separated from loop() so these never run
|
||||||
|
* on packet-driven events. */
|
||||||
|
if (companion_mesh_ptr) {
|
||||||
|
companion_mesh_ptr->maintenanceLoop();
|
||||||
|
}
|
||||||
|
|
||||||
mesh_housekeeping_ui_refresh();
|
mesh_housekeeping_ui_refresh();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -328,13 +328,25 @@ static void repeater_event_loop(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ZEPHCORE_LORA
|
#ifdef ZEPHCORE_LORA
|
||||||
if (repeater_mesh_ptr) {
|
/* Packet processing — only on radio/CLI/TX events */
|
||||||
|
if (repeater_mesh_ptr &&
|
||||||
|
(events & (MESH_EVENT_LORA_RX | MESH_EVENT_LORA_TX_DONE |
|
||||||
|
MESH_EVENT_CLI_RX | MESH_EVENT_TX_DRAIN))) {
|
||||||
repeater_mesh_ptr->loop();
|
repeater_mesh_ptr->loop();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Periodic housekeeping — update display with live data */
|
/* Periodic housekeeping — maintenance + display refresh */
|
||||||
if (events & MESH_EVENT_HOUSEKEEPING) {
|
if (events & MESH_EVENT_HOUSEKEEPING) {
|
||||||
|
#ifdef ZEPHCORE_LORA
|
||||||
|
/* Radio maintenance: noise floor calibration, AGC reset,
|
||||||
|
* RX watchdog. Separated from loop() so these never run
|
||||||
|
* on packet-driven events. */
|
||||||
|
if (repeater_mesh_ptr) {
|
||||||
|
repeater_mesh_ptr->maintenanceLoop();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
ui_set_clock(rtc_clock.getCurrentTime());
|
ui_set_clock(rtc_clock.getCurrentTime());
|
||||||
|
|
||||||
#ifdef ZEPHCORE_LORA
|
#ifdef ZEPHCORE_LORA
|
||||||
|
|||||||
Reference in New Issue
Block a user