Add Linux PTY support and device simulator

This commit is contained in:
Dieter Lang 2026-08-10 12:42:22 +02:00
parent 49f72c376a
commit 2a61b5a79c
6 changed files with 714 additions and 120 deletions

View file

@ -5,7 +5,11 @@ Alle wesentlichen Änderungen am Projekt werden in dieser Datei dokumentiert.
## [Unreleased] ## [Unreleased]
### Added ### Added
- Linux-PTY-Unterstützung für die clientseitige virtuelle serielle Schnittstelle
- Abstraktion der virtuellen seriellen Schnittstelle über `VirtualSerial`
- Automatisierter bidirektionaler PTY-Test mit allen 256 möglichen Bytewerten
- Eigenständiges Programm `rs2322tcp-device-sim` als Simulator für ein angeschlossenes serielles Gerät
- Hexadezimale Darstellung der vom Geräte-Simulator empfangenen Rohdaten
- Implementierung der seriellen Schnittstellenschicht unter `internal/serial` - Implementierung der seriellen Schnittstellenschicht unter `internal/serial`
- Verwendung von `go.bug.st/serial v1.7.1` für die Ansteuerung der seriellen Schnittstellen - Verwendung von `go.bug.st/serial v1.7.1` für die Ansteuerung der seriellen Schnittstellen
- Konfigurationsunterstützung für Baudrate, Datenbits, Parität und Stopbits - Konfigurationsunterstützung für Baudrate, Datenbits, Parität und Stopbits
@ -45,7 +49,8 @@ Alle wesentlichen Änderungen am Projekt werden in dieser Datei dokumentiert.
- Race Condition im `DataListener` erkannt und behoben - Race Condition im `DataListener` erkannt und behoben
- Client-/Server-Integrationstest erfolgreich - Client-/Server-Integrationstest erfolgreich
- Race-Test des Client-/Server-Integrationstests erfolgreich - Race-Test des Client-/Server-Integrationstests erfolgreich
- Automatisierter bidirektionaler PTY-Test mit einem vollständigen Bytebereich von `0x00` bis `0xFF`
- Race-Detection für die neue Linux-PTY-Komponente erfolgreich
### Status ### Status
Die grundlegende TCP-/RS232-Datenübertragung ist serverseitig implementiert und der Client kann inzwischen die Control-Verbindung sowie die zugehörige Data-Verbindung aufbauen. Die grundlegende TCP-/RS232-Datenübertragung ist serverseitig implementiert und der Client kann inzwischen die Control-Verbindung sowie die zugehörige Data-Verbindung aufbauen.
@ -91,8 +96,16 @@ TCP → RS232
RS232 → TCP RS232 → TCP
``` ```
Die clientseitige Bereitstellung einer virtuellen seriellen Schnittstelle für die vorhandene Hersteller-Software ist noch nicht implementiert. Die clientseitige Linux-PTY-Grundlage für eine virtuelle serielle Schnittstelle
ist implementiert und durch automatisierte Tests abgesichert.
Der Geräte-Simulator ist als eigenständiges Testprogramm vorhanden und kann
eine serielle Schnittstelle öffnen und empfangene Rohdaten hexadezimal
anzeigen.
Die TX-Funktion des Geräte-Simulators sowie die vollständige Integration der
virtuellen seriellen Schnittstelle in den Client-Datenpfad sind noch nicht
implementiert.
--- ---
## [0.0.1] - 2026-08-09 ## [0.0.1] - 2026-08-09

355
README.md
View file

@ -1,26 +1,34 @@
# rs2322tcp # rs2322tcp
`rs2322tcp` ermöglicht die transparente Übertragung von RS232-Daten über TCP/IP. `rs2322tcp` ermöglicht die transparente Übertragung von RS232-Daten über
TCP/IP.
Das Projekt ist für den Einsatz geeignet, bei dem eine vorhandene Software eine serielle Schnittstelle erwartet, das zugehörige Gerät sich jedoch an einem entfernten Standort befindet. Das Projekt ist für den Einsatz geeignet, bei dem eine vorhandene
Software eine serielle Schnittstelle erwartet, das zugehörige Gerät sich
jedoch an einem entfernten Standort befindet.
## Ziel ## Ziel
Auf der Client-Seite soll vorhandene Hersteller-Software eine normale serielle Schnittstelle vorfinden. Auf der Client-Seite soll vorhandene Hersteller-Software eine normale
serielle Schnittstelle vorfinden.
Die Daten werden vom `rs2322tcp-client` über TCP an einen entfernten `rs2322tcp-server` übertragen. Der Server verbindet die Netzwerkverbindung mit einer realen seriellen Schnittstelle. Die Daten werden vom `rs2322tcp-client` über TCP an einen entfernten
`rs2322tcp-server` übertragen. Der Server verbindet die
Netzwerkverbindung mit einer realen seriellen Schnittstelle.
Damit sollen weder die vorhandene Software auf dem Client noch die angeschlossenen Geräte auf der Remote-Seite erkennen müssen, dass die Verbindung über ein Netzwerk erfolgt. Damit sollen weder die vorhandene Software auf dem Client noch die
angeschlossenen Geräte auf der Remote-Seite erkennen müssen, dass die
Verbindung über ein Netzwerk erfolgt.
Typische Anwendungen sind beispielsweise: Typische Anwendungen sind beispielsweise:
- Funkgeräte mit CAT-Steuerung - Funkgeräte mit CAT-Steuerung
- Antennenrotoren mit herstellerspezifischen seriellen Protokollen - Antennenrotoren mit herstellerspezifischen seriellen Protokollen
- andere Geräte, die über RS232 gesteuert werden - andere Geräte, die über RS232 gesteuert werden
## Architektur ## Architektur
```text ``` text
Client-PC Raspberry Pi 5 Client-PC Raspberry Pi 5
───────────────── ───────────────── ───────────────── ─────────────────
@ -40,79 +48,77 @@ rs2322tcp-client │
Funkgerät Rotor Funkgerät Rotor
``` ```
## Aktueller Datenpfad ## Aktueller Server-Datenpfad
Die serverseitige TCP-/RS232-Verbindung sowie die grundlegende Client-Control- und Client-Data-Kommunikation sind inzwischen implementiert: Die serverseitige TCP-/RS232-Verbindung ist implementiert:
```text ``` text
Client TCP-Control
├── TCP-Control
│ │ Session
│ ▼
│ Session ├── Gerät 1 ──► dynamischer Data-Port
│ │ │ │
│ ▼ │ ▼
│ GET_DEVICES │ TCP Data
│ │ │ │
│ ▼ │ ▼
│ Geräteinformationen │ Serial Layer
│ │ │ │
│ ▼ │ ▼
│ dynamischer Data-Port │ RS232
└── TCP-Data └── Gerät 2 ──► dynamischer Data-Port
DataListener
DataConnection
Serial Layer
RS232
``` ```
Die serielle Schnittstelle wird erst geöffnet, wenn ein Client tatsächlich eine Data-Verbindung zum entsprechenden dynamischen TCP-Port aufbaut. Die serielle Schnittstelle wird erst geöffnet, wenn ein Client
tatsächlich eine Data-Verbindung zum entsprechenden dynamischen TCP-Port
aufbaut.
Pro konfiguriertem Gerät ist nur eine aktive Data-Verbindung vorgesehen. Pro konfiguriertem Gerät ist nur eine aktive Data-Verbindung vorgesehen.
Die Datenübertragung erfolgt bidirektional: Die Datenübertragung erfolgt bidirektional:
```text ``` text
TCP ───────────────► RS232 TCP ───────────────► RS232
TCP ◄────────────── RS232 TCP ◄────────────── RS232
``` ```
Der komplette Client-/Server-Datenpfad wird inzwischen durch einen Integrationstest mit virtuellen seriellen Schnittstellen geprüft.
## Client ## Client
Der Client soll gleichberechtigt unter folgenden Betriebssystemen eingesetzt werden können: Der Client soll gleichberechtigt unter folgenden Betriebssystemen
eingesetzt werden können:
- Windows - Windows
- Linux - Linux
Die plattformspezifische Bereitstellung der virtuellen seriellen Schnittstelle wird vom gemeinsamen Client-Kern getrennt. Die plattformspezifische Bereitstellung der virtuellen seriellen
Schnittstelle wird vom gemeinsamen Client-Kern getrennt.
Der gemeinsame Client-Kern kann inzwischen: Unter Linux ist die virtuelle serielle Schnittstelle inzwischen über
einen direkt aus Go erzeugten PTY implementiert. Die Implementierung
verwendet `/dev/ptmx` und stellt den zugehörigen PTY-Slave unter
`/dev/pts/N` bereit.
- die Control-Verbindung zum Server aufbauen Die PTY-Schnittstelle wird im Raw Mode betrieben. Dadurch werden die
- den Control-Handshake durchführen übertragenen Daten als transparenter Byte-Strom behandelt.
- die konfigurierten Geräte vom Server abfragen
- den vom Server gelieferten dynamischen Data-Port verwenden
- die TCP-Data-Verbindung zum Server aufbauen
Die clientseitige virtuelle serielle Schnittstelle ist derzeit noch nicht implementiert. Die allgemeine Client-Logik verwendet dafür die Abstraktion
`VirtualSerial`. Die Linux-spezifische Implementierung ist von dieser
Abstraktion getrennt.
Für die PTY-Funktion ist keine externe Laufzeitabhängigkeit wie `socat`
erforderlich.
## Server ## Server
Der Server ist zunächst für den Betrieb auf einem Raspberry Pi 5 vorgesehen. Der Server ist zunächst für den Betrieb auf einem Raspberry Pi 5
vorgesehen.
Mehrere USB-to-RS232-Adapter können angeschlossen werden. Anzahl, Bezeichnung und serielle Parameter der Anschlüsse werden über eine Konfigurationsdatei festgelegt. Mehrere USB-to-RS232-Adapter können angeschlossen werden. Anzahl,
Bezeichnung und serielle Parameter der Anschlüsse werden über eine
Konfigurationsdatei festgelegt.
Die serverseitige Serial-Kommunikation verwendet `go.bug.st/serial`. Die serverseitige Serial-Kommunikation verwendet `go.bug.st/serial`.
@ -120,52 +126,139 @@ Die serverseitige Serial-Kommunikation verwendet `go.bug.st/serial`.
Für die Ansteuerung der seriellen Schnittstellen wird derzeit verwendet: Für die Ansteuerung der seriellen Schnittstellen wird derzeit verwendet:
```text ``` text
go.bug.st/serial v1.7.1 go.bug.st/serial v1.7.1
``` ```
Die Bibliothek wurde zusätzlich auf dem privaten Git-Server des Projektes gespiegelt, um bei einem Ausfall des ursprünglichen Anbieters weiterhin auf den verwendeten Quellstand zugreifen zu können: Die Bibliothek wurde zusätzlich auf dem privaten Git-Server des
Projektes gespiegelt, um bei einem Ausfall des ursprünglichen Anbieters
weiterhin auf den verwendeten Quellstand zugreifen zu können:
```text ``` text
git.lang-dieter.de/third-party/go-serial git.lang-dieter.de/third-party/go-serial
``` ```
Die verwendete Version `v1.7.1` ist auf dem Backup-Repository einschließlich Git-Tag vorhanden. Die verwendete Version `v1.7.1` ist auf dem Backup-Repository
einschließlich Git-Tag vorhanden.
## Netzwerk ## Netzwerk
Für die Übertragung der RS232-Daten wird TCP verwendet. Für die Übertragung der RS232-Daten wird TCP verwendet.
Die beteiligten Systeme werden zunächst über ein vertrauenswürdiges Tailscale-Netz verbunden. Die beteiligten Systeme werden zunächst über ein vertrauenswürdiges
Tailscale-Netz verbunden.
Eine zusätzliche TLS-Verschlüsselung ist deshalb derzeit nicht vorgesehen. Eine zusätzliche TLS-Verschlüsselung ist deshalb derzeit nicht
vorgesehen.
Für eine spätere Übertragung von Audio-Daten ist ein separates Netzwerk-/Transportkonzept vorgesehen. Hierfür soll UDP verwendet bzw. untersucht werden. Für eine spätere Übertragung von Audio-Daten ist ein separates
Netzwerk-/Transportkonzept vorgesehen. Hierfür soll UDP verwendet bzw.
untersucht werden.
## Konfiguration ## Konfiguration
Client und Server erhalten jeweils eine eigene JSON-Konfiguration. Client und Server erhalten jeweils eine eigene JSON-Konfiguration.
Über die Konfiguration sollen unter anderem festgelegt werden können: Über die Konfiguration werden unter anderem festgelegt:
- Netzwerkparameter - Netzwerkparameter
- serielle Schnittstelle - serielle Schnittstelle
- Baudrate - Baudrate
- Datenbits - Datenbits
- Parität - Parität
- Stopbits - Stopbits
- Bezeichnung des Gerätes - Bezeichnung des Gerätes
- weitere für die jeweilige Schnittstelle erforderliche Parameter - weitere für die jeweilige Schnittstelle erforderliche Parameter
Die konkrete Konfigurationsstruktur wird im weiteren Projektverlauf festgelegt. Die konkrete Konfigurationsstruktur befindet sich in der Entwicklung.
## Diagnose und Logging ## Diagnose und Logging
Für Debugging und Diagnose soll eine Protokollierung des übertragenen Datenverkehrs möglich sein. Für Debugging und Diagnose soll eine Protokollierung des übertragenen
Datenverkehrs möglich sein.
Insbesondere soll eine Darstellung der übertragenen Bytes möglich sein, um Probleme bei der Kommunikation zwischen Hersteller-Software und Gerät analysieren zu können. Insbesondere soll eine Darstellung der übertragenen Bytes möglich sein,
um Probleme bei der Kommunikation zwischen Hersteller-Software und Gerät
analysieren zu können.
Während der Entwicklung kann `socat` als zusätzliches Werkzeug für Tests und Diagnose eingesetzt werden. Während der Entwicklung kann `socat` als zusätzliches Werkzeug für Tests
und Diagnose eingesetzt werden.
## Virtuelle serielle Schnittstelle unter Linux
Für den Linux-Client wird die virtuelle serielle Schnittstelle direkt
über den Linux-PTY-Mechanismus erzeugt.
Dabei entsteht ein PTY-Paar:
``` text
/dev/ptmx
PTY Master
PTY Slave
/dev/pts/N
```
Die externe Hersteller-Software arbeitet mit dem PTY-Slave `/dev/pts/N`.
Der `rs2322tcp-client` verwendet den PTY-Master und transportiert die
Bytes über TCP zum Server.
Die PTY-Implementierung arbeitet im Raw Mode und wurde mit einem
vollständigen Bytebereich von `0x00` bis `0xFF` geprüft.
Dabei wurde die bidirektionale Übertragung aller 256 möglichen Bytewerte
erfolgreich getestet.
## Geräte-Simulator
Das Programm `rs2322tcp-device-sim` dient als Simulator für ein an der
seriellen Schnittstelle angeschlossenes Gerät.
Der Simulator ist bewusst als eigenständiges Programm ausgeführt. Der
`rs2322tcp-server` muss nicht wissen, ob am seriellen Port ein echtes
Gerät oder der Simulator angeschlossen ist.
Der aktuelle Simulator kann eine serielle Schnittstelle öffnen und
empfangene Rohdaten als Hexdump anzeigen.
Beispiel:
``` bash
rs2322tcp-device-sim -port /dev/pts/7
```
Beispielausgabe:
``` text
rs2322tcp-device-sim
Port: /dev/pts/7
Warte auf Daten ...
12:34:01.123 RX 5 Bytes
46 41 00 10 0D
```
Die Daten werden zunächst nicht interpretiert. Dadurch kann der
Simulator auch zum Beobachten unbekannter serieller Protokolle verwendet
werden.
Der Simulator soll insbesondere auch als Sniffer dienen können, wenn
externe Hersteller-Software untersucht werden soll. Dadurch lassen sich
die von einer Software gesendeten seriellen Kommandos zunächst als
Rohdaten beobachten.
Geplante Erweiterungen sind:
- manuelles Senden von Rohbytes
- Anzeige von RX und TX
- automatische Geräteantworten
- Protokollsimulation
- Aufzeichnung und Wiedergabe von Datenströmen
## Tests ## Tests
@ -173,35 +266,50 @@ Die wichtigsten Komponenten verfügen über automatisierte Tests.
Der aktuelle Stand wird unter anderem mit folgenden Befehlen geprüft: Der aktuelle Stand wird unter anderem mit folgenden Befehlen geprüft:
```bash ``` bash
go test ./... go test ./...
``` ```
und: und:
```bash ``` bash
go test -race ./... go test -race ./...
``` ```
Der Race Detector wird eingesetzt, um Probleme bei der nebenläufigen Verarbeitung von Sessions, Data-Listenern und DataConnections zu erkennen. Der Race Detector wird eingesetzt, um Probleme bei der nebenläufigen
Verarbeitung von Sessions, Data-Listenern und DataConnections zu
erkennen.
Für die serielle Datenübertragung werden virtuelle serielle Schnittstellen über `socat` verwendet. Dadurch kann der Datenpfad ohne angeschlossene RS232-Hardware getestet werden. Die Linux-PTY-Komponente verfügt über einen automatisierten
bidirektionalen Byte-Test.
Dabei werden insbesondere beide Übertragungsrichtungen geprüft: Dabei werden alle 256 möglichen Bytewerte übertragen:
```text ``` text
TCP → RS232 00 01 02 03 ... FD FE FF
RS232 → TCP
``` ```
Zusätzlich wird die vollständige Client-/Server-Kommunikation über den echten `ControlServer`-Lifecycle (`Listen()` / `Serve()`) getestet. Geprüft werden beide Richtungen:
``` text
PTY-Slave → Client
Client → PTY-Slave
```
Für den Integrationstest des Client-/Server-Datenpfades werden virtuelle
serielle Schnittstellen verwendet. Während der Entwicklung kann hierfür
`socat` eingesetzt werden.
Damit kann der Datenpfad ohne angeschlossene RS232-Hardware getestet
werden.
## Projektstruktur ## Projektstruktur
```text ``` text
rs2322tcp/ rs2322tcp/
├── cmd/ ├── cmd/
│ ├── rs2322tcp-client/ │ ├── rs2322tcp-client/
│ ├── rs2322tcp-device-sim/
│ └── rs2322tcp-server/ │ └── rs2322tcp-server/
├── configs/ ├── configs/
├── docs/ ├── docs/
@ -224,31 +332,34 @@ rs2322tcp/
Der Build erfolgt über das zentrale Build-Skript: Der Build erfolgt über das zentrale Build-Skript:
```bash ``` bash
./scripts/build.sh ./scripts/build.sh
``` ```
Das Skript führt unter anderem folgende Schritte aus: Das Skript führt unter anderem folgende Schritte aus:
- Formatierung des Go-Quellcodes - Formatierung des Go-Quellcodes
- Ausführung der Tests - Ausführung der Tests
- Bereinigung der Go-Module - Bereinigung der Go-Module
- Ermittlung der Git-Versionsinformationen - Ermittlung der Git-Versionsinformationen
- Build der vorgesehenen Zielplattformen - Build der vorgesehenen Zielplattformen
- Einbettung der Versionsinformationen über den Go-Linker - Einbettung der Versionsinformationen über den Go-Linker
- Erzeugung von SHA256-Prüfsummen - Erzeugung von SHA256-Prüfsummen
Aktuell werden folgende Builds erzeugt: Aktuell werden folgende Builds erzeugt:
- Linux amd64 Client und Server - Linux amd64 -- Client und Server
- Linux arm64 Server - Linux arm64 -- Server
- Windows amd64 Client - Linux armv7 -- Server
- Linux armv6 -- Server
## Versionierung ## Versionierung
Die Versionierung erfolgt über Git. Die Versionierung erfolgt über Git.
Die Git-Informationen werden beim Build in die Binärdateien eingebettet. Neben der Version werden unter anderem Build-Nummer, Commit und Build-Datum erfasst. Die Git-Informationen werden beim Build in die Binärdateien eingebettet.
Neben der Version werden unter anderem Build-Nummer, Commit und
Build-Datum erfasst.
Release-Versionen werden über Git-Tags gekennzeichnet. Release-Versionen werden über Git-Tags gekennzeichnet.
@ -256,27 +367,37 @@ Release-Versionen werden über Git-Tags gekennzeichnet.
Das Projekt befindet sich weiterhin in der frühen Entwicklungsphase. Das Projekt befindet sich weiterhin in der frühen Entwicklungsphase.
Die ursprüngliche Projektgrundlage aus Version `0.0.1` wurde inzwischen um eine funktionierende serverseitige TCP-/RS232-Datenübertragung und die grundlegende Client-Control-/Data-Kommunikation erweitert. Die ursprüngliche Projektgrundlage aus Version `0.0.1` wurde inzwischen
um eine funktionierende serverseitige TCP-/RS232-Datenübertragung und
die clientseitige Grundlage für virtuelle serielle Schnittstellen
erweitert.
Implementiert und getestet sind derzeit: Implementiert und getestet sind derzeit:
- Server-Control-Verbindung - Server-Control-Verbindung
- Client-Control-Verbindung - Session-Verwaltung
- `HELLO`-Handshake - dynamische Data-Ports
- `GET_DEVICES` - DataConnection
- Session-Verwaltung - bidirektionale TCP-/RS232-Datenübertragung
- dynamische Data-Ports - Serial-Abstraktion
- Clientseitiger Aufbau der Data-Verbindung - Konfiguration der seriellen Parameter
- DataConnection - Client-Control-Verbindung
- bidirektionale TCP-/RS232-Datenübertragung - Client-DataConnection
- Serial-Abstraktion - Client-Integrationstests
- Konfiguration der seriellen Parameter - Linux-PTY-Abstraktion
- virtuelle serielle Integrationstests - Linux-PTY im Raw Mode
- Client-/Server-End-to-End-Integrationstest - automatisierter bidirektionaler 256-Byte-PTY-Test
- nebenläufigkeitssichere Data-Listener - nebenläufigkeitssichere Data-Listener
- Race-Detection - Race-Detection
- eigenständiger Geräte-Simulator mit RX-Hexdump
Noch nicht implementiert ist insbesondere die clientseitige virtuelle serielle Schnittstelle für Windows und Linux. Noch nicht vollständig implementiert sind insbesondere:
- die vollständige Anbindung der virtuellen seriellen Schnittstelle an
den produktiven Client-Anwendungsdatenpfad
- die Windows-Implementierung der virtuellen seriellen Schnittstelle
- TX-Funktion des Geräte-Simulators
- automatische Geräteantworten und Protokollsimulation
## Lizenz ## Lizenz

View file

@ -0,0 +1,164 @@
/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: main.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Einfacher Simulator für ein serielles Endgerät.
*
* Der Simulator öffnet eine angegebene serielle Schnittstelle und protokolliert
* alle vom angeschlossenen System empfangenen Bytes als Hexdump.
*
* Der Simulator stellt bewusst keine eigene PTY-Erzeugung bereit. Für
* Entwicklung und Integrationstests kann ihm beispielsweise ein unter Linux
* erzeugter PTY-Slave (/dev/pts/N) übergeben werden. Im späteren Testaufbau
* kann derselbe Simulator jedoch auch direkt mit einer echten seriellen
* Schnittstelle verbunden werden.
*
* Der Simulator dient zunächst als passives Gegenstück zum
* rs2322tcp-server. Er soll insbesondere ermöglichen, die vom Server an ein
* simuliertes Gerät übertragenen Bytes sichtbar zu machen.
*
* Zukünftige Erweiterungen können unter anderem das manuelle Senden von
* Bytes, automatische Geräteantworten, Protokollsimulation und das Aufzeichnen
* bzw. Wiedergeben von Datenströmen umfassen.
*
* Aufruf:
*
* rs2322tcp-device-sim -port /dev/pts/7
*
* Ausgabe:
*
* rs2322tcp-device-sim
* Port: /dev/pts/7
* Warte auf Daten ...
*
* Empfangene Daten werden mit Zeitstempel, Richtung und Byteanzahl ausgegeben:
*
* 12:34:01.123 RX 5 Bytes
* 46 41 00 10 0D
*
* Der Simulator interpretiert die empfangenen Daten nicht. Die Darstellung
* erfolgt ausschließlich als Rohdaten in hexadezimaler Form.
*
* ============================================================================
*/
package main
import (
"flag"
"fmt"
"io"
"log"
"time"
"golang.org/x/sys/unix"
)
func main() {
port := flag.String(
"port",
"",
"serielle Schnittstelle, z.B. /dev/pts/7",
)
flag.Parse()
if *port == "" {
log.Fatal("serielle Schnittstelle fehlt: -port <device>")
}
// Die serielle Schnittstelle wird als normaler Dateideskriptor geöffnet.
// Der Simulator benötigt zunächst keine eigene Serial-Abstraktion, da er
// bewusst ein möglichst einfaches externes Testprogramm darstellen soll.
fd, err := unix.Open(
*port,
unix.O_RDWR|unix.O_NOCTTY,
0,
)
if err != nil {
log.Fatalf(
"serielle Schnittstelle %s öffnen: %v",
*port,
err,
)
}
defer unix.Close(fd)
fmt.Println("rs2322tcp-device-sim")
fmt.Printf("Port: %s\n", *port)
fmt.Println("Warte auf Daten ...")
fmt.Println()
// Der Simulator arbeitet byteorientiert. Die Größe des Puffers ist
// bewusst ausreichend groß gewählt, damit auch größere Datenblöcke
// ohne unnötige Fragmentierung gelesen werden können.
buf := make([]byte, 4096)
for {
n, err := unix.Read(fd, buf)
if err != nil {
// Ein unterbrochener Systemaufruf ist kein Fehler des
// seriellen Datenstroms und wird einfach wiederholt.
if err == unix.EINTR {
continue
}
// Bei einem PTY kann das Schließen der Gegenstelle unter
// Linux als EIO gemeldet werden. Für den Simulator bedeutet
// dies, dass die serielle Gegenstelle nicht mehr verfügbar ist.
if err == unix.EIO {
fmt.Println("Serielle Schnittstelle wurde geschlossen.")
return
}
if err == io.EOF {
fmt.Println("Serielle Schnittstelle wurde geschlossen.")
return
}
log.Fatalf(
"Lesen von %s: %v",
*port,
err,
)
}
if n == 0 {
continue
}
printRX(buf[:n])
}
}
// printRX gibt einen empfangenen Datenblock als Hexdump aus.
//
// Die Daten werden absichtlich nicht als Text interpretiert. Dadurch bleiben
// auch nicht druckbare Bytes und Bytes oberhalb von 0x7F sichtbar und können
// bei der Analyse serieller Protokolle eindeutig nachvollzogen werden.
func printRX(data []byte) {
fmt.Printf(
"%s RX %d Bytes\n",
time.Now().Format("15:04:05.000"),
len(data),
)
for offset := 0; offset < len(data); offset += 16 {
end := offset + 16
if end > len(data) {
end = len(data)
}
fmt.Printf(" ")
for _, b := range data[offset:end] {
fmt.Printf("%02X ", b)
}
fmt.Println()
}
}

View file

@ -0,0 +1,167 @@
//go:build linux
package client
import (
"fmt"
"golang.org/x/sys/unix"
)
type ptySerial struct {
masterFD int
slaveFD int
path string
}
func newVirtualSerial() (VirtualSerial, error) {
masterFD, err := unix.Open(
"/dev/ptmx",
unix.O_RDWR|unix.O_NOCTTY,
0,
)
if err != nil {
return nil, fmt.Errorf("open PTY master: %w", err)
}
// Wenn die weitere Initialisierung fehlschlägt,
// darf der Master nicht offen bleiben.
closeMaster := true
defer func() {
if closeMaster {
_ = unix.Close(masterFD)
}
}()
// PTY-Slave entsperren.
if err := unix.IoctlSetPointerInt(
masterFD,
unix.TIOCSPTLCK,
0,
); err != nil {
return nil, fmt.Errorf("unlock PTY slave: %w", err)
}
// PTY-Nummer ermitteln.
ptsNumber, err := unix.IoctlGetInt(
masterFD,
unix.TIOCGPTN,
)
if err != nil {
return nil, fmt.Errorf("get PTY number: %w", err)
}
path := fmt.Sprintf("/dev/pts/%d", ptsNumber)
// Slave öffnen, um die Terminalparameter konfigurieren zu können.
slaveFD, err := unix.Open(
path,
unix.O_RDWR|unix.O_NOCTTY,
0,
)
if err != nil {
return nil, fmt.Errorf("open PTY slave %s: %w", path, err)
}
closeSlave := true
defer func() {
if closeSlave {
_ = unix.Close(slaveFD)
}
}()
if err := configurePTY(slaveFD); err != nil {
return nil, err
}
closeMaster = false
closeSlave = false
return &ptySerial{
masterFD: masterFD,
slaveFD: slaveFD,
path: path,
}, nil
}
func configurePTY(fd int) error {
termios, err := unix.IoctlGetTermios(
fd,
unix.TCGETS,
)
if err != nil {
return fmt.Errorf("get PTY termios: %w", err)
}
// Raw Mode:
// keine Canonical-Verarbeitung,
// kein Echo,
// keine Signalinterpretation,
// keine CR/LF-Konvertierung,
// keine XON/XOFF-Verarbeitung.
termios.Iflag &^= unix.IGNBRK |
unix.BRKINT |
unix.PARMRK |
unix.ISTRIP |
unix.INLCR |
unix.IGNCR |
unix.ICRNL |
unix.IXON
termios.Oflag &^= unix.OPOST
termios.Lflag &^= unix.ECHO |
unix.ECHONL |
unix.ICANON |
unix.ISIG |
unix.IEXTEN
termios.Cflag &^= unix.CSIZE | unix.PARENB
termios.Cflag |= unix.CS8
// Byte-orientiertes, blockierendes Lesen.
termios.Cc[unix.VMIN] = 1
termios.Cc[unix.VTIME] = 0
if err := unix.IoctlSetTermios(
fd,
unix.TCSETS,
termios,
); err != nil {
return fmt.Errorf("set PTY raw mode: %w", err)
}
return nil
}
func (p *ptySerial) Path() string {
return p.path
}
func (p *ptySerial) Read(b []byte) (int, error) {
return unix.Read(p.masterFD, b)
}
func (p *ptySerial) Write(b []byte) (int, error) {
return unix.Write(p.masterFD, b)
}
func (p *ptySerial) Close() error {
var firstErr error
if p.masterFD >= 0 {
if err := unix.Close(p.masterFD); err != nil {
firstErr = err
}
p.masterFD = -1
}
if p.slaveFD >= 0 {
if err := unix.Close(p.slaveFD); err != nil && firstErr == nil {
firstErr = err
}
p.slaveFD = -1
}
return firstErr
}

View file

@ -0,0 +1,112 @@
//go:build linux
package client
import (
"bytes"
"testing"
"golang.org/x/sys/unix"
)
func TestVirtualSerialPTYByteStream(t *testing.T) {
serial, err := newVirtualSerial()
if err != nil {
t.Fatalf("newVirtualSerial() failed: %v", err)
}
defer serial.Close()
path := serial.Path()
if path == "" {
t.Fatal("virtual serial returned an empty path")
}
t.Logf("virtual serial path: %s", path)
// Die externe Software wird durch diesen direkten Zugriff
// auf den PTY-Slave simuliert.
slaveFD, err := unix.Open(
path,
unix.O_RDWR|unix.O_NOCTTY,
0,
)
if err != nil {
t.Fatalf("open PTY slave %s: %v", path, err)
}
defer unix.Close(slaveFD)
expected := make([]byte, 256)
for i := range expected {
expected[i] = byte(i)
}
// ------------------------------------------------------------
// Slave -> Client
// ------------------------------------------------------------
n, err := unix.Write(slaveFD, expected)
if err != nil {
t.Fatalf("write to PTY slave failed: %v", err)
}
if n != len(expected) {
t.Fatalf(
"write to PTY slave incomplete: got %d, want %d",
n,
len(expected),
)
}
received := make([]byte, len(expected))
offset := 0
for offset < len(received) {
n, err := serial.Read(received[offset:])
if err != nil {
t.Fatalf("read from virtual serial failed: %v", err)
}
offset += n
}
if !bytes.Equal(received, expected) {
t.Fatalf("Slave -> Client data mismatch")
}
// ------------------------------------------------------------
// Client -> Slave
// ------------------------------------------------------------
n, err = serial.Write(expected)
if err != nil {
t.Fatalf("write to virtual serial failed: %v", err)
}
if n != len(expected) {
t.Fatalf(
"write from virtual serial incomplete: got %d, want %d",
n,
len(expected),
)
}
received = make([]byte, len(expected))
offset = 0
for offset < len(received) {
n, err := unix.Read(slaveFD, received[offset:])
if err != nil {
t.Fatalf("read from PTY slave failed: %v", err)
}
offset += n
}
if !bytes.Equal(received, expected) {
t.Fatalf("Client -> Slave data mismatch")
}
t.Log("256-byte bidirectional PTY test successful")
}

View file

@ -0,0 +1,17 @@
package client
import "io"
// VirtualSerial repräsentiert eine vom Client bereitgestellte
// virtuelle serielle Schnittstelle.
//
// Path liefert den Namen der für die Anwendung sichtbaren
// seriellen Schnittstelle, zum Beispiel /dev/pts/1.
//
// Das io.ReadWriteCloser-Interface repräsentiert die interne
// Byte-Verbindung zum virtuellen seriellen Gerät.
type VirtualSerial interface {
io.ReadWriteCloser
Path() string
}