Improve dashboard master config and separate slave deadzone updates.
Always show master deadzone input with read/set controls; apply bulk slave changes via slaves_only without changing the master's BMA456. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
85aeab85c0
commit
80fb9cf55e
@ -62,8 +62,9 @@ The dashboard can configure nodes using the same UART commands as the CLI:
|
|||||||
|
|
||||||
| UI action | CLI equivalent |
|
| UI action | CLI equivalent |
|
||||||
|-----------|------------------|
|
|-----------|------------------|
|
||||||
| Master / slave deadzone | `deadzone -set -value N -client ID` |
|
| Nur Master | `deadzone -set -value N -client 0` |
|
||||||
| Alle Slaves | `deadzone -set -value N -all` |
|
| Einzelner Slave | `deadzone -set -value N -client ID` |
|
||||||
|
| Alle Slaves | per-slave ESP-NOW (Master bleibt unverändert; CLI `-all` setzt auch den Master) |
|
||||||
| Unicast test | `unicast-test -client ID` |
|
| Unicast test | `unicast-test -client ID` |
|
||||||
|
|
||||||
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `POST /api/unicast-test`.
|
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `POST /api/unicast-test`.
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -21,6 +22,8 @@ type deadzoneAPIRequest struct {
|
|||||||
Deadzone uint32 `json:"deadzone"`
|
Deadzone uint32 `json:"deadzone"`
|
||||||
ClientID uint32 `json:"client_id"`
|
ClientID uint32 `json:"client_id"`
|
||||||
AllClients bool `json:"all_clients"`
|
AllClients bool `json:"all_clients"`
|
||||||
|
// SlavesOnly: with all_clients, push to ESP-NOW slaves only (master BMA456 unchanged).
|
||||||
|
SlavesOnly bool `json:"slaves_only"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type unicastAPIRequest struct {
|
type unicastAPIRequest struct {
|
||||||
@ -84,12 +87,30 @@ func serveDeadzonePost(w http.ResponseWriter, r *http.Request, link *managedSeri
|
|||||||
writeJSON(w, http.StatusBadRequest, deadzoneAPIResponse{Error: "invalid JSON"})
|
writeJSON(w, http.StatusBadRequest, deadzoneAPIResponse{Error: "invalid JSON"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := link.AccelDeadzone(&pb.AccelDeadzoneRequest{
|
if body.AllClients && body.SlavesOnly {
|
||||||
|
updated, err := applyDeadzoneToSlaves(link, body.Deadzone)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, deadzoneAPIResponse{
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, deadzoneAPIResponse{
|
||||||
|
Deadzone: body.Deadzone,
|
||||||
|
Success: updated > 0,
|
||||||
|
SlavesUpdated: updated,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &pb.AccelDeadzoneRequest{
|
||||||
Write: true,
|
Write: true,
|
||||||
Deadzone: body.Deadzone,
|
Deadzone: body.Deadzone,
|
||||||
ClientId: body.ClientID,
|
ClientId: body.ClientID,
|
||||||
AllClients: body.AllClients,
|
AllClients: body.AllClients,
|
||||||
})
|
}
|
||||||
|
// client_id 0 without all_clients: master BMA456 only (same as CLI -client 0).
|
||||||
|
resp, err := link.AccelDeadzone(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, http.StatusServiceUnavailable, deadzoneAPIResponse{
|
writeJSON(w, http.StatusServiceUnavailable, deadzoneAPIResponse{
|
||||||
ClientID: body.ClientID,
|
ClientID: body.ClientID,
|
||||||
@ -105,6 +126,36 @@ func serveDeadzonePost(w http.ResponseWriter, r *http.Request, link *managedSeri
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyDeadzoneToSlaves sets deadzone on each registered slave via per-client UART/ESP-NOW.
|
||||||
|
// Does not change the master's local BMA456 (use client_id 0 for that).
|
||||||
|
func applyDeadzoneToSlaves(link *managedSerial, deadzone uint32) (uint32, error) {
|
||||||
|
clients, err := link.listClients()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var updated uint32
|
||||||
|
for _, c := range clients {
|
||||||
|
resp, err := link.AccelDeadzone(&pb.AccelDeadzoneRequest{
|
||||||
|
Write: true,
|
||||||
|
Deadzone: deadzone,
|
||||||
|
ClientId: c.GetId(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resp.GetSuccess() {
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(clients) == 0 {
|
||||||
|
return 0, fmt.Errorf("no slaves registered")
|
||||||
|
}
|
||||||
|
if updated == 0 {
|
||||||
|
return 0, fmt.Errorf("deadzone not applied to any slave")
|
||||||
|
}
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
func serveUnicastTest(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
func serveUnicastTest(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||||
var body unicastAPIRequest
|
var body unicastAPIRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
|||||||
@ -118,6 +118,12 @@
|
|||||||
}
|
}
|
||||||
.btn-sm { font-size: 0.8rem; }
|
.btn-sm { font-size: 0.8rem; }
|
||||||
.dz-input { width: 5.5rem; }
|
.dz-input { width: 5.5rem; }
|
||||||
|
.config-input { max-width: 8rem; }
|
||||||
|
.config-block {
|
||||||
|
border-top: 1px solid var(--pp-border);
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body x-data="dashboard()" x-init="connect()">
|
<body x-data="dashboard()" x-init="connect()">
|
||||||
@ -145,9 +151,10 @@
|
|||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<section class="col-lg-4">
|
<section class="col-lg-4">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header">Master</div>
|
<div class="card-header">Master (BMA456 lokal)</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-muted small mb-2" x-text="'UART ' + (state.serial_port || '—')"></p>
|
<p class="text-muted small mb-2" x-text="'UART ' + (state.serial_port || '—')"></p>
|
||||||
|
<p class="text-muted small mb-3">Deadzone nur am Master — Slaves bleiben unverändert.</p>
|
||||||
<template x-if="!state.uart_connected">
|
<template x-if="!state.uart_connected">
|
||||||
<div class="alert alert-warning py-2 mb-2"
|
<div class="alert alert-warning py-2 mb-2"
|
||||||
x-text="state.serial_error || 'UART disconnected — reconnecting…'"></div>
|
x-text="state.serial_error || 'UART disconnected — reconnecting…'"></div>
|
||||||
@ -156,7 +163,7 @@
|
|||||||
<div class="alert alert-danger py-2" x-text="state.serial_error || 'Serial error'"></div>
|
<div class="alert alert-danger py-2" x-text="state.serial_error || 'Serial error'"></div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="state.master?.ok">
|
<template x-if="state.master?.ok">
|
||||||
<dl class="row mb-3">
|
<dl class="row mb-0">
|
||||||
<dt class="col-5 text-muted">Version</dt>
|
<dt class="col-5 text-muted">Version</dt>
|
||||||
<dd class="col-7" x-text="state.master.version"></dd>
|
<dd class="col-7" x-text="state.master.version"></dd>
|
||||||
<dt class="col-5 text-muted">Git</dt>
|
<dt class="col-5 text-muted">Git</dt>
|
||||||
@ -164,21 +171,38 @@
|
|||||||
<dt class="col-5 text-muted">Deadzone</dt>
|
<dt class="col-5 text-muted">Deadzone</dt>
|
||||||
<dd class="col-7" x-text="state.master.deadzone != null ? state.master.deadzone + ' LSB' : '—'"></dd>
|
<dd class="col-7" x-text="state.master.deadzone != null ? state.master.deadzone + ' LSB' : '—'"></dd>
|
||||||
</dl>
|
</dl>
|
||||||
<div class="d-flex flex-wrap gap-2 align-items-center" x-show="state.uart_connected">
|
|
||||||
<input type="number" class="form-control form-control-sm dz-input"
|
|
||||||
min="0" max="4095" placeholder="LSB"
|
|
||||||
x-model.number="masterDz"
|
|
||||||
:disabled="busy">
|
|
||||||
<button type="button" class="btn btn-primary btn-sm"
|
|
||||||
@click="setDeadzone(0, masterDz)"
|
|
||||||
:disabled="busy || !state.uart_connected">
|
|
||||||
Master setzen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<template x-if="state.master && !state.master.ok">
|
<template x-if="state.master && !state.master.ok">
|
||||||
<div class="alert alert-warning py-2" x-text="state.master.error"></div>
|
<div class="alert alert-warning py-2 mb-0" x-text="state.master.error"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div class="config-block">
|
||||||
|
<h6 class="text-secondary mb-2">Konfiguration Master</h6>
|
||||||
|
<label class="form-label text-muted small mb-1" for="master-dz-input">
|
||||||
|
Accel Deadzone (LSB)
|
||||||
|
</label>
|
||||||
|
<div class="d-flex flex-wrap gap-2 align-items-end">
|
||||||
|
<input id="master-dz-input" type="number"
|
||||||
|
class="form-control form-control-sm config-input"
|
||||||
|
min="0" max="4095" step="1"
|
||||||
|
placeholder="z. B. 100"
|
||||||
|
x-model.number="masterDz"
|
||||||
|
:disabled="busy || !state.uart_connected">
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
|
@click="readMasterDeadzone()"
|
||||||
|
:disabled="busy || !state.uart_connected">
|
||||||
|
Lesen
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm"
|
||||||
|
@click="setMasterDeadzone()"
|
||||||
|
: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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -200,7 +224,8 @@
|
|||||||
<span class="badge bg-secondary" x-text="(state.clients || []).length + ' registered'"></span>
|
<span class="badge bg-secondary" x-text="(state.clients || []).length + ' registered'"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<p class="text-muted small px-3 pt-2 mb-0">Slaves per ESP-NOW — Master-Deadzone bleibt separat.</p>
|
||||||
|
<div class="card-body p-0 pt-2">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table pp-table table-hover">
|
<table class="table pp-table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
@ -307,7 +332,7 @@
|
|||||||
this.configMsgOk = ok;
|
this.configMsgOk = ok;
|
||||||
setTimeout(() => { this.configMsg = ''; }, 5000);
|
setTimeout(() => { this.configMsg = ''; }, 5000);
|
||||||
},
|
},
|
||||||
async setDeadzone(clientId, deadzone) {
|
async setDeadzone(clientId, deadzone, opts = {}) {
|
||||||
if (deadzone == null || deadzone < 0) {
|
if (deadzone == null || deadzone < 0) {
|
||||||
this.flash('Ungültiger Deadzone-Wert', false);
|
this.flash('Ungültiger Deadzone-Wert', false);
|
||||||
return;
|
return;
|
||||||
@ -321,12 +346,19 @@
|
|||||||
write: true,
|
write: true,
|
||||||
deadzone: deadzone,
|
deadzone: deadzone,
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
all_clients: false
|
all_clients: !!opts.allClients,
|
||||||
|
slaves_only: !!opts.slavesOnly
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (!r.ok || !data.success) {
|
if (!r.ok || !data.success) {
|
||||||
this.flash(data.error || `Deadzone für Client ${clientId} fehlgeschlagen`, false);
|
const err = data.error ||
|
||||||
|
(opts.slavesOnly ? 'Deadzone für Slaves fehlgeschlagen' : `Deadzone für Client ${clientId} fehlgeschlagen`);
|
||||||
|
this.flash(err, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (opts.slavesOnly) {
|
||||||
|
this.flash(`Slaves: Deadzone ${data.deadzone} LSB (${data.slaves_updated} per ESP-NOW, Master unverändert)`, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const who = clientId === 0 ? 'Master' : `Slave ${clientId}`;
|
const who = clientId === 0 ? 'Master' : `Slave ${clientId}`;
|
||||||
@ -337,35 +369,33 @@
|
|||||||
this.busy = false;
|
this.busy = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async setDeadzoneAll(deadzone) {
|
async readMasterDeadzone() {
|
||||||
if (deadzone == null || deadzone < 0) {
|
|
||||||
this.flash('Ungültiger Deadzone-Wert', false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/deadzone', {
|
const r = await fetch('/api/deadzone?client_id=0');
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
write: true,
|
|
||||||
deadzone: deadzone,
|
|
||||||
client_id: 0,
|
|
||||||
all_clients: true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (!r.ok || !data.success) {
|
if (!r.ok || !data.success) {
|
||||||
this.flash(data.error || 'Deadzone für alle Slaves fehlgeschlagen', false);
|
this.flash(data.error || 'Master-Deadzone lesen fehlgeschlagen', false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.flash(`Alle Slaves: Deadzone ${data.deadzone} LSB (${data.slaves_updated} per ESP-NOW)`, true);
|
this.masterDz = data.deadzone;
|
||||||
|
this.flash(`Master: Deadzone ${data.deadzone} LSB`, true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.flash(String(e), false);
|
this.flash(String(e), false);
|
||||||
} finally {
|
} finally {
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async setMasterDeadzone() {
|
||||||
|
await this.setDeadzone(0, this.masterDz);
|
||||||
|
},
|
||||||
|
async setDeadzoneAll(deadzone) {
|
||||||
|
if (deadzone == null || deadzone < 0) {
|
||||||
|
this.flash('Ungültiger Deadzone-Wert', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.setDeadzone(0, deadzone, { allClients: true, slavesOnly: true });
|
||||||
|
},
|
||||||
async unicastTest(clientId) {
|
async unicastTest(clientId) {
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user