Compare commits

...

2 commits

Author SHA1 Message Date
Dieter Lang
cd84ea8439 2026-08-04 09:15 remote-shack inclusive Spannungs-Modul 2026-08-04 09:16:10 +02:00
Dieter Lang
4ec2d7c9a9 2026-06-18 client-rig Modus-Schaltflächen funktionieren 2026-06-18 17:55:09 +02:00
13 changed files with 356 additions and 192 deletions

View file

@ -82,45 +82,41 @@ go mod tidy
weitermachen:
==============================================================================
PROJEKT-MERKER: REMOTE-SHACK START AM MORGEN
PROJEKT-MERKER: REMOTE-SHACK START AM MORGEN - NEXT LEVEL RIG-CONTROL
==============================================================================
AKTUELLER STATUS (HEUTE ERREICHT):
----------------------------------
* Yaesu FT-897 Treiber: Vollständig fehlerfrei implementiert. Die 9-stellige
BCD-Arithmetik läuft in beide Richtungen bitgenau synchron (inkl. Breitband
wie 90,700 MHz).
* Audio-Klicken eliminiert: Durch die Entlastung des gRPC-Monitor-Tickers
(Frequenz alle 500ms, S-Meter alle 1000ms) ist das NF-Knacken im
Lautsprecher des Transceivers vollständig verschwunden.
* Puffer-Sicherheit: Die neue Methode 'clearSerialBuffer()' flusht das
Linux-TTY vor jeder Interaktion und verhindert asynchrone Bit-Verschiebungen.
* Dynamische Bänder: Die Amateurfunkbänder wurden komplett aus dem Go-Code
in die 'client-config.toml' ausgelagert. Die 'internal/config/config.go'
liest diese inklusive automatischem Werkseinstellungs-Fallback fehlerfrei ein.
AKTUELLER STATUS:
-----------------
* SDR-Rig-Client: Vollständig auf ein modernes SDR-Design umgestellt.
Die Frequenzanzeige ist massiv vergrößert (canvas.Text, Größe 36, Monospace)
und durch unverschiebbare Tausender-Punkte (014.250.000) in MHz-, kHz-
und Hz-Blöcke strukturiert. Die Bandtasten werden dynamisch geladen.
* Sat-Rotor-Client: Geometrisch entkoppelt im 300x300-Format. Elevation
wurde für Erdverbindungen/Troposcatter auf -20° bis 90° erweitert. Der
Kreismittelpunkt wurde auf Y=200 angehoben, um Platz nach unten zu
schaffen. Ein neuer -15°-Bogen-Button ist integriert. Die originale
tokenbasierte gRPC-Schreib-Validierung via TargetPosition ist lückenlos aktiv.
* Kompilierung: Das build.sh-Skript ist repariert und berücksichtigt
CGO_ENABLED=1. Alle Komponenten bauen fehlerfrei direkt in die
Zielordner (./app/server/ und ./app/clients/).
* Architektur-Entscheidung: Wechsel auf ein flexibles Connector-Modell
(Abstraktionsschicht analog zu Hamlib). Der gRPC-Server spricht nur noch
mit generischen Interfaces. Die Hardware (ProSistel, SPID etc.) wird rein
über die TOML-Konfigurationsdateien eingekoppelt, ohne den Core-Code zu verändern.
NÄCHSTE SCHRITTE FÜR MORGEN:
----------------------------
1. Erweiterung der Client-GUI (SDR-Erscheinungsbild maximieren):
- Einbau von farbigen Aktivitäts-Indikatoren für die Betriebsarten-Buttons
(z. B. der gerade aktive Modus leuchtet in sattem Grün oder Blau,
während die inaktiven Buttons im Standard-Design verbleiben).
- Integration einer visuellen S-Meter-Balkenanzeige (Fortschrittsbalken/
ProgressBar), um die vom Server gelieferten Werte (S0-S9) grafisch
darzustellen, anstatt nur als Text.
NÄCHSTE SCHRITTE:
-----------------
1. Implementierung des konkreten ProSistel-Seriell-Protokolls im neuen
Connector unter internal/driver/.
2. Vorbereitung des SPID-Rotor-Connectors für das Gerät des Bekannten.
3. Einbindung des ADS1115-I2C-Treibers im Server für die reine Shack-
Betriebsspannungs-Telemetrie (Entkoppelt von der Rotor-Grad-Messung).
2. Integration neuer CAT-Komfortfunktionen im Treiber & GUI:
- VFO A/B Umschaltung (Opcode 0x85) & Split-Betrieb (Opcode 0x02/0x82).
- Repeater-Shift (Ablagerichtung Minus/Plus/Simplex für Relais via Opcode 0x09).
- CTCSS/DCS Subaudioton-Steuerung für den VHF/UHF-Relaisfunk.
BEREITZUSTELLENDE DATEIEN FÜR DEN START:
----------------------------------------
Bitte lade zu Beginn des neuen Chats diese Dateien hoch, um die Code-Basis
direkt abzugleichen:
1. internal/driver/interfaces.go (Das aktuelle Interface-Gerüst)
2. server-config.toml & client-config.toml (Für die neuen Hardware-Felder)
3. Technische Infos zum ProSistel-Protokoll (Befehlssatz für Az/El über Seriell)
3. Danach: Weitermarsch zum SPID-Rotor-Connector (Protokoll-Klärung).
BEREITZUSTELLENDE DATEIEN FÜR DEN START MORGEN:
----------------------------------------------
* internal/driver/yaesu_ft897.go (Der aktuell perfekt flushende Treiber)
* server/main.go & client-rig/main.go (Für die UI- und gRPC-Erweiterungen)
==============================================================================

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -30,3 +30,42 @@ go build -o ./app/clients/client-volt ./cmd/client-volt/main.go
find ./app -type f -exec chmod +x {} +
echo "✅ Alle Binaries erfolgreich in ./app/ erstellt."
# 3. Die Service-Datei universell generiert (nutzt systemd %h/%u Specifiers)
cat << 'EOF' > ./app/spannung.service
# ==============================================================================
# KURZANLEITUNG ZUR INSTALLATION UND VERWALTUNG
# ==============================================================================
# 1. Datei kopieren nach: /etc/systemd/system/spannung.service
# 2. Systemd neu laden: sudo systemctl daemon-reload
# 3. Autostart aktivieren: sudo systemctl enable spannung.service
# 4. Dienst starten: sudo systemctl start spannung.service
# ==============================================================================
[Unit]
Description=Spannung Control Service
After=network-online.target i2c.service
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
ExecStartPre=/bin/bash -c 'for i in {1..10}; do i2cdetect -y 1 | grep -E "^40:" | grep -q -E "\b48\b" && exit 0; echo "warte auf ADS1115..."; sleep 2; done; exit 1'
ExecStart=%h/spannung/app/spannung-arm32_7
WorkingDirectory=%h/spannung/app
User=%u
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=60s
StartLimitBurst=3
StandardOutput=append:%h/spannung/app/spannung.log
StandardError=append:%h/spannung/app/spannung.log
[Install]
WantedBy=multi-user.target
EOF
echo "📝 Service-Datei universell generiert (nutzt systemd %h/%u Specifiers)"
echo "📂 Inhalt des fertigen app-Verzeichnisses:"
ls -la app

View file

@ -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

View file

@ -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")

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 {
// 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)

View file

@ -20,7 +20,7 @@ ssh -t "$PI_USER@$PI_HOST" "
REAL_PATH=\$(pwd)
# Pfade in Service-Files fixen
sed -i \"s|TARGET_HOME_DIR/remote-shack/app/server|\$REAL_PATH|g\" ./*.service
#sed -i \"s|TARGET_HOME_DIR/remote-shack/app/server|\$REAL_PATH|g\" ./*.service
sudo cp ./*.service /etc/systemd/system/
sudo systemctl daemon-reload

View file

@ -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

View file

@ -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