rs2322tcp/internal/server/data_connection.go

333 lines
6.9 KiB
Go

/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: internal/server/data_connection.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Bidirektionale Datenverbindung zwischen einer TCP-Verbindung und
* einem seriellen Gerätekanal.
* ============================================================================
*/
package server
import (
"fmt"
"io"
"log"
"net"
"sync"
)
///////////////////////////////////////////////////////////////////////////////
// DataConnection
///////////////////////////////////////////////////////////////////////////////
// DataConnection connects one TCP connection with one serial device.
//
// Data is transferred bidirectionally:
//
// 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
hardwareErrorResponse string
serialMonitor bool
serialPort string
closeOnce sync.Once
closeErr error
}
///////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////
// NewDataConnection creates a new bidirectional data connection.
func NewDataConnection(
tcp net.Conn,
serial io.ReadWriteCloser,
hardwareErrorResponse string,
) (*DataConnection, error) {
if tcp == nil {
return nil, fmt.Errorf("TCP connection is nil")
}
if serial == nil {
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,
hardwareErrorResponse: hardwareErrorResponse,
}, nil
}
///////////////////////////////////////////////////////////////////////////////
// Properties
///////////////////////////////////////////////////////////////////////////////
// TCPConn returns the TCP connection.
func (c *DataConnection) TCPConn() net.Conn {
if c == nil {
return nil
}
return c.tcp
}
// SerialConn returns the serial connection.
func (c *DataConnection) SerialConn() io.ReadWriteCloser {
if c == nil {
return nil
}
return c.serial
}
// SetSerialMonitor enables or disables the data monitor.
//
// If enabled, transmitted and received data is written to the server log.
// serialPort is used only for identifying the physical interface in the
// monitor output.
func (c *DataConnection) SetSerialMonitor(
enabled bool,
serialPort string,
) {
if c == nil {
return
}
c.serialMonitor = enabled
c.serialPort = serialPort
}
///////////////////////////////////////////////////////////////////////////////
// Data monitor
///////////////////////////////////////////////////////////////////////////////
// logTCPData writes TCP data to the server log.
func (c *DataConnection) logTCPData(
direction string,
data []byte,
) {
if c == nil || !c.serialMonitor || len(data) == 0 {
return
}
log.Printf(
"%s % X",
direction,
data,
)
}
// logSerialData writes serial data to the server log.
func (c *DataConnection) logSerialData(
direction string,
data []byte,
) {
if c == nil || !c.serialMonitor || len(data) == 0 {
return
}
log.Printf(
"%s [SERIAL %s] % X",
direction,
c.serialPort,
data,
)
}
///////////////////////////////////////////////////////////////////////////////
// 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 {
c.logTCPData("TCP RX", buffer[:n])
written, writeErr := c.serial.Write(buffer[:n])
if writeErr != nil {
_, _ = io.WriteString(
c.tcp,
c.hardwareErrorResponse,
)
return writeErr
}
if written != n {
_, _ = io.WriteString(
c.tcp,
c.hardwareErrorResponse,
)
return io.ErrShortWrite
}
c.logSerialData("SERIAL TX", buffer[:n])
}
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")
}
buffer := make([]byte, 32*1024)
for {
n, err := c.serial.Read(buffer)
if n > 0 {
c.logSerialData("SERIAL RX", buffer[:n])
written := 0
for written < n {
count, writeErr := c.tcp.Write(
buffer[written:n],
)
written += count
if writeErr != nil {
return writeErr
}
if count == 0 {
return io.ErrShortWrite
}
}
c.logTCPData("TCP TX", buffer[:n])
}
if err != nil {
return err
}
}
}
///////////////////////////////////////////////////////////////////////////////
// Run
///////////////////////////////////////////////////////////////////////////////
// Run transfers data in both directions.
//
// 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 {
return fmt.Errorf("data connection is nil")
}
var wg sync.WaitGroup
errCh := make(chan error, 2)
wg.Add(2)
go func() {
defer wg.Done()
errCh <- c.copyTCPToSerial()
}()
go func() {
defer wg.Done()
errCh <- c.copySerialToTCP()
}()
err := <-errCh
_ = c.Close()
wg.Wait()
if err == nil || err == io.EOF {
return nil
}
return err
}
///////////////////////////////////////////////////////////////////////////////
// Lifecycle
///////////////////////////////////////////////////////////////////////////////
// Close closes both sides of the data connection.
//
// Close is safe to call multiple times.
func (c *DataConnection) Close() error {
if c == nil {
return nil
}
c.closeOnce.Do(func() {
var firstErr error
if c.tcp != nil {
if err := c.tcp.Close(); err != nil {
firstErr = err
}
}
if c.serial != nil {
if err := c.serial.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
c.closeErr = firstErr
})
return c.closeErr
}