/* * ============================================================================ * 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 }