Version 0.0.8 - Virtuelle Ports validieren und GUI verbessern

This commit is contained in:
Dieter Lang 2026-08-12 15:57:46 +02:00
parent 90189ab4dc
commit 81b06a16a0
11 changed files with 1386 additions and 167 deletions

View file

@ -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
View file

@ -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

View file

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

View file

@ -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
}

View file

@ -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,

View file

@ -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
}

View file

@ -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) &&

View 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
}

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

View 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,
)
}
}

View file

@ -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,