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.
|
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
|
## [0.0.4] - 2026-08-10
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,17 @@
|
||||||
"address": "100.64.0.10",
|
"address": "100.64.0.10",
|
||||||
"port": 5000
|
"port": 5000
|
||||||
},
|
},
|
||||||
|
"virtual_port_range": {
|
||||||
|
"first": 100,
|
||||||
|
"last": 199
|
||||||
|
},
|
||||||
"virtual_ports": [
|
"virtual_ports": [
|
||||||
{
|
{
|
||||||
"port": "COM7",
|
"port": "/dev/ttyUSB100",
|
||||||
"remote_device": "radio"
|
"remote_device": "radio"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"port": "COM8",
|
"port": "/dev/ttyUSB101",
|
||||||
"remote_device": "rotor"
|
"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
|
// VirtualSerial repräsentiert eine vom Client bereitgestellte
|
||||||
// virtuelle serielle Schnittstelle.
|
// virtuelle serielle Schnittstelle.
|
||||||
//
|
//
|
||||||
// Path liefert den Namen der für die Anwendung sichtbaren
|
// Path liefert den Pfad des internen virtuellen seriellen Geräts,
|
||||||
// seriellen Schnittstelle, zum Beispiel /dev/pts/1.
|
// beispielsweise /dev/pts/2.
|
||||||
//
|
//
|
||||||
// Das io.ReadWriteCloser-Interface repräsentiert die interne
|
// Das io.ReadWriteCloser-Interface repräsentiert die interne
|
||||||
// Byte-Verbindung zum virtuellen seriellen Gerät.
|
// Byte-Verbindung zum virtuellen seriellen Gerät.
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
Package config provides configuration types and JSON handling for rs2322tcp.
|
Package config provides configuration types and JSON handling for rs2322tcp.
|
||||||
|
|
||||||
The server configuration describes the physical serial devices available
|
The server configuration describes the physical serial devices available
|
||||||
on the remote system. The client configuration describes the connection
|
on the remote system. The client configuration describes the connection to
|
||||||
to the server and the user's assignment of virtual serial ports to remote
|
the server and the user's assignment of virtual serial ports to remote
|
||||||
devices.
|
devices.
|
||||||
|
|
||||||
Project: rs2322tcp
|
Project: rs2322tcp
|
||||||
|
|
@ -50,8 +50,9 @@ type DeviceConfig struct {
|
||||||
|
|
||||||
// ClientConfig contains the complete client configuration.
|
// ClientConfig contains the complete client configuration.
|
||||||
type ClientConfig struct {
|
type ClientConfig struct {
|
||||||
Server ServerConnectionConfig `json:"server"`
|
Server ServerConnectionConfig `json:"server"`
|
||||||
VirtualPorts []VirtualPortConfig `json:"virtual_ports"`
|
VirtualPortRange VirtualPortRangeConfig `json:"virtual_port_range"`
|
||||||
|
VirtualPorts []VirtualPortConfig `json:"virtual_ports"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConnectionConfig contains the connection information for the
|
// ServerConnectionConfig contains the connection information for the
|
||||||
|
|
@ -61,6 +62,13 @@ type ServerConnectionConfig struct {
|
||||||
Port int `json:"port"`
|
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
|
// VirtualPortConfig describes one local virtual serial port and the
|
||||||
// remote device assigned to it.
|
// remote device assigned to it.
|
||||||
type VirtualPortConfig struct {
|
type VirtualPortConfig struct {
|
||||||
|
|
@ -68,6 +76,28 @@ type VirtualPortConfig struct {
|
||||||
RemoteDevice string `json:"remote_device"`
|
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
|
// JSON loading
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -95,6 +125,12 @@ func LoadClient(filename string) (*ClientConfig, error) {
|
||||||
return nil, err
|
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 {
|
if err := cfg.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +161,10 @@ func SaveClient(filename string, cfg *ClientConfig) error {
|
||||||
return fmt.Errorf("client configuration is nil")
|
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 {
|
if err := cfg.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -189,6 +229,16 @@ func (cfg *ClientConfig) Validate() error {
|
||||||
return fmt.Errorf("invalid server port: %d", cfg.Server.Port)
|
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)
|
ports := make(map[string]bool)
|
||||||
|
|
||||||
for i, virtualPort := range cfg.VirtualPorts {
|
for i, virtualPort := range cfg.VirtualPorts {
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,16 @@ func TestLoadClient(t *testing.T) {
|
||||||
cfg.Server.Port, 5000)
|
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 {
|
if len(cfg.VirtualPorts) != 1 {
|
||||||
t.Fatalf("len(VirtualPorts) = %d, want 1",
|
t.Fatalf("len(VirtualPorts) = %d, want 1",
|
||||||
len(cfg.VirtualPorts))
|
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
|
// Save and reload
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -153,6 +199,10 @@ func TestSaveAndLoadClient(t *testing.T) {
|
||||||
Address: "100.64.0.10",
|
Address: "100.64.0.10",
|
||||||
Port: 5000,
|
Port: 5000,
|
||||||
},
|
},
|
||||||
|
VirtualPortRange: config.VirtualPortRangeConfig{
|
||||||
|
First: 200,
|
||||||
|
Last: 299,
|
||||||
|
},
|
||||||
VirtualPorts: []config.VirtualPortConfig{
|
VirtualPorts: []config.VirtualPortConfig{
|
||||||
{
|
{
|
||||||
Port: "COM7",
|
Port: "COM7",
|
||||||
|
|
@ -178,6 +228,10 @@ func TestSaveAndLoadClient(t *testing.T) {
|
||||||
t.Errorf("loaded Server differs from original")
|
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) {
|
if len(loaded.VirtualPorts) != len(original.VirtualPorts) {
|
||||||
t.Fatalf("len(VirtualPorts) = %d, want %d",
|
t.Fatalf("len(VirtualPorts) = %d, want %d",
|
||||||
len(loaded.VirtualPorts),
|
len(loaded.VirtualPorts),
|
||||||
|
|
@ -228,6 +282,7 @@ func TestClientValidationDuplicateVirtualPort(t *testing.T) {
|
||||||
Server: config.ServerConnectionConfig{
|
Server: config.ServerConnectionConfig{
|
||||||
Port: 5000,
|
Port: 5000,
|
||||||
},
|
},
|
||||||
|
VirtualPortRange: config.DefaultVirtualPortRange(),
|
||||||
VirtualPorts: []config.VirtualPortConfig{
|
VirtualPorts: []config.VirtualPortConfig{
|
||||||
{
|
{
|
||||||
Port: "COM7",
|
Port: "COM7",
|
||||||
|
|
@ -262,6 +317,7 @@ func TestClientValidationInvalidPort(t *testing.T) {
|
||||||
Server: config.ServerConnectionConfig{
|
Server: config.ServerConnectionConfig{
|
||||||
Port: 0,
|
Port: 0,
|
||||||
},
|
},
|
||||||
|
VirtualPortRange: config.DefaultVirtualPortRange(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cfg.Validate(); err == nil {
|
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
|
// Example configurations
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -393,13 +497,13 @@ func TestServerRemoteDevices(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if list.Devices[0].BaudRate != 9600 {
|
if list.Devices[0].BaudRate != 9600 {
|
||||||
t.Errorf("Devices[0].BaudRate = %d, want %d",
|
t.Errorf("Devices[0].BaudRate = %d, want 9600",
|
||||||
list.Devices[0].BaudRate, 9600)
|
list.Devices[0].BaudRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if list.Devices[1].BaudRate != 4800 {
|
if list.Devices[1].BaudRate != 4800 {
|
||||||
t.Errorf("Devices[1].BaudRate = %d, want %d",
|
t.Errorf("Devices[1].BaudRate = %d, want 4800",
|
||||||
list.Devices[1].BaudRate, 4800)
|
list.Devices[1].BaudRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -449,7 +553,7 @@ func TestRemoteDeviceJSON(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(decoded.Devices) != 1 {
|
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" {
|
if decoded.Devices[0].ID != "radio" {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue