diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index afb64ceeea..0831b12f8a 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -36,7 +36,7 @@ CONFIG_SCHEMA = ( display.BASIC_DISPLAY_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Nextion), - cv.Optional(CONF_TFT_URL): cv.All(cv.string, cv.only_with_arduino), + cv.Optional(CONF_TFT_URL): cv.url, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, cv.Optional(CONF_ON_SETUP): automation.validate_automation( { @@ -85,10 +85,10 @@ async def to_code(config): if CONF_TFT_URL in config: cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) - if CORE.is_esp32: + if CORE.is_esp32 and CORE.using_arduino: cg.add_library("WiFiClientSecure", None) cg.add_library("HTTPClient", None) - if CORE.is_esp8266: + elif CORE.is_esp8266 and CORE.using_arduino: cg.add_library("ESP8266HTTPClient", None) if CONF_TOUCH_SLEEP_TIMEOUT in config: diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 76bdb283f6..0134595050 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -128,7 +128,7 @@ void Nextion::dump_config() { ESP_LOGCONFIG(TAG, " Wake On Touch: %s", this->auto_wake_on_touch_ ? "True" : "False"); if (this->touch_sleep_timeout_ != 0) { - ESP_LOGCONFIG(TAG, " Touch Timeout: %d", this->touch_sleep_timeout_); + ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu32, this->touch_sleep_timeout_); } if (this->wake_up_page_ != -1) { @@ -868,6 +868,12 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool start = millis(); while ((timeout == 0 && this->available()) || millis() - start <= timeout) { + if (!this->available()) { + App.feed_wdt(); + delay(1); + continue; + } + this->read_byte(&c); if (c == 0xFF) { nr_of_ff_bytes++; @@ -886,7 +892,7 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool } } App.feed_wdt(); - delay(1); + delay(2); if (exit_flag || ff_flag) { break; diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 7b5641b711..f3a13d2170 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -12,14 +12,18 @@ #include "esphome/components/display/display_color_utils.h" #ifdef USE_NEXTION_TFT_UPLOAD +#ifdef ARDUINO #ifdef USE_ESP32 #include -#endif +#endif // USE_ESP32 #ifdef USE_ESP8266 #include #include -#endif -#endif +#endif // USE_ESP8266 +#elif defined(USE_ESP_IDF) +#include +#endif // ARDUINO vs ESP-IDF +#endif // USE_NEXTION_TFT_UPLOAD namespace esphome { namespace nextion { @@ -685,16 +689,18 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe #ifdef USE_NEXTION_TFT_UPLOAD /** - * Set the tft file URL. https seems problamtic with arduino.. + * Set the tft file URL. https seems problematic with arduino.. */ void set_tft_url(const std::string &tft_url) { this->tft_url_ = tft_url; } #endif /** - * Upload the tft file and softreset the Nextion + * Upload the tft file and soft reset Nextion + * @return bool True: Transfer completed successfuly, False: Transfer failed. */ - void upload_tft(); + bool upload_tft(); + void dump_config() override; /** @@ -817,16 +823,16 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe BearSSL::WiFiClientSecure *wifi_client_secure_{nullptr}; WiFiClient *get_wifi_client_(); #endif - + int content_length_ = 0; + int tft_size_ = 0; +#ifdef ARDUINO /** * will request chunk_size chunks from the web server * and send each to the nextion - * @param int contentLength Total size of the file - * @param uint32_t chunk_size - * @return true if success, false for failure. + * @param HTTPClient http HTTP client handler. + * @param int range_start Position of next byte to transfer. + * @return position of last byte transferred, -1 for failure. */ - int content_length_ = 0; - int tft_size_ = 0; int upload_by_chunks_(HTTPClient *http, int range_start); bool upload_with_range_(uint32_t range_start, uint32_t range_end); @@ -839,7 +845,30 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * @return true if success, false for failure. */ bool upload_from_buffer_(const uint8_t *file_buf, size_t buf_size); - void upload_end_(); + /** + * Ends the upload process, restart Nextion and, if successful, + * restarts ESP + * @param bool url successful True: Transfer completed successfuly, False: Transfer failed. + * @return bool True: Transfer completed successfuly, False: Transfer failed. + */ + bool upload_end_(bool successful); +#elif defined(USE_ESP_IDF) + /** + * will request 4096 bytes chunks from the web server + * and send each to Nextion + * @param std::string url Full url for download. + * @param int range_start Position of next byte to transfer. + * @return position of last byte transferred, -1 for failure. + */ + int upload_range(const std::string &url, int range_start); + /** + * Ends the upload process, restart Nextion and, if successful, + * restarts ESP + * @param bool url successful True: Transfer completed successfuly, False: Transfer failed. + * @return bool True: Transfer completed successfuly, False: Transfer failed. + */ + bool upload_end(bool successful); +#endif // ARDUINO vs ESP-IDF #endif // USE_NEXTION_TFT_UPLOAD diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index a3157c731a..35530d1a7f 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -55,7 +55,7 @@ void Nextion::set_protocol_reparse_mode(bool active_mode) { // Set Colors void Nextion::set_component_background_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, color); + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%" PRIu32, component, color); } void Nextion::set_component_background_color(const char *component, const char *color) { @@ -68,7 +68,8 @@ void Nextion::set_component_background_color(const char *component, Color color) } void Nextion::set_component_pressed_background_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%d", component, color); + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%" PRIu32, component, + color); } void Nextion::set_component_pressed_background_color(const char *component, const char *color) { @@ -89,7 +90,7 @@ void Nextion::set_component_picc(const char *component, uint8_t pic_id) { } void Nextion::set_component_font_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%d", component, color); + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%" PRIu32, component, color); } void Nextion::set_component_font_color(const char *component, const char *color) { @@ -102,7 +103,7 @@ void Nextion::set_component_font_color(const char *component, Color color) { } void Nextion::set_component_pressed_font_color(const char *component, uint32_t color) { - this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%d", component, color); + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%" PRIu32, component, color); } void Nextion::set_component_pressed_font_color(const char *component, const char *color) { diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp similarity index 94% rename from esphome/components/nextion/nextion_upload.cpp rename to esphome/components/nextion/nextion_upload_arduino.cpp index 9e6884398c..d1f9f44c2b 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -1,5 +1,6 @@ #include "nextion.h" +#ifdef ARDUINO #ifdef USE_NEXTION_TFT_UPLOAD #include "esphome/core/application.h" @@ -128,15 +129,15 @@ int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { return range_end + 1; } -void Nextion::upload_tft() { +bool Nextion::upload_tft() { if (this->is_updating_) { ESP_LOGD(TAG, "Currently updating"); - return; + return false; } if (!network::is_connected()) { ESP_LOGD(TAG, "network is not connected"); - return; + return false; } this->is_updating_ = true; @@ -164,7 +165,7 @@ void Nextion::upload_tft() { ESP_LOGD(TAG, "connection failed"); ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); allocator.deallocate(this->transfer_buffer_, this->transfer_buffer_size_); - return; + return false; } else { ESP_LOGD(TAG, "Connected"); } @@ -192,7 +193,7 @@ void Nextion::upload_tft() { } if ((code != 200 && code != 206) || tries > 5) { - this->upload_end_(); + return this->upload_end_(false); } String content_range_string = http.header("Content-Range"); @@ -203,7 +204,7 @@ void Nextion::upload_tft() { if (this->content_length_ < 4096) { ESP_LOGE(TAG, "Failed to get file size"); - this->upload_end_(); + return this->upload_end_(false); } ESP_LOGD(TAG, "Updating Nextion %s...", this->device_model_.c_str()); @@ -246,7 +247,7 @@ void Nextion::upload_tft() { ESP_LOGD(TAG, "preparation for tft update done"); } else { ESP_LOGD(TAG, "preparation for tft update failed %d \"%s\"", response[0], response.c_str()); - this->upload_end_(); + return this->upload_end_(false); } // Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096 @@ -280,7 +281,7 @@ void Nextion::upload_tft() { this->transfer_buffer_ = allocator.allocate(chunk_size); if (!this->transfer_buffer_) - this->upload_end_(); + return this->upload_end_(false); } this->transfer_buffer_size_ = chunk_size; @@ -295,7 +296,7 @@ void Nextion::upload_tft() { result = this->upload_by_chunks_(&http, result); if (result < 0) { ESP_LOGD(TAG, "Error updating Nextion!"); - this->upload_end_(); + return this->upload_end_(false); } App.feed_wdt(); // NOLINTNEXTLINE(readability-static-accessed-through-instance) @@ -303,15 +304,19 @@ void Nextion::upload_tft() { } ESP_LOGD(TAG, "Successfully updated Nextion!"); - this->upload_end_(); + return this->upload_end_(true); } -void Nextion::upload_end_() { +bool Nextion::upload_end_(bool successful) { + this->is_updating_ = false; ESP_LOGD(TAG, "Restarting Nextion"); this->soft_reset(); - delay(1500); // NOLINT - ESP_LOGD(TAG, "Restarting esphome"); - ESP.restart(); // NOLINT(readability-static-accessed-through-instance) + if (successful) { + delay(1500); // NOLINT + ESP_LOGD(TAG, "Restarting esphome"); + ESP.restart(); // NOLINT(readability-static-accessed-through-instance) + } + return successful; } #ifdef USE_ESP8266 @@ -337,3 +342,4 @@ WiFiClient *Nextion::get_wifi_client_() { } // namespace esphome #endif // USE_NEXTION_TFT_UPLOAD +#endif // ARDUINO diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp new file mode 100644 index 0000000000..58f5659ade --- /dev/null +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -0,0 +1,268 @@ +#include "nextion.h" + +#ifdef USE_ESP_IDF +#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 +#include + +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 + +int Nextion::upload_range(const std::string &url, int range_start) { + ESP_LOGVV(TAG, "url: %s", url.c_str()); + uint range_size = this->tft_size_ - range_start; + ESP_LOGVV(TAG, "tft_size_: %i", this->tft_size_); + ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + int range_end = (range_start == 0) ? std::min(this->tft_size_, 16383) : this->tft_size_; + if (range_size <= 0 or range_end <= range_start) { + ESP_LOGE(TAG, "Invalid range"); + ESP_LOGD(TAG, "Range start: %i", range_start); + ESP_LOGD(TAG, "Range end: %i", range_end); + ESP_LOGD(TAG, "Range size: %i", range_size); + return -1; + } + + esp_http_client_config_t config = { + .url = url.c_str(), + .cert_pem = nullptr, + }; + esp_http_client_handle_t client = esp_http_client_init(&config); + + char range_header[64]; + sprintf(range_header, "bytes=%d-%d", range_start, range_end); + ESP_LOGV(TAG, "Requesting range: %s", range_header); + esp_http_client_set_header(client, "Range", range_header); + ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size()); + + ESP_LOGV(TAG, "Opening http connetion"); + esp_err_t err; + if ((err = esp_http_client_open(client, 0)) != ESP_OK) { + ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); + esp_http_client_cleanup(client); + return -1; + } + + ESP_LOGV(TAG, "Fetch content length"); + int content_length = esp_http_client_fetch_headers(client); + ESP_LOGV(TAG, "content_length = %d", content_length); + if (content_length <= 0) { + ESP_LOGE(TAG, "Failed to get content length: %d", content_length); + esp_http_client_cleanup(client); + return -1; + } + + int total_read_len = 0, read_len; + + ESP_LOGV(TAG, "Allocate buffer"); + uint8_t *buffer = new uint8_t[4096]; + std::string recv_string; + if (buffer == nullptr) { + ESP_LOGE(TAG, "Failed to allocate memory for buffer"); + ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + } else { + ESP_LOGV(TAG, "Memory for buffer allocated successfully"); + + while (true) { + App.feed_wdt(); + ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size()); + int read_len = esp_http_client_read(client, reinterpret_cast(buffer), 4096); + ESP_LOGVV(TAG, "Read %d bytes from HTTP client, writing to UART", read_len); + if (read_len > 0) { + this->write_array(buffer, read_len); + ESP_LOGVV(TAG, "Write to UART successful"); + this->recv_ret_string_(recv_string, 5000, true); + this->content_length_ -= read_len; + ESP_LOGD(TAG, "Uploaded %0.2f %%, remaining %d bytes", + 100.0 * (this->tft_size_ - this->content_length_) / this->tft_size_, this->content_length_); + if (recv_string[0] != 0x05) { // 0x05 == "ok" + ESP_LOGD( + TAG, "recv_string [%s]", + format_hex_pretty(reinterpret_cast(recv_string.data()), recv_string.size()).c_str()); + } + // handle partial upload request + if (recv_string[0] == 0x08 && recv_string.size() == 5) { + 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; + // Deallocate the buffer when done + delete[] buffer; + ESP_LOGVV(TAG, "Memory for buffer deallocated"); + esp_http_client_cleanup(client); + esp_http_client_close(client); + return result; + } + } + 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 + } + } + + // Deallocate the buffer when done + delete[] buffer; + ESP_LOGVV(TAG, "Memory for buffer deallocated"); + } + esp_http_client_cleanup(client); + esp_http_client_close(client); + return range_end + 1; +} + +bool Nextion::upload_tft() { + ESP_LOGD(TAG, "Nextion TFT upload requested"); + ESP_LOGD(TAG, "url: %s", this->tft_url_.c_str()); + + if (this->is_updating_) { + ESP_LOGW(TAG, "Currently updating"); + return false; + } + + if (!network::is_connected()) { + ESP_LOGE(TAG, "Network is not connected"); + return false; + } + + this->is_updating_ = true; + + // Define the configuration for the HTTP client + ESP_LOGV(TAG, "Establishing connection to HTTP server"); + ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size()); + esp_http_client_config_t config = { + .url = this->tft_url_.c_str(), + .cert_pem = nullptr, + .method = HTTP_METHOD_HEAD, + .timeout_ms = 15000, + }; + + // Initialize the HTTP client with the configuration + ESP_LOGV(TAG, "Initializing HTTP client"); + ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + esp_http_client_handle_t http = esp_http_client_init(&config); + if (!http) { + ESP_LOGE(TAG, "Failed to initialize HTTP client."); + return this->upload_end(false); + } + + // Perform the HTTP request + ESP_LOGV(TAG, "Check if the client could connect"); + ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + esp_err_t err = esp_http_client_perform(http); + if (err != ESP_OK) { + ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err)); + esp_http_client_cleanup(http); + return this->upload_end(false); + } + + // Check the HTTP Status Code + int status_code = esp_http_client_get_status_code(http); + ESP_LOGV(TAG, "HTTP Status Code: %d", status_code); + size_t tft_file_size = esp_http_client_get_content_length(http); + ESP_LOGD(TAG, "TFT file size: %zu", tft_file_size); + + if (tft_file_size < 4096) { + ESP_LOGE(TAG, "File size check failed. Size: %zu", tft_file_size); + esp_http_client_cleanup(http); + return this->upload_end(false); + } else { + ESP_LOGV(TAG, "File size check passed. Proceeding..."); + } + this->content_length_ = tft_file_size; + this->tft_size_ = tft_file_size; + + ESP_LOGD(TAG, "Updating Nextion"); + // The Nextion will ignore the update command if it is sleeping + + this->send_command_("sleep=0"); + this->set_backlight_brightness(1.0); + vTaskDelay(pdMS_TO_TICKS(250)); // NOLINT + + 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,%" PRIu32 ",1", this->content_length_, this->parent_->get_baud_rate()); + + // Clear serial receive buffer + uint8_t d; + while (this->available()) { + this->read_byte(&d); + }; + + this->send_command_(command); + + std::string response; + ESP_LOGV(TAG, "Waiting for upgrade response"); + this->recv_ret_string_(response, 2048, 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]", + format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str()); + + if (response.find(0x05) != std::string::npos) { + ESP_LOGV(TAG, "Preparation for tft update done"); + } else { + ESP_LOGE(TAG, "Preparation for tft update failed %d \"%s\"", response[0], response.c_str()); + esp_http_client_cleanup(http); + return this->upload_end(false); + } + + ESP_LOGD(TAG, "Updating tft from \"%s\" with a file size of %d, Heap Size %" PRIu32, this->tft_url_.c_str(), + content_length_, esp_get_free_heap_size()); + + ESP_LOGV(TAG, "Starting transfer by chunks loop"); + int result = 0; + while (content_length_ > 0) { + result = upload_range(this->tft_url_.c_str(), result); + if (result < 0) { + ESP_LOGE(TAG, "Error updating Nextion!"); + esp_http_client_cleanup(http); + return this->upload_end(false); + } + App.feed_wdt(); + ESP_LOGV(TAG, "Heap Size %" PRIu32 ", Bytes left %d", esp_get_free_heap_size(), content_length_); + } + + ESP_LOGD(TAG, "Successfully updated Nextion!"); + + ESP_LOGD(TAG, "Close HTTP connection"); + esp_http_client_close(http); + esp_http_client_cleanup(http); + return upload_end(true); +} + +bool Nextion::upload_end(bool successful) { + this->is_updating_ = false; + ESP_LOGD(TAG, "Restarting Nextion"); + this->soft_reset(); + vTaskDelay(pdMS_TO_TICKS(1500)); // NOLINT + if (successful) { + ESP_LOGD(TAG, "Restarting esphome"); + esp_restart(); // NOLINT(readability-static-accessed-through-instance) + } + return successful; +} + +} // namespace nextion +} // namespace esphome + +#endif // USE_NEXTION_TFT_UPLOAD +#endif // USE_ESP_IDF