Inject commit hash to FW, show on frontend
This commit is contained in:
@@ -25,6 +25,9 @@ upload_speed = 115200
|
|||||||
; Uncomment and set the correct port if auto-detect fails:
|
; Uncomment and set the correct port if auto-detect fails:
|
||||||
; upload_port = COM3
|
; upload_port = COM3
|
||||||
|
|
||||||
|
; ── Build scripts ─────────────────────────────────────────────────────────────
|
||||||
|
extra_scripts = pre:scripts/git_hash.py
|
||||||
|
|
||||||
; ── Build flags ───────────────────────────────────────────────────────────────
|
; ── Build flags ───────────────────────────────────────────────────────────────
|
||||||
build_flags =
|
build_flags =
|
||||||
-DARDUINO_Seeed_XIAO_nRF52840_Sense
|
-DARDUINO_Seeed_XIAO_nRF52840_Sense
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
PlatformIO pre-build script: injects the short git commit hash into firmware
|
||||||
|
as GIT_HASH (a C string literal), and writes web/version.js so the web UI
|
||||||
|
can compare against it.
|
||||||
|
|
||||||
|
Usage: referenced from platformio.ini as:
|
||||||
|
extra_scripts = pre:scripts/git_hash.py
|
||||||
|
"""
|
||||||
|
import subprocess, os, re
|
||||||
|
|
||||||
|
Import("env") # noqa: F821 — PlatformIO injects this
|
||||||
|
|
||||||
|
def get_git_hash():
|
||||||
|
try:
|
||||||
|
h = subprocess.check_output(
|
||||||
|
["git", "rev-parse", "--short=7", "HEAD"],
|
||||||
|
cwd=env.subst("$PROJECT_DIR"), # noqa: F821
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
).decode().strip()
|
||||||
|
# Verify it looks like a hex hash (safety check)
|
||||||
|
if re.fullmatch(r"[0-9a-f]{4,12}", h):
|
||||||
|
return h
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
git_hash = get_git_hash()
|
||||||
|
print(f"[git_hash] short hash = {git_hash}")
|
||||||
|
|
||||||
|
# ── Inject into firmware build ────────────────────────────────────────────────
|
||||||
|
env.Append(CPPDEFINES=[("GIT_HASH", f'\\"{git_hash}\\"')]) # noqa: F821
|
||||||
|
|
||||||
|
# ── Write web/version.js ──────────────────────────────────────────────────────
|
||||||
|
web_dir = os.path.join(env.subst("$PROJECT_DIR"), "web") # noqa: F821
|
||||||
|
ver_file = os.path.join(web_dir, "version.js")
|
||||||
|
os.makedirs(web_dir, exist_ok=True)
|
||||||
|
with open(ver_file, "w") as f:
|
||||||
|
f.write(f"// Auto-generated by scripts/git_hash.py — do not edit\n")
|
||||||
|
f.write(f"const FIRMWARE_BUILD_HASH = '{git_hash}';\n")
|
||||||
|
print(f"[git_hash] wrote {ver_file}")
|
||||||
@@ -7,10 +7,15 @@ using namespace Adafruit_LittleFS_Namespace;
|
|||||||
extern File cfgFile;
|
extern File cfgFile;
|
||||||
|
|
||||||
// ─── BLE Config Service objects ───────────────────────────────────────────────
|
// ─── BLE Config Service objects ───────────────────────────────────────────────
|
||||||
|
#ifndef GIT_HASH
|
||||||
|
#define GIT_HASH "unknown"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef FEATURE_CONFIG_SERVICE
|
#ifdef FEATURE_CONFIG_SERVICE
|
||||||
BLEService cfgService (0x1234);
|
BLEService cfgService (0x1234);
|
||||||
BLECharacteristic cfgBlob (0x1235); // ConfigBlob R/W 20 bytes
|
BLECharacteristic cfgBlob (0x1235); // ConfigBlob R/W 20 bytes
|
||||||
BLECharacteristic cfgCommand (0x1236); // Command W 1 byte
|
BLECharacteristic cfgCommand (0x1236); // Command W 1 byte
|
||||||
|
BLECharacteristic cfgGitHash (0x1239); // GitHash R 8 bytes (7-char hash + NUL)
|
||||||
#ifdef FEATURE_TELEMETRY
|
#ifdef FEATURE_TELEMETRY
|
||||||
BLECharacteristic cfgTelemetry(0x1237); // Telemetry R/N 24 bytes
|
BLECharacteristic cfgTelemetry(0x1237); // Telemetry R/N 24 bytes
|
||||||
#endif
|
#endif
|
||||||
@@ -148,6 +153,14 @@ void setupConfigService() {
|
|||||||
cfgCommand.setWriteCallback(onCommandWrite);
|
cfgCommand.setWriteCallback(onCommandWrite);
|
||||||
cfgCommand.begin();
|
cfgCommand.begin();
|
||||||
|
|
||||||
|
// Git hash — 8-byte fixed field (7 hex chars + NUL), read-only
|
||||||
|
cfgGitHash.setProperties(CHR_PROPS_READ);
|
||||||
|
cfgGitHash.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
|
||||||
|
cfgGitHash.setFixedLen(8);
|
||||||
|
cfgGitHash.begin();
|
||||||
|
{ uint8_t buf[8] = {}; strncpy((char*)buf, GIT_HASH, 7); cfgGitHash.write(buf, 8); }
|
||||||
|
Serial.print("[BUILD] git="); Serial.println(GIT_HASH);
|
||||||
|
|
||||||
#ifdef FEATURE_TELEMETRY
|
#ifdef FEATURE_TELEMETRY
|
||||||
cfgTelemetry.setProperties(CHR_PROPS_READ | CHR_PROPS_NOTIFY);
|
cfgTelemetry.setProperties(CHR_PROPS_READ | CHR_PROPS_NOTIFY);
|
||||||
cfgTelemetry.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
|
cfgTelemetry.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
extern BLEService cfgService;
|
extern BLEService cfgService;
|
||||||
extern BLECharacteristic cfgBlob;
|
extern BLECharacteristic cfgBlob;
|
||||||
extern BLECharacteristic cfgCommand;
|
extern BLECharacteristic cfgCommand;
|
||||||
|
extern BLECharacteristic cfgGitHash;
|
||||||
#ifdef FEATURE_TELEMETRY
|
#ifdef FEATURE_TELEMETRY
|
||||||
extern BLECharacteristic cfgTelemetry;
|
extern BLECharacteristic cfgTelemetry;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
// ─── ATT table size ───────────────────────────────────────────────────────────
|
// ─── ATT table size ───────────────────────────────────────────────────────────
|
||||||
#define _ATT_BASE 900
|
#define _ATT_BASE 900
|
||||||
#ifdef FEATURE_CONFIG_SERVICE
|
#ifdef FEATURE_CONFIG_SERVICE
|
||||||
#define _ATT_CFG 80
|
#define _ATT_CFG 100 // +20 for cfgGitHash characteristic
|
||||||
#else
|
#else
|
||||||
#define _ATT_CFG 0
|
#define _ATT_CFG 0
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+47
-1
@@ -2,10 +2,11 @@
|
|||||||
// v3.3: 4 characteristics instead of 10
|
// v3.3: 4 characteristics instead of 10
|
||||||
const SVC_UUID = '00001234-0000-1000-8000-00805f9b34fb';
|
const SVC_UUID = '00001234-0000-1000-8000-00805f9b34fb';
|
||||||
const CHR = {
|
const CHR = {
|
||||||
configBlob: '00001235-0000-1000-8000-00805f9b34fb', // ConfigBlob R/W 16 bytes
|
configBlob: '00001235-0000-1000-8000-00805f9b34fb', // ConfigBlob R/W 20 bytes
|
||||||
command: '00001236-0000-1000-8000-00805f9b34fb', // Command W 1 byte
|
command: '00001236-0000-1000-8000-00805f9b34fb', // Command W 1 byte
|
||||||
telemetry: '00001237-0000-1000-8000-00805f9b34fb', // Telemetry R/N 24 bytes
|
telemetry: '00001237-0000-1000-8000-00805f9b34fb', // Telemetry R/N 24 bytes
|
||||||
imuStream: '00001238-0000-1000-8000-00805f9b34fb', // ImuStream N 14 bytes
|
imuStream: '00001238-0000-1000-8000-00805f9b34fb', // ImuStream N 14 bytes
|
||||||
|
gitHash: '00001239-0000-1000-8000-00805f9b34fb', // GitHash R 8 bytes
|
||||||
};
|
};
|
||||||
|
|
||||||
// Local shadow of the current config (kept in sync with device)
|
// Local shadow of the current config (kept in sync with device)
|
||||||
@@ -113,10 +114,14 @@ async function discoverServices() {
|
|||||||
chars.command = await svc.getCharacteristic(CHR.command);
|
chars.command = await svc.getCharacteristic(CHR.command);
|
||||||
chars.telemetry = await svc.getCharacteristic(CHR.telemetry);
|
chars.telemetry = await svc.getCharacteristic(CHR.telemetry);
|
||||||
chars.imuStream = await svc.getCharacteristic(CHR.imuStream);
|
chars.imuStream = await svc.getCharacteristic(CHR.imuStream);
|
||||||
|
chars.gitHash = await svc.getCharacteristic(CHR.gitHash);
|
||||||
|
|
||||||
// Read config blob and populate UI
|
// Read config blob and populate UI
|
||||||
await readConfigBlob();
|
await readConfigBlob();
|
||||||
|
|
||||||
|
// Read firmware git hash and check against web build hash
|
||||||
|
await checkHashMatch();
|
||||||
|
|
||||||
// Telemetry notify (1 Hz) — also carries chargeStatus
|
// Telemetry notify (1 Hz) — also carries chargeStatus
|
||||||
chars.telemetry.addEventListener('characteristicvaluechanged', e => parseTelemetry(e.target.value));
|
chars.telemetry.addEventListener('characteristicvaluechanged', e => parseTelemetry(e.target.value));
|
||||||
await chars.telemetry.startNotifications();
|
await chars.telemetry.startNotifications();
|
||||||
@@ -149,6 +154,46 @@ async function discoverServices() {
|
|||||||
} catch(e) { log('Battery service unavailable','warn'); }
|
} catch(e) { log('Battery service unavailable','warn'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Firmware / web hash mismatch banner ──────────────────────────────────────
|
||||||
|
async function checkHashMatch() {
|
||||||
|
const banner = document.getElementById('hashMismatchBanner');
|
||||||
|
if (!chars.gitHash) return;
|
||||||
|
let fwHash = 'unknown';
|
||||||
|
try {
|
||||||
|
const dv = await chars.gitHash.readValue();
|
||||||
|
const bytes = new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength);
|
||||||
|
// Find NUL terminator or use full length
|
||||||
|
let end = bytes.indexOf(0);
|
||||||
|
if (end === -1) end = bytes.length;
|
||||||
|
fwHash = new TextDecoder().decode(bytes.subarray(0, end));
|
||||||
|
} catch(e) { log(`Hash read failed: ${e.message}`, 'warn'); }
|
||||||
|
|
||||||
|
// FIRMWARE_BUILD_HASH comes from web/version.js (written by scripts/git_hash.py at build time)
|
||||||
|
const webHash = (typeof FIRMWARE_BUILD_HASH !== 'undefined') ? FIRMWARE_BUILD_HASH : 'unknown';
|
||||||
|
|
||||||
|
log(`Firmware hash: ${fwHash} · Web hash: ${webHash}`, fwHash === webHash ? 'ok' : 'warn');
|
||||||
|
|
||||||
|
if (fwHash === 'unknown' || webHash === 'unknown' || fwHash === webHash) {
|
||||||
|
banner.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
banner.style.cssText = [
|
||||||
|
'display:flex', 'align-items:center', 'justify-content:center', 'gap:12px',
|
||||||
|
'background:#7a2020', 'color:#ffd0d0', 'font-family:var(--mono)',
|
||||||
|
'font-size:11px', 'padding:6px 16px', 'border-bottom:1px solid #c04040',
|
||||||
|
'position:sticky', 'top:0', 'z-index:100',
|
||||||
|
].join(';');
|
||||||
|
banner.innerHTML =
|
||||||
|
`<span style="font-size:14px">⚠</span>` +
|
||||||
|
`<span>FIRMWARE / WEB MISMATCH — ` +
|
||||||
|
`firmware <b>${fwHash}</b> · web <b>${webHash}</b> — ` +
|
||||||
|
`flash firmware or reload the page after a <code>pio run</code></span>` +
|
||||||
|
`<button onclick="document.getElementById('hashMismatchBanner').style.display='none'" ` +
|
||||||
|
`style="margin-left:8px;background:none;border:1px solid #c04040;color:#ffd0d0;` +
|
||||||
|
`cursor:pointer;padding:2px 8px;font-family:var(--mono);font-size:10px">✕</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
// ── ConfigBlob read / write ──────────────────────────────────────────────────
|
// ── ConfigBlob read / write ──────────────────────────────────────────────────
|
||||||
// ConfigBlob layout (20 bytes LE):
|
// ConfigBlob layout (20 bytes LE):
|
||||||
// float sensitivity [0], float deadZone [4], float accelStrength [8]
|
// float sensitivity [0], float deadZone [4], float accelStrength [8]
|
||||||
@@ -419,6 +464,7 @@ function onDisconnected() {
|
|||||||
document.getElementById('badgeCharging').classList.remove('show');
|
document.getElementById('badgeCharging').classList.remove('show');
|
||||||
document.getElementById('badgeFull').classList.remove('show');
|
document.getElementById('badgeFull').classList.remove('show');
|
||||||
imuSubscribed = false; vizPaused = true; vizUpdateIndicator(); streamDiagReset();
|
imuSubscribed = false; vizPaused = true; vizUpdateIndicator(); streamDiagReset();
|
||||||
|
document.getElementById('hashMismatchBanner').style.display = 'none';
|
||||||
clearTelemetry();
|
clearTelemetry();
|
||||||
if (!userDisconnected && document.getElementById('autoReconnect').checked && savedDevice) {
|
if (!userDisconnected && document.getElementById('autoReconnect').checked && savedDevice) {
|
||||||
log('Auto-reconnecting…','info');
|
log('Auto-reconnecting…','info');
|
||||||
|
|||||||
@@ -7,9 +7,12 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Barlow+Condensed:wght@300;400;600;700;900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Barlow+Condensed:wght@300;400;600;700;900&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
|
||||||
|
<script src="version.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="disconnected">
|
<body class="disconnected">
|
||||||
|
|
||||||
|
<div id="hashMismatchBanner" style="display:none"></div>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
<div class="logo">IMU<span>·</span>Mouse</div>
|
<div class="logo">IMU<span>·</span>Mouse</div>
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Auto-generated by scripts/git_hash.py — do not edit
|
||||||
|
const FIRMWARE_BUILD_HASH = 'a3b5425';
|
||||||
Reference in New Issue
Block a user