Best Practices for Checking and Displaying External Device State - Designing Beyond a Single 'Connected'

· Updated: · · Windows, External Devices, Device Integration, State Management, UI/UX, Monitoring

Revision history (1 updates, last updated Sep 1, 2026)

A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.

Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614560)
First published
Cite this article(DOI: 10.5281/zenodo.21614559)

This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.

Go Komura (2026). Best Practices for Checking and Displaying External Device State - Designing Beyond a Single 'Connected'. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614559 https://comcomponent.com/en/blog/2026/03/20/002-external-device-state-check-display-best-practices/

DOI (latest version)
10.5281/zenodo.21614559
DOI (this version)
10.5281/zenodo.22218882

Industrial cameras, barcode readers, PLCs, measurement instruments, printers, serial devices, USB devices. In Windows apps that talk to external devices, the on-screen state display drifting away from reality breaks things far more often than an actual defect itself does, and it breaks them first.

For example, states like these.

  • The OS can see the device, but another process holds it and it cannot be used
  • open succeeded, but homing, warm-up, or authentication has not finished
  • The device is still attached, but it has stopped responding
  • The acquisition thread has died, yet the last value is still sitting on the screen
  • It is an unexpected unit or firmware, but the screen simply says “Connected”

What you really want to know here is not just whether it is connected. It is what can safely be done right now.

What you want to know is whether an operation is safeDiagram showing that what you really want to know in a device-integration app is not only whether the device is connected but what can safely be done right now, and that things break because the on-screen state display drifts from reality before any actual defect does.Is it connectedOnly part of what you need to knowWhat can I safely do right nowWhat you really want to knowA display that drifts from reality breaks things first

Figure 1: The point of a state display is not to report connectivity but to tell the operator which operations are safe right now.

Who this article is for, and what it assumes

Item Details
Intended readers Anyone designing or implementing Windows apps that talk to external devices. In particular, anyone trying to cut down support calls of the form “the screen says Connected but nothing works” in an existing app
Assumed knowledge Being able to write an application in some language. No knowledge of any particular SDK or device driver is assumed
Assumed environment Windows desktop applications. That said, the way state is split up and the display principles themselves do not depend on the OS
Out of scope How to call a specific vendor SDK, and driver-side implementation

Terms used in this article

Here is a one-line gloss for each term before we start.

Term One-line meaning
PLC Programmable Logic Controller. An industrial controller used to control production equipment
firmware Software embedded in the device. Even units with the same model number can be on different versions
heartbeat A lightweight query or notification exchanged periodically to confirm the peer is alive
poll / event Polling means we query periodically; an event means we wait to be notified by the other side
stale A value that is old. Acquisition worked at some point, but there is no basis for calling the value now on screen current
freshness budget The upper bound you fix in advance: past this much time, a value no longer counts as current
flapping State bouncing back and forth over a short window. Caused by flaky contacts or momentary drops
reconcile Cross-checking and realigning internal state with what is actually there
interlock A safety mechanism that halts motion. The equipment will not move while it is open
PnP Plug and Play. The OS mechanism that detects and configures device connection and removal
RTT Round-trip time. The time from issuing a query to receiving the response

1. The Conclusion First

The single most effective thing in checking and displaying external device state is not collapsing the state into one boolean.

At minimum, you want to keep these separate.

  • Presence: is it visible to the OS?
  • Session established: has our app completed open / login / initialize?
  • Responsiveness: does it answer heartbeats or status queries?
  • Operational readiness: can it accept the actual operation right now?
  • Data freshness: are the on-screen values recent?
  • Configuration match: is it the expected unit, model, firmware?
  • Monitoring health: is the monitoring pipeline itself alive?

Put very bluntly:

Presence belongs to the OS side, usability to the app side, and freshness to the display side.

Just keeping these three unmixed makes the state display considerably more stable.

What the display side answersWhat only the app can answerWhat the OS can answerIf this is off, nothing else holdingmakes the device usableIf this has stopped,every judgment goes staleData freshnessAre the displayed values recentMonitoring healthIs the monitoring code itself aliveSessionHas open / login / initialize completedResponsivenessDoes a lightweight query return in timeOperational readinessCan it accept an operation right nowConfiguration matchIs it the expected unit, model, firmwarePresenceIs the target interface visible

Figure 2: Do not collapse state into a single boolean; hold it split by who can answer the question. A tier holding does not mean the tier below it holds

Knowledge map for this article

In a Windows app that integrates with external devices, the backbone of the design is to hold seven state axes separately inside the app, namely presence, session establishment, responsiveness, operational readiness, data freshness, configuration match, and monitoring health. Collapsing them into a single Connected indicator leaves the operator unable to decide the next move, so the UI shows them in three layers of summary, reason, and details, with wording that gives the state plus the reason plus the next action. Data freshness is judged with a monotonically increasing timestamp and a freshness budget rather than the wall clock, which avoids wrong judgments caused by clock skew against the ValueTimestamp coming from the device. The monitoring worker and the UI are separated through a state store, the subscription is tied to Control.Disposed so that a BeginInvoke call issued while the window is being torn down does not drag the monitoring worker down with it, reconnection is done with backoff, and flapping is smoothed out before the display commits to a state.

Checking and displaying external device stateDiagram showing how holding seven separate state axes of presence, session establishment, responsiveness, operational readiness, data freshness, configuration match, and monitoring health prevents a single Connected indicator from leading the operator to a wrong judgmentmay causerequiresrequiresrequiresrequiresrequiresrequiresrequirespreventsmitigatesmitigatesshould come beforeverified byverified byverified byrequiresmay causemay causepreventsmay causemay causepreventsmay causemitigatespreventsmay causemitigatesrequirespreventsverified byrequiresMulti-Axis Device State ModelCollapsing Device State into ConnectedUnclear Next Action for OperatorExistence (State Axis)Session (State Axis)Responsiveness (State Axis)Readiness (State Axis)Freshness (State Axis)Identity Match (State Axis)Monitoring Health (State Axis)Three-Tier Status PanelStatus, Reason, Next Action WordingDevice Enumeration at StartupDevice Arrival/Removal NotificationsHeartbeat PollingFreshness BudgetMonotonic TimestampWall-Clock Time JumpsFreshness MisjudgmentClock Skew Against Device ValueTimestampStale Data Shown as LiveUnstable Device IdentificationDevice MisidentificationReconnect with BackoffReconnect Storm (Tight Retry Loop)Flapping DebounceMonitoring Worker / UI SeparationMonitoring Work on the UI ThreadBeginInvoke Race During Form DisposalMonitoring Worker Killed by UI ExceptionSubscription Bound to Control.DisposedStable Device Identity Key

In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (31 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle

2. Why “Connected” Is Dangerous

The word “Connected” silently takes on multiple meanings in a single label.

In reality, at least these questions are mixed together.

  1. Can the OS see the device’s interface?
  2. Has our app managed to open / login / initialize the device?
  3. Does it answer a lightweight query within the deadline?
  4. Can the operation we just requested be executed safely right now?
  5. Are the values on screen recent?
  6. Is it the expected unit, model, and firmware?

Which of these six are satisfied changes what “usable” means.

For instance, the following four are all different.

  • Not connected The OS has not found the target interface in the first place
  • Connected / Verifying Physically visible, but initialization or authentication has not finished
  • Connected / Not available Responding, but unable to operate due to warming up, busy, an interlock, no media, and so on
  • Stale value Acquisition used to work, but the on-screen value has exceeded the freshness budget

Collapse all of these into “Connected” and the operator cannot decide what to do.

The questions Connected has to carryDiagram showing that the single label Connected silently carries several questions - whether the OS can see the device, whether it answers, whether it can be operated, whether the values are recent, whether it is the expected unit - and that flattening all of them leaves the operator unable to decide what to do.Is it visible to the OSEverything collapses into ConnectedDoes it answer, can it be operatedAre the values recent, is it the expected unitThe operator cannot decide the next move

Figure 3: The single word Connected silently carries several questions with different answers.

3. The States to Separate First

Our recommendation: keep internal state multi-axis, and summarize for the UI as needed.

3.1 The state axes to keep internally

Axis What it means Typical check Example for the UI
Presence Is the target interface visible to the OS? Enumeration at startup, arrival / removal notifications Not connected / Connected
Session Has our app completed open / login / initialize? Handle / SDK initialization result Verifying / Initializing
Responsiveness Does it answer status queries or heartbeats? Lightweight query with timeout Responding / Slow response / No response
Operational readiness Is the actual operation possible right now? Device-specific status Available / Busy / Warming up
Data freshness Are the displayed values recent? Timestamp / sequence Current / Stale value
Configuration match Does it match the expected device? Model / serial / firmware / profile Target device / Unexpected device
Monitoring health Is the app’s monitoring path alive? Worker heartbeat / loop lag Monitoring / Monitoring stopped

What matters here is separating a device in a bad state from a state the app cannot observe.

3.2 The UI does not need to show everything flat

Holding multi-axis state internally sounds like it would make the screen noisy. But the UI does not need to present everything with equal weight.

Our recommendation is three layers.

  • A summary state at the top
  • The reason beneath it
  • A details panel when needed

For example:

  • Summary: Connected / Not available
  • Reason: Warming up About 18 seconds remaining
  • Details: model serial firmware last heartbeat last frame time

Split like this, you can add a lot of information while keeping it quite readable.

As a screen skeleton, the layout runs like this.

+-- Upstream process camera -------------------------------------------+
|
| [Summary]  ! Connected / Not available
| [Reason]     Warming up - about 18 seconds remaining
|
| [Details]  v Expand                    (collapsed by default)
|              model           ACME-CAM-2000
|              serial          A1B2C3
|              firmware        2.4.1
|              last heartbeat  10:23:41.512   (0.5 s ago)
|              last frame      10:23:41.402   (0.6 s ago)
|
+----------------------------------------------------------------------+

The point is that it is fine for fewer people to read each layer as you go down. Anyone reads the summary in one second; the reason is read by whoever wants to know why it stopped; the details are opened only by whoever is doing triage. Lay it out on that assumption and the screen stays quiet no matter how much you add to the details.

Conversely, showing model and serial permanently at the same size as the summary buries the one line that matters most.

The three layers of summary, reason, and detailsDiagram showing that the UI is split into a summary state on top, the reason beneath it, and a details panel when needed, and that laying it out on the assumption that fewer people read each layer down keeps it readable even as information is added.Summary: anyone reads it in one secondReason: whoever wants to know why it stoppedDetails: only whoever is doing triage opens itHeavy details still leave the screen quiet

Figure 4: Split into three layers on the assumption that each layer down is read by fewer people.

4. Best Practices for State Checking

4.1 Enumeration at startup, plus arrival / removal notifications

The foundation for handling external devices on Windows is to enumerate existing devices at startup and receive arrival / removal notifications afterward.

Three points worth nailing down in particular:

  • Notifications alone do not pick up devices that already exist
  • For runtime communication, interface classes are more natural than setup classes
  • Removal notifications and I/O errors can appear out of order relative to each other

The practical rule is simple.

  1. Enumerate at startup
  2. Subscribe to notifications
  3. On a notification, re-enumerate and reconcile internal state
The practical rule for enumeration and notificationsDiagram showing the practical rule that because notifications alone do not pick up devices that already exist, you enumerate at startup, subscribe to arrival and removal notifications afterward, and re-enumerate to reconcile internal state whenever a notification arrives.1. Enumerate at startup2. Subscribe to notifications3. On a notification, re-enumerate and reconcileNotifications alone miss devices that already exist

Figure 5: Track presence with all three together: enumeration, notifications, and reconciliation.

4.2 Separate “present,” “openable,” “responding,” and “usable”

Trouble with external devices increases when these are handled as one.

  • Present The interface is visible to the OS
  • Openable You can hold a handle / session without contention from another process or permission problems
  • Responding It answers a lightweight query within the timeout
  • Usable It can accept the actual operation

These four are not the same.

Present, openable, responding, and usable are differentDiagram showing that present meaning the interface is visible to the OS, openable meaning a session can be held without contention or permission problems, responding meaning a lightweight query returns within the timeout, and usable meaning the actual operation is accepted are four different things.PresentOpenableRespondingUsableThese four are not the same

Figure 6: There are four verification stages from present to usable.

4.3 Mix events and polling

Rather than committing entirely to event-based or entirely to poll-based, in practice the workable split is events for detection, polling for health checks.

  • Arrival / removal: events
  • Heartbeats / status queries: polling
  • Freshness judgments: timestamps / sequences

This division makes it easy to decouple connection detection from actual usability.

Events for detection, polling for healthDiagram showing that instead of committing entirely to one style, arrival and removal are detected through events, health is checked by periodic polling of heartbeats and status queries, and freshness is judged from timestamps or sequence numbers.Detecting arrival and removaleventheartbeat and status querypollFreshness judgmenttimestamp / sequence

Figure 7: Do not make events and polling compete; use each for the job it suits.

4.4 Separate the monitoring pipeline from the UI

If you run open / read / status queries directly on the UI thread, display concerns and monitoring concerns mix all too easily.

Our recommendation:

  • A monitoring worker updates a state store
  • The UI subscribes to the state store and renders
  • UI actions are passed to the monitoring layer as commands

This also makes it easier to treat “monitoring stopped” and “device stopped” separately.

Separate monitoring and the UI with a state storeDiagram showing that a monitoring worker updates the state store, the UI subscribes to the state store and renders, and UI actions are passed to the monitoring layer as commands, a one-way arrangement that makes it easier to treat monitoring stopped and device stopped separately.updatessubscribes and rendersactions passed as commandsMonitoring workerstate storeUIOne-way: only the worker writes

Figure 8: Put a state store between monitoring and the UI, and keep the flow one-way.

Written in C#, the skeleton comes out about this size (assuming .NET 8 / C# 12). The trick is to keep it one-way: only the monitoring worker writes, and the UI only reads and renders.

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

public enum DeviceAvailability
{
    Unknown,        // never observed even once
    Absent,         // the interface is not visible to the OS
    Initializing,   // in the middle of open / login / initialize
    Ready,          // able to accept operations
    Unavailable,    // responding, but busy / warming up and so on make it unusable
    NotResponding,  // heartbeats do not come back
    Mismatched,     // unexpected unit / firmware
}

// Immutable snapshot handed to the UI. Make it a record so value equality detects changes
public sealed record DeviceSnapshot(
    string DeviceKey,                 // a key that does not drift, such as a serial number
    string DisplayName,
    DeviceAvailability Availability,
    string Reason,                    // the reason, such as warming up
    DateTimeOffset? LastSuccessAt,    // when observation last succeeded
    long Sequence,                    // sequence number assigned by the device
    string FirmwareVersion);

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

    public event Action<DeviceSnapshot>? Changed;

    // Only the monitoring worker calls this
    public void Publish(DeviceSnapshot snapshot)
    {
        _snapshots.TryGetValue(snapshot.DeviceKey, out var previous);
        _snapshots[snapshot.DeviceKey] = snapshot;

        // Notify only when the value changed. Notifying on every poll repaints the UI for nothing
        if (previous != snapshot)
        {
            Changed?.Invoke(snapshot);
        }
    }

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

On the UI side, confine subscription and the hop back to the UI thread to a single place.

using System;
using System.Windows.Forms;

public sealed class DeviceStatusPresenter
{
    private readonly DeviceStateStore _store;
    private readonly Control _uiContext;   // foothold for getting back onto the 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;
        // Tie the subscription to the lifetime of the window, so a forgotten Dispose
        // does not turn into posting updates to a closed form forever
        _uiContext.Disposed += (_, _) => Dispose();
        _store.Changed += OnChanged;      // forget to subscribe and updates never reach the screen
    }

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

    private void OnChanged(DeviceSnapshot snapshot)
    {
        // The monitoring worker keeps running while the window is closing.
        // BeginInvoke after the handle is gone throws, and that exception
        // escapes through Changed?.Invoke into the monitoring worker.
        // The failure looks like: closing the window stopped monitoring
        if (_uiContext.IsDisposed || _uiContext.Disposing || !_uiContext.IsHandleCreated)
        {
            return;
        }

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

            Render(snapshot);
        }
        catch (ObjectDisposedException)
        {
            // Closed after passing the checks above. This gap cannot be eliminated in principle,
            // so absorb it by giving up the render. Monitoring keeps running
        }
        catch (InvalidOperationException)
        {
            // Handle not created or already destroyed. Same as above
        }
    }

    private void Render(DeviceSnapshot snapshot)
    {
        _summary.Text = snapshot.Availability switch
        {
            DeviceAvailability.Absent => "Not connected",
            DeviceAvailability.Initializing => "Connected / Verifying",
            DeviceAvailability.Ready => "Available",
            DeviceAvailability.Unavailable => "Connected / Not available",
            DeviceAvailability.NotResponding => "No response",
            DeviceAvailability.Mismatched => "Unexpected device",
            _ => "Verifying",
        };
        _reason.Text = snapshot.Reason;
    }
}

The moment the window closes is the most fragile point in this arrangement. Changed?.Invoke(snapshot) calls the handler synchronously, on the monitoring worker’s thread. Call BeginInvoke after the form has closed and the control’s handle has been destroyed and it throws, and that exception escapes through Publish into the monitoring worker. Cleaning up the screen takes down monitoring itself. Worse, the symptom is “it crashes occasionally on exit,” so the repro conditions are hard to pin down.

Unsubscribing in Dispose is not enough. If the worker has already started an Invoke while you are unsubscribing, that call can no longer be stopped. There are three things to get right.

  • Tie the subscription to the lifetime of the window. Unsubscribe automatically on Control.Disposed so that a forgotten Dispose does not turn into posting to a closed form forever
  • Do not post to a control that is being disposed or already disposed. Check IsDisposed / Disposing / IsHandleCreated and drop the update on the spot
  • Absorb the gap that remains with a catch. Nothing can rule out the window closing between the check and BeginInvoke. Catch ObjectDisposedException and InvalidOperationException here and give up only the display. Without that catch, monitoring goes down with it

The key is deciding up front that display updates during shutdown are expendable. That single render is worth nothing, whereas the monitoring worker staying alive is worth a great deal.

Taking the monitor down with the windowDiagram showing that Changed invokes handlers synchronously on the monitoring thread, so an exception from BeginInvoke after the handle is destroyed escapes through Publish into the monitoring worker and stops monitoring, which is why the subscription is tied to the lifetime of the window and the remaining gap is caught so that only the display is given up.to prevent thisThe window closesBeginInvoke after disposal throwsThe exception escapes into the monitoring workerClosing the window stopped monitoringTie the subscription to the window lifetimeCatch the remaining gap and give up only the display

Figure 9: Give up the one render at closing time, and keep the monitoring worker alive instead.

4.5 Judge freshness by separating “is it arriving” from “are the contents advancing”

Looking only at the reception time is not enough for a data freshness judgment. There is a failure mode where the callbacks from the SDK keep arriving while the value’s timestamp or sequence has stopped moving.

So separate how recent the reception is from how recent the contents are.

using System;

public sealed record Reading(
    long Sequence,                  // sequence number assigned by the device
    DateTimeOffset ValueTimestamp,  // time the device stamped on the value
    DateTimeOffset ReceivedAt,      // time the app received it. For display
    long ReceivedTicks);            // monotonic timestamp of that same reception. For judgments

public enum Freshness
{
    Fresh,
    Stale,
    Unknown,
}

public static class FreshnessPolicy
{
    // freshness budget: past this, do not show it with the face of a live value
    public static readonly TimeSpan Budget = TimeSpan.FromSeconds(5);

    /// <param name="lastAdvancedTicks">Monotonic timestamp of the last time the sequence advanced</param>
    /// <param name="nowTicks">Monotonic timestamp at the moment of the judgment</param>
    public static Freshness Evaluate(
        Reading? previous, Reading? current,
        long lastAdvancedTicks, long nowTicks, TimeProvider clock)
    {
        if (current is null)
        {
            return Freshness.Unknown;   // nothing has ever been acquired
        }

        if (clock.GetElapsedTime(current.ReceivedTicks, nowTicks) > Budget)
        {
            return Freshness.Stale;     // nothing is arriving at all
        }

        if (previous is null)
        {
            return Freshness.Fresh;     // nothing to compare against on the first one, so judge on reception time alone
        }

        if (current.Sequence < previous.Sequence)
        {
            // The sequence went backward. Suspect a device restart, a swap to another unit, or SDK re-initialization
            return Freshness.Unknown;
        }

        if (current.Sequence == previous.Sequence &&
            clock.GetElapsedTime(lastAdvancedTicks, nowTicks) > Budget)
        {
            // Data keeps arriving, but the contents are not being updated
            return Freshness.Stale;
        }

        return Freshness.Fresh;
    }
}

Hold lastAdvancedTicks on our own clock, like this.

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

    // Call on every reception. ReceivedAt and ReceivedTicks record the same reception
    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;
        }
    }

    // If reception stops completely, Observe is never called again.
    // Call this periodically from a timer to re-measure the age of the last value received.
    // It does not update state, so it is safe to call any number of times
    public Freshness Reevaluate()
    {
        lock (_gate)
        {
            return FreshnessPolicy.Evaluate(
                _previous, _previous, _lastAdvancedTicks, clock.GetTimestamp(), clock);
        }
    }
}

Observe alone cannot detect that the device has gone quiet. Observe runs only on reception, and the Reading handed to it at that moment was created a fraction of a second earlier. The gap between ReceivedTicks and now is essentially zero, so the only way this path yields Stale is the case where data keeps arriving but the sequence stops advancing. When the callbacks from the SDK stop entirely - the failure you most want to catch - Observe is never called, and the screen freezes showing the last Fresh it computed.

So provide an entry point that re-judges on a period independent of reception. That is Reevaluate above, called from a timer. Make the period shorter than the budget (roughly once a second for a 5-second budget). Match the two lengths and, in the worst case, you stay unaware for close to twice the budget.

Observe alone cannot detect silenceDiagram showing that Observe runs only on reception, so when the callbacks from the SDK stop entirely Observe is never called and the screen freezes on the last Fresh it computed, which is why Reevaluate is called on a period independent of reception to re-measure the age of the last value.the fixCallbacks stop entirelyObserve is never called againThe screen freezes on the last FreshCall Reevaluate from a timerMake the period shorter than the budget

Figure 10: The failure you most want to catch, total silence, is invisible to a reception-driven check.

// System.Threading.Timer. Keep the judgment moving even when nothing is received.
// RenderFreshness is your own method that pushes to the screen through the same path as the presenter in 4.4
_freshnessTimer = new Timer(
    _ => RenderFreshness(_tracker.Reevaluate()),
    null, TimeSpan.Zero, TimeSpan.FromSeconds(1));

Observe and Reevaluate arrive from different threads at the same time, so the internals of FreshnessTracker are guarded by a lock. Skip that and the swap of _previous interleaves with reads of it, producing a hard-to-chase bug where the judgment is occasionally one generation behind.

The key here is not to take the difference against ValueTimestamp. ValueTimestamp was stamped by the device’s clock, and nothing guarantees it agrees with ours. Subtract one from the other and a device whose clock merely runs behind makes a value that just arrived look stale, while a device running ahead keeps a frozen value looking fresh indefinitely. Always apply the budget to elapsed time measured on our own clock. Keep ValueTimestamp for showing what point in time the device claims the value is from, and for combining with the sequence number as evidence that the device side has stalled.

And even “our own clock” is not enough if it means subtracting DateTimeOffset values. That is a wall clock, and it jumps on NTP sync, manual changes, and daylight saving transitions. If the time moves backward, elapsed time goes negative and a disconnected device stays Fresh. If it moves forward, a value that just arrived turns Stale on the spot. The screens that run around the clock are the ones that hit this.

So use a monotonic timestamp for the budget check. TimeProvider.GetTimestamp() returns a high-resolution, Stopwatch-based value, and GetElapsedTime(start, end) gives the elapsed time between two of them (see the references in section 10; .NET 8 and later). Keep the wall-clock ReceivedAt only for printing “received at 10:15:03” on screen, and keep it away from any “how many seconds have passed” judgment. Being able to swap in a test clock is another benefit of going through TimeProvider.

Judge freshness with a monotonic clockDiagram showing that the wall clock jumps on NTP sync or manual changes, so subtracting wall clock readings can leave a disconnected device looking Fresh or mark a value that just arrived as Stale, which is why the budget is checked with a monotonic timestamp and the wall clock reception time is kept for display only.which is whyThe wall clock jumps on NTP or manual changesElapsed time goes negative or far too largeThe freshness judgment is wrongJudge with a monotonic timestampKeep the wall clock reception time for display only

Figure 11: Never using the wall clock for how many seconds have passed is the foundation of freshness judgment.

With this in place, the moment the UI receives Freshness.Stale it can apply the policy from 5.3 directly: show the age next to the value, and drop it from operability decisions.

Unknown is kept separate from Stale so that not known yet and old do not get mixed. The first may resolve if you wait; the second will not.

4.6 Stabilize unit identification

If you track state only by cosmetic identifiers such as friendly names or COM3, it becomes easy to confuse one unit with another.

Where possible, it is safer to internally hold keys that do not drift, such as:

  • a serial number
  • a logical device id
  • a stable device path
  • the device’s own unit ID

5. Best Practices for Display

5.1 The one-page decision table

Actual state UI summary Supplementary display
No interface Not connected Check the cable, power, and USB connection
Interface present, initializing Connected / Verifying Initializing, authenticating, warming up
Responding, operating conditions not met Connected / Not available Busy, no media, interlock open
Responding, value stale Connected / Stale value Last updated 12 seconds ago
No response No response Reconnecting, communication timeout
Unexpected unit Unexpected device Model / serial / firmware mismatch
Monitoring pipeline stopped Monitoring fault Monitoring worker stopped; restart required

5.2 Word messages as “state + reason + next action”

Error or Fault alone is weak as a display. Messages built on these three elements leave the operator far less lost.

  • State: what is happening
  • Reason: why the app reached that judgment
  • Next action: what to do

For example:

  • Connected / Not available - Warming up - Please wait about 18 seconds
  • No response - Heartbeat timeout - Check the cable and power
  • Unexpected device - Firmware 2.1.0 required - Check the target device

Lining these up against the wording you actually see in the field makes it obvious what is missing.

Common bad wording What is missing Rewrite
Error No state, no reason, no next action No response - Heartbeat timeout - Check the cable and power
Connected The state is vague. It still does not say whether the device is usable right now Connected / Not available - Warming up - Please wait about 18 seconds
Device not found No reason and no next action Not connected - The target interface is not enumerated - Check the cable and power
0x80070005 occurred No human-readable state and no action Not available - Cannot open the port. 0x80070005 access denied - Check whether another app is using the same port
Retrying... No sense of until when, how many times, or what happens next Reconnecting, attempt 3 of 10 - Next attempt in 8 seconds - You can also reconnect manually
Normal No indication of as of when it was normal Available - Last updated 0.5 seconds ago

What the rewrites have in common is this: can the operator decide the next move from that screen alone? If the best you can write is “contact support,” the shortfall is not in the wording; it is in the state design.

Word messages as state plus reason plus next actionDiagram showing that Error or Fault alone is weak and that leaning on three elements - the state saying what is happening, the reason saying why the app judged so, and the next action saying what to do - lets the operator decide the next move from that screen alone.State: what is happeningWording that leaves the operator in no doubtReason: why the app judged soNext action: what to doCan the next move be decided from this screen alone

Figure 12: You can grade a message by whether all three elements are present.

5.3 Do not hide stale data

A last known value is useful. But it is safer not to present it with the face of a live value.

Our recommendations:

  • A timestamp next to the value
  • An age display for the value
  • Change the color or label when it goes stale
  • Exclude it from operability decisions after a set time

5.4 Vary where things are shown by severity

The status bar is convenient, but easy to miss. You want to avoid putting a critical fault only in the corner of the status bar.

  • Minor state changes: status bar
  • Cautions that allow work to continue: inline notice
  • Faults requiring operations to stop: the main display area, a dialog, a banner

That division of labor is the straightforward one.

5.5 For multi-device views, separate summary from detail

In screens that handle many devices, showing full detail for every unit at all times becomes unreadable.

  • An overall summary at the top
  • A row per device below
  • A details pane on selection

This three-tier layout balances at-a-glance awareness with per-unit triage.

The screen skeleton looks like this.

+-- Device list ---------------------------------------------------------------------+
|
| [Overall summary]  Available 6 / 8      Caution 1      Fault 1
|
| [Row per device]
|     State          Display name        Reason              Last update
|     -----------    ----------------    ----------------    -----------
|     Available      Upstream camera     -                   0.5 s ago
|     Available      Label printer       -                   1.2 s ago
|   > Not available  Inspection camera   Warming up          0.6 s ago   <- selected
|     No response    Barcode reader      heartbeat timeout   48 s ago
|
| [Details pane]  Inspection camera
|     serial A1B2C3 / firmware 2.4.1 / about 18 seconds remaining
|     [ Reconnect ]   [ Open log ]
|
+------------------------------------------------------------------------------------+

This three-tier structure serves three readers at once on the same screen: whoever only looks at the overall summary (can we run the line today), whoever reads the rows (which device has stopped), and whoever opens the details pane (what will fix it).

For row ordering, bringing faults to the top works better. But if the order reshuffles every second people click the wrong row, so sort on the committed state after flapping has been smoothed out (see 6.2).

Three tiers serve three kinds of readerDiagram showing that a multi-device screen split into an overall summary at the top, a row per device below, and a details pane on selection serves, on one screen, the person asking whether the line can run today, the person asking which device stopped, and the person asking what will fix it.Overall summaryCan the line run todayRow per deviceWhich device has stoppedDetails paneWhat will fix it

Figure 13: Build a multi-device screen as three tiers for three different readers.

6. Best Practices for Reconnection and Operations

6.1 Reconnect with backoff

When a device stops responding, it is safer not to hammer reconnection in the tightest possible loop, because it

  • loads the device / driver / SDK
  • floods the logs
  • worsens transient instability
  • makes the UI jitter violently

The realistic approach:

  • Retry immediately the first time
  • If that fails, lengthen the interval in stages
  • Set an upper bound
  • Provide a manual Reconnect as well

Drawing the states and their transition conditions shows where backoff and the reconnect limit actually bite.

Not found even after enumerationStartup enumeration / arrival notificationArrival notificationRemoval notification (from any state)UnknownAbsentPresentopen / login / initializeInitialization and configuration check succeededInitialization failed (retry is still an option)Unexpected unit, model, or firmwarewarming up / running / interlockAble to accept operationsI/O errorI/O error / no response confirmedApply backoffWait elapsedReconnect limit reachedManual reconnectManual reconnect (no automatic return)Fix the configuration, then reconnect manuallyDetectedOpeningReadyFaultMismatchBusyReconnectingRetryExhausted

Figure 14: This diagram covers only the lifetime of the connection and the session; data freshness is not in it. Freshness is an axis independent of operational readiness (Figure 1), and a value can go stale the same way in Ready as in Busy, so mixing it in as a state invites confusion such as “the values cut out mid-run and it was Ready again by the time it came back.” On screen, hold the states in this diagram and the freshness judgment from section 4.5 separately and combine them. Reaching the retry limit does not drop the device to Absent (not connected); it stays in RetryExhausted, from which it never returns automatically - a device can be broken while remaining visible, and reporting it as not connected sends the operator into the wrong recovery step. A configuration mismatch is not fixed by retrying either, so it is kept out of the automatic reconnect loop

6.2 Smooth out flapping

In situations like flaky USB contacts or momentary network drops, the state bounces back and forth in a short time. Feeding raw events straight to the UI here is quite hard to read.

So the workable split is:

  • Keep raw events as-is in the internal log
  • Have the UI wait through a short confirmation window before committing the display
  • But show critical faults immediately
Smooth out flapping before committing the displayDiagram showing that when a flaky contact or momentary drop makes the state bounce back and forth, raw events are kept as-is in the internal log, the UI waits through a short confirmation window before committing a display, and only critical faults are shown immediately.State bounces back and forthRaw events go to the log unchangedThe UI waits, then commits a displayOnly critical faults are shown at once

Figure 15: Raw events go to the log; the UI gets the smoothed, committed state.

6.3 The minimum log fields to keep

Improving state display goes hand in hand with log design.

Item Example
timestamp 2026-03-20T10:23:41.512+09:00
stable device key camera:A1B2C3
display name Upstream process camera
old state -> new state Ready -> Stale
reason heartbeat timeout firmware mismatch
error code HRESULT Win32 SDK code
last success 2026-03-20T10:23:36.011+09:00
age / RTT 5.5s 320ms
retry count 3
app / firmware version App 1.8.2 / FW 2.4.1

The most important of all is the state transition log.

6.4 Do not conflate monitoring stopped with device stopped

  • The poll loop died on an exception
  • SDK callbacks stopped
  • The acquisition worker deadlocked
  • Only the state store updates stopped

In these cases, the device may be alive but the app cannot observe it. If you display this state as just Not connected or No response, it looks like a device-side problem.

So the health of the monitoring path deserves its own axis.

Do not make a stopped monitor look like a stopped deviceDiagram showing that when the poll loop dies or callbacks stop the device may be alive while the app simply cannot observe it, and reporting that as not connected or no response makes it look like a device-side problem, which is why monitoring health is held on its own axis.which is whyThe poll loop dies on an exceptionThe device is alive but unobservedCallbacks or the worker stopReporting Not connected blames the deviceHold monitoring health on its own axis

Figure 16: Do not let “cannot observe” be mistaken for “the device is broken.”

7. Easily Missed Points by Device Type

7.1 USB / PnP devices

  • Notifications alone do not pick up existing devices
  • At runtime, interface classes are more natural than setup classes
  • Composite devices may expose multiple interfaces
  • Removal notifications and I/O errors can appear out of order

7.2 Serial devices

Seeing COMx is no grounds for comfort.

  • The port exists, but the target device is not attached to it
  • Another process has it open
  • It has already stopped responding
  • Reads / writes hang on timeouts

For serial, it is especially safe to separate present, responding, and available.

7.3 Network devices

It is better not to equate a successful ping with the app being able to use the device.

There are stages:

  • Can the name be resolved?
  • Can a TCP connection be made?
  • Can the application-layer handshake complete?
  • Is the status ready?
  • Are the values fresh?
The stages between ping succeeding and the device being usableDiagram showing that for network devices there are stages - name resolution, TCP connection, application-layer handshake, status ready, and values fresh - and that a successful ping must not be equated with the device being usable.Name resolvesTCP connectsApplication-layer handshake completesStatus is readyValues are freshping succeeding does not mean usable

Figure 17: Getting to usable on a network device takes five stages.

7.4 SDK-dependent cameras / measurement instruments

It is safer not to declare a device live merely because SDK callbacks are arriving.

  • The callback thread itself stalls
  • Frames arrive but the timestamps are not advancing
  • The image stream arrives but the control channel is dead
  • Settings have not been reapplied after a reconnect

These things happen, so holding a view of health from outside the SDK gives extra assurance.

8. Things Not to Do

  • Collapse state into the three of Connected / Not connected / Error
  • Assume notifications alone also pick up existing devices
  • Treat a successful open as Available outright
  • Present a last known value with a fresh face
  • Omit the timestamp from the display
  • Run open / read / status queries on the UI thread
  • Run retries in the tightest possible loop
  • Show critical faults only in the status bar
  • Conflate Not connected with Monitoring stopped
  • Identify units only by friendly name or COM3

9. Summary

What really matters in device-integration apps is deciding what you have to verify before you are entitled to say how much.

This breakdown in particular pays off:

It is present Our app can open it It is responding That operation is possible right now The on-screen values are recent

Separate these five.

On top of that, the practical guidelines roughly are:

  • Enumerate at startup, notifications afterward
  • Decide usability from heartbeats and device-specific status
  • Give displayed values a timestamp and an age
  • Put critical faults where they are hard to miss
  • Do not let monitoring faults masquerade as device faults

In practice, how rarely the display drifts from reality matters far more than being able to say “Connected.”

10. References

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Windows App Development

In external device integration apps, not just the communication layer but the consistency between state management and UI display directly affects operational quality, so sorting it out at design time keeps failures down.

Frequently Asked Questions

Common questions about the topic of this article.

Why is it not enough to display a device as 'Connected'?
Because the label 'Connected' flattens several separate questions into one: whether the OS can see the device, whether the app has managed to open it, whether it answers at all, whether it can be operated right now, whether the on-screen values are recent, and whether it is the expected unit. 'Not connected', 'Connected / Verifying', 'Connected / Not available', and 'Stale value' are all different states, and once every one of them is flattened into 'Connected', the operator has no way to decide what to do.
How should external device state be tracked internally?
Track it as separate axes: presence (is it visible to the OS), session established (has open / login / initialize completed), responsiveness (does it answer a heartbeat), operational readiness (can it accept an operation right now), data freshness (are the on-screen values recent), configuration match (is it the expected unit and firmware), and monitoring health (is the monitoring code itself alive). Put bluntly, presence belongs to the OS side, usability to the app side, and freshness to the display side. Showing the result in three UI layers - summary, reason, details - keeps it readable.
Should device detection and health checking use events or polling?
Rather than committing entirely to one or the other, the split that works in practice is events for detection and polling for health checks. Concretely: take arrival and removal as event notifications, run heartbeats and status queries as periodic polls, and judge freshness from timestamps or sequence numbers. Notifications alone will not pick up devices that are already present, so the practical rule is to enumerate at startup, subscribe to notifications afterward, and re-enumerate on every notification to reconcile internal state.
How should reconnection to a device that stopped responding be implemented?
Do not hammer it in the tightest possible loop; add backoff. The realistic shape is to retry immediately the first time, lengthen the interval in stages when that fails, set an upper bound, and also provide a manual 'Reconnect' button. A tight loop loads the device and the SDK, floods the logs, and makes transient instability worse. For flapping - state bouncing back and forth over a short window because of a flaky USB contact, for example - it works well to have the UI wait through a short confirmation period before committing to a display.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog