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>
121 lines
3.0 KiB
Go
121 lines
3.0 KiB
Go
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
|
|
}
|
|
}
|
|
}
|