|
Home IO Control
ESPHome add-on for IO-Homecontrol devices
|
Status: Superseded by ADR 0021 · Recorded: 2026-08
The LR1121's radio firmware is field-upgradable and versioned by Semtech, separately from this component. This project already reported that version at boot and pointed at Semtech's release notes when it looked outdated, but the only official way to act on that was Semtech's own reference tool, which targets an ST Nucleo dev board, not this project's hardware. Rather than require unrelated hardware and tooling just to apply an update, this project builds its own updater on top of the same underlying wire mechanism (the bootloader-mode SPI transport and opcodes) Semtech's tool uses, so a flash can be triggered directly from the existing build via a Home Assistant button.
Two facts shape the problem. First, Semtech gates each transceiver image on the bootloader version beneath it: 0x0101/0x0102/0x0103 require bootloader 0x2100, and 0x0104 requires 0x2101. Second, moving 0x2100 → 0x2101 is a separate operation with its own opcode family (0x8100/0x8101/0x8102) that rewrites the bootloader itself.
A third fact sets the stakes. Semtech's advisory SEM-PSA-2026-001 lists LR1121 as affected when running "TRX FW versions earlier than `0x0104` and BL2 FW versions earlier than `0x2101`" — it names a bootloader threshold as well as a transceiver one. The most severe of the three CVEs it covers, CVE-2025-14859 (CVSS 7.0), is a secure-boot weakness: a non-standard hash vulnerable to second-preimage attacks, letting forged firmware pass signature verification. Secure boot is the bootloader's first documented task. So a transceiver-only updater probably cannot close all three — it cannot fix a flaw in the code that authenticates transceiver firmware.
That last step is inference and is recorded as such: the advisory never maps individual CVEs to individual components, and its "and" could be read as describing the affected population rather than prescribing two fixes. What is not in doubt is that Semtech names 0x2101 at all, which it would have no reason to do if the transceiver update sufficed. Against that, all three CVEs require physical access to the SPI interface, are not remotely exploitable, and do not disclose cryptographic keys — for a hub inside a house this is defence-in-depth, not an emergency.
That second operation is categorically riskier than the first, and the reason is structural rather than statistical. Entering bootloader mode is a GPIO strap — BUSY held low across a reset — so it does not depend on the transceiver firmware being valid or even present. Erasing and rewriting the transceiver region is therefore always undoable: whatever state a failed write leaves behind, the next reset still lands in the bootloader and a retry still works. A failed bootloader write would remove exactly that property, because the bootloader is what makes bootloader-mode re-entry — and therefore recovery from anything — possible at all. A bricked bootloader is not recoverable by this project.
Option 3. The component can flash any Semtech-published LR1121 transceiver image the user points it at, and does not call the 0x81xx bootloader-updater opcodes. The goal for the transceiver-firmware path is the general capability, not a particular version — a firmware Semtech publishes after this code was written must work without a code change here.
Bootloader updates are the categorically riskier operation described above and are not implemented. This is a staged decision, not a permanent one: the transceiver-only capability ships first because it is unconditionally recoverable, and whether to add the bootloader-updater path is a question to revisit once that capability has a track record, not something ruled out for good.
The image is not vendored. A lr1121_firmware_update: block names a github:// path; the build fetches it, verifies it against its .bin.md5 sidecar, and generates a header. The block's mere presence is the build flag, so adding one line and removing it again is the whole lifecycle — deliberately a recompile each way rather than a runtime toggle, because a build that can erase the radio should not be the build that runs permanently.
The load-bearing safety rule is about what happens after a bootloader excursion, not before it. Entering bootloader mode resets the chip, which destroys everything configure_radio_() set up; Reboot(0x00) returns the chip to its firmware but does not restore that configuration. So:
flowchart TB
E["enter_bootloader()<br/><i>hardware reset — chip is now unconfigured</i>"]
B{"when?"}
I["radio_->init() runs next<br/><i>boot-time read: nothing was configured yet</i>"]
R["App.safe_reboot()<br/><i>every other path, including every failure</i>"]
X["a plain return<br/><i>radio answers SPI, looks initialized, never works again</i>"]
E --> B
B -->|at boot| I
B -->|anywhere else| R
B -.->|forbidden| X
style X stroke-dasharray: 4 4
There is no third branch. An early return on an error path is the one way to get this wrong, and it fails silently rather than loudly, which is why it is stated as an invariant rather than left to reviewer attention. This holds even when enter_bootloader() itself reports failure: its entry sequence runs unconditionally, on GPIO lines, before the confirmatory read that determines its return value, so a failure there means entry could not be confirmed, not that it did not happen.
Every input to the flash decision — bootloader version, installed firmware version, target version — is known before the user presses anything and cannot change afterwards, so the verdict is computed once at boot and printed in the config dump. A rejection therefore costs no chip access at all: the button reads a cached verdict rather than resetting the radio to re-derive one.
The bootloader/target compatibility table is treated as a depreciating snapshot, not an allow-list. A known-incompatible pair is refused, but a target the table has never heard of is routed through a two-press confirmation rather than rejected — otherwise the feature would rot the first time Semtech ships anything new.