Harden BLE host link and expose latency tools in the dashboard.
Add BLE RTT/stream tests and live tick median in the UI, fix concurrent WebSocket writes and late BATTERY replies, and skip 2M PHY updates that assert under ESP-NOW coexistence. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+182
-3
@@ -269,6 +269,89 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card mb-4" x-show="isBLEHost()">
|
||||
<div class="card-header">BLE-Tests</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-3">
|
||||
Host↔Master-Latenz und Stream-Tick-Fenster (wie CLI <code>ble-rtt</code> / <code>ble-stream</code>).
|
||||
Dashboard-Polling pausiert während des Laufs.
|
||||
</p>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary mb-2">RTT (VERSION)</h6>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-end mb-2">
|
||||
<div>
|
||||
<label class="form-label text-muted small mb-1" for="ble-rtt-count">Samples</label>
|
||||
<input id="ble-rtt-count" type="number" class="form-control form-control-sm config-input"
|
||||
min="1" max="500" x-model.number="bleRtt.count" :disabled="bleTestBusy">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label text-muted small mb-1" for="ble-rtt-warmup">Warmup</label>
|
||||
<input id="ble-rtt-warmup" type="number" class="form-control form-control-sm config-input"
|
||||
min="0" max="100" x-model.number="bleRtt.warmup" :disabled="bleTestBusy">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
@click="runBleRtt()" :disabled="bleTestBusy || busy"
|
||||
x-text="bleTestBusy === 'rtt' ? 'Läuft…' : 'Start RTT'"></button>
|
||||
</div>
|
||||
<template x-if="bleRtt.result">
|
||||
<dl class="row small mb-0">
|
||||
<dt class="col-5 text-muted">n / warmup</dt>
|
||||
<dd class="col-7" x-text="bleRtt.result.stats?.n + ' / ' + bleRtt.result.warmup"></dd>
|
||||
<dt class="col-5 text-muted">min / median</dt>
|
||||
<dd class="col-7" x-text="fmtMs(bleRtt.result.stats?.min_ms) + ' / ' + fmtMs(bleRtt.result.stats?.median_ms)"></dd>
|
||||
<dt class="col-5 text-muted">mean / max</dt>
|
||||
<dd class="col-7" x-text="fmtMs(bleRtt.result.stats?.mean_ms) + ' / ' + fmtMs(bleRtt.result.stats?.max_ms)"></dd>
|
||||
<dt class="col-5 text-muted" x-show="bleRtt.result.error">Fehler</dt>
|
||||
<dd class="col-7 text-danger" x-show="bleRtt.result.error" x-text="bleRtt.result.error"></dd>
|
||||
</dl>
|
||||
</template>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary mb-2">Stream-Ticks</h6>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-end mb-2">
|
||||
<div>
|
||||
<label class="form-label text-muted small mb-1" for="ble-stream-count">Ticks</label>
|
||||
<input id="ble-stream-count" type="number" class="form-control form-control-sm config-input"
|
||||
min="2" max="500" x-model.number="bleStream.count" :disabled="bleTestBusy">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label text-muted small mb-1" for="ble-stream-period">Period ms</label>
|
||||
<input id="ble-stream-period" type="number" class="form-control form-control-sm config-input"
|
||||
min="8" max="16" x-model.number="bleStream.period_ms" :disabled="bleTestBusy">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label text-muted small mb-1" for="ble-stream-tol">Tol ms</label>
|
||||
<input id="ble-stream-tol" type="number" class="form-control form-control-sm config-input"
|
||||
min="0" max="20" step="0.5" x-model.number="bleStream.tol_ms" :disabled="bleTestBusy">
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
@click="runBleStream()" :disabled="bleTestBusy || busy"
|
||||
x-text="bleTestBusy === 'stream' ? 'Läuft…' : 'Start Stream'"></button>
|
||||
</div>
|
||||
<template x-if="bleStream.result">
|
||||
<dl class="row small mb-0">
|
||||
<dt class="col-5 text-muted">received</dt>
|
||||
<dd class="col-7" x-text="bleStream.result.received + ' / ' + bleStream.result.requested"></dd>
|
||||
<dt class="col-5 text-muted">window_ok</dt>
|
||||
<dd class="col-7"
|
||||
:class="bleStream.result.window_ok ? 'text-success' : 'text-warning'"
|
||||
x-text="bleStream.result.window_ok ? 'yes' : 'no'"></dd>
|
||||
<dt class="col-5 text-muted">dT ESP</dt>
|
||||
<dd class="col-7" x-text="fmtStats(bleStream.result.dt_esp_ms)"></dd>
|
||||
<dt class="col-5 text-muted">dR PC</dt>
|
||||
<dd class="col-7" x-text="fmtStats(bleStream.result.dr_pc_ms)"></dd>
|
||||
<dt class="col-5 text-muted">outside tol</dt>
|
||||
<dd class="col-7" x-text="bleStream.result.outside_tol + ' / ' + (bleStream.result.dt_esp_ms?.n || 0)"></dd>
|
||||
<dt class="col-5 text-muted" x-show="bleStream.result.error">Fehler</dt>
|
||||
<dd class="col-7 text-danger" x-show="bleStream.result.error" x-text="bleStream.result.error"></dd>
|
||||
</dl>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="row g-4">
|
||||
<section class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
@@ -461,9 +544,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small px-3 pt-2 mb-0">
|
||||
<strong>Live-Stream</strong> startet die schnelle <code>CACHE_STATUS</code>-Abfrage (~16 ms).
|
||||
Pro Slave <strong>Accel</strong> aktiviert den ESP-NOW-Accel-Stream; Tap-Notify (S/D/T) steuert
|
||||
die Tap-Arten auf dem Slave.
|
||||
<strong>Live-Stream</strong>: UART ≈ <code>CACHE_STATUS</code>-Poll (~16 ms); BLE ≈
|
||||
<code>BLE_STREAM_TICK</code>-Push. Pro Slave <strong>Accel</strong> aktiviert den ESP-NOW-Stream;
|
||||
Tap-Notify (S/D/T) steuert die Tap-Arten auf dem Slave.
|
||||
</p>
|
||||
<div class="px-3 pb-2 d-flex flex-wrap gap-2 align-items-center">
|
||||
<button type="button"
|
||||
@@ -472,6 +555,9 @@
|
||||
@click="setLiveStream(!state.live_stream)"
|
||||
:disabled="busy || !hostUp() || !(state.clients || []).length"
|
||||
x-text="state.live_stream ? 'Live-Stream aus' : 'Live-Stream an'"></button>
|
||||
<span class="text-muted small font-monospace" x-show="isBLEHost() && state.live_stream"
|
||||
x-text="bleTickLabel()"
|
||||
title="Median Δt zwischen BLE_STREAM_TICK (esp_timestamp_us)"></span>
|
||||
<span class="text-muted small">Tap alle Slaves:</span>
|
||||
<label class="tap-toggle"><input type="checkbox" x-model="allTapSingle" :disabled="busy"> S</label>
|
||||
<label class="tap-toggle"><input type="checkbox" x-model="allTapDouble" :disabled="busy"> D</label>
|
||||
@@ -815,6 +901,9 @@
|
||||
bleScanning: false,
|
||||
bleScanErr: '',
|
||||
hostBusy: false,
|
||||
bleTestBusy: false,
|
||||
bleRtt: { count: 50, warmup: 5, result: null },
|
||||
bleStream: { count: 50, period_ms: 16, tol_ms: 2, result: null },
|
||||
masterDz: 100,
|
||||
masterDzSeeded: false,
|
||||
masterLogLevel: 0,
|
||||
@@ -867,6 +956,82 @@
|
||||
isUARTHost() {
|
||||
return this.hostUp() && (this.state?.host_transport || 'uart') === 'uart';
|
||||
},
|
||||
isBLEHost() {
|
||||
return this.hostUp() && this.state?.host_transport === 'ble';
|
||||
},
|
||||
bleTickLabel() {
|
||||
const t = this.state?.ble_tick;
|
||||
if (!t || !t.samples) return 'BLE tick: …';
|
||||
const med = Number(t.median_ms).toFixed(2);
|
||||
const period = t.period_ms != null ? t.period_ms : '—';
|
||||
return `BLE tick median ${med} ms (n=${t.samples}, period ${period} ms)`;
|
||||
},
|
||||
fmtMs(v) {
|
||||
if (v == null || Number.isNaN(v)) return '—';
|
||||
return Number(v).toFixed(3) + ' ms';
|
||||
},
|
||||
fmtStats(s) {
|
||||
if (!s?.n) return '—';
|
||||
return `min ${this.fmtMs(s.min_ms)} · med ${this.fmtMs(s.median_ms)} · mean ${this.fmtMs(s.mean_ms)} · max ${this.fmtMs(s.max_ms)}`;
|
||||
},
|
||||
async runBleRtt() {
|
||||
this.bleTestBusy = 'rtt';
|
||||
this.bleRtt.result = null;
|
||||
try {
|
||||
const r = await fetch('/api/ble-rtt', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
count: this.bleRtt.count,
|
||||
warmup: this.bleRtt.warmup,
|
||||
}),
|
||||
});
|
||||
const data = await r.json();
|
||||
this.bleRtt.result = data;
|
||||
if (!data.success) {
|
||||
this.flash(data.error || 'BLE RTT fehlgeschlagen', false);
|
||||
} else {
|
||||
this.flash(
|
||||
`BLE RTT: mean ${Number(data.stats?.mean_ms).toFixed(3)} ms (n=${data.stats?.n})`,
|
||||
true
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.flash(String(e), false);
|
||||
} finally {
|
||||
this.bleTestBusy = false;
|
||||
}
|
||||
},
|
||||
async runBleStream() {
|
||||
this.bleTestBusy = 'stream';
|
||||
this.bleStream.result = null;
|
||||
try {
|
||||
const r = await fetch('/api/ble-stream', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
count: this.bleStream.count,
|
||||
period_ms: this.bleStream.period_ms,
|
||||
tol_ms: this.bleStream.tol_ms,
|
||||
timeout_s: 20,
|
||||
}),
|
||||
});
|
||||
const data = await r.json();
|
||||
this.bleStream.result = data;
|
||||
if (!data.success) {
|
||||
this.flash(data.error || 'BLE Stream-Test fehlgeschlagen', false);
|
||||
} else {
|
||||
this.flash(
|
||||
`BLE Stream: ${data.received}/${data.requested} ticks, window_ok=${data.window_ok ? 'yes' : 'no'}`,
|
||||
true
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
this.flash(String(e), false);
|
||||
} finally {
|
||||
this.bleTestBusy = false;
|
||||
}
|
||||
},
|
||||
hostBadge() {
|
||||
const t = this.state?.host_transport || 'none';
|
||||
if (!this.hostUp()) return 'off';
|
||||
@@ -981,9 +1146,14 @@
|
||||
if (this._tapTimer) clearInterval(this._tapTimer);
|
||||
this._tapTimer = setInterval(() => { this._tapClock++; }, 250);
|
||||
const connect = () => {
|
||||
if (this.ws && (this.ws.readyState === WebSocket.OPEN ||
|
||||
this.ws.readyState === WebSocket.CONNECTING)) {
|
||||
try { this.ws.close(); } catch (_) {}
|
||||
}
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.onopen = () => {
|
||||
this.wsConnected = true;
|
||||
this._lastWsAt = Date.now();
|
||||
this.refreshBattery();
|
||||
this.refreshSerialPorts();
|
||||
};
|
||||
@@ -993,6 +1163,7 @@
|
||||
};
|
||||
this.ws.onmessage = (e) => {
|
||||
try {
|
||||
this._lastWsAt = Date.now();
|
||||
const msg = JSON.parse(e.data);
|
||||
if (msg.type === 'ota_progress') {
|
||||
this.applyOTAProgress(msg);
|
||||
@@ -1024,6 +1195,14 @@
|
||||
} catch (_) {}
|
||||
};
|
||||
};
|
||||
if (this._wsWatch) clearInterval(this._wsWatch);
|
||||
// If server dropped us from the hub without closing, force reconnect.
|
||||
this._wsWatch = setInterval(() => {
|
||||
if (!this.wsConnected || !this.state?.live_stream) return;
|
||||
if (Date.now() - (this._lastWsAt || 0) > 4000) {
|
||||
try { this.ws?.close(); } catch (_) {}
|
||||
}
|
||||
}, 2000);
|
||||
connect();
|
||||
},
|
||||
preserveBatteryInState(prev, next) {
|
||||
|
||||
Reference in New Issue
Block a user