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) |
|
||||
+---------------------------------+
|
||||
|
||||
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/
|
||||
├── api/ # Protobuf-Definitionen für gRPC (.proto)
|
||||
|
|
@ -81,5 +81,46 @@ go mod tidy
|
|||
|
||||
|
||||
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
|
||||
|
||||
mu sync.Mutex
|
||||
activeOperator string
|
||||
activeOperator string // Enthält das Token des aktuellen Operators
|
||||
tokenToName map[string]string
|
||||
}
|
||||
|
||||
|
|
@ -45,13 +45,52 @@ func main() {
|
|||
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: driver.NewDummyRig(),
|
||||
rotor: driver.NewDummyRotor(),
|
||||
relais: driver.NewDummyRelais(),
|
||||
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)
|
||||
|
|
@ -108,7 +147,7 @@ func (s *ShackServer) checkAccess(token string) bool {
|
|||
if _, valid := s.tokenToName[token]; !valid { return false }
|
||||
if s.activeOperator == "" {
|
||||
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
|
||||
}
|
||||
|
|
@ -120,16 +159,47 @@ func (s *ShackServer) OperatorSession(stream api.RigService_OperatorSessionServe
|
|||
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 {
|
||||
s.mu.Lock()
|
||||
if s.activeOperator == token { s.activeOperator = "" }
|
||||
s.mu.Unlock()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -139,12 +209,28 @@ func (s *ShackServer) MonitorSession(req *api.Empty, stream api.RigService_Monit
|
|||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stream.Context().Done(): return nil
|
||||
case <-stream.Context().Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
f, _ := s.rig.GetFrequency()
|
||||
m, _ := s.rig.GetMode()
|
||||
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/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // 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
|
||||
golang.org/x/image v0.24.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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
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/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
type ServerSettings struct {
|
||||
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.
|
||||
|
|
@ -24,7 +40,7 @@ type ServerConfig struct {
|
|||
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 string `toml:"type"` // Z.B. "G-5500", "Spid", "Sat-Rotor"
|
||||
Port string `toml:"port"` // Z.B. "/dev/ttyUSB0" oder "I2C"
|
||||
|
|
@ -37,7 +53,7 @@ type ClientSettings struct {
|
|||
ServerAddress string `toml:"server_address"`
|
||||
StationName string `toml:"station_name"`
|
||||
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.
|
||||
|
|
@ -50,6 +66,16 @@ func LoadServerConfig(path string) (*ServerConfig, error) {
|
|||
cfg := &ServerConfig{
|
||||
Server: ServerSettings{
|
||||
ListenAddress: "0.0.0.0:50051",
|
||||
HardwareRig: RigHardwareSettings{
|
||||
Type: "Dummy",
|
||||
Port: "Simulation",
|
||||
Baud: 4800,
|
||||
},
|
||||
HardwareRotor: RotorHardwareSettings{
|
||||
Type: "Dummy",
|
||||
Port: "Simulation",
|
||||
Baud: 9600,
|
||||
},
|
||||
Modules: []ADS1115Module{
|
||||
{
|
||||
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]
|
||||
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]]
|
||||
address = "0x48"
|
||||
labels = ["Hauptstrom 13.8V", "Logikbus 5V", "Relais-Schiene", "Rauschen (Frei)"]
|
||||
|
|
|
|||
Loading…
Reference in a new issue