remote-shack/cmd/client-volt/main.go
2026-06-13 13:02:54 +02:00

210 lines
5.8 KiB
Go

// Package main implementiert das eigenständige Fyne-GUI-Fenster für die Spannungsüberwachung (Volt Monitor).
// Es liest die Messwerte einer dynamischen Anzahl von ADS1115-Platinen automatisch über gRPC-Streaming ein.
package main
import (
"context"
"fmt"
"log"
"math/rand"
"sort"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
shack "remote-shack/api"
"remote-shack/internal/config"
)
// VoltClientGUI verwaltet die dynamischen Anzeigen für alle gelieferten analogen Kanäle.
type VoltClientGUI struct {
window fyne.Window
telemetryCl shack.TelemetryServiceClient
mainContainer *fyne.Container
statusLabel *widget.Label
voltBinds map[int32]binding.String
progressBars map[int32]*widget.ProgressBar
uiBuilt bool
}
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("=== Starte Skalierbaren Remote-Shack Volt-Client ===")
cfg, err := config.LoadClientConfig("client-config.toml")
if err != nil {
log.Fatalf("Fehler beim Laden der client-config.toml: %v", err)
}
conn, err := grpc.Dial(cfg.Client.ServerAddress, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Verbindung zum gRPC-Server fehlgeschlagen: %v", err)
}
defer conn.Close()
telemetryClient := shack.NewTelemetryServiceClient(conn)
myApp := app.NewWithID("de.remote-shack.client.volt")
myWindow := myApp.NewWindow("Remote-Shack: Volt-Monitor")
g := &VoltClientGUI{
window: myWindow,
telemetryCl: telemetryClient,
voltBinds: make(map[int32]binding.String),
progressBars: make(map[int32]*widget.ProgressBar),
uiBuilt: false,
}
g.statusLabel = widget.NewLabelWithStyle("Status: Synchronisiere...", fyne.TextAlignCenter, fyne.TextStyle{Italic: true})
g.mainContainer = container.NewVBox(
widget.NewLabelWithStyle("--- TELEMETRIE (0..12 V) ---", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
)
g.window.SetContent(container.NewBorder(nil, container.NewVBox(widget.NewSeparator(), g.statusLabel), nil, nil, g.mainContainer))
g.window.Resize(fyne.NewSize(340, 500))
go g.startTelemetryStreamLoop()
myWindow.ShowAndRun()
}
// buildDynamicUI generiert die Text- und Balkenanzeigen dynamisch passend zur empfangenen Kanalanzahl.
func (g *VoltClientGUI) buildDynamicUI(channels []int32, labels map[int32]string, isSim bool) {
for _, ch := range channels {
g.voltBinds[ch] = binding.NewString()
// FIX: Präfix bei aktiver Simulation für maximale Klarheit im Shack
prefix := ""
if isSim {
prefix = "[SIM] "
}
_ = g.voltBinds[ch].Set(fmt.Sprintf("%s%s: --.- V", prefix, labels[ch]))
bar := widget.NewProgressBar()
bar.Min = 0.0
bar.Max = 12.0
g.progressBars[ch] = bar
g.mainContainer.Add(widget.NewLabelWithData(g.voltBinds[ch]))
g.mainContainer.Add(bar)
}
g.mainContainer.Refresh()
g.uiBuilt = true
}
// startTelemetryStreamLoop empfängt die berechnetenspannungswerte fortlaufend vom Server oder simuliert sie lokal.
func (g *VoltClientGUI) startTelemetryStreamLoop() {
dataChan := make(chan *shack.VoltageResponse, 1)
errChan := make(chan error, 1)
demoLabels := map[int32]string{
1: "Hauptstrom 13.8V",
2: "Logikbus 5V",
3: "Relais-Schiene",
4: "Rauschen (Frei)",
5: "Backup-Batterie",
}
go func() {
stream, err := g.telemetryCl.StreamVoltages(context.Background(), &shack.Empty{})
if err != nil {
errChan <- err
return
}
for {
res, err := stream.Recv()
if err != nil {
errChan <- err
return
}
dataChan <- res
}
}()
for {
select {
case res := <-dataChan:
if !g.uiBuilt && res.Channels != nil && len(res.Channels) > 0 {
var sortedChannels []int32
labels := make(map[int32]string)
for ch, reading := range res.Channels {
sortedChannels = append(sortedChannels, ch)
labels[ch] = reading.Label
}
sort.Slice(sortedChannels, func(i, j int) bool { return sortedChannels[i] < sortedChannels[j] })
fyne.Do(func() { g.buildDynamicUI(sortedChannels, labels, false) })
}
fyne.Do(func() {
g.statusLabel.SetText("Verbindung: Empfange Live-Daten")
for ch, reading := range res.Channels {
if bar, ok := g.progressBars[ch]; ok {
val := reading.Voltage
if val < 0 { val = 0 }
if val > 12 { val = 12 }
_ = g.voltBinds[ch].Set(fmt.Sprintf("%s: %.2f V", reading.Label, val))
bar.SetValue(float64(val))
}
}
})
case <-errChan:
g.runLocalSimulation(demoLabels)
return
case <-time.After(2 * time.Second):
if !g.uiBuilt {
log.Println("[Volt-Monitor] Keine Serverdaten empfangen. Erzwinge lokalen Simulator...")
g.runLocalSimulation(demoLabels)
return
}
}
}
}
// runLocalSimulation übernimmt die regelmäßige Aktualisierung der GUI mit Dummy-Messwerten.
func (g *VoltClientGUI) runLocalSimulation(demoLabels map[int32]string) {
if !g.uiBuilt {
fyne.Do(func() {
// Übergibt 'true' für isSim an das UI-Builder-Subsystem
g.buildDynamicUI([]int32{1, 2, 3, 4, 5}, demoLabels, true)
})
}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
for {
fyne.Do(func() {
// FIX: Markanter Warntext unten im Fenster platziert
g.statusLabel.SetText("⚠️ SIMULATION (KEINE HARDWARE)")
var ch int32
for ch = 1; ch <= 5; ch++ {
var val float32
switch ch {
case 1:
val = 13.80 + (r.Float32() * 0.2 - 0.1)
case 2:
val = 5.02 + (r.Float32() * 0.04 - 0.02)
case 5:
val = 9.15 + (r.Float32() * 0.08 - 0.04)
default:
val = r.Float32() * 12.0
}
if val < 0 { val = 0 }
if val > 12 { val = 12 }
_ = g.voltBinds[ch].Set(fmt.Sprintf("[SIM] %s: %.2f V", demoLabels[ch], val))
g.progressBars[ch].SetValue(float64(val))
}
})
time.Sleep(500 * time.Millisecond)
}
}