ESPHome  2024.12.2
nextion_upload_idf.cpp
Go to the documentation of this file.
1 #include "nextion.h"
2 
3 #ifdef USE_NEXTION_TFT_UPLOAD
4 #ifdef USE_ESP_IDF
5 
7 #include "esphome/core/defines.h"
8 #include "esphome/core/util.h"
9 #include "esphome/core/log.h"
11 #include <cinttypes>
12 #include <esp_heap_caps.h>
13 #include <esp_http_client.h>
14 
15 namespace esphome {
16 namespace nextion {
17 static const char *const TAG = "nextion.upload.idf";
18 
19 // Followed guide
20 // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
21 
22 int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &range_start) {
23  uint32_t range_size = this->tft_size_ - range_start;
24  ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size());
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_LOGD(TAG, "Range end: %" PRIu32, range_end);
29  ESP_LOGD(TAG, "Range size: %" PRIu32, range_size);
30  ESP_LOGE(TAG, "Invalid range");
31  return -1;
32  }
33 
34  char range_header[32];
35  sprintf(range_header, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end);
36  ESP_LOGV(TAG, "Requesting range: %s", range_header);
37  esp_http_client_set_header(http_client, "Range", range_header);
38  ESP_LOGV(TAG, "Opening HTTP connetion");
39  esp_err_t err = esp_http_client_open(http_client, 0);
40  if (err != ESP_OK) {
41  ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
42  return -1;
43  }
44 
45  ESP_LOGV(TAG, "Fetch content length");
46  const int chunk_size = esp_http_client_fetch_headers(http_client);
47  ESP_LOGV(TAG, "content_length = %d", chunk_size);
48  if (chunk_size <= 0) {
49  ESP_LOGE(TAG, "Failed to get chunk's content length: %d", chunk_size);
50  return -1;
51  }
52 
53  // Allocate the buffer dynamically
55  uint8_t *buffer = allocator.allocate(4096);
56  if (!buffer) {
57  ESP_LOGE(TAG, "Failed to allocate upload buffer");
58  return -1;
59  }
60 
61  std::string recv_string;
62  while (true) {
63  App.feed_wdt();
64  const uint16_t buffer_size =
65  this->content_length_ < 4096 ? this->content_length_ : 4096; // Limits buffer to the remaining data
66  ESP_LOGV(TAG, "Fetching %" PRIu16 " bytes from HTTP", buffer_size);
67  uint16_t read_len = 0;
68  int partial_read_len = 0;
69  uint8_t retries = 0;
70  // Attempt to read the chunk with retries.
71  while (retries < 5 && read_len < buffer_size) {
72  partial_read_len =
73  esp_http_client_read(http_client, reinterpret_cast<char *>(buffer) + read_len, buffer_size - read_len);
74  if (partial_read_len > 0) {
75  read_len += partial_read_len; // Accumulate the total read length.
76  // Reset retries on successful read.
77  retries = 0;
78  } else {
79  // If no data was read, increment retries.
80  retries++;
81  vTaskDelay(pdMS_TO_TICKS(2)); // NOLINT
82  }
83  App.feed_wdt(); // Feed the watchdog timer.
84  }
85  if (read_len != buffer_size) {
86  // Did not receive the full package within the timeout period
87  ESP_LOGE(TAG, "Failed to read full package, received only %" PRIu16 " of %" PRIu16 " bytes", read_len,
88  buffer_size);
89  // Deallocate buffer
90  allocator.deallocate(buffer, 4096);
91  buffer = nullptr;
92  return -1;
93  }
94  ESP_LOGV(TAG, "%d bytes fetched, writing it to UART", read_len);
95  if (read_len > 0) {
96  recv_string.clear();
97  this->write_array(buffer, buffer_size);
98  App.feed_wdt();
99  this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true);
100  this->content_length_ -= read_len;
101  const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
102 #ifdef USE_PSRAM
103  ESP_LOGD(
104  TAG,
105  "Uploaded %0.2f%%, remaining %" PRIu32 " bytes, free heap: %" PRIu32 " (DRAM) + %" PRIu32 " (PSRAM) bytes",
106  upload_percentage, this->content_length_, static_cast<uint32_t>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
107  static_cast<uint32_t>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)));
108 #else
109  ESP_LOGD(TAG, "Uploaded %0.2f%%, remaining %" PRIu32 " bytes, free heap: %" PRIu32 " bytes", upload_percentage,
110  this->content_length_, static_cast<uint32_t>(esp_get_free_heap_size()));
111 #endif
113  if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request
114  ESP_LOGD(TAG, "recv_string [%s]",
115  format_hex_pretty(reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()).c_str());
116  uint32_t result = 0;
117  for (int j = 0; j < 4; ++j) {
118  result += static_cast<uint8_t>(recv_string[j + 1]) << (8 * j);
119  }
120  if (result > 0) {
121  ESP_LOGI(TAG, "Nextion reported new range %" PRIu32, result);
122  this->content_length_ = this->tft_size_ - result;
123  range_start = result;
124  } else {
125  range_start = range_end + 1;
126  }
127  // Deallocate buffer
128  allocator.deallocate(buffer, 4096);
129  buffer = nullptr;
130  return range_end + 1;
131  } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok"
132  ESP_LOGE(TAG, "Invalid response from Nextion: [%s]",
133  format_hex_pretty(reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()).c_str());
134  // Deallocate buffer
135  allocator.deallocate(buffer, 4096);
136  buffer = nullptr;
137  return -1;
138  }
139 
140  recv_string.clear();
141  } else if (read_len == 0) {
142  ESP_LOGV(TAG, "End of HTTP response reached");
143  break; // Exit the loop if there is no more data to read
144  } else {
145  ESP_LOGE(TAG, "Failed to read from HTTP client, error code: %" PRIu16, read_len);
146  break; // Exit the loop on error
147  }
148  }
149  range_start = range_end + 1;
150  // Deallocate buffer
151  allocator.deallocate(buffer, 4096);
152  buffer = nullptr;
153  return range_end + 1;
154 }
155 
156 bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
157  ESP_LOGD(TAG, "Nextion TFT upload requested");
158  ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse));
159  ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str());
160 
161  if (this->is_updating_) {
162  ESP_LOGW(TAG, "Currently uploading");
163  return false;
164  }
165 
166  if (!network::is_connected()) {
167  ESP_LOGE(TAG, "Network is not connected");
168  return false;
169  }
170 
171  this->is_updating_ = true;
172 
173  if (exit_reparse) {
174  ESP_LOGD(TAG, "Exiting Nextion reparse mode");
175  if (!this->set_protocol_reparse_mode(false)) {
176  ESP_LOGW(TAG, "Failed to request Nextion to exit reparse mode");
177  return false;
178  }
179  }
180 
181  // Check if baud rate is supported
182  this->original_baud_rate_ = this->parent_->get_baud_rate();
183  static const std::vector<uint32_t> SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600,
184  115200, 230400, 250000, 256000, 512000, 921600};
185  if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) {
186  baud_rate = this->original_baud_rate_;
187  }
188  ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
189 
190  // Define the configuration for the HTTP client
191  ESP_LOGV(TAG, "Initializing HTTP client");
192  ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size());
193  esp_http_client_config_t config = {
194  .url = this->tft_url_.c_str(),
195  .cert_pem = nullptr,
196  .method = HTTP_METHOD_HEAD,
197  .timeout_ms = 15000,
198  .disable_auto_redirect = false,
199  .max_redirection_count = 10,
200  };
201  // Initialize the HTTP client with the configuration
202  esp_http_client_handle_t http_client = esp_http_client_init(&config);
203  if (!http_client) {
204  ESP_LOGE(TAG, "Failed to initialize HTTP client.");
205  return this->upload_end_(false);
206  }
207 
208  esp_err_t err = esp_http_client_set_header(http_client, "Connection", "keep-alive");
209  if (err != ESP_OK) {
210  ESP_LOGE(TAG, "HTTP set header failed: %s", esp_err_to_name(err));
211  esp_http_client_cleanup(http_client);
212  return this->upload_end_(false);
213  }
214 
215  // Perform the HTTP request
216  ESP_LOGV(TAG, "Check if the client could connect");
217  ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size());
218  err = esp_http_client_perform(http_client);
219  if (err != ESP_OK) {
220  ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err));
221  esp_http_client_cleanup(http_client);
222  return this->upload_end_(false);
223  }
224 
225  // Check the HTTP Status Code
226  ESP_LOGV(TAG, "Check the HTTP Status Code");
227  ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size());
228  int status_code = esp_http_client_get_status_code(http_client);
229  if (status_code != 200 && status_code != 206) {
230  return this->upload_end_(false);
231  }
232 
233  this->tft_size_ = esp_http_client_get_content_length(http_client);
234 
235  ESP_LOGD(TAG, "TFT file size: %zu bytes", this->tft_size_);
236  if (this->tft_size_ < 4096 || this->tft_size_ > 134217728) {
237  ESP_LOGE(TAG, "File size check failed.");
238  ESP_LOGD(TAG, "Close HTTP connection");
239  esp_http_client_close(http_client);
240  esp_http_client_cleanup(http_client);
241  ESP_LOGV(TAG, "Connection closed");
242  return this->upload_end_(false);
243  } else {
244  ESP_LOGV(TAG, "File size check passed. Proceeding...");
245  }
246  this->content_length_ = this->tft_size_;
247 
248  ESP_LOGD(TAG, "Uploading Nextion");
249 
250  // The Nextion will ignore the upload command if it is sleeping
251  ESP_LOGV(TAG, "Wake-up Nextion");
252  this->ignore_is_setup_ = true;
253  this->send_command_("sleep=0");
254  this->send_command_("dim=100");
255  vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT
256  ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size());
257 
258  App.feed_wdt();
259  char command[128];
260  // Tells the Nextion the content length of the tft file and baud rate it will be sent at
261  // Once the Nextion accepts the command it will wait until the file is successfully uploaded
262  // If it fails for any reason a power cycle of the display will be needed
263  sprintf(command, "whmi-wris %" PRIu32 ",%" PRIu32 ",1", this->content_length_, baud_rate);
264 
265  // Clear serial receive buffer
266  ESP_LOGV(TAG, "Clear serial receive buffer");
267  this->reset_(false);
268  vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT
269  ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size());
270 
271  ESP_LOGV(TAG, "Send upload instruction: %s", command);
272  this->send_command_(command);
273 
274  if (baud_rate != this->original_baud_rate_) {
275  ESP_LOGD(TAG, "Changing baud rate from %" PRIu32 " to %" PRIu32 " bps", this->original_baud_rate_, baud_rate);
276  this->parent_->set_baud_rate(baud_rate);
277  this->parent_->load_settings();
278  }
279 
280  std::string response;
281  ESP_LOGV(TAG, "Waiting for upgrade response");
282  this->recv_ret_string_(response, 5000, true); // This can take some time to return
283 
284  // The Nextion display will, if it's ready to accept data, send a 0x05 byte.
285  ESP_LOGD(TAG, "Upgrade response is [%s] - %zu byte(s)",
286  format_hex_pretty(reinterpret_cast<const uint8_t *>(response.data()), response.size()).c_str(),
287  response.length());
288  ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size());
289 
290  if (response.find(0x05) != std::string::npos) {
291  ESP_LOGV(TAG, "Preparation for TFT upload done");
292  } else {
293  ESP_LOGE(TAG, "Preparation for TFT upload failed %d \"%s\"", response[0], response.c_str());
294  ESP_LOGD(TAG, "Close HTTP connection");
295  esp_http_client_close(http_client);
296  esp_http_client_cleanup(http_client);
297  ESP_LOGV(TAG, "Connection closed");
298  return this->upload_end_(false);
299  }
300 
301  ESP_LOGV(TAG, "Change the method to GET before starting the download");
302  esp_err_t set_method_result = esp_http_client_set_method(http_client, HTTP_METHOD_GET);
303  if (set_method_result != ESP_OK) {
304  ESP_LOGE(TAG, "Failed to set HTTP method to GET: %s", esp_err_to_name(set_method_result));
305  return this->upload_end_(false);
306  }
307 
308  ESP_LOGD(TAG, "Uploading TFT to Nextion:");
309  ESP_LOGD(TAG, " URL: %s", this->tft_url_.c_str());
310  ESP_LOGD(TAG, " File size: %" PRIu32 " bytes", this->content_length_);
311  ESP_LOGD(TAG, " Free heap: %" PRIu32, esp_get_free_heap_size());
312 
313  // Proceed with the content download as before
314 
315  ESP_LOGV(TAG, "Starting transfer by chunks loop");
316 
317  uint32_t position = 0;
318  while (this->content_length_ > 0) {
319  int upload_result = upload_by_chunks_(http_client, position);
320  if (upload_result < 0) {
321  ESP_LOGE(TAG, "Error uploading TFT to Nextion!");
322  ESP_LOGD(TAG, "Close HTTP connection");
323  esp_http_client_close(http_client);
324  esp_http_client_cleanup(http_client);
325  ESP_LOGV(TAG, "Connection closed");
326  return this->upload_end_(false);
327  }
328  App.feed_wdt();
329  ESP_LOGV(TAG, "Free heap: %" PRIu32 ", Bytes left: %" PRIu32, esp_get_free_heap_size(), this->content_length_);
330  }
331 
332  ESP_LOGD(TAG, "Successfully uploaded TFT to Nextion!");
333 
334  ESP_LOGD(TAG, "Close HTTP connection");
335  esp_http_client_close(http_client);
336  esp_http_client_cleanup(http_client);
337  ESP_LOGV(TAG, "Connection closed");
338  return this->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 } // namespace nextion
364 } // namespace esphome
365 
366 #endif // USE_ESP_IDF
367 #endif // USE_NEXTION_TFT_UPLOAD
bool ignore_is_setup_
Sends commands ignoring of the Nextion has been setup.
Definition: nextion.h:1232
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:369
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.
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
T * allocate(size_t n)
Definition: helpers.h:690
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
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:1279
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
void deallocate(T *p, size_t n)
Definition: helpers.h:709
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
An STL allocator that uses SPI or internal RAM.
Definition: helpers.h:675
uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag)
Definition: nextion.cpp:919
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:26