Add BMA456 tap detection with ESP-NOW notify and host snapshot API.
Slaves forward configured tap kinds to the master; goTool exposes CLI, dashboard, REST, and WebSocket with separate notify vs receive and 2s display cache. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
type tapReceiveAPIResponse struct {
|
||||
ClientID uint32 `json:"client_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
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("PUT /api/clients/{clientID}/tap-receive", func(w http.ResponseWriter, r *http.Request) {
|
||||
clientID, err := parsePathClientID(r)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, tapReceiveAPIResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
serveClientTapReceivePut(w, r, clientID, hub)
|
||||
})
|
||||
|
||||
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 serveClientTapReceivePut(w http.ResponseWriter, r *http.Request, clientID uint32, hub *wsHub) {
|
||||
var body struct {
|
||||
Enable bool `json:"enable"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, tapReceiveAPIResponse{Error: "invalid JSON"})
|
||||
return
|
||||
}
|
||||
if hub != nil {
|
||||
hub.patchClientTapReceive(clientID, body.Enable)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tapReceiveAPIResponse{
|
||||
ClientID: clientID,
|
||||
Enabled: body.Enable,
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user