Add Linux PTY support and device simulator
This commit is contained in:
parent
49f72c376a
commit
2a61b5a79c
6 changed files with 714 additions and 120 deletions
19
CHANGELOG.md
19
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
|
||||
|
|
|
|||
355
README.md
355
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
|
||||
|
||||
|
|
|
|||
164
cmd/rs2322tcp-device-sim/main.go
Normal file
164
cmd/rs2322tcp-device-sim/main.go
Normal 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()
|
||||
}
|
||||
}
|
||||
167
internal/client/pty_linux.go
Normal file
167
internal/client/pty_linux.go
Normal 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
|
||||
}
|
||||
112
internal/client/pty_linux_test.go
Normal file
112
internal/client/pty_linux_test.go
Normal 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")
|
||||
}
|
||||
17
internal/client/virtual_serial.go
Normal file
17
internal/client/virtual_serial.go
Normal 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
|
||||
}
|
||||
Loading…
Reference in a new issue