Add Go UART tool to query firmware VERSION over serial.
Framed protobuf client for /dev/ttyUSB0 at 921600 baud with generated uart_messages types matching the device protocol. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a1629fb3db
commit
bde4c473ef
26
goTool/README.md
Normal file
26
goTool/README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# goTool
|
||||||
|
|
||||||
|
Host-side UART utilities for Powerpod.
|
||||||
|
|
||||||
|
## version command
|
||||||
|
|
||||||
|
Sends `MessageType.VERSION` (0x03) in a framed UART packet and prints the protobuf response.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd goTool
|
||||||
|
go mod tidy
|
||||||
|
go run . -port /dev/ttyUSB0
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
- `-port` — serial device (required)
|
||||||
|
- `-baud` — default `921600` (must match firmware)
|
||||||
|
|
||||||
|
## Regenerate protobuf
|
||||||
|
|
||||||
|
```bash
|
||||||
|
protoc --go_out=./pb --go_opt=paths=source_relative \
|
||||||
|
--go_opt=Muart_messages.proto=powerpod/gotool/pb \
|
||||||
|
-I ../main/proto ../main/proto/uart_messages.proto
|
||||||
|
```
|
||||||
13
goTool/go.mod
Normal file
13
goTool/go.mod
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
module powerpod/gotool
|
||||||
|
|
||||||
|
go 1.26.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
go.bug.st/serial v1.6.4
|
||||||
|
google.golang.org/protobuf v1.36.11
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/creack/goselect v0.1.2 // indirect
|
||||||
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
|
)
|
||||||
18
goTool/go.sum
Normal file
18
goTool/go.sum
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
|
||||||
|
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
|
||||||
|
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
|
||||||
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
90
goTool/main.go
Normal file
90
goTool/main.go
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"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
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := &serial.Mode{
|
||||||
|
BaudRate: *baud,
|
||||||
|
DataBits: 8,
|
||||||
|
Parity: serial.NoParity,
|
||||||
|
StopBits: serial.OneStopBit,
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := serial.Open(*portName, mode)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("open serial: %v", err)
|
||||||
|
}
|
||||||
|
defer port.Close()
|
||||||
|
|
||||||
|
if err := port.SetReadTimeout(readTimeout); err != nil {
|
||||||
|
log.Fatalf("set read timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
frame, err := uartframe.EncodeFrame([]byte{versionCmdID})
|
||||||
|
if err != nil {
|
||||||
|
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())
|
||||||
|
}
|
||||||
1035
goTool/pb/uart_messages.pb.go
Normal file
1035
goTool/pb/uart_messages.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
126
goTool/uart/frame.go
Normal file
126
goTool/uart/frame.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package uart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
StartMarker = 0xAA
|
||||||
|
StopMarker = 0xCC
|
||||||
|
MaxPayload = 252
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidFrame = errors.New("invalid uart frame")
|
||||||
|
ErrTimeout = errors.New("read timeout")
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncodeFrame builds a framed packet: 0xAA len payload xor(payload) 0xCC.
|
||||||
|
func EncodeFrame(payload []byte) ([]byte, error) {
|
||||||
|
if len(payload) == 0 || len(payload) > MaxPayload {
|
||||||
|
return nil, fmt.Errorf("payload length %d out of range 1..%d", len(payload), MaxPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
frame := make([]byte, 0, 4+len(payload))
|
||||||
|
frame = append(frame, StartMarker, byte(len(payload)))
|
||||||
|
var checksum byte
|
||||||
|
for _, b := range payload {
|
||||||
|
frame = append(frame, b)
|
||||||
|
checksum ^= b
|
||||||
|
}
|
||||||
|
frame = append(frame, checksum, StopMarker)
|
||||||
|
return frame, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
state int
|
||||||
|
len int
|
||||||
|
payload []byte
|
||||||
|
index int
|
||||||
|
checksum byte
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
stateStart = iota
|
||||||
|
stateLen
|
||||||
|
stateData
|
||||||
|
stateChecksum
|
||||||
|
stateStop
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewParser() *Parser {
|
||||||
|
return &Parser{state: stateStart}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed ingests one byte. ok is true when a complete frame is ready.
|
||||||
|
func (p *Parser) Feed(b byte) (payload []byte, ok bool, err error) {
|
||||||
|
switch p.state {
|
||||||
|
case stateStart:
|
||||||
|
if b == StartMarker {
|
||||||
|
p.index = 0
|
||||||
|
p.checksum = 0
|
||||||
|
p.state = stateLen
|
||||||
|
}
|
||||||
|
case stateLen:
|
||||||
|
if b == 0 || b > MaxPayload {
|
||||||
|
p.state = stateStart
|
||||||
|
} else {
|
||||||
|
p.len = int(b)
|
||||||
|
p.payload = make([]byte, p.len)
|
||||||
|
p.state = stateData
|
||||||
|
}
|
||||||
|
case stateData:
|
||||||
|
p.payload[p.index] = b
|
||||||
|
p.checksum ^= b
|
||||||
|
p.index++
|
||||||
|
if p.index >= p.len {
|
||||||
|
p.state = stateChecksum
|
||||||
|
}
|
||||||
|
case stateChecksum:
|
||||||
|
if b == p.checksum {
|
||||||
|
p.state = stateStop
|
||||||
|
} else {
|
||||||
|
p.state = stateStart
|
||||||
|
return nil, false, ErrInvalidFrame
|
||||||
|
}
|
||||||
|
case stateStop:
|
||||||
|
p.state = stateStart
|
||||||
|
if b == StopMarker {
|
||||||
|
out := make([]byte, len(p.payload))
|
||||||
|
copy(out, p.payload)
|
||||||
|
return out, true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFrame reads bytes from r until one full frame is parsed or an error occurs.
|
||||||
|
func ReadFrame(r io.Reader, buf []byte) ([]byte, error) {
|
||||||
|
if buf == nil {
|
||||||
|
buf = make([]byte, 256)
|
||||||
|
}
|
||||||
|
parser := NewParser()
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err := r.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
payload, ok, perr := parser.Feed(buf[i])
|
||||||
|
if perr != nil {
|
||||||
|
return nil, perr
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil, ErrInvalidFrame
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user