<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Brian Michel </title>
    <description>The latest articles on DEV Community by Brian Michel  (@bmcutting).</description>
    <link>https://dev.to/bmcutting</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4085061%2F9d42fd4e-b527-4bd3-8365-94f408357ec9.jpg</url>
      <title>DEV Community: Brian Michel </title>
      <link>https://dev.to/bmcutting</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bmcutting"/>
    <language>en</language>
    <item>
      <title>Una interfaz de software para protocolos industriales</title>
      <dc:creator>Brian Michel </dc:creator>
      <pubDate>Sun, 30 Aug 2026 23:08:53 +0000</pubDate>
      <link>https://dev.to/bmcutting/una-interfaz-de-software-para-protocolos-industriales-15g9</link>
      <guid>https://dev.to/bmcutting/una-interfaz-de-software-para-protocolos-industriales-15g9</guid>
      <description>&lt;p&gt;La integración de dispositivos de campo tropieza casi siempre con el mismo&lt;br&gt;
obstáculo: los equipos que hay que reunir en una misma vista no hablan el&lt;br&gt;
mismo protocolo, y ninguno de esos protocolos fue diseñado pensando en los&lt;br&gt;
demás.&lt;/p&gt;

&lt;p&gt;Una instalación industrial rara vez habla un solo protocolo. Conviven&lt;br&gt;
medidores Modbus TCP que solo responden cuando reciben una petición,&lt;br&gt;
sensores que publican por MQTT cada vez que tienen algo que comunicar y&lt;br&gt;
equipos que exponen sus datos por OPC UA, con su propio modelo de&lt;br&gt;
información y su propio sistema de tipos. Todos ellos deben aparecer en la&lt;br&gt;
misma vista.&lt;/p&gt;

&lt;p&gt;La dificultad no es de comunicación —cada protocolo está bien documentado&lt;br&gt;
y dispone de bibliotecas maduras—, sino de representación. El registro&lt;br&gt;
Modbus llega como 16 bits sin unidad ni marca de tiempo, y el instante de&lt;br&gt;
la lectura es el instante en que se preguntó. El mensaje MQTT llega como&lt;br&gt;
JSON con un timestamp generado en el dispositivo. El nodo OPC UA llega con&lt;br&gt;
su tipo declarado, su marca de tiempo de origen y su código de calidad.&lt;br&gt;
Son tres representaciones distintas de la misma idea —una medición— y la&lt;br&gt;
capa de presentación termina con una rama de código por cada una.&lt;/p&gt;
&lt;h2&gt;
  
  
  La capa intermedia
&lt;/h2&gt;

&lt;p&gt;La solución es una capa entre la vista y los dispositivos: una interfaz&lt;br&gt;
de comunicación que exponga un contrato único, independiente del&lt;br&gt;
protocolo que haya debajo. Quien la consuma —una vista, un panel de&lt;br&gt;
supervisión, un servicio de alarmas— recibe siempre el mismo tipo de&lt;br&gt;
dato, con independencia de si procede de Modbus, de MQTT o de OPC UA. La&lt;br&gt;
traducción ocurre por debajo de la interfaz, dentro de cada canal, y no se&lt;br&gt;
propaga hacia arriba.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft5zq3sckyol8vr62zerp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft5zq3sckyol8vr62zerp.png" alt="Comparación entre una aplicación acoplada a cada protocolo y la misma aplicación sobre una capa de abstracción" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sin capa intermedia, cada protocolo llega hasta la lógica de negocio. Con&lt;br&gt;
ella, la aplicación programa contra un contrato único y la diferencia se&lt;br&gt;
resuelve por debajo.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Este planteamiento aporta, entre otras ventajas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El programador de aplicación deja de aprender protocolos.&lt;/strong&gt; No
necesita saber que Modbus numera los registros desde 1 pero los
direcciona desde 0, ni cómo se negocia un QoS en MQTT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La información queda centralizada sin casarse con un fabricante.&lt;/strong&gt;
Pueden convivir marcas y protocolos distintos en la misma instalación,
porque la diferencia se absorbe en el canal y no en la aplicación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Se prescinde del hardware traductor.&lt;/strong&gt; Los gateways que convierten
todo a un protocolo común suponen un coste, ocupan espacio en el tablero
y añaden un punto de fallo. Si la traducción es software, desaparecen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese último punto merece detenerse, porque va contra lo que se hace&lt;br&gt;
habitualmente. Cuando se busca cómo integrar Modbus con MQTT, lo que se&lt;br&gt;
encuentra son puentes: un microcontrolador con su sistema operativo en&lt;br&gt;
tiempo real que consulta los registros por un lado y publica en temas por&lt;br&gt;
el otro, o un gateway comercial que unifica varios protocolos en el&lt;br&gt;
tablero. Funcionan, están bien documentados, y son la respuesta estándar&lt;br&gt;
del sector.&lt;/p&gt;

&lt;p&gt;La diferencia es dónde se pone la traducción. Un puente la pone en un&lt;br&gt;
equipo: hay que comprarlo, alimentarlo, configurarlo y mantenerlo, y su&lt;br&gt;
período de consulta es un parámetro suyo que la aplicación no ve ni&lt;br&gt;
controla. Una interfaz la sitúa en el código de la propia aplicación, y&lt;br&gt;
entonces ese período —y las demás decisiones de compromiso— dejan de estar&lt;br&gt;
ocultas en el firmware de un equipo y pasan a ser código que se puede&lt;br&gt;
leer, ajustar y versionar.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. La interfaz
&lt;/h2&gt;

&lt;p&gt;El primer paso es acotar el conjunto mínimo de operaciones: aquellas que&lt;br&gt;
cualquier canal necesita, con independencia del protocolo. En una primera&lt;br&gt;
aproximación son cuatro: conectar, desconectar, leer y escribir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IDeviceChannel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ConnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DisconnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeviceData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeviceData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quien consume esta interfaz dispone de esos métodos y nada más: desconoce&lt;br&gt;
la implementación de cada protocolo, la forma de establecer la conexión y&lt;br&gt;
los tipos de dato nativos que maneja cada uno.&lt;/p&gt;

&lt;p&gt;Conviene notar que esos métodos no son todos de la misma naturaleza.&lt;br&gt;
&lt;code&gt;ReadAsync&lt;/code&gt; y &lt;code&gt;WriteAsync&lt;/code&gt; son operaciones sobre datos: entran y salen&lt;br&gt;
valores. &lt;code&gt;ConnectAsync&lt;/code&gt; y &lt;code&gt;DisconnectAsync&lt;/code&gt; pertenecen al ciclo de vida:&lt;br&gt;
no producen datos, sino que establecen y liberan el enlace, junto con los&lt;br&gt;
recursos asociados —el socket, la sesión con el intermediario, las&lt;br&gt;
suscripciones abiertas—. Y el ciclo de vida es donde reside la&lt;br&gt;
complejidad real: reconexión, expiración de plazos, el tratamiento de una&lt;br&gt;
escritura pendiente cuando el enlace se interrumpe, o qué debe ocurrir con&lt;br&gt;
una suscripción activa cuando se cierra el canal.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Dos modelos de comunicación diferentes
&lt;/h2&gt;

&lt;p&gt;Llegados a este punto aparece la cuestión que decide si el diseño&lt;br&gt;
funciona o se convierte en una capa que estorba: &lt;strong&gt;los protocolos no son&lt;br&gt;
equivalentes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;La diferencia está en quién toma la iniciativa. En Modbus el dispositivo&lt;br&gt;
es pasivo: no comunica nada hasta que se le pregunta, y para saber si un&lt;br&gt;
valor ha cambiado hay que volver a preguntar. A ese preguntar de forma&lt;br&gt;
repetida se le llama &lt;strong&gt;sondeo&lt;/strong&gt; (&lt;em&gt;polling&lt;/em&gt;). En MQTT ocurre lo contrario:&lt;br&gt;
el dispositivo publica cuando tiene algo que comunicar y el intermediario&lt;br&gt;
lo reparte entre quienes se hayan suscrito; la aplicación no pregunta,&lt;br&gt;
solo espera. Es lo que se conoce como &lt;strong&gt;notificación&lt;/strong&gt; o &lt;em&gt;push&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;No es una diferencia de formato que se resuelva con un conversor, y una&lt;br&gt;
interfaz debe decidir de qué lado se sitúa:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Si el contrato es &lt;code&gt;ReadAsync()&lt;/code&gt;, se impone el sondeo sobre un protocolo
que ya notificaba. El sensor MQTT publica un valor a las 10:00:03 y la
aplicación lo lee a las 10:00:10: se han introducido siete segundos de
latencia y se han perdido los valores intermedios que el dispositivo sí
envió.&lt;/li&gt;
&lt;li&gt;Si el contrato es &lt;code&gt;SubscribeAsync()&lt;/code&gt;, hay que construir la notificación
donde no existe. El canal Modbus queda obligado a sondear internamente y
emitir un valor cuando detecta un cambio. Funciona, pero el período de
sondeo queda escondido en la implementación, y el consumidor cree
recibir los cambios en el momento en que se producen cuando en realidad
los recibe cada 500 ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ninguna de las dos opciones es gratuita. La segunda suele ser preferible&lt;br&gt;
—resulta más sencillo simular la notificación sobre un protocolo de&lt;br&gt;
consulta que a la inversa, y no degrada a los protocolos que ya&lt;br&gt;
notificaban— pero exige que el período de sondeo forme parte del contrato&lt;br&gt;
y no sea un detalle enterrado en la implementación. En C# el mecanismo&lt;br&gt;
natural para una suscripción es &lt;code&gt;IAsyncEnumerable&amp;lt;Reading&amp;gt;&lt;/code&gt;, y el período&lt;br&gt;
cabe en la propia firma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IDeviceChannel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ConnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DisconnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeviceData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeviceData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SubscribeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;DeviceData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;period&lt;/code&gt; expresa una sola cosa: cada cuánto quiere el consumidor tener&lt;br&gt;
noticias de ese dato. Si indica dos segundos, está diciendo que no acepta&lt;br&gt;
enterarse de un cambio con más de dos segundos de retraso.&lt;/p&gt;

&lt;p&gt;Cada canal cumple ese compromiso con los medios de su protocolo, y son&lt;br&gt;
distintos. El canal Modbus, que no dispone de notificación, consulta al&lt;br&gt;
esclavo con esa cadencia: cada dos segundos sale una petición por el&lt;br&gt;
enlace, cambie el valor o no. El canal MQTT no consulta nada, porque el&lt;br&gt;
intermediario ya le entrega las publicaciones conforme llegan; si el&lt;br&gt;
sensor publica cada medio segundo, el consumidor recibe noticias cada&lt;br&gt;
medio segundo y el período no llega a intervenir. Solo actúa en el caso&lt;br&gt;
contrario, cuando transcurren los dos segundos sin publicación alguna: un&lt;br&gt;
dispositivo que deja de emitir no genera ningún evento, y sin ese plazo el&lt;br&gt;
silencio de un sensor averiado sería indistinguible del de un proceso&lt;br&gt;
estable.&lt;/p&gt;

&lt;p&gt;Un mismo parámetro y dos comportamientos opuestos —en un caso marca cada&lt;br&gt;
cuánto preguntar, en el otro cuánto esperar antes de dar señales—, y esa&lt;br&gt;
es exactamente la razón de que exista. Unificar no consiste en que ambos&lt;br&gt;
canales hagan lo mismo, porque no pueden: consiste en que ambos ofrezcan&lt;br&gt;
la misma garantía al consumidor por los medios que cada protocolo permite.&lt;br&gt;
Quien consume los datos no sabe cuál de los dos canales tiene debajo, y ya&lt;br&gt;
no lo necesita, porque lo que le importa —cada cuánto tendrá noticias—&lt;br&gt;
está escrito en el contrato en lugar de enterrado en la implementación.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Lo que cada protocolo no implementa
&lt;/h2&gt;

&lt;p&gt;Todo lo anterior es diseño sobre el papel. Las consecuencias aparecen al&lt;br&gt;
implementar el contrato dos veces, porque entonces se hace evidente que&lt;br&gt;
&lt;strong&gt;ninguno de los dos protocolos lo cubre entero&lt;/strong&gt;, y que lo que le falta a&lt;br&gt;
uno es justo lo que el otro resuelve de fábrica.&lt;/p&gt;

&lt;p&gt;En Modbus la lectura es literalmente el protocolo y no requiere trabajo&lt;br&gt;
adicional; lo que no existe es la suscripción. En MQTT ocurre lo inverso:&lt;br&gt;
la notificación es el protocolo, y no hay ningún interlocutor al que&lt;br&gt;
consultar el valor actual. Como el contrato exige las cinco operaciones,&lt;br&gt;
cada canal debe construir la mitad que su protocolo no le da.&lt;/p&gt;
&lt;h3&gt;
  
  
  El canal Modbus construye la notificación
&lt;/h3&gt;

&lt;p&gt;Sin mecanismo nativo de notificación, la única salida es sondear. El canal&lt;br&gt;
consulta el dato con la cadencia que indica &lt;code&gt;period&lt;/code&gt;, lo compara con el&lt;br&gt;
valor anterior y emite una lectura solo cuando ha cambiado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Reading&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="nf"&gt;ValuesEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastValue&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;lastValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El bucle funciona, con dos ajustes que conviene mencionar. El retardo se&lt;br&gt;
aplica &lt;em&gt;después&lt;/em&gt; de la lectura, de modo que cada vuelta cuesta &lt;code&gt;period&lt;/code&gt;&lt;br&gt;
más lo que haya tardado la consulta: con un período de 500 ms y una&lt;br&gt;
lectura de 20, el consumidor recibe noticias cada 520 y sigue creyendo que&lt;br&gt;
son 500; basta medir la lectura y esperar solo los 480 restantes para que&lt;br&gt;
el ciclo dure lo prometido. Y como Modbus TCP admite una transacción a la&lt;br&gt;
vez sobre un mismo socket, este sondeo comparte enlace con las lecturas&lt;br&gt;
que el consumidor sigue pidiendo por su cuenta, así que todas las&lt;br&gt;
transacciones deben ejecutarse de una en una para que las tramas no se&lt;br&gt;
entrelacen.&lt;/p&gt;

&lt;p&gt;Ninguno de los dos ajustes es costoso. Lo que conviene retener es de dónde&lt;br&gt;
salen: nada de esto existía en Modbus, y ambos aparecen únicamente porque&lt;br&gt;
el canal se comprometió a avisar de los cambios. El consumidor pidió una&lt;br&gt;
suscripción, y el precio de esa suscripción queda del lado del canal,&lt;br&gt;
invisible para él.&lt;/p&gt;
&lt;h3&gt;
  
  
  El canal MQTT construye la lectura
&lt;/h3&gt;

&lt;p&gt;En el canal MQTT el problema es el inverso. Un valor solo se conoce en el&lt;br&gt;
instante en que el dispositivo lo publica, y ante una consulta a destiempo&lt;br&gt;
no hay nada que devolver. La salida es recordar: suscribirse al conectar y&lt;br&gt;
guardar cada publicación conforme llega.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;OnMessageReceived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MqttApplicationMessageReceivedEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApplicationMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cacheLock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;_lastReadings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApplicationMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetUtcNow&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada publicación se almacena junto al instante en que fue recibida, y&lt;br&gt;
&lt;code&gt;ReadAsync&lt;/code&gt; la recupera de ese almacén: localiza el tema y devuelve el&lt;br&gt;
último valor registrado. La lectura no interroga al dispositivo —eso sigue&lt;br&gt;
siendo imposible— sino a la memoria del canal.&lt;/p&gt;

&lt;p&gt;Resta un problema de interpretación. MQTT transporta una secuencia de&lt;br&gt;
bytes sin significado declarado: un tema puede contener una temperatura,&lt;br&gt;
un estado de marcha o una cadena de texto, y todos llegan al canal en la&lt;br&gt;
misma forma. Determinar el tipo de cada uno exige información que el&lt;br&gt;
protocolo no transmite, de modo que es el propio dato quien la declara y&lt;br&gt;
el canal quien la aplica en el momento de la lectura:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PayloadType&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;MqttPayloadType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;  &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CultureInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvariantCulture&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;MqttPayloadType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;MqttPayloadType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;    &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La conversión ocurre en la lectura y no al recibir el mensaje, y esa&lt;br&gt;
decisión importa: el mismo tema podría interpretarse de formas distintas&lt;br&gt;
según quién lo consulte, de modo que el almacén conserva los bytes tal&lt;br&gt;
como llegaron y cada consulta los traduce según lo que declare su dato.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Cuando la misma llamada significa dos cosas
&lt;/h2&gt;

&lt;p&gt;Lo anterior era una diferencia de interpretación, y se resolvía&lt;br&gt;
declarando el tipo junto al dato. Hay una más profunda, que ninguna&lt;br&gt;
declaración arregla, y es la que mejor enseña dónde está el límite real&lt;br&gt;
de este tipo de abstracción.&lt;/p&gt;

&lt;p&gt;Un caché que solo guarda el último valor tiene un problema: si el&lt;br&gt;
consumidor lee dos veces y el dispositivo no ha publicado en medio,&lt;br&gt;
recibe el mismo dato otra vez sin poder distinguir si es nuevo o&lt;br&gt;
repetido. Una forma de resolverlo es vaciar la entrada al leerla, de&lt;br&gt;
modo que la lectura entrega el valor pendiente y deja el hueco vacío. Así&lt;br&gt;
se resolvió en una implementación anterior de este mismo canal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt; &lt;span class="nf"&gt;ReadValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeviceData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;Reading&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_lastPayloads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_lastPayloads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;_lastPayloads&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// la lectura consume el valor&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No hay valor disponible para ese dato"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conviene examinar la consecuencia. En el canal Modbus, &lt;code&gt;ReadAsync&lt;/code&gt; es&lt;br&gt;
idempotente: dos consultas seguidas devuelven el mismo valor, porque se&lt;br&gt;
interroga al dispositivo y el dispositivo sigue valiendo lo que vale. En&lt;br&gt;
el canal MQTT, la segunda lectura devuelve vacío aunque nada haya&lt;br&gt;
cambiado en la instalación.&lt;/p&gt;

&lt;p&gt;Y no es que una implementación esté mal. Es que la misma firma está&lt;br&gt;
sosteniendo dos operaciones que ni siquiera son del mismo tipo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;en Modbus, &lt;code&gt;ReadAsync&lt;/code&gt; significa &lt;strong&gt;"dame el estado actual"&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;en MQTT, significa &lt;strong&gt;"dame lo que haya llegado desde la última vez que
pregunté"&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lo primero es consultar un valor. Lo segundo es consumir de una cola de&lt;br&gt;
un solo elemento. El contrato no distingue entre ambas cosas —no tiene&lt;br&gt;
con qué—, así que el consumidor recibe una u otra según qué canal le&lt;br&gt;
esté usando, sin manera de saber cuál. El consumidor acaba escribiendo un&lt;br&gt;
&lt;code&gt;if (valor == null)&lt;/code&gt; para tratar el caso excepcional, y ese &lt;code&gt;if&lt;/code&gt; es puro&lt;br&gt;
sondeo: en Modbus no se cumple nunca y en MQTT se cumple la mitad de las&lt;br&gt;
veces.&lt;/p&gt;

&lt;p&gt;En ese punto la abstracción deja de ser una comodidad y pasa a ser una&lt;br&gt;
fuente de error. No porque el código esté mal escrito, sino porque el&lt;br&gt;
contrato daba por supuesto que los cuatro métodos significaban lo mismo&lt;br&gt;
en ambas implementaciones, y ese supuesto no se sostiene.&lt;/p&gt;

&lt;p&gt;La salida no es renunciar a la interfaz: es hacer explícito lo que el&lt;br&gt;
contrato estaba escondiendo. Que &lt;code&gt;Reading&lt;/code&gt; diga cuándo se generó el&lt;br&gt;
valor y si es el mismo de antes; que una lectura sin dato disponible sea&lt;br&gt;
un estado previsto y con nombre, no un fallo genérico; que el consumidor&lt;br&gt;
pueda saber si está ante un valor consultado o ante uno recibido. Cada&lt;br&gt;
una de esas decisiones ensancha el contrato y le quita elegancia —y cada&lt;br&gt;
una sustituye una suposición silenciosa por algo que se puede leer en la&lt;br&gt;
firma.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cómo queda resuelto
&lt;/h3&gt;

&lt;p&gt;El canal del repositorio ya no vacía la entrada. La lectura localiza el&lt;br&gt;
tema y devuelve lo que haya, sin tocar el almacén:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;lock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_cacheLock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;_lastReadings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mqttData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetUtcNow&lt;/span&gt;&lt;span class="p"&gt;())));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mqttData&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dos líneas que cambian y tres consecuencias. La primera es que &lt;code&gt;ReadAsync&lt;/code&gt;&lt;br&gt;
vuelve a ser idempotente: dos consultas seguidas devuelven lo mismo, igual&lt;br&gt;
que en Modbus, y la lectura deja de ser una operación que altera el canal.&lt;/p&gt;

&lt;p&gt;La segunda es que la pregunta que motivaba el vaciado —si el valor es&lt;br&gt;
nuevo o repetido— la responde ahora la marca de tiempo, que se sella al&lt;br&gt;
recibir la publicación y no al leerla:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Reading&lt;/span&gt; &lt;span class="n"&gt;reading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetUtcNow&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Al consumidor le basta comparar el &lt;code&gt;Timestamp&lt;/code&gt; con el de la lectura&lt;br&gt;
anterior: si no ha variado, no ha habido publicación nueva. Es la misma&lt;br&gt;
información que daba el hueco vacío, sin destruir el valor para&lt;br&gt;
obtenerla, y además dice cuánto hace en lugar de limitarse a un sí o un&lt;br&gt;
no.&lt;/p&gt;

&lt;p&gt;La tercera es que la ausencia de valor deja de viajar como fallo. Antes,&lt;br&gt;
un tema sobre el que nadie había publicado devolvía &lt;code&gt;Result.Failure&lt;/code&gt; y&lt;br&gt;
resultaba indistinguible de un enlace roto; ahora devuelve un resultado&lt;br&gt;
correcto con un &lt;code&gt;Reading&lt;/code&gt; vacío, y son dos preguntas separadas: &lt;code&gt;Result&lt;/code&gt;&lt;br&gt;
responde a si se pudo hablar con el dispositivo, &lt;code&gt;HasValue&lt;/code&gt; a si hay algo&lt;br&gt;
que entregar.&lt;/p&gt;

&lt;p&gt;Con esas tres piezas, &lt;code&gt;ReadAsync&lt;/code&gt; significa lo mismo en los dos canales&lt;br&gt;
—entregar el último estado conocido junto al instante en que se conoció—&lt;br&gt;
y el &lt;code&gt;if&lt;/code&gt; del consumidor deja de ser un parche para el caso excepcional.&lt;br&gt;
Conviene señalar que el problema no desaparece del todo, sino que cambia&lt;br&gt;
de sitio: quien quiera saber si ya había visto ese valor debe guardar el&lt;br&gt;
último &lt;code&gt;Timestamp&lt;/code&gt; recibido. La diferencia es que esa decisión pasa a&lt;br&gt;
tomarla el consumidor, que es el único que sabe qué antigüedad le sirve.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. El contrato completo
&lt;/h2&gt;

&lt;p&gt;Con lo visto hasta aquí, el contrato ya funciona sobre dos protocolos&lt;br&gt;
opuestos. Lo que falta es cerrar los supuestos que quedaban implícitos, y&lt;br&gt;
todos se cierran del mismo modo: &lt;strong&gt;llevando al contrato lo que el&lt;br&gt;
consumidor necesita saber y hasta ahora quedaba en la implementación&lt;/strong&gt;.&lt;br&gt;
Ese trabajo se concentra en dos lugares, el valor que se entrega y el dato&lt;br&gt;
que se solicita.&lt;/p&gt;
&lt;h3&gt;
  
  
  Lo que el contrato declara sobre cada valor
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ReadAsync&lt;/code&gt; devuelve un valor obtenido de formas muy distintas: en Modbus&lt;br&gt;
es el resultado de interrogar al esclavo en ese momento; en MQTT, la&lt;br&gt;
última publicación recibida, que puede ser de hace cinco minutos. Esa&lt;br&gt;
diferencia no se puede eliminar, pero sí se puede hacer explícita en el&lt;br&gt;
propio valor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Reading&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;Timestamp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;HasValue&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tres miembros que resuelven tres cuestiones. &lt;strong&gt;El instante en que se&lt;br&gt;
obtuvo&lt;/strong&gt; permite al consumidor juzgar la vigencia con su propio criterio,&lt;br&gt;
que es el único válido: veinte segundos son inmejorables para la&lt;br&gt;
temperatura de una nave e inaceptables para un enclavamiento, y el canal&lt;br&gt;
no sabe cuál de los dos casos tiene delante. &lt;strong&gt;La ausencia de valor&lt;/strong&gt; deja&lt;br&gt;
de confundirse con un fallo: que nadie haya publicado todavía en un tema&lt;br&gt;
no significa que el enlace esté roto, de modo que &lt;code&gt;Result&lt;/code&gt; responde a si&lt;br&gt;
se pudo hablar con el dispositivo y &lt;code&gt;HasValue&lt;/code&gt; a si hay algo que entregar.&lt;br&gt;
Y &lt;strong&gt;el hecho de que la lectura no altere nada&lt;/strong&gt; mantiene la operación&lt;br&gt;
idempotente: el almacén conserva el valor tras entregarlo, y la novedad se&lt;br&gt;
deduce comparando la marca de tiempo, no vaciando la entrada.&lt;/p&gt;

&lt;p&gt;Con estos tres datos el consumidor puede escribir la misma lógica para&lt;br&gt;
cualquier protocolo. Deja de preguntarse de dónde viene el valor y pasa a&lt;br&gt;
preguntarse si le sirve, que es lo único que necesitaba saber.&lt;/p&gt;
&lt;h3&gt;
  
  
  Lo que el contrato declara sobre cada dato
&lt;/h3&gt;

&lt;p&gt;Queda la información que los protocolos transportan pero no explican.&lt;br&gt;
Modbus entrega dieciséis bits sin decir qué representan: que dos registros&lt;br&gt;
consecutivos formen un número decimal, un entero con signo o dos valores&lt;br&gt;
independientes está en el manual del fabricante, y el orden en que se&lt;br&gt;
disponen esas dos mitades varía según el equipo. MQTT entrega bytes con el&lt;br&gt;
mismo silencio.&lt;/p&gt;

&lt;p&gt;Esa información existe siempre; simplemente no viaja en la trama. Y si no&lt;br&gt;
viaja en la trama, se declara donde el canal pueda consultarla:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ModbusDeviceData&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DeviceData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;ModbusRegisterType&lt;/span&gt; &lt;span class="n"&gt;RegisterType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt; &lt;span class="n"&gt;StartAddress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;ModbusDataType&lt;/span&gt; &lt;span class="n"&gt;DataType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ModbusWordOrder&lt;/span&gt; &lt;span class="n"&gt;WordOrder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ModbusWordOrder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HighWordFirst&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El resultado es que &lt;code&gt;Reading.Value&lt;/code&gt; deja de ser una secuencia de bits para&lt;br&gt;
ser un número, un valor lógico o un texto, con independencia del protocolo&lt;br&gt;
que lo haya traído. La declaración es específica de Modbus, pero vive en&lt;br&gt;
una subclase que solo el canal conoce; el código que dibuja la tabla nunca&lt;br&gt;
la ve.&lt;/p&gt;

&lt;p&gt;El mismo criterio resuelve un caso más delicado. Modbus impide escribir en&lt;br&gt;
un registro de entrada, porque el protocolo lo define de solo lectura;&lt;br&gt;
pero una bobina que la instalación no permite accionar —un enclavamiento&lt;br&gt;
de seguridad— es escribible para el protocolo y no debe serlo para la&lt;br&gt;
aplicación. Esa restricción tampoco viaja en ninguna trama, así que se&lt;br&gt;
declara junto al dato, y el canal la hace cumplir antes de tocar el&lt;br&gt;
enlace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;DeviceData&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DataAccess&lt;/span&gt; &lt;span class="n"&gt;Access&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DataAccess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadWrite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  El contrato resultante
&lt;/h3&gt;

&lt;p&gt;Las operaciones siguen siendo las mismas. Lo que ha cambiado es que ahora&lt;br&gt;
describen con precisión lo que devuelven:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IDeviceChannel&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncDisposable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;DeviceId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsConnected&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ConnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;DisconnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeviceData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeviceData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;IAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Reading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SubscribeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;DeviceData&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso es cuanto ve quien consume los datos. Debajo hay dos implementaciones&lt;br&gt;
sin nada en común —una serializa transacciones sobre un socket y sondea en&lt;br&gt;
bucle; la otra mantiene un almacén alimentado por eventos del&lt;br&gt;
intermediario— y ninguna de esas dos realidades se filtra hacia arriba. El&lt;br&gt;
código que lee una temperatura por Modbus y una ocupación por MQTT es el&lt;br&gt;
mismo código recorriendo la misma lista.&lt;/p&gt;

&lt;p&gt;Añadir un tercer protocolo consiste en implementar esta interfaz una vez&lt;br&gt;
más: ni los canales existentes ni el consumidor se modifican. Y las&lt;br&gt;
preguntas que hay que responder al hacerlo son las que respondieron estos&lt;br&gt;
dos —qué parte del contrato trae el protocolo de fábrica, qué hay que&lt;br&gt;
construirle, y dónde queda declarado lo que no transmite.&lt;/p&gt;

&lt;p&gt;Hay además una consecuencia que conviene no pasar por alto. Si todo lo que&lt;br&gt;
distingue a un dato —dónde se encuentra, cómo se interpreta, si admite&lt;br&gt;
escritura— cabe en un &lt;code&gt;DeviceData&lt;/code&gt;, la instalación completa puede&lt;br&gt;
describirse en un archivo de configuración en lugar de en código. Un&lt;br&gt;
sensor nuevo es una entrada más en ese archivo, y el único punto del&lt;br&gt;
programa que menciona un protocolo es la función que decide qué subclase&lt;br&gt;
construir para cada entrada. Por encima de ella no queda ni una referencia&lt;br&gt;
a los canales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;La implementación de un contrato único sobre Modbus TCP y MQTT confirma&lt;br&gt;
que la unificación es viable incluso entre protocolos sin elementos&lt;br&gt;
comunes, y delimita con precisión su alcance y su coste. Del recorrido&lt;br&gt;
anterior se desprenden las siguientes conclusiones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La unificación de formatos no presenta contrapartidas.&lt;/strong&gt; Que un&lt;br&gt;
registro de dieciséis bits y una publicación MQTT se representen&lt;br&gt;
mediante un mismo tipo es una operación de traducción sin consecuencias&lt;br&gt;
para el consumidor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La unificación de semánticas tiene siempre un coste.&lt;/strong&gt; Equiparar la&lt;br&gt;
consulta y la notificación exige construir en cada canal la operación&lt;br&gt;
que su protocolo no proporciona, lo que introduce un sondeo que compite&lt;br&gt;
por el enlace, un almacén que debe mantenerse y una conversión que debe&lt;br&gt;
declararse.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ese coste no se elimina omitiéndolo del contrato.&lt;/strong&gt; Cuando no se&lt;br&gt;
declara, se manifiesta como un período de muestreo no elegido por el&lt;br&gt;
consumidor, como un código de error con significados distintos según la&lt;br&gt;
implementación, o como una operación de lectura que altera el estado del&lt;br&gt;
canal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La resolución en software traslada las decisiones a quien integra.&lt;/strong&gt;&lt;br&gt;
Frente al gateway o la plataforma SCADA, que fijan de antemano los&lt;br&gt;
protocolos admitidos y los compromisos aplicables, aquí incorporar un&lt;br&gt;
protocolo consiste en implementar la interfaz una vez más y los valores&lt;br&gt;
de compromiso se ajustan a cada instalación.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La automatización industrial y el desarrollo de software proceden de&lt;br&gt;
tradiciones distintas: la primera razona en registros, esclavos y tiempos&lt;br&gt;
de ciclo; la segunda, en contratos, tipos y dependencias. Una interfaz de&lt;br&gt;
estas características constituye el punto de contacto entre ambas, y es&lt;br&gt;
también donde se concentra el trabajo de diseño.&lt;/p&gt;

&lt;h3&gt;
  
  
  El código
&lt;/h3&gt;

&lt;p&gt;El contrato descrito, los dos canales y un consumidor que los emplea sin&lt;br&gt;
código específico de protocolo están disponibles en&lt;br&gt;
&lt;a href="https://github.com/bmcutting/multi-protocol-device-interface" rel="noopener noreferrer"&gt;multi-protocol-device-interface&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Incluye una aplicación de consola desde la que se ejecutan las cinco&lt;br&gt;
operaciones del contrato, con dispositivos simulados dentro del propio&lt;br&gt;
proceso o contra software de simulación externo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lectura complementaria
&lt;/h3&gt;

&lt;p&gt;El sistema en el que se integra una capa de comunicación como esta abarca&lt;br&gt;
bastante más que el contrato con el dispositivo. Para el lector interesado&lt;br&gt;
en esa perspectiva, Medina Rodríguez, Pérez Villanueva y Prieto Moreno&lt;br&gt;
desarrollan una capa completa de Computación al Borde para un sistema de&lt;br&gt;
gestión de operaciones de fabricación, en la que una interfaz de&lt;br&gt;
comunicación resuelve la conexión con las capas superior e inferior.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Medina Rodríguez, Z., Pérez Villanueva, H. D. y Prieto Moreno, A.&lt;br&gt;
(2024). Propuesta de arquitectura para la capa de Computación al Borde&lt;br&gt;
en entornos de Industria 4.0. &lt;em&gt;Revista Científica de Ingeniería&lt;br&gt;
Electrónica, Automática y Comunicaciones&lt;/em&gt;, 45(2). ISSN 1815-5928.&lt;br&gt;
&lt;a href="https://dialnet.unirioja.es/servlet/articulo?codigo=9781718" rel="noopener noreferrer"&gt;https://dialnet.unirioja.es/servlet/articulo?codigo=9781718&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>iot</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
