From 39a1fea75da6f15263a0b0227032922adf5d3698 Mon Sep 17 00:00:00 2001 From: Dieter Lang Date: Fri, 21 Aug 2026 11:02:25 +0200 Subject: [PATCH] technischen Windows-Client erfolgreich mit Rot2Prog getestet --- cmd/rs2322tcp-client-test/main.go | 44 ++++ configs/client.json | 10 +- internal/client/connection.go | 49 +--- internal/client/connection_test.go | 39 +-- internal/client/virtual_port_manager.go | 51 +++- internal/client/virtual_port_manager_open.go | 163 ++++++++++++ internal/client/virtual_port_manager_test.go | 12 +- internal/client/virtual_port_number_linux.go | 59 +++++ .../client/virtual_port_number_windows.go | 62 +++++ internal/client/virtual_port_open_linux.go | 77 ++++++ internal/client/virtual_port_open_windows.go | 84 +++++++ internal/client/virtual_port_path_linux.go | 43 ++++ internal/client/virtual_port_path_windows.go | 48 ++++ internal/client/virtual_port_validation.go | 188 +++++++++++++- .../client/virtual_port_validation_linux.go | 234 ++++++------------ .../client/virtual_port_validation_windows.go | 132 ++++++++++ internal/client/virtual_serial_windows.go | 114 +++++++++ 17 files changed, 1154 insertions(+), 255 deletions(-) create mode 100644 cmd/rs2322tcp-client-test/main.go create mode 100644 internal/client/virtual_port_manager_open.go create mode 100644 internal/client/virtual_port_number_linux.go create mode 100644 internal/client/virtual_port_number_windows.go create mode 100644 internal/client/virtual_port_open_linux.go create mode 100644 internal/client/virtual_port_open_windows.go create mode 100644 internal/client/virtual_port_path_linux.go create mode 100644 internal/client/virtual_port_path_windows.go create mode 100644 internal/client/virtual_port_validation_windows.go create mode 100644 internal/client/virtual_serial_windows.go diff --git a/cmd/rs2322tcp-client-test/main.go b/cmd/rs2322tcp-client-test/main.go new file mode 100644 index 0000000..034878f --- /dev/null +++ b/cmd/rs2322tcp-client-test/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "git.lang-dieter.de/rs2322tcp/internal/client" +) + +func main() { + const configFile = "./configs/client.json" + + application, err := client.NewApplication(configFile) + if err != nil { + fmt.Fprintf(os.Stderr, "NewApplication: %v\n", err) + os.Exit(1) + } + + if err := application.Start(); err != nil { + fmt.Fprintf(os.Stderr, "Application.Start: %v\n", err) + os.Exit(1) + } + + fmt.Println("rs2322tcp technical client started") + fmt.Println("Press Ctrl+C to stop.") + + signals := make(chan os.Signal, 1) + signal.Notify( + signals, + os.Interrupt, + syscall.SIGTERM, + ) + + <-signals + + fmt.Println("Stopping client...") + + if err := application.Close(); err != nil { + fmt.Fprintf(os.Stderr, "Application.Close: %v\n", err) + os.Exit(1) + } +} diff --git a/configs/client.json b/configs/client.json index 6541eaf..d8f17ac 100644 --- a/configs/client.json +++ b/configs/client.json @@ -1,6 +1,6 @@ { "server": { - "address": "127.0.0.1", + "address": "100.64.0.1", "port": 5000 }, "virtual_port_range": { @@ -9,12 +9,8 @@ }, "virtual_ports": [ { - "port": "/dev/ttyUSB100", - "remote_device": "radio" - }, - { - "port": "/dev/ttyUSB101", + "port": "COM100", "remote_device": "rotor" } ] -} +} \ No newline at end of file diff --git a/internal/client/connection.go b/internal/client/connection.go index fc16f38..30497de 100644 --- a/internal/client/connection.go +++ b/internal/client/connection.go @@ -11,12 +11,7 @@ * und dem zugehörigen entfernten Gerät. * * Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt - * übernommen. Eine Konfiguration wie - * - * /dev/ttyUSB100 -> radio - * - * führt somit technisch auch zur Erzeugung beziehungsweise Verwendung von - * /dev/ttyUSB100 und nicht einfach zum nächsten freien Port. + * übernommen. * ============================================================================ */ package client @@ -24,53 +19,11 @@ package client import ( "fmt" "net" - "strconv" - "strings" "git.lang-dieter.de/rs2322tcp/internal/config" "git.lang-dieter.de/rs2322tcp/internal/transport" ) -/////////////////////////////////////////////////////////////////////////////// -// Virtual port configuration -/////////////////////////////////////////////////////////////////////////////// - -// virtualPortNumber converts a configured /dev/ttyUSBxxx path into its -// numeric virtual port number. -// -// The client configuration stores the complete Linux device path, while -// VirtualPortManager works with the numeric port number. Only the exact -// /dev/ttyUSB format is accepted here. -func virtualPortNumber(path string) (int, error) { - const prefix = "/dev/ttyUSB" - - if !strings.HasPrefix(path, prefix) { - return 0, fmt.Errorf( - "invalid virtual port %q: expected /dev/ttyUSB", - path, - ) - } - - value := strings.TrimPrefix(path, prefix) - - if value == "" { - return 0, fmt.Errorf( - "invalid virtual port %q: port number is missing", - path, - ) - } - - number, err := strconv.Atoi(value) - if err != nil { - return 0, fmt.Errorf( - "invalid virtual port %q: invalid port number", - path, - ) - } - - return number, nil -} - /////////////////////////////////////////////////////////////////////////////// // Virtual port connection /////////////////////////////////////////////////////////////////////////////// diff --git a/internal/client/connection_test.go b/internal/client/connection_test.go index 74f9192..a074eac 100644 --- a/internal/client/connection_test.go +++ b/internal/client/connection_test.go @@ -144,7 +144,7 @@ func TestConnectVirtualPortNilClient(t *testing.T) { ) cfg := config.VirtualPortConfig{ - Port: "/dev/ttyUSB100", + Port: virtualPortPath(100), RemoteDevice: "radio", } @@ -172,7 +172,7 @@ func TestConnectVirtualPortNilClient(t *testing.T) { func TestConnectVirtualPortNilManager(t *testing.T) { cfg := config.VirtualPortConfig{ - Port: "/dev/ttyUSB100", + Port: virtualPortPath(100), RemoteDevice: "radio", } @@ -209,7 +209,7 @@ func TestConnectVirtualPortUnknownDevice(t *testing.T) { client := newTestClient(t) cfg := config.VirtualPortConfig{ - Port: "/dev/ttyUSB100", + Port: virtualPortPath(100), RemoteDevice: "unknown", } @@ -249,7 +249,7 @@ func TestConnectVirtualPortClosesPortWhenDataConnectionFails(t *testing.T) { client := newTestClient(t) cfg := config.VirtualPortConfig{ - Port: "/dev/ttyUSB100", + Port: virtualPortPath(100), RemoteDevice: "radio", } @@ -323,7 +323,7 @@ func TestConnectVirtualPortSuccess(t *testing.T) { ) cfg := config.VirtualPortConfig{ - Port: "/dev/ttyUSB100", + Port: virtualPortPath(100), RemoteDevice: "radio", } @@ -348,11 +348,11 @@ func TestConnectVirtualPortSuccess(t *testing.T) { defer virtualPort.Close() defer dataConn.Close() - if virtualPort.Path() != "/dev/ttyUSB100" { + if virtualPort.Path() != virtualPortPath(100) { t.Fatalf( "virtual port path = %q, want %q", virtualPort.Path(), - "/dev/ttyUSB100", + virtualPortPath(100), ) } @@ -386,7 +386,7 @@ func TestConnectVirtualPortUsesConfiguredPort(t *testing.T) { ) cfg := config.VirtualPortConfig{ - Port: "/dev/ttyUSB150", + Port: virtualPortPath(100), RemoteDevice: "radio", } @@ -411,24 +411,29 @@ func TestConnectVirtualPortUsesConfiguredPort(t *testing.T) { defer virtualPort.Close() defer dataConn.Close() - if virtualPort.Path() != "/dev/ttyUSB150" { + if virtualPort.Path() != virtualPortPath(100) { t.Fatalf( "virtual port path = %q, want %q", virtualPort.Path(), - "/dev/ttyUSB150", + virtualPortPath(100), ) } - // Port 100 must still be available. This proves that the connection - // did not simply take the first free port from the configured range. + // Port 100 is the configured port and therefore must be reserved. + if !manager.used[100] { + t.Fatal("configured port 100 is not reserved") + } + + // Port 101 must still be available. This proves that the connection + // did not simply take another free port. number, err := manager.Reserve() if err != nil { t.Fatalf("Reserve() after connection: %v", err) } - if number != 100 { + if number != 101 { t.Fatalf( - "first free port after connection = %d, want 100", + "first free port after connection = %d, want 101", number, ) } @@ -463,7 +468,7 @@ func TestConnectBridgeSuccess(t *testing.T) { ) cfg := config.VirtualPortConfig{ - Port: "/dev/ttyUSB100", + Port: virtualPortPath(100), RemoteDevice: "radio", } @@ -500,11 +505,11 @@ func TestConnectBridgeSuccess(t *testing.T) { t.Fatal("bridge TCP endpoint does not match data connection") } - if virtualPort.Path() != "/dev/ttyUSB100" { + if virtualPort.Path() != virtualPortPath(100) { t.Fatalf( "virtual port path = %q, want %q", virtualPort.Path(), - "/dev/ttyUSB100", + virtualPortPath(100), ) } diff --git a/internal/client/virtual_port_manager.go b/internal/client/virtual_port_manager.go index a365f90..baf675d 100644 --- a/internal/client/virtual_port_manager.go +++ b/internal/client/virtual_port_manager.go @@ -11,19 +11,21 @@ * seriellen Ports. * * Der VirtualPortManager verwaltet den für rs2322tcp vorgesehenen Bereich - * von /dev/ttyUSBxxx-Schnittstellen. + * virtueller serieller Schnittstellen. + * + * Die konkrete Darstellung eines virtuellen Ports ist plattformabhängig. + * Unter Linux werden beispielsweise /dev/ttyUSBxxx-Schnittstellen verwendet, + * während unter Windows COMxxx-Schnittstellen verwendet werden. + * + * Die eigentliche Erzeugung beziehungsweise Bereitstellung des seriellen + * Ports erfolgt ebenfalls plattformabhängig und ist nicht Bestandteil dieses + * Managers. * * Ports können entweder automatisch über den nächsten freien Port oder * gezielt über eine vorgegebene Portnummer reserviert werden. - * - * Die gezielte Reservierung ist insbesondere für die aus client.json - * geladene Konfiguration erforderlich. Dadurch bleibt eine Zuordnung wie - * - * /dev/ttyUSB100 -> radio - * - * auch technisch exakt erhalten. * ============================================================================ */ + package client import ( @@ -37,12 +39,15 @@ import ( // VirtualPortManager /////////////////////////////////////////////////////////////////////////////// -// VirtualPortManager verwaltet den reservierten Bereich virtueller -// /dev/ttyUSBxxx-Schnittstellen. +// VirtualPortManager verwaltet den für rs2322tcp reservierten Bereich +// virtueller serieller Schnittstellen. // // Der reservierte Portbereich und die Belegungstabelle werden durch einen // Mutex geschützt, da mehrere Bridges beziehungsweise Goroutinen gleichzeitig // virtuelle Ports öffnen und freigeben können. +// +// Die konkrete Darstellung des sichtbaren Portnamens wird über +// virtualPortPath() von der jeweiligen Plattform bestimmt. type VirtualPortManager struct { mu sync.Mutex @@ -51,6 +56,10 @@ type VirtualPortManager struct { used map[int]bool } +/////////////////////////////////////////////////////////////////////////////// +// Constructor +/////////////////////////////////////////////////////////////////////////////// + // NewVirtualPortManager erzeugt einen VirtualPortManager aus dem // konfigurierten Portbereich. func NewVirtualPortManager( @@ -63,16 +72,32 @@ func NewVirtualPortManager( } } +/////////////////////////////////////////////////////////////////////////////// +// Port path +/////////////////////////////////////////////////////////////////////////////// + // PortPath erzeugt den für die externe Software sichtbaren Gerätenamen // für eine Portnummer. +// +// Die konkrete Darstellung ist plattformabhängig: +// +// Linux /dev/ttyUSB100 +// Windows COM100 +// +// Die Plattformimplementierung befindet sich in +// virtual_port_path_linux.go beziehungsweise virtual_port_path_windows.go. func (m *VirtualPortManager) PortPath(number int) string { if m == nil { return "" } - return fmt.Sprintf("/dev/ttyUSB%d", number) + return virtualPortPath(number) } +/////////////////////////////////////////////////////////////////////////////// +// Reservation +/////////////////////////////////////////////////////////////////////////////// + // Reserve reserviert den nächsten freien virtuellen Port. // // Es wird immer mit dem kleinsten freien Port im konfigurierten Bereich @@ -141,6 +166,10 @@ func (m *VirtualPortManager) ReserveSpecific(number int) error { return nil } +/////////////////////////////////////////////////////////////////////////////// +// Release +/////////////////////////////////////////////////////////////////////////////// + // Release gibt einen zuvor reservierten Port wieder frei. func (m *VirtualPortManager) Release(number int) { if m == nil { diff --git a/internal/client/virtual_port_manager_open.go b/internal/client/virtual_port_manager_open.go new file mode 100644 index 0000000..6157b58 --- /dev/null +++ b/internal/client/virtual_port_manager_open.go @@ -0,0 +1,163 @@ +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_port_manager_open.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Plattformneutrale Verwaltung geöffneter virtueller Ports. + * + * Die konkrete Erzeugung eines virtuellen seriellen Ports erfolgt über + * openVirtualPort() in der jeweiligen Plattformimplementierung. + * ============================================================================ + */ + +package client + +import ( + "fmt" + "sync" +) + +/////////////////////////////////////////////////////////////////////////////// +// Managed virtual port +/////////////////////////////////////////////////////////////////////////////// + +// ManagedVirtualPort verbindet einen reservierten Port mit einer +// VirtualSerial-Instanz und verwaltet deren Lebenszyklus. +type ManagedVirtualPort struct { + mu sync.Mutex + + port *VirtualPort + manager *VirtualPortManager + number int +} + +// Path returns the device path visible to the external application. +func (p *ManagedVirtualPort) Path() string { + if p == nil { + return "" + } + + p.mu.Lock() + defer p.mu.Unlock() + + if p.port == nil { + return "" + } + + return p.port.Path() +} + +// Read reads data from the underlying virtual serial device. +func (p *ManagedVirtualPort) Read(b []byte) (int, error) { + if p == nil { + return 0, fmt.Errorf("managed virtual port is nil") + } + + p.mu.Lock() + port := p.port + p.mu.Unlock() + + if port == nil { + return 0, fmt.Errorf("managed virtual port is closed") + } + + return port.Read(b) +} + +// Write writes data to the underlying virtual serial device. +func (p *ManagedVirtualPort) Write(b []byte) (int, error) { + if p == nil { + return 0, fmt.Errorf("managed virtual port is nil") + } + + p.mu.Lock() + port := p.port + p.mu.Unlock() + + if port == nil { + return 0, fmt.Errorf("managed virtual port is closed") + } + + return port.Write(b) +} + +// Close closes the managed virtual port and releases its reservation. +func (p *ManagedVirtualPort) Close() error { + if p == nil { + return nil + } + + p.mu.Lock() + + port := p.port + p.port = nil + + manager := p.manager + number := p.number + + p.mu.Unlock() + + if port == nil { + return nil + } + + err := port.Close() + + if manager != nil { + manager.Release(number) + } + + return err +} + +/////////////////////////////////////////////////////////////////////////////// +// Open +/////////////////////////////////////////////////////////////////////////////// + +// Open creates a new virtual serial port using the next free port number. +func (m *VirtualPortManager) Open() (*ManagedVirtualPort, error) { + if m == nil { + return nil, fmt.Errorf("virtual port manager is nil") + } + + number, err := m.Reserve() + if err != nil { + return nil, err + } + + port, err := openVirtualPort(m, number) + if err != nil { + m.Release(number) + + return nil, err + } + + return port, nil +} + +// OpenSpecific creates a new virtual serial port using exactly the specified +// port number. +func (m *VirtualPortManager) OpenSpecific( + number int, +) (*ManagedVirtualPort, error) { + if m == nil { + return nil, fmt.Errorf("virtual port manager is nil") + } + + if err := m.ReserveSpecific(number); err != nil { + return nil, err + } + + port, err := openVirtualPort(m, number) + if err != nil { + m.Release(number) + + return nil, err + } + + return port, nil +} diff --git a/internal/client/virtual_port_manager_test.go b/internal/client/virtual_port_manager_test.go index 7d59fc3..d607036 100644 --- a/internal/client/virtual_port_manager_test.go +++ b/internal/client/virtual_port_manager_test.go @@ -107,8 +107,10 @@ func TestVirtualPortManagerRelease(t *testing.T) { } if reused != 100 { - t.Errorf("Reserve() after Release() = %d, want 100", - reused) + t.Errorf( + "Reserve() after Release() = %d, want 100", + reused, + ) } } @@ -179,15 +181,15 @@ func TestVirtualPortManagerPortPath(t *testing.T) { }{ { number: 100, - want: "/dev/ttyUSB100", + want: virtualPortPath(100), }, { number: 123, - want: "/dev/ttyUSB123", + want: virtualPortPath(123), }, { number: 199, - want: "/dev/ttyUSB199", + want: virtualPortPath(199), }, } diff --git a/internal/client/virtual_port_number_linux.go b/internal/client/virtual_port_number_linux.go new file mode 100644 index 0000000..e21f758 --- /dev/null +++ b/internal/client/virtual_port_number_linux.go @@ -0,0 +1,59 @@ +//go:build linux + +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_port_number_linux.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Linux-spezifische Umwandlung eines konfigurierten virtuellen + * seriellen Ports in dessen numerische Portnummer. + * ============================================================================ + */ + +package client + +import ( + "fmt" + "strconv" + "strings" +) + +/////////////////////////////////////////////////////////////////////////////// +// Virtual port number +/////////////////////////////////////////////////////////////////////////////// + +// virtualPortNumber converts a configured /dev/ttyUSBxxx path into its +// numeric virtual port number. +func virtualPortNumber(path string) (int, error) { + const prefix = "/dev/ttyUSB" + + if !strings.HasPrefix(path, prefix) { + return 0, fmt.Errorf( + "invalid virtual port %q: expected /dev/ttyUSB", + path, + ) + } + + value := strings.TrimPrefix(path, prefix) + + if value == "" { + return 0, fmt.Errorf( + "invalid virtual port %q: port number is missing", + path, + ) + } + + number, err := strconv.Atoi(value) + if err != nil { + return 0, fmt.Errorf( + "invalid virtual port %q: invalid port number", + path, + ) + } + + return number, nil +} diff --git a/internal/client/virtual_port_number_windows.go b/internal/client/virtual_port_number_windows.go new file mode 100644 index 0000000..33f0146 --- /dev/null +++ b/internal/client/virtual_port_number_windows.go @@ -0,0 +1,62 @@ +//go:build windows + +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_port_number_windows.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Windows-spezifische Umwandlung eines konfigurierten virtuellen + * seriellen COM-Ports in dessen numerische Portnummer. + * ============================================================================ + */ + +package client + +import ( + "fmt" + "strconv" + "strings" +) + +/////////////////////////////////////////////////////////////////////////////// +// Virtual port number +/////////////////////////////////////////////////////////////////////////////// + +// virtualPortNumber converts a configured COMxxx path into its numeric +// virtual port number. +func virtualPortNumber(path string) (int, error) { + const prefix = "COM" + + if !strings.HasPrefix( + strings.ToUpper(path), + prefix, + ) { + return 0, fmt.Errorf( + "invalid virtual port %q: expected COM", + path, + ) + } + + value := path[len(prefix):] + + if value == "" { + return 0, fmt.Errorf( + "invalid virtual port %q: port number is missing", + path, + ) + } + + number, err := strconv.Atoi(value) + if err != nil { + return 0, fmt.Errorf( + "invalid virtual port %q: invalid port number", + path, + ) + } + + return number, nil +} diff --git a/internal/client/virtual_port_open_linux.go b/internal/client/virtual_port_open_linux.go new file mode 100644 index 0000000..31d8033 --- /dev/null +++ b/internal/client/virtual_port_open_linux.go @@ -0,0 +1,77 @@ +//go:build linux + +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_port_open_linux.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Linux-spezifische Erzeugung eines verwalteten virtuellen rs2322tcp-Ports. + * + * Die virtuelle serielle Schnittstelle wird über ein PTY-Paar erzeugt. + * Der öffentliche /dev/ttyUSBxxx-Eintrag wird über den bereits vorhandenen + * internen Link auf den PTY-Slave abgebildet. + * ============================================================================ + */ + +package client + +import "fmt" + +/////////////////////////////////////////////////////////////////////////////// +// Open virtual port +/////////////////////////////////////////////////////////////////////////////// + +// openVirtualPort creates and initializes a Linux virtual serial port. +// +// The port number must already have been reserved by the +// VirtualPortManager. +// +// On failure, the caller remains responsible for releasing the reservation. +func openVirtualPort( + manager *VirtualPortManager, + number int, +) (*ManagedVirtualPort, error) { + if manager == nil { + return nil, fmt.Errorf("virtual port manager is nil") + } + + portPath := manager.PortPath(number) + + serial, err := newVirtualSerial() + if err != nil { + return nil, fmt.Errorf( + "create virtual serial for %s: %w", + portPath, + err, + ) + } + + if err := setVirtualPortLink( + portPath, + serial.Path(), + ); err != nil { + _ = serial.Close() + + return nil, fmt.Errorf( + "bind %s to %s: %w", + portPath, + serial.Path(), + err, + ) + } + + port := NewVirtualPort( + portPath, + serial, + ) + + return &ManagedVirtualPort{ + port: port, + manager: manager, + number: number, + }, nil +} diff --git a/internal/client/virtual_port_open_windows.go b/internal/client/virtual_port_open_windows.go new file mode 100644 index 0000000..28a08bb --- /dev/null +++ b/internal/client/virtual_port_open_windows.go @@ -0,0 +1,84 @@ +//go:build windows + +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_port_open_windows.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Windows-spezifische Erzeugung eines verwalteten virtuellen rs2322tcp-Ports. + * + * Der konfigurierte virtuelle COM-Port wird über die bereits vorhandene + * serielle Infrastruktur geöffnet. + * ============================================================================ + */ + +package client + +import ( + "fmt" + + bugserial "go.bug.st/serial" +) + +/////////////////////////////////////////////////////////////////////////////// +// Open virtual port +/////////////////////////////////////////////////////////////////////////////// + +// openVirtualPort creates and initializes a Windows virtual serial port. +// +// The port number must already have been reserved by the +// VirtualPortManager. +// +// The visible COM port is provided by the platform-specific PortPath() +// implementation. The actual COM-port pair configuration is managed +// externally by the Windows virtual COM-port driver. +func openVirtualPort( + manager *VirtualPortManager, + number int, +) (*ManagedVirtualPort, error) { + if manager == nil { + return nil, fmt.Errorf("virtual port manager is nil") + } + + portPath := manager.PortPath(number) + if portPath == "" { + return nil, fmt.Errorf( + "virtual port path is empty for port %d", + number, + ) + } + + mode := &bugserial.Mode{ + BaudRate: 9600, + DataBits: 8, + Parity: bugserial.NoParity, + StopBits: bugserial.OneStopBit, + } + + serial, err := bugserial.Open(portPath, mode) + if err != nil { + return nil, fmt.Errorf( + "open virtual serial port %s: %w", + portPath, + err, + ) + } + + virtualPort := NewVirtualPort( + portPath, + &windowsSerial{ + port: serial, + path: portPath, + }, + ) + + return &ManagedVirtualPort{ + port: virtualPort, + manager: manager, + number: number, + }, nil +} diff --git a/internal/client/virtual_port_path_linux.go b/internal/client/virtual_port_path_linux.go new file mode 100644 index 0000000..d0d40f4 --- /dev/null +++ b/internal/client/virtual_port_path_linux.go @@ -0,0 +1,43 @@ +//go:build linux + +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_port_path_linux.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Linux-spezifische Darstellung eines für rs2322tcp reservierten + * virtuellen seriellen Ports. + * + * Unter Linux werden die für rs2322tcp sichtbaren virtuellen Ports als + * + * /dev/ttyUSBxxx + * + * dargestellt. + * + * Die eigentliche Bereitstellung des virtuellen seriellen Ports erfolgt + * unabhängig davon in der Linux-spezifischen VirtualSerial-Implementierung. + * ============================================================================ + */ + +package client + +import "fmt" + +/////////////////////////////////////////////////////////////////////////////// +// Virtual port path +/////////////////////////////////////////////////////////////////////////////// + +// virtualPortPath erzeugt den unter Linux sichtbaren Gerätenamen für eine +// virtuelle rs2322tcp-Schnittstelle. +// +// Beispiel: +// +// number = 100 +// result = /dev/ttyUSB100 +func virtualPortPath(number int) string { + return fmt.Sprintf("/dev/ttyUSB%d", number) +} diff --git a/internal/client/virtual_port_path_windows.go b/internal/client/virtual_port_path_windows.go new file mode 100644 index 0000000..e3de888 --- /dev/null +++ b/internal/client/virtual_port_path_windows.go @@ -0,0 +1,48 @@ +//go:build windows + +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_port_path_windows.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Windows-spezifische Darstellung eines für rs2322tcp reservierten + * virtuellen seriellen Ports. + * + * Unter Windows werden die für rs2322tcp sichtbaren virtuellen Ports als + * + * COMxxx + * + * dargestellt. + * + * Die eigentliche Bereitstellung des virtuellen COM-Ports erfolgt durch die + * Windows-spezifische serielle Infrastruktur beziehungsweise später durch + * den für rs2322tcp vorgesehenen virtuellen COM-Port-Treiber. + * + * Diese Datei enthält ausschließlich die plattformabhängige Darstellung des + * Portnamens. Die gemeinsame Portverwaltung bleibt in + * virtual_port_manager.go erhalten. + * ============================================================================ + */ + +package client + +import "fmt" + +/////////////////////////////////////////////////////////////////////////////// +// Virtual port path +/////////////////////////////////////////////////////////////////////////////// + +// virtualPortPath erzeugt den unter Windows sichtbaren Gerätenamen für eine +// virtuelle rs2322tcp-Schnittstelle. +// +// Beispiel: +// +// number = 10 +// result = COM10 +func virtualPortPath(number int) string { + return fmt.Sprintf("COM%d", number) +} diff --git a/internal/client/virtual_port_validation.go b/internal/client/virtual_port_validation.go index db324a1..94e49ee 100644 --- a/internal/client/virtual_port_validation.go +++ b/internal/client/virtual_port_validation.go @@ -7,13 +7,22 @@ * SPDX-License-Identifier: GPL-3.0-or-later * * Beschreibung: - * Vergleich der in client.json definierten virtuellen Ports mit den - * tatsächlich vorhandenen virtuellen Schnittstellen. + * Plattformneutrale Prüfung der in client.json definierten virtuellen Ports + * gegen die tatsächlich vorhandenen virtuellen Schnittstellen. + * + * Die Ermittlung der vorhandenen Ports erfolgt über die + * plattformabhängige Funktion existingVirtualPortLinks(). * ============================================================================ */ package client -import "sort" +import ( + "fmt" + "sort" + "strings" + + "git.lang-dieter.de/rs2322tcp/internal/config" +) /////////////////////////////////////////////////////////////////////////////// // Virtual port validation @@ -62,10 +71,7 @@ func compareVirtualPorts( continue } - result.Missing = append( - result.Missing, - port, - ) + result.Missing = append(result.Missing, port) } for port := range availableSet { @@ -73,10 +79,7 @@ func compareVirtualPorts( continue } - result.Extra = append( - result.Extra, - port, - ) + result.Extra = append(result.Extra, port) } sort.Strings(result.Missing) @@ -84,3 +87,166 @@ func compareVirtualPorts( return result } + +/////////////////////////////////////////////////////////////////////////////// +// Local virtual-port validation +/////////////////////////////////////////////////////////////////////////////// + +// validateLocalVirtualPorts compares the virtual ports defined in +// client.json with the virtual ports currently available on the local +// computer. +// +// The detection of available ports is performed by the platform-specific +// existingVirtualPortLinks() implementation. +// +// A mismatch prevents the client from starting. +func validateLocalVirtualPorts( + cfg *config.ClientConfig, +) error { + if cfg == nil { + return fmt.Errorf("client configuration is nil") + } + + configured := make( + []string, + 0, + len(cfg.VirtualPorts), + ) + + for _, virtualPort := range cfg.VirtualPorts { + configured = append( + configured, + virtualPort.Port, + ) + } + + available, err := existingVirtualPortLinks( + cfg.VirtualPortRange, + ) + if err != nil { + return fmt.Errorf( + "prüfen der virtuellen Ports fehlgeschlagen: %w", + err, + ) + } + + result := compareVirtualPorts( + configured, + available, + ) + + if len(result.Missing) == 0 && + len(result.Extra) == 0 { + return nil + } + + var message strings.Builder + + //////////////////////////////////////////////////////////////////////////// + // Missing ports + //////////////////////////////////////////////////////////////////////////// + + if len(result.Missing) > 0 && + len(result.Extra) == 0 { + + message.WriteString( + "Virtuelle Ports nicht eingerichtet", + ) + + message.WriteString( + "\n\nFolgende Ports sind in client.json " + + "eingetragen, aber auf diesem Rechner " + + "nicht eingerichtet:\n\n", + ) + + for _, port := range result.Missing { + message.WriteString(" ") + message.WriteString(port) + message.WriteByte('\n') + } + + message.WriteString( + "\nBitte richten Sie diese virtuellen Ports " + + "entsprechend der Installationsanleitung ein.", + ) + + return fmt.Errorf( + "%s", + strings.TrimSpace(message.String()), + ) + } + + //////////////////////////////////////////////////////////////////////////// + // Extra ports + //////////////////////////////////////////////////////////////////////////// + + if len(result.Missing) == 0 && + len(result.Extra) > 0 { + + message.WriteString( + "Virtuelle Ports nicht konfiguriert", + ) + + message.WriteString( + "\n\nFolgende virtuelle Ports sind auf diesem " + + "Rechner eingerichtet, aber nicht in " + + "client.json eingetragen:\n\n", + ) + + for _, port := range result.Extra { + message.WriteString(" ") + message.WriteString(port) + message.WriteByte('\n') + } + + message.WriteString( + "\nBitte ergänzen Sie die entsprechenden " + + "Einträge in client.json.", + ) + + return fmt.Errorf( + "%s", + strings.TrimSpace(message.String()), + ) + } + + //////////////////////////////////////////////////////////////////////////// + // Missing and extra ports + //////////////////////////////////////////////////////////////////////////// + + message.WriteString( + "Virtuelle Portkonfiguration stimmt nicht überein", + ) + + message.WriteString( + "\n\nIn client.json eingetragen, aber nicht " + + "eingerichtet:\n\n", + ) + + for _, port := range result.Missing { + message.WriteString(" ") + message.WriteString(port) + message.WriteByte('\n') + } + + message.WriteString( + "\nEingerichtet, aber nicht in client.json " + + "eingetragen:\n\n", + ) + + for _, port := range result.Extra { + message.WriteString(" ") + message.WriteString(port) + message.WriteByte('\n') + } + + message.WriteString( + "\nBitte korrigieren Sie die virtuelle " + + "Portkonfiguration.", + ) + + return fmt.Errorf( + "%s", + strings.TrimSpace(message.String()), + ) +} diff --git a/internal/client/virtual_port_validation_linux.go b/internal/client/virtual_port_validation_linux.go index 6a93285..a9332b9 100644 --- a/internal/client/virtual_port_validation_linux.go +++ b/internal/client/virtual_port_validation_linux.go @@ -9,176 +9,98 @@ * SPDX-License-Identifier: GPL-3.0-or-later * * Beschreibung: - * Linux-spezifische Prüfung der lokal vorhandenen virtuellen - * rs2322tcp-Schnittstellen gegen die Client-Konfiguration. + * Linux-spezifische Ermittlung der lokal vorhandenen virtuellen + * rs2322tcp-Schnittstellen. + * + * Die öffentliche /dev/ttyUSBxxx-Schnittstelle wird ausschließlich gelesen. + * Der Client verändert diese Schnittstellen hier nicht. * ============================================================================ */ package client import ( "fmt" - "strings" + "os" + "path/filepath" "git.lang-dieter.de/rs2322tcp/internal/config" ) /////////////////////////////////////////////////////////////////////////////// -// Local virtual-port validation +// Existing virtual ports /////////////////////////////////////////////////////////////////////////////// -// validateLocalVirtualPorts compares the virtual ports defined in -// client.json with the public symbolic links currently available on Linux. +// existingVirtualPortLinks returns all public virtual-port paths that +// currently exist as symbolic links. // -// The check is read-only and does not require root privileges. +// Only ports inside the configured virtual-port range are considered. +// The public /dev/ttyUSBxxx links are only inspected; they are never +// created, changed, or removed by this function. // -// A mismatch prevents the client from starting. -func validateLocalVirtualPorts( - cfg *config.ClientConfig, -) error { - if cfg == nil { - return fmt.Errorf("client configuration is nil") - } - - configured := make( - []string, - 0, - len(cfg.VirtualPorts), - ) - - for _, virtualPort := range cfg.VirtualPorts { - configured = append( - configured, - virtualPort.Port, - ) - } - - available, err := existingVirtualPortLinks( - cfg.VirtualPortRange, - ) - if err != nil { - return fmt.Errorf( - "prüfen der virtuellen Ports fehlgeschlagen: %w", - err, - ) - } - - result := compareVirtualPorts( - configured, - available, - ) - - if len(result.Missing) == 0 && - len(result.Extra) == 0 { - return nil - } - - var message strings.Builder - - //////////////////////////////////////////////////////////////////////////// - // Missing ports - //////////////////////////////////////////////////////////////////////////// - - if len(result.Missing) > 0 && - len(result.Extra) == 0 { - - message.WriteString( - "Virtuelle Ports nicht eingerichtet", - ) - - message.WriteString( - "\n\nFolgende Ports sind in client.json " + - "eingetragen, aber auf diesem Rechner " + - "nicht eingerichtet:\n\n", - ) - - for _, port := range result.Missing { - message.WriteString(" ") - message.WriteString(port) - message.WriteByte('\n') - } - - message.WriteString( - "\nBitte richten Sie diese virtuellen Ports " + - "entsprechend der Installationsanleitung ein.", - ) - - return fmt.Errorf( - "%s", - strings.TrimSpace(message.String()), - ) - } - - //////////////////////////////////////////////////////////////////////////// - // Extra ports - //////////////////////////////////////////////////////////////////////////// - - if len(result.Missing) == 0 && - len(result.Extra) > 0 { - - message.WriteString( - "Virtuelle Ports nicht konfiguriert", - ) - - message.WriteString( - "\n\nFolgende virtuelle Ports sind auf diesem " + - "Rechner eingerichtet, aber nicht in " + - "client.json eingetragen:\n\n", - ) - - for _, port := range result.Extra { - message.WriteString(" ") - message.WriteString(port) - message.WriteByte('\n') - } - - message.WriteString( - "\nBitte ergänzen Sie die entsprechenden " + - "Einträge in client.json.", - ) - - return fmt.Errorf( - "%s", - strings.TrimSpace(message.String()), - ) - } - - //////////////////////////////////////////////////////////////////////////// - // Missing and extra ports - //////////////////////////////////////////////////////////////////////////// - - message.WriteString( - "Virtuelle Portkonfiguration stimmt nicht überein", - ) - - message.WriteString( - "\n\nIn client.json eingetragen, aber nicht " + - "eingerichtet:\n\n", - ) - - for _, port := range result.Missing { - message.WriteString(" ") - message.WriteString(port) - message.WriteByte('\n') - } - - message.WriteString( - "\nEingerichtet, aber nicht in client.json " + - "eingetragen:\n\n", - ) - - for _, port := range result.Extra { - message.WriteString(" ") - message.WriteString(port) - message.WriteByte('\n') - } - - message.WriteString( - "\nBitte korrigieren Sie die virtuelle " + - "Portkonfiguration.", - ) - - return fmt.Errorf( - "%s", - strings.TrimSpace(message.String()), +// This check only requires read access and therefore does not require +// root privileges. +func existingVirtualPortLinks( + cfg config.VirtualPortRangeConfig, +) ([]string, error) { + return existingVirtualPortLinksInDirectory( + cfg, + "/dev", ) } + +/////////////////////////////////////////////////////////////////////////////// +// Existing virtual ports in directory +/////////////////////////////////////////////////////////////////////////////// + +// existingVirtualPortLinksInDirectory returns all virtual-port paths in +// directory that currently exist as symbolic links. +// +// This helper is deliberately separated from existingVirtualPortLinks so +// the filesystem inspection can be tested without modifying /dev. +func existingVirtualPortLinksInDirectory( + cfg config.VirtualPortRangeConfig, + directory string, +) ([]string, error) { + if directory == "" { + return nil, fmt.Errorf("virtual port directory is empty") + } + + manager := NewVirtualPortManager(cfg) + if manager == nil { + return nil, fmt.Errorf("virtual port manager is nil") + } + + links := make([]string, 0) + + for number := cfg.First; number <= cfg.Last; number++ { + portName := filepath.Base( + manager.PortPath(number), + ) + + portPath := filepath.Join(directory, portName) + + info, err := os.Lstat(portPath) + if err != nil { + if os.IsNotExist(err) { + continue + } + + return nil, fmt.Errorf( + "inspect virtual port %q: %w", + portPath, + err, + ) + } + + if info.Mode()&os.ModeSymlink == 0 { + continue + } + + links = append( + links, + filepath.Join(directory, portName), + ) + } + + return links, nil +} diff --git a/internal/client/virtual_port_validation_windows.go b/internal/client/virtual_port_validation_windows.go new file mode 100644 index 0000000..1cdb103 --- /dev/null +++ b/internal/client/virtual_port_validation_windows.go @@ -0,0 +1,132 @@ +//go:build windows + +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_port_validation_windows.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Windows-spezifische Ermittlung der lokal vorhandenen seriellen + * Schnittstellen für rs2322tcp. + * + * Unter Windows werden virtuelle Ports als COMxxx-Schnittstellen + * bereitgestellt. + * + * Diese Implementierung prüft den konfigurierten Portbereich und liefert + * die tatsächlich vorhandenen COM-Schnittstellen zurück. + * + * Die Funktion verändert keine Geräte und installiert keine Treiber. + * ============================================================================ + */ + +package client + +import ( + "fmt" + + "golang.org/x/sys/windows/registry" + + "git.lang-dieter.de/rs2322tcp/internal/config" +) + +/////////////////////////////////////////////////////////////////////////////// +// Existing virtual ports +/////////////////////////////////////////////////////////////////////////////// + +// existingVirtualPortLinks returns all COM ports in the configured range +// that are currently registered as serial ports in Windows. +// +// The Windows serial-port information is obtained from the Plug-and-Play +// device registry. No device is created, modified, or removed. +func existingVirtualPortLinks( + cfg config.VirtualPortRangeConfig, +) ([]string, error) { + key, err := registry.OpenKey( + registry.LOCAL_MACHINE, + `HARDWARE\DEVICEMAP\SERIALCOMM`, + registry.READ, + ) + if err != nil { + return nil, fmt.Errorf( + "open Windows serial-port registry: %w", + err, + ) + } + defer key.Close() + + values, err := key.ReadValueNames(0) + if err != nil { + return nil, fmt.Errorf( + "read Windows serial-port registry: %w", + err, + ) + } + + available := make([]string, 0) + + for _, valueName := range values { + portName, _, err := key.GetStringValue(valueName) + if err != nil { + continue + } + + number, ok := windowsCOMPortNumber(portName) + if !ok { + continue + } + + if number < cfg.First || number > cfg.Last { + continue + } + + available = append( + available, + portName, + ) + } + + return available, nil +} + +/////////////////////////////////////////////////////////////////////////////// +// COM port number +/////////////////////////////////////////////////////////////////////////////// + +// windowsCOMPortNumber extracts the numeric part of a Windows COM port name. +// +// Examples: +// +// COM1 -> 1 +// COM10 -> 10 +// +// The comparison is case-insensitive. +func windowsCOMPortNumber(portName string) (int, bool) { + if len(portName) < 4 { + return 0, false + } + + if (portName[0] != 'C' && portName[0] != 'c') || + (portName[1] != 'O' && portName[1] != 'o') || + (portName[2] != 'M' && portName[2] != 'm') { + return 0, false + } + + number := 0 + + for _, char := range portName[3:] { + if char < '0' || char > '9' { + return 0, false + } + + number = number*10 + int(char-'0') + } + + if number == 0 { + return 0, false + } + + return number, true +} diff --git a/internal/client/virtual_serial_windows.go b/internal/client/virtual_serial_windows.go new file mode 100644 index 0000000..4f8440b --- /dev/null +++ b/internal/client/virtual_serial_windows.go @@ -0,0 +1,114 @@ +//go:build windows + +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: internal/client/virtual_serial_windows.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Windows-spezifische Implementierung der virtuellen seriellen Schnittstelle + * über go.bug.st/serial. + * + * Der konfigurierte COM-Port wird direkt geöffnet. Die Zuordnung eines + * virtuellen COM-Port-Paares erfolgt außerhalb des rs2322tcp-Clients. + * ============================================================================ + */ + +package client + +import ( + "fmt" + "sync" + + bugserial "go.bug.st/serial" +) + +/////////////////////////////////////////////////////////////////////////////// +// windowsSerial +/////////////////////////////////////////////////////////////////////////////// + +// windowsSerial implements VirtualSerial using a Windows COM port. +type windowsSerial struct { + mu sync.Mutex + port bugserial.Port + path string +} + +/////////////////////////////////////////////////////////////////////////////// +// Path +/////////////////////////////////////////////////////////////////////////////// + +// Path returns the Windows COM-port name. +func (p *windowsSerial) Path() string { + if p == nil { + return "" + } + + return p.path +} + +/////////////////////////////////////////////////////////////////////////////// +// Read / Write +/////////////////////////////////////////////////////////////////////////////// + +// Read reads raw bytes from the COM port. +func (p *windowsSerial) Read(b []byte) (int, error) { + if p == nil { + return 0, fmt.Errorf("virtual serial port is nil") + } + + p.mu.Lock() + port := p.port + p.mu.Unlock() + + if port == nil { + return 0, fmt.Errorf("virtual serial port is closed") + } + + return port.Read(b) +} + +// Write writes raw bytes to the COM port. +func (p *windowsSerial) Write(b []byte) (int, error) { + if p == nil { + return 0, fmt.Errorf("virtual serial port is nil") + } + + p.mu.Lock() + port := p.port + p.mu.Unlock() + + if port == nil { + return 0, fmt.Errorf("virtual serial port is closed") + } + + return port.Write(b) +} + +/////////////////////////////////////////////////////////////////////////////// +// Close +/////////////////////////////////////////////////////////////////////////////// + +// Close closes the Windows COM port. +func (p *windowsSerial) Close() error { + if p == nil { + return nil + } + + p.mu.Lock() + + if p.port == nil { + p.mu.Unlock() + return nil + } + + port := p.port + p.port = nil + + p.mu.Unlock() + + return port.Close() +}