Add goTool clients subcommand for CLIENT_INFO query.
Refactor into version/clients subcommands with shared serial framing to list registered slaves from the master over UART. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
92e146e2ed
commit
16bfbd1091
@ -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
|
||||
|
||||
|
||||
46
goTool/cmd_clients.go
Normal file
46
goTool/cmd_clients.go
Normal file
@ -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
|
||||
}
|
||||
34
goTool/cmd_version.go
Normal file
34
goTool/cmd_version.go
Normal file
@ -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
|
||||
}
|
||||
@ -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 <command>\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())
|
||||
}
|
||||
|
||||
65
goTool/serialport.go
Normal file
65
goTool/serialport.go
Normal file
@ -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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user