Attempt to add jerk correction
This commit is contained in:
+109
-3
@@ -11,7 +11,7 @@ const CHR = {
|
||||
|
||||
// Local shadow of the current config (kept in sync with device)
|
||||
const config = { sensitivity:600, deadZone:0.06, accelStrength:0.08, curve:0, axisFlip:0, chargeMode:1,
|
||||
tapThreshold:12, tapAction:0, tapKey:0, tapMod:0 };
|
||||
tapThreshold:12, tapAction:0, tapKey:0, tapMod:0, jerkThreshold:2000 };
|
||||
|
||||
let device=null, server=null, chars={}, userDisconnected=false;
|
||||
let currentChargeStatus=0, currentBattPct=null, currentBattVoltage=null;
|
||||
@@ -222,6 +222,9 @@ async function readConfigBlob() {
|
||||
config.tapKey = view.getUint8(17);
|
||||
config.tapMod = view.getUint8(18);
|
||||
}
|
||||
if (view.byteLength >= 24) {
|
||||
config.jerkThreshold = view.getFloat32(20, true);
|
||||
}
|
||||
applyConfigToUI();
|
||||
log(`Config loaded — sens=${config.sensitivity.toFixed(0)} dz=${config.deadZone.toFixed(3)} tapThr=${config.tapThreshold}`,'ok');
|
||||
} catch(e) { log(`Config read error: ${e.message}`,'err'); }
|
||||
@@ -238,6 +241,8 @@ function applyConfigToUI() {
|
||||
document.getElementById('flipX').checked = !!(config.axisFlip & 1);
|
||||
document.getElementById('flipY').checked = !!(config.axisFlip & 2);
|
||||
setChargeModeUI(config.chargeMode);
|
||||
document.getElementById('slJerkThreshold').value = config.jerkThreshold;
|
||||
updateDisplay('jerkThreshold', config.jerkThreshold);
|
||||
document.getElementById('slTapThreshold').value = config.tapThreshold;
|
||||
updateDisplay('tapThreshold', config.tapThreshold);
|
||||
setTapActionUI(config.tapAction);
|
||||
@@ -267,9 +272,10 @@ async function _doWriteConfigBlob() {
|
||||
| (document.getElementById('tapModShift').checked ? 0x02 : 0)
|
||||
| (document.getElementById('tapModAlt').checked ? 0x04 : 0)
|
||||
| (document.getElementById('tapModGui').checked ? 0x08 : 0);
|
||||
config.jerkThreshold = +document.getElementById('slJerkThreshold').value;
|
||||
// config.curve, config.chargeMode, config.tapAction, config.tapKey updated directly
|
||||
|
||||
const buf = new ArrayBuffer(20);
|
||||
const buf = new ArrayBuffer(24);
|
||||
const view = new DataView(buf);
|
||||
view.setFloat32(0, config.sensitivity, true);
|
||||
view.setFloat32(4, config.deadZone, true);
|
||||
@@ -282,6 +288,7 @@ async function _doWriteConfigBlob() {
|
||||
view.setUint8(17, config.tapKey);
|
||||
view.setUint8(18, config.tapMod);
|
||||
view.setUint8(19, 0);
|
||||
view.setFloat32(20, config.jerkThreshold, true);
|
||||
|
||||
try {
|
||||
await gattWrite(chars.configBlob, buf);
|
||||
@@ -440,16 +447,111 @@ function toggleAdvanced(on) {
|
||||
advancedMode = on;
|
||||
localStorage.setItem('advanced', on);
|
||||
document.getElementById('ciVoltItem').style.display = on ? '' : 'none';
|
||||
document.getElementById('debugBtn').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';
|
||||
}
|
||||
|
||||
// ── IMU Debug Recorder ────────────────────────────────────────────────────────
|
||||
let debugModalOpen = false;
|
||||
let debugRecording = false;
|
||||
let debugBuffer = [];
|
||||
const DEBUG_LIVE_ROWS = 40;
|
||||
let debugLiveRing = [];
|
||||
let debugT0 = 0;
|
||||
|
||||
function openDebugModal() {
|
||||
debugModalOpen = true;
|
||||
debugT0 = Date.now();
|
||||
debugLiveRing = [];
|
||||
document.getElementById('debugOverlay').classList.add('show');
|
||||
// Auto-start IMU stream if not already running
|
||||
if (!imuSubscribed && chars.imuStream) vizSetPaused(false);
|
||||
}
|
||||
function closeDebugModal() {
|
||||
debugModalOpen = false;
|
||||
document.getElementById('debugOverlay').classList.remove('show');
|
||||
}
|
||||
|
||||
function feedDebugRow(gyroX, gyroZ, accelX, accelY, accelZ, moveX, moveY, flags) {
|
||||
if (!debugModalOpen) return;
|
||||
const ms = Date.now() - debugT0;
|
||||
const row = { ms, gyroX, gyroZ, accelX, accelY, accelZ, moveX, moveY, flags };
|
||||
|
||||
// Live ring buffer
|
||||
debugLiveRing.push(row);
|
||||
if (debugLiveRing.length > DEBUG_LIVE_ROWS) debugLiveRing.shift();
|
||||
|
||||
// Recording buffer
|
||||
if (debugRecording) {
|
||||
debugBuffer.push(row);
|
||||
document.getElementById('debugRecCount').textContent = debugBuffer.length + ' samples';
|
||||
}
|
||||
|
||||
// Shock indicator
|
||||
const shocked = !!(flags & 0x08);
|
||||
const badge = document.getElementById('debugShockBadge');
|
||||
badge.classList.toggle('active', shocked);
|
||||
|
||||
// Render live table
|
||||
const tbody = document.getElementById('debugRows');
|
||||
tbody.innerHTML = '';
|
||||
for (const r of debugLiveRing) {
|
||||
const f = [];
|
||||
if (r.flags & 0x01) f.push('idle');
|
||||
if (r.flags & 0x02) f.push('tap1');
|
||||
if (r.flags & 0x04) f.push('tap2');
|
||||
if (r.flags & 0x08) f.push('shock');
|
||||
const tr = document.createElement('tr');
|
||||
if (r.flags & 0x08) tr.className = 'shock-row';
|
||||
tr.innerHTML =
|
||||
`<td>${r.ms}</td><td>${r.gyroX}</td><td>${r.gyroZ}</td>` +
|
||||
`<td>${r.accelX}</td><td>${r.accelY}</td><td>${r.accelZ}</td>` +
|
||||
`<td>${r.moveX}</td><td>${r.moveY}</td><td>${f.join(' ')}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
tbody.parentElement.parentElement.scrollTop = tbody.parentElement.parentElement.scrollHeight;
|
||||
}
|
||||
|
||||
function toggleDebugRec() {
|
||||
debugRecording = !debugRecording;
|
||||
const btn = document.getElementById('debugRecBtn');
|
||||
btn.classList.toggle('recording', debugRecording);
|
||||
btn.textContent = debugRecording ? '■ STOP' : '● REC';
|
||||
if (debugRecording) { debugBuffer = []; debugT0 = Date.now(); }
|
||||
document.getElementById('debugRecCount').textContent = debugBuffer.length + ' samples';
|
||||
}
|
||||
|
||||
function saveDebugCSV() {
|
||||
if (!debugBuffer.length) { log('No recorded data to save','warn'); return; }
|
||||
const header = 'ms,gyroX_mDPS,gyroZ_mDPS,accelX_mg,accelY_mg,accelZ_mg,moveX,moveY,flags\n';
|
||||
const rows = debugBuffer.map(r =>
|
||||
`${r.ms},${r.gyroX},${r.gyroZ},${r.accelX},${r.accelY},${r.accelZ},${r.moveX},${r.moveY},${r.flags}`
|
||||
).join('\n');
|
||||
const blob = new Blob([header + rows], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = `imu_debug_${new Date().toISOString().slice(0,19).replace(/:/g,'-')}.csv`;
|
||||
a.click(); URL.revokeObjectURL(url);
|
||||
log(`Saved ${debugBuffer.length} samples as CSV`,'ok');
|
||||
}
|
||||
|
||||
function clearDebugRec() {
|
||||
debugBuffer = [];
|
||||
debugRecording = false;
|
||||
const btn = document.getElementById('debugRecBtn');
|
||||
btn.classList.remove('recording');
|
||||
btn.textContent = '● REC';
|
||||
document.getElementById('debugRecCount').textContent = '0 samples';
|
||||
}
|
||||
|
||||
// ── Param display ─────────────────────────────────────────────────────────────
|
||||
function updateDisplay(key, val) {
|
||||
const map = {
|
||||
sensitivity: ['valSensitivity', v=>parseFloat(v).toFixed(0)],
|
||||
deadZone: ['valDeadZone', v=>parseFloat(v).toFixed(3)],
|
||||
accel: ['valAccel', v=>parseFloat(v).toFixed(2)],
|
||||
jerkThreshold:['valJerkThreshold',v=>parseFloat(v).toFixed(0)],
|
||||
tapThreshold: ['valTapThreshold', v=>(parseFloat(v)*62.5).toFixed(0)+' mg'],
|
||||
};
|
||||
const [id,fmt] = map[key];
|
||||
@@ -601,7 +703,6 @@ async function vizSetPaused(paused) {
|
||||
}
|
||||
|
||||
function parseImuStream(dv) {
|
||||
if (vizPaused) return;
|
||||
let view;
|
||||
try {
|
||||
view = dv instanceof DataView ? new DataView(dv.buffer, dv.byteOffset, dv.byteLength) : new DataView(dv);
|
||||
@@ -625,6 +726,11 @@ function parseImuStream(dv) {
|
||||
moveY = view.getInt8(11);
|
||||
flags = view.getUint8(12);
|
||||
} catch(e) { log(`parseImuStream: parse error — ${e.message}`,'err'); return; }
|
||||
|
||||
// Feed debug recorder (even when viz is paused)
|
||||
feedDebugRow(gyroX, gyroZ, accelX, accelY, accelZ, moveX, moveY, flags);
|
||||
|
||||
if (vizPaused) return;
|
||||
const idle = !!(flags & 0x01);
|
||||
const single = !!(flags & 0x02);
|
||||
const dbl = !!(flags & 0x04);
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<div class="batt-cells" id="battCells"></div>
|
||||
<span id="battPct">--%</span>
|
||||
</div>
|
||||
<button class="btn btn-debug" id="debugBtn" onclick="openDebugModal()" style="display:none" title="IMU Debug Recorder"><span>DBG</span></button>
|
||||
<button class="btn btn-theme" id="themeBtn" onclick="cycleTheme()"><span>AUTO</span></button>
|
||||
<div class="status-pill" id="statusPill"><div class="dot"></div><span id="statusText">DISCONNECTED</span></div>
|
||||
<button class="btn btn-connect" id="connectBtn" onclick="doConnect()"><span>Connect</span></button>
|
||||
@@ -100,6 +101,12 @@
|
||||
|
||||
<div class="section-label">Tap Configuration</div>
|
||||
<div class="card">
|
||||
<div class="param">
|
||||
<div><div class="param-label">Tap Freeze Sensitivity</div><div class="param-desc">Jerk² threshold — lower = more aggressive cursor freeze during taps</div></div>
|
||||
<input type="range" id="slJerkThreshold" min="500" max="10000" step="100" value="2000"
|
||||
oninput="updateDisplay('jerkThreshold',this.value)" onchange="writeConfigBlob()">
|
||||
<div class="param-value" id="valJerkThreshold">2000</div>
|
||||
</div>
|
||||
<div class="param">
|
||||
<div><div class="param-label">Tap Threshold</div><div class="param-desc">Impact force needed · 1 LSB ≈ 62.5 mg at ±2g</div></div>
|
||||
<input type="range" id="slTapThreshold" min="1" max="31" step="1" value="12"
|
||||
@@ -223,6 +230,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay" id="debugOverlay">
|
||||
<div class="modal debug-modal">
|
||||
<div class="debug-header">
|
||||
<h3>IMU Debug Recorder</h3>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<span class="debug-shock-badge" id="debugShockBadge">SHOCK</span>
|
||||
<button class="debug-close" onclick="closeDebugModal()">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="debug-live" id="debugLive">
|
||||
<table class="debug-table">
|
||||
<thead><tr>
|
||||
<th>ms</th><th>gX</th><th>gZ</th><th>aX</th><th>aY</th><th>aZ</th><th>mX</th><th>mY</th><th>flags</th>
|
||||
</tr></thead>
|
||||
<tbody id="debugRows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="debug-controls">
|
||||
<button class="debug-rec-btn" id="debugRecBtn" onclick="toggleDebugRec()">● REC</button>
|
||||
<span class="debug-rec-count" id="debugRecCount">0 samples</span>
|
||||
<button class="debug-ctrl-btn" onclick="saveDebugCSV()">Save CSV</button>
|
||||
<button class="debug-ctrl-btn" onclick="clearDebugRec()">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -131,6 +131,8 @@
|
||||
.btn:disabled { border-color:var(--dim); color:var(--dim); cursor:not-allowed; }
|
||||
.btn:disabled::before { display:none; }
|
||||
.btn:disabled:hover { color:var(--dim); }
|
||||
.btn-debug { border:1px solid var(--dim); color:var(--label); min-width:52px; text-align:center; font-size:10px; padding:6px 10px; }
|
||||
.btn-debug::before { background:var(--accent); }
|
||||
.btn-theme { border:1px solid var(--dim); color:var(--label); min-width:72px; text-align:center; }
|
||||
.btn-theme::before { background:var(--text); }
|
||||
|
||||
@@ -267,6 +269,29 @@
|
||||
.btn-confirm { border-color:var(--accent2); color:var(--accent2); }
|
||||
.btn-confirm:hover { background:var(--accent2); color:var(--bg); }
|
||||
|
||||
/* ── Debug modal ────────────────────────────────────────────────────────── */
|
||||
.debug-modal { max-width:720px; padding:20px; border-color:var(--accent); }
|
||||
.debug-modal h3 { color:var(--accent); margin-bottom:0; }
|
||||
.debug-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:14px; }
|
||||
.debug-close { background:none; border:1px solid var(--border); color:var(--label); font-size:14px; cursor:pointer; padding:2px 8px; font-family:var(--mono); }
|
||||
.debug-close:hover { border-color:var(--text); color:var(--text); }
|
||||
.debug-live { max-height:340px; overflow-y:auto; border:1px solid var(--border); margin-bottom:12px; }
|
||||
.debug-table { width:100%; border-collapse:collapse; font-family:var(--mono); font-size:10px; }
|
||||
.debug-table thead { position:sticky; top:0; background:var(--panel); z-index:1; }
|
||||
.debug-table th { padding:4px 6px; text-align:right; color:var(--label); font-weight:400; border-bottom:1px solid var(--border); letter-spacing:0.1em; text-transform:uppercase; }
|
||||
.debug-table td { padding:2px 6px; text-align:right; color:var(--text); border-bottom:1px solid color-mix(in srgb, var(--border) 40%, transparent); white-space:nowrap; }
|
||||
.debug-table td:last-child { text-align:left; color:var(--label); }
|
||||
.debug-table .shock-row td { color:var(--accent2); }
|
||||
.debug-controls { display:flex; align-items:center; gap:10px; }
|
||||
.debug-rec-btn { font-family:var(--mono); font-size:12px; font-weight:700; padding:6px 14px; cursor:pointer; border:1px solid var(--accent2); color:var(--accent2); background:transparent; letter-spacing:0.1em; }
|
||||
.debug-rec-btn.recording { background:var(--accent2); color:var(--bg); animation:rec-pulse 1s infinite; }
|
||||
@keyframes rec-pulse { 0%,100%{opacity:1} 50%{opacity:0.6} }
|
||||
.debug-rec-count { font-family:var(--mono); font-size:10px; color:var(--label); }
|
||||
.debug-ctrl-btn { font-family:var(--mono); font-size:10px; padding:5px 10px; cursor:pointer; border:1px solid var(--border); color:var(--label); background:transparent; }
|
||||
.debug-ctrl-btn:hover { border-color:var(--text); color:var(--text); }
|
||||
.debug-shock-badge { font-family:var(--mono); font-size:9px; letter-spacing:0.15em; padding:2px 8px; border:1px solid var(--border); color:var(--border); opacity:0.3; transition:all 0.15s; }
|
||||
.debug-shock-badge.active { border-color:var(--accent2); color:var(--accent2); opacity:1; }
|
||||
|
||||
.no-ble { grid-column:1/-1; text-align:center; padding:80px 24px; }
|
||||
.no-ble h2 { font-family:var(--sans); font-size:28px; font-weight:700; color:var(--accent2); margin-bottom:12px; }
|
||||
.no-ble p { font-size:13px; color:var(--label); line-height:1.8; }
|
||||
|
||||
Reference in New Issue
Block a user