Gerätezuordnung in GUI verwalten und speichern
This commit is contained in:
parent
5e595fe460
commit
90189ab4dc
9 changed files with 897 additions and 201 deletions
44
CHANGELOG.md
44
CHANGELOG.md
|
|
@ -2,6 +2,50 @@
|
|||
|
||||
Alle wesentlichen Änderungen am Projekt werden in dieser Datei dokumentiert.
|
||||
|
||||
## [0.0.7] - 2026-08-12
|
||||
|
||||
### Added
|
||||
|
||||
- Pflege der Gerätezuordnung über die grafische Benutzeroberfläche
|
||||
- Zuordnung der vom Server angebotenen Geräte zu den konfigurierten
|
||||
virtuellen seriellen Schnittstellen
|
||||
- Unterstützung des Zustands „nicht verbunden“
|
||||
- Freie virtuelle Schnittstellen werden bei der Gerätezuordnung automatisch
|
||||
neu angeboten
|
||||
- Verhinderung der gleichzeitigen Zuordnung einer virtuellen Schnittstelle
|
||||
zu mehreren Geräten
|
||||
- Bereits zugeordnete Geräte können nur auf ihre aktuelle Schnittstelle oder
|
||||
„nicht verbunden“ gesetzt werden
|
||||
- Ungespeicherte Änderungen werden beim Schließen des Hauptfensters erkannt
|
||||
und dem Anwender angezeigt
|
||||
- Änderungen werden beim Schließen des Gerätezuordnungsfensters in der
|
||||
Client-Konfiguration gespeichert
|
||||
- Virtuelle Schnittstellen bleiben auch ohne Gerätezuordnung in der
|
||||
Client-Konfiguration erhalten
|
||||
- Ein leerer Wert für `remote_device` kennzeichnet eine freie virtuelle
|
||||
Schnittstelle
|
||||
- `Application.SaveConfig()` zum zentralen Speichern der Client-Konfiguration
|
||||
|
||||
### Changed
|
||||
|
||||
- Validierung der Client-Konfiguration erlaubt nun virtuelle Schnittstellen
|
||||
ohne aktuelle Gerätezuordnung
|
||||
- Die Gerätezuordnung arbeitet auf Basis der vorhandenen virtuellen
|
||||
Schnittstellen und verändert bei einer Trennung nur deren
|
||||
`remote_device`-Zuordnung
|
||||
|
||||
### Tests
|
||||
|
||||
- Gerätezuordnung mit mehreren Server-Geräten praktisch getestet
|
||||
- Freigabe einer virtuellen Schnittstelle und anschließende erneute Auswahl
|
||||
erfolgreich getestet
|
||||
- Speicherung einer Gerätezuordnung über die GUI erfolgreich getestet
|
||||
- Speicherung einer getrennten Gerätezuordnung mit leerem
|
||||
`remote_device` erfolgreich getestet
|
||||
- `go test ./internal/config` erfolgreich
|
||||
- `go test ./internal/gui` erfolgreich
|
||||
- `go test -race ./internal/gui` erfolgreich
|
||||
- `git diff --check` erfolgreich
|
||||
## [0.0.6] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -26,6 +26,16 @@
|
|||
"data_bits": 8,
|
||||
"parity": "none",
|
||||
"stop_bits": 1
|
||||
},
|
||||
{
|
||||
"id": "gps",
|
||||
"name": "GPS-Testgerät",
|
||||
"serial_port": "/dev/ttyUSB3",
|
||||
"baud_rate": 600,
|
||||
"data_bits": 8,
|
||||
"parity": "none",
|
||||
"stop_bits": 1
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
|
|
|
|||
3
go.mod
3
go.mod
|
|
@ -7,6 +7,8 @@ require (
|
|||
golang.org/x/sys v0.43.0
|
||||
)
|
||||
|
||||
require github.com/FyshOS/fancyfs v0.0.1 // indirect
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.8.0
|
||||
fyne.io/systray v1.12.2 // indirect
|
||||
|
|
@ -29,7 +31,6 @@ require (
|
|||
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.24 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
||||
|
|
|
|||
3
go.sum
3
go.sum
|
|
@ -4,11 +4,12 @@ fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA=
|
|||
fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/FyshOS/fancyfs v0.0.1 h1:kgvm7VvwOMLkYTqSflplp62SlMVWQ2uAoHw9CXwXHYg=
|
||||
github.com/FyshOS/fancyfs v0.0.1/go.mod h1:S5SHVz/5R72iCXOxCqdcyTPSlg3JxNd0gaHyGBSrY8A=
|
||||
github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync=
|
||||
github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
|
|
|
|||
|
|
@ -298,3 +298,32 @@ func (a *Application) Running() bool {
|
|||
|
||||
return a.runtime != nil && a.client != nil
|
||||
}
|
||||
// SaveConfig saves the supplied client configuration to the configuration
|
||||
// file used by this Application.
|
||||
//
|
||||
// After a successful save the supplied configuration also becomes the
|
||||
// current in-memory configuration.
|
||||
//
|
||||
// The running Runtime is not restarted automatically. A subsequent
|
||||
// Reconnect or application restart will use the saved configuration.
|
||||
func (a *Application) SaveConfig(
|
||||
cfg *config.ClientConfig,
|
||||
) error {
|
||||
if a == nil {
|
||||
return fmt.Errorf("application is nil")
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("client configuration is nil")
|
||||
}
|
||||
|
||||
if err := config.SaveClient(a.configFile, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.config = cfg
|
||||
a.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ type VirtualPortRangeConfig struct {
|
|||
|
||||
// VirtualPortConfig describes one local virtual serial port and the
|
||||
// remote device assigned to it.
|
||||
//
|
||||
// RemoteDevice may be empty. An empty RemoteDevice means that the virtual
|
||||
// port exists but is currently not assigned to a remote device.
|
||||
type VirtualPortConfig struct {
|
||||
Port string `json:"port"`
|
||||
RemoteDevice string `json:"remote_device"`
|
||||
|
|
@ -229,6 +232,9 @@ func (cfg *ServerConfig) Validate() error {
|
|||
}
|
||||
|
||||
// Validate checks the client configuration for basic errors.
|
||||
//
|
||||
// A virtual port may exist without a remote-device assignment. This is a
|
||||
// valid state and means that the virtual port is currently free.
|
||||
func (cfg *ClientConfig) Validate() error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("client configuration is nil")
|
||||
|
|
@ -239,30 +245,36 @@ func (cfg *ClientConfig) Validate() error {
|
|||
}
|
||||
|
||||
if cfg.VirtualPortRange.First < DefaultVirtualPortFirst {
|
||||
return fmt.Errorf("virtual port range first must be >= %d: %d",
|
||||
DefaultVirtualPortFirst, cfg.VirtualPortRange.First)
|
||||
return fmt.Errorf(
|
||||
"virtual port range first must be >= %d: %d",
|
||||
DefaultVirtualPortFirst,
|
||||
cfg.VirtualPortRange.First,
|
||||
)
|
||||
}
|
||||
|
||||
if cfg.VirtualPortRange.Last < cfg.VirtualPortRange.First {
|
||||
return fmt.Errorf("invalid virtual port range: %d-%d",
|
||||
cfg.VirtualPortRange.First, cfg.VirtualPortRange.Last)
|
||||
return fmt.Errorf(
|
||||
"invalid virtual port range: %d-%d",
|
||||
cfg.VirtualPortRange.First,
|
||||
cfg.VirtualPortRange.Last,
|
||||
)
|
||||
}
|
||||
|
||||
ports := make(map[string]bool)
|
||||
|
||||
for i, virtualPort := range cfg.VirtualPorts {
|
||||
if virtualPort.Port == "" {
|
||||
return fmt.Errorf("virtual port %d: port is empty", i)
|
||||
}
|
||||
|
||||
if virtualPort.RemoteDevice == "" {
|
||||
return fmt.Errorf("virtual port %q: remote device is empty",
|
||||
virtualPort.Port)
|
||||
return fmt.Errorf(
|
||||
"virtual port %d: port is empty",
|
||||
i,
|
||||
)
|
||||
}
|
||||
|
||||
if ports[virtualPort.Port] {
|
||||
return fmt.Errorf("duplicate virtual port: %q",
|
||||
virtualPort.Port)
|
||||
return fmt.Errorf(
|
||||
"duplicate virtual port: %q",
|
||||
virtualPort.Port,
|
||||
)
|
||||
}
|
||||
|
||||
ports[virtualPort.Port] = true
|
||||
|
|
@ -278,11 +290,19 @@ func (cfg *ClientConfig) Validate() error {
|
|||
func loadJSON(filename string, target interface{}) error {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read configuration %q: %w", filename, err)
|
||||
return fmt.Errorf(
|
||||
"read configuration %q: %w",
|
||||
filename,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, target); err != nil {
|
||||
return fmt.Errorf("parse configuration %q: %w", filename, err)
|
||||
return fmt.Errorf(
|
||||
"parse configuration %q: %w",
|
||||
filename,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -297,7 +317,11 @@ func saveJSON(filename string, value interface{}) error {
|
|||
data = append(data, '\n')
|
||||
|
||||
if err := os.WriteFile(filename, data, 0644); err != nil {
|
||||
return fmt.Errorf("write configuration %q: %w", filename, err)
|
||||
return fmt.Errorf(
|
||||
"write configuration %q: %w",
|
||||
filename,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -391,8 +391,8 @@ func TestExampleServerConfig(t *testing.T) {
|
|||
t.Fatalf("LoadServer() failed: %v", err)
|
||||
}
|
||||
|
||||
if len(cfg.Devices) != 2 {
|
||||
t.Fatalf("len(Devices) = %d, want 2", len(cfg.Devices))
|
||||
if len(cfg.Devices) != 3 {
|
||||
t.Fatalf("len(Devices) = %d, want 3", len(cfg.Devices))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,44 +7,43 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* Grundgerüst der grafischen Benutzeroberfläche des rs2322tcp-Clients.
|
||||
*
|
||||
* Die GUI verwendet Fyne und stellt die Benutzeroberfläche für den
|
||||
* technischen rs2322tcp-Client bereit.
|
||||
* Grafische Benutzeroberfläche des rs2322tcp-Clients.
|
||||
*
|
||||
* Die eigentliche Kommunikation mit dem Server sowie die Verwaltung der
|
||||
* virtuellen seriellen Ports bleiben vollständig in internal/client.
|
||||
*
|
||||
* Der technische Client wird im Hintergrund gestartet, damit das Fyne-
|
||||
* Fenster unmittelbar angezeigt werden kann. Dadurch bleibt die GUI auch
|
||||
* dann bedienbar, wenn der Server nicht erreichbar ist oder der Aufbau
|
||||
* der Verbindung längere Zeit benötigt.
|
||||
* Die Gerätezuordnung wird in einem separaten Fenster bearbeitet.
|
||||
*
|
||||
* Aktualisierungen von Fyne-Widgets aus einer Hintergrund-Goroutine werden
|
||||
* über fyne.Do() auf den Fyne-GUI-Thread übertragen.
|
||||
* Änderungen an der Gerätezuordnung werden zunächst ausschließlich im
|
||||
* Arbeitsspeicher des Zuordnungsfensters gehalten.
|
||||
*
|
||||
* Die GUI stellt außerdem die Funktion "Server neu verbinden" bereit.
|
||||
* Dabei wird die bestehende technische Client-Anwendung beendet und
|
||||
* anschließend vollständig neu gestartet. Dadurch wird die aktuelle
|
||||
* client.json erneut geladen.
|
||||
* Beim Schließen des Zuordnungsfensters werden vorhandene Änderungen
|
||||
* gespeichert.
|
||||
*
|
||||
* Die Gerätezuordnung wird zunächst ausschließlich angezeigt. Änderungen
|
||||
* an der Zuordnung und deren Speicherung werden in einem späteren
|
||||
* Entwicklungsschritt ergänzt.
|
||||
* Wird dagegen das Hauptfenster geschlossen, während das Zuordnungsfenster
|
||||
* noch ungespeicherte Änderungen enthält, wird der Anwender gefragt, ob
|
||||
* diese Änderungen verworfen werden sollen.
|
||||
* ============================================================================
|
||||
*/
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"git.lang-dieter.de/rs2322tcp/internal/client"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// App
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// App represents the graphical rs2322tcp client application.
|
||||
//
|
||||
// App owns the Fyne window and the technical client application. The GUI
|
||||
|
|
@ -62,12 +61,17 @@ type App struct {
|
|||
assignmentButton *widget.Button
|
||||
|
||||
assignmentWindow fyne.Window
|
||||
assignmentEditor *AssignmentEditor
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Construction
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// NewApp creates the graphical rs2322tcp client application.
|
||||
//
|
||||
// No server connection is established by NewApp. The GUI can therefore be
|
||||
// displayed immediately after this function returns.
|
||||
// No network connection is established by NewApp. Start must be called
|
||||
// separately.
|
||||
func NewApp(
|
||||
fyneApp fyne.App,
|
||||
configFile string,
|
||||
|
|
@ -104,13 +108,21 @@ func NewApp(
|
|||
|
||||
app.buildContent()
|
||||
|
||||
// Das Schließen des Hauptfensters wird abgefangen, damit ein noch
|
||||
// geöffnetes Zuordnungsfenster mit ungespeicherten Änderungen nicht
|
||||
// stillschweigend verworfen wird.
|
||||
window.SetCloseIntercept(
|
||||
app.handleMainWindowClose,
|
||||
)
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
// buildContent creates the initial main-window content.
|
||||
//
|
||||
// The layout is deliberately simple. More detailed status information and
|
||||
// the device-assignment dialog will be added in later development steps.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Main window
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// buildContent creates the main-window content.
|
||||
func (a *App) buildContent() {
|
||||
title := widget.NewLabel("rs2322tcp Client")
|
||||
|
||||
|
|
@ -125,20 +137,22 @@ func (a *App) buildContent() {
|
|||
a.window.SetContent(content)
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Client lifecycle
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Start starts the technical client application in the background.
|
||||
//
|
||||
// The Fyne window is deliberately not blocked by the network connection.
|
||||
// The status shown by the GUI is updated through fyne.Do(), because the
|
||||
// technical client runs outside the Fyne GUI thread.
|
||||
//
|
||||
// While the initial connection is being established, the reconnect button
|
||||
// is disabled. This prevents multiple concurrent connection attempts.
|
||||
// The Fyne window remains responsive while the connection to the server is
|
||||
// established.
|
||||
func (a *App) Start() {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.statusLabel.SetText("Server: Verbindung wird aufgebaut ...")
|
||||
a.statusLabel.SetText(
|
||||
"Server: Verbindung wird aufgebaut ...",
|
||||
)
|
||||
a.errorLabel.SetText("")
|
||||
a.reconnectButton.Disable()
|
||||
|
||||
|
|
@ -147,7 +161,9 @@ func (a *App) Start() {
|
|||
|
||||
if err != nil {
|
||||
fyne.Do(func() {
|
||||
a.statusLabel.SetText("Server: nicht verbunden")
|
||||
a.statusLabel.SetText(
|
||||
"Server: nicht verbunden",
|
||||
)
|
||||
a.errorLabel.SetText(
|
||||
fmt.Sprintf("Fehler: %v", err),
|
||||
)
|
||||
|
|
@ -174,20 +190,14 @@ func (a *App) Start() {
|
|||
}
|
||||
|
||||
// reconnect starts a complete reconnect of the technical client.
|
||||
//
|
||||
// The reconnect operation is deliberately handled by
|
||||
// client.Application.Reconnect(). The GUI therefore does not need to know
|
||||
// how the technical client closes connections, reloads the configuration,
|
||||
// connects to the server or starts the Runtime.
|
||||
//
|
||||
// While reconnecting, the button is disabled so that only one reconnect
|
||||
// operation can be active at a time.
|
||||
func (a *App) reconnect() {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.statusLabel.SetText("Server: Verbindung wird neu aufgebaut ...")
|
||||
a.statusLabel.SetText(
|
||||
"Server: Verbindung wird neu aufgebaut ...",
|
||||
)
|
||||
a.errorLabel.SetText("")
|
||||
a.reconnectButton.Disable()
|
||||
|
||||
|
|
@ -196,7 +206,9 @@ func (a *App) reconnect() {
|
|||
|
||||
if err != nil {
|
||||
fyne.Do(func() {
|
||||
a.statusLabel.SetText("Server: nicht verbunden")
|
||||
a.statusLabel.SetText(
|
||||
"Server: nicht verbunden",
|
||||
)
|
||||
a.errorLabel.SetText(
|
||||
fmt.Sprintf("Fehler: %v", err),
|
||||
)
|
||||
|
|
@ -222,19 +234,17 @@ func (a *App) reconnect() {
|
|||
}()
|
||||
}
|
||||
|
||||
// showAssignments displays the current local-to-remote device assignments.
|
||||
//
|
||||
// This first implementation is deliberately read-only. It obtains the
|
||||
// configuration and the current server device list from client.Application
|
||||
// and converts them through the GUI-independent assignment functions.
|
||||
//
|
||||
// No configuration is changed or written by this function.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Device assignment
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// showAssignments opens the editable device-assignment window.
|
||||
func (a *App) showAssignments() {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Do not create another assignment window when one is already open.
|
||||
// Nur ein Zuordnungsfenster gleichzeitig.
|
||||
if a.assignmentWindow != nil {
|
||||
a.assignmentWindow.Show()
|
||||
return
|
||||
|
|
@ -243,133 +253,395 @@ func (a *App) showAssignments() {
|
|||
cfg := a.clientApplication.Config()
|
||||
devices := a.clientApplication.Devices()
|
||||
|
||||
assignments := BuildAssignments(cfg, devices)
|
||||
unassigned := UnassignedDevices(cfg, devices)
|
||||
|
||||
content := container.NewVBox(
|
||||
widget.NewLabel("Lokale Zuordnungen"),
|
||||
editor := NewAssignmentEditor(
|
||||
cfg,
|
||||
devices,
|
||||
)
|
||||
|
||||
if len(assignments) == 0 {
|
||||
content.Add(
|
||||
widget.NewLabel("Keine lokalen Zuordnungen vorhanden."),
|
||||
)
|
||||
} else {
|
||||
for _, assignment := range assignments {
|
||||
status := "nicht verfügbar"
|
||||
|
||||
if assignment.Available {
|
||||
status = "verfügbar"
|
||||
}
|
||||
|
||||
content.Add(
|
||||
widget.NewLabel(
|
||||
fmt.Sprintf(
|
||||
"%s -> %s (%s)",
|
||||
assignment.LocalPort,
|
||||
assignment.RemoteDevice,
|
||||
status,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
content.Add(widget.NewSeparator())
|
||||
content.Add(
|
||||
widget.NewLabel("Server-Geräte ohne lokale Zuordnung"),
|
||||
)
|
||||
|
||||
if len(unassigned) == 0 {
|
||||
content.Add(
|
||||
widget.NewLabel(
|
||||
"Keine nicht verwendeten Server-Geräte.",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
for _, device := range unassigned {
|
||||
content.Add(
|
||||
widget.NewLabel(
|
||||
fmt.Sprintf(
|
||||
"%s – noch nicht verwendet",
|
||||
device.ID,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assignmentWindow := a.fyneApp.NewWindow(
|
||||
window := a.fyneApp.NewWindow(
|
||||
"Gerätezuordnung",
|
||||
)
|
||||
assignmentWindow.Resize(fyne.NewSize(650, 450))
|
||||
|
||||
// Remember the child window so it can be closed together with
|
||||
// the main window.
|
||||
a.assignmentWindow = assignmentWindow
|
||||
window.Resize(
|
||||
fyne.NewSize(650, 450),
|
||||
)
|
||||
|
||||
assignmentWindow.SetOnClosed(func() {
|
||||
// Only clear the reference if this is still the current
|
||||
// assignment window.
|
||||
if a.assignmentWindow == assignmentWindow {
|
||||
a.assignmentWindow = window
|
||||
a.assignmentEditor = editor
|
||||
|
||||
// Beim Schließen über das Fenstersymbol wird dieselbe Logik wie beim
|
||||
// Button "Schließen" verwendet.
|
||||
window.SetCloseIntercept(
|
||||
func() {
|
||||
a.handleAssignmentWindowClose(
|
||||
window,
|
||||
editor,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
window.SetOnClosed(func() {
|
||||
if a.assignmentWindow == window {
|
||||
a.assignmentWindow = nil
|
||||
a.assignmentEditor = nil
|
||||
}
|
||||
})
|
||||
|
||||
assignmentWindow.SetContent(
|
||||
container.NewBorder(
|
||||
nil,
|
||||
widget.NewButton(
|
||||
"Schließen",
|
||||
func() {
|
||||
assignmentWindow.Close()
|
||||
},
|
||||
),
|
||||
nil,
|
||||
nil,
|
||||
container.NewVScroll(content),
|
||||
a.refreshAssignmentWindow(
|
||||
window,
|
||||
editor,
|
||||
)
|
||||
|
||||
window.Show()
|
||||
}
|
||||
|
||||
// refreshAssignmentWindow rebuilds all assignment Select widgets.
|
||||
//
|
||||
// This is intentional. When a device is disconnected, its former port
|
||||
// becomes free and must immediately become selectable for the other
|
||||
// currently unconnected devices.
|
||||
func (a *App) refreshAssignmentWindow(
|
||||
window fyne.Window,
|
||||
editor *AssignmentEditor,
|
||||
) {
|
||||
if a == nil || window == nil || editor == nil {
|
||||
return
|
||||
}
|
||||
|
||||
deviceIDs := editorDeviceIDs(editor)
|
||||
|
||||
objects := make(
|
||||
[]fyne.CanvasObject,
|
||||
0,
|
||||
len(deviceIDs)*2,
|
||||
)
|
||||
|
||||
objects = append(
|
||||
objects,
|
||||
widget.NewLabelWithStyle(
|
||||
"Server-Gerät",
|
||||
fyne.TextAlignLeading,
|
||||
fyne.TextStyle{Bold: true},
|
||||
),
|
||||
)
|
||||
|
||||
assignmentWindow.Show()
|
||||
objects = append(
|
||||
objects,
|
||||
widget.NewLabelWithStyle(
|
||||
"Lokale Schnittstelle",
|
||||
fyne.TextAlignLeading,
|
||||
fyne.TextStyle{Bold: true},
|
||||
),
|
||||
)
|
||||
|
||||
for _, deviceID := range deviceIDs {
|
||||
deviceID := deviceID
|
||||
|
||||
deviceLabel := widget.NewLabel(deviceID)
|
||||
|
||||
selectBox := widget.NewSelect(
|
||||
editor.Options(deviceID),
|
||||
nil,
|
||||
)
|
||||
|
||||
currentPort := editor.CurrentPort(deviceID)
|
||||
|
||||
if currentPort == "" {
|
||||
selectBox.SetSelected(
|
||||
NotConnected,
|
||||
)
|
||||
} else {
|
||||
selectBox.SetSelected(
|
||||
currentPort,
|
||||
)
|
||||
}
|
||||
|
||||
selectBox.OnChanged = func(selected string) {
|
||||
if !editor.Set(
|
||||
deviceID,
|
||||
selected,
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Nach jeder Änderung werden sämtliche Selectboxen neu
|
||||
// aufgebaut. Dadurch werden gerade freigegebene Ports
|
||||
// sofort bei allen anderen Geräten sichtbar.
|
||||
a.refreshAssignmentWindow(
|
||||
window,
|
||||
editor,
|
||||
)
|
||||
}
|
||||
|
||||
objects = append(
|
||||
objects,
|
||||
deviceLabel,
|
||||
selectBox,
|
||||
)
|
||||
}
|
||||
|
||||
grid := container.New(
|
||||
layout.NewFormLayout(),
|
||||
objects...,
|
||||
)
|
||||
|
||||
info := widget.NewLabel(
|
||||
"Die Änderungen werden beim Schließen gespeichert.",
|
||||
)
|
||||
|
||||
closeButton := widget.NewButton(
|
||||
"Schließen",
|
||||
func() {
|
||||
a.handleAssignmentWindowClose(
|
||||
window,
|
||||
editor,
|
||||
)
|
||||
},
|
||||
)
|
||||
footer := container.NewVBox(
|
||||
info,
|
||||
closeButton,
|
||||
)
|
||||
|
||||
window.SetContent(
|
||||
container.NewBorder(
|
||||
nil,
|
||||
footer,
|
||||
nil,
|
||||
nil,
|
||||
container.NewVScroll(grid),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// closeAssignmentWindow closes the currently open device-assignment window.
|
||||
// editorDeviceIDs returns all server-device IDs known by the assignment
|
||||
// editor in deterministic order.
|
||||
func editorDeviceIDs(
|
||||
editor *AssignmentEditor,
|
||||
) []string {
|
||||
if editor == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
deviceIDs := make(
|
||||
[]string,
|
||||
0,
|
||||
len(editor.Current),
|
||||
)
|
||||
|
||||
for deviceID := range editor.Current {
|
||||
deviceIDs = append(
|
||||
deviceIDs,
|
||||
deviceID,
|
||||
)
|
||||
}
|
||||
|
||||
sort.Strings(deviceIDs)
|
||||
|
||||
return deviceIDs
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Assignment saving
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// handleAssignmentWindowClose handles closing of the assignment window.
|
||||
//
|
||||
// This is deliberately kept separate from Close() so that the child window
|
||||
// can be closed without changing any configuration or technical client
|
||||
// state.
|
||||
func (a *App) closeAssignmentWindow() {
|
||||
// The "Schließen" button saves changed assignments. If there are no changes,
|
||||
// the window is simply closed.
|
||||
func (a *App) handleAssignmentWindowClose(
|
||||
window fyne.Window,
|
||||
editor *AssignmentEditor,
|
||||
) {
|
||||
if a == nil || window == nil || editor == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !editor.Dirty() {
|
||||
a.closeAssignmentWindowWithoutPrompt(
|
||||
window,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
a.saveAssignmentsAndClose(
|
||||
window,
|
||||
editor,
|
||||
)
|
||||
}
|
||||
|
||||
// saveAssignmentsAndClose converts the editor state into a client
|
||||
// configuration, saves it through client.Application and closes the
|
||||
// assignment window only after a successful save.
|
||||
func (a *App) saveAssignmentsAndClose(
|
||||
window fyne.Window,
|
||||
editor *AssignmentEditor,
|
||||
) {
|
||||
if a == nil || window == nil || editor == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := a.clientApplication.Config()
|
||||
if cfg == nil {
|
||||
dialog.ShowError(
|
||||
fmt.Errorf(
|
||||
"Client-Konfiguration ist nicht verfügbar",
|
||||
),
|
||||
window,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
updatedCfg := editor.ApplyToConfig(cfg)
|
||||
if updatedCfg == nil {
|
||||
dialog.ShowError(
|
||||
fmt.Errorf(
|
||||
"Gerätezuordnung konnte nicht übernommen werden",
|
||||
),
|
||||
window,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.clientApplication.SaveConfig(
|
||||
updatedCfg,
|
||||
); err != nil {
|
||||
dialog.ShowError(
|
||||
fmt.Errorf(
|
||||
"Client-Konfiguration konnte nicht gespeichert werden: %w",
|
||||
err,
|
||||
),
|
||||
window,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Erst nach erfolgreichem Speichern schließen.
|
||||
a.closeAssignmentWindowWithoutPrompt(
|
||||
window,
|
||||
)
|
||||
}
|
||||
|
||||
// closeAssignmentWindowWithoutPrompt closes the assignment window without
|
||||
// invoking its close intercept again.
|
||||
func (a *App) closeAssignmentWindowWithoutPrompt(
|
||||
window fyne.Window,
|
||||
) {
|
||||
if window == nil {
|
||||
return
|
||||
}
|
||||
|
||||
window.SetCloseIntercept(nil)
|
||||
window.Close()
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Main window closing
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// handleMainWindowClose handles closing of the main window.
|
||||
//
|
||||
// If the assignment window is open and contains unsaved changes, the user
|
||||
// must explicitly decide whether those changes should be discarded.
|
||||
func (a *App) handleMainWindowClose() {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if a.assignmentWindow == nil {
|
||||
if a.assignmentEditor == nil ||
|
||||
!a.assignmentEditor.Dirty() {
|
||||
|
||||
a.finishMainWindowClose()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
window := a.assignmentWindow
|
||||
a.assignmentWindow = nil
|
||||
window.Close()
|
||||
confirm := dialog.NewConfirm(
|
||||
"Ungespeicherte Änderungen",
|
||||
"Die Gerätezuordnung enthält ungespeicherte "+
|
||||
"Änderungen.\n\n"+
|
||||
"Wenn das Hauptfenster geschlossen wird, "+
|
||||
"gehen diese Änderungen verloren.\n\n"+
|
||||
"Möchtest du die Änderungen verwerfen und "+
|
||||
"den Client schließen?",
|
||||
func(discard bool) {
|
||||
if !discard {
|
||||
return
|
||||
}
|
||||
|
||||
a.finishMainWindowClose()
|
||||
},
|
||||
a.window,
|
||||
)
|
||||
|
||||
confirm.SetConfirmText(
|
||||
"Änderungen verwerfen",
|
||||
)
|
||||
|
||||
confirm.SetDismissText(
|
||||
"Abbrechen",
|
||||
)
|
||||
|
||||
confirm.Show()
|
||||
}
|
||||
|
||||
// finishMainWindowClose closes the assignment window first and then the
|
||||
// main application window.
|
||||
//
|
||||
// The close intercepts are removed because the user has already confirmed
|
||||
// the operation.
|
||||
func (a *App) finishMainWindowClose() {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if a.assignmentWindow != nil {
|
||||
window := a.assignmentWindow
|
||||
|
||||
window.SetCloseIntercept(nil)
|
||||
window.Close()
|
||||
|
||||
a.assignmentWindow = nil
|
||||
a.assignmentEditor = nil
|
||||
}
|
||||
|
||||
a.window.SetCloseIntercept(nil)
|
||||
a.window.Close()
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Public lifecycle
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Close closes the technical client application.
|
||||
//
|
||||
// Any open child GUI window is closed first. The technical client is then
|
||||
// shut down.
|
||||
// This method is also used by cmd/rs2322tcp-client after the Fyne main
|
||||
// window has actually closed. Any remaining assignment window is closed
|
||||
// without prompting because the main-window close handling has already
|
||||
// taken place.
|
||||
func (a *App) Close() {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.closeAssignmentWindow()
|
||||
if a.assignmentWindow != nil {
|
||||
window := a.assignmentWindow
|
||||
|
||||
window.SetCloseIntercept(nil)
|
||||
window.Close()
|
||||
|
||||
a.assignmentWindow = nil
|
||||
a.assignmentEditor = nil
|
||||
}
|
||||
|
||||
_ = a.clientApplication.Close()
|
||||
}
|
||||
|
||||
// ShowAndRun displays the main window and starts the Fyne event loop.
|
||||
//
|
||||
// This method blocks until the GUI application terminates.
|
||||
func (a *App) ShowAndRun() {
|
||||
if a == nil {
|
||||
return
|
||||
|
|
@ -379,9 +651,6 @@ func (a *App) ShowAndRun() {
|
|||
}
|
||||
|
||||
// Window returns the main application window.
|
||||
//
|
||||
// The method is provided for the program entry point and for later GUI
|
||||
// initialization that needs access to the window.
|
||||
func (a *App) Window() fyne.Window {
|
||||
if a == nil {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -7,29 +7,34 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* GUI-unabhängige Aufbereitung der Zuordnung zwischen lokalen virtuellen
|
||||
* seriellen Schnittstellen und den vom rs2322tcp-Server angebotenen Geräten.
|
||||
* GUI-unabhängige Aufbereitung und Bearbeitung der Zuordnung zwischen den
|
||||
* vom rs2322tcp-Server angebotenen Geräten und den lokalen virtuellen
|
||||
* seriellen Schnittstellen.
|
||||
*
|
||||
* Die Datei enthält bewusst keine Fyne-Abhängigkeiten.
|
||||
*
|
||||
* Die lokale Client-Konfiguration beschreibt ausschließlich die tatsächlich
|
||||
* gewünschten Verbindungen. Physisch vorhandene /dev/ttyUSB-Schnittstellen
|
||||
* außerhalb des von rs2322tcp verwalteten virtuellen Bereichs werden hier
|
||||
* nicht betrachtet.
|
||||
* Die Änderungen an der Gerätezuordnung werden zunächst ausschließlich im
|
||||
* Arbeitsspeicher des Zuordnungsfensters gehalten. Das Speichern in die
|
||||
* Client-Konfiguration erfolgt beim Schließen des Fensters.
|
||||
*
|
||||
* Ein Server-Gerät kann daher in drei Zuständen erscheinen:
|
||||
* Regeln:
|
||||
*
|
||||
* 1. lokal zugeordnet und aktuell vom Server angeboten
|
||||
* -> verfügbar / verbunden
|
||||
* - Ein Server-Gerät kann höchstens einer lokalen Schnittstelle zugeordnet
|
||||
* werden.
|
||||
*
|
||||
* 2. lokal zugeordnet, aber momentan nicht vom Server angeboten
|
||||
* -> nicht verfügbar
|
||||
* - Eine lokale Schnittstelle kann höchstens einem Server-Gerät zugeordnet
|
||||
* werden.
|
||||
*
|
||||
* 3. vom Server angeboten, aber keiner lokalen Schnittstelle zugeordnet
|
||||
* -> noch nicht verwendet
|
||||
* - "nicht verbunden" ist ein gültiger Zustand.
|
||||
*
|
||||
* Nicht verwendete Server-Geräte werden nicht Bestandteil der
|
||||
* Client-Konfiguration.
|
||||
* - Ist ein Gerät bereits verbunden, kann es nur auf seinen aktuellen Port
|
||||
* oder auf "nicht verbunden" gesetzt werden.
|
||||
*
|
||||
* - Ist ein Gerät nicht verbunden, kann es auf "nicht verbunden" oder auf
|
||||
* einen momentan freien konfigurierten virtuellen Port gesetzt werden.
|
||||
*
|
||||
* - Wird ein Gerät getrennt, bleibt der virtuelle Port erhalten und wird
|
||||
* lediglich frei.
|
||||
* ============================================================================
|
||||
*/
|
||||
package gui
|
||||
|
|
@ -39,6 +44,15 @@ import (
|
|||
"git.lang-dieter.de/rs2322tcp/internal/transport"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Constants
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const (
|
||||
// NotConnected is the GUI representation of an unassigned server device.
|
||||
NotConnected = "nicht verbunden"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Device assignment
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -49,15 +63,262 @@ import (
|
|||
//
|
||||
// Available gibt an, ob das konfigurierte Remote-Gerät momentan in der
|
||||
// vom Server gelieferten Geräteliste vorhanden ist.
|
||||
//
|
||||
// Available == false bedeutet nicht, dass die Konfiguration ungültig ist.
|
||||
// Das Gerät kann lediglich momentan nicht vom Server angeboten werden.
|
||||
type DeviceAssignment struct {
|
||||
LocalPort string
|
||||
RemoteDevice string
|
||||
Available bool
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Assignment editor
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// AssignmentEditor enthält den momentan im GUI bearbeiteten
|
||||
// Zuordnungszustand.
|
||||
//
|
||||
// Original enthält den Zustand beim Öffnen des Fensters.
|
||||
//
|
||||
// Current enthält den momentan vom Anwender bearbeiteten Zustand.
|
||||
//
|
||||
// Der Schlüssel ist jeweils die stabile Remote-Device-ID. Ein leerer
|
||||
// Port bedeutet "nicht verbunden".
|
||||
//
|
||||
// Ports enthält alle konfigurierten virtuellen Ports. Diese Ports bleiben
|
||||
// auch dann erhalten, wenn sie momentan keinem Remote-Gerät zugeordnet sind.
|
||||
type AssignmentEditor struct {
|
||||
Original map[string]string
|
||||
Current map[string]string
|
||||
Ports []string
|
||||
}
|
||||
|
||||
// NewAssignmentEditor erzeugt einen bearbeitbaren Zuordnungszustand aus
|
||||
// der bestehenden Client-Konfiguration und der aktuellen Geräteliste.
|
||||
//
|
||||
// Es werden ausschließlich bereits konfigurierte virtuelle Ports betrachtet.
|
||||
// Das Anlegen neuer virtueller Ports erfolgt in einem späteren
|
||||
// Entwicklungsschritt.
|
||||
func NewAssignmentEditor(
|
||||
cfg *config.ClientConfig,
|
||||
devices []transport.RemoteDeviceInfo,
|
||||
) *AssignmentEditor {
|
||||
editor := &AssignmentEditor{
|
||||
Original: make(map[string]string),
|
||||
Current: make(map[string]string),
|
||||
Ports: make([]string, 0),
|
||||
}
|
||||
|
||||
if cfg != nil {
|
||||
for _, virtualPort := range cfg.VirtualPorts {
|
||||
if virtualPort.Port == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !containsString(
|
||||
editor.Ports,
|
||||
virtualPort.Port,
|
||||
) {
|
||||
editor.Ports = append(
|
||||
editor.Ports,
|
||||
virtualPort.Port,
|
||||
)
|
||||
}
|
||||
|
||||
if virtualPort.RemoteDevice == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
editor.Current[virtualPort.RemoteDevice] =
|
||||
virtualPort.Port
|
||||
}
|
||||
}
|
||||
|
||||
for _, device := range devices {
|
||||
if _, exists := editor.Current[device.ID]; !exists {
|
||||
editor.Current[device.ID] = ""
|
||||
}
|
||||
}
|
||||
|
||||
editor.Original = cloneAssignments(
|
||||
editor.Current,
|
||||
)
|
||||
|
||||
return editor
|
||||
}
|
||||
|
||||
// CurrentPort returns the currently selected local port for a server device.
|
||||
//
|
||||
// An empty string means "nicht verbunden".
|
||||
func (e *AssignmentEditor) CurrentPort(
|
||||
deviceID string,
|
||||
) string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return e.Current[deviceID]
|
||||
}
|
||||
|
||||
// Options returns the currently valid Selectbox options for one server
|
||||
// device.
|
||||
//
|
||||
// For an already connected device:
|
||||
//
|
||||
// - current port
|
||||
// - nicht verbunden
|
||||
//
|
||||
// For an unconnected device:
|
||||
//
|
||||
// - nicht verbunden
|
||||
// - all currently free configured virtual ports
|
||||
func (e *AssignmentEditor) Options(
|
||||
deviceID string,
|
||||
) []string {
|
||||
if e == nil {
|
||||
return []string{
|
||||
NotConnected,
|
||||
}
|
||||
}
|
||||
|
||||
currentPort := e.Current[deviceID]
|
||||
|
||||
if currentPort != "" {
|
||||
return []string{
|
||||
currentPort,
|
||||
NotConnected,
|
||||
}
|
||||
}
|
||||
|
||||
options := []string{
|
||||
NotConnected,
|
||||
}
|
||||
|
||||
for _, port := range e.Ports {
|
||||
if e.portUsedByOtherDevice(
|
||||
port,
|
||||
deviceID,
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
options = append(
|
||||
options,
|
||||
port,
|
||||
)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// Set changes the current assignment of one server device.
|
||||
//
|
||||
// An empty port or NotConnected disconnects the device.
|
||||
//
|
||||
// A non-empty port is accepted only when it is a configured virtual port
|
||||
// and is not currently used by another server device.
|
||||
//
|
||||
// The function returns true when the current state was changed.
|
||||
func (e *AssignmentEditor) Set(
|
||||
deviceID string,
|
||||
port string,
|
||||
) bool {
|
||||
if e == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if port == NotConnected {
|
||||
port = ""
|
||||
}
|
||||
|
||||
currentPort := e.Current[deviceID]
|
||||
|
||||
if currentPort == port {
|
||||
return false
|
||||
}
|
||||
|
||||
if port != "" {
|
||||
if !containsString(
|
||||
e.Ports,
|
||||
port,
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if e.portUsedByOtherDevice(
|
||||
port,
|
||||
deviceID,
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
e.Current[deviceID] = port
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Dirty reports whether the current assignment state differs from the
|
||||
// state that was present when the editor was opened.
|
||||
func (e *AssignmentEditor) Dirty() bool {
|
||||
if e == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(e.Original) != len(e.Current) {
|
||||
return true
|
||||
}
|
||||
|
||||
for deviceID, originalPort := range e.Original {
|
||||
if e.Current[deviceID] != originalPort {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ApplyToConfig applies the current assignment state to a copy of the
|
||||
// supplied client configuration.
|
||||
//
|
||||
// Existing virtual ports are always preserved. Only their RemoteDevice
|
||||
// value is changed.
|
||||
//
|
||||
// An empty RemoteDevice means that the virtual port currently has no
|
||||
// server-device assignment.
|
||||
//
|
||||
// The original configuration is not modified.
|
||||
func (e *AssignmentEditor) ApplyToConfig(
|
||||
cfg *config.ClientConfig,
|
||||
) *config.ClientConfig {
|
||||
if e == nil || cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := *cfg
|
||||
|
||||
result.VirtualPorts = make(
|
||||
[]config.VirtualPortConfig,
|
||||
len(cfg.VirtualPorts),
|
||||
)
|
||||
|
||||
for i, virtualPort := range cfg.VirtualPorts {
|
||||
result.VirtualPorts[i] = virtualPort
|
||||
|
||||
// Every existing virtual port remains in the configuration.
|
||||
// It becomes unassigned unless the current GUI state assigns it
|
||||
// to a server device.
|
||||
result.VirtualPorts[i].RemoteDevice = ""
|
||||
|
||||
for deviceID, port := range e.Current {
|
||||
if port == virtualPort.Port {
|
||||
result.VirtualPorts[i].RemoteDevice = deviceID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Build assignments
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -67,9 +328,6 @@ type DeviceAssignment struct {
|
|||
//
|
||||
// Es werden ausschließlich die in der Client-Konfiguration vorhandenen
|
||||
// lokalen virtuellen Ports berücksichtigt.
|
||||
//
|
||||
// Physisch vorhandene serielle Schnittstellen des lokalen Rechners werden
|
||||
// hier nicht gesucht und nicht bewertet.
|
||||
func BuildAssignments(
|
||||
cfg *config.ClientConfig,
|
||||
devices []transport.RemoteDeviceInfo,
|
||||
|
|
@ -78,17 +336,24 @@ func BuildAssignments(
|
|||
return nil
|
||||
}
|
||||
|
||||
assignments := make([]DeviceAssignment, 0, len(cfg.VirtualPorts))
|
||||
assignments := make(
|
||||
[]DeviceAssignment,
|
||||
0,
|
||||
len(cfg.VirtualPorts),
|
||||
)
|
||||
|
||||
for _, virtualPort := range cfg.VirtualPorts {
|
||||
assignments = append(assignments, DeviceAssignment{
|
||||
assignments = append(
|
||||
assignments,
|
||||
DeviceAssignment{
|
||||
LocalPort: virtualPort.Port,
|
||||
RemoteDevice: virtualPort.RemoteDevice,
|
||||
Available: remoteDeviceExists(
|
||||
virtualPort.RemoteDevice,
|
||||
devices,
|
||||
),
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return assignments
|
||||
|
|
@ -100,24 +365,28 @@ func BuildAssignments(
|
|||
|
||||
// UnassignedDevices liefert die vom Server angebotenen Geräte, die aktuell
|
||||
// keiner lokalen virtuellen Schnittstelle zugeordnet sind.
|
||||
//
|
||||
// Diese Geräte werden bewusst nicht in die Client-Konfiguration übernommen.
|
||||
// Erst wenn der Anwender im GUI eine Zuordnung vornimmt, entsteht daraus
|
||||
// ein Eintrag in ClientConfig.VirtualPorts.
|
||||
func UnassignedDevices(
|
||||
cfg *config.ClientConfig,
|
||||
devices []transport.RemoteDeviceInfo,
|
||||
) []transport.RemoteDeviceInfo {
|
||||
assigned := assignedRemoteDevices(cfg)
|
||||
assigned := assignedRemoteDevices(
|
||||
cfg,
|
||||
)
|
||||
|
||||
unassigned := make([]transport.RemoteDeviceInfo, 0)
|
||||
unassigned := make(
|
||||
[]transport.RemoteDeviceInfo,
|
||||
0,
|
||||
)
|
||||
|
||||
for _, device := range devices {
|
||||
if assigned[device.ID] {
|
||||
continue
|
||||
}
|
||||
|
||||
unassigned = append(unassigned, device)
|
||||
unassigned = append(
|
||||
unassigned,
|
||||
device,
|
||||
)
|
||||
}
|
||||
|
||||
return unassigned
|
||||
|
|
@ -127,14 +396,33 @@ func UnassignedDevices(
|
|||
// Internal helpers
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// portUsedByOtherDevice prüft, ob ein Port momentan einem anderen
|
||||
// Server-Gerät zugeordnet ist.
|
||||
func (e *AssignmentEditor) portUsedByOtherDevice(
|
||||
port string,
|
||||
deviceID string,
|
||||
) bool {
|
||||
for otherDeviceID, assignedPort := range e.Current {
|
||||
if otherDeviceID == deviceID {
|
||||
continue
|
||||
}
|
||||
|
||||
if assignedPort == port {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// assignedRemoteDevices erzeugt eine Menge der Remote-Geräte, die bereits
|
||||
// einer lokalen virtuellen Schnittstelle zugeordnet sind.
|
||||
//
|
||||
// Die Remote-Device-ID ist der stabile Schlüssel der Zuordnung.
|
||||
func assignedRemoteDevices(
|
||||
cfg *config.ClientConfig,
|
||||
) map[string]bool {
|
||||
assigned := make(map[string]bool)
|
||||
assigned := make(
|
||||
map[string]bool,
|
||||
)
|
||||
|
||||
if cfg == nil {
|
||||
return assigned
|
||||
|
|
@ -165,3 +453,33 @@ func remoteDeviceExists(
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
// cloneAssignments erzeugt eine unabhängige Kopie einer Zuordnungskarte.
|
||||
func cloneAssignments(
|
||||
assignments map[string]string,
|
||||
) map[string]string {
|
||||
clone := make(
|
||||
map[string]string,
|
||||
len(assignments),
|
||||
)
|
||||
|
||||
for deviceID, port := range assignments {
|
||||
clone[deviceID] = port
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// containsString prüft, ob ein String in einer Liste enthalten ist.
|
||||
func containsString(
|
||||
values []string,
|
||||
value string,
|
||||
) bool {
|
||||
for _, current := range values {
|
||||
if current == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue