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>
240 lines
6.6 KiB
Go
240 lines
6.6 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 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())
|
|
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
|
|
}
|