From 0eccd003f083836b548d2d8bdaa0807969938126 Mon Sep 17 00:00:00 2001 From: Dieter Lang Date: Sat, 22 Aug 2026 15:21:50 +0200 Subject: [PATCH] =?UTF-8?q?Startbytes=20f=C3=BCr=20virtuelle=20Ger=C3=A4te?= =?UTF-8?q?verbindungen=20erg=C3=A4nzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/client.json | 5 +- internal/client/connection.go | 64 +++++++++ internal/config/config.go | 1 + internal/config/config_test.go | 248 +++++++++++++++++++++++++-------- 4 files changed, 259 insertions(+), 59 deletions(-) diff --git a/configs/client.json b/configs/client.json index 275238c..69af052 100644 --- a/configs/client.json +++ b/configs/client.json @@ -1,6 +1,6 @@ { "server": { - "address": "100.64.0.1", + "address": "192.168.178.229", "port": 5000 }, "virtual_port_range": { @@ -10,7 +10,8 @@ "virtual_ports": [ { "port": "COM100", - "remote_device": "rotor" + "remote_device": "rotor", + "startup_bytes": "57 00 00 00 00 00 00 00 00 00 00 1F 20" } ] diff --git a/internal/client/connection.go b/internal/client/connection.go index 30497de..9aefc69 100644 --- a/internal/client/connection.go +++ b/internal/client/connection.go @@ -12,13 +12,18 @@ * * Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt * übernommen. + * + * Optional können nach dem Aufbau der Datenverbindung konfigurierte + * Startbytes an das entfernte Gerät gesendet werden. * ============================================================================ */ package client import ( + "encoding/hex" "fmt" "net" + "strings" "git.lang-dieter.de/rs2322tcp/internal/config" "git.lang-dieter.de/rs2322tcp/internal/transport" @@ -36,6 +41,11 @@ import ( // The local virtual port is opened using exactly the port number specified // in virtualPortConfig.Port. This preserves the user's configured mapping // 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( c *Client, 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 } +// 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 /////////////////////////////////////////////////////////////////////////////// diff --git a/internal/config/config.go b/internal/config/config.go index ec076dc..e706582 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -79,6 +79,7 @@ type VirtualPortRangeConfig struct { type VirtualPortConfig struct { Port string `json:"port"` RemoteDevice string `json:"remote_device"` + StartupBytes string `json:"startup_bytes,omitempty"` } /////////////////////////////////////////////////////////////////////////////// diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0587a1f..523fef9 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -59,13 +59,15 @@ func TestLoadServer(t *testing.T) { } if cfg.Listen.Address != "0.0.0.0" { - t.Errorf("Listen.Address = %q, want %q", - cfg.Listen.Address, "0.0.0.0") + t.Errorf( + "Listen.Address = %q, want %q", + cfg.Listen.Address, + "0.0.0.0", + ) } if cfg.Listen.Port != 5000 { - t.Errorf("Listen.Port = %d, want %d", - cfg.Listen.Port, 5000) + t.Errorf("Listen.Port = %d, want %d", cfg.Listen.Port, 5000) } if len(cfg.Devices) != 1 { @@ -75,18 +77,19 @@ func TestLoadServer(t *testing.T) { device := cfg.Devices[0] if device.ID != "radio" { - t.Errorf("Device.ID = %q, want %q", - device.ID, "radio") + t.Errorf("Device.ID = %q, want %q", device.ID, "radio") } if device.SerialPort != "/dev/ttyUSB0" { - t.Errorf("Device.SerialPort = %q, want %q", - device.SerialPort, "/dev/ttyUSB0") + t.Errorf( + "Device.SerialPort = %q, want %q", + device.SerialPort, + "/dev/ttyUSB0", + ) } if device.BaudRate != 9600 { - t.Errorf("Device.BaudRate = %d, want %d", - device.BaudRate, 9600) + t.Errorf("Device.BaudRate = %d, want %d", device.BaudRate, 9600) } } @@ -121,40 +124,106 @@ func TestLoadClient(t *testing.T) { } if cfg.Server.Address != "100.64.0.10" { - t.Errorf("Server.Address = %q, want %q", - cfg.Server.Address, "100.64.0.10") + t.Errorf( + "Server.Address = %q, want %q", + cfg.Server.Address, + "100.64.0.10", + ) } if cfg.Server.Port != 5000 { - t.Errorf("Server.Port = %d, want %d", - cfg.Server.Port, 5000) + t.Errorf("Server.Port = %d, want %d", cfg.Server.Port, 5000) } if cfg.VirtualPortRange.First != config.DefaultVirtualPortFirst { - t.Errorf("VirtualPortRange.First = %d, want %d", - 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) + 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)) + t.Fatalf( + "len(VirtualPorts) = %d, want 1", + len(cfg.VirtualPorts), + ) } virtualPort := cfg.VirtualPorts[0] if virtualPort.Port != "COM7" { - t.Errorf("VirtualPort.Port = %q, want %q", - virtualPort.Port, "COM7") + t.Errorf( + "VirtualPort.Port = %q, want %q", + virtualPort.Port, + "COM7", + ) } if virtualPort.RemoteDevice != "radio" { - t.Errorf("VirtualPort.RemoteDevice = %q, want %q", - virtualPort.RemoteDevice, "radio") + t.Errorf( + "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 { - t.Errorf("VirtualPortRange.First = %d, want 200", - cfg.VirtualPortRange.First) + 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) + t.Errorf( + "VirtualPortRange.Last = %d, want 299", + cfg.VirtualPortRange.Last, + ) } } @@ -215,6 +288,7 @@ func TestSaveAndLoadClient(t *testing.T) { { Port: "COM7", RemoteDevice: "radio", + StartupBytes: "57 00 00 1F 20", }, { Port: "COM8", @@ -241,9 +315,11 @@ func TestSaveAndLoadClient(t *testing.T) { } 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(original.VirtualPorts)) + len(original.VirtualPorts), + ) } for i := range original.VirtualPorts { @@ -402,8 +478,29 @@ func TestExampleClientConfig(t *testing.T) { t.Fatalf("LoadClient() failed: %v", err) } - if len(cfg.VirtualPorts) != 2 { - t.Fatalf("len(VirtualPorts) = %d, want 2", len(cfg.VirtualPorts)) + if len(cfg.VirtualPorts) != 1 { + 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() if remote.ID != "radio" { - t.Errorf("RemoteDevice.ID = %q, want %q", - remote.ID, "radio") + t.Errorf( + "RemoteDevice.ID = %q, want %q", + remote.ID, + "radio", + ) } if remote.Name != "Funkgerät" { - t.Errorf("RemoteDevice.Name = %q, want %q", - remote.Name, "Funkgerät") + t.Errorf( + "RemoteDevice.Name = %q, want %q", + remote.Name, + "Funkgerät", + ) } if remote.BaudRate != 9600 { - t.Errorf("RemoteDevice.BaudRate = %d, want %d", - remote.BaudRate, 9600) + t.Errorf( + "RemoteDevice.BaudRate = %d, want %d", + remote.BaudRate, + 9600, + ) } if remote.DataBits != 8 { - t.Errorf("RemoteDevice.DataBits = %d, want %d", - remote.DataBits, 8) + t.Errorf( + "RemoteDevice.DataBits = %d, want %d", + remote.DataBits, + 8, + ) } if remote.Parity != "none" { - t.Errorf("RemoteDevice.Parity = %q, want %q", - remote.Parity, "none") + t.Errorf( + "RemoteDevice.Parity = %q, want %q", + remote.Parity, + "none", + ) } if remote.StopBits != 1 { - t.Errorf("RemoteDevice.StopBits = %d, want %d", - remote.StopBits, 1) + t.Errorf( + "RemoteDevice.StopBits = %d, want %d", + remote.StopBits, + 1, + ) } } @@ -490,28 +605,41 @@ func TestServerRemoteDevices(t *testing.T) { } if list.Devices[0].ID != "radio" { - t.Errorf("Devices[0].ID = %q, want %q", - list.Devices[0].ID, "radio") + t.Errorf( + "Devices[0].ID = %q, want %q", + list.Devices[0].ID, + "radio", + ) } if list.Devices[1].ID != "rotor" { - t.Errorf("Devices[1].ID = %q, want %q", - list.Devices[1].ID, "rotor") + t.Errorf( + "Devices[1].ID = %q, want %q", + list.Devices[1].ID, + "rotor", + ) } if list.Devices[0].Name != "Funkgerät" { - t.Errorf("Devices[0].Name = %q, want %q", - list.Devices[0].Name, "Funkgerät") + t.Errorf( + "Devices[0].Name = %q, want %q", + list.Devices[0].Name, + "Funkgerät", + ) } if list.Devices[0].BaudRate != 9600 { - t.Errorf("Devices[0].BaudRate = %d, want 9600", - list.Devices[0].BaudRate) + 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 4800", - list.Devices[1].BaudRate) + t.Errorf( + "Devices[1].BaudRate = %d, want 4800", + list.Devices[1].BaudRate, + ) } } @@ -561,11 +689,17 @@ func TestRemoteDeviceJSON(t *testing.T) { } 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" { - t.Errorf("Devices[0].ID = %q, want %q", - decoded.Devices[0].ID, "radio") + t.Errorf( + "Devices[0].ID = %q, want %q", + decoded.Devices[0].ID, + "radio", + ) } }