package main import ( "flag" "fmt" "log" "os" "time" ) const defaultBaud = 921600 func usage() { fmt.Fprintf(os.Stderr, "usage: gotool [-port /dev/ttyUSB0 | -ble] \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, " battery-uv get/set software LiPo UV protection (per pod / all)\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 "battery-uv", "battery_uv": sp, ok := h.(*serialPort) if !ok { runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd) break } runErr = runBatteryUv(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 }