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]) }