Files
powerpods/goTool/ble_measure.go
T
skrueckenandCursor 178ab46cec 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>
2026-11-01 22:16:28 +01:00

225 lines
5.0 KiB
Go

package main
import (
"fmt"
"math"
"sort"
"time"
pb "powerpod/gotool/pb"
"google.golang.org/protobuf/proto"
)
type latencyStats struct {
N int `json:"n"`
MinMs float64 `json:"min_ms"`
MedMs float64 `json:"median_ms"`
MeanMs float64 `json:"mean_ms"`
MaxMs float64 `json:"max_ms"`
}
type bleRttResult struct {
Success bool `json:"success"`
Count int `json:"count"`
Warmup int `json:"warmup"`
Stats latencyStats `json:"stats"`
Error string `json:"error,omitempty"`
}
type bleStreamResult struct {
Success bool `json:"success"`
Received int `json:"received"`
Requested int `json:"requested"`
PeriodMs uint32 `json:"period_ms"`
TolMs float64 `json:"tol_ms"`
OutsideTol int `json:"outside_tol"`
WindowOK bool `json:"window_ok"`
DTEsp latencyStats `json:"dt_esp_ms"`
DRPc latencyStats `json:"dr_pc_ms"`
ExtraVsFirst latencyStats `json:"extra_vs_first_ms"`
Error string `json:"error,omitempty"`
}
func measureBleRtt(h hostConn, count, warmup int, settle time.Duration) bleRttResult {
out := bleRttResult{Count: count, Warmup: warmup}
if count < 1 {
out.Error = "count must be >= 1"
return out
}
if warmup < 0 {
out.Error = "warmup must be >= 0"
return out
}
if settle > 0 {
time.Sleep(settle)
}
samples := make([]float64, 0, count)
total := warmup + count
for i := 0; i < total; i++ {
t0 := time.Now()
if _, err := hostGetVersion(h); err != nil {
out.Error = fmt.Sprintf("VERSION round-trip %d: %v", i, err)
return out
}
ms := float64(time.Since(t0).Microseconds()) / 1000.0
if i >= warmup {
samples = append(samples, ms)
}
}
out.Stats = makeLatencyStats(samples)
out.Success = true
return out
}
func measureBleStream(h hostConn, clientID, periodMs uint32, count int, tolMs, timeoutSec float64) bleStreamResult {
out := bleStreamResult{
Requested: count,
PeriodMs: periodMs,
TolMs: tolMs,
}
if periodMs < 10 || periodMs > 16 {
out.Error = "period_ms must be 10..16"
return out
}
if count < 2 {
out.Error = "count must be >= 2"
return out
}
sc, ok := h.(hostStreamConn)
if !ok {
out.Error = "BLE stream ticks require BLE host"
return out
}
req := &pb.AccelStreamRequest{
Write: true,
Enable: true,
PeriodMs: periodMs,
}
if clientID == 0 {
req.AllClients = true
} else {
req.ClientId = clientID
}
resp, err := hostAccelStream(h, req)
if err != nil {
out.Error = err.Error()
return out
}
if !resp.GetSuccess() {
out.Error = "accel stream enable failed"
return out
}
type sample struct {
espUs uint64
pcT float64
}
samples := make([]sample, 0, count)
deadline := time.Now().Add(time.Duration(timeoutSec * float64(time.Second)))
if timeoutSec <= 0 {
deadline = time.Now().Add(15 * 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)})
}
off := &pb.AccelStreamRequest{Write: true, Enable: false, AllClients: req.AllClients, ClientId: req.ClientId}
_, _ = hostAccelStream(h, off)
out.Received = len(samples)
if len(samples) < 2 {
out.Error = fmt.Sprintf("need >=2 ticks, got %d (stream running / DIP8=BLE?)", len(samples))
return out
}
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++
}
}
out.OutsideTol = outCount
out.WindowOK = outCount*2 <= len(dT)
out.DTEsp = makeLatencyStats(dT)
out.DRPc = makeLatencyStats(dR)
out.ExtraVsFirst = makeLatencyStats(extra)
out.Success = true
return out
}
func makeLatencyStats(v []float64) latencyStats {
if len(v) == 0 {
return latencyStats{}
}
s := append([]float64(nil), v...)
sort.Float64s(s)
return latencyStats{
N: len(s),
MinMs: s[0],
MedMs: percentile(s, 0.5),
MeanMs: mean(s),
MaxMs: s[len(s)-1],
}
}
func mean(v []float64) float64 {
if len(v) == 0 {
return math.NaN()
}
var s float64
for _, x := range v {
s += x
}
return s / float64(len(v))
}
func percentile(sorted []float64, p float64) float64 {
if len(sorted) == 0 {
return math.NaN()
}
if len(sorted) == 1 {
return sorted[0]
}
idx := p * float64(len(sorted)-1)
lo := int(idx)
hi := lo + 1
if hi >= len(sorted) {
return sorted[lo]
}
frac := idx - float64(lo)
return sorted[lo]*(1-frac) + sorted[hi]*frac
}