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>
220 lines
6.5 KiB
Go
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)
|
|
}
|