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>
This commit is contained in:
+160
-8
@@ -39,6 +39,9 @@ type InputClientSample struct {
|
||||
type InputStreamMessage struct {
|
||||
Type string `json:"type"` // "hello" | "input"
|
||||
Serial string `json:"serial_port,omitempty"`
|
||||
Transport string `json:"transport,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
Connected *bool `json:"connected,omitempty"`
|
||||
IntervalMs int `json:"interval_ms,omitempty"`
|
||||
PreFetchMs int `json:"pre_fetch_ms,omitempty"`
|
||||
TapDisplayMinMs int `json:"tap_display_min_ms,omitempty"`
|
||||
@@ -51,6 +54,33 @@ type InputStreamMessage struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// HostStatusMessage is the reply to get_host / host_connect / host_disconnect.
|
||||
type HostStatusMessage struct {
|
||||
Type string `json:"type"` // "host_status"
|
||||
Transport string `json:"transport"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Connected bool `json:"connected"`
|
||||
BleName string `json:"ble_name,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// SerialPortsMessage is the reply to list_serial_ports.
|
||||
type SerialPortsMessage struct {
|
||||
Type string `json:"type"` // "serial_ports"
|
||||
Ports []string `json:"ports"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// BleScanResultMessage is the reply to ble_scan.
|
||||
type BleScanResultMessage struct {
|
||||
Type string `json:"type"` // "ble_scan_result"
|
||||
Devices []bleScanDevice `json:"devices"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// StreamStatusMessage is the reply to set_stream / get_stream (this connection).
|
||||
type StreamStatusMessage struct {
|
||||
Type string `json:"type"` // "stream_status"
|
||||
@@ -116,12 +146,23 @@ type accelWSCommand struct {
|
||||
DoubleTap *bool `json:"double_tap"`
|
||||
Triple *bool `json:"triple"`
|
||||
AllClients bool `json:"all_clients"`
|
||||
// Host connect / BLE scan
|
||||
Transport string `json:"transport"`
|
||||
Port string `json:"port"`
|
||||
Address string `json:"address"`
|
||||
Name string `json:"name"`
|
||||
Baud int `json:"baud"`
|
||||
TimeoutS float64 `json:"timeout_s"`
|
||||
NameFilter string `json:"name_filter"`
|
||||
}
|
||||
|
||||
type APIInfoResponse struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
SerialPort string `json:"serial_port"`
|
||||
Transport string `json:"transport"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Connected bool `json:"connected"`
|
||||
WebSocket string `json:"websocket"`
|
||||
DefaultIntervalMs int `json:"default_interval_ms"`
|
||||
DefaultPreFetchMs int `json:"default_pre_fetch_ms"`
|
||||
@@ -196,7 +237,7 @@ func clampPreFetch(d time.Duration) time.Duration {
|
||||
return d
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) register(conn *websocket.Conn, portName string) *wsSubscriber {
|
||||
func (h *accelStreamHub) register(conn *websocket.Conn, link *managedSerial) *wsSubscriber {
|
||||
sub := &wsSubscriber{
|
||||
conn: conn,
|
||||
receiveInput: false,
|
||||
@@ -207,14 +248,20 @@ func (h *accelStreamHub) register(conn *websocket.Conn, portName string) *wsSubs
|
||||
h.clients[conn] = sub
|
||||
h.mu.Unlock()
|
||||
|
||||
st := link.HostStatus()
|
||||
connected := st["connected"].(bool)
|
||||
hello := InputStreamMessage{
|
||||
Type: "hello",
|
||||
Serial: portName,
|
||||
Serial: st["endpoint"].(string),
|
||||
Transport: st["transport"].(string),
|
||||
Endpoint: st["endpoint"].(string),
|
||||
Connected: &connected,
|
||||
IntervalMs: int(h.defaultInterval / time.Millisecond),
|
||||
PreFetchMs: int(h.defaultPreFetch / time.Millisecond),
|
||||
TapDisplayMinMs: apiTapDisplayMinMs,
|
||||
Note: "set_tap_notify configures slave S/D/T only; set_stream enables input polling/push on this connection",
|
||||
Commands: []string{
|
||||
"get_host", "list_serial_ports", "ble_scan", "host_connect", "host_disconnect",
|
||||
"list_clients",
|
||||
"set_stream", "get_stream",
|
||||
"set_input_stream", "get_input_stream",
|
||||
@@ -454,6 +501,29 @@ func runInputStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl
|
||||
continue
|
||||
}
|
||||
|
||||
// Prefer BLE_STREAM_TICK when on BLE (firmware push).
|
||||
if link.Transport() == hostTransportBLE {
|
||||
payload, err := link.NextStreamTick(20 * time.Millisecond)
|
||||
if err == nil {
|
||||
cache, cerr := cacheStatusFromBLEStreamPayload(payload)
|
||||
if cerr == nil {
|
||||
hub.ingestTapFromCache(cache)
|
||||
hub.mu.RLock()
|
||||
clients := hub.inputClientsFromCacheLocked(cache, time.Now())
|
||||
hub.mu.RUnlock()
|
||||
hub.deliverInput(InputStreamMessage{
|
||||
Type: "input",
|
||||
T: time.Now().UnixNano(),
|
||||
Success: true,
|
||||
Clients: clients,
|
||||
})
|
||||
pending = nil
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Fall through to CACHE_STATUS poll if no tick yet.
|
||||
}
|
||||
|
||||
needRead, needDeliver, waitPreFetch := hub.streamTiming(now)
|
||||
|
||||
if needRead && pending == nil {
|
||||
@@ -634,6 +704,58 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
|
||||
}
|
||||
|
||||
switch cmd.Type {
|
||||
case "get_host":
|
||||
writeHostStatus(conn, hostStatusFromLink(link, true, ""))
|
||||
|
||||
case "list_serial_ports":
|
||||
ports, err := listSerialPorts()
|
||||
msg := SerialPortsMessage{Type: "serial_ports", Ports: ports, Success: err == nil}
|
||||
if err != nil {
|
||||
msg.Error = err.Error()
|
||||
msg.Ports = []string{}
|
||||
}
|
||||
writeWSJSON(conn, msg)
|
||||
|
||||
case "ble_scan":
|
||||
timeout := time.Duration(cmd.TimeoutS * float64(time.Second))
|
||||
if timeout <= 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
if link.Transport() == hostTransportBLE {
|
||||
_ = link.Disconnect()
|
||||
}
|
||||
devs, err := scanBLEDevices(timeout, cmd.NameFilter)
|
||||
msg := BleScanResultMessage{Type: "ble_scan_result", Devices: devs, Success: err == nil}
|
||||
if err != nil {
|
||||
msg.Error = err.Error()
|
||||
msg.Devices = []bleScanDevice{}
|
||||
}
|
||||
writeWSJSON(conn, msg)
|
||||
|
||||
case "host_connect":
|
||||
var err error
|
||||
switch cmd.Transport {
|
||||
case hostTransportUART:
|
||||
err = link.ConnectUART(cmd.Port, cmd.Baud)
|
||||
case hostTransportBLE:
|
||||
err = link.ConnectBLE(cmd.Address, cmd.Name)
|
||||
default:
|
||||
writeHostStatus(conn, HostStatusMessage{
|
||||
Type: "host_status",
|
||||
Error: "transport must be uart or ble",
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeHostStatus(conn, hostStatusFromLink(link, false, err.Error()))
|
||||
return
|
||||
}
|
||||
writeHostStatus(conn, hostStatusFromLink(link, true, ""))
|
||||
|
||||
case "host_disconnect":
|
||||
_ = link.Disconnect()
|
||||
writeHostStatus(conn, hostStatusFromLink(link, true, ""))
|
||||
|
||||
case "list_clients":
|
||||
clients, err := link.listClientsPoll()
|
||||
if err != nil {
|
||||
@@ -791,13 +913,38 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
|
||||
default:
|
||||
writeStreamStatus(conn, StreamStatusMessage{
|
||||
Type: "stream_status",
|
||||
Error: "unknown type (list_clients, set_stream, get_stream, set_input_stream, get_input_stream, set_tap_notify, get_tap_notify, set_led_ring, get_battery)",
|
||||
Error: "unknown type (get_host, list_serial_ports, ble_scan, host_connect, host_disconnect, list_clients, set_stream, get_stream, set_input_stream, get_input_stream, set_tap_notify, get_tap_notify, set_led_ring, get_battery)",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func serveExternalWS(conn *websocket.Conn, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, portName string, hub *accelStreamHub) {
|
||||
sub := hub.register(conn, portName)
|
||||
func hostStatusFromLink(link *managedSerial, success bool, errMsg string) HostStatusMessage {
|
||||
st := link.HostStatus()
|
||||
return HostStatusMessage{
|
||||
Type: "host_status",
|
||||
Transport: st["transport"].(string),
|
||||
Endpoint: st["endpoint"].(string),
|
||||
Connected: st["connected"].(bool),
|
||||
BleName: st["ble_name"].(string),
|
||||
Success: success && errMsg == "",
|
||||
Error: errMsg,
|
||||
}
|
||||
}
|
||||
|
||||
func writeHostStatus(conn *websocket.Conn, msg HostStatusMessage) {
|
||||
writeWSJSON(conn, msg)
|
||||
}
|
||||
|
||||
func writeWSJSON(conn *websocket.Conn, msg any) {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = conn.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
|
||||
func serveExternalWS(conn *websocket.Conn, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, hub *accelStreamHub) {
|
||||
sub := hub.register(conn, link)
|
||||
defer hub.unregister(conn)
|
||||
defer conn.Close()
|
||||
|
||||
@@ -811,6 +958,7 @@ func serveExternalWS(conn *websocket.Conn, link *managedSerial, dash *wsHub, ctl
|
||||
}
|
||||
|
||||
func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.Duration, hub *accelStreamHub, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl) {
|
||||
_ = portName
|
||||
defMs := int(defaultInterval / time.Millisecond)
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" && r.URL.Path != "/api/v1" && r.URL.Path != "/api/v1/" {
|
||||
@@ -821,17 +969,21 @@ func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
st := link.HostStatus()
|
||||
writeJSON(w, http.StatusOK, APIInfoResponse{
|
||||
Name: "powerpod-external-api",
|
||||
Version: "1",
|
||||
SerialPort: portName,
|
||||
SerialPort: st["endpoint"].(string),
|
||||
Transport: st["transport"].(string),
|
||||
Endpoint: st["endpoint"].(string),
|
||||
Connected: st["connected"].(bool),
|
||||
WebSocket: "/ws",
|
||||
DefaultIntervalMs: defMs,
|
||||
DefaultPreFetchMs: defaultPreFetchMs,
|
||||
MinIntervalMs: int(minAPIStreamInterval / time.Millisecond),
|
||||
MaxIntervalMs: int(maxAPIStreamInterval / time.Millisecond),
|
||||
TapDisplayMinMs: apiTapDisplayMinMs,
|
||||
Description: "WebSocket: set_input_stream + set_stream for input (accel + tap); set_tap_notify configures slave tap kinds",
|
||||
Description: "WebSocket: host_connect/ble_scan + set_input_stream + set_stream for input (accel + tap)",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -841,7 +993,7 @@ func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.
|
||||
log.Printf("api websocket upgrade: %v", err)
|
||||
return
|
||||
}
|
||||
serveExternalWS(conn, link, dash, ctl, tapCtl, portName, hub)
|
||||
serveExternalWS(conn, link, dash, ctl, tapCtl, hub)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user