From 1621ed4330142b40f34b7235a77e684474bb59d6 Mon Sep 17 00:00:00 2001 From: Dieter Lang Date: Tue, 11 Aug 2026 15:19:36 +0200 Subject: [PATCH] =?UTF-8?q?Hardwarefehler=20an=20Client=20zur=C3=BCckmelde?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/server.json | 3 + internal/client/integration_test.go | 19 +++++ internal/client/runtime_integration_test.go | 1 + internal/config/config.go | 12 +++- internal/config/config_test.go | 8 +++ internal/serial/serial.go | 41 +++++++++-- internal/server/control.go | 1 + internal/server/control_test.go | 2 + internal/server/data_connection.go | 79 ++++++++++++++++++--- internal/server/data_connection_test.go | 75 +++++++++++++++++++ internal/server/data_handler.go | 16 +++-- internal/server/data_handler_test.go | 12 ++++ 12 files changed, 247 insertions(+), 22 deletions(-) diff --git a/configs/server.json b/configs/server.json index 1db84ad..977ef60 100644 --- a/configs/server.json +++ b/configs/server.json @@ -3,6 +3,9 @@ "address": "0.0.0.0", "port": 5000 }, + + "hardware_error_response": "ERROR - HARDWARE NOT AVAILABLE", + "devices": [ { "id": "radio", diff --git a/internal/client/integration_test.go b/internal/client/integration_test.go index 5735cd6..3041811 100644 --- a/internal/client/integration_test.go +++ b/internal/client/integration_test.go @@ -29,6 +29,16 @@ import ( "git.lang-dieter.de/rs2322tcp/internal/server" ) +/////////////////////////////////////////////////////////////////////////////// +// Test configuration +/////////////////////////////////////////////////////////////////////////////// + +const testHardwareErrorResponse = "TEST ERROR RESPONSE" + +/////////////////////////////////////////////////////////////////////////////// +// Virtual serial pair +/////////////////////////////////////////////////////////////////////////////// + func startVirtualSerialPair(t *testing.T) (string, string, func()) { t.Helper() @@ -79,6 +89,10 @@ func startVirtualSerialPair(t *testing.T) (string, string, func()) { } } +/////////////////////////////////////////////////////////////////////////////// +// Read helper +/////////////////////////////////////////////////////////////////////////////// + func readExactWithTimeout( t *testing.T, reader io.Reader, @@ -105,6 +119,10 @@ func readExactWithTimeout( } } +/////////////////////////////////////////////////////////////////////////////// +// Integration test +/////////////////////////////////////////////////////////////////////////////// + func TestClientServerSerialIntegration(t *testing.T) { serialA, serialB, cleanup := startVirtualSerialPair(t) defer cleanup() @@ -130,6 +148,7 @@ func TestClientServerSerialIntegration(t *testing.T) { Address: "127.0.0.1", Port: controlPort, }, + HardwareErrorResponse: testHardwareErrorResponse, Devices: []config.DeviceConfig{ { ID: "radio", diff --git a/internal/client/runtime_integration_test.go b/internal/client/runtime_integration_test.go index e6900e4..d22aecb 100644 --- a/internal/client/runtime_integration_test.go +++ b/internal/client/runtime_integration_test.go @@ -82,6 +82,7 @@ func TestClientRuntimeSerialIntegration(t *testing.T) { Address: "127.0.0.1", Port: controlPort, }, + HardwareErrorResponse: testHardwareErrorResponse, Devices: []config.DeviceConfig{ { ID: "radio", diff --git a/internal/config/config.go b/internal/config/config.go index fa52cfe..59ccdc9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -23,8 +23,9 @@ import ( // ServerConfig contains the complete server configuration. type ServerConfig struct { - Listen ListenConfig `json:"listen"` - Devices []DeviceConfig `json:"devices"` + Listen ListenConfig `json:"listen"` + HardwareErrorResponse string `json:"hardware_error_response"` + Devices []DeviceConfig `json:"devices"` } // ListenConfig contains the network listener configuration. @@ -88,6 +89,9 @@ const ( // DefaultVirtualPortLast is the last virtual USB serial port number // used when no virtual port range is specified. DefaultVirtualPortLast = 199 + + // Fehlermeldung wenn Hardware nicht erreichbar ist + DefaultHardwareErrorResponse = "ERROR - HARDWARE NOT AVAILABLE" ) // DefaultVirtualPortRange returns the default virtual port range. @@ -110,6 +114,10 @@ func LoadServer(filename string) (*ServerConfig, error) { return nil, err } + if cfg.HardwareErrorResponse == "" { + cfg.HardwareErrorResponse = DefaultHardwareErrorResponse + } + if err := cfg.Validate(); err != nil { return nil, err } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index c09cf95..878aefd 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -50,6 +50,14 @@ func TestLoadServer(t *testing.T) { t.Fatalf("LoadServer() failed: %v", err) } + if cfg.HardwareErrorResponse != config.DefaultHardwareErrorResponse { + t.Errorf( + "HardwareErrorResponse = %q, want %q", + cfg.HardwareErrorResponse, + config.DefaultHardwareErrorResponse, + ) + } + if cfg.Listen.Address != "0.0.0.0" { t.Errorf("Listen.Address = %q, want %q", cfg.Listen.Address, "0.0.0.0") diff --git a/internal/serial/serial.go b/internal/serial/serial.go index 82bbd37..05871d9 100644 --- a/internal/serial/serial.go +++ b/internal/serial/serial.go @@ -16,6 +16,7 @@ package serial import ( "fmt" "io" + "sync" bugserial "go.bug.st/serial" @@ -28,6 +29,7 @@ import ( // Connection represents an opened serial connection. type Connection struct { + mu sync.Mutex port bugserial.Port } @@ -126,20 +128,36 @@ func createMode(device config.DeviceConfig) (*bugserial.Mode, error) { // Read reads data from the serial connection. func (c *Connection) Read(p []byte) (int, error) { - if c == nil || c.port == nil { + if c == nil { return 0, io.ErrClosedPipe } - return c.port.Read(p) + c.mu.Lock() + port := c.port + c.mu.Unlock() + + if port == nil { + return 0, io.ErrClosedPipe + } + + return port.Read(p) } // Write writes data to the serial connection. func (c *Connection) Write(p []byte) (int, error) { - if c == nil || c.port == nil { + if c == nil { return 0, io.ErrClosedPipe } - return c.port.Write(p) + c.mu.Lock() + port := c.port + c.mu.Unlock() + + if port == nil { + return 0, io.ErrClosedPipe + } + + return port.Write(p) } /////////////////////////////////////////////////////////////////////////////// @@ -148,12 +166,21 @@ func (c *Connection) Write(p []byte) (int, error) { // Close closes the serial connection. func (c *Connection) Close() error { - if c == nil || c.port == nil { + if c == nil { return nil } - err := c.port.Close() + c.mu.Lock() + + if c.port == nil { + c.mu.Unlock() + return nil + } + + port := c.port c.port = nil - return err + c.mu.Unlock() + + return port.Close() } diff --git a/internal/server/control.go b/internal/server/control.go index c3e82f2..f79215a 100644 --- a/internal/server/control.go +++ b/internal/server/control.go @@ -336,6 +336,7 @@ func (s *ControlServer) runDataListener( dataConnection, err := NewDataConnection( tcpConn, serialConn, + s.config.HardwareErrorResponse, ) if err != nil { log.Printf( diff --git a/internal/server/control_test.go b/internal/server/control_test.go index d8567c8..e1978c2 100644 --- a/internal/server/control_test.go +++ b/internal/server/control_test.go @@ -40,6 +40,7 @@ func testServerConfig() *config.ServerConfig { Address: "127.0.0.1", Port: 5000, }, + HardwareErrorResponse: testHardwareErrorResponse, Devices: []config.DeviceConfig{ { ID: "radio", @@ -451,6 +452,7 @@ func TestControlServerDataConnection(t *testing.T) { Address: "127.0.0.1", Port: 5000, }, + HardwareErrorResponse: testHardwareErrorResponse, Devices: []config.DeviceConfig{ { ID: "radio", diff --git a/internal/server/data_connection.go b/internal/server/data_connection.go index 199e1d6..586881a 100644 --- a/internal/server/data_connection.go +++ b/internal/server/data_connection.go @@ -1,7 +1,7 @@ /* * ============================================================================ * Projekt.....: rs2322tcp - * Datei.......: data_connection.go + * Datei.......: internal/server/data_connection.go * Copyright (C) 2026 Dieter Lang * * SPDX-License-Identifier: GPL-3.0-or-later @@ -31,12 +31,17 @@ import ( // TCP -> Serial // TCP <- Serial // +// If writing TCP data to the serial device fails, the configured hardware +// error response is sent back over the TCP connection before the connection +// is terminated. +// // The serial side is represented by an io.ReadWriteCloser so that this // server component does not depend directly on the concrete serial // implementation. type DataConnection struct { - tcp net.Conn - serial io.ReadWriteCloser + tcp net.Conn + serial io.ReadWriteCloser + hardwareErrorResponse string closeOnce sync.Once closeErr error @@ -50,6 +55,7 @@ type DataConnection struct { func NewDataConnection( tcp net.Conn, serial io.ReadWriteCloser, + hardwareErrorResponse string, ) (*DataConnection, error) { if tcp == nil { return nil, fmt.Errorf("TCP connection is nil") @@ -59,9 +65,14 @@ func NewDataConnection( return nil, fmt.Errorf("serial connection is nil") } + if hardwareErrorResponse == "" { + return nil, fmt.Errorf("hardware error response is empty") + } + return &DataConnection{ - tcp: tcp, - serial: serial, + tcp: tcp, + serial: serial, + hardwareErrorResponse: hardwareErrorResponse, }, nil } @@ -87,6 +98,54 @@ func (c *DataConnection) SerialConn() io.ReadWriteCloser { return c.serial } +/////////////////////////////////////////////////////////////////////////////// +// Data transfer +/////////////////////////////////////////////////////////////////////////////// + +// copyTCPToSerial transfers data from the TCP connection to the serial +// device. +// +// If writing to the serial device fails, the configured hardware error +// response is sent back to the TCP client before the transfer terminates. +func (c *DataConnection) copyTCPToSerial() error { + if c == nil { + return fmt.Errorf("data connection is nil") + } + + buffer := make([]byte, 32*1024) + + for { + n, err := c.tcp.Read(buffer) + + if n > 0 { + if _, writeErr := c.serial.Write(buffer[:n]); writeErr != nil { + _, _ = io.WriteString( + c.tcp, + c.hardwareErrorResponse, + ) + + return writeErr + } + } + + if err != nil { + return err + } + } +} + +// copySerialToTCP transfers data from the serial device to the TCP +// connection. +func (c *DataConnection) copySerialToTCP() error { + if c == nil { + return fmt.Errorf("data connection is nil") + } + + _, err := io.Copy(c.tcp, c.serial) + + return err +} + /////////////////////////////////////////////////////////////////////////////// // Run /////////////////////////////////////////////////////////////////////////////// @@ -96,6 +155,10 @@ func (c *DataConnection) SerialConn() io.ReadWriteCloser { // Run blocks until one of the two transfer directions terminates. // The other direction is then stopped and both connections are closed. // +// If writing data from TCP to the serial device fails, the configured +// hardware error response is sent to the TCP client before the connection +// is closed. +// // The first non-EOF transfer error is returned. func (c *DataConnection) Run() error { if c == nil { @@ -111,15 +174,13 @@ func (c *DataConnection) Run() error { go func() { defer wg.Done() - _, err := io.Copy(c.serial, c.tcp) - errCh <- err + errCh <- c.copyTCPToSerial() }() go func() { defer wg.Done() - _, err := io.Copy(c.tcp, c.serial) - errCh <- err + errCh <- c.copySerialToTCP() }() err := <-errCh diff --git a/internal/server/data_connection_test.go b/internal/server/data_connection_test.go index 314755d..d45732b 100644 --- a/internal/server/data_connection_test.go +++ b/internal/server/data_connection_test.go @@ -14,6 +14,7 @@ package server_test import ( "bytes" + "errors" "io" "net" "sync" @@ -31,6 +32,8 @@ type testSerialConnection struct { reader *bytes.Reader writer bytes.Buffer + writeErr error + mu sync.Mutex closed bool @@ -45,6 +48,14 @@ func newTestSerialConnection(data []byte) *testSerialConnection { } } +func newFailingTestSerialConnection(writeErr error) *testSerialConnection { + return &testSerialConnection{ + reader: bytes.NewReader(nil), + writeErr: writeErr, + closeCh: make(chan struct{}), + } +} + func (s *testSerialConnection) Read(p []byte) (int, error) { s.mu.Lock() @@ -76,6 +87,10 @@ func (s *testSerialConnection) Write(p []byte) (int, error) { return 0, io.ErrClosedPipe } + if s.writeErr != nil { + return 0, s.writeErr + } + return s.writer.Write(p) } @@ -116,6 +131,7 @@ func TestNewDataConnection(t *testing.T) { connection, err := server.NewDataConnection( tcpServer, serial, + testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataConnection() failed: %v", err) @@ -136,6 +152,7 @@ func TestNewDataConnectionRejectsNilTCP(t *testing.T) { connection, err := server.NewDataConnection( nil, serial, + testHardwareErrorResponse, ) if err == nil { @@ -155,6 +172,7 @@ func TestNewDataConnectionRejectsNilSerial(t *testing.T) { connection, err := server.NewDataConnection( tcpServer, nil, + testHardwareErrorResponse, ) if err == nil { @@ -179,6 +197,7 @@ func TestDataConnectionTCPToSerial(t *testing.T) { connection, err := server.NewDataConnection( tcpServer, serial, + testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataConnection() failed: %v", err) @@ -223,6 +242,60 @@ func TestDataConnectionTCPToSerial(t *testing.T) { } } +/////////////////////////////////////////////////////////////////////////////// +// TCP -> Serial error +/////////////////////////////////////////////////////////////////////////////// + +func TestDataConnectionTCPToSerialWriteError(t *testing.T) { + tcpServer, tcpClient := net.Pipe() + defer tcpClient.Close() + + writeErr := errors.New("hardware write failed") + + serial := newFailingTestSerialConnection(writeErr) + + connection, err := server.NewDataConnection( + tcpServer, + serial, + testHardwareErrorResponse, + ) + if err != nil { + t.Fatalf("NewDataConnection() failed: %v", err) + } + + done := make(chan error, 1) + + go func() { + done <- connection.Run() + }() + + testData := []byte("hello unavailable hardware") + + if _, err := tcpClient.Write(testData); err != nil { + t.Fatalf("TCP Write() failed: %v", err) + } + + errorResponse := make([]byte, len(testHardwareErrorResponse)) + + if _, err := io.ReadFull(tcpClient, errorResponse); err != nil { + t.Fatalf("TCP Read() failed: %v", err) + } + + if string(errorResponse) != testHardwareErrorResponse { + t.Fatalf( + "error response = %q, want %q", + string(errorResponse), + testHardwareErrorResponse, + ) + } + + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("DataConnection.Run() did not terminate") + } +} + /////////////////////////////////////////////////////////////////////////////// // Serial -> TCP /////////////////////////////////////////////////////////////////////////////// @@ -238,6 +311,7 @@ func TestDataConnectionSerialToTCP(t *testing.T) { connection, err := server.NewDataConnection( tcpServer, serial, + testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataConnection() failed: %v", err) @@ -285,6 +359,7 @@ func TestDataConnectionClose(t *testing.T) { connection, err := server.NewDataConnection( tcpServer, serial, + testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataConnection() failed: %v", err) diff --git a/internal/server/data_handler.go b/internal/server/data_handler.go index 274ad5c..1ef1549 100644 --- a/internal/server/data_handler.go +++ b/internal/server/data_handler.go @@ -38,8 +38,9 @@ type SerialFactory func() (io.ReadWriteCloser, error) // DataHandler accepts TCP data connections and connects them to a serial // device. type DataHandler struct { - listener *DataListener - serialFactory SerialFactory + listener *DataListener + serialFactory SerialFactory + hardwareErrorResponse string } /////////////////////////////////////////////////////////////////////////////// @@ -53,6 +54,7 @@ type DataHandler struct { func NewDataHandler( listener *DataListener, serialFactory SerialFactory, + hardwareErrorResponse string, ) (*DataHandler, error) { if listener == nil { return nil, fmt.Errorf("data listener is nil") @@ -62,9 +64,14 @@ func NewDataHandler( return nil, fmt.Errorf("serial factory is nil") } + if hardwareErrorResponse == "" { + return nil, fmt.Errorf("hardware error response is empty") + } + return &DataHandler{ - listener: listener, - serialFactory: serialFactory, + listener: listener, + serialFactory: serialFactory, + hardwareErrorResponse: hardwareErrorResponse, }, nil } @@ -114,6 +121,7 @@ func (h *DataHandler) handleConnection(tcpConn net.Conn) { dataConnection, err := NewDataConnection( tcpConn, serialConn, + h.hardwareErrorResponse, ) if err != nil { log.Printf("create data connection: %v", err) diff --git a/internal/server/data_handler_test.go b/internal/server/data_handler_test.go index ca40d56..7812d98 100644 --- a/internal/server/data_handler_test.go +++ b/internal/server/data_handler_test.go @@ -24,6 +24,12 @@ import ( "git.lang-dieter.de/rs2322tcp/internal/server" ) +/////////////////////////////////////////////////////////////////////////////// +// Test constants +/////////////////////////////////////////////////////////////////////////////// + +const testHardwareErrorResponse = "TEST ERROR RESPONSE" + /////////////////////////////////////////////////////////////////////////////// // Test serial connection /////////////////////////////////////////////////////////////////////////////// @@ -119,6 +125,7 @@ func TestNewDataHandler(t *testing.T) { func() (io.ReadWriteCloser, error) { return serial, nil }, + testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataHandler() failed: %v", err) @@ -135,6 +142,7 @@ func TestNewDataHandlerRejectsNilListener(t *testing.T) { func() (io.ReadWriteCloser, error) { return newHandlerTestSerial(nil), nil }, + testHardwareErrorResponse, ) if err == nil { @@ -156,6 +164,7 @@ func TestNewDataHandlerRejectsNilFactory(t *testing.T) { handler, err := server.NewDataHandler( listener, nil, + testHardwareErrorResponse, ) if err == nil { @@ -185,6 +194,7 @@ func TestDataHandlerTCPToSerial(t *testing.T) { func() (io.ReadWriteCloser, error) { return serial, nil }, + testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataHandler() failed: %v", err) @@ -255,6 +265,7 @@ func TestDataHandlerSerialToTCP(t *testing.T) { func() (io.ReadWriteCloser, error) { return serial, nil }, + testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataHandler() failed: %v", err) @@ -311,6 +322,7 @@ func TestDataHandlerSerialFactoryError(t *testing.T) { func() (io.ReadWriteCloser, error) { return nil, io.ErrClosedPipe }, + testHardwareErrorResponse, ) if err != nil { t.Fatalf("NewDataHandler() failed: %v", err)