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>
225 lines
5.0 KiB
Go
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
|
|
}
|