2026-06-13 20:00 GUI für Rotorsteuerung eingerichtet
This commit is contained in:
parent
3967c46264
commit
6d407cea87
9 changed files with 237 additions and 85 deletions
|
|
@ -81,5 +81,5 @@ go mod tidy
|
|||
|
||||
|
||||
weitermachen:
|
||||
PROJEKT-STATUS Der gRPC-Server und alle drei Einzel-Clients (rig-client, rotor-client, relais-client) sind erfolgreich auf den neuen AuthService mit dynamischer Session-Token-Injektion und die korrigierten Protobuf-Feldnamen (Go 1.21 / Fyne v2.7) umgestellt und fehlerfrei lokal kompiliert. Die Konfigurationsschicht (internal/config/config.go) ist vollständig repariert.NÄCHSTER SCHRITT: Gemeinsamer Live-Test-Start aller vier Komponenten in getrennten Terminals und Überprüfung der flüssigen Vektornadel-Bewegung im Rotor-Kompass bei simulierter Antennenbewegung. Vorher aber noch ein buils.sh wie aus nachfolgendem Beispiel
|
||||
PROJEKT-MERKER: SAT-ROTOR RADIALDESIGNAktueller Status: Beide Kompasse laufen geometrisch entkoppelt und nebeneinander im 300x300 Pixel-Format mit transparenten Kreisen. Azimut nutzt den vollen Ring. Elevation nutzt den bogenförmigen Viertelkreis (-5° bis 90°) und besitzt jetzt 6 dedizierte Bogen-Buttons (0°, 15°, 30°, 45°, 60°, 90°), die sich harmonisch an die Krümmung der Skala anschmiegen. Beim Drücken der Elevations-Tasten bleibt der Azimut unberührt.Morgiger Start:Einfügen der Himmelsrichtungs-Textmarker (N, O, S, W) per canvas.NewText.Start des Linux-I2C-Bus-Treibers für den Raspberry Pi.
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -2,3 +2,9 @@
|
|||
station_name = "DL1ABC"
|
||||
server_address = "127.0.0.1:50051" # Im Live-Betrieb dann die Tailscale-IP
|
||||
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_elevation = 15.0 # Deine bevorzugte Home-Elevation
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// Package main implementiert das eigenständige Fyne-GUI-Fenster für die Rotorsteuerung (Rotor Control).
|
||||
// Es nutzt Fyne Data Binding für die UI und gRPC-Metadaten zur tokenbasierten Authentifizierung.
|
||||
// Es nutzt Fyne Data Binding für die UI, visualisiert Azimut und Elevation nebeneinander mit entkoppelten Ist/Soll-Zeigern.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
|
|
@ -26,22 +27,50 @@ import (
|
|||
"remote-shack/internal/config"
|
||||
)
|
||||
|
||||
// RotorClientGUI verwaltet alle UI-Komponenten und gRPC-Verbindungen des Rotor-Kompasses.
|
||||
type FixedSizeLayout struct {
|
||||
size fyne.Size
|
||||
}
|
||||
|
||||
func NewFixedSizeLayout(size fyne.Size) fyne.Layout {
|
||||
return &FixedSizeLayout{size: size}
|
||||
}
|
||||
|
||||
func (f *FixedSizeLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
||||
for _, obj := range objects {
|
||||
if _, ok := obj.(*widget.Button); !ok {
|
||||
obj.Resize(f.size)
|
||||
obj.Move(fyne.NewPos(0, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FixedSizeLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
|
||||
return f.size
|
||||
}
|
||||
|
||||
type RotorClientGUI struct {
|
||||
window fyne.Window
|
||||
grpcClient shack.RotorServiceClient
|
||||
azBind binding.String
|
||||
azNeedle *canvas.Line
|
||||
azInput *widget.Entry
|
||||
statusLabel *widget.Label // FIX: Neues Status-Label für konsistente Rechteanzeige
|
||||
token string
|
||||
window fyne.Window
|
||||
grpcClient shack.RotorServiceClient
|
||||
azBind binding.String
|
||||
elBind binding.String
|
||||
azNeedle *canvas.Line
|
||||
azTargetNeedle *canvas.Line
|
||||
elNeedle *canvas.Line
|
||||
elTargetNeedle *canvas.Line
|
||||
azInput *widget.Entry
|
||||
elInput *widget.Entry
|
||||
statusLabel *widget.Label
|
||||
token string
|
||||
homeAzimuth float64
|
||||
homeElevation float64
|
||||
targetAzimuth float64
|
||||
targetElevation float64
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.Println("=== Starte Remote-Shack Rotor-Client ===")
|
||||
log.Println("=== Starte Remote-Shack Sat-Rotor-Client ===")
|
||||
|
||||
// Läuft auf der Client-Maschine und lädt die client-config.toml
|
||||
cfg, err := config.LoadClientConfig("client-config.toml")
|
||||
if err != nil { log.Fatalf("Config Fehler: %v", err) }
|
||||
|
||||
|
|
@ -51,21 +80,17 @@ func main() {
|
|||
token = string(tokenBytes)
|
||||
log.Printf("[Auth] Geteiltes Session-Token erfolgreich geladen: %s", token)
|
||||
} else {
|
||||
log.Println("[Auth] Keine aktive Sitzung vorhanden. Starte im Nur-Lesen-Modus...")
|
||||
log.Println("[Auth] Keine active Sitzung vorhanden. Starte im Nur-Lesen-Modus...")
|
||||
token = ""
|
||||
}
|
||||
|
||||
unaryInterceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
if token != "" {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "operator-token", token)
|
||||
}
|
||||
if token != "" { ctx = metadata.AppendToOutgoingContext(ctx, "operator-token", token) }
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
|
||||
streamInterceptor := func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
if token != "" {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "operator-token", token)
|
||||
}
|
||||
if token != "" { ctx = metadata.AppendToOutgoingContext(ctx, "operator-token", token) }
|
||||
return streamer(ctx, desc, cc, method, opts...)
|
||||
}
|
||||
|
||||
|
|
@ -78,113 +103,219 @@ func main() {
|
|||
defer conn.Close()
|
||||
|
||||
gui := &RotorClientGUI{
|
||||
grpcClient: shack.NewRotorServiceClient(conn),
|
||||
azBind: binding.NewString(),
|
||||
token: token,
|
||||
grpcClient: shack.NewRotorServiceClient(conn),
|
||||
azBind: binding.NewString(),
|
||||
elBind: binding.NewString(),
|
||||
token: token,
|
||||
homeAzimuth: cfg.Client.Rotor.HomeAzimuth,
|
||||
homeElevation: cfg.Client.Rotor.HomeElevation,
|
||||
targetAzimuth: 0.0,
|
||||
targetElevation: 0.0,
|
||||
}
|
||||
|
||||
myApp := app.NewWithID("de.remote-shack.client.rotor")
|
||||
gui.window = myApp.NewWindow("Remote-Shack: Rotor Control")
|
||||
gui.window = myApp.NewWindow(fmt.Sprintf("Rotor Control - Typ: %s (%s)", cfg.Client.Rotor.Type, cfg.Client.Rotor.Port))
|
||||
gui.buildUI()
|
||||
|
||||
gui.window.Resize(fyne.NewSize(300, 420)) // Höhe minimal angepasst für das neue Label
|
||||
|
||||
gui.window.Resize(fyne.NewSize(620, 620))
|
||||
go gui.startLivePositionLoop()
|
||||
gui.window.ShowAndRun()
|
||||
}
|
||||
|
||||
// buildUI erzeugt alle visuellen Komponenten und verknüpft sie mit den Data-Bindings.
|
||||
func (g *RotorClientGUI) buildUI() {
|
||||
g.azNeedle = canvas.NewLine(theme.ForegroundColor())
|
||||
g.azNeedle.StrokeWidth = 3
|
||||
g.azNeedle.Position1 = fyne.NewPos(100, 100)
|
||||
g.azNeedle.Position2 = fyne.NewPos(100, 20)
|
||||
redColor := color.RGBA{R: 230, G: 30, B: 30, A: 255}
|
||||
greenColor := color.RGBA{R: 30, G: 200, B: 30, A: 255}
|
||||
|
||||
g.azInput = widget.NewEntry()
|
||||
g.azInput.SetPlaceHolder("Ziel-Azimut (z.B. 145)")
|
||||
btn := widget.NewButton("Position Setzen", g.sendTargetPosition)
|
||||
// 1. Azimut-Zentrum (Fixierte 300x300 Box, Center bei 150,150)
|
||||
g.azNeedle = canvas.NewLine(redColor)
|
||||
g.azNeedle.StrokeWidth = 4
|
||||
g.azNeedle.Position1 = fyne.NewPos(150, 150)
|
||||
g.azNeedle.Position2 = fyne.NewPos(150, 45)
|
||||
|
||||
g.azTargetNeedle = canvas.NewLine(greenColor)
|
||||
g.azTargetNeedle.StrokeWidth = 3
|
||||
g.azTargetNeedle.Position1 = fyne.NewPos(150, 150)
|
||||
g.azTargetNeedle.Position2 = fyne.NewPos(150, 45)
|
||||
|
||||
azCircle := canvas.NewCircle(theme.ForegroundColor())
|
||||
azCircle.StrokeWidth = 2
|
||||
azCircle.FillColor = color.Transparent
|
||||
azCircle.Resize(fyne.NewSize(210, 210))
|
||||
azCircle.Move(fyne.NewPos(45, 45))
|
||||
|
||||
azDisplay := container.NewWithoutLayout(azCircle, g.azTargetNeedle, g.azNeedle)
|
||||
azDisplay.Resize(fyne.NewSize(300, 300))
|
||||
|
||||
// 2. Tasten für den äußeren Windrosen-Ring (Radius 128 Pixel vom Center 150,150)
|
||||
btnN := widget.NewButton("N (0°)", func() { g.executeDirectMove(0, g.targetElevation) })
|
||||
btnNO := widget.NewButton("NO (45°)", func() { g.executeDirectMove(45, g.targetElevation) })
|
||||
btnO := widget.NewButton("O (90°)", func() { g.executeDirectMove(90, g.targetElevation) })
|
||||
btnSO := widget.NewButton("SO (135°)", func() { g.executeDirectMove(135, g.targetElevation) })
|
||||
btnS := widget.NewButton("S (180°)", func() { g.executeDirectMove(180, g.targetElevation) })
|
||||
btnSW := widget.NewButton("SW (225°)", func() { g.executeDirectMove(225, g.targetElevation) })
|
||||
btnW := widget.NewButton("W (270°)", func() { g.executeDirectMove(270, g.targetElevation) })
|
||||
btnNW := widget.NewButton("NW (315°)", func() { g.executeDirectMove(315, g.targetElevation) })
|
||||
|
||||
btnN.Move(fyne.NewPos(100, 2)); btnN.Resize(fyne.NewSize(100, 36))
|
||||
btnNO.Move(fyne.NewPos(242, 40)); btnNO.Resize(fyne.NewSize(100, 36))
|
||||
btnO.Move(fyne.NewPos(298, 132)); btnO.Resize(fyne.NewSize(100, 36))
|
||||
btnSO.Move(fyne.NewPos(242, 224)); btnSO.Resize(fyne.NewSize(100, 36))
|
||||
btnS.Move(fyne.NewPos(100, 262)); btnS.Resize(fyne.NewSize(100, 36))
|
||||
btnSW.Move(fyne.NewPos(-42, 224)); btnSW.Resize(fyne.NewSize(100, 36))
|
||||
btnW.Move(fyne.NewPos(-98, 132)); btnW.Resize(fyne.NewSize(100, 36))
|
||||
btnNW.Move(fyne.NewPos(-42, 40)); btnNW.Resize(fyne.NewSize(100, 36))
|
||||
|
||||
rawWindrose := container.NewWithoutLayout(
|
||||
azDisplay, btnN, btnNO, btnO, btnSO, btnS, btnSW, btnW, btnNW,
|
||||
)
|
||||
windroseContainer := container.New(NewFixedSizeLayout(fyne.NewSize(300, 300)), rawWindrose)
|
||||
|
||||
// 3. Elevation-Ausschnitt (Center links-unten bei 15,240, Grafikbox-Breite 240)
|
||||
g.elNeedle = canvas.NewLine(redColor)
|
||||
g.elNeedle.StrokeWidth = 4
|
||||
g.elNeedle.Position1 = fyne.NewPos(15, 240)
|
||||
g.elNeedle.Position2 = fyne.NewPos(235, 240)
|
||||
|
||||
g.elTargetNeedle = canvas.NewLine(greenColor)
|
||||
g.elTargetNeedle.StrokeWidth = 3
|
||||
g.elTargetNeedle.Position1 = fyne.NewPos(15, 240)
|
||||
g.elTargetNeedle.Position2 = fyne.NewPos(235, 240)
|
||||
|
||||
elArc := canvas.NewCircle(theme.ForegroundColor())
|
||||
elArc.StrokeWidth = 2
|
||||
elArc.FillColor = color.Transparent
|
||||
elArc.Resize(fyne.NewSize(440, 440))
|
||||
elArc.Move(fyne.NewPos(-205, 20))
|
||||
|
||||
horizonLine := canvas.NewLine(theme.DisabledColor())
|
||||
horizonLine.StrokeWidth = 1
|
||||
horizonLine.Position1 = fyne.NewPos(15, 240)
|
||||
horizonLine.Position2 = fyne.NewPos(235, 240)
|
||||
|
||||
// Bogenförmige Elevations-Tasten (Radius 240 Px vom Ursprung 15,240 nach rechts oben)
|
||||
btnEl0 := widget.NewButton("0°", func() { g.executeDirectMove(g.targetAzimuth, 0) })
|
||||
btnEl15 := widget.NewButton("15°", func() { g.executeDirectMove(g.targetAzimuth, 15) })
|
||||
btnEl30 := widget.NewButton("30°", func() { g.executeDirectMove(g.targetAzimuth, 30) })
|
||||
btnEl45 := widget.NewButton("45°", func() { g.executeDirectMove(g.targetAzimuth, 45) })
|
||||
btnEl60 := widget.NewButton("60°", func() { g.executeDirectMove(g.targetAzimuth, 60) })
|
||||
btnEl90 := widget.NewButton("90°", func() { g.executeDirectMove(g.targetAzimuth, 90) })
|
||||
|
||||
btnEl0.Move(fyne.NewPos(242, 224)); btnEl0.Resize(fyne.NewSize(56, 32))
|
||||
btnEl15.Move(fyne.NewPos(232, 162)); btnEl15.Resize(fyne.NewSize(56, 32))
|
||||
btnEl30.Move(fyne.NewPos(206, 105)); btnEl30.Resize(fyne.NewSize(56, 32))
|
||||
btnEl45.Move(fyne.NewPos(166, 58)); btnEl45.Resize(fyne.NewSize(56, 32))
|
||||
btnEl60.Move(fyne.NewPos(115, 24)); btnEl60.Resize(fyne.NewSize(56, 32))
|
||||
btnEl90.Move(fyne.NewPos(15, 2)); btnEl90.Resize(fyne.NewSize(56, 32))
|
||||
|
||||
elDisplay := container.NewWithoutLayout(
|
||||
elArc, horizonLine, g.elTargetNeedle, g.elNeedle,
|
||||
btnEl0, btnEl15, btnEl30, btnEl45, btnEl60, btnEl90,
|
||||
)
|
||||
elDisplay.Resize(fyne.NewSize(300, 300))
|
||||
|
||||
// 4. Steuerungs- und Textelemente
|
||||
g.azInput = widget.NewEntry(); g.azInput.SetPlaceHolder("Azimut°")
|
||||
g.elInput = widget.NewEntry(); g.elInput.SetPlaceHolder("Elev°")
|
||||
btnSet := widget.NewButtonWithIcon("Anfahren", theme.NavigateNextIcon(), g.sendManualPosition)
|
||||
btnHome := widget.NewButtonWithIcon("HOME", theme.HomeIcon(), func() { g.executeDirectMove(g.homeAzimuth, g.homeElevation) })
|
||||
|
||||
// FIX: Status-Label analog zum Relais-Client initialisieren
|
||||
g.statusLabel = widget.NewLabelWithStyle("Status: Synchronisiere...", fyne.TextAlignCenter, fyne.TextStyle{Italic: true})
|
||||
|
||||
compassContainer := container.NewWithoutLayout(g.azNeedle)
|
||||
compassContainer.Resize(fyne.NewSize(200, 200))
|
||||
displayHeader := container.NewGridWithColumns(2,
|
||||
container.NewVBox(widget.NewLabelWithStyle("AZIMUT (0-360°)", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), container.NewCenter(windroseContainer), widget.NewLabelWithData(g.azBind)),
|
||||
container.NewVBox(widget.NewLabelWithStyle("ELEVATION (-5° bis 90°)", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), container.NewCenter(elDisplay), widget.NewLabelWithData(g.elBind)),
|
||||
)
|
||||
|
||||
g.window.SetContent(container.NewVBox(
|
||||
widget.NewLabelWithStyle("--- ROTOR COMPASS ---", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||||
widget.NewLabelWithData(g.azBind),
|
||||
container.NewCenter(compassContainer),
|
||||
g.azInput,
|
||||
btn,
|
||||
content := container.NewVBox(
|
||||
displayHeader,
|
||||
widget.NewSeparator(),
|
||||
g.statusLabel, // Konsistente Platzierung ganz unten im Fenster
|
||||
))
|
||||
container.NewGridWithColumns(2,
|
||||
container.NewVBox(widget.NewLabelWithStyle("--- DIREKTZUGRIFF ---", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), container.NewCenter(btnHome)),
|
||||
container.NewVBox(widget.NewLabelWithStyle("--- MANUELLE KOORDINATEN ---", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), container.NewGridWithColumns(3, g.azInput, g.elInput, btnSet)),
|
||||
),
|
||||
widget.NewSeparator(),
|
||||
g.statusLabel,
|
||||
)
|
||||
|
||||
g.window.SetContent(container.NewMax(content))
|
||||
}
|
||||
|
||||
// startLivePositionLoop liest den Server-Status im Hintergrund und stößt UI-Updates via fyne.Do an.
|
||||
func (g *RotorClientGUI) startLivePositionLoop() {
|
||||
firstSync := true // Schützt den initialen RAM-Zustand beim Einschalten
|
||||
for {
|
||||
stream, err := g.grpcClient.LivePosition(context.Background(), &shack.Empty{})
|
||||
if err != nil {
|
||||
fyne.Do(func() {
|
||||
g.statusLabel.SetText("Fehler: Keine Verbindung")
|
||||
})
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
fyne.Do(func() { g.statusLabel.SetText("Fehler: Keine Verbindung") })
|
||||
time.Sleep(2 * time.Second); continue
|
||||
}
|
||||
for {
|
||||
status, err := stream.Recv()
|
||||
if err != nil { break }
|
||||
_ = g.azBind.Set(fmt.Sprintf("Azimut: %.1f°", status.AzimuthIs))
|
||||
|
||||
// Dynamische Überprüfung der Rechte im Hintergrund-Loop für das Status-Label
|
||||
_ = g.azBind.Set(fmt.Sprintf("Ist-Azimut: %.1f°", status.AzimuthIs))
|
||||
_ = g.elBind.Set(fmt.Sprintf("Ist-Elevation: %.1f°", status.ElevationIs))
|
||||
|
||||
// FIX: Beim Programmstart oder Reconnect die RAM-Zielvariablen sofort mit den echten Ist-Werten füttern!
|
||||
if firstSync {
|
||||
g.targetAzimuth = status.AzimuthIs
|
||||
g.targetElevation = status.ElevationIs
|
||||
firstSync = false
|
||||
}
|
||||
|
||||
tokenBytes, err := os.ReadFile(".shack_session")
|
||||
hasRights := err == nil && len(tokenBytes) > 0
|
||||
|
||||
fyne.Do(func() {
|
||||
if hasRights {
|
||||
g.statusLabel.SetText("Verbindung: Aktiv (Operator)")
|
||||
} else {
|
||||
g.statusLabel.SetText("Verbindung: Nur Lesen (Keine Rechte)")
|
||||
}
|
||||
if hasRights { g.statusLabel.SetText("Verbindung: Aktiv (Operator)") } else { g.statusLabel.SetText("Verbindung: Nur Lesen (Keine Rechte)") }
|
||||
|
||||
rad := (status.AzimuthIs - 90) * math.Pi / 180.0
|
||||
g.azNeedle.Position2 = fyne.NewPos(
|
||||
float32(100.0+80.0*math.Cos(rad)),
|
||||
float32(100.0+80.0*math.Sin(rad)),
|
||||
)
|
||||
// Azimut Ist-Vektor (Center 150,150)
|
||||
radAz := (status.AzimuthIs - 90) * math.Pi / 180.0
|
||||
g.azNeedle.Position2 = fyne.NewPos(float32(150.0+100.0*math.Cos(radAz)), float32(150.0+100.0*math.Sin(radAz)))
|
||||
g.azNeedle.Refresh()
|
||||
|
||||
// Elevation Ist-Vektor (Center 15,240, Radius 220 Px)
|
||||
radEl := (-status.ElevationIs) * math.Pi / 180.0
|
||||
g.elNeedle.Position2 = fyne.NewPos(float32(15.0+220.0*math.Cos(radEl)), float32(240.0+220.0*math.Sin(radEl)))
|
||||
g.elNeedle.Refresh()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendTargetPosition übermittelt den eingegebenen Richtwert an den gRPC-Server.
|
||||
func (g *RotorClientGUI) sendTargetPosition() {
|
||||
// FIX: Rechte-Prüfung direkt beim Klick abfangen und Senden softwareseitig blockieren
|
||||
func (g *RotorClientGUI) sendManualPosition() {
|
||||
az, _ := strconv.ParseFloat(g.azInput.Text, 64)
|
||||
el, _ := strconv.ParseFloat(g.elInput.Text, 64)
|
||||
g.executeDirectMove(az, el)
|
||||
}
|
||||
|
||||
func (g *RotorClientGUI) executeDirectMove(az float64, el float64) {
|
||||
tokenBytes, err := os.ReadFile(".shack_session")
|
||||
if err != nil || len(tokenBytes) == 0 {
|
||||
fyne.Do(func() {
|
||||
g.statusLabel.SetText("⚠️ Blockiert: Keine Operator-Rechte!")
|
||||
})
|
||||
log.Println("[Sicherheit] Sendeversuch blockiert: Keine aktive Operator-Sitzung vorhanden.")
|
||||
fyne.Do(func() { g.statusLabel.SetText("⚠️ Blockiert: Keine Operator-Rechte!") })
|
||||
return
|
||||
}
|
||||
g.token = string(tokenBytes)
|
||||
|
||||
val, err := strconv.ParseFloat(g.azInput.Text, 64)
|
||||
if err != nil {
|
||||
log.Printf("Ungültige Eingabe: %v", err)
|
||||
return
|
||||
}
|
||||
// FIX: Die Variablen werden jetzt absolut lückenlos im RAM festgehalten
|
||||
g.targetAzimuth = az
|
||||
g.targetElevation = el
|
||||
|
||||
fyne.Do(func() {
|
||||
// Azimut Soll-Vektor (Center 150,150)
|
||||
radAzTar := (az - 90) * math.Pi / 180.0
|
||||
g.azTargetNeedle.Position2 = fyne.NewPos(float32(150.0+100.0*math.Cos(radAzTar)), float32(150.0+100.0*math.Sin(radAzTar)))
|
||||
g.azTargetNeedle.Refresh()
|
||||
|
||||
// Elevation Soll-Vektor (Center 15,240)
|
||||
radElTar := (-el) * math.Pi / 180.0
|
||||
g.elTargetNeedle.Position2 = fyne.NewPos(float32(15.0+220.0*math.Cos(radElTar)), float32(240.0+220.0*math.Sin(radElTar)))
|
||||
g.elTargetNeedle.Refresh()
|
||||
})
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "operator-token", g.token)
|
||||
_, err = g.grpcClient.TargetPosition(ctx, &shack.RotorTarget{Azimuth: val})
|
||||
if err != nil {
|
||||
log.Printf("Fehler beim Senden der Zielposition: %v", err)
|
||||
_, err = g.grpcClient.TargetPosition(ctx, &shack.RotorTarget{Azimuth: az, Elevation: el})
|
||||
if err != nil {
|
||||
log.Printf("Fehler beim Positionieren: %v", err)
|
||||
} else {
|
||||
fyne.Do(func() {
|
||||
g.statusLabel.SetText("Verbindung: Aktiv (Positioniert)")
|
||||
})
|
||||
fyne.Do(func() { g.statusLabel.SetText(fmt.Sprintf("Fahre Position an: AZ %.1f° | EL %.1f°", az, el)) })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ package config
|
|||
|
||||
import (
|
||||
"os"
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
// ADS1115Module definiert die Hardware-Parameter und Kanal-Labels einer ADC-Platine.
|
||||
|
|
@ -24,11 +24,20 @@ type ServerConfig struct {
|
|||
Server ServerSettings `toml:"server"`
|
||||
}
|
||||
|
||||
// RotorSettings definiert die hardware- und systemspezifischen Parameter eines Rotors.
|
||||
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)
|
||||
}
|
||||
|
||||
// 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"`
|
||||
ServerAddress string `toml:"server_address"`
|
||||
StationName string `toml:"station_name"`
|
||||
RefreshRateMs int `toml:"refresh_rate_ms"`
|
||||
Rotor RotorSettings `toml:"rotor"` // ERWEITERUNG: Rotorspezifische Einstellungen
|
||||
}
|
||||
|
||||
// ClientConfig bündelt alle Konfigurationseinstellungen des GUI-Clients.
|
||||
|
|
@ -68,6 +77,12 @@ func LoadClientConfig(path string) (*ClientConfig, error) {
|
|||
ServerAddress: "127.0.0.1:50051",
|
||||
StationName: "DL-UNKNOWN",
|
||||
RefreshRateMs: 250,
|
||||
Rotor: RotorSettings{
|
||||
Type: "Standard",
|
||||
Port: "Simulation",
|
||||
HomeAzimuth: 180.0, // Standardmäßig Süden
|
||||
HomeElevation: 0.0, // Standardmäßig Horizont
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue