01 · Punto de partida
Un número con contexto
En los temas anteriores el dato nació, fue corregido y se le asignó una frecuencia de lectura. Ahora hay que ponerlo en el sobre que viajará por la red. Lo más tentador es enviar solo el número (4.3) porque es corto y fácil. Funciona en el prototipo con un sensor. Pero con cien equipos, tres tipos de sensor y un servidor que debe interpretarlo años después, ese número solo es un acertijo.
Un buen mensaje responde por sí mismo a las preguntas básicas: quién, cuándo, qué, cuánto y con qué calidad. Y como cada byte cuesta energía, ancho de banda y dinero, tiene que responderlas con la menor cantidad de bytes posible. Este tema trata de ese equilibrio entre claridad y economía, que cierra el bloque de sensores y prepara el de redes.
Los tamaños que aparecen en el tema no son estimaciones: el código de Python y el laboratorio construyen los mensajes reales y cuentan sus bytes.
02 · Estructura
Anatomía de una lectura
Una lectura de telemetría bien formada lleva, por lo menos, los campos de la tabla. No siempre hacen falta todos en cada mensaje, pero hay que decidir conscientemente cuáles se incluyen y cuáles se deducen del contexto (por ejemplo, el identificador puede ir en el tópico MQTT en lugar del cuerpo).
| Campo | Para qué sirve | Ejemplo | Error típico |
|---|---|---|---|
| Identificador del dispositivo | Saber quién envió la lectura | camara-07 | Un identificador compartido por varios equipos |
| Momento de la medición | Ordenar los datos y detectar huecos | 2026-11-07T14:32:00Z | Poner la hora de llegada o una hora local sin zona |
| Valor | La medida en sí | 4.3 | Texto con coma decimal o separador de miles |
| Unidad (o magnitud) | Dar sentido al valor | temperatura_c | Unidad implícita que cambia entre modelos |
| Calidad | Decir cuánto confiar en el dato (tema 10) | ok, sensor_sin_respuesta | Reemplazar una lectura inválida por un cero |
| Estado del dispositivo | Vigilar la salud del equipo | bateria_pct: 87, señal | No informarlo, y enterarse cuando el equipo muere |
| Versión | Saber cómo interpretar el mensaje (esquema, firmware, calibración) | v: 2 | Cambiar el significado de un campo sin avisar |
| Número de secuencia | Detectar mensajes perdidos o duplicados | seq: 1042 | Contar a ciegas y reiniciar el contador al arrancar |
Los dos primeros campos son los que más se subestiman. Sin identificador, los datos no se pueden atribuir; sin momento, no se pueden ordenar ni comparar, y un envío atrasado por un corte de red parece una lectura nueva. Recordá el principio del tema 9: la marca de tiempo se toma al medir, no al enviar.
03 · Representación
Valores, unidades y tiempo
Nombres con la unidad adentro
El nombre del campo es la documentación más barata. temperatura_c o presion_hpa dicen en qué unidad están sin consultar nada. Conviene usar unidades del Sistema Internacional o las convencionales de cada rubro, mantenerlas fijas para siempre y, si algún día cambian, crear un campo nuevo en lugar de reutilizar el nombre con otro significado.
Enteros escalados en lugar de decimales
Un sensor de ±0,3 °C no necesita ocho decimales. Representar 22,3 °C como el entero 223 («décimas de grado») ocupa dos bytes, mientras que un número de punto flotante de doble precisión ocupa ocho y uno de simple precisión, cuatro. Los enteros escalados son compactos, exactos (no tienen los errores de redondeo de los decimales binarios, donde 0,1 no se representa exactamente) y rápidos de procesar. Con dos bytes con signo se representan de −32.768 a 32.767: en décimas de grado, de −3276,8 a 3276,7 °C, de sobra para cualquier temperatura ambiente.
La regla es simple: el factor de escala se documenta en el esquema, no en cada mensaje. El servidor sabe que el campo t está en décimas de grado y divide al recibir.
El tiempo, siempre en hora universal
Hay dos representaciones prácticas. El formato ISO 8601 en UTC (2026-11-07T14:32:00Z) es legible y inequívoco, pero ocupa 20 bytes. La hora Unix o epoch (segundos desde el 1 de enero de 1970 en UTC, por ejemplo 1794061920) ocupa cuatro bytes como entero. Para mensajes cortos conviene el segundo; para depurar, el primero. La conversión a la hora local, con su huso horario y su horario de verano, se hace al mostrar, nunca al guardar.
04 · Formatos
Formatos para el mensaje
Una vez decididos los campos, hay que codificarlos en bytes. Las opciones se ordenan en un espectro entre «legible por personas» y «compacto».
| Formato | Tipo | Ventajas | Inconvenientes |
|---|---|---|---|
| CSV o texto plano | Texto, sin estructura | Mínimo; fácil de armar con sprintf | Depende del orden de las columnas; sin tipos; difícil de ampliar |
| JSON | Texto, autodescriptivo | Legible, universal, lo entiende cualquier servidor y navegador | Repite los nombres de los campos en cada mensaje; los números ocupan una cifra por dígito |
| CBOR y MessagePack | Binario, autodescriptivo | Misma estructura que JSON (mapas, listas, textos, números) en mucho menos espacio | No se pueden leer a simple vista; necesitan biblioteca |
| Protocol Buffers y similares | Binario, con esquema externo | Muy compacto; el esquema se comparte aparte, con versionado integrado | Hay que compilar y distribuir el esquema; más herramientas |
| Binario propio | Binario, formato a medida | El más compacto posible; control total | Hay que escribir y mantener codificador y decodificador, y manejar orden de bytes y versiones |
Para ver la diferencia, sirve medir la misma lectura en cuatro formatos. Una lectura de temperatura con identificador, momento, valor y batería ocupa:
- JSON con claves largas: 112 bytes. Los nombres (
dispositivo,momento,temperatura_c) y la hora en texto son la mayor parte. - JSON compacto (claves de una letra, hora en epoch, valor entero): 49 bytes.
- CBOR con la misma estructura: 27 bytes.
- Binario propio: 13 bytes.
Ningún formato es el correcto en abstracto. JSON es el punto de partida razonable cuando el canal es Wi-Fi y el volumen es bajo: se depura a simple vista. CBOR es un excelente término medio, porque conserva la estructura y el significado de los campos con un tercio del tamaño. El binario propio se reserva para canales muy exigentes, como LoRa, donde cada byte importa. Una buena práctica es decodificar y convertir a un formato legible lo antes posible en el servidor.
05 · Agrupar
Agrupar lecturas
Si el dispositivo lee cada minuto y envía cada lectura por separado, repite en cada mensaje el identificador, la cabecera del protocolo y, en el peor caso, los nombres de los campos. La alternativa es el lote: se acumulan varias lecturas y se envían juntas, con un tiempo base y un intervalo fijo en lugar de una marca de tiempo por lectura.
Ese esquema, usado en el código de este tema, elimina las marcas de tiempo individuales. Con el formato compacto, una lectura sola cuesta 49 bytes, pero diez en un lote cuestan 76 bytes (7,6 por lectura) y sesenta cuestan 226 (3,8 por lectura). En CBOR, sesenta lecturas pesan 146 bytes: 2,4 por lectura.
Pero agrupar tiene un precio que hay que conocer:
- Latencia. La primera lectura del lote espera a que se complete para viajar. Un lote de 60 lecturas por minuto tarda una hora en salir.
- Pérdida concentrada. Si un lote se pierde, se pierde todo su período en una sola vez. Los lotes grandes necesitan confirmación y reenvío.
- Memoria. El dispositivo debe guardar las lecturas pendientes, y si se reinicia, perderlas si están solo en RAM.
- Las alarmas no esperan. Una condición urgente se envía de inmediato en un mensaje propio; solo la telemetría rutinaria se agrupa.
El lote es también la forma natural de vaciar el almacenamiento local después de un corte de red: todo lo acumulado sale de una vez, con las marcas de tiempo originales, cuando vuelve la conexión.
06 · Cambio
Un esquema que sobreviva a los cambios
Un producto IoT vive años, y en ese tiempo el mensaje va a cambiar: aparece un sensor nuevo, hay que sumar un campo, cambia la calibración. Pero los equipos ya instalados siguen enviando la versión vieja, y no todos se actualizan a la vez. El esquema debe diseñarse para convivir con versiones distintas.
- Un campo de versión desde el primer día, aunque valga siempre 1.
- Agregar es seguro, cambiar no. Sumar un campo opcional no rompe a quien no lo conoce; cambiar la unidad o el significado de uno existente sí. Si hay que cambiar, se crea un campo nuevo.
- Ignorar lo desconocido. El receptor debe aceptar sin error los campos que no reconoce; así, un servidor viejo sigue funcionando cuando llegan equipos nuevos.
- Valores por defecto para los campos ausentes, de modo que un equipo viejo que no envía la batería no genere un error en el servidor.
- Documentar y probar. El esquema es un contrato entre firmware y plataforma: se escribe, se versiona y se prueba con mensajes reales de cada versión.
En formatos binarios a medida, el campo de versión y la longitud variable son lo que permite crecer. Un mensaje de formato fijo, sin versión, es una trampa: cualquier cambio exige actualizar todos los equipos y el servidor al mismo tiempo, algo imposible en una flota real.
07 · Prudencia
Qué no poner y qué no creer
Un mensaje es una entrada hacia tu sistema, y como toda entrada merece desconfianza y cuidado.
- No incluir secretos. Claves, contraseñas o tokens no viajan en la telemetría. La autenticación pertenece al canal, no al contenido (bloque de seguridad).
- Minimizar datos personales. Si el dispositivo está en una casa o lo lleva una persona, cada dato que sale (ubicación, hábitos, salud) es sensible. Se envía lo necesario y se justifica cada campo.
- El servidor no confía en el mensaje. Valida tipos, rangos y tamaños, y descarta lo que no cumple. Un equipo comprometido, o un error de firmware, puede enviar cualquier cosa.
- Límites de tamaño. Un mensaje debe caber en los búferes del dispositivo, en el payload del canal y en lo que el servidor acepta. Reservar memoria con un tamaño máximo conocido evita desbordes.
- Texto bien formado. Los textos con comillas, saltos de línea o caracteres especiales deben escaparse al armar el JSON; concatenar cadenas a mano es un clásico generador de mensajes rotos.
08 · Analogía con programación
Medir el tamaño de un mensaje en Python
El programa construye, con el mismo contenido, la telemetría de una cámara frigorífica (identificador, hora de la primera lectura, intervalo de 60 segundos, temperaturas en décimas de grado y batería) en los cuatro formatos y mide sus bytes reales para 1, 10 y 60 lecturas por mensaje. Incluye un codificador CBOR mínimo (enteros, textos, listas y mapas) escrito a mano, para ver cómo funciona por dentro, y un empaquetado binario con struct. Con una lectura por minuto, calcula los mensajes por día y los megabytes por mes.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
import json
import struct
from datetime import datetime, timedelta, timezone
BASE = datetime(2026, 11, 7, 14, 32, tzinfo=timezone.utc) # instante de la primera lectura
INTERVALO_S = 60 # una lectura por minuto
LECTURAS_DIA = 1440
def valor_decimas(i):
return 41 + (i * 3) % 8 # temperatura en décimas de grado: 4,1 a 4,8 °C
def cbor_cabeza(mayor, n):
if n < 24:
return bytes([mayor << 5 | n])
if n < 256:
return bytes([mayor << 5 | 24, n])
if n < 65536:
return bytes([mayor << 5 | 25]) + n.to_bytes(2, "big")
return bytes([mayor << 5 | 26]) + n.to_bytes(4, "big")
def cbor(x):
"""Codificador CBOR mínimo: enteros no negativos, textos, listas y mapas."""
if isinstance(x, int):
return cbor_cabeza(0, x)
if isinstance(x, str):
b = x.encode("utf-8")
return cbor_cabeza(3, len(b)) + b
if isinstance(x, list):
return cbor_cabeza(4, len(x)) + b"".join(cbor(v) for v in x)
return cbor_cabeza(5, len(x)) + b"".join(cbor(k) + cbor(v) for k, v in x.items())
def mensaje(formato, n):
valores = [valor_decimas(i) for i in range(n)]
t0 = int(BASE.timestamp())
if formato == "json largo": # una lectura completa por cada medida
lecturas = [{"momento": (BASE + timedelta(seconds=i * INTERVALO_S)).strftime("%Y-%m-%dT%H:%M:%SZ"),
"temperatura_c": valores[i] / 10} for i in range(n)]
return json.dumps({"dispositivo": "camara-07", "bateria_pct": 87, "lecturas": lecturas},
separators=(",", ":")).encode()
compacto = {"d": "c07", "t": t0, "i": INTERVALO_S, "v": valores, "b": 87}
if formato == "json compacto":
return json.dumps(compacto, separators=(",", ":")).encode()
if formato == "CBOR":
return cbor(compacto)
return struct.pack("<IIBH", 7, t0, 87, INTERVALO_S) + struct.pack(f"<{n}h", *valores) # binario propio
print("formato lecturas/mensaje bytes bytes/lectura mensajes/día MB/mes")
for formato in ("json largo", "json compacto", "CBOR", "binario"):
for n in (1, 10, 60):
b = len(mensaje(formato, n))
mensajes = LECTURAS_DIA / n
mb = mensajes * b * 30 / 1e6
print(f"{formato:<14} {n:>10} {b:>12} {b / n:>14.1f} {mensajes:>14.0f} {mb:>9.2f}")
print()
print(mensaje("json largo", 1).decode())
print(mensaje("json compacto", 3).decode())
Resultado con los valores iniciales. Con una lectura por mensaje: JSON largo 112 bytes (4,84 MB por mes), JSON compacto 49 (2,12 MB), CBOR 27 (1,17 MB) y binario 13 (0,56 MB). Con diez lecturas: 607, 76, 45 y 31 bytes; con sesenta: 3357, 226, 146 y 131. En el extremo, sesenta lecturas en binario ocupan 131 bytes (2,2 por lectura, 0,09 MB por mes) contra 3357 del JSON largo: unas 26 veces menos bytes en total. Al final el programa imprime los dos mensajes JSON para ver cómo se ven: el largo, legible pero pesado, y el compacto, que apenas se lee pero ocupa la mitad.
El resultado desmiente una intuición: la mayor parte de un mensaje común no son datos sino estructura. En la lectura de JSON largo hay tres caracteres de valor útil (4.1) y más de cien de nombres, comillas y marcas de tiempo. Agrupar y compactar recupera casi toda esa diferencia, sin perder información.
09 · Exploración interactiva
¿Cabe mi mensaje?, de extremo a extremo
Este laboratorio construye los mismos mensajes que el código anterior y cuenta sus bytes. Elegí el formato, cuántas lecturas se agrupan y el canal. Las barras comparan los cuatro formatos con las mismas lecturas, en escala logarítmica; la línea naranja es el límite del canal y las barras rojas son los que no caben. Los límites de LoRa son valores típicos de los planes de velocidad más lento y más rápido de LoRaWAN, y el costo del celular es un valor didáctico.
Tamaño de un mensaje de telemetría
Elegí los parámetros y presioná «Armar el mensaje».
Recorrido guiado
- Con los valores iniciales (CBOR, 10 lecturas, LoRa lenta), armá el mensaje: 45 bytes, 4,5 bytes por lectura, 0,19 MB por mes. Cabe en el límite de 51 bytes: «Eficiente».
- Cambiá a JSON compacto: 76 bytes, no cabe en LoRa lenta. Misma información, otro formato. Pasá el canal a LoRa rápida (222 bytes) y ahora cabe.
- Volvé a LoRa lenta y elegí JSON con claves largas: 112 bytes con una lectura y 607 con diez. No cabe ni en LoRa rápida con diez lecturas.
- Con binario propio: 1 lectura son 13 bytes, 10 lecturas 31 bytes, y con 60 lecturas 131 bytes, que no caben en LoRa lenta pero sí en la rápida. Probá con el límite de 51 bytes cuántas lecturas entran: 20 (11 + 2 × 20 = 51).
- Elegí el canal Celular con JSON largo y una lectura por mensaje: 4,84 MB por mes y US$ 9,68 por equipo. Con CBOR y 60 lecturas por mensaje: 0,11 MB y US$ 0,21. El costo del formato se multiplica por toda la flota.
- Con Wi-Fi, el mensaje siempre «cabe», pero el resultado marca «Cabe, pero pesa» si superás 1 MB por mes. El límite no es técnico: es de costo y de energía, porque cada byte extra mantiene la radio encendida más tiempo.
Desafío: el costo de repetir la cabecera
El JSON compacto con una lectura pesa 49 bytes y con diez, 76. La diferencia (27 bytes) es el costo de nueve lecturas más: 3 bytes cada una (dos cifras y una coma). Los 46 bytes restantes son la cabecera: nombres de campo, identificador, tiempo base, intervalo y batería. Enviar diez lecturas por separado cuesta 10 × 49 = 490 bytes; en un lote cuesta 76. Se ahorra un 84 % sin perder ninguna medida. Esa cabecera repetida es el mayor desperdicio de la mayoría de los sistemas IoT: se paga por los datos una vez y por el sobre cada vez.
10 · Método de trabajo
Cómo diseñar un mensaje de telemetría
- Listá las preguntas que debe poder contestar quien reciba el mensaje: quién, cuándo, qué, cuánto, con qué calidad, en qué estado está el equipo.
- Elegí la representación de cada campo: enteros escalados para las medidas, tiempo en hora universal, identificador corto pero único.
- Calculá el presupuesto de bytes del canal: payload máximo, energía por byte, costo por megabyte.
- Elegí el formato: JSON si hay margen y se prioriza la depuración; CBOR como término medio; binario propio solo si el canal lo exige.
- Agrupá lecturas con tiempo base e intervalo, y definí qué se envía de inmediato (alarmas) y qué en lote.
- Agregá versión y reglas de evolución: campos opcionales, valores por defecto, ignorar lo desconocido.
- Escribí el contrato con ejemplos y pruebas que ejecuten codificador y decodificador con mensajes de cada versión.
- Medí bytes reales en el dispositivo y verificá que el mensaje peor (la mayor cantidad de campos y los números más largos) siga cabiendo.
Y un criterio práctico: empezá por el formato más legible que el canal tolere, medí, y compactá solo lo que lo justifique. La legibilidad también tiene valor, y se paga en depuración.
11 · Comprensión
Confusiones frecuentes
«JSON es siempre la mejor opción»
Es excelente por su legibilidad y su soporte universal, pero es de los formatos más pesados: repite los nombres y escribe los números como texto. En canales con payload limitado o con costo por byte, un formato binario compacto ahorra mucho.
«Un formato binario propio es siempre mejor porque es más corto»
Es el más pequeño, pero obliga a mantener un codificador y un decodificador, a manejar el orden de los bytes y a versionar a mano. Si CBOR alcanza, ahorra mucho trabajo y errores por pocos bytes de diferencia.
«Los decimales se envían como decimales»
Es más compacto y exacto enviar un entero escalado (223 en lugar de 22,3). El punto flotante ocupa más y arrastra errores de representación; se evita en los mensajes de bajo consumo.
«La hora la pone el servidor al recibir»
Con cortes de red y envíos en lote, la hora de recepción no es la de la medición. El dispositivo debe poner el momento de cada lectura, en UTC, o los datos pierden su significado.
«Un mensaje más corto siempre ahorra energía»
Ahorra el tiempo de transmisión, pero si la compactación obliga a muchos cálculos, o a mantener la radio encendida para reenviar lo que no se entendió, puede no compensar. Lo que más ahorra no es acortar un mensaje sino mandar menos mensajes, agrupando.
«El esquema se define después, cuando el producto crezca»
Con los equipos ya instalados, cada cambio es una actualización en campo. Un campo de versión y reglas de compatibilidad desde el primer mensaje son casi gratuitos al inicio y carísimos de agregar después.
12 · Práctica guiada
Predecí antes de comprobar
1. Entero escalado
Querés enviar temperaturas con resolución de 0,01 °C entre −40 y 85 °C. ¿Cómo las representarías y cuántos bytes necesitas por valor?
Ver solución
Como entero escalado en centésimas de grado: −4000 a 8500. Entra en un entero de 16 bits con signo (−32.768 a 32.767), así que bastan 2 bytes por valor. Un decimal de doble precisión usaría 8.
2. Texto contra binario
El mensaje JSON {"t":22.3} pesa 10 bytes. ¿Cuántos pesaría el mismo dato como entero con signo de 16 bits (223 décimas)? ¿Cuántas veces menos?
Ver solución
El JSON tiene 10 caracteres (llaves, comillas, dos puntos, la clave, el valor). El entero de 16 bits ocupa 2 bytes: cinco veces menos. El precio es que el receptor debe conocer el significado del campo y la escala.
3. Cuántas lecturas entran
El límite de LoRa lenta es 51 bytes. Con los tamaños del tema, ¿cuántas lecturas caben en un mensaje de binario propio (11 + 2n), de CBOR (25 + 2n) y de JSON compacto (46 + 3n)?
Ver solución
Binario: 11 + 2n ≤ 51 → n ≤ 20. CBOR: 25 + 2n ≤ 51 → n ≤ 13. JSON compacto: 46 + 3n ≤ 51 → n ≤ 1. El mismo canal admite entre una y veinte lecturas según el formato.
4. Ahorro de enviar en lote
Enviar diez lecturas por separado en JSON compacto cuesta 10 × 49 bytes. En un lote cuesta 76. ¿Cuánto se ahorra, en bytes y en porcentaje?
Ver solución
Por separado: 490 bytes. En lote: 76. Ahorro de 414 bytes, es decir 84,5 %. Además, se hace una transmisión en lugar de diez, por lo que la radio se enciende una sola vez.
5. El año 2038
Un dispositivo guarda la hora como entero de 32 bits con signo. ¿Qué ocurre el 19 de enero de 2038 y cómo se evita?
Ver solución
El contador de segundos supera 2.147.483.647 (2³¹ − 1) y se desborda a un número negativo: la hora salta a 1901. Se evita usando un entero de 32 bits sin signo (válido hasta 2106) o de 64 bits. Un equipo con vida de 15 años instalado hoy llegaría a esa fecha.
6. Compatibilidad
Agregás al firmware un campo nuevo de humedad. El servidor viejo recibe mensajes con ese campo. ¿Qué debe hacer? Y si un equipo viejo no envía humedad, ¿qué hace el servidor nuevo?
Ver solución
El servidor viejo debe ignorar el campo desconocido y procesar el resto, sin rechazar el mensaje. El servidor nuevo debe tratar la humedad como opcional: si falta, usa un valor por defecto (por ejemplo, «sin dato») y no falla. Así conviven ambas versiones mientras la flota se actualiza.
13 · Síntesis
Ideas para recordar
- Un mensaje es una lectura con contexto: quién, cuándo, qué, cuánto y con qué calidad, más el estado del dispositivo y la versión.
- El nombre de cada campo lleva su unidad; si una unidad cambia, se crea un campo nuevo en lugar de reutilizar el anterior.
- Los enteros escalados son más compactos y exactos que los decimales; la escala se documenta en el esquema.
- El tiempo se guarda en UTC y se toma al medir; la epoch en segundos ocupa cuatro bytes, el ISO 8601 veinte.
- JSON es legible pero pesado; CBOR conserva la estructura con un tercio del tamaño; el binario propio es el más chico a costa de mantenimiento.
- La mayor parte de un mensaje común es estructura y no datos; agrupar lecturas con tiempo base e intervalo evita repetirla.
- Agrupar suma latencia y concentra pérdidas; las alarmas se envían de inmediato.
- El esquema debe incluir versión, campos opcionales y reglas para ignorar lo desconocido, de modo que sobreviva a los cambios.
- No se envían secretos, se minimizan los datos personales y el servidor valida todo lo que recibe.
¿Listo para el bloque 3?
Antes de avanzar, diseñá el mensaje de telemetría de un dispositivo que conozcas: elegí los campos, la representación de cada uno, el formato y la cantidad de lecturas por lote. Calculá cuántos bytes pesa, cuántos mensajes por día genera y cuántos megabytes por mes. Si cabe en el canal que pensás usar con margen, el diseño está listo para pasar a la red.
En el próximo tema empezamos el bloque de redes y conectividad con las redes para dispositivos IoT: Wi-Fi, Bluetooth, Zigbee, LoRa y celular, cuál conviene para cada caso y qué alcance, velocidad y consumo ofrece cada una.