This commit is contained in:
2026-08-27 19:41:00 +03:00
parent de0e8f8ad3
commit 0d80f575c1
389 changed files with 183499 additions and 0 deletions
+94
View File
@@ -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
}
+54
View File
@@ -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
}
}
}
}
+42
View File
@@ -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)
}
}
+48
View File
@@ -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()
}
+57
View File
@@ -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)
}
+11
View File
@@ -0,0 +1,11 @@
package grpc
import (
"context"
pb "spektr/internal/grpc/pb"
)
type Handler interface {
Handle(context.Context, *pb.ClientMessage) *pb.ServerMessage
}
+147
View File
@@ -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
}
+1
View File
@@ -0,0 +1 @@
package mock
+1
View File
@@ -0,0 +1 @@
package uart
+1
View File
@@ -0,0 +1 @@
package uart
+80
View File
@@ -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()
}