technischen Windows-Client erfolgreich mit Rot2Prog getestet

This commit is contained in:
Dieter Lang 2026-08-21 11:02:25 +02:00
parent 81b06a16a0
commit 39a1fea75d
17 changed files with 1154 additions and 255 deletions

View file

@ -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)
}
}

View file

@ -1,6 +1,6 @@
{ {
"server": { "server": {
"address": "127.0.0.1", "address": "100.64.0.1",
"port": 5000 "port": 5000
}, },
"virtual_port_range": { "virtual_port_range": {
@ -9,12 +9,8 @@
}, },
"virtual_ports": [ "virtual_ports": [
{ {
"port": "/dev/ttyUSB100", "port": "COM100",
"remote_device": "radio"
},
{
"port": "/dev/ttyUSB101",
"remote_device": "rotor" "remote_device": "rotor"
} }
] ]
} }

View file

@ -11,12 +11,7 @@
* und dem zugehörigen entfernten Gerät. * und dem zugehörigen entfernten Gerät.
* *
* Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt * Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt
* übernommen. Eine Konfiguration wie * übernommen.
*
* /dev/ttyUSB100 -> radio
*
* führt somit technisch auch zur Erzeugung beziehungsweise Verwendung von
* /dev/ttyUSB100 und nicht einfach zum nächsten freien Port.
* ============================================================================ * ============================================================================
*/ */
package client package client
@ -24,53 +19,11 @@ package client
import ( import (
"fmt" "fmt"
"net" "net"
"strconv"
"strings"
"git.lang-dieter.de/rs2322tcp/internal/config" "git.lang-dieter.de/rs2322tcp/internal/config"
"git.lang-dieter.de/rs2322tcp/internal/transport" "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<number> 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<number>",
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 // Virtual port connection
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////

View file

@ -144,7 +144,7 @@ func TestConnectVirtualPortNilClient(t *testing.T) {
) )
cfg := config.VirtualPortConfig{ cfg := config.VirtualPortConfig{
Port: "/dev/ttyUSB100", Port: virtualPortPath(100),
RemoteDevice: "radio", RemoteDevice: "radio",
} }
@ -172,7 +172,7 @@ func TestConnectVirtualPortNilClient(t *testing.T) {
func TestConnectVirtualPortNilManager(t *testing.T) { func TestConnectVirtualPortNilManager(t *testing.T) {
cfg := config.VirtualPortConfig{ cfg := config.VirtualPortConfig{
Port: "/dev/ttyUSB100", Port: virtualPortPath(100),
RemoteDevice: "radio", RemoteDevice: "radio",
} }
@ -209,7 +209,7 @@ func TestConnectVirtualPortUnknownDevice(t *testing.T) {
client := newTestClient(t) client := newTestClient(t)
cfg := config.VirtualPortConfig{ cfg := config.VirtualPortConfig{
Port: "/dev/ttyUSB100", Port: virtualPortPath(100),
RemoteDevice: "unknown", RemoteDevice: "unknown",
} }
@ -249,7 +249,7 @@ func TestConnectVirtualPortClosesPortWhenDataConnectionFails(t *testing.T) {
client := newTestClient(t) client := newTestClient(t)
cfg := config.VirtualPortConfig{ cfg := config.VirtualPortConfig{
Port: "/dev/ttyUSB100", Port: virtualPortPath(100),
RemoteDevice: "radio", RemoteDevice: "radio",
} }
@ -323,7 +323,7 @@ func TestConnectVirtualPortSuccess(t *testing.T) {
) )
cfg := config.VirtualPortConfig{ cfg := config.VirtualPortConfig{
Port: "/dev/ttyUSB100", Port: virtualPortPath(100),
RemoteDevice: "radio", RemoteDevice: "radio",
} }
@ -348,11 +348,11 @@ func TestConnectVirtualPortSuccess(t *testing.T) {
defer virtualPort.Close() defer virtualPort.Close()
defer dataConn.Close() defer dataConn.Close()
if virtualPort.Path() != "/dev/ttyUSB100" { if virtualPort.Path() != virtualPortPath(100) {
t.Fatalf( t.Fatalf(
"virtual port path = %q, want %q", "virtual port path = %q, want %q",
virtualPort.Path(), virtualPort.Path(),
"/dev/ttyUSB100", virtualPortPath(100),
) )
} }
@ -386,7 +386,7 @@ func TestConnectVirtualPortUsesConfiguredPort(t *testing.T) {
) )
cfg := config.VirtualPortConfig{ cfg := config.VirtualPortConfig{
Port: "/dev/ttyUSB150", Port: virtualPortPath(100),
RemoteDevice: "radio", RemoteDevice: "radio",
} }
@ -411,24 +411,29 @@ func TestConnectVirtualPortUsesConfiguredPort(t *testing.T) {
defer virtualPort.Close() defer virtualPort.Close()
defer dataConn.Close() defer dataConn.Close()
if virtualPort.Path() != "/dev/ttyUSB150" { if virtualPort.Path() != virtualPortPath(100) {
t.Fatalf( t.Fatalf(
"virtual port path = %q, want %q", "virtual port path = %q, want %q",
virtualPort.Path(), virtualPort.Path(),
"/dev/ttyUSB150", virtualPortPath(100),
) )
} }
// Port 100 must still be available. This proves that the connection // Port 100 is the configured port and therefore must be reserved.
// did not simply take the first free port from the configured range. 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() number, err := manager.Reserve()
if err != nil { if err != nil {
t.Fatalf("Reserve() after connection: %v", err) t.Fatalf("Reserve() after connection: %v", err)
} }
if number != 100 { if number != 101 {
t.Fatalf( t.Fatalf(
"first free port after connection = %d, want 100", "first free port after connection = %d, want 101",
number, number,
) )
} }
@ -463,7 +468,7 @@ func TestConnectBridgeSuccess(t *testing.T) {
) )
cfg := config.VirtualPortConfig{ cfg := config.VirtualPortConfig{
Port: "/dev/ttyUSB100", Port: virtualPortPath(100),
RemoteDevice: "radio", RemoteDevice: "radio",
} }
@ -500,11 +505,11 @@ func TestConnectBridgeSuccess(t *testing.T) {
t.Fatal("bridge TCP endpoint does not match data connection") t.Fatal("bridge TCP endpoint does not match data connection")
} }
if virtualPort.Path() != "/dev/ttyUSB100" { if virtualPort.Path() != virtualPortPath(100) {
t.Fatalf( t.Fatalf(
"virtual port path = %q, want %q", "virtual port path = %q, want %q",
virtualPort.Path(), virtualPort.Path(),
"/dev/ttyUSB100", virtualPortPath(100),
) )
} }

View file

@ -11,19 +11,21 @@
* seriellen Ports. * seriellen Ports.
* *
* Der VirtualPortManager verwaltet den für rs2322tcp vorgesehenen Bereich * 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 * Ports können entweder automatisch über den nächsten freien Port oder
* gezielt über eine vorgegebene Portnummer reserviert werden. * 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 package client
import ( import (
@ -37,12 +39,15 @@ import (
// VirtualPortManager // VirtualPortManager
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// VirtualPortManager verwaltet den reservierten Bereich virtueller // VirtualPortManager verwaltet den für rs2322tcp reservierten Bereich
// /dev/ttyUSBxxx-Schnittstellen. // virtueller serieller Schnittstellen.
// //
// Der reservierte Portbereich und die Belegungstabelle werden durch einen // Der reservierte Portbereich und die Belegungstabelle werden durch einen
// Mutex geschützt, da mehrere Bridges beziehungsweise Goroutinen gleichzeitig // Mutex geschützt, da mehrere Bridges beziehungsweise Goroutinen gleichzeitig
// virtuelle Ports öffnen und freigeben können. // virtuelle Ports öffnen und freigeben können.
//
// Die konkrete Darstellung des sichtbaren Portnamens wird über
// virtualPortPath() von der jeweiligen Plattform bestimmt.
type VirtualPortManager struct { type VirtualPortManager struct {
mu sync.Mutex mu sync.Mutex
@ -51,6 +56,10 @@ type VirtualPortManager struct {
used map[int]bool used map[int]bool
} }
///////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////
// NewVirtualPortManager erzeugt einen VirtualPortManager aus dem // NewVirtualPortManager erzeugt einen VirtualPortManager aus dem
// konfigurierten Portbereich. // konfigurierten Portbereich.
func NewVirtualPortManager( func NewVirtualPortManager(
@ -63,16 +72,32 @@ func NewVirtualPortManager(
} }
} }
///////////////////////////////////////////////////////////////////////////////
// Port path
///////////////////////////////////////////////////////////////////////////////
// PortPath erzeugt den für die externe Software sichtbaren Gerätenamen // PortPath erzeugt den für die externe Software sichtbaren Gerätenamen
// für eine Portnummer. // 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 { func (m *VirtualPortManager) PortPath(number int) string {
if m == nil { if m == nil {
return "" return ""
} }
return fmt.Sprintf("/dev/ttyUSB%d", number) return virtualPortPath(number)
} }
///////////////////////////////////////////////////////////////////////////////
// Reservation
///////////////////////////////////////////////////////////////////////////////
// Reserve reserviert den nächsten freien virtuellen Port. // Reserve reserviert den nächsten freien virtuellen Port.
// //
// Es wird immer mit dem kleinsten freien Port im konfigurierten Bereich // Es wird immer mit dem kleinsten freien Port im konfigurierten Bereich
@ -141,6 +166,10 @@ func (m *VirtualPortManager) ReserveSpecific(number int) error {
return nil return nil
} }
///////////////////////////////////////////////////////////////////////////////
// Release
///////////////////////////////////////////////////////////////////////////////
// Release gibt einen zuvor reservierten Port wieder frei. // Release gibt einen zuvor reservierten Port wieder frei.
func (m *VirtualPortManager) Release(number int) { func (m *VirtualPortManager) Release(number int) {
if m == nil { if m == nil {

View file

@ -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
}

View file

@ -107,8 +107,10 @@ func TestVirtualPortManagerRelease(t *testing.T) {
} }
if reused != 100 { if reused != 100 {
t.Errorf("Reserve() after Release() = %d, want 100", t.Errorf(
reused) "Reserve() after Release() = %d, want 100",
reused,
)
} }
} }
@ -179,15 +181,15 @@ func TestVirtualPortManagerPortPath(t *testing.T) {
}{ }{
{ {
number: 100, number: 100,
want: "/dev/ttyUSB100", want: virtualPortPath(100),
}, },
{ {
number: 123, number: 123,
want: "/dev/ttyUSB123", want: virtualPortPath(123),
}, },
{ {
number: 199, number: 199,
want: "/dev/ttyUSB199", want: virtualPortPath(199),
}, },
} }

View file

@ -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<number>",
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
}

View file

@ -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<number>",
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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -7,13 +7,22 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* Beschreibung: * Beschreibung:
* Vergleich der in client.json definierten virtuellen Ports mit den * Plattformneutrale Prüfung der in client.json definierten virtuellen Ports
* tatsächlich vorhandenen virtuellen Schnittstellen. * gegen die tatsächlich vorhandenen virtuellen Schnittstellen.
*
* Die Ermittlung der vorhandenen Ports erfolgt über die
* plattformabhängige Funktion existingVirtualPortLinks().
* ============================================================================ * ============================================================================
*/ */
package client package client
import "sort" import (
"fmt"
"sort"
"strings"
"git.lang-dieter.de/rs2322tcp/internal/config"
)
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Virtual port validation // Virtual port validation
@ -62,10 +71,7 @@ func compareVirtualPorts(
continue continue
} }
result.Missing = append( result.Missing = append(result.Missing, port)
result.Missing,
port,
)
} }
for port := range availableSet { for port := range availableSet {
@ -73,10 +79,7 @@ func compareVirtualPorts(
continue continue
} }
result.Extra = append( result.Extra = append(result.Extra, port)
result.Extra,
port,
)
} }
sort.Strings(result.Missing) sort.Strings(result.Missing)
@ -84,3 +87,166 @@ func compareVirtualPorts(
return result 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()),
)
}

View file

@ -9,176 +9,98 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
* *
* Beschreibung: * Beschreibung:
* Linux-spezifische Prüfung der lokal vorhandenen virtuellen * Linux-spezifische Ermittlung der lokal vorhandenen virtuellen
* rs2322tcp-Schnittstellen gegen die Client-Konfiguration. * rs2322tcp-Schnittstellen.
*
* Die öffentliche /dev/ttyUSBxxx-Schnittstelle wird ausschließlich gelesen.
* Der Client verändert diese Schnittstellen hier nicht.
* ============================================================================ * ============================================================================
*/ */
package client package client
import ( import (
"fmt" "fmt"
"strings" "os"
"path/filepath"
"git.lang-dieter.de/rs2322tcp/internal/config" "git.lang-dieter.de/rs2322tcp/internal/config"
) )
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Local virtual-port validation // Existing virtual ports
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// validateLocalVirtualPorts compares the virtual ports defined in // existingVirtualPortLinks returns all public virtual-port paths that
// client.json with the public symbolic links currently available on Linux. // 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. // This check only requires read access and therefore does not require
func validateLocalVirtualPorts( // root privileges.
cfg *config.ClientConfig, func existingVirtualPortLinks(
) error { cfg config.VirtualPortRangeConfig,
if cfg == nil { ) ([]string, error) {
return fmt.Errorf("client configuration is nil") return existingVirtualPortLinksInDirectory(
} cfg,
"/dev",
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()),
) )
} }
///////////////////////////////////////////////////////////////////////////////
// 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
}

View file

@ -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
}

View file

@ -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()
}