From 063f5056f23b7a3999016f6b60028bd724c3837b Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Mon, 2 Feb 2026 11:21:00 +0700 Subject: [PATCH 01/26] Fixed RefCountedDigitalPin.h to release claim correctly. Ensure no negative claims number. --- src/helpers/RefCountedDigitalPin.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/helpers/RefCountedDigitalPin.h b/src/helpers/RefCountedDigitalPin.h index 753f6c30..4cf53cda 100644 --- a/src/helpers/RefCountedDigitalPin.h +++ b/src/helpers/RefCountedDigitalPin.h @@ -20,10 +20,12 @@ public: digitalWrite(_pin, _active); } } + void release() { - _claims--; if (_claims == 0) { digitalWrite(_pin, !_active); + } else { + _claims--; } } }; From 39fb2902ec5653971a62fb308b9d2e56e77f7480 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Thu, 5 Feb 2026 22:42:02 +0700 Subject: [PATCH 02/26] Avoid negative _claims --- src/helpers/RefCountedDigitalPin.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/helpers/RefCountedDigitalPin.h b/src/helpers/RefCountedDigitalPin.h index 4cf53cda..f30c4c58 100644 --- a/src/helpers/RefCountedDigitalPin.h +++ b/src/helpers/RefCountedDigitalPin.h @@ -22,10 +22,11 @@ public: } void release() { + if (_claims == 0) return; // avoid negative _claims + + _claims--; if (_claims == 0) { digitalWrite(_pin, !_active); - } else { - _claims--; } } }; From f6603fe7a5edf8b8197e45b7e909c6e781507511 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Thu, 5 Feb 2026 23:26:08 +0700 Subject: [PATCH 03/26] Set back PIN_VEXT_EN_ACTIVE=HIGH --- variants/heltec_v4/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index c5011e0e..fdddcd5d 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -22,7 +22,7 @@ build_flags = -D P_LORA_PA_TX_EN=46 ; PA CPS - GC1109 TX PA full(High) / bypass(Low) -D PIN_USER_BTN=0 -D PIN_VEXT_EN=36 - -D PIN_VEXT_EN_ACTIVE=LOW + -D PIN_VEXT_EN_ACTIVE=HIGH -D LORA_TX_POWER=10 ;If it is configured as 10 here, the final output will be 22 dbm. -D MAX_LORA_TX_POWER=22 ; Max SX1262 output -D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX From 44b80d00c202a9783773086ef93f64bc0ce85457 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Thu, 5 Feb 2026 23:27:10 +0700 Subject: [PATCH 04/26] Disabled periph_power for Heltec v4's display --- variants/heltec_v4/target.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_v4/target.cpp b/variants/heltec_v4/target.cpp index f971cc60..54fc05e8 100644 --- a/variants/heltec_v4/target.cpp +++ b/variants/heltec_v4/target.cpp @@ -24,7 +24,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #endif #ifdef DISPLAY_CLASS - DISPLAY_CLASS display(&(board.periph_power)); + DISPLAY_CLASS display(NULL); MomentaryButton user_btn(PIN_USER_BTN, 1000, true); #endif From 13d0dff9182bf7931b9b6c87b67d7e5120301099 Mon Sep 17 00:00:00 2001 From: Kevin Le Date: Wed, 18 Feb 2026 22:29:33 +0700 Subject: [PATCH 05/26] Reverted to use GPIO 17, 18 as I2C for Heltec v4 repeater --- variants/heltec_v4/platformio.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index fdddcd5d..71ffc2e6 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -54,8 +54,6 @@ build_flags = -D PIN_BOARD_SDA=17 -D PIN_BOARD_SCL=18 -D PIN_OLED_RESET=21 - -D ENV_PIN_SDA=4 - -D ENV_PIN_SCL=3 build_src_filter= ${Heltec_lora32_v4.build_src_filter} lib_deps = ${Heltec_lora32_v4.lib_deps} From 8ad17d1022242ac066021778e5ca4b79c839225f Mon Sep 17 00:00:00 2001 From: enricolorenzoni59 Date: Sat, 28 Feb 2026 09:07:30 +0000 Subject: [PATCH 06/26] `gps sync` reply: fill buffer with text --- src/helpers/CommonCLI.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index f2f961b9..e20bbb1c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -717,6 +717,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch LocationProvider * l = _sensors->getLocationProvider(); if (l != NULL) { l->syncTime(); + strcpy(reply, "ok"); + } else { + strcpy(reply, "gps provider not found"); } } else if (memcmp(command, "gps setloc", 10) == 0) { _prefs->node_lat = _sensors->node_lat; From 329e40819720981b2c208831ab38ac907a849970 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Fri, 6 Feb 2026 01:57:27 +0100 Subject: [PATCH 07/26] Hold GC1109 PA_POWER during deep sleep for LNA RX wake The GC1109 FEM needs its VFEM_Ctrl pin held HIGH during deep sleep to keep the LNA active, enabling proper RX sensitivity for wake-on-packet. Without this, the LNA is unpowered during sleep and RX wake sensitivity is degraded by ~17dB. Release RTC holds in begin() after configuring GPIO registers (not before) to ensure glitch-free pin transitions on wake. Trade-off: ~6.5mA additional sleep current for significantly improved wake-on-packet range. --- variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp | 11 +++++++++-- variants/heltec_tracker_v2/platformio.ini | 10 +++++----- variants/heltec_v4/HeltecV4Board.cpp | 11 +++++++++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index 4975d5cd..1b694c11 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -6,12 +6,17 @@ void HeltecTrackerV2Board::begin() { pinMode(PIN_ADC_CTRL, OUTPUT); digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive + // Set up digital GPIO registers before releasing RTC hold. The hold latches + // the pad state including function select, so register writes accumulate + // without affecting the pad. On hold release, all changes apply atomically + // (IO MUX switches to digital GPIO with output already HIGH — no glitch). pinMode(P_LORA_PA_POWER, OUTPUT); digitalWrite(P_LORA_PA_POWER,HIGH); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_POWER); - rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_EN); pinMode(P_LORA_PA_EN, OUTPUT); digitalWrite(P_LORA_PA_EN,HIGH); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_EN); pinMode(P_LORA_PA_TX_EN, OUTPUT); digitalWrite(P_LORA_PA_TX_EN,LOW); @@ -48,7 +53,9 @@ void HeltecTrackerV2Board::begin() { rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_EN); //It also needs to be enabled in receive mode + // Hold GC1109 FEM pins during sleep to keep LNA active for RX wake + rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); + rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_EN); if (pin_wake_btn < 0) { esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index 25d16f2f..af41b4f5 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -17,11 +17,11 @@ build_flags = -D P_LORA_SCLK=9 -D P_LORA_MISO=11 -D P_LORA_MOSI=10 - -D P_LORA_PA_POWER=7 ;power en - -D P_LORA_PA_EN=4 - -D P_LORA_PA_TX_EN=46 ;enable tx - -D LORA_TX_POWER=10 ;If it is configured as 10 here, the final output will be 22 dbm. - -D MAX_LORA_TX_POWER=22 ;Max SX1262 output + -D P_LORA_PA_POWER=7 ; VFEM_Ctrl - GC1109 LDO power enable + -D P_LORA_PA_EN=4 ; CSD - GC1109 chip enable (HIGH=on) + -D P_LORA_PA_TX_EN=46 ; CPS - GC1109 PA mode (HIGH=full PA, LOW=bypass) + -D LORA_TX_POWER=10 ; 10dBm + ~11dB GC1109 gain = ~21dBm output + -D MAX_LORA_TX_POWER=22 ; Max SX1262 output -> ~28dBm at antenna -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 92f93437..626f2577 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -7,12 +7,17 @@ void HeltecV4Board::begin() { pinMode(PIN_ADC_CTRL, OUTPUT); digitalWrite(PIN_ADC_CTRL, LOW); // Initially inactive + // Set up digital GPIO registers before releasing RTC hold. The hold latches + // the pad state including function select, so register writes accumulate + // without affecting the pad. On hold release, all changes apply atomically + // (IO MUX switches to digital GPIO with output already HIGH — no glitch). pinMode(P_LORA_PA_POWER, OUTPUT); digitalWrite(P_LORA_PA_POWER,HIGH); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_POWER); - rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_EN); pinMode(P_LORA_PA_EN, OUTPUT); digitalWrite(P_LORA_PA_EN,HIGH); + rtc_gpio_hold_dis((gpio_num_t)P_LORA_PA_EN); pinMode(P_LORA_PA_TX_EN, OUTPUT); digitalWrite(P_LORA_PA_TX_EN,LOW); @@ -50,7 +55,9 @@ void HeltecV4Board::begin() { rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS); - rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_EN); //It also needs to be enabled in receive mode + // Hold GC1109 FEM pins during sleep to keep LNA active for RX wake + rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_POWER); + rtc_gpio_hold_en((gpio_num_t)P_LORA_PA_EN); if (pin_wake_btn < 0) { esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet From 2bb6f636a4ae18da2f793501effe9265cd9654cd Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sun, 8 Feb 2026 16:36:13 +0100 Subject: [PATCH 08/26] Add 1ms delay after powering PA (cold-boot) --- variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp | 7 +++++-- variants/heltec_v4/HeltecV4Board.cpp | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index 1b694c11..bd7f680e 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -20,9 +20,12 @@ void HeltecTrackerV2Board::begin() { pinMode(P_LORA_PA_TX_EN, OUTPUT); digitalWrite(P_LORA_PA_TX_EN,LOW); - periph_power.begin(); - esp_reset_reason_t reason = esp_reset_reason(); + if (reason != ESP_RST_DEEPSLEEP) { + delay(1); // GC1109 startup time after cold power-on + } + + periph_power.begin(); if (reason == ESP_RST_DEEPSLEEP) { long wakeup_source = esp_sleep_get_ext1_wakeup_status(); if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep) diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 626f2577..8186f2d4 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -21,10 +21,12 @@ void HeltecV4Board::begin() { pinMode(P_LORA_PA_TX_EN, OUTPUT); digitalWrite(P_LORA_PA_TX_EN,LOW); + esp_reset_reason_t reason = esp_reset_reason(); + if (reason != ESP_RST_DEEPSLEEP) { + delay(1); // GC1109 startup time after cold power-on + } periph_power.begin(); - - esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_DEEPSLEEP) { long wakeup_source = esp_sleep_get_ext1_wakeup_status(); if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep) From d9e67222f59391a4352bd406ed21bed69ae0dc22 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 25 Feb 2026 09:11:23 +0100 Subject: [PATCH 09/26] prefs is 5 char length :nerd: --- src/helpers/CommonCLI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index e20bbb1c..fd631273 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -749,7 +749,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"); From 70f1ad4aebf22a70a663f310487bbb8eb167ddf8 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 25 Feb 2026 00:26:38 +0100 Subject: [PATCH 10/26] Fix RAK3401 SKY66122-11 FEM control: enable CSD/CPS for proper PA and LNA operation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The RAK13302 1W module uses a Skyworks SKY66122-11 front-end module with three digital control pins (CSD, CTX, CPS) that must be actively driven by the host MCU. The previous code only managed CTX (GPIO 31) — toggling it for TX/RX — but never initialized CSD (GPIO 24) or CPS (GPIO 21), leaving them floating with no pull-up/pull-down resistors on the PCB. With floating CSD and CPS, the SKY66122 was in an undefined operating mode: - The 30 dB TX PA may not have been reliably engaging - The 16 dB RX LNA was never reliably active, degrading receive sensitivity --- variants/rak3401/RAK3401Board.cpp | 33 ++++++++++++++++++++++++++----- variants/rak3401/RAK3401Board.h | 9 +++++---- variants/rak3401/variant.h | 11 ++++++++--- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index b9431c92..e2a9f318 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -23,10 +23,33 @@ void RAK3401Board::begin() { pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); -#ifdef P_LORA_PA_EN - // Initialize RAK13302 1W LoRa transceiver module PA control pin + // Initialize SKY66122-11 FEM on the RAK13302 module. + // CSD (P0.24) and CPS (P0.21) must be HIGH for both TX and RX modes. + // CTX (P0.31) selects TX(HIGH) vs RX(LOW) and also enables the 5V boost + // converter that powers the PA section (VCC1/VCC2). + // The LNA section (VSUP1/VCC0) runs on 3.3V and works with boost off. + pinMode(P_LORA_PA_CSD, OUTPUT); + digitalWrite(P_LORA_PA_CSD, HIGH); // CSD=1: enable FEM + + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, HIGH); // CPS=1: enable TX/RX paths + pinMode(P_LORA_PA_EN, OUTPUT); - digitalWrite(P_LORA_PA_EN, LOW); // Start with PA disabled - delay(10); // Allow PA module to initialize + digitalWrite(P_LORA_PA_EN, LOW); // CTX=0: RX mode, boost off + + delay(1); // SKY66122 turn-on settling time +} + +#ifdef NRF52_POWER_MANAGEMENT +void RAK3401Board::initiateShutdown(uint8_t reason) { + // Put SKY66122 in guaranteed <1 uA shutdown (Mode 4: CSD=0, CTX=0, CPS=0) + digitalWrite(P_LORA_PA_EN, LOW); // CTX=0, boost off + digitalWrite(SX126X_POWER_EN, LOW); // CPS=0 + digitalWrite(P_LORA_PA_CSD, LOW); // CSD=0 + + // Disable 3V3 switched peripherals + digitalWrite(PIN_3V3_EN, LOW); + + enterSystemOff(reason); +} #endif -} \ No newline at end of file diff --git a/variants/rak3401/RAK3401Board.h b/variants/rak3401/RAK3401Board.h index 20edf906..8ca5b52e 100644 --- a/variants/rak3401/RAK3401Board.h +++ b/variants/rak3401/RAK3401Board.h @@ -38,13 +38,14 @@ public: return "RAK 3401"; } -#ifdef P_LORA_PA_EN + // SKY66122 FEM TX/RX switching via CTX pin. + // CTX=HIGH: TX mode + 5V boost ON (PA powered from VCC1/VCC2) + // CTX=LOW: RX mode + 5V boost OFF (LNA powered from VSUP1 at 3.3V) void onBeforeTransmit() override { - digitalWrite(P_LORA_PA_EN, HIGH); // Enable PA before transmission + digitalWrite(P_LORA_PA_EN, HIGH); // CTX=1: TX mode, boost on } void onAfterTransmit() override { - digitalWrite(P_LORA_PA_EN, LOW); // Disable PA after transmission to save power + digitalWrite(P_LORA_PA_EN, LOW); // CTX=0: RX mode, boost off } -#endif }; diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 56fe0816..f2ef4ace 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -147,8 +147,14 @@ static const uint8_t AREF = PIN_AREF; #define SX126X_BUSY (9) #define SX126X_RESET (4) -#define SX126X_POWER_EN (21) -// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +// SKY66122-11 FEM control pins (active HIGH, active LOW = shutdown <1uA) +// CSD+CPS must be HIGH for TX and RX; CTX selects TX(HIGH) vs RX(LOW) +// CTX also enables the 5V boost converter for the PA during TX +#define P_LORA_PA_CSD (24) // P0.24 -> SKY66122 CSD (pin 11) - FEM enable +#define SX126X_POWER_EN (21) // P0.21 -> SKY66122 CPS (pin 1) - path select +#define P_LORA_PA_EN (31) // P0.31 -> SKY66122 CTX (pin 2) - TX/RX + boost EN + +// DIO2 has a NC 0R footprint (R25) to CTX; not connected by default #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 @@ -159,7 +165,6 @@ static const uint8_t AREF = PIN_AREF; #define P_LORA_DIO_1 SX126X_DIO1 #define P_LORA_BUSY SX126X_BUSY #define P_LORA_RESET SX126X_RESET -#define P_LORA_PA_EN 31 // enables 3.3V periphery like GPS or IO Module // Do not toggle this for GPS power savings From ac2aa03b0903200f81a0e6981d56e194a4ce8ae7 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 25 Feb 2026 01:18:16 +0100 Subject: [PATCH 11/26] Add SX126X_REGISTER_PATCH for RAK3401 --- variants/rak3401/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 7467ceb9..ecea0317 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -11,6 +11,7 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX with SKY66122 FEM build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak3401> + From 5a5568ed56ac25d4de6e10e9d2cde07d6bf686c1 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Thu, 26 Feb 2026 08:57:56 +0100 Subject: [PATCH 12/26] Drive CTX low first --- variants/rak3401/RAK3401Board.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index e2a9f318..4c18c6dd 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -28,15 +28,18 @@ void RAK3401Board::begin() { // CTX (P0.31) selects TX(HIGH) vs RX(LOW) and also enables the 5V boost // converter that powers the PA section (VCC1/VCC2). // The LNA section (VSUP1/VCC0) runs on 3.3V and works with boost off. + // + // Drive CTX LOW first to prevent transient TX mode (Mode 2) while CSD/CPS + // are being enabled — the RAK13302 has no pull-downs on these pins. + pinMode(P_LORA_PA_EN, OUTPUT); + digitalWrite(P_LORA_PA_EN, LOW); // CTX=0: RX mode, boost off + pinMode(P_LORA_PA_CSD, OUTPUT); digitalWrite(P_LORA_PA_CSD, HIGH); // CSD=1: enable FEM pinMode(SX126X_POWER_EN, OUTPUT); digitalWrite(SX126X_POWER_EN, HIGH); // CPS=1: enable TX/RX paths - pinMode(P_LORA_PA_EN, OUTPUT); - digitalWrite(P_LORA_PA_EN, LOW); // CTX=0: RX mode, boost off - delay(1); // SKY66122 turn-on settling time } From 49d831350171f0ee377a781cd646382b7988ab86 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Fri, 27 Feb 2026 11:30:46 +0100 Subject: [PATCH 13/26] Fix pin mapping & TX switch (it's DIO2) --- variants/rak3401/RAK3401Board.cpp | 34 +++++++++++-------------------- variants/rak3401/RAK3401Board.h | 12 ++--------- variants/rak3401/variant.h | 15 +++++++------- 3 files changed, 22 insertions(+), 39 deletions(-) diff --git a/variants/rak3401/RAK3401Board.cpp b/variants/rak3401/RAK3401Board.cpp index 4c18c6dd..33e1de42 100644 --- a/variants/rak3401/RAK3401Board.cpp +++ b/variants/rak3401/RAK3401Board.cpp @@ -20,37 +20,27 @@ void RAK3401Board::begin() { Wire.begin(); + // PIN_3V3_EN (WB_IO2, P0.34) controls the 3V3_S switched peripheral rail + // AND the 5V boost regulator (U5) on the RAK13302 that powers the SKY66122 PA. + // Must stay HIGH during radio operation — do not toggle for power saving. pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); - // Initialize SKY66122-11 FEM on the RAK13302 module. - // CSD (P0.24) and CPS (P0.21) must be HIGH for both TX and RX modes. - // CTX (P0.31) selects TX(HIGH) vs RX(LOW) and also enables the 5V boost - // converter that powers the PA section (VCC1/VCC2). - // The LNA section (VSUP1/VCC0) runs on 3.3V and works with boost off. - // - // Drive CTX LOW first to prevent transient TX mode (Mode 2) while CSD/CPS - // are being enabled — the RAK13302 has no pull-downs on these pins. - pinMode(P_LORA_PA_EN, OUTPUT); - digitalWrite(P_LORA_PA_EN, LOW); // CTX=0: RX mode, boost off - - pinMode(P_LORA_PA_CSD, OUTPUT); - digitalWrite(P_LORA_PA_CSD, HIGH); // CSD=1: enable FEM - + // Enable SKY66122-11 FEM on the RAK13302 module. + // CSD and CPS are tied together on the RAK13302 PCB, routed to IO3 (P0.21). + // HIGH = FEM active (LNA for RX, PA path available for TX). + // TX/RX switching (CTX) is handled by SX1262 DIO2 via SetDIO2AsRfSwitchCtrl. pinMode(SX126X_POWER_EN, OUTPUT); - digitalWrite(SX126X_POWER_EN, HIGH); // CPS=1: enable TX/RX paths - - delay(1); // SKY66122 turn-on settling time + digitalWrite(SX126X_POWER_EN, HIGH); + delay(1); // SKY66122 turn-on settling time (tON = 3us typ) } #ifdef NRF52_POWER_MANAGEMENT void RAK3401Board::initiateShutdown(uint8_t reason) { - // Put SKY66122 in guaranteed <1 uA shutdown (Mode 4: CSD=0, CTX=0, CPS=0) - digitalWrite(P_LORA_PA_EN, LOW); // CTX=0, boost off - digitalWrite(SX126X_POWER_EN, LOW); // CPS=0 - digitalWrite(P_LORA_PA_CSD, LOW); // CSD=0 + // Disable SKY66122 FEM (CSD+CPS LOW = shutdown, <1 uA) + digitalWrite(SX126X_POWER_EN, LOW); - // Disable 3V3 switched peripherals + // Disable 3V3 switched peripherals and 5V boost digitalWrite(PIN_3V3_EN, LOW); enterSystemOff(reason); diff --git a/variants/rak3401/RAK3401Board.h b/variants/rak3401/RAK3401Board.h index 8ca5b52e..3a080d5e 100644 --- a/variants/rak3401/RAK3401Board.h +++ b/variants/rak3401/RAK3401Board.h @@ -38,14 +38,6 @@ public: return "RAK 3401"; } - // SKY66122 FEM TX/RX switching via CTX pin. - // CTX=HIGH: TX mode + 5V boost ON (PA powered from VCC1/VCC2) - // CTX=LOW: RX mode + 5V boost OFF (LNA powered from VSUP1 at 3.3V) - void onBeforeTransmit() override { - digitalWrite(P_LORA_PA_EN, HIGH); // CTX=1: TX mode, boost on - } - - void onAfterTransmit() override { - digitalWrite(P_LORA_PA_EN, LOW); // CTX=0: RX mode, boost off - } + // TX/RX switching is handled by SX1262 DIO2 -> SKY66122 CTX (hardware-timed). + // No onBeforeTransmit/onAfterTransmit overrides needed. }; diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index f2ef4ace..268aec53 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -147,14 +147,15 @@ static const uint8_t AREF = PIN_AREF; #define SX126X_BUSY (9) #define SX126X_RESET (4) -// SKY66122-11 FEM control pins (active HIGH, active LOW = shutdown <1uA) -// CSD+CPS must be HIGH for TX and RX; CTX selects TX(HIGH) vs RX(LOW) -// CTX also enables the 5V boost converter for the PA during TX -#define P_LORA_PA_CSD (24) // P0.24 -> SKY66122 CSD (pin 11) - FEM enable -#define SX126X_POWER_EN (21) // P0.21 -> SKY66122 CPS (pin 1) - path select -#define P_LORA_PA_EN (31) // P0.31 -> SKY66122 CTX (pin 2) - TX/RX + boost EN +// SKY66122-11 FEM control on the RAK13302 module: +// CSD + CPS are tied together on the PCB, routed to WisBlock IO3 (P0.21). +// Setting IO3 HIGH enables the FEM (LNA for RX, PA path for TX). +// CTX is connected to SX1262 DIO2 — the radio handles TX/RX switching +// in hardware via SetDIO2AsRfSwitchCtrl (microsecond-accurate, no GPIO needed). +// The 5V boost for the PA is enabled by WB_IO2 (P0.34 = PIN_3V3_EN). +#define SX126X_POWER_EN (21) // P0.21 = IO3 -> SKY66122 CSD+CPS (FEM enable) -// DIO2 has a NC 0R footprint (R25) to CTX; not connected by default +// CTX is driven by SX1262 DIO2, not a GPIO #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 From f81ec4b14ca7126e17d0005bf0c64adc8d87c821 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Thu, 19 Feb 2026 14:44:25 +0100 Subject: [PATCH 14/26] fix agc reset --- src/helpers/radiolib/RadioLibWrappers.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index cf3e1266..a4b4c3ba 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -57,8 +57,11 @@ void RadioLibWrapper::resetAGC() { // make sure we're not mid-receive of packet! if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) return; - // NOTE: according to higher powers, just issuing RadioLib's startReceive() will reset the AGC. - // revisit this if a better impl is discovered. + // Warm sleep powers down the entire analog frontend (including AGC), forcing a + // fresh gain calibration on the next startReceive(). A plain standby->startReceive + // cycle does NOT reset the AGC — the analog state can persist across STDBY_RC. + // The ~1-2 ms sleep gap is negligible vs the preamble budget (131 ms at SF11/BW250). + _radio->sleep(); state = STATE_IDLE; // trigger a startReceive() } From a2dc2eb50cda605be7ef619f358024162fb48009 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Thu, 19 Feb 2026 16:16:21 +0100 Subject: [PATCH 15/26] when doing AGC reset, call Calibrate(0x7F) 1. warm sleep 2. wake to stdby 3. Calibrate(0x7F) to reset all internal blocks 4. re-apply DIO2 RF / boosted gain & register patch to make sure everything is as it was --- src/helpers/radiolib/CustomLLCC68Wrapper.h | 26 +++++++++++++++++ src/helpers/radiolib/CustomSTM32WLxWrapper.h | 26 +++++++++++++++++ src/helpers/radiolib/CustomSX1262Wrapper.h | 30 ++++++++++++++++++++ src/helpers/radiolib/CustomSX1268Wrapper.h | 26 +++++++++++++++++ src/helpers/radiolib/RadioLibWrappers.cpp | 10 +++---- src/helpers/radiolib/RadioLibWrappers.h | 1 + 6 files changed, 114 insertions(+), 5 deletions(-) diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index f7dd7a9f..826c8ed2 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -19,4 +19,30 @@ public: int sf = ((CustomLLCC68 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + + void doResetAGC() override { + auto* radio = (CustomLLCC68 *)_radio; + radio->sleep(true); + radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); + uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; + radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); + radio->mod->hal->delay(5); + uint32_t start = millis(); + while (radio->mod->hal->digitalRead(radio->mod->getGpio())) { + if (millis() - start > 50) break; + radio->mod->hal->yield(); + } +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif +#ifdef SX126X_REGISTER_PATCH + uint8_t r_data = 0; + radio->readRegister(0x8B5, &r_data, 1); + r_data |= 0x01; + radio->writeRegister(0x8B5, &r_data, 1); +#endif + } }; diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index 9e2d0441..ed65b188 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -20,4 +20,30 @@ public: int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + + void doResetAGC() override { + auto* radio = (CustomSTM32WLx *)_radio; + radio->sleep(true); + radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); + uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; + radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); + radio->mod->hal->delay(5); + uint32_t start = millis(); + while (radio->mod->hal->digitalRead(radio->mod->getGpio())) { + if (millis() - start > 50) break; + radio->mod->hal->yield(); + } +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif +#ifdef SX126X_REGISTER_PATCH + uint8_t r_data = 0; + radio->readRegister(0x8B5, &r_data, 1); + r_data |= 0x01; + radio->writeRegister(0x8B5, &r_data, 1); +#endif + } }; diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 1afee5e8..505b4996 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -22,4 +22,34 @@ public: virtual void powerOff() override { ((CustomSX1262 *)_radio)->sleep(false); } + + void doResetAGC() override { + auto* radio = (CustomSX1262 *)_radio; + // Warm sleep powers down analog frontend (resets AGC gain state) + radio->sleep(true); + // Wake to STDBY_RC for calibration + radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); + // Recalibrate all blocks (ADC, PLL, image, oscillators) + uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; + radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); + radio->mod->hal->delay(5); + uint32_t start = millis(); + while (radio->mod->hal->digitalRead(radio->mod->getGpio())) { + if (millis() - start > 50) break; + radio->mod->hal->yield(); + } + // Re-apply RX settings that calibration may reset +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif +#ifdef SX126X_REGISTER_PATCH + uint8_t r_data = 0; + radio->readRegister(0x8B5, &r_data, 1); + r_data |= 0x01; + radio->writeRegister(0x8B5, &r_data, 1); +#endif + } }; diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 5d7106b4..c87ee977 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -19,4 +19,30 @@ public: int sf = ((CustomSX1268 *)_radio)->spreadingFactor; return packetScoreInt(snr, sf, packet_len); } + + void doResetAGC() override { + auto* radio = (CustomSX1268 *)_radio; + radio->sleep(true); + radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); + uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; + radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); + radio->mod->hal->delay(5); + uint32_t start = millis(); + while (radio->mod->hal->digitalRead(radio->mod->getGpio())) { + if (millis() - start > 50) break; + radio->mod->hal->yield(); + } +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif +#ifdef SX126X_REGISTER_PATCH + uint8_t r_data = 0; + radio->readRegister(0x8B5, &r_data, 1); + r_data |= 0x01; + radio->writeRegister(0x8B5, &r_data, 1); +#endif + } }; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index a4b4c3ba..53a4b0a2 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -53,15 +53,15 @@ void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) { } } +void RadioLibWrapper::doResetAGC() { + _radio->sleep(); // warm sleep to reset analog frontend +} + void RadioLibWrapper::resetAGC() { // make sure we're not mid-receive of packet! if ((state & STATE_INT_READY) != 0 || isReceivingPacket()) return; - // Warm sleep powers down the entire analog frontend (including AGC), forcing a - // fresh gain calibration on the next startReceive(). A plain standby->startReceive - // cycle does NOT reset the AGC — the analog state can persist across STDBY_RC. - // The ~1-2 ms sleep gap is negligible vs the preamble budget (131 ms at SF11/BW250). - _radio->sleep(); + doResetAGC(); state = STATE_IDLE; // trigger a startReceive() } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 9ac1bbae..b338b03a 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -16,6 +16,7 @@ protected: void startRecv(); float packetScoreInt(float snr, int sf, int packet_len); virtual bool isReceivingPacket() =0; + virtual void doResetAGC(); public: RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; } From 9106ab46e1a05103c244ab6dd981d5cfdd3fbccd Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Thu, 19 Feb 2026 16:52:57 +0100 Subject: [PATCH 16/26] reset noise_floor sampling after agc reset --- src/helpers/radiolib/RadioLibWrappers.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 53a4b0a2..2216ca8f 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -63,6 +63,14 @@ void RadioLibWrapper::resetAGC() { doResetAGC(); state = STATE_IDLE; // trigger a startReceive() + + // Reset noise floor sampling so it reconverges from scratch. + // Without this, a stuck _noise_floor of -120 makes the sampling threshold + // too low (-106) to accept normal samples (~-105), self-reinforcing the + // stuck value even after the receiver has recovered. + _noise_floor = 0; + _num_floor_samples = 0; + _floor_sample_sum = 0; } void RadioLibWrapper::loop() { From b2032e11b66048ffd45b7d0280255df5bb58783e Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Thu, 19 Feb 2026 17:34:48 +0100 Subject: [PATCH 17/26] make it more dry --- src/helpers/radiolib/CustomLLCC68Wrapper.h | 27 ++-------------- src/helpers/radiolib/CustomSTM32WLxWrapper.h | 27 ++-------------- src/helpers/radiolib/CustomSX1262Wrapper.h | 31 ++---------------- src/helpers/radiolib/CustomSX1268Wrapper.h | 27 ++-------------- src/helpers/radiolib/SX126xReset.h | 33 ++++++++++++++++++++ 5 files changed, 41 insertions(+), 104 deletions(-) create mode 100644 src/helpers/radiolib/SX126xReset.h diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index 826c8ed2..9e783a95 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -2,6 +2,7 @@ #include "CustomLLCC68.h" #include "RadioLibWrappers.h" +#include "SX126xReset.h" class CustomLLCC68Wrapper : public RadioLibWrapper { public: @@ -20,29 +21,5 @@ public: return packetScoreInt(snr, sf, packet_len); } - void doResetAGC() override { - auto* radio = (CustomLLCC68 *)_radio; - radio->sleep(true); - radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); - uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; - radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); - radio->mod->hal->delay(5); - uint32_t start = millis(); - while (radio->mod->hal->digitalRead(radio->mod->getGpio())) { - if (millis() - start > 50) break; - radio->mod->hal->yield(); - } -#ifdef SX126X_DIO2_AS_RF_SWITCH - radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); -#endif -#ifdef SX126X_RX_BOOSTED_GAIN - radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); -#endif -#ifdef SX126X_REGISTER_PATCH - uint8_t r_data = 0; - radio->readRegister(0x8B5, &r_data, 1); - r_data |= 0x01; - radio->writeRegister(0x8B5, &r_data, 1); -#endif - } + void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } }; diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index ed65b188..e3e52029 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -2,6 +2,7 @@ #include "CustomSTM32WLx.h" #include "RadioLibWrappers.h" +#include "SX126xReset.h" #include class CustomSTM32WLxWrapper : public RadioLibWrapper { @@ -21,29 +22,5 @@ public: return packetScoreInt(snr, sf, packet_len); } - void doResetAGC() override { - auto* radio = (CustomSTM32WLx *)_radio; - radio->sleep(true); - radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); - uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; - radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); - radio->mod->hal->delay(5); - uint32_t start = millis(); - while (radio->mod->hal->digitalRead(radio->mod->getGpio())) { - if (millis() - start > 50) break; - radio->mod->hal->yield(); - } -#ifdef SX126X_DIO2_AS_RF_SWITCH - radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); -#endif -#ifdef SX126X_RX_BOOSTED_GAIN - radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); -#endif -#ifdef SX126X_REGISTER_PATCH - uint8_t r_data = 0; - radio->readRegister(0x8B5, &r_data, 1); - r_data |= 0x01; - radio->writeRegister(0x8B5, &r_data, 1); -#endif - } + void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } }; diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 505b4996..5856720b 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -2,6 +2,7 @@ #include "CustomSX1262.h" #include "RadioLibWrappers.h" +#include "SX126xReset.h" class CustomSX1262Wrapper : public RadioLibWrapper { public: @@ -23,33 +24,5 @@ public: ((CustomSX1262 *)_radio)->sleep(false); } - void doResetAGC() override { - auto* radio = (CustomSX1262 *)_radio; - // Warm sleep powers down analog frontend (resets AGC gain state) - radio->sleep(true); - // Wake to STDBY_RC for calibration - radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); - // Recalibrate all blocks (ADC, PLL, image, oscillators) - uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; - radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); - radio->mod->hal->delay(5); - uint32_t start = millis(); - while (radio->mod->hal->digitalRead(radio->mod->getGpio())) { - if (millis() - start > 50) break; - radio->mod->hal->yield(); - } - // Re-apply RX settings that calibration may reset -#ifdef SX126X_DIO2_AS_RF_SWITCH - radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); -#endif -#ifdef SX126X_RX_BOOSTED_GAIN - radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); -#endif -#ifdef SX126X_REGISTER_PATCH - uint8_t r_data = 0; - radio->readRegister(0x8B5, &r_data, 1); - r_data |= 0x01; - radio->writeRegister(0x8B5, &r_data, 1); -#endif - } + void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } }; diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index c87ee977..5149fc43 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -2,6 +2,7 @@ #include "CustomSX1268.h" #include "RadioLibWrappers.h" +#include "SX126xReset.h" class CustomSX1268Wrapper : public RadioLibWrapper { public: @@ -20,29 +21,5 @@ public: return packetScoreInt(snr, sf, packet_len); } - void doResetAGC() override { - auto* radio = (CustomSX1268 *)_radio; - radio->sleep(true); - radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); - uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; - radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); - radio->mod->hal->delay(5); - uint32_t start = millis(); - while (radio->mod->hal->digitalRead(radio->mod->getGpio())) { - if (millis() - start > 50) break; - radio->mod->hal->yield(); - } -#ifdef SX126X_DIO2_AS_RF_SWITCH - radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); -#endif -#ifdef SX126X_RX_BOOSTED_GAIN - radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); -#endif -#ifdef SX126X_REGISTER_PATCH - uint8_t r_data = 0; - radio->readRegister(0x8B5, &r_data, 1); - r_data |= 0x01; - radio->writeRegister(0x8B5, &r_data, 1); -#endif - } + void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } }; diff --git a/src/helpers/radiolib/SX126xReset.h b/src/helpers/radiolib/SX126xReset.h new file mode 100644 index 00000000..ba08ef8d --- /dev/null +++ b/src/helpers/radiolib/SX126xReset.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +// Full receiver reset for all SX126x-family chips (SX1262, SX1268, LLCC68, STM32WLx). +// Warm sleep powers down analog, Calibrate(0x7F) refreshes ADC/PLL/image calibration, +// then re-applies RX settings that calibration may reset. +inline void sx126xResetAGC(SX126x* radio) { + radio->sleep(true); + radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); + + uint8_t calData = RADIOLIB_SX126X_CALIBRATE_ALL; + radio->mod->SPIwriteStream(RADIOLIB_SX126X_CMD_CALIBRATE, &calData, 1, true, false); + radio->mod->hal->delay(5); + uint32_t start = millis(); + while (radio->mod->hal->digitalRead(radio->mod->getGpio())) { + if (millis() - start > 50) break; + radio->mod->hal->yield(); + } + +#ifdef SX126X_DIO2_AS_RF_SWITCH + radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); +#endif +#ifdef SX126X_RX_BOOSTED_GAIN + radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); +#endif +#ifdef SX126X_REGISTER_PATCH + uint8_t r_data = 0; + radio->readRegister(0x8B5, &r_data, 1); + r_data |= 0x01; + radio->writeRegister(0x8B5, &r_data, 1); +#endif +} From f54948e06db394c74269b064e0d92dfc193c0f64 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sat, 21 Feb 2026 15:33:38 +0100 Subject: [PATCH 18/26] Also implement LR11x10 AGC reset Similar to SX126x but simpler. --- src/helpers/radiolib/CustomLR1110Wrapper.h | 4 +++- src/helpers/radiolib/LR11x0Reset.h | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/helpers/radiolib/LR11x0Reset.h diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 947bb51d..be4a6cde 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -2,11 +2,13 @@ #include "CustomLR1110.h" #include "RadioLibWrappers.h" +#include "LR11x0Reset.h" class CustomLR1110Wrapper : public RadioLibWrapper { public: CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } - bool isReceivingPacket() override { + void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio); } + bool isReceivingPacket() override { return ((CustomLR1110 *)_radio)->isReceiving(); } float getCurrentRSSI() override { diff --git a/src/helpers/radiolib/LR11x0Reset.h b/src/helpers/radiolib/LR11x0Reset.h new file mode 100644 index 00000000..539ed44e --- /dev/null +++ b/src/helpers/radiolib/LR11x0Reset.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +// Full receiver reset for LR11x0-family chips (LR1110, LR1120, LR1121). +// Warm sleep powers down analog, calibrate(0x3F) refreshes all calibration blocks, +// then re-applies RX settings that calibration may reset. +inline void lr11x0ResetAGC(LR11x0* radio) { + radio->sleep(true, 0); + radio->standby(RADIOLIB_LR11X0_STANDBY_RC, true); + + radio->calibrate(RADIOLIB_LR11X0_CALIBRATE_ALL); + +#ifdef RX_BOOSTED_GAIN + radio->setRxBoostedGainMode(RX_BOOSTED_GAIN); +#endif +} From 85f764a114c299644587d40cb2ec08a15f4cf4d1 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sat, 21 Feb 2026 17:34:28 +0100 Subject: [PATCH 19/26] Calibrate configured frequency for AGC reset --- src/helpers/radiolib/CustomLR1110.h | 2 ++ src/helpers/radiolib/CustomLR1110Wrapper.h | 2 +- src/helpers/radiolib/LR11x0Reset.h | 6 +++++- src/helpers/radiolib/SX126xReset.h | 4 ++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index e4332013..b1f68080 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -20,6 +20,8 @@ class CustomLR1110 : public LR1110 { return len; } + float getFreqMHz() const { return freqMHz; } + bool isReceiving() { uint16_t irq = getIrqStatus(); bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index be4a6cde..a1e0a493 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -7,7 +7,7 @@ class CustomLR1110Wrapper : public RadioLibWrapper { public: CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } - void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio); } + void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } bool isReceivingPacket() override { return ((CustomLR1110 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/LR11x0Reset.h b/src/helpers/radiolib/LR11x0Reset.h index 539ed44e..47cca627 100644 --- a/src/helpers/radiolib/LR11x0Reset.h +++ b/src/helpers/radiolib/LR11x0Reset.h @@ -5,12 +5,16 @@ // Full receiver reset for LR11x0-family chips (LR1110, LR1120, LR1121). // Warm sleep powers down analog, calibrate(0x3F) refreshes all calibration blocks, // then re-applies RX settings that calibration may reset. -inline void lr11x0ResetAGC(LR11x0* radio) { +inline void lr11x0ResetAGC(LR11x0* radio, float freqMHz) { radio->sleep(true, 0); radio->standby(RADIOLIB_LR11X0_STANDBY_RC, true); radio->calibrate(RADIOLIB_LR11X0_CALIBRATE_ALL); + // calibrate(0x3F) defaults image calibration to an unknown band. + // Re-calibrate for the actual operating frequency (band=4MHz matches RadioLib default). + radio->calibrateImageRejection(freqMHz - 4.0f, freqMHz + 4.0f); + #ifdef RX_BOOSTED_GAIN radio->setRxBoostedGainMode(RX_BOOSTED_GAIN); #endif diff --git a/src/helpers/radiolib/SX126xReset.h b/src/helpers/radiolib/SX126xReset.h index ba08ef8d..39ddb73e 100644 --- a/src/helpers/radiolib/SX126xReset.h +++ b/src/helpers/radiolib/SX126xReset.h @@ -18,6 +18,10 @@ inline void sx126xResetAGC(SX126x* radio) { radio->mod->hal->yield(); } + // Calibrate(0x7F) defaults image calibration to 902-928MHz band. + // Re-calibrate for the actual operating frequency. + radio->calibrateImage(radio->freqMHz); + #ifdef SX126X_DIO2_AS_RF_SWITCH radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); #endif From 9bae9d0ed2a6de07847ec3e4ae47c5346028ab9e Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sat, 21 Feb 2026 17:42:33 +0100 Subject: [PATCH 20/26] fix comment, we know the band now after checking LR1110 user manual --- src/helpers/radiolib/LR11x0Reset.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/radiolib/LR11x0Reset.h b/src/helpers/radiolib/LR11x0Reset.h index 47cca627..d06ffc53 100644 --- a/src/helpers/radiolib/LR11x0Reset.h +++ b/src/helpers/radiolib/LR11x0Reset.h @@ -11,7 +11,7 @@ inline void lr11x0ResetAGC(LR11x0* radio, float freqMHz) { radio->calibrate(RADIOLIB_LR11X0_CALIBRATE_ALL); - // calibrate(0x3F) defaults image calibration to an unknown band. + // calibrate(0x3F) defaults image calibration to 902-928MHz band. // Re-calibrate for the actual operating frequency (band=4MHz matches RadioLib default). radio->calibrateImageRejection(freqMHz - 4.0f, freqMHz + 4.0f); From 59d9770ab97dce5cf47a142e7ff38b2bb686d0ac Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Fri, 9 Jan 2026 05:06:17 +0100 Subject: [PATCH 21/26] Add GPS support Heltec Wireless Tracker v1.x Pin mapping verified against HTIT-Tracker V0.5 schematic: - GPIO35 (GPS_EN): N-ch MOSFET drives P-ch high-side switch, active HIGH - GPIO36 (GPS_RST): hardware reset, active LOW - GPIO33/34: UART TX/RX Delegates power management to MicroNMEALocationProvider begin()/stop() which independently controls GPS power via GPS_EN and shares VEXT with the display through RefCountedDigitalPin. --- variants/heltec_tracker/platformio.ini | 5 +++++ variants/heltec_tracker/target.cpp | 11 +++++------ variants/heltec_tracker/target.h | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index dba05dcf..1dbda126 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -32,6 +32,11 @@ build_flags = -D PIN_TFT_LEDA_CTL=21 ; LEDK (switches on/off via mosfet to create the ground) -D PIN_GPS_RX=33 -D PIN_GPS_TX=34 + -D PIN_GPS_EN=35 ; N-ch MOSFET Q2 drives P-ch high-side switch → active HIGH (default) + -D PIN_GPS_RESET=36 + -D PIN_GPS_RESET_ACTIVE=LOW + -D GPS_BAUD_RATE=115200 + -D ENV_INCLUDE_GPS=1 -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 diff --git a/variants/heltec_tracker/target.cpp b/variants/heltec_tracker/target.cpp index 25c2634b..f801bacb 100644 --- a/variants/heltec_tracker/target.cpp +++ b/variants/heltec_tracker/target.cpp @@ -16,7 +16,8 @@ WRAPPER_CLASS radio_driver(radio, board); ESP32RTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); -MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); +// GPS_EN (GPIO35) drives N-ch MOSFET → P-ch high-side switch; GPS_RESET (GPIO36) active LOW +MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock, GPS_RESET, GPS_EN, &board.periph_power); HWTSensorManager sensors = HWTSensorManager(nmea); #ifdef DISPLAY_CLASS @@ -58,18 +59,16 @@ mesh::LocalIdentity radio_new_identity() { void HWTSensorManager::start_gps() { if (!gps_active) { - board.periph_power.claim(); - + _location->begin(); // Claims periph_power via RefCountedDigitalPin gps_active = true; - Serial1.println("$CFGSYS,h35155*68"); + Serial1.println("$CFGSYS,h35155*68"); // Configure GPS for all constellations } } void HWTSensorManager::stop_gps() { if (gps_active) { gps_active = false; - - board.periph_power.release(); + _location->stop(); // Releases periph_power via RefCountedDigitalPin } } diff --git a/variants/heltec_tracker/target.h b/variants/heltec_tracker/target.h index 5296fb2c..29099f46 100644 --- a/variants/heltec_tracker/target.h +++ b/variants/heltec_tracker/target.h @@ -28,6 +28,7 @@ public: const char* getSettingName(int i) const override; const char* getSettingValue(int i) const override; bool setSettingValue(const char* name, const char* value) override; + LocationProvider* getLocationProvider() override { return _location; } }; extern HeltecV3Board board; From 8a9a0dca5f6c1cc554d3b7c001b38d5937bb281d Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Mon, 9 Feb 2026 10:56:17 +0100 Subject: [PATCH 22/26] Fix GPS +8mA power leak when disabled (nRF52) On the T114, GPS_RESET (pin 38) is the same pin as PIN_3V3_EN. MicroNMEALocationProvider::begin() sets pin 38 HIGH (powering the 3V3 rail) but stop() never set it back LOW, leaving the GPS module powered even when disabled. Assert reset pin in stop() to mirror begin(), and guard _location->loop() behind gps_active check. Fixes meshcore-dev/MeshCore#1628 --- src/helpers/sensors/EnvironmentSensorManager.cpp | 4 +++- src/helpers/sensors/MicroNMEALocationProvider.h | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index a75d378c..f7b08508 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -707,7 +707,9 @@ void EnvironmentSensorManager::loop() { static long next_gps_update = 0; #if ENV_INCLUDE_GPS - _location->loop(); + if (gps_active) { + _location->loop(); + } if (millis() > next_gps_update) { if(gps_active){ diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index 574570a3..1de75327 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -79,7 +79,10 @@ public : if (_pin_en != -1) { digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE); } - if (_peripher_power) _peripher_power->release(); + if (_pin_reset != -1) { + digitalWrite(_pin_reset, GPS_RESET_FORCE); + } + if (_peripher_power) _peripher_power->release(); } bool isEnabled() override { From 00566741f65fd90ab3f563cb198fcc142b909b59 Mon Sep 17 00:00:00 2001 From: Wouter Bijen Date: Mon, 2 Mar 2026 20:41:41 +0100 Subject: [PATCH 23/26] Add configurable max hops filter for auto-add contacts Filter auto-add of new contacts by hop count (issues #1533, #1546). Setting is configurable from the companion app via extended CMD_SET/GET_AUTOADD_CONFIG protocol (0 = no limit, 1-63 = max hops). Co-Authored-By: Claude Opus 4.6 --- examples/companion_radio/DataStore.cpp | 2 ++ examples/companion_radio/MyMesh.cpp | 10 +++++++++- examples/companion_radio/MyMesh.h | 1 + examples/companion_radio/NodePrefs.h | 1 + src/helpers/BaseChatMesh.cpp | 9 +++++++++ src/helpers/BaseChatMesh.h | 1 + 6 files changed, 23 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index fba64e8c..d9ebacb4 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -229,6 +229,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85 file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 + file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88 file.close(); } @@ -265,6 +266,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85 file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 + file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88 file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index c96f7e01..7477ce8e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -318,6 +318,10 @@ bool MyMesh::shouldOverwriteWhenFull() const { return (_prefs.autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) != 0; } +uint8_t MyMesh::getAutoAddMaxHops() const { + return _prefs.autoadd_max_hops; +} + void MyMesh::onContactOverwrite(const uint8_t* pub_key) { _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage if (_serial->isConnected()) { @@ -1785,12 +1789,16 @@ void MyMesh::handleCmdFrame(size_t len) { } } else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) { _prefs.autoadd_config = cmd_frame[1]; + if (len >= 3) { + _prefs.autoadd_max_hops = cmd_frame[2]; + } savePrefs(); - writeOKFrame(); + writeOKFrame(); } else if (cmd_frame[0] == CMD_GET_AUTOADD_CONFIG) { int i = 0; out_frame[i++] = RESP_CODE_AUTOADD_CONFIG; out_frame[i++] = _prefs.autoadd_config; + out_frame[i++] = _prefs.autoadd_max_hops; _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ) { int i = 0; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 87e6cf33..fe2c19bf 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -119,6 +119,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; bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index ec60c94a..3fd96660 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -30,4 +30,5 @@ struct NodePrefs { // persisted to file uint8_t autoadd_config; // bitmask for auto-add contacts config uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending + uint8_t autoadd_max_hops; // 0 = no limit, 1-63 = max hops for auto-add }; \ No newline at end of file diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 5ec678c7..279e361c 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -141,6 +141,15 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, return; } + // check hop limit for new contacts (0 = no limit) + 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); // let UI know + return; + } + from = allocateContactSlot(); if (from == NULL) { ContactInfo ci; diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index fd391b98..ad14cc1f 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -98,6 +98,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-63 = max hops for auto-add 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; From c016db86d5e18d1cbd4a90e1b1391b532d90feea Mon Sep 17 00:00:00 2001 From: Wouter Bijen Date: Tue, 3 Mar 2026 08:37:22 +0100 Subject: [PATCH 24/26] Address PR review: subtract-1 encoding and clamp max_hops - Change > to >= so stored value 1 means direct/0-hop only (liamcottle) - Clamp max_hops to 63 on write since getPathHashCount() caps at 63 (robekl) - Update comments to reflect encoding: 0=no limit, 1=direct only, N=up to N-1 hops Co-Authored-By: Claude Opus 4.6 --- examples/companion_radio/MyMesh.cpp | 2 +- examples/companion_radio/NodePrefs.h | 2 +- src/helpers/BaseChatMesh.cpp | 4 ++-- src/helpers/BaseChatMesh.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 7477ce8e..6ec24ab1 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1790,7 +1790,7 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) { _prefs.autoadd_config = cmd_frame[1]; if (len >= 3) { - _prefs.autoadd_max_hops = cmd_frame[2]; + _prefs.autoadd_max_hops = min(cmd_frame[2], (uint8_t)63); } savePrefs(); writeOKFrame(); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 3fd96660..0a59a6dc 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -30,5 +30,5 @@ struct NodePrefs { // persisted to file uint8_t autoadd_config; // bitmask for auto-add contacts config uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending - uint8_t autoadd_max_hops; // 0 = no limit, 1-63 = max hops for auto-add + uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct only, N = up to N-1 hops (max 63) }; \ No newline at end of file diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 279e361c..84c6ae4a 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -141,9 +141,9 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, return; } - // check hop limit for new contacts (0 = no limit) + // check hop limit for new contacts (0 = no limit, 1 = direct only, N = up to N-1 hops) uint8_t max_hops = getAutoAddMaxHops(); - if (max_hops > 0 && packet->getPathHashCount() > max_hops) { + if (max_hops > 0 && packet->getPathHashCount() >= max_hops) { ContactInfo ci; populateContactFromAdvert(ci, id, parser, timestamp); onDiscoveredContact(ci, true, packet->path_len, packet->path); // let UI know diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index ad14cc1f..0dd88739 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -98,7 +98,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-63 = max hops for auto-add + virtual uint8_t getAutoAddMaxHops() const { return 0; } // 0 = no limit, 1 = direct only, 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; From 2cb08775c010bd1474a0b57d942812965406beda Mon Sep 17 00:00:00 2001 From: Wouter Bijen Date: Tue, 3 Mar 2026 08:40:17 +0100 Subject: [PATCH 25/26] Clarify comment wording: 1 = direct (0 hops) Co-Authored-By: Claude Opus 4.6 --- examples/companion_radio/NodePrefs.h | 2 +- src/helpers/BaseChatMesh.cpp | 2 +- src/helpers/BaseChatMesh.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 0a59a6dc..0c887802 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -30,5 +30,5 @@ struct NodePrefs { // persisted to file uint8_t autoadd_config; // bitmask for auto-add contacts config uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending - uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct only, N = up to N-1 hops (max 63) + uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 63) }; \ No newline at end of file diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 84c6ae4a..33d7edbe 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -141,7 +141,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, return; } - // check hop limit for new contacts (0 = no limit, 1 = direct only, N = up to N-1 hops) + // 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; diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 0dd88739..ab90d581 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -98,7 +98,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 only, N = up to N-1 hops + 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; From 1d190ad9440d54b01afee1d2c763c43a2459170c Mon Sep 17 00:00:00 2001 From: Wouter Bijen Date: Tue, 3 Mar 2026 09:05:53 +0100 Subject: [PATCH 26/26] Clamp max_hops to 64 to cover full protocol hop range (0-63) --- examples/companion_radio/MyMesh.cpp | 2 +- examples/companion_radio/NodePrefs.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6ec24ab1..1f71a9bc 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1790,7 +1790,7 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_SET_AUTOADD_CONFIG) { _prefs.autoadd_config = cmd_frame[1]; if (len >= 3) { - _prefs.autoadd_max_hops = min(cmd_frame[2], (uint8_t)63); + _prefs.autoadd_max_hops = min(cmd_frame[2], (uint8_t)64); } savePrefs(); writeOKFrame(); diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 0c887802..090209c1 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -30,5 +30,5 @@ struct NodePrefs { // persisted to file uint8_t autoadd_config; // bitmask for auto-add contacts config uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending - uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 63) + uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) }; \ No newline at end of file