Add configuration slider for double tapping

This commit is contained in:
2026-03-01 20:45:23 +01:00
parent ef97d8f32a
commit f155d16399
8 changed files with 248 additions and 52 deletions
+56 -13
View File
@@ -9,7 +9,8 @@ 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 };
const config = { sensitivity:600, deadZone:0.06, accelStrength:0.08, curve:0, axisFlip:0, chargeMode:1,
tapThreshold:12, tapAction:0, tapKey:0, tapMod:0 };
let device=null, server=null, chars={}, userDisconnected=false;
let currentChargeStatus=0, currentBattPct=null;
@@ -104,23 +105,30 @@ async function discoverServices() {
}
// ── ConfigBlob read / write ──────────────────────────────────────────────────
// ConfigBlob layout (16 bytes LE):
// ConfigBlob layout (20 bytes LE):
// float sensitivity [0], float deadZone [4], float accelStrength [8]
// uint8 curve [12], uint8 axisFlip [13], uint8 chargeMode [14], uint8 pad [15]
// uint8 curve [12], uint8 axisFlip [13], uint8 chargeMode [14]
// uint8 tapThreshold [15], uint8 tapAction [16], uint8 tapKey [17], uint8 tapMod [18], uint8 pad [19]
async function readConfigBlob() {
if (!chars.configBlob) return;
try {
const dv = await chars.configBlob.readValue();
const view = new DataView(dv.buffer ?? dv);
const view = dv instanceof DataView ? new DataView(dv.buffer, dv.byteOffset, dv.byteLength) : new DataView(dv);
config.sensitivity = view.getFloat32(0, true);
config.deadZone = view.getFloat32(4, true);
config.accelStrength = view.getFloat32(8, true);
config.curve = view.getUint8(12);
config.axisFlip = view.getUint8(13);
config.chargeMode = view.getUint8(14);
if (view.byteLength >= 20) {
config.tapThreshold = view.getUint8(15);
config.tapAction = view.getUint8(16);
config.tapKey = view.getUint8(17);
config.tapMod = view.getUint8(18);
}
applyConfigToUI();
log(`Config loaded — sens=${config.sensitivity.toFixed(0)} dz=${config.deadZone.toFixed(3)}`,'ok');
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'); }
}
@@ -135,6 +143,14 @@ function applyConfigToUI() {
document.getElementById('flipX').checked = !!(config.axisFlip & 1);
document.getElementById('flipY').checked = !!(config.axisFlip & 2);
setChargeModeUI(config.chargeMode);
document.getElementById('slTapThreshold').value = config.tapThreshold;
updateDisplay('tapThreshold', config.tapThreshold);
setTapActionUI(config.tapAction);
document.getElementById('tapKeyHex').value = config.tapKey ? config.tapKey.toString(16).padStart(2,'0') : '';
document.getElementById('tapModCtrl').checked = !!(config.tapMod & 0x01);
document.getElementById('tapModShift').checked = !!(config.tapMod & 0x02);
document.getElementById('tapModAlt').checked = !!(config.tapMod & 0x04);
document.getElementById('tapModGui').checked = !!(config.tapMod & 0x08);
}
async function writeConfigBlob() {
@@ -146,9 +162,14 @@ async function writeConfigBlob() {
config.accelStrength = +document.getElementById('slAccel').value;
config.axisFlip = (document.getElementById('flipX').checked ? 1 : 0)
| (document.getElementById('flipY').checked ? 2 : 0);
// config.curve and config.chargeMode are updated directly by setCurve/setChargeMode
config.tapThreshold = +document.getElementById('slTapThreshold').value;
config.tapMod = (document.getElementById('tapModCtrl').checked ? 0x01 : 0)
| (document.getElementById('tapModShift').checked ? 0x02 : 0)
| (document.getElementById('tapModAlt').checked ? 0x04 : 0)
| (document.getElementById('tapModGui').checked ? 0x08 : 0);
// config.curve, config.chargeMode, config.tapAction, config.tapKey updated directly
const buf = new ArrayBuffer(16);
const buf = new ArrayBuffer(20);
const view = new DataView(buf);
view.setFloat32(0, config.sensitivity, true);
view.setFloat32(4, config.deadZone, true);
@@ -156,11 +177,15 @@ async function writeConfigBlob() {
view.setUint8(12, config.curve);
view.setUint8(13, config.axisFlip);
view.setUint8(14, config.chargeMode);
view.setUint8(15, 0);
view.setUint8(15, config.tapThreshold);
view.setUint8(16, config.tapAction);
view.setUint8(17, config.tapKey);
view.setUint8(18, config.tapMod);
view.setUint8(19, 0);
try {
await chars.configBlob.writeValue(buf);
log(`Config written — sens=${config.sensitivity.toFixed(0)} dz=${config.deadZone.toFixed(3)} curve=${config.curve} chg=${config.chargeMode}`,'ok');
log(`Config written — sens=${config.sensitivity.toFixed(0)} tapThr=${config.tapThreshold} tapAction=${config.tapAction}`,'ok');
} catch(e) { log(`Config write failed: ${e.message}`,'err'); }
}
@@ -193,6 +218,23 @@ function setChargeModeUI(val) {
document.getElementById('ciMode').textContent = ['Off (0mA)','50 mA','100 mA'][val] ?? '--';
}
async function setTapAction(val) {
config.tapAction = val;
setTapActionUI(val);
await writeConfigBlob();
}
function setTapActionUI(val) {
['tapActLeft','tapActRight','tapActMiddle','tapActKey'].forEach((id,i) =>
document.getElementById(id).classList.toggle('active', i===val));
document.getElementById('tapKeyRow').style.display = val===3 ? 'flex' : 'none';
}
function onTapKeyInput() {
const hex = document.getElementById('tapKeyHex').value.trim();
const code = parseInt(hex, 16);
config.tapKey = (isNaN(code) || code < 0 || code > 255) ? 0 : code;
writeConfigBlob();
}
async function sendCalibrate() {
if (!chars.command) return;
try { await chars.command.writeValue(new Uint8Array([0x01])); log('Calibration sent — hold still!','warn'); }
@@ -289,9 +331,10 @@ function updateChargeUI() {
// ── 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)],
sensitivity: ['valSensitivity', v=>parseFloat(v).toFixed(0)],
deadZone: ['valDeadZone', v=>parseFloat(v).toFixed(3)],
accel: ['valAccel', v=>parseFloat(v).toFixed(2)],
tapThreshold: ['valTapThreshold', v=>(parseFloat(v)*62.5).toFixed(0)+' mg'],
};
const [id,fmt] = map[key];
document.getElementById(id).textContent = fmt(val);
@@ -304,7 +347,7 @@ function setStatus(state) {
pill.className='status-pill '+state;
document.body.className=state;
const cBtn=document.getElementById('connectBtn'), dBtn=document.getElementById('disconnectBtn');
const inputs=document.querySelectorAll('input[type=range],.seg-btn,.toggle input,.cmd-btn');
const inputs=document.querySelectorAll('input[type=range],.seg-btn,.toggle input,.cmd-btn,#tapKeyHex,.mod-btn input');
if (state==='connected') {
cBtn.style.display='none'; dBtn.style.display='';
inputs.forEach(el=>el.disabled=false);
+32
View File
@@ -89,6 +89,38 @@
</div>
</div>
<div class="section-label">Tap Configuration</div>
<div class="card">
<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"
oninput="updateDisplay('tapThreshold',this.value)" onchange="writeConfigBlob()">
<div class="param-value" id="valTapThreshold">750 mg</div>
</div>
<div class="param" style="border-bottom:none;padding-bottom:0">
<div><div class="param-label">Double-Tap Action</div><div class="param-desc">What a double-tap sends</div></div>
<div class="segmented" style="grid-column:2/4">
<button class="seg-btn active" id="tapActLeft" onclick="setTapAction(0)" disabled>LEFT</button>
<button class="seg-btn" id="tapActRight" onclick="setTapAction(1)" disabled>RIGHT</button>
<button class="seg-btn" id="tapActMiddle" onclick="setTapAction(2)" disabled>MIDDLE</button>
<button class="seg-btn" id="tapActKey" onclick="setTapAction(3)" disabled>KEY</button>
</div>
</div>
<div class="tap-key-row" id="tapKeyRow" style="display:none">
<div class="param-label" style="font-size:11px">HID Keycode (hex)</div>
<input type="text" id="tapKeyHex" placeholder="e.g. 28 = Enter" maxlength="4"
style="font-family:var(--mono);font-size:12px;background:var(--bg);color:var(--text);border:1px solid var(--border);padding:4px 8px;width:110px"
oninput="onTapKeyInput()" disabled>
<div class="param-label" style="font-size:11px;margin-left:12px">Modifier</div>
<div style="display:flex;gap:6px;flex-wrap:wrap">
<label class="mod-btn"><input type="checkbox" id="tapModCtrl" onchange="writeConfigBlob()" disabled><span>Ctrl</span></label>
<label class="mod-btn"><input type="checkbox" id="tapModShift" onchange="writeConfigBlob()" disabled><span>Shift</span></label>
<label class="mod-btn"><input type="checkbox" id="tapModAlt" onchange="writeConfigBlob()" disabled><span>Alt</span></label>
<label class="mod-btn"><input type="checkbox" id="tapModGui" onchange="writeConfigBlob()" disabled><span>GUI</span></label>
</div>
</div>
</div>
<div class="section-label">Axis Configuration</div>
<div class="card">
<div class="flip-row">
+7
View File
@@ -266,6 +266,13 @@
body.disconnected .card { opacity:0.45; pointer-events:none; transition:opacity 0.3s; }
body.disconnected .cmd-grid { opacity:0.45; pointer-events:none; transition:opacity 0.3s; }
.tap-key-row { display:flex; align-items:center; gap:10px; padding-top:12px; flex-wrap:wrap; }
.mod-btn { display:flex; align-items:center; gap:4px; cursor:pointer; font-family:var(--mono); font-size:10px; color:var(--label); user-select:none; }
.mod-btn input { display:none; }
.mod-btn span { padding:4px 8px; border:1px solid var(--border); background:transparent; transition:all 0.15s; }
.mod-btn input:checked + span { background:var(--accent); color:var(--bg); border-color:var(--accent); font-weight:bold; }
.mod-btn input:disabled + span { opacity:0.35; cursor:not-allowed; }
.tap-flash { position:absolute; inset:0; pointer-events:none; opacity:0; transition:opacity 0.25s; }
.tap-flash.left { background:radial-gradient(circle at center, var(--tap-left) 0%, transparent 70%); }
.tap-flash.right { background:radial-gradient(circle at center, var(--tap-right) 0%, transparent 70%); }