From b1efd9948b4804a0abbbce5883eaa032463f43be Mon Sep 17 00:00:00 2001 From: mmmorks Date: Mon, 7 Sep 2026 14:16:18 -0700 Subject: [PATCH] radiolib: bring LinuxSX1262 to parity with CustomSX1262 LinuxSX1262 derived from SX1262 directly and carried its own two-line isReceiving() plus a copy of getRxBoostedGainMode(). Upstream's RX watchdog -- a startReceive() that also enables the preamble IRQ, the deadlines that bound a header or preamble which never becomes a packet, and the millis setters that size them -- landed in CustomSX1262 and never here. That left the Linux repeater with an unbounded isReceiving(): nothing cleared a latched HEADER_VALID, so a header whose packet never arrived reported busy forever, deferring every transmit until getCADFailMaxDuration() expired and forced it through. The other half of that check was dead code -- plain SX1262::startReceive() enables RADIOLIB_IRQ_RX_DEFAULT_FLAGS, which does not include PREAMBLE_DETECTED, so the bit could never latch. Deriving from CustomSX1262 both enables it and bounds it. None of that logic is Linux-specific, and upstream keeps fixing it. Derive from CustomSX1262 instead. std_init() stays, shadowing the non-virtual base, because that part genuinely differs: the MCU variants pick regulator, RF switch and gain per board at compile time, while one binary here serves every HAT and reads all of it from meshcored.ini. Two of those compile-time knobs had no Linux equivalent at all, so they become keys: use_regulator_ldo (SX126X_USE_REGULATOR_LDO, begin()'s useRegulatorLDO) rx_register_patch (SX126X_REGISTER_PATCH, bit 0 of register 0x8B5) Both default to the compile-time defaults (DC-DC, no patch), so existing installs are unaffected. LinuxSX1262Wrapper follows the same way. It hand-implemented eleven RadioLibWrapper virtuals; ten of them were the CustomSX1262Wrapper version with a different cast spelled out, so it now derives from CustomSX1262Wrapper and inherits them. That is what makes the wrapper track upstream rather than drift from it -- a method added to RadioLibWrapper now arrives here implemented, instead of breaking the Linux build when it is pure virtual or silently no-opping on Linux alone when it has a default -- and it is why the README's "Upstream-sync fragility" known gap goes away. powerOff()'s cold sleep comes along with it; nothing on Linux calls it today. One side effect: CustomSX1262Wrapper.h defines USE_SX1262, which on Linux guards only a _prefs.rx_boosted_gain default that MyMesh::begin() already overwrites from meshcored.ini. The eleventh virtual, doResetAGC(), stays overridden, and Linux gains it at all for the first time: without it the daemon fell through to RadioLibWrapper's bare sleep(), so `set agc.reset.interval` was quietly a weaker knob here than anywhere else. The full reset (warm sleep, Calibrate(0x7F), calibrateImage() for the band) drops DIO2-as-RF-switch, RX boosted gain and the 0x8B5 patch, and the inherited version restores DIO2 and the patch from SX126X_* build flags this variant does not define. So SX126xReset.h gains a second entry point taking the settings as a struct, with the recalibration and the settings re-application each factored into one helper; the MCU overload keeps its #ifdef path and its behaviour. sx126xApplyRegisterPatch() replaces the register-poke that had grown to two verbatim copies. The boosted gain is read back off the chip here, as every other SX126x wrapper does it -- the ini value is only the starting point, and both the persisted pref and `set radio.rxgain` change it afterwards, so restoring the ini value would revert them at the first reset tick. Also names the downcast once: _radio is held as the base mesh::Radio, and the wrapper repeated ((LinuxSX1262 *)_radio)-> twelve times. Inheriting removes all of them; the two uses left, both in doResetAGC(), go through an r() accessor. Verified: clean linux_repeater build in the arm64 container. Neither new key is exercised on hardware -- that needs a module that wants them -- and the reset path is untested on hardware. --- src/helpers/radiolib/CustomSX1262.h | 6 +- src/helpers/radiolib/LinuxSX1262.h | 60 +++++++++++++------- src/helpers/radiolib/LinuxSX1262Wrapper.h | 68 ++++++++++++----------- src/helpers/radiolib/SX126xReset.h | 59 +++++++++++++++++--- variants/linux/LinuxBoard.cpp | 4 ++ variants/linux/LinuxBoard.h | 7 +++ variants/linux/README.md | 9 +-- variants/linux/meshcored.ini | 2 + 8 files changed, 150 insertions(+), 65 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index b4ee6c97aa..5add5c16b2 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -2,6 +2,7 @@ #include #include "MeshCore.h" +#include "SX126xReset.h" class CustomSX1262 : public SX1262 { uint32_t _preambleMillis = 66; @@ -89,10 +90,7 @@ class CustomSX1262 : public SX1262 { // for improved RX with Heltec v4 #ifdef SX126X_REGISTER_PATCH - uint8_t r_data = 0; - readRegister(0x8B5, &r_data, 1); - r_data |= 0x01; - writeRegister(0x8B5, &r_data, 1); + sx126xApplyRegisterPatch(this); #endif MESH_DEBUG_PRINTLN("SX1262 status=0x%02X device_errors=0x%04X", getStatus(), getDeviceErrors()); diff --git a/src/helpers/radiolib/LinuxSX1262.h b/src/helpers/radiolib/LinuxSX1262.h index bed50ee92e..1bc188bce9 100644 --- a/src/helpers/radiolib/LinuxSX1262.h +++ b/src/helpers/radiolib/LinuxSX1262.h @@ -1,26 +1,48 @@ #pragma once #include +#include "MeshCore.h" +#include "CustomSX1262.h" +#include "SX126xReset.h" +// For the LinuxBoard definition behind `board` below: std_init() and +// rxSettings() both read board.config, so the complete type is needed here. +// Reached transitively via target.h today, but named so this header does not +// depend on include order. +#include "LinuxBoard.h" -#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received -#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 #define SX126X_PREAMBLE_LENGTH 16 extern LinuxBoard board; -class LinuxSX1262 : public SX1262 { +// The Linux build's SX1262. +// +// Everything that is not Linux-specific comes from CustomSX1262 unchanged -- +// the RX watchdog (startReceive/isReceiving and their preamble/header +// deadlines), the millis setters, the RX-boost readback. That logic is shared +// with every SX126x board and upstream keeps fixing it, so forking it here +// would mean re-copying each fix by hand and silently missing the ones nobody +// notices. +// +// What genuinely differs is initialisation: the MCU variants pick frequency, +// regulator, RF-switch and gain per board at compile time, but one binary here +// serves every HAT, so all of it comes from meshcored.ini at runtime. +class LinuxSX1262 : public CustomSX1262 { public: - LinuxSX1262(Module *mod) : SX1262(mod) { } + LinuxSX1262(Module *mod) : CustomSX1262(mod) { } + // Shadows (not overrides) CustomSX1262::std_init(), which is non-virtual + // and reads LORA_*/SX126X_* build flags this variant does not define. bool std_init(SPIClass* spi = NULL) { - LinuxConfig config = board.config; + const LinuxConfig& config = board.config; Serial.printf("Radio begin %f %f %d %d %f\n", config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, config.lora_tcxo); - int status = begin(config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, config.lora_tx_power, SX126X_PREAMBLE_LENGTH, config.lora_tcxo); + MESH_DEBUG_PRINTLN("SX1262 regulator requested: %s", config.use_regulator_ldo ? "LDO" : "DC-DC"); + int status = begin(config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, config.lora_tx_power, SX126X_PREAMBLE_LENGTH, config.lora_tcxo, config.use_regulator_ldo); // if radio init fails with -707/-706, try again with tcxo voltage set to 0.0f if (status == RADIOLIB_ERR_SPI_CMD_FAILED || status == RADIOLIB_ERR_SPI_CMD_INVALID) { - status = begin(config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, config.lora_tx_power, SX126X_PREAMBLE_LENGTH, 0.0f); + MESH_DEBUG_PRINTLN("SX1262 init failed with error %d, retrying with TCXO at 0.0V", status); + status = begin(config.lora_freq, config.lora_bw, config.lora_sf, config.lora_cr, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, config.lora_tx_power, SX126X_PREAMBLE_LENGTH, 0.0f, config.use_regulator_ldo); } if (status != RADIOLIB_ERR_NONE) { Serial.print("ERROR: radio init failed: "); @@ -31,24 +53,24 @@ class LinuxSX1262 : public SX1262 { setCRC(1); setCurrentLimit(config.current_limit); - setDio2AsRfSwitch(config.dio2_as_rf_switch); - setRxBoostedGainMode(config.rx_boosted_gain); + sx126xApplyRxSettings(this, rxSettings()); if (config.lora_rxen_pin != RADIOLIB_NC || config.lora_txen_pin != RADIOLIB_NC) { setRfSwitchPins(config.lora_rxen_pin, config.lora_txen_pin); } - return true; - } + MESH_DEBUG_PRINTLN("SX1262 status=0x%02X device_errors=0x%04X", getStatus(), getDeviceErrors()); - bool isReceiving() { - uint16_t irq = getIrqFlags(); - bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); - return detected; + return true; } - bool getRxBoostedGainMode() { - uint8_t rxGain = 0; - readRegister(RADIOLIB_SX126X_REG_RX_GAIN, &rxGain, 1); - return (rxGain == RADIOLIB_SX126X_RX_GAIN_BOOSTED); + // The RX settings for this node, as configured in meshcored.ini. Applied at + // init and re-applied after every AGC reset, both via sx126xApplyRxSettings() + // -- which is where the MCU variants instead read their SX126X_* build flags. + SX126xRxSettings rxSettings() const { + SX126xRxSettings s; + s.dio2_as_rf_switch = board.config.dio2_as_rf_switch; + s.rx_boosted_gain = board.config.rx_boosted_gain; + s.register_patch = board.config.rx_register_patch; + return s; } }; diff --git a/src/helpers/radiolib/LinuxSX1262Wrapper.h b/src/helpers/radiolib/LinuxSX1262Wrapper.h index 78c1e7cefb..f63968dce3 100644 --- a/src/helpers/radiolib/LinuxSX1262Wrapper.h +++ b/src/helpers/radiolib/LinuxSX1262Wrapper.h @@ -1,39 +1,45 @@ #pragma once +#include "CustomSX1262Wrapper.h" #include "LinuxSX1262.h" -#include "RadioLibWrappers.h" +#include "SX126xReset.h" -class LinuxSX1262Wrapper : public RadioLibWrapper { -public: - LinuxSX1262Wrapper(LinuxSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } - - void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override { - ((LinuxSX1262 *)_radio)->setFrequency(freq); - ((LinuxSX1262 *)_radio)->setSpreadingFactor(sf); - ((LinuxSX1262 *)_radio)->setBandwidth(bw); - ((LinuxSX1262 *)_radio)->setCodingRate(cr); - updatePreamble(sf); - } +// LinuxSX1262 is a CustomSX1262, so its wrapper is a CustomSX1262Wrapper. +// +// setParams(), isReceivingPacket(), the RSSI/SNR readers, packetScore(), +// getSpreadingFactor(), set/getRxBoostedGainMode() and powerOff() were all +// byte-for-byte the base class's implementation with a different cast spelled +// out, so they are inherited rather than restated. That is also what keeps this +// file honest against upstream: a method added to RadioLibWrapper now arrives +// here implemented, the same way it arrives on every other SX126x board, +// instead of breaking the Linux build (pure virtual) or silently no-opping on +// it alone (virtual with a default). +class LinuxSX1262Wrapper : public CustomSX1262Wrapper { + // _radio is held as the base mesh::Radio. The inherited members downcast it to + // CustomSX1262, which is as far as they need to see; this names the + // LinuxSX1262 downcast for the parts only the Linux subclass has. It is always + // a LinuxSX1262 -- the constructor takes one by reference -- so this is a + // naming convenience, not a checked conversion. + LinuxSX1262* r() const { return (LinuxSX1262 *)_radio; } - bool isReceivingPacket() override { - return ((LinuxSX1262 *)_radio)->isReceiving(); - } - float getCurrentRSSI() override { - return ((LinuxSX1262 *)_radio)->getRSSI(false); - } - float getLastRSSI() const override { return ((LinuxSX1262 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((LinuxSX1262 *)_radio)->getSNR(); } - - float packetScore(float snr, int packet_len) override { - int sf = ((LinuxSX1262 *)_radio)->spreadingFactor; - return packetScoreInt(snr, sf, packet_len); - } - uint8_t getSpreadingFactor() const override { return ((LinuxSX1262 *)_radio)->spreadingFactor; } +public: + LinuxSX1262Wrapper(LinuxSX1262& radio, mesh::MainBoard& board) : CustomSX1262Wrapper(radio, board) { } - bool setRxBoostedGainMode(bool en) override { - return ((LinuxSX1262 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; - } - bool getRxBoostedGainMode() const override { - return ((LinuxSX1262 *)_radio)->getRxBoostedGainMode(); + // The one override that is not the inherited behaviour. Recalibration drops + // DIO2-as-RF-switch, RX boosted gain and the 0x8B5 patch, and the inherited + // version restores the first and third from SX126X_* build flags -- none of + // which this variant defines, so on Linux it would silently drop both. + // rxSettings() supplies them from meshcored.ini instead. + // + // The gain is read back off the chip, exactly as the inherited version does + // it, and deliberately not taken from rxSettings(): the ini value is only the + // starting point, and both the persisted pref and `set radio.rxgain` change + // it afterwards. Restoring the ini value here would revert them at the first + // agc.reset.interval tick, with `get radio.rxgain` still reporting the value + // the radio had stopped using. + void doResetAGC() override { + SX126xRxSettings rx = r()->rxSettings(); + rx.rx_boosted_gain = getRxBoostedGainMode(); + sx126xResetAGC(r(), rx); } }; diff --git a/src/helpers/radiolib/SX126xReset.h b/src/helpers/radiolib/SX126xReset.h index 472eb33bca..0616f6cf2d 100644 --- a/src/helpers/radiolib/SX126xReset.h +++ b/src/helpers/radiolib/SX126xReset.h @@ -2,10 +2,41 @@ #include +// RX settings that Calibrate(0x7F) does not preserve, so every reset has to +// restate them. Boards that fix these at build time (every MCU variant) do not +// need this -- see sx126xResetAGC() below, which reads their SX126X_* flags +// directly. It exists for targets configured at runtime instead: the Linux +// daemon serves every HAT from one binary, so its values come from +// meshcored.ini and cannot be macros. +struct SX126xRxSettings { + bool dio2_as_rf_switch = false; + bool rx_boosted_gain = false; + bool register_patch = false; // 0x8B5 RX-sensitivity patch +}; + +// The RX-sensitivity patch upstream added for the Heltec v4. Undocumented by +// Semtech, hence the bare register number. +inline void sx126xApplyRegisterPatch(SX126x* radio) { + uint8_t r_data = 0; + radio->readRegister(0x8B5, &r_data, 1); + r_data |= 0x01; + radio->writeRegister(0x8B5, &r_data, 1); +} + +// Apply the RX settings calibration does not preserve. Both initial +// configuration and every later AGC reset go through here, so a setting added +// to SX126xRxSettings reaches both rather than having to be remembered twice. +inline void sx126xApplyRxSettings(SX126x* radio, const SX126xRxSettings& rx) { + radio->setDio2AsRfSwitch(rx.dio2_as_rf_switch); + radio->setRxBoostedGainMode(rx.rx_boosted_gain); + if (rx.register_patch) sx126xApplyRegisterPatch(radio); +} + // 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, bool rx_boost_gain) { +// Warm sleep powers down analog, Calibrate(0x7F) refreshes ADC/PLL/image calibration. +// The caller then re-applies the RX settings calibration may have reset, via one of +// the two sx126xResetAGC() entry points below. +inline void sx126xRecalibrate(SX126x* radio) { radio->sleep(true); radio->standby(RADIOLIB_SX126X_STANDBY_RC, true); @@ -21,6 +52,13 @@ inline void sx126xResetAGC(SX126x* radio, bool rx_boost_gain) { // Calibrate(0x7F) defaults image calibration to 902-928MHz band. // Re-calibrate for the actual operating frequency. radio->calibrateImage(radio->freqMHz); +} + +// MCU variants. The SX126X_* build flags still decide *which* settings apply, +// but the boosted-gain value is passed in -- callers read it back off the chip +// -- so a reset no longer clobbers a gain mode that was changed at runtime. +inline void sx126xResetAGC(SX126x* radio, bool rx_boost_gain) { + sx126xRecalibrate(radio); #ifdef SX126X_DIO2_AS_RF_SWITCH radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); @@ -29,9 +67,16 @@ inline void sx126xResetAGC(SX126x* radio, bool rx_boost_gain) { radio->setRxBoostedGainMode(rx_boost_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); + sx126xApplyRegisterPatch(radio); #endif } + +// Runtime-configured targets. Every setting comes from the caller and none of +// the SX126X_* macros are consulted, which makes this the single place they are +// applied rather than having the caller re-apply them afterwards: a setting +// added to SX126xRxSettings then reaches these targets too, instead of being +// silently dropped on them until someone notices. +inline void sx126xResetAGC(SX126x* radio, const SX126xRxSettings& rx) { + sx126xRecalibrate(radio); + sx126xApplyRxSettings(radio, rx); +} diff --git a/variants/linux/LinuxBoard.cpp b/variants/linux/LinuxBoard.cpp index d48f16a0eb..2f4c08465c 100644 --- a/variants/linux/LinuxBoard.cpp +++ b/variants/linux/LinuxBoard.cpp @@ -445,6 +445,10 @@ LinuxConfig::LoadResult LinuxConfig::load(const char *filename) { if (parse_bool(key, value, &bval, &result.bad_values)) dio2_as_rf_switch = bval; } else if (strcmp(key, "rx_boosted_gain") == 0) { if (parse_bool(key, value, &bval, &result.bad_values)) rx_boosted_gain = bval; + } else if (strcmp(key, "use_regulator_ldo") == 0) { + if (parse_bool(key, value, &bval, &result.bad_values)) use_regulator_ldo = bval; + } else if (strcmp(key, "rx_register_patch") == 0) { + if (parse_bool(key, value, &bval, &result.bad_values)) rx_register_patch = bval; } else if (strcmp(key, "lora_irq_pin") == 0) { if (parse_pin(key, value, PIN_REQUIRED, &pin, &result.bad_values)) lora_irq_pin = (uint32_t) pin; } else if (strcmp(key, "lora_reset_pin") == 0) { diff --git a/variants/linux/LinuxBoard.h b/variants/linux/LinuxBoard.h index 8c2916d0b8..cfe5cffd37 100644 --- a/variants/linux/LinuxBoard.h +++ b/variants/linux/LinuxBoard.h @@ -34,6 +34,13 @@ class LinuxConfig { bool dio2_as_rf_switch = false; bool rx_boosted_gain = true; + // The MCU variants pick these per board at compile time, via the + // SX126X_USE_REGULATOR_LDO and SX126X_REGISTER_PATCH build flags. One binary + // here serves every HAT, so they are runtime config instead. Defaults match + // the compile-time defaults: DC-DC, no patch. + bool use_regulator_ldo = false; + bool rx_register_patch = false; + const char* spidev = "/dev/spidev0.0"; const char* lora_gpiochip = "gpiochip0"; diff --git a/variants/linux/README.md b/variants/linux/README.md index 0c8912bc0b..e2405b4973 100644 --- a/variants/linux/README.md +++ b/variants/linux/README.md @@ -119,14 +119,16 @@ Key settings: | `current_limit` | `140` | Radio over-current protection limit in mA | | `dio2_as_rf_switch` | `0` | `1` = use DIO2 to drive the TX/RX RF switch. **Required for the Waveshare Core1262** (without it the radio inits but TX/RX are dead); depends on module wiring | | `rx_boosted_gain` | `1` | `1` enables the SX126x RX boosted-gain mode; `0` disables | +| `use_regulator_ldo` | `0` | `1` powers the radio from the LDO instead of the DC-DC converter. Only for modules built without the DC-DC inductor | +| `rx_register_patch` | `0` | `1` applies the SX126x RX-sensitivity patch (bit 0 of register `0x8B5`). Try it if a HAT receives poorly | | `advert_name` | `"Linux Repeater"` | Node name, first-run default only | | `admin_password` | `"password"` | Admin password, **change this**, first-run default only | | `lat` / `lon` | `0.0` | GPS coordinates for advertisement, first-run default only | Comments (`#`, `;`), blank lines and `[section]` headers are ignored. Boolean -settings (`dio2_as_rf_switch`, `rx_boosted_gain`) accept `1`/`0`, -`true`/`false`, `on`/`off` or `yes`/`no`, case-insensitively; anything else is -a fatal invalid value. +settings (`dio2_as_rf_switch`, `rx_boosted_gain`, `use_regulator_ldo`, +`rx_register_patch`) accept `1`/`0`, `true`/`false`, `on`/`off` or `yes`/`no`, +case-insensitively; anything else is a fatal invalid value. #### Config validation @@ -285,4 +287,3 @@ sudo systemctl start meshcored - **Only repeater firmware**, there is no `linux_companion` target yet; companion radio support (BLE/serial interface to a phone app) is not implemented for Linux. - **Serial `erase` command is a no-op**, `formatFileSystem()` returns `false` on Linux, so the interactive serial `erase` command reports failure. To wipe the filesystem, use the `--erase` *startup* flag (or clear the VFS dir) instead, see step 5. - **No power management**, `board.sleep()` is a no-op; the power-saving loop in `main.cpp` never actually sleeps. -- **Upstream-sync fragility**, the radio wrapper (`LinuxSX1262Wrapper`) implements the `RadioLibWrapper` interface by hand, so it can drift from upstream in two ways: a new **pure-virtual** method breaks the Linux build (e.g. `setParams()`), and a new **virtual-with-default** method silently no-ops on Linux until overridden (e.g. `set`/`getRxBoostedGainMode()`, which reported and applied the wrong state until added). Mirror `CustomSX1262Wrapper` when syncing. diff --git a/variants/linux/meshcored.ini b/variants/linux/meshcored.ini index d7b17d790c..fdf65b8d13 100644 --- a/variants/linux/meshcored.ini +++ b/variants/linux/meshcored.ini @@ -27,3 +27,5 @@ lora_tcxo = 1.8 #current_limit = 140 #dio2_as_rf_switch = 1 #rx_boosted_gain = 1 +#use_regulator_ldo = 1 # LDO instead of DC-DC; only for modules wired without a DC-DC inductor +#rx_register_patch = 1 # RX sensitivity patch (register 0x8B5); harmless, helps some modules