diff --git a/goTool/README.md b/goTool/README.md index 2870417..687f2c3 100644 --- a/goTool/README.md +++ b/goTool/README.md @@ -9,7 +9,8 @@ Full system documentation (roles, ESP-NOW, framing, protobuf): [`../main/README. ```bash cd goTool go mod tidy -go run . -port /dev/ttyUSB0 +go run . -port /dev/ttyUSB0 version +go run . -port /dev/ttyUSB0 clients ``` | Flag | Default | Description | @@ -17,7 +18,21 @@ go run . -port /dev/ttyUSB0 | `-port` | (required) | Serial port on master UART (GPIO2/3 adapter) | | `-baud` | `921600` | Must match firmware `UART_BAUD_RATE` | -Implements the VERSION command (`MessageType` = 3): sends framed `03`, decodes protobuf `UartMessage.version_response`. +### Commands + +| Command | UART payload | Description | +|---------|--------------|-------------| +| `version` | `0x03` | Prints `version` and `git_hash` from firmware | +| `clients` | `0x04` | Lists slaves registered on the master via ESP-NOW | + +`clients` requires slaves to have responded to master discover broadcasts first. + +Example output: + +``` +clients (2): + [0] id=42 mac=aabbccddeeff ver=1 available=true used=false last_ping=12345 last_success_ping=12345 +``` ## Regenerate protobuf diff --git a/goTool/cmd_clients.go b/goTool/cmd_clients.go new file mode 100644 index 0000000..b0c4e97 --- /dev/null +++ b/goTool/cmd_clients.go @@ -0,0 +1,46 @@ +package main + +import ( + "encoding/hex" + "fmt" + + "google.golang.org/protobuf/proto" + + "powerpod/gotool/pb" +) + +func runClients(sp *serialPort) error { + payload, err := sp.exchange(byte(pb.MessageType_CLIENT_INFO), "CLIENT_INFO") + if err != nil { + return err + } + + var msg pb.UartMessage + if err := proto.Unmarshal(payload[1:], &msg); err != nil { + return fmt.Errorf("decode protobuf: %w", err) + } + + if msg.GetType() != pb.MessageType_CLIENT_INFO { + return fmt.Errorf("unexpected message type %v", msg.GetType()) + } + + info := msg.GetClientInfoResponse() + if info == nil { + return fmt.Errorf("response missing client_info_response") + } + + clients := info.GetClients() + if len(clients) == 0 { + fmt.Println("no clients registered") + return nil + } + + fmt.Printf("clients (%d):\n", len(clients)) + for i, c := range clients { + mac := hex.EncodeToString(c.GetMac()) + fmt.Printf(" [%d] id=%d mac=%s ver=%d available=%v used=%v last_ping=%d last_success_ping=%d\n", + i, c.GetId(), mac, c.GetVersion(), c.GetAvailable(), c.GetUsed(), + c.GetLastPing(), c.GetLastSuccessPing()) + } + return nil +} diff --git a/goTool/cmd_version.go b/goTool/cmd_version.go new file mode 100644 index 0000000..45576a6 --- /dev/null +++ b/goTool/cmd_version.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + + "google.golang.org/protobuf/proto" + + "powerpod/gotool/pb" +) + +func runVersion(sp *serialPort) error { + payload, err := sp.exchange(byte(pb.MessageType_VERSION), "VERSION") + if err != nil { + return err + } + + var msg pb.UartMessage + if err := proto.Unmarshal(payload[1:], &msg); err != nil { + return fmt.Errorf("decode protobuf: %w", err) + } + + if msg.GetType() != pb.MessageType_VERSION { + return fmt.Errorf("unexpected message type %v", msg.GetType()) + } + + ver := msg.GetVersionResponse() + if ver == nil { + return fmt.Errorf("response missing version_response") + } + + fmt.Printf("version: %d\n", ver.GetVersion()) + fmt.Printf("git_hash: %s\n", ver.GetGitHash()) + return nil +} diff --git a/goTool/main.go b/goTool/main.go index 6d47584..b7f441b 100644 --- a/goTool/main.go +++ b/goTool/main.go @@ -5,86 +5,49 @@ import ( "fmt" "log" "os" - "time" - - "go.bug.st/serial" - "google.golang.org/protobuf/proto" - - "powerpod/gotool/pb" - uartframe "powerpod/gotool/uart" ) -const ( - defaultBaud = 921600 - versionCmdID = byte(pb.MessageType_VERSION) - readTimeout = 3 * time.Second -) +const defaultBaud = 921600 + +func usage() { + fmt.Fprintf(os.Stderr, "usage: gotool -port /dev/ttyUSB0 \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\n") + flag.PrintDefaults() +} func main() { portName := flag.String("port", "", "serial port (e.g. /dev/ttyUSB0)") baud := flag.Int("baud", defaultBaud, "UART baud rate") flag.Parse() - if *portName == "" { - fmt.Fprintln(os.Stderr, "usage: gotool -port /dev/ttyUSB0") - flag.PrintDefaults() + if *portName == "" || flag.NArg() < 1 { + usage() os.Exit(2) } - mode := &serial.Mode{ - BaudRate: *baud, - DataBits: 8, - Parity: serial.NoParity, - StopBits: serial.OneStopBit, - } + cmd := flag.Arg(0) - port, err := serial.Open(*portName, mode) + sp, err := openSerial(*portName, *baud) if err != nil { log.Fatalf("open serial: %v", err) } - defer port.Close() + defer sp.Close() - if err := port.SetReadTimeout(readTimeout); err != nil { - log.Fatalf("set read timeout: %v", err) + var runErr error + switch cmd { + case "version": + runErr = runVersion(sp) + case "clients", "client-info": + runErr = runClients(sp) + default: + fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd) + usage() + os.Exit(2) } - frame, err := uartframe.EncodeFrame([]byte{versionCmdID}) - if err != nil { - log.Fatalf("encode frame: %v", err) + if runErr != nil { + log.Fatal(runErr) } - - log.Printf("sending VERSION command (%d bytes): % x", len(frame), frame) - if _, err := port.Write(frame); err != nil { - log.Fatalf("write: %v", err) - } - - payload, err := uartframe.ReadFrame(port, nil) - if err != nil { - log.Fatalf("read response: %v", err) - } - - log.Printf("response payload (%d bytes): % x", len(payload), payload) - if len(payload) == 0 { - log.Fatal("empty response payload") - } - if payload[0] != versionCmdID { - log.Fatalf("unexpected command id 0x%02x (want 0x%02x)", payload[0], versionCmdID) - } - - var msg pb.UartMessage - if err := proto.Unmarshal(payload[1:], &msg); err != nil { - log.Fatalf("decode protobuf: %v", err) - } - - if msg.GetType() != pb.MessageType_VERSION { - log.Fatalf("unexpected message type %v", msg.GetType()) - } - - ver := msg.GetVersionResponse() - if ver == nil { - log.Fatal("response missing version_response") - } - - fmt.Printf("version: %d\n", ver.GetVersion()) - fmt.Printf("git_hash: %s\n", ver.GetGitHash()) } diff --git a/goTool/serialport.go b/goTool/serialport.go new file mode 100644 index 0000000..2319fe2 --- /dev/null +++ b/goTool/serialport.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "log" + "time" + + "go.bug.st/serial" + uartframe "powerpod/gotool/uart" +) + +const readTimeout = 3 * time.Second + +type serialPort struct { + port serial.Port +} + +func openSerial(portName string, baud int) (*serialPort, error) { + mode := &serial.Mode{ + BaudRate: baud, + DataBits: 8, + Parity: serial.NoParity, + StopBits: serial.OneStopBit, + } + + port, err := serial.Open(portName, mode) + if err != nil { + return nil, err + } + if err := port.SetReadTimeout(readTimeout); err != nil { + port.Close() + return nil, err + } + return &serialPort{port: port}, nil +} + +func (s *serialPort) Close() error { + return s.port.Close() +} + +func (s *serialPort) exchange(cmdID byte, cmdName string) ([]byte, error) { + frame, err := uartframe.EncodeFrame([]byte{cmdID}) + if err != nil { + return nil, fmt.Errorf("encode frame: %w", err) + } + + log.Printf("sending %s command (%d bytes): % x", cmdName, len(frame), frame) + if _, err := s.port.Write(frame); err != nil { + return nil, fmt.Errorf("write: %w", err) + } + + payload, err := uartframe.ReadFrame(s.port, nil) + if err != nil { + return nil, fmt.Errorf("read response: %w", err) + } + + log.Printf("response payload (%d bytes): % x", len(payload), payload) + if len(payload) == 0 { + return nil, fmt.Errorf("empty response payload") + } + if payload[0] != cmdID { + return nil, fmt.Errorf("unexpected command id 0x%02x (want 0x%02x)", payload[0], cmdID) + } + return payload, nil +}