Expose best_effort/fast/wait/reliable via UART, dashboard, and CLI so loss vs latency can be compared under real multi-pod load. Co-authored-by: Cursor <cursoragent@cursor.com>
283 lines
9.2 KiB
Go
283 lines
9.2 KiB
Go
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, " clients-reset-ids clear master id map + live registry (notifies online slaves)\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 (optional -port / -ble; connect from UI)\n")
|
||
fmt.Fprintf(os.Stderr, " ota host OTA upload (A/B partitions; UART or BLE)\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")
|
||
fmt.Fprintf(os.Stderr, " input-send-mode get/set slave accel/tap ESP-NOW send path\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 && *portName != "" {
|
||
fmt.Fprintf(os.Stderr, "use either -port or -ble, not both\n")
|
||
os.Exit(2)
|
||
}
|
||
runErr = runServe(serveOpts{
|
||
portName: *portName,
|
||
baud: *baud,
|
||
useBLE: *useBLE,
|
||
bleName: *bleName,
|
||
settle: time.Duration(*bleSettle * float64(time.Second)),
|
||
}, flag.Args()[1:])
|
||
case "version", "clients", "client-info", "clients-reset-ids", "clients_reset_ids", "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", "input-send-mode", "input_send_mode", "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)
|
||
}
|
||
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 "clients-reset-ids", "clients_reset_ids":
|
||
sp, ok := h.(*serialPort)
|
||
if !ok {
|
||
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
|
||
break
|
||
}
|
||
runErr = runClientsResetIds(sp)
|
||
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 "input-send-mode", "input_send_mode":
|
||
runErr = runInputSendModeCmd(h, flag.Args()[1:])
|
||
case "ota":
|
||
t, ok := h.(otaTransport)
|
||
if !ok {
|
||
runErr = fmt.Errorf("ota: unsupported host transport")
|
||
break
|
||
}
|
||
runErr = runOTA(t, flag.Args()[1:])
|
||
case "ota-progress", "ota_progress":
|
||
t, ok := h.(otaTransport)
|
||
if !ok {
|
||
runErr = fmt.Errorf("ota-progress: unsupported host transport")
|
||
break
|
||
}
|
||
runErr = runOtaProgress(t, 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
|
||
}
|