Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
lr1121_firmware_decisions.h
Go to the documentation of this file.
1#pragma once
2
3/// @file lr1121_firmware_decisions.h
4/// @brief Pure decision logic for the LR1121 transceiver-firmware-update feature.
5/// @ingroup hioc_radio
6///
7/// Header-only pure functions, no I/O — same style as hub_decisions.h and
8/// radio_lr1121.h::lr1121_firmware_is_outdated(), fully host-testable without radio hardware.
9/// Deliberately does not include radio_lr1121.h: LR1121_DEVICE_TYPE_FOR_FIRMWARE_DECISIONS below
10/// is a local copy of that file's LR1121_DEVICE_TYPE (0x03), kept independent so this header pulls
11/// in nothing but cstdint — the same reasoning that keeps hub_decisions.h free of driver headers.
12
13#include <cstdint>
14
15namespace esphome {
16namespace home_io_control {
17
18/// @brief LR1121 GetVersion `type` byte in normal mode — must match radio_lr1121.h's
19/// LR1121_DEVICE_TYPE (0x03). Kept as a separate constant, not a shared header include, so this
20/// header stays dependency-free; see the file header for why. A static_assert in
21/// tests/radio_lr1121_firmware_updater_test.cpp (a translation unit that already includes both
22/// headers) guards the two from silently drifting apart.
23inline constexpr uint8_t LR1121_DEVICE_TYPE_FOR_FIRMWARE_DECISIONS = 0x03;
24
25/// @brief Normal-mode GetVersion `type` bytes for the two chips an LR1121 is most easily confused
26/// with — companions to LR1121_DEVICE_TYPE_FOR_FIRMWARE_DECISIONS above, used only to name the
27/// chip family in a REJECT_WRONG_CHIP message. Last checked 2026-08-05.
28inline constexpr uint8_t LR1110_DEVICE_TYPE_FOR_FIRMWARE_DECISIONS = 0x01;
29inline constexpr uint8_t LR1120_DEVICE_TYPE_FOR_FIRMWARE_DECISIONS = 0x02;
30
31/// @brief LR1121 GetVersion `type` byte reported while running the *bootloader*
32/// (LR11XX_TYPE_PRODUCTION_MODE) — must match radio_lr1121_firmware_updater.h's
33/// LR1121_UPDATER_BOOTLOADER_TYPE (0xDF). Kept as a separate constant for the same
34/// dependency-free reason as LR1121_DEVICE_TYPE_FOR_FIRMWARE_DECISIONS above. Note this byte
35/// identifies production silicon, not chip family — bootloader `type` is 0xDF on an LR1120 or
36/// LR1110 too, which is why chip family is decided from the bootloader *version*, not this byte.
37inline constexpr uint8_t LR1121_BOOTLOADER_TYPE_FOR_FIRMWARE_DECISIONS = 0xDF;
38
39/// LR1121 bootloader versions, from Semtech's published compatibility matrix. An LR1121
40/// reports one of these two; LR1120 reports 0x2000/0x2001 and LR1110 0x6500/0x1001, which is
41/// what makes checking against these two a chip-family check as well as a capability check.
42inline constexpr uint16_t LR1121_BOOTLOADER_2100 = 0x2100;
43inline constexpr uint16_t LR1121_BOOTLOADER_2101 = 0x2101;
44
45/// @brief Bootloader versions reported by the two chips an LR1121 is most easily confused with —
46/// used only to name the chip family in a REJECT_WRONG_CHIP message. Same snapshot discipline as
47/// LR1121_BOOTLOADER_2100/2101; last checked 2026-08-05.
48inline constexpr uint16_t LR1120_BOOTLOADER_2000 = 0x2000;
49inline constexpr uint16_t LR1120_BOOTLOADER_2001 = 0x2001;
50inline constexpr uint16_t LR1110_BOOTLOADER_6500 = 0x6500;
51inline constexpr uint16_t LR1110_BOOTLOADER_1001 = 0x1001;
52
53/// @brief Version the `lr1121_loader_2100.bin` bootloader-*loader* image reports of itself.
54/// Numerically identical to LR1121_BOOTLOADER_2100, but a distinct concept: this is the loader
55/// firmware's own self-reported version, which Semtech's compatibility rule requires to *equal*
56/// the bootloader version currently running before the loader may be used -- not a
57/// "requires bootloader >= X" rule like LR1121_KNOWN_BOOTLOADER_REQUIREMENTS below.
58inline constexpr uint16_t LR1121_LOADER_2100 = 0x2100;
59
60/// @return true if `bootloader_version` is one of the two bootloader versions an LR1121 (as
61/// opposed to an LR1120 or LR1110) can report.
62[[nodiscard]] constexpr bool lr1121_bootloader_is_lr1121(uint16_t bootloader_version) {
63 return bootloader_version == LR1121_BOOTLOADER_2100 || bootloader_version == LR1121_BOOTLOADER_2101;
64}
65
66/// @brief Human-readable chip family for a bootloader version that is not one of the two LR1121
67/// values above, for an actionable REJECT_WRONG_CHIP message. Moved here from hub wiring so the
68/// LR1120/LR1110 bootloader IDs live in one place (next to the LR1121 ones they are compared
69/// against) and the mapping is host-testable without a hub.
70[[nodiscard]] constexpr const char *lr1121_chip_family_for_bootloader(uint16_t bootloader_version) {
71 if (bootloader_version == LR1120_BOOTLOADER_2000 || bootloader_version == LR1120_BOOTLOADER_2001)
72 return "an LR1120";
73 if (bootloader_version == LR1110_BOOTLOADER_6500 || bootloader_version == LR1110_BOOTLOADER_1001)
74 return "an LR1110";
75 return "an unrecognized chip";
76}
77
78/// @brief Human-readable chip family for a normal-mode device_type that is not the LR1121 value
79/// above, for an actionable REJECT_WRONG_CHIP message (the layer-3 counterpart of
80/// lr1121_chip_family_for_bootloader() above).
81[[nodiscard]] constexpr const char *lr1121_chip_family_for_device_type(uint8_t device_type) {
83 return "an LR1110";
85 return "an LR1120";
86 return "an unrecognized chip";
87}
88
89/// @brief One (target firmware, required bootloader) pairing from Semtech's published
90/// compatibility matrix.
92 uint16_t target_fw;
93 uint16_t bootloader;
94};
95
96/// Bootloader requirements we know about, copied from Semtech's published pairings
97/// (SWTL001/application/src/lr11xx_update_utils.c :: compatibility_matrix[]): 0x0101/0x0102/0x0103
98/// need bootloader 0x2100, 0x0104 needs 0x2101. This is an ADVISORY list of pairs we can warn
99/// about, not a compatibility authority — it is a point-in-time snapshot and a target absent from
100/// it is "unverified", never "incompatible". Extending this when Semtech publishes a new image is
101/// a one-line edit; it belongs next to LR1121_KNOWN_LATEST_FW_* (radio_lr1121.h) as the same kind
102/// of snapshot, with the same staleness discipline. Last checked 2026-08-05.
109
110/// @brief Whether a target firmware version is known to work with a given bootloader version.
111enum class BootloaderSupport : uint8_t {
112 SUPPORTED, ///< Both versions known, and the pairing is in LR1121_KNOWN_BOOTLOADER_REQUIREMENTS.
113 UNSUPPORTED, ///< target_fw is known, but not paired with this bootloader_version.
114 UNKNOWN_TARGET, ///< target_fw does not appear in LR1121_KNOWN_BOOTLOADER_REQUIREMENTS at all.
115};
116
117/// @brief Look up whether `target_fw` is known to require `bootloader_version`.
118///
119/// A target absent from LR1121_KNOWN_BOOTLOADER_REQUIREMENTS is UNKNOWN_TARGET, never
120/// UNSUPPORTED — the table is a snapshot of what Semtech had published when this file was last
121/// updated, not an allow-list; a future firmware version this build has never heard of must not
122/// be rejected on that basis alone (see lr1121_flash_decision()'s three-way rule).
123[[nodiscard]] constexpr BootloaderSupport lr1121_bootloader_supports_target(uint16_t target_fw,
124 uint16_t bootloader_version) {
125 for (const auto &requirement : LR1121_KNOWN_BOOTLOADER_REQUIREMENTS) {
126 if (requirement.target_fw == target_fw) {
127 return requirement.bootloader == bootloader_version ? BootloaderSupport::SUPPORTED
129 }
130 }
132}
133
134/// @brief Required bootloader for a known target firmware version.
135/// @return The paired bootloader from LR1121_KNOWN_BOOTLOADER_REQUIREMENTS, or 0 when target_fw
136/// is not in the table -- an "unverified", not "incompatible", target (see that table's
137/// comment), and never a real requirement value since no table entry uses 0.
138[[nodiscard]] constexpr uint16_t lr1121_required_bootloader_for(uint16_t target_fw) {
139 for (const auto &requirement : LR1121_KNOWN_BOOTLOADER_REQUIREMENTS) {
140 if (requirement.target_fw == target_fw)
141 return requirement.bootloader;
142 }
143 return 0;
144}
145
146/// @brief Direction of a bootloader/target mismatch, for messaging.
147///
148/// lr1121_flash_decision()'s REJECT_BOOTLOADER_TOO_OLD verdict fires whenever
149/// lr1121_bootloader_supports_target() returns UNSUPPORTED, which covers both directions: the
150/// target needs a newer bootloader than this chip has (the only direction reachable before the
151/// bootloader-update feature existed), and the target needs an OLDER bootloader than this chip
152/// has -- a downgrade, unreachable until a chip can actually be running 0x2101. Used for
153/// *messaging* only, in both the transceiver-only and bootloader-update builds -- it never
154/// changes lr1121_flash_decision()'s own verdict.
155enum class BootloaderMismatch : uint8_t {
156 NONE, ///< target_fw is unknown, or its required bootloader matches bootloader_version.
157 TARGET_NEEDS_NEWER, ///< The "too old" direction -- what REJECT_BOOTLOADER_TOO_OLD has always meant until now.
158 TARGET_NEEDS_OLDER, ///< The "too new" direction -- a downgrade, newly reachable once this feature ships.
159};
160
161/// @brief Classify a bootloader/target mismatch by direction; see BootloaderMismatch.
162[[nodiscard]] constexpr BootloaderMismatch lr1121_bootloader_mismatch_kind(uint16_t target_fw,
163 uint16_t bootloader_version) {
164 const uint16_t required = lr1121_required_bootloader_for(target_fw);
165 if (required == 0 || required == bootloader_version)
167 return required > bootloader_version ? BootloaderMismatch::TARGET_NEEDS_NEWER
169}
170
171/// @brief Whether the three-stage bootloader-rewrite sequence (ADR 0021) is applicable, and if
172/// not, why.
173enum class BootloaderUpgradePath : uint8_t {
174 NOT_APPLICABLE, ///< No block, or no upgrade needed/possible to evaluate. Keep the original verdict.
175 AVAILABLE, ///< Three-stage is possible. Still requires the arming switch to actually run.
176 BLOCKED_UNKNOWN_TARGET, ///< Block present, but this build cannot know what the target needs.
177 BLOCKED_BOOTLOADER_NEWER, ///< Target needs an OLDER bootloader. Not reachable today, likely never.
178};
179
180/// @brief Whether the three-stage bootloader upgrade is applicable for the current cached state.
181///
182/// A post-filter, consulted only when lr1121_flash_decision() has already returned
183/// REJECT_BOOTLOADER_TOO_OLD -- it never runs earlier and never changes that function's verdict
184/// (see this header's file comment and lr1121_flash_decision()'s doc comment). The evaluation
185/// order below *is* the specification -- each rule exists to close a specific way an irreversible
186/// write could be justified on insufficient evidence:
187/// 1. !block_present -> NOT_APPLICABLE -- feature not built in.
188/// 2. !bootloader_version_known -> NOT_APPLICABLE -- an unknown current bootloader cannot
189/// justify an irreversible write. Note run_lr1121_flash_sequence_() *adopts* a fresh reading
190/// at flash time for the ordinary transceiver path; that allowance must not extend to here.
191/// 3. bootloader_version doesn't identify an LR1121 -> NOT_APPLICABLE -- REJECT_WRONG_CHIP owns
192/// wrong-chip messaging, this function does not duplicate it.
193/// 4. loader_fw != bootloader_version -> NOT_APPLICABLE -- Semtech's equality rule for the
194/// loader image (see LR1121_LOADER_2100); also the "chip is already on 0x2101" case, since
195/// no 0x2101-chip can equal the 0x2100 loader.
196/// 5. target not in the compatibility table (required == 0) -> BLOCKED_UNKNOWN_TARGET -- never
197/// gamble an irreversible write on an unrecognised target.
198/// 6. required == bootloader_version -> NOT_APPLICABLE -- no upgrade needed.
199/// 7. required > bootloader_version -> AVAILABLE -- the one path that proceeds.
200/// 8. else (required < bootloader_version) -> BLOCKED_BOOTLOADER_NEWER -- a downgrade. The
201/// 0x8101 report includes an anti-rollback check, whose exact semantics Semtech does not
202/// document, but which most likely makes this permanently impossible; ADR 0021 records how
203/// far that is inference.
204/// @param block_present Whether a `bootloader:` sub-block is configured (the build flag).
205/// @param bootloader_version_known Whether the boot-time excursion successfully read a bootloader
206/// version -- same "unknown is never evidence" sentinel rule as lr1121_flash_decision().
207/// @param bootloader_version Bootloader version read at boot; meaningless if !bootloader_version_known.
208/// @param loader_fw Version parsed from the bootloader: sub-block's loader source: image.
209/// @param target_fw Configured target firmware version (0 if unknown).
210[[nodiscard]] constexpr BootloaderUpgradePath lr1121_bootloader_upgrade_path(bool block_present,
211 bool bootloader_version_known,
212 uint16_t bootloader_version,
213 uint16_t loader_fw, uint16_t target_fw) {
214 if (!block_present)
216 if (!bootloader_version_known)
218 if (!lr1121_bootloader_is_lr1121(bootloader_version))
220 if (loader_fw != bootloader_version)
222
223 const uint16_t required = lr1121_required_bootloader_for(target_fw);
224 if (required == 0)
226 if (required == bootloader_version)
228 if (required > bootloader_version)
231}
232
233/// @brief Outcome of lr1121_flash_decision().
234enum class FlashDecision : uint8_t {
235 PROCEED, ///< Safe to erase and write.
236 ALREADY_INSTALLED, ///< target_fw == installed_fw (both known) — the post-success state.
237 NEEDS_CONFIRMATION, ///< Not unsafe, but not an unambiguous "yes" either — needs a second press.
238 REJECT_WRONG_CHIP, ///< device_type or bootloader_version doesn't identify an LR1121.
239 REJECT_BOOTLOADER_TOO_OLD, ///< target_fw is known and positively incompatible with this bootloader.
240};
241
242/// @brief The single decision point for whether/how to flash `target_fw`.
243///
244/// Order of checks. Layers 3 and 4 below are the two chip-identity checks (normal-mode
245/// device_type and bootloader-mode version); everything else exists to make sure an absent read
246/// is never mistaken for evidence:
247/// 1. Boot excursion never completed at all (bootloader_version unknown) -- nothing below can
248/// be evaluated, so this is checked first and short-circuits straight to NEEDS_CONFIRMATION.
249/// 2. Layer 4 -- bootloader-mode identity: `type` must be the production-silicon byte AND the
250/// bootloader version must be one an LR1121 actually reports (as opposed to an LR1120 or
251/// LR1110, which also answer bootloader-mode GetVersion, just with different values).
252/// 3. Normal-mode read never happened (device_type unknown, e.g. after a failed init()) --
253/// unknown is not evidence of the wrong chip, so this is checked *before* layer 3 can turn
254/// it into a false REJECT_WRONG_CHIP.
255/// 4. Layer 3 -- normal-mode chip identity: distinguishes LR1121 from LR1110/LR1120 while
256/// normal-mode firmware can still answer.
257/// 5. Bootloader/target compatibility (the three-way rule -- known-compatible proceeds,
258/// known-incompatible refuses, unknown-target asks for confirmation rather than refusing so
259/// a future firmware release keeps working without a code change here).
260/// 6. Only once the pairing is positively known-good: the not-newer-so-confirm version compare.
261///
262/// Every input below has its own "unknown" sentinel, and every one of them routes to
263/// NEEDS_CONFIRMATION rather than a rejection or a false PROCEED -- an absent read is never
264/// evidence of anything, safe or unsafe:
265/// - `bootloader_version == 0`: the boot-time excursion never successfully read one. 0x0000 is
266/// not a value any real LR11xx bootloader reports.
267/// - `device_type == 0`: the normal-mode read never happened at all (failed init(), or the read
268/// itself failed). Deliberately still allowed to reach a flash after confirmation -- a radio
269/// that failed to initialize is exactly the case reflashing is meant to recover.
270/// - `target_fw == 0`: the build could not derive a version from the filename and none was
271/// configured; can never match a LR1121_KNOWN_BOOTLOADER_REQUIREMENTS entry (none uses 0), so
272/// it always resolves to BootloaderSupport::UNKNOWN_TARGET.
273/// - `installed_fw == 0`: the installed version is unknown (device_type known-good but the
274/// firmware-version bytes could not be read) -- treated exactly like "not newer", never as
275/// "older than everything" the way a naive `target_fw > installed_fw` would.
276///
277/// @param device_type Chip identity byte from a *normal-mode* GetVersion (layer 3); 0 if that
278/// read never happened.
279/// @param bootloader_chip_type `type` byte from the *bootloader-mode* GetVersion read at boot
280/// (layer 4); 0xDF (LR1121_BOOTLOADER_TYPE_FOR_FIRMWARE_DECISIONS) for production silicon
281/// of any LR11xx family, or 0 if the boot excursion never completed.
282/// @param bootloader_version Bootloader version read at boot; one of
283/// LR1121_BOOTLOADER_2100/2101 for a genuine LR1121, or 0 if never successfully read.
284/// @param installed_fw Currently-installed transceiver firmware version (0 if unknown, e.g. after
285/// a failed init()).
286/// @param target_fw Configured target firmware version (0 if unknown — see above).
287/// @param already_confirmed True on a second button press within the confirmation window; allows
288/// every non-hard-reject outcome to proceed instead of asking again.
289/// @return The decision; see FlashDecision.
290[[nodiscard]] constexpr FlashDecision lr1121_flash_decision(uint8_t device_type, uint8_t bootloader_chip_type,
291 uint16_t bootloader_version, uint16_t installed_fw,
292 uint16_t target_fw, bool already_confirmed) {
293 if (bootloader_version == 0)
295
296 // Layer 4: bootloader-mode identity. `type` is LR11XX_TYPE_PRODUCTION_MODE regardless of chip
297 // family, so family comes from the bootloader version itself, not this byte.
298 if (bootloader_chip_type != LR1121_BOOTLOADER_TYPE_FOR_FIRMWARE_DECISIONS ||
299 !lr1121_bootloader_is_lr1121(bootloader_version))
301
302 if (device_type == 0)
304
305 // Layer 3: normal-mode chip identity.
308
309 const BootloaderSupport support = lr1121_bootloader_supports_target(target_fw, bootloader_version);
310 if (support == BootloaderSupport::UNSUPPORTED)
314
315 // support == SUPPORTED: both versions are known and the pairing is a positive match, so the
316 // version numbers are meaningful to compare (unlike the UNKNOWN_TARGET case above) -- provided
317 // installed_fw was actually read; if not, that is "unknown", not "older than everything".
318 if (installed_fw == 0)
320 if (target_fw == installed_fw)
321 return already_confirmed ? FlashDecision::PROCEED : FlashDecision::ALREADY_INSTALLED;
322 if (target_fw > installed_fw)
325}
326
327} // namespace home_io_control
328} // namespace esphome
constexpr uint8_t LR1120_DEVICE_TYPE_FOR_FIRMWARE_DECISIONS
constexpr uint16_t lr1121_required_bootloader_for(uint16_t target_fw)
Required bootloader for a known target firmware version.
constexpr uint16_t LR1110_BOOTLOADER_6500
constexpr Lr1121BootloaderRequirement LR1121_KNOWN_BOOTLOADER_REQUIREMENTS[]
Bootloader requirements we know about, copied from Semtech's published pairings (SWTL001/application/...
constexpr uint16_t LR1121_BOOTLOADER_2100
LR1121 bootloader versions, from Semtech's published compatibility matrix.
constexpr uint16_t LR1110_BOOTLOADER_1001
constexpr BootloaderSupport lr1121_bootloader_supports_target(uint16_t target_fw, uint16_t bootloader_version)
Look up whether target_fw is known to require bootloader_version.
constexpr uint16_t LR1121_BOOTLOADER_2101
constexpr uint16_t LR1121_LOADER_2100
Version the lr1121_loader_2100.bin bootloader-*loader* image reports of itself.
constexpr BootloaderUpgradePath lr1121_bootloader_upgrade_path(bool block_present, bool bootloader_version_known, uint16_t bootloader_version, uint16_t loader_fw, uint16_t target_fw)
Whether the three-stage bootloader upgrade is applicable for the current cached state.
BootloaderUpgradePath
Whether the three-stage bootloader-rewrite sequence (ADR 0021) is applicable, and if not,...
@ AVAILABLE
Three-stage is possible. Still requires the arming switch to actually run.
@ BLOCKED_BOOTLOADER_NEWER
Target needs an OLDER bootloader. Not reachable today, likely never.
@ BLOCKED_UNKNOWN_TARGET
Block present, but this build cannot know what the target needs.
@ NOT_APPLICABLE
No block, or no upgrade needed/possible to evaluate. Keep the original verdict.
constexpr const char * lr1121_chip_family_for_bootloader(uint16_t bootloader_version)
Human-readable chip family for a bootloader version that is not one of the two LR1121 values above,...
constexpr bool lr1121_bootloader_is_lr1121(uint16_t bootloader_version)
BootloaderSupport
Whether a target firmware version is known to work with a given bootloader version.
@ SUPPORTED
Both versions known, and the pairing is in LR1121_KNOWN_BOOTLOADER_REQUIREMENTS.
@ UNSUPPORTED
target_fw is known, but not paired with this bootloader_version.
@ UNKNOWN_TARGET
target_fw does not appear in LR1121_KNOWN_BOOTLOADER_REQUIREMENTS at all.
constexpr FlashDecision lr1121_flash_decision(uint8_t device_type, uint8_t bootloader_chip_type, uint16_t bootloader_version, uint16_t installed_fw, uint16_t target_fw, bool already_confirmed)
The single decision point for whether/how to flash target_fw.
FlashDecision
Outcome of lr1121_flash_decision().
@ REJECT_WRONG_CHIP
device_type or bootloader_version doesn't identify an LR1121.
@ NEEDS_CONFIRMATION
Not unsafe, but not an unambiguous "yes" either — needs a second press.
@ REJECT_BOOTLOADER_TOO_OLD
target_fw is known and positively incompatible with this bootloader.
@ ALREADY_INSTALLED
target_fw == installed_fw (both known) — the post-success state.
constexpr uint8_t LR1121_BOOTLOADER_TYPE_FOR_FIRMWARE_DECISIONS
LR1121 GetVersion type byte reported while running the bootloader (LR11XX_TYPE_PRODUCTION_MODE) — mus...
constexpr BootloaderMismatch lr1121_bootloader_mismatch_kind(uint16_t target_fw, uint16_t bootloader_version)
Classify a bootloader/target mismatch by direction; see BootloaderMismatch.
constexpr uint16_t LR1120_BOOTLOADER_2001
constexpr uint8_t LR1121_DEVICE_TYPE_FOR_FIRMWARE_DECISIONS
LR1121 GetVersion type byte in normal mode — must match radio_lr1121.h's LR1121_DEVICE_TYPE (0x03).
constexpr const char * lr1121_chip_family_for_device_type(uint8_t device_type)
Human-readable chip family for a normal-mode device_type that is not the LR1121 value above,...
constexpr uint8_t LR1110_DEVICE_TYPE_FOR_FIRMWARE_DECISIONS
Normal-mode GetVersion type bytes for the two chips an LR1121 is most easily confused with — companio...
constexpr uint16_t LR1120_BOOTLOADER_2000
Bootloader versions reported by the two chips an LR1121 is most easily confused with — used only to n...
BootloaderMismatch
Direction of a bootloader/target mismatch, for messaging.
@ TARGET_NEEDS_NEWER
The "too old" direction – what REJECT_BOOTLOADER_TOO_OLD has always meant until now.
@ TARGET_NEEDS_OLDER
The "too new" direction – a downgrade, newly reachable once this feature ships.
@ NONE
target_fw is unknown, or its required bootloader matches bootloader_version.
One (target firmware, required bootloader) pairing from Semtech's published compatibility matrix.