#include "nextion.h" #ifdef USE_NEXTION_TFT_UPLOAD #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/util.h" #include "esphome/core/log.h" #include "esphome/components/network/util.h" #include #ifdef USE_ESP32 #include #ifdef USE_ESP_IDF #include #endif // USE_ESP_IDF #endif namespace esphome { namespace nextion { static const char *const TAG = "nextion.upload"; // Followed guide // https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 inline uint32_t Nextion::get_free_heap_() { #if defined(USE_ESP32) return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); #elif defined(USE_ESP8266) return EspClass::getFreeHeap(); #endif // USE_ESP32 vs USE_ESP8266 } #ifdef ARDUINO int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { #else // ESP-IDF int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &range_start) { #endif // ARDUINO vs ESP-IDF uint32_t range_size = this->tft_size_ - range_start; ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_()); uint32_t range_end = ((upload_first_chunk_sent_ or this->tft_size_ < 4096) ? this->tft_size_ : 4096) - 1; ESP_LOGD(TAG, "Range start: %" PRIu32, range_start); if (range_size <= 0 or range_end <= range_start) { ESP_LOGD(TAG, "Range end: %" PRIu32, range_end); ESP_LOGD(TAG, "Range size: %" PRIu32, range_size); ESP_LOGE(TAG, "Invalid range"); return -1; } char range_header[32]; sprintf(range_header, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end); ESP_LOGV(TAG, "Requesting range: %s", range_header); #ifdef ARDUINO http_client.addHeader("Range", range_header); int code = http_client.GET(); if (code != HTTP_CODE_OK and code != HTTP_CODE_PARTIAL_CONTENT) { ESP_LOGW(TAG, "HTTP Request failed; Error: %s", HTTPClient::errorToString(code).c_str()); return -1; } #else // ESP-IDF esp_http_client_set_header(http_client, "Range", range_header); ESP_LOGV(TAG, "Opening HTTP connetion"); esp_err_t err; if ((err = esp_http_client_open(http_client, 0)) != ESP_OK) { ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); return -1; } ESP_LOGV(TAG, "Fetch content length"); const int chunk_size = esp_http_client_fetch_headers(http_client); ESP_LOGV(TAG, "content_length = %d", chunk_size); if (chunk_size <= 0) { ESP_LOGE(TAG, "Failed to get chunk's content length: %d", chunk_size); return -1; } #endif // ARDUINO vs ESP-IDF // Allocate the buffer dynamically ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); uint8_t *buffer = allocator.allocate(4096); if (!buffer) { ESP_LOGE(TAG, "Failed to allocate upload buffer"); return -1; } std::string recv_string; while (true) { App.feed_wdt(); const uint16_t buffer_size = this->content_length_ < 4096 ? this->content_length_ : 4096; // Limits buffer to the remaining data ESP_LOGV(TAG, "Fetching %" PRIu16 " bytes from HTTP", buffer_size); uint16_t read_len = 0; int partial_read_len = 0; #ifdef ARDUINO const uint32_t start_time = millis(); while (read_len < buffer_size && millis() - start_time < 5000) { if (http_client.getStreamPtr()->available() > 0) { partial_read_len = http_client.getStreamPtr()->readBytes(reinterpret_cast(buffer) + read_len, buffer_size - read_len); read_len += partial_read_len; if (partial_read_len > 0) { App.feed_wdt(); delay(2); } } } #else // ESP-IDF while (retries < 5 && read_len < buffer_size) { partial_read_len = esp_http_client_read(http_client, reinterpret_cast(buffer) + read_len, buffer_size - read_len); if (partial_read_len > 0) { read_len += partial_read_len; // Accumulate the total read length. // Reset retries on successful read. retries = 0; } else { // If no data was read, increment retries. retries++; vTaskDelay(pdMS_TO_TICKS(2)); // NOLINT } App.feed_wdt(); // Feed the watchdog timer. } #endif if (read_len != buffer_size) { // Did not receive the full package within the timeout period ESP_LOGE(TAG, "Failed to read full package, received only %" PRIu16 " of %" PRIu16 " bytes", read_len, buffer_size); // Deallocate buffer allocator.deallocate(buffer, 4096); buffer = nullptr; return -1; } ESP_LOGV(TAG, "%d bytes fetched, writing it to UART", read_len); if (read_len > 0) { recv_string.clear(); this->write_array(buffer, buffer_size); App.feed_wdt(); this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true); this->content_length_ -= read_len; const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_; #if defined(USE_ESP32) && defined(USE_PSRAM) ESP_LOGD( TAG, "Uploaded %0.2f%%, remaining %" PRIu32 " bytes, free heap: %" PRIu32 " (DRAM) + %" PRIu32 " (PSRAM) bytes", upload_percentage, this->content_length_, static_cast(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)), static_cast(heap_caps_get_free_size(MALLOC_CAP_SPIRAM))); #else ESP_LOGD(TAG, "Uploaded %0.2f%%, remaining %" PRIu32 " bytes, free heap: %" PRIu32 " bytes", upload_percentage, this->content_length_, this->get_free_heap_()); #endif upload_first_chunk_sent_ = true; if (recv_string[0] == 0x08 && recv_string.size() == 5) { // handle partial upload request ESP_LOGD(TAG, "recv_string [%s]", format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); uint32_t result = 0; for (int j = 0; j < 4; ++j) { result += static_cast(recv_string[j + 1]) << (8 * j); } if (result > 0) { ESP_LOGI(TAG, "Nextion reported new range %" PRIu32, result); this->content_length_ = this->tft_size_ - result; range_start = result; } else { range_start = range_end + 1; } // Deallocate buffer allocator.deallocate(buffer, 4096); buffer = nullptr; return range_end + 1; } else if (recv_string[0] != 0x05 and recv_string[0] != 0x08) { // 0x05 == "ok" ESP_LOGE(TAG, "Invalid response from Nextion: [%s]", format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); // Deallocate buffer allocator.deallocate(buffer, 4096); buffer = nullptr; return -1; } recv_string.clear(); } else if (read_len == 0) { ESP_LOGV(TAG, "End of HTTP response reached"); break; // Exit the loop if there is no more data to read } else { ESP_LOGE(TAG, "Failed to read from HTTP client, error code: %d", read_len); break; // Exit the loop on error } } range_start = range_end + 1; // Deallocate buffer allocator.deallocate(buffer, 4096); buffer = nullptr; return range_end + 1; } bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { ESP_LOGD(TAG, "Nextion TFT upload requested"); ESP_LOGD(TAG, "Exit reparse: %s", YESNO(exit_reparse)); ESP_LOGD(TAG, "URL: %s", this->tft_url_.c_str()); if (this->is_updating_) { ESP_LOGW(TAG, "Currently uploading"); return false; } if (!network::is_connected()) { ESP_LOGE(TAG, "Network is not connected"); return false; } this->is_updating_ = true; if (exit_reparse) { ESP_LOGD(TAG, "Exiting Nextion reparse mode"); if (!this->set_protocol_reparse_mode(false)) { ESP_LOGW(TAG, "Failed to request Nextion to exit reparse mode"); return false; } } // Check if baud rate is supported this->original_baud_rate_ = this->parent_->get_baud_rate(); static const std::vector SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600, 115200, 230400, 250000, 256000, 512000, 921600}; if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) { baud_rate = this->original_baud_rate_; } ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate); // Define the configuration for the HTTP client ESP_LOGV(TAG, "Initializing HTTP client"); ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_()); #ifdef ARDUINO HTTPClient http_client; http_client.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along bool begin_status = false; #ifdef USE_ESP32 begin_status = http_client.begin(this->tft_url_.c_str()); #endif #ifdef USE_ESP8266 #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); #elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) http_client.setFollowRedirects(true); #endif #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) http_client.setRedirectLimit(3); #endif begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str()); #endif // USE_ESP8266 if (!begin_status) { this->is_updating_ = false; ESP_LOGD(TAG, "Connection failed"); return false; } else { ESP_LOGD(TAG, "Connected"); } http_client.addHeader("Range", "bytes=0-255"); const char *header_names[] = {"Content-Range"}; http_client.collectHeaders(header_names, 1); ESP_LOGD(TAG, "Requesting URL: %s", this->tft_url_.c_str()); http_client.setReuse(true); // try up to 5 times. DNS sometimes needs a second try or so int tries = 1; int http_status_code = http_client.GET(); delay(100); // NOLINT App.feed_wdt(); while (http_status_code != 200 && http_status_code != 206 && tries <= 5) { ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s, retrying (%d/5)", this->tft_url_.c_str(), HTTPClient::errorToString(http_status_code).c_str(), tries); delay(250); // NOLINT App.feed_wdt(); http_status_code = http_client.GET(); ++tries; } #elif // ESP-IDF esp_http_client_config_t config = { .url = this->tft_url_.c_str(), .cert_pem = nullptr, .method = HTTP_METHOD_HEAD, .timeout_ms = 15000, .disable_auto_redirect = false, .max_redirection_count = 10, }; // Initialize the HTTP client with the configuration esp_http_client_handle_t http_client = esp_http_client_init(&config); if (!http_client) { ESP_LOGE(TAG, "Failed to initialize HTTP client."); return this->upload_end_(false); } esp_err_t err = esp_http_client_set_header(http_client, "Connection", "keep-alive"); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP set header failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(http_client); return this->upload_end_(false); } // Perform the HTTP request ESP_LOGV(TAG, "Check if the client could connect"); ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size()); err = esp_http_client_perform(http_client); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err)); esp_http_client_cleanup(http_client); return this->upload_end_(false); } // Check the HTTP Status Code ESP_LOGV(TAG, "Check the HTTP Status Code"); ESP_LOGV(TAG, "Free heap: %" PRIu32, esp_get_free_heap_size()); int http_status_code = esp_http_client_get_status_code(http_client); #endif // ARDUINO vs ESP-IDF if (http_status_code != 200 and http_status_code != 206) { return this->upload_end_(false); } #ifdef ARDUINO std::string content_range_string = http_client.header("Content-Range"); content_range_string.remove(0, 12); this->tft_size_ = content_range_string.toInt(); #else // ESP-IDF this->tft_size_ = esp_http_client_get_content_length(http_client); #endif ESP_LOGD(TAG, "TFT file size: %zu bytes", this->tft_size_); if (this->tft_size_ < 4096) { ESP_LOGE(TAG, "File size check failed."); this->close_http_client_(http_client); return this->upload_end_(false); } else { ESP_LOGV(TAG, "File size check passed. Proceeding..."); } this->content_length_ = this->tft_size_; ESP_LOGD(TAG, "Uploading Nextion"); // The Nextion will ignore the upload command if it is sleeping ESP_LOGV(TAG, "Wake-up Nextion"); this->ignore_is_setup_ = true; this->send_command_("sleep=0"); this->send_command_("dim=100"); delay(250); // NOLINT ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_()); App.feed_wdt(); char command[128]; // Tells the Nextion the content length of the tft file and baud rate it will be sent at // Once the Nextion accepts the command it will wait until the file is successfully uploaded // If it fails for any reason a power cycle of the display will be needed sprintf(command, "whmi-wris %d,%d,1", this->content_length_, baud_rate); // Clear serial receive buffer ESP_LOGV(TAG, "Clear serial receive buffer"); this->reset_(false); delay(250); // NOLINT ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_()); ESP_LOGV(TAG, "Send upload instruction: %s", command); this->send_command_(command); if (baud_rate != this->original_baud_rate_) { ESP_LOGD(TAG, "Changing baud rate from %" PRIu32 " to %" PRIu32 " bps", this->original_baud_rate_, baud_rate); this->parent_->set_baud_rate(baud_rate); this->parent_->load_settings(); } App.feed_wdt(); std::string response; ESP_LOGV(TAG, "Waiting for upgrade response"); this->recv_ret_string_(response, 5000, true); // This can take some time to return // The Nextion display will, if it's ready to accept data, send a 0x05 byte. ESP_LOGD(TAG, "Upgrade response is [%s] - %zu byte(s)", format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str(), response.length()); ESP_LOGV(TAG, "Free heap: %" PRIu32, this->get_free_heap_()); if (response.find(0x05) != std::string::npos) { ESP_LOGV(TAG, "Preparation for TFT upload done"); } else { ESP_LOGE(TAG, "Preparation for TFT upload failed %d \"%s\"", response[0], response.c_str()); this->close_http_client_(http_client); return this->upload_end_(false); } #ifdef USE_ESP_IDF ESP_LOGV(TAG, "Change the method to GET before starting the download"); esp_err_t set_method_result = esp_http_client_set_method(http_client, HTTP_METHOD_GET); if (set_method_result != ESP_OK) { ESP_LOGE(TAG, "Failed to set HTTP method to GET: %s", esp_err_to_name(set_method_result)); this->close_http_client_(http_client); return this->upload_end_(false); } #endif // USE_ESP_IDF ESP_LOGD(TAG, "Uploading TFT to Nextion:"); ESP_LOGD(TAG, " URL: %s", this->tft_url_.c_str()); ESP_LOGD(TAG, " File size: %d bytes", this->content_length_); ESP_LOGD(TAG, " Free heap: %" PRIu32, this->get_free_heap_()); // Proceed with the content download as before ESP_LOGV(TAG, "Starting transfer by chunks loop"); uint32_t position = 0; while (this->content_length_ > 0) { int upload_result = upload_by_chunks_(http_client, position); if (upload_result < 0) { ESP_LOGE(TAG, "Error uploading TFT to Nextion!"); this->close_http_client_(http_client); return this->upload_end_(false); } App.feed_wdt(); ESP_LOGV(TAG, "Free heap: %" PRIu32 ", Bytes left: %" PRIu32, this->get_free_heap_(), this->content_length_); } ESP_LOGD(TAG, "Successfully uploaded TFT to Nextion!"); this->close_http_client_(http_client); return upload_end_(true); } #ifdef ARDUINO void Nextion::close_http_client_(HttpClient &http_client) { ESP_LOGD(TAG, "Close HTTP connection"); http_client.end(); ESP_LOGV(TAG, "Connection closed"); } #else // ESP-IDF void Nextion::close_http_client_(esp_http_client_handle_t http_client) { ESP_LOGD(TAG, "Close HTTP connection"); esp_http_client_close(http_client); esp_http_client_cleanup(http_client); ESP_LOGV(TAG, "Connection closed"); } #endif // ARDUINO vs ESP-IDF bool Nextion::upload_end_(bool successful) { ESP_LOGD(TAG, "Nextion TFT upload finished: %s", YESNO(successful)); this->is_updating_ = false; this->ignore_is_setup_ = false; uint32_t baud_rate = this->parent_->get_baud_rate(); if (baud_rate != this->original_baud_rate_) { ESP_LOGD(TAG, "Changing baud rate back from %" PRIu32 " to %" PRIu32 " bps", baud_rate, this->original_baud_rate_); this->parent_->set_baud_rate(this->original_baud_rate_); this->parent_->load_settings(); } if (successful) { ESP_LOGD(TAG, "Restarting ESPHome"); delay(1500); // NOLINT arch_restart(); } else { ESP_LOGE(TAG, "Nextion TFT upload failed"); } return successful; } #ifdef USE_ESP8266 WiFiClient *Nextion::get_wifi_client_() { if (this->tft_url_.compare(0, 6, "https:") == 0) { if (this->wifi_client_secure_ == nullptr) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); this->wifi_client_secure_->setInsecure(); this->wifi_client_secure_->setBufferSizes(512, 512); } return this->wifi_client_secure_; } if (this->wifi_client_ == nullptr) { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) this->wifi_client_ = new WiFiClient(); } return this->wifi_client_; } #endif // USE_ESP8266 } // namespace nextion } // namespace esphome #endif // USE_NEXTION_TFT_UPLOAD