Best Practices für die Prüfung und Anzeige des Zustands externer Geräte — Design jenseits eines einzigen „Verbunden“

· · Windows, Externe Geräte, Geräteintegration, Zustandsverwaltung, UI/UX, Überwachung

Industriekameras, Barcode-Leser, SPS, Messgeräte, Drucker, serielle Geräte, USB-Geräte. In Windows-Anwendungen, die mit externen Geräten verbunden sind, entstehen Störfälle sehr häufig nicht durch den eigentlichen Defekt selbst, sondern zuerst dadurch, dass die Zustandsanzeige auf dem Bildschirm von der Realität abweicht.

Zum Beispiel solche Zustände.

  • Das OS sieht das Gerät, aber ein anderer Prozess hält es belegt, sodass es nicht genutzt werden kann
  • open war erfolgreich, aber Referenzfahrt, Warm-up oder Authentifizierung sind noch nicht abgeschlossen
  • Das Gerät hängt noch dran, antwortet aber längst nicht mehr
  • Der Erfassungs-Thread ist abgestorben, aber der letzte Wert bleibt auf dem Bildschirm stehen
  • Es handelt sich um ein unerwartetes Gerät oder eine unerwartete Firmware, trotzdem steht dort einfach „Verbunden“

Was man hier eigentlich wissen möchte, ist nicht nur, ob eine Verbindung besteht. Es ist, was man gerade gefahrlos tun darf.

Zielgruppe und Voraussetzungen dieses Artikels

Punkt Inhalt
Zielgruppe Personen, die Windows-Anwendungen entwerfen und implementieren, die mit externen Geräten verbunden sind. Gedacht für alle, die bei einer bestehenden App die Rückmeldungen „der Bildschirm zeigt Verbunden, aber es funktioniert nicht“ verringern möchten
Vorausgesetztes Wissen Die Fähigkeit, in irgendeiner Sprache Anwendungen zu schreiben. Kenntnisse eines bestimmten SDKs oder Gerätetreibers werden nicht vorausgesetzt
Vorausgesetzte Umgebung Es wird von einer Windows-Desktopanwendung ausgegangen. Die Art der Zustandstrennung und der Anzeigegedanke selbst sind jedoch nicht vom Betriebssystem abhängig
Nicht behandelt Die Nutzung einzelner Hersteller-SDK-APIs, die treiberseitige Implementierung

In diesem Artikel verwendete Begriffe

Begriffe, die im Original auf Englisch stehen, werden hier vorab zeilenweise erklärt. Sie bleiben auch im deutschen Text in ihrer englischen Form stehen.

Begriff Bedeutung in einer Zeile
PLC Programmable Logic Controller. Ein Industriecontroller zur Steuerung von Produktionsanlagen
firmware In das Gerät eingebettete Software. Auch bei gleicher Modellnummer kann die Version je nach Exemplar unterschiedlich sein
heartbeat Eine leichte, regelmäßig ausgetauschte Abfrage oder Benachrichtigung zur Lebenszeichenprüfung
poll / event poll ist das Verfahren, bei dem man selbst regelmäßig nachfragt; event ist das Verfahren, bei dem man auf eine Benachrichtigung der Gegenseite wartet
stale Ein Zustand, in dem ein Wert veraltet ist. Er konnte zwar abgerufen werden, aber der aktuell angezeigte Wert ist nicht mehr unbedingt aktuell
freshness budget Die vorab festgelegte Obergrenze: „Wird diese Zeit überschritten, gilt der Wert nicht mehr als aktuell“
flapping Ein Zustand, der in kurzer Zeit hin- und herspringt. Tritt bei Wackelkontakten oder kurzen Unterbrechungen auf
reconcile Den internen Zustand mit der Realität abgleichen und neu in Übereinstimmung bringen
interlock Ein Sicherheitsmechanismus, der den Betrieb stoppt. Solange er offen ist, bewegt sich das Gerät nicht
PnP Plug and Play. Der Mechanismus, mit dem das OS das Anschließen und Trennen eines Geräts erkennt und konfiguriert
RTT round-trip time. Die Zeit von der Anfrage bis zum Eintreffen der Antwort

1. Das Wichtigste zuerst

Bei der Prüfung und Anzeige des Zustands externer Geräte wirkt am stärksten: den Zustand nicht auf ein einziges Boolean zusammenzufassen.

Mindestens diese Punkte sollten getrennt gehalten werden.

  • Existenz: Ist es für das OS sichtbar?
  • Sitzungsaufbau: Hat die eigene App open / login / initialize abgeschlossen?
  • Antwortverhalten: Antwortet es auf Heartbeat oder Status-Abfragen?
  • Betriebsbereitschaft: Kann die eigentliche Operation gerade entgegengenommen werden?
  • Datenaktualität: Sind die angezeigten Werte aktuell?
  • Konfigurationsübereinstimmung: Ist es das erwartete Gerät, Modell, die erwartete Firmware?
  • Überwachungsintegrität: Lebt der Überwachungsprozess überhaupt noch?

Ziemlich vereinfacht gesagt:

Die Existenzprüfung liegt beim OS, die Nutzbarkeit bei der App, die Aktualitätsbeurteilung beim Bildschirm.

Allein wenn man diese drei nicht vermischt, wird die Zustandsanzeige deutlich stabiler.

Was die Anzeigeseite beantwortetWas nur die App beantworten kannWas das OS beantworten kannStimmt dies nicht,ist es auch bei allem anderen unbrauchbarSteht dies still,veralten alle BeurteilungenDatenaktualitätIst der angezeigte Wert aktuell?ÜberwachungsintegritätLebt der Überwachungsprozess selbst noch?Sitzungopen / login / initialize abgeschlossen?AntwortverhaltenAntwortet eine leichte Anfrage fristgerecht?BetriebsbereitschaftKann jetzt eine Operation entgegengenommen werden?KonfigurationsübereinstimmungErwartetes Gerät / Modell / Firmware?ExistenzIst das interface sichtbar?

Abbildung 1: Den Zustand nicht auf ein einziges Boolean zusammenfassen, sondern danach trennen, wer die Frage beantworten kann. Selbst wenn die obere Ebene erfüllt ist, heißt das nicht, dass auch die untere erfüllt ist

2. Warum „Verbunden“ gefährlich ist

Das Wort „Verbunden“ trägt in einem einzigen Begriff stillschweigend mehrere Bedeutungen.

Tatsächlich sind darin mindestens diese Fragen vermischt.

  1. Sieht das OS das interface des Zielgeräts?
  2. Konnte die eigene App das Gerät öffnen / anmelden / initialisieren?
  3. Antwortet es fristgerecht auf eine leichte Anfrage?
  4. Kann die gerade angeforderte Operation gefahrlos ausgeführt werden?
  5. Ist der auf dem Bildschirm angezeigte Wert aktuell?
  6. Ist es das erwartete Gerät, Modell, die erwartete Firmware?

Je nachdem, welche dieser sechs Bedingungen erfüllt sind, ändert sich die Bedeutung von „nutzbar“.

Zum Beispiel sind die folgenden vier Zustände alle verschieden.

  • Nicht verbunden Das OS hat das Ziel-Interface von vornherein nicht gefunden
  • Verbunden / wird geprüft Physisch sichtbar, aber Initialisierung oder Authentifizierung sind noch nicht abgeschlossen
  • Verbunden / nicht nutzbar Es antwortet zwar, kann aber wegen warming up, busy, interlock, fehlendem Medium usw. nicht arbeiten
  • Wert veraltet Früher konnte abgerufen werden, aber der angezeigte Wert überschreitet das freshness budget

Fasst man all dies zu „Verbunden“ zusammen, kann der Bediener nicht entscheiden, was zu tun ist.

3. Die zuerst zu trennenden Zustände

Empfehlenswert ist: den internen Zustand mehrachsig halten und ihn für die UI bei Bedarf zusammenfassen.

3.1 Die intern zu trennenden Zustandsachsen

Achse Was sie bedeutet Typische Prüfmethode Beispiel für die UI-Anzeige
Existenz Ist das Ziel-Interface für das OS sichtbar? Aufzählung beim Start, arrival-/removal-Benachrichtigung Nicht verbunden / Verbunden
Sitzung Hat die eigene App open / login / initialize abgeschlossen? Ergebnis der Handle-/SDK-Initialisierung Wird geprüft / Wird initialisiert
Antwortverhalten Antwortet es auf Status-Abfrage oder Heartbeat? Leichte Anfrage mit Timeout Antwortet / Antwort verzögert / Keine Antwort
Betriebsbereitschaft Ist die eigentliche Operation gerade möglich? Gerätespezifischer Status Nutzbar / busy / warming up
Datenaktualität Ist der angezeigte Wert aktuell? Timestamp / Sequenz Aktuell / Wert veraltet
Konfigurationsübereinstimmung Stimmt es mit dem erwarteten Gerät überein? Modell / Seriennummer / Firmware / Profil Zielgerät / Unerwartetes Gerät
Überwachungsintegrität Lebt der Überwachungspfad der App noch? Worker-Heartbeat / Loop-Verzögerung Wird überwacht / Überwachung gestoppt

Wichtig ist hier, einen schlechten Gerätezustand von einem für die App nicht beobachtbaren Zustand zu trennen.

3.2 Die UI muss nicht alles gleichrangig zeigen

Ein mehrachsiger interner Zustand klingt danach, als würde der Bildschirm unübersichtlich. Aber die UI muss nicht alles mit gleichem Gewicht anzeigen.

Empfehlenswert sind drei Ebenen.

  • Oben der Zusammenfassungszustand
  • Darunter der Grund
  • Bei Bedarf ein Detailpanel

Zum Beispiel:

  • Zusammenfassung: Verbunden / nicht nutzbar
  • Grund: Warming up Noch etwa 18 Sekunden
  • Detail: Modell Seriennummer Firmware letzter Heartbeat letzte Frame-Zeit

Trennt man so, lässt sich die Informationsmenge erheblich erhöhen, ohne die Lesbarkeit zu verschlechtern.

Das Grundgerüst des Bildschirms sieht dann etwa so aus.

+-- Kamera vorgelagerter Prozess -----------------------------+
|
| [Zusammenfassung]  ! Verbunden / nicht nutzbar
| [Grund]              Warming up - noch etwa 18 Sekunden
|
| [Detail]  v ausklappen                   (standardmäßig eingeklappt)
|           Modell          ACME-CAM-2000
|           Seriennummer    A1B2C3
|           Firmware        2.4.1
|           letzter Heartbeat  10:23:41.512   (vor 0,5 s)
|           letztes Frame      10:23:41.402   (vor 0,6 s)
|
+---------------------------------------------------------------+

Wichtig ist: je weiter unten, desto weniger Leser dürfen es sein. Die Zusammenfassung liest jeder in einer Sekunde, den Grund liest, wer wissen will, „warum es steht“, und das Detail öffnet nur, wer die Ursache eingrenzt. Mit dieser Prämisse angeordnet, wird der Bildschirm auch bei umfangreicherem Detail nicht unruhig.

Zeigt man umgekehrt Modell oder Seriennummer dauerhaft in derselben Größe wie die Zusammenfassung, geht die wichtigste Zeile darin unter.

4. Best Practices für die Zustandsprüfung

4.1 Aufzählung beim Start und Ankunfts-/Entfernungsbenachrichtigungen

Die Grundlage für den Umgang mit externen Geräten unter Windows ist, beim Start die vorhandenen Geräte aufzuzählen und danach arrival-/removal-Benachrichtigungen zu empfangen.

Besonders wichtig sind diese drei Punkte.

  • Benachrichtigungen allein erfassen keine bereits vorhandenen Geräte
  • Für runtime communication ist eine interface class natürlicher als eine setup class
  • Die Reihenfolge von remove-Benachrichtigung und I/O-Fehler kann sich vertauschen

Als praktische Regel ist es einfach.

  1. Beim Start aufzählen
  2. Benachrichtigungen abonnieren
  3. Bei Erhalt einer Benachrichtigung erneut aufzählen und den internen Zustand abgleichen (reconcile)

4.2 „Existiert“, „lässt sich öffnen“, „antwortet“ und „ist nutzbar“ trennen

Störfälle bei externen Geräten häufen sich, wenn man dies zusammen behandelt.

  • Existiert Das interface ist für das OS sichtbar
  • Lässt sich öffnen Handle bzw. Sitzung lassen sich ohne Konflikt mit einem anderen Prozess oder Berechtigungsprobleme halten
  • Antwortet Es antwortet innerhalb des Timeouts auf eine leichte Anfrage
  • Ist nutzbar Die eigentliche Operation kann entgegengenommen werden

Diese vier sind nicht dasselbe.

4.3 event und poll kombinieren

Statt sich vollständig auf eine reine event- oder eine reine poll-Basis festzulegen, ist in der Praxis Erkennung per event, Integritätsprüfung per poll gut handhabbar.

  • arrival / removal: event
  • heartbeat / status query: poll
  • Aktualitätsbeurteilung: timestamp / sequence

Mit dieser Aufteilung lassen sich Verbindungserkennung und tatsächliche Nutzbarkeit leichter trennen.

4.4 Überwachungsprozess und UI trennen

Führt man open / read / status query direkt im UI-Thread aus, vermischen sich Anzeigebelange und Überwachungsbelange leicht.

Empfehlenswert ist folgende Form:

  • Ein Überwachungs-Worker aktualisiert einen state store
  • Die UI abonniert den state store und rendert
  • UI-Bedienungen werden als command an die Überwachungsschicht übergeben

Damit lassen sich „Überwachung gestoppt“ und „Gerät gestoppt“ leichter getrennt behandeln.

Als Grundgerüst in C# geschrieben, sieht das etwa in diesem Umfang aus (unter der Annahme von .NET 8 / C# 12). Der Trick ist eine Einbahnstraße: Nur der Überwachungs-Worker schreibt, die UI liest nur und zeichnet.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

public enum DeviceAvailability
{
    Unknown,        // Noch kein einziges Mal beobachtet
    Absent,         // Interface für das OS nicht sichtbar
    Initializing,   // open / login / initialize läuft
    Ready,          // Operation kann entgegengenommen werden
    Unavailable,    // Antwortet, ist aber wegen busy / warming up usw. nicht nutzbar
    NotResponding,  // Heartbeat antwortet nicht
    Mismatched,     // Unerwartetes Gerät / Firmware
}

// Unveränderlicher Snapshot für die UI. Als record, damit Unterschiede über Werte-Gleichheit erkannt werden
public sealed record DeviceSnapshot(
    string DeviceKey,                 // Robuster Schlüssel, z. B. die Seriennummer
    string DisplayName,
    DeviceAvailability Availability,
    string Reason,                    // Grund wie „Warming up“
    DateTimeOffset? LastSuccessAt,    // Zeitpunkt der letzten erfolgreichen Beobachtung
    long Sequence,                    // Vom Gerät vergebene Sequenznummer
    string FirmwareVersion);

public sealed class DeviceStateStore
{
    private readonly ConcurrentDictionary<string, DeviceSnapshot> _snapshots = new();

    public event Action<DeviceSnapshot>? Changed;

    // Wird nur vom Überwachungs-Worker aufgerufen
    public void Publish(DeviceSnapshot snapshot)
    {
        _snapshots.TryGetValue(snapshot.DeviceKey, out var previous);
        _snapshots[snapshot.DeviceKey] = snapshot;

        // Nur benachrichtigen, wenn sich der Wert geändert hat. Bei jedem poll zu
        // benachrichtigen würde die UI unnötig neu rendern
        if (previous != snapshot)
        {
            Changed?.Invoke(snapshot);
        }
    }

    public IReadOnlyList<DeviceSnapshot> Current() => _snapshots.Values.ToList();
}

Die UI-Seite kapselt das Abonnieren und das Zurückwechseln auf den UI-Thread an einer einzigen Stelle.

using System;
using System.Windows.Forms;

public sealed class DeviceStatusPresenter
{
    private readonly DeviceStateStore _store;
    private readonly Control _uiContext;   // Anker für den Wechsel zurück auf den UI-Thread
    private readonly Label _summary;
    private readonly Label _reason;

    public DeviceStatusPresenter(DeviceStateStore store, Control uiContext, Label summary, Label reason)
    {
        _store = store;
        _uiContext = uiContext;
        _summary = summary;
        _reason = reason;
        // Das Abonnement an die Lebensdauer des Bildschirms binden. So verwandelt sich ein
        // vergessener Dispose-Aufruf nicht in „es wird weiter an das geschlossene Formular gesendet“
        _uiContext.Disposed += (_, _) => Dispose();
        _store.Changed += OnChanged;      // Vergisst man das Abonnieren, ändert sich der Bildschirm trotz Update nicht
    }

    public void Dispose() => _store.Changed -= OnChanged;

    private void OnChanged(DeviceSnapshot snapshot)
    {
        // Der Überwachungs-Worker läuft auch weiter, während der Bildschirm geschlossen wird.
        // Ein BeginInvoke nach dem Zerstören des Handles wirft eine Ausnahme, und diese
        // Ausnahme läuft über Changed?.Invoke zurück zum Überwachungs-Worker.
        // Das äußert sich als „beim Schließen des Bildschirms stoppte die Überwachung“
        if (_uiContext.IsDisposed || _uiContext.Disposing || !_uiContext.IsHandleCreated)
        {
            return;
        }

        try
        {
            if (_uiContext.InvokeRequired)
            {
                _uiContext.BeginInvoke(() => Render(snapshot));
                return;
            }

            Render(snapshot);
        }
        catch (ObjectDisposedException)
        {
            // Wurde nach der obigen Prüfung geschlossen. Diese Lücke lässt sich prinzipiell
            // nicht schließen, daher wird die Anzeige aufgegeben. Die Überwachung wird nicht gestoppt
        }
        catch (InvalidOperationException)
        {
            // Handle nicht erstellt / bereits zerstört. Ebenso
        }
    }

    private void Render(DeviceSnapshot snapshot)
    {
        _summary.Text = snapshot.Availability switch
        {
            DeviceAvailability.Absent => "Nicht verbunden",
            DeviceAvailability.Initializing => "Verbunden / wird geprüft",
            DeviceAvailability.Ready => "Nutzbar",
            DeviceAvailability.Unavailable => "Verbunden / nicht nutzbar",
            DeviceAvailability.NotResponding => "Keine Antwort",
            DeviceAvailability.Mismatched => "Unerwartetes Gerät",
            _ => "Wird geprüft",
        };
        _reason.Text = snapshot.Reason;
    }
}

Der Moment des Schließens ist bei diesem Aufbau die anfälligste Stelle. Changed?.Invoke(snapshot) ruft den Handler synchron aus dem Thread des Überwachungs-Workers auf. Wird nach dem Schließen des Formulars und dem Zerstören des Control-Handles BeginInvoke aufgerufen, entsteht eine Ausnahme, und diese Ausnahme läuft über Publish zurück zum Überwachungs-Worker. Das Aufräumen des Bildschirms reißt damit die Überwachung selbst mit. Und das Symptom ist „stürzt beim Beenden manchmal ab“ — die Reproduktionsbedingungen sind kaum zu fassen.

Es reicht nicht, das Abonnement nur mit Dispose zu entfernen. Hat der Worker mitten im Abmelden bereits mit Invoke begonnen, lässt sich dieser Aufruf nicht mehr stoppen. Drei Punkte sind entscheidend.

  • Das Abonnement an die Lebensdauer des Bildschirms binden. Über Control.Disposed wird das Abonnement automatisch entfernt, sodass ein vergessenes Dispose sich nicht in „sendet weiter an das geschlossene Formular“ verwandelt
  • Nicht an sich schließende oder bereits geschlossene Controls senden. IsDisposed / Disposing / IsHandleCreated prüfen und an dieser Stelle sofort verwerfen
  • Die verbleibende Lücke mit einer Ausnahme abfangen. Dass zwischen der Prüfung und BeginInvoke geschlossen wird, lässt sich prinzipiell nicht ausschließen. ObjectDisposedException und InvalidOperationException werden hier abgefangen, und nur die Anzeige wird aufgegeben. Fängt man sie nicht ab, reißt es die Überwachung mit

Der entscheidende Punkt ist die Festlegung: „Ein Anzeige-Update beim Schließen darf verworfen werden.“ Diese eine Darstellung hat keinen Wert, aber dass der Überwachungs-Worker weiterlebt, hat einen.

4.5 freshness getrennt danach beurteilen, ob Daten ankommen und ob sich der Inhalt weiterentwickelt

Die Beurteilung der Datenaktualität reicht nicht aus, wenn man nur auf den Empfangszeitpunkt schaut. Denn es gibt die Fehlerform, dass der callback vom SDK weiter eintrifft, aber der Timestamp oder die Sequenznummer des Werts stehen bleibt.

Deshalb trennt man die Aktualität des Empfangs von der Aktualität des Inhalts.

using System;

public sealed record Reading(
    long Sequence,                  // Vom Gerät vergebene Sequenznummer
    DateTimeOffset ValueTimestamp,  // Vom Gerät dem Wert zugewiesener Zeitpunkt
    DateTimeOffset ReceivedAt,      // Zeitpunkt, zu dem die App empfangen hat. Für die Anzeige
    long ReceivedTicks);            // Monoton steigender Timestamp desselben Empfangs. Für die Beurteilung

public enum Freshness
{
    Fresh,
    Stale,
    Unknown,
}

public static class FreshnessPolicy
{
    // freshness budget: wird dies überschritten, nicht mehr mit dem Gesicht von live zeigen
    public static readonly TimeSpan Budget = TimeSpan.FromSeconds(5);

    /// <param name="lastAdvancedTicks">Monoton steigender Timestamp, zu dem die Sequenznummer zuletzt weitergerückt ist</param>
    /// <param name="nowTicks">Monoton steigender Timestamp zum Zeitpunkt der Beurteilung</param>
    public static Freshness Evaluate(
        Reading? previous, Reading? current,
        long lastAdvancedTicks, long nowTicks, TimeProvider clock)
    {
        if (current is null)
        {
            return Freshness.Unknown;   // Noch nie erfolgreich abgerufen
        }

        if (clock.GetElapsedTime(current.ReceivedTicks, nowTicks) > Budget)
        {
            return Freshness.Stale;     // Es kommt schon grundsätzlich nichts mehr an
        }

        if (previous is null)
        {
            return Freshness.Fresh;     // Beim ersten Mal gibt es nichts zum Vergleichen, daher nur nach dem Empfangszeitpunkt beurteilen
        }

        if (current.Sequence < previous.Sequence)
        {
            // Die Sequenznummer ist zurückgesprungen. Neustart des Geräts, Austausch gegen ein
            // anderes Exemplar oder SDK-Reinitialisierung vermuten
            return Freshness.Unknown;
        }

        if (current.Sequence == previous.Sequence &&
            clock.GetElapsedTime(lastAdvancedTicks, nowTicks) > Budget)
        {
            // Der Empfang läuft weiter, aber der Inhalt wird nicht mehr aktualisiert
            return Freshness.Stale;
        }

        return Freshness.Fresh;
    }
}

lastAdvancedTicks wird wie folgt mit der eigenen Uhr gehalten.

public sealed class FreshnessTracker(TimeProvider clock)
{
    private readonly object _gate = new();
    private Reading? _previous;
    private long _lastAdvancedTicks;

    // Wird bei jedem Empfang aufgerufen. ReceivedAt und ReceivedTicks gehören zum selben Empfang
    public Reading Capture(long sequence, DateTimeOffset valueTimestamp) =>
        new(sequence, valueTimestamp, clock.GetLocalNow(), clock.GetTimestamp());

    public Freshness Observe(Reading current)
    {
        lock (_gate)
        {
            if (_previous is null || current.Sequence > _previous.Sequence)
            {
                _lastAdvancedTicks = current.ReceivedTicks;
            }

            var result = FreshnessPolicy.Evaluate(
                _previous, current, _lastAdvancedTicks, clock.GetTimestamp(), clock);
            _previous = current;
            return result;
        }
    }

    // Bricht der Empfang vollständig ab, wird Observe kein weiteres Mal aufgerufen.
    // Ein Timer ruft dies regelmäßig auf und beurteilt die „Alterung“ des zuletzt empfangenen Werts neu.
    // Der Zustand wird dabei nicht verändert, daher ist beliebig häufiges Aufrufen sicher
    public Freshness Reevaluate()
    {
        lock (_gate)
        {
            return FreshnessPolicy.Evaluate(
                _previous, _previous, _lastAdvancedTicks, clock.GetTimestamp(), clock);
        }
    }
}

Mit Observe allein lässt sich nicht erkennen, dass ein Gerät verstummt ist. Observe wird nur beim Empfang aufgerufen, und das dabei übergebene Reading wurde gerade eben erst erzeugt. Die Differenz zwischen ReceivedTicks und der aktuellen Zeit liegt daher fast bei null, sodass über diesen Pfad Stale nur dann entsteht, wenn „der Empfang zwar weiterläuft, die Sequenznummer aber nicht voranschreitet“. Bleibt der callback vom SDK vollständig aus — die Fehlerform, die man am dringendsten wissen möchte —, wird Observe gar nicht mehr aufgerufen, und der Bildschirm bleibt beim zuletzt berechneten Fresh stehen.

Deshalb richtet man einen von der Empfangsrate unabhängigen, periodischen Prüfpfad ein. Genau das ist das obige Reevaluate, das aus einem Timer aufgerufen wird. Der Zyklus sollte kürzer als das budget sein (bei einem budget von 5 Sekunden etwa alle 1 Sekunde). Bei gleicher Länge kann es im schlimmsten Fall fast doppelt so lange dauern, bis es bemerkt wird.

// System.Threading.Timer. Die Beurteilung läuft auch ohne Empfang weiter.
// RenderFreshness ist eine eigene Methode, die wie der Presenter aus 4.4 über denselben
// Pfad auf den Bildschirm angewendet wird.
_freshnessTimer = new Timer(
    _ => RenderFreshness(_tracker.Reevaluate()),
    null, TimeSpan.Zero, TimeSpan.FromSeconds(1));

Da Observe und Reevaluate gleichzeitig von verschiedenen Threads aufgerufen werden, wird das Innere von FreshnessTracker mit lock geschützt. Lässt man das weg, vermischen sich das Ersetzen und das Auslesen von _previous, was zu dem schwer aufzuspürenden Fehler führt, dass gelegentlich eine um eine Generation veraltete Beurteilung ausgegeben wird.

Entscheidend ist hier, nicht die Differenz zu ValueTimestamp zu bilden. ValueTimestamp ist ein Wert, der von der Uhr des Geräts vergeben wurde, und es gibt keine Garantie, dass diese mit der eigenen Uhr übereinstimmt. Zieht man beide voneinander ab, wird ein gerade eben eingetroffener Wert allein deshalb stale, weil die Geräteuhr nachgeht, und umgekehrt bleibt ein stehen gebliebener Wert dauerhaft fresh, wenn die Geräteuhr vorgeht. Das budget muss stets auf die mit der eigenen Uhr gemessene verstrichene Zeit angewendet werden. ValueTimestamp sollte nur dazu dienen, auf dem Bildschirm anzuzeigen, „von wann das Gerät den Wert angibt“, oder zusammen mit der Sequenznummer als Hinweis darauf zu dienen, dass das Gerät selbst gestoppt hat.

Und auch diese „eigene Uhr“ reicht nicht als Differenz von DateTimeOffset aus. Das ist eine Wanduhr, die durch NTP-Synchronisation, manuelle Einstellung oder die Umstellung der Sommerzeit springen kann. Läuft die Zeit zurück, wird die verstrichene Zeit negativ, und obwohl das Gerät getrennt ist, bleibt es bei Fresh. Läuft sie vor, wird ein gerade eben eingetroffener Wert in diesem Moment Stale. Bildschirme, die 24 Stunden laufen, treffen das besonders oft.

Für die budget-Beurteilung wird daher ein monoton steigender Timestamp verwendet. TimeProvider.GetTimestamp() liefert einen hochauflösenden, auf Stopwatch basierenden Wert, und GetElapsedTime(Start, Ende) liefert die verstrichene Zeit zwischen zwei Zeitpunkten (siehe die Referenzlinks in Kapitel 10; .NET 8 oder neuer). Das wanduhrbasierte ReceivedAt bleibt sicherheitshalber nur dazu, auf dem Bildschirm „Empfangen um 10:15:03“ anzuzeigen, und wird bei der Beurteilung „wie viele Sekunden sind vergangen“ nicht angefasst — diese Aufteilung ist die sichere. Ein weiterer Vorteil, TimeProvider dazwischenzuschalten, ist, dass sich für Tests eine Testuhr einsetzen lässt.

Mit diesem Aufbau kann die UI-Seite, sobald sie Freshness.Stale erhält, unmittelbar die Richtlinie aus 5.3 anwenden — „neben dem Wert das age anzeigen“, „aus der Bedienbarkeitsbeurteilung ausschließen“.

Dass Unknown getrennt von Stale gehalten wird, dient dazu, noch unbekannt nicht mit veraltet zu vermischen. Ersteres kann sich durch Abwarten klären, Letzteres bessert sich durch Abwarten nicht.

4.6 Die Geräteidentifikation stabilisieren

Verfolgt man den Zustand nur anhand rein optischer Bezeichner wie eines friendly name oder COM3, kommt es leicht zu einer Verwechslung von Geräten.

Sicherer ist es, nach Möglichkeit intern einen robusten Schlüssel zu führen, etwa:

  • serial number
  • logical device id
  • stable device path
  • die geräteseitige eigene ID

5. Best Practices für die Anzeige

5.1 Entscheidungstabelle auf einen Blick

Tatsächlicher Zustand UI-Zusammenfassung Ergänzende Anzeige
Kein interface Nicht verbunden Kabel, Stromversorgung, USB-Anschluss prüfen
interface vorhanden, wird initialisiert Verbunden / wird geprüft Wird initialisiert, authentifiziert, aufgewärmt
Antwortet, Betriebsbedingung nicht erfüllt Verbunden / nicht nutzbar busy, kein Medium, interlock offen
Antwortet, Wert veraltet Verbunden / Wert veraltet Letztes Update vor 12 Sekunden
Keine Antwort Keine Antwort Wird neu verbunden, Kommunikations-Timeout
Unerwartetes Gerät Unerwartetes Gerät Modell / Seriennummer / Firmware stimmen nicht überein
Überwachung gestoppt Überwachungsstörung Überwachungs-Worker gestoppt, Neustart nötig

5.2 Formulierung als „Zustand + Grund + nächste Handlung“

Fehler oder Störung allein sind als Anzeige schwach. Formuliert man die Meldung entlang dieser drei Elemente, ist der Bediener deutlich weniger ratlos.

  • Zustand: Was passiert
  • Grund: Warum diese Beurteilung erfolgt ist
  • Nächste Handlung: Was zu tun ist

Zum Beispiel:

  • Verbunden / nicht nutzbar - Warming up - Bitte etwa 18 Sekunden warten
  • Keine Antwort - Heartbeat-Timeout - Kabel und Stromversorgung prüfen
  • Unerwartetes Gerät - Firmware 2.1.0 erforderlich - Zielgerät prüfen

Vergleicht man das mit Formulierungen, die in der Praxis häufig anzutreffen sind, wird deutlich, was jeweils fehlt.

Häufige unzureichende Formulierung Was fehlt Beispiel für eine bessere Formulierung
Fehler Weder Zustand noch Grund noch nächste Handlung Keine Antwort - Heartbeat-Timeout - Kabel und Stromversorgung prüfen
Verbunden Zustand mehrdeutig. Es bleibt unklar, ob es gerade nutzbar ist Verbunden / nicht nutzbar - Warming up - Bitte etwa 18 Sekunden warten
Gerät wurde nicht gefunden Kein Grund, keine nächste Handlung Nicht verbunden - Das Ziel-Interface ist nicht aufgelistet - Kabel und Stromversorgung prüfen
Fehler 0x80070005 ist aufgetreten Kein für Menschen lesbarer Zustand, keine Handlung Nicht nutzbar - Port kann nicht geöffnet werden. 0x80070005 Zugriff verweigert - Prüfen, ob eine andere App denselben Port verwendet
Es wird erneut versucht ... Unklar, bis wann, wie oft, was als Nächstes passiert Wiederverbindung Versuch 3 / max. 10 - Nächster Versuch in 8 Sekunden - Auch manuelles Wiederverbinden möglich
Normal Unklar, ab welchem Zeitpunkt normal Nutzbar - Letztes Update vor 0,5 Sekunden

Gemeinsam ist den verbesserten Formulierungen: Kann der Bediener allein anhand dieses Bildschirms die nächste Handlung entscheiden? Bleibt einem nur „Bitte den Support kontaktieren“ übrig, dann fehlt nicht die Formulierung, sondern das Zustandsdesign selbst.

5.3 Veraltete Daten nicht verbergen

Ein last known value ist nützlich. Es ist aber sicherer, ihn nicht mit dem Gesicht eines live value zu zeigen.

Empfehlenswert ist:

  • Ein Timestamp neben dem Wert
  • Eine age-Anzeige für den Wert
  • Farbe oder Label ändern, sobald er stale wird
  • Nach Ablauf einer bestimmten Zeit aus der Bedienbarkeitsbeurteilung ausschließen

5.4 Anzeigeort je nach Wichtigkeit wechseln

Die status bar ist praktisch, wird aber leicht übersehen. Eine critical-Störung nur in der Ecke der status bar zu platzieren, sollte vermieden werden.

  • Geringfügige Zustandsänderung: status bar
  • Hinweis, bei dem die Arbeit fortgesetzt werden kann: inline notice
  • Störung, die einen Bedienstopp erfordert: Hauptanzeigebereich, Dialog, Banner

Diese Aufteilung ist naheliegend.

5.5 Bei der Anzeige mehrerer Geräte Zusammenfassung und Detail trennen

Bei Bildschirmen, die mehrere Geräte behandeln, wird es unübersichtlich, wenn ständig alle Details aller Geräte angezeigt werden.

  • Oben eine Gesamtübersicht
  • Darunter eine Zeile pro Gerät
  • Bei Auswahl ein Detailpanel

In dieser dreistufigen Anordnung lassen sich Gesamtüberblick und Einzelfalleingrenzung gut miteinander vereinen.

Das Grundgerüst des Bildschirms sieht dann so aus.

+-- Geräteliste ----------------------------------------------+
|
| [Gesamtübersicht]  Nutzbar 6 / 8      Hinweis 1      Störung 1
|
| [Zeile pro Gerät]
|     Zustand        Anzeigename         Grund                 Letztes Update
|     --------        ---------------    ----------------      --------
|     Nutzbar         Kamera vorgel.      -                     vor 0,5 s
|     Nutzbar         Labeldrucker        -                     vor 1,2 s
|   > Nicht nutzbar    Prüfkamera         Warming up             vor 0,6 s   <- ausgewählt
|     Keine Antwort   Barcode-Leser       heartbeat timeout     vor 48 s
|
| [Detailpanel]  Prüfkamera
|     serial A1B2C3 / firmware 2.4.1 / noch etwa 18 Sekunden
|     [ Wiederverbinden ]   [ Log öffnen ]
|
+----------------------------------------------------------------+

Mit diesem dreistufigen Aufbau lassen sich auf demselben Bildschirm gleichzeitig bedienen: wer nur die Gesamtübersicht ansieht (kann die Linie heute laufen), wer die Zeilen ansieht (welches Gerät steht) und wer das Detailpanel ansieht (was zu tun ist, um es zu beheben).

Für die Reihenfolge der Zeilen ist es handhabbarer, Störungen nach oben zu ziehen. Springt die Reihenfolge jedoch jede Sekunde um, kommt es zu Fehlklicks. Die Neusortierung sollte daher erst nach dem in 6.2 beschriebenen Glätten von Flapping, auf Basis des endgültigen Zustands, erfolgen.

6. Best Practices für Wiederverbindung und Betrieb

6.1 Wiederverbindung mit Backoff versehen

Es ist sicherer, bei einer Wiederverbindung nach einem Antwortstopp nicht in der kürzestmöglichen Schleife immer wieder anzuklopfen.

  • Es belastet Gerät / Treiber / SDK
  • Die Logs laufen über
  • Vorübergehende Instabilität verschlimmert sich
  • Die UI flackert stark

Realistisch ist:

  • Beim ersten Mal sofort retry
  • Falls das nicht klappt, das Intervall stufenweise verlängern
  • Eine Obergrenze setzen
  • Zusätzlich einen manuellen Wiederverbinden-Button anbieten

Stellt man Zustände und Übergangsbedingungen als Diagramm dar, wird sichtbar, wo Backoff und die Obergrenze für Wiederverbindungen wirken.

Auch nach Aufzählung nicht gefundenAufzählung beim Start / AnkunftsbenachrichtigungAnkunftsbenachrichtigungEntfernungsbenachrichtigung (aus jedem Zustand)UnknownAbsentPresentopen / login / initializeInitialisierung und Konfigurationsprüfung erfolgreichInitialisierung fehlgeschlagen (Wiederholung möglich)Unerwartetes Gerät / Modell / Firmwarewarming up / läuft / interlockOperation kann entgegengenommen werdenI/O-FehlerI/O-Fehler / Ausbleiben der Antwort bestätigtBackoff einlegenWartezeit abgelaufenObergrenze der Wiederverbindungen erreichtManuelle WiederverbindungManuelle Wiederverbindung (kehrt automatisch nicht zurück)Nach Korrektur der Konfiguration manuell wiederverbindenDetectedOpeningReadyFaultMismatchBusyReconnectingRetryExhausted

Abbildung 2: Dieses Diagramm behandelt nur die Lebensdauer von Verbindung und Sitzung, die Datenaktualität ist nicht enthalten. Aktualität ist eine von der Betriebsbereitschaft unabhängige Achse (Abbildung 1) — sowohl in Ready als auch in Busy kann sie in gleicher Weise veralten. Vermischt man sie mit dem Zustand, entstehen Verwechslungen wie „während des Betriebs riss der Wert ab, und nach der Rückkehr war es bereits Ready“. Auf dem Bildschirm werden der Zustand aus diesem Diagramm und die Aktualitätsbeurteilung aus Abschnitt 4.5 getrennt gehalten und kombiniert. Auch bei Erreichen der Wiederholungsobergrenze wird nicht auf Absent (nicht verbunden) zurückgesetzt, sondern im nicht automatisch zurückkehrenden RetryExhausted verharrt — das Gerät kann sichtbar bleiben und dennoch defekt sein, und die Anzeige „nicht verbunden“ würde zu einer falschen Wiederherstellungsmaßnahme verleiten. Auch eine Konfigurationsabweichung lässt sich nicht durch Wiederholung beheben, weshalb sie aus dem Kreis der automatischen Wiederverbindung ausgeschlossen ist

6.2 Flapping glätten

Bei Situationen wie einem USB-Wackelkontakt oder kurzen Netzwerkunterbrechungen springt der Zustand in kurzer Zeit hin und her. Zeigt man hier die rohen Events unverändert in der UI, wird das ziemlich unübersichtlich.

Deshalb ist diese Aufteilung handhabbar:

  • Das interne Log behält die rohen Events unverändert
  • Die UI wartet eine kurze Bestätigungsphase ab, bevor sie die Anzeige festlegt
  • Kritische Störungen werden jedoch sofort angezeigt

6.3 Die mindestens aufzubewahrenden Logs

Die Verbesserung der Zustandsanzeige geht praktisch Hand in Hand mit dem Log-Design.

Feld Beispiel
Timestamp 2026-03-20T10:23:41.512+09:00
Robuster Geräteschlüssel camera:A1B2C3
Anzeigename Kamera vorgelagerter Prozess
Alter Zustand -> neuer Zustand Ready -> Stale
Grund heartbeat timeout firmware mismatch
Fehlercode HRESULT Win32 SDK code
Letzter Erfolg 2026-03-20T10:23:36.011+09:00
Age / RTT 5,5 s 320 ms
Anzahl Wiederholungen 3
App-/Firmware-Version App 1.8.2 / FW 2.4.1

Besonders wichtig ist das Zustandsübergangslog.

6.4 Überwachungsstopp nicht mit Gerätestopp verwechseln

  • Die poll loop ist an einer Ausnahme gestorben
  • Der SDK-callback ist ausgeblieben
  • Der acquisition worker ist in ein deadlock geraten
  • Nur die Aktualisierung des state store ist stehen geblieben

In solchen Fällen kann das Gerät noch leben, aber die App kann es nicht mehr beobachten. Zeigt man diesen Zustand nur als Nicht verbunden oder Keine Antwort an, sieht es wie ein geräteseitiges Problem aus.

Deshalb ist es besser, die Integrität des Überwachungspfads als eigene Achse zu führen.

7. Leicht übersehene Punkte nach Gerätetyp

7.1 USB-/PnP-Geräte

  • Benachrichtigungen allein erfassen keine bereits vorhandenen Geräte
  • Zur Laufzeit ist eine interface class natürlicher als eine setup class
  • Ein composite device kann mehrere interfaces bereitstellen
  • Die Reihenfolge von remove-Benachrichtigung und I/O-Fehler kann sich vertauschen

7.2 Serielle Geräte

Dass COMx sichtbar ist, ist noch kein Grund zur Beruhigung.

  • Der Port selbst existiert, aber das Zielgerät hängt nicht daran
  • Ein anderer Prozess hat ihn geöffnet
  • Es antwortet schon lange nicht mehr
  • read / write bleiben durch Timeout hängen

Bei seriellen Geräten ist es besonders sicher, Existenz, Antwort und Nutzbarkeit getrennt zu halten.

7.3 Netzwerkgeräte

Ein erfolgreicher ping sollte nicht mit der Nutzbarkeit für die App gleichgesetzt werden.

  • Lässt sich der Name auflösen?
  • Lässt sich eine TCP-Verbindung herstellen?
  • Gelingt der Handshake auf Anwendungsschicht?
  • Ist der Status ready?
  • Ist der Wert fresh?

Es gibt hier mehrere Stufen.

7.4 SDK-abhängige Kameras / Messgeräte

Es ist sicherer, nicht allein deshalb, weil callbacks vom SDK eintreffen, von live auszugehen.

  • Der callback thread selbst bleibt stehen
  • Frames kommen an, aber der Timestamp schreitet nicht voran
  • Der image stream kommt an, aber der control channel ist tot
  • Die Neuanwendung der Konfiguration nach einem reconnect ist noch nicht abgeschlossen

Da so etwas vorkommt, gibt zusätzliche Sicherheit, auch eine Integritätsprüfung von außerhalb des SDKs zu führen.

8. Was man nicht tun sollte

  • Den Zustand auf Verbunden / Nicht verbunden / Fehler zusammenfassen
  • Annehmen, dass Benachrichtigungen allein auch bereits vorhandene Geräte erfassen
  • Einen erfolgreichen open unmittelbar als Nutzbar werten
  • Einen last known value mit dem Gesicht von fresh zeigen
  • Den Timestamp nicht anzeigen
  • open / read / status query im UI-Thread ausführen
  • retry in kürzestmöglicher Schleife durchführen
  • Eine critical-Störung nur in der status bar anzeigen
  • Nicht verbunden mit Überwachung gestoppt verwechseln
  • Ein Gerät nur über friendly name oder COM3 identifizieren

9. Zusammenfassung

Wirklich wichtig bei Anwendungen zur Anbindung externer Geräte ist es, festzulegen, was man geprüft haben muss, um wie viel sagen zu dürfen.

Besonders wirksam ist diese Trennung:

Es ist vorhanden Die eigene App kann es öffnen Es antwortet Die Operation ist jetzt möglich Der angezeigte Wert ist aktuell

Diese fünf Punkte trennen.

Darauf aufbauend lauten die praktischen Leitlinien grob:

  • Beim Start aufzählen, danach Benachrichtigungen
  • Nutzbarkeit anhand von Heartbeat und gerätespezifischem Status entscheiden
  • Angezeigten Werten Timestamp und age mitgeben
  • Kritische Störungen an einem schwer zu übersehenden Ort anzeigen
  • Überwachungsstörungen nicht als Gerätestörung erscheinen lassen

Wichtiger als sagen zu können „Verbunden“ ist in der Praxis, wie selten diese Anzeige von der Realität abweicht.

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Windows-App-Entwicklung

In Anwendungen zur Anbindung externer Geräte hängt die Betriebsqualität nicht nur von der Kommunikationsverarbeitung ab, sondern auch von der Konsistenz zwischen Zustandsverwaltung und UI-Anzeige. Wird dies bereits in der Entwurfsphase geordnet, sinkt die Zahl der Störfälle.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Warum reicht es nicht aus, den Gerätezustand nur als „Verbunden“ anzuzeigen?
Weil das Wort „Verbunden“ mehrere Fragen stillschweigend zu einer zusammenfasst: Ist das Gerät für das OS sichtbar, konnte die eigene App es öffnen, antwortet es, kann jetzt tatsächlich bedient werden, sind die angezeigten Werte aktuell, und handelt es sich um das erwartete Gerät. „Nicht verbunden“, „verbunden / wird geprüft“, „verbunden / nicht nutzbar“ und „Wert veraltet“ sind alles unterschiedliche Zustände. Werden sie alle auf „Verbunden“ zusammengefasst, kann der Bediener nicht entscheiden, was zu tun ist.
Wie sollte der Zustand eines externen Geräts intern aufgeteilt gehalten werden?
Getrennt gehalten werden sollten Existenz (ist es für das OS sichtbar), Sitzungsaufbau (ist open/login/initialize abgeschlossen), Antwortverhalten (antwortet es auf einen Heartbeat), Betriebsbereitschaft (kann jetzt eine Operation entgegengenommen werden), Datenaktualität (sind die angezeigten Werte aktuell), Konfigurationsübereinstimmung (ist es das erwartete Gerät bzw. die erwartete Firmware) und Überwachungsintegrität (lebt der Überwachungsprozess selbst noch). Grob gesagt: Die Existenzprüfung liegt beim OS, die Nutzbarkeit bei der App und die Aktualitätsbeurteilung beim Bildschirm. Die UI wird lesbarer, wenn man sie in drei Ebenen — Zusammenfassung, Grund, Detail — gliedert.
Sollte die Geräteerkennung und die Prüfung der Betriebsintegrität über Events oder Polling erfolgen?
Statt sich vollständig auf eine der beiden Varianten festzulegen, ist in der Praxis die Aufteilung Erkennung per Event, Integritätsprüfung per Polling gut handhabbar. Konkret: arrival/removal werden über Event-Benachrichtigungen empfangen, Heartbeat oder Status-Abfragen erfolgen per regelmäßigem Polling, und die Aktualität wird über Timestamp oder Sequenznummer beurteilt. Da Benachrichtigungen allein bereits vorhandene Geräte nicht erfassen, gilt in der Praxis die Regel: beim Start aufzählen, danach Benachrichtigungen abonnieren, und bei Erhalt einer Benachrichtigung erneut aufzählen und den internen Zustand abgleichen (reconcile).
Wie sollte die Wiederverbindung zu einem nicht mehr antwortenden Gerät implementiert werden?
Nicht in der kürzestmöglichen Schleife immer wieder anklopfen, sondern mit Backoff versehen. Realistisch ist: beim ersten Mal sofort erneut versuchen, danach die Intervalle stufenweise verlängern, eine Obergrenze setzen und zusätzlich einen manuellen „Wiederverbinden“-Button anbieten. Ein Vorgehen in kürzestmöglicher Schleife belastet Gerät und SDK, lässt die Logs überlaufen und verschlimmert vorübergehende Instabilität. Bei Flapping — etwa durch einen wackligen USB-Kontakt, bei dem der Zustand in kurzer Zeit hin- und herspringt — hilft es, auf der UI-Seite eine kurze Bestätigungsphase abzuwarten, bevor die Anzeige endgültig festgelegt wird.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog