powerpods/goTool/api_tap.go
simon f512936d97 Add CACHE_STATUS UART poll and dashboard live stream.
Combine cached accel and tap in one low-overhead master command for ~16 ms
host polling. The dashboard uses a single live-stream toggle plus per-slave
accel-stream controls; fix live_stream state so polling is not cleared every
slow client refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 20:57:54 +02:00

220 lines
6.5 KiB
Go

package main
import (
"encoding/json"
"fmt"
"net/http"
"powerpod/gotool/pb"
)
type tapNotifyAPIRequest struct {
Write bool `json:"write"`
ClientID uint32 `json:"client_id"`
AllClients bool `json:"all_clients"`
Single bool `json:"single"`
DoubleTap bool `json:"double_tap"`
Triple bool `json:"triple"`
}
type tapNotifyAPIResponse struct {
ClientID uint32 `json:"client_id"`
Success bool `json:"success"`
SlavesUpdated uint32 `json:"slaves_updated"`
Single bool `json:"single"`
DoubleTap bool `json:"double_tap"`
Triple bool `json:"triple"`
Error string `json:"error,omitempty"`
}
type tapSnapshotAPIResponse struct {
Events []tapEventView `json:"events"`
Error string `json:"error,omitempty"`
}
type tapEventView struct {
ClientID uint32 `json:"client_id"`
Kind string `json:"kind"`
AgeMs uint32 `json:"age_ms"`
}
func mountTapAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl) {
mux.HandleFunc("GET /api/clients/{clientID}/tap-notify", func(w http.ResponseWriter, r *http.Request) {
clientID, err := parsePathClientID(r)
if err != nil {
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: err.Error()})
return
}
serveTapNotifyGet(w, clientID, link)
})
mux.HandleFunc("PUT /api/clients/{clientID}/tap-notify", func(w http.ResponseWriter, r *http.Request) {
clientID, err := parsePathClientID(r)
if err != nil {
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: err.Error()})
return
}
serveClientTapNotifyPut(w, r, clientID, link, hub, tapCtl)
})
mux.HandleFunc("/api/tap-notify", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
serveTapNotifyGetQuery(w, r, link)
case http.MethodPost:
serveTapNotifyPost(w, r, link, hub, tapCtl)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})
mux.HandleFunc("/api/tap-snapshot", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
serveTapSnapshotGet(w, r, link)
})
}
func applyTapNotifyClient(link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl, clientID uint32, single, doubleTap, triple bool) tapNotifyAPIResponse {
return applyTapNotifyClientWS(link, hub, tapCtl, clientID, single, doubleTap, triple)
}
func serveTapNotifyGet(w http.ResponseWriter, clientID uint32, link *managedSerial) {
resp, err := link.TapNotifyPoll(&pb.TapNotifyRequest{
Write: false,
ClientId: clientID,
})
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, tapNotifyAPIResponse{
ClientID: clientID,
Error: err.Error(),
})
return
}
writeJSON(w, http.StatusOK, tapNotifyAPIResponse{
ClientID: resp.GetClientId(),
Success: resp.GetSuccess(),
Single: resp.GetSingle(),
DoubleTap: resp.GetDoubleTap(),
Triple: resp.GetTriple(),
})
}
func serveTapNotifyGetQuery(w http.ResponseWriter, r *http.Request, link *managedSerial) {
clientID, err := parseUintQuery(r, "client_id", 0)
if err != nil || clientID == 0 {
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "client_id required"})
return
}
serveTapNotifyGet(w, clientID, link)
}
type clientTapNotifyBody struct {
Single bool `json:"single"`
DoubleTap bool `json:"double_tap"`
Triple bool `json:"triple"`
}
func serveClientTapNotifyPut(w http.ResponseWriter, r *http.Request, clientID uint32, link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl) {
var body clientTapNotifyBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "invalid JSON"})
return
}
out := applyTapNotifyClient(link, hub, tapCtl, clientID, body.Single, body.DoubleTap, body.Triple)
status := http.StatusOK
if out.Error != "" || !out.Success {
status = http.StatusServiceUnavailable
}
writeJSON(w, status, out)
}
func serveTapNotifyPost(w http.ResponseWriter, r *http.Request, link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl) {
var body tapNotifyAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "invalid JSON"})
return
}
if body.AllClients {
updated, err := applyTapNotifyAll(link, hub, tapCtl, body.Single, body.DoubleTap, body.Triple)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, tapNotifyAPIResponse{Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, tapNotifyAPIResponse{
Success: updated > 0,
SlavesUpdated: updated,
Single: body.Single,
DoubleTap: body.DoubleTap,
Triple: body.Triple,
})
return
}
if body.ClientID == 0 {
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "client_id required"})
return
}
out := applyTapNotifyClient(link, hub, tapCtl, body.ClientID, body.Single, body.DoubleTap, body.Triple)
status := http.StatusOK
if out.Error != "" || !out.Success {
status = http.StatusServiceUnavailable
}
writeJSON(w, status, out)
}
func applyTapNotifyAll(link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl, single, doubleTap, triple bool) (uint32, error) {
resp, err := link.TapNotify(&pb.TapNotifyRequest{
Write: true,
AllClients: true,
Single: single,
DoubleTap: doubleTap,
Triple: triple,
})
if err != nil {
return 0, err
}
if !resp.GetSuccess() {
return 0, fmt.Errorf("tap notify not applied to any slave")
}
if hub != nil || tapCtl != nil {
clients, _ := link.listClientsPoll()
for _, c := range clients {
if tapCtl != nil {
tapCtl.Set(c.GetId(), single, doubleTap, triple)
}
if hub != nil {
hub.patchClientTapNotify(c.GetId(), single, doubleTap, triple)
}
}
}
return resp.GetSlavesUpdated(), nil
}
func serveTapSnapshotGet(w http.ResponseWriter, r *http.Request, link *managedSerial) {
clientID, err := parseUintQuery(r, "client_id", 0)
if err != nil {
writeJSON(w, http.StatusBadRequest, tapSnapshotAPIResponse{Error: err.Error()})
return
}
resp, err := link.readTapSnapshotPoll(clientID)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, tapSnapshotAPIResponse{Error: err.Error()})
return
}
out := tapSnapshotAPIResponse{Events: make([]tapEventView, 0, len(resp.GetEvents()))}
for _, e := range resp.GetEvents() {
if !e.GetValid() {
continue
}
out.Events = append(out.Events, tapEventView{
ClientID: e.GetClientId(),
Kind: tapKindLabel(e.GetKind()),
AgeMs: e.GetAgeMs(),
})
}
writeJSON(w, http.StatusOK, out)
}