Add battery voltage reading to web UI, apply fixed VBAT multiplier, closes #3

This commit is contained in:
2026-03-02 21:01:58 +01:00
parent c41a2932ba
commit 6ecae74483
6 changed files with 43 additions and 8 deletions

View File

@@ -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) {

View File

@@ -1,5 +1,6 @@
#include "ble_config.h"
#include "tap.h"
#include "battery.h"
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
@@ -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));
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -86,10 +86,15 @@
<button class="seg-btn fast" id="chgFast" onclick="setChargeMode(2)" disabled>FAST · 100mA</button>
</div>
</div>
<div class="charge-info">
<div class="charge-info" id="chargeInfo">
<div class="ci-item"><div class="ci-val" id="ciStatus">--</div><div class="ci-lbl">Status</div></div>
<div class="ci-item"><div class="ci-val" id="ciMode">--</div><div class="ci-lbl">Current</div></div>
<div class="ci-item"><div class="ci-val" id="ciPct">--%</div><div class="ci-lbl">Level</div></div>
<div class="ci-item ci-advanced" id="ciVoltItem" style="display:none"><div class="ci-val accent" id="ciVolt">--</div><div class="ci-lbl">Voltage</div></div>
</div>
<div class="advanced-row">
<label class="toggle"><input type="checkbox" id="advancedToggle" onchange="toggleAdvanced(this.checked)"><div class="toggle-track"></div><div class="toggle-thumb"></div></label>
<span class="advanced-label">ADVANCED</span>
</div>
</div>

View File

@@ -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%; }