remote-shack/cmd/server/main.go

311 lines
9.2 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)
}
}
// MonitorSession streamt den aktuellen Zustand der Hardware an alle passiven Clients.
// Optimiert: Verlangsamt die seriellen Abfragen drastisch, um das hardwarebedingte
// Audio-Klicken (NF-Muting) im Lautsprecher des Yaesu FT-897 vollständig zu eliminieren.
func (s *ShackServer) MonitorSession(req *api.Empty, stream api.RigService_MonitorSessionServer) error {
// Der Ticker für das Streaming an die GUI bleibt flüssig bei 250ms...
ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop()
var counter int
var lastFreq int64
var lastMode string
var lastSMeter int
for {
select {
case <-stream.Context().Done():
return nil
case <-ticker.C:
counter++
// 1. FREQUENZ- & MODUS-ABFRAGE (Alle 500ms = jeden 2. Takt)
// Das schont die Auslastung der internen Transceiver-CPU im Leerlauf.
if counter%2 == 0 {
if f, err := s.rig.GetFrequency(); err == nil {
lastFreq = f
}
if m, err := s.rig.GetMode(); err == nil {
lastMode = m
}
}
// 2. S-METER-ABFRAGE (Alle 1000ms = jeden 4. Takt)
// KRITISCH: Die S-Meter-Abfrage (0xE7) verursacht das stärkste NF-Knacken.
// Durch die Reduzierung auf 1 Sekunde wird das Geräusch im normalen Betrieb unhörbar
// oder tritt nur noch extrem selten auf.
if counter%4 == 0 {
if sm, err := s.rig.GetSMeter(); err == nil {
lastSMeter = sm
}
counter = 0 // Zähler zurücksetzen, um Overflows zu verhindern
}
// 3. Rechteinhaber ermitteln
s.mu.Lock()
opToken := s.activeOperator
opName := "FREI"
if name, exists := s.tokenToName[opToken]; exists && opToken != "" {
opName = name
}
s.mu.Unlock()
// 4. Daten an die Fyne-GUI senden
_ = stream.Send(&api.RigStatus{
FrequencyHz: lastFreq,
Mode: lastMode,
SMeter: int32(lastSMeter),
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
}