powerpods/goTool/api_tap.go
simon 31e539052a Unify cache polling on CACHE_STATUS and split API docs.
Replace separate accel/tap snapshot UART commands with one clients[] response
that omits unsubscribed fields; remove snapshot handlers and CLI commands.
Add goTool/docs for WebSocket streams and REST; tap-snapshot REST uses CACHE_STATUS.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 21:23:09 +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
}
cache, err := link.readCacheStatusPoll()
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, tapSnapshotAPIResponse{Error: err.Error()})
return
}
out := tapSnapshotAPIResponse{Events: make([]tapEventView, 0)}
for _, e := range tapEventsFromCacheStatus(cache) {
if clientID != 0 && e.GetClientId() != clientID {
continue
}
out.Events = append(out.Events, tapEventView{
ClientID: e.GetClientId(),
Kind: tapKindLabel(e.GetKind()),
AgeMs: e.GetAgeMs(),
})
}
writeJSON(w, http.StatusOK, out)
}