rs2322tcp/cmd/rs2322tcp-device-sim/main.go
2026-08-10 13:51:26 +02:00

307 lines
7.4 KiB
Go

/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: main.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Interaktiver Simulator für ein serielles Endgerät.
*
* Der Simulator öffnet eine angegebene serielle Schnittstelle und protokolliert
* alle vom angeschlossenen System empfangenen Bytes als Hexdump.
*
* Zusätzlich können über die Standardeingabe hexadezimal angegebene Bytefolgen
* an das serielle Gerät gesendet werden. Dadurch kann der Simulator sowohl als
* passiver Sniffer als auch als einfaches interaktives Testgerät verwendet
* werden.
*
* 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 externes Gegenstück zum
* rs2322tcp-server. Er ermöglicht insbesondere, die vom Server an ein
* simuliertes Gerät übertragenen Bytes sichtbar zu machen und umgekehrt
* definierte Bytefolgen an den Server zurückzusenden.
*
* Eingabeformat:
*
* TX> 46 41 00 10 0D
*
* Die Leerzeichen zwischen den Bytes sind optional. Groß- und Kleinschreibung
* der Hex-Ziffern spielt keine Rolle.
*
* Zukünftige Erweiterungen können unter anderem 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 und gesendete Daten werden mit Zeitstempel, Richtung und
* Byteanzahl ausgegeben:
*
* 12:34:01.123 RX 5 Bytes
* 46 41 00 10 0D
*
* 12:34:05.456 TX 5 Bytes
* 46 41 01 10 0D
*
* Der Simulator interpretiert die seriellen Daten nicht. Die Darstellung
* erfolgt ausschließlich als Rohdaten in hexadezimaler Form.
*
* ============================================================================
*/
package main
import (
"bufio"
"encoding/hex"
"flag"
"fmt"
"io"
"log"
"os"
"strings"
"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()
fmt.Println("Hex-Bytes senden mit:")
fmt.Println("TX> 46 41 00 10 0D")
fmt.Println()
// Die Standardeingabe wird parallel zur seriellen Schnittstelle gelesen.
// Dadurch kann der Simulator gleichzeitig Daten empfangen und senden.
go readTX(fd)
// 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])
}
}
// readTX liest Zeilen von stdin und interpretiert sie als Folge von
// hexadezimal angegebenen Bytes.
//
// Die Eingabe wird bewusst zeilenorientiert verarbeitet. Dadurch kann eine
// komplette Bytefolge zuerst geprüft werden, bevor sie an die serielle
// Schnittstelle gesendet wird.
func readTX(fd int) {
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("TX> ")
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
fmt.Print("TX> ")
continue
}
data, err := parseHexBytes(line)
if err != nil {
fmt.Printf("Ungültige Hex-Eingabe: %v\n", err)
fmt.Print("TX> ")
continue
}
if len(data) == 0 {
fmt.Print("TX> ")
continue
}
if err := writeAll(fd, data); err != nil {
fmt.Fprintf(os.Stderr, "Senden: %v\n", err)
return
}
printTX(data)
fmt.Print("TX> ")
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Lesen von stdin: %v\n", err)
}
// Bei EOF (Ctrl+D) wird der Dateideskriptor geschlossen. Dadurch wird
// der blockierende Lesevorgang im Haupt-Goroutine beendet und der
// Simulator kann sauber verlassen werden.
_ = unix.Close(fd)
}
// parseHexBytes wandelt eine zeilenorientierte Hex-Eingabe in einen
// Byte-Block um.
//
// Leerzeichen zwischen den Bytes sind erlaubt und werden entfernt. Dadurch
// werden beispielsweise sowohl
//
// 46 41 00 10 0D
//
// als auch
//
// 464100100D
//
// akzeptiert. Die Funktion selbst enthält keine Ausgabe und ist deshalb
// unabhängig vom interaktiven Terminalbetrieb testbar.
func parseHexBytes(line string) ([]byte, error) {
fields := strings.Fields(line)
hexText := strings.Join(fields, "")
if hexText == "" {
return nil, nil
}
data, err := hex.DecodeString(hexText)
if err != nil {
return nil, err
}
return data, nil
}
// writeAll schreibt alle Bytes des angegebenen Datenblocks auf den seriellen
// Dateideskriptor. unix.Write() darf weniger Bytes als angefordert schreiben.
func writeAll(fd int, data []byte) error {
for len(data) > 0 {
n, err := unix.Write(fd, data)
if err != nil {
if err == unix.EINTR {
continue
}
return err
}
if n == 0 {
return io.ErrShortWrite
}
data = data[n:]
}
return nil
}
// 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) {
printHexDump("RX", data)
}
// printTX gibt einen gesendeten Datenblock als Hexdump aus.
func printTX(data []byte) {
printHexDump("TX", data)
}
// printHexDump gibt einen Datenblock mit Zeitstempel, Richtung und Byteanzahl
// als Hexdump aus.
func printHexDump(direction string, data []byte) {
fmt.Printf(
"%s %s %d Bytes\n",
time.Now().Format("15:04:05.000"),
direction,
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()
}
}