From 2a61b5a79cdb749e212e09e7eabfbf6bd4085210 Mon Sep 17 00:00:00 2001 From: Dieter Lang Date: Mon, 10 Aug 2026 12:42:22 +0200 Subject: [PATCH] Add Linux PTY support and device simulator --- CHANGELOG.md | 19 +- README.md | 355 ++++++++++++++++++++---------- cmd/rs2322tcp-device-sim/main.go | 164 ++++++++++++++ internal/client/pty_linux.go | 167 ++++++++++++++ internal/client/pty_linux_test.go | 112 ++++++++++ internal/client/virtual_serial.go | 17 ++ 6 files changed, 714 insertions(+), 120 deletions(-) create mode 100644 cmd/rs2322tcp-device-sim/main.go create mode 100644 internal/client/pty_linux.go create mode 100644 internal/client/pty_linux_test.go create mode 100644 internal/client/virtual_serial.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a1d6de6..a95d314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ Alle wesentlichen Änderungen am Projekt werden in dieser Datei dokumentiert. ## [Unreleased] ### 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` - 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 @@ -45,7 +49,8 @@ Alle wesentlichen Änderungen am Projekt werden in dieser Datei dokumentiert. - Race Condition im `DataListener` erkannt und behoben - Client-/Server-Integrationstest 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 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 ``` -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 diff --git a/README.md b/README.md index baa12b1..a689995 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,34 @@ # 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 -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: -- Funkgeräte mit CAT-Steuerung -- Antennenrotoren mit herstellerspezifischen seriellen Protokollen -- andere Geräte, die über RS232 gesteuert werden +- Funkgeräte mit CAT-Steuerung +- Antennenrotoren mit herstellerspezifischen seriellen Protokollen +- andere Geräte, die über RS232 gesteuert werden ## Architektur -```text +``` text Client-PC Raspberry Pi 5 ───────────────── ───────────────── @@ -40,79 +48,77 @@ rs2322tcp-client │ 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 -Client - │ - ├── TCP-Control - │ │ - │ ▼ - │ Session - │ │ - │ ▼ - │ GET_DEVICES - │ │ - │ ▼ - │ Geräteinformationen - │ │ - │ ▼ - │ dynamischer Data-Port - │ - └── TCP-Data - │ - ▼ - DataListener - │ - ▼ - DataConnection - │ - ▼ - Serial Layer - │ - ▼ - RS232 +``` text +TCP-Control + │ + ▼ + Session + │ + ├── Gerät 1 ──► dynamischer Data-Port + │ │ + │ ▼ + │ TCP Data + │ │ + │ ▼ + │ Serial Layer + │ │ + │ ▼ + │ RS232 + │ + └── Gerät 2 ──► dynamischer Data-Port ``` -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. Die Datenübertragung erfolgt bidirektional: -```text +``` text TCP ───────────────► RS232 TCP ◄────────────── RS232 ``` -Der komplette Client-/Server-Datenpfad wird inzwischen durch einen Integrationstest mit virtuellen seriellen Schnittstellen geprüft. - ## Client -Der Client soll gleichberechtigt unter folgenden Betriebssystemen eingesetzt werden können: +Der Client soll gleichberechtigt unter folgenden Betriebssystemen +eingesetzt werden können: -- Windows -- Linux +- Windows +- 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 -- den Control-Handshake durchführen -- die konfigurierten Geräte vom Server abfragen -- den vom Server gelieferten dynamischen Data-Port verwenden -- die TCP-Data-Verbindung zum Server aufbauen +Die PTY-Schnittstelle wird im Raw Mode betrieben. Dadurch werden die +übertragenen Daten als transparenter Byte-Strom behandelt. -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 -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`. @@ -120,52 +126,139 @@ Die serverseitige Serial-Kommunikation verwendet `go.bug.st/serial`. Für die Ansteuerung der seriellen Schnittstellen wird derzeit verwendet: -```text +``` text 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 ``` -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 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 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 -- serielle Schnittstelle -- Baudrate -- Datenbits -- Parität -- Stopbits -- Bezeichnung des Gerätes -- weitere für die jeweilige Schnittstelle erforderliche Parameter +- Netzwerkparameter +- serielle Schnittstelle +- Baudrate +- Datenbits +- Parität +- Stopbits +- Bezeichnung des Gerätes +- 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 -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 @@ -173,35 +266,50 @@ Die wichtigsten Komponenten verfügen über automatisierte Tests. Der aktuelle Stand wird unter anderem mit folgenden Befehlen geprüft: -```bash +``` bash go test ./... ``` und: -```bash +``` bash 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 -TCP → RS232 -RS232 → TCP +``` text +00 01 02 03 ... FD FE FF ``` -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 -```text +``` text rs2322tcp/ ├── cmd/ │ ├── rs2322tcp-client/ +│ ├── rs2322tcp-device-sim/ │ └── rs2322tcp-server/ ├── configs/ ├── docs/ @@ -224,31 +332,34 @@ rs2322tcp/ Der Build erfolgt über das zentrale Build-Skript: -```bash +``` bash ./scripts/build.sh ``` Das Skript führt unter anderem folgende Schritte aus: -- Formatierung des Go-Quellcodes -- Ausführung der Tests -- Bereinigung der Go-Module -- Ermittlung der Git-Versionsinformationen -- Build der vorgesehenen Zielplattformen -- Einbettung der Versionsinformationen über den Go-Linker -- Erzeugung von SHA256-Prüfsummen +- Formatierung des Go-Quellcodes +- Ausführung der Tests +- Bereinigung der Go-Module +- Ermittlung der Git-Versionsinformationen +- Build der vorgesehenen Zielplattformen +- Einbettung der Versionsinformationen über den Go-Linker +- Erzeugung von SHA256-Prüfsummen Aktuell werden folgende Builds erzeugt: -- Linux amd64 – Client und Server -- Linux arm64 – Server -- Windows amd64 – Client +- Linux amd64 -- Client und Server +- Linux arm64 -- Server +- Linux armv7 -- Server +- Linux armv6 -- Server ## Versionierung 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. @@ -256,27 +367,37 @@ Release-Versionen werden über Git-Tags gekennzeichnet. 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: -- Server-Control-Verbindung -- Client-Control-Verbindung -- `HELLO`-Handshake -- `GET_DEVICES` -- Session-Verwaltung -- dynamische Data-Ports -- Clientseitiger Aufbau der Data-Verbindung -- DataConnection -- bidirektionale TCP-/RS232-Datenübertragung -- Serial-Abstraktion -- Konfiguration der seriellen Parameter -- virtuelle serielle Integrationstests -- Client-/Server-End-to-End-Integrationstest -- nebenläufigkeitssichere Data-Listener -- Race-Detection +- Server-Control-Verbindung +- Session-Verwaltung +- dynamische Data-Ports +- DataConnection +- bidirektionale TCP-/RS232-Datenübertragung +- Serial-Abstraktion +- Konfiguration der seriellen Parameter +- Client-Control-Verbindung +- Client-DataConnection +- Client-Integrationstests +- Linux-PTY-Abstraktion +- Linux-PTY im Raw Mode +- automatisierter bidirektionaler 256-Byte-PTY-Test +- nebenläufigkeitssichere Data-Listener +- 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 diff --git a/cmd/rs2322tcp-device-sim/main.go b/cmd/rs2322tcp-device-sim/main.go new file mode 100644 index 0000000..4ec516f --- /dev/null +++ b/cmd/rs2322tcp-device-sim/main.go @@ -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 ") + } + + // 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() + } +} diff --git a/internal/client/pty_linux.go b/internal/client/pty_linux.go new file mode 100644 index 0000000..3438636 --- /dev/null +++ b/internal/client/pty_linux.go @@ -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 +} diff --git a/internal/client/pty_linux_test.go b/internal/client/pty_linux_test.go new file mode 100644 index 0000000..3355c42 --- /dev/null +++ b/internal/client/pty_linux_test.go @@ -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") +} diff --git a/internal/client/virtual_serial.go b/internal/client/virtual_serial.go new file mode 100644 index 0000000..513e35d --- /dev/null +++ b/internal/client/virtual_serial.go @@ -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 +}