Files
powerpods/goTool/api_host.go
T
skrueckenandCursor aa67edfe94 Add dynamic UART/BLE host connect for serve, dashboard, and WS.
Serve starts without a required port; REST/WS/UI can scan and switch transports, with BLE live ticks and UART-only OTA.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-23 22:40:28 +02:00

141 lines
4.0 KiB
Go

package main
import (
"encoding/json"
"net/http"
"time"
)
type hostAPIResponse struct {
Transport string `json:"transport"`
Endpoint string `json:"endpoint"`
Connected bool `json:"connected"`
BleName string `json:"ble_name,omitempty"`
Error string `json:"error,omitempty"`
}
type hostConnectRequest struct {
Transport string `json:"transport"` // uart|ble
Port string `json:"port,omitempty"`
Address string `json:"address,omitempty"`
Name string `json:"name,omitempty"`
Baud int `json:"baud,omitempty"`
}
type bleScanRequest struct {
TimeoutS float64 `json:"timeout_s"`
NameFilter string `json:"name_filter"`
}
type bleScanResponse struct {
Devices []bleScanDevice `json:"devices"`
Error string `json:"error,omitempty"`
}
type serialPortsResponse struct {
Ports []string `json:"ports"`
Error string `json:"error,omitempty"`
}
func mountHostAPI(mux *http.ServeMux, link *managedHost) {
mux.HandleFunc("/api/host", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
st := link.HostStatus()
writeJSON(w, http.StatusOK, hostAPIResponse{
Transport: st["transport"].(string),
Endpoint: st["endpoint"].(string),
Connected: st["connected"].(bool),
BleName: st["ble_name"].(string),
})
})
mux.HandleFunc("/api/host/connect", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body hostConnectRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, hostAPIResponse{Error: "invalid JSON"})
return
}
var err error
switch body.Transport {
case hostTransportUART:
err = link.ConnectUART(body.Port, body.Baud)
case hostTransportBLE:
err = link.ConnectBLE(body.Address, body.Name)
default:
writeJSON(w, http.StatusBadRequest, hostAPIResponse{Error: "transport must be uart or ble"})
return
}
st := link.HostStatus()
resp := hostAPIResponse{
Transport: st["transport"].(string),
Endpoint: st["endpoint"].(string),
Connected: st["connected"].(bool),
BleName: st["ble_name"].(string),
}
if err != nil {
resp.Error = err.Error()
resp.Connected = false
writeJSON(w, http.StatusBadRequest, resp)
return
}
writeJSON(w, http.StatusOK, resp)
})
mux.HandleFunc("/api/host/disconnect", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
_ = link.Disconnect()
st := link.HostStatus()
writeJSON(w, http.StatusOK, hostAPIResponse{
Transport: st["transport"].(string),
Endpoint: st["endpoint"].(string),
Connected: false,
})
})
mux.HandleFunc("/api/serial/ports", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
ports, err := listSerialPorts()
if err != nil {
writeJSON(w, http.StatusOK, serialPortsResponse{Ports: []string{}, Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, serialPortsResponse{Ports: ports})
})
mux.HandleFunc("/api/ble/scan", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body bleScanRequest
_ = json.NewDecoder(r.Body).Decode(&body)
timeout := time.Duration(body.TimeoutS * float64(time.Second))
if timeout <= 0 {
timeout = 5 * time.Second
}
// Disconnect active BLE before scan (BlueZ adapter contention).
if link.Transport() == hostTransportBLE {
_ = link.Disconnect()
}
devs, err := scanBLEDevices(timeout, body.NameFilter)
if err != nil {
writeJSON(w, http.StatusOK, bleScanResponse{Devices: []bleScanDevice{}, Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, bleScanResponse{Devices: devs})
})
}