Make controller input best-effort and harden OTA rollback.

Accel/tap use fire-and-forget ESP-NOW with a 10 ms FreeRTOS-safe cadence, OTA recovers from seq gaps and auto-reboots after success, and bootloader app rollback confirms after a stable boot.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-01 22:16:28 +01:00
co-authored by Cursor
parent ae8ef40820
commit 178ab46cec
35 changed files with 236 additions and 79 deletions
+2 -2
View File
@@ -27,7 +27,7 @@ go run . -port /dev/ttyUSB0 clients
| `deadzone` | `0x06` | Get/set accelerometer deadzone LSB (`-set`, `-value`, `-client`, `-all`) |
| `tap-config` | `0x20` | Get/set BMA456 tap detection params (`-set`, `-client`, `-all`, `-sens`, `-max-gest`, `-shock`, `-quiet`, `-wait`, `-axis`, `-single`, `-double`, `-triple`) |
| `tap-notify` | `0x1b` | Get/set which tap kinds (single/double/triple) notify via ESP-NOW (`-set`, `-client`, `-all`, `-single`, `-double`, `-triple`) |
| `cache-status` | `0x1d` | Subscribed accel + tap cache (`CACHE_STATUS`); one UART round-trip for 16 ms polling |
| `cache-status` | `0x1d` | Subscribed accel + tap cache (`CACHE_STATUS`); one UART round-trip for ~8 ms polling |
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
| `echo-ping` | `0x1e` | ESP-NOW echo round-trip to one slave (`-client`); prints `rtt_ms` (host UART chain) and `esp_rtt_us` (master ESP-NOW, raw µs) |
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
@@ -79,7 +79,7 @@ Open [http://localhost:8080](http://localhost:8080) — Host-Verbindung panel (U
| Column | Meaning |
|--------|---------|
| Tap-Notify (S/D/T) | Which tap kinds the **slave** sends to the master over ESP-NOW (UART `TAP_NOTIFY`) — does **not** poll UART |
| Tap (An/Aus) | Host **receive**: poll master tap cache (~16 ms) and show last tap for **≥2 s** |
| Tap (An/Aus) | Host **receive**: poll master tap cache (~8 ms) and show last tap for **≥2 s** |
Enable notify first, then turn receive on to see events. Same split as the external WebSocket API (`set_tap_notify` vs `set_tap_stream`).
+1 -1
View File
@@ -94,7 +94,7 @@ func serveBleStreamTest(w http.ResponseWriter, r *http.Request, link *managedHos
return
}
count := 50
periodMs := uint32(16)
periodMs := uint32(10)
tolMs := 2.0
timeoutS := 15.0
var clientID uint32
+1 -1
View File
@@ -16,7 +16,7 @@ import (
)
const (
defaultAccelStreamInterval = 16 * time.Millisecond
defaultAccelStreamInterval = 10 * time.Millisecond
defaultPreFetchMs = 2
minAPIStreamInterval = 1 * time.Millisecond
maxAPIStreamInterval = 10 * time.Second
+2 -2
View File
@@ -79,8 +79,8 @@ func measureBleStream(h hostConn, clientID, periodMs uint32, count int, tolMs, t
PeriodMs: periodMs,
TolMs: tolMs,
}
if periodMs < 8 || periodMs > 16 {
out.Error = "period_ms must be 8..16"
if periodMs < 10 || periodMs > 16 {
out.Error = "period_ms must be 10..16"
return out
}
if count < 2 {
+1 -1
View File
@@ -13,7 +13,7 @@ func runAccelStreamCmd(h hostConn, args []string) error {
all := fs.Bool("all", false, "all registered clients")
enable := fs.Bool("enable", true, "enable stream")
disable := fs.Bool("disable", false, "disable stream")
period := fs.Uint("period", 0, "BLE push period_ms 8–16 (0=leave/default)")
period := fs.Uint("period", 0, "BLE push period_ms 10–16 (0=leave/default)")
if err := fs.Parse(args); err != nil {
return err
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
func runBleStream(h hostConn, args []string) error {
fs := flag.NewFlagSet("ble-stream", flag.ExitOnError)
clientID := fs.Uint("client", 0, "slave client id (0 = all_clients)")
periodMs := fs.Uint("period", 16, "BLE push period_ms (8–16)")
periodMs := fs.Uint("period", 10, "BLE push period_ms (10–16)")
count := fs.Int("count", 50, "number of BleStreamTick samples")
tolMs := fs.Float64("tol", 2.0, "tolerance ms around period for window_ok")
duration := fs.Float64("timeout", 15.0, "max seconds waiting for ticks")
+2 -2
View File
@@ -75,7 +75,7 @@ type DashboardState struct {
UARTConnected bool `json:"uart_connected"`
SerialOK bool `json:"serial_ok"`
SerialError string `json:"serial_error,omitempty"`
/** Host: fast CACHE_STATUS poll (~16 ms) for accel + tap. */
/** Host: fast CACHE_STATUS poll (~8 ms) for accel + tap. */
LiveStream bool `json:"live_stream"`
/** BLE live path: inter-tick stats from BleStreamTick.esp_timestamp_us. */
BleTick *BleTickView `json:"ble_tick,omitempty"`
@@ -489,7 +489,7 @@ func (h *wsHub) snapshotClients() []ClientView {
return out
}
// patchLiveStream toggles host CACHE_STATUS polling (~16 ms).
// patchLiveStream toggles host CACHE_STATUS polling (~8 ms).
func (h *wsHub) patchLiveStream(enabled bool) {
h.mu.Lock()
h.liveStream = enabled
+2 -2
View File
@@ -148,13 +148,13 @@ POST /api/ble-rtt
→ {"success":true,"count":50,"warmup":5,"stats":{"n":50,"min_ms":…,"median_ms":…,"mean_ms":…,"max_ms":…}}
POST /api/ble-stream
{"count":50,"period_ms":16,"tol_ms":2,"timeout_s":15,"client_id":0}
{"count":50,"period_ms":8,"tol_ms":2,"timeout_s":15,"client_id":0}
→ {"success":true,"received":50,"requested":50,"window_ok":true,"dt_esp_ms":{…},"dr_pc_ms":{…},…}
```
Pauses dashboard/API polling while running. Requires active BLE host (`DIP8=1`).
### Live stream (host `CACHE_STATUS` poll ~16 ms; BLE uses `BLE_STREAM_TICK`)
### Live stream (host `CACHE_STATUS` poll ~8 ms; BLE uses `BLE_STREAM_TICK`)
```http
GET /api/live-stream
+2 -2
View File
@@ -21,7 +21,7 @@ const (
otaDistQueryTimeout = 2 * time.Second
otaDistEmitMinInterval = 150 * time.Millisecond
// Pace host chunks so the master UART RX ring is not overrun (~20 frames/block).
otaHostChunkPaceUART = 3 * time.Millisecond
otaHostChunkPaceUART = 5 * time.Millisecond
// Slightly slower for BLE WriteWithoutResponse flow control.
otaHostChunkPaceBLE = 6 * time.Millisecond
otaBlockMaxRetries = 3
@@ -389,7 +389,7 @@ func runOTAUnlocked(t otaTransport, firmware []byte, onProgress otaProgressFn) e
mergeSlaveProgressResponse(prog, slaveBytes, slaveDetails)
}
notify("done", "", 100,
fmt.Sprintf("Fertig — %d bytes, Boot-Slot %d. Master und Slaves neu starten.",
fmt.Sprintf("Fertig — %d bytes, Boot-Slot %d. Geräte starten neu auf die neue Firmware.",
st.GetBytesWritten(), st.GetTargetSlot()),
OTAProgress{
Bytes: st.GetBytesWritten(), Slot: st.GetTargetSlot(),