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:
+100
-4
@@ -19,7 +19,10 @@ type MasterView struct {
|
||||
GitHash string `json:"git_hash"`
|
||||
RunningPartition string `json:"running_partition,omitempty"`
|
||||
Deadzone uint32 `json:"deadzone,omitempty"`
|
||||
OK bool `json:"ok"`
|
||||
Lipo1 lipoReadingJSON `json:"lipo1"`
|
||||
Lipo2 lipoReadingJSON `json:"lipo2"`
|
||||
BatteryAgeMs uint32 `json:"battery_age_ms,omitempty"`
|
||||
OK bool `json:"ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
@@ -37,7 +40,10 @@ type ClientView struct {
|
||||
AccelY int32 `json:"accel_y"`
|
||||
AccelZ int32 `json:"accel_z"`
|
||||
AccelAgeMs uint32 `json:"accel_age_ms"`
|
||||
AccelStream bool `json:"accel_stream"`
|
||||
AccelStream bool `json:"accel_stream"`
|
||||
Lipo1 lipoReadingJSON `json:"lipo1"`
|
||||
Lipo2 lipoReadingJSON `json:"lipo2"`
|
||||
BatteryAgeMs uint32 `json:"battery_age_ms,omitempty"`
|
||||
}
|
||||
|
||||
type DashboardState struct {
|
||||
@@ -62,8 +68,16 @@ func newWSHub() *wsHub {
|
||||
|
||||
func (h *wsHub) setState(st DashboardState) {
|
||||
h.mu.Lock()
|
||||
prev := h.state.Clients
|
||||
st.Clients = preserveClientAccel(st.Clients, prev)
|
||||
prev := h.state
|
||||
st.Clients = preserveClientAccel(st.Clients, prev.Clients)
|
||||
st.Clients = preserveClientBattery(st.Clients, prev.Clients)
|
||||
if !st.Master.Lipo1.Valid && !st.Master.Lipo2.Valid {
|
||||
if prev.Master.Lipo1.Valid || prev.Master.Lipo2.Valid {
|
||||
st.Master.Lipo1 = prev.Master.Lipo1
|
||||
st.Master.Lipo2 = prev.Master.Lipo2
|
||||
st.Master.BatteryAgeMs = prev.Master.BatteryAgeMs
|
||||
}
|
||||
}
|
||||
h.state = st
|
||||
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||
for c := range h.clients {
|
||||
@@ -156,6 +170,33 @@ func preserveClientAccel(newClients, oldClients []ClientView) []ClientView {
|
||||
return out
|
||||
}
|
||||
|
||||
func preserveClientBattery(newClients, oldClients []ClientView) []ClientView {
|
||||
if len(oldClients) == 0 {
|
||||
return newClients
|
||||
}
|
||||
oldByID := make(map[uint32]ClientView, len(oldClients))
|
||||
for _, c := range oldClients {
|
||||
oldByID[c.ID] = c
|
||||
}
|
||||
out := make([]ClientView, len(newClients))
|
||||
for i, c := range newClients {
|
||||
out[i] = c
|
||||
if c.Lipo1.Valid || c.Lipo2.Valid {
|
||||
continue
|
||||
}
|
||||
prev, ok := oldByID[c.ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if prev.Lipo1.Valid || prev.Lipo2.Valid {
|
||||
out[i].Lipo1 = prev.Lipo1
|
||||
out[i].Lipo2 = prev.Lipo2
|
||||
out[i].BatteryAgeMs = prev.BatteryAgeMs
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func anyClientAccelStream(clients []ClientView) bool {
|
||||
for _, c := range clients {
|
||||
if c.AccelStream {
|
||||
@@ -294,6 +335,7 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState, s
|
||||
}
|
||||
st.Clients = append(st.Clients, cv)
|
||||
}
|
||||
applyBatteryToState(link, &st)
|
||||
if anyClientAccelStream(st.Clients) {
|
||||
for i := range st.Clients {
|
||||
if !st.Clients[i].AccelStream {
|
||||
@@ -319,6 +361,60 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState, s
|
||||
return st
|
||||
}
|
||||
|
||||
func applyBatteryToState(link *managedSerial, st *DashboardState) {
|
||||
bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true})
|
||||
if err != nil {
|
||||
log.Printf("battery poll: %v", err)
|
||||
return
|
||||
}
|
||||
applyBatterySamplesToState(st, batterySamplesFromPB(bat.GetSamples()))
|
||||
}
|
||||
|
||||
func (h *wsHub) mergeBattery(samples []batterySampleJSON) {
|
||||
if len(samples) == 0 {
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
st := h.state
|
||||
applyBatterySamplesToState(&st, samples)
|
||||
st.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||
h.state = st
|
||||
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||
for c := range h.clients {
|
||||
conns = append(conns, c)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
data, err := json.Marshal(st)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, c := range conns {
|
||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
}
|
||||
|
||||
func runBatteryPoller(link *managedSerial, hub *wsHub, interval time.Duration, stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if hub.clientCount() == 0 {
|
||||
continue
|
||||
}
|
||||
bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
hub.mergeBattery(batterySamplesFromPB(bat.GetSamples()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runAccelDashboardPoller(link *managedSerial, hub *wsHub, interval time.Duration, stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
Reference in New Issue
Block a user