Slaves report pack voltages every 30s; the master caches them for fast BATTERY_STATUS reads. goTool exposes REST/WebSocket and shows values in the dashboard, with a nanopb fix so optional lipo submessages encode. Co-authored-by: Cursor <cursoragent@cursor.com>
132 lines
3.2 KiB
Go
132 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.bug.st/serial"
|
|
uartframe "powerpod/gotool/uart"
|
|
)
|
|
|
|
const readTimeout = 3 * time.Second
|
|
|
|
// batteryReadTimeout: master may query each slave over ESP-NOW (~400 ms each).
|
|
const batteryReadTimeout = 12 * time.Second
|
|
|
|
type serialPort struct {
|
|
port serial.Port
|
|
mu sync.Mutex
|
|
quiet bool
|
|
}
|
|
|
|
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) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.exchangePayloadLocked(payload, cmdName, readTimeout)
|
|
}
|
|
|
|
func (s *serialPort) exchangePayloadForBattery(payload []byte, cmdName string) ([]byte, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.exchangePayloadLocked(payload, cmdName, batteryReadTimeout)
|
|
}
|
|
|
|
func (s *serialPort) exchangePayloadLocked(payload []byte, cmdName string, timeout time.Duration) ([]byte, error) {
|
|
if len(payload) == 0 {
|
|
return nil, fmt.Errorf("empty payload")
|
|
}
|
|
frame, err := uartframe.EncodeFrame(payload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode frame: %w", err)
|
|
}
|
|
|
|
if !s.quiet {
|
|
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)
|
|
}
|
|
|
|
if timeout <= 0 {
|
|
timeout = readTimeout
|
|
}
|
|
if err := s.port.SetReadTimeout(timeout); err != nil {
|
|
return nil, fmt.Errorf("set read timeout: %w", err)
|
|
}
|
|
defer func() { _ = s.port.SetReadTimeout(readTimeout) }()
|
|
|
|
respPayload, err := uartframe.ReadFrame(s.port, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read response: %w", err)
|
|
}
|
|
|
|
if !s.quiet {
|
|
log.Printf("response payload (%d bytes): % x", len(respPayload), respPayload)
|
|
}
|
|
if len(respPayload) == 0 {
|
|
return nil, fmt.Errorf("empty response payload")
|
|
}
|
|
return respPayload, nil
|
|
}
|
|
|
|
func (s *serialPort) exchange(cmdID byte, cmdName string) ([]byte, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.exchangeLocked(cmdID, cmdName)
|
|
}
|
|
|
|
func (s *serialPort) exchangeLocked(cmdID byte, cmdName string) ([]byte, error) {
|
|
frame, err := uartframe.EncodeFrame([]byte{cmdID})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode frame: %w", err)
|
|
}
|
|
|
|
if !s.quiet {
|
|
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)
|
|
}
|
|
|
|
if !s.quiet {
|
|
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
|
|
}
|