485 lines
11 KiB
Go
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
|
|
}
|