Skip to content

Commit b736abc

Browse files
fhirschmannclaude
andcommitted
feat: rolling build version (rN) with navbar up-to-date badge
gitVersion.py now embeds a rolling build revision (rN from the commit count) alongside the git describe; the release workflow publishes it in version.json. A passive /version endpoint compares the running build to the latest rolling release; the web UI shows the build as a navbar badge (green = up to date, amber = update available, dim = unknown) and lists it in the info dialog. New nav.updateAvailable / systeminfo.lblBuild locale keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent bf3c03d commit b736abc

8 files changed

Lines changed: 148 additions & 4 deletions

File tree

.github/workflows/release-firmware.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ jobs:
6060
# `describe` must match what the firmware embeds (gitVersion.py uses the same command),
6161
# so the device can tell whether it already runs this build.
6262
DESCRIBE="$(git describe --always --dirty)"
63-
printf '{"version":"%s","short":"%s","describe":"%s","date":"%s","ref":"%s"}\n' \
64-
"$GITHUB_SHA" "${GITHUB_SHA:0:7}" "$DESCRIBE" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$GITHUB_REF_NAME" \
63+
# rolling build revision (commit count), same scheme as gitVersion.py -> "r249"
64+
BUILD="r$(git rev-list --count HEAD)"
65+
printf '{"version":"%s","short":"%s","describe":"%s","build":"%s","date":"%s","ref":"%s"}\n' \
66+
"$GITHUB_SHA" "${GITHUB_SHA:0:7}" "$DESCRIBE" "$BUILD" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$GITHUB_REF_NAME" \
6567
> release/version.json
6668
cat release/version.json
6769

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
9595
| Bluetooth-mode dropdown in the control tab (Normal / Speaker / Headphones), mirroring the sleep-timer dropdown; selecting a mode restarts the device into it and the button highlights the active BT mode | [`d57f24a`](../../commit/d57f24a) |
9696
| Mobile-optimized control tab: the hard-to-drag volume slider is replaced by full-width louder/quieter (+ EQ) buttons on phones, and the control header stacks so the legend stays on one line with the action buttons below | [`aef1765`](../../commit/aef1765) |
9797
| System-information dialog rendered as a clean property/value table (instead of preformatted text) and extended with the PN5180 RFID-reader firmware version (read once from the reader's EEPROM at init and exposed via `/info`) | [`715d867`](../../commit/715d867) |
98+
| Rolling build version (`rN` from the commit count, embedded via `gitVersion.py` and published in the release `version.json`) shown as a navbar badge — green when the device runs the latest rolling release, amber when an update is available (passive `/version` check); also listed in the info dialog | [`VERSIONHASH`](../../commit/VERSIONHASH) |
9899

99100
#### Feature highlights
100101

gitVersion.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#define __GIT_REVISION_H__
2424
constexpr const char gitRevision[] = "Git-revision: {git_revision}";
2525
constexpr const char gitRevShort[] = "\\"{git_revision}\\"";
26+
constexpr const char buildRevision[] = "{build_revision}";
2627
#endif
2728
"""
2829

@@ -42,14 +43,31 @@ def git_revision():
4243
return "unknown"
4344

4445

46+
def build_revision():
47+
"""Returns a rolling build revision like 'r249' (commit count), '-dirty' if applicable."""
48+
try:
49+
count = subprocess.check_output(
50+
["git", "rev-list", "--count", "HEAD"],
51+
text=True,
52+
stderr=subprocess.PIPE,
53+
).strip()
54+
dirty = subprocess.run(["git", "diff", "--quiet"]).returncode != 0
55+
return f"r{count}{'-dirty' if dirty else ''}"
56+
except (subprocess.CalledProcessError, OSError):
57+
return "r0"
58+
59+
4560
def generate():
4661
"""Generates header file."""
4762
print("GENERATING GIT REVISION HEADER FILE")
4863
gitrev = git_revision()
49-
print(f' "{gitrev}" -> {OUTPUT_PATH}')
64+
buildrev = build_revision()
65+
print(f' "{gitrev}" ({buildrev}) -> {OUTPUT_PATH}')
5066
OUTPUT_PATH.parent.mkdir(exist_ok=True, parents=True)
5167
with OUTPUT_PATH.open("w") as output_file:
52-
output_file.write(TEMPLATE.format(git_revision=gitrev))
68+
output_file.write(
69+
TEMPLATE.format(git_revision=gitrev, build_revision=buildrev)
70+
)
5371

5472

5573
generate()

html/locales/de.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"saved": "Einstellungen gespeichert."
2828
},
2929
"nav": {
30+
"updateAvailable": "Update verfügbar",
3031
"control": "Steuerung",
3132
"rfid": "Dateien",
3233
"wifi": "WLAN",
@@ -596,6 +597,7 @@
596597
},
597598
"systeminfo": {
598599
"lblSoftware": "Software",
600+
"lblBuild": "Build",
599601
"lblGit": "Git-Revision",
600602
"lblEnv": "Arduino / ESP-IDF",
601603
"lblChip": "Chip",

html/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"saved": "Settings saved."
2828
},
2929
"nav": {
30+
"updateAvailable": "Update available",
3031
"control": "Control",
3132
"rfid": "Files",
3233
"wifi": "WiFi",
@@ -598,6 +599,7 @@
598599
},
599600
"systeminfo": {
600601
"lblSoftware": "Software",
602+
"lblBuild": "Build",
601603
"lblGit": "Git revision",
602604
"lblEnv": "Arduino / ESP-IDF",
603605
"lblChip": "Chip",

html/locales/fr.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"saved": "Paramètres enregistrés."
2828
},
2929
"nav": {
30+
"updateAvailable": "Mise à jour disponible",
3031
"control": "Contrôle",
3132
"rfid": "Fichiers",
3233
"wifi": "WiFi",
@@ -585,6 +586,7 @@
585586
},
586587
"systeminfo": {
587588
"lblSoftware": "Logiciel",
589+
"lblBuild": "Build",
588590
"lblGit": "Révision Git",
589591
"lblEnv": "Arduino / ESP-IDF",
590592
"lblChip": "Puce",

html/management.html

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,37 @@
226226
color: #ff2a6d;
227227
}
228228

229+
/* navbar firmware-version badge (green = up to date, amber = update available) */
230+
.cp-version-badge {
231+
font-family: "Share Tech Mono", monospace;
232+
font-size: .8rem;
233+
letter-spacing: .05em;
234+
padding: 1px 8px;
235+
margin-left: 8px;
236+
border-radius: 4px;
237+
border: 1px solid transparent;
238+
text-decoration: none;
239+
white-space: nowrap;
240+
align-self: center;
241+
}
242+
243+
.cp-version-badge.up-to-date {
244+
color: var(--cp-green);
245+
border-color: rgba(10, 255, 157, .45);
246+
text-shadow: 0 0 8px rgba(10, 255, 157, .5);
247+
}
248+
249+
.cp-version-badge.update-available {
250+
color: var(--cp-yellow);
251+
border-color: rgba(252, 238, 10, .45);
252+
text-shadow: 0 0 8px rgba(252, 238, 10, .5);
253+
}
254+
255+
.cp-version-badge.unknown {
256+
color: var(--cp-dim);
257+
border-color: var(--cp-border);
258+
}
259+
229260
#menuToggle {
230261
color: var(--cp-cyan) !important;
231262
text-shadow: 0 0 8px rgba(0, 240, 255, .6);
@@ -1401,6 +1432,7 @@
14011432
<span id="navbar-brand-co" class="d-none d-md-inline">Leo Industries AT-1</span>
14021433
<span id="navbar-heading" class="d-none d-md-inline" data-i18n="title"></span>
14031434
</a>
1435+
<a class="navbar-text cp-version-badge unknown" id="versionBadge" href="https://github.com/fhirschmann/leoino/releases" target="_blank" rel="noopener" style="display:none;"></a>
14041436
<div class="collapse navbar-collapse justify-content-end">
14051437
<span class="navbar-text" id="connLost" data-i18n="[title]toast.conlost"><i class="fas fa-link-slash"></i></span>
14061438
<span class="navbar-text" id="actionOk" aria-hidden="true"><i class="fas fa-check"></i> OK</span>
@@ -3846,6 +3878,37 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
38463878
}
38473879
}
38483880

3881+
// Fetch the running build + rolling-release up-to-date state and reflect it in the navbar badge.
3882+
async function refreshVersionBadge(retries) {
3883+
const el = document.getElementById('versionBadge');
3884+
if (!el) return;
3885+
let v;
3886+
try {
3887+
v = await (await fetch("http://" + host + "/version", { cache: "no-store" })).json();
3888+
} catch (e) {
3889+
return;
3890+
}
3891+
if (!v || !v.build) return;
3892+
el.style.display = '';
3893+
el.classList.remove('up-to-date', 'update-available', 'unknown');
3894+
if (v.upToDate === 1) {
3895+
el.classList.add('up-to-date');
3896+
el.textContent = v.build;
3897+
el.title = i18next.t("tools.fwupdate.github.uptodate", "Up to date");
3898+
} else if (v.upToDate === 0) {
3899+
el.classList.add('update-available');
3900+
el.textContent = v.build + " ⬆";
3901+
el.title = i18next.t("nav.updateAvailable", "Update available") + (v.latest ? " (" + v.latest + ")" : "");
3902+
} else {
3903+
el.classList.add('unknown');
3904+
el.textContent = v.build;
3905+
el.title = v.build;
3906+
if ((retries || 0) < 5) {
3907+
setTimeout(function () { refreshVersionBadge((retries || 0) + 1); }, 4000);
3908+
}
3909+
}
3910+
}
3911+
38493912
function updateOperationModeIndicator(opmode) {
38503913
const btStartSource = document.getElementById('btStartSource');
38513914
const btStartSink = document.getElementById('btStartSink');
@@ -3912,6 +3975,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
39123975
socket.send('{"ssids":{"ssids":"ssids"}}'); // get ssids
39133976
socket.send('{"trackinfo":{"trackinfo":"trackinfo"}}'); // get trackinfo
39143977
socket.send('{"coverimg":{"coverimg":"coverimg"}}'); // get cover image
3978+
refreshVersionBadge(0); // update the navbar firmware-version badge
39153979
};
39163980
socket.onclose = function (e) {
39173981
console.log('Socket is closed. Reconnect will be attempted in 5 seconds.', e.reason);
@@ -5474,6 +5538,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
54745538
};
54755539

54765540
add('systeminfo.lblSoftware', info.software.version);
5541+
add('systeminfo.lblBuild', info.software.build);
54775542
add('systeminfo.lblGit', info.software.git);
54785543
add('systeminfo.lblEnv', info.software.arduino + ' / ' + info.software.idf);
54795544
add('systeminfo.lblChip', info.hardware.model + ' (rev ' + info.hardware.revision + ')');

src/Web.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,47 @@ void Web_TriggerGithubOta(void) {
636636
#endif
637637
}
638638

639+
// Passive "is this build the latest release?" check, used only for the UI version badge
640+
// (independent of the OTA flasher). -1 = unknown/not yet checked, 0 = update available, 1 = up to date.
641+
static volatile int8_t gFirmwareUpToDate = -1;
642+
static char gLatestBuild[24] = "";
643+
644+
// Fetches the rolling release's version.json and compares it to the running build (background task).
645+
static void versionCheckTask(void *parameter) {
646+
WiFiClientSecure client;
647+
client.setInsecure();
648+
HTTPClient http;
649+
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
650+
http.setConnectTimeout(8000);
651+
if (http.begin(client, githubVersionUrl)) {
652+
if (http.GET() == 200) {
653+
JsonDocument doc;
654+
if (deserializeJson(doc, http.getString()) == DeserializationError::Ok) {
655+
String latest = doc["describe"].as<String>();
656+
String latestBuild = doc["build"].as<String>();
657+
String current = String(gitRevShort);
658+
current.replace("\"", "");
659+
if (latest.length() > 0) {
660+
gFirmwareUpToDate = (latest == current) ? 1 : 0;
661+
}
662+
if (latestBuild.length() > 0) {
663+
strncpy(gLatestBuild, latestBuild.c_str(), sizeof(gLatestBuild) - 1);
664+
gLatestBuild[sizeof(gLatestBuild) - 1] = '\0';
665+
}
666+
}
667+
}
668+
http.end();
669+
}
670+
vTaskDelete(NULL);
671+
}
672+
673+
// Kick off a background version check (no-op on boards without OTA support).
674+
void Web_CheckForUpdate(void) {
675+
#ifdef BOARD_HAS_16MB_FLASH_AND_OTA_SUPPORT
676+
xTaskCreatePinnedToCore(versionCheckTask, "verCheck", 8192, NULL, 1, NULL, 1);
677+
#endif
678+
}
679+
639680
void webserverStart(void) {
640681
if (!webserverStarted && (Wlan_IsConnected() || (WiFi.getMode() == WIFI_AP))) {
641682
// password protection (no-op when no password is set or in accesspoint-mode)
@@ -793,6 +834,15 @@ void webserverStart(void) {
793834
snprintf(buf, sizeof(buf), "{\"status\":%u,\"progress\":%u,\"message\":\"%s\"}", gGithubOtaStatus, gGithubOtaProgress, gGithubOtaMsg);
794835
request->send(200, "application/json", buf);
795836
});
837+
// running build + rolling-release up-to-date state, used for the navbar version badge
838+
wServer.on("/version", HTTP_GET, [](AsyncWebServerRequest *request) {
839+
if (gFirmwareUpToDate == -1) {
840+
Web_CheckForUpdate(); // lazy first check
841+
}
842+
char buf[160];
843+
snprintf(buf, sizeof(buf), "{\"build\":\"%s\",\"upToDate\":%d,\"latest\":\"%s\"}", buildRevision, (int) gFirmwareUpToDate, gLatestBuild);
844+
request->send(200, "application/json", buf);
845+
});
796846

797847
// ESP-restart
798848
wServer.on("/restart", HTTP_POST, [](AsyncWebServerRequest *request) {
@@ -977,6 +1027,7 @@ void webserverStart(void) {
9771027
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
9781028
wServer.begin();
9791029
webserverStarted = true;
1030+
Web_CheckForUpdate(); // populate the version badge in the background
9801031
Log_Println(httpReady, LOGLEVEL_NOTICE);
9811032
// start a first WiFi scan (to get a WiFi list more quickly in webview)
9821033
WiFi.scanNetworks(true, false, true, 120);
@@ -1703,6 +1754,7 @@ void handleGetInfo(AsyncWebServerRequest *request) {
17031754
JsonObject softwareObj = infoObj["software"].to<JsonObject>();
17041755
softwareObj["version"] = (String) softwareRevision;
17051756
softwareObj["git"] = (String) gitRevision;
1757+
softwareObj["build"] = (String) buildRevision;
17061758
softwareObj["arduino"] = String(ESP_ARDUINO_VERSION_MAJOR) + "." + String(ESP_ARDUINO_VERSION_MINOR) + "." + String(ESP_ARDUINO_VERSION_PATCH);
17071759
softwareObj["idf"] = String(ESP.getSdkVersion());
17081760
}

0 commit comments

Comments
 (0)