Add LiPo battery monitoring with ESP-NOW cache and dashboard API.

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>
This commit is contained in:
2026-05-29 20:14:28 +02:00
co-authored by Cursor
parent eb67a46158
commit 3cb0b5bbe9
30 changed files with 1618 additions and 150 deletions
+80
View File
@@ -59,6 +59,86 @@ func (m *managedSerial) readAccelSnapshotPoll(clientID uint32) (*pb.AccelSnapsho
return decodeAccelSnapshotPayload(respPayload)
}
func decodeBatteryStatusPayload(payload []byte) (*pb.BatteryStatusResponse, error) {
if len(payload) < 2 {
return nil, fmt.Errorf("short battery response")
}
if payload[0] != byte(pb.MessageType_BATTERY_STATUS) {
return nil, fmt.Errorf("unexpected command id 0x%02x (want 0x%02x)",
payload[0], byte(pb.MessageType_BATTERY_STATUS))
}
var msg pb.UartMessage
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
if msg.GetType() != pb.MessageType_BATTERY_STATUS {
return nil, fmt.Errorf("unexpected type %v", msg.GetType())
}
r := msg.GetBatteryStatusResponse()
if r == nil {
return nil, fmt.Errorf("missing battery_status_response")
}
return r, nil
}
func (m *managedSerial) BatteryStatus(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) {
var resp *pb.BatteryStatusResponse
err := m.withPort(func(sp *serialPort) error {
var e error
resp, e = sp.batteryStatus(req)
return e
})
return resp, err
}
func (m *managedSerial) BatteryStatusPoll(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_BATTERY_STATUS,
Payload: &pb.UartMessage_BatteryStatusRequest{
BatteryStatusRequest: req,
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_BATTERY_STATUS)}, body...)
respPayload, err := m.batteryStatusPayloadPoll(payload)
if err != nil {
return nil, err
}
return decodeBatteryStatusPayload(respPayload)
}
func (m *managedSerial) batteryStatusPayloadPoll(payload []byte) ([]byte, error) {
var resp []byte
err := m.withPortPoll(func(sp *serialPort) error {
var e error
resp, e = sp.exchangePayloadForBattery(payload, "BATTERY_STATUS")
return e
})
return resp, err
}
func (s *serialPort) batteryStatus(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_BATTERY_STATUS,
Payload: &pb.UartMessage_BatteryStatusRequest{
BatteryStatusRequest: req,
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_BATTERY_STATUS)}, body...)
respPayload, err := s.exchangePayloadForBattery(payload, "BATTERY_STATUS")
if err != nil {
return nil, err
}
return decodeBatteryStatusPayload(respPayload)
}
func (m *managedSerial) AccelStream(req *pb.AccelStreamRequest) (*pb.AccelStreamResponse, error) {
return m.accelStreamVia(m.withPort, req)
}