Skip to content

radiolib: bring LinuxSX1262 to parity with CustomSX1262 - #3

Open
mmmorks wants to merge 1 commit into
pr/02-config-validationfrom
pr/03-sx1262-parity
Open

radiolib: bring LinuxSX1262 to parity with CustomSX1262#3
mmmorks wants to merge 1 commit into
pr/02-config-validationfrom
pr/03-sx1262-parity

Conversation

@mmmorks

@mmmorks mmmorks commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Staged on the fork for review. Final destination: l5yth/meshcore-linux linux. Base here is pr/02-config-validation so the diff shows only this change.

Summary

LinuxSX1262 carried a hand-copied subset of CustomSX1262 (the RX watchdog, the millis setters, the boosted-gain readback) and had drifted from it. This derives it from CustomSX1262 instead, keeps only the genuinely Linux-specific part (a std_init() that reads its settings from meshcored.ini at runtime), and closes the two gaps that drift had opened: two SX126x build-flag knobs with no Linux equivalent, and an AGC reset that was a bare sleep() on Linux.

What changed

  • src/helpers/radiolib/LinuxSX1262.h: class LinuxSX1262 : public CustomSX1262. Upstream's RX-IRQ-timeout state machine (bounding a latched PREAMBLE_DETECTED that never becomes a packet) now applies on Linux too; before this, isReceiving() here could report busy forever after a preamble that went nowhere, deferring every transmit until getCADFailMaxDuration(). std_init() shadows the non-virtual base and takes LinuxConfig by reference.
  • Two new meshcored.ini keys, mirroring the compile-time flags the MCU variants use, defaulting to the compile-time defaults (DC-DC, no patch) so existing installs are unaffected:
    • use_regulator_ldo (SX126X_USE_REGULATOR_LDO, begin()'s useRegulatorLDO)
    • rx_register_patch (SX126X_REGISTER_PATCH, bit 0 of register 0x8B5)
  • LinuxSX1262Wrapper:
    • doResetAGC() override running the full SX126x reset (warm sleep, Calibrate(0x7F), calibrateImage() for the configured band), like every other SX126x board. Without it, set agc_int fell through to RadioLibWrapper's bare sleep() fallback.
    • powerOff() for parity with CustomSX1262Wrapper (cold sleep). Nothing calls it on Linux today.
    • The seventeen ((LinuxSX1262 *)_radio)-> casts become one r() accessor.
  • src/helpers/radiolib/SX126xReset.h: recalibration drops DIO2-as-RF-switch, RX boosted gain and the 0x8B5 patch, and the existing sx126xResetAGC(radio, bool) restores them from SX126X_* build flags. A runtime-configured target has none of those, so this adds a second overload taking an SX126xRxSettings struct, factors the recalibration and the settings re-application into sx126xRecalibrate() / sx126xApplyRxSettings(), and replaces the three verbatim copies of the register poke with sx126xApplyRegisterPatch(). The MCU overload keeps its #ifdef path and behaviour.
  • CustomSX1262.h: uses sx126xApplyRegisterPatch() (no behaviour change).
  • README: settings-table rows for the two keys; the "Upstream-sync fragility" Known Gap is removed, since the wrapper no longer re-implements the interface by hand.

Why

Everything in the copied code is shared SX126x logic that upstream keeps fixing (the RX timeout handling, which IRQ flag a timeout clears, the setters themselves all changed this year). Keeping a fork of it meant re-applying each fix by hand and silently missing the ones nobody noticed; the missed RX-timeout port above is the concrete example.

How it was tested

  • Clean linux_repeater build for arm64 in the container (build-docker.sh).
  • Native test suite unchanged and passing.
  • Not exercised on hardware: neither new key (needs a module that wants LDO or the patch) nor the reset path itself.

Dependencies

Stacked on pr/02-config-validation (uses its parse_bool() for the two new keys). Review the last commit only until that merges.

Shared code touched

  • src/helpers/radiolib/SX126xReset.h (additive: struct, two helpers, a second sx126xResetAGC overload; the existing overload's behaviour is unchanged)
  • src/helpers/radiolib/CustomSX1262.h (register poke replaced by the helper; no behaviour change)
  • src/helpers/radiolib/LinuxSX1262.h, LinuxSX1262Wrapper.h (Linux-only)

Noticed in passing, not changed here: CustomLLCC68Wrapper.h calls sx126xResetAGC((SX126x *)_radio) with one argument, which does not match either overload's signature. That is pre-existing in this tree.

@mmmorks
mmmorks force-pushed the pr/02-config-validation branch from b61c48c to 037b93f Compare September 8, 2026 04:11
@mmmorks
mmmorks force-pushed the pr/03-sx1262-parity branch from d1082d7 to 294f8c5 Compare September 8, 2026 04:11
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.
@mmmorks
mmmorks force-pushed the pr/02-config-validation branch from 037b93f to 8ecdd24 Compare September 12, 2026 21:21
@mmmorks
mmmorks force-pushed the pr/03-sx1262-parity branch from 294f8c5 to b1efd99 Compare September 12, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant