diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp index e116239..c338923 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.cpp +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -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'", diff --git a/zephcore/adapters/radio/LR1110Radio.cpp b/zephcore/adapters/radio/LR1110Radio.cpp index c9911b6..de0fdd0 100644 --- a/zephcore/adapters/radio/LR1110Radio.cpp +++ b/zephcore/adapters/radio/LR1110Radio.cpp @@ -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 */ diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index 7ed501a..d140b40 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -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() diff --git a/zephcore/adapters/radio/SX126xRadio.cpp b/zephcore/adapters/radio/SX126xRadio.cpp index b0b1c32..762ed76 100644 --- a/zephcore/adapters/radio/SX126xRadio.cpp +++ b/zephcore/adapters/radio/SX126xRadio.cpp @@ -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 */ diff --git a/zephcore/adapters/radio/radio_common.h b/zephcore/adapters/radio/radio_common.h index fa18f59..f7c6e94 100644 --- a/zephcore/adapters/radio/radio_common.h +++ b/zephcore/adapters/radio/radio_common.h @@ -11,10 +11,14 @@ #include -/* --- 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. diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index 5302b3c..149979f 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -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; } diff --git a/zephcore/app/CompanionMesh.h b/zephcore/app/CompanionMesh.h index 26b14a1..432dc64 100644 --- a/zephcore/app/CompanionMesh.h +++ b/zephcore/app/CompanionMesh.h @@ -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; diff --git a/zephcore/helpers/BaseChatMesh.cpp b/zephcore/helpers/BaseChatMesh.cpp index b8f8f9b..847b09b 100644 --- a/zephcore/helpers/BaseChatMesh.cpp +++ b/zephcore/helpers/BaseChatMesh.cpp @@ -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"); diff --git a/zephcore/helpers/BaseChatMesh.h b/zephcore/helpers/BaseChatMesh.h index 2536cfa..2f1b519 100644 --- a/zephcore/helpers/BaseChatMesh.h +++ b/zephcore/helpers/BaseChatMesh.h @@ -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; diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 8bc688a..bc74a9f 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -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"); diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h index 703eda1..68f0192 100644 --- a/zephcore/helpers/NodePrefs.h +++ b/zephcore/helpers/NodePrefs.h @@ -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. diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c index 01b3f66..d8196a3 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c @@ -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, diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h index 813a3d7..25d2c27 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.h @@ -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 diff --git a/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h b/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h index 5f74515..00ccd67 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h +++ b/zephcore/patches/zephyr-new/drivers/lora/native/sx126x/sx126x_ext.h @@ -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