Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
lr1121_firmware_update_controller.h
Go to the documentation of this file.
1#pragma once
2
3/// @file lr1121_firmware_update_controller.h
4/// @brief LR1121 transceiver-firmware-update feature — orchestration collaborator.
5/// @ingroup hioc_hub
6///
7/// Owns the impure side of the feature: the boot-time bootloader-version excursion, the cached
8/// flash verdict, the two-press confirmation window, and the button-triggered flash sequence
9/// itself. The pure decision logic lives in lr1121_firmware_decisions.h; the bootloader-mode SPI
10/// transport lives in radio_lr1121_firmware_updater.h/.cpp. ADR 0020 and ADR 0021 record the
11/// design; the single most important rule they state is repeated in the .cpp because it is easy to
12/// violate by accident.
13///
14/// Header-weight note: `FlashDecision` / `BootloaderUpgradePath` are scoped enums with a fixed
15/// underlying type and `Lr1121FirmwareUpdater` is held only as a pointer, so all three are
16/// forward-declared here and the heavy headers are pulled in by the .cpp alone. Including them
17/// here would drag them back into hub_core.h transitively and lose the payoff.
18
19// IOHOME_LR1121_FIRMWARE_UPDATE is only visible after something pulls in esphome/core/defines.h
20// (ESPHome codegen's cg.add_define() lands there, not as a compiler -D flag) — the #include below
21// must run before the #ifdef check, not after, mirroring radio_lr1121_firmware_updater.h. hub_core.h
22// already includes esphome/core/hal.h before this header, but lr1121_firmware_update_controller.cpp
23// reaches the define through this include alone.
24#include "esphome/core/hal.h"
25
26#ifdef IOHOME_LR1121_FIRMWARE_UPDATE
27
28#include "hub_hooks.h"
29
30#include <cstdint>
31#include <string>
32#include <vector>
33
34namespace esphome {
35namespace home_io_control {
36
37// Forward declarations — the full definitions are included only in the .cpp.
38// InternalGPIOPin is NOT forward-declared here: it lives in namespace esphome (not
39// esphome::home_io_control), and declaring it in this namespace would shadow the real type and
40// break RadioDriver's constructor. esphome/core/hal.h (included above) already provides it.
41class Lr1121FirmwareUpdater;
42enum class FlashDecision : uint8_t;
43enum class BootloaderUpgradePath : uint8_t;
44class RadioDriver;
45class SpiAccess;
47
48/// @brief Orchestrates the LR1121 transceiver-firmware-update feature.
49///
50/// Constructed once by IOHomeControlComponent (guarded by IOHOME_LR1121_FIRMWARE_UPDATE);
51/// non-copyable because it holds injected callbacks and pointers into hub member addresses.
52/// @ingroup hioc_hub
53class Lr1121FirmwareUpdateController {
54 public:
55 /// @param radio Double pointer to the hub's active radio driver (may be null after a failed
56 /// init(); reflashing is exactly that recovery case).
57 /// @param spi SPI bus access — the hub itself (it implements SpiAccess).
58 /// @param rst_pin Double pointer to the hub's radio reset pin (set after construction).
59 /// @param busy_pin Double pointer to the hub's radio BUSY pin (set after construction).
60 /// @param busy Pointer to the hub's `busy_` flag (protected; guards every radio action).
61 /// @param begin_blocking_excursion Raises the blocking-warn threshold (see BeginBlockingExcursionFn).
62 /// @param hub Hub pointer — used ONLY as the self key for App.scheduler.set_timeout(), never
63 /// to reach a protected hub member.
64 Lr1121FirmwareUpdateController(RadioDriver **radio, SpiAccess *spi, InternalGPIOPin **rst_pin,
65 InternalGPIOPin **busy_pin, bool *busy,
66 BeginBlockingExcursionFn begin_blocking_excursion, IOHomeControlComponent *hub);
67
68 /// Non-copyable — holds injected callbacks and pointers into hub member addresses.
69 Lr1121FirmwareUpdateController(const Lr1121FirmwareUpdateController &) = delete;
70 Lr1121FirmwareUpdateController &operator=(const Lr1121FirmwareUpdateController &) = delete;
71
72 /// @brief Boot-time bootloader-version excursion.
73 ///
74 /// Called from setup() after select_and_construct_radio_() and before radio_->init() — at that
75 /// point nothing has configured the radio yet, so a bootloader excursion costs one extra chip
76 /// reset and needs no reboot afterward (unlike every other bootloader excursion in this
77 /// feature). Constructs the Lr1121FirmwareUpdater, runs the excursion, and caches the bootloader
78 /// version/type. Never fails setup(): a failed read just leaves the bootloader version "unknown"
79 /// and lets radio_->init() proceed normally.
80 void run_boot_time_bootloader_read();
81
82 /// @brief Compute and cache the flash verdict once radio_->init() has produced (or failed to
83 /// produce) an installed-firmware-version read.
84 ///
85 /// Must run after init(), not during the boot-time excursion above: the installed version comes
86 /// from configure_radio_(), which runs inside init(). Called from setup() regardless of whether
87 /// init() succeeded — see the null-radio recovery-path reasoning in trigger()'s guard 0.
88 void cache_flash_verdict();
89
90 /// @brief Emit the bootloader version and cached flash verdict to the config dump.
91 /// Called from dump_config(), next to the existing radio_->dump_debug() call.
92 void dump_debug() const;
93
94 /// @brief Pure content behind dump_debug(), factored out so it is testable without a
95 /// log-capturing harness (ESP_LOGCONFIG is a no-op in host tests). Always includes the verdict
96 /// line when the verdict is known, independent of whether the bootloader version is — see the
97 /// implementation for why that independence matters.
98 std::vector<std::string> debug_lines() const;
99
100 /// @brief Human-readable explanation of the cached verdict, shared by dump_debug() and trigger()
101 /// so the boot-time config dump and a button-press log always say the same thing.
102 /// @return A complete log message (no trailing newline).
103 std::string describe_flash_verdict() const;
104
105 /// @brief Entry point for the "Flash LR1121 Radio Firmware" button.
106 ///
107 /// See the .cpp for the full contract, including the safety invariant that every bootloader
108 /// excursion this triggers must end in either radio_->init() or App.safe_reboot() — there is no
109 /// third option.
110 void trigger();
111
112#ifdef IOHOME_LR1121_BOOTLOADER_UPDATE
113 /// @brief User-facing text for a bootloader-rewrite refusal at button-press time.
114 ///
115 /// Separate from describe_flash_verdict() rather than a suffix on it: that function opens with
116 /// "CANNOT PROCEED", which reads as final and would then contradict an explanation that the
117 /// rewrite is available. Leads with the outcome (nothing happened), then the reason, then the
118 /// next action. Returns a string rather than logging directly so it stays testable — host builds
119 /// compile the ESP_LOG* macros to no-ops.
120 /// @param path The cached upgrade path that produced the refusal.
121 /// @return The message to log.
122 std::string describe_bootloader_refusal(BootloaderUpgradePath path) const;
123
124 /// @brief Set by the "Allow LR1121 Bootloader Rewrite (Irreversible)" switch's write_state().
125 ///
126 /// A permission, not an override: this can only convert a cached
127 /// BootloaderUpgradePath::AVAILABLE verdict into "run the three-stage sequence" (bootloader
128 /// ADR 0021) — it never affects REJECT_WRONG_CHIP, the post-entry sanity check, the busy_ guard,
129 /// or any other verdict. Read once, at button-press time (trigger()); the ESPHome loop is
130 /// blocked for the whole three-stage sequence once it starts, so the switch cannot change
131 /// mid-flash. Deliberately not named anything with "armed" — lr1121_flash_confirmation_armed_
132 /// already means the two-press window this switch *replaces* for its own path, and a reader must
133 /// never have to guess which is meant.
134 void set_bootloader_rewrite_allowed(bool allowed) { this->bootloader_rewrite_allowed_ = allowed; }
135#endif
136
137 // --- State (public so the host tests that script individual stages can preset and inspect it;
138 // names kept verbatim from the pre-extraction IOHomeControlComponent members). ---
139
140 /// Heap-allocated in run_boot_time_bootloader_read(), like radio_ — constructed once, used by
141 /// both the boot-time excursion and any later button press. Never deleted/reconstructed at runtime.
142 Lr1121FirmwareUpdater *lr1121_firmware_updater_{nullptr};
143 bool lr1121_bootloader_version_known_{false}; ///< False until the boot-time excursion succeeds.
144 uint8_t lr1121_bootloader_chip_type_{0}; ///< `type` byte from the boot-time bootloader GetVersion.
145 uint16_t lr1121_bootloader_version_{0}; ///< Bootloader version from the boot-time excursion.
146 bool lr1121_flash_verdict_known_{false}; ///< False until cache_flash_verdict() has run.
147 /// Cached verdict (see decisions header). No NSDMI here because FlashDecision is only
148 /// forward-declared in this header; it is initialized in the constructor's init-list, and any
149 /// later constructor added to this class MUST do the same.
150 FlashDecision lr1121_flash_verdict_;
151 uint16_t lr1121_installed_fw_{0}; ///< Installed firmware version at the time the verdict was cached (0=unknown).
152 /// `device_type` byte from the same normal-mode GetVersion that produced lr1121_installed_fw_
153 /// (0=unknown, e.g. after a failed init()) — kept alongside it so describe_flash_verdict() can
154 /// name which chip a REJECT_WRONG_CHIP verdict actually saw. See lr1121_flash_decision()'s
155 /// `device_type` parameter (layer 3) for why this is a distinct value from
156 /// lr1121_bootloader_chip_type_ above (layer 4, bootloader-mode).
157 uint8_t lr1121_installed_device_type_{0};
158 bool lr1121_flash_confirmation_armed_{false}; ///< True during the two-press confirmation window.
159#ifdef IOHOME_LR1121_BOOTLOADER_UPDATE
160 /// Set by the arming switch (IOHomeLr1121BootloaderRewriteSwitch); see
161 /// set_bootloader_rewrite_allowed()'s comment above for what this may and may not affect.
162 bool bootloader_rewrite_allowed_{false};
163#endif
164
165 private:
166 /// @brief Arm the two-press confirmation window and schedule its auto-disarm.
167 /// Mirrors key_extraction_responder.cpp's KEY_EXTRACTION_AUTO_OFF_MS idiom (named set_timeout,
168 /// guard against a stale callback after a fresh press already disarmed).
169 void arm_flash_confirmation_();
170
171 /// @brief The bootloader-entry-through-post-flash-verify sequence, run only once trigger() has
172 /// decided to actually flash. Split out from that method to keep its own cognitive complexity
173 /// within clang-tidy's threshold.
174 ///
175 /// Per the safety invariant: once this method's bootloader entry succeeds, every exit —
176 /// including every failure path — ends in `App.safe_reboot()`. There is no `return` in here that
177 /// leaves the chip unconfigured without also rebooting the ESP32.
178 void run_flash_sequence_();
179
180#ifdef IOHOME_LR1121_BOOTLOADER_UPDATE
181 /// @brief The three-stage bootloader-rewrite sequence (ADR 0021): write the loader image, reboot
182 /// into it, rewrite the bootloader via 0x81xx, then write the transceiver image. Run only once
183 /// trigger() has decided the switch permits it and the cached path is
184 /// BootloaderUpgradePath::AVAILABLE.
185 ///
186 /// Same safety invariant as run_flash_sequence_(): every exit past the first enter_bootloader()
187 /// call is App.safe_reboot(), including every stage's failure path — a stage-2 failure must
188 /// reboot, not fall through to stage 3. Stage 2 (0x8100 in flight) is the one step with no
189 /// recovery path in this project; every log line in that stage must say so plainly and must
190 /// never reuse this component's ordinary "press again" phrasing.
191 void run_bootloader_upgrade_sequence_();
192#endif
193
194 RadioDriver **radio_;
195 SpiAccess *spi_;
196 InternalGPIOPin **rst_pin_;
197 InternalGPIOPin **busy_pin_;
198 bool *busy_;
199 BeginBlockingExcursionFn begin_blocking_excursion_;
201};
202
203} // namespace home_io_control
204} // namespace esphome
205
206#endif // IOHOME_LR1121_FIRMWARE_UPDATE
The main IO-Homecontrol component.
Definition hub_core.h:91
Abstract radio driver for IO-Homecontrol.
Interface for SPI bus access.
Injected-capability callback aliases shared by the hub's collaborator objects.
BootloaderUpgradePath
Whether the three-stage bootloader-rewrite sequence (ADR 0021) is applicable, and if not,...
FlashDecision
Outcome of lr1121_flash_decision().
std::function< void()> BeginBlockingExcursionFn
Raises the hub's "operation took a long time" warning threshold for a blocking radio excursion — writ...
Definition hub_hooks.h:39