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 }