/* * ============================================================================ * 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 }