348 lines
5.7 KiB
Go
348 lines
5.7 KiB
Go
//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()
|
|
}
|
|
}
|