Files
powerpods/goTool/main.go
T
skrueckenandCursor 92a13d7fd0 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>
2026-10-23 16:11:10 +02:00

270 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"flag"
"fmt"
"log"
"os"
"time"
)
const defaultBaud = 921600
func usage() {
fmt.Fprintf(os.Stderr, "usage: gotool [-port /dev/ttyUSB0 | -ble] <command>\n")
fmt.Fprintf(os.Stderr, " DIP8 on master: 0=UART (-port), 1=BLE (-ble); reboot after DIP change\n")
fmt.Fprintf(os.Stderr, " test uses uart.master from bench config when -port is omitted\n\n")
fmt.Fprintf(os.Stderr, "commands:\n")
fmt.Fprintf(os.Stderr, " version firmware version and git hash\n")
fmt.Fprintf(os.Stderr, " clients registered ESP-NOW slaves on the master\n")
fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\n")
fmt.Fprintf(os.Stderr, " tap-config get/set BMA456 tap detection parameters\n")
fmt.Fprintf(os.Stderr, " tap-notify get/set which tap kinds notify via ESP-NOW\n")
fmt.Fprintf(os.Stderr, " cache-status subscribed accel + tap cache (one round-trip)\n")
fmt.Fprintf(os.Stderr, " accel-stream enable/disable ESP-NOW accel stream (+ BLE period)\n")
fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n")
fmt.Fprintf(os.Stderr, " echo-ping ESP-NOW timestamp echo round-trip to one slave\n")
fmt.Fprintf(os.Stderr, " ble-rtt Host↔Master RTT via VERSION (N samples)\n")
fmt.Fprintf(os.Stderr, " ble-stream enable stream + measure BleStreamTick window\n")
fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n")
fmt.Fprintf(os.Stderr, " serve web dashboard (UART only)\n")
fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions; UART host only)\n")
fmt.Fprintf(os.Stderr, " ota-progress query per-slave ESP-NOW OTA progress on master\n")
fmt.Fprintf(os.Stderr, " led-ring set LED ring progress bar (0–100%%, rgb, intensity)\n")
fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n")
fmt.Fprintf(os.Stderr, " restart reboot master or slave (ESP-NOW)\n")
fmt.Fprintf(os.Stderr, " log-level get/set master ESP-IDF log level (global)\n\n")
flag.PrintDefaults()
}
func main() {
portName := flag.String("port", "", "serial port (e.g. /dev/ttyUSB0); XOR with -ble")
baud := flag.Int("baud", defaultBaud, "UART baud rate")
useBLE := flag.Bool("ble", false, "use BLE host transport (DIP8=1); XOR with -port")
bleName := flag.String("ble-name", bleDefaultName, "BLE advertised name")
bleSettle := flag.Float64("ble-settle", 1.0, "seconds to settle after BLE connect")
flag.Parse()
if flag.NArg() < 1 {
usage()
os.Exit(2)
}
cmd := flag.Arg(0)
var runErr error
switch cmd {
case "test", "autotest":
if *useBLE {
runErr = fmt.Errorf("autotest requires -port (UART)")
break
}
runErr = runTest(*portName, *baud, flag.Args()[1:])
case "serve", "web", "dashboard":
if *useBLE {
fmt.Fprintf(os.Stderr, "serve requires -port (UART only in this release)\n")
os.Exit(2)
}
if *portName == "" {
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
usage()
os.Exit(2)
}
runErr = runServe(*portName, *baud, flag.Args()[1:])
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "tap-config", "tap_config", "tap-notify", "tap_notify", "cache-status", "cache_status", "accel-stream", "accel_stream", "unicast-test", "unicast_test", "echo-ping", "echo_ping", "ble-rtt", "ble_rtt", "ble-stream", "ble_stream", "led-ring", "led_ring", "find-me", "find_me", "restart", "log-level", "log_level", "ota", "ota-progress", "ota_progress":
if *useBLE && *portName != "" {
fmt.Fprintf(os.Stderr, "use either -port or -ble, not both\n")
os.Exit(2)
}
if !*useBLE && *portName == "" {
fmt.Fprintf(os.Stderr, "command %q requires -port or -ble\n\n", cmd)
usage()
os.Exit(2)
}
if cmd == "ota" || cmd == "ota-progress" || cmd == "ota_progress" {
if *useBLE {
fmt.Fprintf(os.Stderr, "OTA requires UART host (DIP8=0, -port)\n")
os.Exit(2)
}
}
var h hostConn
if *useBLE {
bl, err := openBLE(*bleName, time.Duration(*bleSettle*float64(time.Second)))
if err != nil {
log.Fatalf("open BLE: %v", err)
}
h = bl
} else {
sp, err := openSerial(*portName, *baud)
if err != nil {
log.Fatalf("open serial: %v", err)
}
h = sp
}
registerShutdown(func() { _ = h.Close() })
enableShutdownOnInterrupt()
defer h.Close()
switch cmd {
case "version":
runErr = runVersionHost(h)
case "clients", "client-info":
runErr = runClientsHost(h)
case "cache-status", "cache_status":
runErr = runCacheStatusHost(h)
case "accel-stream", "accel_stream":
runErr = runAccelStreamCmd(h, flag.Args()[1:])
case "echo-ping", "echo_ping":
runErr = runEchoPingHost(h, flag.Args()[1:])
case "ble-rtt", "ble_rtt":
runErr = runBleRtt(h, flag.Args()[1:])
case "ble-stream", "ble_stream":
runErr = runBleStream(h, flag.Args()[1:])
case "deadzone", "accel-deadzone":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runDeadzone(sp, flag.Args()[1:])
case "tap-config", "tap_config":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runTapConfig(sp, flag.Args()[1:])
case "tap-notify", "tap_notify":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runTapNotify(sp, flag.Args()[1:])
case "unicast-test", "unicast_test":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runUnicastTest(sp, flag.Args()[1:])
case "led-ring", "led_ring":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runLedRing(sp, flag.Args()[1:])
case "find-me", "find_me":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runFindMe(sp, flag.Args()[1:])
case "restart":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runRestart(sp, flag.Args()[1:])
case "log-level", "log_level":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runLogLevel(sp, flag.Args()[1:])
case "ota":
sp := h.(*serialPort)
runErr = runOTA(sp, flag.Args()[1:])
case "ota-progress", "ota_progress":
sp := h.(*serialPort)
runErr = runOtaProgress(sp, flag.Args()[1:])
}
default:
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
usage()
os.Exit(2)
}
if runErr != nil {
log.Fatal(runErr)
}
}
func runVersionHost(h hostConn) error {
ver, err := hostGetVersion(h)
if err != nil {
return err
}
fmt.Printf("version: %d\n", ver.GetVersion())
fmt.Printf("git_hash: %s\n", ver.GetGitHash())
return nil
}
func runClientsHost(h hostConn) error {
clients, err := hostListClients(h)
if err != nil {
return err
}
if len(clients) == 0 {
fmt.Println("no clients registered")
return nil
}
fmt.Printf("clients (%d):\n", len(clients))
for i, c := range clients {
fmt.Printf(" [%d] id=%d ver=%d available=%v used=%v\n",
i, c.GetId(), c.GetVersion(), c.GetAvailable(), c.GetUsed())
}
return nil
}
func runCacheStatusHost(h hostConn) error {
r, err := hostReadCacheStatus(h)
if err != nil {
return err
}
clients := r.GetClients()
if len(clients) == 0 {
fmt.Println("(no slaves with accel stream or tap notify enabled)")
return nil
}
for _, c := range clients {
id := c.GetClientId()
if a := c.GetAccel(); a != nil {
if !a.GetValid() {
fmt.Printf("client %d accel: no sample yet\n", id)
} else {
fmt.Printf("client %d accel: x=%d y=%d z=%d (age %d ms)\n",
id, a.GetX(), a.GetY(), a.GetZ(), a.GetAgeMs())
}
}
if t := c.GetTap(); t != nil {
fmt.Printf("client %d tap: %s (age %d ms)\n",
id, tapKindLabel(t.GetKind()), t.GetAgeMs())
}
}
return nil
}
func runEchoPingHost(h hostConn, args []string) error {
fs := flag.NewFlagSet("echo-ping", flag.ExitOnError)
clientID := fs.Uint("client", 0, "slave client id from `clients`")
if err := fs.Parse(args); err != nil {
return err
}
if *clientID == 0 {
return fmt.Errorf("client id required (see `gotool clients`)")
}
r, err := hostEchoPing(h, uint32(*clientID))
if err != nil {
return err
}
fmt.Printf("echo ping: success=%v client_id=%d rtt_ms=%.3f (host↔master↔slave) esp_rtt_us=%d (ESP-NOW only)\n",
r.Success, r.ClientID, r.RttMs, r.EspRttUs)
return nil
}