remote-shack/internal/driver/yaesu_ft897.go

251 lines
6.1 KiB
Go

package driver
import (
"fmt"
"io"
"sync"
"time"
"github.com/tarm/serial"
)
// 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
cachedMode string // Interner RAM-Puffer für die Betriebsart
}
// NewFT897Driver initialisiert die Instanz und setzt den Standardmodus auf USB.
func NewFT897Driver() *FT897Driver {
return &FT897Driver{
cachedMode: "USB",
}
}
// Connect öffnet die serielle Schnittstelle mit optimierten Timeouts.
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")
}
// 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 * 100,
}
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 sauber.
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
}
// 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
}
}
}
// SetFrequency sendet die neue Frequenz im korrekten BCD-Format.
func (d *FT897Driver) SetFrequency(hz int64) error {
d.mu.Lock()
defer d.mu.Unlock()
if d.port == nil {
return fmt.Errorf("transceiver nicht verbunden")
}
// 1. Altlasten aus dem seriellen Puffer spülen
d.clearSerialBuffer()
// 2. Skalierung auf 10-Hz-Schritte des Yaesu
remain := hz / 10
// 3. BCD packen
var bcd [4]byte
for i := 3; i >= 0; i-- {
digit1 := remain % 10
remain /= 10
digit2 := remain % 10
remain /= 10
bcd[i] = byte((digit2 << 4) | digit1)
}
cmd := []byte{bcd[0], bcd[1], bcd[2], bcd[3], 0x01}
if _, err := d.port.Write(cmd); err != nil {
return fmt.Errorf("fehler beim senden des set-frequency befehls: %w", err)
}
return nil
}
// 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()
if d.port == nil {
return fmt.Errorf("transceiver nicht verbunden")
}
// Puffer vor dem Schreiben reinigen
d.clearSerialBuffer()
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
default: return fmt.Errorf("modus %s wird vom ft897-treiber nicht unterstützt", mode)
}
cmd := []byte{modeByte, 0x00, 0x00, 0x00, 0x07}
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
}
// 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()
if d.port == nil {
return fmt.Errorf("transceiver nicht verbunden")
}
d.clearSerialBuffer()
opcode := byte(0x08)
if !active {
opcode = 0x88
}
cmd := []byte{0x00, 0x00, 0x00, 0x00, opcode}
_, err := d.port.Write(cmd)
return err
}
// GetSMeter fragt die Signalstärke 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")
}
// 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, fmt.Errorf("fehler beim lesen des s-meter-bytes: %w", err)
}
rawStrength := int(buf[0] & 0x0F)
sMeterValue := (rawStrength * 9) / 15
return sMeterValue, nil
}