ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
safe_mode.cpp
Go to the documentation of this file.
1#include "safe_mode.h"
2
4#include "esphome/core/hal.h"
5#include "esphome/core/log.h"
6#include "esphome/core/util.h"
7
8#include <cerrno>
9#include <cinttypes>
10#include <cstdio>
11
12#ifdef USE_OTA_ROLLBACK
13#ifdef USE_ZEPHYR
14#include <zephyr/dfu/mcuboot.h>
15#elif defined(USE_ESP32)
16#include <esp_ota_ops.h>
17#include <esp_system.h>
18#include <esp_image_format.h>
19#endif
20#endif
21
22namespace esphome::safe_mode {
23
24static const char *const TAG = "safe_mode";
25
26#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) && !defined(USE_OTA_PARTITIONS)
27// Find a non-running app partition. If verify is true, only returns a partition
28// whose image passes verification (expensive: reads flash). Returns nullptr if none found.
29static const esp_partition_t *find_alternate_app_partition(bool verify) {
30 const esp_partition_t *running = esp_ota_get_running_partition();
31 const esp_partition_t *result = nullptr;
32 esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_ANY, nullptr);
33 while (it != nullptr) {
34 const esp_partition_t *p = esp_partition_get(it);
35 if (p->address != running->address) {
36 if (!verify) {
37 result = p;
38 break;
39 }
40 esp_image_metadata_t data = {};
41 const esp_partition_pos_t part_pos = {
42 .offset = p->address,
43 .size = p->size,
44 };
45 if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &part_pos, &data) == ESP_OK) {
46 result = p;
47 break;
48 }
49 }
50 it = esp_partition_next(it);
51 }
52 esp_partition_iterator_release(it);
53 return result;
54}
55#endif
56
58 ESP_LOGCONFIG(TAG,
59 "Safe Mode:\n"
60 " Successful after: %" PRIu32 "s\n"
61 " Invoke after: %u attempts\n"
62 " Duration: %" PRIu32 "s",
63 this->safe_mode_boot_is_good_after_ / 1000, // because milliseconds
65 this->safe_mode_enable_time_ / 1000); // because milliseconds
66#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
67 const char *state_str;
68 if (this->ota_state_ == ESP_OTA_IMG_NEW) {
69#ifdef USE_OTA_PARTITIONS
70 state_str = "support unknown";
71#else
72 state_str = "not supported";
73#endif
74 } else if (this->ota_state_ == ESP_OTA_IMG_PENDING_VERIFY) {
75 state_str = "supported";
76 } else {
77 state_str = "support unknown";
78 }
79 ESP_LOGCONFIG(TAG, " Bootloader rollback: %s", state_str);
80#endif
81
83 auto remaining_restarts = this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_;
84 if (remaining_restarts) {
85 ESP_LOGW(TAG, "Last reset too quick; invoke in %" PRIu32 " restarts", remaining_restarts);
86 } else {
87 ESP_LOGW(TAG, "SAFE MODE IS ACTIVE");
88 }
89 }
90
91#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
92 const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition();
93 if (last_invalid != nullptr) {
94 ESP_LOGW(TAG,
95 "OTA rollback detected! Rolled back from partition '%s'\n"
96 " The device reset before the boot was marked successful",
97 last_invalid->label);
98 if (esp_reset_reason() == ESP_RST_BROWNOUT) {
99 ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n"
100 " See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
101 }
102 }
103 if (!this->app_ota_possible_) {
104 ESP_LOGW(TAG, "OTA updates are impossible.");
105#ifdef USE_OTA_PARTITIONS
106 ESP_LOGW(TAG, " OTA partition table update or serial flashing is required.");
107#else
108 if (find_alternate_app_partition(false) != nullptr) {
109 ESP_LOGW(TAG, " Activate safe mode to reboot to the recovery partition.");
110 } else {
111 ESP_LOGE(TAG, " No recovery partition available; serial flashing is required.");
112 }
113#endif
114 }
115#endif
116}
117
119
120#ifdef USE_OTA_ROLLBACK
122 // Mark the running app image as valid so the bootloader will not roll back
123 // to the previously flashed image
124#if defined(USE_ZEPHYR)
125 if (!boot_is_img_confirmed()) {
126 boot_write_img_confirmed();
127 }
128#elif defined(USE_ESP32)
129 // esp_ota_mark_app_valid_cancel_rollback() acts on the partition selected
130 // for the next boot, not the running one. After an OTA update those differ:
131 // the new image is already selected, and marking it valid before it has ever
132 // booted would disable rollback protection for that update.
133 if (esp_ota_get_running_partition() == esp_ota_get_boot_partition()) {
134 esp_ota_mark_app_valid_cancel_rollback();
135 }
136#endif
137}
138#endif
139
141 this->clean_rtc();
142 this->boot_successful_ = true;
143#ifdef USE_OTA_ROLLBACK
144 this->confirm_app_image_();
145#endif
146 // Disable loop since we no longer need to check
147 this->disable_loop();
148}
149
151 if (!this->boot_successful_ &&
152 (App.get_loop_component_start_time() - this->safe_mode_start_time_) > this->safe_mode_boot_is_good_after_) {
153 // successful boot, reset counter
154 ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
155 this->mark_successful();
156 }
157}
158
160 uint32_t current_rtc = this->read_rtc_();
161
162 if (pending && current_rtc != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) {
163 ESP_LOGI(TAG, "Device will enter on next boot");
165 }
166
167 if (!pending && current_rtc == SafeModeComponent::ENTER_SAFE_MODE_MAGIC) {
168 ESP_LOGI(TAG, "Safe mode pending has been cleared");
169 this->clean_rtc();
170 }
171}
172
176
177bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after,
178 bool in_flash) {
180 this->safe_mode_enable_time_ = enable_time;
181 this->safe_mode_boot_is_good_after_ = boot_is_good_after;
182 this->safe_mode_num_attempts_ = num_attempts;
184
185#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
186 // Check partition state to detect if bootloader supports rollback
187 const esp_partition_t *running_part = esp_ota_get_running_partition();
188 esp_ota_get_state_partition(running_part, &this->ota_state_);
189 const esp_partition_t *next_part = esp_ota_get_next_update_partition(nullptr);
190 this->app_ota_possible_ = (next_part != nullptr && next_part != running_part);
191#endif
192
193 uint32_t rtc_val = this->read_rtc_();
194 this->safe_mode_rtc_value_ = rtc_val;
195
196 bool is_manual = rtc_val == SafeModeComponent::ENTER_SAFE_MODE_MAGIC;
197
198 if (is_manual) {
199 ESP_LOGI(TAG, "Manual mode");
200 } else {
201 ESP_LOGCONFIG(TAG, "Unsuccessful boot attempts: %" PRIu32, rtc_val);
202 }
203
204 if (rtc_val < num_attempts && !is_manual) {
205 // increment counter
206 this->write_rtc_(rtc_val + 1);
207 return false;
208 }
209
210 this->clean_rtc();
211
212 if (!is_manual) {
213 ESP_LOGE(TAG, "Boot loop detected");
214 }
215
216#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) && !defined(USE_OTA_PARTITIONS)
217 // Allow recovery of soft-bricked devices
218 // Instead of starting safe_mode, reboot to the other app partition if all conditions are met:
219 // - app OTA is impossible (for example because the other app partition has type 'factory')
220 // - the other app partition contains a valid app (for example Tasmota safeboot image or ESPHome)
221 // - allow_partition_access is not configured making recovery via partition table update impossible
222 // Image verification is deferred until here so the cost is only paid when entering safe mode,
223 // not on every boot.
224 if (!this->app_ota_possible_) {
225 const esp_partition_t *rollback_part = find_alternate_app_partition(true);
226 if (rollback_part != nullptr) {
227 esp_err_t err = esp_ota_set_boot_partition(rollback_part);
228 if (err == ESP_OK) {
229 ESP_LOGW(TAG, "OTA updates are impossible. Rebooting to recovery app.");
230 App.reboot();
231 } else {
232 ESP_LOGE(TAG, "Failed to set recovery boot partition: %s", esp_err_to_name(err));
233 }
234 }
235 }
236#endif
237
238 this->status_set_error();
239 this->set_timeout(enable_time, []() {
240 ESP_LOGW(TAG, "Timeout, restarting");
241 App.reboot();
242 });
243
244 // Delay here to allow power to stabilize before Wi-Fi/Ethernet is initialised
245 delay(300); // NOLINT
246 App.setup();
247
248 ESP_LOGW(TAG, "SAFE MODE IS ACTIVE");
249
250#ifdef USE_SAFE_MODE_CALLBACK
251 this->safe_mode_callback_.call();
252#endif
253
254 return true;
255}
256
261
264 if (!this->rtc_.load(&val))
265 return 0;
266 return val;
267}
268
270 // Save without sync - preferences will be written at shutdown or by IntervalSyncer.
271 // This avoids blocking the loop for 50+ ms on flash write. If the device crashes
272 // before sync, the boot wasn't really successful anyway and the counter should
273 // remain incremented.
274 uint32_t val = 0;
275 this->rtc_.save(&val);
276}
277
280 this->clean_rtc();
281#if defined(USE_OTA_ROLLBACK) && defined(USE_SAFE_MODE_BOOT_IS_GOOD_ON_SHUTDOWN)
282 // An orderly shutdown (deep sleep, restart, power off) means the firmware is
283 // functional, so confirm the running app image even if boot_is_good_after has
284 // not elapsed yet. Without this, a device that enters deep sleep shortly
285 // after waking would have every OTA update rolled back by the bootloader on
286 // the next wake. Can be turned off with boot_is_good_on_shutdown: false for
287 // strict rollback semantics.
288 this->confirm_app_image_();
289#endif
290}
291
292} // namespace esphome::safe_mode
void setup()
Reserve space for components to avoid memory fragmentation.
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void disable_loop()
Disable this component's loop.
uint32_t safe_mode_enable_time_
The time safe mode should remain active for.
Definition safe_mode.h:51
bool boot_successful_
set to true after boot is considered successful
Definition safe_mode.h:58
uint32_t safe_mode_start_time_
stores when safe mode was enabled
Definition safe_mode.h:53
uint32_t safe_mode_boot_is_good_after_
The amount of time after which the boot is considered successful.
Definition safe_mode.h:50
float get_setup_priority() const override
StaticCallbackManager< ESPHOME_SAFE_MODE_CALLBACK_COUNT, void()> safe_mode_callback_
Definition safe_mode.h:66
esp_ota_img_states_t ota_state_
Definition safe_mode.h:55
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after, bool in_flash)
void set_safe_mode_pending(const bool &pending)
Set to true if the next startup will enter safe mode.
static const uint32_t ENTER_SAFE_MODE_MAGIC
a magic number to indicate that safe mode should be entered on next boot
Definition safe_mode.h:69
mopeka_std_values val[3]
std::span< const uint8_t > data
constexpr uint32_t RTC_KEY
RTC key for storing boot loop counter - used by safe_mode and preferences backends.
Definition safe_mode.h:15
constexpr float AFTER_WIFI
For components that should be initialized after WiFi is connected.
Definition component.h:55
ESPPreferences * global_preferences
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24
bool sync()
Commit pending writes to flash.
Definition preferences.h:33