Add BLE host transport selectable via DIP8 alongside UART.

Master can speak the same framed protobuf protocol over NimBLE or UART; goTool gains -ble, ble-rtt, and ble-stream timing checks for ESP-NOW coexistence.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-23 16:11:10 +02:00
co-authored by Cursor
parent d7441cb85f
commit 92a13d7fd0
28 changed files with 2306 additions and 250 deletions
+128
View File
@@ -0,0 +1,128 @@
package main
import (
"flag"
"fmt"
"math"
"sort"
"time"
pb "powerpod/gotool/pb"
"google.golang.org/protobuf/proto"
)
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)")
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")
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
}
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])
return nil
}
func printStat(name string, v []float64) {
if len(v) == 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])
}