ESPHome  2024.4.0
nextion_upload_arduino.cpp
Go to the documentation of this file.
1 #include "nextion.h"
2 
3 #ifdef ARDUINO
4 #ifdef USE_NEXTION_TFT_UPLOAD
5 
7 #include "esphome/core/defines.h"
8 #include "esphome/core/util.h"
9 #include "esphome/core/log.h"
11 
12 #ifdef USE_ESP32
13 #include <esp_heap_caps.h>
14 #endif
15 
16 namespace esphome {
17 namespace nextion {
18 static const char *const TAG = "nextion.upload.arduino";
19 
20 // Followed guide
21 // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
22 
23 int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) {
24  int range_end = 0;
25 
26  if (range_start == 0 && this->transfer_buffer_size_ > 16384) { // Start small at the first run in case of a big skip
27  range_end = 16384 - 1;
28  } else {
29  range_end = range_start + this->transfer_buffer_size_ - 1;
30  }
31 
32  if (range_end > this->tft_size_)
33  range_end = this->tft_size_;
34 
35 #ifdef USE_ESP8266
36 #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0)
37  http->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
38 #elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
39  http->setFollowRedirects(true);
40 #endif
41 #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
42  http->setRedirectLimit(3);
43 #endif
44 #endif // USE_ESP8266
45 
46  char range_header[64];
47  sprintf(range_header, "bytes=%d-%d", range_start, range_end);
48 
49  ESP_LOGD(TAG, "Requesting range: %s", range_header);
50 
51  int tries = 1;
52  int code = 0;
53  bool begin_status = false;
54  while (tries <= 5) {
55 #ifdef USE_ESP32
56  begin_status = http->begin(this->tft_url_.c_str());
57 #endif
58 #ifdef USE_ESP8266
59  begin_status = http->begin(*this->get_wifi_client_(), this->tft_url_.c_str());
60 #endif
61 
62  ++tries;
63  if (!begin_status) {
64  ESP_LOGD(TAG, "upload_by_chunks_: connection failed");
65  delay(500); // NOLINT
66  continue;
67  }
68 
69  http->addHeader("Range", range_header);
70 
71  code = http->GET();
72  if (code == 200 || code == 206) {
73  break;
74  }
75  ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s, retries(%d/5)", this->tft_url_.c_str(),
76  HTTPClient::errorToString(code).c_str(), tries);
77  http->end();
78  App.feed_wdt();
79  delay(500); // NOLINT
80  }
81 
82  if (tries > 5) {
83  return -1;
84  }
85 
86  std::string recv_string;
87  size_t size = 0;
88  int fetched = 0;
89  int range = range_end - range_start;
90 
91  while (fetched < range) {
92  size = http->getStreamPtr()->available();
93  if (!size) {
94  App.feed_wdt();
95  delay(0);
96  continue;
97  }
98  int c = http->getStreamPtr()->readBytes(
99  &this->transfer_buffer_[fetched], ((size > this->transfer_buffer_size_) ? this->transfer_buffer_size_ : size));
100  fetched += c;
101  }
102  http->end();
103  ESP_LOGN(TAG, "Fetched %d of %d bytes", fetched, this->content_length_);
104 
105  // upload fetched segments to the display in 4KB chunks
106  int write_len;
107  for (int i = 0; i < range; i += 4096) {
108  App.feed_wdt();
109  write_len = this->content_length_ < 4096 ? this->content_length_ : 4096;
110  this->write_array(&this->transfer_buffer_[i], write_len);
111  this->content_length_ -= write_len;
112  ESP_LOGD(TAG, "Uploaded %0.2f %%; %d bytes remaining",
113  100.0 * (this->tft_size_ - this->content_length_) / this->tft_size_, this->content_length_);
114 
115  if (!this->upload_first_chunk_sent_) {
116  this->upload_first_chunk_sent_ = true;
117  delay(500); // NOLINT
118  }
119 
120  this->recv_ret_string_(recv_string, 4096, true);
121  if (recv_string[0] != 0x05) { // 0x05 == "ok"
122  ESP_LOGD(TAG, "recv_string [%s]",
123  format_hex_pretty(reinterpret_cast<const uint8_t *>(recv_string.data()), recv_string.size()).c_str());
124  }
125 
126  // handle partial upload request
127  if (recv_string[0] == 0x08 && recv_string.size() == 5) {
128  uint32_t result = 0;
129  for (int j = 0; j < 4; ++j) {
130  result += static_cast<uint8_t>(recv_string[j + 1]) << (8 * j);
131  }
132  if (result > 0) {
133  ESP_LOGD(TAG, "Nextion reported new range %d", result);
134  this->content_length_ = this->tft_size_ - result;
135  return result;
136  }
137  }
138  recv_string.clear();
139  }
140 
141  return range_end + 1;
142 }
143 
145  ESP_LOGD(TAG, "Nextion TFT upload requested");
146  ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str());
147 
148  if (this->is_updating_) {
149  ESP_LOGD(TAG, "Currently updating");
150  return false;
151  }
152 
153  if (!network::is_connected()) {
154  ESP_LOGD(TAG, "network is not connected");
155  return false;
156  }
157 
158  this->is_updating_ = true;
159 
160  HTTPClient http;
161  http.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along
162  bool begin_status = false;
163 #ifdef USE_ESP32
164  begin_status = http.begin(this->tft_url_.c_str());
165 #endif
166 #ifdef USE_ESP8266
167 #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0)
168  http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
169 #elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
170  http.setFollowRedirects(true);
171 #endif
172 #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
173  http.setRedirectLimit(3);
174 #endif
175  begin_status = http.begin(*this->get_wifi_client_(), this->tft_url_.c_str());
176 #endif
177 
178  if (!begin_status) {
179  this->is_updating_ = false;
180  ESP_LOGD(TAG, "Connection failed");
182  allocator.deallocate(this->transfer_buffer_, this->transfer_buffer_size_);
183  return false;
184  } else {
185  ESP_LOGD(TAG, "Connected");
186  }
187 
188  http.addHeader("Range", "bytes=0-255");
189  const char *header_names[] = {"Content-Range"};
190  http.collectHeaders(header_names, 1);
191  ESP_LOGD(TAG, "Requesting URL: %s", this->tft_url_.c_str());
192 
193  http.setReuse(true);
194  // try up to 5 times. DNS sometimes needs a second try or so
195  int tries = 1;
196  int code = http.GET();
197  delay(100); // NOLINT
198 
199  App.feed_wdt();
200  while (code != 200 && code != 206 && tries <= 5) {
201  ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s, retrying (%d/5)", this->tft_url_.c_str(),
202  HTTPClient::errorToString(code).c_str(), tries);
203 
204  delay(250); // NOLINT
205  App.feed_wdt();
206  code = http.GET();
207  ++tries;
208  }
209 
210  if ((code != 200 && code != 206) || tries > 5) {
211  return this->upload_end_(false);
212  }
213 
214  String content_range_string = http.header("Content-Range");
215  content_range_string.remove(0, 12);
216  this->content_length_ = content_range_string.toInt();
217  this->tft_size_ = content_length_;
218  http.end();
219 
220  if (this->content_length_ < 4096) {
221  ESP_LOGE(TAG, "Failed to get file size");
222  return this->upload_end_(false);
223  }
224 
225  ESP_LOGD(TAG, "Updating Nextion %s...", this->device_model_.c_str());
226  // The Nextion will ignore the update command if it is sleeping
227 
228  this->send_command_("sleep=0");
229  this->set_backlight_brightness(1.0);
230  delay(250); // NOLINT
231 
232  App.feed_wdt();
233 
234  char command[128];
235  // Tells the Nextion the content length of the tft file and baud rate it will be sent at
236  // Once the Nextion accepts the command it will wait until the file is successfully uploaded
237  // If it fails for any reason a power cycle of the display will be needed
238  sprintf(command, "whmi-wris %d,%d,1", this->content_length_, this->parent_->get_baud_rate());
239 
240  // Clear serial receive buffer
241  uint8_t d;
242  while (this->available()) {
243  this->read_byte(&d);
244  };
245 
246  this->send_command_(command);
247 
248  App.feed_wdt();
249 
250  std::string response;
251  ESP_LOGD(TAG, "Waiting for upgrade response");
252  this->recv_ret_string_(response, 2000, true); // This can take some time to return
253 
254  // The Nextion display will, if it's ready to accept data, send a 0x05 byte.
255  ESP_LOGD(TAG, "Upgrade response is [%s] - %zu bytes",
256  format_hex_pretty(reinterpret_cast<const uint8_t *>(response.data()), response.size()).c_str(),
257  response.length());
258 
259  for (size_t i = 0; i < response.length(); i++) {
260  ESP_LOGD(TAG, "Available %d : 0x%02X", i, response[i]);
261  }
262 
263  if (response.find(0x05) != std::string::npos) {
264  ESP_LOGD(TAG, "preparation for tft update done");
265  } else {
266  ESP_LOGD(TAG, "preparation for tft update failed %d \"%s\"", response[0], response.c_str());
267  return this->upload_end_(false);
268  }
269 
270  // Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096
271 #ifdef USE_ESP32
272  uint32_t chunk_size = 8192;
273  if (heap_caps_get_free_size(MALLOC_CAP_SPIRAM) > 0) {
274  chunk_size = this->content_length_;
275  } else {
276  if (ESP.getFreeHeap() > 81920) { // Ensure some FreeHeap to other things and limit chunk size
277  chunk_size = ESP.getFreeHeap() - 65536;
278  chunk_size = int(chunk_size / 4096) * 4096;
279  chunk_size = chunk_size > 65536 ? 65536 : chunk_size;
280  } else if (ESP.getFreeHeap() < 32768) {
281  chunk_size = 4096;
282  }
283  }
284 #else
285  // NOLINTNEXTLINE(readability-static-accessed-through-instance)
286  uint32_t chunk_size = ESP.getFreeHeap() < 16384 ? 4096 : 8192;
287 #endif
288 
289  if (this->transfer_buffer_ == nullptr) {
291  // NOLINTNEXTLINE(readability-static-accessed-through-instance)
292  ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap());
293  this->transfer_buffer_ = allocator.allocate(chunk_size);
294  if (this->transfer_buffer_ == nullptr) { // Try a smaller size
295  ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size);
296  chunk_size = 4096;
297  ESP_LOGD(TAG, "Allocating %d buffer", chunk_size);
298  this->transfer_buffer_ = allocator.allocate(chunk_size);
299 
300  if (!this->transfer_buffer_)
301  return this->upload_end_(false);
302  }
303 
304  this->transfer_buffer_size_ = chunk_size;
305  }
306 
307  // NOLINTNEXTLINE(readability-static-accessed-through-instance)
308  ESP_LOGD(TAG, "Updating tft from \"%s\" with a file size of %d using %zu chunksize, Heap Size %d",
309  this->tft_url_.c_str(), this->content_length_, this->transfer_buffer_size_, ESP.getFreeHeap());
310 
311  int result = 0;
312  while (this->content_length_ > 0) {
313  result = this->upload_by_chunks_(&http, result);
314  if (result < 0) {
315  ESP_LOGD(TAG, "Error updating Nextion!");
316  return this->upload_end_(false);
317  }
318  App.feed_wdt();
319  // NOLINTNEXTLINE(readability-static-accessed-through-instance)
320  ESP_LOGD(TAG, "Heap Size %d, Bytes left %d", ESP.getFreeHeap(), this->content_length_);
321  }
322  ESP_LOGD(TAG, "Successfully updated Nextion!");
323 
324  return this->upload_end_(true);
325 }
326 
327 bool Nextion::upload_end_(bool successful) {
328  this->is_updating_ = false;
329  ESP_LOGD(TAG, "Restarting Nextion");
330  this->soft_reset();
331  if (successful) {
332  delay(1500); // NOLINT
333  ESP_LOGD(TAG, "Restarting esphome");
334  ESP.restart(); // NOLINT(readability-static-accessed-through-instance)
335  }
336  return successful;
337 }
338 
339 #ifdef USE_ESP8266
341  if (this->tft_url_.compare(0, 6, "https:") == 0) {
342  if (this->wifi_client_secure_ == nullptr) {
343  // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
344  this->wifi_client_secure_ = new BearSSL::WiFiClientSecure();
345  this->wifi_client_secure_->setInsecure();
346  this->wifi_client_secure_->setBufferSizes(512, 512);
347  }
348  return this->wifi_client_secure_;
349  }
350 
351  if (this->wifi_client_ == nullptr) {
352  // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
353  this->wifi_client_ = new WiFiClient();
354  }
355  return this->wifi_client_;
356 }
357 #endif
358 } // namespace nextion
359 } // namespace esphome
360 
361 #endif // USE_NEXTION_TFT_UPLOAD
362 #endif // ARDUINO
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:361
void write_array(const uint8_t *data, size_t len)
Definition: uart.h:21
An STL allocator that uses SPI RAM.
Definition: helpers.h:645
void deallocate(T *p, size_t n)
Definition: helpers.h:672
int upload_by_chunks_(HTTPClient *http, int range_start)
will request chunk_size chunks from the web server and send each to the nextion
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:1072
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
bool read_byte(uint8_t *data)
Definition: uart.h:29
Application App
Global storage of Application pointer - only one Application can exist.
uint8_t * transfer_buffer_
Definition: nextion.h:1151
void set_backlight_brightness(float brightness)
Set the brightness of the backlight.
std::string device_model_
Definition: nextion.h:1142
void soft_reset()
Softreset the Nextion.
bool upload_tft()
Upload the tft file and soft reset Nextion.
WiFiClient * wifi_client_
Definition: nextion.h:1071
This is a workaround until we can figure out a way to get the tflite-micro idf component code availab...
Definition: a01nyub.cpp:7
uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag)
Definition: nextion.cpp:890
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:26