From 23066573e56528a6be6bf2aef27cc23edad8446e Mon Sep 17 00:00:00 2001 From: agessaman Date: Thu, 6 Aug 2026 15:29:14 -0700 Subject: [PATCH 01/19] fix(station-g3): expose FEM gain preferences --- docs/cli_commands.md | 15 +++++ examples/simple_repeater/MyMesh.cpp | 2 + examples/simple_room_server/MyMesh.cpp | 2 + examples/simple_sensor/SensorMesh.cpp | 2 + src/MeshCore.h | 4 ++ src/helpers/CommonCLI.cpp | 32 ++++++++- src/helpers/CommonCLI.h | 4 +- variants/station_g3_esp32/LoRaFEMControl.cpp | 69 ++++++++++++++++++++ variants/station_g3_esp32/LoRaFEMControl.h | 20 ++++++ variants/station_g3_esp32/StationG3Board.cpp | 35 +++++++++- variants/station_g3_esp32/StationG3Board.h | 53 +++++---------- variants/station_g3_esp32/platformio.ini | 4 +- 12 files changed, 198 insertions(+), 44 deletions(-) create mode 100644 variants/station_g3_esp32/LoRaFEMControl.cpp create mode 100644 variants/station_g3_esp32/LoRaFEMControl.h diff --git a/docs/cli_commands.md b/docs/cli_commands.md index b618ae2b..390c8e04 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -291,6 +291,21 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change the LoRa FEM transmit-path gain state on supported boards +**Usage:** +- `get radio.fem.txgain` +- `set radio.fem.txgain ` + +**Parameters:** +- `state`: `on`|`off` + +**Notes:** +- This controls a software-selectable external LoRa FEM transmit gain where the board supports it. +- On Station G3, remove the PA PL1 jumper to allow software control. `on` selects PA PL1 high/short and `off` selects PA PL1 low/open. The PA PL2 hardware jumper determines whether this switches between power levels 1/3 or 2/4. +- Select an operating level and SX1262 transmit power that comply with local RF limits and the Station G3 power-supply requirements. + +--- + ### System #### View or change this node's name diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index c93ba1a4..d3c2e160 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -913,6 +913,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc #endif #endif _prefs.radio_fem_rxgain = 1; + _prefs.radio_fem_txgain = 0; pending_discover_tag = 0; pending_discover_until = 0; @@ -962,6 +963,7 @@ void MyMesh::begin(FILESYSTEM *fs) { MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); + board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 0aff39cc..f7ec1480 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -683,6 +683,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc #endif #endif _prefs.radio_fem_rxgain = 1; + _prefs.radio_fem_txgain = 0; next_post_idx = 0; next_client_idx = 0; @@ -726,6 +727,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); + board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 17f8e323..9bfa5ec6 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -735,6 +735,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; _prefs.radio_fem_rxgain = 1; + _prefs.radio_fem_txgain = 0; memset(default_scope.key, 0, sizeof(default_scope.key)); } @@ -771,6 +772,7 @@ void SensorMesh::begin(FILESYSTEM* fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); + board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); updateAdvertTimer(); updateFloodAdvertTimer(); diff --git a/src/MeshCore.h b/src/MeshCore.h index 89e60b1f..e67371ef 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -67,6 +67,10 @@ public: virtual bool setLoRaFemLnaEnabled(bool enable) { return false; } virtual bool canControlLoRaFemLna() const { return false; } virtual bool isLoRaFemLnaEnabled() const { return false; } + // Software-selectable external FEM transmit gain. This is not a PA power switch. + virtual bool setLoRaFemPaGainEnabled(bool enable) { return false; } + virtual bool canControlLoRaFemPaGain() const { return false; } + virtual bool isLoRaFemPaGainEnabled() const { return false; } // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 07181e16..56a52a0b 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -102,7 +102,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 + file.read((uint8_t *)&_prefs->radio_fem_txgain, sizeof(_prefs->radio_fem_txgain)); // 295 + // next: 296 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -133,6 +134,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy // sanitise settings _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean + _prefs->radio_fem_txgain = constrain(_prefs->radio_fem_txgain, 0, 1); // boolean _prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean file.close(); @@ -562,6 +564,28 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error: state must be on or off"); } + } else if (memcmp(config, "radio.fem.txgain ", 17) == 0) { + if (!_board->canControlLoRaFemPaGain()) { + strcpy(reply, "Error: unsupported"); + } else if (memcmp(&config[17], "on", 2) == 0) { + if (_board->setLoRaFemPaGainEnabled(true)) { + _prefs->radio_fem_txgain = 1; + savePrefs(); + strcpy(reply, "OK - LoRa FEM TX gain on"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else if (memcmp(&config[17], "off", 3) == 0) { + if (_board->setLoRaFemPaGainEnabled(false)) { + _prefs->radio_fem_txgain = 0; + savePrefs(); + strcpy(reply, "OK - LoRa FEM TX gain off"); + } else { + strcpy(reply, "Error: failed to apply LoRa FEM TX gain"); + } + } else { + strcpy(reply, "Error: state must be on or off"); + } } else if (memcmp(config, "radio ", 6) == 0) { strcpy(tmp, &config[6]); const char *parts[4]; @@ -827,6 +851,12 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep } else { sprintf(reply, "> %s", _board->isLoRaFemLnaEnabled() ? "on" : "off"); } + } else if (memcmp(config, "radio.fem.txgain", 16) == 0) { + if (!_board->canControlLoRaFemPaGain()) { + strcpy(reply, "Error: unsupported"); + } else { + sprintf(reply, "> %s", _board->isLoRaFemPaGainEnabled() ? "on" : "off"); + } } else if (memcmp(config, "radio", 5) == 0) { char freq[16], bw[16]; strcpy(freq, StrHelper::ftoa(_prefs->freq)); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 2a9ec43b..237c758e 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -65,6 +65,7 @@ public: char owner_info[120]; uint8_t rx_boosted_gain = 0; // power settings uint8_t radio_fem_rxgain = 0; // LoRa FEM RX gain setting + uint8_t radio_fem_txgain = 0; // LoRa FEM TX gain setting uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t loop_detect = 0; uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean) @@ -82,7 +83,8 @@ private: def("cad", _parent->cad_enabled); def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); - def("fem_rxgain", _parent->rx_boosted_gain); + def("fem_rxgain", _parent->radio_fem_rxgain); + def("fem_txgain", _parent->radio_fem_txgain); def("tx", _parent->tx_power_dbm); def("af", _parent->airtime_factor); def("rxdelay", _parent->rx_delay_base); diff --git a/variants/station_g3_esp32/LoRaFEMControl.cpp b/variants/station_g3_esp32/LoRaFEMControl.cpp new file mode 100644 index 00000000..04ac2ff8 --- /dev/null +++ b/variants/station_g3_esp32/LoRaFEMControl.cpp @@ -0,0 +1,69 @@ +#include "LoRaFEMControl.h" + +#include +#include + +void LoRaFEMControl::init() { +#ifdef P_PA1_EN + rtc_gpio_hold_dis((gpio_num_t)P_PA1_EN); + pinMode(P_PA1_EN, OUTPUT); + setPAGainEnable(pa_gain_enabled); +#endif + +#ifdef P_PRIMARY_LNA_EN + rtc_gpio_hold_dis((gpio_num_t)P_PRIMARY_LNA_EN); + pinMode(P_PRIMARY_LNA_EN, OUTPUT); + setRxModeEnable(); +#endif +} + +void LoRaFEMControl::setSleepModeEnable() { +#ifdef P_PA1_EN + // PA PL1 low/open selects the lower of the two hardware-jumper-selected levels. + digitalWrite(P_PA1_EN, !P_PA1_EN_ACTIVE); +#endif +#ifdef P_PRIMARY_LNA_EN + // Preserve the existing Station G3 power-off state. + digitalWrite(P_PRIMARY_LNA_EN, P_PRIMARY_LNA_EN_ACTIVE); +#endif +} + +void LoRaFEMControl::setTxModeEnable() { +#ifdef P_PRIMARY_LNA_EN + digitalWrite(P_PRIMARY_LNA_EN, !P_PRIMARY_LNA_EN_ACTIVE); +#endif +} + +void LoRaFEMControl::setRxModeEnable() { +#ifdef P_PRIMARY_LNA_EN + digitalWrite(P_PRIMARY_LNA_EN, lna_enabled ? P_PRIMARY_LNA_EN_ACTIVE : !P_PRIMARY_LNA_EN_ACTIVE); +#endif +} + +void LoRaFEMControl::setLNAEnable(bool enabled) { + lna_enabled = enabled; + setRxModeEnable(); +} + +void LoRaFEMControl::setPAGainEnable(bool enabled) { + pa_gain_enabled = enabled; +#ifdef P_PA1_EN + digitalWrite(P_PA1_EN, enabled ? P_PA1_EN_ACTIVE : !P_PA1_EN_ACTIVE); +#endif +} + +bool LoRaFEMControl::canControlLNA() const { +#ifdef P_PRIMARY_LNA_EN + return true; +#else + return false; +#endif +} + +bool LoRaFEMControl::canControlPAGain() const { +#ifdef P_PA1_EN + return true; +#else + return false; +#endif +} diff --git a/variants/station_g3_esp32/LoRaFEMControl.h b/variants/station_g3_esp32/LoRaFEMControl.h new file mode 100644 index 00000000..429d6127 --- /dev/null +++ b/variants/station_g3_esp32/LoRaFEMControl.h @@ -0,0 +1,20 @@ +#pragma once + +class LoRaFEMControl { +public: + void init(); + void setSleepModeEnable(); + void setTxModeEnable(); + void setRxModeEnable(); + void setLNAEnable(bool enabled); + void setPAGainEnable(bool enabled); + + bool canControlLNA() const; + bool canControlPAGain() const; + bool isLNAEnabled() const { return lna_enabled; } + bool isPAGainEnabled() const { return pa_gain_enabled; } + +private: + bool lna_enabled = true; + bool pa_gain_enabled = false; +}; diff --git a/variants/station_g3_esp32/StationG3Board.cpp b/variants/station_g3_esp32/StationG3Board.cpp index 4a498311..dd863aca 100644 --- a/variants/station_g3_esp32/StationG3Board.cpp +++ b/variants/station_g3_esp32/StationG3Board.cpp @@ -1,15 +1,46 @@ #include "StationG3Board.h" void StationG3Board::powerOff() { + loRaFEMControl.setSleepModeEnable(); #ifdef P_PA1_EN - setPAModeHigh(false); rtc_gpio_hold_en((gpio_num_t)P_PA1_EN); #endif #ifdef P_PRIMARY_LNA_EN - setPrimaryLNAControl(true); rtc_gpio_hold_en((gpio_num_t)P_PRIMARY_LNA_EN); #endif ESP32Board::powerOff(); } + +bool StationG3Board::setLoRaFemLnaEnabled(bool enable) { + if (!loRaFEMControl.canControlLNA()) { + return false; + } + loRaFEMControl.setLNAEnable(enable); + return true; +} + +bool StationG3Board::canControlLoRaFemLna() const { + return loRaFEMControl.canControlLNA(); +} + +bool StationG3Board::isLoRaFemLnaEnabled() const { + return loRaFEMControl.isLNAEnabled(); +} + +bool StationG3Board::setLoRaFemPaGainEnabled(bool enable) { + if (!loRaFEMControl.canControlPAGain()) { + return false; + } + loRaFEMControl.setPAGainEnable(enable); + return true; +} + +bool StationG3Board::canControlLoRaFemPaGain() const { + return loRaFEMControl.canControlPAGain(); +} + +bool StationG3Board::isLoRaFemPaGainEnabled() const { + return loRaFEMControl.isPAGainEnabled(); +} diff --git a/variants/station_g3_esp32/StationG3Board.h b/variants/station_g3_esp32/StationG3Board.h index 4b1fb81c..52628eb6 100644 --- a/variants/station_g3_esp32/StationG3Board.h +++ b/variants/station_g3_esp32/StationG3Board.h @@ -3,45 +3,15 @@ #include #include #include - -#ifndef P_PRIMARY_LNA_EN_ACTIVE -#define P_PRIMARY_LNA_EN_ACTIVE LOW -#endif - -#ifndef P_PA1_EN_ACTIVE -#define P_PA1_EN_ACTIVE HIGH -#endif +#include "LoRaFEMControl.h" class StationG3Board : public ESP32Board { - void setPAModeHigh(bool enabled) { -#ifdef P_PA1_EN - // Station G3 PA PL1 mode: LOW/open is PA low, HIGH/short is PA high. - digitalWrite(P_PA1_EN, enabled ? P_PA1_EN_ACTIVE : !P_PA1_EN_ACTIVE); -#endif - } - - void setPrimaryLNAControl(bool enabled) { -#ifdef P_PRIMARY_LNA_EN - // Station G3 primary LNA mode is active-low: LOW/open is LNA on, HIGH/short is LNA off. - digitalWrite(P_PRIMARY_LNA_EN, enabled ? P_PRIMARY_LNA_EN_ACTIVE : !P_PRIMARY_LNA_EN_ACTIVE); -#endif - } - public: + LoRaFEMControl loRaFEMControl; + void begin() { ESP32Board::begin(); - -#ifdef P_PA1_EN - rtc_gpio_hold_dis((gpio_num_t)P_PA1_EN); - pinMode(P_PA1_EN, OUTPUT); - setPAModeHigh(false); -#endif - -#ifdef P_PRIMARY_LNA_EN - rtc_gpio_hold_dis((gpio_num_t)P_PRIMARY_LNA_EN); - pinMode(P_PRIMARY_LNA_EN, OUTPUT); - setPrimaryLNAControl(true); -#endif + loRaFEMControl.init(); esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_DEEPSLEEP) { @@ -56,23 +26,30 @@ public: } void setPrimaryLNAEnable(bool enabled) { - setPrimaryLNAControl(enabled); + loRaFEMControl.setLNAEnable(enabled); } void setPrimaryPAHighPower(bool enabled) { - setPAModeHigh(enabled); + loRaFEMControl.setPAGainEnable(enabled); } void onBeforeTransmit() override { ESP32Board::onBeforeTransmit(); - setPrimaryLNAControl(false); + loRaFEMControl.setTxModeEnable(); } void onAfterTransmit() override { ESP32Board::onAfterTransmit(); - setPrimaryLNAControl(true); + loRaFEMControl.setRxModeEnable(); } + bool setLoRaFemLnaEnabled(bool enable) override; + bool canControlLoRaFemLna() const override; + bool isLoRaFemLnaEnabled() const override; + bool setLoRaFemPaGainEnabled(bool enable) override; + bool canControlLoRaFemPaGain() const override; + bool isLoRaFemPaGainEnabled() const override; + void powerOff() override; uint16_t getBattMilliVolts() override { diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index e4a66a18..074d6a2e 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -17,11 +17,11 @@ build_flags = -D P_LORA_SCLK=12 -D P_LORA_MISO=14 -D P_LORA_MOSI=13 - -D P_PA1_EN=9 ; PA PL1 Mode: LOW/open is PA low, HIGH/short is PA high. + -D P_PA1_EN=9 ; PA PL1 Mode: LOW/open selects low level, HIGH/short selects high level. -D P_PA1_EN_ACTIVE=HIGH -D P_PRIMARY_LNA_EN=10 ; Primary Slot LNA Mode: LOW/open is LNA on, HIGH/short is LNA off. -D P_PRIMARY_LNA_EN_ACTIVE=LOW - -D LORA_TX_POWER=7 ; configured as 7dbm, because the final output will be ~27dbm (~0.5w) if the PA is enabled. + -D LORA_TX_POWER=7 ; SX1262 input power to the Station G3 PA; final output depends on PA PL1/PL2 level. -D MAX_LORA_TX_POWER=22 ; -D P_LORA_TX_LED=35 -D PIN_BOARD_SDA=5 From c58c9b2f31798b468bad861b021b395ed857c110 Mon Sep 17 00:00:00 2001 From: Adam Gessaman Date: Sat, 8 Aug 2026 14:53:57 -0700 Subject: [PATCH 02/19] fix(station-g3): apply FEM PA level at TX start PA PL1 re-targets the PA's DC-DC supply rail rather than selecting a logic-level gain, and the serial CLI is serviced on every main-loop pass regardless of whether a transmit is in flight. A `set radio.fem.txgain` write could therefore move the rail mid-transmit, while the SX1262 was still driving the PA at full input power. Record the requested level in setPAGainEnable() and drive the pin from setTxModeEnable(), which runs from onBeforeTransmit() ahead of startTransmit(). The level only matters while transmitting, so deferring costs nothing. Document that the pref is saved immediately but applied at the next transmit, so `get radio.fem.txgain` can lead the hardware until then. --- docs/cli_commands.md | 1 + variants/station_g3_esp32/LoRaFEMControl.cpp | 14 ++++++++++++-- variants/station_g3_esp32/LoRaFEMControl.h | 2 ++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 390c8e04..8772b929 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -303,6 +303,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - This controls a software-selectable external LoRa FEM transmit gain where the board supports it. - On Station G3, remove the PA PL1 jumper to allow software control. `on` selects PA PL1 high/short and `off` selects PA PL1 low/open. The PA PL2 hardware jumper determines whether this switches between power levels 1/3 or 2/4. - Select an operating level and SX1262 transmit power that comply with local RF limits and the Station G3 power-supply requirements. +- The setting is saved immediately, but on Station G3 the level is applied to the hardware at the start of the next transmit, so that the PA supply rail is never re-targeted while the PA is being driven. `get` reports the configured state, which may lead the hardware until the node next transmits. --- diff --git a/variants/station_g3_esp32/LoRaFEMControl.cpp b/variants/station_g3_esp32/LoRaFEMControl.cpp index 04ac2ff8..8d4f90f6 100644 --- a/variants/station_g3_esp32/LoRaFEMControl.cpp +++ b/variants/station_g3_esp32/LoRaFEMControl.cpp @@ -7,7 +7,7 @@ void LoRaFEMControl::init() { #ifdef P_PA1_EN rtc_gpio_hold_dis((gpio_num_t)P_PA1_EN); pinMode(P_PA1_EN, OUTPUT); - setPAGainEnable(pa_gain_enabled); + applyPAGain(); #endif #ifdef P_PRIMARY_LNA_EN @@ -29,6 +29,10 @@ void LoRaFEMControl::setSleepModeEnable() { } void LoRaFEMControl::setTxModeEnable() { + // Latch the requested PA level here, before the SX1262 starts driving the PA. PA PL1 + // retargets the PA's DC-DC rail, so moving it mid-transmit collapses the supply while + // the PA is still driven at full input power. + applyPAGain(); #ifdef P_PRIMARY_LNA_EN digitalWrite(P_PRIMARY_LNA_EN, !P_PRIMARY_LNA_EN_ACTIVE); #endif @@ -46,9 +50,15 @@ void LoRaFEMControl::setLNAEnable(bool enabled) { } void LoRaFEMControl::setPAGainEnable(bool enabled) { + // Recorded only -- the pin is driven from setTxModeEnable(). The PA level only matters + // while transmitting, so deferring costs nothing and keeps the rail change out of an + // in-flight transmit (the CLI runs on every main-loop pass, including mid-TX). pa_gain_enabled = enabled; +} + +void LoRaFEMControl::applyPAGain() { #ifdef P_PA1_EN - digitalWrite(P_PA1_EN, enabled ? P_PA1_EN_ACTIVE : !P_PA1_EN_ACTIVE); + digitalWrite(P_PA1_EN, pa_gain_enabled ? P_PA1_EN_ACTIVE : !P_PA1_EN_ACTIVE); #endif } diff --git a/variants/station_g3_esp32/LoRaFEMControl.h b/variants/station_g3_esp32/LoRaFEMControl.h index 429d6127..f622de91 100644 --- a/variants/station_g3_esp32/LoRaFEMControl.h +++ b/variants/station_g3_esp32/LoRaFEMControl.h @@ -15,6 +15,8 @@ public: bool isPAGainEnabled() const { return pa_gain_enabled; } private: + void applyPAGain(); + bool lna_enabled = true; bool pa_gain_enabled = false; }; From b75a548d7f03c6404009274f2c58063a8e36cef9 Mon Sep 17 00:00:00 2001 From: taco Date: Sun, 9 Aug 2026 16:50:44 +1000 Subject: [PATCH 03/19] build fix for R1 Neo repeater added missing declarations for user_btn and NullDisplayDriver --- variants/muziworks_r1_neo/platformio.ini | 1 + variants/muziworks_r1_neo/target.cpp | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/variants/muziworks_r1_neo/platformio.ini b/variants/muziworks_r1_neo/platformio.ini index 52dc3e38..2fbb8ad6 100644 --- a/variants/muziworks_r1_neo/platformio.ini +++ b/variants/muziworks_r1_neo/platformio.ini @@ -10,6 +10,7 @@ build_flags = ${nrf52_base.build_flags} -D NRF52_POWER_MANAGEMENT -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper + -D DISPLAY_CLASS=NullDisplayDriver -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 diff --git a/variants/muziworks_r1_neo/target.cpp b/variants/muziworks_r1_neo/target.cpp index 68655c5d..f667a282 100644 --- a/variants/muziworks_r1_neo/target.cpp +++ b/variants/muziworks_r1_neo/target.cpp @@ -4,12 +4,17 @@ R1NeoBoard board; -DISPLAY_CLASS display; - RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); - WRAPPER_CLASS radio_driver(radio, board); +#ifdef DISPLAY_CLASS + NullDisplayDriver display; +#endif + +#ifdef PIN_USER_BTN +MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); From e8bbcd4580f6c5dd86e011edd7ffe48d1a069f8a Mon Sep 17 00:00:00 2001 From: taco Date: Sun, 9 Aug 2026 16:51:45 +1000 Subject: [PATCH 04/19] R1 neo: remove stray lib_deps entry --- variants/muziworks_r1_neo/platformio.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/variants/muziworks_r1_neo/platformio.ini b/variants/muziworks_r1_neo/platformio.ini index 2fbb8ad6..c139e34a 100644 --- a/variants/muziworks_r1_neo/platformio.ini +++ b/variants/muziworks_r1_neo/platformio.ini @@ -101,7 +101,6 @@ build_src_filter = ${R1Neo.build_src_filter} +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${R1Neo.lib_deps} - ${rak4631.lib_deps} densaugeo/base64 @ ~1.4.0 end2endzone/NonBlockingRTTTL@^1.3.0 From 39934d6999143397591f7c32b9fb468c2f336498 Mon Sep 17 00:00:00 2001 From: taco Date: Sun, 9 Aug 2026 18:33:27 +1000 Subject: [PATCH 05/19] minewsemi: build fix and platformio.ini tidy up fixes minewsemi repeater failing build due to user_btn missing. --- variants/minewsemi_me25ls01/platformio.ini | 18 +----------------- variants/minewsemi_me25ls01/target.cpp | 4 ++-- variants/minewsemi_me25ls01/target.h | 9 +++++---- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/variants/minewsemi_me25ls01/platformio.ini b/variants/minewsemi_me25ls01/platformio.ini index d115a1f1..1743e719 100644 --- a/variants/minewsemi_me25ls01/platformio.ini +++ b/variants/minewsemi_me25ls01/platformio.ini @@ -21,6 +21,7 @@ build_flags = ${nrf52_base.build_flags} -D ENV_INCLUDE_INA219=1 build_src_filter = ${nrf52_base.build_src_filter} + + + +<../variants/minewsemi_me25ls01> + debug_tool = jlink @@ -55,7 +56,6 @@ build_flags = ${me25ls01.build_flags} ;-D PIN_BUZZER=25 ;-D PIN_BUZZER_EN=37 build_src_filter = ${me25ls01.build_src_filter} - + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> @@ -63,10 +63,6 @@ build_src_filter = ${me25ls01.build_src_filter} [env:Minewsemi_me25ls01_repeater] extends = me25ls01 build_flags = ${me25ls01.build_flags} - -D MAX_CONTACTS=100 - -D MAX_GROUP_CHANNELS=8 - -D BLE_PIN_CODE=123456 -; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 @@ -85,10 +81,6 @@ build_src_filter = ${me25ls01.build_src_filter} [env:Minewsemi_me25ls01_room_server] extends = me25ls01 build_flags = ${me25ls01.build_flags} - -D MAX_CONTACTS=100 - -D MAX_GROUP_CHANNELS=8 -; -D BLE_PIN_CODE=123456 -; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 @@ -103,15 +95,10 @@ build_flags = ${me25ls01.build_flags} -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} +<../examples/simple_room_server> - + [env:Minewsemi_me25ls01_terminal_chat] extends = me25ls01 build_flags = ${me25ls01.build_flags} - -D MAX_CONTACTS=100 - -D MAX_GROUP_CHANNELS=8 - -D BLE_PIN_CODE=123456 -; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 @@ -126,7 +113,6 @@ build_flags = ${me25ls01.build_flags} -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} +<../examples/simple_secure_chat/main.cpp> - + [env:Minewsemi_me25ls01_companion_radio_usb] extends = me25ls01 @@ -137,7 +123,6 @@ build_flags = ${me25ls01.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 ;-D BLE_PIN_CODE=123456 -; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 @@ -147,7 +132,6 @@ build_flags = ${me25ls01.build_flags} -D ENABLE_USB_INTERFACE build_src_filter = ${me25ls01.build_src_filter} + - + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> diff --git a/variants/minewsemi_me25ls01/target.cpp b/variants/minewsemi_me25ls01/target.cpp index 9944a38b..41fc71a7 100644 --- a/variants/minewsemi_me25ls01/target.cpp +++ b/variants/minewsemi_me25ls01/target.cpp @@ -4,7 +4,6 @@ MinewsemiME25LS01Board board; RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); - WRAPPER_CLASS radio_driver(radio, board); VolatileRTCClock rtc_clock; @@ -18,7 +17,8 @@ extern EnvironmentSensorManager sensors; #endif #ifdef DISPLAY_CLASS - NullDisplayDriver display; + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); #endif #ifndef LORA_CR diff --git a/variants/minewsemi_me25ls01/target.h b/variants/minewsemi_me25ls01/target.h index 978e616b..db44058c 100644 --- a/variants/minewsemi_me25ls01/target.h +++ b/variants/minewsemi_me25ls01/target.h @@ -11,16 +11,17 @@ #include #ifdef DISPLAY_CLASS #include -#endif - -#ifdef DISPLAY_CLASS - extern NullDisplayDriver display; + #include #endif extern MinewsemiME25LS01Board board; extern WRAPPER_CLASS radio_driver; extern VolatileRTCClock rtc_clock; extern EnvironmentSensorManager sensors; +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif bool radio_init(); mesh::LocalIdentity radio_new_identity(); From ce62c8b5d7fd90be844ab56e2f3a7d70983aaa23 Mon Sep 17 00:00:00 2001 From: taco Date: Sun, 9 Aug 2026 21:41:22 +1000 Subject: [PATCH 06/19] LR2021: add PREAMBLE_DETECTED bit and IRQ timeout logic --- src/helpers/radiolib/CustomLR2021.h | 59 +++++++++++++++++++++- src/helpers/radiolib/CustomLR2021Wrapper.h | 4 ++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/helpers/radiolib/CustomLR2021.h b/src/helpers/radiolib/CustomLR2021.h index 17944ef0..a89ae943 100644 --- a/src/helpers/radiolib/CustomLR2021.h +++ b/src/helpers/radiolib/CustomLR2021.h @@ -4,6 +4,10 @@ #include "MeshCore.h" class CustomLR2021 : public LR2021 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; bool _rx_boosted = false; public: @@ -66,11 +70,62 @@ class CustomLR2021 : public LR2021 { bool getRxBoostedGainMode() const { return _rx_boosted; } + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags + return LR2021::startReceive(RADIOLIB_LR2021_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + } + bool isReceiving() { uint32_t irq = getIrqStatus(); - bool detected = ((irq & RADIOLIB_LR2021_IRQ_SYNCWORD_VALID) || (irq & RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED)); - return detected; + bool preamble = irq & RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED; // bit 5 + bool header = irq & RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID; // bit 6 + bool hdrErr = irq & RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR; // bit 9 + uint32_t now = millis(); + if (hdrErr) { + clearIrqFlags(RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID | RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqFlags(RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR2021_IRQ_LORA_HEADER_VALID | RADIOLIB_LR2021_IRQ_LORA_HDR_CRC_ERROR); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqFlags(RADIOLIB_LR2021_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; } + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); + } + uint8_t getSpreadingFactor() const { return spreadingFactor; } }; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomLR2021Wrapper.h b/src/helpers/radiolib/CustomLR2021Wrapper.h index 87989831..8299e2bf 100644 --- a/src/helpers/radiolib/CustomLR2021Wrapper.h +++ b/src/helpers/radiolib/CustomLR2021Wrapper.h @@ -22,6 +22,10 @@ public: ((CustomLR2021 *)_radio)->setCodingRate(cr); updatePreamble(sf); applySideDetectorConfig(); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomLR2021 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomLR2021 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); + } bool configSideDetectors(const uint8_t* sideDetSFs, uint8_t num, float bw) override { From ecb8c945601ae348917b82eed9babfb8213989c2 Mon Sep 17 00:00:00 2001 From: Florent Date: Sun, 9 Aug 2026 14:46:23 -0400 Subject: [PATCH 07/19] nrf52: call nRFCrypto.begin()/end() only once --- src/Identity.cpp | 2 -- src/Utils.cpp | 12 ------------ src/helpers/NRF52Board.cpp | 13 +++++++++++++ src/helpers/radiolib/RadioLibWrappers.h | 2 -- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Identity.cpp b/src/Identity.cpp index 25419fd4..51a01ae7 100644 --- a/src/Identity.cpp +++ b/src/Identity.cpp @@ -27,11 +27,9 @@ bool Identity::verify(const uint8_t* sig, const uint8_t* message, int msg_len) c // needs much less, around 600-700bytes. The CC310 workspace is static, faster, // should save power at scale as well. static CRYS_ECEDW_TempBuff_t cc310_tmp; - nRFCrypto.begin(); CRYSError_t rc = CRYS_ECEDW_Verify((uint8_t*)sig, CRYS_ECEDW_SIGNATURE_BYTES, (uint8_t*)pub_key, CRYS_ECEDW_MOD_SIZE_IN_BYTES, (uint8_t*)message, (size_t)msg_len, &cc310_tmp); - nRFCrypto.end(); return rc == CRYS_OK; #elif 0 // NOTE: memory corruption bug was found in this function!! diff --git a/src/Utils.cpp b/src/Utils.cpp index d4bc8c45..5ae7f0e2 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -24,9 +24,7 @@ uint32_t RNG::nextInt(uint32_t _min, uint32_t _max) { void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* msg, int msg_len) { #ifdef USE_CC310_HW_CRYPTO static CRYS_HASH_Result_t result; - nRFCrypto.begin(); CRYS_HASH(CRYS_HASH_SHA256_mode, (uint8_t*)msg, (size_t)msg_len, result); - nRFCrypto.end(); memcpy(hash, result, hash_len); #else SHA256 sha; @@ -39,12 +37,10 @@ void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* frag1, int fra #ifdef USE_CC310_HW_CRYPTO static CRYS_HASHUserContext_t ctx; static CRYS_HASH_Result_t result; - nRFCrypto.begin(); CRYS_HASH_Init(&ctx, CRYS_HASH_SHA256_mode); CRYS_HASH_Update(&ctx, (uint8_t*)frag1, (size_t)frag1_len); CRYS_HASH_Update(&ctx, (uint8_t*)frag2, (size_t)frag2_len); CRYS_HASH_Finish(&ctx, result); - nRFCrypto.end(); memcpy(hash, result, hash_len); #else SHA256 sha; @@ -62,7 +58,6 @@ int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* s const uint8_t* sp = src; size_t dummy_out = 0; - nRFCrypto.begin(); SaSi_AesInit(&ctx, SASI_AES_DECRYPT, SASI_AES_MODE_ECB, SASI_AES_PADDING_NONE); SaSi_AesSetKey(&ctx, SASI_AES_USER_KEY, &keyData, sizeof(keyData)); while (sp - src < src_len) { @@ -71,7 +66,6 @@ int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* s } SaSi_AesFinish(&ctx, 0, NULL, 0, NULL, &dummy_out); SaSi_AesFree(&ctx); - nRFCrypto.end(); return sp - src; #else AES128 aes; @@ -95,7 +89,6 @@ int Utils::encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* s uint8_t* dp = dest; size_t dummy_out = 0; - nRFCrypto.begin(); SaSi_AesInit(&ctx, SASI_AES_ENCRYPT, SASI_AES_MODE_ECB, SASI_AES_PADDING_NONE); SaSi_AesSetKey(&ctx, SASI_AES_USER_KEY, &keyData, sizeof(keyData)); while (src_len >= 16) { @@ -110,7 +103,6 @@ int Utils::encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* s } SaSi_AesFinish(&ctx, 0, NULL, 0, NULL, &dummy_out); SaSi_AesFree(&ctx); - nRFCrypto.end(); return dp - dest; #else AES128 aes; @@ -138,11 +130,9 @@ int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uin #ifdef USE_CC310_HW_CRYPTO static CRYS_HMACUserContext_t hmac_ctx; static CRYS_HASH_Result_t hmac_result; - nRFCrypto.begin(); CRYS_HMAC_Init(&hmac_ctx, CRYS_HASH_SHA256_mode, (uint8_t*)shared_secret, PUB_KEY_SIZE); CRYS_HMAC_Update(&hmac_ctx, dest + CIPHER_MAC_SIZE, enc_len); CRYS_HMAC_Finish(&hmac_ctx, hmac_result); - nRFCrypto.end(); memcpy(dest, hmac_result, CIPHER_MAC_SIZE); #else SHA256 sha; @@ -162,11 +152,9 @@ int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uin { static CRYS_HMACUserContext_t hmac_ctx; static CRYS_HASH_Result_t hmac_result; - nRFCrypto.begin(); CRYS_HMAC_Init(&hmac_ctx, CRYS_HASH_SHA256_mode, (uint8_t*)shared_secret, PUB_KEY_SIZE); CRYS_HMAC_Update(&hmac_ctx, (uint8_t*)(src + CIPHER_MAC_SIZE), src_len - CIPHER_MAC_SIZE); CRYS_HMAC_Finish(&hmac_ctx, hmac_result); - nRFCrypto.end(); memcpy(hmac, hmac_result, CIPHER_MAC_SIZE); } #else diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index b6c8fec5..eb88c897 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -5,6 +5,10 @@ #include #include +#ifdef USE_CC310_HW_CRYPTO +#include +#endif + static BLEDfu bledfu; static void connect_callback(uint16_t conn_handle) { @@ -21,6 +25,11 @@ static void disconnect_callback(uint16_t conn_handle, uint8_t reason) { void NRF52Board::begin() { startup_reason = BD_STARTUP_NORMAL; + + #ifdef USE_CC310_HW_CRYPTO + // CC310 TRNG is higher quality and environment-independent vs radio RSSI noise. + nRFCrypto.begin(); + #endif } #ifdef NRF52_POWER_MANAGEMENT @@ -352,6 +361,10 @@ void NRF52Board::shutdownPeripherals() { sensors.getLocationProvider()->stop(); } +#ifdef USE_CC310_HW_CRYPTO + nRFCrypto.end(); +#endif + // Flush serial buffers Serial.flush(); delay(100); diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 99f5ebbd..5db1e41f 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -93,9 +93,7 @@ public: void random(uint8_t* dest, size_t sz) override { #ifdef USE_CC310_HW_CRYPTO // CC310 TRNG is higher quality and environment-independent vs radio RSSI noise. - nRFCrypto.begin(); nRFCrypto.Random.generate(dest, (uint16_t)sz); - nRFCrypto.end(); #else for (int i = 0; i < sz; i++) { dest[i] = _radio->randomByte() ^ (::random(0, 256) & 0xFF); From 6f491f30332226a57291cce6c7c3864e931b374d Mon Sep 17 00:00:00 2001 From: Dan Theisen Date: Sun, 9 Aug 2026 18:04:24 -0700 Subject: [PATCH 08/19] RX Boosted Gain was set back to the compiled in value rather than the current/flash value set by the user when the ACG reset is triggered. This commit fixes this. --- src/helpers/radiolib/CustomLR1110Wrapper.h | 3 ++- src/helpers/radiolib/CustomSTM32WLxWrapper.h | 2 +- src/helpers/radiolib/CustomSX1262Wrapper.h | 4 ++-- src/helpers/radiolib/CustomSX1268Wrapper.h | 4 ++-- src/helpers/radiolib/LR11x0Reset.h | 4 ++-- src/helpers/radiolib/SX126xReset.h | 4 ++-- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 44230c61..e7aaeb93 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -19,7 +19,6 @@ public: ((CustomLR1110 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } - void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } bool isReceivingPacket() override { return ((CustomLR1110 *)_radio)->isReceiving(); } @@ -50,4 +49,6 @@ public: bool getRxBoostedGainMode() const override { return ((CustomLR1110 *)_radio)->getRxBoostedGainMode(); } + + void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz(), getRxBoostedGainMode()); } }; diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index a792a877..a4ba4971 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -35,5 +35,5 @@ public: } uint8_t getSpreadingFactor() const override { return ((CustomSTM32WLx *)_radio)->spreadingFactor; } - void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } + void doResetAGC() override { sx126xResetAGC((SX126x *)_radio, getRxBoostedGainMode()); } }; diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index bfea50ec..be314471 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -41,12 +41,12 @@ public: ((CustomSX1262 *)_radio)->sleep(false); } - void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } - bool setRxBoostedGainMode(bool en) override { return ((CustomSX1262 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; } bool getRxBoostedGainMode() const override { return ((CustomSX1262 *)_radio)->getRxBoostedGainMode(); } + + void doResetAGC() override { sx126xResetAGC((SX126x *)_radio, getRxBoostedGainMode()); } }; diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 104ba08b..70f5dabd 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -38,12 +38,12 @@ public: } uint8_t getSpreadingFactor() const override { return ((CustomSX1268 *)_radio)->spreadingFactor; } - void doResetAGC() override { sx126xResetAGC((SX126x *)_radio); } - bool setRxBoostedGainMode(bool en) override { return ((CustomSX1268 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; } bool getRxBoostedGainMode() const override { return ((CustomSX1268 *)_radio)->getRxBoostedGainMode(); } + + void doResetAGC() override { sx126xResetAGC((SX126x *)_radio, getRxBoostedGainMode()); } }; diff --git a/src/helpers/radiolib/LR11x0Reset.h b/src/helpers/radiolib/LR11x0Reset.h index d06ffc53..cdfc1f9f 100644 --- a/src/helpers/radiolib/LR11x0Reset.h +++ b/src/helpers/radiolib/LR11x0Reset.h @@ -5,7 +5,7 @@ // 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, float freqMHz) { +inline void lr11x0ResetAGC(LR11x0* radio, float freqMHz, bool rx_boost_gain) { radio->sleep(true, 0); radio->standby(RADIOLIB_LR11X0_STANDBY_RC, true); @@ -16,6 +16,6 @@ inline void lr11x0ResetAGC(LR11x0* radio, float freqMHz) { radio->calibrateImageRejection(freqMHz - 4.0f, freqMHz + 4.0f); #ifdef RX_BOOSTED_GAIN - radio->setRxBoostedGainMode(RX_BOOSTED_GAIN); + radio->setRxBoostedGainMode(rx_boost_gain); #endif } diff --git a/src/helpers/radiolib/SX126xReset.h b/src/helpers/radiolib/SX126xReset.h index 39ddb73e..472eb33b 100644 --- a/src/helpers/radiolib/SX126xReset.h +++ b/src/helpers/radiolib/SX126xReset.h @@ -5,7 +5,7 @@ // 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) { +inline void sx126xResetAGC(SX126x* radio, bool rx_boost_gain) { radio->sleep(true); radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); @@ -26,7 +26,7 @@ inline void sx126xResetAGC(SX126x* radio) { radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); #endif #ifdef SX126X_RX_BOOSTED_GAIN - radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); + radio->setRxBoostedGainMode(rx_boost_gain); #endif #ifdef SX126X_REGISTER_PATCH uint8_t r_data = 0; From 32bd6d48d6152420fc868de3edfd137cb4d92898 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Mon, 10 Aug 2026 13:39:00 +0200 Subject: [PATCH 09/19] Fix T-beam Supreme S3 display --- src/helpers/ui/SH1106Display.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index c3840c02..8b91d857 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -24,8 +24,21 @@ bool SH1106Display::begin() { // Wire must already be initialised by board.begin() before this is called. // Boards with non-standard SH1106 addresses should define DISPLAY_ADDRESS - // in their variant/platformio configuration. - return i2c_probe(Wire, DISPLAY_ADDRESS) && display.begin(DISPLAY_ADDRESS, true); + // in their variant/platformio configuration. The SA0 strap selects 0x3C or + // 0x3D and differs between revisions of the same board (e.g. T-Beam + // Supreme), so fall back to the other address of the pair. + uint8_t addr = 0; + if (i2c_probe(Wire, DISPLAY_ADDRESS)) { + addr = DISPLAY_ADDRESS; + } else if (i2c_probe(Wire, DISPLAY_ADDRESS ^ 1)) { + addr = DISPLAY_ADDRESS ^ 1; + } + // Run the Adafruit init even when no panel answered: it is what allocates + // the frame buffer and the I2C device. Skipping it leaves i2c_dev and + // spi_dev NULL, and UITask::begin() calls turnOn() regardless of our + // return value, which then dereferences the null spi_dev. + bool ok = display.begin(addr ? addr : DISPLAY_ADDRESS, true); + return addr != 0 && ok; } void SH1106Display::turnOn() From 3138ad99ba7b4cd3b78c0f7a43b6bec6f67d3c94 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Mon, 10 Aug 2026 16:56:56 +0200 Subject: [PATCH 10/19] move RadioLibWrapper noise_floor level message under special debug flag --- src/helpers/radiolib/RadioLibWrappers.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index dc851e5c..b9c095ac 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -15,7 +15,7 @@ static volatile uint8_t state = STATE_IDLE; // this function is called when a complete packet // is transmitted by the module -static +static #if defined(ESP8266) || defined(ESP32) ICACHE_RAM_ATTR #endif @@ -100,7 +100,9 @@ void RadioLibWrapper::loop() { } _floor_sample_sum = 0; + #ifdef MESH_DEBUG_NOISE_FLOOR MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor); + #endif } } @@ -224,10 +226,10 @@ static float snr_threshold[] = { -17.5,// SF11 needs at least -17.5 dB SNR -20 // SF12 needs at least -20 dB SNR }; - + float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) { if (sf < 7) return 0.0f; - + if (snr < snr_threshold[sf - 7]) return 0.0f; // Below threshold, no chance of success auto success_rate_based_on_snr = (snr - snr_threshold[sf - 7]) / 10.0; @@ -243,7 +245,7 @@ PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t // preamble + syncword + sfd + header uint32_t preamble_us = (((preambleSymbols + 8) * 4 + sfCoeff1_x4) * tsym_us) / 4; - + // airtime for max packet at current radio settings uint32_t total_us = _radio->getTimeOnAir(MAX_TRANS_UNIT); // airtime for payload only (no preamble, header or SOF) From a82dc1b9da98df20764cecb70275816c00d91150 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Mon, 10 Aug 2026 20:13:59 +0200 Subject: [PATCH 11/19] make promicro pinmap more tidy, move the LoRa pins to variant.h --- variants/promicro/PromicroBoard.h | 13 ------------- variants/promicro/variant.cpp | 32 +++++++++++++++++++++++-------- variants/promicro/variant.h | 18 +++++++++++++++-- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/variants/promicro/PromicroBoard.h b/variants/promicro/PromicroBoard.h index b190c47c..ee16c965 100644 --- a/variants/promicro/PromicroBoard.h +++ b/variants/promicro/PromicroBoard.h @@ -4,19 +4,6 @@ #include #include -#define P_LORA_NSS 13 //P1.13 45 -#define P_LORA_DIO_1 11 //P0.10 10 -#define P_LORA_RESET 10 //P0.09 9 -#define P_LORA_BUSY 16 //P0.29 29 -#define P_LORA_MISO 15 //P0.02 2 -#define P_LORA_SCLK 12 //P1.11 43 -#define P_LORA_MOSI 14 //P1.15 47 -#define SX126X_POWER_EN 21 //P0.13 13 -#define SX126X_RXEN 2 //P0.17 -#define SX126X_TXEN RADIOLIB_NC -#define SX126X_DIO2_AS_RF_SWITCH true -#define SX126X_DIO3_TCXO_VOLTAGE (1.8f) - #define PIN_VBAT_READ 17 #define ADC_MULTIPLIER (1.815f) // dependent on voltage divider resistors. TODO: more accurate battery tracking diff --git a/variants/promicro/variant.cpp b/variants/promicro/variant.cpp index 0a4c3aac..b7d281ac 100644 --- a/variants/promicro/variant.cpp +++ b/variants/promicro/variant.cpp @@ -3,13 +3,29 @@ #include "wiring_digital.h" const uint32_t g_ADigitalPinMap[] = { - 8, 6, 17, 20, 22, 24, 32, 11, 36, 38, - 9, 10, 43, 45, 47, 2, 29, 31, - 33, 34, 37, - 13, 15 + 8, // P0.08 = GPIO 0 + 6, // P0.06 = GPIO 1 + 17, // P0.17 = GPIO 2 + 20, // P0.20 = GPIO 3 + 22, // P0.22 = GPIO 4 + 24, // P0.24 = GPIO 5 + 32, // P1.00 = GPIO 6 + 11, // P0.11 = GPIO 7 + 36, // P1.04 = GPIO 8 + 38, // P1.06 = GPIO 9 + 9, // P0.09 = GPIO 10 + 10, // P0.10 = GPIO 11 + 43, // P1.11 = GPIO 12 + 45, // P1.13 = GPIO 13 + 47, // P1.15 = GPIO 14 + 2, // P0.02 = GPIO 15 + 29, // P0.29 = GPIO 16 + 31, // P0.31 = GPIO 17 + 33, // P1.01 = GPIO 18 + 34, // P1.02 = GPIO 19 + 37, // P1.05 = GPIO 20 + 13, // P0.13 = GPIO 21 + 15 // P0.15 = GPIO 22 }; -void initVariant() -{ -} - +void initVariant() {} diff --git a/variants/promicro/variant.h b/variants/promicro/variant.h index 98489da1..e8a78331 100644 --- a/variants/promicro/variant.h +++ b/variants/promicro/variant.h @@ -7,9 +7,9 @@ #pragma once #include "WVariant.h" - + //////////////////////////////////////////////////////////////////////////////// - // Low frequency clock source + // Low frequency clock source #define VARIANT_MCK (64000000ul) @@ -79,4 +79,18 @@ #define PIN_BUTTON1 (6) #define BUTTON_PIN PIN_BUTTON1 +////////////////////////////////////////////////////////////////////////////// +// LoRa +#define P_LORA_NSS (13) +#define P_LORA_DIO_1 (11) +#define P_LORA_RESET (10) +#define P_LORA_BUSY (16) +#define P_LORA_MISO (15) +#define P_LORA_SCLK (12) +#define P_LORA_MOSI (14) +#define SX126X_POWER_EN (21) +#define SX126X_RXEN (2) +#define SX126X_TXEN (-1) +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE (1.8f) From e2aa7b98f9b586adb806756f2cc5e5a6a534fb7e Mon Sep 17 00:00:00 2001 From: agessaman Date: Mon, 10 Aug 2026 11:59:26 -0700 Subject: [PATCH 12/19] feat(companion_radio): add external FEM gain preferences for RX and TX for companions Introduced consistent preferences for external LoRa FEM RX and TX gain settings in NodePrefs. Updated companion MyMesh to apply these settings during initialization and transmission. Added unit tests to verify the round-trip serialization of these new preferences. --- examples/companion_radio/MyMesh.cpp | 4 + examples/companion_radio/NodePrefs.h | 7 +- src/helpers/CommonCLI.cpp | 3 +- .../test_companion_node_prefs.cpp | 80 +++++++++++++++++++ .../test_config_serializer.cpp | 65 ++++++++++++--- 5 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 test/test_companion_node_prefs/test_companion_node_prefs.cpp diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b03334c1..2c334066 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -885,6 +885,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.tx_power_dbm = LORA_TX_POWER; _prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_interval = 0; // No automatic GPS updates by default + _prefs.radio_fem_rxgain = 1; + _prefs.radio_fem_txgain = 0; //_prefs.rx_delay_base = 10.0f; enable once new algo fixed _prefs.setRepeatEn(false); #if defined(USE_SX1262) || defined(USE_SX1268) @@ -974,6 +976,8 @@ void MyMesh::begin(bool has_display) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); + board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); + board.setLoRaFemPaGainEnabled(_prefs.radio_fem_txgain); MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s", radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled"); } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 39a5386a..5e67daaa 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -32,6 +32,8 @@ public: uint32_t gps_interval = 0; // GPS read interval in seconds uint8_t autoadd_config = 0; // bitmask for auto-add contacts config uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode (0=power saving, 1=boosted) + uint8_t radio_fem_rxgain = 0; // external LoRa FEM RX gain (LNA) + uint8_t radio_fem_txgain = 0; // external LoRa FEM TX gain (low by default) uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd uint8_t path_hash_mode = 0; // which path mode to use when sending uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) @@ -50,7 +52,8 @@ private: //def("cad", _parent->cad_enabled); //def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); - def("fem_rxgain", _parent->rx_boosted_gain); + def("fem_rxgain", _parent->radio_fem_rxgain); + def("fem_txgain", _parent->radio_fem_txgain); def("tx", _parent->tx_power_dbm); def("af", _parent->airtime_factor); def("rxdelay", _parent->rx_delay_base); @@ -133,4 +136,4 @@ public: // new accessor methods bool isRepeatEn() const { return repeat.disable_fwd == 0; } void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } -}; \ No newline at end of file +}; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 56a52a0b..b318bb58 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -102,8 +102,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - file.read((uint8_t *)&_prefs->radio_fem_txgain, sizeof(_prefs->radio_fem_txgain)); // 295 - // next: 296 + // next: 295 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); diff --git a/test/test_companion_node_prefs/test_companion_node_prefs.cpp b/test/test_companion_node_prefs/test_companion_node_prefs.cpp new file mode 100644 index 00000000..6d3cebdf --- /dev/null +++ b/test/test_companion_node_prefs/test_companion_node_prefs.cpp @@ -0,0 +1,80 @@ +#include + +#include +#include +#include + +#include "../../examples/companion_radio/NodePrefs.h" + +class ReplayStream : public Stream { + const char* _text; + int _pos = 0; + int _len; + +public: + explicit ReplayStream(const char* text) : _text(text), _len(strlen(text)) { } + + int available() override { return _len - _pos; } + int read() override { return _pos < _len ? _text[_pos++] : -1; } + int peek() override { return _pos < _len ? _text[_pos] : -1; } +}; + +class CaptureStream : public Stream { + std::string _text; + + size_t emit(long long value) { + char text[24]; + int length = snprintf(text, sizeof(text), "%lld", value); + return write(reinterpret_cast(text), length); + } + +public: + size_t write(uint8_t value) override { + _text.push_back(static_cast(value)); + return 1; + } + + size_t write(const uint8_t* buffer, size_t size) override { + _text.append(reinterpret_cast(buffer), size); + return size; + } + + size_t print(unsigned char value, int = DEC) override { return emit(value); } + size_t print(int value, int = DEC) override { return emit(value); } + size_t print(unsigned int value, int = DEC) override { return emit(value); } + size_t print(long value, int = DEC) override { return emit(value); } + size_t print(unsigned long value, int = DEC) override { return emit(value); } + size_t print(long long value, int = DEC) override { return emit(value); } + size_t print(unsigned long long value, int = DEC) override { return emit(value); } + + const std::string& text() const { return _text; } +}; + +TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) { + NodePrefs saved; + saved.rx_boosted_gain = 0; + saved.radio_fem_rxgain = 1; + saved.radio_fem_txgain = 0; + + CaptureStream output; + ASSERT_TRUE(saved.saveSerial(output)); + EXPECT_NE(std::string::npos, output.text().find("rxgain:0")); + EXPECT_NE(std::string::npos, output.text().find("fem_rxgain:1")); + EXPECT_NE(std::string::npos, output.text().find("fem_txgain:0")); + + ReplayStream input("{radio:{rxgain:1,fem_rxgain:0,fem_txgain:1}}"); + NodePrefs loaded; + loaded.rx_boosted_gain = 0; + loaded.radio_fem_rxgain = 1; + loaded.radio_fem_txgain = 0; + + ASSERT_TRUE(loaded.loadSerial(input)); + EXPECT_EQ(1, loaded.rx_boosted_gain); + EXPECT_EQ(0, loaded.radio_fem_rxgain); + EXPECT_EQ(1, loaded.radio_fem_txgain); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp index 7a13f487..27c3c811 100644 --- a/test/test_config_serializer/test_config_serializer.cpp +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -1,6 +1,14 @@ #include #include "helpers/ConfigSerializer.h" +class NativeFileSystem { +public: + void mkdir(const char*) { } +}; +#define FILESYSTEM NativeFileSystem +#include "helpers/CommonCLI.h" +#undef FILESYSTEM + #define TEST_INT_S "56" #define TEST_INT 56 #define TEST_FLOAT_S "-6.123" @@ -21,6 +29,19 @@ public: class MockPrintStream : public Stream { int len = 0; uint8_t _buf[1024]; + + size_t printSigned(long long value) { + char text[24]; + snprintf(text, sizeof(text), "%lld", value); + return Print::print(text); + } + + size_t printUnsigned(unsigned long long value) { + char text[24]; + snprintf(text, sizeof(text), "%llu", value); + return Print::print(text); + } + public: size_t write(uint8_t b) override { if (len < sizeof(_buf)) { @@ -30,17 +51,17 @@ public: return 0; } - size_t print(unsigned char b, int r) override { if (b == TEST_INT) return Print::print(TEST_INT_S); return 0; } - size_t print(int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } - size_t print(unsigned int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } - size_t print(long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } - size_t print(unsigned long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } - size_t print(long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } - size_t print(unsigned long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } - size_t print(double v, int p = 2) override { - if (p == 6) return Print::print(TEST_DOUBLE_S); - if (p == 4) return Print::print(TEST_FLOAT_S); - return 0; + size_t print(unsigned char v, int r) override { return printUnsigned(v); } + size_t print(int v, int r) override { return printSigned(v); } + size_t print(unsigned int v, int r) override { return printUnsigned(v); } + size_t print(long v, int r) override { return printSigned(v); } + size_t print(unsigned long v, int r) override { return printUnsigned(v); } + size_t print(long long v, int r) override { return printSigned(v); } + size_t print(unsigned long long v, int r) override { return printUnsigned(v); } + size_t print(double v, int p = 2) override { + char text[32]; + snprintf(text, sizeof(text), "%.*f", p, v); + return Print::print(text); } int getLength() const { return len; } @@ -171,6 +192,28 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) { EXPECT_TRUE(match); } +TEST(NodePrefs, FemGainSettingsRoundTrip) { + NodePrefs saved; + saved.radio_fem_rxgain = 0; + saved.radio_fem_txgain = 1; + + MockPrintStream output; + ASSERT_TRUE(saved.saveSerial(output)); + + std::string serialised(reinterpret_cast(output.getBytes()), output.getLength()); + EXPECT_NE(std::string::npos, serialised.find("fem_rxgain:0")); + EXPECT_NE(std::string::npos, serialised.find("fem_txgain:1")); + + MockInputStream input(serialised.c_str()); + NodePrefs loaded; + loaded.radio_fem_rxgain = 1; + loaded.radio_fem_txgain = 0; + + ASSERT_TRUE(loaded.loadSerial(input)); + EXPECT_EQ(0, loaded.radio_fem_rxgain); + EXPECT_EQ(1, loaded.radio_fem_txgain); +} + // ── main ─────────────────────────────────────────────────────── From 5cce5cfe9766585fab8559054ebffd8933a847c4 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Tue, 11 Aug 2026 10:37:20 +0200 Subject: [PATCH 13/19] removed misleading 'GPIO' from pin numbers --- variants/promicro/variant.cpp | 46 +++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/variants/promicro/variant.cpp b/variants/promicro/variant.cpp index b7d281ac..69b61745 100644 --- a/variants/promicro/variant.cpp +++ b/variants/promicro/variant.cpp @@ -3,29 +3,29 @@ #include "wiring_digital.h" const uint32_t g_ADigitalPinMap[] = { - 8, // P0.08 = GPIO 0 - 6, // P0.06 = GPIO 1 - 17, // P0.17 = GPIO 2 - 20, // P0.20 = GPIO 3 - 22, // P0.22 = GPIO 4 - 24, // P0.24 = GPIO 5 - 32, // P1.00 = GPIO 6 - 11, // P0.11 = GPIO 7 - 36, // P1.04 = GPIO 8 - 38, // P1.06 = GPIO 9 - 9, // P0.09 = GPIO 10 - 10, // P0.10 = GPIO 11 - 43, // P1.11 = GPIO 12 - 45, // P1.13 = GPIO 13 - 47, // P1.15 = GPIO 14 - 2, // P0.02 = GPIO 15 - 29, // P0.29 = GPIO 16 - 31, // P0.31 = GPIO 17 - 33, // P1.01 = GPIO 18 - 34, // P1.02 = GPIO 19 - 37, // P1.05 = GPIO 20 - 13, // P0.13 = GPIO 21 - 15 // P0.15 = GPIO 22 + 8, // P0.08 = 0 + 6, // P0.06 = 1 + 17, // P0.17 = 2 + 20, // P0.20 = 3 + 22, // P0.22 = 4 + 24, // P0.24 = 5 + 32, // P1.00 = 6 + 11, // P0.11 = 7 + 36, // P1.04 = 8 + 38, // P1.06 = 9 + 9, // P0.09 = 10 + 10, // P0.10 = 11 + 43, // P1.11 = 12 + 45, // P1.13 = 13 + 47, // P1.15 = 14 + 2, // P0.02 = 15 + 29, // P0.29 = 16 + 31, // P0.31 = 17 + 33, // P1.01 = 18 + 34, // P1.02 = 19 + 37, // P1.05 = 20 + 13, // P0.13 = 21 + 15 // P0.15 = 22 }; void initVariant() {} From 9c60f9696ecdab55c1527ca2235bfc2c022bb290 Mon Sep 17 00:00:00 2001 From: taco Date: Wed, 12 Aug 2026 14:04:50 +1000 Subject: [PATCH 14/19] t096 compiler layout workaround see https://github.com/meshcore-dev/MeshCore/issues/3151 --- src/helpers/CommonCLI.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 2a9ec43b..3143acd2 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -79,7 +79,8 @@ private: def("bw", _parent->bw); def("sf", _parent->sf); def("cr", _parent->cr); - def("cad", _parent->cad_enabled); + static const char radio_cad_key[] = {'c', 'a', 'd', '\0'}; // workaround for T096, don't touch without testing T096 repeater still transmit + def(radio_cad_key, _parent->cad_enabled); def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); def("fem_rxgain", _parent->rx_boosted_gain); From 82de18fb0e1b74382fb1c190ad62de0c0aef1e60 Mon Sep 17 00:00:00 2001 From: taco Date: Wed, 12 Aug 2026 14:07:31 +1000 Subject: [PATCH 15/19] T096 PA_CTX pin direction workaround --- variants/heltec_t096/LoRaFEMControl.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/heltec_t096/LoRaFEMControl.cpp b/variants/heltec_t096/LoRaFEMControl.cpp index fb60a8f7..45bbfd64 100644 --- a/variants/heltec_t096/LoRaFEMControl.cpp +++ b/variants/heltec_t096/LoRaFEMControl.cpp @@ -21,6 +21,7 @@ void LoRaFEMControl::setSleepModeEnable(void) void LoRaFEMControl::setTxModeEnable(void) { + pinMode(P_LORA_KCT8103L_PA_CTX, OUTPUT); // force pinMode before transmit as temporary workaround for https://github.com/meshcore-dev/MeshCore/issues/3151 digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); digitalWrite(P_LORA_KCT8103L_PA_CTX, HIGH); } From 9d6dcc673e4fdc12cd67c0fde1487dbed9e6c39f Mon Sep 17 00:00:00 2001 From: taco Date: Wed, 12 Aug 2026 16:08:05 +1000 Subject: [PATCH 16/19] set T-Echo Card TCXO voltage to 3.0 as per https://github.com/meshcore-dev/MeshCore/pull/3140 and https://github.com/Xinyuan-LilyGO/T-Echo-Lite/issues/14 --- variants/lilygo_techo_card/variant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/lilygo_techo_card/variant.h b/variants/lilygo_techo_card/variant.h index 28c5f550..699453e0 100644 --- a/variants/lilygo_techo_card/variant.h +++ b/variants/lilygo_techo_card/variant.h @@ -107,7 +107,7 @@ #define P_LORA_NSS (11) // P0.11 #define SX126X_RXEN (33) // P1.01 #define SX126X_TXEN (27) // P0.27 -#define SX126X_DIO3_TCXO_VOLTAGE (1.8f) +#define SX126X_DIO3_TCXO_VOLTAGE (3.0f) //////////////////////////////////////////////////////////////////////////////// From 264d77816864b2a4f7c67e6e47869895cbe23f86 Mon Sep 17 00:00:00 2001 From: taco Date: Wed, 12 Aug 2026 22:28:05 +1000 Subject: [PATCH 17/19] Revert "T096 PA_CTX pin direction workaround" This reverts commit 82de18fb0e1b74382fb1c190ad62de0c0aef1e60. --- variants/heltec_t096/LoRaFEMControl.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/variants/heltec_t096/LoRaFEMControl.cpp b/variants/heltec_t096/LoRaFEMControl.cpp index 45bbfd64..fb60a8f7 100644 --- a/variants/heltec_t096/LoRaFEMControl.cpp +++ b/variants/heltec_t096/LoRaFEMControl.cpp @@ -21,7 +21,6 @@ void LoRaFEMControl::setSleepModeEnable(void) void LoRaFEMControl::setTxModeEnable(void) { - pinMode(P_LORA_KCT8103L_PA_CTX, OUTPUT); // force pinMode before transmit as temporary workaround for https://github.com/meshcore-dev/MeshCore/issues/3151 digitalWrite(P_LORA_KCT8103L_PA_CSD, HIGH); digitalWrite(P_LORA_KCT8103L_PA_CTX, HIGH); } From f43e1cde8938c38371fdf93a51dc1c8b4e289384 Mon Sep 17 00:00:00 2001 From: taco Date: Wed, 12 Aug 2026 22:28:36 +1000 Subject: [PATCH 18/19] Revert "t096 compiler layout workaround" This reverts commit 9c60f9696ecdab55c1527ca2235bfc2c022bb290. --- src/helpers/CommonCLI.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 3143acd2..2a9ec43b 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -79,8 +79,7 @@ private: def("bw", _parent->bw); def("sf", _parent->sf); def("cr", _parent->cr); - static const char radio_cad_key[] = {'c', 'a', 'd', '\0'}; // workaround for T096, don't touch without testing T096 repeater still transmit - def(radio_cad_key, _parent->cad_enabled); + def("cad", _parent->cad_enabled); def("int_thr", _parent->interference_threshold); def("rxgain", _parent->rx_boosted_gain); def("fem_rxgain", _parent->rx_boosted_gain); From 49d9f9a83931815bb937a8ef72f74161e7d2a45d Mon Sep 17 00:00:00 2001 From: taco Date: Wed, 12 Aug 2026 22:31:57 +1000 Subject: [PATCH 19/19] Heltec T096: fix PIN_SPI1_MISO Setting SPI pins to values that are OOB of the g_ADigitalPinMap[] array causes OOB reads. This variant.h has pin 0 as 0xFF which passes through as NRFX_SPIM_PIN_NOT_USED. --- variants/heltec_t096/variant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_t096/variant.h b/variants/heltec_t096/variant.h index c240c1f2..bfbf2e63 100644 --- a/variants/heltec_t096/variant.h +++ b/variants/heltec_t096/variant.h @@ -104,7 +104,7 @@ #define PIN_SPI_SCK (32 + 8) #define PIN_SPI_NSS LORA_CS -#define PIN_SPI1_MISO (-1) +#define PIN_SPI1_MISO (0) #define PIN_SPI1_MOSI (0+17) #define PIN_SPI1_SCK (0+20)