package main import ( "encoding/json" "fmt" "io" "net/http" "strconv" "powerpod/gotool/pb" ) const otaMaxFirmwareSize = 2 * 1024 * 1024 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"` } type findMeAPIRequest struct { ClientID uint32 `json:"client_id"` } type findMeAPIResponse struct { Success bool `json:"success"` ClientID uint32 `json:"client_id,omitempty"` Error string `json:"error,omitempty"` } type restartAPIRequest struct { ClientID uint32 `json:"client_id"` } type restartAPIResponse struct { Success bool `json:"success"` ClientID uint32 `json:"client_id,omitempty"` Error string `json:"error,omitempty"` } type otaAPIResponse struct { Success bool `json:"success"` BytesWritten uint32 `json:"bytes_written,omitempty"` TargetSlot uint32 `json:"target_slot,omitempty"` Error string `json:"error,omitempty"` } func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub) { 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) }) mux.HandleFunc("/api/find-me", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } serveFindMe(w, r, link) }) mux.HandleFunc("/api/restart", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } serveRestart(w, r, link) }) mux.HandleFunc("/api/ota", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } serveOTAUpload(w, r, link, hub) }) } func serveOTAUpload(w http.ResponseWriter, r *http.Request, link *managedSerial, hub *wsHub) { if err := r.ParseMultipartForm(otaMaxFirmwareSize); err != nil { writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "invalid form"}) return } file, _, err := r.FormFile("firmware") if err != nil { writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "firmware file required"}) return } defer file.Close() data, err := io.ReadAll(io.LimitReader(file, otaMaxFirmwareSize)) if err != nil { writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: err.Error()}) return } if len(data) == 0 { writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "empty firmware"}) return } var last OTAProgress err = runOTAUpload(link, data, func(p OTAProgress) { last = p if hub != nil { hub.broadcastRaw(p) } }) if err != nil { if hub != nil { hub.broadcastRaw(OTAProgress{Type: "ota_progress", Phase: "error", Message: err.Error()}) } writeJSON(w, http.StatusServiceUnavailable, otaAPIResponse{Error: err.Error()}) return } writeJSON(w, http.StatusOK, otaAPIResponse{ Success: true, BytesWritten: last.Bytes, TargetSlot: last.Slot, }) } 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 serveRestart(w http.ResponseWriter, r *http.Request, link *managedSerial) { var body restartAPIRequest if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeJSON(w, http.StatusBadRequest, restartAPIResponse{Error: "invalid JSON"}) return } if err := link.Restart(body.ClientID); err != nil { writeJSON(w, http.StatusServiceUnavailable, restartAPIResponse{ ClientID: body.ClientID, Error: err.Error(), }) return } writeJSON(w, http.StatusOK, restartAPIResponse{Success: true, ClientID: body.ClientID}) } func serveFindMe(w http.ResponseWriter, r *http.Request, link *managedSerial) { var body findMeAPIRequest if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeJSON(w, http.StatusBadRequest, findMeAPIResponse{Error: "invalid JSON"}) return } if err := link.FindMe(body.ClientID); err != nil { writeJSON(w, http.StatusServiceUnavailable, findMeAPIResponse{ ClientID: body.ClientID, Error: err.Error(), }) return } writeJSON(w, http.StatusOK, findMeAPIResponse{Success: true, ClientID: body.ClientID}) } 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) }