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:
2026-10-23 23:27:39 +02:00
co-authored by Cursor
parent aa67edfe94
commit af20635047
14 changed files with 893 additions and 293 deletions
+12 -100
View File
@@ -3,12 +3,6 @@ package main
import (
"flag"
"fmt"
"math"
"sort"
"time"
pb "powerpod/gotool/pb"
"google.golang.org/protobuf/proto"
)
func runBleStream(h hostConn, args []string) error {
@@ -21,108 +15,26 @@ func runBleStream(h hostConn, args []string) error {
if err := fs.Parse(args); err != nil {
return err
}
if *periodMs < 8 || *periodMs > 16 {
return fmt.Errorf("--period must be 8..16")
}
if *count < 2 {
return fmt.Errorf("--count must be >= 2")
}
sc, ok := h.(hostStreamConn)
if !ok {
return fmt.Errorf("ble-stream requires -ble (UART has no push ticks)")
}
req := &pb.AccelStreamRequest{
Write: true,
Enable: true,
PeriodMs: uint32(*periodMs),
}
if *clientID == 0 {
req.AllClients = true
} else {
req.ClientId = uint32(*clientID)
}
resp, err := hostAccelStream(h, req)
if err != nil {
return err
}
if !resp.GetSuccess() {
return fmt.Errorf("accel stream enable failed")
}
fmt.Printf("accel stream on; period_ms=%d; collecting %d ticks…\n", *periodMs, *count)
type sample struct {
espUs uint64
pcT float64
out := measureBleStream(h, uint32(*clientID), uint32(*periodMs), *count, *tolMs, *duration)
if !out.Success {
return fmt.Errorf("%s", out.Error)
}
samples := make([]sample, 0, *count)
deadline := time.Now().Add(time.Duration(*duration * float64(time.Second)))
for len(samples) < *count {
remain := time.Until(deadline)
if remain <= 0 {
break
}
payload, err := sc.NextStreamTick(remain)
if err != nil {
break
}
pcT := time.Now().UnixMicro()
var msg pb.UartMessage
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
continue
}
tick := msg.GetBleStreamTick()
if tick == nil {
continue
}
samples = append(samples, sample{espUs: tick.GetEspTimestampUs(), pcT: float64(pcT)})
}
// Best-effort disable
off := &pb.AccelStreamRequest{Write: true, Enable: false, AllClients: req.AllClients, ClientId: req.ClientId}
_, _ = hostAccelStream(h, off)
if len(samples) < 2 {
return fmt.Errorf("need >=2 ticks, got %d (is stream running / DIP8=BLE?)", len(samples))
}
var dT, dR, extra []float64
esp0 := samples[0].espUs
pc0 := samples[0].pcT
for i := 1; i < len(samples); i++ {
dT = append(dT, float64(samples[i].espUs-samples[i-1].espUs)/1000.0)
dR = append(dR, (samples[i].pcT-samples[i-1].pcT)/1000.0)
relEsp := float64(samples[i].espUs-esp0) / 1000.0
relPc := (samples[i].pcT - pc0) / 1000.0
extra = append(extra, relPc-relEsp)
}
period := float64(*periodMs)
outCount := 0
for _, dt := range dT {
if math.Abs(dt-period) > *tolMs {
outCount++
}
}
windowOK := outCount*2 <= len(dT) // majority inside tolerance
fmt.Printf("received=%d/%d period_ms=%d tol=±%.1f ms\n", len(samples), *count, *periodMs, *tolMs)
printStat("dT ESP inter-depart", dT)
printStat("dR PC inter-arrive", dR)
printStat("extra vs first", extra)
fmt.Printf("outside_tol=%d/%d window_ok=%s\n", outCount, len(dT), map[bool]string{true: "yes", false: "no"}[windowOK])
fmt.Printf("received=%d/%d period_ms=%d tol=±%.1f ms\n", out.Received, out.Requested, out.PeriodMs, out.TolMs)
printStat("dT ESP inter-depart", out.DTEsp)
printStat("dR PC inter-arrive", out.DRPc)
printStat("extra vs first", out.ExtraVsFirst)
fmt.Printf("outside_tol=%d/%d window_ok=%s\n", out.OutsideTol, out.DTEsp.N,
map[bool]string{true: "yes", false: "no"}[out.WindowOK])
return nil
}
func printStat(name string, v []float64) {
if len(v) == 0 {
func printStat(name string, s latencyStats) {
if s.N == 0 {
fmt.Printf("%s: (none)\n", name)
return
}
s := append([]float64(nil), v...)
sort.Float64s(s)
fmt.Printf("%s: n=%d min=%.3f median=%.3f mean=%.3f max=%.3f ms\n",
name, len(s), s[0], percentile(s, 0.5), mean(s), s[len(s)-1])
name, s.N, s.MinMs, s.MedMs, s.MeanMs, s.MaxMs)
}