port upstream: AGC reset, autoadd max hops, noise floor EMA, fixes

- AGC reset: proper warm sleep + Calibrate(ALL) + image recal for both
  SX126x and LR1110 (was cancel+restart / no-op respectively)
- Guard AGC reset against mid-receive (matches upstream RadioLib)
- Noise floor calibration: replace blocking 64-sample burst with
  single-sample EMA (alpha=1/8) per housekeeping tick — zero blocking,
  better temporal spread for real-world noise floor tracking
- Auto-add max hops: filter new contacts by hop count (NodePrefs,
  BaseChatMesh, CompanionMesh, ZephyrDataStore persistence)
- Fix CommonCLI memcmp("prefs", 4) → 5
This commit is contained in:
liquidraver
2026-03-03 23:26:58 +01:00
parent 86efd92f52
commit 383f23f448
14 changed files with 143 additions and 31 deletions
@@ -571,7 +571,12 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs)
off += 4;
prefs.autoadd_config = buf[off++];
/* Offset 91: rx_boost (ZephCore extension — Arduino stops at 91 bytes) */
/* Offset 91: autoadd_max_hops (matches Arduino layout) */
if (off < len) {
prefs.autoadd_max_hops = buf[off++];
}
/* Offset 92: rx_boost (ZephCore extension — Arduino stops at 92 bytes) */
if (off < len) {
prefs.rx_boost = buf[off++];
} else {
@@ -621,9 +626,11 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
memcpy(&buf[off], &prefs.gps_interval, sizeof(uint32_t));
off += 4;
buf[off++] = prefs.autoadd_config;
/* Offset 91: rx_boost (ZephCore extension — Arduino ignores) */
/* Offset 91: autoadd_max_hops (matches Arduino layout) */
buf[off++] = prefs.autoadd_max_hops;
/* Offset 92: rx_boost (ZephCore extension — Arduino ignores) */
buf[off++] = prefs.rx_boost;
/* Total: 92 bytes (Arduino reads 91, ZephCore reads 92) */
/* Total: 93 bytes (Arduino reads 92, ZephCore reads 93) */
bool ok = openWrite(prefsFile(), buf, off);
LOG_INF("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'",
+2 -1
View File
@@ -91,7 +91,8 @@ void LR1110Radio::hwSetRxDutyCycle(bool enable)
void LR1110Radio::hwResetAGC()
{
/* LR1110 doesn't have the same AGC issues as SX126x */
/* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX boost */
lr11xx_reset_agc(_dev);
}
} /* namespace mesh */
+33 -17
View File
@@ -510,33 +510,49 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold)
return;
}
int64_t start = k_uptime_get();
int sum = 0;
int count = 0;
for (int i = 0; i < NUM_NOISE_FLOOR_SAMPLES; i++) {
if (isReceiving()) {
break;
}
int16_t rssi = hwGetCurrentRSSI();
if (rssi < _noise_floor + NOISE_FLOOR_SAMPLING_THRESHOLD) {
sum += rssi;
count++;
}
/* Skip if mid-receive — don't want signal energy in the floor. */
if (isReceiving()) {
return;
}
int64_t elapsed = k_uptime_get() - start;
if (count >= NUM_NOISE_FLOOR_SAMPLES / 2) {
_noise_floor = sum / count;
int16_t rssi = hwGetCurrentRSSI();
/* First sample after reset (DEFAULT_NOISE_FLOOR == 0): seed directly. */
if (_noise_floor == DEFAULT_NOISE_FLOOR) {
_noise_floor = rssi;
if (_noise_floor < -120) _noise_floor = -120;
if (_noise_floor > -50) _noise_floor = -50;
LOG_DBG("noise_floor_cal: seed=%d", _noise_floor);
return;
}
LOG_DBG("noise_floor_cal: %d samples/%d total, floor=%d, took %lld ms",
count, NUM_NOISE_FLOOR_SAMPLES, _noise_floor, elapsed);
/* Reject samples above floor + threshold (interference / signal). */
if (rssi >= _noise_floor + NOISE_FLOOR_SAMPLING_THRESHOLD) {
return;
}
/* EMA: floor += (sample - floor) >> SHIFT. Integer-only. */
_noise_floor += (rssi - _noise_floor) >> NOISE_FLOOR_EMA_SHIFT;
if (_noise_floor < -120) _noise_floor = -120;
if (_noise_floor > -50) _noise_floor = -50;
LOG_DBG("noise_floor_cal: rssi=%d, floor=%d", rssi, _noise_floor);
}
void LoRaRadioBase::resetAGC()
{
/* Don't reset AGC while actively receiving a packet — warm sleep would
* corrupt it. The Dispatcher will retry next interval. */
if (isReceiving()) {
return;
}
hwResetAGC();
/* Reset noise floor sampling so it reconverges from scratch.
* Without this, a stuck _noise_floor of -120 makes the sampling threshold
* too low to accept normal samples, self-reinforcing the stuck value. */
_noise_floor = DEFAULT_NOISE_FLOOR;
}
bool LoRaRadioBase::isReceiving()
+2 -4
View File
@@ -91,10 +91,8 @@ void SX126xRadio::hwSetRxDutyCycle(bool enable)
void SX126xRadio::hwResetAGC()
{
if (_in_recv_mode && !isReceiving()) {
hwCancelReceive();
startReceive();
}
/* Warm sleep → Calibrate(ALL) → re-calibrate image → re-apply RX settings */
sx126x_reset_agc(_dev);
}
} /* namespace mesh */
+7 -3
View File
@@ -11,10 +11,14 @@
#include <zephyr/drivers/lora.h>
/* --- Noise floor calibration --- */
#define NUM_NOISE_FLOOR_SAMPLES 64
/* --- Noise floor calibration (EMA) ---
* Single RSSI sample per tick, smoothed with exponential moving average.
* alpha = 1/8 (bit-shiftable): new_floor = floor + (sample - floor) / 8
* Convergence: ~8 ticks (~40s at 5s housekeeping) to track a step change.
* Samples above floor + SAMPLING_THRESHOLD are rejected (interference). */
#define NOISE_FLOOR_EMA_SHIFT 3 /* alpha = 1 / (1 << 3) = 1/8 */
#define NOISE_FLOOR_SAMPLING_THRESHOLD 14 /* only sample if rssi < floor + threshold */
#define DEFAULT_NOISE_FLOOR 0 /* first calibration accepts all samples */
#define DEFAULT_NOISE_FLOOR 0 /* accept all samples until first update */
/* --- RX ring buffer ---
* 8 slots buffer ~40ms+ of back-to-back arrivals at SF7/BW500.
+12 -2
View File
@@ -1171,6 +1171,11 @@ bool CompanionMesh::shouldOverwriteWhenFull() const
return (prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0;
}
uint8_t CompanionMesh::getAutoAddMaxHops() const
{
return prefs.autoadd_max_hops;
}
void CompanionMesh::onContactsFull()
{
sendPush(PUSH_CODE_CONTACTS_FULL);
@@ -2538,16 +2543,21 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
case CMD_SET_AUTOADD_CONFIG:
if (len >= 2) {
prefs.autoadd_config = data[1];
LOG_INF("SET_AUTOADD_CONFIG: autoadd_config=0x%02x", prefs.autoadd_config);
if (len >= 3) {
prefs.autoadd_max_hops = (data[2] > 64) ? 64 : data[2];
}
LOG_INF("SET_AUTOADD_CONFIG: autoadd_config=0x%02x max_hops=%u",
prefs.autoadd_config, prefs.autoadd_max_hops);
_store->savePrefs(prefs);
}
sendPacketOk();
return true;
case CMD_GET_AUTOADD_CONFIG: {
uint8_t rsp[2];
uint8_t rsp[3];
rsp[0] = PACKET_AUTOADD_CONFIG;
rsp[1] = prefs.autoadd_config;
rsp[2] = prefs.autoadd_max_hops;
writeFrame(rsp, sizeof(rsp));
return true;
}
+1
View File
@@ -252,6 +252,7 @@ protected:
bool isAutoAddEnabled() const override;
bool shouldAutoAddContactType(uint8_t type) const override;
bool shouldOverwriteWhenFull() const override;
uint8_t getAutoAddMaxHops() const override;
void onContactsFull() override;
void onContactOverwrite(const uint8_t *pub_key) override;
+9
View File
@@ -166,6 +166,15 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id,
return;
}
// check hop limit for new contacts (0 = no limit, 1 = direct (0 hops), N = up to N-1 hops)
uint8_t max_hops = getAutoAddMaxHops();
if (max_hops > 0 && packet->getPathHashCount() >= max_hops) {
ContactInfo ci;
populateContactFromAdvert(ci, id, parser, timestamp);
onDiscoveredContact(ci, true, packet->path_len, packet->path);
return;
}
from = allocateContactSlot();
if (from == nullptr) {
LOG_WRN("onAdvertRecv: contact table full, cannot allocate slot");
+1
View File
@@ -118,6 +118,7 @@ protected:
virtual bool shouldAutoAddContactType(uint8_t type) const { return true; }
virtual void onContactsFull() {}
virtual bool shouldOverwriteWhenFull() const { return false; }
virtual uint8_t getAutoAddMaxHops() const { return 0; } // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops
virtual void onContactOverwrite(const uint8_t *pub_key) {}
virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t *path) = 0;
virtual ContactInfo *processAck(const uint8_t *data) = 0;
+1 -1
View File
@@ -723,7 +723,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
_prefs->advert_loc_policy = ADVERT_LOC_SHARE;
savePrefs();
strcpy(reply, "ok");
} else if (memcmp(command + 11, "prefs", 4) == 0) {
} else if (memcmp(command + 11, "prefs", 5) == 0) {
_prefs->advert_loc_policy = ADVERT_LOC_PREFS;
savePrefs();
strcpy(reply, "ok");
+1
View File
@@ -69,6 +69,7 @@ struct NodePrefs {
uint8_t autoadd_config;
uint8_t client_repeat; // 1 = offgrid mode (forward packets), 0 = companion only
uint8_t path_hash_mode; // which path mode to use when sending (0-2)
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
};
/* Default prefs — MUST match LoRaConfig.h defaults for radio interop.
@@ -818,6 +818,44 @@ uint32_t lr11xx_get_random(const struct device *dev)
return random;
}
void lr11xx_reset_agc(const struct device *dev)
{
struct lr11xx_data *data = dev->data;
k_mutex_lock(&data->spi_mutex, K_FOREVER);
/* Warm sleep — powers down analog frontend (resets AGC gain state) */
lr11xx_system_sleep_cfg_t sleep_cfg = {
.is_warm_start = true,
.is_rtc_timeout = false,
};
lr11xx_system_set_sleep(&data->hal_ctx, sleep_cfg, 0);
k_sleep(K_USEC(500));
/* Wake to STDBY_RC for calibration */
lr11xx_system_set_standby(&data->hal_ctx, LR11XX_SYSTEM_STANDBY_CFG_RC);
/* Recalibrate all blocks */
lr11xx_system_calibrate(&data->hal_ctx, 0x3F);
/* Re-calibrate image rejection for the actual operating frequency */
if (data->configured) {
uint16_t freq_mhz = data->modem_cfg.frequency / 1000000;
lr11xx_system_calibrate_image_in_mhz(&data->hal_ctx,
freq_mhz - 4, freq_mhz + 4);
}
/* Re-apply RX boost if it was enabled */
if (data->rx_boost_enabled) {
lr11xx_radio_cfg_rx_boosted(&data->hal_ctx, true);
lr11xx_system_clear_irq_status(&data->hal_ctx,
LR11XX_SYSTEM_IRQ_ALL_MASK);
data->rx_boost_applied = true;
}
k_mutex_unlock(&data->spi_mutex);
}
/* ── Deferred hardware init (runs on first lora_config call) ────────── */
static int lr11xx_hw_init(struct lr11xx_data *data,
@@ -73,6 +73,19 @@ void lr11xx_set_rx_boost(const struct device *dev, bool enable);
*/
uint32_t lr11xx_get_random(const struct device *dev);
/**
* @brief Reset AGC by performing warm sleep + full recalibration
*
* Warm sleep powers down the analog frontend (resets AGC gain state),
* then Calibrate(0x3F) refreshes all blocks. Re-applies image
* calibration for the operating frequency and RX boost afterward.
*
* Must be called while NOT actively receiving a packet.
*
* @param dev LoRa device
*/
void lr11xx_reset_agc(const struct device *dev);
#ifdef __cplusplus
}
#endif
@@ -61,6 +61,19 @@ void sx126x_set_rx_duty_cycle(const struct device *dev, bool enable);
*/
void sx126x_set_rx_boost(const struct device *dev, bool enable);
/**
* @brief Reset AGC by performing warm sleep + full recalibration
*
* Warm sleep powers down the analog frontend (resets AGC gain state),
* then Calibrate(0x7F) refreshes all blocks (ADC, PLL, image, oscillators).
* Re-applies DIO2 RF switch, RX boosted gain, and image calibration afterward.
*
* Must be called while NOT actively receiving a packet.
*
* @param dev LoRa device
*/
void sx126x_reset_agc(const struct device *dev);
#ifdef __cplusplus
}
#endif