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:
+12
-100
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user