ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
nextion_upload_arduino.cpp
Go to the documentation of this file.
1#include "nextion.h"
2
3#ifdef USE_NEXTION_TFT_UPLOAD
4#ifdef USE_ESP8266
5
6#include <cinttypes>
11#include "esphome/core/log.h"
12#include "esphome/core/util.h"
13
14namespace esphome::nextion {
15
16static const char *const TAG = "nextion.upload.arduino";
17static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
18
19// Timeout for display acknowledgment during TFT upload (ms).
20// A single value is used for all chunks; the happy path returns as soon as
21// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field
22// reports showed the previous 500ms steady-state value was too tight for
23// some firmware variants.
24static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000;
25
26// Followed guide
27// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
28
29int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) {
30 uint32_t range_size = this->tft_size_ - range_start;
31 ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap());
32 uint32_t range_end = ((this->upload_first_chunk_sent_ || this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1;
33 ESP_LOGD(TAG, "Range start: %" PRIu32, range_start);
34 if (range_size <= 0 || range_end <= range_start) {
35 ESP_LOGE(TAG, "Invalid range end: %" PRIu32 ", size: %" PRIu32, range_end, range_size);
36 return -1;
37 }
38
39 char range_header[32];
40 buf_append_printf(range_header, sizeof(range_header), 0, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
41 ESP_LOGV(TAG, "Range: %s", range_header);
42 http_client.addHeader("Range", range_header);
43 int code = http_client.GET();
44 if (code != HTTP_CODE_OK && code != HTTP_CODE_PARTIAL_CONTENT) {
45 ESP_LOGW(TAG, "HTTP failed: %s", HTTPClient::errorToString(code).c_str());
46 return -1;
47 }
48
49 // Allocate the buffer dynamically
50 RAMAllocator<uint8_t> allocator;
51 uint8_t *buffer = allocator.allocate(4096);
52 if (!buffer) {
53 ESP_LOGE(TAG, "Buffer alloc failed");
54 return -1;
55 }
56
57 std::string recv_string;
58 while (true) {
59 App.feed_wdt();
60 const uint16_t buffer_size =
61 this->content_length_ < 4096 ? this->content_length_ : 4096; // Limits buffer to the remaining data
62 ESP_LOGV(TAG, "Fetch %" PRIu16 " bytes", buffer_size);
63 uint16_t read_len = 0;
64 int partial_read_len = 0;
65 const uint32_t start_time = App.get_loop_component_start_time();
66 while (read_len < buffer_size && App.get_loop_component_start_time() - start_time < 5000) {
67 if (http_client.getStreamPtr()->available() > 0) {
68 partial_read_len =
69 http_client.getStreamPtr()->readBytes(reinterpret_cast<char *>(buffer) + read_len, buffer_size - read_len);
70 read_len += partial_read_len;
71 if (partial_read_len > 0) {
72 App.feed_wdt();
73 delay(2);
74 }
75 }
76 }
77 if (read_len != buffer_size) {
78 // Did not receive the full package within the timeout period
79 ESP_LOGE(TAG, "Read failed: %" PRIu16 "/%" PRIu16 " bytes", read_len, buffer_size);
80 // Deallocate buffer
81 allocator.deallocate(buffer, 4096);
82 buffer = nullptr;
83 return -1;
84 }
85 ESP_LOGV(TAG, "Fetched %d bytes", read_len);
86 if (read_len > 0) {
87 recv_string.clear();
88 this->write_array(buffer, buffer_size);
89 App.feed_wdt();
90 this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true);
91
92 // Some Nextion firmware variants (notably bootloader/recovery mode on panels
93 // with no installed TFT) emit the 5-byte 0x08+position fast-mode ack with a
94 // multi-second gap between the leading 0x08 byte and the 4 trailing position
95 // bytes. recv_ret_string_ returns after the first byte; manually drain the
96 // trailing bytes from the UART before continuing.
97 if (!recv_string.empty() && recv_string[0] == 0x08 && recv_string.size() < 5) {
98 const uint32_t deadline = millis() + NEXTION_UPLOAD_ACK_TIMEOUT_MS;
99 while (recv_string.size() < 5 && millis() < deadline) {
100 if (this->available()) {
101 uint8_t b = 0;
102 if (this->read_byte(&b)) {
103 recv_string.push_back(static_cast<char>(b));
104 }
105 } else {
106 delay(5); // NOLINT
107 App.feed_wdt();
108 }
109 }
110 if (recv_string.size() < 5) {
111 ESP_LOGE(TAG, "Truncated 0x08 response: got %zu bytes within %" PRIu32 "ms", recv_string.size(),
112 NEXTION_UPLOAD_ACK_TIMEOUT_MS);
113 allocator.deallocate(buffer, 4096);
114 buffer = nullptr;
115 return -1;
116 }
117 }
118 this->content_length_ -= read_len;
119 const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
120 ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_,
121 EspClass::getFreeHeap());
122 this->upload_first_chunk_sent_ = true;
123 if (recv_string.empty()) {
124 ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS);
125 allocator.deallocate(buffer, 4096);
126 buffer = nullptr;
127 return -1;
128 }
129 if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request
130 char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)];
131 ESP_LOGD(
132 TAG, "Recv: [%s]",
133 format_hex_pretty_to(hex_buf, reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()));
134 uint32_t result = 0;
135 for (int j = 0; j < 4; ++j) {
136 result += static_cast<uint8_t>(recv_string[j + 1]) << (8 * j);
137 }
138 if (result > 0) {
139 ESP_LOGI(TAG, "New range: %" PRIu32, result);
140 this->content_length_ = this->tft_size_ - result;
141 range_start = result;
142 } else {
143 range_start = range_end + 1;
144 }
145 // Deallocate buffer
146 allocator.deallocate(buffer, 4096);
147 buffer = nullptr;
148 return range_end + 1;
149 } else if (recv_string[0] != 0x05 && recv_string[0] != 0x08) { // 0x05 == "ok"
150 char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)];
151 ESP_LOGE(
152 TAG, "Invalid response: [%s]",
153 format_hex_pretty_to(hex_buf, reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()));
154 // Deallocate buffer
155 allocator.deallocate(buffer, 4096);
156 buffer = nullptr;
157 return -1;
158 }
159
160 recv_string.clear();
161 } else if (read_len == 0) {
162 ESP_LOGV(TAG, "HTTP end");
163 break; // Exit the loop if there is no more data to read
164 } else {
165 ESP_LOGE(TAG, "HTTP read failed: %d", read_len);
166 break; // Exit the loop on error
167 }
168 }
169 range_start = range_end + 1;
170 // Deallocate buffer
171 allocator.deallocate(buffer, 4096);
172 buffer = nullptr;
173 return range_end + 1;
174}
175
176bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
177 ESP_LOGD(TAG, "TFT upload requested, exit reparse: %s, URL: %s", YESNO(exit_reparse), this->tft_url_.c_str());
178
179 if (this->connection_state_.is_updating_) {
180 ESP_LOGW(TAG, "Upload in progress");
181 return false;
182 }
183
184 if (!network::is_connected()) {
185 ESP_LOGE(TAG, "No network");
186 return false;
187 }
188
189 this->connection_state_.is_updating_ = true;
190
191 if (exit_reparse) {
192 ESP_LOGD(TAG, "Exit reparse mode");
193 if (!this->set_protocol_reparse_mode(false)) {
194 ESP_LOGW(TAG, "Exit reparse failed");
195 return false;
196 }
197 }
198
199 // Check if baud rate is supported
201 if (baud_rate <= 0) {
202 baud_rate = this->original_baud_rate_;
203 }
204 ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
205
206 // Define the configuration for the HTTP client
207 ESP_LOGV(TAG, "Init HTTP client, heap: %" PRIu32, EspClass::getFreeHeap());
208 HTTPClient http_client;
209 http_client.setTimeout(this->tft_upload_http_timeout_);
210
211 bool begin_status = false;
212#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0)
213 http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
214#elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
215 http_client.setFollowRedirects(true);
216#endif
217#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
218 http_client.setRedirectLimit(3);
219#endif
220 begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str());
221 if (!begin_status) {
222 this->connection_state_.is_updating_ = false;
223 ESP_LOGD(TAG, "Connection failed");
224 return false;
225 } else {
226 ESP_LOGD(TAG, "Connected");
227 }
228 http_client.addHeader("Range", "bytes=0-255");
229 const char *header_names[] = {"Content-Range"};
230 http_client.collectHeaders(header_names, 1);
231 ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str());
232 http_client.setReuse(true);
233
234 int tries = 1;
235 int code = http_client.GET();
236 delay(100); // NOLINT
237
238 App.feed_wdt();
239 while (code != 200 && code != 206 && tries <= this->tft_upload_http_retries_) {
240 ESP_LOGW(TAG, "HTTP fail: URL: %s; Error: %s, retry %d/%u", this->tft_url_.c_str(),
241 HTTPClient::errorToString(code).c_str(), tries, this->tft_upload_http_retries_);
242
243 delay(250); // NOLINT
244 App.feed_wdt();
245 code = http_client.GET();
246 ++tries;
247 }
248
249 if (code != 200 && code != 206) {
250 ESP_LOGE(TAG, "HTTP request failed with status %d", code);
251 return this->upload_end_(false);
252 }
253
254 String content_range_string = http_client.header("Content-Range");
255 content_range_string.remove(0, 12);
256 this->tft_size_ = content_range_string.toInt();
257
258 ESP_LOGD(TAG, "TFT size: %zu bytes", this->tft_size_);
259 if (this->tft_size_ < 4096) {
260 ESP_LOGE(TAG, "Size check failed");
261 ESP_LOGD(TAG, "Close HTTP");
262 http_client.end();
263 ESP_LOGV(TAG, "Connection closed");
264 return this->upload_end_(false);
265 } else {
266 ESP_LOGV(TAG, "Size check OK");
267 }
268 this->content_length_ = this->tft_size_;
269
270 ESP_LOGD(TAG, "Uploading");
271
272 // The Nextion will ignore the upload command if it is sleeping
273 ESP_LOGV(TAG, "Wake-up");
274 this->connection_state_.ignore_is_setup_ = true;
275 this->send_command_("sleep=0");
276 this->send_command_("dim=100");
277 delay(250); // NOLINT
278 ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap());
279
280 App.feed_wdt();
281 char command[128];
282 // Tells the Nextion the content length of the tft file and baud rate it will be sent at
283 // Once the Nextion accepts the command it will wait until the file is successfully uploaded
284 // If it fails for any reason a power cycle of the display will be needed
285 snprintf(command, sizeof(command), "whmi-wris %" PRIu32 ",%" PRIu32 ",1", this->content_length_, baud_rate);
286
287 // Clear serial receive buffer
288 ESP_LOGV(TAG, "Clear RX buffer");
289 this->reset_(false);
290 delay(250); // NOLINT
291
292 ESP_LOGV(TAG, "Heap: %" PRIu32 ", upload cmd: %s", EspClass::getFreeHeap(), command);
293 this->send_command_(command);
294
295 if (baud_rate != this->original_baud_rate_) {
296 ESP_LOGD(TAG, "Baud: %" PRIu32 "->%" PRIu32, this->original_baud_rate_, baud_rate);
297 this->parent_->set_baud_rate(baud_rate);
298 this->parent_->load_settings();
299 }
300
301 App.feed_wdt();
302
303 std::string response;
304 ESP_LOGV(TAG, "Wait upload resp");
305 this->recv_ret_string_(response, 5000, true); // This can take some time to return
306
307 // The Nextion display will, if it's ready to accept data, send a 0x05 byte.
308 char hex_buf[format_hex_pretty_size(NEXTION_MAX_RESPONSE_LOG_BYTES)];
309 ESP_LOGD(TAG, "Upload resp: [%s] %zu B",
310 format_hex_pretty_to(hex_buf, reinterpret_cast<const uint8_t *>(response.data()), response.size()),
311 response.length());
312 ESP_LOGV(TAG, "Heap: %" PRIu32, EspClass::getFreeHeap());
313
314 if (response.find(0x05) != std::string::npos) {
315 ESP_LOGV(TAG, "Upload prep done");
316 } else {
317 ESP_LOGE(TAG, "Prep failed %d '%s'", response[0], response.c_str());
318 ESP_LOGD(TAG, "Close HTTP");
319 http_client.end();
320 ESP_LOGV(TAG, "Connection closed");
321 return this->upload_end_(false);
322 }
323
324 ESP_LOGD(TAG,
325 "Upload TFT:\n"
326 " URL: %s\n"
327 " Size: %d bytes\n"
328 " Heap: %" PRIu32,
329 this->tft_url_.c_str(), this->content_length_, EspClass::getFreeHeap());
330
331 // Proceed with the content download as before
332
333 ESP_LOGV(TAG, "Start chunk transfer");
334
335 uint32_t position = 0;
336 while (this->content_length_ > 0) {
337 int upload_result = upload_by_chunks_(http_client, position);
338 if (upload_result < 0) {
339 ESP_LOGE(TAG, "Upload error");
340 ESP_LOGD(TAG, "Close HTTP");
341 http_client.end();
342 ESP_LOGV(TAG, "Connection closed");
343 return this->upload_end_(false);
344 }
345 App.feed_wdt();
346 ESP_LOGV(TAG, "Heap: %" PRIu32 " left: %" PRIu32, EspClass::getFreeHeap(), this->content_length_);
347 }
348
349 ESP_LOGD(TAG, "Upload complete");
350
351 ESP_LOGV(TAG, "Close HTTP");
352 http_client.end();
353 ESP_LOGV(TAG, "Connection closed");
354 return upload_end_(true);
355}
356
358 if (this->tft_url_.starts_with("https:")) {
359 if (this->wifi_client_secure_ == nullptr) {
360 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
361 this->wifi_client_secure_ = new BearSSL::WiFiClientSecure();
362 this->wifi_client_secure_->setInsecure();
363 this->wifi_client_secure_->setBufferSizes(512, 512);
364 }
365 return this->wifi_client_secure_;
366 }
367
368 if (this->wifi_client_ == nullptr) {
369 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
370 this->wifi_client_ = new WiFiClient();
371 }
372 return this->wifi_client_;
373}
374
375} // namespace esphome::nextion
376
377#endif // USE_ESP8266
378#endif // USE_NEXTION_TFT_UPLOAD
void feed_wdt()
Feed the task watchdog.
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.
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2073
void deallocate(T *p, size_t n)
Definition helpers.h:2130
T * allocate(size_t n)
Definition helpers.h:2100
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
WiFiClient * wifi_client_
Definition nextion.h:1546
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.
BearSSL::WiFiClientSecure * wifi_client_secure_
Definition nextion.h:1547
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)
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:340
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:1400
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