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:
2026-10-23 22:40:28 +02:00
co-authored by Cursor
parent 92a13d7fd0
commit aa67edfe94
18 changed files with 1573 additions and 404 deletions
+114
View File
@@ -73,6 +73,120 @@ func hostAccelStream(h hostConn, req *pb.AccelStreamRequest) (*pb.AccelStreamRes
return r, nil
}
func hostFindMe(h hostConn, clientID uint32) error {
msg := &pb.UartMessage{
Type: pb.MessageType_FIND_ME,
Payload: &pb.UartMessage_EspnowFindMeRequest{
EspnowFindMeRequest: &pb.EspNowFindMeRequest{ClientId: clientID},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_FIND_ME)}, body...)
respPayload, err := h.exchangePayload(payload, "FIND_ME")
if err != nil {
return err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return fmt.Errorf("decode: %w", err)
}
r := respMsg.GetEspnowFindMeResponse()
if r == nil {
return fmt.Errorf("missing espnow_find_me_response")
}
if !r.GetSuccess() {
return fmt.Errorf("find-me rejected (client_id=%d)", r.GetClientId())
}
return nil
}
func hostRestart(h hostConn, clientID uint32) error {
msg := &pb.UartMessage{
Type: pb.MessageType_RESTART,
Payload: &pb.UartMessage_RestartRequest{
RestartRequest: &pb.RestartRequest{ClientId: clientID},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_RESTART)}, body...)
respPayload, err := h.exchangePayload(payload, "RESTART")
if err != nil {
return err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return fmt.Errorf("decode: %w", err)
}
r := respMsg.GetRestartResponse()
if r == nil {
return fmt.Errorf("missing restart_response")
}
if !r.GetSuccess() {
return fmt.Errorf("restart rejected (client_id=%d)", r.GetClientId())
}
return nil
}
func hostSetLogLevel(h hostConn, write bool, level uint32) (*pb.SetLogLevelResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_SET_LOG_LEVEL,
Payload: &pb.UartMessage_SetLogLevelRequest{
SetLogLevelRequest: &pb.SetLogLevelRequest{Write: write, Level: level},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_SET_LOG_LEVEL)}, body...)
respPayload, err := h.exchangePayload(payload, "SET_LOG_LEVEL")
if err != nil {
return nil, err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetSetLogLevelResponse()
if r == nil {
return nil, fmt.Errorf("missing set_log_level_response")
}
return r, nil
}
func hostLedRing(h hostConn, req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_LED_RING,
Payload: &pb.UartMessage_LedRingProgressRequest{
LedRingProgressRequest: req,
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_LED_RING)}, body...)
respPayload, err := h.exchangePayload(payload, "LED_RING")
if err != nil {
return nil, err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetLedRingProgressResponse()
if r == nil {
return nil, fmt.Errorf("missing led_ring_progress_response")
}
return r, nil
}
func hostEchoPing(h hostConn, clientID uint32) (*EchoPingResult, error) {
t0 := time.Now()
timestampUs := uint64(t0.UnixMicro())