diff --git a/components/nspanel_easy/versioning.cpp b/components/nspanel_easy/versioning.cpp index 014e64db..144ec9e0 100644 --- a/components/nspanel_easy/versioning.cpp +++ b/components/nspanel_easy/versioning.cpp @@ -29,6 +29,43 @@ bool calver_gte(const std::string &version, const std::string &min_version) { return ver_s >= min_s; } +bool tft_ver_gte(const std::string &version, const std::string &min_version) { + int ver_maj = 0, ver_min = 0; + int min_maj = 0, min_min = 0; + + // Parse both version strings — accept "major.minor" or bare "major". + // After parsing, reject trailing garbage by checking that sscanf consumed + // the entire string: re-format what was parsed and compare against input. + // This rejects "16.", "16x", "16.1.2", etc. + // NOLINT suppresses cert-err34-c; structural validation is done below. + const int ver_fields = sscanf(version.c_str(), "%d.%d", &ver_maj, &ver_min); // NOLINT(cert-err34-c) + const int min_fields = sscanf(min_version.c_str(), "%d.%d", &min_maj, &min_min); // NOLINT(cert-err34-c) + + if (ver_fields < 1 || min_fields < 1) + return false; // Fail conservatively on empty or unparseable input + + // Reject partial matches by reconstructing the parsed value and comparing + // it against the original string — catches "16.", "16x", "16.1.2", etc. + char ver_buf[16], min_buf[16]; + if (ver_fields == 1) + snprintf(ver_buf, sizeof(ver_buf), "%d", ver_maj); + else + snprintf(ver_buf, sizeof(ver_buf), "%d.%d", ver_maj, ver_min); + if (min_fields == 1) + snprintf(min_buf, sizeof(min_buf), "%d", min_maj); + else + snprintf(min_buf, sizeof(min_buf), "%d.%d", min_maj, min_min); + + if (version != ver_buf || min_version != min_buf) + return false; // Trailing garbage or extra segments detected + + // Compare major first, then minor. + // Returns false conservatively on any older segment. + if (ver_maj != min_maj) + return ver_maj > min_maj; + return ver_min >= min_min; +} + } // namespace esphome::nspanel_easy #endif // NSPANEL_EASY_VERSIONING diff --git a/components/nspanel_easy/versioning.h b/components/nspanel_easy/versioning.h index 4d582e32..45a06a05 100644 --- a/components/nspanel_easy/versioning.h +++ b/components/nspanel_easy/versioning.h @@ -41,6 +41,37 @@ namespace esphome::nspanel_easy { */ bool calver_gte(const std::string &version, const std::string &min_version); +/** + * @brief Compare two 2-segment TFT version strings segment by segment. + * + * Compares two version strings in the format `major.minor` (e.g. "16.12") + * using numeric comparison per segment. This avoids the lexicographic ordering + * pitfall where "16.2" would incorrectly sort after "16.12" as strings. + * + * The comparison evaluates as: version >= min_version + * + * Both segments are optional — a bare integer (e.g. "16") is accepted and + * treated as `major.0`. + * + * @param version The version string to test (e.g. the TFT version received from the display). + * @param min_version The minimum required version string to compare against. + * @return true if version is equal to or newer than min_version. + * @return false if version is older than min_version, or if either string is malformed. + * + * @note Returns false conservatively when either string cannot be parsed, + * to avoid incorrectly passing a version check on bad input. + * + * @code + * tft_ver_gte("16.12", "16.2") // true — minor 12 > minor 2 + * tft_ver_gte("16.2", "16.12") // false — minor 2 < minor 12 + * tft_ver_gte("16.1", "16.1") // true — equal + * tft_ver_gte("17", "16.9") // true — major 17 > major 16 + * tft_ver_gte("16", "16.0") // true — treated as 16.0 >= 16.0 + * tft_ver_gte("", "16.1") // false — malformed input + * @endcode + */ +bool tft_ver_gte(const std::string &version, const std::string &min_version); + } // namespace esphome::nspanel_easy #endif // NSPANEL_EASY_VERSIONING diff --git a/esphome/nspanel_esphome_addon_upload_tft.yaml b/esphome/nspanel_esphome_addon_upload_tft.yaml index 6241b88f..e3cf6bbb 100644 --- a/esphome/nspanel_esphome_addon_upload_tft.yaml +++ b/esphome/nspanel_esphome_addon_upload_tft.yaml @@ -91,7 +91,7 @@ script: // Automatic upload is enabled #if NSPANEL_EASY_ADDON_UPLOAD_TFT_AUTOMATICALLY // Return if sensor has value and versions match (no update needed) - if (version_tft->state >= ${min_tft_version}) { + if (tft_ver_gte(version_tft->state, "${min_tft_version}")) { ESP_LOGV("${TAG_UPLOAD_TFT}", "Supported version"); return; } @@ -102,7 +102,7 @@ script: return; } - ESP_LOGI("${TAG_UPLOAD_TFT}", "Auto updating TFT from '%.1f' to '${min_tft_version}'", version_tft->state); + ESP_LOGI("${TAG_UPLOAD_TFT}", "Auto updating TFT from '%s' to '${min_tft_version}'", version_tft->state.c_str()); upload_tft->execute(""); #else // Automatic upload is disabled ESP_LOGV("${TAG_UPLOAD_TFT}", "Auto updating is disabled"); diff --git a/esphome/nspanel_esphome_hw_display.yaml b/esphome/nspanel_esphome_hw_display.yaml index 51af4ac3..d9db9b8e 100644 --- a/esphome/nspanel_esphome_hw_display.yaml +++ b/esphome/nspanel_esphome_hw_display.yaml @@ -207,8 +207,8 @@ script: ESP_LOGCONFIG("${TAG_HW_DISPLAY}", " Init: True"); } else ESP_LOGW("${TAG_HW_DISPLAY}", " Init: False"); - if (version_tft->state > 0) - ESP_LOGCONFIG("${TAG_HW_DISPLAY}", " TFT: %" PRIu16, static_cast(version_tft->state)); + if (!version_tft->state.empty()) + ESP_LOGCONFIG("${TAG_HW_DISPLAY}", " TFT: %s", version_tft->state.c_str()); else ESP_LOGW("${TAG_HW_DISPLAY}", " TFT: UNKNOWN"); @@ -540,7 +540,7 @@ script: count: 180 # seconds then: - lambda: |- - if (version_tft->state > 0) wait_for_version_tft->stop(); + if (!version_tft->state.empty()) wait_for_version_tft->stop(); if (iteration % 5 == 0) boot_log->execute("Boot", "Waiting for TFT version"); - delay: 1s @@ -557,7 +557,7 @@ script: - lambda: |- ESP_LOGV("${TAG_HW_DISPLAY}", "Wake-up (%s->%s)", page_names[current_page_id], page_names[wakeup_page_id]); ESP_LOGV("${TAG_HW_DISPLAY}", " reset timer: %s", YESNO(reset_timer)); - if (version_tft->state <= 0) { + if (version_tft->state.empty()) { ESP_LOGE("${TAG_HW_DISPLAY}", "TFT version invalid"); return; } diff --git a/esphome/nspanel_esphome_page_boot.yaml b/esphome/nspanel_esphome_page_boot.yaml index 4542babc..0c72cff1 100644 --- a/esphome/nspanel_esphome_page_boot.yaml +++ b/esphome/nspanel_esphome_page_boot.yaml @@ -64,24 +64,28 @@ script: disp1->send_command("tm_esphome.en=0"); // Parse display parameters - display_mode = atoi(params[2].c_str()); + display_mode = atoi(params[2].c_str()); display_charset = atoi(params[3].c_str()); - char *end = nullptr; - const float parsed_version_tft = strtof(params[4].c_str(), &end); + // Validate TFT version string — must be bare "major" or "major.minor", + // with both sides non-empty and no more than one dot. + const std::string &ver_str = params[4]; + const size_t dot = ver_str.find('.'); const bool tft_ver_valid = - end != nullptr && // strtof out-param was set - end != params[4].c_str() && // at least one character was consumed - *end == '\0' && // entire string was consumed (no trailing garbage) - parsed_version_tft > 0.0f; // must be positive - version_tft->publish_state(tft_ver_valid ? parsed_version_tft : NAN); + !ver_str.empty() && + ver_str.find_first_not_of("0123456789.") == std::string::npos && // digits and dot only + (dot == std::string::npos || // bare integer, no dot + (dot != 0 && // dot not at start + dot != ver_str.size() - 1 && // dot not at end + ver_str.find('.', dot + 1) == std::string::npos)); // no second dot + version_tft->publish_state(tft_ver_valid ? ver_str : ""); ESP_LOGI("${TAG_PAGE_BOOT}", "Display params:"); ESP_LOGI("${TAG_PAGE_BOOT}", " Mode: %" PRIu8, display_mode); ESP_LOGI("${TAG_PAGE_BOOT}", " Charset: %" PRIu8, display_charset); - ESP_LOGI("${TAG_PAGE_BOOT}", " TFT version: %.1f%s", parsed_version_tft, tft_ver_valid ? "" : " (INVALID)"); + ESP_LOGI("${TAG_PAGE_BOOT}", " TFT version: %s%s", ver_str.c_str(), tft_ver_valid ? "" : " (INVALID)"); - system_flags.display_settings_received = tft_ver_valid and display_mode > 0 and display_charset > 0; + system_flags.display_settings_received = tft_ver_valid && display_mode > 0 && display_charset > 0; } - id: page_boot @@ -163,8 +167,9 @@ script: - id: !extend watchdog_round then: - lambda: |- - if (current_page_id == get_page_id("boot") and version_tft->state > 0) - wakeup->execute(true); + if (current_page_id != get_page_id("boot")) return; + if (version_tft->state.empty()) return; + wakeup->execute(true); text_sensor: - id: !extend version_blueprint diff --git a/esphome/nspanel_esphome_version.yaml b/esphome/nspanel_esphome_version.yaml index de05a307..e17f29d6 100644 --- a/esphome/nspanel_esphome_version.yaml +++ b/esphome/nspanel_esphome_version.yaml @@ -65,7 +65,7 @@ script: ESP_LOGV("${TAG_VERSIONING}", " Versions:"); ESP_LOGV("${TAG_VERSIONING}", " Blueprint: %s", version_blueprint->state.c_str()); ESP_LOGV("${TAG_VERSIONING}", " ESPHome: ${version}"); - ESP_LOGV("${TAG_VERSIONING}", " TFT: %.1f", version_tft->state); + ESP_LOGV("${TAG_VERSIONING}", " TFT: %s", version_tft->state.c_str()); if (reset_flag) system_flags.version_check_ok = false; if (system_flags.version_check_ok) { ESP_LOGD("${TAG_VERSIONING}", "VERSION_CHECK_OK is already set"); @@ -82,7 +82,7 @@ script: ESP_LOGD("${TAG_VERSIONING}", "Versions:"); ESP_LOGD("${TAG_VERSIONING}", " Blueprint: %s", version_blueprint->state.c_str()); ESP_LOGD("${TAG_VERSIONING}", " ESPHome: ${version}"); - ESP_LOGD("${TAG_VERSIONING}", " TFT: %.1f", version_tft->state); + ESP_LOGD("${TAG_VERSIONING}", " TFT: %s", version_tft->state.c_str()); system_flags.version_check_ok = true; if (!calver_gte(version_blueprint->state, "${min_blueprint_version}")) { ESP_LOGW("${TAG_VERSIONING}", "Blueprint version mismatch!"); @@ -102,9 +102,9 @@ script: ESP_LOGE("${TAG_VERSIONING}", "https://github.com/edwardtfn/NSPanel-Easy/blob/main/docs/howto.md#update-blueprint"); } } - if (version_tft->state < ${min_tft_version}) { + if (!tft_ver_gte(version_tft->state, "${min_tft_version}")) { ESP_LOGW("${TAG_VERSIONING}", "TFT version mismatch!"); - if (version_tft->state > 0) { + if (!version_tft->state.empty()) { ESP_LOGE("${TAG_VERSIONING}", "TFT v${min_tft_version} is required"); system_flags.version_check_ok = false; } @@ -114,12 +114,7 @@ script: {"blueprint", version_blueprint->state.c_str() }, {"min_blueprint_version", "${min_blueprint_version}"}, {"esphome", "${version}"}, - {"tft", [&]() -> std::string { - if (std::isnan(version_tft->state)) return "0"; - char buf[16]; - snprintf(buf, sizeof(buf), "%.1f", version_tft->state); - return buf; - }()}, + {"tft", version_tft->state.c_str() }, {"min_tft_version", "${min_tft_version}"}, #ifdef NSPANEL_EASY_ADDON_UPLOAD_TFT {"upload_tft_installed", "true"}, @@ -185,18 +180,6 @@ script: - script.execute: wait_for_version_blueprint - script.wait: wait_for_version_blueprint -sensor: - - id: version_tft - name: "Version - TFT" - platform: template - entity_category: diagnostic - icon: mdi:tag-text-outline - internal: false - update_interval: never - accuracy_decimals: 1 - # Initial state is empty, which HA renders as unavailable. - # Published by nspanel_esphome_page_boot when the TFT version is received from the display. - text_sensor: - id: version_esphome name: "Version - ESPHome" @@ -219,9 +202,22 @@ text_sensor: on_value: then: - lambda: |- - ESP_LOGI("${TAG_VERSIONING}", "Blueprint version: %s", version_blueprint->state.c_str()); - blueprint_status_flags.version = (calver_gte(version_blueprint->state, "${min_blueprint_version}")); - check_versions->execute(!blueprint_status_flags.version - || (version_tft->state <= 0) - || std::isnan(version_tft->state)); + ESP_LOGI("${TAG_VERSIONING}", "Blueprint version: %s", x.c_str()); + blueprint_status_flags.version = (calver_gte(x, "${min_blueprint_version}")); + check_versions->execute(!blueprint_status_flags.version || version_tft->state.empty()); + + - id: version_tft + name: "Version - TFT" + platform: template + entity_category: diagnostic + icon: mdi:tag-text-outline + internal: false + update_interval: never + on_value: + then: + - lambda: |- + ESP_LOGI("${TAG_VERSIONING}", "TFT version: %s", x.c_str()); + check_versions->execute(!blueprint_status_flags.version || x.empty()); + # Initial state is empty, which HA renders as unavailable. + # Published by nspanel_esphome_page_boot when the TFT version is received from the display. ... diff --git a/nspanel_easy_blueprint.yaml b/nspanel_easy_blueprint.yaml index ada8af98..73392643 100644 --- a/nspanel_easy_blueprint.yaml +++ b/nspanel_easy_blueprint.yaml @@ -6,7 +6,7 @@ ################################################################################################## --- blueprint: - name: NSPanel Easy Configuration (v2026.4.6) + name: NSPanel Easy Configuration (v9999.99.9) author: Edward Firmo (https://github.com/edwardtfn) description: > # NSPanel Easy Configuration via Blueprint @@ -4044,7 +4044,7 @@ trigger_variables: }} variables: - blueprint_version: 2026.4.6 + blueprint_version: 9999.99.9 pages: current: '{{ states(currentpage) }}' alarm: "alarm"