Files
powerpods/goTool/uart/frame.go
T
skrueckenandCursor 9046677460 Raise host frame payload limit so multi-client UART replies fit.
Use a uint16 LE length and 1024-byte max payload so CLIENT_INFO and BATTERY_STATUS no longer fail encoding past ~10 connected pods.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-01 23:43:33 +01:00

148 lines
2.9 KiB
Go

package uart
import (
"errors"
"fmt"
"io"
"time"
)
const (
StartMarker = 0xAA
StopMarker = 0xCC
// Must match main/uart.h MAX_PAYLOAD_SIZE.
MaxPayload = 1024
// AA | len_lo | len_hi | payload | xor | CC
FrameOverhead = 5
)
var (
ErrInvalidFrame = errors.New("invalid uart frame")
ErrTimeout = errors.New("read timeout")
)
// EncodeFrame builds a framed packet: 0xAA len_lo len_hi 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)
}
n := len(payload)
frame := make([]byte, 0, FrameOverhead+n)
frame = append(frame, StartMarker, byte(n&0xff), byte((n>>8)&0xff))
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
lenLo byte
payload []byte
index int
checksum byte
}
const (
stateStart = iota
stateLenLo
stateLenHi
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 = stateLenLo
}
case stateLenLo:
p.lenLo = b
p.state = stateLenHi
case stateLenHi:
n := int(p.lenLo) | (int(b) << 8)
if n == 0 || n > MaxPayload {
p.state = stateStart
} else {
p.len = n
p.payload = make([]byte, p.len)
p.index = 0
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.
// maxWait bounds total wait time; zero means no limit (serial read timeouts retry forever).
func ReadFrame(r io.Reader, buf []byte, maxWait time.Duration) ([]byte, error) {
if buf == nil {
buf = make([]byte, 1024)
}
parser := NewParser()
var deadline time.Time
if maxWait > 0 {
deadline = time.Now().Add(maxWait)
}
for {
if !deadline.IsZero() && !time.Now().Before(deadline) {
return nil, ErrTimeout
}
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
}
}
}