diff --git a/source/battery.cpp b/source/battery.cpp index 44a9562..bd87788 100644 --- a/source/battery.cpp +++ b/source/battery.cpp @@ -20,8 +20,10 @@ void initBatteryADC() { float readBatteryVoltage() { // 8 quick reads, no delay() calls, no analogReference() change int32_t raw=0; for (int i=0; i<8; i++) raw += analogRead(PIN_VBAT_READ); raw /= 8; - // Seeed XIAO nRF52840 Sense: 1MΩ + 510kΩ voltage divider on VBAT → multiply by 1510/510 - return (raw / 4096.0f) * 3.0f * (1510.0f / 510.0f); + // Seeed XIAO nRF52840 Sense: 1MΩ + 510kΩ voltage divider on VBAT. + // Theoretical ratio is 1510/510 = 2.961, but real resistor tolerances + // shift the actual ratio. Calibrated: 3.90V actual / 3.78V reported → ×1.0317. + return (raw / 4096.0f) * 3.0f * (1510.0f / 510.0f) * 1.0317f; } int batteryPercent(float v) { diff --git a/source/ble_config.cpp b/source/ble_config.cpp index d0da28e..9835a6a 100644 --- a/source/ble_config.cpp +++ b/source/ble_config.cpp @@ -1,5 +1,6 @@ #include "ble_config.h" #include "tap.h" +#include "battery.h" #include #include @@ -200,6 +201,9 @@ void pushTelemetry(unsigned long now) { telem.leftClicks = statLeftClicks; telem.rightClicks = statRightClicks; #endif + #ifdef FEATURE_BATTERY_MONITOR + telem.battVoltage = readBatteryVoltage(); + #endif cfgTelemetry.write ((uint8_t*)&telem, sizeof(telem)); cfgTelemetry.notify((uint8_t*)&telem, sizeof(telem)); } diff --git a/source/config.h b/source/config.h index fb25e9d..c81f20d 100644 --- a/source/config.h +++ b/source/config.h @@ -114,8 +114,9 @@ struct __attribute__((packed)) TelemetryPacket { uint16_t recalCount; // [20] uint8_t chargeStatus; // [22] uint8_t _pad; // [23] + float battVoltage; // [24] raw battery voltage (V) }; -static_assert(sizeof(TelemetryPacket) == 24, "TelemetryPacket must be 24 bytes"); +static_assert(sizeof(TelemetryPacket) == 28, "TelemetryPacket must be 28 bytes"); extern TelemetryPacket telem; #endif diff --git a/web/app.js b/web/app.js index a8a3a77..187d699 100644 --- a/web/app.js +++ b/web/app.js @@ -14,7 +14,8 @@ const config = { sensitivity:600, deadZone:0.06, accelStrength:0.08, curve:0, ax tapThreshold:12, tapAction:0, tapKey:0, tapMod:0 }; let device=null, server=null, chars={}, userDisconnected=false; -let currentChargeStatus=0, currentBattPct=null; +let currentChargeStatus=0, currentBattPct=null, currentBattVoltage=null; +let advancedMode = localStorage.getItem('advanced') === 'true'; // ── GATT write queue (prevents "operation already in progress") ─────────────── // Serialises all GATT writes. Features: @@ -351,10 +352,11 @@ async function doReset() { } // ── Telemetry ──────────────────────────────────────────────────────────────── -// TelemetryPacket (24 bytes LE): +// TelemetryPacket (28 bytes LE — backwards compatible with 24-byte v3.3): // uint32 uptime [0], uint32 leftClicks [4], uint32 rightClicks [8] // float temp [12], float biasRms [16] // uint16 recalCount [20], uint8 chargeStatus [22], uint8 pad [23] +// float battVoltage [24] (new in v3.4, absent on older firmware) function parseTelemetry(dv) { let view; try { @@ -364,11 +366,11 @@ function parseTelemetry(dv) { if (view.byteLength < 24) { const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength); const hex = Array.from(bytes).map(b=>b.toString(16).padStart(2,'0')).join(' '); - log(`TELEM: expected 24B, got ${view.byteLength}B — MTU too small? raw: ${hex}`,'err'); + log(`TELEM: expected 24-28B, got ${view.byteLength}B — MTU too small? raw: ${hex}`,'err'); return; } - let uptime, leftClicks, rightClicks, temp, biasRms, recalCount, chargeStatus; + let uptime, leftClicks, rightClicks, temp, biasRms, recalCount, chargeStatus, battVoltage=null; try { uptime = view.getUint32(0, true); leftClicks = view.getUint32(4, true); @@ -377,6 +379,7 @@ function parseTelemetry(dv) { biasRms = view.getFloat32(16,true); recalCount = view.getUint16(20, true); chargeStatus= view.getUint8(22); + if (view.byteLength >= 28) battVoltage = view.getFloat32(24, true); } catch(e) { log(`parseTelemetry: parse error at offset — ${e.message}`,'err'); return; } document.getElementById('telTemp').textContent = temp.toFixed(1)+'°'; @@ -393,6 +396,11 @@ function parseTelemetry(dv) { currentChargeStatus = chargeStatus; updateChargeUI(); } + + if (battVoltage !== null) { + currentBattVoltage = battVoltage; + document.getElementById('ciVolt').textContent = battVoltage.toFixed(2) + 'V'; + } } function formatUptime(s) { const h=Math.floor(s/3600), m=Math.floor((s%3600)/60), ss=s%60; @@ -427,6 +435,15 @@ function updateChargeUI() { if (currentBattPct!==null) updateBatteryBar(currentBattPct, currentChargeStatus); } +// ── Advanced toggle ─────────────────────────────────────────────────────── +function toggleAdvanced(on) { + advancedMode = on; + localStorage.setItem('advanced', on); + document.getElementById('ciVoltItem').style.display = on ? '' : 'none'; + // Switch charge-info grid between 3 and 4 columns + document.getElementById('chargeInfo').style.gridTemplateColumns = on ? '1fr 1fr 1fr 1fr' : '1fr 1fr 1fr'; +} + // ── Param display ───────────────────────────────────────────────────────────── function updateDisplay(key, val) { const map = { @@ -815,6 +832,9 @@ function applyTheme(t) { themeIdx = Math.max(0, THEMES.indexOf(saved)); applyTheme(saved); initOrientViewer(); + // Restore advanced toggle state + document.getElementById('advancedToggle').checked = advancedMode; + if (advancedMode) toggleAdvanced(true); })(); if (!navigator.bluetooth) { diff --git a/web/index.html b/web/index.html index b3945e0..67f466b 100644 --- a/web/index.html +++ b/web/index.html @@ -86,10 +86,15 @@ -
+
--
Status
--
Current
--%
Level
+ +
+
+ + ADVANCED
diff --git a/web/style.css b/web/style.css index b5fb669..8127f93 100644 --- a/web/style.css +++ b/web/style.css @@ -252,6 +252,9 @@ .ci-val { font-family:var(--sans); font-size:16px; font-weight:700; } .ci-lbl { font-size:9px; letter-spacing:0.2em; text-transform:uppercase; color:var(--label); margin-top:3px; } + .advanced-row { display:flex; align-items:center; gap:8px; margin-top:10px; justify-content:flex-end; } + .advanced-label { font-family:var(--mono); font-size:9px; color:var(--label); letter-spacing:0.15em; } + .overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88); z-index:500; align-items:center; justify-content:center; } .overlay.show { display:flex; } .modal { background:var(--panel); border:1px solid var(--accent2); padding:28px; max-width:360px; width:100%; }