Add configurable local virtual serial ports
This commit is contained in:
parent
ea4499b93f
commit
1d3239da71
13 changed files with 1345 additions and 13 deletions
35
CHANGELOG.md
35
CHANGELOG.md
|
|
@ -2,6 +2,41 @@
|
|||
|
||||
Alle wesentlichen Änderungen am Projekt werden in dieser Datei dokumentiert.
|
||||
|
||||
## [0.0.5] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
||||
- Konfigurierbarer Bereich für lokale virtuelle serielle Schnittstellen
|
||||
- Standardbereich für virtuelle Ports von `/dev/ttyUSB100` bis
|
||||
`/dev/ttyUSB199`
|
||||
- Abweichender Portbereich kann über `virtual_port_range` in der
|
||||
Client-Konfiguration festgelegt werden
|
||||
- Rückwärtskompatibilität für bestehende Client-Konfigurationen ohne
|
||||
`virtual_port_range`
|
||||
- Verwaltung virtueller Ports über `VirtualPortManager`
|
||||
- Automatische Auswahl des nächsten freien virtuellen Ports
|
||||
- Freigabe reservierter Ports bei deren Schließen
|
||||
- Linux-spezifische Verwaltung der virtuellen Port-Symlinks unter
|
||||
`~/.rs2322tcp/virtual/`
|
||||
- Verknüpfung der virtuellen `/dev/ttyUSBxxx`-Schnittstellen mit den
|
||||
vom Client erzeugten PTYs
|
||||
- Einführung von `VirtualPort` und `ManagedVirtualPort`
|
||||
- Umfangreiche Tests für Portauswahl, Portfreigabe, Symlink-Verwaltung
|
||||
und Linux-PTY-Integration
|
||||
|
||||
### Tests
|
||||
|
||||
- `go test ./...` erfolgreich
|
||||
- `go test -race ./...` erfolgreich
|
||||
- Bidirektionale Kommunikation über `/dev/ttyUSB100` erfolgreich getestet
|
||||
- Externe Software kann ausschließlich `/dev/ttyUSB100` verwenden
|
||||
- Übertragung vom virtuellen `/dev/ttyUSB100` zum Client erfolgreich getestet
|
||||
- Übertragung vom Client zum virtuellen `/dev/ttyUSB100` erfolgreich getestet
|
||||
- Zweistufige Symlink-Kette von `/dev/ttyUSB100` über
|
||||
`~/.rs2322tcp/virtual/ttyUSB100` bis zum PTY erfolgreich getestet
|
||||
|
||||
---
|
||||
|
||||
## [0.0.4] - 2026-08-10
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@
|
|||
"address": "100.64.0.10",
|
||||
"port": 5000
|
||||
},
|
||||
"virtual_port_range": {
|
||||
"first": 100,
|
||||
"last": 199
|
||||
},
|
||||
"virtual_ports": [
|
||||
{
|
||||
"port": "COM7",
|
||||
"port": "/dev/ttyUSB100",
|
||||
"remote_device": "radio"
|
||||
},
|
||||
{
|
||||
"port": "COM8",
|
||||
"port": "/dev/ttyUSB101",
|
||||
"remote_device": "rotor"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
70
internal/client/virtual_port.go
Normal file
70
internal/client/virtual_port.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
Package client contains the client-side components of rs2322tcp.
|
||||
|
||||
Project: rs2322tcp
|
||||
Module: git.lang-dieter.de/rs2322tcp
|
||||
*/
|
||||
package client
|
||||
|
||||
import "io"
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Virtual port
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// VirtualPort represents one local virtual serial port.
|
||||
//
|
||||
// The visible path is the device name presented to the external application,
|
||||
// for example /dev/ttyUSB100.
|
||||
//
|
||||
// The underlying VirtualSerial remains an internal implementation detail and
|
||||
// may use a PTY such as /dev/pts/2.
|
||||
type VirtualPort struct {
|
||||
path string
|
||||
serial VirtualSerial
|
||||
}
|
||||
|
||||
// NewVirtualPort creates a virtual port with the specified visible path and
|
||||
// underlying virtual serial device.
|
||||
func NewVirtualPort(path string, serial VirtualSerial) *VirtualPort {
|
||||
return &VirtualPort{
|
||||
path: path,
|
||||
serial: serial,
|
||||
}
|
||||
}
|
||||
|
||||
// Path returns the device path visible to the external application.
|
||||
func (p *VirtualPort) Path() string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return p.path
|
||||
}
|
||||
|
||||
// Read reads data from the underlying virtual serial device.
|
||||
func (p *VirtualPort) Read(b []byte) (int, error) {
|
||||
if p == nil || p.serial == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
return p.serial.Read(b)
|
||||
}
|
||||
|
||||
// Write writes data to the underlying virtual serial device.
|
||||
func (p *VirtualPort) Write(b []byte) (int, error) {
|
||||
if p == nil || p.serial == nil {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
return p.serial.Write(b)
|
||||
}
|
||||
|
||||
// Close closes the underlying virtual serial device.
|
||||
func (p *VirtualPort) Close() error {
|
||||
if p == nil || p.serial == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.serial.Close()
|
||||
}
|
||||
140
internal/client/virtual_port_linux.go
Normal file
140
internal/client/virtual_port_linux.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
//go:build linux
|
||||
|
||||
/*
|
||||
* ============================================================================
|
||||
* Projekt.....: rs2322tcp
|
||||
* Datei.......: internal/client/virtual_port_linux.go
|
||||
* Copyright (C) 2026 Dieter Lang
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* Linux-spezifische Verwaltung der internen Symlinks für virtuelle
|
||||
* rs2322tcp-Seriellschnittstellen.
|
||||
*
|
||||
* Der öffentliche Gerätename /dev/ttyUSBxxx wird bei der Installation
|
||||
* vorbereitet. Der Client verwaltet ausschließlich den benutzerbezogenen
|
||||
* Link unter ~/.rs2322tcp/virtual/.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
virtualDirectoryName = ".rs2322tcp/virtual"
|
||||
)
|
||||
|
||||
// virtualPortDirectory returns the directory in the current user's home
|
||||
// directory used for the internal virtual-port links.
|
||||
func virtualPortDirectory() (string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get user home directory: %w", err)
|
||||
}
|
||||
|
||||
return filepath.Join(home, virtualDirectoryName), nil
|
||||
}
|
||||
|
||||
// virtualPortLinkPath returns the path of the internal link for a virtual
|
||||
// serial port.
|
||||
func virtualPortLinkPath(portPath string) (string, error) {
|
||||
directory, err := virtualPortDirectory()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(directory, filepath.Base(portPath)), nil
|
||||
}
|
||||
|
||||
// ensureVirtualPortDirectory creates the internal virtual-port directory
|
||||
// if it does not already exist.
|
||||
func ensureVirtualPortDirectory() error {
|
||||
directory, err := virtualPortDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(directory, 0755); err != nil {
|
||||
return fmt.Errorf(
|
||||
"create virtual port directory %q: %w",
|
||||
directory,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setVirtualPortLink creates or replaces the internal link for a virtual
|
||||
// port.
|
||||
//
|
||||
// The link itself is located below ~/.rs2322tcp/virtual/ and points to the
|
||||
// current PTY, for example /dev/pts/2.
|
||||
func setVirtualPortLink(portPath string, target string) error {
|
||||
if portPath == "" {
|
||||
return fmt.Errorf("virtual port path is empty")
|
||||
}
|
||||
|
||||
if target == "" {
|
||||
return fmt.Errorf("virtual port target is empty")
|
||||
}
|
||||
|
||||
if err := ensureVirtualPortDirectory(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
linkPath, err := virtualPortLinkPath(portPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove an existing link. The installation-owned outer /dev/ttyUSBxxx
|
||||
// link is deliberately never touched here.
|
||||
if err := os.Remove(linkPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf(
|
||||
"remove existing virtual port link %q: %w",
|
||||
linkPath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if err := os.Symlink(target, linkPath); err != nil {
|
||||
return fmt.Errorf(
|
||||
"create virtual port link %q -> %q: %w",
|
||||
linkPath,
|
||||
target,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeVirtualPortLink removes the internal link for a virtual port.
|
||||
//
|
||||
// The public /dev/ttyUSBxxx link is never modified.
|
||||
func removeVirtualPortLink(portPath string) error {
|
||||
if portPath == "" {
|
||||
return fmt.Errorf("virtual port path is empty")
|
||||
}
|
||||
|
||||
linkPath, err := virtualPortLinkPath(portPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Remove(linkPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf(
|
||||
"remove virtual port link %q: %w",
|
||||
linkPath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
153
internal/client/virtual_port_linux_test.go
Normal file
153
internal/client/virtual_port_linux_test.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
//go:build linux
|
||||
|
||||
/*
|
||||
* ============================================================================
|
||||
* Projekt.....: rs2322tcp
|
||||
* Datei.......: internal/client/virtual_port_linux_test.go
|
||||
* Copyright (C) 2026 Dieter Lang
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* Tests für die Linux-spezifische Verwaltung der internen Symlinks
|
||||
* virtueller rs2322tcp-Seriellschnittstellen.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVirtualPortLinkPath(t *testing.T) {
|
||||
path, err := virtualPortLinkPath("/dev/ttyUSB100")
|
||||
if err != nil {
|
||||
t.Fatalf("virtualPortLinkPath() failed: %v", err)
|
||||
}
|
||||
|
||||
wantSuffix := filepath.Join(".rs2322tcp", "virtual", "ttyUSB100")
|
||||
|
||||
if !filepath.IsAbs(path) {
|
||||
t.Fatalf("path is not absolute: %q", path)
|
||||
}
|
||||
|
||||
if !hasPathSuffix(path, wantSuffix) {
|
||||
t.Errorf(
|
||||
"virtualPortLinkPath() = %q, want suffix %q",
|
||||
path,
|
||||
wantSuffix,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAndRemoveVirtualPortLink(t *testing.T) {
|
||||
tempHome := t.TempDir()
|
||||
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
portPath := "/dev/ttyUSB100"
|
||||
target := "/dev/pts/2"
|
||||
|
||||
if err := setVirtualPortLink(portPath, target); err != nil {
|
||||
t.Fatalf("setVirtualPortLink() failed: %v", err)
|
||||
}
|
||||
|
||||
linkPath, err := virtualPortLinkPath(portPath)
|
||||
if err != nil {
|
||||
t.Fatalf("virtualPortLinkPath() failed: %v", err)
|
||||
}
|
||||
|
||||
got, err := os.Readlink(linkPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Readlink() failed: %v", err)
|
||||
}
|
||||
|
||||
if got != target {
|
||||
t.Errorf(
|
||||
"link target = %q, want %q",
|
||||
got,
|
||||
target,
|
||||
)
|
||||
}
|
||||
|
||||
if err := removeVirtualPortLink(portPath); err != nil {
|
||||
t.Fatalf("removeVirtualPortLink() failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Lstat(linkPath); !os.IsNotExist(err) {
|
||||
t.Fatalf(
|
||||
"link still exists after removal: %q",
|
||||
linkPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetVirtualPortLinkReplacesExistingLink(t *testing.T) {
|
||||
tempHome := t.TempDir()
|
||||
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
portPath := "/dev/ttyUSB100"
|
||||
|
||||
if err := setVirtualPortLink(portPath, "/dev/pts/2"); err != nil {
|
||||
t.Fatalf("first setVirtualPortLink() failed: %v", err)
|
||||
}
|
||||
|
||||
if err := setVirtualPortLink(portPath, "/dev/pts/7"); err != nil {
|
||||
t.Fatalf("second setVirtualPortLink() failed: %v", err)
|
||||
}
|
||||
|
||||
linkPath, err := virtualPortLinkPath(portPath)
|
||||
if err != nil {
|
||||
t.Fatalf("virtualPortLinkPath() failed: %v", err)
|
||||
}
|
||||
|
||||
got, err := os.Readlink(linkPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Readlink() failed: %v", err)
|
||||
}
|
||||
|
||||
if got != "/dev/pts/7" {
|
||||
t.Errorf(
|
||||
"link target = %q, want %q",
|
||||
got,
|
||||
"/dev/pts/7",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveVirtualPortLinkMissing(t *testing.T) {
|
||||
tempHome := t.TempDir()
|
||||
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
if err := removeVirtualPortLink("/dev/ttyUSB100"); err != nil {
|
||||
t.Fatalf(
|
||||
"removeVirtualPortLink() failed for missing link: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortLinkPathUsesBaseName(t *testing.T) {
|
||||
path, err := virtualPortLinkPath("/dev/ttyUSB123")
|
||||
if err != nil {
|
||||
t.Fatalf("virtualPortLinkPath() failed: %v", err)
|
||||
}
|
||||
|
||||
if filepath.Base(path) != "ttyUSB123" {
|
||||
t.Errorf(
|
||||
"filepath.Base() = %q, want %q",
|
||||
filepath.Base(path),
|
||||
"ttyUSB123",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func hasPathSuffix(path string, suffix string) bool {
|
||||
return path == suffix ||
|
||||
len(path) > len(suffix) &&
|
||||
path[len(path)-len(suffix):] == suffix
|
||||
}
|
||||
85
internal/client/virtual_port_manager.go
Normal file
85
internal/client/virtual_port_manager.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* ============================================================================
|
||||
* Projekt.....: rs2322tcp
|
||||
* Datei.......: internal/client/virtual_port_manager.go
|
||||
* Copyright (C) 2026 Dieter Lang
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* Verwaltung und Auswahl der für rs2322tcp reservierten virtuellen
|
||||
* seriellen Ports.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// VirtualPortManager
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// VirtualPortManager verwaltet den reservierten Bereich virtueller
|
||||
// /dev/ttyUSBxxx-Schnittstellen.
|
||||
type VirtualPortManager struct {
|
||||
first int
|
||||
last int
|
||||
used map[int]bool
|
||||
}
|
||||
|
||||
// NewVirtualPortManager erzeugt einen VirtualPortManager aus der
|
||||
// konfigurierten Portbereich.
|
||||
func NewVirtualPortManager(cfg config.VirtualPortRangeConfig) *VirtualPortManager {
|
||||
return &VirtualPortManager{
|
||||
first: cfg.First,
|
||||
last: cfg.Last,
|
||||
used: make(map[int]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// PortPath erzeugt den für die externe Software sichtbaren Gerätenamen
|
||||
// für eine Portnummer.
|
||||
func (m *VirtualPortManager) PortPath(number int) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/dev/ttyUSB%d", number)
|
||||
}
|
||||
|
||||
// Reserve reserviert den nächsten freien virtuellen Port.
|
||||
// Es wird immer mit dem kleinsten freien Port im konfigurierten Bereich
|
||||
// begonnen.
|
||||
func (m *VirtualPortManager) Reserve() (int, error) {
|
||||
if m == nil {
|
||||
return 0, fmt.Errorf("virtual port manager is nil")
|
||||
}
|
||||
|
||||
for number := m.first; number <= m.last; number++ {
|
||||
if m.used[number] {
|
||||
continue
|
||||
}
|
||||
|
||||
m.used[number] = true
|
||||
return number, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf(
|
||||
"no free virtual port in range %d-%d",
|
||||
m.first,
|
||||
m.last,
|
||||
)
|
||||
}
|
||||
|
||||
// Release gibt einen zuvor reservierten Port wieder frei.
|
||||
func (m *VirtualPortManager) Release(number int) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
delete(m.used, number)
|
||||
}
|
||||
137
internal/client/virtual_port_manager_linux.go
Normal file
137
internal/client/virtual_port_manager_linux.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
//go:build linux
|
||||
|
||||
/*
|
||||
* ============================================================================
|
||||
* Projekt.....: rs2322tcp
|
||||
* Datei.......: internal/client/virtual_port_manager_linux.go
|
||||
* Copyright (C) 2026 Dieter Lang
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* Linux-spezifische Erzeugung und Verwaltung virtueller rs2322tcp-Ports.
|
||||
*
|
||||
* Der öffentliche /dev/ttyUSBxxx-Eintrag wird bei der Installation
|
||||
* bereitgestellt. Der Client verwaltet ausschließlich den internen Link
|
||||
* unter ~/.rs2322tcp/virtual/.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
||||
import "fmt"
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Managed virtual port
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// ManagedVirtualPort verbindet einen reservierten Port mit einer
|
||||
// VirtualSerial-Instanz und verwaltet deren Lebenszyklus.
|
||||
type ManagedVirtualPort struct {
|
||||
port *VirtualPort
|
||||
manager *VirtualPortManager
|
||||
number int
|
||||
}
|
||||
|
||||
// Path returns the device path visible to the external application.
|
||||
func (p *ManagedVirtualPort) Path() string {
|
||||
if p == nil || 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 || p.port == nil {
|
||||
return 0, fmt.Errorf("managed virtual port is nil")
|
||||
}
|
||||
|
||||
return p.port.Read(b)
|
||||
}
|
||||
|
||||
// Write writes data to the underlying virtual serial device.
|
||||
func (p *ManagedVirtualPort) Write(b []byte) (int, error) {
|
||||
if p == nil || p.port == nil {
|
||||
return 0, fmt.Errorf("managed virtual port is nil")
|
||||
}
|
||||
|
||||
return p.port.Write(b)
|
||||
}
|
||||
|
||||
// Close removes the internal link, closes the PTY and releases the
|
||||
// reservation.
|
||||
func (p *ManagedVirtualPort) Close() error {
|
||||
if p == nil || p.port == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
|
||||
if err := removeVirtualPortLink(p.port.Path()); err != nil {
|
||||
firstErr = err
|
||||
}
|
||||
|
||||
if err := p.port.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
|
||||
if p.manager != nil {
|
||||
p.manager.Release(p.number)
|
||||
}
|
||||
|
||||
p.port = nil
|
||||
|
||||
return firstErr
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Open
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Open creates a new virtual serial port using the next free port number.
|
||||
//
|
||||
// The internal PTY is created by newVirtualSerial(). The internal
|
||||
// ~/.rs2322tcp/virtual/ttyUSBxxx link is then pointed at the PTY.
|
||||
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
|
||||
}
|
||||
|
||||
portPath := m.PortPath(number)
|
||||
|
||||
serial, err := newVirtualSerial()
|
||||
if err != nil {
|
||||
m.Release(number)
|
||||
return nil, fmt.Errorf(
|
||||
"create virtual serial for %s: %w",
|
||||
portPath,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if err := setVirtualPortLink(portPath, serial.Path()); err != nil {
|
||||
_ = serial.Close()
|
||||
m.Release(number)
|
||||
|
||||
return nil, fmt.Errorf(
|
||||
"bind %s to %s: %w",
|
||||
portPath,
|
||||
serial.Path(),
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
port := NewVirtualPort(portPath, serial)
|
||||
|
||||
return &ManagedVirtualPort{
|
||||
port: port,
|
||||
manager: m,
|
||||
number: number,
|
||||
}, nil
|
||||
}
|
||||
221
internal/client/virtual_port_manager_linux_test.go
Normal file
221
internal/client/virtual_port_manager_linux_test.go
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
//go:build linux
|
||||
|
||||
/*
|
||||
* ============================================================================
|
||||
* Projekt.....: rs2322tcp
|
||||
* Datei.......: internal/client/virtual_port_manager_linux_test.go
|
||||
* Copyright (C) 2026 Dieter Lang
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* Linux-spezifische Tests für die Erzeugung virtueller rs2322tcp-Ports.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Tests
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func TestVirtualPortManagerOpen(t *testing.T) {
|
||||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
manager := NewVirtualPortManager(
|
||||
config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
},
|
||||
)
|
||||
|
||||
port, err := manager.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("Open() failed: %v", err)
|
||||
}
|
||||
|
||||
if port == nil {
|
||||
t.Fatal("Open() returned nil")
|
||||
}
|
||||
|
||||
if port.Path() != "/dev/ttyUSB100" {
|
||||
t.Errorf(
|
||||
"Path() = %q, want %q",
|
||||
port.Path(),
|
||||
"/dev/ttyUSB100",
|
||||
)
|
||||
}
|
||||
|
||||
linkPath, err := virtualPortLinkPath(port.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("virtualPortLinkPath() failed: %v", err)
|
||||
}
|
||||
|
||||
target, err := os.Readlink(linkPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Readlink() failed: %v", err)
|
||||
}
|
||||
|
||||
if target == "" {
|
||||
t.Fatal("internal virtual port link has empty target")
|
||||
}
|
||||
|
||||
if err := port.Close(); err != nil {
|
||||
t.Fatalf("Close() failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Lstat(linkPath); !os.IsNotExist(err) {
|
||||
t.Fatalf(
|
||||
"internal link still exists after Close(): %q",
|
||||
linkPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerOpenUsesNextPort(t *testing.T) {
|
||||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
manager := NewVirtualPortManager(
|
||||
config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 101,
|
||||
},
|
||||
)
|
||||
|
||||
first, err := manager.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("first Open() failed: %v", err)
|
||||
}
|
||||
|
||||
second, err := manager.Open()
|
||||
if err != nil {
|
||||
_ = first.Close()
|
||||
t.Fatalf("second Open() failed: %v", err)
|
||||
}
|
||||
|
||||
if first.Path() != "/dev/ttyUSB100" {
|
||||
t.Errorf(
|
||||
"first.Path() = %q, want %q",
|
||||
first.Path(),
|
||||
"/dev/ttyUSB100",
|
||||
)
|
||||
}
|
||||
|
||||
if second.Path() != "/dev/ttyUSB101" {
|
||||
t.Errorf(
|
||||
"second.Path() = %q, want %q",
|
||||
second.Path(),
|
||||
"/dev/ttyUSB101",
|
||||
)
|
||||
}
|
||||
|
||||
if err := first.Close(); err != nil {
|
||||
t.Fatalf("first Close() failed: %v", err)
|
||||
}
|
||||
|
||||
if err := second.Close(); err != nil {
|
||||
t.Fatalf("second Close() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerOpenReusesReleasedPort(t *testing.T) {
|
||||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
manager := NewVirtualPortManager(
|
||||
config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 100,
|
||||
},
|
||||
)
|
||||
|
||||
first, err := manager.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("first Open() failed: %v", err)
|
||||
}
|
||||
|
||||
if first.Path() != "/dev/ttyUSB100" {
|
||||
t.Fatalf(
|
||||
"first.Path() = %q, want %q",
|
||||
first.Path(),
|
||||
"/dev/ttyUSB100",
|
||||
)
|
||||
}
|
||||
|
||||
if err := first.Close(); err != nil {
|
||||
t.Fatalf("first Close() failed: %v", err)
|
||||
}
|
||||
|
||||
second, err := manager.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("second Open() failed: %v", err)
|
||||
}
|
||||
|
||||
if second.Path() != "/dev/ttyUSB100" {
|
||||
t.Errorf(
|
||||
"second.Path() = %q, want %q",
|
||||
second.Path(),
|
||||
"/dev/ttyUSB100",
|
||||
)
|
||||
}
|
||||
|
||||
if err := second.Close(); err != nil {
|
||||
t.Fatalf("second Close() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerOpenNoFreePort(t *testing.T) {
|
||||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
manager := NewVirtualPortManager(
|
||||
config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 100,
|
||||
},
|
||||
)
|
||||
|
||||
port, err := manager.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("first Open() failed: %v", err)
|
||||
}
|
||||
|
||||
defer port.Close()
|
||||
|
||||
if _, err := manager.Open(); err == nil {
|
||||
t.Fatal("second Open() succeeded, want no-free-port error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagedVirtualPortDataPath(t *testing.T) {
|
||||
tempHome := t.TempDir()
|
||||
t.Setenv("HOME", tempHome)
|
||||
|
||||
manager := NewVirtualPortManager(
|
||||
config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 100,
|
||||
},
|
||||
)
|
||||
|
||||
port, err := manager.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("Open() failed: %v", err)
|
||||
}
|
||||
|
||||
defer port.Close()
|
||||
|
||||
data := []byte{0x46, 0x41, 0x00, 0x10, 0x0D}
|
||||
|
||||
if _, err := port.Write(data); err != nil {
|
||||
t.Fatalf("Write() failed: %v", err)
|
||||
}
|
||||
}
|
||||
216
internal/client/virtual_port_manager_test.go
Normal file
216
internal/client/virtual_port_manager_test.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* ============================================================================
|
||||
* Projekt.....: rs2322tcp
|
||||
* Datei.......: internal/client/virtual_port_manager_test.go
|
||||
* Copyright (C) 2026 Dieter Lang
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* Tests für die Auswahl und Verwaltung virtueller serieller Ports.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Tests
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func TestNewVirtualPortManager(t *testing.T) {
|
||||
cfg := config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
}
|
||||
|
||||
manager := NewVirtualPortManager(cfg)
|
||||
|
||||
if manager == nil {
|
||||
t.Fatal("NewVirtualPortManager() returned nil")
|
||||
}
|
||||
|
||||
if manager.first != 100 {
|
||||
t.Errorf("first = %d, want 100", manager.first)
|
||||
}
|
||||
|
||||
if manager.last != 199 {
|
||||
t.Errorf("last = %d, want 199", manager.last)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerReserveFirst(t *testing.T) {
|
||||
manager := NewVirtualPortManager(config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
})
|
||||
|
||||
number, err := manager.Reserve()
|
||||
if err != nil {
|
||||
t.Fatalf("Reserve() failed: %v", err)
|
||||
}
|
||||
|
||||
if number != 100 {
|
||||
t.Errorf("Reserve() = %d, want 100", number)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerReserveNextFree(t *testing.T) {
|
||||
manager := NewVirtualPortManager(config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
})
|
||||
|
||||
first, err := manager.Reserve()
|
||||
if err != nil {
|
||||
t.Fatalf("first Reserve() failed: %v", err)
|
||||
}
|
||||
|
||||
second, err := manager.Reserve()
|
||||
if err != nil {
|
||||
t.Fatalf("second Reserve() failed: %v", err)
|
||||
}
|
||||
|
||||
if first != 100 {
|
||||
t.Errorf("first Reserve() = %d, want 100", first)
|
||||
}
|
||||
|
||||
if second != 101 {
|
||||
t.Errorf("second Reserve() = %d, want 101", second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerRelease(t *testing.T) {
|
||||
manager := NewVirtualPortManager(config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
})
|
||||
|
||||
number, err := manager.Reserve()
|
||||
if err != nil {
|
||||
t.Fatalf("Reserve() failed: %v", err)
|
||||
}
|
||||
|
||||
manager.Release(number)
|
||||
|
||||
reused, err := manager.Reserve()
|
||||
if err != nil {
|
||||
t.Fatalf("Reserve() after Release() failed: %v", err)
|
||||
}
|
||||
|
||||
if reused != 100 {
|
||||
t.Errorf("Reserve() after Release() = %d, want 100",
|
||||
reused)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerSkipsReservedPorts(t *testing.T) {
|
||||
manager := NewVirtualPortManager(config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
})
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
if _, err := manager.Reserve(); err != nil {
|
||||
t.Fatalf("Reserve() %d failed: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
number, err := manager.Reserve()
|
||||
if err != nil {
|
||||
t.Fatalf("fourth Reserve() failed: %v", err)
|
||||
}
|
||||
|
||||
if number != 103 {
|
||||
t.Errorf("fourth Reserve() = %d, want 103", number)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerCustomRange(t *testing.T) {
|
||||
manager := NewVirtualPortManager(config.VirtualPortRangeConfig{
|
||||
First: 200,
|
||||
Last: 299,
|
||||
})
|
||||
|
||||
number, err := manager.Reserve()
|
||||
if err != nil {
|
||||
t.Fatalf("Reserve() failed: %v", err)
|
||||
}
|
||||
|
||||
if number != 200 {
|
||||
t.Errorf("Reserve() = %d, want 200", number)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerNoFreePort(t *testing.T) {
|
||||
manager := NewVirtualPortManager(config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 102,
|
||||
})
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
if _, err := manager.Reserve(); err != nil {
|
||||
t.Fatalf("Reserve() %d failed: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := manager.Reserve(); err == nil {
|
||||
t.Fatal("Reserve() succeeded, want no-free-port error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerPortPath(t *testing.T) {
|
||||
manager := NewVirtualPortManager(config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 199,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
number int
|
||||
want string
|
||||
}{
|
||||
{
|
||||
number: 100,
|
||||
want: "/dev/ttyUSB100",
|
||||
},
|
||||
{
|
||||
number: 123,
|
||||
want: "/dev/ttyUSB123",
|
||||
},
|
||||
{
|
||||
number: 199,
|
||||
want: "/dev/ttyUSB199",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := manager.PortPath(test.number)
|
||||
|
||||
if got != test.want {
|
||||
t.Errorf(
|
||||
"PortPath(%d) = %q, want %q",
|
||||
test.number,
|
||||
got,
|
||||
test.want,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortManagerNil(t *testing.T) {
|
||||
var manager *VirtualPortManager
|
||||
|
||||
if path := manager.PortPath(100); path != "" {
|
||||
t.Errorf("nil PortPath() = %q, want empty string", path)
|
||||
}
|
||||
|
||||
if _, err := manager.Reserve(); err == nil {
|
||||
t.Fatal("nil Reserve() succeeded, want error")
|
||||
}
|
||||
|
||||
manager.Release(100)
|
||||
}
|
||||
117
internal/client/virtual_port_test.go
Normal file
117
internal/client/virtual_port_test.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* ============================================================================
|
||||
* Projekt.....: rs2322tcp
|
||||
* Datei.......: internal/client/virtual_port_test.go
|
||||
* Copyright (C) 2026 Dieter Lang
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* Tests für die lokale virtuelle seriellen Schnittstelle.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
||||
import "testing"
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Tests
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func TestNewVirtualPort(t *testing.T) {
|
||||
serial := newTestVirtualSerial(nil)
|
||||
|
||||
port := NewVirtualPort("/dev/ttyUSB100", serial)
|
||||
|
||||
if port == nil {
|
||||
t.Fatal("NewVirtualPort() returned nil")
|
||||
}
|
||||
|
||||
if port.Path() != "/dev/ttyUSB100" {
|
||||
t.Errorf("Path() = %q, want %q",
|
||||
port.Path(), "/dev/ttyUSB100")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortRead(t *testing.T) {
|
||||
expected := []byte{0x46, 0x41, 0x00, 0x10, 0x0D}
|
||||
|
||||
serial := newTestVirtualSerial(expected)
|
||||
port := NewVirtualPort("/dev/ttyUSB100", serial)
|
||||
|
||||
buffer := make([]byte, len(expected))
|
||||
|
||||
n, err := port.Read(buffer)
|
||||
if err != nil {
|
||||
t.Fatalf("Read() failed: %v", err)
|
||||
}
|
||||
|
||||
if n != len(expected) {
|
||||
t.Fatalf("Read() returned %d bytes, want %d",
|
||||
n, len(expected))
|
||||
}
|
||||
|
||||
for i := range expected {
|
||||
if buffer[i] != expected[i] {
|
||||
t.Errorf("buffer[%d] = %02X, want %02X",
|
||||
i, buffer[i], expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortWrite(t *testing.T) {
|
||||
serial := newTestVirtualSerial(nil)
|
||||
port := NewVirtualPort("/dev/ttyUSB100", serial)
|
||||
|
||||
data := []byte{0x46, 0x41, 0x00, 0x10, 0x0D}
|
||||
|
||||
n, err := port.Write(data)
|
||||
if err != nil {
|
||||
t.Fatalf("Write() failed: %v", err)
|
||||
}
|
||||
|
||||
if n != len(data) {
|
||||
t.Fatalf("Write() returned %d, want %d",
|
||||
n, len(data))
|
||||
}
|
||||
|
||||
written := serial.Written()
|
||||
|
||||
if len(written) != len(data) {
|
||||
t.Fatalf("underlying serial received %d bytes, want %d",
|
||||
len(written), len(data))
|
||||
}
|
||||
|
||||
for i := range data {
|
||||
if written[i] != data[i] {
|
||||
t.Errorf("written[%d] = %02X, want %02X",
|
||||
i, written[i], data[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortClose(t *testing.T) {
|
||||
serial := newTestVirtualSerial(nil)
|
||||
port := NewVirtualPort("/dev/ttyUSB100", serial)
|
||||
|
||||
if err := port.Close(); err != nil {
|
||||
t.Fatalf("Close() failed: %v", err)
|
||||
}
|
||||
|
||||
// Ein zweiter Close darf keinen Fehler erzeugen.
|
||||
if err := port.Close(); err != nil {
|
||||
t.Fatalf("second Close() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVirtualPortNil(t *testing.T) {
|
||||
var port *VirtualPort
|
||||
|
||||
if port.Path() != "" {
|
||||
t.Errorf("nil Path() = %q, want empty string", port.Path())
|
||||
}
|
||||
|
||||
if err := port.Close(); err != nil {
|
||||
t.Errorf("nil Close() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,8 @@ import "io"
|
|||
// VirtualSerial repräsentiert eine vom Client bereitgestellte
|
||||
// virtuelle serielle Schnittstelle.
|
||||
//
|
||||
// Path liefert den Namen der für die Anwendung sichtbaren
|
||||
// seriellen Schnittstelle, zum Beispiel /dev/pts/1.
|
||||
// Path liefert den Pfad des internen virtuellen seriellen Geräts,
|
||||
// beispielsweise /dev/pts/2.
|
||||
//
|
||||
// Das io.ReadWriteCloser-Interface repräsentiert die interne
|
||||
// Byte-Verbindung zum virtuellen seriellen Gerät.
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
Package config provides configuration types and JSON handling for rs2322tcp.
|
||||
|
||||
The server configuration describes the physical serial devices available
|
||||
on the remote system. The client configuration describes the connection
|
||||
to the server and the user's assignment of virtual serial ports to remote
|
||||
on the remote system. The client configuration describes the connection to
|
||||
the server and the user's assignment of virtual serial ports to remote
|
||||
devices.
|
||||
|
||||
Project: rs2322tcp
|
||||
|
|
@ -51,6 +51,7 @@ type DeviceConfig struct {
|
|||
// ClientConfig contains the complete client configuration.
|
||||
type ClientConfig struct {
|
||||
Server ServerConnectionConfig `json:"server"`
|
||||
VirtualPortRange VirtualPortRangeConfig `json:"virtual_port_range"`
|
||||
VirtualPorts []VirtualPortConfig `json:"virtual_ports"`
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +62,13 @@ type ServerConnectionConfig struct {
|
|||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
// VirtualPortRangeConfig defines the reserved range of /dev/ttyUSB device
|
||||
// numbers that may be used for virtual rs2322tcp ports.
|
||||
type VirtualPortRangeConfig struct {
|
||||
First int `json:"first"`
|
||||
Last int `json:"last"`
|
||||
}
|
||||
|
||||
// VirtualPortConfig describes one local virtual serial port and the
|
||||
// remote device assigned to it.
|
||||
type VirtualPortConfig struct {
|
||||
|
|
@ -68,6 +76,28 @@ type VirtualPortConfig struct {
|
|||
RemoteDevice string `json:"remote_device"`
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Defaults
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const (
|
||||
// DefaultVirtualPortFirst is the first virtual USB serial port number
|
||||
// used when no virtual port range is specified.
|
||||
DefaultVirtualPortFirst = 100
|
||||
|
||||
// DefaultVirtualPortLast is the last virtual USB serial port number
|
||||
// used when no virtual port range is specified.
|
||||
DefaultVirtualPortLast = 199
|
||||
)
|
||||
|
||||
// DefaultVirtualPortRange returns the default virtual port range.
|
||||
func DefaultVirtualPortRange() VirtualPortRangeConfig {
|
||||
return VirtualPortRangeConfig{
|
||||
First: DefaultVirtualPortFirst,
|
||||
Last: DefaultVirtualPortLast,
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// JSON loading
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -95,6 +125,12 @@ func LoadClient(filename string) (*ClientConfig, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// A missing virtual_port_range is intentionally supported for backwards
|
||||
// compatibility. Apply the documented default before validation.
|
||||
if cfg.VirtualPortRange.First == 0 && cfg.VirtualPortRange.Last == 0 {
|
||||
cfg.VirtualPortRange = DefaultVirtualPortRange()
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -125,6 +161,10 @@ func SaveClient(filename string, cfg *ClientConfig) error {
|
|||
return fmt.Errorf("client configuration is nil")
|
||||
}
|
||||
|
||||
if cfg.VirtualPortRange.First == 0 && cfg.VirtualPortRange.Last == 0 {
|
||||
cfg.VirtualPortRange = DefaultVirtualPortRange()
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -189,6 +229,16 @@ func (cfg *ClientConfig) Validate() error {
|
|||
return fmt.Errorf("invalid server port: %d", cfg.Server.Port)
|
||||
}
|
||||
|
||||
if cfg.VirtualPortRange.First < DefaultVirtualPortFirst {
|
||||
return fmt.Errorf("virtual port range first must be >= %d: %d",
|
||||
DefaultVirtualPortFirst, cfg.VirtualPortRange.First)
|
||||
}
|
||||
|
||||
if cfg.VirtualPortRange.Last < cfg.VirtualPortRange.First {
|
||||
return fmt.Errorf("invalid virtual port range: %d-%d",
|
||||
cfg.VirtualPortRange.First, cfg.VirtualPortRange.Last)
|
||||
}
|
||||
|
||||
ports := make(map[string]bool)
|
||||
|
||||
for i, virtualPort := range cfg.VirtualPorts {
|
||||
|
|
|
|||
|
|
@ -122,6 +122,16 @@ func TestLoadClient(t *testing.T) {
|
|||
cfg.Server.Port, 5000)
|
||||
}
|
||||
|
||||
if cfg.VirtualPortRange.First != config.DefaultVirtualPortFirst {
|
||||
t.Errorf("VirtualPortRange.First = %d, want %d",
|
||||
cfg.VirtualPortRange.First, config.DefaultVirtualPortFirst)
|
||||
}
|
||||
|
||||
if cfg.VirtualPortRange.Last != config.DefaultVirtualPortLast {
|
||||
t.Errorf("VirtualPortRange.Last = %d, want %d",
|
||||
cfg.VirtualPortRange.Last, config.DefaultVirtualPortLast)
|
||||
}
|
||||
|
||||
if len(cfg.VirtualPorts) != 1 {
|
||||
t.Fatalf("len(VirtualPorts) = %d, want 1",
|
||||
len(cfg.VirtualPorts))
|
||||
|
|
@ -140,6 +150,42 @@ func TestLoadClient(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadClientCustomVirtualPortRange(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
filename := filepath.Join(dir, "client.json")
|
||||
|
||||
data := `{
|
||||
"server": {
|
||||
"address": "100.64.0.10",
|
||||
"port": 5000
|
||||
},
|
||||
"virtual_port_range": {
|
||||
"first": 200,
|
||||
"last": 299
|
||||
},
|
||||
"virtual_ports": []
|
||||
}`
|
||||
|
||||
if err := os.WriteFile(filename, []byte(data), 0644); err != nil {
|
||||
t.Fatalf("write test configuration: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := config.LoadClient(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadClient() failed: %v", err)
|
||||
}
|
||||
|
||||
if cfg.VirtualPortRange.First != 200 {
|
||||
t.Errorf("VirtualPortRange.First = %d, want 200",
|
||||
cfg.VirtualPortRange.First)
|
||||
}
|
||||
|
||||
if cfg.VirtualPortRange.Last != 299 {
|
||||
t.Errorf("VirtualPortRange.Last = %d, want 299",
|
||||
cfg.VirtualPortRange.Last)
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Save and reload
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -153,6 +199,10 @@ func TestSaveAndLoadClient(t *testing.T) {
|
|||
Address: "100.64.0.10",
|
||||
Port: 5000,
|
||||
},
|
||||
VirtualPortRange: config.VirtualPortRangeConfig{
|
||||
First: 200,
|
||||
Last: 299,
|
||||
},
|
||||
VirtualPorts: []config.VirtualPortConfig{
|
||||
{
|
||||
Port: "COM7",
|
||||
|
|
@ -178,6 +228,10 @@ func TestSaveAndLoadClient(t *testing.T) {
|
|||
t.Errorf("loaded Server differs from original")
|
||||
}
|
||||
|
||||
if loaded.VirtualPortRange != original.VirtualPortRange {
|
||||
t.Errorf("loaded VirtualPortRange differs from original")
|
||||
}
|
||||
|
||||
if len(loaded.VirtualPorts) != len(original.VirtualPorts) {
|
||||
t.Fatalf("len(VirtualPorts) = %d, want %d",
|
||||
len(loaded.VirtualPorts),
|
||||
|
|
@ -228,6 +282,7 @@ func TestClientValidationDuplicateVirtualPort(t *testing.T) {
|
|||
Server: config.ServerConnectionConfig{
|
||||
Port: 5000,
|
||||
},
|
||||
VirtualPortRange: config.DefaultVirtualPortRange(),
|
||||
VirtualPorts: []config.VirtualPortConfig{
|
||||
{
|
||||
Port: "COM7",
|
||||
|
|
@ -262,6 +317,7 @@ func TestClientValidationInvalidPort(t *testing.T) {
|
|||
Server: config.ServerConnectionConfig{
|
||||
Port: 0,
|
||||
},
|
||||
VirtualPortRange: config.DefaultVirtualPortRange(),
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
|
|
@ -269,6 +325,54 @@ func TestClientValidationInvalidPort(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientValidationVirtualPortRangeBelowReservedArea(t *testing.T) {
|
||||
cfg := &config.ClientConfig{
|
||||
Server: config.ServerConnectionConfig{
|
||||
Port: 5000,
|
||||
},
|
||||
VirtualPortRange: config.VirtualPortRangeConfig{
|
||||
First: 99,
|
||||
Last: 199,
|
||||
},
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("Validate() succeeded, want range-below-reserved-area error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientValidationVirtualPortRangeReversed(t *testing.T) {
|
||||
cfg := &config.ClientConfig{
|
||||
Server: config.ServerConnectionConfig{
|
||||
Port: 5000,
|
||||
},
|
||||
VirtualPortRange: config.VirtualPortRangeConfig{
|
||||
First: 200,
|
||||
Last: 100,
|
||||
},
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("Validate() succeeded, want reversed-range error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientValidationVirtualPortRangeValid(t *testing.T) {
|
||||
cfg := &config.ClientConfig{
|
||||
Server: config.ServerConnectionConfig{
|
||||
Port: 5000,
|
||||
},
|
||||
VirtualPortRange: config.VirtualPortRangeConfig{
|
||||
First: 200,
|
||||
Last: 299,
|
||||
},
|
||||
}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Example configurations
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -393,13 +497,13 @@ func TestServerRemoteDevices(t *testing.T) {
|
|||
}
|
||||
|
||||
if list.Devices[0].BaudRate != 9600 {
|
||||
t.Errorf("Devices[0].BaudRate = %d, want %d",
|
||||
list.Devices[0].BaudRate, 9600)
|
||||
t.Errorf("Devices[0].BaudRate = %d, want 9600",
|
||||
list.Devices[0].BaudRate)
|
||||
}
|
||||
|
||||
if list.Devices[1].BaudRate != 4800 {
|
||||
t.Errorf("Devices[1].BaudRate = %d, want %d",
|
||||
list.Devices[1].BaudRate, 4800)
|
||||
t.Errorf("Devices[1].BaudRate = %d, want 4800",
|
||||
list.Devices[1].BaudRate)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -449,7 +553,7 @@ func TestRemoteDeviceJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
if len(decoded.Devices) != 1 {
|
||||
t.Fatalf("len(Devices) = %d, want 1", len(decoded.Devices))
|
||||
t.Fatalf("len(decoded.Devices) = %d, want 1", len(decoded.Devices))
|
||||
}
|
||||
|
||||
if decoded.Devices[0].ID != "radio" {
|
||||
|
|
|
|||
Loading…
Reference in a new issue