2026-06-18 client-rig Modus-Schaltflächen funktionieren
This commit is contained in:
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.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue