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