diff --git a/.gitignore b/.gitignore index 071293d..d0af3ba 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ Nextion2Text.* # Ignore IntelliJ IDEA project directory .idea + +# Ignore dev folder +dev \ No newline at end of file diff --git a/nspanel_blueprint.yaml b/nspanel_blueprint.yaml index 26e5162..ecf52d9 100644 --- a/nspanel_blueprint.yaml +++ b/nspanel_blueprint.yaml @@ -33,7 +33,7 @@ blueprint: 🎉 Roadmap can be found here: [Roadmap](https://github.com/Blackymas/NSPanel_HA_Blueprint/labels/roadmap) - â„šī¸ Version: v4.0.3dev + â„šī¸ Version: v4.1dev3 source_url: https://github.com/Blackymas/NSPanel_HA_Blueprint/blob/main/nspanel_blueprint.yaml domain: automation @@ -3587,7 +3587,7 @@ trigger_variables: variables: ##### GENERAL ##### - blueprint_version: '4.0.3dev' + blueprint_version: '4.1dev3' date_format_temp: !input 'date_format' #Avoid breaking change for existing users with legacy type format date_format: > diff --git a/nspanel_esphome.yaml b/nspanel_esphome.yaml index fa4c096..bcea4dd 100644 --- a/nspanel_esphome.yaml +++ b/nspanel_esphome.yaml @@ -7,8 +7,14 @@ substitutions: + ################## Defaults ################## + # Just in case user forgets to set something # + nextion_update_url: "https://github.com/Blackymas/NSPanel_HA_Blueprint/raw/main/nspanel_eu.tft" + # nextion_update_blank_url: "https://github.com/Blackymas/NSPanel_HA_Blueprint/raw/main/custom_configuration/nspanel_blank.tft" + ############################################## + ##### DON'T CHANGE THIS ##### - version: "4.0.2" + version: "4.1dev3" ############################# ##### WIFI SETUP ##### @@ -67,10 +73,10 @@ output: ##### UART FOR NEXTION DISPLAY ##### uart: - id: tf_uart - tx_pin: 16 - rx_pin: 17 - baud_rate: 115200 + - id: tf_uart + tx_pin: 16 + rx_pin: 17 + baud_rate: 115200 ##### Keeps time display updated ##### time: @@ -108,7 +114,7 @@ button: id: nextion_init state: false - delay: 16ms - - lambda: id(disp1).upload_tft(); + - lambda: id(upload_tft).execute("${nextion_update_url}"); ##### EXIT REPARSE TFT DISPLAY ##### - name: ${device_name} Exit reparse @@ -139,7 +145,7 @@ api: - binary_sensor.template.publish: id: nextion_init state: false - - lambda: 'id(disp1)->upload_tft();' + - lambda: 'id(upload_tft).execute("${nextion_update_url}");' ##### SERVICE TO UPDATE THE TFT FILE from URL ##### - service: upload_tft_url @@ -150,8 +156,7 @@ api: - binary_sensor.template.publish: id: nextion_init state: false - - lambda: 'id(disp1)->set_tft_url(url.c_str());' - - lambda: 'id(disp1)->upload_tft();' + - lambda: 'id(upload_tft).execute(url.c_str());' ##### Service to send a command "printf" directly to the display ##### - service: send_command_printf @@ -1245,15 +1250,15 @@ text_sensor: id(disp1).set_component_text_printf("keyb_num.bclear", "%s", "\uE641"); //mdi:eraser-variant id(disp1).set_component_text_printf("keyb_num.benter", "%s", "\uE12B"); //mdi:check } - else if (page=="weather01") id(disp1).set_component_text_printf("page_index", "%s", "\uE764\uE765\uE765\uE765\uE765"); // 1/5 - else if (page=="weather02") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE764\uE765\uE765\uE765"); // 2/5 - else if (page=="weather03") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE765\uE764\uE765\uE765"); // 3/5 - else if (page=="weather04") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE765\uE765\uE764\uE765"); // 4/5 - else if (page=="weather05") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE765\uE765\uE765\uE764"); // 5/5 - else if (page=="buttonpage01" or page=="entitypage01") id(disp1).set_component_text_printf("page_index", "%s", "\uE764\uE765\uE765\uE765"); // 1/4 - else if (page=="buttonpage02" or page=="entitypage02") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE764\uE765\uE765"); // 2/4 - else if (page=="buttonpage03" or page=="entitypage03") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE765\uE764\uE765"); // 3/4 - else if (page=="buttonpage04" or page=="entitypage04") id(disp1).set_component_text_printf("page_index", "%s", "\uE765\uE765\uE765\uE764"); // 4/4 + else if (page=="weather01") id(disp1).set_component_text_printf("page_index", "%s", "●○○○○"); // 1/5 + else if (page=="weather02") id(disp1).set_component_text_printf("page_index", "%s", "○●○○○"); // 2/5 + else if (page=="weather03") id(disp1).set_component_text_printf("page_index", "%s", "○○●○○"); // 3/5 + else if (page=="weather04") id(disp1).set_component_text_printf("page_index", "%s", "○○○●○"); // 4/5 + else if (page=="weather05") id(disp1).set_component_text_printf("page_index", "%s", "○○○○●"); // 5/5 + else if (page=="buttonpage01" or page=="entitypage01") id(disp1).set_component_text_printf("page_index", "%s", "●○○○"); // 1/4 + else if (page=="buttonpage02" or page=="entitypage02") id(disp1).set_component_text_printf("page_index", "%s", "○●○○"); // 2/4 + else if (page=="buttonpage03" or page=="entitypage03") id(disp1).set_component_text_printf("page_index", "%s", "○○●○"); // 3/4 + else if (page=="buttonpage04" or page=="entitypage04") id(disp1).set_component_text_printf("page_index", "%s", "○○○●"); // 4/4 else if (page=="settings") { //id(disp1).set_component_text_printf("bt_sleep", "%s", (id(sleep_mode).state) ? "\uEA19" : "\uEA18"); //mdi:toggle-switch-outline or mdi:toggle-switch-off-outline @@ -1665,11 +1670,13 @@ display: uart_id: tf_uart tft_url: ${nextion_update_url} on_page: # I couldn't make this trigger to work, so used text_sensor nspanelevent and localevent instead - - lambda: ESP_LOGW("display.disp1", "NEXTION PAGE CHANGED"); + then: + - lambda: ESP_LOGW("display.disp1", "NEXTION PAGE CHANGED"); on_setup: then: - lambda: |- id(disp1).goto_page("boot"); + id(disp1).send_command_printf("bkcmd=3"); id(disp1).set_component_text_printf("boot.esph_version", "%s", "${version}"); // ### esphome-version ### id(disp1).show_component("bt_reboot"); id(timer_reset_all).execute("boot"); @@ -2274,7 +2281,402 @@ script: else if (id(wakeup_page_name).state == "alarm") wakeup_page_id = 23; id(disp1).set_component_value("orign", wakeup_page_id); - ##### ADD-ONS ############################################################ + - id: upload_tft + mode: single + parameters: + url: string + then: + - lambda: |- + ESP_LOGD("script.upload_tft", "Starting..."); + + std::vector buffer_; + + bool is_updating_ = false; + + uint8_t *transfer_buffer_{nullptr}; + size_t transfer_buffer_size_; + bool upload_first_chunk_sent_ = false; + + int content_length_ = 0; + int tft_size_ = 0; + + bool power_cycle_display = []() + { + id(screen_power).turn_off(); + delay(1500); + id(screen_power).turn_on(); + delay(1500); + return true; + }; + + auto send_nextion_command = [](const std::string &command) -> bool + { + ESP_LOGD("script.upload_tft.send_nextion_command", "Sending: %s", command.c_str()); + id(tf_uart).write_str(command.c_str()); + const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF}; + id(tf_uart).write_array(to_send, sizeof(to_send)); + return true; + }; + + auto recv_ret_string_ = [](std::string &response, uint32_t timeout, bool recv_flag) -> uint16_t + { + uint16_t ret; + uint8_t c = 0; + uint8_t nr_of_ff_bytes = 0; + uint64_t start; + bool exit_flag = false; + bool ff_flag = false; + + start = millis(); + + while ((timeout == 0 && id(tf_uart).available()) || millis() - start <= timeout) + { + if (!id(tf_uart).available()) + { + App.feed_wdt(); + continue; + } + + id(tf_uart).read_byte(&c); + if (c == 0xFF) + { + nr_of_ff_bytes++; + } + else + { + nr_of_ff_bytes = 0; + ff_flag = false; + } + + if (nr_of_ff_bytes >= 3) + ff_flag = true; + + response += (char) c; + if (recv_flag) + { + if (response.find(0x05) != std::string::npos) + { + exit_flag = true; + } + } + App.feed_wdt(); + delay(2); + + if (exit_flag || ff_flag) + { + break; + } + } + + if (ff_flag) + response = response.substr(0, response.length() - 3); // Remove last 3 0xFF + + ret = response.length(); + return ret; + }; + + auto upload_end_ = [&](bool retry) -> bool + { + ESP_LOGD("script.upload_tft.upload_end_", "Restarting Nextion"); + send_nextion_command("rest"); + if (is_updating_) is_updating_ = not retry; + if (retry) + { + ESP_LOGD("script.upload_tft.upload_end_", "Nextion TFT upload will try again"); + send_nextion_command("rest"); + delay(1500); + } + else + { + ESP_LOGD("script.upload_tft.upload_end_", "Turn off Nextion"); + //id(screen_power).turn_off(); + delay(1500); + ESP_LOGD("script.upload_tft.upload_end_", "Restarting esphome"); + ESP.restart(); + } + return not retry; + }; + + auto upload_by_chunks_ = [&](HTTPClient *http, const std::string &url, int range_start) -> int + { + int range_end; + + if (range_start == 0 && transfer_buffer_size_ > 16384) { // Start small at the first run in case of a big skip + range_end = 16384 - 1; + } else { + range_end = range_start + transfer_buffer_size_ - 1; + } + + if (range_end > tft_size_) + range_end = tft_size_; + + char range_header[64]; + sprintf(range_header, "bytes=%d-%d", range_start, range_end); + + ESP_LOGD("script.upload_tft.upload_by_chunks", "Requesting range: %s", range_header); + + int tries = 1; + int code; + bool begin_status; + while (tries <= 5) { + begin_status = http->begin(url.c_str()); + + ++tries; + if (!begin_status) { + ESP_LOGD("script.upload_tft.upload_by_chunks", "upload_by_chunks_: connection failed"); + continue; + }; + + http->addHeader("Range", range_header); + + code = http->GET(); + if (code == 200 || code == 206) { + break; + } + ESP_LOGW("script.upload_tft.upload_by_chunks", "HTTP Request failed; URL: %s; Error: %s, retries(%d/5)", url.c_str(), + HTTPClient::errorToString(code).c_str(), tries); + http->end(); + delay(500); + } + + if (tries > 5) { + return -1; + } + + std::string recv_string; + size_t size; + int fetched = 0; + int range = range_end - range_start; + int write_len; + + // fetch next segment from HTTP stream + while (fetched < range) { + size = http->getStreamPtr()->available(); + if (!size) { + App.feed_wdt(); + delay(2); + continue; + } + int c = http->getStreamPtr()->readBytes( + &transfer_buffer_[fetched], ((size > transfer_buffer_size_) ? transfer_buffer_size_ : size)); + fetched += c; + } + http->end(); + ESP_LOGD("script.upload_tft.upload_by_chunks", "fetched %d bytes", fetched); + + // upload fetched segments to the display in 4KB chunks + for (int i = 0; i < range; i += 4096) { + App.feed_wdt(); + write_len = content_length_ < 4096 ? content_length_ : 4096; + id(tf_uart).write_array(&transfer_buffer_[i], write_len); + content_length_ -= write_len; + ESP_LOGD("script.upload_tft.upload_by_chunks", "Uploaded %0.1f %%, remaining %d Bytes", + 100.0 * (tft_size_ - content_length_) / tft_size_, content_length_); + + if (!upload_first_chunk_sent_) { + upload_first_chunk_sent_ = true; + delay(500); + } + + recv_ret_string_(recv_string, 5000, true); + if (recv_string[0] != 0x05) { // 0x05 == "ok" + ESP_LOGD("script.upload_tft.upload_by_chunks", "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_LOGD("script.upload_tft.upload_by_chunks", "Nextion reported new range %d", result); + content_length_ = tft_size_ - result; + return result; + } + } + + recv_string.clear(); + } + return range_end + 1; + }; + + auto upload_tft = [&](const std::string &url, unsigned int update_baud_rate_) -> bool + { + ESP_LOGD("script.upload_tft.upload_tft", "Nextion TFT upload requested"); + ESP_LOGD("script.upload_tft.upload_tft", "url: %s", url.c_str()); + ESP_LOGD("script.upload_tft.upload_tft", "baud_rate: %i", update_baud_rate_); + + if (is_updating_) { + ESP_LOGD("script.upload_tft.upload_tft", "Currently updating"); + return upload_end_(false); + } + + if (!network::is_connected()) { + ESP_LOGD("script.upload_tft.upload_tft", "network is not connected"); + return upload_end_(false); + } + + send_nextion_command("DRAKJHSUYDGBNCJHGJKSHBDN"); + send_nextion_command("recmod=0"); + send_nextion_command("recmod=0"); + send_nextion_command("connect"); + + is_updating_ = true; + + HTTPClient http; + http.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along + http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + bool begin_status = http.begin(url.c_str()); + + if (!begin_status) { + is_updating_ = false; + ESP_LOGD("script.upload_tft.upload_tft", "connection failed"); + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + allocator.deallocate(transfer_buffer_, transfer_buffer_size_); + return upload_end_(true); + } else { + ESP_LOGD("script.upload_tft.upload_tft", "Connected"); + } + + http.addHeader("Range", "bytes=0-255"); + const char *header_names[] = {"Content-Range"}; + http.collectHeaders(header_names, 1); + ESP_LOGD("script.upload_tft.upload_tft", "Requesting URL: %s", url.c_str()); + + http.setReuse(true); + // try up to 5 times. DNS sometimes needs a second try or so + int tries = 1; + int code = http.GET(); + delay(100); + + while (code != 200 && code != 206 && tries <= 5) { + ESP_LOGW("script.upload_tft.upload_tft", "HTTP Request failed; URL: %s; Error: %s, retrying (%d/5)", url.c_str(), + HTTPClient::errorToString(code).c_str(), tries); + + delay(250); + code = http.GET(); + ++tries; + } + + if ((code != 200 && code != 206) || tries > 5) { + return upload_end_(true); + } + + String content_range_string = http.header("Content-Range"); + content_range_string.remove(0, 12); + content_length_ = content_range_string.toInt(); + tft_size_ = content_length_; + http.end(); + + if (content_length_ < 4096) { + ESP_LOGE("script.upload_tft.upload_tft", "Failed to get file size"); + return upload_end_(true); + } + + ESP_LOGD("script.upload_tft.upload_tft", "Updating Nextion"); + // The Nextion will ignore the update command if it is sleeping + + 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", content_length_, update_baud_rate_); + + // Clear serial receive buffer + uint8_t d; + while (id(tf_uart).available()) { + id(tf_uart).read_byte(&d); + }; + + send_nextion_command(command); + + if (update_baud_rate_ != id(tf_uart).get_baud_rate()) + { + id(tf_uart).set_baud_rate(update_baud_rate_); + id(tf_uart).setup(); + } + + std::string response; + ESP_LOGD("script.upload_tft.upload_tft", "Waiting for upgrade response"); + recv_ret_string_(response, 15000, 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("script.upload_tft.upload_tft", "Upgrade response is [%s]", + format_hex_pretty(reinterpret_cast(response.data()), response.size()).c_str()); + + if (response.find(0x05) != std::string::npos) { + ESP_LOGD("script.upload_tft.upload_tft", "preparation for tft update done"); + } else { + ESP_LOGD("script.upload_tft.upload_tft", "preparation for tft update failed %d \"%s\"", response[0], response.c_str()); + return upload_end_(true); + } + + // Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096 + uint32_t chunk_size = 8192; + if (ESP.getFreeHeap() > 40960) { // 32K to keep on hand + chunk_size = ESP.getFreeHeap() - 32768; + chunk_size = chunk_size > 65536 ? 65536 : chunk_size; + } else if (ESP.getFreeHeap() < 10240) { + chunk_size = 4096; + } + + if (transfer_buffer_ == nullptr) { + ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); + ESP_LOGD("script.upload_tft.upload_tft", "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); + transfer_buffer_ = allocator.allocate(chunk_size); + if (transfer_buffer_ == nullptr) { // Try a smaller size + ESP_LOGD("script.upload_tft.upload_tft", "Could not allocate buffer size: %d trying 4096 instead", chunk_size); + chunk_size = 4096; + ESP_LOGD("script.upload_tft.upload_tft", "Allocating %d buffer", chunk_size); + transfer_buffer_ = allocator.allocate(chunk_size); + + if (!transfer_buffer_) + { + return upload_end_(true); + } + } + + transfer_buffer_size_ = chunk_size; + } + + ESP_LOGD("script.upload_tft.upload_tft", "Updating tft from \"%s\" with a file size of %d using %zu chunksize, Heap Size %d", + url.c_str(), content_length_, transfer_buffer_size_, ESP.getFreeHeap()); + + int result = 0; + while (content_length_ > 0) { + result = upload_by_chunks_(&http, url, result); + if (result < 0) { + ESP_LOGD("script.upload_tft.upload_tft", "Error updating Nextion!"); + return upload_end_(true); + } + App.feed_wdt(); + ESP_LOGD("script.upload_tft.upload_tft", "Heap Size %d, Bytes left %d", ESP.getFreeHeap(), content_length_); + } + is_updating_ = false; + ESP_LOGD("script.upload_tft.upload_tft", "Successfully updated Nextion!"); + + return upload_end_(false); + }; + + if (upload_tft(url, id(tf_uart).get_baud_rate())) ESP.restart(); + power_cycle_display; + if (upload_tft(url, 921600)) ESP.restart(); + power_cycle_display; + if (upload_tft(url, 9600)) ESP.restart(); + power_cycle_display; + ESP_LOGE("script.upload_tft", "TFT upload failed."); + ESP_LOGW("script.upload_tft", "Trying Nextion standard upload"); + id(disp1)->set_tft_url(url.c_str()); + id(disp1)->upload_tft(); + ESP_LOGD("script.upload_tft.upload_end_", "Restarting esphome"); + ESP.restart(); + + ESP_LOGD("script.upload_tft", "Finished!"); + + + ##### ADD-ONS ############################################################ ##### Add-on - Climate ##### - id: addon_climate_service_call mode: restart diff --git a/nspanel_eu.HMI b/nspanel_eu.HMI index eceafba..d8684fd 100644 Binary files a/nspanel_eu.HMI and b/nspanel_eu.HMI differ diff --git a/nspanel_eu.tft b/nspanel_eu.tft index 2ffae25..eaa9247 100644 Binary files a/nspanel_eu.tft and b/nspanel_eu.tft differ diff --git a/nspanel_eu_code/boot.txt b/nspanel_eu_code/boot.txt index 9b066c5..0e15f3a 100644 --- a/nspanel_eu_code/boot.txt +++ b/nspanel_eu_code/boot.txt @@ -134,7 +134,7 @@ Text tft_version Dragging : 0 Send Component ID : disabled Associated Keyboard: none - Text : 4.0.3dev + Text : 4.1dev3 Max. Text Size : 9 Text esph_version diff --git a/nspanel_us.HMI b/nspanel_us.HMI index 003eb78..58d78ef 100644 Binary files a/nspanel_us.HMI and b/nspanel_us.HMI differ diff --git a/nspanel_us.tft b/nspanel_us.tft index ec405d7..4d44b4b 100644 Binary files a/nspanel_us.tft and b/nspanel_us.tft differ diff --git a/nspanel_us_code/boot.txt b/nspanel_us_code/boot.txt index 9b066c5..0e15f3a 100644 --- a/nspanel_us_code/boot.txt +++ b/nspanel_us_code/boot.txt @@ -134,7 +134,7 @@ Text tft_version Dragging : 0 Send Component ID : disabled Associated Keyboard: none - Text : 4.0.3dev + Text : 4.1dev3 Max. Text Size : 9 Text esph_version diff --git a/nspanel_us_land.HMI b/nspanel_us_land.HMI index 8776054..2e88864 100644 Binary files a/nspanel_us_land.HMI and b/nspanel_us_land.HMI differ diff --git a/nspanel_us_land.tft b/nspanel_us_land.tft index fe43580..25706c3 100644 Binary files a/nspanel_us_land.tft and b/nspanel_us_land.tft differ diff --git a/nspanel_us_land_code/boot.txt b/nspanel_us_land_code/boot.txt index 9b066c5..0e15f3a 100644 --- a/nspanel_us_land_code/boot.txt +++ b/nspanel_us_land_code/boot.txt @@ -134,7 +134,7 @@ Text tft_version Dragging : 0 Send Component ID : disabled Associated Keyboard: none - Text : 4.0.3dev + Text : 4.1dev3 Max. Text Size : 9 Text esph_version