279 lines
8.1 KiB
Go
279 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/metadata"
|
|
|
|
"remote-shack/api"
|
|
"remote-shack/internal/config"
|
|
"remote-shack/internal/driver"
|
|
)
|
|
|
|
type ShackServer struct {
|
|
api.UnimplementedAuthServiceServer
|
|
api.UnimplementedRigServiceServer
|
|
api.UnimplementedRotorServiceServer
|
|
api.UnimplementedRelaisServiceServer
|
|
|
|
rig driver.RigDriver
|
|
rotor driver.RotorDriver
|
|
relais driver.RelaisDriver
|
|
|
|
mu sync.Mutex
|
|
activeOperator string // Enthält das Token des aktuellen Operators
|
|
tokenToName map[string]string
|
|
}
|
|
|
|
func main() {
|
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
log.Println("=== Starte Remote-Shack Server v2.5 ===")
|
|
|
|
cfg, err := config.LoadServerConfig("server-config.toml")
|
|
if err != nil {
|
|
log.Printf("server-config.toml fehlt, nutze Default-Werte")
|
|
cfg = &config.ServerConfig{Server: config.ServerSettings{ListenAddress: "0.0.0.0:50051"}}
|
|
}
|
|
|
|
// 1. Rig-Treiber anhand der TOML initialisieren
|
|
var rigHardware driver.RigDriver
|
|
if cfg.Server.HardwareRig.Type == "FT897" {
|
|
log.Printf("[Hardware] Initialisiere Yaesu FT-897 an %s mit %d Baud...", cfg.Server.HardwareRig.Port, cfg.Server.HardwareRig.Baud)
|
|
ft897 := driver.NewFT897Driver()
|
|
if err := ft897.Connect(cfg.Server.HardwareRig.Port, cfg.Server.HardwareRig.Baud); err != nil {
|
|
log.Printf("[CRITICAL] Fehler beim Connect zum FT-897: %v. Nutze Dummy-Rig als Fallback!", err)
|
|
rigHardware = driver.NewDummyRig()
|
|
} else {
|
|
rigHardware = ft897
|
|
}
|
|
} else {
|
|
log.Println("[Hardware] Kein Rig-Typ definiert. Verwende Dummy-Rig.")
|
|
rigHardware = driver.NewDummyRig()
|
|
}
|
|
|
|
// 2. Rotor-Treiber anhand der TOML initialisieren
|
|
var rotorHardware driver.RotorDriver
|
|
if cfg.Server.HardwareRotor.Type == "ProSistel" {
|
|
log.Printf("[Hardware] Initialisiere ProSistel Rotor an %s mit %d Baud...", cfg.Server.HardwareRotor.Port, cfg.Server.HardwareRotor.Baud)
|
|
prosistel := driver.NewProSistelDriver()
|
|
if err := prosistel.Connect(cfg.Server.HardwareRotor.Port, cfg.Server.HardwareRotor.Baud); err != nil {
|
|
log.Printf("[CRITICAL] Fehler beim Connect zum ProSistel-Rotor: %v. Nutze Dummy-Rotor als Fallback!", err)
|
|
rotorHardware = driver.NewDummyRotor()
|
|
} else {
|
|
rotorHardware = prosistel
|
|
}
|
|
} else {
|
|
log.Println("[Hardware] Kein Rotor-Typ definiert. Verwende Dummy-Rotor.")
|
|
rotorHardware = driver.NewDummyRotor()
|
|
}
|
|
|
|
server := &ShackServer{
|
|
rig: rigHardware,
|
|
rotor: rotorHardware,
|
|
relais: driver.NewDummyRelais(), // Später I2C ADS1115 / Relais-Board
|
|
tokenToName: make(map[string]string),
|
|
}
|
|
|
|
// Ressourcen-Freigabe beim Beenden sichern
|
|
defer func() {
|
|
log.Println("[Hardware] Schließe Schnittstellen...")
|
|
_ = server.rig.Disconnect()
|
|
_ = server.rotor.Disconnect()
|
|
}()
|
|
|
|
lis, err := net.Listen("tcp", cfg.Server.ListenAddress)
|
|
if err != nil {
|
|
log.Fatalf("Listen Fehler: %v", err)
|
|
}
|
|
|
|
grpcServer := grpc.NewServer()
|
|
api.RegisterAuthServiceServer(grpcServer, server)
|
|
api.RegisterRigServiceServer(grpcServer, server)
|
|
api.RegisterRotorServiceServer(grpcServer, server)
|
|
api.RegisterRelaisServiceServer(grpcServer, server)
|
|
|
|
go func() {
|
|
if err := grpcServer.Serve(lis); err != nil {
|
|
log.Printf("Server-Fehler: %v", err)
|
|
}
|
|
}()
|
|
|
|
stop := make(chan os.Signal, 1)
|
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
|
<-stop
|
|
grpcServer.GracefulStop()
|
|
}
|
|
|
|
// --- AUTH SERVICE ---
|
|
func (s *ShackServer) Login(ctx context.Context, req *api.LoginRequest) (*api.LoginResponse, error) {
|
|
if req.Username == "" {
|
|
return nil, fmt.Errorf("username darf nicht leer sein")
|
|
}
|
|
b := make([]byte, 16)
|
|
rand.Read(b)
|
|
token := fmt.Sprintf("shack-tok-%x", b)
|
|
|
|
s.mu.Lock()
|
|
s.tokenToName[token] = req.Username
|
|
s.mu.Unlock()
|
|
|
|
log.Printf("[Auth] Login: '%s' -> %s", req.Username, token)
|
|
return &api.LoginResponse{Token: token}, nil
|
|
}
|
|
|
|
// --- HELPER ---
|
|
func (s *ShackServer) getVirtualOperator(ctx context.Context) string {
|
|
md, ok := metadata.FromIncomingContext(ctx)
|
|
if !ok { return "" }
|
|
tokens := md.Get("operator-token")
|
|
if len(tokens) == 0 { return "" }
|
|
return tokens[0]
|
|
}
|
|
|
|
func (s *ShackServer) checkAccess(token string) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if token == "" { return false }
|
|
if _, valid := s.tokenToName[token]; !valid { return false }
|
|
if s.activeOperator == "" {
|
|
s.activeOperator = token
|
|
log.Printf("[Rechte] %s (%s) ist nun aktiver Operator.", s.tokenToName[token], token)
|
|
}
|
|
return s.activeOperator == token
|
|
}
|
|
|
|
// --- RIG SERVICE ---
|
|
func (s *ShackServer) OperatorSession(stream api.RigService_OperatorSessionServer) error {
|
|
token := s.getVirtualOperator(stream.Context())
|
|
if !s.checkAccess(token) {
|
|
return fmt.Errorf("zugriff verweigert: nicht aktiver operator")
|
|
}
|
|
|
|
defer func() {
|
|
s.mu.Lock()
|
|
if s.activeOperator == token {
|
|
s.activeOperator = ""
|
|
}
|
|
s.mu.Unlock()
|
|
}()
|
|
|
|
log.Printf("[Server] Operator-Stream erfolgreich etabliert. Warte auf Kommandos...")
|
|
|
|
for {
|
|
cmd, err := stream.Recv()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cmd.Mode == "DISCONNECT" {
|
|
log.Println("[Server] Operator hat sich regulär abgemeldet.")
|
|
return nil
|
|
}
|
|
|
|
// AUSFÜHRLICHES DIAGNOSE-LOG:
|
|
// Hier prüfen wir im Terminal, was gRPC tatsächlich aus dem Netzwerk auspackt!
|
|
log.Printf("[gRPC-In] Rohdaten empfangen -> Frequenz-Feld: %d, Modus-Feld: '%s', PTT: %v",
|
|
cmd.FrequencyHz, cmd.Mode, cmd.Ptt)
|
|
|
|
// Hardware-Ansteuerung mit Absicherung
|
|
if cmd.FrequencyHz > 0 {
|
|
log.Printf("[Hardware-Befehl] Sende SetFrequency(%d Hz) an den Yaesu-Treiber...", cmd.FrequencyHz)
|
|
if err := s.rig.SetFrequency(cmd.FrequencyHz); err != nil {
|
|
log.Printf("[Hardware-FEHLER] Yaesu verweigert Frequenz: %v", err)
|
|
}
|
|
}
|
|
|
|
if cmd.Mode != "" && cmd.Mode != "DISCONNECT" {
|
|
log.Printf("[Hardware-Befehl] Sende SetMode('%s') an den Yaesu-Treiber...", cmd.Mode)
|
|
if err := s.rig.SetMode(cmd.Mode); err != nil {
|
|
log.Printf("[Hardware-FEHLER] Yaesu verweigert Modus: %v", err)
|
|
}
|
|
}
|
|
|
|
_ = s.rig.SetPTT(cmd.Ptt)
|
|
}
|
|
}
|
|
|
|
func (s *ShackServer) MonitorSession(req *api.Empty, stream api.RigService_MonitorSessionServer) error {
|
|
ticker := time.NewTicker(250 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-stream.Context().Done():
|
|
return nil
|
|
case <-ticker.C:
|
|
f, _ := s.rig.GetFrequency()
|
|
m, _ := s.rig.GetMode()
|
|
s_meter, _ := s.rig.GetSMeter()
|
|
|
|
s.mu.Lock()
|
|
opToken := s.activeOperator
|
|
opName := "FREI"
|
|
if name, exists := s.tokenToName[opToken]; exists && opToken != "" {
|
|
opName = name
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
// Übertrage den verifizierten Betreibernamen an das SDR-Display des Clients
|
|
_ = stream.Send(&api.RigStatus{
|
|
FrequencyHz: f,
|
|
Mode: m,
|
|
SMeter: int32(s_meter),
|
|
ActiveOperatorName: opName,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- ROTOR SERVICE ---
|
|
func (s *ShackServer) TargetPosition(ctx context.Context, req *api.RotorTarget) (*api.Empty, error) {
|
|
token := s.getVirtualOperator(ctx)
|
|
if !s.checkAccess(token) {
|
|
return &api.Empty{}, fmt.Errorf("zugriff verweigert")
|
|
}
|
|
_ = s.rotor.SetPosition(req.Azimuth, req.Elevation)
|
|
return &api.Empty{}, nil
|
|
}
|
|
|
|
func (s *ShackServer) LivePosition(req *api.Empty, stream api.RotorService_LivePositionServer) error {
|
|
ticker := time.NewTicker(150 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-stream.Context().Done(): return nil
|
|
case <-ticker.C:
|
|
az, el, _ := s.rotor.GetPosition()
|
|
_ = stream.Send(&api.RotorStatus{AzimuthIs: az, ElevationIs: el})
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- RELAIS SERVICE ---
|
|
func (s *ShackServer) SetRelais(ctx context.Context, req *api.RelaisRequest) (*api.RelaisResponse, error) {
|
|
token := s.getVirtualOperator(ctx)
|
|
if !s.checkAccess(token) {
|
|
return &api.RelaisResponse{}, fmt.Errorf("zugriff verweigert")
|
|
}
|
|
_ = s.relais.SetPin(int(req.Pin), req.State)
|
|
pins, _ := s.relais.GetAllPins()
|
|
res := make(map[int32]bool)
|
|
for k, v := range pins { res[int32(k)] = v }
|
|
return &api.RelaisResponse{Pins: res}, nil
|
|
}
|
|
|
|
func (s *ShackServer) GetRelaisState(ctx context.Context, req *api.Empty) (*api.RelaisResponse, error) {
|
|
pins, _ := s.relais.GetAllPins()
|
|
res := make(map[int32]bool)
|
|
for k, v := range pins { res[int32(k)] = v }
|
|
return &api.RelaisResponse{Pins: res}, nil
|
|
}
|