Add battery voltage reading to web UI, apply fixed VBAT multiplier, closes #3
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
28
web/app.js
28
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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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%; }
|
||||
|
||||
Reference in New Issue
Block a user