powerpods/goTool/api_serve.go
simon 80fb9cf55e Improve dashboard master config and separate slave deadzone updates.
Always show master deadzone input with read/set controls; apply bulk slave
changes via slaves_only without changing the master's BMA456.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 00:15:35 +02:00

200 lines
5.2 KiB
Go

package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"powerpod/gotool/pb"
)
type deadzoneAPIResponse struct {
Deadzone uint32 `json:"deadzone"`
ClientID uint32 `json:"client_id"`
Success bool `json:"success"`
SlavesUpdated uint32 `json:"slaves_updated"`
Error string `json:"error,omitempty"`
}
type deadzoneAPIRequest struct {
Write bool `json:"write"`
Deadzone uint32 `json:"deadzone"`
ClientID uint32 `json:"client_id"`
AllClients bool `json:"all_clients"`
// SlavesOnly: with all_clients, push to ESP-NOW slaves only (master BMA456 unchanged).
SlavesOnly bool `json:"slaves_only"`
}
type unicastAPIRequest struct {
ClientID uint32 `json:"client_id"`
Seq uint32 `json:"seq"`
}
type unicastAPIResponse struct {
Success bool `json:"success"`
Seq uint32 `json:"seq"`
Error string `json:"error,omitempty"`
}
func mountServeAPI(mux *http.ServeMux, link *managedSerial) {
mux.HandleFunc("/api/deadzone", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
serveDeadzoneGet(w, r, link)
case http.MethodPost:
serveDeadzonePost(w, r, link)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})
mux.HandleFunc("/api/unicast-test", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
serveUnicastTest(w, r, link)
})
}
func serveDeadzoneGet(w http.ResponseWriter, r *http.Request, link *managedSerial) {
clientID, err := parseUintQuery(r, "client_id", 0)
if err != nil {
writeJSON(w, http.StatusBadRequest, deadzoneAPIResponse{Error: err.Error()})
return
}
resp, err := link.AccelDeadzone(&pb.AccelDeadzoneRequest{
Write: false,
ClientId: clientID,
})
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, deadzoneAPIResponse{
ClientID: clientID,
Error: err.Error(),
})
return
}
writeJSON(w, http.StatusOK, deadzoneAPIResponse{
Deadzone: resp.GetDeadzone(),
ClientID: resp.GetClientId(),
Success: resp.GetSuccess(),
})
}
func serveDeadzonePost(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body deadzoneAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, deadzoneAPIResponse{Error: "invalid JSON"})
return
}
if body.AllClients && body.SlavesOnly {
updated, err := applyDeadzoneToSlaves(link, body.Deadzone)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, deadzoneAPIResponse{
Error: err.Error(),
})
return
}
writeJSON(w, http.StatusOK, deadzoneAPIResponse{
Deadzone: body.Deadzone,
Success: updated > 0,
SlavesUpdated: updated,
})
return
}
req := &pb.AccelDeadzoneRequest{
Write: true,
Deadzone: body.Deadzone,
ClientId: body.ClientID,
AllClients: body.AllClients,
}
// client_id 0 without all_clients: master BMA456 only (same as CLI -client 0).
resp, err := link.AccelDeadzone(req)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, deadzoneAPIResponse{
ClientID: body.ClientID,
Error: err.Error(),
})
return
}
writeJSON(w, http.StatusOK, deadzoneAPIResponse{
Deadzone: resp.GetDeadzone(),
ClientID: resp.GetClientId(),
Success: resp.GetSuccess(),
SlavesUpdated: resp.GetSlavesUpdated(),
})
}
// applyDeadzoneToSlaves sets deadzone on each registered slave via per-client UART/ESP-NOW.
// Does not change the master's local BMA456 (use client_id 0 for that).
func applyDeadzoneToSlaves(link *managedSerial, deadzone uint32) (uint32, error) {
clients, err := link.listClients()
if err != nil {
return 0, err
}
var updated uint32
for _, c := range clients {
resp, err := link.AccelDeadzone(&pb.AccelDeadzoneRequest{
Write: true,
Deadzone: deadzone,
ClientId: c.GetId(),
})
if err != nil {
continue
}
if resp.GetSuccess() {
updated++
}
}
if len(clients) == 0 {
return 0, fmt.Errorf("no slaves registered")
}
if updated == 0 {
return 0, fmt.Errorf("deadzone not applied to any slave")
}
return updated, nil
}
func serveUnicastTest(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body unicastAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, unicastAPIResponse{Error: "invalid JSON"})
return
}
if body.ClientID == 0 {
writeJSON(w, http.StatusBadRequest, unicastAPIResponse{Error: "client_id required"})
return
}
if body.Seq == 0 {
body.Seq = 1
}
resp, err := link.EspnowUnicastTest(body.ClientID, body.Seq)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, unicastAPIResponse{Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, unicastAPIResponse{
Success: resp.GetSuccess(),
Seq: resp.GetSeq(),
})
}
func parseUintQuery(r *http.Request, key string, def uint32) (uint32, error) {
s := r.URL.Query().Get(key)
if s == "" {
return def, nil
}
v, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return 0, err
}
return uint32(v), nil
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}