Startbytes für virtuelle Geräteverbindungen ergänzen

This commit is contained in:
Dieter Lang 2026-08-22 15:21:50 +02:00
parent f2428a2007
commit 0eccd003f0
4 changed files with 259 additions and 59 deletions

View file

@ -1,6 +1,6 @@
{ {
"server": { "server": {
"address": "100.64.0.1", "address": "192.168.178.229",
"port": 5000 "port": 5000
}, },
"virtual_port_range": { "virtual_port_range": {
@ -10,7 +10,8 @@
"virtual_ports": [ "virtual_ports": [
{ {
"port": "COM100", "port": "COM100",
"remote_device": "rotor" "remote_device": "rotor",
"startup_bytes": "57 00 00 00 00 00 00 00 00 00 00 1F 20"
} }
] ]

View file

@ -12,13 +12,18 @@
* *
* Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt * Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt
* übernommen. * übernommen.
*
* Optional können nach dem Aufbau der Datenverbindung konfigurierte
* Startbytes an das entfernte Gerät gesendet werden.
* ============================================================================ * ============================================================================
*/ */
package client package client
import ( import (
"encoding/hex"
"fmt" "fmt"
"net" "net"
"strings"
"git.lang-dieter.de/rs2322tcp/internal/config" "git.lang-dieter.de/rs2322tcp/internal/config"
"git.lang-dieter.de/rs2322tcp/internal/transport" "git.lang-dieter.de/rs2322tcp/internal/transport"
@ -36,6 +41,11 @@ import (
// The local virtual port is opened using exactly the port number specified // The local virtual port is opened using exactly the port number specified
// in virtualPortConfig.Port. This preserves the user's configured mapping // in virtualPortConfig.Port. This preserves the user's configured mapping
// between local virtual interfaces and remote devices. // between local virtual interfaces and remote devices.
//
// If StartupBytes are configured, they are sent once immediately after the
// TCP data connection has been established. Invalid startup bytes do not
// abort the connection setup. A warning is printed and the connection
// continues normally.
func connectVirtualPort( func connectVirtualPort(
c *Client, c *Client,
manager *VirtualPortManager, manager *VirtualPortManager,
@ -81,9 +91,63 @@ func connectVirtualPort(
) )
} }
if err := sendStartupBytes(conn, virtualPortConfig); err != nil {
fmt.Printf(
"warning: startup bytes for %q were not sent: %v\n",
virtualPortConfig.RemoteDevice,
err,
)
}
return virtualPort, conn, nil return virtualPort, conn, nil
} }
// sendStartupBytes sends the optional configured startup byte sequence over
// the already established TCP data connection.
//
// An empty StartupBytes configuration does not send anything and is not an
// error.
//
// The configured value may contain spaces between hexadecimal bytes, for
// example:
//
// 57 00 00 00 00 00 00 00 00 00 00 1F 20
func sendStartupBytes(
conn net.Conn,
virtualPortConfig config.VirtualPortConfig,
) error {
startupBytes := strings.TrimSpace(virtualPortConfig.StartupBytes)
if startupBytes == "" {
return nil
}
hexText := strings.Join(strings.Fields(startupBytes), "")
data, err := hex.DecodeString(hexText)
if err != nil {
return fmt.Errorf("invalid hexadecimal byte sequence: %w", err)
}
if len(data) == 0 {
return nil
}
written, err := conn.Write(data)
if err != nil {
return fmt.Errorf("send startup bytes: %w", err)
}
if written != len(data) {
return fmt.Errorf(
"send startup bytes: wrote %d of %d bytes",
written,
len(data),
)
}
return nil
}
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Bridge connection // Bridge connection
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////

View file

@ -79,6 +79,7 @@ type VirtualPortRangeConfig struct {
type VirtualPortConfig struct { type VirtualPortConfig struct {
Port string `json:"port"` Port string `json:"port"`
RemoteDevice string `json:"remote_device"` RemoteDevice string `json:"remote_device"`
StartupBytes string `json:"startup_bytes,omitempty"`
} }
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////

View file

@ -59,13 +59,15 @@ func TestLoadServer(t *testing.T) {
} }
if cfg.Listen.Address != "0.0.0.0" { if cfg.Listen.Address != "0.0.0.0" {
t.Errorf("Listen.Address = %q, want %q", t.Errorf(
cfg.Listen.Address, "0.0.0.0") "Listen.Address = %q, want %q",
cfg.Listen.Address,
"0.0.0.0",
)
} }
if cfg.Listen.Port != 5000 { if cfg.Listen.Port != 5000 {
t.Errorf("Listen.Port = %d, want %d", t.Errorf("Listen.Port = %d, want %d", cfg.Listen.Port, 5000)
cfg.Listen.Port, 5000)
} }
if len(cfg.Devices) != 1 { if len(cfg.Devices) != 1 {
@ -75,18 +77,19 @@ func TestLoadServer(t *testing.T) {
device := cfg.Devices[0] device := cfg.Devices[0]
if device.ID != "radio" { if device.ID != "radio" {
t.Errorf("Device.ID = %q, want %q", t.Errorf("Device.ID = %q, want %q", device.ID, "radio")
device.ID, "radio")
} }
if device.SerialPort != "/dev/ttyUSB0" { if device.SerialPort != "/dev/ttyUSB0" {
t.Errorf("Device.SerialPort = %q, want %q", t.Errorf(
device.SerialPort, "/dev/ttyUSB0") "Device.SerialPort = %q, want %q",
device.SerialPort,
"/dev/ttyUSB0",
)
} }
if device.BaudRate != 9600 { if device.BaudRate != 9600 {
t.Errorf("Device.BaudRate = %d, want %d", t.Errorf("Device.BaudRate = %d, want %d", device.BaudRate, 9600)
device.BaudRate, 9600)
} }
} }
@ -121,40 +124,106 @@ func TestLoadClient(t *testing.T) {
} }
if cfg.Server.Address != "100.64.0.10" { if cfg.Server.Address != "100.64.0.10" {
t.Errorf("Server.Address = %q, want %q", t.Errorf(
cfg.Server.Address, "100.64.0.10") "Server.Address = %q, want %q",
cfg.Server.Address,
"100.64.0.10",
)
} }
if cfg.Server.Port != 5000 { if cfg.Server.Port != 5000 {
t.Errorf("Server.Port = %d, want %d", t.Errorf("Server.Port = %d, want %d", cfg.Server.Port, 5000)
cfg.Server.Port, 5000)
} }
if cfg.VirtualPortRange.First != config.DefaultVirtualPortFirst { if cfg.VirtualPortRange.First != config.DefaultVirtualPortFirst {
t.Errorf("VirtualPortRange.First = %d, want %d", t.Errorf(
cfg.VirtualPortRange.First, config.DefaultVirtualPortFirst) "VirtualPortRange.First = %d, want %d",
cfg.VirtualPortRange.First,
config.DefaultVirtualPortFirst,
)
} }
if cfg.VirtualPortRange.Last != config.DefaultVirtualPortLast { if cfg.VirtualPortRange.Last != config.DefaultVirtualPortLast {
t.Errorf("VirtualPortRange.Last = %d, want %d", t.Errorf(
cfg.VirtualPortRange.Last, config.DefaultVirtualPortLast) "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(cfg.VirtualPorts)) "len(VirtualPorts) = %d, want 1",
len(cfg.VirtualPorts),
)
} }
virtualPort := cfg.VirtualPorts[0] virtualPort := cfg.VirtualPorts[0]
if virtualPort.Port != "COM7" { if virtualPort.Port != "COM7" {
t.Errorf("VirtualPort.Port = %q, want %q", t.Errorf(
virtualPort.Port, "COM7") "VirtualPort.Port = %q, want %q",
virtualPort.Port,
"COM7",
)
} }
if virtualPort.RemoteDevice != "radio" { if virtualPort.RemoteDevice != "radio" {
t.Errorf("VirtualPort.RemoteDevice = %q, want %q", t.Errorf(
virtualPort.RemoteDevice, "radio") "VirtualPort.RemoteDevice = %q, want %q",
virtualPort.RemoteDevice,
"radio",
)
}
if virtualPort.StartupBytes != "" {
t.Errorf(
"VirtualPort.StartupBytes = %q, want empty",
virtualPort.StartupBytes,
)
}
}
func TestLoadClientWithStartupBytes(t *testing.T) {
dir := t.TempDir()
filename := filepath.Join(dir, "client.json")
data := `{
"server": {
"address": "100.64.0.10",
"port": 5000
},
"virtual_ports": [
{
"port": "COM7",
"remote_device": "radio",
"startup_bytes": "57 00 00 1F 20"
}
]
}`
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 len(cfg.VirtualPorts) != 1 {
t.Fatalf(
"len(VirtualPorts) = %d, want 1",
len(cfg.VirtualPorts),
)
}
if cfg.VirtualPorts[0].StartupBytes != "57 00 00 1F 20" {
t.Errorf(
"VirtualPort.StartupBytes = %q, want %q",
cfg.VirtualPorts[0].StartupBytes,
"57 00 00 1F 20",
)
} }
} }
@ -184,13 +253,17 @@ func TestLoadClientCustomVirtualPortRange(t *testing.T) {
} }
if cfg.VirtualPortRange.First != 200 { if cfg.VirtualPortRange.First != 200 {
t.Errorf("VirtualPortRange.First = %d, want 200", t.Errorf(
cfg.VirtualPortRange.First) "VirtualPortRange.First = %d, want 200",
cfg.VirtualPortRange.First,
)
} }
if cfg.VirtualPortRange.Last != 299 { if cfg.VirtualPortRange.Last != 299 {
t.Errorf("VirtualPortRange.Last = %d, want 299", t.Errorf(
cfg.VirtualPortRange.Last) "VirtualPortRange.Last = %d, want 299",
cfg.VirtualPortRange.Last,
)
} }
} }
@ -215,6 +288,7 @@ func TestSaveAndLoadClient(t *testing.T) {
{ {
Port: "COM7", Port: "COM7",
RemoteDevice: "radio", RemoteDevice: "radio",
StartupBytes: "57 00 00 1F 20",
}, },
{ {
Port: "COM8", Port: "COM8",
@ -241,9 +315,11 @@ func TestSaveAndLoadClient(t *testing.T) {
} }
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),
len(original.VirtualPorts)) len(original.VirtualPorts),
)
} }
for i := range original.VirtualPorts { for i := range original.VirtualPorts {
@ -402,8 +478,29 @@ func TestExampleClientConfig(t *testing.T) {
t.Fatalf("LoadClient() failed: %v", err) t.Fatalf("LoadClient() failed: %v", err)
} }
if len(cfg.VirtualPorts) != 2 { if len(cfg.VirtualPorts) != 1 {
t.Fatalf("len(VirtualPorts) = %d, want 2", len(cfg.VirtualPorts)) t.Fatalf(
"len(VirtualPorts) = %d, want 1",
len(cfg.VirtualPorts),
)
}
port := cfg.VirtualPorts[0]
if port.Port != "COM100" {
t.Errorf(
"VirtualPorts[0].Port = %q, want %q",
port.Port,
"COM100",
)
}
if port.RemoteDevice != "rotor" {
t.Errorf(
"VirtualPorts[0].RemoteDevice = %q, want %q",
port.RemoteDevice,
"rotor",
)
} }
} }
@ -425,33 +522,51 @@ func TestDeviceRemoteDevice(t *testing.T) {
remote := device.RemoteDevice() remote := device.RemoteDevice()
if remote.ID != "radio" { if remote.ID != "radio" {
t.Errorf("RemoteDevice.ID = %q, want %q", t.Errorf(
remote.ID, "radio") "RemoteDevice.ID = %q, want %q",
remote.ID,
"radio",
)
} }
if remote.Name != "Funkgerät" { if remote.Name != "Funkgerät" {
t.Errorf("RemoteDevice.Name = %q, want %q", t.Errorf(
remote.Name, "Funkgerät") "RemoteDevice.Name = %q, want %q",
remote.Name,
"Funkgerät",
)
} }
if remote.BaudRate != 9600 { if remote.BaudRate != 9600 {
t.Errorf("RemoteDevice.BaudRate = %d, want %d", t.Errorf(
remote.BaudRate, 9600) "RemoteDevice.BaudRate = %d, want %d",
remote.BaudRate,
9600,
)
} }
if remote.DataBits != 8 { if remote.DataBits != 8 {
t.Errorf("RemoteDevice.DataBits = %d, want %d", t.Errorf(
remote.DataBits, 8) "RemoteDevice.DataBits = %d, want %d",
remote.DataBits,
8,
)
} }
if remote.Parity != "none" { if remote.Parity != "none" {
t.Errorf("RemoteDevice.Parity = %q, want %q", t.Errorf(
remote.Parity, "none") "RemoteDevice.Parity = %q, want %q",
remote.Parity,
"none",
)
} }
if remote.StopBits != 1 { if remote.StopBits != 1 {
t.Errorf("RemoteDevice.StopBits = %d, want %d", t.Errorf(
remote.StopBits, 1) "RemoteDevice.StopBits = %d, want %d",
remote.StopBits,
1,
)
} }
} }
@ -490,28 +605,41 @@ func TestServerRemoteDevices(t *testing.T) {
} }
if list.Devices[0].ID != "radio" { if list.Devices[0].ID != "radio" {
t.Errorf("Devices[0].ID = %q, want %q", t.Errorf(
list.Devices[0].ID, "radio") "Devices[0].ID = %q, want %q",
list.Devices[0].ID,
"radio",
)
} }
if list.Devices[1].ID != "rotor" { if list.Devices[1].ID != "rotor" {
t.Errorf("Devices[1].ID = %q, want %q", t.Errorf(
list.Devices[1].ID, "rotor") "Devices[1].ID = %q, want %q",
list.Devices[1].ID,
"rotor",
)
} }
if list.Devices[0].Name != "Funkgerät" { if list.Devices[0].Name != "Funkgerät" {
t.Errorf("Devices[0].Name = %q, want %q", t.Errorf(
list.Devices[0].Name, "Funkgerät") "Devices[0].Name = %q, want %q",
list.Devices[0].Name,
"Funkgerät",
)
} }
if list.Devices[0].BaudRate != 9600 { if list.Devices[0].BaudRate != 9600 {
t.Errorf("Devices[0].BaudRate = %d, want 9600", t.Errorf(
list.Devices[0].BaudRate) "Devices[0].BaudRate = %d, want 9600",
list.Devices[0].BaudRate,
)
} }
if list.Devices[1].BaudRate != 4800 { if list.Devices[1].BaudRate != 4800 {
t.Errorf("Devices[1].BaudRate = %d, want 4800", t.Errorf(
list.Devices[1].BaudRate) "Devices[1].BaudRate = %d, want 4800",
list.Devices[1].BaudRate,
)
} }
} }
@ -561,11 +689,17 @@ func TestRemoteDeviceJSON(t *testing.T) {
} }
if len(decoded.Devices) != 1 { if len(decoded.Devices) != 1 {
t.Fatalf("len(decoded.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" {
t.Errorf("Devices[0].ID = %q, want %q", t.Errorf(
decoded.Devices[0].ID, "radio") "Devices[0].ID = %q, want %q",
decoded.Devices[0].ID,
"radio",
)
} }
} }