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
|
```bash
|
||||||
cd goTool
|
cd goTool
|
||||||
go mod tidy
|
go mod tidy
|
||||||
go run . -port /dev/ttyUSB0
|
go run . -port /dev/ttyUSB0 version
|
||||||
|
go run . -port /dev/ttyUSB0 clients
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
@ -17,7 +18,21 @@ go run . -port /dev/ttyUSB0
|
|||||||
| `-port` | (required) | Serial port on master UART (GPIO2/3 adapter) |
|
| `-port` | (required) | Serial port on master UART (GPIO2/3 adapter) |
|
||||||
| `-baud` | `921600` | Must match firmware `UART_BAUD_RATE` |
|
| `-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
|
## 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"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.bug.st/serial"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
|
|
||||||
"powerpod/gotool/pb"
|
|
||||||
uartframe "powerpod/gotool/uart"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const defaultBaud = 921600
|
||||||
defaultBaud = 921600
|
|
||||||
versionCmdID = byte(pb.MessageType_VERSION)
|
func usage() {
|
||||||
readTimeout = 3 * time.Second
|
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() {
|
func main() {
|
||||||
portName := flag.String("port", "", "serial port (e.g. /dev/ttyUSB0)")
|
portName := flag.String("port", "", "serial port (e.g. /dev/ttyUSB0)")
|
||||||
baud := flag.Int("baud", defaultBaud, "UART baud rate")
|
baud := flag.Int("baud", defaultBaud, "UART baud rate")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *portName == "" {
|
if *portName == "" || flag.NArg() < 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gotool -port /dev/ttyUSB0")
|
usage()
|
||||||
flag.PrintDefaults()
|
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
mode := &serial.Mode{
|
cmd := flag.Arg(0)
|
||||||
BaudRate: *baud,
|
|
||||||
DataBits: 8,
|
|
||||||
Parity: serial.NoParity,
|
|
||||||
StopBits: serial.OneStopBit,
|
|
||||||
}
|
|
||||||
|
|
||||||
port, err := serial.Open(*portName, mode)
|
sp, err := openSerial(*portName, *baud)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("open serial: %v", err)
|
log.Fatalf("open serial: %v", err)
|
||||||
}
|
}
|
||||||
defer port.Close()
|
defer sp.Close()
|
||||||
|
|
||||||
if err := port.SetReadTimeout(readTimeout); err != nil {
|
var runErr error
|
||||||
log.Fatalf("set read timeout: %v", err)
|
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 runErr != nil {
|
||||||
if err != nil {
|
log.Fatal(runErr)
|
||||||
log.Fatalf("encode frame: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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