ESPHome 2026.8.0b4
Loading...
Searching...
No Matches
nextion_upload_esp32.cpp
Go to the documentation of this file.
1#include "nextion.h"
2
3#ifdef USE_NEXTION_TFT_UPLOAD
4#ifdef USE_ESP32
5
6#include <esp_heap_caps.h>
7#include <esp_http_client.h>
8#include <cinttypes>
14#include "esphome/core/log.h"
15#include "esphome/core/util.h"
16
17namespace esphome::nextion {
18
19static const char *const TAG = "nextion.upload.esp32";
20static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
21
22// Timeout for display acknowledgment during TFT upload (ms).
23// A single value is used for all chunks; the happy path returns as soon as
24// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field
25// reports showed the previous 500ms steady-state value was too tight for
26// some firmware variants.
27static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000;
28
29// Followed guide
30// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
31
32int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &range_start) {
33 uint32_t range_size = this->tft_size_ - range_start;
34 ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size());
35 uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1;
36 ESP_LOGD(TAG, "Range start: %" PRIu32, range_start);
37 if (range_size <= 0 or range_end <= range_start) {
38 ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size);
39 return -1;
40 }
41
42 char range_header[32];
43 buf_append_printf(range_header, sizeof(range_header), 0, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
44 ESP_LOGV(TAG, "Range: %s", range_header);
45 esp_http_client_set_header(http_client, "Range", range_header);
46 ESP_LOGV(TAG, "Open HTTP");
47 int chunk_size = -1;
48 int status_code = -1;
49 esp_err_t last_err = ESP_FAIL;
50 for (uint8_t attempt = 0; attempt < this->tft_upload_http_retries_; attempt++) {
51 status_code = -1;
52 last_err = esp_http_client_open(http_client, 0);
53 if (last_err == ESP_OK) {
54 ESP_LOGV(TAG, "Fetch length");
55 chunk_size = esp_http_client_fetch_headers(http_client);
56 ESP_LOGV(TAG, "Length: %d", chunk_size);
57 if (chunk_size >= 0) {
58 status_code = esp_http_client_get_status_code(http_client);
59 // Accept the requested range (206 with exact length) or a full-body 200
60 // only from offset 0; elsewhere a 200 replays the file and corrupts the display.
61 if (chunk_size > 0 &&
62 ((status_code == 206 && chunk_size == static_cast<int>(range_end - range_start + 1)) ||
63 (status_code == 200 && range_start == 0 && chunk_size == static_cast<int>(this->tft_size_)))) {
64 break;
65 }
66 if (status_code == 200) {
67 if (range_start == 0) {
68 ESP_LOGE(TAG, "Unexpected length for 200 response: %d (expected %d)", chunk_size,
69 static_cast<int>(this->tft_size_));
70 } else {
71 // A server that ignored the range once will ignore it again
72 ESP_LOGE(TAG, "Server does not support range requests (got 200 at offset %" PRIu32 ")", range_start);
73 }
74 chunk_size = -1;
75 last_err = ESP_FAIL;
76 break;
77 }
78 ESP_LOGW(TAG, "Bad response: status %d, length %d (expected %" PRIu32 ")", status_code, chunk_size,
79 range_end - range_start + 1);
80 chunk_size = -1;
81 last_err = ESP_FAIL;
82 // A 4xx (except timeout/rate-limit) won't improve on retry
83 if (status_code >= 400 && status_code < 500 && status_code != 408 && status_code != 429) {
84 break;
85 }
86 } else {
87 ESP_LOGW(TAG, "Get length failed: %d", chunk_size);
88 last_err = ESP_FAIL;
89 }
90 } else {
91 ESP_LOGW(TAG, "HTTP open failed: %s", esp_err_to_name(last_err));
92 }
93 // The server may have dropped the keep-alive connection while the display
94 // was busy processing a chunk; close so the next attempt reconnects.
95 esp_http_client_close(http_client);
96 vTaskDelay(pdMS_TO_TICKS(2)); // NOLINT
97 App.feed_wdt();
98 }
99 if (chunk_size <= 0) {
100 ESP_LOGE(TAG, "HTTP request failed, last status: %d, last error: %s", status_code, esp_err_to_name(last_err));
101 return -1;
102 }
103
104 // Allocate the buffer dynamically
105 RAMAllocator<uint8_t> allocator;
106 uint8_t *buffer = allocator.allocate(4096);
107 if (!buffer) {
108 ESP_LOGE(TAG, "Buffer alloc failed");
109 return -1;
110 }
111
112 std::string recv_string;
113 while (true) {
114 App.feed_wdt();
115 const uint16_t buffer_size =
116 this->content_length_ < 4096 ? this->content_length_ : 4096; // Limits buffer to the remaining data
117 ESP_LOGV(TAG, "Fetch %" PRIu16 " bytes", buffer_size);
118 uint16_t read_len = 0;
119 int partial_read_len = 0;
120 uint8_t retries = 0;
121 // Attempt to read the chunk with retries.
122 while (retries < this->tft_upload_http_retries_ && read_len < buffer_size) {
123 partial_read_len =
124 esp_http_client_read(http_client, reinterpret_cast<char *>(buffer) + read_len, buffer_size - read_len);
125 if (partial_read_len > 0) {
126 read_len += partial_read_len; // Accumulate the total read length.
127 // Reset retries on successful read.
128 retries = 0;
129 } else {
130 // If no data was read, increment retries.
131 retries++;
132 vTaskDelay(pdMS_TO_TICKS(2)); // NOLINT
133 }
134 App.feed_wdt(); // Feed the watchdog timer.
135 }
136 if (read_len != buffer_size) {
137 // Did not receive the full package within the timeout period
138 ESP_LOGE(TAG, "Read failed: %" PRIu16 "/%" PRIu16 " bytes", read_len, buffer_size);
139 // Deallocate buffer
140 allocator.deallocate(buffer, 4096);
141 buffer = nullptr;
142 return -1;
143 }
144 ESP_LOGV(TAG, "Fetched %d bytes", read_len);
145 if (read_len > 0) {
146 recv_string.clear();
147 this->write_array(buffer, buffer_size);
148 App.feed_wdt();
149 this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true);
150
151 // Some Nextion firmware variants (notably bootloader/recovery mode on panels
152 // with no installed TFT) emit the 5-byte 0x08+position fast-mode ack with a
153 // multi-second gap between the leading 0x08 byte and the 4 trailing position
154 // bytes. recv_ret_string_ returns after the first byte; manually drain the
155 // trailing bytes from the UART before continuing.
156 if (!recv_string.empty() && recv_string[0] == 0x08 && recv_string.size() < 5) {
157 const uint32_t deadline = millis() + NEXTION_UPLOAD_ACK_TIMEOUT_MS;
158 while (recv_string.size() < 5 && millis() < deadline) {
159 if (this->available()) {
160 uint8_t b = 0;
161 if (this->read_byte(&b)) {
162 recv_string.push_back(static_cast<char>(b));
163 }
164 } else {
165 vTaskDelay(pdMS_TO_TICKS(5)); // NOLINT
166 App.feed_wdt();
167 }
168 }
169 if (recv_string.size() < 5) {
170 ESP_LOGE(TAG, "Truncated 0x08 response: got %zu bytes within %" PRIu32 "ms", recv_string.size(),
171 NEXTION_UPLOAD_ACK_TIMEOUT_MS);
172 allocator.deallocate(buffer, 4096);
173 buffer = nullptr;
174 return -1;
175 }
176 }
177 this->content_length_ -= read_len;
178 const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
179#ifdef USE_PSRAM
180 ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 "+%" PRIu32 ")", upload_percentage,
181 this->content_length_, static_cast<uint32_t>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
182 static_cast<uint32_t>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)));
183#else
184 ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_,
185 static_cast<uint32_t>(esp_get_free_heap_size()));
186#endif
188 if (recv_string.empty()) {
189 ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS);
190 allocator.deallocate(buffer, 4096);
191 buffer = nullptr;
192 return -1;
193 }
194 if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request
195 char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)];
196 ESP_LOGD(
197 TAG, "Recv: [%s]",
198 format_hex_pretty_to(hex_buf, reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()));
199 uint32_t result = 0;
200 for (int j = 0; j < 4; ++j) {
201 result += static_cast<uint8_t>(recv_string[j + 1]) << (8 * j);
202 }
203 if (result > 0) {
204 ESP_LOGI(TAG, "New range: %" PRIu32, result);
205 this->content_length_ = this->tft_size_ - result;
206 range_start = result;
207 } else {
208 range_start = range_end + 1;
209 }
210 // The response body may be only partially read; close so the next
211 // range request starts on a clean connection.
212 esp_http_client_close(http_client);
213 // Deallocate buffer
214 allocator.deallocate(buffer, 4096);
215 buffer = nullptr;
216 return range_end + 1;
217 } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok"
218 char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)];
219 ESP_LOGE(
220 TAG, "Invalid response: [%s]",
221 format_hex_pretty_to(hex_buf, reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()));
222 // Deallocate buffer
223 allocator.deallocate(buffer, 4096);
224 buffer = nullptr;
225 return -1;
226 }
227
228 recv_string.clear();
229 } else if (read_len == 0) {
230 ESP_LOGV(TAG, "HTTP end");
231 break; // Exit the loop if there is no more data to read
232 } else {
233 ESP_LOGE(TAG, "HTTP read failed: %" PRIu16, read_len);
234 break; // Exit the loop on error
235 }
236 }
237 range_start = range_end + 1;
238 // Deallocate buffer
239 allocator.deallocate(buffer, 4096);
240 buffer = nullptr;
241 return range_end + 1;
242}
243
244bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
245 ESP_LOGD(TAG, "TFT upload requested, exit reparse: %s, URL: %s", YESNO(exit_reparse), this->tft_url_.c_str());
246
247 if (this->connection_state_.is_updating_) {
248 ESP_LOGW(TAG, "Upload in progress");
249 return false;
250 }
251
252 if (!network::is_connected()) {
253 ESP_LOGE(TAG, "No network");
254 return false;
255 }
256
257 // Temporarily adjust watchdog timeout for the duration of the TFT upload
258 watchdog::WatchdogManager wdm(this->tft_upload_watchdog_timeout_);
259
260 this->connection_state_.is_updating_ = true;
261
262 if (exit_reparse) {
263 ESP_LOGD(TAG, "Exit reparse mode");
264 if (!this->set_protocol_reparse_mode(false)) {
265 ESP_LOGW(TAG, "Exit reparse failed");
266 return false;
267 }
268 }
269
270 // Check if baud rate is supported
272 if (baud_rate <= 0) {
273 baud_rate = this->original_baud_rate_;
274 }
275 ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
276
277 // Define the configuration for the HTTP client
278 ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, esp_get_free_heap_size());
279 esp_http_client_config_t config = {
280 .url = this->tft_url_.c_str(),
281 .cert_pem = nullptr,
282 .method = HTTP_METHOD_HEAD,
283 .timeout_ms = static_cast<int>(this->tft_upload_http_timeout_),
284 .disable_auto_redirect = false,
285 .max_redirection_count = 10,
286 };
287 // Initialize the HTTP client with the configuration
288 esp_http_client_handle_t http_client = esp_http_client_init(&config);
289 if (!http_client) {
290 ESP_LOGE(TAG, "HTTP init failed");
291 return this->upload_end_(false);
292 }
293
294 esp_err_t err = esp_http_client_set_header(http_client, "Connection", "keep-alive");
295 if (err != ESP_OK) {
296 ESP_LOGE(TAG, "Set header failed: %s", esp_err_to_name(err));
297 esp_http_client_cleanup(http_client);
298 return this->upload_end_(false);
299 }
300
301 // Perform the HTTP request
302 ESP_LOGV(TAG, "Check connection, heap: %" PRIu32, esp_get_free_heap_size());
303 err = esp_http_client_perform(http_client);
304 if (err != ESP_OK) {
305 ESP_LOGE(TAG, "HTTP failed: %s", esp_err_to_name(err));
306 esp_http_client_cleanup(http_client);
307 return this->upload_end_(false);
308 }
309
310 // Check the HTTP Status Code
311 ESP_LOGV(TAG, "Check status, heap: %" PRIu32, esp_get_free_heap_size());
312 int status_code = esp_http_client_get_status_code(http_client);
313 if (status_code != 200 && status_code != 206) {
314 ESP_LOGE(TAG, "HTTP request failed with status %d", status_code);
315 return this->upload_end_(false);
316 }
317
318 this->tft_size_ = esp_http_client_get_content_length(http_client);
319
320 ESP_LOGD(TAG, "TFT size: %zu bytes", this->tft_size_);
321 if (this->tft_size_ < 4096 || this->tft_size_ > 134217728) {
322 ESP_LOGE(TAG, "Size check failed");
323 ESP_LOGD(TAG, "Close HTTP");
324 esp_http_client_close(http_client);
325 esp_http_client_cleanup(http_client);
326 ESP_LOGV(TAG, "Connection closed");
327 return this->upload_end_(false);
328 } else {
329 ESP_LOGV(TAG, "Size check OK");
330 }
331 this->content_length_ = this->tft_size_;
332
333 ESP_LOGD(TAG, "Uploading");
334
335 // The Nextion will ignore the upload command if it is sleeping
336 ESP_LOGV(TAG, "Wake-up");
337 this->connection_state_.ignore_is_setup_ = true;
338 this->send_command_("sleep=0");
339 this->send_command_("dim=100");
340 vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT
341 ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size());
342
343 App.feed_wdt();
344 char command[128];
345 // Tells the Nextion the content length of the tft file and baud rate it will be sent at
346 // Once the Nextion accepts the command it will wait until the file is successfully uploaded
347 // If it fails for any reason a power cycle of the display will be needed
348 snprintf(command, sizeof(command), "whmi-wris %" PRIu32 ",%" PRIu32 ",1", this->content_length_, baud_rate);
349
350 // Clear serial receive buffer
351 ESP_LOGV(TAG, "Clear RX buffer");
352 this->reset_(false);
353 vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT
354 ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size());
355
356 ESP_LOGV(TAG, "Upload cmd: %s", command);
357 this->send_command_(command);
358
359 if (baud_rate != this->original_baud_rate_) {
360 ESP_LOGD(TAG, "Baud: %" PRIu32 "->%" PRIu32, this->original_baud_rate_, baud_rate);
361 this->parent_->set_baud_rate(baud_rate);
362 this->parent_->load_settings();
363 }
364
365 std::string response;
366 ESP_LOGV(TAG, "Wait upload resp");
367 this->recv_ret_string_(response, 5000, true); // This can take some time to return
368
369 // The Nextion display will, if it's ready to accept data, send a 0x05 byte.
370 char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)];
371 ESP_LOGD(TAG, "Upload resp: [%s] %zu B",
372 format_hex_pretty_to(hex_buf, reinterpret_cast<const uint8_t *>(response.data()), response.size()),
373 response.length());
374 ESP_LOGV(TAG, "Heap: %" PRIu32, esp_get_free_heap_size());
375
376 if (response.find(0x05) != std::string::npos) {
377 ESP_LOGV(TAG, "Upload prep done");
378 } else {
379 ESP_LOGE(TAG, "Upload prep failed %d '%s'", response[0], response.c_str());
380 ESP_LOGD(TAG, "Close HTTP");
381 esp_http_client_close(http_client);
382 esp_http_client_cleanup(http_client);
383 ESP_LOGV(TAG, "Connection closed");
384 return this->upload_end_(false);
385 }
386
387 ESP_LOGV(TAG, "Set method to GET");
388 esp_err_t set_method_result = esp_http_client_set_method(http_client, HTTP_METHOD_GET);
389 if (set_method_result != ESP_OK) {
390 ESP_LOGE(TAG, "Set GET failed: %s", esp_err_to_name(set_method_result));
391 return this->upload_end_(false);
392 }
393
394 ESP_LOGD(TAG,
395 "Uploading TFT:\n"
396 " URL: %s\n"
397 " Size: %" PRIu32 " bytes\n"
398 " Heap: %" PRIu32,
399 this->tft_url_.c_str(), this->content_length_, esp_get_free_heap_size());
400
401 // Proceed with the content download as before
402
403 ESP_LOGV(TAG, "Start chunk transfer");
404
405 uint32_t position = 0;
406 while (this->content_length_ > 0) {
407 int upload_result = upload_by_chunks_(http_client, position);
408 if (upload_result < 0) {
409 ESP_LOGE(TAG, "TFT upload error");
410 ESP_LOGD(TAG, "Close HTTP");
411 esp_http_client_close(http_client);
412 esp_http_client_cleanup(http_client);
413 ESP_LOGV(TAG, "Connection closed");
414 return this->upload_end_(false);
415 }
416 App.feed_wdt();
417 ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, esp_get_free_heap_size(), this->content_length_);
418 }
419
420 ESP_LOGD(TAG, "TFT upload complete, closing HTTP");
421 esp_http_client_close(http_client);
422 esp_http_client_cleanup(http_client);
423 ESP_LOGV(TAG, "Connection closed");
424 return this->upload_end_(true);
425}
426
427} // namespace esphome::nextion
428
429#endif // USE_ESP32
430#endif // USE_NEXTION_TFT_UPLOAD
void feed_wdt()
Feed the task watchdog.
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2099
void deallocate(T *p, size_t n)
Definition helpers.h:2156
T * allocate(size_t n)
Definition helpers.h:2126
int upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &range_start)
will request 4096 bytes chunks from the web server and send each to Nextion
bool send_command_(const std::string &command)
Manually send a raw command to the display and don't wait for an acknowledgement packet.
uint8_t tft_upload_http_retries_
HTTP retry count (default: 5)
Definition nextion.h:1556
bool upload_tft(uint32_t baud_rate=0, bool exit_reparse=true)
Uploads the TFT file to the Nextion display.
uint16_t tft_upload_http_timeout_
HTTP timeout in ms (default: 4.5s)
Definition nextion.h:1555
bool set_protocol_reparse_mode(bool active_mode)
Sets the Nextion display's protocol reparse mode.
bool upload_end_(bool successful)
Ends the upload process, restart Nextion and, if successful, restarts ESP.
uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag)
struct esphome::nextion::Nextion::@147 connection_state_
Status flags for Nextion display state management.
void reset_(bool reset_nextion=true)
uint32_t tft_upload_watchdog_timeout_
WDT timeout in ms (0 = no adjustment)
Definition nextion.h:1559
void set_baud_rate(uint32_t baud_rate)
virtual void load_settings(bool dump_config)=0
Load the UART settings.
UARTComponent * parent_
Definition uart.h:73
bool read_byte(uint8_t *data)
Definition uart.h:34
void write_array(const uint8_t *data, size_t len)
Definition uart.h:26
float position
Definition cover.h:0
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:28
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:406
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1426
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