From 294f8c5ffeb3a1e8d48d8377cc1fdcb6afb90cc1 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 carried its own copy of the SX126x RX watchdog -- the preamble/header deadline state, startReceive(), isReceiving(), the millis setters and getRxBoostedGainMode() -- and that copy had drifted: upstream's RX-IRQ-timeout work (bounding a latched PREAMBLE_DETECTED that never becomes a packet) landed in every Custom*Wrapper but not here, so the Linux repeater kept an unbounded isReceiving() that reported busy forever after a preamble that went nowhere, deferring every transmit until getCADFailMaxDuration(). 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. The wrapper gains the doResetAGC() override every other SX126x board has. Without it Linux fell through to RadioLibWrapper's fallback -- a bare sleep() -- so `set agc_int` 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 shared sx126xResetAGC() restores them from SX126X_* build flags that this variant does not define. So SX126xReset.h gains a second entry point that takes 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 three verbatim copies. powerOff() comes along for parity (cold sleep rather than the base class's warm one); nothing on Linux calls it today. Also names the downcast once: _radio is held as the base mesh::Radio, and the wrapper repeated ((LinuxSX1262 *)_radio)-> seventeen times. 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. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01EXSCjgNEbJfHwLjD2WSHW4 --- src/helpers/radiolib/CustomSX1262.h | 6 +-- src/helpers/radiolib/LinuxSX1262.h | 59 +++++++++++++++-------- src/helpers/radiolib/LinuxSX1262Wrapper.h | 56 ++++++++++++++++----- src/helpers/radiolib/SX126xReset.h | 59 ++++++++++++++++++++--- variants/linux/LinuxBoard.cpp | 2 + variants/linux/LinuxBoard.h | 7 +++ variants/linux/README.md | 9 ++-- variants/linux/meshcored.ini | 2 + 8 files changed, 153 insertions(+), 47 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..889603dcca 100644 --- a/src/helpers/radiolib/LinuxSX1262.h +++ b/src/helpers/radiolib/LinuxSX1262.h @@ -1,26 +1,47 @@ #pragma once #include +#include "MeshCore.h" +#include "CustomSX1262.h" +#include "SX126xReset.h" +// For the LinuxBoard definition behind `board` below. Reached transitively via +// target.h today, but named here so this header does not depend on include +// order -- LinuxSX1262Wrapper.h calls methods on it. +#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 +52,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..7bf90d2f3b 100644 --- a/src/helpers/radiolib/LinuxSX1262Wrapper.h +++ b/src/helpers/radiolib/LinuxSX1262Wrapper.h @@ -2,38 +2,68 @@ #include "LinuxSX1262.h" #include "RadioLibWrappers.h" +#include "SX126xReset.h" class LinuxSX1262Wrapper : public RadioLibWrapper { + // _radio is held as the base mesh::Radio, so every use here needs the + // downcast. 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; } + 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); + r()->setFrequency(freq); + r()->setSpreadingFactor(sf); + r()->setBandwidth(bw); + r()->setCodingRate(cr); updatePreamble(sf); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + r()->setPreambleMillis(pm.preambleMillis); + r()->setMaxPayloadMillis(pm.payloadMillis); + } + + // Full SX126x receiver reset (warm sleep, recalibrate, re-image the configured + // band), the same one every other SX126x board gets. Without this override the + // base class falls back to a bare sleep(), so `set agc_int` was quietly a + // weaker knob on Linux than everywhere else. + // + // The settings argument is the Linux-specific part: recalibration drops + // DIO2-as-RF-switch, RX boosted gain and the 0x8B5 patch, and the shared + // helper restores them from the SX126X_* build flags, none of which are + // defined here -- those come from meshcored.ini. Handing it the runtime values + // makes it restore the right ones, rather than restoring the wrong ones for + // this caller to redo afterwards. + void doResetAGC() override { + sx126xResetAGC(r(), r()->rxSettings()); + } + + // Cold sleep, matching CustomSX1262Wrapper. The only caller shuts the daemon + // down straight afterwards, so there is no configuration worth retaining and + // the deeper state is the better one to leave the modem in. + void powerOff() override { + r()->sleep(false); } bool isReceivingPacket() override { - return ((LinuxSX1262 *)_radio)->isReceiving(); + return r()->isReceiving(); } float getCurrentRSSI() override { - return ((LinuxSX1262 *)_radio)->getRSSI(false); + return r()->getRSSI(false); } - float getLastRSSI() const override { return ((LinuxSX1262 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((LinuxSX1262 *)_radio)->getSNR(); } + float getLastRSSI() const override { return r()->getRSSI(); } + float getLastSNR() const override { return r()->getSNR(); } float packetScore(float snr, int packet_len) override { - int sf = ((LinuxSX1262 *)_radio)->spreadingFactor; - return packetScoreInt(snr, sf, packet_len); + return packetScoreInt(snr, r()->spreadingFactor, packet_len); } - uint8_t getSpreadingFactor() const override { return ((LinuxSX1262 *)_radio)->spreadingFactor; } + uint8_t getSpreadingFactor() const override { return r()->spreadingFactor; } bool setRxBoostedGainMode(bool en) override { - return ((LinuxSX1262 *)_radio)->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; + return r()->setRxBoostedGainMode(en) == RADIOLIB_ERR_NONE; } bool getRxBoostedGainMode() const override { - return ((LinuxSX1262 *)_radio)->getRxBoostedGainMode(); + return r()->getRxBoostedGainMode(); } }; 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 4ff8b3ea3c..b0fb94ad18 100644 --- a/variants/linux/LinuxBoard.cpp +++ b/variants/linux/LinuxBoard.cpp @@ -390,6 +390,8 @@ LinuxConfig::LoadResult LinuxConfig::load(const char *filename) { else if (strcmp(key, "current_limit") == 0) { if (parse_float(key, value, &fval, &result.bad_values)) current_limit = fval; } else if (strcmp(key, "dio2_as_rf_switch") == 0) { 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, 0, &pin, &result.bad_values)) lora_irq_pin = (uint32_t) pin; } else if (strcmp(key, "lora_reset_pin") == 0) { if (parse_pin(key, value, 0, &pin, &result.bad_values)) lora_reset_pin = (uint32_t) pin; } 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 bd6ae1d778..65e1f767ae 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 @@ -276,4 +278,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