Files
powerpods/goTool/host_conn.go
T
skrueckenandCursor 1adaddd6c8 Add selectable ESP-NOW send path for accel/tap input.
Expose best_effort/fast/wait/reliable via UART, dashboard, and CLI so
loss vs latency can be compared under real multi-pod load.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-01 22:34:03 +01:00

267 lines
7.4 KiB
Go

package main
import (
"fmt"
"time"
"google.golang.org/protobuf/proto"
pb "powerpod/gotool/pb"
)
// hostConn is the UART or BLE host transport used by CLI commands.
type hostConn interface {
exchangePayload(payload []byte, cmdName string) ([]byte, error)
exchange(cmdID byte, cmdName string) ([]byte, error)
Close() error
}
// optional stream source (BLE only)
type hostStreamConn interface {
hostConn
// NextStreamTick waits for a BLE_STREAM_TICK payload (type byte + protobuf).
NextStreamTick(timeout time.Duration) ([]byte, error)
}
func hostGetVersion(h hostConn) (*pb.VersionResponse, error) {
payload, err := h.exchange(byte(pb.MessageType_VERSION), "VERSION")
if err != nil {
return nil, err
}
return decodeVersionPayload(payload)
}
func hostListClients(h hostConn) ([]*pb.ClientInfo, error) {
payload, err := h.exchange(byte(pb.MessageType_CLIENT_INFO), "CLIENT_INFO")
if err != nil {
return nil, err
}
return decodeClientsPayload(payload)
}
func hostReadCacheStatus(h hostConn) (*pb.CacheStatusResponse, error) {
payload, err := h.exchange(byte(pb.MessageType_CACHE_STATUS), "CACHE_STATUS")
if err != nil {
return nil, err
}
return decodeCacheStatusPayload(payload)
}
func hostAccelStream(h hostConn, req *pb.AccelStreamRequest) (*pb.AccelStreamResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_ACCEL_STREAM,
Payload: &pb.UartMessage_AccelStreamRequest{
AccelStreamRequest: req,
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_ACCEL_STREAM)}, body...)
respPayload, err := h.exchangePayload(payload, "ACCEL_STREAM")
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.GetAccelStreamResponse()
if r == nil {
return nil, fmt.Errorf("missing accel_stream_response")
}
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 hostInputSendMode(h hostConn, req *pb.InputSendModeRequest) (*pb.InputSendModeResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_INPUT_SEND_MODE,
Payload: &pb.UartMessage_InputSendModeRequest{
InputSendModeRequest: req,
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_INPUT_SEND_MODE)}, body...)
respPayload, err := h.exchangePayload(payload, "INPUT_SEND_MODE")
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.GetInputSendModeResponse()
if r == nil {
return nil, fmt.Errorf("missing input_send_mode_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())
req := &pb.EspNowEchoPingRequest{
ClientId: clientID,
TimestampUs: timestampUs,
}
msg := &pb.UartMessage{
Type: pb.MessageType_ESPNOW_ECHO_PING,
Payload: &pb.UartMessage_EspnowEchoPingRequest{
EspnowEchoPingRequest: req,
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_ESPNOW_ECHO_PING)}, body...)
respPayload, err := h.exchangePayload(payload, "ESPNOW_ECHO_PING")
if err != nil {
return nil, err
}
rttMs := float64(time.Since(t0).Microseconds()) / 1000.0
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetEspnowEchoPingResponse()
if r == nil {
return nil, fmt.Errorf("missing espnow_echo_ping_response")
}
if !r.GetSuccess() {
return &EchoPingResult{
Success: false,
ClientID: r.GetClientId(),
RttMs: rttMs,
}, nil
}
if r.GetTimestampUs() != timestampUs {
return nil, fmt.Errorf("timestamp mismatch: sent %d got %d", timestampUs, r.GetTimestampUs())
}
return &EchoPingResult{
Success: true,
ClientID: r.GetClientId(),
TimestampUs: r.GetTimestampUs(),
RttMs: rttMs,
EspRttUs: r.GetEspRttUs(),
}, nil
}