rs2322tcp/internal/gui/assignment.go
2026-08-12 12:42:24 +02:00

485 lines
11 KiB
Go

/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: internal/gui/assignment.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* 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 Ä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.
*
* Regeln:
*
* - Ein Server-Gerät kann höchstens einer lokalen Schnittstelle zugeordnet
* werden.
*
* - Eine lokale Schnittstelle kann höchstens einem Server-Gerät zugeordnet
* werden.
*
* - "nicht verbunden" ist ein gültiger Zustand.
*
* - 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
import (
"git.lang-dieter.de/rs2322tcp/internal/config"
"git.lang-dieter.de/rs2322tcp/internal/transport"
)
///////////////////////////////////////////////////////////////////////////////
// Constants
///////////////////////////////////////////////////////////////////////////////
const (
// NotConnected is the GUI representation of an unassigned server device.
NotConnected = "nicht verbunden"
)
///////////////////////////////////////////////////////////////////////////////
// Device assignment
///////////////////////////////////////////////////////////////////////////////
// DeviceAssignment beschreibt eine bereits in der Client-Konfiguration
// vorhandene Zuordnung zwischen einer lokalen virtuellen Schnittstelle
// und einem entfernten Gerät.
//
// Available gibt an, ob das konfigurierte Remote-Gerät momentan in der
// vom Server gelieferten Geräteliste vorhanden ist.
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
///////////////////////////////////////////////////////////////////////////////
// BuildAssignments erzeugt die Zuordnungsansicht aus der bestehenden
// Client-Konfiguration und der aktuell vom Server gelieferten Geräteliste.
//
// Es werden ausschließlich die in der Client-Konfiguration vorhandenen
// lokalen virtuellen Ports berücksichtigt.
func BuildAssignments(
cfg *config.ClientConfig,
devices []transport.RemoteDeviceInfo,
) []DeviceAssignment {
if cfg == nil {
return nil
}
assignments := make(
[]DeviceAssignment,
0,
len(cfg.VirtualPorts),
)
for _, virtualPort := range cfg.VirtualPorts {
assignments = append(
assignments,
DeviceAssignment{
LocalPort: virtualPort.Port,
RemoteDevice: virtualPort.RemoteDevice,
Available: remoteDeviceExists(
virtualPort.RemoteDevice,
devices,
),
},
)
}
return assignments
}
///////////////////////////////////////////////////////////////////////////////
// Unassigned devices
///////////////////////////////////////////////////////////////////////////////
// UnassignedDevices liefert die vom Server angebotenen Geräte, die aktuell
// keiner lokalen virtuellen Schnittstelle zugeordnet sind.
func UnassignedDevices(
cfg *config.ClientConfig,
devices []transport.RemoteDeviceInfo,
) []transport.RemoteDeviceInfo {
assigned := assignedRemoteDevices(
cfg,
)
unassigned := make(
[]transport.RemoteDeviceInfo,
0,
)
for _, device := range devices {
if assigned[device.ID] {
continue
}
unassigned = append(
unassigned,
device,
)
}
return unassigned
}
///////////////////////////////////////////////////////////////////////////////
// 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.
func assignedRemoteDevices(
cfg *config.ClientConfig,
) map[string]bool {
assigned := make(
map[string]bool,
)
if cfg == nil {
return assigned
}
for _, virtualPort := range cfg.VirtualPorts {
if virtualPort.RemoteDevice == "" {
continue
}
assigned[virtualPort.RemoteDevice] = true
}
return assigned
}
// remoteDeviceExists prüft, ob ein Remote-Gerät mit der angegebenen ID in
// der aktuellen Geräteliste des Servers vorhanden ist.
func remoteDeviceExists(
id string,
devices []transport.RemoteDeviceInfo,
) bool {
for _, device := range devices {
if device.ID == id {
return true
}
}
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
}