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