ESPHome  2024.10.2
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_ARDUINO
5 
7 #include "esphome/core/defines.h"
8 #include "esphome/core/util.h"
9 #include "esphome/core/log.h"
11 #include <cinttypes>
12 
13 #ifdef USE_ESP32
14 #include <esp_heap_caps.h>
15 #endif
16 
17 namespace esphome {
18 namespace nextion {
19 static const char *const TAG = "nextion.upload.arduino";
20 
21 // Followed guide
22 // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
23 
24 inline uint32_t Nextion::get_free_heap_() {
25 #if defined(USE_ESP32)
26  return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
27 #elif defined(USE_ESP8266)
28  return EspClass::getFreeHeap();
29 #endif // USE_ESP32 vs USE_ESP8266
30 }
31 
32 int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) {
33  uint32_t range_size = this->tft_size_ - range_start;
34  ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_());
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_LOGD(TAG, "Range end: %" PRIu32, range_end);
39  ESP_LOGD(TAG, "Range size: %" PRIu32, range_size);
40  ESP_LOGE(TAG, "Invalid range");
41  return -1;
42  }
43 
44  char range_header[32];
45  sprintf(range_header, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
46  ESP_LOGV(TAG, "Requesting range: %s", range_header);
47  http_client.addHeader("Range", range_header);
48  int code = http_client.GET();
49  if (code != HTTP_CODE_OK and code != HTTP_CODE_PARTIAL_CONTENT) {
50  ESP_LOGW(TAG, "HTTP Request failed; Error: %s", HTTPClient::errorToString(code).c_str());
51  return -1;
52  }
53 
54  // Allocate the buffer dynamically
56  uint8_t *buffer = allocator.allocate(4096);
57  if (!buffer) {
58  ESP_LOGE(TAG, "Failed to allocate upload buffer");
59  return -1;
60  }
61 
62  std::string recv_string;
63  while (true) {
64  App.feed_wdt();
65  const uint16_t buffer_size =
66  this->content_length_ < 4096 ? this->content_length_ : 4096; // Limits buffer to the remaining data
67  ESP_LOGV(TAG, "Fetching %" PRIu16 " bytes from HTTP", buffer_size);
68  uint16_t read_len = 0;
69  int partial_read_len = 0;
70  const uint32_t start_time = millis();
71  while (read_len < buffer_size && millis() - start_time < 5000) {
72  if (http_client.getStreamPtr()->available() > 0) {
73  partial_read_len =
74  http_client.getStreamPtr()->readBytes(reinterpret_cast<char *>(buffer) + read_len, buffer_size - read_len);
75  read_len += partial_read_len;
76  if (partial_read_len > 0) {
77  App.feed_wdt();
78  delay(2);
79  }
80  }
81  }
82  if (read_len != buffer_size) {
83  // Did not receive the full package within the timeout period
84  ESP_LOGE(TAG, "Failed to read full package, received only %" PRIu16 " of %" PRIu16 " bytes", read_len,
85  buffer_size);
86  // Deallocate buffer
87  allocator.deallocate(buffer, 4096);
88  buffer = nullptr;
89  return -1;
90  }
91  ESP_LOGV(TAG, "%d bytes fetched, writing it to UART", read_len);
92  if (read_len > 0) {
93  recv_string.clear();
94  this->write_array(buffer, buffer_size);
95  App.feed_wdt();
96  this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true);
97  this->content_length_ -= read_len;
98  const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
99 #if defined(USE_ESP32) && defined(USE_PSRAM)
100  ESP_LOGD(
101  TAG,
102  "Uploaded %0.2f%%, remaining %" PRIu32 " bytes, free heap: %" PRIu32 " (DRAM) + %" PRIu32 " (PSRAM) bytes",
103  upload_percentage, this->content_length_, static_cast<uint32_t>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
104  static_cast<uint32_t>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)));
105 #else
106  ESP_LOGD(TAG, "Uploaded %0.2f%%, remaining %" PRIu32 " bytes, free heap: %" PRIu32 " bytes", upload_percentage,
107  this->content_length_, this->get_free_heap_());
108 #endif
110  if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request
111  ESP_LOGD(TAG, "recv_string [%s]",
112  format_hex_pretty(reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()).c_str());
113  uint32_t result = 0;
114  for (int j = 0; j < 4; ++j) {
115  result += static_cast<uint8_t>(recv_string[j + 1]) << (8 * j);
116  }
117  if (result > 0) {
118  ESP_LOGI(TAG, "Nextion reported new range %" PRIu32, result);
119  this->content_length_ = this->tft_size_ - result;
120  range_start = result;
121  } else {
122  range_start = range_end + 1;
123  }
124  // Deallocate buffer
125  allocator.deallocate(buffer, 4096);
126  buffer = nullptr;
127  return range_end + 1;
128  } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok"
129  ESP_LOGE(TAG, "Invalid response from Nextion: [%s]",
130  format_hex_pretty(reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()).c_str());
131  // Deallocate buffer
132  allocator.deallocate(buffer, 4096);
133  buffer = nullptr;
134  return -1;
135  }
136 
137  recv_string.clear();
138  } else if (read_len == 0) {
139  ESP_LOGV(TAG, "End of HTTP response reached");
140  break; // Exit the loop if there is no more data to read
141  } else {
142  ESP_LOGE(TAG, "Failed to read from HTTP client, error code: %d", read_len);
143  break; // Exit the loop on error
144  }
145  }
146  range_start = range_end + 1;
147  // Deallocate buffer
148  allocator.deallocate(buffer, 4096);
149  buffer = nullptr;
150  return range_end + 1;
151 }
152 
153 bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
154  ESP_LOGD(TAG, "Nextion TFT upload requested");
155  ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse));
156  ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str());
157 
158  if (this->is_updating_) {
159  ESP_LOGW(TAG, "Currently uploading");
160  return false;
161  }
162 
163  if (!network::is_connected()) {
164  ESP_LOGE(TAG, "Network is not connected");
165  return false;
166  }
167 
168  this->is_updating_ = true;
169 
170  if (exit_reparse) {
171  ESP_LOGD(TAG, "Exiting Nextion reparse mode");
172  if (!this->set_protocol_reparse_mode(false)) {
173  ESP_LOGW(TAG, "Failed to request Nextion to exit reparse mode");
174  return false;
175  }
176  }
177 
178  // Check if baud rate is supported
179  this->original_baud_rate_ = this->parent_->get_baud_rate();
180  static const std::vector<uint32_t> SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600,
181  115200, 230400, 250000, 256000, 512000, 921600};
182  if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) {
183  baud_rate = this->original_baud_rate_;
184  }
185  ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
186 
187  // Define the configuration for the HTTP client
188  ESP_LOGV(TAG, "Initializing HTTP client");
189  ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_());
190  HTTPClient http_client;
191  http_client.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along
192 
193  bool begin_status = false;
194 #ifdef USE_ESP32
195  begin_status = http_client.begin(this->tft_url_.c_str());
196 #endif
197 #ifdef USE_ESP8266
198 #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0)
199  http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
200 #elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
201  http_client.setFollowRedirects(true);
202 #endif
203 #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
204  http_client.setRedirectLimit(3);
205 #endif
206  begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str());
207 #endif // USE_ESP8266
208  if (!begin_status) {
209  this->is_updating_ = false;
210  ESP_LOGD(TAG, "Connection failed");
211  return false;
212  } else {
213  ESP_LOGD(TAG, "Connected");
214  }
215  http_client.addHeader("Range", "bytes=0-255");
216  const char *header_names[] = {"Content-Range"};
217  http_client.collectHeaders(header_names, 1);
218  ESP_LOGD(TAG, "Requesting URL: %s", this->tft_url_.c_str());
219  http_client.setReuse(true);
220  // try up to 5 times. DNS sometimes needs a second try or so
221  int tries = 1;
222  int code = http_client.GET();
223  delay(100); // NOLINT
224 
225  App.feed_wdt();
226  while (code != 200 && code != 206 && tries <= 5) {
227  ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s, retrying (%d/5)", this->tft_url_.c_str(),
228  HTTPClient::errorToString(code).c_str(), tries);
229 
230  delay(250); // NOLINT
231  App.feed_wdt();
232  code = http_client.GET();
233  ++tries;
234  }
235 
236  if (code != 200 and code != 206) {
237  return this->upload_end_(false);
238  }
239 
240  String content_range_string = http_client.header("Content-Range");
241  content_range_string.remove(0, 12);
242  this->tft_size_ = content_range_string.toInt();
243 
244  ESP_LOGD(TAG, "TFT file size: %zu bytes", this->tft_size_);
245  if (this->tft_size_ < 4096) {
246  ESP_LOGE(TAG, "File size check failed.");
247  ESP_LOGD(TAG, "Close HTTP connection");
248  http_client.end();
249  ESP_LOGV(TAG, "Connection closed");
250  return this->upload_end_(false);
251  } else {
252  ESP_LOGV(TAG, "File size check passed. Proceeding...");
253  }
254  this->content_length_ = this->tft_size_;
255 
256  ESP_LOGD(TAG, "Uploading Nextion");
257 
258  // The Nextion will ignore the upload command if it is sleeping
259  ESP_LOGV(TAG, "Wake-up Nextion");
260  this->ignore_is_setup_ = true;
261  this->send_command_("sleep=0");
262  this->send_command_("dim=100");
263  delay(250); // NOLINT
264  ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_());
265 
266  App.feed_wdt();
267  char command[128];
268  // Tells the Nextion the content length of the tft file and baud rate it will be sent at
269  // Once the Nextion accepts the command it will wait until the file is successfully uploaded
270  // If it fails for any reason a power cycle of the display will be needed
271  sprintf(command, "whmi-wris %d,%d,1", this->content_length_, baud_rate);
272 
273  // Clear serial receive buffer
274  ESP_LOGV(TAG, "Clear serial receive buffer");
275  this->reset_(false);
276  delay(250); // NOLINT
277  ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_());
278 
279  ESP_LOGV(TAG, "Send upload instruction: %s", command);
280  this->send_command_(command);
281 
282  if (baud_rate != this->original_baud_rate_) {
283  ESP_LOGD(TAG, "Changing baud rate from %" PRIu32 " to %" PRIu32 " bps", this->original_baud_rate_, baud_rate);
284  this->parent_->set_baud_rate(baud_rate);
285  this->parent_->load_settings();
286  }
287 
288  App.feed_wdt();
289 
290  std::string response;
291  ESP_LOGV(TAG, "Waiting for upgrade response");
292  this->recv_ret_string_(response, 5000, true); // This can take some time to return
293 
294  // The Nextion display will, if it's ready to accept data, send a 0x05 byte.
295  ESP_LOGD(TAG, "Upgrade response is [%s] - %zu byte(s)",
296  format_hex_pretty(reinterpret_cast<const uint8_t *>(response.data()), response.size()).c_str(),
297  response.length());
298  ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_());
299 
300  if (response.find(0x05) != std::string::npos) {
301  ESP_LOGV(TAG, "Preparation for TFT upload done");
302  } else {
303  ESP_LOGE(TAG, "Preparation for TFT upload failed %d \"%s\"", response[0], response.c_str());
304  ESP_LOGD(TAG, "Close HTTP connection");
305  http_client.end();
306  ESP_LOGV(TAG, "Connection closed");
307  return this->upload_end_(false);
308  }
309 
310  ESP_LOGD(TAG, "Uploading TFT to Nextion:");
311  ESP_LOGD(TAG, " URL: %s", this->tft_url_.c_str());
312  ESP_LOGD(TAG, " File size: %d bytes", this->content_length_);
313  ESP_LOGD(TAG, " Free heap: %" PRIu32, this->get_free_heap_());
314 
315  // Proceed with the content download as before
316 
317  ESP_LOGV(TAG, "Starting transfer by chunks loop");
318 
319  uint32_t position = 0;
320  while (this->content_length_ > 0) {
321  int upload_result = upload_by_chunks_(http_client, position);
322  if (upload_result < 0) {
323  ESP_LOGE(TAG, "Error uploading TFT to Nextion!");
324  ESP_LOGD(TAG, "Close HTTP connection");
325  http_client.end();
326  ESP_LOGV(TAG, "Connection closed");
327  return this->upload_end_(false);
328  }
329  App.feed_wdt();
330  ESP_LOGV(TAG, "Free heap: %" PRIu32 ", Bytes left: %" PRIu32, this->get_free_heap_(), this->content_length_);
331  }
332 
333  ESP_LOGD(TAG, "Successfully uploaded TFT to Nextion!");
334 
335  ESP_LOGD(TAG, "Close HTTP connection");
336  http_client.end();
337  ESP_LOGV(TAG, "Connection closed");
338  return upload_end_(true);
339 }
340 
341 bool Nextion::upload_end_(bool successful) {
342  ESP_LOGD(TAG, "Nextion TFT upload finished: %s", YESNO(successful));
343  this->is_updating_ = false;
344  this->ignore_is_setup_ = false;
345 
346  uint32_t baud_rate = this->parent_->get_baud_rate();
347  if (baud_rate != this->original_baud_rate_) {
348  ESP_LOGD(TAG, "Changing baud rate back from %" PRIu32 " to %" PRIu32 " bps", baud_rate, this->original_baud_rate_);
350  this->parent_->load_settings();
351  }
352 
353  if (successful) {
354  ESP_LOGD(TAG, "Restarting ESPHome");
355  delay(1500); // NOLINT
356  arch_restart();
357  } else {
358  ESP_LOGE(TAG, "Nextion TFT upload failed");
359  }
360  return successful;
361 }
362 
363 #ifdef USE_ESP8266
365  if (this->tft_url_.compare(0, 6, "https:") == 0) {
366  if (this->wifi_client_secure_ == nullptr) {
367  // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
368  this->wifi_client_secure_ = new BearSSL::WiFiClientSecure();
369  this->wifi_client_secure_->setInsecure();
370  this->wifi_client_secure_->setBufferSizes(512, 512);
371  }
372  return this->wifi_client_secure_;
373  }
374 
375  if (this->wifi_client_ == nullptr) {
376  // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
377  this->wifi_client_ = new WiFiClient();
378  }
379  return this->wifi_client_;
380 }
381 #endif // USE_ESP8266
382 
383 } // namespace nextion
384 } // namespace esphome
385 
386 #endif // USE_ARDUINO
387 #endif // USE_NEXTION_TFT_UPLOAD
bool ignore_is_setup_
Sends commands ignoring of the Nextion has been setup.
Definition: nextion.h:1226
uint32_t get_baud_rate() const
std::string format_hex_pretty(const uint8_t *data, size_t length)
Format the byte array data of length len in pretty-printed, human-readable hex.
Definition: helpers.cpp:359
void write_array(const uint8_t *data, size_t len)
Definition: uart.h:21
bool upload_tft(uint32_t baud_rate=0, bool exit_reparse=true)
Uploads the TFT file to the Nextion display.
uint32_t get_free_heap_()
Returns the ESP Free Heap memory.
void deallocate(T *p, size_t n)
Definition: helpers.h:686
bool send_command_(const std::string &command)
Manually send a raw command to the display and don&#39;t wait for an acknowledgement packet.
Definition: nextion.cpp:29
BearSSL::WiFiClientSecure * wifi_client_secure_
Definition: nextion.h:1267
bool upload_end_(bool successful)
Ends the upload process, restart Nextion and, if successful, restarts ESP.
UARTComponent * parent_
Definition: uart.h:68
bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition: util.cpp:15
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
virtual void load_settings(bool dump_config)
Load the UART settings.
int upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start)
will request chunk_size chunks from the web server and send each to the nextion
bool set_protocol_reparse_mode(bool active_mode)
Sets the Nextion display&#39;s protocol reparse mode.
uint32_t original_baud_rate_
Definition: nextion.h:1273
Application App
Global storage of Application pointer - only one Application can exist.
void set_baud_rate(uint32_t baud_rate)
void arch_restart()
Definition: core.cpp:29
WiFiClient * wifi_client_
Definition: nextion.h:1266
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
void reset_(bool reset_nextion=true)
Definition: nextion.cpp:127
float position
Definition: cover.h:14
uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag)
Definition: nextion.cpp:916
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:26