Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions components/nspanel_easy/versioning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// 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
31 changes: 31 additions & 0 deletions components/nspanel_easy/versioning.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions esphome/nspanel_esphome_addon_upload_tft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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");
Expand Down
8 changes: 4 additions & 4 deletions esphome/nspanel_esphome_hw_display.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint16_t>(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");

Expand Down Expand Up @@ -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

Expand All @@ -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;
}
Expand Down
29 changes: 17 additions & 12 deletions esphome/nspanel_esphome_page_boot.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

- id: page_boot
Expand Down Expand Up @@ -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
Expand Down
50 changes: 23 additions & 27 deletions esphome/nspanel_esphome_version.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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!");
Expand All @@ -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;
}
Expand All @@ -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"},
Expand Down Expand Up @@ -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"
Expand All @@ -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.
...
4 changes: 2 additions & 2 deletions nspanel_easy_blueprint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -4044,7 +4044,7 @@ trigger_variables:
}}

variables:
blueprint_version: 2026.4.6
blueprint_version: 9999.99.9
pages:
current: '{{ states(currentpage) }}'
alarm: "alarm"
Expand Down
Loading