Add runtime BMA456 tap config with NVS persistence and goTool UI.

Expose multitap parameters over UART/ESP-NOW (like deadzone), store per node in NVS, and allow get/set from CLI, REST, and the dashboard without poll-overwrite while editing.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
simon
2026-09-26 13:07:13 +02:00
co-authored by Cursor
parent f006f8b912
commit d7441cb85f
32 changed files with 1859 additions and 150 deletions
+191 -2
View File
@@ -291,6 +291,69 @@
Setzen
</button>
</div>
<h6 class="text-secondary mb-2 mt-3">Tap-Erkennung (BMA456)</h6>
<div class="row g-2 small">
<div class="col-6 col-md-4">
<label class="form-label text-muted mb-0" for="tap-sens">sens (0–15)</label>
<input id="tap-sens" type="number" class="form-control form-control-sm"
min="0" max="15" x-model.number="masterTapCfg.tap_sens_thres"
:disabled="busy || !state.uart_connected">
</div>
<div class="col-6 col-md-4">
<label class="form-label text-muted mb-0" for="tap-max-gest">max_gest (×5ms)</label>
<input id="tap-max-gest" type="number" class="form-control form-control-sm"
min="0" max="65535" x-model.number="masterTapCfg.max_gest_dur"
:disabled="busy || !state.uart_connected">
</div>
<div class="col-6 col-md-4">
<label class="form-label text-muted mb-0" for="tap-shock">shock (×5ms)</label>
<input id="tap-shock" type="number" class="form-control form-control-sm"
min="0" max="65535" x-model.number="masterTapCfg.tap_shock_dur"
:disabled="busy || !state.uart_connected">
</div>
<div class="col-6 col-md-4">
<label class="form-label text-muted mb-0" for="tap-quiet">quiet (×5ms)</label>
<input id="tap-quiet" type="number" class="form-control form-control-sm"
min="0" max="65535" x-model.number="masterTapCfg.quite_time_after_gest"
:disabled="busy || !state.uart_connected">
</div>
<div class="col-6 col-md-4">
<label class="form-label text-muted mb-0" for="tap-wait">wait (0|1)</label>
<input id="tap-wait" type="number" class="form-control form-control-sm"
min="0" max="1" x-model.number="masterTapCfg.wait_for_timeout"
:disabled="busy || !state.uart_connected">
</div>
<div class="col-6 col-md-4">
<label class="form-label text-muted mb-0" for="tap-axis">axis (0=X…2=Z)</label>
<input id="tap-axis" type="number" class="form-control form-control-sm"
min="0" max="2" x-model.number="masterTapCfg.axis_sel"
:disabled="busy || !state.uart_connected">
</div>
</div>
<div class="d-flex flex-wrap gap-3 align-items-center mt-2 small">
<label class="tap-toggle mb-0">
<input type="checkbox" x-model="masterTapCfg.enable_single"
:disabled="busy || !state.uart_connected"> Single
</label>
<label class="tap-toggle mb-0">
<input type="checkbox" x-model="masterTapCfg.enable_double"
:disabled="busy || !state.uart_connected"> Double
</label>
<label class="tap-toggle mb-0">
<input type="checkbox" x-model="masterTapCfg.enable_triple"
:disabled="busy || !state.uart_connected"> Triple
</label>
<button type="button" class="btn btn-outline-secondary btn-sm"
@click="readMasterTapConfig()"
:disabled="busy || !state.uart_connected">
Lesen
</button>
<button type="button" class="btn btn-primary btn-sm"
@click="setMasterTapConfig()"
:disabled="busy || !state.uart_connected">
Setzen
</button>
</div>
<p class="text-muted small mt-2 mb-0" x-show="!state.uart_connected">
UART nicht verbunden — Eingabe gesperrt.
</p>
@@ -313,6 +376,12 @@
:disabled="busy || !state.uart_connected">
Alle Slaves
</button>
<button type="button" class="btn btn-outline-secondary btn-sm"
@click="setTapConfigAll()"
:disabled="busy || !state.uart_connected"
title="Master-Tap-Config an alle Slaves (ESP-NOW)">
Tap-Cfg alle
</button>
<span class="badge bg-secondary" x-text="(state.clients || []).length + ' registered'"></span>
</div>
</div>
@@ -348,6 +417,7 @@
<th>Ver</th>
<th>Status</th>
<th>Deadzone</th>
<th>Tap-Cfg</th>
<th>Accel (LSB)</th>
<th>Akku</th>
<th>Accel</th>
@@ -358,7 +428,7 @@
</thead>
<tbody>
<template x-if="!(state.clients || []).length">
<tr><td colspan="11" class="text-muted text-center py-4">No clients</td></tr>
<tr><td colspan="12" class="text-muted text-center py-4">No clients</td></tr>
</template>
<template x-for="c in (state.clients || [])" :key="c.id + c.mac">
<tr>
@@ -371,6 +441,15 @@
x-text="c.available ? 'available' : 'inactive'"></span>
</td>
<td x-text="c.deadzone != null ? c.deadzone : '—'"></td>
<td class="small">
<span x-text="formatTapCfg(c.tap_config)" :title="tapCfgTitle(c.tap_config)"></span>
<button type="button" class="btn btn-outline-primary btn-sm ms-1 py-0 px-1"
@click="setTapConfig(c.id, masterTapCfg)"
:disabled="busy || !state.uart_connected || !c.available"
title="Master-Tap-Config an diesen Slave">
Set
</button>
</td>
<td>
<span class="accel"
:class="accelCellClass(c)"
@@ -655,7 +734,20 @@
ws: null,
wsConnected: false,
masterDz: 100,
masterDzSeeded: false,
masterLogLevel: 0,
masterTapCfg: {
tap_sens_thres: 5,
max_gest_dur: 100,
tap_shock_dur: 6,
quite_time_after_gest: 60,
wait_for_timeout: 0,
axis_sel: 2,
enable_single: true,
enable_double: true,
enable_triple: true
},
masterTapCfgSeeded: false,
allDz: 100,
allTapSingle: false,
allTapDouble: false,
@@ -719,8 +811,13 @@
this.state = msg;
this.preserveBatteryInState(prev, this.state);
this.syncTapDisplay(msg.clients || []);
if (msg.master?.deadzone != null) {
if (!this.masterDzSeeded && msg.master?.deadzone != null) {
this.masterDz = msg.master.deadzone;
this.masterDzSeeded = true;
}
if (!this.masterTapCfgSeeded && msg.master?.tap_config) {
this.applyMasterTapCfg(msg.master.tap_config);
this.masterTapCfgSeeded = true;
}
for (const c of (msg.clients || [])) {
if (c.deadzone != null && this.slaveDz[c.id] == null) {
@@ -1124,6 +1221,10 @@
this.flash(`Slaves: Deadzone ${data.deadzone} LSB (${data.slaves_updated} per ESP-NOW, Master unverändert)`, true);
return;
}
if (clientId === 0) {
this.masterDz = data.deadzone;
this.masterDzSeeded = true;
}
const who = clientId === 0 ? 'Master' : `Slave ${clientId}`;
this.flash(`${who}: Deadzone ${data.deadzone} LSB gesetzt`, true);
} catch (e) {
@@ -1142,6 +1243,7 @@
return;
}
this.masterDz = data.deadzone;
this.masterDzSeeded = true;
this.flash(`Master: Deadzone ${data.deadzone} LSB`, true);
} catch (e) {
this.flash(String(e), false);
@@ -1152,6 +1254,93 @@
async setMasterDeadzone() {
await this.setDeadzone(0, this.masterDz);
},
formatTapCfg(tc) {
if (!tc) return '—';
return `s${tc.tap_sens_thres}`;
},
tapCfgTitle(tc) {
if (!tc) return '';
const kinds = [tc.enable_single && 'S', tc.enable_double && 'D', tc.enable_triple && 'T'].filter(Boolean).join('/');
return `sens=${tc.tap_sens_thres} max_gest=${tc.max_gest_dur} shock=${tc.tap_shock_dur} quiet=${tc.quite_time_after_gest} wait=${tc.wait_for_timeout} axis=${tc.axis_sel} (${kinds || 'aus'})`;
},
applyMasterTapCfg(data) {
if (!data) return;
this.masterTapCfg = {
tap_sens_thres: data.tap_sens_thres,
max_gest_dur: data.max_gest_dur,
tap_shock_dur: data.tap_shock_dur,
quite_time_after_gest: data.quite_time_after_gest,
wait_for_timeout: data.wait_for_timeout,
axis_sel: data.axis_sel,
enable_single: data.enable_single,
enable_double: data.enable_double,
enable_triple: data.enable_triple
};
this.masterTapCfgSeeded = true;
},
async setTapConfig(clientId, cfg, opts = {}) {
if (!cfg) {
this.flash('Keine Tap-Config', false);
return;
}
this.busy = true;
try {
const r = await fetch('/api/tap-config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
write: true,
client_id: clientId,
all_clients: !!opts.allClients,
slaves_only: !!opts.slavesOnly,
...cfg
})
});
const data = await r.json();
if (!r.ok || !data.success) {
const err = data.error ||
(opts.slavesOnly ? 'Tap-Config für Slaves fehlgeschlagen' : `Tap-Config für Client ${clientId} fehlgeschlagen`);
this.flash(err, false);
return;
}
if (opts.slavesOnly) {
this.flash(`Slaves: Tap-Config gesetzt (${data.slaves_updated} per ESP-NOW, Master unverändert)`, true);
return;
}
const who = clientId === 0 ? 'Master' : `Slave ${clientId}`;
this.flash(`${who}: Tap-Config gesetzt (sens=${data.tap_sens_thres})`, true);
if (clientId === 0) {
this.applyMasterTapCfg(data);
}
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
},
async readMasterTapConfig() {
this.busy = true;
try {
const r = await fetch('/api/tap-config?client_id=0');
const data = await r.json();
if (!r.ok || !data.success) {
this.flash(data.error || 'Master-Tap-Config lesen fehlgeschlagen', false);
return;
}
this.applyMasterTapCfg(data);
this.flash(`Master: Tap-Config gelesen (sens=${data.tap_sens_thres})`, true);
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
},
async setMasterTapConfig() {
await this.setTapConfig(0, this.masterTapCfg);
},
async setTapConfigAll() {
await this.setTapConfig(0, this.masterTapCfg, { allClients: true, slavesOnly: true });
},
async readMasterLogLevel() {
this.busy = true;
try {