Version 0.0.8 - Virtuelle Ports validieren und GUI verbessern
This commit is contained in:
parent
90189ab4dc
commit
81b06a16a0
11 changed files with 1386 additions and 167 deletions
38
CHANGELOG.md
38
CHANGELOG.md
|
|
@ -2,6 +2,44 @@
|
|||
|
||||
Alle wesentlichen Änderungen am Projekt werden in dieser Datei dokumentiert.
|
||||
|
||||
## [0.0.8] - 2026-08-12
|
||||
|
||||
### Added
|
||||
|
||||
- Prüfung der lokal vorhandenen virtuellen Ports beim Client-Start
|
||||
- Abgleich der vorhandenen symbolischen Links mit den in `client.json`
|
||||
definierten virtuellen Schnittstellen
|
||||
- Erkennung von in `client.json` definierten, aber lokal nicht vorhandenen
|
||||
virtuellen Ports
|
||||
- Erkennung von lokal vorhandenen, aber nicht in `client.json` definierten
|
||||
virtuellen Ports
|
||||
- Verständlicher Fehlerdialog bei einer abweichenden virtuellen
|
||||
Portkonfiguration
|
||||
- Beenden des Clients nach Bestätigung einer fehlerhaften
|
||||
Portkonfiguration
|
||||
|
||||
### Changed
|
||||
|
||||
- Virtuelle Ports werden entsprechend der in `client.json` festgelegten
|
||||
Portnummern gezielt geöffnet
|
||||
- Die Verwaltung virtueller Ports arbeitet weiterhin ohne Root-Rechte
|
||||
- Der Fehlerdialog verwendet einen einzelnen OK-Button
|
||||
- Allgemeine Schriftgröße der grafischen Benutzeroberfläche auf 22 erhöht
|
||||
- Hauptfenster des Clients auf 800 × 600 Pixel vergrößert
|
||||
|
||||
### Tests
|
||||
|
||||
- Prüfung vorhandener und konfigurierter virtueller Ports erfolgreich getestet
|
||||
- Fehlender virtueller Port erfolgreich erkannt und im Fehlerdialog angezeigt
|
||||
- Zusätzlicher, nicht konfigurierter virtueller Port erfolgreich erkannt
|
||||
- Clientstart nach Korrektur der virtuellen Portkonfiguration erfolgreich getestet
|
||||
- Gerätezuordnung nach Neustart des Clients erfolgreich aus `client.json`
|
||||
geladen
|
||||
- `go test ./...` erfolgreich
|
||||
- `go test -race ./internal/client` erfolgreich
|
||||
- `go test -race ./internal/gui` erfolgreich
|
||||
- `git diff --check` erfolgreich
|
||||
|
||||
## [0.0.7] - 2026-08-12
|
||||
|
||||
### Added
|
||||
|
|
|
|||
419
README.md
419
README.md
|
|
@ -48,9 +48,9 @@ rs2322tcp-client │
|
|||
Funkgerät Rotor
|
||||
```
|
||||
|
||||
## Aktueller Server-Datenpfad
|
||||
## Aktueller Datenpfad
|
||||
|
||||
Die serverseitige TCP-/RS232-Verbindung ist implementiert:
|
||||
Der serverseitige TCP-/RS232-Datenpfad ist implementiert.
|
||||
|
||||
```text
|
||||
TCP-Control
|
||||
|
|
@ -85,32 +85,86 @@ TCP ───────────────► RS232
|
|||
TCP ◄────────────── RS232
|
||||
```
|
||||
|
||||
Auch der Client-Datenpfad ist implementiert:
|
||||
|
||||
```text
|
||||
Virtueller serieller Port
|
||||
│
|
||||
▼
|
||||
rs2322tcp-client
|
||||
│
|
||||
│ TCP-Control
|
||||
▼
|
||||
Session
|
||||
│
|
||||
│ TCP-Data
|
||||
▼
|
||||
rs2322tcp-server
|
||||
│
|
||||
▼
|
||||
reale serielle
|
||||
Schnittstelle
|
||||
│
|
||||
▼
|
||||
Gerät
|
||||
```
|
||||
|
||||
Der Client kann mehrere konfigurierte virtuelle serielle Schnittstellen
|
||||
gleichzeitig verwalten.
|
||||
|
||||
## Client
|
||||
|
||||
Der Client soll gleichberechtigt unter folgenden Betriebssystemen
|
||||
eingesetzt werden können:
|
||||
|
||||
- Windows
|
||||
- Linux
|
||||
- Windows
|
||||
|
||||
Die plattformspezifische Bereitstellung der virtuellen seriellen
|
||||
Schnittstelle wird vom gemeinsamen Client-Kern getrennt.
|
||||
Die gemeinsame Client-Logik ist von der plattformspezifischen
|
||||
Bereitstellung der virtuellen seriellen Schnittstelle getrennt.
|
||||
|
||||
Unter Linux ist die virtuelle serielle Schnittstelle inzwischen über
|
||||
einen direkt aus Go erzeugten PTY implementiert. Die Implementierung
|
||||
verwendet `/dev/ptmx` und stellt den zugehörigen PTY-Slave unter
|
||||
`/dev/pts/N` bereit.
|
||||
Der Client besteht im Wesentlichen aus:
|
||||
|
||||
- Control-Verbindung zum Server
|
||||
- Verwaltung der vom Server angebotenen Geräte
|
||||
- Runtime zur Verwaltung mehrerer virtueller Ports
|
||||
- virtuellen seriellen Schnittstellen
|
||||
- TCP-Data-Connections
|
||||
- bidirektionalen Bridges zwischen virtuellem Port und TCP-Verbindung
|
||||
- grafischer Benutzeroberfläche
|
||||
|
||||
### Linux
|
||||
|
||||
Unter Linux ist die virtuelle serielle Schnittstelle über einen direkt
|
||||
aus Go erzeugten PTY implementiert.
|
||||
|
||||
Die Implementierung verwendet `/dev/ptmx` und stellt den zugehörigen
|
||||
PTY-Slave unter `/dev/pts/N` bereit.
|
||||
|
||||
Die PTY-Schnittstelle wird im Raw Mode betrieben. Dadurch werden die
|
||||
übertragenen Daten als transparenter Byte-Strom behandelt.
|
||||
|
||||
Die allgemeine Client-Logik verwendet dafür die Abstraktion
|
||||
`VirtualSerial`. Die Linux-spezifische Implementierung ist von dieser
|
||||
Abstraktion getrennt.
|
||||
`VirtualSerial`.
|
||||
|
||||
Die Linux-spezifische Implementierung ist von dieser Abstraktion
|
||||
getrennt.
|
||||
|
||||
Für die PTY-Funktion ist keine externe Laufzeitabhängigkeit wie `socat`
|
||||
erforderlich.
|
||||
|
||||
### Windows
|
||||
|
||||
Die gemeinsame Client-Architektur ist bereits auf eine
|
||||
plattformabhängige Implementierung der virtuellen seriellen
|
||||
Schnittstelle vorbereitet.
|
||||
|
||||
Die Windows-Implementierung der virtuellen seriellen Schnittstelle ist
|
||||
noch nicht vollständig implementiert.
|
||||
|
||||
Der spätere Windows-Client soll dieselbe gemeinsame Client- und
|
||||
Konfigurationslogik verwenden.
|
||||
|
||||
## Server
|
||||
|
||||
Der Server ist zunächst für den Betrieb auf einem Raspberry Pi 5
|
||||
|
|
@ -159,18 +213,206 @@ untersucht werden.
|
|||
|
||||
Client und Server erhalten jeweils eine eigene JSON-Konfiguration.
|
||||
|
||||
Über die Konfiguration werden unter anderem festgelegt:
|
||||
### Client-Konfiguration
|
||||
|
||||
- Netzwerkparameter
|
||||
- serielle Schnittstelle
|
||||
- Baudrate
|
||||
- Datenbits
|
||||
- Parität
|
||||
- Stopbits
|
||||
- Bezeichnung des Gerätes
|
||||
- weitere für die jeweilige Schnittstelle erforderliche Parameter
|
||||
Über die Client-Konfiguration werden unter anderem festgelegt:
|
||||
|
||||
Die konkrete Konfigurationsstruktur befindet sich in der Entwicklung.
|
||||
- Server-Adresse
|
||||
- Server-Port
|
||||
- reservierter Bereich der virtuellen seriellen Schnittstellen
|
||||
- Zuordnung virtueller Schnittstellen zu Remote-Geräten
|
||||
|
||||
Beispiel:
|
||||
|
||||
```json
|
||||
{
|
||||
"server": {
|
||||
"address": "127.0.0.1",
|
||||
"port": 5000
|
||||
},
|
||||
"virtual_port_range": {
|
||||
"first": 100,
|
||||
"last": 199
|
||||
},
|
||||
"virtual_ports": [
|
||||
{
|
||||
"port": "/dev/ttyUSB100",
|
||||
"remote_device": "radio"
|
||||
},
|
||||
{
|
||||
"port": "/dev/ttyUSB101",
|
||||
"remote_device": "rotor"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Der konfigurierte Bereich
|
||||
|
||||
```json
|
||||
"virtual_port_range": {
|
||||
"first": 100,
|
||||
"last": 199
|
||||
}
|
||||
```
|
||||
|
||||
definiert den für `rs2322tcp` vorgesehenen Bereich virtueller
|
||||
Schnittstellen.
|
||||
|
||||
Die konkrete Zuordnung wird über `virtual_ports` festgelegt.
|
||||
|
||||
Beispielsweise:
|
||||
|
||||
```text
|
||||
/dev/ttyUSB100 -> radio
|
||||
/dev/ttyUSB101 -> rotor
|
||||
```
|
||||
|
||||
Ein leerer Wert für `remote_device` kennzeichnet eine freie virtuelle
|
||||
Schnittstelle.
|
||||
|
||||
Virtuelle Schnittstellen bleiben dadurch auch ohne aktuelle
|
||||
Gerätezuordnung in der Client-Konfiguration erhalten.
|
||||
|
||||
### Gerätezuordnung
|
||||
|
||||
Die vom Server angebotenen Geräte können über die grafische
|
||||
Benutzeroberfläche den konfigurierten virtuellen Schnittstellen
|
||||
zugeordnet werden.
|
||||
|
||||
Dabei gelten folgende Regeln:
|
||||
|
||||
- Eine virtuelle Schnittstelle kann nur einem Gerät zugeordnet werden.
|
||||
- Ein Gerät kann nicht gleichzeitig mehreren virtuellen Schnittstellen
|
||||
zugeordnet werden.
|
||||
- Eine bereits belegte Schnittstelle kann auf ihre aktuelle Zuordnung
|
||||
oder auf „nicht verbunden“ gesetzt werden.
|
||||
- Freie virtuelle Schnittstellen werden automatisch zur Auswahl
|
||||
angeboten.
|
||||
- Änderungen werden zunächst nur im Arbeitsspeicher gehalten.
|
||||
- Beim Schließen des Gerätezuordnungsfensters werden die Änderungen in
|
||||
`client.json` gespeichert.
|
||||
- Wird das Hauptfenster geschlossen, während ungespeicherte Änderungen
|
||||
vorhanden sind, wird der Anwender darauf hingewiesen.
|
||||
|
||||
## Prüfung der virtuellen seriellen Schnittstellen
|
||||
|
||||
Beim Start des Linux-Clients werden die in `client.json` definierten
|
||||
virtuellen Schnittstellen mit den tatsächlich vorhandenen öffentlichen
|
||||
symbolischen Links verglichen.
|
||||
|
||||
Dabei werden ausschließlich zwei Listen miteinander verglichen:
|
||||
|
||||
```text
|
||||
vorhandene symbolische Links
|
||||
↕
|
||||
definierte virtuelle Ports
|
||||
```
|
||||
|
||||
Es werden insbesondere folgende Abweichungen erkannt:
|
||||
|
||||
- Ein in `client.json` definierter Port besitzt keinen entsprechenden
|
||||
öffentlichen Symlink.
|
||||
- Ein öffentlicher Symlink ist vorhanden, aber nicht in `client.json`
|
||||
eingetragen.
|
||||
|
||||
Bei einer Abweichung wird der Clientstart abgebrochen und dem Anwender
|
||||
ein verständlicher Hinweis angezeigt.
|
||||
|
||||
Der Client legt fehlende öffentliche Links nicht automatisch an.
|
||||
|
||||
Damit bleibt die Systemkonfiguration unter der Kontrolle des Anwenders.
|
||||
|
||||
Die Prüfung selbst verändert keine Systemdateien und benötigt keine
|
||||
Root-Rechte.
|
||||
|
||||
## Manuelle Einrichtung der virtuellen seriellen Ports unter Linux
|
||||
|
||||
Die öffentlichen virtuellen seriellen Ports werden unter Linux einmalig
|
||||
eingerichtet.
|
||||
|
||||
Der laufende `rs2322tcp-client` benötigt anschließend keine Root-Rechte.
|
||||
|
||||
Zunächst wird das Verzeichnis für die internen virtuellen Links angelegt:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.rs2322tcp/virtual
|
||||
```
|
||||
|
||||
Anschließend werden die öffentlichen `/dev/ttyUSBxxx`-Links einmalig
|
||||
mit administrativen Rechten angelegt.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```bash
|
||||
sudo ln -s "$HOME/.rs2322tcp/virtual/ttyUSB100" /dev/ttyUSB100
|
||||
sudo ln -s "$HOME/.rs2322tcp/virtual/ttyUSB101" /dev/ttyUSB101
|
||||
```
|
||||
|
||||
Die Zuordnung ist:
|
||||
|
||||
```text
|
||||
/dev/ttyUSB100 -> ~/.rs2322tcp/virtual/ttyUSB100 -> PTY
|
||||
/dev/ttyUSB101 -> ~/.rs2322tcp/virtual/ttyUSB101 -> PTY
|
||||
```
|
||||
|
||||
Die äußeren Links unter `/dev` gehören dabei `root`. Das ist beabsichtigt.
|
||||
|
||||
Der Client selbst läuft anschließend als normaler Benutzer.
|
||||
|
||||
Die internen Links unter:
|
||||
|
||||
```text
|
||||
~/.rs2322tcp/virtual/
|
||||
```
|
||||
|
||||
werden vom Client auf die jeweils verwendeten PTYs gesetzt.
|
||||
|
||||
Die Einrichtung eines neuen virtuellen Ports besteht daher aus:
|
||||
|
||||
1. Einen freien Port aus dem konfigurierten Bereich auswählen.
|
||||
2. Den öffentlichen `/dev/ttyUSBxxx`-Symlink einmalig anlegen.
|
||||
3. Den Port in `client.json` eintragen.
|
||||
4. Den Client starten.
|
||||
5. Die Gerätezuordnung gegebenenfalls über die GUI vornehmen.
|
||||
|
||||
Der Client prüft beim Start, ob die Konfiguration und die vorhandenen
|
||||
Symlinks übereinstimmen.
|
||||
|
||||
Die konkrete Einrichtung ist Linux-spezifisch. Für Windows wird später
|
||||
eine eigene plattformspezifische Lösung verwendet.
|
||||
|
||||
## Virtuelle serielle Schnittstelle unter Linux
|
||||
|
||||
Für den Linux-Client wird die virtuelle serielle Schnittstelle direkt
|
||||
über den Linux-PTY-Mechanismus erzeugt.
|
||||
|
||||
Dabei entsteht ein PTY-Paar:
|
||||
|
||||
```text
|
||||
/dev/ptmx
|
||||
│
|
||||
▼
|
||||
PTY Master
|
||||
│
|
||||
↕
|
||||
PTY Slave
|
||||
│
|
||||
▼
|
||||
/dev/pts/N
|
||||
```
|
||||
|
||||
Der PTY-Slave wird über den vom Anwender eingerichteten virtuellen
|
||||
`/dev/ttyUSBxxx`-Symlink erreichbar.
|
||||
|
||||
Der `rs2322tcp-client` verwendet den PTY-Master und transportiert die
|
||||
Bytes über TCP zum Server.
|
||||
|
||||
Die PTY-Implementierung arbeitet im Raw Mode und wurde mit einem
|
||||
vollständigen Bytebereich von `0x00` bis `0xFF` geprüft.
|
||||
|
||||
Dabei wurde die bidirektionale Übertragung aller 256 möglichen Bytewerte
|
||||
erfolgreich getestet.
|
||||
|
||||
## Diagnose und Logging
|
||||
|
||||
|
|
@ -267,7 +509,7 @@ Rotor:
|
|||
1 Stopbit
|
||||
```
|
||||
|
||||
Der Client stellt dem Anwender dafür den virtuellen Port:
|
||||
Der Client stellt dem Anwender dafür beispielsweise den virtuellen Port:
|
||||
|
||||
```text
|
||||
/dev/ttyUSB101
|
||||
|
|
@ -326,80 +568,6 @@ Damit wurde die bidirektionale Übertragung zwischen dem virtuellen
|
|||
seriellen Port des Clients und dem realen SPID-Rot2Prog über TCP
|
||||
erfolgreich nachgewiesen.
|
||||
|
||||
## Manuelle Einrichtung der virtuellen seriellen Ports
|
||||
|
||||
Die virtuellen seriellen Ports werden vor dem Start des Clients manuell
|
||||
eingerichtet. Dadurch muss der `rs2322tcp-client` selbst nicht als
|
||||
`root` bzw. mit `sudo` laufen.
|
||||
|
||||
Zunächst das Verzeichnis für die internen virtuellen Links anlegen:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.rs2322tcp/virtual
|
||||
```
|
||||
|
||||
Anschließend werden die öffentlichen `/dev/ttyUSBxxx`-Links einmalig
|
||||
mit administrativen Rechten angelegt:
|
||||
|
||||
```bash
|
||||
sudo ln -s "$HOME/.rs2322tcp/virtual/ttyUSB100" /dev/ttyUSB100
|
||||
sudo ln -s "$HOME/.rs2322tcp/virtual/ttyUSB101" /dev/ttyUSB101
|
||||
```
|
||||
|
||||
Die Zuordnung ist:
|
||||
|
||||
```text
|
||||
/dev/ttyUSB100 -> ~/.rs2322tcp/virtual/ttyUSB100 -> PTY
|
||||
/dev/ttyUSB101 -> ~/.rs2322tcp/virtual/ttyUSB101 -> PTY
|
||||
```
|
||||
|
||||
Die äußeren Links unter `/dev` gehören dabei `root`. Das ist beabsichtigt.
|
||||
Der Client selbst läuft anschließend als normaler Benutzer.
|
||||
|
||||
Die internen Links unter `~/.rs2322tcp/virtual/` werden vom Client auf die
|
||||
jeweils verwendeten PTYs gesetzt.
|
||||
|
||||
Die beiden virtuellen Ports werden in der Client-Konfiguration den
|
||||
Geräten zugeordnet:
|
||||
|
||||
```text
|
||||
/dev/ttyUSB100 -> radio
|
||||
/dev/ttyUSB101 -> rotor
|
||||
```
|
||||
|
||||
Die manuelle Einrichtung muss nur erfolgen, wenn die `/dev/ttyUSB100`-
|
||||
und `/dev/ttyUSB101`-Links noch nicht vorhanden sind.
|
||||
|
||||
## Virtuelle serielle Schnittstelle unter Linux
|
||||
|
||||
Für den Linux-Client wird die virtuelle serielle Schnittstelle direkt
|
||||
über den Linux-PTY-Mechanismus erzeugt.
|
||||
|
||||
Dabei entsteht ein PTY-Paar:
|
||||
|
||||
```text
|
||||
/dev/ptmx
|
||||
│
|
||||
▼
|
||||
PTY Master
|
||||
│
|
||||
↕
|
||||
PTY Slave
|
||||
│
|
||||
▼
|
||||
/dev/pts/N
|
||||
```
|
||||
|
||||
Die externe Hersteller-Software arbeitet mit dem PTY-Slave `/dev/pts/N`.
|
||||
Der `rs2322tcp-client` verwendet den PTY-Master und transportiert die
|
||||
Bytes über TCP zum Server.
|
||||
|
||||
Die PTY-Implementierung arbeitet im Raw Mode und wurde mit einem
|
||||
vollständigen Bytebereich von `0x00` bis `0xFF` geprüft.
|
||||
|
||||
Dabei wurde die bidirektionale Übertragung aller 256 möglichen Bytewerte
|
||||
erfolgreich getestet.
|
||||
|
||||
## Geräte-Simulator
|
||||
|
||||
Das Programm `rs2322tcp-device-sim` dient als Simulator für ein an der
|
||||
|
|
@ -458,13 +626,13 @@ Auch Groß- und Kleinschreibung kann gemischt werden:
|
|||
0a FF 01 b7
|
||||
```
|
||||
|
||||
Ungültige Eingaben wie
|
||||
Ungültige Eingaben wie:
|
||||
|
||||
```text
|
||||
GG
|
||||
```
|
||||
|
||||
oder
|
||||
oder:
|
||||
|
||||
```text
|
||||
4
|
||||
|
|
@ -540,8 +708,7 @@ empfangenen Bytes lediglich an.
|
|||
|
||||
## Externe USB-to-Seriell-Adapter
|
||||
|
||||
Für den späteren praktischen Einsatz sind USB-to-Seriell-Adapter
|
||||
vorgesehen.
|
||||
Für den praktischen Einsatz sind USB-to-Seriell-Adapter vorgesehen.
|
||||
|
||||
Unter Linux werden solche Adapter normalerweise als serielle Gerätedateien
|
||||
bereitgestellt, beispielsweise:
|
||||
|
|
@ -578,12 +745,28 @@ go test -race ./...
|
|||
```
|
||||
|
||||
Der Race Detector wird eingesetzt, um Probleme bei der nebenläufigen
|
||||
Verarbeitung von Sessions, Data-Listenern, DataConnections und Client-
|
||||
Komponenten zu erkennen.
|
||||
Verarbeitung von Sessions, Data-Listenern, DataConnections und
|
||||
Client-Komponenten zu erkennen.
|
||||
|
||||
Die Linux-PTY-Komponente wurde mit einem vollständigen Bytebereich von
|
||||
`0x00` bis `0xFF` geprüft.
|
||||
|
||||
Die Verwaltung der virtuellen Ports verfügt über Tests für:
|
||||
|
||||
- automatische Portreservierung
|
||||
- gezielte Reservierung einer konfigurierten Portnummer
|
||||
- Freigabe virtueller Ports
|
||||
- Erstellung und Entfernung der internen PTY-Links
|
||||
- Abgleich vorhandener virtueller Ports mit der Client-Konfiguration
|
||||
|
||||
Die Gerätezuordnung verfügt über Tests für:
|
||||
|
||||
- Zuordnung von Geräten zu virtuellen Ports
|
||||
- Freigabe virtueller Ports
|
||||
- Vermeidung doppelter Zuordnungen
|
||||
- Speicherung der Konfiguration
|
||||
- Laden der gespeicherten Zuordnung
|
||||
|
||||
Der Geräte-Simulator verfügt zusätzlich über automatisierte Tests für
|
||||
die Hex-Konvertierung und die Behandlung ungültiger Eingaben.
|
||||
|
||||
|
|
@ -644,6 +827,9 @@ Aktuell werden folgende Builds erzeugt:
|
|||
- Linux armv7 -- Server
|
||||
- Linux armv6 -- Server
|
||||
|
||||
Die gemeinsame Client-Architektur ist für eine spätere
|
||||
Windows-Unterstützung vorbereitet.
|
||||
|
||||
## Versionierung
|
||||
|
||||
Die Versionierung erfolgt über Git.
|
||||
|
|
@ -654,17 +840,20 @@ Build-Datum erfasst.
|
|||
|
||||
Release-Versionen werden über Git-Tags gekennzeichnet.
|
||||
|
||||
Aktueller Stand:
|
||||
Aktueller Entwicklungsstand:
|
||||
|
||||
- `v0.0.1` – erste Projektversion
|
||||
- `v0.0.1` – Projektgrundlage und Buildsystem
|
||||
- `v0.0.2` – Client-Control- und Data-Verbindungen
|
||||
- `v0.0.3` – Linux-PTY-Unterstützung und Geräte-Simulator
|
||||
- `v0.0.4` – in Vorbereitung: interaktive TX-Funktion und automatisierte
|
||||
Tests des Geräte-Simulators
|
||||
- `v0.0.4` – interaktive TX-Funktion des Geräte-Simulators
|
||||
- `v0.0.5` – konfigurierbare lokale virtuelle serielle Ports
|
||||
- `v0.0.6` – Client-Runtime und Serial-Bridge
|
||||
- `0.0.7` – Geräteerkennung und grafische Gerätezuordnung
|
||||
- `0.0.8` – Validierung der virtuellen Ports und Überarbeitung der Client-GUI
|
||||
|
||||
## Entwicklungsstand
|
||||
|
||||
Das Projekt befindet sich weiterhin in der frühen Entwicklungsphase.
|
||||
Das Projekt befindet sich weiterhin in der Entwicklungsphase.
|
||||
|
||||
Implementiert und getestet sind derzeit:
|
||||
|
||||
|
|
@ -677,7 +866,8 @@ Implementiert und getestet sind derzeit:
|
|||
- Konfiguration der seriellen Parameter
|
||||
- Client-Control-Verbindung
|
||||
- Client-DataConnection
|
||||
- Client-Integrationstests
|
||||
- Client-Runtime zur Verwaltung mehrerer virtueller Ports
|
||||
- gezielte Zuordnung konfigurierter virtueller Portnummern
|
||||
- Linux-PTY-Abstraktion
|
||||
- Linux-PTY im Raw Mode
|
||||
- automatisierter bidirektionaler 256-Byte-PTY-Test
|
||||
|
|
@ -686,13 +876,22 @@ Implementiert und getestet sind derzeit:
|
|||
- eigenständiger Geräte-Simulator mit RX-Hexdump
|
||||
- interaktive TX-Hex-Eingabe des Geräte-Simulators
|
||||
- automatisierte Tests der Hex-Konvertierung
|
||||
- grafische Gerätezuordnung im Client
|
||||
- Speicherung der Gerätezuordnung in `client.json`
|
||||
- Laden der Gerätezuordnung beim Neustart
|
||||
- Validierung der vorhandenen virtuellen Ports beim Clientstart
|
||||
- Abgleich von vorhandenen Symlinks und konfigurierten virtuellen Ports
|
||||
- Linux-Portverwaltung ohne Root-Rechte im laufenden Client
|
||||
- verständlicher Fehlerdialog bei fehlerhafter Portkonfiguration
|
||||
- anpassbare allgemeine Schriftgröße der GUI
|
||||
- überarbeitete Fenstergrößen der GUI
|
||||
|
||||
Noch nicht vollständig implementiert sind insbesondere:
|
||||
|
||||
- die vollständige Anbindung der virtuellen seriellen Schnittstelle an
|
||||
den produktiven Client-Anwendungsdatenpfad
|
||||
- die Windows-Implementierung der virtuellen seriellen Schnittstelle
|
||||
- automatische Geräteantworten und Protokollsimulation
|
||||
- weitere produktive Funktionen des Clients und der GUI
|
||||
- weiterführende Diagnose- und Logging-Funktionen
|
||||
|
||||
## Lizenz
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@
|
|||
* Einstiegspunkt für den grafischen rs2322tcp-Client.
|
||||
*
|
||||
* Der Client wird als eigenständige Desktop-Anwendung ausgeführt. Die
|
||||
* grafische Oberfläche basiert auf Fyne und bildet sowohl die
|
||||
* Statusanzeige als auch später die Konfiguration der lokalen virtuellen
|
||||
* seriellen Ports ab.
|
||||
* grafische Oberfläche basiert auf Fyne und bildet sowohl die Statusanzeige
|
||||
* als auch später die Konfiguration der lokalen virtuellen seriellen
|
||||
* Ports ab.
|
||||
*
|
||||
* Die eigentliche Client- und Runtime-Logik bleibt in den internen
|
||||
* Packages gekapselt. Diese Datei ist bewusst auf den Programmstart
|
||||
|
|
@ -23,24 +23,93 @@ package main
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"image/color"
|
||||
|
||||
"git.lang-dieter.de/rs2322tcp/internal/gui"
|
||||
"git.lang-dieter.de/rs2322tcp/internal/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Printf("rs2322tcp-client %s\n", version.Version)
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Application theme
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
application := app.NewWithID("git.lang-dieter.de.rs2322tcp.client")
|
||||
// applicationTheme keeps the standard Fyne theme and increases the general
|
||||
// text size for better readability.
|
||||
//
|
||||
// All other colors, fonts, icons and dimensions remain unchanged.
|
||||
type applicationTheme struct{}
|
||||
|
||||
// Ensure applicationTheme implements fyne.Theme.
|
||||
var _ fyne.Theme = (*applicationTheme)(nil)
|
||||
|
||||
// Color returns the standard Fyne theme color.
|
||||
func (t *applicationTheme) Color(
|
||||
name fyne.ThemeColorName,
|
||||
variant fyne.ThemeVariant,
|
||||
) color.Color {
|
||||
return theme.DefaultTheme().Color(name, variant)
|
||||
}
|
||||
|
||||
// Font returns the standard Fyne theme font.
|
||||
func (t *applicationTheme) Font(
|
||||
style fyne.TextStyle,
|
||||
) fyne.Resource {
|
||||
return theme.DefaultTheme().Font(style)
|
||||
}
|
||||
|
||||
// Icon returns the standard Fyne theme icon.
|
||||
func (t *applicationTheme) Icon(
|
||||
name fyne.ThemeIconName,
|
||||
) fyne.Resource {
|
||||
return theme.DefaultTheme().Icon(name)
|
||||
}
|
||||
|
||||
// Size returns the standard Fyne theme size, except for normal text.
|
||||
//
|
||||
// Normal text is increased from the Fyne default to 22 px for better
|
||||
// readability.
|
||||
func (t *applicationTheme) Size(
|
||||
name fyne.ThemeSizeName,
|
||||
) float32 {
|
||||
if name == theme.SizeNameText {
|
||||
return 22
|
||||
}
|
||||
|
||||
return theme.DefaultTheme().Size(name)
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Main
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func main() {
|
||||
fmt.Printf(
|
||||
"rs2322tcp-client %s\n",
|
||||
version.Version,
|
||||
)
|
||||
|
||||
application := app.NewWithID(
|
||||
"git.lang-dieter.de.rs2322tcp.client",
|
||||
)
|
||||
|
||||
// Use the standard Fyne theme with a larger general text size.
|
||||
application.Settings().SetTheme(
|
||||
&applicationTheme{},
|
||||
)
|
||||
|
||||
guiApplication, err := gui.NewApp(
|
||||
application,
|
||||
"./configs/client.json",
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("GUI konnte nicht gestartet werden: %v\n", err)
|
||||
fmt.Printf(
|
||||
"GUI konnte nicht gestartet werden: %v\n",
|
||||
err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -54,9 +123,11 @@ func main() {
|
|||
// Beim Schließen des Hauptfensters wird die technische Client-
|
||||
// Anwendung beendet. Dadurch werden Runtime, Bridges, virtuelle
|
||||
// Ports und die Serververbindung sauber geschlossen.
|
||||
guiApplication.Window().SetOnClosed(func() {
|
||||
guiApplication.Window().SetOnClosed(
|
||||
func() {
|
||||
guiApplication.Close()
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// ShowAndRun zeigt das Fenster und startet den Fyne-Eventloop.
|
||||
guiApplication.ShowAndRun()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@
|
|||
* Konfiguration laden
|
||||
* |
|
||||
* v
|
||||
* Lokale virtuelle Ports prüfen
|
||||
* |
|
||||
* v
|
||||
* Server verbinden
|
||||
* |
|
||||
* v
|
||||
|
|
@ -93,8 +96,8 @@ func NewApplication(configFile string) (*Application, error) {
|
|||
// Start
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Start loads the client configuration, connects to the server and starts
|
||||
// the client Runtime.
|
||||
// Start loads the client configuration, validates the locally available
|
||||
// virtual ports, connects to the server and starts the client Runtime.
|
||||
//
|
||||
// The Runtime is started asynchronously because Runtime.Run blocks while
|
||||
// the configured virtual serial connections are active.
|
||||
|
|
@ -118,6 +121,16 @@ func (a *Application) Start() error {
|
|||
return fmt.Errorf("load client configuration: %w", err)
|
||||
}
|
||||
|
||||
// Before connecting to the server, make sure that the virtual ports
|
||||
// configured in client.json match the locally installed virtual ports.
|
||||
//
|
||||
// This check is deliberately performed before New(address), so an
|
||||
// invalid local virtual-port configuration prevents the client from
|
||||
// starting at all.
|
||||
if err := validateLocalVirtualPorts(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
address := fmt.Sprintf(
|
||||
"%s:%d",
|
||||
cfg.Server.Address,
|
||||
|
|
@ -234,6 +247,10 @@ func (a *Application) Close() error {
|
|||
return firstErr
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Configuration
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Config returns a copy of the currently loaded client configuration.
|
||||
//
|
||||
// The configuration is available after NewApplication, even when the
|
||||
|
|
@ -260,6 +277,36 @@ func (a *Application) Config() *config.ClientConfig {
|
|||
return &cfg
|
||||
}
|
||||
|
||||
// SaveConfig saves the supplied client configuration to the configuration
|
||||
// file used by this Application.
|
||||
//
|
||||
// After a successful save the supplied configuration also becomes the
|
||||
// current in-memory configuration.
|
||||
//
|
||||
// The running Runtime is not restarted automatically. A subsequent
|
||||
// Reconnect or application restart will use the saved configuration.
|
||||
func (a *Application) SaveConfig(
|
||||
cfg *config.ClientConfig,
|
||||
) error {
|
||||
if a == nil {
|
||||
return fmt.Errorf("application is nil")
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("client configuration is nil")
|
||||
}
|
||||
|
||||
if err := config.SaveClient(a.configFile, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.config = cfg
|
||||
a.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Devices
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -298,32 +345,3 @@ func (a *Application) Running() bool {
|
|||
|
||||
return a.runtime != nil && a.client != nil
|
||||
}
|
||||
// SaveConfig saves the supplied client configuration to the configuration
|
||||
// file used by this Application.
|
||||
//
|
||||
// After a successful save the supplied configuration also becomes the
|
||||
// current in-memory configuration.
|
||||
//
|
||||
// The running Runtime is not restarted automatically. A subsequent
|
||||
// Reconnect or application restart will use the saved configuration.
|
||||
func (a *Application) SaveConfig(
|
||||
cfg *config.ClientConfig,
|
||||
) error {
|
||||
if a == nil {
|
||||
return fmt.Errorf("application is nil")
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("client configuration is nil")
|
||||
}
|
||||
|
||||
if err := config.SaveClient(a.configFile, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.config = cfg
|
||||
a.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@ func (r *Runtime) Run(
|
|||
}
|
||||
|
||||
for _, virtualPortConfig := range cfg.VirtualPorts {
|
||||
if virtualPortConfig.RemoteDevice == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
bridge, _, _, err := connectBridge(
|
||||
r.client,
|
||||
r.manager,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -130,7 +132,7 @@ func removeVirtualPortLink(portPath string) error {
|
|||
|
||||
if err := os.Remove(linkPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf(
|
||||
"remove virtual port link %q: %w",
|
||||
"remove existing virtual port link %q: %w",
|
||||
linkPath,
|
||||
err,
|
||||
)
|
||||
|
|
@ -138,3 +140,73 @@ func removeVirtualPortLink(portPath string) error {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ package client
|
|||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||
)
|
||||
|
||||
func TestVirtualPortLinkPath(t *testing.T) {
|
||||
|
|
@ -27,7 +30,11 @@ func TestVirtualPortLinkPath(t *testing.T) {
|
|||
t.Fatalf("virtualPortLinkPath() failed: %v", err)
|
||||
}
|
||||
|
||||
wantSuffix := filepath.Join(".rs2322tcp", "virtual", "ttyUSB100")
|
||||
wantSuffix := filepath.Join(
|
||||
".rs2322tcp",
|
||||
"virtual",
|
||||
"ttyUSB100",
|
||||
)
|
||||
|
||||
if !filepath.IsAbs(path) {
|
||||
t.Fatalf("path is not absolute: %q", path)
|
||||
|
|
@ -91,12 +98,24 @@ func TestSetVirtualPortLinkReplacesExistingLink(t *testing.T) {
|
|||
|
||||
portPath := "/dev/ttyUSB100"
|
||||
|
||||
if err := setVirtualPortLink(portPath, "/dev/pts/2"); err != nil {
|
||||
t.Fatalf("first setVirtualPortLink() failed: %v", err)
|
||||
if err := setVirtualPortLink(
|
||||
portPath,
|
||||
"/dev/pts/2",
|
||||
); err != nil {
|
||||
t.Fatalf(
|
||||
"first setVirtualPortLink() failed: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if err := setVirtualPortLink(portPath, "/dev/pts/7"); err != nil {
|
||||
t.Fatalf("second setVirtualPortLink() failed: %v", err)
|
||||
if err := setVirtualPortLink(
|
||||
portPath,
|
||||
"/dev/pts/7",
|
||||
); err != nil {
|
||||
t.Fatalf(
|
||||
"second setVirtualPortLink() failed: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
linkPath, err := virtualPortLinkPath(portPath)
|
||||
|
|
@ -146,6 +165,182 @@ func TestVirtualPortLinkPathUsesBaseName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestExistingVirtualPortLinksNone(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
cfg := config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 102,
|
||||
}
|
||||
|
||||
got, err := existingVirtualPortLinksInDirectory(
|
||||
cfg,
|
||||
tempDir,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf(
|
||||
"existingVirtualPortLinksInDirectory() failed: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if len(got) != 0 {
|
||||
t.Fatalf(
|
||||
"existingVirtualPortLinksInDirectory() = %v, want empty list",
|
||||
got,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExistingVirtualPortLinksFindsSymlinks(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
cfg := config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 102,
|
||||
}
|
||||
|
||||
for _, number := range []int{100, 102} {
|
||||
path := filepath.Join(
|
||||
tempDir,
|
||||
"ttyUSB"+itoa(number),
|
||||
)
|
||||
|
||||
if err := os.Symlink(
|
||||
"/dev/pts/"+itoa(number),
|
||||
path,
|
||||
); err != nil {
|
||||
t.Fatalf(
|
||||
"create test symlink %q: %v",
|
||||
path,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
got, err := existingVirtualPortLinksInDirectory(
|
||||
cfg,
|
||||
tempDir,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf(
|
||||
"existingVirtualPortLinksInDirectory() failed: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
want := []string{
|
||||
filepath.Join(tempDir, "ttyUSB100"),
|
||||
filepath.Join(tempDir, "ttyUSB102"),
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf(
|
||||
"existingVirtualPortLinksInDirectory() = %v, want %v",
|
||||
got,
|
||||
want,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExistingVirtualPortLinksIgnoresRegularFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
cfg := config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 102,
|
||||
}
|
||||
|
||||
path := filepath.Join(tempDir, "ttyUSB100")
|
||||
|
||||
if err := os.WriteFile(
|
||||
path,
|
||||
[]byte("not a symlink"),
|
||||
0644,
|
||||
); err != nil {
|
||||
t.Fatalf(
|
||||
"create regular test file %q: %v",
|
||||
path,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
got, err := existingVirtualPortLinksInDirectory(
|
||||
cfg,
|
||||
tempDir,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf(
|
||||
"existingVirtualPortLinksInDirectory() failed: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if len(got) != 0 {
|
||||
t.Fatalf(
|
||||
"regular file was detected as virtual port: %v",
|
||||
got,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExistingVirtualPortLinksIgnoresOutsideRange(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
cfg := config.VirtualPortRangeConfig{
|
||||
First: 100,
|
||||
Last: 102,
|
||||
}
|
||||
|
||||
path := filepath.Join(tempDir, "ttyUSB103")
|
||||
|
||||
if err := os.Symlink(
|
||||
"/dev/pts/103",
|
||||
path,
|
||||
); err != nil {
|
||||
t.Fatalf(
|
||||
"create outside-range symlink %q: %v",
|
||||
path,
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
got, err := existingVirtualPortLinksInDirectory(
|
||||
cfg,
|
||||
tempDir,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf(
|
||||
"existingVirtualPortLinksInDirectory() failed: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if len(got) != 0 {
|
||||
t.Fatalf(
|
||||
"outside-range symlink was detected: %v",
|
||||
got,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func itoa(number int) string {
|
||||
const digits = "0123456789"
|
||||
|
||||
if number == 0 {
|
||||
return "0"
|
||||
}
|
||||
|
||||
result := ""
|
||||
|
||||
for number > 0 {
|
||||
result = string(digits[number%10]) + result
|
||||
number /= 10
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func hasPathSuffix(path string, suffix string) bool {
|
||||
return path == suffix ||
|
||||
len(path) > len(suffix) &&
|
||||
|
|
|
|||
86
internal/client/virtual_port_validation.go
Normal file
86
internal/client/virtual_port_validation.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* ============================================================================
|
||||
* Projekt.....: rs2322tcp
|
||||
* Datei.......: internal/client/virtual_port_validation.go
|
||||
* Copyright (C) 2026 Dieter Lang
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* Vergleich der in client.json definierten virtuellen Ports mit den
|
||||
* tatsächlich vorhandenen virtuellen Schnittstellen.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
||||
import "sort"
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Virtual port validation
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// VirtualPortValidationResult contains the differences between configured
|
||||
// and locally available virtual ports.
|
||||
type VirtualPortValidationResult struct {
|
||||
Missing []string
|
||||
Extra []string
|
||||
}
|
||||
|
||||
// compareVirtualPorts compares the configured virtual ports with the
|
||||
// locally available virtual ports.
|
||||
//
|
||||
// Missing contains ports configured in client.json that are not available
|
||||
// locally.
|
||||
//
|
||||
// Extra contains locally available ports that are not configured in
|
||||
// client.json.
|
||||
//
|
||||
// The order of both input lists is irrelevant.
|
||||
func compareVirtualPorts(
|
||||
configured []string,
|
||||
available []string,
|
||||
) VirtualPortValidationResult {
|
||||
configuredSet := make(map[string]struct{}, len(configured))
|
||||
|
||||
for _, port := range configured {
|
||||
configuredSet[port] = struct{}{}
|
||||
}
|
||||
|
||||
availableSet := make(map[string]struct{}, len(available))
|
||||
|
||||
for _, port := range available {
|
||||
availableSet[port] = struct{}{}
|
||||
}
|
||||
|
||||
result := VirtualPortValidationResult{
|
||||
Missing: make([]string, 0),
|
||||
Extra: make([]string, 0),
|
||||
}
|
||||
|
||||
for port := range configuredSet {
|
||||
if _, ok := availableSet[port]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
result.Missing = append(
|
||||
result.Missing,
|
||||
port,
|
||||
)
|
||||
}
|
||||
|
||||
for port := range availableSet {
|
||||
if _, ok := configuredSet[port]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
result.Extra = append(
|
||||
result.Extra,
|
||||
port,
|
||||
)
|
||||
}
|
||||
|
||||
sort.Strings(result.Missing)
|
||||
sort.Strings(result.Extra)
|
||||
|
||||
return result
|
||||
}
|
||||
184
internal/client/virtual_port_validation_linux.go
Normal file
184
internal/client/virtual_port_validation_linux.go
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
//go:build linux
|
||||
|
||||
/*
|
||||
* ============================================================================
|
||||
* Projekt.....: rs2322tcp
|
||||
* Datei.......: internal/client/virtual_port_validation_linux.go
|
||||
* Copyright (C) 2026 Dieter Lang
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* Linux-spezifische Prüfung der lokal vorhandenen virtuellen
|
||||
* rs2322tcp-Schnittstellen gegen die Client-Konfiguration.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||
)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Local virtual-port validation
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// validateLocalVirtualPorts compares the virtual ports defined in
|
||||
// client.json with the public symbolic links currently available on Linux.
|
||||
//
|
||||
// The check is read-only and does not require root privileges.
|
||||
//
|
||||
// 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()),
|
||||
)
|
||||
}
|
||||
283
internal/client/virtual_port_validation_test.go
Normal file
283
internal/client/virtual_port_validation_test.go
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
/*
|
||||
* ============================================================================
|
||||
* Projekt.....: rs2322tcp
|
||||
* Datei.......: internal/client/virtual_port_validation_test.go
|
||||
* Copyright (C) 2026 Dieter Lang
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Beschreibung:
|
||||
* Tests für den Vergleich der konfigurierten und tatsächlich vorhandenen
|
||||
* virtuellen rs2322tcp-Schnittstellen.
|
||||
* ============================================================================
|
||||
*/
|
||||
package client
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompareVirtualPortsIdentical(t *testing.T) {
|
||||
configured := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB101",
|
||||
}
|
||||
|
||||
available := []string{
|
||||
"/dev/ttyUSB101",
|
||||
"/dev/ttyUSB100",
|
||||
}
|
||||
|
||||
got := compareVirtualPorts(
|
||||
configured,
|
||||
available,
|
||||
)
|
||||
|
||||
if len(got.Missing) != 0 {
|
||||
t.Errorf(
|
||||
"Missing = %v, want empty",
|
||||
got.Missing,
|
||||
)
|
||||
}
|
||||
|
||||
if len(got.Extra) != 0 {
|
||||
t.Errorf(
|
||||
"Extra = %v, want empty",
|
||||
got.Extra,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareVirtualPortsMissing(t *testing.T) {
|
||||
configured := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB101",
|
||||
"/dev/ttyUSB102",
|
||||
}
|
||||
|
||||
available := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB101",
|
||||
}
|
||||
|
||||
got := compareVirtualPorts(
|
||||
configured,
|
||||
available,
|
||||
)
|
||||
|
||||
wantMissing := []string{
|
||||
"/dev/ttyUSB102",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got.Missing, wantMissing) {
|
||||
t.Errorf(
|
||||
"Missing = %v, want %v",
|
||||
got.Missing,
|
||||
wantMissing,
|
||||
)
|
||||
}
|
||||
|
||||
if len(got.Extra) != 0 {
|
||||
t.Errorf(
|
||||
"Extra = %v, want empty",
|
||||
got.Extra,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareVirtualPortsExtra(t *testing.T) {
|
||||
configured := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB101",
|
||||
}
|
||||
|
||||
available := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB101",
|
||||
"/dev/ttyUSB103",
|
||||
}
|
||||
|
||||
got := compareVirtualPorts(
|
||||
configured,
|
||||
available,
|
||||
)
|
||||
|
||||
if len(got.Missing) != 0 {
|
||||
t.Errorf(
|
||||
"Missing = %v, want empty",
|
||||
got.Missing,
|
||||
)
|
||||
}
|
||||
|
||||
wantExtra := []string{
|
||||
"/dev/ttyUSB103",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got.Extra, wantExtra) {
|
||||
t.Errorf(
|
||||
"Extra = %v, want %v",
|
||||
got.Extra,
|
||||
wantExtra,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareVirtualPortsMissingAndExtra(t *testing.T) {
|
||||
configured := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB101",
|
||||
"/dev/ttyUSB102",
|
||||
}
|
||||
|
||||
available := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB101",
|
||||
"/dev/ttyUSB103",
|
||||
}
|
||||
|
||||
got := compareVirtualPorts(
|
||||
configured,
|
||||
available,
|
||||
)
|
||||
|
||||
wantMissing := []string{
|
||||
"/dev/ttyUSB102",
|
||||
}
|
||||
|
||||
wantExtra := []string{
|
||||
"/dev/ttyUSB103",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got.Missing, wantMissing) {
|
||||
t.Errorf(
|
||||
"Missing = %v, want %v",
|
||||
got.Missing,
|
||||
wantMissing,
|
||||
)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got.Extra, wantExtra) {
|
||||
t.Errorf(
|
||||
"Extra = %v, want %v",
|
||||
got.Extra,
|
||||
wantExtra,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareVirtualPortsEmpty(t *testing.T) {
|
||||
got := compareVirtualPorts(
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
if len(got.Missing) != 0 {
|
||||
t.Errorf(
|
||||
"Missing = %v, want empty",
|
||||
got.Missing,
|
||||
)
|
||||
}
|
||||
|
||||
if len(got.Extra) != 0 {
|
||||
t.Errorf(
|
||||
"Extra = %v, want empty",
|
||||
got.Extra,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareVirtualPortsConfiguredOnly(t *testing.T) {
|
||||
configured := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB101",
|
||||
}
|
||||
|
||||
got := compareVirtualPorts(
|
||||
configured,
|
||||
nil,
|
||||
)
|
||||
|
||||
wantMissing := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB101",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got.Missing, wantMissing) {
|
||||
t.Errorf(
|
||||
"Missing = %v, want %v",
|
||||
got.Missing,
|
||||
wantMissing,
|
||||
)
|
||||
}
|
||||
|
||||
if len(got.Extra) != 0 {
|
||||
t.Errorf(
|
||||
"Extra = %v, want empty",
|
||||
got.Extra,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareVirtualPortsAvailableOnly(t *testing.T) {
|
||||
available := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB101",
|
||||
}
|
||||
|
||||
got := compareVirtualPorts(
|
||||
nil,
|
||||
available,
|
||||
)
|
||||
|
||||
if len(got.Missing) != 0 {
|
||||
t.Errorf(
|
||||
"Missing = %v, want empty",
|
||||
got.Missing,
|
||||
)
|
||||
}
|
||||
|
||||
wantExtra := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB101",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got.Extra, wantExtra) {
|
||||
t.Errorf(
|
||||
"Extra = %v, want %v",
|
||||
got.Extra,
|
||||
wantExtra,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareVirtualPortsIgnoresDuplicates(t *testing.T) {
|
||||
configured := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB100",
|
||||
}
|
||||
|
||||
available := []string{
|
||||
"/dev/ttyUSB100",
|
||||
"/dev/ttyUSB100",
|
||||
}
|
||||
|
||||
got := compareVirtualPorts(
|
||||
configured,
|
||||
available,
|
||||
)
|
||||
|
||||
if len(got.Missing) != 0 {
|
||||
t.Errorf(
|
||||
"Missing = %v, want empty",
|
||||
got.Missing,
|
||||
)
|
||||
}
|
||||
|
||||
if len(got.Extra) != 0 {
|
||||
t.Errorf(
|
||||
"Extra = %v, want empty",
|
||||
got.Extra,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ func NewApp(
|
|||
}
|
||||
|
||||
window := fyneApp.NewWindow("rs2322tcp Client")
|
||||
window.Resize(fyne.NewSize(600, 400))
|
||||
window.Resize(fyne.NewSize(800, 600))
|
||||
|
||||
app := &App{
|
||||
fyneApp: fyneApp,
|
||||
|
|
@ -164,10 +164,9 @@ func (a *App) Start() {
|
|||
a.statusLabel.SetText(
|
||||
"Server: nicht verbunden",
|
||||
)
|
||||
a.errorLabel.SetText(
|
||||
fmt.Sprintf("Fehler: %v", err),
|
||||
)
|
||||
a.reconnectButton.Enable()
|
||||
a.errorLabel.SetText("")
|
||||
|
||||
a.showStartupError(err)
|
||||
})
|
||||
|
||||
return
|
||||
|
|
@ -209,10 +208,9 @@ func (a *App) reconnect() {
|
|||
a.statusLabel.SetText(
|
||||
"Server: nicht verbunden",
|
||||
)
|
||||
a.errorLabel.SetText(
|
||||
fmt.Sprintf("Fehler: %v", err),
|
||||
)
|
||||
a.reconnectButton.Enable()
|
||||
a.errorLabel.SetText("")
|
||||
|
||||
a.showStartupError(err)
|
||||
})
|
||||
|
||||
return
|
||||
|
|
@ -234,6 +232,76 @@ func (a *App) reconnect() {
|
|||
}()
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Startup error
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// showStartupError displays a modal error dialog for a client startup error.
|
||||
//
|
||||
// The dialog intentionally has only an "OK" button. A client configuration
|
||||
// error must not be bypassed because starting the client with inconsistent
|
||||
// virtual-port configuration could result in communication being directed
|
||||
// to the wrong or missing local interface.
|
||||
//
|
||||
// After the user acknowledges the error, the technical client is closed and
|
||||
// the complete Fyne application is terminated.
|
||||
func (a *App) showStartupError(err error) {
|
||||
if a == nil || a.window == nil || err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
title := widget.NewLabelWithStyle(
|
||||
"Die virtuellen Ports konnten nicht geprüft werden.",
|
||||
fyne.TextAlignLeading,
|
||||
fyne.TextStyle{
|
||||
Bold: true,
|
||||
},
|
||||
)
|
||||
|
||||
message := widget.NewLabel(
|
||||
err.Error(),
|
||||
)
|
||||
|
||||
message.Wrapping = fyne.TextWrapWord
|
||||
|
||||
content := container.NewVBox(
|
||||
title,
|
||||
widget.NewSeparator(),
|
||||
message,
|
||||
)
|
||||
|
||||
confirm := dialog.NewCustomWithoutButtons(
|
||||
"Virtuelle Ports",
|
||||
container.NewPadded(content),
|
||||
a.window,
|
||||
)
|
||||
|
||||
okButton := widget.NewButton(
|
||||
"OK",
|
||||
func() {
|
||||
confirm.Dismiss()
|
||||
|
||||
_ = a.clientApplication.Close()
|
||||
|
||||
a.window.SetCloseIntercept(nil)
|
||||
|
||||
a.fyneApp.Quit()
|
||||
},
|
||||
)
|
||||
|
||||
confirm.SetButtons(
|
||||
[]fyne.CanvasObject{
|
||||
okButton,
|
||||
},
|
||||
)
|
||||
|
||||
confirm.Show()
|
||||
|
||||
confirm.Resize(
|
||||
fyne.NewSize(650, 420),
|
||||
)
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Device assignment
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
|
@ -398,6 +466,7 @@ func (a *App) refreshAssignmentWindow(
|
|||
)
|
||||
},
|
||||
)
|
||||
|
||||
footer := container.NewVBox(
|
||||
info,
|
||||
closeButton,
|
||||
|
|
|
|||
Loading…
Reference in a new issue