Cámaras industriales, lectores de código de barras, PLC, instrumentos de medición, impresoras, dispositivos serie, dispositivos USB. En las aplicaciones de Windows que se conectan con dispositivos externos, los incidentes ocurren con bastante frecuencia no tanto por el fallo real en sí, sino porque la indicación de estado en pantalla se desvía de la realidad.
Por ejemplo, se dan situaciones como estas:
- El SO lo ve, pero otro proceso lo tiene tomado y no se puede usar
- Se logró hacer
open, pero todavía no terminó el retorno al origen, el calentamiento o la autenticación - El dispositivo sigue conectado físicamente, pero ya dejó de responder
- El hilo de adquisición murió, pero en pantalla sigue el último valor
- Es un dispositivo o firmware inesperado, y aun así se muestra simplemente «Conectado»
Lo que realmente se necesita saber aquí no es solo si está conectado, sino qué se puede hacer con seguridad en este momento.
Público objetivo y contexto de este artículo
| Elemento | Contenido |
|---|---|
| Público objetivo | Personas que diseñan e implementan aplicaciones de Windows conectadas a dispositivos externos. Está pensado para quienes quieren reducir las consultas del tipo «la pantalla dice conectado pero no funciona» en aplicaciones ya existentes |
| Conocimientos previos | Saber escribir aplicaciones en algún lenguaje. No se presupone conocimiento de un SDK o controlador de dispositivo concreto |
| Entorno previsto | Se asume una aplicación de escritorio de Windows. No obstante, la forma de separar los estados y el criterio de presentación no dependen del SO |
| Fuera de alcance | El uso de las API de SDK de fabricantes concretos y la implementación del controlador |
Terminología utilizada en este artículo
A continuación se resumen, en una línea cada uno, los términos que aparecen en inglés tal cual.
| Término | Significado en una línea |
|---|---|
| PLC | Programmable Logic Controller. Controlador industrial usado para el control de equipos de producción |
| firmware | Software embebido en el dispositivo. Incluso con el mismo modelo, la versión puede variar entre unidades |
| heartbeat | Consulta o notificación ligera que se intercambia periódicamente para confirmar que algo sigue vivo |
| poll / event | poll es el método en que uno mismo consulta periódicamente; event es el método en que se espera una notificación de la otra parte |
| stale | Estado en el que el valor está obsoleto. Se pudo obtener, pero no se puede afirmar que el valor mostrado ahora en pantalla sea reciente |
| freshness budget | Límite establecido de antemano: «si se supera este tiempo, el valor deja de considerarse reciente» |
| flapping | Que el estado vaya y venga en poco tiempo. Ocurre por mal contacto o cortes momentáneos |
| reconcile | Cotejar y volver a ajustar el estado interno a la realidad |
| interlock | Mecanismo que detiene el funcionamiento por seguridad. Mientras está abierto, el equipo no se mueve |
| PnP | Plug and Play. Mecanismo por el cual el SO detecta la conexión y desconexión de dispositivos y los configura |
| RTT | round-trip time. Tiempo transcurrido desde que se envía una consulta hasta que llega la respuesta |
1. Conclusión principal
Lo más eficaz al verificar y mostrar el estado de un dispositivo externo es no reducir el estado a un único booleano.
Como mínimo, conviene mantener separados estos aspectos:
- Existencia: si el SO lo ve
- Sesión establecida: si la propia aplicación ya hizo open / login / initialize
- Capacidad de respuesta: si responde al heartbeat o a status query
- Preparación funcional: si puede aceptar operaciones reales en este momento
- Frescura de datos: si el valor en pantalla es reciente
- Coincidencia de configuración: si es el dispositivo, modelo y firmware esperados
- Salud de la monitorización: si el propio proceso de monitorización sigue vivo
Dicho de forma bastante simplificada, es así:
La verificación de existencia corresponde al SO, la disponibilidad de uso a la aplicación y la evaluación de frescura a la pantalla.
Basta con no mezclar estos tres aspectos para que la indicación de estado sea mucho más estable.
flowchart TB
accTitle: Ejes de estado de un dispositivo externo según quién puede responderlos
accDescr: Diagrama que separa el estado de un dispositivo externo en tres grupos según quién puede responder a cada pregunta: el SO responde la existencia; solo la aplicación responde la sesión, la capacidad de respuesta, la preparación funcional y la coincidencia de configuración; y la pantalla responde la frescura de datos y la salud de la monitorización
subgraph OS["Lo que puede responder el SO"]
E["Existencia<br/>¿Es visible la interfaz del dispositivo?"]
end
subgraph APP["Lo que solo la aplicación puede responder"]
S["Sesión<br/>¿open / login / initialize completado?"]
R["Capacidad de respuesta<br/>¿Responde a una consulta ligera dentro del plazo?"]
F["Preparación funcional<br/>¿Puede aceptar operaciones ahora mismo?"]
C["Coincidencia de configuración<br/>¿Es el dispositivo, modelo y firmware esperado?"]
end
subgraph UIL["Lo que responde la pantalla"]
D["Frescura de datos<br/>¿El valor mostrado es reciente?"]
W["Salud de la monitorización<br/>¿Sigue vivo el proceso de monitorización?"]
end
E --> S --> R --> F --> D
C -.->|"Si esto falla,<br/>no se puede usar aunque todo lo demás se cumpla"| F
W -.->|"Si se detiene,<br/>todas las evaluaciones quedan desactualizadas"| D
Figura 1: en lugar de reducir el estado a un único booleano, hay que separarlo según quién puede responder a cada pregunta. Que el nivel superior se cumpla no garantiza que el inferior también se cumpla
2. Por qué el estado «Conectado» es peligroso
El texto «Conectado» carga, sin que nadie lo pida, con varios significados distintos en una sola frase.
En realidad, mezcla al menos las siguientes preguntas:
- ¿El SO ve la interfaz del dispositivo en cuestión?
- ¿La propia aplicación logró hacer open / login / initialize del dispositivo?
- ¿Responde a una consulta ligera dentro del plazo establecido?
- ¿Puede ejecutar con seguridad la operación solicitada ahora mismo?
- ¿El valor que aparece en pantalla es reciente?
- ¿Es el dispositivo, modelo y firmware esperados?
El significado de «se puede usar» cambia según cuáles de estas seis condiciones se cumplan.
Por ejemplo, los siguientes cuatro casos son completamente distintos:
- No conectado el SO ni siquiera ha encontrado la interfaz en cuestión
- Conectado / Verificando es visible físicamente, pero la inicialización o la autenticación todavía no han terminado
- Conectado / No disponible responde, pero no puede funcionar por estar en warming up, busy, interlock, sin media, etc.
- Valor obsoleto antes se podía obtener el valor, pero el que hay en pantalla ha superado el freshness budget
Si todo esto se reduce a «Conectado», el operador no puede determinar qué hacer.
3. Estados que conviene separar desde el principio
Lo recomendable es mantener el estado interno en varios ejes y resumirlo en la interfaz según sea necesario.
3.1 Ejes de estado que conviene separar internamente
| Eje | Qué significa | Método de verificación típico | Ejemplo de lo que mostrar en la interfaz |
|---|---|---|---|
| Existencia | Si el SO ve la interfaz en cuestión | Enumeración al iniciar, notificaciones de arrival / removal | No conectado / Conectado |
| Sesión | Si la propia aplicación ya hizo open / login / initialize | Resultado de inicialización de handle / SDK | Verificando / Inicializando |
| Capacidad de respuesta | Si responde a status query o heartbeat | Consulta ligera con timeout | Responde / Respuesta retrasada / Sin respuesta |
| Preparación funcional | Si la operación real es posible ahora mismo | status específico del dispositivo | Disponible / busy / warming up |
| Frescura de datos | Si el valor mostrado es reciente | timestamp / sequence | Actualizado / Valor obsoleto |
| Coincidencia de configuración | Si coincide con el dispositivo esperado | model / serial / firmware / profile | Dispositivo esperado / Dispositivo inesperado |
| Salud de la monitorización | Si la ruta de monitorización de la aplicación sigue viva | worker heartbeat / loop lag | Monitorizando / Monitorización detenida |
Lo importante aquí es distinguir entre un estado en el que el dispositivo está mal y un estado en el que la aplicación no puede observarlo.
3.2 La interfaz no tiene que mostrarlo todo al mismo nivel
Mantener varios ejes internamente puede dar la impresión de que la pantalla se va a saturar. Pero la interfaz no tiene que presentarlo todo con el mismo peso.
Lo recomendable son tres niveles.
- En la parte superior, el estado resumido
- Debajo, el motivo
- Si hace falta, un panel de detalle
Por ejemplo, separándolo así:
- Resumen:
Conectado / No disponible - Motivo:
En calentamientoQuedan unos 18 segundos - Detalle:
modelserialfirmwarelast heartbeatlast frame time
se gana mucha legibilidad aunque aumente la cantidad de información.
En cuanto a la estructura de la pantalla, quedaría así:
+-- Cámara de preproceso ---------------------------------------+
|
| [Resumen] ! Conectado / No disponible
| [Motivo] En calentamiento - quedan unos 18 segundos
|
| [Detalle] v Expandir (contraído por defecto)
| model ACME-CAM-2000
| serial A1B2C3
| firmware 2.4.1
| last heartbeat 10:23:41.512 (hace 0.5 s)
| last frame 10:23:41.402 (hace 0.6 s)
|
+-----------------------------------------------------------------+
El punto clave es que cuanto más se baja, menos gente necesita leerlo, y eso está bien. El resumen lo lee cualquiera en un segundo; el motivo lo lee quien quiere saber «por qué está detenido»; el detalle solo lo abre quien hace el diagnóstico. Con esta premisa, aunque el detalle sea extenso, la pantalla no se satura.
Al contrario, si model o serial se muestran siempre con el mismo tamaño que el resumen, la línea más importante queda enterrada.
4. Buenas prácticas para la verificación de estados
4.1 Enumeración al iniciar y notificaciones de llegada/eliminación
La base para manejar dispositivos externos en Windows es enumerar los dispositivos existentes al iniciar y, a partir de ahí, recibir notificaciones de arrival / removal.
Hay tres puntos que conviene tener especialmente presentes:
- Las notificaciones por sí solas no detectan los dispositivos ya existentes
- Para la comunicación en tiempo de ejecución, es más natural usar interface class que setup class
- El orden en que se perciben la notificación de remove y el I/O error puede invertirse
Como regla práctica, es sencillo:
- Enumerar al iniciar
- Suscribirse a las notificaciones
- Al recibir una notificación, volver a enumerar y reconciliar el estado interno
4.2 Separar «existir», «poder abrir», «responder» y «poder usar»
Los incidentes con dispositivos externos aumentan cuando estos aspectos se tratan como si fueran uno solo.
- Existir el SO ve la interfaz
- Poder abrir se puede obtener un handle / sesión sin conflictos con otros procesos ni problemas de permisos
- Responder contesta a una consulta ligera dentro del timeout
- Poder usar puede aceptar operaciones reales
Estos cuatro aspectos no son lo mismo.
4.3 Combinar event y poll
En lugar de decantarse completamente por un enfoque basado solo en event o solo en poll, en la práctica resulta más manejable que la detección use event y la verificación de salud use poll.
- arrival / removal mediante event
- heartbeat / status query mediante poll
- la evaluación de freshness mediante timestamp / sequence
Con este reparto, resulta más fácil separar la detección de la conexión de la disponibilidad real de uso.
4.4 Separar el procesamiento de monitorización de la interfaz
Si se ejecutan directamente open / read / status query en el hilo de la interfaz, las necesidades de la presentación y las del procesamiento de monitorización se mezclan con facilidad.
Lo recomendable es:
- El worker de monitorización actualiza el state store
- La interfaz se suscribe al state store y dibuja a partir de él
- Las operaciones de la interfaz se pasan a la capa de monitorización como command
Con esto resulta más fácil tratar por separado la detención de la monitorización y la detención del dispositivo.
Si se escribe el esqueleto en C#, queda aproximadamente así (se asume .NET 8 / C# 12). El truco está en hacerlo unidireccional: solo el worker de monitorización escribe, y la interfaz únicamente lee y dibuja.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
public enum DeviceAvailability
{
Unknown, // Todavía no se ha observado ni una sola vez
Absent, // El SO no ve la interfaz
Initializing, // En proceso de open / login / initialize
Ready, // Puede aceptar operaciones
Unavailable, // Responde, pero no se puede usar por busy / warming up, etc.
NotResponding, // El heartbeat no responde
Mismatched, // Dispositivo o firmware inesperado
}
// Instantánea inmutable que se pasa a la interfaz. Se usa record para detectar diferencias por igualdad de valores
public sealed record DeviceSnapshot(
string DeviceKey, // Clave estable, como el serial number
string DisplayName,
DeviceAvailability Availability,
string Reason, // Motivo, como «En calentamiento»
DateTimeOffset? LastSuccessAt, // Momento de la última observación exitosa
long Sequence, // Número de secuencia que asigna el propio dispositivo
string FirmwareVersion);
public sealed class DeviceStateStore
{
private readonly ConcurrentDictionary<string, DeviceSnapshot> _snapshots = new();
public event Action<DeviceSnapshot>? Changed;
// Solo lo llama el worker de monitorización
public void Publish(DeviceSnapshot snapshot)
{
_snapshots.TryGetValue(snapshot.DeviceKey, out var previous);
_snapshots[snapshot.DeviceKey] = snapshot;
// Notifica solo cuando cambia el valor. Notificar en cada poll haría que la interfaz se redibujara innecesariamente
if (previous != snapshot)
{
Changed?.Invoke(snapshot);
}
}
public IReadOnlyList<DeviceSnapshot> Current() => _snapshots.Values.ToList();
}
El lado de la interfaz confina en un solo lugar la parte que va desde la suscripción hasta el retorno al hilo de la interfaz.
using System;
using System.Windows.Forms;
public sealed class DeviceStatusPresenter
{
private readonly DeviceStateStore _store;
private readonly Control _uiContext; // Punto de apoyo para volver al hilo de la interfaz
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;
// Vincula la suscripción al ciclo de vida de la pantalla. Evita que olvidar
// llamar a Dispose se convierta en «seguir enviando actualizaciones a un formulario ya cerrado»
_uiContext.Disposed += (_, _) => Dispose();
_store.Changed += OnChanged; // Si se olvida la suscripción, aunque haya actualizaciones la pantalla no cambia
}
public void Dispose() => _store.Changed -= OnChanged;
private void OnChanged(DeviceSnapshot snapshot)
{
// El worker de monitorización sigue funcionando aunque la pantalla se esté cerrando.
// Llamar a BeginInvoke después de que el handle se destruye lanza una excepción, y esa
// excepción se propaga a través de Changed?.Invoke hasta el propio worker de monitorización.
// El resultado es el fallo típico de «al cerrar la pantalla, la monitorización se detuvo»
if (_uiContext.IsDisposed || _uiContext.Disposing || !_uiContext.IsHandleCreated)
{
return;
}
try
{
if (_uiContext.InvokeRequired)
{
_uiContext.BeginInvoke(() => Render(snapshot));
return;
}
Render(snapshot);
}
catch (ObjectDisposedException)
{
// Se cerró justo después de pasar la comprobación anterior. Esta ventana no se puede
// eliminar en principio, así que se asume renunciando a la actualización visual. La monitorización no se detiene
}
catch (InvalidOperationException)
{
// El handle no se ha creado o ya se destruyó. Igual que el caso anterior
}
}
private void Render(DeviceSnapshot snapshot)
{
_summary.Text = snapshot.Availability switch
{
DeviceAvailability.Absent => "No conectado",
DeviceAvailability.Initializing => "Conectado / Verificando",
DeviceAvailability.Ready => "Disponible",
DeviceAvailability.Unavailable => "Conectado / No disponible",
DeviceAvailability.NotResponding => "Sin respuesta",
DeviceAvailability.Mismatched => "Dispositivo inesperado",
_ => "Verificando",
};
_reason.Text = snapshot.Reason;
}
}
El instante en que se cierra la pantalla es, con este diseño, el punto más frágil. Changed?.Invoke(snapshot) llama al controlador de forma síncrona desde el hilo del worker de monitorización. Si se llama a BeginInvoke después de que el formulario se cerró y el handle del control se destruyó, se produce una excepción, y esa excepción se propaga a través de Publish hasta el propio worker de monitorización. El simple hecho de limpiar la pantalla acaba tumbando la monitorización. Y como el síntoma es «a veces se cae al cerrar», resulta difícil identificar las condiciones para reproducirlo.
No basta con eliminar la suscripción en Dispose. Si el worker ya empezó a llamar a Invoke mientras se está eliminando la suscripción, esa llamada ya no se puede detener. Hay tres puntos clave que cubrir:
- Vincular la suscripción al ciclo de vida de la pantalla. Con
Control.Disposedse elimina la suscripción automáticamente, evitando que olvidar llamar aDisposese convierta en «seguir enviando actualizaciones a un formulario ya cerrado» - No enviar nada a un control que se está destruyendo o ya se destruyó. Se comprueban
IsDisposed/Disposing/IsHandleCreatedy se descarta ahí mismo - La ventana que aún queda se cubre con manejo de excepciones. No se puede eliminar en principio la posibilidad de que se cierre entre la comprobación y
BeginInvoke. Se capturan aquíObjectDisposedExceptioneInvalidOperationException, de forma que solo se renuncia a la actualización visual. Si no se capturan, la monitorización se ve arrastrada
El punto clave es decidir de antemano que «la actualización visual en el momento de cerrar se puede descartar». Ese único redibujado no tiene valor, pero sí lo tiene que el worker de monitorización siga vivo.
4.5 Evaluar la frescura separando «si está llegando» de «si el contenido avanza»
Evaluar la frescura de los datos fijándose solo en el momento de recepción no es suficiente, porque existe el fallo en el que el callback del SDK sigue llegando mientras el timestamp o el sequence del valor se han quedado detenidos.
Por eso conviene separar la novedad de la recepción de la novedad del contenido.
using System;
public sealed record Reading(
long Sequence, // Número de secuencia que asigna el propio dispositivo
DateTimeOffset ValueTimestamp, // Momento que el propio dispositivo asignó al valor
DateTimeOffset ReceivedAt, // Momento en que la aplicación lo recibió. Para mostrar en pantalla
long ReceivedTicks); // Marca de tiempo monótona creciente de esa misma recepción. Para la evaluación
public enum Freshness
{
Fresh,
Stale,
Unknown,
}
public static class FreshnessPolicy
{
// freshness budget: superado este límite, deja de mostrarse como si fuera en vivo
public static readonly TimeSpan Budget = TimeSpan.FromSeconds(5);
/// <param name="lastAdvancedTicks">Marca de tiempo monótona creciente de la última vez que avanzó el número de secuencia</param>
/// <param name="nowTicks">Marca de tiempo monótona creciente en el momento de la evaluación</param>
public static Freshness Evaluate(
Reading? previous, Reading? current,
long lastAdvancedTicks, long nowTicks, TimeProvider clock)
{
if (current is null)
{
return Freshness.Unknown; // Todavía no se ha obtenido ni una vez
}
if (clock.GetElapsedTime(current.ReceivedTicks, nowTicks) > Budget)
{
return Freshness.Stale; // Directamente no ha llegado
}
if (previous is null)
{
return Freshness.Fresh; // La primera vez no hay nada con qué comparar, así que se decide solo con el momento de recepción
}
if (current.Sequence < previous.Sequence)
{
// El número de secuencia retrocedió. Sospechar de un reinicio del dispositivo, un cambio a otra unidad o una reinicialización del SDK
return Freshness.Unknown;
}
if (current.Sequence == previous.Sequence &&
clock.GetElapsedTime(lastAdvancedTicks, nowTicks) > Budget)
{
// La recepción continúa, pero el contenido no se actualiza
return Freshness.Stale;
}
return Freshness.Fresh;
}
}
lastAdvancedTicks se mantiene, como se muestra a continuación, con el reloj propio.
public sealed class FreshnessTracker(TimeProvider clock)
{
private readonly object _gate = new();
private Reading? _previous;
private long _lastAdvancedTicks;
// Se llama en cada recepción. ReceivedAt y ReceivedTicks son el registro de esa misma recepción
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;
}
}
// Si la recepción se interrumpe por completo, Observe deja de llamarse para siempre.
// Se llama a esto periódicamente desde un temporizador para volver a medir la «antigüedad»
// del último valor recibido. No actualiza el estado, así que es seguro llamarlo cuantas veces haga falta
public Freshness Reevaluate()
{
lock (_gate)
{
return FreshnessPolicy.Evaluate(
_previous, _previous, _lastAdvancedTicks, clock.GetTimestamp(), clock);
}
}
}
Observe por sí solo no puede detectar que el dispositivo se quedó callado. Observe solo se llama cuando llega una recepción, y además el Reading que se le pasa en ese momento acaba de crearse. Como la diferencia entre ReceivedTicks y el momento actual es prácticamente cero, por esta vía solo se llega a Stale cuando «la recepción continúa pero el número de secuencia no avanza». Cuando el callback del SDK se detiene por completo — el fallo que en realidad más interesa detectar — Observe deja de llamarse, y la pantalla se queda congelada mostrando el último Fresh calculado.
Por eso conviene disponer de un punto de entrada que vuelva a evaluar con un ciclo independiente de la recepción. Ese es el Reevaluate anterior, al que se llama desde un temporizador. El ciclo debe ser más corto que el budget (si el budget es de 5 segundos, algo así como cada 1 segundo). Si se usa la misma duración, en el peor caso se tarda casi el doble del budget en darse cuenta.
// System.Threading.Timer. La evaluación avanza aunque no haya recepción.
// RenderFreshness es un método propio que aplica el resultado a la pantalla por la misma ruta que el presenter de 4.4
_freshnessTimer = new Timer(
_ => RenderFreshness(_tracker.Reevaluate()),
null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
Como Observe y Reevaluate pueden llegar simultáneamente desde hilos distintos, el interior de FreshnessTracker se protege con lock. Si se omite esto, la sustitución y la lectura de _previous se mezclan y aparece un fallo difícil de rastrear en el que de vez en cuando se obtiene una evaluación una generación más antigua.
El punto clave aquí es no calcular la diferencia contra ValueTimestamp. ValueTimestamp es un valor marcado con el reloj del propio dispositivo, y no hay garantía de que coincida con el reloj local. Si se restan ambos, un valor recién llegado se vuelve stale con solo que el reloj del dispositivo esté atrasado, y a la inversa, si va adelantado, un valor detenido se queda fresh indefinidamente. El budget debe aplicarse siempre al tiempo transcurrido medido con el reloj propio. El uso de ValueTimestamp debe limitarse a mostrar en pantalla «de qué momento dice el dispositivo que es el valor» o, combinado con el número de secuencia, a servir de indicio para sospechar de una detención del lado del dispositivo.
Y ese «reloj propio» tampoco es suficiente si se calcula restando valores de DateTimeOffset. Ese es el reloj de pared, y puede saltar por sincronización NTP, un ajuste manual o un cambio de horario de verano. Si el reloj retrocede, el tiempo transcurrido se vuelve negativo y el dispositivo se queda como Fresh aunque esté desconectado. Si avanza, un valor recién llegado se vuelve Stale en ese mismo instante. Cuanto más tiempo lleve la pantalla funcionando sin parar (24 horas), más probable es que esto ocurra.
Por eso, para evaluar el budget se usa una marca de tiempo monótona creciente. TimeProvider.GetTimestamp() devuelve un valor de alta precisión basado en Stopwatch, y GetElapsedTime(inicio, fin) permite obtener el tiempo transcurrido entre dos puntos (véanse las referencias del capítulo 10; disponible desde .NET 8). Es más seguro repartir las funciones así: el ReceivedAt del reloj de pared se conserva únicamente para mostrar en pantalla algo como «recibido a las 10:15:03», y no se deja que intervenga en la evaluación de «cuántos segundos han pasado». Otra ventaja de usar TimeProvider de por medio es que se puede sustituir por un reloj de pruebas.
Con este diseño, en cuanto la interfaz recibe Freshness.Stale puede aplicar directamente la política de 5.3: «mostrar la antigüedad (age) junto al valor» y «excluirlo de la evaluación de operatividad».
Unknown se mantiene separado de Stale para no mezclar «todavía no se sabe» con «está obsoleto». Lo primero puede resolverse esperando, pero lo segundo no se arregla por mucho que se espere.
4.6 Estabilizar la identificación del dispositivo
Si el estado se rastrea solo con identificadores superficiales como el friendly name o COM3, es fácil confundir un dispositivo con otro.
Es más seguro mantener internamente una clave estable, como:
- serial number
- logical device id
- stable device path
- el ID de unidad propio del dispositivo
5. Buenas prácticas para la presentación en pantalla
5.1 Tabla de referencia rápida
| Estado real | Resumen en la interfaz | Información complementaria |
|---|---|---|
| Sin interfaz | No conectado | Verificar cable, alimentación y conexión USB |
| Con interfaz, inicializando | Conectado / Verificando | Inicializando, autenticando, en calentamiento |
| Responde, condiciones de operación no cumplidas | Conectado / No disponible | busy, sin media, interlock abierto |
| Responde, valor obsoleto | Conectado / Valor obsoleto | Última actualización hace 12 segundos |
| Sin respuesta | Sin respuesta | Reconectando, timeout de comunicación |
| Dispositivo inesperado | Dispositivo inesperado | Discrepancia de model / serial / firmware |
| Monitorización detenida | Anomalía de monitorización | Worker de monitorización detenido, requiere reinicio |
5.2 El texto debe ser «estado + motivo + siguiente acción»
Textos como Error o Anomalía por sí solos son insuficientes para una pantalla.
Es mejor que el mensaje se apoye en estos tres elementos, para que al operador le cueste menos decidir:
- Estado: qué está ocurriendo
- Motivo: por qué se llegó a esa conclusión
- Siguiente acción: qué se debe hacer
Por ejemplo, así:
Conectado / No disponible - En calentamiento - Espere unos 18 segundosSin respuesta - Timeout de heartbeat - Verifique el cable y la alimentaciónDispositivo inesperado - Se requiere el firmware 2.1.0 - Verifique el dispositivo
Por el contrario, al ponerlos junto a los textos que suelen verse en el terreno, queda claro qué les falta.
| Texto habitual poco adecuado | Qué le falta | Ejemplo de reescritura |
|---|---|---|
Error |
No tiene estado, ni motivo, ni siguiente acción | Sin respuesta - Timeout de heartbeat - Verifique el cable y la alimentación |
Conectado |
Estado ambiguo. Al final no queda claro si se puede usar ahora | Conectado / No disponible - En calentamiento - Espere unos 18 segundos |
No se encuentra el dispositivo |
Falta el motivo y la siguiente acción | No conectado - La interfaz correspondiente no se ha enumerado - Verifique el cable y la alimentación |
Se produjo 0x80070005 |
No hay un estado ni una acción legibles para una persona | No disponible - No se puede abrir (open) el puerto. 0x80070005: acceso denegado - Verifique que ninguna otra aplicación esté usando el mismo puerto |
Reintentando... |
No indica hasta cuándo, cuántas veces ni qué pasará después | Reconectando, intento 3 de 10 - Próximo intento en 8 segundos - También puede reconectar manualmente |
Normal |
No indica de qué momento es ese «normal» | Disponible - Última actualización hace 0.5 segundos |
Lo que tienen en común los ejemplos de reescritura es si el operador puede decidir su siguiente paso mirando únicamente esa pantalla. Si el único texto posible es «contacte con soporte», el problema no está en la redacción, sino en que el diseño de estados es insuficiente.
5.3 No ocultar los datos obsoletos (stale data)
El last known value es útil. Sin embargo, es más seguro no mostrarlo con la apariencia de un valor en vivo.
Lo recomendable es:
- Mostrar el timestamp junto al valor
- Mostrar la antigüedad (age) del valor
- Cambiar el color o la etiqueta cuando pasa a stale
- Excluirlo de la evaluación de operatividad al superar un tiempo determinado
5.4 Cambiar el lugar de presentación según la importancia
La status bar es útil, pero es fácil pasarla por alto. Conviene evitar colocar una anomalía critical solo en un rincón de la status bar.
- Cambio de estado leve: status bar
- Aviso que permite continuar el trabajo: inline notice
- Anomalía que requiere detener la operación: área principal de la pantalla, diálogo o banner
Este reparto es el más directo.
5.5 En pantallas con varios equipos, separar el resumen del detalle
En una pantalla que maneja varios dispositivos, mostrar siempre el detalle completo de todos ellos dificulta la lectura.
- Arriba, un resumen general
- Debajo, una fila por dispositivo
- Al seleccionar, un panel de detalle
Con esta estructura de tres niveles resulta más fácil combinar la visión de conjunto con el diagnóstico individual.
La estructura de la pantalla quedaría así.
+-- Lista de equipos ------------------------------------------+
|
| [Resumen general] Disponibles 6 / 8 Atención 1 Anomalía 1
|
| [Fila por dispositivo]
| Estado Nombre Motivo Últ. actualización
| ---------- --------------- ---------------- --------
| Disponible Cámara de preproceso - hace 0.5 s
| Disponible Impresora de etiquetas - hace 1.2 s
| > No disponible Cámara de inspección En calentamiento hace 0.6 s <- Seleccionada
| Sin respuesta Lector de código Timeout de heartbeat hace 48 s
|
| [Panel de detalle] Cámara de inspección
| serial A1B2C3 / firmware 2.4.1 / quedan unos 18 segundos
| [ Reconectar ] [ Abrir registro ]
|
+----------------------------------------------------------------+
Con esta estructura de tres niveles se puede satisfacer al mismo tiempo, en la misma pantalla, a quien solo mira el resumen general (si hoy se puede echar a andar la línea o no), a quien mira las filas (qué dispositivo está detenido) y a quien mira el panel de detalle (qué hay que hacer para solucionarlo).
En cuanto al orden de las filas, es más manejable llevar las anomalías hacia arriba. Ahora bien, si el orden cambia cada segundo se producen pulsaciones equivocadas, así que la reordenación debe hacerse sobre el estado ya confirmado, después de amortiguar el flapping (véase 6.2).
6. Buenas prácticas de reconexión y operación
6.1 La reconexión debe incluir backoff
Es más seguro no insistir con un bucle lo más corto posible cuando se reconecta tras perder la respuesta.
- Sobrecarga el device / driver / SDK
- Inunda los registros
- Agrava la inestabilidad temporal
- Hace que la interfaz oscile con fuerza
por estas razones.
Lo realista es:
- Reintentar de inmediato la primera vez
- Si falla, alargar el intervalo progresivamente
- Establecer un límite máximo
- Ofrecer también un
Reconectarmanual
Al representar en un diagrama los estados y las condiciones de transición, se ve dónde actúan el backoff y el límite de reconexión.
stateDiagram-v2
accTitle: Ciclo de vida de la conexión y la sesión de un dispositivo externo
accDescr: Diagrama de estados que muestra el ciclo de vida de la conexión y la sesión de un dispositivo externo, desde que es desconocido hasta que está listo, ocupado, en fallo o en reconexión con backoff, sin incluir la frescura de datos
[*] --> Unknown
Unknown --> Absent: No se encuentra tras la enumeración
Unknown --> Present: Enumeración al iniciar / notificación de llegada
Absent --> Present: Notificación de llegada
Present --> Absent: Notificación de eliminación (desde cualquier estado)
state Present {
[*] --> Detected
Detected --> Opening: open / login / initialize
Opening --> Ready: Inicialización y verificación de configuración correctas
Opening --> Fault: Fallo de inicialización (con margen para reintentar)
Opening --> Mismatch: Dispositivo, modelo o firmware inesperado
Ready --> Busy: warming up / en ejecución / interlock
Busy --> Ready: Puede aceptar operaciones
Busy --> Fault: Error de E/S
Ready --> Fault: Error de E/S / sin respuesta confirmado
Fault --> Reconnecting: Se aplica backoff
Reconnecting --> Opening: Ha transcurrido el tiempo de espera
Reconnecting --> RetryExhausted: Se alcanza el límite de reconexión
Fault --> Opening: Reconexión manual
RetryExhausted --> Opening: Reconexión manual (no vuelve automáticamente)
Mismatch --> Opening: Se corrige la configuración y se reconecta manualmente
}
Figura 2: este diagrama trata únicamente el ciclo de vida de la conexión y la sesión; no incluye la frescura de datos. La frescura es un eje independiente de la preparación funcional (figura 1) y puede quedar obsoleta igual tanto en Ready como en Busy; si se mezclara como un estado más, se producirían confusiones como «el valor se interrumpió durante la ejecución y, al recuperarse, ya estaba en Ready». En la pantalla, los estados de este diagrama y la evaluación de frescura de la sección 4.5 se mantienen por separado y se combinan. Incluso al alcanzar el límite de reintentos, no se retrocede a Absent (no conectado), sino que se permanece en RetryExhausted, que no vuelve automáticamente: el dispositivo puede seguir siendo visible y aun así estar averiado, y mostrarlo como no conectado induciría a una operación de recuperación errónea. La discrepancia de configuración tampoco se soluciona reintentando, por lo que también queda fuera del ciclo de reconexión automática
6.2 Amortiguar el flapping
En situaciones como un mal contacto USB o un corte momentáneo de red, el estado va y viene en poco tiempo. Si en ese momento se muestran los eventos en bruto directamente en la interfaz, resulta bastante difícil de leer.
Por eso resulta manejable este reparto:
- El registro interno conserva los eventos en bruto
- La interfaz espera un breve periodo de confirmación antes de mostrar el estado definitivo
- Sin embargo, una anomalía critical se muestra de inmediato
6.3 Registros mínimos que deben conservarse
Mejorar la presentación de estados va prácticamente de la mano del diseño de registros.
| Elemento | Ejemplo |
|---|---|
| timestamp | 2026-03-20T10:23:41.512+09:00 |
| stable device key | camera:A1B2C3 |
| Nombre mostrado | Cámara de preproceso |
| Estado anterior -> Estado nuevo | Ready -> Stale |
| Motivo | heartbeat timeout firmware mismatch |
| Código de error | 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 |
Lo especialmente importante es el registro de transición de estados.
6.4 No confundir la detención de la monitorización con la del equipo
- El poll loop murió por una excepción
- El callback del SDK se detuvo
- El acquisition worker entró en deadlock
- Solo se detuvo la actualización del state store
En estos casos, el dispositivo puede seguir vivo aunque la aplicación no pueda observarlo.
Si este estado se muestra únicamente como No conectado o Sin respuesta, parecerá un problema del propio dispositivo.
Por eso conviene mantener la salud de la ruta de monitorización como un eje aparte.
7. Puntos que suelen pasarse por alto según el tipo de equipo
7.1 Dispositivos USB / PnP
- Las notificaciones por sí solas no detectan un existing device
- En tiempo de ejecución, es más natural usar interface class que setup class
- Un composite device puede exponer varias interfaces
- El orden en que se perciben la notificación de remove y el I/O error puede invertirse
7.2 Dispositivos serie
Que COMx sea visible no basta para confiarse.
- El puerto existe, pero el dispositivo en cuestión no está conectado a él
- Otro proceso lo tiene abierto (open)
- Ya dejó de responder
- Read / write se quedan bloqueados por timeout
En los dispositivos serie, es especialmente seguro separar existencia, respuesta y disponibilidad de uso.
7.3 Dispositivos de red
No conviene equiparar que ping funcione con que la aplicación pueda usar el dispositivo.
- Si se puede resolver el nombre
- Si se puede establecer la conexión TCP
- Si se puede completar el handshake de la capa de aplicación
- Si el status está en ready
- Si el valor está fresh
Hay varias etapas.
7.4 Cámaras e instrumentos de medición dependientes de SDK
No es seguro dar por hecho que algo está en vivo solo porque llega el callback del SDK.
- El propio callback thread se detiene
- El frame llega, pero el timestamp no avanza
- El image stream llega, pero el control channel está muerto
- Tras un reconnect, todavía no ha terminado de reaplicarse la configuración
Como pueden ocurrir estas situaciones, es tranquilizador contar también con una evaluación de salud vista desde fuera del SDK.
8. Prácticas que se deben evitar
- Reducir el estado a solo tres valores:
Conectado / No conectado / Error - Suponer que las notificaciones por sí solas también detectan los existing device
- Considerar que el éxito de
openequivale directamente aDisponible - Mostrar el last known value con apariencia de fresh
- No mostrar el timestamp
- Ejecutar open / read / status query en el hilo de la interfaz
- Ejecutar los reintentos en un bucle lo más corto posible
- Mostrar una anomalía critical solo en la status bar
- Confundir
No conectadoconMonitorización detenida - Identificar el dispositivo solo con el friendly name o
COM3
9. Resumen
Lo verdaderamente importante en una aplicación que integra dispositivos externos es decidir qué se debe verificar para poder afirmar hasta dónde.
En particular, esta separación resulta eficaz:
Existe La propia aplicación puede abrirlo Responde Puede realizar esa operación ahora mismo El valor en pantalla es reciente
Separar estos cinco aspectos.
Sobre esa base, las pautas prácticas son, a grandes rasgos, estas:
- Al iniciar, enumerar; a partir de ahí, notificaciones
- Decidir la disponibilidad de uso con heartbeat y el status específico del dispositivo
- Adjuntar timestamp y age a los valores mostrados
- Mostrar las anomalías critical en un lugar difícil de pasar por alto
- No hacer que una anomalía de monitorización parezca una anomalía del dispositivo
En la práctica, importa mucho más cuánto se resiste esa indicación a desviarse de la realidad que el simple hecho de poder mostrar «Conectado».
10. Referencias
- Microsoft Learn, TimeProvider Class (que
GetTimestampdevuelve un valor de alta precisión basado enStopwatch, y queGetElapsedTime(Int64, Int64)permite obtener el tiempo transcurrido entre dos puntos) - Microsoft Learn, CM_Register_Notification
- Microsoft Learn, Registering for Notification of Device Interface Arrival and Device Removal
- Microsoft Learn, Registering for Device Notification
- Microsoft Learn, Comparison of setup classes and interface classes
- Microsoft Learn, Device Information Sets
- Microsoft Learn, SetupDiEnumDeviceInterfaces
- Microsoft Learn, Communications functions
- Microsoft Learn, ClearCommError
- Microsoft Learn, COMMTIMEOUTS structure
- Microsoft Learn, WaitCommEvent
- Microsoft Learn, Monitoring Communications Events
- Microsoft Learn, Status Bars (Design basics)
- Microsoft Learn, UX checklist for desktop applications
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Cómo funcionan el portapapeles y arrastrar y soltar — Gestionar correctamente la transferencia de datos OLE en aplicaciones empresariales
Una tabla de Excel se deforma al pegarla y deja de poder pegarse si cierra el origen: es el portapapeles colocando el mismo contenido en ...
WPR/WPA en la práctica — Introducción al análisis de rendimiento del sistema para «todo el PC va lento»
Problemas como «todo el PC va lento» o «el arranque es lento», que el Administrador de tareas no explica, se investigan con WPR/WPA leyen...
El apagado de Windows visto desde la aplicación ── cómo sobrevivir correctamente a la notificación de cierre, el reinicio y el corte de energía
Un reinicio nocturno de Windows Update corrompió datos de medición: ese accidente se evita con buen diseño. Explicamos, con fuentes ofici...
Introducción a la accesibilidad en aplicaciones Windows — UI Automation y cómo prepararse para la obligatoriedad del ajuste razonable
Ante la reforma legal japonesa vigente desde abril de 2024, explicamos UI Automation, el mecanismo con el que los lectores de pantalla le...
Fuentes japonesas y las trampas de los caracteres — JIS2004, selectores de variantes de ideogramas y caracteres externos en aplicaciones empresariales
«El carácter 葛 cambia de forma entre pantalla e informe», «no aparece el carácter del nombre»: los problemas de caracteres se aclaran sep...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
En las aplicaciones que integran dispositivos externos, la calidad operativa depende tanto del procesamiento de comunicación como de la coherencia entre la gestión de estados y la presentación en la interfaz; organizarlo desde la fase de diseño reduce los incidentes.
Consultoría técnica y revisión de diseño
Un diseño de estados que no se limita a «Conectado» resulta más fácil de evaluar si se revisan por separado los ejes de detección, verificación de respuesta, disponibilidad, frescura de datos y reconexión.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Por qué no basta con mostrar el estado del dispositivo como «Conectado»?
- Porque el texto «Conectado» comprime en una sola frase varias preguntas distintas: si el SO lo ve, si la propia aplicación logró hacer open, si responde, si se puede operar ahora mismo, si el valor en pantalla es reciente y si es el dispositivo esperado. Por ejemplo, «No conectado», «Conectado / Verificando», «Conectado / No disponible» y «Valor obsoleto» son estados completamente distintos, y si todos se reducen a «Conectado», el operador no puede saber qué hacer.
- ¿Cómo se debería separar internamente el estado de un dispositivo externo?
- Se deben mantener por separado: existencia (si el SO lo ve), sesión establecida (si ya se hizo open/login/initialize), capacidad de respuesta (si responde al heartbeat), preparación funcional (si puede aceptar operaciones ahora mismo), frescura de datos (si el valor en pantalla es reciente), coincidencia de configuración (si es el dispositivo y firmware esperado) y salud de la monitorización (si el propio proceso de monitorización sigue vivo). Dicho de forma simplificada, el reparto es: la verificación de existencia corresponde al SO, la disponibilidad de uso a la aplicación y la evaluación de frescura a la pantalla. Si la interfaz se presenta en tres niveles (resumen, motivo y detalle), resulta más fácil de leer.
- ¿La detección y la verificación de salud del dispositivo deben hacerse con eventos o con sondeo (polling)?
- En lugar de decantarse por completo por uno de los dos, en la práctica resulta más manejable repartir las tareas: detección mediante event y verificación de salud mediante poll. En concreto, arrival/removal se reciben como notificaciones de eventos, heartbeat y status query se realizan mediante sondeo periódico, y la evaluación de frescura se hace con timestamp o sequence. Sin embargo, las notificaciones por sí solas no detectan los dispositivos ya existentes, así que la regla práctica es enumerar al iniciar, suscribirse a las notificaciones a partir de entonces y, al recibir una notificación, volver a enumerar para reconciliar el estado interno.
- ¿Cómo se debe implementar la reconexión a un dispositivo que dejó de responder?
- No conviene insistir en un bucle lo más corto posible; hay que incorporar backoff. Lo más realista es reintentar de inmediato la primera vez, alargar el intervalo progresivamente si falla, establecer un límite máximo y ofrecer también un botón manual de «Reconectar». Un bucle demasiado corto sobrecarga el dispositivo y el SDK, inunda los registros y agrava la inestabilidad temporal. Además, para el flapping (el estado va y viene en poco tiempo, por ejemplo por un mal contacto USB), resulta eficaz que la interfaz espere un breve periodo de confirmación antes de mostrar el estado definitivo.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.