From f2428a2007e8bdb25db410d91234cfbd1c1c195a Mon Sep 17 00:00:00 2001 From: Dieter Date: Sat, 22 Aug 2026 15:19:00 +0200 Subject: [PATCH] =?UTF-8?q?PTY-Testwerkzeug=20und=20Server-Testkonfigurati?= =?UTF-8?q?on=20erg=C3=A4nzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/rs2322tcp-pty-test/main.go | 348 +++++++++++++++++++++++++++++++++ configs/server-test.json | 41 ++++ 2 files changed, 389 insertions(+) create mode 100644 cmd/rs2322tcp-pty-test/main.go create mode 100644 configs/server-test.json diff --git a/cmd/rs2322tcp-pty-test/main.go b/cmd/rs2322tcp-pty-test/main.go new file mode 100644 index 0000000..b383b51 --- /dev/null +++ b/cmd/rs2322tcp-pty-test/main.go @@ -0,0 +1,348 @@ +//go:build linux + +/* + * ============================================================================ + * Projekt.....: rs2322tcp + * Datei.......: cmd/rs2322tcp-pty-test/main.go + * Copyright (C) 2026 Dieter Lang + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Beschreibung: + * Entwicklungs- und Testwerkzeug zum Bereitstellen eines einzelnen + * bidirektionalen Linux-PTYs als simuliertes serielles Endgerät. + * + * Der erzeugte PTY-Slave wird vom rs2322tcp-server wie eine echte serielle + * Schnittstelle verwendet. Der Helper arbeitet auf dem zugehörigen + * PTY-Master als Gegenstelle. + * + * Alle vom Server empfangenen Daten werden als RX-Hexdump ausgegeben. + * + * Bei der bekannten Rotor-Anfrage wird automatisch die konfigurierte + * Rotor-Antwort zurückgesendet. Alle anderen empfangenen Daten werden + * unverändert als Echo zurückgesendet. + * + * Der PTY-Slave bleibt während der gesamten Laufzeit des Helpers geöffnet. + * Dadurch bleibt die virtuelle serielle Gegenstelle auch dann verfügbar, + * wenn der Server den Port erst nach dem Start des Helpers öffnet. + * + * Beim Beenden werden Master und Slave geschlossen. Der Test-Port + * verschwindet anschließend wieder aus /dev/pts. + * ============================================================================ + */ + +package main + +import ( + "bytes" + "fmt" + "io" + "os" + "os/signal" + "syscall" + "time" + + "golang.org/x/sys/unix" +) + +var ( + rotorRequest = []byte{ + 0x57, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x1F, + 0x20, + } + + rotorResponse = []byte{ + 0x57, + 0x07, + 0x01, + 0x06, + 0x00, + 0x01, + 0x03, + 0x06, + 0x02, + 0x00, + 0x01, + 0x20, + } +) + +func main() { + masterFD, slaveFD, slavePath, err := createPTY() + if err != nil { + fmt.Fprintf( + os.Stderr, + "PTY-Erzeugung fehlgeschlagen: %v\n", + err, + ) + os.Exit(1) + } + + defer unix.Close(masterFD) + defer unix.Close(slaveFD) + + fmt.Println("rs2322tcp-pty-test") + fmt.Printf("Port: %s\n", slavePath) + fmt.Println("Modus: Rotor-Simulation mit Echo-Fallback") + fmt.Println("Warte auf Daten ...") + + signals := make(chan os.Signal, 1) + signal.Notify( + signals, + os.Interrupt, + syscall.SIGTERM, + ) + defer signal.Stop(signals) + + done := make(chan struct{}) + + go func() { + handleSerial(masterFD) + close(done) + }() + + select { + case <-signals: + fmt.Println() + fmt.Println("PTY-Testport wird geschlossen.") + + case <-done: + fmt.Println() + fmt.Println("PTY-Gegenstelle wurde geschlossen.") + } +} + +func handleSerial(fd int) { + buf := make([]byte, 4096) + + for { + n, err := unix.Read(fd, buf) + if err != nil { + if err == unix.EINTR { + continue + } + + if err == unix.EIO || err == io.EOF { + return + } + + fmt.Fprintf( + os.Stderr, + "Lesen vom PTY-Master: %v\n", + err, + ) + return + } + + if n == 0 { + continue + } + + received := append([]byte(nil), buf[:n]...) + + printHexDump("RX", received) + + response := responseFor(received) + + if err := writeAll(fd, response); err != nil { + fmt.Fprintf( + os.Stderr, + "Schreiben zum PTY-Master: %v\n", + err, + ) + return + } + + printHexDump("TX", response) + } +} + +func responseFor(received []byte) []byte { + if bytes.Equal(received, rotorRequest) { + return rotorResponse + } + + return received +} + +func createPTY() (int, int, string, error) { + masterFD, err := unix.Open( + "/dev/ptmx", + unix.O_RDWR|unix.O_NOCTTY, + 0, + ) + if err != nil { + return -1, -1, "", fmt.Errorf( + "open PTY master: %w", + err, + ) + } + + closeMasterOnError := true + defer func() { + if closeMasterOnError { + _ = unix.Close(masterFD) + } + }() + + if err := unix.IoctlSetPointerInt( + masterFD, + unix.TIOCSPTLCK, + 0, + ); err != nil { + return -1, -1, "", fmt.Errorf( + "unlock PTY slave: %w", + err, + ) + } + + ptsNumber, err := unix.IoctlGetInt( + masterFD, + unix.TIOCGPTN, + ) + if err != nil { + return -1, -1, "", fmt.Errorf( + "get PTY number: %w", + err, + ) + } + + slavePath := fmt.Sprintf( + "/dev/pts/%d", + ptsNumber, + ) + + slaveFD, err := unix.Open( + slavePath, + unix.O_RDWR|unix.O_NOCTTY, + 0, + ) + if err != nil { + return -1, -1, "", fmt.Errorf( + "open PTY slave %s: %w", + slavePath, + err, + ) + } + + closeSlaveOnError := true + defer func() { + if closeSlaveOnError { + _ = unix.Close(slaveFD) + } + }() + + if err := configurePTY(slaveFD); err != nil { + return -1, -1, "", err + } + + closeMasterOnError = false + closeSlaveOnError = false + + return masterFD, slaveFD, slavePath, nil +} + +func configurePTY(fd int) error { + termios, err := unix.IoctlGetTermios( + fd, + unix.TCGETS, + ) + if err != nil { + return fmt.Errorf( + "get PTY termios: %w", + err, + ) + } + + 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 + + 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 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 +} + +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.Print(" ") + + for _, b := range data[offset:end] { + fmt.Printf("%02X ", b) + } + + fmt.Println() + } +} diff --git a/configs/server-test.json b/configs/server-test.json new file mode 100644 index 0000000..5bff734 --- /dev/null +++ b/configs/server-test.json @@ -0,0 +1,41 @@ +{ + "listen": { + "address": "0.0.0.0", + "port": 5000 + }, + + "hardware_error_response": "ERROR - HARDWARE NOT AVAILABLE", + + "serial_monitor": true, + + "devices": [ + { + "id": "radio", + "name": "Funkgerät", + "serial_port": "/dev/ttyUSB1", + "baud_rate": 9600, + "data_bits": 8, + "parity": "none", + "stop_bits": 1 + }, + { + "id": "rotor", + "name": "Antennenrotor", + "serial_port": "/dev/pts/0", + "baud_rate": 600, + "data_bits": 8, + "parity": "none", + "stop_bits": 1 + }, + { + "id": "gps", + "name": "GPS-Testgerät", + "serial_port": "/dev/ttyUSB3", + "baud_rate": 600, + "data_bits": 8, + "parity": "none", + "stop_bits": 1 + } + + ] +}