Compare commits
2 commits
f82f9bde86
...
cd84ea8439
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd84ea8439 | ||
|
|
4ec2d7c9a9 |
13 changed files with 356 additions and 192 deletions
70
Notizen.txt
70
Notizen.txt
|
|
@ -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.
39
build.sh
39
build.sh
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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