230 lines
5.2 KiB
Go
230 lines
5.2 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"
|
|
"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
|
|
|
|
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
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// 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 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
|
|
}
|