Add configuration slider for double tapping
This commit is contained in:
+56
-13
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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%); }
|
||||
|
||||
Reference in New Issue
Block a user