rs2322tcp/internal/client/connection.go

188 lines
4.8 KiB
Go

/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: internal/client/connection.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Aufbau einer Verbindung zwischen einem lokalen virtuellen seriellen Port
* und dem zugehörigen entfernten Gerät.
*
* Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt
* übernommen.
*
* Optional können nach dem Aufbau der Datenverbindung konfigurierte
* Startbytes an das entfernte Gerät gesendet werden.
* ============================================================================
*/
package client
import (
"encoding/hex"
"fmt"
"net"
"strings"
"git.lang-dieter.de/rs2322tcp/internal/config"
"git.lang-dieter.de/rs2322tcp/internal/transport"
)
///////////////////////////////////////////////////////////////////////////////
// Virtual port connection
///////////////////////////////////////////////////////////////////////////////
// connectVirtualPort opens the configured local virtual port and the
// corresponding remote TCP data connection and returns both endpoints.
//
// The caller owns the returned virtual port and TCP connection.
//
// The local virtual port is opened using exactly the port number specified
// in virtualPortConfig.Port. This preserves the user's configured mapping
// between local virtual interfaces and remote devices.
//
// If StartupBytes are configured, they are sent once immediately after the
// TCP data connection has been established. Invalid startup bytes do not
// abort the connection setup. A warning is printed and the connection
// continues normally.
func connectVirtualPort(
c *Client,
manager *VirtualPortManager,
virtualPortConfig config.VirtualPortConfig,
devices []transport.RemoteDeviceInfo,
) (*ManagedVirtualPort, net.Conn, error) {
if c == nil || c.conn == nil {
return nil, nil, fmt.Errorf("client is not connected")
}
if manager == nil {
return nil, nil, fmt.Errorf("virtual port manager is nil")
}
device, err := findRemoteDevice(virtualPortConfig, devices)
if err != nil {
return nil, nil, err
}
portNumber, err := virtualPortNumber(virtualPortConfig.Port)
if err != nil {
return nil, nil, err
}
virtualPort, err := manager.OpenSpecific(portNumber)
if err != nil {
return nil, nil, fmt.Errorf(
"open virtual port %q for %q: %w",
virtualPortConfig.Port,
virtualPortConfig.RemoteDevice,
err,
)
}
conn, err := c.OpenDataConnection(device)
if err != nil {
_ = virtualPort.Close()
return nil, nil, fmt.Errorf(
"open data connection for %q: %w",
device.ID,
err,
)
}
if err := sendStartupBytes(conn, virtualPortConfig); err != nil {
fmt.Printf(
"warning: startup bytes for %q were not sent: %v\n",
virtualPortConfig.RemoteDevice,
err,
)
}
return virtualPort, conn, nil
}
// sendStartupBytes sends the optional configured startup byte sequence over
// the already established TCP data connection.
//
// An empty StartupBytes configuration does not send anything and is not an
// error.
//
// The configured value may contain spaces between hexadecimal bytes, for
// example:
//
// 57 00 00 00 00 00 00 00 00 00 00 1F 20
func sendStartupBytes(
conn net.Conn,
virtualPortConfig config.VirtualPortConfig,
) error {
startupBytes := strings.TrimSpace(virtualPortConfig.StartupBytes)
if startupBytes == "" {
return nil
}
hexText := strings.Join(strings.Fields(startupBytes), "")
data, err := hex.DecodeString(hexText)
if err != nil {
return fmt.Errorf("invalid hexadecimal byte sequence: %w", err)
}
if len(data) == 0 {
return nil
}
written, err := conn.Write(data)
if err != nil {
return fmt.Errorf("send startup bytes: %w", err)
}
if written != len(data) {
return fmt.Errorf(
"send startup bytes: wrote %d of %d bytes",
written,
len(data),
)
}
return nil
}
///////////////////////////////////////////////////////////////////////////////
// Bridge connection
///////////////////////////////////////////////////////////////////////////////
// connectBridge creates the local virtual port, opens the corresponding
// remote TCP data connection and creates the data bridge.
//
// The caller owns the returned bridge and its endpoints.
func connectBridge(
c *Client,
manager *VirtualPortManager,
virtualPortConfig config.VirtualPortConfig,
devices []transport.RemoteDeviceInfo,
) (*Bridge, *ManagedVirtualPort, net.Conn, error) {
virtualPort, conn, err := connectVirtualPort(
c,
manager,
virtualPortConfig,
devices,
)
if err != nil {
return nil, nil, nil, err
}
bridge, err := NewBridge(virtualPort, conn)
if err != nil {
_ = conn.Close()
_ = virtualPort.Close()
return nil, nil, nil, fmt.Errorf(
"create bridge for %q: %w",
virtualPortConfig.RemoteDevice,
err,
)
}
return bridge, virtualPort, conn, nil
}