ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ota_backend_esp8266.cpp
Go to the documentation of this file.
1#ifdef USE_ESP8266
3#include "ota_backend.h"
4
9#include "esphome/core/log.h"
10
11#include <Esp.h>
12#include <esp8266_peri.h>
13
14#include <cinttypes>
15
16extern "C" {
17#include <c_types.h>
18#include <eboot_command.h>
19#include <flash_hal.h>
20#include <spi_flash.h>
21#include <user_interface.h>
22}
23
24// Note: FLASH_SECTOR_SIZE (0x1000) is already defined in spi_flash_geometry.h
25
26// Flash header offsets
27static constexpr uint8_t FLASH_MODE_OFFSET = 2;
28
29// Firmware magic bytes
30static constexpr uint8_t FIRMWARE_MAGIC = 0xE9;
31static constexpr uint8_t GZIP_MAGIC_1 = 0x1F;
32static constexpr uint8_t GZIP_MAGIC_2 = 0x8B;
33
34// ESP8266 flash memory base address (memory-mapped flash starts here)
35static constexpr uint32_t FLASH_BASE_ADDRESS = 0x40200000;
36
37// Boot mode extraction from GPI register (bits 16-19 contain boot mode)
38static constexpr int BOOT_MODE_SHIFT = 16;
39static constexpr int BOOT_MODE_MASK = 0xf;
40
41// Boot mode indicating UART download mode (OTA not possible)
42static constexpr int BOOT_MODE_UART_DOWNLOAD = 1;
43
44// Minimum buffer size when memory is constrained
45static constexpr size_t MIN_BUFFER_SIZE = 256;
46
47namespace esphome::ota {
48
49ESPHOME_LOG_TAG(TAG, "ota");
50
51std::unique_ptr<ESP8266OTABackend> make_ota_backend() { return make_unique<ESP8266OTABackend>(); }
52
54 if (!this->lock_.try_acquire())
56 if (ota_type != OTA_TYPE_UPDATE_APP) {
58 }
59 // Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space
60 if (image_size == 0) {
61 // Round down to sector boundary: subtract one sector, then mask to sector alignment
62 // NOLINTNEXTLINE(readability-static-accessed-through-instance)
63 image_size = (ESP.getFreeSketchSpace() - FLASH_SECTOR_SIZE) & ~(FLASH_SECTOR_SIZE - 1);
64 }
65
66 // Check boot mode - if boot mode is UART download mode,
67 // we will not be able to reset into normal mode once update is done
68 // NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- GPI is MMIO at a fixed address
69 int boot_mode = (GPI >> BOOT_MODE_SHIFT) & BOOT_MODE_MASK;
70 if (boot_mode == BOOT_MODE_UART_DOWNLOAD) {
72 }
73
74 // Check flash configuration - real size must be >= configured size
75 // NOLINTNEXTLINE(readability-static-accessed-through-instance)
76 if (!ESP.checkFlashConfig(false)) {
78 }
79
80 // Get current sketch size
81 // NOLINTNEXTLINE(readability-static-accessed-through-instance)
82 uint32_t sketch_size = ESP.getSketchSize();
83
84 // Size of current sketch rounded to sector boundary
85 uint32_t current_sketch_size = (sketch_size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1));
86
87 // Size of update rounded to sector boundary
88 uint32_t rounded_size = (image_size + FLASH_SECTOR_SIZE - 1) & (~(FLASH_SECTOR_SIZE - 1));
89
90 // End of available space for sketch and update (start of filesystem)
91 uint32_t update_end_address = FS_start - FLASH_BASE_ADDRESS;
92
93 // Calculate start address for the update (write from end backwards)
94 this->start_address_ = (update_end_address > rounded_size) ? (update_end_address - rounded_size) : 0;
95
96 // Check if there's enough space for both current sketch and update
97 if (this->start_address_ < current_sketch_size) {
99 }
100
101 // Allocate buffer for sector writes (use smaller buffer if memory constrained)
102 // NOLINTNEXTLINE(readability-static-accessed-through-instance)
103 this->buffer_size_ = (ESP.getFreeHeap() > 2 * FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : MIN_BUFFER_SIZE;
104
105 // ESP8266's umm_malloc guarantees 4-byte aligned allocations, which is required
106 // for spi_flash_write(). This is the same pattern used by Arduino's Updater class.
107 this->buffer_ = make_unique<uint8_t[]>(this->buffer_size_);
108 if (!this->buffer_) {
110 }
111
112 this->current_address_ = this->start_address_;
113 this->image_size_ = image_size;
114 this->bytes_received_ = 0;
115 this->buffer_len_ = 0;
116 this->md5_set_ = false;
117
118 // Disable WiFi sleep during update
119 wifi_set_sleep_type(NONE_SLEEP_T);
120
121 // Prevent preference writes during update
123
124 // Initialize MD5 computation
125 this->md5_.init();
126
127 ESP_LOGD(TAG, "OTA begin: start=0x%08" PRIX32 ", size=%zu", this->start_address_, image_size);
128
129 return OTA_RESPONSE_OK;
130}
131
133 // Parse hex string to bytes
134 if (parse_hex(md5, this->expected_md5_, 16)) {
135 this->md5_set_ = true;
136 }
137}
138
140 if (!this->buffer_) {
142 }
143
144 size_t written = 0;
145 while (written < len) {
146 // Calculate how much we can buffer
147 size_t to_buffer = std::min(len - written, this->buffer_size_ - this->buffer_len_);
148 memcpy(this->buffer_.get() + this->buffer_len_, data + written, to_buffer);
149 this->buffer_len_ += to_buffer;
150 this->bytes_received_ += to_buffer;
151 written += to_buffer;
152
153 // If buffer is full, write to flash
154 if (this->buffer_len_ == this->buffer_size_ && !this->write_buffer_()) {
156 }
157 }
158
159 return OTA_RESPONSE_OK;
160}
161
163 if ((this->current_address_ % FLASH_SECTOR_SIZE) != 0) {
164 return true; // Not at sector boundary
165 }
166
167 App.feed_wdt();
168 if (spi_flash_erase_sector(this->current_address_ / FLASH_SECTOR_SIZE) != SPI_FLASH_RESULT_OK) {
169 ESP_LOGE(TAG, "Flash erase failed at 0x%08" PRIX32, this->current_address_);
170 return false;
171 }
172 return true;
173}
174
176 App.feed_wdt();
177 if (spi_flash_write(this->current_address_, reinterpret_cast<uint32_t *>(this->buffer_.get()), this->buffer_len_) !=
178 SPI_FLASH_RESULT_OK) {
179 ESP_LOGE(TAG, "Flash write failed at 0x%08" PRIX32, this->current_address_);
180 return false;
181 }
182 return true;
183}
184
186 if (this->buffer_len_ == 0) {
187 return true;
188 }
189
190 if (!this->erase_sector_if_needed_()) {
191 return false;
192 }
193
194 // Patch flash mode in first sector if needed
195 // This is analogous to what esptool.py does when it receives a --flash_mode argument
196 bool is_first_sector = (this->current_address_ == this->start_address_);
197 uint8_t original_flash_mode = 0;
198 bool patched_flash_mode = false;
199
200 // Only patch if we have enough bytes to access flash mode offset and it's not GZIP
201 if (is_first_sector && this->buffer_len_ > FLASH_MODE_OFFSET && this->buffer_[0] != GZIP_MAGIC_1) {
202 // Not GZIP compressed - check and patch flash mode
203 uint8_t current_flash_mode = this->get_flash_chip_mode_();
204 uint8_t buffer_flash_mode = this->buffer_[FLASH_MODE_OFFSET];
205
206 if (buffer_flash_mode != current_flash_mode) {
207 original_flash_mode = buffer_flash_mode;
208 this->buffer_[FLASH_MODE_OFFSET] = current_flash_mode;
209 patched_flash_mode = true;
210 }
211 }
212
213 if (!this->flash_write_()) {
214 return false;
215 }
216
217 // Restore original flash mode for MD5 calculation
218 if (patched_flash_mode) {
219 this->buffer_[FLASH_MODE_OFFSET] = original_flash_mode;
220 }
221
222 // Update MD5 with original (unpatched) data
223 this->md5_.add(this->buffer_.get(), this->buffer_len_);
224
225 this->current_address_ += this->buffer_len_;
226 this->buffer_len_ = 0;
227
228 return true;
229}
230
232 // Similar to write_buffer_(), but without flash mode patching or MD5 update (for final padded write)
233 if (this->buffer_len_ == 0) {
234 return true;
235 }
236
237 if (!this->erase_sector_if_needed_() || !this->flash_write_()) {
238 return false;
239 }
240
241 this->current_address_ += this->buffer_len_;
242 this->buffer_len_ = 0;
243
244 return true;
245}
246
248 // Write any remaining buffered data
249 if (this->buffer_len_ > 0) {
250 // Add actual data to MD5 before padding
251 this->md5_.add(this->buffer_.get(), this->buffer_len_);
252
253 // Pad to 4-byte alignment for flash write
254 while (this->buffer_len_ % 4 != 0) {
255 this->buffer_[this->buffer_len_++] = 0xFF;
256 }
257 if (!this->write_buffer_final_()) {
258 this->abort();
260 }
261 }
262
263 // Calculate actual bytes written (exact uploaded size, excluding flash write padding)
264 size_t actual_size = this->bytes_received_;
265
266 // Check if any data was written
267 if (actual_size == 0) {
268 ESP_LOGE(TAG, "No data written");
269 this->abort();
271 }
272
273 // Verify MD5 if set (strict mode), otherwise use lenient mode
274 // In lenient mode (no MD5), we accept whatever was written
275 if (this->md5_set_) {
276 this->md5_.calculate();
277 if (!this->md5_.equals_bytes(this->expected_md5_)) {
278 ESP_LOGE(TAG, "MD5 mismatch");
279 this->abort();
281 }
282 } else {
283 // Lenient mode: adjust size to what was actually written
284 // This matches Arduino's Update.end(true) behavior
285 this->image_size_ = actual_size;
286 }
287
288 // Verify firmware header
289 if (!this->verify_end_()) {
290 this->abort();
292 }
293
294 // Write eboot command to copy firmware on next boot
295 eboot_command ebcmd;
296 ebcmd.action = ACTION_COPY_RAW;
297 ebcmd.args[0] = this->start_address_;
298 ebcmd.args[1] = 0x00000; // Destination: start of flash
299 ebcmd.args[2] = this->image_size_;
300 eboot_command_write(&ebcmd);
301
302 ESP_LOGI(TAG, "OTA update staged: 0x%08" PRIX32 " -> 0x00000, size=%zu", this->start_address_, this->image_size_);
303
304 // Clean up
305 this->buffer_.reset();
307
308 this->lock_.hold_until_reboot();
309 return OTA_RESPONSE_OK;
310}
311
313 if (!this->lock_.is_held()) // refused in begin()
314 return;
315 this->buffer_.reset();
316 this->buffer_len_ = 0;
317 this->image_size_ = 0;
318 this->bytes_received_ = 0;
320}
321
323 uint32_t buf;
324 if (spi_flash_read(this->start_address_, &buf, 4) != SPI_FLASH_RESULT_OK) {
325 ESP_LOGE(TAG, "Failed to read firmware header");
326 return false;
327 }
328
329 uint8_t *bytes = reinterpret_cast<uint8_t *>(&buf);
330
331 // Check for GZIP (compressed firmware)
332 if (bytes[0] == GZIP_MAGIC_1 && bytes[1] == GZIP_MAGIC_2) {
333 // GZIP compressed - can't verify further
334 return true;
335 }
336
337 // Check firmware magic byte
338 if (bytes[0] != FIRMWARE_MAGIC) {
339 ESP_LOGE(TAG, "Invalid firmware magic: 0x%02X (expected 0x%02X)", bytes[0], FIRMWARE_MAGIC);
340 return false;
341 }
342
343#if !FLASH_MAP_SUPPORT
344 // Check if new firmware's flash size fits (only when auto-detection is disabled)
345 // With FLASH_MAP_SUPPORT (modern cores), flash size is auto-detected from chip
346 // NOLINTNEXTLINE(readability-static-accessed-through-instance)
347 uint32_t bin_flash_size = ESP.magicFlashChipSize((bytes[3] & 0xf0) >> 4);
348 // NOLINTNEXTLINE(readability-static-accessed-through-instance)
349 if (bin_flash_size > ESP.getFlashChipRealSize()) {
350 ESP_LOGE(TAG, "Firmware flash size (%" PRIu32 ") exceeds chip size (%" PRIu32 ")", bin_flash_size,
351 ESP.getFlashChipRealSize());
352 return false;
353 }
354#endif
355
356 return true;
357}
358
360 uint32_t data;
361 if (spi_flash_read(0x0000, &data, 4) != SPI_FLASH_RESULT_OK) {
362 return 0; // Default to QIO
363 }
364 return (reinterpret_cast<uint8_t *>(&data))[FLASH_MODE_OFFSET];
365}
366
367} // namespace esphome::ota
368#endif // USE_ESP8266
void feed_wdt()
Feed the task watchdog.
bool equals_bytes(const uint8_t *expected)
Compare the hash against a provided byte-encoded hash.
Definition hash_base.h:32
void calculate() override
Compute the digest, based on the provided data.
Definition md5.cpp:16
void add(const uint8_t *data, size_t len) override
Add bytes of data for the digest.
Definition md5.cpp:14
void init() override
Initialize a new MD5 digest computation.
Definition md5.cpp:9
bool write_buffer_final_()
Write buffered data to flash without MD5 update (for final padded write)
OTAResponseTypes write(uint8_t *data, size_t len)
std::unique_ptr< uint8_t[]> buffer_
bool erase_sector_if_needed_()
Erase flash sector if current address is at sector boundary.
bool write_buffer_()
Write buffered data to flash and update MD5.
OTAResponseTypes begin(size_t image_size, OTAType ota_type=OTA_TYPE_UPDATE_APP)
bool verify_end_()
Verify the firmware header is valid.
bool flash_write_()
Write buffer to flash (does not update address or clear buffer)
uint8_t get_flash_chip_mode_()
Get current flash chip mode from flash header.
void hold_until_reboot()
Keep the lock set until reboot; is_held() becomes false.
bool try_acquire()
Returns false if held elsewhere.
void preferences_prevent_write(bool prevent)
ESPHOME_LOG_TAG(TAG, "ota")
@ OTA_RESPONSE_ERROR_MD5_MISMATCH
Definition ota_backend.h:44
@ OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG
Definition ota_backend.h:39
@ OTA_RESPONSE_ERROR_WRITING_FLASH
Definition ota_backend.h:36
@ OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE
Definition ota_backend.h:41
@ OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE
Definition ota_backend.h:47
@ OTA_RESPONSE_ERROR_UPDATE_END
Definition ota_backend.h:37
@ OTA_RESPONSE_ERROR_UPDATE_BUSY
Definition ota_backend.h:54
@ OTA_RESPONSE_ERROR_UNKNOWN
Definition ota_backend.h:55
@ OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING
Definition ota_backend.h:38
std::unique_ptr< ArduinoLibreTinyOTABackend > make_ota_backend()
const void size_t len
Definition hal.h:68
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count)
Parse bytes from a hex-encoded string into a byte array.
Definition helpers.cpp:370
int written
Definition helpers.h:1262
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t