Files
powerpods/goTool/main.go
T
skrueckenandCursor 7f28a48a10 Add runtime UV protection toggle and treat near-zero sense as external power.
Per-pod NVS enable (default off) via UART/ESP-NOW/dashboard, and skip UV latch when LiPo ADC reads ~0 mV so USB/external supply still boots.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-01 22:51:17 +01:00

291 lines
9.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, " 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
}