PTY-Testwerkzeug und Server-Testkonfiguration ergänzen

This commit is contained in:
Dieter 2026-08-22 15:19:00 +02:00
parent 21d622e040
commit f2428a2007
2 changed files with 389 additions and 0 deletions

View file

@ -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()
}
}

41
configs/server-test.json Normal file
View file

@ -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
}
]
}