2026-06-18 17:09 Frequenzanzeige funktioniert jetzt
This commit is contained in:
parent
c954c2292a
commit
f82f9bde86
15 changed files with 854 additions and 18 deletions
45
Notizen.txt
45
Notizen.txt
|
|
@ -12,7 +12,7 @@ Funktionsprinzip:
|
||||||
+-------------------+ +-----------------------+ | (Listener: View only + RX) |
|
+-------------------+ +-----------------------+ | (Listener: View only + RX) |
|
||||||
+---------------------------------+
|
+---------------------------------+
|
||||||
|
|
||||||
mkdir -p remote-shack/{api,cmd/{server,client-rig,client-rotor,client-relais},internal/{config,driver,audio}}
|
mkdir -p remote-shack/{api,cmd/{server,client-rig,client-rotor,client-relais,test-rig},internal/{config,driver,audio}}
|
||||||
|
|
||||||
remote-shack/
|
remote-shack/
|
||||||
├── api/ # Protobuf-Definitionen für gRPC (.proto)
|
├── api/ # Protobuf-Definitionen für gRPC (.proto)
|
||||||
|
|
@ -81,5 +81,46 @@ go mod tidy
|
||||||
|
|
||||||
|
|
||||||
weitermachen:
|
weitermachen:
|
||||||
PROJEKT-MERKER: SAT-ROTOR RADIALDESIGNAktueller Status: Beide Kompasse laufen geometrisch entkoppelt und nebeneinander im 300x300 Pixel-Format mit transparenten Kreisen. Azimut nutzt den vollen Ring. Elevation nutzt den bogenförmigen Viertelkreis (-5° bis 90°) und besitzt jetzt 6 dedizierte Bogen-Buttons (0°, 15°, 30°, 45°, 60°, 90°), die sich harmonisch an die Krümmung der Skala anschmiegen. Beim Drücken der Elevations-Tasten bleibt der Azimut unberührt.Morgiger Start:Einfügen der Himmelsrichtungs-Textmarker (N, O, S, W) per canvas.NewText.Start des Linux-I2C-Bus-Treibers für den Raspberry Pi.
|
==============================================================================
|
||||||
|
PROJEKT-MERKER: REMOTE-SHACK START AM MORGEN
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
AKTUELLER STATUS:
|
||||||
|
-----------------
|
||||||
|
* SDR-Rig-Client: Vollständig auf ein modernes SDR-Design umgestellt.
|
||||||
|
Die Frequenzanzeige ist massiv vergrößert (canvas.Text, Größe 36, Monospace)
|
||||||
|
und durch unverschiebbare Tausender-Punkte (014.250.000) in MHz-, kHz-
|
||||||
|
und Hz-Blöcke strukturiert. Die Bandtasten werden dynamisch geladen.
|
||||||
|
|
||||||
|
* Sat-Rotor-Client: Geometrisch entkoppelt im 300x300-Format. Elevation
|
||||||
|
wurde für Erdverbindungen/Troposcatter auf -20° bis 90° erweitert. Der
|
||||||
|
Kreismittelpunkt wurde auf Y=200 angehoben, um Platz nach unten zu
|
||||||
|
schaffen. Ein neuer -15°-Bogen-Button ist integriert. Die originale
|
||||||
|
tokenbasierte gRPC-Schreib-Validierung via TargetPosition ist lückenlos aktiv.
|
||||||
|
|
||||||
|
* Kompilierung: Das build.sh-Skript ist repariert und berücksichtigt
|
||||||
|
CGO_ENABLED=1. Alle Komponenten bauen fehlerfrei direkt in die
|
||||||
|
Zielordner (./app/server/ und ./app/clients/).
|
||||||
|
|
||||||
|
* Architektur-Entscheidung: Wechsel auf ein flexibles Connector-Modell
|
||||||
|
(Abstraktionsschicht analog zu Hamlib). Der gRPC-Server spricht nur noch
|
||||||
|
mit generischen Interfaces. Die Hardware (ProSistel, SPID etc.) wird rein
|
||||||
|
über die TOML-Konfigurationsdateien eingekoppelt, ohne den Core-Code zu verändern.
|
||||||
|
|
||||||
|
NÄCHSTE SCHRITTE:
|
||||||
|
-----------------
|
||||||
|
1. Implementierung des konkreten ProSistel-Seriell-Protokolls im neuen
|
||||||
|
Connector unter internal/driver/.
|
||||||
|
2. Vorbereitung des SPID-Rotor-Connectors für das Gerät des Bekannten.
|
||||||
|
3. Einbindung des ADS1115-I2C-Treibers im Server für die reine Shack-
|
||||||
|
Betriebsspannungs-Telemetrie (Entkoppelt von der Rotor-Grad-Messung).
|
||||||
|
|
||||||
|
BEREITZUSTELLENDE DATEIEN FÜR DEN START:
|
||||||
|
----------------------------------------
|
||||||
|
Bitte lade zu Beginn des neuen Chats diese Dateien hoch, um die Code-Basis
|
||||||
|
direkt abzugleichen:
|
||||||
|
1. internal/driver/interfaces.go (Das aktuelle Interface-Gerüst)
|
||||||
|
2. server-config.toml & client-config.toml (Für die neuen Hardware-Felder)
|
||||||
|
3. Technische Infos zum ProSistel-Protokoll (Befehlssatz für Az/El über Seriell)
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -31,7 +31,7 @@ type ShackServer struct {
|
||||||
relais driver.RelaisDriver
|
relais driver.RelaisDriver
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
activeOperator string
|
activeOperator string // Enthält das Token des aktuellen Operators
|
||||||
tokenToName map[string]string
|
tokenToName map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,13 +45,52 @@ func main() {
|
||||||
cfg = &config.ServerConfig{Server: config.ServerSettings{ListenAddress: "0.0.0.0:50051"}}
|
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{
|
server := &ShackServer{
|
||||||
rig: driver.NewDummyRig(),
|
rig: rigHardware,
|
||||||
rotor: driver.NewDummyRotor(),
|
rotor: rotorHardware,
|
||||||
relais: driver.NewDummyRelais(),
|
relais: driver.NewDummyRelais(), // Später I2C ADS1115 / Relais-Board
|
||||||
tokenToName: make(map[string]string),
|
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)
|
lis, err := net.Listen("tcp", cfg.Server.ListenAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Listen Fehler: %v", err)
|
log.Fatalf("Listen Fehler: %v", err)
|
||||||
|
|
@ -108,7 +147,7 @@ func (s *ShackServer) checkAccess(token string) bool {
|
||||||
if _, valid := s.tokenToName[token]; !valid { return false }
|
if _, valid := s.tokenToName[token]; !valid { return false }
|
||||||
if s.activeOperator == "" {
|
if s.activeOperator == "" {
|
||||||
s.activeOperator = token
|
s.activeOperator = token
|
||||||
log.Printf("[Rechte] %s ist nun aktiver Operator.", s.tokenToName[token])
|
log.Printf("[Rechte] %s (%s) ist nun aktiver Operator.", s.tokenToName[token], token)
|
||||||
}
|
}
|
||||||
return s.activeOperator == token
|
return s.activeOperator == token
|
||||||
}
|
}
|
||||||
|
|
@ -120,16 +159,47 @@ func (s *ShackServer) OperatorSession(stream api.RigService_OperatorSessionServe
|
||||||
return fmt.Errorf("zugriff verweigert: nicht aktiver operator")
|
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 {
|
for {
|
||||||
cmd, err := stream.Recv()
|
cmd, err := stream.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.mu.Lock()
|
|
||||||
if s.activeOperator == token { s.activeOperator = "" }
|
|
||||||
s.mu.Unlock()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if cmd.FrequencyHz > 0 { _ = s.rig.SetFrequency(cmd.FrequencyHz) }
|
|
||||||
if cmd.Mode != "" { _ = s.rig.SetMode(cmd.Mode) }
|
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)
|
_ = s.rig.SetPTT(cmd.Ptt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -139,12 +209,28 @@ func (s *ShackServer) MonitorSession(req *api.Empty, stream api.RigService_Monit
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-stream.Context().Done(): return nil
|
case <-stream.Context().Done():
|
||||||
|
return nil
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
f, _ := s.rig.GetFrequency()
|
f, _ := s.rig.GetFrequency()
|
||||||
m, _ := s.rig.GetMode()
|
m, _ := s.rig.GetMode()
|
||||||
s_meter, _ := s.rig.GetSMeter()
|
s_meter, _ := s.rig.GetSMeter()
|
||||||
_ = stream.Send(&api.RigStatus{FrequencyHz: f, Mode: m, SMeter: int32(s_meter)})
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
74
cmd/test_rig/main.go
Normal file
74
cmd/test_rig/main.go
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"remote-shack/internal/driver"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("=== Starte direkten Hardware-Test: Yaesu FT-897 ===")
|
||||||
|
|
||||||
|
// 1. Treiber-Instanz erstellen
|
||||||
|
rig := driver.NewFT897Driver()
|
||||||
|
|
||||||
|
// 2. Verbindung mit den neuen 38400 Baud aufbauen
|
||||||
|
port := "/dev/ttyUSB0"
|
||||||
|
baud := 38400
|
||||||
|
log.Printf("Öffne Schnittstelle %s mit %d Baud...", port, baud)
|
||||||
|
|
||||||
|
if err := rig.Connect(port, baud); err != nil {
|
||||||
|
log.Fatalf("VERBINDUNGSFEHLER: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = rig.Disconnect()
|
||||||
|
log.Println("Schnittstelle sauber geschlossen.")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 3. Aktuelle Frequenz und Mode auslesen (Ist-Zustand abfragen)
|
||||||
|
log.Println("Frage aktuellen Transceiver-Status ab...")
|
||||||
|
time.Sleep(200 * time.Millisecond) // Dem Controller kurz Zeit geben
|
||||||
|
|
||||||
|
startFreq, err := rig.GetFrequency()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warnung: Konnte Frequenz nicht lesen: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("-> Aktuelle Frequenz am Gerät: %d Hz", startFreq)
|
||||||
|
}
|
||||||
|
|
||||||
|
startMode, err := rig.GetMode()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warnung: Konnte Modus nicht lesen: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("-> Aktueller Modus am Gerät: %s", startMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Test-Kommando: Frequenz auf das 20m-Band setzen (14.074.000 Hz)
|
||||||
|
targetFreq := int64(14074000)
|
||||||
|
log.Printf("Setze Test-Frequenz auf FT8-Frequenz: %d Hz...", targetFreq)
|
||||||
|
if err := rig.SetFrequency(targetFreq); err != nil {
|
||||||
|
log.Fatalf("Fehler beim Setzen der Frequenz: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Test-Kommando: Mode auf USB setzen
|
||||||
|
targetMode := "USB"
|
||||||
|
log.Printf("Schalte Betriebsart auf: %s...", targetMode)
|
||||||
|
if err := rig.SetMode(targetMode); err != nil {
|
||||||
|
log.Printf("Fehler beim Setzen des Modus: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. S-Meter Live-Test (3 Sekunden lang abfragen)
|
||||||
|
log.Println("Starte 3-sekündige S-Meter Überwachung...")
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
smeter, err := rig.GetSMeter()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("S-Meter Fehler: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("[Live Status] S-Meter Wert: S%d", smeter)
|
||||||
|
}
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("=== Test erfolgreich abgeschlossen! ===")
|
||||||
|
}
|
||||||
1
go.mod
1
go.mod
|
|
@ -37,6 +37,7 @@ require (
|
||||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 // indirect
|
||||||
github.com/yuin/goldmark v1.7.8 // indirect
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
golang.org/x/image v0.24.0 // indirect
|
golang.org/x/image v0.24.0 // indirect
|
||||||
golang.org/x/net v0.51.0 // indirect
|
golang.org/x/net v0.51.0 // indirect
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -79,6 +79,8 @@ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqd
|
||||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
|
||||||
|
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,26 @@ type ADS1115Module struct {
|
||||||
Labels []string `toml:"labels"` // Genau 4 Beschriftungen für die Kanäle A0 bis A3
|
Labels []string `toml:"labels"` // Genau 4 Beschriftungen für die Kanäle A0 bis A3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RigHardwareSettings definiert die Parameter für das CAT-Interface des Transceivers.
|
||||||
|
type RigHardwareSettings struct {
|
||||||
|
Type string `toml:"type"` // Z.B. "FT897"
|
||||||
|
Port string `toml:"port"` // Z.B. "/dev/ttyUSB0"
|
||||||
|
Baud int `toml:"baud"` // Z.B. 38400
|
||||||
|
}
|
||||||
|
|
||||||
|
// RotorHardwareSettings definiert die Parameter für das serielle Protokoll des Rotors.
|
||||||
|
type RotorHardwareSettings struct {
|
||||||
|
Type string `toml:"type"` // Z.B. "ProSistel" oder "SPID"
|
||||||
|
Port string `toml:"port"` // Z.B. "/dev/ttyUSB1"
|
||||||
|
Baud int `toml:"baud"` // Z.B. 9600
|
||||||
|
}
|
||||||
|
|
||||||
// ServerSettings definiert die Netzwerkparameter des Server-Daemons.
|
// ServerSettings definiert die Netzwerkparameter des Server-Daemons.
|
||||||
type ServerSettings struct {
|
type ServerSettings struct {
|
||||||
ListenAddress string `toml:"listen_address"`
|
ListenAddress string `toml:"listen_address"`
|
||||||
Modules []ADS1115Module `toml:"modules"` // Unterstützt dynamisch 1 bis 4 Platinen
|
HardwareRig RigHardwareSettings `toml:"hardware_rig"` // ERWEITERUNG für Yaesu
|
||||||
|
HardwareRotor RotorHardwareSettings `toml:"hardware_rotor"` // ERWEITERUNG für ProSistel/SPID
|
||||||
|
Modules []ADS1115Module `toml:"modules"` // Korrekt innerhalb der server-Sektion platziert
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig bündelt alle Konfigurationseinstellungen des Servers.
|
// ServerConfig bündelt alle Konfigurationseinstellungen des Servers.
|
||||||
|
|
@ -24,7 +40,7 @@ type ServerConfig struct {
|
||||||
Server ServerSettings `toml:"server"`
|
Server ServerSettings `toml:"server"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RotorSettings definiert die hardware- und systemspezifischen Parameter eines Rotors.
|
// RotorSettings definiert die hardware- und systemspezifischen Parameter eines Rotors (Client-Sicht).
|
||||||
type RotorSettings struct {
|
type RotorSettings struct {
|
||||||
Type string `toml:"type"` // Z.B. "G-5500", "Spid", "Sat-Rotor"
|
Type string `toml:"type"` // Z.B. "G-5500", "Spid", "Sat-Rotor"
|
||||||
Port string `toml:"port"` // Z.B. "/dev/ttyUSB0" oder "I2C"
|
Port string `toml:"port"` // Z.B. "/dev/ttyUSB0" oder "I2C"
|
||||||
|
|
@ -37,7 +53,7 @@ type ClientSettings struct {
|
||||||
ServerAddress string `toml:"server_address"`
|
ServerAddress string `toml:"server_address"`
|
||||||
StationName string `toml:"station_name"`
|
StationName string `toml:"station_name"`
|
||||||
RefreshRateMs int `toml:"refresh_rate_ms"`
|
RefreshRateMs int `toml:"refresh_rate_ms"`
|
||||||
Rotor RotorSettings `toml:"rotor"` // ERWEITERUNG: Rotorspezifische Einstellungen
|
Rotor RotorSettings `toml:"rotor"` // Rotorspezifische Einstellungen
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientConfig bündelt alle Konfigurationseinstellungen des GUI-Clients.
|
// ClientConfig bündelt alle Konfigurationseinstellungen des GUI-Clients.
|
||||||
|
|
@ -50,6 +66,16 @@ func LoadServerConfig(path string) (*ServerConfig, error) {
|
||||||
cfg := &ServerConfig{
|
cfg := &ServerConfig{
|
||||||
Server: ServerSettings{
|
Server: ServerSettings{
|
||||||
ListenAddress: "0.0.0.0:50051",
|
ListenAddress: "0.0.0.0:50051",
|
||||||
|
HardwareRig: RigHardwareSettings{
|
||||||
|
Type: "Dummy",
|
||||||
|
Port: "Simulation",
|
||||||
|
Baud: 4800,
|
||||||
|
},
|
||||||
|
HardwareRotor: RotorHardwareSettings{
|
||||||
|
Type: "Dummy",
|
||||||
|
Port: "Simulation",
|
||||||
|
Baud: 9600,
|
||||||
|
},
|
||||||
Modules: []ADS1115Module{
|
Modules: []ADS1115Module{
|
||||||
{
|
{
|
||||||
Address: "0x48",
|
Address: "0x48",
|
||||||
|
|
|
||||||
138
internal/driver/prosistel.go
Normal file
138
internal/driver/prosistel.go
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
package driver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tarm/serial"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProSistelDriver implementiert das RotorDriver-Interface für ProSistel-Controller.
|
||||||
|
type ProSistelDriver struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
port io.ReadWriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProSistelDriver initialisiert eine neue Instanz des Treibers.
|
||||||
|
func NewProSistelDriver() *ProSistelDriver {
|
||||||
|
return &ProSistelDriver{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect öffnet die serielle Schnittstelle zum ProSistel-Controller.
|
||||||
|
func (p *ProSistelDriver) Connect(portPath string, baud int) error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if p.port != nil {
|
||||||
|
return fmt.Errorf("port bereits geöffnet")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &serial.Config{
|
||||||
|
Name: portPath,
|
||||||
|
Baud: baud,
|
||||||
|
ReadTimeout: time.Millisecond * 500,
|
||||||
|
}
|
||||||
|
|
||||||
|
sp, err := serial.OpenPort(config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fehler beim öffnen von %s: %w", portPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.port = sp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect schließt die serielle Schnittstelle sauber.
|
||||||
|
func (p *ProSistelDriver) Disconnect() error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if p.port == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.port.Close()
|
||||||
|
p.port = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPosition sendet die Zielkoordinaten an den ProSistel-Controller.
|
||||||
|
func (p *ProSistelDriver) SetPosition(azimuth, elevation float64) error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if p.port == nil {
|
||||||
|
return fmt.Errorf("treiber nicht verbunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
azInt := int(azimuth + 0.5)
|
||||||
|
elInt := int(elevation + 0.5)
|
||||||
|
|
||||||
|
if azInt < 0 { azInt = 0 }
|
||||||
|
if azInt > 360 { azInt = 360 }
|
||||||
|
|
||||||
|
cmd := fmt.Sprintf("*O%03d*W%03d\r", azInt, elInt)
|
||||||
|
|
||||||
|
_, err := p.port.Write([]byte(cmd))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fehler beim senden des set-befehls: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPosition liest die aktuellen Ist-Werte des Rotors aus.
|
||||||
|
func (p *ProSistelDriver) GetPosition() (az, el float64, err error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if p.port == nil {
|
||||||
|
return 0, 0, fmt.Errorf("treiber nicht verbunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
az, err = p.queryValue("*A000\r", "A")
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("fehler bei azimut-abfrage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
el, err = p.queryValue("*E000\r", "E")
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("fehler bei elevations-abfrage: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return az, el, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hilfsfunktion zum Senden einer Anfrage und Parsen der Antwort
|
||||||
|
func (p *ProSistelDriver) queryValue(cmd string, prefix string) (float64, error) {
|
||||||
|
if _, err := p.port.Write([]byte(cmd)); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 32)
|
||||||
|
n, err := p.port.Read(buf)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response := strings.TrimSpace(string(buf[:n]))
|
||||||
|
if response == "" {
|
||||||
|
return 0, fmt.Errorf("keine antwort vom controller erhalten")
|
||||||
|
}
|
||||||
|
|
||||||
|
cleaned := response
|
||||||
|
cleaned = strings.ReplaceAll(cleaned, prefix+"=", "")
|
||||||
|
cleaned = strings.ReplaceAll(cleaned, prefix, "")
|
||||||
|
cleaned = strings.TrimSpace(cleaned)
|
||||||
|
|
||||||
|
val, err := strconv.ParseFloat(cleaned, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("ungültiges antwortformat '%s': %w", response, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
250
internal/driver/yaesu_ft897.go
Normal file
250
internal/driver/yaesu_ft897.go
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
package driver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tarm/serial"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FT897Driver implementiert das RigDriver-Interface für den Yaesu FT-897 (CAT-Protokoll).
|
||||||
|
type FT897Driver struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
port io.ReadWriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFT897Driver initialisiert eine neue Instanz für das Yaesu-CAT-Protokoll.
|
||||||
|
func NewFT897Driver() *FT897Driver {
|
||||||
|
return &FT897Driver{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect öffnet die serielle Schnittstelle (z.B. FTDI-Kabel / CT-62 Ersatz).
|
||||||
|
func (d *FT897Driver) Connect(portPath string, baud int) error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.port != nil {
|
||||||
|
return fmt.Errorf("port bereits geöffnet")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &serial.Config{
|
||||||
|
Name: portPath,
|
||||||
|
Baud: baud,
|
||||||
|
ReadTimeout: time.Millisecond * 300, // Schnelles Timeout für flüssiges VFO-Streaming
|
||||||
|
}
|
||||||
|
|
||||||
|
sp, err := serial.OpenPort(config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fehler beim öffnen des cat-ports %s: %w", portPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.port = sp
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect schließt das serielle Gerät.
|
||||||
|
func (d *FT897Driver) Disconnect() error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.port == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := d.port.Close()
|
||||||
|
d.port = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFrequency liest die aktuelle Betriebsfrequenz des Funkgeräts aus.
|
||||||
|
// Der Yaesu FT-897 liefert bei der Statusabfrage (Opcode 0x03) ein 5-Byte Paket zurück.
|
||||||
|
// Die ersten 4 Bytes enthalten die Frequenz als BCD gepackt in 10-Hz-Schritten.
|
||||||
|
// Das 5. Byte enthält die aktuelle Betriebsart (Mode).
|
||||||
|
func (d *FT897Driver) GetFrequency() (int64, error) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
// 1. Sicherheitshalt: Prüfen, ob die serielle Verbindung aktiv ist
|
||||||
|
if d.port == nil {
|
||||||
|
return 0, fmt.Errorf("transceiver nicht verbunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. CAT-Abfragebefehl an das Funkgerät senden.
|
||||||
|
// Das Paket besteht aus 4 Dummy-Bytes und dem Opcode 0x03 (Read Status).
|
||||||
|
cmd := []byte{0x00, 0x00, 0x00, 0x00, 0x03}
|
||||||
|
if _, err := d.port.Write(cmd); err != nil {
|
||||||
|
return 0, fmt.Errorf("fehler beim senden der statusabfrage (opcode 0x03): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Die 5-Byte-Antwort der Transceiver-Firmware blockierend einlesen
|
||||||
|
buf := make([]byte, 5)
|
||||||
|
if _, err := io.ReadFull(d.port, buf); err != nil {
|
||||||
|
return 0, fmt.Errorf("fehler beim lesen der 5-byte cat-antwort: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. BCD-Entschlüsselung (Rechnerische Rekonstruktion der Frequenz)
|
||||||
|
// Wir laufen durch die ersten 4 Bytes. Jedes Byte enthält zwei Dezimalziffern.
|
||||||
|
var freq int64
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
// Bit-Shifting: Die oberen 4 Bit isolieren und nach rechts schieben (z.B. 0x14 -> 1)
|
||||||
|
high := int64(buf[i] >> 4)
|
||||||
|
// Bit-Maskierung: Die unteren 4 Bit isolieren (z.B. 0x14 & 0x0F -> 4)
|
||||||
|
low := int64(buf[i] & 0x0F)
|
||||||
|
|
||||||
|
// Die bestehende Zahl um zwei Dezimalstellen nach links schieben
|
||||||
|
// und die beiden neu decodierten Ziffern am Ende anfügen.
|
||||||
|
freq = freq*100 + (high*10 + low)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. SKALIERUNGS-KORREKTUR:
|
||||||
|
// Da der Yaesu FT-897 die Frequenz im BCD-Register nativ in 10-Hz-Schritten ausgibt,
|
||||||
|
// müssen wir das Zwischenergebnis zwingend mit 10 multiplizieren.
|
||||||
|
// Beispiel: Aus 14436000 (10-Hz-Einheiten) wird 144360000 Hz (Echte Hertz).
|
||||||
|
freq = freq * 10
|
||||||
|
|
||||||
|
return freq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFrequency stellt die Betriebsfrequenz des Funkgeräts in Hertz (Hz) ein.
|
||||||
|
// Der Yaesu FT-897 erwartet ein 5-Byte-Paket: 4 Bytes Frequenz im BCD-Format
|
||||||
|
// (aufgelöst in 10-Hz-Schritten) und als 5. Byte den Opcode 0x01.
|
||||||
|
// Beispiel: 145.600.000 Hz wird zu den BCD-Bytes [0x14, 0x56, 0x00, 0x00] + 0x01.
|
||||||
|
func (d *FT897Driver) SetFrequency(hz int64) error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
// 1. Sicherheitshalt: Verbindung prüfen
|
||||||
|
if d.port == nil {
|
||||||
|
return fmt.Errorf("transceiver nicht verbunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. SKALIERUNGS-ANPASSUNG:
|
||||||
|
// Da das CAT-Protokoll des Yaesu die Frequenz in 10-Hz-Schritten verarbeitet,
|
||||||
|
// müssen wir den übergebenen Hertz-Wert zuerst durch 10 teilen.
|
||||||
|
// Beispiel: 145600000 Hz / 10 = 14560000 (8-stellige Basis für die 4 BCD-Bytes).
|
||||||
|
remain := hz / 10
|
||||||
|
|
||||||
|
// 3. BCD-Verschlüsselung (Packen der Ziffern von hinten nach vorne)
|
||||||
|
var bcd [4]byte
|
||||||
|
for i := 3; i >= 0; i-- {
|
||||||
|
// Isoliere die niederwertige Ziffer (Low-Nibble, z.B. 0)
|
||||||
|
digit1 := remain % 10
|
||||||
|
remain /= 10
|
||||||
|
|
||||||
|
// Isoliere die höherwertige Ziffer des Paars (High-Nibble, z.B. 0)
|
||||||
|
digit2 := remain % 10
|
||||||
|
remain /= 10
|
||||||
|
|
||||||
|
// Schiebe die zweite Ziffer in die oberen 4 Bits (Bitwise OR) und speichere das Byte
|
||||||
|
bcd[i] = byte((digit2 << 4) | digit1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Befehlspaket zusammensetzen: 4 Bytes Frequenz-BCD + 1 Byte Schreib-Opcode (0x01)
|
||||||
|
cmd := []byte{bcd[0], bcd[1], bcd[2], bcd[3], 0x01}
|
||||||
|
|
||||||
|
// 5. Paket über die serielle Schnittstelle an den Yaesu abfeuern
|
||||||
|
if _, err := d.port.Write(cmd); err != nil {
|
||||||
|
return fmt.Errorf("fehler beim senden des set-frequency befehls: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMode schaltet das Modulationsrelais im FT-897 um (Opcode 0x07).
|
||||||
|
func (d *FT897Driver) SetMode(mode string) error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.port == nil {
|
||||||
|
return fmt.Errorf("transceiver nicht verbunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
var modeByte byte
|
||||||
|
switch mode {
|
||||||
|
case "LSB": modeByte = 0x00
|
||||||
|
case "USB": modeByte = 0x01
|
||||||
|
case "CW": modeByte = 0x02
|
||||||
|
case "AM": modeByte = 0x04
|
||||||
|
case "FM": modeByte = 0x08
|
||||||
|
case "FT8": modeByte = 0x01 // FT8 nutzt technisch das USB-Seitenband
|
||||||
|
default: return fmt.Errorf("modus %s wird vom ft897-treiber nicht unterstützt", mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := []byte{modeByte, 0x00, 0x00, 0x00, 0x07}
|
||||||
|
_, err := d.port.Write(cmd)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMode holt das aktuelle Status-Byte vom Gerät.
|
||||||
|
func (d *FT897Driver) GetMode() (string, error) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.port == nil {
|
||||||
|
return "",fmt.Errorf("transceiver nicht verbunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := d.port.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x03}); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 5)
|
||||||
|
if _, err := io.ReadFull(d.port, buf); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// KORREKTUR: Expliziter Index-Zugriff auf das fünfte Byte (buf[4]) für das Mode-Register
|
||||||
|
switch buf[4] & 0x0F {
|
||||||
|
case 0x00: return "LSB", nil
|
||||||
|
case 0x01: return "USB", nil
|
||||||
|
case 0x02: return "CW", nil
|
||||||
|
case 0x04: return "AM", nil
|
||||||
|
case 0x08: return "FM", nil
|
||||||
|
}
|
||||||
|
return "USB", nil // Fallback für digitale Untermodi
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPTT schaltet den Sender ein (Opcode 0x08) oder aus (Opcode 0x88).
|
||||||
|
func (d *FT897Driver) SetPTT(active bool) error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.port == nil {
|
||||||
|
return fmt.Errorf("transceiver nicht verbunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
opcode := byte(0x08)
|
||||||
|
if !active {
|
||||||
|
opcode = 0x88
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := []byte{0x00, 0x00, 0x00, 0x00, opcode}
|
||||||
|
_, err := d.port.Write(cmd)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSMeter fragt das S-Meter über den internen ADC-Status ab (Opcode 0xE7).
|
||||||
|
func (d *FT897Driver) GetSMeter() (int, error) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.port == nil {
|
||||||
|
return 0, fmt.Errorf("transceiver nicht verbunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := d.port.Write([]byte{0x00, 0x00, 0x00, 0x00, 0xE7}); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
if _, err := io.ReadFull(d.port, buf); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// KORREKTUR: Zugriff auf buf[0] statt auf das gesamte Slice
|
||||||
|
rawStrength := int(buf[0] & 0x0F)
|
||||||
|
sMeterValue := (rawStrength * 9) / 15
|
||||||
|
return sMeterValue, nil
|
||||||
|
}
|
||||||
208
internal/server/rig_server.go
Normal file
208
internal/server/rig_server.go
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
shack "remote-shack/api"
|
||||||
|
"remote-shack/internal/driver"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RigServer verwaltet den gRPC-Dienst für das Funkgerät und den Operator-Status.
|
||||||
|
type RigServer struct {
|
||||||
|
shack.UnimplementedRigServiceServer
|
||||||
|
mu sync.Mutex
|
||||||
|
driver driver.RigDriver
|
||||||
|
|
||||||
|
// Sitzungsverwaltung
|
||||||
|
activeOperator string
|
||||||
|
operatorStream shack.RigService_OperatorSessionServer
|
||||||
|
|
||||||
|
// Aktueller Hardware-Zustand für passive Zuhörer
|
||||||
|
currentFreq int64
|
||||||
|
currentMode string
|
||||||
|
currentSMeter int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRigServer erstellt eine neue Instanz des gRPC-Rig-Servers.
|
||||||
|
func NewRigServer(d driver.RigDriver) *RigServer {
|
||||||
|
s := &RigServer{
|
||||||
|
driver: d,
|
||||||
|
currentMode: "USB",
|
||||||
|
}
|
||||||
|
// Starte die Hintergrund-Synchronisation für S-Meter und Frequenz-Updates
|
||||||
|
go s.hardwareSyncLoop()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFrequency wird vom aktiven Operator aufgerufen (Direktzugriff oder Dekadentasten)
|
||||||
|
func (s *RigServer) SetFrequency(ctx context.Context, req *shack.FrequencyRequest) (*shack.EmptyResponse, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
// Validierung: Nur der aktive Operator darf senden
|
||||||
|
if err := s.validateOperator(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.driver.SetFrequency(req.Hz); err != nil {
|
||||||
|
return nil, fmt.Errorf("hardware-fehler beim setzen der frequenz: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.currentFreq = req.Hz
|
||||||
|
return &shack.EmptyResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMode schaltet die Betriebsart auf Hardware-Ebene um
|
||||||
|
func (s *RigServer) SetMode(ctx context.Context, req *shack.ModeRequest) (*shack.EmptyResponse, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if err := s.validateOperator(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.driver.SetMode(req.Mode); err != nil {
|
||||||
|
return nil, fmt.Errorf("hardware-fehler beim setzen des modus: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.currentMode = req.Mode
|
||||||
|
return &shack.EmptyResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatus liefert den schnellen Zwischenspeicher-Status (wird für Fallbacks genutzt)
|
||||||
|
func (s *RigServer) GetStatus(ctx context.Context, req *shack.EmptyRequest) (*shack.StatusResponse, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
return &shack.StatusResponse{
|
||||||
|
Hz: s.currentFreq,
|
||||||
|
Mode: s.currentMode,
|
||||||
|
SMeter: int32(s.currentSMeter),
|
||||||
|
ActiveOperatorName: s.activeOperator,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperatorSession verwaltet den langlebigen, bidirektionalen gRPC-Stream für die Steuerung
|
||||||
|
func (s *RigServer) OperatorSession(stream shack.RigService_OperatorSessionServer) error {
|
||||||
|
// Den Betreibernamen aus dem gRPC-Metadaten-Interceptor extrahieren
|
||||||
|
md, ok := grpcMetadata.FromIncomingContext(stream.Context())
|
||||||
|
var opName string
|
||||||
|
if ok && len(md["operator-token"]) > 0 {
|
||||||
|
opName = md["operator-token"][0] // Einfachheitshalber nutzen wir den Namen als Token
|
||||||
|
} else {
|
||||||
|
opName = "Gast"
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
if s.activeOperator != "" && s.activeOperator != opName {
|
||||||
|
s.mu.Unlock()
|
||||||
|
return fmt.Errorf("shack ist bereits durch %s blockiert", s.activeOperator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sitzung zuweisen
|
||||||
|
s.activeOperator = opName
|
||||||
|
s.operatorStream = stream
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
log.Printf("[Server] %s hat die exklusive Rig-Steuerung übernommen.", opName)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.activeOperator = ""
|
||||||
|
s.operatorStream = nil
|
||||||
|
s.mu.Unlock()
|
||||||
|
log.Printf("[Server] %s hat die Steuerung freigegeben.", opName)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Nachrichten vom Operator empfangen und verarbeiten
|
||||||
|
for {
|
||||||
|
cmd, err := stream.Recv()
|
||||||
|
if err != nil {
|
||||||
|
return err // Verbindungsabbruch beendet die Session sauber über das defer
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Mode == "DISCONNECT" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
// Nur aktualisieren, wenn gültige Werte geschickt werden
|
||||||
|
if cmd.FrequencyHz > 0 {
|
||||||
|
_ = s.driver.SetFrequency(cmd.FrequencyHz)
|
||||||
|
s.currentFreq = cmd.FrequencyHz
|
||||||
|
}
|
||||||
|
if cmd.Mode != "" && cmd.Mode != "DISCONNECT" {
|
||||||
|
_ = s.driver.SetMode(cmd.Mode)
|
||||||
|
s.currentMode = cmd.Mode
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonitorSession streamt den aktuellen Hardware-Zustand an alle passiven Zuhörer
|
||||||
|
func (s *RigServer) MonitorSession(req *shack.Empty, stream shack.RigService_MonitorSessionServer) error {
|
||||||
|
ticker := time.NewTicker(250 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stream.Context().Done():
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
|
s.mu.Lock()
|
||||||
|
opName := s.activeOperator
|
||||||
|
if opName == "" {
|
||||||
|
opName = "FREI"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := stream.Send(&shack.RigStatus{
|
||||||
|
FrequencyHz: s.currentFreq,
|
||||||
|
Mode: s.currentMode,
|
||||||
|
SMeter: int32(s.currentSMeter),
|
||||||
|
ActiveOperatorName: opName,
|
||||||
|
})
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hilfsfunktion zur Operator-Validierung
|
||||||
|
func (s *RigServer) validateOperator(ctx context.Context) error {
|
||||||
|
// Falls kein Stream offen ist, ist das System frei (Sicherheits-Fallback)
|
||||||
|
if s.activeOperator == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hardwareSyncLoop liest im Hintergrund zyklisch das S-Meter und Änderungen am physischen VFO aus
|
||||||
|
func (s *RigServer) hardwareSyncLoop() {
|
||||||
|
ticker := time.NewTicker(300 * time.Millisecond)
|
||||||
|
for range ticker.C {
|
||||||
|
s.mu.Lock()
|
||||||
|
// Wenn kein Operator eingeloggt ist, lesen wir die Frequenz vom Gerät (falls jemand am echten VFO dreht)
|
||||||
|
if s.activeOperator == "" {
|
||||||
|
if freq, err := s.driver.GetFrequency(); err == nil && freq > 0 {
|
||||||
|
s.currentFreq = freq
|
||||||
|
}
|
||||||
|
if mode, err := s.driver.GetMode(); err == nil && mode != "" {
|
||||||
|
s.currentMode = mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// S-Meter wird immer gelesen, um es im SDR-Display anzuzeigen
|
||||||
|
if sm, err := s.driver.GetSMeter(); err == nil {
|
||||||
|
s.currentSMeter = sm
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
[server]
|
[server]
|
||||||
listen_address = "0.0.0.0:50051"
|
listen_address = "0.0.0.0:50051"
|
||||||
|
|
||||||
|
[server.hardware_rig]
|
||||||
|
type = "FT897"
|
||||||
|
port = "/dev/ttyUSB0"
|
||||||
|
baud = 38400
|
||||||
|
|
||||||
|
[server.hardware_rotor]
|
||||||
|
type = "ProSistel"
|
||||||
|
port = "/dev/ttyUSB1"
|
||||||
|
baud = 9600
|
||||||
|
|
||||||
[[server.modules]]
|
[[server.modules]]
|
||||||
address = "0x48"
|
address = "0x48"
|
||||||
labels = ["Hauptstrom 13.8V", "Logikbus 5V", "Relais-Schiene", "Rauschen (Frei)"]
|
labels = ["Hauptstrom 13.8V", "Logikbus 5V", "Relais-Schiene", "Rauschen (Frei)"]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue