Master can speak the same framed protobuf protocol over NimBLE or UART; goTool gains -ble, ble-rtt, and ble-stream timing checks for ESP-NOW coexistence. Co-authored-by: Cursor <cursoragent@cursor.com>
126 lines
3.3 KiB
Go
126 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
pb "powerpod/gotool/pb"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// 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 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
|
|
}
|