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",
"port": 5000
},
"hardware_error_response": "ERROR - HARDWARE NOT AVAILABLE",
"devices": [
{
"id": "radio",

View file

@ -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",

View file

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

View file

@ -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
}

View file

@ -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")

View file

@ -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()
}

View file

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

View file

@ -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",

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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)