lora: replace custom fri3d_2026 SX1262 driver with upstream micropython-lib driver + adapter - #231
Draft
ThomasFarstrike wants to merge 62 commits into
Draft
ThomasFarstrike wants to merge 62 commits into
ThomasFarstrike wants to merge 62 commits into
Conversation
…on-lib + adapter
Replace the 2426-line forked drivers/lora/sx1262.py with the official
upstream micropython-lib lora driver (4 files, ~1450 lines verbatim)
plus thin adapter layers that encapsulate all board-specific quirks.
New files:
lib/lora/ - upstream lora package (modem, sx126x, sync_modem, __init__)
mpos/lora_spi_adapter.py - wraps SPI.Device as standard machine.SPI
mpos/lora_adapter.py - wraps upstream SX1262 in existing caller API
Enhanced:
mpos/lora_manager.py - acquire/release lock, CH32 reset_chip(),
optional watchdog with auto-recovery
Updated callers:
board/fri3d_2026.py - import from mpos.lora_adapter
apps/lora_chat/lora_chat.py - import + acquire/release + watchdog
Deleted:
drivers/lora/sx1262.py - replaced by upstream + adapter
Single upstream deviation: from machine import Pin guarded with
try/except in sx126x.py (Unix desktop port has no machine.Pin).
No other changes to upstream files.
LoRaManager now provides:
acquire(app_name)/release(app_name) - exclusive radio lock
reset_chip() - CH32 IO expander reset
start_watchdog()/stop_watchdog() - auto-recovery from SPI wedges
is_healthy() - status probe
micropySX126X/ (lilygo watcher driver) left untouched for a follow-up PR.
Refs: PR #222, issue #229
_irq_handler called start_recv(continuous=True) on TX_DONE but start_recv checks self._tx and returns early if it's still True. Since the adapter's non-blocking send() path never calls poll_send(), self._tx stayed True permanently — the radio never re-entered continuous RX after the first TX, and subsequent sends wedged. Fix: call poll_send() instead of start_recv() in the TX_DONE branch. poll_send() clears self._tx, then _check_recv() properly restarts RX.
…me to 5ms Two fixes in the adapter (no upstream changes): 1. Clear device errors before configure() in begin(). After TCXO init in the upstream constructor, the datasheet-expected XOSC_START_ERR (0x20) can reappear between __init__ and the first configure() call on some boards. _check_error() at the end of configure() treats it as fatal → RuntimeError. Clearing stale errors first fixes this; any genuine errors from configure() itself are still caught by its own _check_error() call. 2. Bump dio3_tcxo_start_time_us from 1000→5000 to match the old driver's default TCXO delay, giving slow-starting oscillators more margin.
lora_adapter.py: - Add sys.print_exception(e) in both blocking and non-blocking send() branches so the actual exception is visible on serial (was swallowed by bare except: return 0, -1) - Same in _irq_handler for poll_send() exceptions lora_manager.py: - reset_chip(): wrap exp.config writes with mpos.ui.task_handler.disable()/enable() to avoid I2C bus contention between the config write and LVGL's periodic expander reads (buttons, joystick). Without this, config register writes silently fail on CH32 firmware v2.0.1 — see issue #224. - Use try/finally to guarantee task_handler is re-enabled even if the write raises.
The upstream prepare_send() calls _standby() with STDBY_XOSC mode (1), which turns on the crystal oscillator. Per DS 13.3.6, this sets XOSC_START_ERR (0x20). On some boards the XOSC stabilizes fast enough that the flag clears before _check_error() runs — on others (socketed modules, different TCXO path) it doesn't, causing RuntimeError on every send. Fix: _clear_errors() before prepare_send() in both blocking and non-blocking paths. Safe because prepare_send() has its own _check_error() that catches genuinely new errors from the standby and setup commands.
Per DS 13.3.6, XOSC_START_ERR (0x20) is expected after any transition that restarts the crystal oscillator — notably _standby() with STDBY_XOSC mode (1), called by prepare_send() and start_recv(). The upstream constructor clears it once after TCXO init, but the error reappears after every subsequent STDBY_XOSC transition. On some hardware (socketed modules, slower oscillators) the flag stays set long enough for the immediately-following _check_error() to raise. Fix: skip raising on op_error 0x20. All other errors still raise. This is the second upstream deviation in sx126x.py (after the machine.Pin try/except for Unix desktop compat).
MicroPython const() values in compiled .mpy modules are compile-time inlined and not importable across modules. The previous attempt to import _IRQ_TX_DONE etc. from lora.sx126x fails at boot with 'ImportError: cant import name _IRQ_TX_DONE'. Define the hardware register masks locally with private names (_TX_DONE, _RX_DONE, _CRC_ERR) and expose them as public class attributes (PolledSX126x.TX_DONE etc.).
Use SPI.Device.lock()/unlock() (C-level spi_device_acquire_bus / spi_device_release_bus) in write_readinto() and readinto() so the shared SPI bus can't be preempted between per-byte calls while CS is held low. Closes the remaining gap from PR #222.
Contributor
Author
|
Integrating the upstream LoRa driver itself was relatively easy but so far I was unable to make the whole perfectly stable. Some challenges:
So we made good progress, but since the final result isn't perfectly stable. Meanwhile, people have built working apps on the existing driver, and Fri3d Camp 2026 is about to start. So I think it's safer to play it safe by shipping only:
...while staying on the current LoRa driver for now. Let's see how much the fixes help, how stable we can get the LoRa, and then revisit this. |
ThomasFarstrike
marked this pull request as draft
August 19, 2026 10:18
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replace the 2426-line forked drivers/lora/sx1262.py with the official upstream micropython-lib lora driver (4 files, ~1450 lines verbatim) plus thin adapter layers that encapsulate all board-specific quirks.
New files:
lib/lora/ - upstream lora package (modem, sx126x, sync_modem, init)
mpos/lora_spi_adapter.py - wraps SPI.Device as standard machine.SPI
mpos/lora_adapter.py - wraps upstream SX1262 in existing caller API
Enhanced:
mpos/lora_manager.py - acquire/release lock, CH32 reset_chip(),
optional watchdog with auto-recovery
Updated callers:
board/fri3d_2026.py - import from mpos.lora_adapter
apps/lora_chat/lora_chat.py - import + acquire/release + watchdog
Deleted:
drivers/lora/sx1262.py - replaced by upstream + adapter
Single upstream deviation: from machine import Pin guarded with try/except in sx126x.py (Unix desktop port has no machine.Pin). No other changes to upstream files.
LoRaManager now provides:
acquire(app_name)/release(app_name) - exclusive radio lock
reset_chip() - CH32 IO expander reset
start_watchdog()/stop_watchdog() - auto-recovery from SPI wedges
is_healthy() - status probe
micropySX126X/ (lilygo watcher driver) left untouched for a follow-up PR.
Refs: PR #222, issue #229