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.
|
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
|
## [0.0.7] - 2026-08-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
419
README.md
419
README.md
|
|
@ -48,9 +48,9 @@ rs2322tcp-client │
|
||||||
Funkgerät Rotor
|
Funkgerät Rotor
|
||||||
```
|
```
|
||||||
|
|
||||||
## Aktueller Server-Datenpfad
|
## Aktueller Datenpfad
|
||||||
|
|
||||||
Die serverseitige TCP-/RS232-Verbindung ist implementiert:
|
Der serverseitige TCP-/RS232-Datenpfad ist implementiert.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
TCP-Control
|
TCP-Control
|
||||||
|
|
@ -85,32 +85,86 @@ TCP ───────────────► RS232
|
||||||
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
|
## Client
|
||||||
|
|
||||||
Der Client soll gleichberechtigt unter folgenden Betriebssystemen
|
Der Client soll gleichberechtigt unter folgenden Betriebssystemen
|
||||||
eingesetzt werden können:
|
eingesetzt werden können:
|
||||||
|
|
||||||
- Windows
|
|
||||||
- Linux
|
- Linux
|
||||||
|
- Windows
|
||||||
|
|
||||||
Die plattformspezifische Bereitstellung der virtuellen seriellen
|
Die gemeinsame Client-Logik ist von der plattformspezifischen
|
||||||
Schnittstelle wird vom gemeinsamen Client-Kern getrennt.
|
Bereitstellung der virtuellen seriellen Schnittstelle getrennt.
|
||||||
|
|
||||||
Unter Linux ist die virtuelle serielle Schnittstelle inzwischen über
|
Der Client besteht im Wesentlichen aus:
|
||||||
einen direkt aus Go erzeugten PTY implementiert. Die Implementierung
|
|
||||||
verwendet `/dev/ptmx` und stellt den zugehörigen PTY-Slave unter
|
- Control-Verbindung zum Server
|
||||||
`/dev/pts/N` bereit.
|
- 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
|
Die PTY-Schnittstelle wird im Raw Mode betrieben. Dadurch werden die
|
||||||
übertragenen Daten als transparenter Byte-Strom behandelt.
|
übertragenen Daten als transparenter Byte-Strom behandelt.
|
||||||
|
|
||||||
Die allgemeine Client-Logik verwendet dafür die Abstraktion
|
Die allgemeine Client-Logik verwendet dafür die Abstraktion
|
||||||
`VirtualSerial`. Die Linux-spezifische Implementierung ist von dieser
|
`VirtualSerial`.
|
||||||
Abstraktion getrennt.
|
|
||||||
|
Die Linux-spezifische Implementierung ist von dieser Abstraktion
|
||||||
|
getrennt.
|
||||||
|
|
||||||
Für die PTY-Funktion ist keine externe Laufzeitabhängigkeit wie `socat`
|
Für die PTY-Funktion ist keine externe Laufzeitabhängigkeit wie `socat`
|
||||||
erforderlich.
|
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
|
## Server
|
||||||
|
|
||||||
Der Server ist zunächst für den Betrieb auf einem Raspberry Pi 5
|
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.
|
Client und Server erhalten jeweils eine eigene JSON-Konfiguration.
|
||||||
|
|
||||||
Über die Konfiguration werden unter anderem festgelegt:
|
### Client-Konfiguration
|
||||||
|
|
||||||
- Netzwerkparameter
|
Über die Client-Konfiguration werden unter anderem festgelegt:
|
||||||
- serielle Schnittstelle
|
|
||||||
- Baudrate
|
|
||||||
- Datenbits
|
|
||||||
- Parität
|
|
||||||
- Stopbits
|
|
||||||
- Bezeichnung des Gerätes
|
|
||||||
- weitere für die jeweilige Schnittstelle erforderliche Parameter
|
|
||||||
|
|
||||||
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
|
## Diagnose und Logging
|
||||||
|
|
||||||
|
|
@ -267,7 +509,7 @@ Rotor:
|
||||||
1 Stopbit
|
1 Stopbit
|
||||||
```
|
```
|
||||||
|
|
||||||
Der Client stellt dem Anwender dafür den virtuellen Port:
|
Der Client stellt dem Anwender dafür beispielsweise den virtuellen Port:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/dev/ttyUSB101
|
/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
|
seriellen Port des Clients und dem realen SPID-Rot2Prog über TCP
|
||||||
erfolgreich nachgewiesen.
|
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
|
## Geräte-Simulator
|
||||||
|
|
||||||
Das Programm `rs2322tcp-device-sim` dient als Simulator für ein an der
|
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
|
0a FF 01 b7
|
||||||
```
|
```
|
||||||
|
|
||||||
Ungültige Eingaben wie
|
Ungültige Eingaben wie:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
GG
|
GG
|
||||||
```
|
```
|
||||||
|
|
||||||
oder
|
oder:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
4
|
4
|
||||||
|
|
@ -540,8 +708,7 @@ empfangenen Bytes lediglich an.
|
||||||
|
|
||||||
## Externe USB-to-Seriell-Adapter
|
## Externe USB-to-Seriell-Adapter
|
||||||
|
|
||||||
Für den späteren praktischen Einsatz sind USB-to-Seriell-Adapter
|
Für den praktischen Einsatz sind USB-to-Seriell-Adapter vorgesehen.
|
||||||
vorgesehen.
|
|
||||||
|
|
||||||
Unter Linux werden solche Adapter normalerweise als serielle Gerätedateien
|
Unter Linux werden solche Adapter normalerweise als serielle Gerätedateien
|
||||||
bereitgestellt, beispielsweise:
|
bereitgestellt, beispielsweise:
|
||||||
|
|
@ -578,12 +745,28 @@ go test -race ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
Der Race Detector wird eingesetzt, um Probleme bei der nebenläufigen
|
Der Race Detector wird eingesetzt, um Probleme bei der nebenläufigen
|
||||||
Verarbeitung von Sessions, Data-Listenern, DataConnections und Client-
|
Verarbeitung von Sessions, Data-Listenern, DataConnections und
|
||||||
Komponenten zu erkennen.
|
Client-Komponenten zu erkennen.
|
||||||
|
|
||||||
Die Linux-PTY-Komponente wurde mit einem vollständigen Bytebereich von
|
Die Linux-PTY-Komponente wurde mit einem vollständigen Bytebereich von
|
||||||
`0x00` bis `0xFF` geprüft.
|
`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
|
Der Geräte-Simulator verfügt zusätzlich über automatisierte Tests für
|
||||||
die Hex-Konvertierung und die Behandlung ungültiger Eingaben.
|
die Hex-Konvertierung und die Behandlung ungültiger Eingaben.
|
||||||
|
|
||||||
|
|
@ -644,6 +827,9 @@ Aktuell werden folgende Builds erzeugt:
|
||||||
- Linux armv7 -- Server
|
- Linux armv7 -- Server
|
||||||
- Linux armv6 -- Server
|
- Linux armv6 -- Server
|
||||||
|
|
||||||
|
Die gemeinsame Client-Architektur ist für eine spätere
|
||||||
|
Windows-Unterstützung vorbereitet.
|
||||||
|
|
||||||
## Versionierung
|
## Versionierung
|
||||||
|
|
||||||
Die Versionierung erfolgt über Git.
|
Die Versionierung erfolgt über Git.
|
||||||
|
|
@ -654,17 +840,20 @@ Build-Datum erfasst.
|
||||||
|
|
||||||
Release-Versionen werden über Git-Tags gekennzeichnet.
|
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.2` – Client-Control- und Data-Verbindungen
|
||||||
- `v0.0.3` – Linux-PTY-Unterstützung und Geräte-Simulator
|
- `v0.0.3` – Linux-PTY-Unterstützung und Geräte-Simulator
|
||||||
- `v0.0.4` – in Vorbereitung: interaktive TX-Funktion und automatisierte
|
- `v0.0.4` – interaktive TX-Funktion des Geräte-Simulators
|
||||||
Tests 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
|
## Entwicklungsstand
|
||||||
|
|
||||||
Das Projekt befindet sich weiterhin in der frühen Entwicklungsphase.
|
Das Projekt befindet sich weiterhin in der Entwicklungsphase.
|
||||||
|
|
||||||
Implementiert und getestet sind derzeit:
|
Implementiert und getestet sind derzeit:
|
||||||
|
|
||||||
|
|
@ -677,7 +866,8 @@ Implementiert und getestet sind derzeit:
|
||||||
- Konfiguration der seriellen Parameter
|
- Konfiguration der seriellen Parameter
|
||||||
- Client-Control-Verbindung
|
- Client-Control-Verbindung
|
||||||
- Client-DataConnection
|
- Client-DataConnection
|
||||||
- Client-Integrationstests
|
- Client-Runtime zur Verwaltung mehrerer virtueller Ports
|
||||||
|
- gezielte Zuordnung konfigurierter virtueller Portnummern
|
||||||
- Linux-PTY-Abstraktion
|
- Linux-PTY-Abstraktion
|
||||||
- Linux-PTY im Raw Mode
|
- Linux-PTY im Raw Mode
|
||||||
- automatisierter bidirektionaler 256-Byte-PTY-Test
|
- automatisierter bidirektionaler 256-Byte-PTY-Test
|
||||||
|
|
@ -686,13 +876,22 @@ Implementiert und getestet sind derzeit:
|
||||||
- eigenständiger Geräte-Simulator mit RX-Hexdump
|
- eigenständiger Geräte-Simulator mit RX-Hexdump
|
||||||
- interaktive TX-Hex-Eingabe des Geräte-Simulators
|
- interaktive TX-Hex-Eingabe des Geräte-Simulators
|
||||||
- automatisierte Tests der Hex-Konvertierung
|
- 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:
|
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
|
- die Windows-Implementierung der virtuellen seriellen Schnittstelle
|
||||||
- automatische Geräteantworten und Protokollsimulation
|
- automatische Geräteantworten und Protokollsimulation
|
||||||
|
- weitere produktive Funktionen des Clients und der GUI
|
||||||
|
- weiterführende Diagnose- und Logging-Funktionen
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@
|
||||||
* Einstiegspunkt für den grafischen rs2322tcp-Client.
|
* Einstiegspunkt für den grafischen rs2322tcp-Client.
|
||||||
*
|
*
|
||||||
* Der Client wird als eigenständige Desktop-Anwendung ausgeführt. Die
|
* Der Client wird als eigenständige Desktop-Anwendung ausgeführt. Die
|
||||||
* grafische Oberfläche basiert auf Fyne und bildet sowohl die
|
* grafische Oberfläche basiert auf Fyne und bildet sowohl die Statusanzeige
|
||||||
* Statusanzeige als auch später die Konfiguration der lokalen virtuellen
|
* als auch später die Konfiguration der lokalen virtuellen seriellen
|
||||||
* seriellen Ports ab.
|
* Ports ab.
|
||||||
*
|
*
|
||||||
* Die eigentliche Client- und Runtime-Logik bleibt in den internen
|
* Die eigentliche Client- und Runtime-Logik bleibt in den internen
|
||||||
* Packages gekapselt. Diese Datei ist bewusst auf den Programmstart
|
* Packages gekapselt. Diese Datei ist bewusst auf den Programmstart
|
||||||
|
|
@ -23,24 +23,93 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/app"
|
"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/gui"
|
||||||
"git.lang-dieter.de/rs2322tcp/internal/version"
|
"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(
|
guiApplication, err := gui.NewApp(
|
||||||
application,
|
application,
|
||||||
"./configs/client.json",
|
"./configs/client.json",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("GUI konnte nicht gestartet werden: %v\n", err)
|
fmt.Printf(
|
||||||
|
"GUI konnte nicht gestartet werden: %v\n",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,9 +123,11 @@ func main() {
|
||||||
// Beim Schließen des Hauptfensters wird die technische Client-
|
// Beim Schließen des Hauptfensters wird die technische Client-
|
||||||
// Anwendung beendet. Dadurch werden Runtime, Bridges, virtuelle
|
// Anwendung beendet. Dadurch werden Runtime, Bridges, virtuelle
|
||||||
// Ports und die Serververbindung sauber geschlossen.
|
// Ports und die Serververbindung sauber geschlossen.
|
||||||
guiApplication.Window().SetOnClosed(func() {
|
guiApplication.Window().SetOnClosed(
|
||||||
guiApplication.Close()
|
func() {
|
||||||
})
|
guiApplication.Close()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// ShowAndRun zeigt das Fenster und startet den Fyne-Eventloop.
|
// ShowAndRun zeigt das Fenster und startet den Fyne-Eventloop.
|
||||||
guiApplication.ShowAndRun()
|
guiApplication.ShowAndRun()
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@
|
||||||
* Konfiguration laden
|
* Konfiguration laden
|
||||||
* |
|
* |
|
||||||
* v
|
* v
|
||||||
|
* Lokale virtuelle Ports prüfen
|
||||||
|
* |
|
||||||
|
* v
|
||||||
* Server verbinden
|
* Server verbinden
|
||||||
* |
|
* |
|
||||||
* v
|
* v
|
||||||
|
|
@ -93,8 +96,8 @@ func NewApplication(configFile string) (*Application, error) {
|
||||||
// Start
|
// Start
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// Start loads the client configuration, connects to the server and starts
|
// Start loads the client configuration, validates the locally available
|
||||||
// the client Runtime.
|
// virtual ports, connects to the server and starts the client Runtime.
|
||||||
//
|
//
|
||||||
// The Runtime is started asynchronously because Runtime.Run blocks while
|
// The Runtime is started asynchronously because Runtime.Run blocks while
|
||||||
// the configured virtual serial connections are active.
|
// the configured virtual serial connections are active.
|
||||||
|
|
@ -118,6 +121,16 @@ func (a *Application) Start() error {
|
||||||
return fmt.Errorf("load client configuration: %w", err)
|
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(
|
address := fmt.Sprintf(
|
||||||
"%s:%d",
|
"%s:%d",
|
||||||
cfg.Server.Address,
|
cfg.Server.Address,
|
||||||
|
|
@ -234,6 +247,10 @@ func (a *Application) Close() error {
|
||||||
return firstErr
|
return firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Configuration
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// Config returns a copy of the currently loaded client configuration.
|
// Config returns a copy of the currently loaded client configuration.
|
||||||
//
|
//
|
||||||
// The configuration is available after NewApplication, even when the
|
// The configuration is available after NewApplication, even when the
|
||||||
|
|
@ -260,6 +277,36 @@ func (a *Application) Config() *config.ClientConfig {
|
||||||
return &cfg
|
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
|
// Devices
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -298,32 +345,3 @@ func (a *Application) Running() bool {
|
||||||
|
|
||||||
return a.runtime != nil && a.client != nil
|
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 {
|
for _, virtualPortConfig := range cfg.VirtualPorts {
|
||||||
|
if virtualPortConfig.RemoteDevice == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
bridge, _, _, err := connectBridge(
|
bridge, _, _, err := connectBridge(
|
||||||
r.client,
|
r.client,
|
||||||
r.manager,
|
r.manager,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -130,7 +132,7 @@ func removeVirtualPortLink(portPath string) error {
|
||||||
|
|
||||||
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 virtual port link %q: %w",
|
"remove existing virtual port link %q: %w",
|
||||||
linkPath,
|
linkPath,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
|
|
@ -138,3 +140,73 @@ 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@ package client
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.lang-dieter.de/rs2322tcp/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestVirtualPortLinkPath(t *testing.T) {
|
func TestVirtualPortLinkPath(t *testing.T) {
|
||||||
|
|
@ -27,7 +30,11 @@ func TestVirtualPortLinkPath(t *testing.T) {
|
||||||
t.Fatalf("virtualPortLinkPath() failed: %v", err)
|
t.Fatalf("virtualPortLinkPath() failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantSuffix := filepath.Join(".rs2322tcp", "virtual", "ttyUSB100")
|
wantSuffix := filepath.Join(
|
||||||
|
".rs2322tcp",
|
||||||
|
"virtual",
|
||||||
|
"ttyUSB100",
|
||||||
|
)
|
||||||
|
|
||||||
if !filepath.IsAbs(path) {
|
if !filepath.IsAbs(path) {
|
||||||
t.Fatalf("path is not absolute: %q", path)
|
t.Fatalf("path is not absolute: %q", path)
|
||||||
|
|
@ -91,12 +98,24 @@ func TestSetVirtualPortLinkReplacesExistingLink(t *testing.T) {
|
||||||
|
|
||||||
portPath := "/dev/ttyUSB100"
|
portPath := "/dev/ttyUSB100"
|
||||||
|
|
||||||
if err := setVirtualPortLink(portPath, "/dev/pts/2"); err != nil {
|
if err := setVirtualPortLink(
|
||||||
t.Fatalf("first setVirtualPortLink() failed: %v", err)
|
portPath,
|
||||||
|
"/dev/pts/2",
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf(
|
||||||
|
"first setVirtualPortLink() failed: %v",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := setVirtualPortLink(portPath, "/dev/pts/7"); err != nil {
|
if err := setVirtualPortLink(
|
||||||
t.Fatalf("second setVirtualPortLink() failed: %v", err)
|
portPath,
|
||||||
|
"/dev/pts/7",
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf(
|
||||||
|
"second setVirtualPortLink() failed: %v",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
linkPath, err := virtualPortLinkPath(portPath)
|
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 {
|
func hasPathSuffix(path string, suffix string) bool {
|
||||||
return path == suffix ||
|
return path == suffix ||
|
||||||
len(path) > len(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 := fyneApp.NewWindow("rs2322tcp Client")
|
||||||
window.Resize(fyne.NewSize(600, 400))
|
window.Resize(fyne.NewSize(800, 600))
|
||||||
|
|
||||||
app := &App{
|
app := &App{
|
||||||
fyneApp: fyneApp,
|
fyneApp: fyneApp,
|
||||||
|
|
@ -164,10 +164,9 @@ func (a *App) Start() {
|
||||||
a.statusLabel.SetText(
|
a.statusLabel.SetText(
|
||||||
"Server: nicht verbunden",
|
"Server: nicht verbunden",
|
||||||
)
|
)
|
||||||
a.errorLabel.SetText(
|
a.errorLabel.SetText("")
|
||||||
fmt.Sprintf("Fehler: %v", err),
|
|
||||||
)
|
a.showStartupError(err)
|
||||||
a.reconnectButton.Enable()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
@ -209,10 +208,9 @@ func (a *App) reconnect() {
|
||||||
a.statusLabel.SetText(
|
a.statusLabel.SetText(
|
||||||
"Server: nicht verbunden",
|
"Server: nicht verbunden",
|
||||||
)
|
)
|
||||||
a.errorLabel.SetText(
|
a.errorLabel.SetText("")
|
||||||
fmt.Sprintf("Fehler: %v", err),
|
|
||||||
)
|
a.showStartupError(err)
|
||||||
a.reconnectButton.Enable()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
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
|
// Device assignment
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
@ -398,6 +466,7 @@ func (a *App) refreshAssignmentWindow(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
footer := container.NewVBox(
|
footer := container.NewVBox(
|
||||||
info,
|
info,
|
||||||
closeButton,
|
closeButton,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue