Compare commits
14 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea8913fb69 | ||
|
|
27b97316fc | ||
|
|
8b7d081960 | ||
| da212e943b | |||
| 3a85b17422 | |||
|
|
c9e3ba052a | ||
| d890aba7b4 | |||
| 0eccd003f0 | |||
| f2428a2007 | |||
| 21d622e040 | |||
|
|
d9a70b9225 | ||
|
|
61bd03b863 | ||
| a6cd04ece4 | |||
| 39a1fea75d |
36 changed files with 3092 additions and 644 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
111
CHANGELOG.md
111
CHANGELOG.md
|
|
@ -2,6 +2,117 @@
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
|
||||||
|
- Linux-Portverwaltung nach der Windows-Client-Erweiterung korrigiert.
|
||||||
|
- Doppelte Linux-Definitionen der virtuellen Portverwaltung entfernt.
|
||||||
|
- Plattformübergreifende Trennung der virtuellen Port-Erzeugung korrigiert.
|
||||||
|
- Dauerhafte Installations-Symlinks für virtuelle Linux-Ports bleiben beim Schließen eines Ports erhalten.
|
||||||
|
- Gesamter Go-Testlauf unter Linux erfolgreich.
|
||||||
|
|
||||||
|
## [0.0.9] - 2026-08-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Windows-Unterstützung für den rs2322tcp-Client
|
||||||
|
- Plattformabhängige Verwaltung virtueller serieller Schnittstellen
|
||||||
|
- Windows-COM-Port-Unterstützung über `go.bug.st/serial`
|
||||||
|
- Unterstützung des virtuellen Windows-Portbereichs COM100 bis COM199
|
||||||
|
- Windows-spezifische Portvalidierung und Portpfadverwaltung
|
||||||
|
- Technischer Windows-Client unter `cmd/rs2322tcp-client-test`
|
||||||
|
- Plattformabhängige Implementierungen für das Öffnen und Verwalten
|
||||||
|
virtueller serieller Schnittstellen
|
||||||
|
- Windows-spezifische Implementierung von `VirtualSerial`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Virtuelle Portverwaltung für Linux und Windows auf eine gemeinsame
|
||||||
|
Schnittstelle umgestellt
|
||||||
|
- Clientseitige Portvalidierung für die unterschiedlichen Plattformen
|
||||||
|
erweitert
|
||||||
|
- Client-Konfiguration für den Windows-Betrieb angepasst
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Windows 11 / amd64 erfolgreich getestet
|
||||||
|
- Windows-COM-Port COM100 über com0com erfolgreich geöffnet
|
||||||
|
- com0com-Portpaar `COM10 ↔ COM100` erfolgreich getestet
|
||||||
|
- Verbindung des Windows-Clients über Tailscale zum Raspberry-Pi-Server
|
||||||
|
erfolgreich getestet
|
||||||
|
- Bidirektionale Datenübertragung zwischen Windows-Client und
|
||||||
|
Raspberry-Pi-Server erfolgreich getestet
|
||||||
|
- Bidirektionale Datenübertragung mit einem realen SPID Rot2Prog
|
||||||
|
erfolgreich getestet
|
||||||
|
- Rot2Prog mit `P5 = 59` (SPID) und Function Mode `A` (Auto) erfolgreich
|
||||||
|
betrieben
|
||||||
|
- Tatsächliche Antwort des Rot2Prog erfolgreich über den vollständigen
|
||||||
|
Datenweg übertragen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
57 07 01 06 00 01 03 06 02 00 01 20
|
||||||
|
```
|
||||||
|
|
||||||
|
- `go test ./internal/client` erfolgreich
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
Der Windows-Client kann über einen virtuellen Windows-COM-Port mit dem
|
||||||
|
rs2322tcp-Server auf einem Raspberry Pi kommunizieren.
|
||||||
|
|
||||||
|
Der vollständige Datenweg wurde mit einem realen SPID Rot2Prog
|
||||||
|
bidirektional erfolgreich getestet:
|
||||||
|
|
||||||
|
```text
|
||||||
|
COM10
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
com0com
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
COM100
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
rs2322tcp-client (Windows)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
TCP / Tailscale
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
rs2322tcp-server (Raspberry Pi)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
RS232 / SPID Rot2Prog
|
||||||
|
```
|
||||||
|
|
||||||
|
Der technische Windows-Client bleibt als separates Testprogramm erhalten.
|
||||||
|
|
||||||
|
Die grafische Fyne-Oberfläche des Windows-Clients ist noch nicht
|
||||||
|
vollständig abgeschlossen.
|
||||||
|
|
||||||
|
|
||||||
## [0.0.8] - 2026-08-12
|
## [0.0.8] - 2026-08-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
BIN
Icon.png
Normal file
BIN
Icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
243
README.md
243
README.md
|
|
@ -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
239
cmd/com0com-test/main.go
Normal 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
|
||||||
|
}
|
||||||
44
cmd/rs2322tcp-client-test/main.go
Normal file
44
cmd/rs2322tcp-client-test/main.go
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.lang-dieter.de/rs2322tcp/internal/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
const configFile = "./configs/client.json"
|
||||||
|
|
||||||
|
application, err := client.NewApplication(configFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "NewApplication: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := application.Start(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Application.Start: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("rs2322tcp technical client started")
|
||||||
|
fmt.Println("Press Ctrl+C to stop.")
|
||||||
|
|
||||||
|
signals := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(
|
||||||
|
signals,
|
||||||
|
os.Interrupt,
|
||||||
|
syscall.SIGTERM,
|
||||||
|
)
|
||||||
|
|
||||||
|
<-signals
|
||||||
|
|
||||||
|
fmt.Println("Stopping client...")
|
||||||
|
|
||||||
|
if err := application.Close(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Application.Close: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,11 +10,9 @@
|
||||||
"virtual_ports": [
|
"virtual_ports": [
|
||||||
{
|
{
|
||||||
"port": "/dev/ttyUSB100",
|
"port": "/dev/ttyUSB100",
|
||||||
"remote_device": "radio"
|
"remote_device": "rotor",
|
||||||
},
|
"startup_bytes": "57 00 00 00 00 00 00 00 00 00 00 1F 20"
|
||||||
{
|
|
||||||
"port": "/dev/ttyUSB101",
|
|
||||||
"remote_device": "rotor"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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": 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
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -11,66 +11,24 @@
|
||||||
* und dem zugehörigen entfernten Gerät.
|
* und dem zugehörigen entfernten Gerät.
|
||||||
*
|
*
|
||||||
* Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt
|
* Die konfigurierte lokale virtuelle Schnittstelle wird dabei exakt
|
||||||
* übernommen. Eine Konfiguration wie
|
* übernommen.
|
||||||
*
|
*
|
||||||
* /dev/ttyUSB100 -> radio
|
* Optional können nach dem Aufbau der Datenverbindung konfigurierte
|
||||||
*
|
* Startbytes an das entfernte Gerät gesendet werden.
|
||||||
* führt somit technisch auch zur Erzeugung beziehungsweise Verwendung von
|
|
||||||
* /dev/ttyUSB100 und nicht einfach zum nächsten freien Port.
|
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*/
|
*/
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Virtual port configuration
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// virtualPortNumber converts a configured /dev/ttyUSBxxx path into its
|
|
||||||
// numeric virtual port number.
|
|
||||||
//
|
|
||||||
// The client configuration stores the complete Linux device path, while
|
|
||||||
// VirtualPortManager works with the numeric port number. Only the exact
|
|
||||||
// /dev/ttyUSB<number> format is accepted here.
|
|
||||||
func virtualPortNumber(path string) (int, error) {
|
|
||||||
const prefix = "/dev/ttyUSB"
|
|
||||||
|
|
||||||
if !strings.HasPrefix(path, prefix) {
|
|
||||||
return 0, fmt.Errorf(
|
|
||||||
"invalid virtual port %q: expected /dev/ttyUSB<number>",
|
|
||||||
path,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
value := strings.TrimPrefix(path, prefix)
|
|
||||||
|
|
||||||
if value == "" {
|
|
||||||
return 0, fmt.Errorf(
|
|
||||||
"invalid virtual port %q: port number is missing",
|
|
||||||
path,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
number, err := strconv.Atoi(value)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf(
|
|
||||||
"invalid virtual port %q: invalid port number",
|
|
||||||
path,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return number, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
// Virtual port connection
|
// Virtual port connection
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -83,6 +41,11 @@ func virtualPortNumber(path string) (int, error) {
|
||||||
// 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,
|
||||||
|
|
@ -128,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
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ func TestConnectVirtualPortNilClient(t *testing.T) {
|
||||||
)
|
)
|
||||||
|
|
||||||
cfg := config.VirtualPortConfig{
|
cfg := config.VirtualPortConfig{
|
||||||
Port: "/dev/ttyUSB100",
|
Port: virtualPortPath(100),
|
||||||
RemoteDevice: "radio",
|
RemoteDevice: "radio",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,7 +172,7 @@ func TestConnectVirtualPortNilClient(t *testing.T) {
|
||||||
|
|
||||||
func TestConnectVirtualPortNilManager(t *testing.T) {
|
func TestConnectVirtualPortNilManager(t *testing.T) {
|
||||||
cfg := config.VirtualPortConfig{
|
cfg := config.VirtualPortConfig{
|
||||||
Port: "/dev/ttyUSB100",
|
Port: virtualPortPath(100),
|
||||||
RemoteDevice: "radio",
|
RemoteDevice: "radio",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,7 +209,7 @@ func TestConnectVirtualPortUnknownDevice(t *testing.T) {
|
||||||
client := newTestClient(t)
|
client := newTestClient(t)
|
||||||
|
|
||||||
cfg := config.VirtualPortConfig{
|
cfg := config.VirtualPortConfig{
|
||||||
Port: "/dev/ttyUSB100",
|
Port: virtualPortPath(100),
|
||||||
RemoteDevice: "unknown",
|
RemoteDevice: "unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,7 +249,7 @@ func TestConnectVirtualPortClosesPortWhenDataConnectionFails(t *testing.T) {
|
||||||
client := newTestClient(t)
|
client := newTestClient(t)
|
||||||
|
|
||||||
cfg := config.VirtualPortConfig{
|
cfg := config.VirtualPortConfig{
|
||||||
Port: "/dev/ttyUSB100",
|
Port: virtualPortPath(100),
|
||||||
RemoteDevice: "radio",
|
RemoteDevice: "radio",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,7 +323,7 @@ func TestConnectVirtualPortSuccess(t *testing.T) {
|
||||||
)
|
)
|
||||||
|
|
||||||
cfg := config.VirtualPortConfig{
|
cfg := config.VirtualPortConfig{
|
||||||
Port: "/dev/ttyUSB100",
|
Port: virtualPortPath(100),
|
||||||
RemoteDevice: "radio",
|
RemoteDevice: "radio",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,11 +348,11 @@ func TestConnectVirtualPortSuccess(t *testing.T) {
|
||||||
defer virtualPort.Close()
|
defer virtualPort.Close()
|
||||||
defer dataConn.Close()
|
defer dataConn.Close()
|
||||||
|
|
||||||
if virtualPort.Path() != "/dev/ttyUSB100" {
|
if virtualPort.Path() != virtualPortPath(100) {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"virtual port path = %q, want %q",
|
"virtual port path = %q, want %q",
|
||||||
virtualPort.Path(),
|
virtualPort.Path(),
|
||||||
"/dev/ttyUSB100",
|
virtualPortPath(100),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -386,7 +386,7 @@ func TestConnectVirtualPortUsesConfiguredPort(t *testing.T) {
|
||||||
)
|
)
|
||||||
|
|
||||||
cfg := config.VirtualPortConfig{
|
cfg := config.VirtualPortConfig{
|
||||||
Port: "/dev/ttyUSB150",
|
Port: virtualPortPath(100),
|
||||||
RemoteDevice: "radio",
|
RemoteDevice: "radio",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,24 +411,29 @@ func TestConnectVirtualPortUsesConfiguredPort(t *testing.T) {
|
||||||
defer virtualPort.Close()
|
defer virtualPort.Close()
|
||||||
defer dataConn.Close()
|
defer dataConn.Close()
|
||||||
|
|
||||||
if virtualPort.Path() != "/dev/ttyUSB150" {
|
if virtualPort.Path() != virtualPortPath(100) {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"virtual port path = %q, want %q",
|
"virtual port path = %q, want %q",
|
||||||
virtualPort.Path(),
|
virtualPort.Path(),
|
||||||
"/dev/ttyUSB150",
|
virtualPortPath(100),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Port 100 must still be available. This proves that the connection
|
// Port 100 is the configured port and therefore must be reserved.
|
||||||
// did not simply take the first free port from the configured range.
|
if !manager.used[100] {
|
||||||
|
t.Fatal("configured port 100 is not reserved")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Port 101 must still be available. This proves that the connection
|
||||||
|
// did not simply take another free port.
|
||||||
number, err := manager.Reserve()
|
number, err := manager.Reserve()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Reserve() after connection: %v", err)
|
t.Fatalf("Reserve() after connection: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if number != 100 {
|
if number != 101 {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"first free port after connection = %d, want 100",
|
"first free port after connection = %d, want 101",
|
||||||
number,
|
number,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -463,7 +468,7 @@ func TestConnectBridgeSuccess(t *testing.T) {
|
||||||
)
|
)
|
||||||
|
|
||||||
cfg := config.VirtualPortConfig{
|
cfg := config.VirtualPortConfig{
|
||||||
Port: "/dev/ttyUSB100",
|
Port: virtualPortPath(100),
|
||||||
RemoteDevice: "radio",
|
RemoteDevice: "radio",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,11 +505,11 @@ func TestConnectBridgeSuccess(t *testing.T) {
|
||||||
t.Fatal("bridge TCP endpoint does not match data connection")
|
t.Fatal("bridge TCP endpoint does not match data connection")
|
||||||
}
|
}
|
||||||
|
|
||||||
if virtualPort.Path() != "/dev/ttyUSB100" {
|
if virtualPort.Path() != virtualPortPath(100) {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"virtual port path = %q, want %q",
|
"virtual port path = %q, want %q",
|
||||||
virtualPort.Path(),
|
virtualPort.Path(),
|
||||||
"/dev/ttyUSB100",
|
virtualPortPath(100),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,13 @@
|
||||||
* Link unter ~/.rs2322tcp/virtual/.
|
* Link unter ~/.rs2322tcp/virtual/.
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"git.lang-dieter.de/rs2322tcp/internal/config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -77,6 +76,9 @@ func ensureVirtualPortDirectory() error {
|
||||||
//
|
//
|
||||||
// The link itself is located below ~/.rs2322tcp/virtual/ and points to the
|
// The link itself is located below ~/.rs2322tcp/virtual/ and points to the
|
||||||
// current PTY, for example /dev/pts/2.
|
// current PTY, for example /dev/pts/2.
|
||||||
|
//
|
||||||
|
// The public /dev/ttyUSBxxx installation link is deliberately never
|
||||||
|
// modified here.
|
||||||
func setVirtualPortLink(portPath string, target string) error {
|
func setVirtualPortLink(portPath string, target string) error {
|
||||||
if portPath == "" {
|
if portPath == "" {
|
||||||
return fmt.Errorf("virtual port path is empty")
|
return fmt.Errorf("virtual port path is empty")
|
||||||
|
|
@ -95,8 +97,10 @@ func setVirtualPortLink(portPath string, target string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove an existing link. The installation-owned outer /dev/ttyUSBxxx
|
// Replace only the user-owned internal link.
|
||||||
// link is deliberately never touched here.
|
//
|
||||||
|
// The installation-owned /dev/ttyUSBxxx link is deliberately never
|
||||||
|
// touched here.
|
||||||
if err := os.Remove(linkPath); err != nil && !os.IsNotExist(err) {
|
if err := os.Remove(linkPath); err != nil && !os.IsNotExist(err) {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"remove existing virtual port link %q: %w",
|
"remove existing virtual port link %q: %w",
|
||||||
|
|
@ -117,9 +121,12 @@ func setVirtualPortLink(portPath string, target string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeVirtualPortLink removes the internal link for a virtual port.
|
// removeVirtualPortLink removes the user-owned internal link for a virtual
|
||||||
|
// port.
|
||||||
//
|
//
|
||||||
// The public /dev/ttyUSBxxx link is never modified.
|
// This function is retained for explicit link-management operations and
|
||||||
|
// tests. It is NOT called by ManagedVirtualPort.Close(), because the
|
||||||
|
// installation-owned symlink chain must remain available.
|
||||||
func removeVirtualPortLink(portPath string) error {
|
func removeVirtualPortLink(portPath string) error {
|
||||||
if portPath == "" {
|
if portPath == "" {
|
||||||
return fmt.Errorf("virtual port path is empty")
|
return fmt.Errorf("virtual port path is empty")
|
||||||
|
|
@ -140,73 +147,3 @@ func removeVirtualPortLink(portPath string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// existingVirtualPortLinks returns all public virtual-port paths that
|
|
||||||
// currently exist as symbolic links.
|
|
||||||
//
|
|
||||||
// Only ports inside the configured virtual-port range are considered.
|
|
||||||
// The public /dev/ttyUSBxxx links are only inspected; they are never
|
|
||||||
// created, changed, or removed by this function.
|
|
||||||
//
|
|
||||||
// This check only requires read access and therefore does not require
|
|
||||||
// root privileges.
|
|
||||||
func existingVirtualPortLinks(
|
|
||||||
cfg config.VirtualPortRangeConfig,
|
|
||||||
) ([]string, error) {
|
|
||||||
return existingVirtualPortLinksInDirectory(
|
|
||||||
cfg,
|
|
||||||
"/dev",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// existingVirtualPortLinksInDirectory returns all virtual-port paths in
|
|
||||||
// directory that currently exist as symbolic links.
|
|
||||||
//
|
|
||||||
// This helper is deliberately separated from existingVirtualPortLinks so
|
|
||||||
// the filesystem inspection can be tested without modifying /dev.
|
|
||||||
func existingVirtualPortLinksInDirectory(
|
|
||||||
cfg config.VirtualPortRangeConfig,
|
|
||||||
directory string,
|
|
||||||
) ([]string, error) {
|
|
||||||
if directory == "" {
|
|
||||||
return nil, fmt.Errorf("virtual port directory is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
manager := NewVirtualPortManager(cfg)
|
|
||||||
if manager == nil {
|
|
||||||
return nil, fmt.Errorf("virtual port manager is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
links := make([]string, 0)
|
|
||||||
|
|
||||||
for number := cfg.First; number <= cfg.Last; number++ {
|
|
||||||
portName := filepath.Base(
|
|
||||||
manager.PortPath(number),
|
|
||||||
)
|
|
||||||
portPath := filepath.Join(directory, portName)
|
|
||||||
|
|
||||||
info, err := os.Lstat(portPath)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"inspect virtual port %q: %w",
|
|
||||||
portPath,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Mode()&os.ModeSymlink == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
links = append(
|
|
||||||
links,
|
|
||||||
filepath.Join(directory, portName),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return links, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -11,19 +11,21 @@
|
||||||
* seriellen Ports.
|
* seriellen Ports.
|
||||||
*
|
*
|
||||||
* Der VirtualPortManager verwaltet den für rs2322tcp vorgesehenen Bereich
|
* Der VirtualPortManager verwaltet den für rs2322tcp vorgesehenen Bereich
|
||||||
* von /dev/ttyUSBxxx-Schnittstellen.
|
* virtueller serieller Schnittstellen.
|
||||||
|
*
|
||||||
|
* Die konkrete Darstellung eines virtuellen Ports ist plattformabhängig.
|
||||||
|
* Unter Linux werden beispielsweise /dev/ttyUSBxxx-Schnittstellen verwendet,
|
||||||
|
* während unter Windows COMxxx-Schnittstellen verwendet werden.
|
||||||
|
*
|
||||||
|
* Die eigentliche Erzeugung beziehungsweise Bereitstellung des seriellen
|
||||||
|
* Ports erfolgt ebenfalls plattformabhängig und ist nicht Bestandteil dieses
|
||||||
|
* Managers.
|
||||||
*
|
*
|
||||||
* Ports können entweder automatisch über den nächsten freien Port oder
|
* Ports können entweder automatisch über den nächsten freien Port oder
|
||||||
* gezielt über eine vorgegebene Portnummer reserviert werden.
|
* gezielt über eine vorgegebene Portnummer reserviert werden.
|
||||||
*
|
|
||||||
* Die gezielte Reservierung ist insbesondere für die aus client.json
|
|
||||||
* geladene Konfiguration erforderlich. Dadurch bleibt eine Zuordnung wie
|
|
||||||
*
|
|
||||||
* /dev/ttyUSB100 -> radio
|
|
||||||
*
|
|
||||||
* auch technisch exakt erhalten.
|
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -37,12 +39,15 @@ import (
|
||||||
// VirtualPortManager
|
// VirtualPortManager
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// VirtualPortManager verwaltet den reservierten Bereich virtueller
|
// VirtualPortManager verwaltet den für rs2322tcp reservierten Bereich
|
||||||
// /dev/ttyUSBxxx-Schnittstellen.
|
// virtueller serieller Schnittstellen.
|
||||||
//
|
//
|
||||||
// Der reservierte Portbereich und die Belegungstabelle werden durch einen
|
// Der reservierte Portbereich und die Belegungstabelle werden durch einen
|
||||||
// Mutex geschützt, da mehrere Bridges beziehungsweise Goroutinen gleichzeitig
|
// Mutex geschützt, da mehrere Bridges beziehungsweise Goroutinen gleichzeitig
|
||||||
// virtuelle Ports öffnen und freigeben können.
|
// virtuelle Ports öffnen und freigeben können.
|
||||||
|
//
|
||||||
|
// Die konkrete Darstellung des sichtbaren Portnamens wird über
|
||||||
|
// virtualPortPath() von der jeweiligen Plattform bestimmt.
|
||||||
type VirtualPortManager struct {
|
type VirtualPortManager struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
|
|
@ -51,6 +56,10 @@ type VirtualPortManager struct {
|
||||||
used map[int]bool
|
used map[int]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Constructor
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// NewVirtualPortManager erzeugt einen VirtualPortManager aus dem
|
// NewVirtualPortManager erzeugt einen VirtualPortManager aus dem
|
||||||
// konfigurierten Portbereich.
|
// konfigurierten Portbereich.
|
||||||
func NewVirtualPortManager(
|
func NewVirtualPortManager(
|
||||||
|
|
@ -63,16 +72,32 @@ func NewVirtualPortManager(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Port path
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// PortPath erzeugt den für die externe Software sichtbaren Gerätenamen
|
// PortPath erzeugt den für die externe Software sichtbaren Gerätenamen
|
||||||
// für eine Portnummer.
|
// für eine Portnummer.
|
||||||
|
//
|
||||||
|
// Die konkrete Darstellung ist plattformabhängig:
|
||||||
|
//
|
||||||
|
// Linux /dev/ttyUSB100
|
||||||
|
// Windows COM100
|
||||||
|
//
|
||||||
|
// Die Plattformimplementierung befindet sich in
|
||||||
|
// virtual_port_path_linux.go beziehungsweise virtual_port_path_windows.go.
|
||||||
func (m *VirtualPortManager) PortPath(number int) string {
|
func (m *VirtualPortManager) PortPath(number int) string {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("/dev/ttyUSB%d", number)
|
return virtualPortPath(number)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Reservation
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// Reserve reserviert den nächsten freien virtuellen Port.
|
// Reserve reserviert den nächsten freien virtuellen Port.
|
||||||
//
|
//
|
||||||
// Es wird immer mit dem kleinsten freien Port im konfigurierten Bereich
|
// Es wird immer mit dem kleinsten freien Port im konfigurierten Bereich
|
||||||
|
|
@ -141,6 +166,10 @@ func (m *VirtualPortManager) ReserveSpecific(number int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Release
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// Release gibt einen zuvor reservierten Port wieder frei.
|
// Release gibt einen zuvor reservierten Port wieder frei.
|
||||||
func (m *VirtualPortManager) Release(number int) {
|
func (m *VirtualPortManager) Release(number int) {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
|
|
|
||||||
|
|
@ -9,186 +9,39 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* Beschreibung:
|
* Beschreibung:
|
||||||
* Linux-spezifische Erzeugung und Verwaltung virtueller rs2322tcp-Ports.
|
* Linux-spezifische Erzeugung eines virtuellen rs2322tcp-Ports.
|
||||||
*
|
*
|
||||||
* Der öffentliche /dev/ttyUSBxxx-Eintrag wird bei der Installation
|
* Die gemeinsame Verwaltung von ManagedVirtualPort und VirtualPortManager
|
||||||
* bereitgestellt. Der Client verwaltet ausschließlich den internen Link
|
* befindet sich in virtual_port_manager_open.go.
|
||||||
* unter ~/.rs2322tcp/virtual/.
|
|
||||||
*
|
*
|
||||||
* Virtuelle Ports können entweder automatisch über den nächsten freien Port
|
* Unter Linux wird der PTY über newVirtualSerial() erzeugt und der interne
|
||||||
* oder gezielt über eine vorgegebene Portnummer geöffnet werden.
|
* Link unter ~/.rs2322tcp/virtual/ auf diesen PTY gesetzt.
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
// openReserved creates the actual Linux virtual serial port for an already
|
||||||
// Managed virtual port
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// ManagedVirtualPort verbindet einen reservierten Port mit einer
|
|
||||||
// VirtualSerial-Instanz und verwaltet deren Lebenszyklus.
|
|
||||||
//
|
|
||||||
// Der Mutex schützt den internen Port-Pointer gegen gleichzeitige Zugriffe
|
|
||||||
// aus Read, Write und Close.
|
|
||||||
type ManagedVirtualPort struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
|
|
||||||
port *VirtualPort
|
|
||||||
manager *VirtualPortManager
|
|
||||||
number int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path returns the device path visible to the external application.
|
|
||||||
func (p *ManagedVirtualPort) Path() string {
|
|
||||||
if p == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
if p.port == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.port.Path()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads data from the underlying virtual serial device.
|
|
||||||
func (p *ManagedVirtualPort) Read(b []byte) (int, error) {
|
|
||||||
if p == nil {
|
|
||||||
return 0, fmt.Errorf("managed virtual port is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
port := p.port
|
|
||||||
p.mu.Unlock()
|
|
||||||
|
|
||||||
if port == nil {
|
|
||||||
return 0, fmt.Errorf("managed virtual port is closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return port.Read(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes data to the underlying virtual serial device.
|
|
||||||
func (p *ManagedVirtualPort) Write(b []byte) (int, error) {
|
|
||||||
if p == nil {
|
|
||||||
return 0, fmt.Errorf("managed virtual port is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
port := p.port
|
|
||||||
p.mu.Unlock()
|
|
||||||
|
|
||||||
if port == nil {
|
|
||||||
return 0, fmt.Errorf("managed virtual port is closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return port.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close removes the internal link, closes the PTY and releases the
|
|
||||||
// reservation.
|
|
||||||
//
|
|
||||||
// The managed port is detached from the object before the underlying PTY
|
|
||||||
// is closed. This prevents a concurrent Read or Write from accessing the
|
|
||||||
// managed port pointer after Close has taken ownership of it.
|
|
||||||
func (p *ManagedVirtualPort) Close() error {
|
|
||||||
if p == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
|
|
||||||
port := p.port
|
|
||||||
p.port = nil
|
|
||||||
|
|
||||||
manager := p.manager
|
|
||||||
number := p.number
|
|
||||||
|
|
||||||
p.mu.Unlock()
|
|
||||||
|
|
||||||
if port == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstErr error
|
|
||||||
|
|
||||||
if err := removeVirtualPortLink(port.Path()); err != nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := port.Close(); err != nil && firstErr == nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
if manager != nil {
|
|
||||||
manager.Release(number)
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstErr
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Open
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// Open creates a new virtual serial port using the next free port number.
|
|
||||||
//
|
|
||||||
// The internal PTY is created by newVirtualSerial(). The internal
|
|
||||||
// ~/.rs2322tcp/virtual/ttyUSBxxx link is then pointed at the PTY.
|
|
||||||
//
|
|
||||||
// Open retains the automatic allocation behavior used by the existing
|
|
||||||
// client code.
|
|
||||||
func (m *VirtualPortManager) Open() (*ManagedVirtualPort, error) {
|
|
||||||
if m == nil {
|
|
||||||
return nil, fmt.Errorf("virtual port manager is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
number, err := m.Reserve()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.openReserved(number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenSpecific creates a new virtual serial port using exactly the specified
|
|
||||||
// port number.
|
|
||||||
//
|
|
||||||
// Unlike Open, this method does not select the next free port. The requested
|
|
||||||
// number is reserved explicitly and is therefore suitable for the
|
|
||||||
// VirtualPortConfig.Port value loaded from client.json.
|
|
||||||
//
|
|
||||||
// If creation of the PTY or the internal link fails, the reservation is
|
|
||||||
// released again.
|
|
||||||
func (m *VirtualPortManager) OpenSpecific(number int) (*ManagedVirtualPort, error) {
|
|
||||||
if m == nil {
|
|
||||||
return nil, fmt.Errorf("virtual port manager is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.ReserveSpecific(number); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.openReserved(number)
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Internal open helper
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// openReserved creates the actual virtual serial port for an already
|
|
||||||
// reserved port number.
|
// reserved port number.
|
||||||
//
|
//
|
||||||
// The caller must reserve the port before calling this method.
|
// The caller must reserve the port before calling this method.
|
||||||
func (m *VirtualPortManager) openReserved(number int) (*ManagedVirtualPort, error) {
|
//
|
||||||
|
// The public /dev/ttyUSBxxx link is not modified here. The installation-owned
|
||||||
|
// link remains untouched. Only the user-owned internal link below
|
||||||
|
// ~/.rs2322tcp/virtual/ is created or updated to point to the newly created
|
||||||
|
// PTY.
|
||||||
|
func openReserved(
|
||||||
|
m *VirtualPortManager,
|
||||||
|
number int,
|
||||||
|
) (*ManagedVirtualPort, error) {
|
||||||
|
if m == nil {
|
||||||
|
return nil, fmt.Errorf("virtual port manager is nil")
|
||||||
|
}
|
||||||
|
|
||||||
portPath := m.PortPath(number)
|
portPath := m.PortPath(number)
|
||||||
|
|
||||||
serial, err := newVirtualSerial()
|
serial, err := newVirtualSerial()
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* Beschreibung:
|
* Beschreibung:
|
||||||
* Linux-spezifische Tests für die Erzeugung virtueller rs2322tcp-Ports.
|
* Linux-spezifische Tests für die Erzeugung und Verwaltung virtueller
|
||||||
|
* rs2322tcp-Ports.
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*/
|
*/
|
||||||
package client
|
package client
|
||||||
|
|
@ -71,10 +72,20 @@ func TestVirtualPortManagerOpen(t *testing.T) {
|
||||||
t.Fatalf("Close() failed: %v", err)
|
t.Fatalf("Close() failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Lstat(linkPath); !os.IsNotExist(err) {
|
targetAfterClose, err := os.Readlink(linkPath)
|
||||||
|
if err != nil {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"internal link still exists after Close(): %q",
|
"internal link does not exist after Close(): %q: %v",
|
||||||
linkPath,
|
linkPath,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetAfterClose != target {
|
||||||
|
t.Errorf(
|
||||||
|
"internal link target after Close() = %q, want %q",
|
||||||
|
targetAfterClose,
|
||||||
|
target,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -125,13 +136,24 @@ func TestVirtualPortManagerOpenSpecific(t *testing.T) {
|
||||||
t.Fatalf("Close() failed: %v", err)
|
t.Fatalf("Close() failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Lstat(linkPath); !os.IsNotExist(err) {
|
targetAfterClose, err := os.Readlink(linkPath)
|
||||||
|
if err != nil {
|
||||||
t.Fatalf(
|
t.Fatalf(
|
||||||
"internal link still exists after Close(): %q",
|
"internal link does not exist after Close(): %q: %v",
|
||||||
linkPath,
|
linkPath,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetAfterClose != target {
|
||||||
|
t.Errorf(
|
||||||
|
"internal link target after Close() = %q, want %q",
|
||||||
|
targetAfterClose,
|
||||||
|
target,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVirtualPortManagerOpenUsesNextPort(t *testing.T) {
|
func TestVirtualPortManagerOpenUsesNextPort(t *testing.T) {
|
||||||
tempHome := t.TempDir()
|
tempHome := t.TempDir()
|
||||||
t.Setenv("HOME", tempHome)
|
t.Setenv("HOME", tempHome)
|
||||||
|
|
|
||||||
163
internal/client/virtual_port_manager_open.go
Normal file
163
internal/client/virtual_port_manager_open.go
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* Projekt.....: rs2322tcp
|
||||||
|
* Datei.......: internal/client/virtual_port_manager_open.go
|
||||||
|
* Copyright (C) 2026 Dieter Lang
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* Beschreibung:
|
||||||
|
* Plattformneutrale Verwaltung geöffneter virtueller Ports.
|
||||||
|
*
|
||||||
|
* Die konkrete Erzeugung eines virtuellen seriellen Ports erfolgt über
|
||||||
|
* openVirtualPort() in der jeweiligen Plattformimplementierung.
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Managed virtual port
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// ManagedVirtualPort verbindet einen reservierten Port mit einer
|
||||||
|
// VirtualSerial-Instanz und verwaltet deren Lebenszyklus.
|
||||||
|
type ManagedVirtualPort struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
port *VirtualPort
|
||||||
|
manager *VirtualPortManager
|
||||||
|
number int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path returns the device path visible to the external application.
|
||||||
|
func (p *ManagedVirtualPort) Path() string {
|
||||||
|
if p == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if p.port == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.port.Path()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads data from the underlying virtual serial device.
|
||||||
|
func (p *ManagedVirtualPort) Read(b []byte) (int, error) {
|
||||||
|
if p == nil {
|
||||||
|
return 0, fmt.Errorf("managed virtual port is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
port := p.port
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
if port == nil {
|
||||||
|
return 0, fmt.Errorf("managed virtual port is closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return port.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes data to the underlying virtual serial device.
|
||||||
|
func (p *ManagedVirtualPort) Write(b []byte) (int, error) {
|
||||||
|
if p == nil {
|
||||||
|
return 0, fmt.Errorf("managed virtual port is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
port := p.port
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
if port == nil {
|
||||||
|
return 0, fmt.Errorf("managed virtual port is closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return port.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the managed virtual port and releases its reservation.
|
||||||
|
func (p *ManagedVirtualPort) Close() error {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
|
||||||
|
port := p.port
|
||||||
|
p.port = nil
|
||||||
|
|
||||||
|
manager := p.manager
|
||||||
|
number := p.number
|
||||||
|
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
if port == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := port.Close()
|
||||||
|
|
||||||
|
if manager != nil {
|
||||||
|
manager.Release(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Open
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Open creates a new virtual serial port using the next free port number.
|
||||||
|
func (m *VirtualPortManager) Open() (*ManagedVirtualPort, error) {
|
||||||
|
if m == nil {
|
||||||
|
return nil, fmt.Errorf("virtual port manager is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
number, err := m.Reserve()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := openVirtualPort(m, number)
|
||||||
|
if err != nil {
|
||||||
|
m.Release(number)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenSpecific creates a new virtual serial port using exactly the specified
|
||||||
|
// port number.
|
||||||
|
func (m *VirtualPortManager) OpenSpecific(
|
||||||
|
number int,
|
||||||
|
) (*ManagedVirtualPort, error) {
|
||||||
|
if m == nil {
|
||||||
|
return nil, fmt.Errorf("virtual port manager is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.ReserveSpecific(number); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := openVirtualPort(m, number)
|
||||||
|
if err != nil {
|
||||||
|
m.Release(number)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
|
@ -107,8 +107,10 @@ func TestVirtualPortManagerRelease(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if reused != 100 {
|
if reused != 100 {
|
||||||
t.Errorf("Reserve() after Release() = %d, want 100",
|
t.Errorf(
|
||||||
reused)
|
"Reserve() after Release() = %d, want 100",
|
||||||
|
reused,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,15 +181,15 @@ func TestVirtualPortManagerPortPath(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
number: 100,
|
number: 100,
|
||||||
want: "/dev/ttyUSB100",
|
want: virtualPortPath(100),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: 123,
|
number: 123,
|
||||||
want: "/dev/ttyUSB123",
|
want: virtualPortPath(123),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: 199,
|
number: 199,
|
||||||
want: "/dev/ttyUSB199",
|
want: virtualPortPath(199),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
59
internal/client/virtual_port_number_linux.go
Normal file
59
internal/client/virtual_port_number_linux.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* Projekt.....: rs2322tcp
|
||||||
|
* Datei.......: internal/client/virtual_port_number_linux.go
|
||||||
|
* Copyright (C) 2026 Dieter Lang
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* Beschreibung:
|
||||||
|
* Linux-spezifische Umwandlung eines konfigurierten virtuellen
|
||||||
|
* seriellen Ports in dessen numerische Portnummer.
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Virtual port number
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// virtualPortNumber converts a configured /dev/ttyUSBxxx path into its
|
||||||
|
// numeric virtual port number.
|
||||||
|
func virtualPortNumber(path string) (int, error) {
|
||||||
|
const prefix = "/dev/ttyUSB"
|
||||||
|
|
||||||
|
if !strings.HasPrefix(path, prefix) {
|
||||||
|
return 0, fmt.Errorf(
|
||||||
|
"invalid virtual port %q: expected /dev/ttyUSB<number>",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
value := strings.TrimPrefix(path, prefix)
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
return 0, fmt.Errorf(
|
||||||
|
"invalid virtual port %q: port number is missing",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
number, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf(
|
||||||
|
"invalid virtual port %q: invalid port number",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return number, nil
|
||||||
|
}
|
||||||
62
internal/client/virtual_port_number_windows.go
Normal file
62
internal/client/virtual_port_number_windows.go
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* Projekt.....: rs2322tcp
|
||||||
|
* Datei.......: internal/client/virtual_port_number_windows.go
|
||||||
|
* Copyright (C) 2026 Dieter Lang
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* Beschreibung:
|
||||||
|
* Windows-spezifische Umwandlung eines konfigurierten virtuellen
|
||||||
|
* seriellen COM-Ports in dessen numerische Portnummer.
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Virtual port number
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// virtualPortNumber converts a configured COMxxx path into its numeric
|
||||||
|
// virtual port number.
|
||||||
|
func virtualPortNumber(path string) (int, error) {
|
||||||
|
const prefix = "COM"
|
||||||
|
|
||||||
|
if !strings.HasPrefix(
|
||||||
|
strings.ToUpper(path),
|
||||||
|
prefix,
|
||||||
|
) {
|
||||||
|
return 0, fmt.Errorf(
|
||||||
|
"invalid virtual port %q: expected COM<number>",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
value := path[len(prefix):]
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
return 0, fmt.Errorf(
|
||||||
|
"invalid virtual port %q: port number is missing",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
number, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf(
|
||||||
|
"invalid virtual port %q: invalid port number",
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return number, nil
|
||||||
|
}
|
||||||
77
internal/client/virtual_port_open_linux.go
Normal file
77
internal/client/virtual_port_open_linux.go
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* Projekt.....: rs2322tcp
|
||||||
|
* Datei.......: internal/client/virtual_port_open_linux.go
|
||||||
|
* Copyright (C) 2026 Dieter Lang
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* Beschreibung:
|
||||||
|
* Linux-spezifische Erzeugung eines verwalteten virtuellen rs2322tcp-Ports.
|
||||||
|
*
|
||||||
|
* Die virtuelle serielle Schnittstelle wird über ein PTY-Paar erzeugt.
|
||||||
|
* Der öffentliche /dev/ttyUSBxxx-Eintrag wird über den bereits vorhandenen
|
||||||
|
* internen Link auf den PTY-Slave abgebildet.
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Open virtual port
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// openVirtualPort creates and initializes a Linux virtual serial port.
|
||||||
|
//
|
||||||
|
// The port number must already have been reserved by the
|
||||||
|
// VirtualPortManager.
|
||||||
|
//
|
||||||
|
// On failure, the caller remains responsible for releasing the reservation.
|
||||||
|
func openVirtualPort(
|
||||||
|
manager *VirtualPortManager,
|
||||||
|
number int,
|
||||||
|
) (*ManagedVirtualPort, error) {
|
||||||
|
if manager == nil {
|
||||||
|
return nil, fmt.Errorf("virtual port manager is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
portPath := manager.PortPath(number)
|
||||||
|
|
||||||
|
serial, err := newVirtualSerial()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"create virtual serial for %s: %w",
|
||||||
|
portPath,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := setVirtualPortLink(
|
||||||
|
portPath,
|
||||||
|
serial.Path(),
|
||||||
|
); err != nil {
|
||||||
|
_ = serial.Close()
|
||||||
|
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"bind %s to %s: %w",
|
||||||
|
portPath,
|
||||||
|
serial.Path(),
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
port := NewVirtualPort(
|
||||||
|
portPath,
|
||||||
|
serial,
|
||||||
|
)
|
||||||
|
|
||||||
|
return &ManagedVirtualPort{
|
||||||
|
port: port,
|
||||||
|
manager: manager,
|
||||||
|
number: number,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
87
internal/client/virtual_port_open_windows.go
Normal file
87
internal/client/virtual_port_open_windows.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* Projekt.....: rs2322tcp
|
||||||
|
* Datei.......: internal/client/virtual_port_open_windows.go
|
||||||
|
* Copyright (C) 2026 Dieter Lang
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* Beschreibung:
|
||||||
|
* Windows-spezifische Erzeugung eines verwalteten virtuellen rs2322tcp-Ports.
|
||||||
|
*
|
||||||
|
* Der konfigurierte virtuelle COM-Port wird einmal geöffnet und anschließend
|
||||||
|
* über dasselbe Handle für Lese- und Schreiboperationen verwendet.
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
bugserial "go.bug.st/serial"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Open virtual port
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// openVirtualPort creates and initializes a Windows virtual serial port.
|
||||||
|
//
|
||||||
|
// The port number must already have been reserved by the
|
||||||
|
// VirtualPortManager.
|
||||||
|
//
|
||||||
|
// The visible COM port is provided by the platform-specific PortPath()
|
||||||
|
// implementation. The actual COM-port pair configuration is managed
|
||||||
|
// 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(
|
||||||
|
manager *VirtualPortManager,
|
||||||
|
number int,
|
||||||
|
) (*ManagedVirtualPort, error) {
|
||||||
|
if manager == nil {
|
||||||
|
return nil, fmt.Errorf("virtual port manager is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
portPath := manager.PortPath(number)
|
||||||
|
if portPath == "" {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"virtual port path is empty for port %d",
|
||||||
|
number,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := &bugserial.Mode{
|
||||||
|
BaudRate: 9600,
|
||||||
|
DataBits: 8,
|
||||||
|
Parity: bugserial.NoParity,
|
||||||
|
StopBits: bugserial.OneStopBit,
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := bugserial.Open(portPath, mode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"open virtual serial port %s: %w",
|
||||||
|
portPath,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
virtualPort := NewVirtualPort(
|
||||||
|
portPath,
|
||||||
|
&windowsSerial{
|
||||||
|
port: port,
|
||||||
|
path: portPath,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return &ManagedVirtualPort{
|
||||||
|
port: virtualPort,
|
||||||
|
manager: manager,
|
||||||
|
number: number,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
43
internal/client/virtual_port_path_linux.go
Normal file
43
internal/client/virtual_port_path_linux.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* Projekt.....: rs2322tcp
|
||||||
|
* Datei.......: internal/client/virtual_port_path_linux.go
|
||||||
|
* Copyright (C) 2026 Dieter Lang
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* Beschreibung:
|
||||||
|
* Linux-spezifische Darstellung eines für rs2322tcp reservierten
|
||||||
|
* virtuellen seriellen Ports.
|
||||||
|
*
|
||||||
|
* Unter Linux werden die für rs2322tcp sichtbaren virtuellen Ports als
|
||||||
|
*
|
||||||
|
* /dev/ttyUSBxxx
|
||||||
|
*
|
||||||
|
* dargestellt.
|
||||||
|
*
|
||||||
|
* Die eigentliche Bereitstellung des virtuellen seriellen Ports erfolgt
|
||||||
|
* unabhängig davon in der Linux-spezifischen VirtualSerial-Implementierung.
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Virtual port path
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// virtualPortPath erzeugt den unter Linux sichtbaren Gerätenamen für eine
|
||||||
|
// virtuelle rs2322tcp-Schnittstelle.
|
||||||
|
//
|
||||||
|
// Beispiel:
|
||||||
|
//
|
||||||
|
// number = 100
|
||||||
|
// result = /dev/ttyUSB100
|
||||||
|
func virtualPortPath(number int) string {
|
||||||
|
return fmt.Sprintf("/dev/ttyUSB%d", number)
|
||||||
|
}
|
||||||
48
internal/client/virtual_port_path_windows.go
Normal file
48
internal/client/virtual_port_path_windows.go
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* Projekt.....: rs2322tcp
|
||||||
|
* Datei.......: internal/client/virtual_port_path_windows.go
|
||||||
|
* Copyright (C) 2026 Dieter Lang
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* Beschreibung:
|
||||||
|
* Windows-spezifische Darstellung eines für rs2322tcp reservierten
|
||||||
|
* virtuellen seriellen Ports.
|
||||||
|
*
|
||||||
|
* Unter Windows werden die für rs2322tcp sichtbaren virtuellen Ports als
|
||||||
|
*
|
||||||
|
* COMxxx
|
||||||
|
*
|
||||||
|
* dargestellt.
|
||||||
|
*
|
||||||
|
* Die eigentliche Bereitstellung des virtuellen COM-Ports erfolgt durch die
|
||||||
|
* Windows-spezifische serielle Infrastruktur beziehungsweise später durch
|
||||||
|
* den für rs2322tcp vorgesehenen virtuellen COM-Port-Treiber.
|
||||||
|
*
|
||||||
|
* Diese Datei enthält ausschließlich die plattformabhängige Darstellung des
|
||||||
|
* Portnamens. Die gemeinsame Portverwaltung bleibt in
|
||||||
|
* virtual_port_manager.go erhalten.
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Virtual port path
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// virtualPortPath erzeugt den unter Windows sichtbaren Gerätenamen für eine
|
||||||
|
// virtuelle rs2322tcp-Schnittstelle.
|
||||||
|
//
|
||||||
|
// Beispiel:
|
||||||
|
//
|
||||||
|
// number = 10
|
||||||
|
// result = COM10
|
||||||
|
func virtualPortPath(number int) string {
|
||||||
|
return fmt.Sprintf("COM%d", number)
|
||||||
|
}
|
||||||
|
|
@ -7,13 +7,22 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* Beschreibung:
|
* Beschreibung:
|
||||||
* Vergleich der in client.json definierten virtuellen Ports mit den
|
* Plattformneutrale Prüfung der in client.json definierten virtuellen Ports
|
||||||
* tatsächlich vorhandenen virtuellen Schnittstellen.
|
* gegen die tatsächlich vorhandenen virtuellen Schnittstellen.
|
||||||
|
*
|
||||||
|
* Die Ermittlung der vorhandenen Ports erfolgt über die
|
||||||
|
* plattformabhängige Funktion existingVirtualPortLinks().
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*/
|
*/
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import "sort"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
// Virtual port validation
|
// Virtual port validation
|
||||||
|
|
@ -62,10 +71,7 @@ func compareVirtualPorts(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Missing = append(
|
result.Missing = append(result.Missing, port)
|
||||||
result.Missing,
|
|
||||||
port,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for port := range availableSet {
|
for port := range availableSet {
|
||||||
|
|
@ -73,10 +79,7 @@ func compareVirtualPorts(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Extra = append(
|
result.Extra = append(result.Extra, port)
|
||||||
result.Extra,
|
|
||||||
port,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(result.Missing)
|
sort.Strings(result.Missing)
|
||||||
|
|
@ -84,3 +87,166 @@ func compareVirtualPorts(
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Local virtual-port validation
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// validateLocalVirtualPorts compares the virtual ports defined in
|
||||||
|
// client.json with the virtual ports currently available on the local
|
||||||
|
// computer.
|
||||||
|
//
|
||||||
|
// The detection of available ports is performed by the platform-specific
|
||||||
|
// existingVirtualPortLinks() implementation.
|
||||||
|
//
|
||||||
|
// A mismatch prevents the client from starting.
|
||||||
|
func validateLocalVirtualPorts(
|
||||||
|
cfg *config.ClientConfig,
|
||||||
|
) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return fmt.Errorf("client configuration is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
configured := make(
|
||||||
|
[]string,
|
||||||
|
0,
|
||||||
|
len(cfg.VirtualPorts),
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, virtualPort := range cfg.VirtualPorts {
|
||||||
|
configured = append(
|
||||||
|
configured,
|
||||||
|
virtualPort.Port,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
available, err := existingVirtualPortLinks(
|
||||||
|
cfg.VirtualPortRange,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"prüfen der virtuellen Ports fehlgeschlagen: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := compareVirtualPorts(
|
||||||
|
configured,
|
||||||
|
available,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(result.Missing) == 0 &&
|
||||||
|
len(result.Extra) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var message strings.Builder
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Missing ports
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
if len(result.Missing) > 0 &&
|
||||||
|
len(result.Extra) == 0 {
|
||||||
|
|
||||||
|
message.WriteString(
|
||||||
|
"Virtuelle Ports nicht eingerichtet",
|
||||||
|
)
|
||||||
|
|
||||||
|
message.WriteString(
|
||||||
|
"\n\nFolgende Ports sind in client.json " +
|
||||||
|
"eingetragen, aber auf diesem Rechner " +
|
||||||
|
"nicht eingerichtet:\n\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, port := range result.Missing {
|
||||||
|
message.WriteString(" ")
|
||||||
|
message.WriteString(port)
|
||||||
|
message.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
message.WriteString(
|
||||||
|
"\nBitte richten Sie diese virtuellen Ports " +
|
||||||
|
"entsprechend der Installationsanleitung ein.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%s",
|
||||||
|
strings.TrimSpace(message.String()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Extra ports
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
if len(result.Missing) == 0 &&
|
||||||
|
len(result.Extra) > 0 {
|
||||||
|
|
||||||
|
message.WriteString(
|
||||||
|
"Virtuelle Ports nicht konfiguriert",
|
||||||
|
)
|
||||||
|
|
||||||
|
message.WriteString(
|
||||||
|
"\n\nFolgende virtuelle Ports sind auf diesem " +
|
||||||
|
"Rechner eingerichtet, aber nicht in " +
|
||||||
|
"client.json eingetragen:\n\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, port := range result.Extra {
|
||||||
|
message.WriteString(" ")
|
||||||
|
message.WriteString(port)
|
||||||
|
message.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
message.WriteString(
|
||||||
|
"\nBitte ergänzen Sie die entsprechenden " +
|
||||||
|
"Einträge in client.json.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%s",
|
||||||
|
strings.TrimSpace(message.String()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Missing and extra ports
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
message.WriteString(
|
||||||
|
"Virtuelle Portkonfiguration stimmt nicht überein",
|
||||||
|
)
|
||||||
|
|
||||||
|
message.WriteString(
|
||||||
|
"\n\nIn client.json eingetragen, aber nicht " +
|
||||||
|
"eingerichtet:\n\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, port := range result.Missing {
|
||||||
|
message.WriteString(" ")
|
||||||
|
message.WriteString(port)
|
||||||
|
message.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
message.WriteString(
|
||||||
|
"\nEingerichtet, aber nicht in client.json " +
|
||||||
|
"eingetragen:\n\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, port := range result.Extra {
|
||||||
|
message.WriteString(" ")
|
||||||
|
message.WriteString(port)
|
||||||
|
message.WriteByte('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
message.WriteString(
|
||||||
|
"\nBitte korrigieren Sie die virtuelle " +
|
||||||
|
"Portkonfiguration.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"%s",
|
||||||
|
strings.TrimSpace(message.String()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,176 +9,98 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* Beschreibung:
|
* Beschreibung:
|
||||||
* Linux-spezifische Prüfung der lokal vorhandenen virtuellen
|
* Linux-spezifische Ermittlung der lokal vorhandenen virtuellen
|
||||||
* rs2322tcp-Schnittstellen gegen die Client-Konfiguration.
|
* rs2322tcp-Schnittstellen.
|
||||||
|
*
|
||||||
|
* Die öffentliche /dev/ttyUSBxxx-Schnittstelle wird ausschließlich gelesen.
|
||||||
|
* Der Client verändert diese Schnittstellen hier nicht.
|
||||||
* ============================================================================
|
* ============================================================================
|
||||||
*/
|
*/
|
||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"git.lang-dieter.de/rs2322tcp/internal/config"
|
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
// Local virtual-port validation
|
// Existing virtual ports
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// validateLocalVirtualPorts compares the virtual ports defined in
|
// existingVirtualPortLinks returns all public virtual-port paths that
|
||||||
// client.json with the public symbolic links currently available on Linux.
|
// currently exist as symbolic links.
|
||||||
//
|
//
|
||||||
// The check is read-only and does not require root privileges.
|
// Only ports inside the configured virtual-port range are considered.
|
||||||
|
// The public /dev/ttyUSBxxx links are only inspected; they are never
|
||||||
|
// created, changed, or removed by this function.
|
||||||
//
|
//
|
||||||
// A mismatch prevents the client from starting.
|
// This check only requires read access and therefore does not require
|
||||||
func validateLocalVirtualPorts(
|
// root privileges.
|
||||||
cfg *config.ClientConfig,
|
func existingVirtualPortLinks(
|
||||||
) error {
|
cfg config.VirtualPortRangeConfig,
|
||||||
if cfg == nil {
|
) ([]string, error) {
|
||||||
return fmt.Errorf("client configuration is nil")
|
return existingVirtualPortLinksInDirectory(
|
||||||
}
|
cfg,
|
||||||
|
"/dev",
|
||||||
configured := make(
|
|
||||||
[]string,
|
|
||||||
0,
|
|
||||||
len(cfg.VirtualPorts),
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, virtualPort := range cfg.VirtualPorts {
|
|
||||||
configured = append(
|
|
||||||
configured,
|
|
||||||
virtualPort.Port,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
available, err := existingVirtualPortLinks(
|
|
||||||
cfg.VirtualPortRange,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"prüfen der virtuellen Ports fehlgeschlagen: %w",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := compareVirtualPorts(
|
|
||||||
configured,
|
|
||||||
available,
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(result.Missing) == 0 &&
|
|
||||||
len(result.Extra) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var message strings.Builder
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Missing ports
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
if len(result.Missing) > 0 &&
|
|
||||||
len(result.Extra) == 0 {
|
|
||||||
|
|
||||||
message.WriteString(
|
|
||||||
"Virtuelle Ports nicht eingerichtet",
|
|
||||||
)
|
|
||||||
|
|
||||||
message.WriteString(
|
|
||||||
"\n\nFolgende Ports sind in client.json " +
|
|
||||||
"eingetragen, aber auf diesem Rechner " +
|
|
||||||
"nicht eingerichtet:\n\n",
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, port := range result.Missing {
|
|
||||||
message.WriteString(" ")
|
|
||||||
message.WriteString(port)
|
|
||||||
message.WriteByte('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
message.WriteString(
|
|
||||||
"\nBitte richten Sie diese virtuellen Ports " +
|
|
||||||
"entsprechend der Installationsanleitung ein.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%s",
|
|
||||||
strings.TrimSpace(message.String()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Extra ports
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
if len(result.Missing) == 0 &&
|
|
||||||
len(result.Extra) > 0 {
|
|
||||||
|
|
||||||
message.WriteString(
|
|
||||||
"Virtuelle Ports nicht konfiguriert",
|
|
||||||
)
|
|
||||||
|
|
||||||
message.WriteString(
|
|
||||||
"\n\nFolgende virtuelle Ports sind auf diesem " +
|
|
||||||
"Rechner eingerichtet, aber nicht in " +
|
|
||||||
"client.json eingetragen:\n\n",
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, port := range result.Extra {
|
|
||||||
message.WriteString(" ")
|
|
||||||
message.WriteString(port)
|
|
||||||
message.WriteByte('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
message.WriteString(
|
|
||||||
"\nBitte ergänzen Sie die entsprechenden " +
|
|
||||||
"Einträge in client.json.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%s",
|
|
||||||
strings.TrimSpace(message.String()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Missing and extra ports
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
message.WriteString(
|
|
||||||
"Virtuelle Portkonfiguration stimmt nicht überein",
|
|
||||||
)
|
|
||||||
|
|
||||||
message.WriteString(
|
|
||||||
"\n\nIn client.json eingetragen, aber nicht " +
|
|
||||||
"eingerichtet:\n\n",
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, port := range result.Missing {
|
|
||||||
message.WriteString(" ")
|
|
||||||
message.WriteString(port)
|
|
||||||
message.WriteByte('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
message.WriteString(
|
|
||||||
"\nEingerichtet, aber nicht in client.json " +
|
|
||||||
"eingetragen:\n\n",
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, port := range result.Extra {
|
|
||||||
message.WriteString(" ")
|
|
||||||
message.WriteString(port)
|
|
||||||
message.WriteByte('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
message.WriteString(
|
|
||||||
"\nBitte korrigieren Sie die virtuelle " +
|
|
||||||
"Portkonfiguration.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%s",
|
|
||||||
strings.TrimSpace(message.String()),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Existing virtual ports in directory
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// existingVirtualPortLinksInDirectory returns all virtual-port paths in
|
||||||
|
// directory that currently exist as symbolic links.
|
||||||
|
//
|
||||||
|
// This helper is deliberately separated from existingVirtualPortLinks so
|
||||||
|
// the filesystem inspection can be tested without modifying /dev.
|
||||||
|
func existingVirtualPortLinksInDirectory(
|
||||||
|
cfg config.VirtualPortRangeConfig,
|
||||||
|
directory string,
|
||||||
|
) ([]string, error) {
|
||||||
|
if directory == "" {
|
||||||
|
return nil, fmt.Errorf("virtual port directory is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := NewVirtualPortManager(cfg)
|
||||||
|
if manager == nil {
|
||||||
|
return nil, fmt.Errorf("virtual port manager is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
links := make([]string, 0)
|
||||||
|
|
||||||
|
for number := cfg.First; number <= cfg.Last; number++ {
|
||||||
|
portName := filepath.Base(
|
||||||
|
manager.PortPath(number),
|
||||||
|
)
|
||||||
|
|
||||||
|
portPath := filepath.Join(directory, portName)
|
||||||
|
|
||||||
|
info, err := os.Lstat(portPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"inspect virtual port %q: %w",
|
||||||
|
portPath,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Mode()&os.ModeSymlink == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
links = append(
|
||||||
|
links,
|
||||||
|
filepath.Join(directory, portName),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
132
internal/client/virtual_port_validation_windows.go
Normal file
132
internal/client/virtual_port_validation_windows.go
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* Projekt.....: rs2322tcp
|
||||||
|
* Datei.......: internal/client/virtual_port_validation_windows.go
|
||||||
|
* Copyright (C) 2026 Dieter Lang
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* Beschreibung:
|
||||||
|
* Windows-spezifische Ermittlung der lokal vorhandenen seriellen
|
||||||
|
* Schnittstellen für rs2322tcp.
|
||||||
|
*
|
||||||
|
* Unter Windows werden virtuelle Ports als COMxxx-Schnittstellen
|
||||||
|
* bereitgestellt.
|
||||||
|
*
|
||||||
|
* Diese Implementierung prüft den konfigurierten Portbereich und liefert
|
||||||
|
* die tatsächlich vorhandenen COM-Schnittstellen zurück.
|
||||||
|
*
|
||||||
|
* Die Funktion verändert keine Geräte und installiert keine Treiber.
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
|
||||||
|
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Existing virtual ports
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// existingVirtualPortLinks returns all COM ports in the configured range
|
||||||
|
// that are currently registered as serial ports in Windows.
|
||||||
|
//
|
||||||
|
// The Windows serial-port information is obtained from the Plug-and-Play
|
||||||
|
// device registry. No device is created, modified, or removed.
|
||||||
|
func existingVirtualPortLinks(
|
||||||
|
cfg config.VirtualPortRangeConfig,
|
||||||
|
) ([]string, error) {
|
||||||
|
key, err := registry.OpenKey(
|
||||||
|
registry.LOCAL_MACHINE,
|
||||||
|
`HARDWARE\DEVICEMAP\SERIALCOMM`,
|
||||||
|
registry.READ,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"open Windows serial-port registry: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
defer key.Close()
|
||||||
|
|
||||||
|
values, err := key.ReadValueNames(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"read Windows serial-port registry: %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
available := make([]string, 0)
|
||||||
|
|
||||||
|
for _, valueName := range values {
|
||||||
|
portName, _, err := key.GetStringValue(valueName)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
number, ok := windowsCOMPortNumber(portName)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if number < cfg.First || number > cfg.Last {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
available = append(
|
||||||
|
available,
|
||||||
|
portName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return available, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// COM port number
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// windowsCOMPortNumber extracts the numeric part of a Windows COM port name.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// COM1 -> 1
|
||||||
|
// COM10 -> 10
|
||||||
|
//
|
||||||
|
// The comparison is case-insensitive.
|
||||||
|
func windowsCOMPortNumber(portName string) (int, bool) {
|
||||||
|
if len(portName) < 4 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (portName[0] != 'C' && portName[0] != 'c') ||
|
||||||
|
(portName[1] != 'O' && portName[1] != 'o') ||
|
||||||
|
(portName[2] != 'M' && portName[2] != 'm') {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
number := 0
|
||||||
|
|
||||||
|
for _, char := range portName[3:] {
|
||||||
|
if char < '0' || char > '9' {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
number = number*10 + int(char-'0')
|
||||||
|
}
|
||||||
|
|
||||||
|
if number == 0 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return number, true
|
||||||
|
}
|
||||||
210
internal/client/virtual_serial_windows.go
Normal file
210
internal/client/virtual_serial_windows.go
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ============================================================================
|
||||||
|
* Projekt.....: rs2322tcp
|
||||||
|
* Datei.......: internal/client/virtual_serial_windows.go
|
||||||
|
* Copyright (C) 2026 Dieter Lang
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* Beschreibung:
|
||||||
|
* Windows-spezifische Implementierung der virtuellen seriellen Schnittstelle
|
||||||
|
* über go.bug.st/serial.
|
||||||
|
*
|
||||||
|
* Ein COM-Port wird unter Windows nur einmal geöffnet. Dasselbe Handle wird
|
||||||
|
* parallel für Lese- und Schreiboperationen verwendet. Damit kann eine
|
||||||
|
* Goroutine blockierend lesen, während eine andere Goroutine Daten schreibt.
|
||||||
|
* ============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
bugserial "go.bug.st/serial"
|
||||||
|
)
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// windowsSerial
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// windowsSerial implements VirtualSerial using one Windows COM-port handle
|
||||||
|
// for both reading and writing.
|
||||||
|
type windowsSerial struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
port bugserial.Port
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Path
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Path returns the Windows COM-port name.
|
||||||
|
func (p *windowsSerial) Path() string {
|
||||||
|
if p == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.path
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Read
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Read reads raw bytes from the Windows COM-port handle.
|
||||||
|
func (p *windowsSerial) Read(b []byte) (int, error) {
|
||||||
|
if p == nil {
|
||||||
|
return 0, fmt.Errorf("virtual serial port is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
port := p.port
|
||||||
|
path := p.path
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
if port == nil {
|
||||||
|
return 0, fmt.Errorf("virtual serial port is closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Write writes raw bytes to the Windows COM-port handle.
|
||||||
|
func (p *windowsSerial) Write(b []byte) (int, error) {
|
||||||
|
if p == nil {
|
||||||
|
return 0, fmt.Errorf("virtual serial port is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
port := p.port
|
||||||
|
path := p.path
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
if port == nil {
|
||||||
|
return 0, fmt.Errorf("virtual serial port is closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 closes the Windows COM-port handle.
|
||||||
|
func (p *windowsSerial) Close() error {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.mu.Lock()
|
||||||
|
|
||||||
|
port := p.port
|
||||||
|
path := p.path
|
||||||
|
|
||||||
|
p.port = nil
|
||||||
|
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
271
scripts/build.sh
271
scripts/build.sh
|
|
@ -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
184
scripts/deploy.sh
Executable 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 "============================================================"
|
||||||
12
scripts/test-rotor-com10.bat
Normal file
12
scripts/test-rotor-com10.bat
Normal 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
|
||||||
80
scripts/test-rotor-com10.ps1
Normal file
80
scripts/test-rotor-com10.ps1
Normal 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()
|
||||||
|
}
|
||||||
20
scripts/test-windows-client.bat
Normal file
20
scripts/test-windows-client.bat
Normal 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
|
||||||
Loading…
Reference in a new issue