diff --git a/app/clients/client-relais b/app/clients/client-relais index 5e89d2c..37b3833 100755 Binary files a/app/clients/client-relais and b/app/clients/client-relais differ diff --git a/app/clients/client-rig b/app/clients/client-rig index be37e1a..77815ec 100755 Binary files a/app/clients/client-rig and b/app/clients/client-rig differ diff --git a/app/clients/client-rotor b/app/clients/client-rotor index c3a93b0..1d7255f 100755 Binary files a/app/clients/client-rotor and b/app/clients/client-rotor differ diff --git a/app/clients/client-volt b/app/clients/client-volt index 7d26b79..918d094 100755 Binary files a/app/clients/client-volt and b/app/clients/client-volt differ diff --git a/app/server/shack-server b/app/server/shack-server index 0a96bad..8a30487 100755 Binary files a/app/server/shack-server and b/app/server/shack-server differ diff --git a/client-config.toml b/client-config.toml index 2d07f7b..f8a5151 100644 --- a/client-config.toml +++ b/client-config.toml @@ -6,5 +6,42 @@ refresh_rate_ms = 250 [client.rotor] type = "G-5500" 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 + +# --- 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 diff --git a/cmd/client-rig/main.go b/cmd/client-rig/main.go index d926f33..3b86179 100644 --- a/cmd/client-rig/main.go +++ b/cmd/client-rig/main.go @@ -107,17 +107,19 @@ func main() { } 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{ grpcClient: shack.NewRigServiceClient(conn), stationName: cfg.Client.StationName, @@ -127,7 +129,7 @@ func main() { opBind: binding.NewString(), currentFreq: 14074000, currentMode: "USB", - configBands: sdrBands, + configBands: sdrBands, // Die aus der TOML generierten Bänder werden hier übergeben } myApp := app.NewWithID("de.remote-shack.client.rig") diff --git a/cmd/server/main.go b/cmd/server/main.go index 091bcd8..1479ad2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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 { + // Der Ticker für das Streaming an die GUI bleibt flüssig bei 250ms... ticker := time.NewTicker(250 * time.Millisecond) defer ticker.Stop() + + var counter int + var lastFreq int64 + var lastMode string + var lastSMeter int + for { select { case <-stream.Context().Done(): return nil case <-ticker.C: - f, _ := s.rig.GetFrequency() - m, _ := s.rig.GetMode() - s_meter, _ := s.rig.GetSMeter() - + counter++ + + // 1. FREQUENZ- & MODUS-ABFRAGE (Alle 500ms = jeden 2. Takt) + // Das schont die Auslastung der internen Transceiver-CPU im Leerlauf. + if counter%2 == 0 { + if f, err := s.rig.GetFrequency(); err == nil { + lastFreq = f + } + if m, err := s.rig.GetMode(); err == nil { + lastMode = m + } + } + + // 2. S-METER-ABFRAGE (Alle 1000ms = jeden 4. Takt) + // KRITISCH: Die S-Meter-Abfrage (0xE7) verursacht das stärkste NF-Knacken. + // Durch die Reduzierung auf 1 Sekunde wird das Geräusch im normalen Betrieb unhörbar + // oder tritt nur noch extrem selten auf. + if counter%4 == 0 { + if sm, err := s.rig.GetSMeter(); err == nil { + lastSMeter = sm + } + counter = 0 // Zähler zurücksetzen, um Overflows zu verhindern + } + + // 3. Rechteinhaber ermitteln s.mu.Lock() opToken := s.activeOperator opName := "FREI" @@ -224,17 +255,18 @@ func (s *ShackServer) MonitorSession(req *api.Empty, stream api.RigService_Monit } s.mu.Unlock() - // Übertrage den verifizierten Betreibernamen an das SDR-Display des Clients + // 4. Daten an die Fyne-GUI senden _ = stream.Send(&api.RigStatus{ - FrequencyHz: f, - Mode: m, - SMeter: int32(s_meter), + FrequencyHz: lastFreq, + Mode: lastMode, + SMeter: int32(lastSMeter), ActiveOperatorName: opName, }) } } } + // --- ROTOR SERVICE --- func (s *ShackServer) TargetPosition(ctx context.Context, req *api.RotorTarget) (*api.Empty, error) { token := s.getVirtualOperator(ctx) diff --git a/internal/config/config.go b/internal/config/config.go index 7cd849d..6546804 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,66 +3,90 @@ package config import ( + "fmt" "os" "github.com/BurntSushi/toml" ) +// ============================================================================ +// SERVER CONFIGURATION STRUCTURES +// ============================================================================ + // ADS1115Module definiert die Hardware-Parameter und Kanal-Labels einer ADC-Platine. +// Wird im Server zur reinen Shack-Betriebsspannungs-Telemetrie genutzt. type ADS1115Module struct { - Address string `toml:"address"` - Labels []string `toml:"labels"` // Genau 4 Beschriftungen für die Kanäle A0 bis A3 + Address string `toml:"address"` // I2C-Hardwareadresse (z.B. "0x48" oder "0x49") + 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. 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 + Type string `toml:"type"` // Hardware-Modell (z.B. "FT897") + Port string `toml:"port"` // Serieller Systempfad (z.B. "/dev/ttyUSB0") + Baud int `toml:"baud"` // Übertragungsgeschwindigkeit (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 + Type string `toml:"type"` // Hardware-Modell (z.B. "ProSistel" oder "SPID") + Port string `toml:"port"` // Serieller Systempfad (z.B. "/dev/ttyUSB1") + Baud int `toml:"baud"` // Übertragungsgeschwindigkeit (z.B. 9600) } // ServerSettings definiert die Netzwerkparameter des Server-Daemons. +// Beinhaltet alle physischen Hardware-Sektionen auf Server-Ebene. type ServerSettings struct { - ListenAddress string `toml:"listen_address"` - 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 + ListenAddress string `toml:"listen_address"` // Netzwerk-Bindung (z.B. "0.0.0.0:50051") + HardwareRig RigHardwareSettings `toml:"hardware_rig"` // Parameter für den Yaesu-Treiber + HardwareRotor RotorHardwareSettings `toml:"hardware_rotor"` // Parameter für den ProSistel/SPID-Treiber + 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 { 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 string `toml:"type"` // Z.B. "G-5500", "Spid", "Sat-Rotor" - Port string `toml:"port"` // Z.B. "/dev/ttyUSB0" oder "I2C" - HomeAzimuth float64 `toml:"home_azimuth"` // Standard-Richtung für Azimut (z.B. 180.0 für Süd) - HomeElevation float64 `toml:"home_elevation"` // Standard-Richtung für Elevation (z.B. 0.0) + Type string `toml:"type"` // Gewünschter Typ des Rotors (z.B. "G-5500") + Port string `toml:"port"` // Lokaler Client-Port oder virtueller Pfad + HomeAzimuth float64 `toml:"home_azimuth"` // Standard-Ausrichtung für Azimut in Grad + HomeElevation float64 `toml:"home_elevation"` // Standard-Ausrichtung für Elevation in Grad } // ClientSettings definiert die Verbindungsparameter für die Fyne-GUIs. type ClientSettings struct { - ServerAddress string `toml:"server_address"` - StationName string `toml:"station_name"` - RefreshRateMs int `toml:"refresh_rate_ms"` - Rotor RotorSettings `toml:"rotor"` // Rotorspezifische Einstellungen + ServerAddress string `toml:"server_address"` // IP des gRPC-Servers (z.B. Tailscale-IP) + StationName string `toml:"station_name"` // Eigenes Rufzeichen (z.B. "DL1ABC") + RefreshRateMs int `toml:"refresh_rate_ms"` // UI-Intervall für Status-Updates + 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 { Client ClientSettings `toml:"client"` } +// ============================================================================ +// CONFIG LOADER FUNCTIONS +// ============================================================================ + // 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) { + // Erzeugung des Default-Server-Gerüsts (Werkseinstellungen) cfg := &ServerConfig{ Server: ServerSettings{ ListenAddress: "0.0.0.0:50051", @@ -79,25 +103,30 @@ func LoadServerConfig(path string) (*ServerConfig, error) { Modules: []ADS1115Module{ { 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) { return cfg, nil } + // Datei einlesen und parsen 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 } // 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) { + // Erzeugung des Default-Client-Gerüsts inklusive aller klassischen Bänder cfg := &ClientConfig{ Client: ClientSettings{ ServerAddress: "127.0.0.1:50051", @@ -109,15 +138,43 @@ func LoadClientConfig(path string) (*ClientConfig, error) { HomeAzimuth: 180.0, // Standardmäßig Süden 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) { return cfg, nil } + // TOML-Datei dekodieren und in unsere Struktur parsen 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 diff --git a/internal/driver/yaesu_ft897.go b/internal/driver/yaesu_ft897.go index 14c7bbd..91d68bc 100644 --- a/internal/driver/yaesu_ft897.go +++ b/internal/driver/yaesu_ft897.go @@ -9,18 +9,22 @@ import ( "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 { - mu sync.Mutex - port io.ReadWriteCloser + mu sync.Mutex + 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 { - 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 { d.mu.Lock() defer d.mu.Unlock() @@ -29,10 +33,12 @@ func (d *FT897Driver) Connect(portPath string, baud int) error { 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{ Name: portPath, Baud: baud, - ReadTimeout: time.Millisecond * 300, // Schnelles Timeout für flüssiges VFO-Streaming + ReadTimeout: time.Millisecond * 100, } sp, err := serial.OpenPort(config) @@ -44,7 +50,7 @@ func (d *FT897Driver) Connect(portPath string, baud int) error { return nil } -// Disconnect schließt das serielle Gerät. +// Disconnect schließt das serielle Gerät sauber. func (d *FT897Driver) Disconnect() error { d.mu.Lock() defer d.mu.Unlock() @@ -58,93 +64,48 @@ func (d *FT897Driver) Disconnect() error { 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") +// clearSerialBuffer liest alle im OS-Puffer festsitzenden Alt-Bytes ungelesen weg. +// Dies verhindert effektiv, dass alte Datenpakete nachfolgende Antworten asynchron verschieben. +func (d *FT897Driver) clearSerialBuffer() { + // Da tarm/serial im non-blocking/timeout Modus arbeitet, lesen wir in einer + // kurzen Schleife so lange, bis keine Daten mehr anstehen (Timeout schlägt an). + dummy := make([]byte, 32) + for { + // Nutze ein extrem kurzes Zeitfenster, um nicht spürbar zu blockieren + n, _ := d.port.Read(dummy) + if n == 0 { + 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. -// 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. +// SetFrequency sendet die neue Frequenz im korrekten BCD-Format. 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). + // 1. Altlasten aus dem seriellen Puffer spülen + d.clearSerialBuffer() + + // 2. Skalierung auf 10-Hz-Schritte des Yaesu remain := hz / 10 - // 3. BCD-Verschlüsselung (Packen der Ziffern von hinten nach vorne) + // 3. BCD packen 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) } @@ -152,7 +113,59 @@ func (d *FT897Driver) SetFrequency(hz int64) error { 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 { d.mu.Lock() defer d.mu.Unlock() @@ -161,6 +174,9 @@ func (d *FT897Driver) SetMode(mode string) error { return fmt.Errorf("transceiver nicht verbunden") } + // Puffer vor dem Schreiben reinigen + d.clearSerialBuffer() + var modeByte byte switch mode { case "LSB": modeByte = 0x00 @@ -168,45 +184,26 @@ func (d *FT897Driver) SetMode(mode string) error { case "CW": modeByte = 0x02 case "AM": modeByte = 0x04 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) } cmd := []byte{modeByte, 0x00, 0x00, 0x00, 0x07} - _, err := d.port.Write(cmd) - return err + if _, err := d.port.Write(cmd); err != nil { + 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. -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). +// SetPTT schaltet den Sender (Opcode 0x08) oder Empfänger (Opcode 0x88). func (d *FT897Driver) SetPTT(active bool) error { d.mu.Lock() defer d.mu.Unlock() @@ -215,6 +212,8 @@ func (d *FT897Driver) SetPTT(active bool) error { return fmt.Errorf("transceiver nicht verbunden") } + d.clearSerialBuffer() + opcode := byte(0x08) if !active { opcode = 0x88 @@ -225,7 +224,7 @@ func (d *FT897Driver) SetPTT(active bool) error { 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) { d.mu.Lock() defer d.mu.Unlock() @@ -234,16 +233,18 @@ func (d *FT897Driver) GetSMeter() (int, error) { 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 { return 0, err } buf := make([]byte, 1) 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) sMeterValue := (rawStrength * 9) / 15 return sMeterValue, nil