Compare commits

...

10 commits

20 changed files with 1814 additions and 172 deletions

7
.gitignore vendored
View file

@ -1,7 +1,14 @@
Notizen.txt Notizen.txt
# Build-Verzeichnis # Build-Verzeichnis
/build/ /build/
# Distributions-Verzeichnis
/dist/
# Temporäre Arbeitsdateien von fyne-cross
/fyne-cross/
# Lokale Build-Binaries # Lokale Build-Binaries
/rs2322tcp-client /rs2322tcp-client
/rs2322tcp-server /rs2322tcp-server

View file

@ -2,6 +2,27 @@
Alle wesentlichen Änderungen am Projekt werden in dieser Datei dokumentiert. Alle wesentlichen Änderungen am Projekt werden in dieser Datei dokumentiert.
## [0.0.11] - 2026-08-25
### Hinzugefügt
- Windows-Client erfolgreich mit Fyne-GUI gestartet.
- Übertragung der konfigurierten `startup_bytes` an das zugeordnete
Remote-Gerät beim Verbindungsaufbau getestet.
### Geändert
- Windows-spezifische Behandlung virtueller serieller Ports angepasst.
- Öffnen und Verwaltung virtueller serieller Schnittstellen unter Windows
überarbeitet.
### Getestet
- Minimaler Fyne-Test unter Windows erfolgreich gestartet.
- `rs2322tcp-client` unter Windows erfolgreich mit Fyne-GUI gestartet.
- Verbindung des Windows-Clients zum `rs2322tcp-server` erfolgreich aufgebaut.
- Rotor-Startcode erfolgreich vom Windows-Client bis zum Server übertragen.
## [0.0.10] - 2026-08-21 ## [0.0.10] - 2026-08-21
### Behoben ### Behoben

BIN
Icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

243
README.md
View file

@ -159,11 +159,13 @@ Die gemeinsame Client-Architektur ist bereits auf eine
plattformabhängige Implementierung der virtuellen seriellen plattformabhängige Implementierung der virtuellen seriellen
Schnittstelle vorbereitet. Schnittstelle vorbereitet.
Die Windows-Implementierung der virtuellen seriellen Schnittstelle ist Unter Windows wird dieselbe gemeinsame Client- und Konfigurationslogik
noch nicht vollständig implementiert. verwendet. Für Tests können vorhandene virtuelle COM-Port-Verbindungen
verwendet werden.
Der spätere Windows-Client soll dieselbe gemeinsame Client- und Mit Version `v0.0.11` wurde der Windows-Client mit funktionierender
Konfigurationslogik verwenden. Fyne-GUI sowie der automatischen Übertragung der konfigurierten Rotor-
Startbytes praktisch getestet.
## Server ## Server
@ -414,6 +416,227 @@ vollständigen Bytebereich von `0x00` bis `0xFF` geprüft.
Dabei wurde die bidirektionale Übertragung aller 256 möglichen Bytewerte Dabei wurde die bidirektionale Übertragung aller 256 möglichen Bytewerte
erfolgreich getestet. erfolgreich getestet.
## Vollständiger Simulationsaufbau auf einem Ubuntu-PC
Der komplette Datenweg kann für Entwicklung und Tests auf einem einzigen
Ubuntu-PC ausgeführt werden. Dazu laufen gleichzeitig drei Programme:
1. `rs2322tcp-pty-test` als simuliertes Rotorgerät
2. `rs2322tcp-server` mit `configs/server-test.json`
3. `rs2322tcp-client`
Der Datenweg ist:
```text
Testprogramm oder Hersteller-Software
/dev/ttyUSB100
rs2322tcp-client
│ TCP
rs2322tcp-server
/dev/pts/N
rs2322tcp-pty-test
simuliertes Rotorgerät
```
### 1. PTY-Simulator starten
In einem ersten Terminal:
```bash
cd ~/rs2322tcp
go run ./cmd/rs2322tcp-pty-test
```
Beispiel:
```text
rs2322tcp-pty-test
Port: /dev/pts/1
Modus: Rotor-Simulation mit Echo-Fallback
Warte auf Daten ...
```
Der ausgegebene PTY-Pfad wird im nächsten Schritt benötigt.
Der Simulator hält Master und Slave während seiner gesamten Laufzeit geöffnet.
Bei der bekannten Rotor-Anfrage wird automatisch die konfigurierte Rotor-Antwort
gesendet. Alle anderen Daten werden als Echo zurückgegeben.
### 2. Testkonfiguration des Servers anpassen
In `configs/server-test.json` wird der Rotor auf den aktuell vom Simulator
ausgegebenen PTY-Slave gesetzt.
Beispiel:
```json
{
"id": "rotor",
"name": "Antennenrotor",
"serial_port": "/dev/pts/1",
"baud_rate": 600,
"data_bits": 8,
"parity": "none",
"stop_bits": 1
}
```
Die PTY-Nummer ist nicht dauerhaft fest. Nach jedem neuen Start des
PTY-Simulators muss der tatsächlich ausgegebene `/dev/pts/N`-Pfad geprüft und
gegebenenfalls manuell in `server-test.json` eingetragen werden.
Im Echtbetrieb wird hier wieder ein fester Hardware-Port verwendet, zum Beispiel:
```text
/dev/ttyUSB0
```
Die serverseitige Zuordnung bleibt bewusst manuell und fest konfiguriert, weil
mehrere unterschiedliche Hardwaregeräte gleichzeitig am Server angeschlossen
sein können.
### 3. Server starten
In einem zweiten Terminal:
```bash
cd ~/rs2322tcp
go run ./cmd/rs2322tcp-server -config ./configs/server-test.json
```
Beispiel:
```text
rs2322tcp-server dev
Control-Server gestartet auf [::]:5000
Geräte: 3
```
### 4. Server-Monitor als Sniffer
Ist in der Server-Konfiguration
```json
"serial_monitor": true
```
gesetzt, protokolliert der Server den übertragenen Datenverkehr. Im
Simulationsaufbau werden dadurch unter anderem die Richtungen
```text
TCP RX
SERIAL TX
SERIAL RX
TCP TX
```
sichtbar.
Ein typischer Ablauf ist:
```text
TCP RX 57 00 00 00 00 00 00 00 00 00 00 1F 20
SERIAL TX 57 00 00 00 00 00 00 00 00 00 00 1F 20
SERIAL RX 57 07 01 06 00 01 03 06 02 00 01 20
TCP TX 57 07 01 06 00 01 03 06 02 00 01 20
```
Der Server arbeitet damit gleichzeitig als Monitor beziehungsweise Sniffer für
den übertragenen seriellen Datenverkehr.
### 5. Client-Konfiguration für den lokalen Test
Für den lokalen Test kann `configs/client.json` beispielsweise so aussehen:
```json
{
"server": {
"address": "127.0.0.1",
"port": 5000
},
"virtual_port_range": {
"first": 100,
"last": 199
},
"virtual_ports": [
{
"port": "/dev/ttyUSB100",
"remote_device": "rotor",
"startup_bytes": "57 00 00 00 00 00 00 00 00 00 00 1F 20"
}
]
}
```
Der öffentliche virtuelle Port `/dev/ttyUSB100` muss vorher wie im Abschnitt
zur manuellen Einrichtung der virtuellen Ports einmalig angelegt worden sein.
### 6. Client starten
In einem dritten Terminal:
```bash
cd ~/rs2322tcp
go run ./cmd/rs2322tcp-client
```
Nach erfolgreichem Verbindungsaufbau werden die konfigurierten `startup_bytes`
automatisch zum zugeordneten Rotor übertragen.
Der Server-Monitor muss dann den vollständigen Ablauf
```text
TCP RX -> SERIAL TX -> SERIAL RX -> TCP TX
```
zeigen.
Damit ist der gesamte Datenweg vom virtuellen Client-Port über TCP zum
simulierten Rotor und wieder zurück geprüft.
### 7. Virtuellen Port direkt testen
Eine Antwort kann beispielsweise mit
```bash
cat /dev/ttyUSB100 | xxd -g 1
```
beobachtet werden.
In einem zweiten Terminal kann eine Rotor-Anfrage gesendet werden:
```bash
printf '\x57\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x20' > /dev/ttyUSB100
```
Der Rückweg erfolgt über denselben vollständigen Datenpfad zurück zu
`/dev/ttyUSB100`.
### Hinweis zu `/dev/pts/N`
`/dev/pts/N` wird in diesem Aufbau ausschließlich als temporärer Test-Port
für den PTY-Simulator verwendet. Die Nummer ist nicht dauerhaft und kann sich
nach einem Neustart des Simulators ändern.
Im produktiven Betrieb sind die serverseitigen Geräte dagegen echte,
fest angeschlossene Hardware-Schnittstellen, beispielsweise:
```text
/dev/ttyUSB0
/dev/ttyUSB1
/dev/ttyUSB3
```
## Diagnose und Logging ## Diagnose und Logging
Für Debugging und Diagnose soll eine Protokollierung des übertragenen Für Debugging und Diagnose soll eine Protokollierung des übertragenen
@ -423,8 +646,9 @@ Insbesondere soll eine Darstellung der übertragenen Bytes möglich sein,
um Probleme bei der Kommunikation zwischen Hersteller-Software und Gerät um Probleme bei der Kommunikation zwischen Hersteller-Software und Gerät
analysieren zu können. analysieren zu können.
Während der Entwicklung kann `socat` als zusätzliches Werkzeug für Tests Für die Linux-PTY-Funktion und den beschriebenen Simulationsaufbau wird
und Diagnose eingesetzt werden. `rs2322tcp` ohne `socat` betrieben. Die benötigten PTYs werden direkt aus Go
erzeugt.
## Hardwaretest mit SPID Rot2Prog ## Hardwaretest mit SPID Rot2Prog
@ -659,8 +883,8 @@ Für die Entwicklung existiert zusätzlich das separate Testprogramm:
~/rs2322tcp-pty-test ~/rs2322tcp-pty-test
``` ```
Es erzeugt unter Linux ein PTY, setzt den Slave in den Raw Mode und zeigt Es erzeugt unter Linux ein PTY, setzt den Slave in den Raw Mode und stellt
den Namen des Slave-Gerätes an. eine simulierte serielle Gegenstelle bereit.
Beispiel: Beispiel:
@ -782,8 +1006,11 @@ git status
```text ```text
rs2322tcp/ rs2322tcp/
├── cmd/ ├── cmd/
│ ├── com0com-test/
│ ├── rs2322tcp-client/ │ ├── rs2322tcp-client/
│ ├── rs2322tcp-client-test/
│ ├── rs2322tcp-device-sim/ │ ├── rs2322tcp-device-sim/
│ ├── rs2322tcp-pty-test/
│ └── rs2322tcp-server/ │ └── rs2322tcp-server/
├── configs/ ├── configs/
├── docs/ ├── docs/

239
cmd/com0com-test/main.go Normal file
View file

@ -0,0 +1,239 @@
//go:build windows
/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: cmd/com0com-test/main.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Direkter Test eines com0com-Portpaares.
*
* Der Test öffnet COM100 und COM10 gleichzeitig und überprüft die
* Datenübertragung in beide Richtungen:
*
* COM100 -> COM10
* COM10 -> COM100
*
* Dadurch kann die Funktion von com0com und go.bug.st/serial unabhängig
* vom rs2322tcp-Client, TCP-Server und PTY-Testaufbau geprüft werden.
* ============================================================================
*/
package main
import (
"fmt"
"log"
"time"
"go.bug.st/serial"
)
func main() {
fmt.Println("rs2322tcp com0com Direkt-Test")
fmt.Println()
mode := &serial.Mode{
BaudRate: 600,
DataBits: 8,
Parity: serial.NoParity,
StopBits: serial.OneStopBit,
}
com10, err := serial.Open("COM10", mode)
if err != nil {
log.Fatalf("COM10 öffnen: %v", err)
}
defer com10.Close()
fmt.Println("COM10 geöffnet.")
com100, err := serial.Open("COM100", mode)
if err != nil {
log.Fatalf("COM100 öffnen: %v", err)
}
defer com100.Close()
fmt.Println("COM100 geöffnet.")
fmt.Println()
// ------------------------------------------------------------------------
// Test 1: COM100 -> COM10
// ------------------------------------------------------------------------
request := []byte{
0x57,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x1F,
0x20,
}
fmt.Printf(
"COM100 TX: %d Bytes\n",
len(request),
)
printHex(request)
n, err := com100.Write(request)
if err != nil {
log.Fatalf("COM100 schreiben: %v", err)
}
if n != len(request) {
log.Fatalf(
"COM100 schreiben: %d von %d Bytes geschrieben",
n,
len(request),
)
}
fmt.Println("COM100 schreiben erfolgreich.")
received := make([]byte, len(request))
if err := com10.SetReadTimeout(2 * time.Second); err != nil {
log.Fatalf("COM10 ReadTimeout setzen: %v", err)
}
n, err = com10.Read(received)
if err != nil {
log.Fatalf("COM10 lesen: %v", err)
}
fmt.Printf(
"COM10 RX: %d Bytes\n",
n,
)
printHex(received[:n])
if n != len(request) {
log.Fatalf(
"COM10: %d Bytes empfangen, erwartet %d",
n,
len(request),
)
}
if !equalBytes(request, received[:n]) {
log.Fatal("COM10: empfangene Daten stimmen nicht mit den gesendeten Daten überein")
}
fmt.Println("Test COM100 -> COM10 erfolgreich.")
fmt.Println()
// ------------------------------------------------------------------------
// Test 2: COM10 -> COM100
// ------------------------------------------------------------------------
response := []byte{
0x57,
0x07,
0x01,
0x06,
0x00,
0x01,
0x03,
0x06,
0x02,
0x00,
0x01,
0x20,
}
fmt.Printf(
"COM10 TX: %d Bytes\n",
len(response),
)
printHex(response)
n, err = com10.Write(response)
if err != nil {
log.Fatalf("COM10 schreiben: %v", err)
}
if n != len(response) {
log.Fatalf(
"COM10 schreiben: %d von %d Bytes geschrieben",
n,
len(response),
)
}
fmt.Println("COM10 schreiben erfolgreich.")
received = make([]byte, len(response))
if err := com100.SetReadTimeout(2 * time.Second); err != nil {
log.Fatalf("COM100 ReadTimeout setzen: %v", err)
}
n, err = com100.Read(received)
if err != nil {
log.Fatalf("COM100 lesen: %v", err)
}
fmt.Printf(
"COM100 RX: %d Bytes\n",
n,
)
printHex(received[:n])
if n != len(response) {
log.Fatalf(
"COM100: %d Bytes empfangen, erwartet %d",
n,
len(response),
)
}
if !equalBytes(response, received[:n]) {
log.Fatal("COM100: empfangene Daten stimmen nicht mit den gesendeten Daten überein")
}
fmt.Println("Test COM10 -> COM100 erfolgreich.")
fmt.Println()
fmt.Println("==========================================")
fmt.Println("Alle com0com-Direkt-Tests erfolgreich.")
fmt.Println("==========================================")
}
// printHex gibt einen Byte-Block hexadezimal aus.
func printHex(data []byte) {
for i, b := range data {
if i > 0 {
fmt.Print(" ")
}
fmt.Printf("%02X", b)
}
fmt.Println()
}
// equalBytes compares two byte slices.
func equalBytes(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

View file

@ -23,10 +23,11 @@ package main
import ( import (
"fmt" "fmt"
"image/color"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/app" "fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/theme"
"image/color"
"git.lang-dieter.de/rs2322tcp/internal/gui" "git.lang-dieter.de/rs2322tcp/internal/gui"
"git.lang-dieter.de/rs2322tcp/internal/version" "git.lang-dieter.de/rs2322tcp/internal/version"
@ -113,11 +114,6 @@ func main() {
return return
} }
// Das technische Client-System wird nach dem Erzeugen des Fensters
// asynchron gestartet. Dadurch bleibt die GUI auch während des
// Verbindungsaufbaus sofort sichtbar und bedienbar.
guiApplication.Start()
// SetOnClosed gehört zum Fyne-Fenster, nicht zur Fyne-Anwendung. // SetOnClosed gehört zum Fyne-Fenster, nicht zur Fyne-Anwendung.
// //
// Beim Schließen des Hauptfensters wird die technische Client- // Beim Schließen des Hauptfensters wird die technische Client-
@ -129,6 +125,16 @@ func main() {
}, },
) )
// ShowAndRun zeigt das Fenster und startet den Fyne-Eventloop. // Zuerst das GUI-Fenster erzeugen und anzeigen.
guiApplication.ShowAndRun() //
// Dadurch wird ein möglicher Fehler beim Start von Fyne/GLFW erkannt,
// bevor die technische Client-Kommunikation gestartet wird.
guiApplication.Window().Show()
// Erst nachdem das Fenster erfolgreich erzeugt wurde, wird das
// technische Client-System asynchron gestartet.
guiApplication.Start()
// Den Fyne-Eventloop starten.
application.Run()
} }

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

View file

@ -1,6 +1,6 @@
{ {
"server": { "server": {
"address": "100.64.0.1", "address": "127.0.0.1",
"port": 5000 "port": 5000
}, },
"virtual_port_range": { "virtual_port_range": {
@ -9,12 +9,9 @@
}, },
"virtual_ports": [ "virtual_ports": [
{ {
"port": "COM100", "port": "/dev/ttyUSB100",
"remote_device": "rotor" "remote_device": "rotor",
}, "startup_bytes": "57 00 00 00 00 00 00 00 00 00 00 1F 20"
{
"port": "COM101",
"remote_device": "radio"
} }
] ]

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": false,
"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/1",
"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
}
]
}

View file

@ -12,13 +12,18 @@
* *
* Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt * Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt
* übernommen. * übernommen.
*
* Optional können nach dem Aufbau der Datenverbindung konfigurierte
* Startbytes an das entfernte Gerät gesendet werden.
* ============================================================================ * ============================================================================
*/ */
package client package client
import ( import (
"encoding/hex"
"fmt" "fmt"
"net" "net"
"strings"
"git.lang-dieter.de/rs2322tcp/internal/config" "git.lang-dieter.de/rs2322tcp/internal/config"
"git.lang-dieter.de/rs2322tcp/internal/transport" "git.lang-dieter.de/rs2322tcp/internal/transport"
@ -36,6 +41,11 @@ import (
// The local virtual port is opened using exactly the port number specified // The local virtual port is opened using exactly the port number specified
// in virtualPortConfig.Port. This preserves the user's configured mapping // in virtualPortConfig.Port. This preserves the user's configured mapping
// between local virtual interfaces and remote devices. // between local virtual interfaces and remote devices.
//
// If StartupBytes are configured, they are sent once immediately after the
// TCP data connection has been established. Invalid startup bytes do not
// abort the connection setup. A warning is printed and the connection
// continues normally.
func connectVirtualPort( func connectVirtualPort(
c *Client, c *Client,
manager *VirtualPortManager, manager *VirtualPortManager,
@ -81,9 +91,63 @@ func connectVirtualPort(
) )
} }
if err := sendStartupBytes(conn, virtualPortConfig); err != nil {
fmt.Printf(
"warning: startup bytes for %q were not sent: %v\n",
virtualPortConfig.RemoteDevice,
err,
)
}
return virtualPort, conn, nil return virtualPort, conn, nil
} }
// sendStartupBytes sends the optional configured startup byte sequence over
// the already established TCP data connection.
//
// An empty StartupBytes configuration does not send anything and is not an
// error.
//
// The configured value may contain spaces between hexadecimal bytes, for
// example:
//
// 57 00 00 00 00 00 00 00 00 00 00 1F 20
func sendStartupBytes(
conn net.Conn,
virtualPortConfig config.VirtualPortConfig,
) error {
startupBytes := strings.TrimSpace(virtualPortConfig.StartupBytes)
if startupBytes == "" {
return nil
}
hexText := strings.Join(strings.Fields(startupBytes), "")
data, err := hex.DecodeString(hexText)
if err != nil {
return fmt.Errorf("invalid hexadecimal byte sequence: %w", err)
}
if len(data) == 0 {
return nil
}
written, err := conn.Write(data)
if err != nil {
return fmt.Errorf("send startup bytes: %w", err)
}
if written != len(data) {
return fmt.Errorf(
"send startup bytes: wrote %d of %d bytes",
written,
len(data),
)
}
return nil
}
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Bridge connection // Bridge connection
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////

View file

@ -205,7 +205,30 @@ func (p *ptySerial) Write(b []byte) (int, error) {
return 0, fmt.Errorf("PTY master is closed") return 0, fmt.Errorf("PTY master is closed")
} }
return unix.Write(fd, b) fmt.Printf(
"PTY TX [%s]: % X\n",
p.path,
b,
)
n, err := unix.Write(fd, b)
if err != nil {
fmt.Printf(
"PTY TX Fehler [%s]: %v\n",
p.path,
err,
)
return n, err
}
fmt.Printf(
"PTY TX erfolgreich [%s]: %d Bytes\n",
p.path,
n,
)
return n, nil
} }
// Close closes the PTY master and slave. // Close closes the PTY master and slave.

View file

@ -11,8 +11,8 @@
* Beschreibung: * Beschreibung:
* Windows-spezifische Erzeugung eines verwalteten virtuellen rs2322tcp-Ports. * Windows-spezifische Erzeugung eines verwalteten virtuellen rs2322tcp-Ports.
* *
* Der konfigurierte virtuelle COM-Port wird über die bereits vorhandene * Der konfigurierte virtuelle COM-Port wird einmal geöffnet und anschließend
* serielle Infrastruktur geöffnet. * über dasselbe Handle für Lese- und Schreiboperationen verwendet.
* ============================================================================ * ============================================================================
*/ */
@ -36,6 +36,9 @@ import (
// The visible COM port is provided by the platform-specific PortPath() // The visible COM port is provided by the platform-specific PortPath()
// implementation. The actual COM-port pair configuration is managed // implementation. The actual COM-port pair configuration is managed
// externally by the Windows virtual COM-port driver. // externally by the Windows virtual COM-port driver.
//
// The COM port is opened exactly once. The resulting handle is used for
// parallel read and write operations.
func openVirtualPort( func openVirtualPort(
manager *VirtualPortManager, manager *VirtualPortManager,
number int, number int,
@ -59,7 +62,7 @@ func openVirtualPort(
StopBits: bugserial.OneStopBit, StopBits: bugserial.OneStopBit,
} }
serial, err := bugserial.Open(portPath, mode) port, err := bugserial.Open(portPath, mode)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"open virtual serial port %s: %w", "open virtual serial port %s: %w",
@ -71,7 +74,7 @@ func openVirtualPort(
virtualPort := NewVirtualPort( virtualPort := NewVirtualPort(
portPath, portPath,
&windowsSerial{ &windowsSerial{
port: serial, port: port,
path: portPath, path: portPath,
}, },
) )

View file

@ -12,8 +12,9 @@
* Windows-spezifische Implementierung der virtuellen seriellen Schnittstelle * Windows-spezifische Implementierung der virtuellen seriellen Schnittstelle
* über go.bug.st/serial. * über go.bug.st/serial.
* *
* Der konfigurierte COM-Port wird direkt geöffnet. Die Zuordnung eines * Ein COM-Port wird unter Windows nur einmal geöffnet. Dasselbe Handle wird
* virtuellen COM-Port-Paares erfolgt außerhalb des rs2322tcp-Clients. * parallel für Lese- und Schreiboperationen verwendet. Damit kann eine
* Goroutine blockierend lesen, während eine andere Goroutine Daten schreibt.
* ============================================================================ * ============================================================================
*/ */
@ -30,9 +31,11 @@ import (
// windowsSerial // windowsSerial
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// windowsSerial implements VirtualSerial using a Windows COM port. // windowsSerial implements VirtualSerial using one Windows COM-port handle
// for both reading and writing.
type windowsSerial struct { type windowsSerial struct {
mu sync.Mutex mu sync.Mutex
port bugserial.Port port bugserial.Port
path string path string
} }
@ -51,10 +54,10 @@ func (p *windowsSerial) Path() string {
} }
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Read / Write // Read
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Read reads raw bytes from the COM port. // Read reads raw bytes from the Windows COM-port handle.
func (p *windowsSerial) Read(b []byte) (int, error) { func (p *windowsSerial) Read(b []byte) (int, error) {
if p == nil { if p == nil {
return 0, fmt.Errorf("virtual serial port is nil") return 0, fmt.Errorf("virtual serial port is nil")
@ -62,16 +65,41 @@ func (p *windowsSerial) Read(b []byte) (int, error) {
p.mu.Lock() p.mu.Lock()
port := p.port port := p.port
path := p.path
p.mu.Unlock() p.mu.Unlock()
if port == nil { if port == nil {
return 0, fmt.Errorf("virtual serial port is closed") return 0, fmt.Errorf("virtual serial port is closed")
} }
return port.Read(b) n, err := port.Read(b)
if n > 0 {
fmt.Printf(
"WINDOWS SERIAL RX [%s]: %d Bytes\n% X\n",
path,
n,
b[:n],
)
}
if err != nil {
fmt.Printf(
"WINDOWS SERIAL RX FEHLER [%s]: nach %d Bytes: %v\n",
path,
n,
err,
)
}
return n, err
} }
// Write writes raw bytes to the COM port. ///////////////////////////////////////////////////////////////////////////////
// Write
///////////////////////////////////////////////////////////////////////////////
// Write writes raw bytes to the Windows COM-port handle.
func (p *windowsSerial) Write(b []byte) (int, error) { func (p *windowsSerial) Write(b []byte) (int, error) {
if p == nil { if p == nil {
return 0, fmt.Errorf("virtual serial port is nil") return 0, fmt.Errorf("virtual serial port is nil")
@ -79,20 +107,67 @@ func (p *windowsSerial) Write(b []byte) (int, error) {
p.mu.Lock() p.mu.Lock()
port := p.port port := p.port
path := p.path
p.mu.Unlock() p.mu.Unlock()
if port == nil { if port == nil {
return 0, fmt.Errorf("virtual serial port is closed") return 0, fmt.Errorf("virtual serial port is closed")
} }
return port.Write(b) fmt.Printf(
"WINDOWS SERIAL TX [%s]: %d Bytes\n% X\n",
path,
len(b),
b,
)
fmt.Printf(
"WINDOWS SERIAL TX [%s]: Rufe port.Write() auf ...\n",
path,
)
n, err := port.Write(b)
fmt.Printf(
"WINDOWS SERIAL TX [%s]: port.Write() zurückgekehrt: %d von %d Bytes\n",
path,
n,
len(b),
)
if err != nil {
fmt.Printf(
"WINDOWS SERIAL TX FEHLER [%s]: %v\n",
path,
err,
)
return n, err
}
if n != len(b) {
fmt.Printf(
"WINDOWS SERIAL TX UNVOLLSTÄNDIG [%s]: %d von %d Bytes\n",
path,
n,
len(b),
)
} else {
fmt.Printf(
"WINDOWS SERIAL TX ERFOLGREICH [%s]: %d Bytes\n",
path,
n,
)
}
return n, nil
} }
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Close // Close
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
// Close closes the Windows COM port. // Close closes the Windows COM-port handle.
func (p *windowsSerial) Close() error { func (p *windowsSerial) Close() error {
if p == nil { if p == nil {
return nil return nil
@ -100,15 +175,36 @@ func (p *windowsSerial) Close() error {
p.mu.Lock() p.mu.Lock()
if p.port == nil {
p.mu.Unlock()
return nil
}
port := p.port port := p.port
path := p.path
p.port = nil p.port = nil
p.mu.Unlock() p.mu.Unlock()
return port.Close() if port == nil {
return nil
}
fmt.Printf(
"WINDOWS SERIAL [%s]: Schließe COM-Port ...\n",
path,
)
if err := port.Close(); err != nil {
fmt.Printf(
"WINDOWS SERIAL [%s]: Fehler beim Schließen des COM-Ports: %v\n",
path,
err,
)
return err
}
fmt.Printf(
"WINDOWS SERIAL [%s]: COM-Port geschlossen.\n",
path,
)
return nil
} }

View file

@ -79,6 +79,7 @@ type VirtualPortRangeConfig struct {
type VirtualPortConfig struct { type VirtualPortConfig struct {
Port string `json:"port"` Port string `json:"port"`
RemoteDevice string `json:"remote_device"` RemoteDevice string `json:"remote_device"`
StartupBytes string `json:"startup_bytes,omitempty"`
} }
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////

View file

@ -8,11 +8,13 @@ package config_test
import ( import (
"encoding/json" "encoding/json"
"git.lang-dieter.de/rs2322tcp/internal/config"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"testing" "testing"
"git.lang-dieter.de/rs2322tcp/internal/config"
) )
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
@ -59,13 +61,15 @@ func TestLoadServer(t *testing.T) {
} }
if cfg.Listen.Address != "0.0.0.0" { if cfg.Listen.Address != "0.0.0.0" {
t.Errorf("Listen.Address = %q, want %q", t.Errorf(
cfg.Listen.Address, "0.0.0.0") "Listen.Address = %q, want %q",
cfg.Listen.Address,
"0.0.0.0",
)
} }
if cfg.Listen.Port != 5000 { if cfg.Listen.Port != 5000 {
t.Errorf("Listen.Port = %d, want %d", t.Errorf("Listen.Port = %d, want %d", cfg.Listen.Port, 5000)
cfg.Listen.Port, 5000)
} }
if len(cfg.Devices) != 1 { if len(cfg.Devices) != 1 {
@ -75,18 +79,19 @@ func TestLoadServer(t *testing.T) {
device := cfg.Devices[0] device := cfg.Devices[0]
if device.ID != "radio" { if device.ID != "radio" {
t.Errorf("Device.ID = %q, want %q", t.Errorf("Device.ID = %q, want %q", device.ID, "radio")
device.ID, "radio")
} }
if device.SerialPort != "/dev/ttyUSB0" { if device.SerialPort != "/dev/ttyUSB0" {
t.Errorf("Device.SerialPort = %q, want %q", t.Errorf(
device.SerialPort, "/dev/ttyUSB0") "Device.SerialPort = %q, want %q",
device.SerialPort,
"/dev/ttyUSB0",
)
} }
if device.BaudRate != 9600 { if device.BaudRate != 9600 {
t.Errorf("Device.BaudRate = %d, want %d", t.Errorf("Device.BaudRate = %d, want %d", device.BaudRate, 9600)
device.BaudRate, 9600)
} }
} }
@ -121,40 +126,106 @@ func TestLoadClient(t *testing.T) {
} }
if cfg.Server.Address != "100.64.0.10" { if cfg.Server.Address != "100.64.0.10" {
t.Errorf("Server.Address = %q, want %q", t.Errorf(
cfg.Server.Address, "100.64.0.10") "Server.Address = %q, want %q",
cfg.Server.Address,
"100.64.0.10",
)
} }
if cfg.Server.Port != 5000 { if cfg.Server.Port != 5000 {
t.Errorf("Server.Port = %d, want %d", t.Errorf("Server.Port = %d, want %d", cfg.Server.Port, 5000)
cfg.Server.Port, 5000)
} }
if cfg.VirtualPortRange.First != config.DefaultVirtualPortFirst { if cfg.VirtualPortRange.First != config.DefaultVirtualPortFirst {
t.Errorf("VirtualPortRange.First = %d, want %d", t.Errorf(
cfg.VirtualPortRange.First, config.DefaultVirtualPortFirst) "VirtualPortRange.First = %d, want %d",
cfg.VirtualPortRange.First,
config.DefaultVirtualPortFirst,
)
} }
if cfg.VirtualPortRange.Last != config.DefaultVirtualPortLast { if cfg.VirtualPortRange.Last != config.DefaultVirtualPortLast {
t.Errorf("VirtualPortRange.Last = %d, want %d", t.Errorf(
cfg.VirtualPortRange.Last, config.DefaultVirtualPortLast) "VirtualPortRange.Last = %d, want %d",
cfg.VirtualPortRange.Last,
config.DefaultVirtualPortLast,
)
} }
if len(cfg.VirtualPorts) != 1 { if len(cfg.VirtualPorts) != 1 {
t.Fatalf("len(VirtualPorts) = %d, want 1", t.Fatalf(
len(cfg.VirtualPorts)) "len(VirtualPorts) = %d, want 1",
len(cfg.VirtualPorts),
)
} }
virtualPort := cfg.VirtualPorts[0] virtualPort := cfg.VirtualPorts[0]
if virtualPort.Port != "COM7" { if virtualPort.Port != "COM7" {
t.Errorf("VirtualPort.Port = %q, want %q", t.Errorf(
virtualPort.Port, "COM7") "VirtualPort.Port = %q, want %q",
virtualPort.Port,
"COM7",
)
} }
if virtualPort.RemoteDevice != "radio" { if virtualPort.RemoteDevice != "radio" {
t.Errorf("VirtualPort.RemoteDevice = %q, want %q", t.Errorf(
virtualPort.RemoteDevice, "radio") "VirtualPort.RemoteDevice = %q, want %q",
virtualPort.RemoteDevice,
"radio",
)
}
if virtualPort.StartupBytes != "" {
t.Errorf(
"VirtualPort.StartupBytes = %q, want empty",
virtualPort.StartupBytes,
)
}
}
func TestLoadClientWithStartupBytes(t *testing.T) {
dir := t.TempDir()
filename := filepath.Join(dir, "client.json")
data := `{
"server": {
"address": "100.64.0.10",
"port": 5000
},
"virtual_ports": [
{
"port": "COM7",
"remote_device": "radio",
"startup_bytes": "57 00 00 1F 20"
}
]
}`
if err := os.WriteFile(filename, []byte(data), 0644); err != nil {
t.Fatalf("write test configuration: %v", err)
}
cfg, err := config.LoadClient(filename)
if err != nil {
t.Fatalf("LoadClient() failed: %v", err)
}
if len(cfg.VirtualPorts) != 1 {
t.Fatalf(
"len(VirtualPorts) = %d, want 1",
len(cfg.VirtualPorts),
)
}
if cfg.VirtualPorts[0].StartupBytes != "57 00 00 1F 20" {
t.Errorf(
"VirtualPort.StartupBytes = %q, want %q",
cfg.VirtualPorts[0].StartupBytes,
"57 00 00 1F 20",
)
} }
} }
@ -184,13 +255,17 @@ func TestLoadClientCustomVirtualPortRange(t *testing.T) {
} }
if cfg.VirtualPortRange.First != 200 { if cfg.VirtualPortRange.First != 200 {
t.Errorf("VirtualPortRange.First = %d, want 200", t.Errorf(
cfg.VirtualPortRange.First) "VirtualPortRange.First = %d, want 200",
cfg.VirtualPortRange.First,
)
} }
if cfg.VirtualPortRange.Last != 299 { if cfg.VirtualPortRange.Last != 299 {
t.Errorf("VirtualPortRange.Last = %d, want 299", t.Errorf(
cfg.VirtualPortRange.Last) "VirtualPortRange.Last = %d, want 299",
cfg.VirtualPortRange.Last,
)
} }
} }
@ -215,6 +290,7 @@ func TestSaveAndLoadClient(t *testing.T) {
{ {
Port: "COM7", Port: "COM7",
RemoteDevice: "radio", RemoteDevice: "radio",
StartupBytes: "57 00 00 1F 20",
}, },
{ {
Port: "COM8", Port: "COM8",
@ -241,9 +317,11 @@ func TestSaveAndLoadClient(t *testing.T) {
} }
if len(loaded.VirtualPorts) != len(original.VirtualPorts) { if len(loaded.VirtualPorts) != len(original.VirtualPorts) {
t.Fatalf("len(VirtualPorts) = %d, want %d", t.Fatalf(
"len(VirtualPorts) = %d, want %d",
len(loaded.VirtualPorts), len(loaded.VirtualPorts),
len(original.VirtualPorts)) len(original.VirtualPorts),
)
} }
for i := range original.VirtualPorts { for i := range original.VirtualPorts {
@ -402,8 +480,35 @@ func TestExampleClientConfig(t *testing.T) {
t.Fatalf("LoadClient() failed: %v", err) t.Fatalf("LoadClient() failed: %v", err)
} }
if len(cfg.VirtualPorts) != 2 { if len(cfg.VirtualPorts) != 1 {
t.Fatalf("len(VirtualPorts) = %d, want 2", len(cfg.VirtualPorts)) t.Fatalf(
"len(VirtualPorts) = %d, want 1",
len(cfg.VirtualPorts),
)
}
expectedPort := "COM100"
if runtime.GOOS == "linux" {
expectedPort = "/dev/ttyUSB100"
}
port := cfg.VirtualPorts[0]
if port.Port != expectedPort {
t.Errorf(
"VirtualPorts[0].Port = %q, want %q",
port.Port,
expectedPort,
)
}
if port.RemoteDevice != "rotor" {
t.Errorf(
"VirtualPorts[0].RemoteDevice = %q, want %q",
port.RemoteDevice,
"rotor",
)
} }
} }
@ -425,33 +530,51 @@ func TestDeviceRemoteDevice(t *testing.T) {
remote := device.RemoteDevice() remote := device.RemoteDevice()
if remote.ID != "radio" { if remote.ID != "radio" {
t.Errorf("RemoteDevice.ID = %q, want %q", t.Errorf(
remote.ID, "radio") "RemoteDevice.ID = %q, want %q",
remote.ID,
"radio",
)
} }
if remote.Name != "Funkgerät" { if remote.Name != "Funkgerät" {
t.Errorf("RemoteDevice.Name = %q, want %q", t.Errorf(
remote.Name, "Funkgerät") "RemoteDevice.Name = %q, want %q",
remote.Name,
"Funkgerät",
)
} }
if remote.BaudRate != 9600 { if remote.BaudRate != 9600 {
t.Errorf("RemoteDevice.BaudRate = %d, want %d", t.Errorf(
remote.BaudRate, 9600) "RemoteDevice.BaudRate = %d, want %d",
remote.BaudRate,
9600,
)
} }
if remote.DataBits != 8 { if remote.DataBits != 8 {
t.Errorf("RemoteDevice.DataBits = %d, want %d", t.Errorf(
remote.DataBits, 8) "RemoteDevice.DataBits = %d, want %d",
remote.DataBits,
8,
)
} }
if remote.Parity != "none" { if remote.Parity != "none" {
t.Errorf("RemoteDevice.Parity = %q, want %q", t.Errorf(
remote.Parity, "none") "RemoteDevice.Parity = %q, want %q",
remote.Parity,
"none",
)
} }
if remote.StopBits != 1 { if remote.StopBits != 1 {
t.Errorf("RemoteDevice.StopBits = %d, want %d", t.Errorf(
remote.StopBits, 1) "RemoteDevice.StopBits = %d, want %d",
remote.StopBits,
1,
)
} }
} }
@ -490,28 +613,41 @@ func TestServerRemoteDevices(t *testing.T) {
} }
if list.Devices[0].ID != "radio" { if list.Devices[0].ID != "radio" {
t.Errorf("Devices[0].ID = %q, want %q", t.Errorf(
list.Devices[0].ID, "radio") "Devices[0].ID = %q, want %q",
list.Devices[0].ID,
"radio",
)
} }
if list.Devices[1].ID != "rotor" { if list.Devices[1].ID != "rotor" {
t.Errorf("Devices[1].ID = %q, want %q", t.Errorf(
list.Devices[1].ID, "rotor") "Devices[1].ID = %q, want %q",
list.Devices[1].ID,
"rotor",
)
} }
if list.Devices[0].Name != "Funkgerät" { if list.Devices[0].Name != "Funkgerät" {
t.Errorf("Devices[0].Name = %q, want %q", t.Errorf(
list.Devices[0].Name, "Funkgerät") "Devices[0].Name = %q, want %q",
list.Devices[0].Name,
"Funkgerät",
)
} }
if list.Devices[0].BaudRate != 9600 { if list.Devices[0].BaudRate != 9600 {
t.Errorf("Devices[0].BaudRate = %d, want 9600", t.Errorf(
list.Devices[0].BaudRate) "Devices[0].BaudRate = %d, want 9600",
list.Devices[0].BaudRate,
)
} }
if list.Devices[1].BaudRate != 4800 { if list.Devices[1].BaudRate != 4800 {
t.Errorf("Devices[1].BaudRate = %d, want 4800", t.Errorf(
list.Devices[1].BaudRate) "Devices[1].BaudRate = %d, want 4800",
list.Devices[1].BaudRate,
)
} }
} }
@ -561,11 +697,17 @@ func TestRemoteDeviceJSON(t *testing.T) {
} }
if len(decoded.Devices) != 1 { if len(decoded.Devices) != 1 {
t.Fatalf("len(decoded.Devices) = %d, want 1", len(decoded.Devices)) t.Fatalf(
"len(decoded.Devices) = %d, want 1",
len(decoded.Devices),
)
} }
if decoded.Devices[0].ID != "radio" { if decoded.Devices[0].ID != "radio" {
t.Errorf("Devices[0].ID = %q, want %q", t.Errorf(
decoded.Devices[0].ID, "radio") "Devices[0].ID = %q, want %q",
decoded.Devices[0].ID,
"radio",
)
} }
} }

View file

@ -2,30 +2,86 @@
# #
# ============================================================================ # ============================================================================
# Projekt.....: rs2322tcp # Projekt.....: rs2322tcp
# Datei.......: build.sh # Datei.......: scripts/build.sh
# #
# Copyright (C) 2026 Dieter Lang # Copyright (C) 2026 Dieter Lang
# #
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# Beschreibung: # Beschreibung:
# Erstellt die Linux- und Windows-Builds des Projektes. # Erstellt die Release-Builds des Projekts rs2322tcp.
# #
# Die Versionsinformationen werden aus Git ermittelt und beim Build # Server:
# über den Go-Linker in die Binärdateien eingebettet. # - Linux amd64
# - Linux arm64
# - Linux 386
# - Linux arm
# #
# Gebaut werden: # Client:
# - Linux amd64: Client und Server # - Linux amd64
# - Linux arm64: Server # - Linux arm64
# - Windows amd64: Client # - Windows amd64
# - Windows 386
#
# Der Server wird CGO-frei direkt mit Go gebaut.
#
# Der Fyne-basierte Client wird für Linux amd64 direkt gebaut.
# Die Cross-Builds für Linux arm64 und Windows werden mit fyne-cross
# in Docker erzeugt.
#
# Voraussetzungen:
# - Go
# - Git
# - Docker
# - fyne-cross
#
# Installation von fyne-cross:
#
# go install github.com/fyne-io/fyne-cross@latest
#
# Windows XP wird derzeit nicht unterstützt.
# ============================================================================ # ============================================================================
# #
set -e set -e
PROJECT=$(basename "$(pwd)") ##############################################################################
BUILDDIR="build" # Projektverzeichnis
MODULE=$(go list -m) ##############################################################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${PROJECT_DIR}"
PROJECT="$(basename "${PROJECT_DIR}")"
BUILD_DIR="build"
FYNE_CROSS_DIR="fyne-cross"
##############################################################################
# fyne-cross
##############################################################################
FYNE_CROSS="${HOME}/go/bin/fyne-cross"
if [ ! -x "${FYNE_CROSS}" ]; then
echo
echo "FEHLER: fyne-cross wurde nicht gefunden:"
echo " ${FYNE_CROSS}"
echo
echo "Installation:"
echo " go install github.com/fyne-io/fyne-cross@latest"
echo
exit 1
fi
FYNE_APP_ID="de.lang-dieter.rs2322tcp"
##############################################################################
# Modul
##############################################################################
MODULE="$(go list -m)"
############################################################################## ##############################################################################
# Farben # Farben
@ -41,20 +97,22 @@ RESET="\033[0m"
# Zeitmessung # Zeitmessung
############################################################################## ##############################################################################
START=$(date +%s) START="$(date +%s)"
############################################################################## ##############################################################################
# Projektinformationen # Projektinformationen
############################################################################## ##############################################################################
VERSION=$(git describe --tags --always 2>/dev/null || echo "dev") VERSION="$(git describe --tags --always 2>/dev/null || echo "dev")"
BUILD=$(git rev-list --count HEAD 2>/dev/null || echo "0") BUILD="$(git rev-list --count HEAD 2>/dev/null || echo "0")"
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown") BRANCH="$(git branch --show-current 2>/dev/null || echo "unknown")"
COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") COMMIT="$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")"
GO_VERSION=$(go version | awk '{print $3}') GO_VERSION="$(go version | awk '{print $3}')"
HOST=$(hostname) HOST="$(hostname)"
DATE=$(date "+%Y-%m-%dT%H:%M:%S") DATE="$(date "+%Y-%m-%dT%H:%M:%S")"
##############################################################################
# Kopf
############################################################################## ##############################################################################
echo echo
@ -64,14 +122,14 @@ echo " ${PROJECT} Build Script"
echo "============================================================" echo "============================================================"
echo -e "${RESET}" echo -e "${RESET}"
printf "%-12s %s\n" "Projekt:" "$PROJECT" printf "%-12s %s\n" "Projekt:" "${PROJECT}"
printf "%-12s %s\n" "Version:" "$VERSION" printf "%-12s %s\n" "Version:" "${VERSION}"
printf "%-12s %s\n" "Build:" "$BUILD" printf "%-12s %s\n" "Build:" "${BUILD}"
printf "%-12s %s\n" "Branch:" "$BRANCH" printf "%-12s %s\n" "Branch:" "${BRANCH}"
printf "%-12s %s\n" "Commit:" "$COMMIT" printf "%-12s %s\n" "Commit:" "${COMMIT}"
printf "%-12s %s\n" "Go:" "$GO_VERSION" printf "%-12s %s\n" "Go:" "${GO_VERSION}"
printf "%-12s %s\n" "Host:" "$HOST" printf "%-12s %s\n" "Host:" "${HOST}"
printf "%-12s %s\n" "Datum:" "$DATE" printf "%-12s %s\n" "Datum:" "${DATE}"
echo echo
@ -97,119 +155,192 @@ echo -e "${YELLOW}Bereinige Module ...${RESET}"
go mod tidy go mod tidy
############################################################################## ##############################################################################
# Build-Verzeichnis # Build-Verzeichnisse
############################################################################## ##############################################################################
echo -e "${YELLOW}Erzeuge Build-Verzeichnis ...${RESET}" echo -e "${YELLOW}Erzeuge Build-Verzeichnis ...${RESET}"
rm -rf "${BUILDDIR}" rm -rf "${BUILD_DIR}"
mkdir -p "${BUILDDIR}" mkdir -p "${BUILD_DIR}"
############################################################################## ##############################################################################
# Linker-Optionen # Linker-Optionen
############################################################################## ##############################################################################
LDFLAGS="-s -w \ LDFLAGS="-s -w \
-X '${MODULE}/internal/version.Version=${VERSION}' \ -X ${MODULE}/internal/version.Version=${VERSION} \
-X '${MODULE}/internal/version.Build=${BUILD}' \ -X ${MODULE}/internal/version.Build=${BUILD} \
-X '${MODULE}/internal/version.Commit=${COMMIT}' \ -X ${MODULE}/internal/version.Commit=${COMMIT} \
-X '${MODULE}/internal/version.Date=${DATE}'" -X ${MODULE}/internal/version.Date=${DATE}"
############################################################################## ##############################################################################
# Buildfunktion # Direkter Go-Build
############################################################################## ##############################################################################
build_target() { build_target() {
GOOS=$1 local GOOS="$1"
GOARCH=$2 local GOARCH="$2"
TARGET=$3 local TARGET="$3"
PROGRAM=$4 local PROGRAM="$4"
OUTPUT=$5 local OUTPUT="$5"
local CGO="$6"
echo echo
echo -e "${BLUE}Baue ${PROGRAM} für ${TARGET}${RESET}" echo -e "${BLUE}Baue ${PROGRAM} für ${TARGET}${RESET}"
mkdir -p "${BUILDDIR}/${TARGET}" mkdir -p "${BUILD_DIR}/${TARGET}"
GOOS="${GOOS}" \ GOOS="${GOOS}" \
GOARCH="${GOARCH}" \ GOARCH="${GOARCH}" \
CGO_ENABLED=0 \ CGO_ENABLED="${CGO}" \
go build \ go build \
-trimpath \ -trimpath \
-ldflags="${LDFLAGS}" \ -ldflags="${LDFLAGS}" \
-o "${BUILDDIR}/${TARGET}/${OUTPUT}" \ -o "${BUILD_DIR}/${TARGET}/${OUTPUT}" \
"./cmd/${PROGRAM}" "./cmd/${PROGRAM}"
( (
cd "${BUILDDIR}/${TARGET}" cd "${BUILD_DIR}/${TARGET}"
sha256sum "${OUTPUT}" > "${OUTPUT}.sha256" sha256sum "${OUTPUT}" > "${OUTPUT}.sha256"
) )
SIZE=$(stat -c%s "${BUILDDIR}/${TARGET}/${OUTPUT}") local SIZE
SIZE_MB=$(awk "BEGIN {printf \"%.2f\", ${SIZE}/1024/1024}") local SIZE_MB
SIZE="$(stat -c%s "${BUILD_DIR}/${TARGET}/${OUTPUT}")"
SIZE_MB="$(awk "BEGIN {printf \"%.2f\", ${SIZE}/1024/1024}")"
echo -e "${GREEN}OK${RESET}" echo -e "${GREEN}OK${RESET}"
echo "Größe : ${SIZE_MB} MB" echo "Größe : ${SIZE_MB} MB"
} }
############################################################################## ##############################################################################
# Linux amd64 # Fyne-Cross-Build
##############################################################################
build_fyne_target() {
local PLATFORM="$1"
local ARCH="$2"
local TARGET="$3"
local OUTPUT="$4"
local SOURCE
local TARGET_DIR
echo
echo -e "${BLUE}Baue rs2322tcp-client für ${TARGET} mit fyne-cross${RESET}"
"${FYNE_CROSS}" "${PLATFORM}" \
-arch="${ARCH}" \
-app-id="${FYNE_APP_ID}" \
-output="rs2322tcp-client" \
"./cmd/rs2322tcp-client"
if [ "${PLATFORM}" = "windows" ]; then
SOURCE="${FYNE_CROSS_DIR}/bin/${PLATFORM}-${ARCH}/rs2322tcp-client.exe"
else
SOURCE="${FYNE_CROSS_DIR}/bin/${PLATFORM}-${ARCH}/rs2322tcp-client"
fi
TARGET_DIR="${BUILD_DIR}/${TARGET}"
if [ ! -f "${SOURCE}" ]; then
echo
echo -e "${RED}FEHLER: fyne-cross-Builddatei wurde nicht gefunden:${RESET}"
echo " ${SOURCE}"
echo
exit 1
fi
mkdir -p "${TARGET_DIR}"
cp "${SOURCE}" "${TARGET_DIR}/${OUTPUT}"
(
cd "${TARGET_DIR}"
sha256sum "${OUTPUT}" > "${OUTPUT}.sha256"
)
local SIZE
local SIZE_MB
SIZE="$(stat -c%s "${TARGET_DIR}/${OUTPUT}")"
SIZE_MB="$(awk "BEGIN {printf \"%.2f\", ${SIZE}/1024/1024}")"
echo -e "${GREEN}OK${RESET}"
echo "Größe : ${SIZE_MB} MB"
}
##############################################################################
# Server-Builds
#
# Der Server ist CGO-frei und kann direkt cross-kompiliert werden.
############################################################################## ##############################################################################
build_target linux amd64 linux-amd64 \ build_target linux amd64 linux-amd64 \
rs2322tcp-client rs2322tcp-client rs2322tcp-server rs2322tcp-server 0
build_target linux amd64 linux-amd64 \
rs2322tcp-server rs2322tcp-server
##############################################################################
# Linux arm64
##############################################################################
build_target linux arm64 linux-arm64 \ build_target linux arm64 linux-arm64 \
rs2322tcp-server rs2322tcp-server rs2322tcp-server rs2322tcp-server 0
build_target linux 386 linux-386 \
rs2322tcp-server rs2322tcp-server 0
build_target linux arm linux-arm \
rs2322tcp-server rs2322tcp-server 0
############################################################################## ##############################################################################
# Client-Builds
##############################################################################
# Linux amd64
build_target linux amd64 linux-amd64 \
rs2322tcp-client rs2322tcp-client 1
# Linux arm64
build_fyne_target linux arm64 linux-arm64 \
rs2322tcp-client
# Windows amd64 # Windows amd64
############################################################################## build_fyne_target windows amd64 windows-amd64 \
rs2322tcp-client.exe
build_target windows amd64 windows-amd64 \ # Windows 386
rs2322tcp-client rs2322tcp-client.exe build_fyne_target windows 386 windows-386 \
rs2322tcp-client.exe
############################################################################## ##############################################################################
# Abschluss # Abschluss
############################################################################## ##############################################################################
END=$(date +%s) END="$(date +%s)"
RUNTIME=$((END-START)) RUNTIME="$((END - START))"
TARGETS=4 FILES="$(find "${BUILD_DIR}" -type f | wc -l)"
FILES=$(find "${BUILDDIR}" -type f | wc -l)
echo echo
echo -e "${BLUE}" echo -e "${BLUE}"
echo "============================================================" echo "============================================================"
echo "Zusammenfassung" echo " Zusammenfassung"
echo "============================================================" echo "============================================================"
echo -e "${RESET}" echo -e "${RESET}"
printf "%-12s %s\n" "Targets:" "$TARGETS" printf "%-12s %s\n" "Dateien:" "${FILES}"
printf "%-12s %s\n" "Dateien:" "$FILES" printf "%-12s %s\n" "Builddauer:" "${RUNTIME} Sekunden"
printf "%-12s %s\n" "Builddauer:" "${RUNTIME} Sekunden"
echo echo
echo -e "${BLUE}" echo -e "${BLUE}"
echo "============================================================" echo "============================================================"
echo "Erzeugte Build-Dateien" echo " Erzeugte Build-Dateien"
echo "============================================================" echo "============================================================"
echo -e "${RESET}" echo -e "${RESET}"
find "${BUILDDIR}" -type f | sort find "${BUILD_DIR}" -type f | sort
echo echo
echo -e "${GREEN}" echo -e "${GREEN}"
echo "============================================================" echo "============================================================"
echo "Alle Builds erfolgreich abgeschlossen." echo " Alle Builds erfolgreich abgeschlossen."
echo "============================================================" echo "============================================================"
echo -e "${RESET}" echo -e "${RESET}"

184
scripts/deploy.sh Executable file
View file

@ -0,0 +1,184 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_NAME="rs2322tcp"
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_DIR="${PROJECT_ROOT}/build"
DIST_DIR="${PROJECT_ROOT}/dist"
CONFIG_DIR="${PROJECT_ROOT}/configs"
VERSION="$(git -C "${PROJECT_ROOT}" describe --tags --always --dirty 2>/dev/null || echo "unknown")"
BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
echo
echo "============================================================"
echo " rs2322tcp Deploy Script"
echo "============================================================"
echo
echo "Projekt: ${PROJECT_NAME}"
echo "Version: ${VERSION}"
echo "Datum: ${BUILD_DATE}"
echo
if [[ ! -d "${BUILD_DIR}" ]]; then
echo "FEHLER: Build-Verzeichnis nicht gefunden:"
echo " ${BUILD_DIR}"
echo
echo "Bitte zuerst ./scripts/build.sh ausführen."
exit 1
fi
if [[ ! -f "${CONFIG_DIR}/server.json" ]]; then
echo "FEHLER: Konfigurationsdatei nicht gefunden:"
echo " ${CONFIG_DIR}/server.json"
exit 1
fi
if [[ ! -f "${CONFIG_DIR}/client.json" ]]; then
echo "FEHLER: Konfigurationsdatei nicht gefunden:"
echo " ${CONFIG_DIR}/client.json"
exit 1
fi
echo "Bereinige Distributions-Verzeichnis ..."
rm -rf "${DIST_DIR}"
mkdir -p "${DIST_DIR}"
create_linux_package() {
local architecture="$1"
local program="$2"
local config_file="$3"
local package_name="$4"
local source_dir="${BUILD_DIR}/${architecture}"
local package_dir="${DIST_DIR}/${architecture}/${package_name}"
if [[ ! -f "${source_dir}/${program}" ]]; then
echo "Überspringe ${package_name} für ${architecture}"
echo "Build-Datei nicht gefunden: ${source_dir}/${program}"
echo
return
fi
echo "Erzeuge ${package_name} für ${architecture}"
rm -rf "${package_dir}"
mkdir -p "${package_dir}"
cp "${source_dir}/${program}" "${package_dir}/"
cp "${CONFIG_DIR}/${config_file}" "${package_dir}/"
tar -C "${DIST_DIR}/${architecture}" \
-czf "${DIST_DIR}/${architecture}/${package_name}-${VERSION}.tar.gz" \
"${package_name}"
(
cd "${DIST_DIR}/${architecture}"
sha256sum "${package_name}-${VERSION}.tar.gz" \
> "${package_name}-${VERSION}.tar.gz.sha256"
)
echo "OK"
echo
}
echo "Erzeuge Linux-Server-Pakete ..."
echo
create_linux_package \
"linux-amd64" \
"rs2322tcp-server" \
"server.json" \
"rs2322tcp-server"
create_linux_package \
"linux-arm64" \
"rs2322tcp-server" \
"server.json" \
"rs2322tcp-server"
create_linux_package \
"linux-386" \
"rs2322tcp-server" \
"server.json" \
"rs2322tcp-server"
create_linux_package \
"linux-arm" \
"rs2322tcp-server" \
"server.json" \
"rs2322tcp-server"
echo "Erzeuge Linux-Client-Pakete ..."
echo
create_linux_package \
"linux-amd64" \
"rs2322tcp-client" \
"client.json" \
"rs2322tcp-client"
create_linux_package \
"linux-arm64" \
"rs2322tcp-client" \
"client.json" \
"rs2322tcp-client"
echo "Erzeuge Windows-Client-Pakete ..."
echo
create_windows_package() {
local architecture="$1"
local package_name="$2"
local source_dir="${BUILD_DIR}/${architecture}"
local package_dir="${DIST_DIR}/${architecture}/${package_name}"
if [[ ! -f "${source_dir}/rs2322tcp-client.exe" ]]; then
echo "Überspringe ${package_name} für ${architecture}"
echo "Build-Datei nicht gefunden: ${source_dir}/rs2322tcp-client.exe"
echo
return
fi
echo "Erzeuge ${package_name} für ${architecture}"
rm -rf "${package_dir}"
mkdir -p "${package_dir}"
cp "${source_dir}/rs2322tcp-client.exe" "${package_dir}/"
cp "${CONFIG_DIR}/client.json" \
"${package_dir}/client-config.json"
(
cd "${DIST_DIR}/${architecture}"
zip -qr "${package_name}-${VERSION}.zip" "${package_name}"
sha256sum "${package_name}-${VERSION}.zip" \
> "${package_name}-${VERSION}.zip.sha256"
)
echo "OK"
echo
}
create_windows_package \
"windows-amd64" \
"rs2322tcp-client"
create_windows_package \
"windows-386" \
"rs2322tcp-client"
echo "============================================================"
echo " Erzeugte Distributions-Dateien"
echo "============================================================"
echo
find "${DIST_DIR}" -type f | sort | sed "s|${PROJECT_ROOT}/||"
echo
echo "============================================================"
echo " Deploy erfolgreich abgeschlossen."
echo "============================================================"

View file

@ -0,0 +1,12 @@
@echo off
cd /d "%~dp0.."
echo ==========================================
echo rs2322tcp COM10 Testsequenz
echo ==========================================
echo.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\scripts\test-rotor-com10.ps1"
echo.
pause

View file

@ -0,0 +1,80 @@
$port = New-Object System.IO.Ports.SerialPort COM10,600,None,8,one
try {
$port.Open()
[byte[]]$request = 0x57,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0x20
$port.Write($request, 0, $request.Length)
$response = New-Object System.Collections.Generic.List[byte]
$timeout = [DateTime]::UtcNow.AddSeconds(2)
while ([DateTime]::UtcNow -lt $timeout) {
while ($port.BytesToRead -gt 0) {
$buffer = New-Object byte[] $port.BytesToRead
$count = $port.Read($buffer, 0, $buffer.Length)
for ($i = 0; $i -lt $count; $i++) {
$response.Add($buffer[$i])
}
}
# Auf das Startbyte 57 synchronisieren.
$start = $response.IndexOf([byte]0x57)
if ($start -ge 0 -and ($response.Count - $start) -ge 12) {
$response = [System.Collections.Generic.List[byte]](
$response.GetRange($start, 12)
)
break
}
Start-Sleep -Milliseconds 20
}
if ($response.Count -eq 12) {
$actual = ($response |
ForEach-Object { "{0:X2}" -f $_ }) -join " "
$expected = "57 07 01 06 00 01 03 06 02 00 01 20"
Write-Host "Bytes empfangen: $($response.Count)"
Write-Host "Antwort: $actual"
if ($actual -eq $expected) {
Write-Host "TEST ERFOLGREICH"
}
else {
Write-Host "TEST FEHLGESCHLAGEN"
Write-Host "Erwartet: $expected"
}
}
else {
Write-Host "Bytes empfangen: $($response.Count)"
if ($response.Count -gt 0) {
$actual = ($response |
ForEach-Object { "{0:X2}" -f $_ }) -join " "
Write-Host "Antwort: $actual"
}
else {
Write-Host "Keine Antwort"
}
Write-Host "TEST FEHLGESCHLAGEN - keine vollstaendige Antwort gefunden"
}
}
finally {
if ($port.IsOpen) {
$port.Close()
}
$port.Dispose()
$port = $null
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}

View file

@ -0,0 +1,20 @@
@echo off
cd /d "%~dp0.."
echo ==========================================
echo rs2322tcp Windows Client Test
echo ==========================================
echo.
echo Starte normalen rs2322tcp-Client...
echo COM100 wird vom Client verwendet.
echo Die konfigurierte Startsequenz wird nach
echo erfolgreichem Verbindungsaufbau automatisch gesendet.
echo.
echo Zum Beenden: Ctrl+C
echo.
go run .\cmd\rs2322tcp-client
echo.
echo Client beendet.
pause