PTY-Testwerkzeug und Server-Testkonfiguration ergänzen
This commit is contained in:
parent
21d622e040
commit
f2428a2007
2 changed files with 389 additions and 0 deletions
348
cmd/rs2322tcp-pty-test/main.go
Normal file
348
cmd/rs2322tcp-pty-test/main.go
Normal 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
41
configs/server-test.json
Normal 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
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue