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:
@@ -0,0 +1,120 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"powerpod/gotool/pb"
|
||||
)
|
||||
|
||||
const (
|
||||
lipoMinMv = 3000
|
||||
lipoMaxMv = 4200
|
||||
)
|
||||
|
||||
type lipoReadingJSON struct {
|
||||
Valid bool `json:"valid"`
|
||||
VoltageMv uint32 `json:"voltage_mv"`
|
||||
Percent int `json:"percent,omitempty"`
|
||||
}
|
||||
|
||||
type batterySampleJSON struct {
|
||||
ClientID uint32 `json:"client_id"`
|
||||
Lipo1 lipoReadingJSON `json:"lipo1"`
|
||||
Lipo2 lipoReadingJSON `json:"lipo2"`
|
||||
AgeMs uint32 `json:"age_ms,omitempty"`
|
||||
}
|
||||
|
||||
type batteryAPIRequest struct {
|
||||
ClientID uint32 `json:"client_id"`
|
||||
AllClients bool `json:"all_clients"`
|
||||
}
|
||||
|
||||
type batteryAPIResponse struct {
|
||||
Type string `json:"type,omitempty"` // battery_status (WebSocket)
|
||||
Success bool `json:"success"`
|
||||
Samples []batterySampleJSON `json:"samples,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func lipoPercent(mv uint32) int {
|
||||
if mv <= lipoMinMv {
|
||||
return 0
|
||||
}
|
||||
if mv >= lipoMaxMv {
|
||||
return 100
|
||||
}
|
||||
return int((mv - lipoMinMv) * 100 / (lipoMaxMv - lipoMinMv))
|
||||
}
|
||||
|
||||
func lipoFromPBMsg(l *pb.LipoReading) lipoReadingJSON {
|
||||
if l == nil {
|
||||
return lipoReadingJSON{}
|
||||
}
|
||||
return lipoFromPB(l.GetValid(), l.GetVoltageMv())
|
||||
}
|
||||
|
||||
func lipoFromPB(valid bool, mv uint32) lipoReadingJSON {
|
||||
out := lipoReadingJSON{Valid: valid, VoltageMv: mv}
|
||||
if valid {
|
||||
out.Percent = lipoPercent(mv)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func batterySamplesFromPB(samples []*pb.BatterySample) []batterySampleJSON {
|
||||
out := make([]batterySampleJSON, 0, len(samples))
|
||||
for _, s := range samples {
|
||||
out = append(out, batterySampleJSON{
|
||||
ClientID: s.GetClientId(),
|
||||
Lipo1: lipoFromPBMsg(s.GetLipo1()),
|
||||
Lipo2: lipoFromPBMsg(s.GetLipo2()),
|
||||
AgeMs: s.GetAgeMs(),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func applyBatteryStatus(link *managedSerial, in batteryAPIRequest) batteryAPIResponse {
|
||||
resp, err := link.BatteryStatus(&pb.BatteryStatusRequest{
|
||||
ClientId: in.ClientID,
|
||||
AllClients: in.AllClients,
|
||||
})
|
||||
if err != nil {
|
||||
return batteryAPIResponse{Error: err.Error()}
|
||||
}
|
||||
samples := batterySamplesFromPB(resp.GetSamples())
|
||||
out := batteryAPIResponse{
|
||||
Success: resp.GetSuccess() || len(samples) > 0,
|
||||
Samples: samples,
|
||||
}
|
||||
if len(samples) == 0 && out.Error == "" {
|
||||
out.Error = "battery status unavailable"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func findBatterySample(samples []batterySampleJSON, clientID uint32) (batterySampleJSON, bool) {
|
||||
for _, s := range samples {
|
||||
if s.ClientID == clientID {
|
||||
return s, true
|
||||
}
|
||||
}
|
||||
return batterySampleJSON{}, false
|
||||
}
|
||||
|
||||
// applyBatterySamplesToState merges UART/REST battery samples into dashboard views.
|
||||
func applyBatterySamplesToState(st *DashboardState, samples []batterySampleJSON) {
|
||||
if st == nil || len(samples) == 0 {
|
||||
return
|
||||
}
|
||||
if m, ok := findBatterySample(samples, 0); ok {
|
||||
st.Master.Lipo1 = m.Lipo1
|
||||
st.Master.Lipo2 = m.Lipo2
|
||||
st.Master.BatteryAgeMs = m.AgeMs
|
||||
}
|
||||
for i := range st.Clients {
|
||||
if s, ok := findBatterySample(samples, st.Clients[i].ID); ok {
|
||||
st.Clients[i].Lipo1 = s.Lipo1
|
||||
st.Clients[i].Lipo2 = s.Lipo2
|
||||
st.Clients[i].BatteryAgeMs = s.AgeMs
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user