2026-06-18 client-rig Modus-Schaltflächen funktionieren

This commit is contained in:
Dieter Lang 2026-06-18 17:55:09 +02:00
parent f82f9bde86
commit 4ec2d7c9a9
10 changed files with 283 additions and 154 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -6,5 +6,42 @@ refresh_rate_ms = 250
[client.rotor] [client.rotor]
type = "G-5500" type = "G-5500"
port = "/dev/ttyUSB0" port = "/dev/ttyUSB0"
home_azimuth = 225.0 # Deine bevorzugte Home-Richtung (z.B. Süd-West) home_azimuth = 225.0 # Deine bevorzugte Home-Richtung (Süd-West)
home_elevation = 15.0 # Deine bevorzugte Home-Elevation home_elevation = 15.0 # Deine bevorzugte Home-Elevation
# --- DYNAMISCHE AMATEURFUNKBÄNDER FÜR DIE SCHNELLWAHLTASTEN ---
[[client.bands]]
name = "160m"
frequency = 1840000
[[client.bands]]
name = "80m"
frequency = 3600000
[[client.bands]]
name = "40m"
frequency = 7074000
[[client.bands]]
name = "20m"
frequency = 14074000
[[client.bands]]
name = "15m"
frequency = 21074000
[[client.bands]]
name = "10m"
frequency = 28074000
[[client.bands]]
name = "2m"
frequency = 145600000
[[client.bands]]
name = "70cm"
frequency = 433500000
[[client.bands]]
name = "FM-Radio"
frequency = 90700000

View file

@ -107,17 +107,19 @@ func main() {
} }
defer conn.Close() defer conn.Close()
sdrBands := []LocalBandConfig{
{Name: "160m", Frequency: 1840000},
{Name: "80m", Frequency: 3600000},
{Name: "40m", Frequency: 7074000},
{Name: "20m", Frequency: 14074000},
{Name: "15m", Frequency: 21074000},
{Name: "10m", Frequency: 28074000},
{Name: "2m", Frequency: 14550000},
{Name: "70cm", Frequency: 433500000},
}
// 1. Konvertierung der TOML-Bänder in die interne GUI-Struktur.
// Wir lesen die geladene Liste dynamisch aus cfg.Client.Bands aus.
var sdrBands []LocalBandConfig
for _, b := range cfg.Client.Bands {
sdrBands = append(sdrBands, LocalBandConfig{
Name: b.Name,
Frequency: b.Frequency,
})
}
log.Printf("[UI-Init] %d Frequenzbänder erfolgreich aus Konfiguration geladen.", len(sdrBands))
// 2. Instanziierung des GUI-Zustands mit den dynamischen Bändern
gui := &RigClientGUI{ gui := &RigClientGUI{
grpcClient: shack.NewRigServiceClient(conn), grpcClient: shack.NewRigServiceClient(conn),
stationName: cfg.Client.StationName, stationName: cfg.Client.StationName,
@ -127,7 +129,7 @@ func main() {
opBind: binding.NewString(), opBind: binding.NewString(),
currentFreq: 14074000, currentFreq: 14074000,
currentMode: "USB", currentMode: "USB",
configBands: sdrBands, configBands: sdrBands, // Die aus der TOML generierten Bänder werden hier übergeben
} }
myApp := app.NewWithID("de.remote-shack.client.rig") myApp := app.NewWithID("de.remote-shack.client.rig")

View file

@ -204,18 +204,49 @@ func (s *ShackServer) OperatorSession(stream api.RigService_OperatorSessionServe
} }
} }
// 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 { 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) ticker := time.NewTicker(250 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
var counter int
var lastFreq int64
var lastMode string
var lastSMeter int
for { for {
select { select {
case <-stream.Context().Done(): case <-stream.Context().Done():
return nil return nil
case <-ticker.C: case <-ticker.C:
f, _ := s.rig.GetFrequency() counter++
m, _ := s.rig.GetMode()
s_meter, _ := s.rig.GetSMeter() // 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() s.mu.Lock()
opToken := s.activeOperator opToken := s.activeOperator
opName := "FREI" opName := "FREI"
@ -224,17 +255,18 @@ func (s *ShackServer) MonitorSession(req *api.Empty, stream api.RigService_Monit
} }
s.mu.Unlock() s.mu.Unlock()
// Übertrage den verifizierten Betreibernamen an das SDR-Display des Clients // 4. Daten an die Fyne-GUI senden
_ = stream.Send(&api.RigStatus{ _ = stream.Send(&api.RigStatus{
FrequencyHz: f, FrequencyHz: lastFreq,
Mode: m, Mode: lastMode,
SMeter: int32(s_meter), SMeter: int32(lastSMeter),
ActiveOperatorName: opName, ActiveOperatorName: opName,
}) })
} }
} }
} }
// --- ROTOR SERVICE --- // --- ROTOR SERVICE ---
func (s *ShackServer) TargetPosition(ctx context.Context, req *api.RotorTarget) (*api.Empty, error) { func (s *ShackServer) TargetPosition(ctx context.Context, req *api.RotorTarget) (*api.Empty, error) {
token := s.getVirtualOperator(ctx) token := s.getVirtualOperator(ctx)

View file

@ -3,66 +3,90 @@
package config package config
import ( import (
"fmt"
"os" "os"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
// ============================================================================
// SERVER CONFIGURATION STRUCTURES
// ============================================================================
// ADS1115Module definiert die Hardware-Parameter und Kanal-Labels einer ADC-Platine. // ADS1115Module definiert die Hardware-Parameter und Kanal-Labels einer ADC-Platine.
// Wird im Server zur reinen Shack-Betriebsspannungs-Telemetrie genutzt.
type ADS1115Module struct { type ADS1115Module struct {
Address string `toml:"address"` Address string `toml:"address"` // I2C-Hardwareadresse (z.B. "0x48" oder "0x49")
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 Analog-Kanäle A0 bis A3
} }
// RigHardwareSettings definiert die Parameter für das CAT-Interface des Transceivers. // RigHardwareSettings definiert die Parameter für das CAT-Interface des Transceivers.
type RigHardwareSettings struct { type RigHardwareSettings struct {
Type string `toml:"type"` // Z.B. "FT897" Type string `toml:"type"` // Hardware-Modell (z.B. "FT897")
Port string `toml:"port"` // Z.B. "/dev/ttyUSB0" Port string `toml:"port"` // Serieller Systempfad (z.B. "/dev/ttyUSB0")
Baud int `toml:"baud"` // Z.B. 38400 Baud int `toml:"baud"` // Übertragungsgeschwindigkeit (z.B. 38400)
} }
// RotorHardwareSettings definiert die Parameter für das serielle Protokoll des Rotors. // RotorHardwareSettings definiert die Parameter für das serielle Protokoll des Rotors.
type RotorHardwareSettings struct { type RotorHardwareSettings struct {
Type string `toml:"type"` // Z.B. "ProSistel" oder "SPID" Type string `toml:"type"` // Hardware-Modell (z.B. "ProSistel" oder "SPID")
Port string `toml:"port"` // Z.B. "/dev/ttyUSB1" Port string `toml:"port"` // Serieller Systempfad (z.B. "/dev/ttyUSB1")
Baud int `toml:"baud"` // Z.B. 9600 Baud int `toml:"baud"` // Übertragungsgeschwindigkeit (z.B. 9600)
} }
// ServerSettings definiert die Netzwerkparameter des Server-Daemons. // ServerSettings definiert die Netzwerkparameter des Server-Daemons.
// Beinhaltet alle physischen Hardware-Sektionen auf Server-Ebene.
type ServerSettings struct { type ServerSettings struct {
ListenAddress string `toml:"listen_address"` ListenAddress string `toml:"listen_address"` // Netzwerk-Bindung (z.B. "0.0.0.0:50051")
HardwareRig RigHardwareSettings `toml:"hardware_rig"` // ERWEITERUNG für Yaesu HardwareRig RigHardwareSettings `toml:"hardware_rig"` // Parameter für den Yaesu-Treiber
HardwareRotor RotorHardwareSettings `toml:"hardware_rotor"` // ERWEITERUNG für ProSistel/SPID HardwareRotor RotorHardwareSettings `toml:"hardware_rotor"` // Parameter für den ProSistel/SPID-Treiber
Modules []ADS1115Module `toml:"modules"` // Korrekt innerhalb der server-Sektion platziert Modules []ADS1115Module `toml:"modules"` // Array für angeschlossene I2C-Spannungssensoren
} }
// ServerConfig bündelt alle Konfigurationseinstellungen des Servers. // ServerConfig bündelt alle Konfigurationseinstellungen des Servers als Root-Struktur.
type ServerConfig struct { type ServerConfig struct {
Server ServerSettings `toml:"server"` Server ServerSettings `toml:"server"`
} }
// RotorSettings definiert die hardware- und systemspezifischen Parameter eines Rotors (Client-Sicht). // ============================================================================
// CLIENT CONFIGURATION STRUCTURES
// ============================================================================
// BandSettings definiert die Struktur für ein einzelnes Frequenzband in der TOML.
type BandSettings struct {
Name string `toml:"name"` // Beschriftung des Buttons in der GUI (z.B. "2m")
Frequency int64 `toml:"frequency"` // Exakte Frequenz in Hertz (Hz)
}
// RotorSettings definiert die hardware- und systemspezifischen Parameter eines Rotors aus Client-Sicht.
type RotorSettings struct { type RotorSettings struct {
Type string `toml:"type"` // Z.B. "G-5500", "Spid", "Sat-Rotor" Type string `toml:"type"` // Gewünschter Typ des Rotors (z.B. "G-5500")
Port string `toml:"port"` // Z.B. "/dev/ttyUSB0" oder "I2C" Port string `toml:"port"` // Lokaler Client-Port oder virtueller Pfad
HomeAzimuth float64 `toml:"home_azimuth"` // Standard-Richtung für Azimut (z.B. 180.0 für Süd) HomeAzimuth float64 `toml:"home_azimuth"` // Standard-Ausrichtung für Azimut in Grad
HomeElevation float64 `toml:"home_elevation"` // Standard-Richtung für Elevation (z.B. 0.0) HomeElevation float64 `toml:"home_elevation"` // Standard-Ausrichtung für Elevation in Grad
} }
// ClientSettings definiert die Verbindungsparameter für die Fyne-GUIs. // ClientSettings definiert die Verbindungsparameter für die Fyne-GUIs.
type ClientSettings struct { type ClientSettings struct {
ServerAddress string `toml:"server_address"` ServerAddress string `toml:"server_address"` // IP des gRPC-Servers (z.B. Tailscale-IP)
StationName string `toml:"station_name"` StationName string `toml:"station_name"` // Eigenes Rufzeichen (z.B. "DL1ABC")
RefreshRateMs int `toml:"refresh_rate_ms"` RefreshRateMs int `toml:"refresh_rate_ms"` // UI-Intervall für Status-Updates
Rotor RotorSettings `toml:"rotor"` // Rotorspezifische Einstellungen Rotor RotorSettings `toml:"rotor"` // Rotorspezifische Client-Einstellungen
Bands []BandSettings `toml:"bands"` // Dynamische Bandliste für die SDR-Schnellwahltasten
} }
// ClientConfig bündelt alle Konfigurationseinstellungen des GUI-Clients. // ClientConfig bündelt alle Konfigurationseinstellungen des GUI-Clients als Root-Struktur.
type ClientConfig struct { type ClientConfig struct {
Client ClientSettings `toml:"client"` Client ClientSettings `toml:"client"`
} }
// ============================================================================
// CONFIG LOADER FUNCTIONS
// ============================================================================
// LoadServerConfig lädt die Konfiguration für den Server-Daemon aus einer TOML-Datei. // LoadServerConfig lädt die Konfiguration für den Server-Daemon aus einer TOML-Datei.
// Falls die Datei fehlt, werden sichere Simulations-Dummys als Rückfallebene geladen.
func LoadServerConfig(path string) (*ServerConfig, error) { func LoadServerConfig(path string) (*ServerConfig, error) {
// Erzeugung des Default-Server-Gerüsts (Werkseinstellungen)
cfg := &ServerConfig{ cfg := &ServerConfig{
Server: ServerSettings{ Server: ServerSettings{
ListenAddress: "0.0.0.0:50051", ListenAddress: "0.0.0.0:50051",
@ -79,25 +103,30 @@ func LoadServerConfig(path string) (*ServerConfig, error) {
Modules: []ADS1115Module{ Modules: []ADS1115Module{
{ {
Address: "0x48", Address: "0x48",
Labels: []string{"Kanal 1", "Kanal 2", "Kanal 3", "Kanal 4"}, Labels: []string{"Hauptstrom 13.8V", "Logikbus 5V", "Relais-Schiene", "Rauschen (Frei)"},
}, },
}, },
}, },
} }
// Falls keine Konfigurationsdatei vorhanden ist, nutzen wir direkt die Dummys
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return cfg, nil return cfg, nil
} }
// Datei einlesen und parsen
if _, err := toml.DecodeFile(path, cfg); err != nil { if _, err := toml.DecodeFile(path, cfg); err != nil {
return nil, err return nil, fmt.Errorf("fehler beim parsen der server-config.toml: %w", err)
} }
return cfg, nil return cfg, nil
} }
// LoadClientConfig lädt die Konfiguration für die GUI-Clients aus einer TOML-Datei. // LoadClientConfig lädt die Konfiguration für die GUI-Clients aus einer TOML-Datei.
// Falls die Datei nicht existiert oder keine Bänder definiert sind, werden die bekannten
// Standardbänder (Default AFU-Bänder) im Speicher erzeugt, um leere GUIs zu verhindern.
func LoadClientConfig(path string) (*ClientConfig, error) { func LoadClientConfig(path string) (*ClientConfig, error) {
// Erzeugung des Default-Client-Gerüsts inklusive aller klassischen Bänder
cfg := &ClientConfig{ cfg := &ClientConfig{
Client: ClientSettings{ Client: ClientSettings{
ServerAddress: "127.0.0.1:50051", ServerAddress: "127.0.0.1:50051",
@ -109,15 +138,43 @@ func LoadClientConfig(path string) (*ClientConfig, error) {
HomeAzimuth: 180.0, // Standardmäßig Süden HomeAzimuth: 180.0, // Standardmäßig Süden
HomeElevation: 0.0, // Standardmäßig Horizont HomeElevation: 0.0, // Standardmäßig Horizont
}, },
// Fallback-Bänder mit korrigierten, 9-stelligen Hertz-Angaben für das 2m/70cm Band
Bands: []BandSettings{
{Name: "160m", Frequency: 1840000},
{Name: "80m", Frequency: 3600000},
{Name: "40m", Frequency: 7074000},
{Name: "20m", Frequency: 14074000},
{Name: "15m", Frequency: 21074000},
{Name: "10m", Frequency: 28074000},
{Name: "2m", Frequency: 145500000}, // 145.500.000 Hz (VHF)
{Name: "70cm", Frequency: 433500000}, // 433.500.000 Hz (UHF)
},
}, },
} }
// Falls keine Konfigurationsdatei existiert, geben wir die Defaults direkt zurück
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return cfg, nil return cfg, nil
} }
// TOML-Datei dekodieren und in unsere Struktur parsen
if _, err := toml.DecodeFile(path, cfg); err != nil { if _, err := toml.DecodeFile(path, cfg); err != nil {
return nil, err return nil, fmt.Errorf("fehler beim parsen der client-config.toml: %w", err)
}
// Sicherheitsprüfung: Wenn die Datei existierte, aber die Sektion "bands" leer war,
// stellen wir die Standardbänder wieder her.
if len(cfg.Client.Bands) == 0 {
cfg.Client.Bands = []BandSettings{
{Name: "160m", Frequency: 1840000},
{Name: "80m", Frequency: 3600000},
{Name: "40m", Frequency: 7074000},
{Name: "20m", Frequency: 14074000},
{Name: "15m", Frequency: 21074000},
{Name: "10m", Frequency: 28074000},
{Name: "2m", Frequency: 145500000},
{Name: "70cm", Frequency: 433500000},
}
} }
return cfg, nil return cfg, nil

View file

@ -9,18 +9,22 @@ import (
"github.com/tarm/serial" "github.com/tarm/serial"
) )
// FT897Driver implementiert das RigDriver-Interface für den Yaesu FT-897 (CAT-Protokoll). // FT897Driver implementiert das RigDriver-Interface für den Yaesu FT-897.
// Er nutzt eine aggressive Puffer-Bereinigung, um Bit-Verschiebungen bei hoher Last zu verhindern.
type FT897Driver struct { type FT897Driver struct {
mu sync.Mutex mu sync.Mutex
port io.ReadWriteCloser port io.ReadWriteCloser
cachedMode string // Interner RAM-Puffer für die Betriebsart
} }
// NewFT897Driver initialisiert eine neue Instanz für das Yaesu-CAT-Protokoll. // NewFT897Driver initialisiert die Instanz und setzt den Standardmodus auf USB.
func NewFT897Driver() *FT897Driver { func NewFT897Driver() *FT897Driver {
return &FT897Driver{} return &FT897Driver{
cachedMode: "USB",
}
} }
// Connect öffnet die serielle Schnittstelle (z.B. FTDI-Kabel / CT-62 Ersatz). // Connect öffnet die serielle Schnittstelle mit optimierten Timeouts.
func (d *FT897Driver) Connect(portPath string, baud int) error { func (d *FT897Driver) Connect(portPath string, baud int) error {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
@ -29,10 +33,12 @@ func (d *FT897Driver) Connect(portPath string, baud int) error {
return fmt.Errorf("port bereits geöffnet") return fmt.Errorf("port bereits geöffnet")
} }
// Ein kürzeres Timeout von 100ms zwingt io.ReadFull dazu, bei unvollständigen
// Paketen sofort abzubrechen, anstatt das gesamte gRPC-System zu verlangsamen.
config := &serial.Config{ config := &serial.Config{
Name: portPath, Name: portPath,
Baud: baud, Baud: baud,
ReadTimeout: time.Millisecond * 300, // Schnelles Timeout für flüssiges VFO-Streaming ReadTimeout: time.Millisecond * 100,
} }
sp, err := serial.OpenPort(config) sp, err := serial.OpenPort(config)
@ -44,7 +50,7 @@ func (d *FT897Driver) Connect(portPath string, baud int) error {
return nil return nil
} }
// Disconnect schließt das serielle Gerät. // Disconnect schließt das serielle Gerät sauber.
func (d *FT897Driver) Disconnect() error { func (d *FT897Driver) Disconnect() error {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
@ -58,93 +64,48 @@ func (d *FT897Driver) Disconnect() error {
return err return err
} }
// GetFrequency liest die aktuelle Betriebsfrequenz des Funkgeräts aus. // clearSerialBuffer liest alle im OS-Puffer festsitzenden Alt-Bytes ungelesen weg.
// Der Yaesu FT-897 liefert bei der Statusabfrage (Opcode 0x03) ein 5-Byte Paket zurück. // Dies verhindert effektiv, dass alte Datenpakete nachfolgende Antworten asynchron verschieben.
// Die ersten 4 Bytes enthalten die Frequenz als BCD gepackt in 10-Hz-Schritten. func (d *FT897Driver) clearSerialBuffer() {
// Das 5. Byte enthält die aktuelle Betriebsart (Mode). // Da tarm/serial im non-blocking/timeout Modus arbeitet, lesen wir in einer
func (d *FT897Driver) GetFrequency() (int64, error) { // kurzen Schleife so lange, bis keine Daten mehr anstehen (Timeout schlägt an).
d.mu.Lock() dummy := make([]byte, 32)
defer d.mu.Unlock() for {
// Nutze ein extrem kurzes Zeitfenster, um nicht spürbar zu blockieren
// 1. Sicherheitshalt: Prüfen, ob die serielle Verbindung aktiv ist n, _ := d.port.Read(dummy)
if d.port == nil { if n == 0 {
return 0, fmt.Errorf("transceiver nicht verbunden") break // Puffer ist komplett leer und sauber
}
} }
// 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. // SetFrequency sendet die neue Frequenz im korrekten BCD-Format.
// 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 { func (d *FT897Driver) SetFrequency(hz int64) error {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
// 1. Sicherheitshalt: Verbindung prüfen
if d.port == nil { if d.port == nil {
return fmt.Errorf("transceiver nicht verbunden") return fmt.Errorf("transceiver nicht verbunden")
} }
// 2. SKALIERUNGS-ANPASSUNG: // 1. Altlasten aus dem seriellen Puffer spülen
// Da das CAT-Protokoll des Yaesu die Frequenz in 10-Hz-Schritten verarbeitet, d.clearSerialBuffer()
// 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). // 2. Skalierung auf 10-Hz-Schritte des Yaesu
remain := hz / 10 remain := hz / 10
// 3. BCD-Verschlüsselung (Packen der Ziffern von hinten nach vorne) // 3. BCD packen
var bcd [4]byte var bcd [4]byte
for i := 3; i >= 0; i-- { for i := 3; i >= 0; i-- {
// Isoliere die niederwertige Ziffer (Low-Nibble, z.B. 0)
digit1 := remain % 10 digit1 := remain % 10
remain /= 10 remain /= 10
// Isoliere die höherwertige Ziffer des Paars (High-Nibble, z.B. 0)
digit2 := remain % 10 digit2 := remain % 10
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) 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} 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 { if _, err := d.port.Write(cmd); err != nil {
return fmt.Errorf("fehler beim senden des set-frequency befehls: %w", err) return fmt.Errorf("fehler beim senden des set-frequency befehls: %w", err)
} }
@ -152,7 +113,59 @@ func (d *FT897Driver) SetFrequency(hz int64) error {
return nil return nil
} }
// SetMode schaltet das Modulationsrelais im FT-897 um (Opcode 0x07). // GetFrequency liest den 5-Byte Block aus und aktualisiert Frequenz und Modus zeitgleich.
func (d *FT897Driver) GetFrequency() (int64, error) {
d.mu.Lock()
defer d.mu.Unlock()
if d.port == nil {
return 0, fmt.Errorf("transceiver nicht verbunden")
}
// 1. ABSOLUT KRITISCH: Vor der Anfrage den Puffer flushen!
// Verhindert, dass wir Bytes aus der vorherigen Abfrage mitlesen.
d.clearSerialBuffer()
// 2. 5-Byte Status-Abfrage senden (Opcode 0x03)
if _, err := d.port.Write([]byte{0x00, 0x00, 0x00, 0x00, 0x03}); err != nil {
return 0, fmt.Errorf("fehler beim senden der statusabfrage: %w", err)
}
// 3. Den Block einlesen
buf := make([]byte, 5)
if _, err := io.ReadFull(d.port, buf); err != nil {
return 0, fmt.Errorf("cat-lesefehler (puffer asynchron): %w", err)
}
// 4. Frequenz aus Byte 0-3 rekonstruieren
var freq int64
for i := 0; i < 4; i++ {
high := int64(buf[i] >> 4)
low := int64(buf[i] & 0x0F)
freq = freq*100 + (high*10 + low)
}
freq = freq * 10
// 5. Modus aus Byte 4 isolieren und im Cache sichern
switch buf[4] & 0x0F {
case 0x00: d.cachedMode = "LSB"
case 0x01: d.cachedMode = "USB"
case 0x02: d.cachedMode = "CW"
case 0x04: d.cachedMode = "AM"
case 0x08: d.cachedMode = "FM"
}
return freq, nil
}
// GetMode liest kollisionsfrei die Betriebsart aus dem RAM-Cache.
func (d *FT897Driver) GetMode() (string, error) {
d.mu.Lock()
defer d.mu.Unlock()
return d.cachedMode, nil
}
// SetMode schaltet die Betriebsart um und aktualisiert sofort den Cache.
func (d *FT897Driver) SetMode(mode string) error { func (d *FT897Driver) SetMode(mode string) error {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
@ -161,6 +174,9 @@ func (d *FT897Driver) SetMode(mode string) error {
return fmt.Errorf("transceiver nicht verbunden") return fmt.Errorf("transceiver nicht verbunden")
} }
// Puffer vor dem Schreiben reinigen
d.clearSerialBuffer()
var modeByte byte var modeByte byte
switch mode { switch mode {
case "LSB": modeByte = 0x00 case "LSB": modeByte = 0x00
@ -168,45 +184,26 @@ func (d *FT897Driver) SetMode(mode string) error {
case "CW": modeByte = 0x02 case "CW": modeByte = 0x02
case "AM": modeByte = 0x04 case "AM": modeByte = 0x04
case "FM": modeByte = 0x08 case "FM": modeByte = 0x08
case "FT8": modeByte = 0x01 // FT8 nutzt technisch das USB-Seitenband case "FT8": modeByte = 0x01
default: return fmt.Errorf("modus %s wird vom ft897-treiber nicht unterstützt", mode) default: return fmt.Errorf("modus %s wird vom ft897-treiber nicht unterstützt", mode)
} }
cmd := []byte{modeByte, 0x00, 0x00, 0x00, 0x07} cmd := []byte{modeByte, 0x00, 0x00, 0x00, 0x07}
_, err := d.port.Write(cmd) if _, err := d.port.Write(cmd); err != nil {
return err return fmt.Errorf("fehler beim senden des modus-befehls: %w", err)
}
// Cache direkt updaten, um Rücksprünge zu verhindern
if mode == "FT8" {
d.cachedMode = "USB"
} else {
d.cachedMode = mode
}
return nil
} }
// GetMode holt das aktuelle Status-Byte vom Gerät. // SetPTT schaltet den Sender (Opcode 0x08) oder Empfänger (Opcode 0x88).
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 { func (d *FT897Driver) SetPTT(active bool) error {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
@ -215,6 +212,8 @@ func (d *FT897Driver) SetPTT(active bool) error {
return fmt.Errorf("transceiver nicht verbunden") return fmt.Errorf("transceiver nicht verbunden")
} }
d.clearSerialBuffer()
opcode := byte(0x08) opcode := byte(0x08)
if !active { if !active {
opcode = 0x88 opcode = 0x88
@ -225,7 +224,7 @@ func (d *FT897Driver) SetPTT(active bool) error {
return err return err
} }
// GetSMeter fragt das S-Meter über den internen ADC-Status ab (Opcode 0xE7). // GetSMeter fragt die Signalstärke ab (Opcode 0xE7).
func (d *FT897Driver) GetSMeter() (int, error) { func (d *FT897Driver) GetSMeter() (int, error) {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
@ -234,16 +233,18 @@ func (d *FT897Driver) GetSMeter() (int, error) {
return 0, fmt.Errorf("transceiver nicht verbunden") return 0, fmt.Errorf("transceiver nicht verbunden")
} }
// Vor der S-Meter-Abfrage Puffer reinigen
d.clearSerialBuffer()
if _, err := d.port.Write([]byte{0x00, 0x00, 0x00, 0x00, 0xE7}); err != nil { if _, err := d.port.Write([]byte{0x00, 0x00, 0x00, 0x00, 0xE7}); err != nil {
return 0, err return 0, err
} }
buf := make([]byte, 1) buf := make([]byte, 1)
if _, err := io.ReadFull(d.port, buf); err != nil { if _, err := io.ReadFull(d.port, buf); err != nil {
return 0, err return 0, fmt.Errorf("fehler beim lesen des s-meter-bytes: %w", err)
} }
// KORREKTUR: Zugriff auf buf[0] statt auf das gesamte Slice
rawStrength := int(buf[0] & 0x0F) rawStrength := int(buf[0] & 0x0F)
sMeterValue := (rawStrength * 9) / 15 sMeterValue := (rawStrength * 9) / 15
return sMeterValue, nil return sMeterValue, nil