remote-shack/internal/driver/yaesu_ft897.go

250 lines
7 KiB
Go

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
}