first
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
GRPCListenAddr string
|
||||
GRPCAdvertiseAddr string
|
||||
DiscoveryPort int
|
||||
UARTDevice string
|
||||
UARTBaud int
|
||||
}
|
||||
|
||||
func Default() Config {
|
||||
return Config{
|
||||
GRPCListenAddr: ":50051",
|
||||
GRPCAdvertiseAddr: "127.0.0.1:50051",
|
||||
DiscoveryPort: 50052,
|
||||
UARTDevice: "/dev/ttyS3",
|
||||
UARTBaud: 115200,
|
||||
}
|
||||
}
|
||||
|
||||
func Load(path string) (Config, error) {
|
||||
cfg := Default()
|
||||
if path == "" {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return cfg, nil
|
||||
}
|
||||
return Config{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
key, value, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
return Config{}, fmt.Errorf("invalid config line: %q", line)
|
||||
}
|
||||
|
||||
key = strings.ToLower(strings.TrimSpace(key))
|
||||
value = strings.TrimSpace(value)
|
||||
value = strings.Trim(value, `"'`)
|
||||
|
||||
switch key {
|
||||
case "grpc_addr":
|
||||
cfg.GRPCListenAddr = value
|
||||
cfg.GRPCAdvertiseAddr = value
|
||||
case "grpc_listen_addr":
|
||||
cfg.GRPCListenAddr = value
|
||||
case "grpc_advertise_addr":
|
||||
cfg.GRPCAdvertiseAddr = value
|
||||
case "discovery_port":
|
||||
n, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("invalid discovery_port: %w", err)
|
||||
}
|
||||
cfg.DiscoveryPort = n
|
||||
case "uart_device":
|
||||
cfg.UARTDevice = value
|
||||
case "uart_baud":
|
||||
n, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("invalid uart_baud: %w", err)
|
||||
}
|
||||
cfg.UARTBaud = n
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
if cfg.GRPCAdvertiseAddr == "" {
|
||||
cfg.GRPCAdvertiseAddr = cfg.GRPCListenAddr
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const requestMagic = "SPEKTR_DISCOVER"
|
||||
const responsePrefix = "SPEKTR|"
|
||||
|
||||
func Discover(ctx context.Context, port int) (string, error) {
|
||||
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
target := &net.UDPAddr{IP: net.IPv4bcast, Port: port}
|
||||
if _, err := conn.WriteToUDP([]byte(requestMagic), target); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := make([]byte, 256)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
n, _, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
return "", fmt.Errorf("discovery timeout")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp := strings.TrimSpace(string(buf[:n]))
|
||||
if strings.HasPrefix(resp, responsePrefix) {
|
||||
addr := strings.TrimSpace(strings.TrimPrefix(resp, responsePrefix))
|
||||
if addr != "" {
|
||||
return addr, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Serve(ctx context.Context, port int, grpcAddr string) error {
|
||||
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: port})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
buf := make([]byte, 256)
|
||||
for {
|
||||
if err := conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, remote, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
if ne, ok := err.(net.Error); ok && ne.Timeout() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.TrimSpace(string(buf[:n])) != requestMagic {
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = conn.WriteToUDP([]byte(responsePrefix+grpcAddr), remote)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
pb "spektr/internal/grpc/pb"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
conn *grpc.ClientConn
|
||||
cancel context.CancelFunc
|
||||
stream pb.ReceiverService_ConnectClient
|
||||
}
|
||||
|
||||
func Dial(ctx context.Context, addr string) (*Client, error) {
|
||||
conn, err := grpc.DialContext(ctx, addr, grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := pb.NewReceiverServiceClient(conn)
|
||||
streamCtx, cancel := context.WithCancel(context.Background())
|
||||
stream, err := client.Connect(streamCtx)
|
||||
if err != nil {
|
||||
cancel()
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{conn: conn, cancel: cancel, stream: stream}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Send(msg *pb.ClientMessage) (*pb.ServerMessage, error) {
|
||||
if err := c.stream.Send(msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.stream.Recv()
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
}
|
||||
return c.conn.Close()
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
|
||||
pb "spektr/internal/grpc/pb"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
pb.UnimplementedReceiverServiceServer
|
||||
handler Handler
|
||||
}
|
||||
|
||||
func (s *server) Connect(stream pb.ReceiverService_ConnectServer) error {
|
||||
log.Println("client connected")
|
||||
for {
|
||||
msg, err := stream.Recv()
|
||||
if err != nil {
|
||||
log.Println("client disconnected:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("recv request_id=%s", msg.GetRequestId())
|
||||
resp := s.handler.Handle(stream.Context(), msg)
|
||||
if resp == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := stream.Send(resp); err != nil {
|
||||
log.Println("send error:", err)
|
||||
return err
|
||||
}
|
||||
log.Printf("sent request_id=%s", resp.GetRequestId())
|
||||
}
|
||||
}
|
||||
|
||||
func Serve(ctx context.Context, addr string, handler Handler) error {
|
||||
lis, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srv := grpc.NewServer()
|
||||
pb.RegisterReceiverServiceServer(srv, &server{handler: handler})
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
srv.GracefulStop()
|
||||
}()
|
||||
|
||||
log.Println("gRPC listening on", addr)
|
||||
return srv.Serve(lis)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
pb "spektr/internal/grpc/pb"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
Handle(context.Context, *pb.ClientMessage) *pb.ServerMessage
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
pb "spektr/internal/grpc/pb"
|
||||
"spektr/internal/uart"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
mu sync.Mutex
|
||||
uart *uart.UART
|
||||
lastFreq uint32
|
||||
}
|
||||
|
||||
var (
|
||||
versionRe = regexp.MustCompile(`(?i)HW[:=]\s*([^\s]+).*?(?:FW|SW)[:=]\s*([^\s]+)`)
|
||||
freqRe = regexp.MustCompile(`(?i)\bf\d{7,9}\b`)
|
||||
)
|
||||
|
||||
func New(u *uart.UART) *Service {
|
||||
return &Service{uart: u}
|
||||
}
|
||||
|
||||
func (s *Service) Handle(ctx context.Context, msg *pb.ClientMessage) *pb.ServerMessage {
|
||||
_ = ctx
|
||||
|
||||
switch payload := msg.GetPayload().(type) {
|
||||
case *pb.ClientMessage_Av:
|
||||
return s.handleAV(msg.GetRequestId())
|
||||
case *pb.ClientMessage_SetFrequency:
|
||||
return s.handleSetFrequency(msg.GetRequestId(), payload.SetFrequency.GetFrequency())
|
||||
case *pb.ClientMessage_GetFrequency:
|
||||
return s.handleGetFrequency(msg.GetRequestId())
|
||||
default:
|
||||
return errorMessage(msg.GetRequestId(), "unknown command")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) handleAV(requestID string) *pb.ServerMessage {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
line, err := s.uart.Exec("av")
|
||||
if err != nil {
|
||||
log.Printf("uart av error: %v", err)
|
||||
return errorMessage(requestID, err.Error())
|
||||
}
|
||||
log.Printf("uart av raw: %q", line)
|
||||
|
||||
hw, sw := parseVersion(line)
|
||||
if hw == "" && sw == "" {
|
||||
hw = line
|
||||
}
|
||||
return &pb.ServerMessage{
|
||||
RequestId: requestID,
|
||||
Payload: &pb.ServerMessage_Av{
|
||||
Av: &pb.AvResponse{Ok: true, Hw: hw, Sw: sw},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) handleSetFrequency(requestID string, frequency uint32) *pb.ServerMessage {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
cmd := encodeFrequencyCommand(frequency)
|
||||
if err := s.uart.Write(cmd); err != nil {
|
||||
log.Printf("uart af write error: %v", err)
|
||||
return errorMessage(requestID, err.Error())
|
||||
}
|
||||
log.Printf("uart af sent: %q", cmd)
|
||||
|
||||
s.lastFreq = frequency
|
||||
|
||||
return &pb.ServerMessage{
|
||||
RequestId: requestID,
|
||||
Payload: &pb.ServerMessage_SetFrequency{
|
||||
SetFrequency: &pb.SetFrequencyResponse{Ok: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) handleGetFrequency(requestID string) *pb.ServerMessage {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
line, err := s.uart.Exec("ar")
|
||||
if err != nil {
|
||||
log.Printf("uart ar error: %v", err)
|
||||
return errorMessage(requestID, err.Error())
|
||||
}
|
||||
log.Printf("uart ar raw: %q", line)
|
||||
|
||||
frequency, err := decodeFrequencyResponse(line)
|
||||
if err != nil {
|
||||
return errorMessage(requestID, err.Error())
|
||||
}
|
||||
|
||||
s.lastFreq = frequency
|
||||
return &pb.ServerMessage{
|
||||
RequestId: requestID,
|
||||
Payload: &pb.ServerMessage_GetFrequency{
|
||||
GetFrequency: &pb.GetFrequencyResponse{Frequency: frequency},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func errorMessage(requestID, message string) *pb.ServerMessage {
|
||||
return &pb.ServerMessage{
|
||||
RequestId: requestID,
|
||||
Payload: &pb.ServerMessage_Error{
|
||||
Error: &pb.Error{Message: message},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func parseVersion(line string) (string, string) {
|
||||
match := versionRe.FindStringSubmatch(line)
|
||||
if len(match) == 3 {
|
||||
return match[1], match[2]
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func encodeFrequencyCommand(frequency uint32) string {
|
||||
return fmt.Sprintf("af%07d", frequency*100)
|
||||
}
|
||||
|
||||
func decodeFrequencyResponse(line string) (uint32, error) {
|
||||
match := freqRe.FindString(line)
|
||||
if match == "" {
|
||||
return 0, fmt.Errorf("invalid frequency response %q", line)
|
||||
}
|
||||
|
||||
value, err := strconv.ParseUint(match[1:], 10, 32)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid frequency response %q: %w", line, err)
|
||||
}
|
||||
|
||||
return uint32(value / 100), nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package mock
|
||||
@@ -0,0 +1 @@
|
||||
package uart
|
||||
@@ -0,0 +1 @@
|
||||
package uart
|
||||
@@ -0,0 +1,80 @@
|
||||
package uart
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tarm/serial"
|
||||
)
|
||||
|
||||
const (
|
||||
BUFFERSIZE = 4096
|
||||
READTIMEOUT = 50 * time.Millisecond
|
||||
IDLE = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
type UART struct {
|
||||
port *serial.Port
|
||||
}
|
||||
|
||||
func New(device string, baud int) (*UART, error) {
|
||||
c := &serial.Config{
|
||||
Name: device,
|
||||
Baud: baud,
|
||||
ReadTimeout: READTIMEOUT,
|
||||
}
|
||||
|
||||
p, err := serial.OpenPort(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UART{port: p}, nil
|
||||
}
|
||||
|
||||
func (u *UART) Write(cmd string) error {
|
||||
_, err := u.port.Write([]byte(strings.TrimSpace(cmd) + "\r"))
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *UART) ReadResponse() (string, error) {
|
||||
buf := make([]byte, BUFFERSIZE)
|
||||
var out bytes.Buffer
|
||||
|
||||
received := false
|
||||
lastData := time.Now()
|
||||
for {
|
||||
n, err := u.port.Read(buf)
|
||||
|
||||
if err != nil && err != io.EOF && !errorsIsTimeout(err) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
received = true
|
||||
lastData = time.Now()
|
||||
out.Write(buf[:n])
|
||||
}
|
||||
|
||||
if received && time.Since(lastData) >= IDLE {
|
||||
return strings.TrimSpace(out.String()), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func errorsIsTimeout(err error) bool {
|
||||
return strings.Contains(strings.ToLower(err.Error()), "timeout")
|
||||
}
|
||||
|
||||
func (u *UART) Exec(cmd string) (string, error) {
|
||||
if err := u.Write(cmd); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.ReadResponse()
|
||||
}
|
||||
|
||||
func (u *UART) Close() error {
|
||||
return u.port.Close()
|
||||
}
|
||||
Reference in New Issue
Block a user