Hardwarefehler an Client zurückmelden

This commit is contained in:
Dieter Lang 2026-08-11 15:19:36 +02:00
parent a33546fbed
commit 1621ed4330
12 changed files with 247 additions and 22 deletions

View file

@ -3,6 +3,9 @@
"address": "0.0.0.0", "address": "0.0.0.0",
"port": 5000 "port": 5000
}, },
"hardware_error_response": "ERROR - HARDWARE NOT AVAILABLE",
"devices": [ "devices": [
{ {
"id": "radio", "id": "radio",

View file

@ -29,6 +29,16 @@ import (
"git.lang-dieter.de/rs2322tcp/internal/server" "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()) { func startVirtualSerialPair(t *testing.T) (string, string, func()) {
t.Helper() t.Helper()
@ -79,6 +89,10 @@ func startVirtualSerialPair(t *testing.T) (string, string, func()) {
} }
} }
///////////////////////////////////////////////////////////////////////////////
// Read helper
///////////////////////////////////////////////////////////////////////////////
func readExactWithTimeout( func readExactWithTimeout(
t *testing.T, t *testing.T,
reader io.Reader, reader io.Reader,
@ -105,6 +119,10 @@ func readExactWithTimeout(
} }
} }
///////////////////////////////////////////////////////////////////////////////
// Integration test
///////////////////////////////////////////////////////////////////////////////
func TestClientServerSerialIntegration(t *testing.T) { func TestClientServerSerialIntegration(t *testing.T) {
serialA, serialB, cleanup := startVirtualSerialPair(t) serialA, serialB, cleanup := startVirtualSerialPair(t)
defer cleanup() defer cleanup()
@ -130,6 +148,7 @@ func TestClientServerSerialIntegration(t *testing.T) {
Address: "127.0.0.1", Address: "127.0.0.1",
Port: controlPort, Port: controlPort,
}, },
HardwareErrorResponse: testHardwareErrorResponse,
Devices: []config.DeviceConfig{ Devices: []config.DeviceConfig{
{ {
ID: "radio", ID: "radio",

View file

@ -82,6 +82,7 @@ func TestClientRuntimeSerialIntegration(t *testing.T) {
Address: "127.0.0.1", Address: "127.0.0.1",
Port: controlPort, Port: controlPort,
}, },
HardwareErrorResponse: testHardwareErrorResponse,
Devices: []config.DeviceConfig{ Devices: []config.DeviceConfig{
{ {
ID: "radio", ID: "radio",

View file

@ -23,8 +23,9 @@ import (
// ServerConfig contains the complete server configuration. // ServerConfig contains the complete server configuration.
type ServerConfig struct { type ServerConfig struct {
Listen ListenConfig `json:"listen"` Listen ListenConfig `json:"listen"`
Devices []DeviceConfig `json:"devices"` HardwareErrorResponse string `json:"hardware_error_response"`
Devices []DeviceConfig `json:"devices"`
} }
// ListenConfig contains the network listener configuration. // ListenConfig contains the network listener configuration.
@ -88,6 +89,9 @@ const (
// DefaultVirtualPortLast is the last virtual USB serial port number // DefaultVirtualPortLast is the last virtual USB serial port number
// used when no virtual port range is specified. // used when no virtual port range is specified.
DefaultVirtualPortLast = 199 DefaultVirtualPortLast = 199
// Fehlermeldung wenn Hardware nicht erreichbar ist
DefaultHardwareErrorResponse = "ERROR - HARDWARE NOT AVAILABLE"
) )
// DefaultVirtualPortRange returns the default virtual port range. // DefaultVirtualPortRange returns the default virtual port range.
@ -110,6 +114,10 @@ func LoadServer(filename string) (*ServerConfig, error) {
return nil, err return nil, err
} }
if cfg.HardwareErrorResponse == "" {
cfg.HardwareErrorResponse = DefaultHardwareErrorResponse
}
if err := cfg.Validate(); err != nil { if err := cfg.Validate(); err != nil {
return nil, err return nil, err
} }

View file

@ -50,6 +50,14 @@ func TestLoadServer(t *testing.T) {
t.Fatalf("LoadServer() failed: %v", err) 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" { if cfg.Listen.Address != "0.0.0.0" {
t.Errorf("Listen.Address = %q, want %q", t.Errorf("Listen.Address = %q, want %q",
cfg.Listen.Address, "0.0.0.0") cfg.Listen.Address, "0.0.0.0")

View file

@ -16,6 +16,7 @@ package serial
import ( import (
"fmt" "fmt"
"io" "io"
"sync"
bugserial "go.bug.st/serial" bugserial "go.bug.st/serial"
@ -28,6 +29,7 @@ import (
// Connection represents an opened serial connection. // Connection represents an opened serial connection.
type Connection struct { type Connection struct {
mu sync.Mutex
port bugserial.Port port bugserial.Port
} }
@ -126,20 +128,36 @@ func createMode(device config.DeviceConfig) (*bugserial.Mode, error) {
// Read reads data from the serial connection. // Read reads data from the serial connection.
func (c *Connection) Read(p []byte) (int, error) { func (c *Connection) Read(p []byte) (int, error) {
if c == nil || c.port == nil { if c == nil {
return 0, io.ErrClosedPipe 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. // Write writes data to the serial connection.
func (c *Connection) Write(p []byte) (int, error) { func (c *Connection) Write(p []byte) (int, error) {
if c == nil || c.port == nil { if c == nil {
return 0, io.ErrClosedPipe 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. // Close closes the serial connection.
func (c *Connection) Close() error { func (c *Connection) Close() error {
if c == nil || c.port == nil { if c == nil {
return 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 c.port = nil
return err c.mu.Unlock()
return port.Close()
} }

View file

@ -336,6 +336,7 @@ func (s *ControlServer) runDataListener(
dataConnection, err := NewDataConnection( dataConnection, err := NewDataConnection(
tcpConn, tcpConn,
serialConn, serialConn,
s.config.HardwareErrorResponse,
) )
if err != nil { if err != nil {
log.Printf( log.Printf(

View file

@ -40,6 +40,7 @@ func testServerConfig() *config.ServerConfig {
Address: "127.0.0.1", Address: "127.0.0.1",
Port: 5000, Port: 5000,
}, },
HardwareErrorResponse: testHardwareErrorResponse,
Devices: []config.DeviceConfig{ Devices: []config.DeviceConfig{
{ {
ID: "radio", ID: "radio",
@ -451,6 +452,7 @@ func TestControlServerDataConnection(t *testing.T) {
Address: "127.0.0.1", Address: "127.0.0.1",
Port: 5000, Port: 5000,
}, },
HardwareErrorResponse: testHardwareErrorResponse,
Devices: []config.DeviceConfig{ Devices: []config.DeviceConfig{
{ {
ID: "radio", ID: "radio",

View file

@ -1,7 +1,7 @@
/* /*
* ============================================================================ * ============================================================================
* Projekt.....: rs2322tcp * Projekt.....: rs2322tcp
* Datei.......: data_connection.go * Datei.......: internal/server/data_connection.go
* Copyright (C) 2026 Dieter Lang * Copyright (C) 2026 Dieter Lang
* *
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
@ -31,12 +31,17 @@ import (
// TCP -> Serial // TCP -> Serial
// 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 // The serial side is represented by an io.ReadWriteCloser so that this
// server component does not depend directly on the concrete serial // server component does not depend directly on the concrete serial
// implementation. // implementation.
type DataConnection struct { type DataConnection struct {
tcp net.Conn tcp net.Conn
serial io.ReadWriteCloser serial io.ReadWriteCloser
hardwareErrorResponse string
closeOnce sync.Once closeOnce sync.Once
closeErr error closeErr error
@ -50,6 +55,7 @@ type DataConnection struct {
func NewDataConnection( func NewDataConnection(
tcp net.Conn, tcp net.Conn,
serial io.ReadWriteCloser, serial io.ReadWriteCloser,
hardwareErrorResponse string,
) (*DataConnection, error) { ) (*DataConnection, error) {
if tcp == nil { if tcp == nil {
return nil, fmt.Errorf("TCP connection is nil") return nil, fmt.Errorf("TCP connection is nil")
@ -59,9 +65,14 @@ func NewDataConnection(
return nil, fmt.Errorf("serial connection is nil") return nil, fmt.Errorf("serial connection is nil")
} }
if hardwareErrorResponse == "" {
return nil, fmt.Errorf("hardware error response is empty")
}
return &DataConnection{ return &DataConnection{
tcp: tcp, tcp: tcp,
serial: serial, serial: serial,
hardwareErrorResponse: hardwareErrorResponse,
}, nil }, nil
} }
@ -87,6 +98,54 @@ func (c *DataConnection) SerialConn() io.ReadWriteCloser {
return c.serial 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 // Run
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -96,6 +155,10 @@ func (c *DataConnection) SerialConn() io.ReadWriteCloser {
// Run blocks until one of the two transfer directions terminates. // Run blocks until one of the two transfer directions terminates.
// The other direction is then stopped and both connections are closed. // 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. // The first non-EOF transfer error is returned.
func (c *DataConnection) Run() error { func (c *DataConnection) Run() error {
if c == nil { if c == nil {
@ -111,15 +174,13 @@ func (c *DataConnection) Run() error {
go func() { go func() {
defer wg.Done() defer wg.Done()
_, err := io.Copy(c.serial, c.tcp) errCh <- c.copyTCPToSerial()
errCh <- err
}() }()
go func() { go func() {
defer wg.Done() defer wg.Done()
_, err := io.Copy(c.tcp, c.serial) errCh <- c.copySerialToTCP()
errCh <- err
}() }()
err := <-errCh err := <-errCh

View file

@ -14,6 +14,7 @@ package server_test
import ( import (
"bytes" "bytes"
"errors"
"io" "io"
"net" "net"
"sync" "sync"
@ -31,6 +32,8 @@ type testSerialConnection struct {
reader *bytes.Reader reader *bytes.Reader
writer bytes.Buffer writer bytes.Buffer
writeErr error
mu sync.Mutex mu sync.Mutex
closed bool 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) { func (s *testSerialConnection) Read(p []byte) (int, error) {
s.mu.Lock() s.mu.Lock()
@ -76,6 +87,10 @@ func (s *testSerialConnection) Write(p []byte) (int, error) {
return 0, io.ErrClosedPipe return 0, io.ErrClosedPipe
} }
if s.writeErr != nil {
return 0, s.writeErr
}
return s.writer.Write(p) return s.writer.Write(p)
} }
@ -116,6 +131,7 @@ func TestNewDataConnection(t *testing.T) {
connection, err := server.NewDataConnection( connection, err := server.NewDataConnection(
tcpServer, tcpServer,
serial, serial,
testHardwareErrorResponse,
) )
if err != nil { if err != nil {
t.Fatalf("NewDataConnection() failed: %v", err) t.Fatalf("NewDataConnection() failed: %v", err)
@ -136,6 +152,7 @@ func TestNewDataConnectionRejectsNilTCP(t *testing.T) {
connection, err := server.NewDataConnection( connection, err := server.NewDataConnection(
nil, nil,
serial, serial,
testHardwareErrorResponse,
) )
if err == nil { if err == nil {
@ -155,6 +172,7 @@ func TestNewDataConnectionRejectsNilSerial(t *testing.T) {
connection, err := server.NewDataConnection( connection, err := server.NewDataConnection(
tcpServer, tcpServer,
nil, nil,
testHardwareErrorResponse,
) )
if err == nil { if err == nil {
@ -179,6 +197,7 @@ func TestDataConnectionTCPToSerial(t *testing.T) {
connection, err := server.NewDataConnection( connection, err := server.NewDataConnection(
tcpServer, tcpServer,
serial, serial,
testHardwareErrorResponse,
) )
if err != nil { if err != nil {
t.Fatalf("NewDataConnection() failed: %v", err) 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 // Serial -> TCP
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -238,6 +311,7 @@ func TestDataConnectionSerialToTCP(t *testing.T) {
connection, err := server.NewDataConnection( connection, err := server.NewDataConnection(
tcpServer, tcpServer,
serial, serial,
testHardwareErrorResponse,
) )
if err != nil { if err != nil {
t.Fatalf("NewDataConnection() failed: %v", err) t.Fatalf("NewDataConnection() failed: %v", err)
@ -285,6 +359,7 @@ func TestDataConnectionClose(t *testing.T) {
connection, err := server.NewDataConnection( connection, err := server.NewDataConnection(
tcpServer, tcpServer,
serial, serial,
testHardwareErrorResponse,
) )
if err != nil { if err != nil {
t.Fatalf("NewDataConnection() failed: %v", err) t.Fatalf("NewDataConnection() failed: %v", err)

View file

@ -38,8 +38,9 @@ type SerialFactory func() (io.ReadWriteCloser, error)
// DataHandler accepts TCP data connections and connects them to a serial // DataHandler accepts TCP data connections and connects them to a serial
// device. // device.
type DataHandler struct { type DataHandler struct {
listener *DataListener listener *DataListener
serialFactory SerialFactory serialFactory SerialFactory
hardwareErrorResponse string
} }
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -53,6 +54,7 @@ type DataHandler struct {
func NewDataHandler( func NewDataHandler(
listener *DataListener, listener *DataListener,
serialFactory SerialFactory, serialFactory SerialFactory,
hardwareErrorResponse string,
) (*DataHandler, error) { ) (*DataHandler, error) {
if listener == nil { if listener == nil {
return nil, fmt.Errorf("data listener is nil") return nil, fmt.Errorf("data listener is nil")
@ -62,9 +64,14 @@ func NewDataHandler(
return nil, fmt.Errorf("serial factory is nil") return nil, fmt.Errorf("serial factory is nil")
} }
if hardwareErrorResponse == "" {
return nil, fmt.Errorf("hardware error response is empty")
}
return &DataHandler{ return &DataHandler{
listener: listener, listener: listener,
serialFactory: serialFactory, serialFactory: serialFactory,
hardwareErrorResponse: hardwareErrorResponse,
}, nil }, nil
} }
@ -114,6 +121,7 @@ func (h *DataHandler) handleConnection(tcpConn net.Conn) {
dataConnection, err := NewDataConnection( dataConnection, err := NewDataConnection(
tcpConn, tcpConn,
serialConn, serialConn,
h.hardwareErrorResponse,
) )
if err != nil { if err != nil {
log.Printf("create data connection: %v", err) log.Printf("create data connection: %v", err)

View file

@ -24,6 +24,12 @@ import (
"git.lang-dieter.de/rs2322tcp/internal/server" "git.lang-dieter.de/rs2322tcp/internal/server"
) )
///////////////////////////////////////////////////////////////////////////////
// Test constants
///////////////////////////////////////////////////////////////////////////////
const testHardwareErrorResponse = "TEST ERROR RESPONSE"
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Test serial connection // Test serial connection
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -119,6 +125,7 @@ func TestNewDataHandler(t *testing.T) {
func() (io.ReadWriteCloser, error) { func() (io.ReadWriteCloser, error) {
return serial, nil return serial, nil
}, },
testHardwareErrorResponse,
) )
if err != nil { if err != nil {
t.Fatalf("NewDataHandler() failed: %v", err) t.Fatalf("NewDataHandler() failed: %v", err)
@ -135,6 +142,7 @@ func TestNewDataHandlerRejectsNilListener(t *testing.T) {
func() (io.ReadWriteCloser, error) { func() (io.ReadWriteCloser, error) {
return newHandlerTestSerial(nil), nil return newHandlerTestSerial(nil), nil
}, },
testHardwareErrorResponse,
) )
if err == nil { if err == nil {
@ -156,6 +164,7 @@ func TestNewDataHandlerRejectsNilFactory(t *testing.T) {
handler, err := server.NewDataHandler( handler, err := server.NewDataHandler(
listener, listener,
nil, nil,
testHardwareErrorResponse,
) )
if err == nil { if err == nil {
@ -185,6 +194,7 @@ func TestDataHandlerTCPToSerial(t *testing.T) {
func() (io.ReadWriteCloser, error) { func() (io.ReadWriteCloser, error) {
return serial, nil return serial, nil
}, },
testHardwareErrorResponse,
) )
if err != nil { if err != nil {
t.Fatalf("NewDataHandler() failed: %v", err) t.Fatalf("NewDataHandler() failed: %v", err)
@ -255,6 +265,7 @@ func TestDataHandlerSerialToTCP(t *testing.T) {
func() (io.ReadWriteCloser, error) { func() (io.ReadWriteCloser, error) {
return serial, nil return serial, nil
}, },
testHardwareErrorResponse,
) )
if err != nil { if err != nil {
t.Fatalf("NewDataHandler() failed: %v", err) t.Fatalf("NewDataHandler() failed: %v", err)
@ -311,6 +322,7 @@ func TestDataHandlerSerialFactoryError(t *testing.T) {
func() (io.ReadWriteCloser, error) { func() (io.ReadWriteCloser, error) {
return nil, io.ErrClosedPipe return nil, io.ErrClosedPipe
}, },
testHardwareErrorResponse,
) )
if err != nil { if err != nil {
t.Fatalf("NewDataHandler() failed: %v", err) t.Fatalf("NewDataHandler() failed: %v", err)