01 · Objetivo
Bytes sin significado
Lo que cruza el cable son bytes, y los bytes no traen etiqueta. Los cuatro de la figura, 41 42 43 44, son la palabra «ABCD» si se leen como texto, más de mil millones si se leen como un entero (y dos valores distintos según el orden), un número con decimales si se leen como coma flotante y dos enteros chicos si se leen de a dos. Ninguna de esas lecturas es «la correcta»: lo es la que el emisor tenía en mente. Todo el significado de un mensaje está en un acuerdo entre los dos extremos: qué campos hay, en qué orden, de qué tamaño, con qué signo, en qué unidad y con qué escala. Ese acuerdo es el formato.
Es peligroso porque los errores de formato no se anuncian. Un CRC valida que los bits llegaron como salieron, no que se interpreten como se escribieron. El receptor que lee al revés los dos bytes de una temperatura obtiene 117,85 °C en lugar de 23,50 °C, sin una sola falla de comunicación. Por eso el formato se documenta como se documenta una interfaz de programación (con una tabla de campos, tamaños, órdenes, rangos y unidades), se prueba con vectores conocidos y se valida en la recepción con el sentido común de los rangos. Es el último eslabón del bloque de protocolos: los temas anteriores hacen que los bytes lleguen; este, que signifiquen lo mismo en los dos lados.
02 · Dos enfoques
Binario o texto
Los formatos se dividen en dos grandes familias. En un formato binario, cada campo ocupa un número fijo de bytes con el valor tal cual (un entero de 16 bits son dos bytes). En un formato de texto, los valores se escriben con caracteres legibles (el número 2350 son los cuatro caracteres «2», «3», «5», «0»), separados por delimitadores. Ya se usaron los dos: NMEA y los comandos AT son texto (tema 10); Modbus, el paquete del tema 41 y el CAN son binarios.
| Aspecto | Binario | Texto |
|---|---|---|
| Tamaño | El mínimo: el valor ocupa lo que necesita | Varias veces mayor: una cifra por carácter, más separadores y, a veces, nombres de campo |
| Velocidad de proceso | Directa: copiar bytes | Hay que convertir cadenas a números y viceversa (atoi, sprintf) |
| Depuración | Ilegible sin la tabla de campos: hace falta un analizador | Se lee en un monitor serie o con un navegador |
| Ambigüedades | Orden de bytes, signo, alineación, tamaño de los tipos | Separador decimal, codificación de caracteres, fin de línea, comillas |
| Evolución | Agregar un campo cambia posiciones: exige versión o diseño previo | Si los campos tienen nombre, un campo nuevo se ignora sin problema |
| Ejemplos | Modbus, CAN, 1-Wire, paquetes propios, BLE | NMEA, comandos AT, HTTP, JSON sobre MQTT |
No hay un ganador. En un enlace lento, con baterías o con tramas chicas (RS-485 a 9600 baudios, una radio de baja potencia, un mensaje de BLE de 20 bytes), el binario gana por mucho: la herramienta 4 muestra que un registro de cuatro campos ocupa 7 bytes en binario y 35 en JSON, y la diferencia se paga en tiempo en el aire y en energía. En un sistema que habla con un navegador, con una nube o con personas, y donde el volumen es chico, el texto estructurado (JSON) ahorra mucho tiempo de desarrollo y depuración. Y hay una tercera posición, cada vez más usada en dispositivos pequeños: un formato binario autodescriptivo, como CBOR (sección 7), que conserva la estructura de JSON con el tamaño de un binario.
Una regla práctica: se empieza con el formato más legible que el enlace permita y se pasa al binario compacto cuando el tamaño, la energía o el tiempo lo exigen. Y se separan las capas: la forma en que se codifica un valor (este tema) es independiente de cómo viaja (el paquete) y cómo se protege (CRC, confirmaciones). Un mismo registro puede viajar como binario dentro de una trama RS-485 y como JSON dentro de un mensaje MQTT, si la puerta de enlace los convierte (tema 51).
03 · Quién va primero
El orden de los bytes
Un entero de más de un byte hay que escribirlo en un orden, y hay dos: big-endian (el byte más significativo primero, como se escribe a mano: 0x092E son 09 2E) y little-endian (el menos significativo primero: 2E 09). Ninguno es mejor; es una convención, y los dos extremos tienen que usar la misma. Quién usa cuál:
| Tecnología | Orden | Comentario |
|---|---|---|
| Procesadores: AVR (Arduino), ESP32, ARM Cortex-M, x86 | Little-endian | Un int guardado en memoria tiene el byte bajo en la dirección más baja |
| Protocolos de red: IP, TCP, UDP | Big-endian («orden de red») | De ahí htons y htonl («host a red») |
| Modbus (tema 25) | Big-endian para los registros de 16 bits | El CRC, en cambio, se envía con el byte bajo primero. Los valores de 32 bits (dos registros) tienen un orden de palabras que cambia entre fabricantes |
| Sensores I²C y SPI (temas 16 a 18) | Depende del chip | La mayoría, big-endian (el byte alto primero), con excepciones: lo dice la hoja de datos |
| BLE y USB | Little-endian | Los campos multibyte de BLE (GATT) y de las descripciones de USB |
| CBOR, ASN.1 y la mayoría de los formatos de datos modernos | Big-endian | Los enteros largos se escriben con el byte alto primero |
Como el procesador y el cable pueden usar órdenes distintos, no se copia memoria a un búfer para enviar un entero: se arma byte a byte, con desplazamientos, en el orden que fija el protocolo. El código es el mismo en cualquier chip y no depende de cómo guarda el procesador:
p[0] = v >> 8; p[1] = v & 0xFF; (big-endian) | p[0] = v & 0xFF; p[1] = v >> 8; (little-endian)
También cuenta el orden de los bits dentro de un byte (el menos significativo primero en la UART, el más significativo en SPI e I²C, por lo general: temas 7, 13 y 17), pero eso lo resuelve el hardware del bus y rara vez llega al programa. El orden de los bytes, en cambio, es del programador, y la herramienta 1 muestra la diferencia para cualquier valor: 2350 (0x092E) leído en el orden contrario es 11 785.
Enteros con signo: el complemento a dos
Los enteros con signo se representan en complemento a dos: el bit más alto indica el signo y los negativos son 2n − |v|. En 16 bits, −1 es 0xFFFF y −2 es 0xFFFE; el patrón 0xFFFE es 65 534 sin signo y −2 con signo. La consecuencia es que el receptor tiene que saber si el campo tiene signo. Leer una temperatura de −2 °C (en centésimas, −200 = 0xFF38) como un entero sin signo da 65 336, o 653,36 °C. Y al leer en C un byte con signo en un entero más ancho, hay que extender el signo ((int16_t) sobre un uint16_t lo hace; ensamblar con | y << sin el cast final, no).
04 · Valores con decimales
Números: enteros, punto fijo y flotante
Para transmitir una magnitud física (23,5 °C, 3,30 V) hay dos soluciones. La primera es un entero escalado (punto fijo): se acuerda una escala y se manda el valor multiplicado por ella, como un entero. La temperatura de 23,50 °C con escala 100 es 2350. El receptor divide. La segunda es la coma flotante (IEEE 754), que guarda el número como una mantisa y un exponente:
| Aspecto | Entero escalado (punto fijo) | float de 32 bits | double de 64 bits |
|---|---|---|---|
| Tamaño | 1, 2 o 4 bytes, a elección | 4 bytes (1 de signo, 8 de exponente, 23 de mantisa) | 8 bytes (1, 11 y 52) |
| Resolución | Fija y exacta (una centésima, con escala 100) | Unas 7 cifras significativas; varía con el valor | Unas 15 o 16 cifras |
| Rango | Acotado: ±327,67 con 16 bits y escala 100 | Enorme (hasta 3,4 × 1038) | Enorme (hasta 1,8 × 10308) |
| Decimales exactos | Sí, a la escala elegida | No: 0,1 se guarda como 0,100000001490116… | No: 0,1 también es aproximado |
| Costo en un microcontrolador | Aritmética entera, rápida | Unidad de coma flotante o emulación por software (AVR: lenta) | En muchos chips, emulado: lento y grande |
| Valores especiales | Ninguno (se puede reservar uno para «sin dato») | NaN, ±infinito | NaN, ±infinito |
Para una medición con rango y resolución conocidos (una temperatura, una tensión, un porcentaje), el entero escalado es casi siempre mejor: ocupa la mitad (2 bytes contra 4), no tiene errores de redondeo que sorprendan, se compara sin cuidados y se calcula sin unidad de coma flotante. Su costo es que la escala y la unidad son parte del formato y hay que documentarlas (centésimas de grado, milivoltios, décimas de por ciento). La coma flotante conviene cuando el rango es enorme o desconocido, o cuando se intercambia con software científico. Si se usa, hay que tener presentes sus trampas: el 0,1 no es exacto, las comparaciones de igualdad no son fiables, y los valores NaN e infinito pueden aparecer y deben validarse. La herramienta 2 muestra los bytes de cada representación y el error.
Se aplican las mismas ideas a todo lo demás. Un booleano no necesita un byte: ocho se guardan en uno, con una máscara por bit (una palabra de estado). Una marca de tiempo se manda como segundos desde una época fija en UTC (el tiempo Unix, un entero de 32 bits sin signo hasta 2106, o con signo hasta 2038), no como «14:03 del martes» (que pierde la zona horaria y los cambios de hora). Un texto de longitud variable se manda con su largo adelante (un byte con la cantidad de caracteres) o con un terminador (el cero de C), con la advertencia de que el terminador no sirve si el texto puede contenerlo. Un enumerado («modo: 0 = reposo, 1 = medición, 2 = alarma») es un entero con una tabla acordada. En cada caso: tamaño, signo, orden, unidad y rango, en una tabla.
05 · Lo que el compilador acomoda
Estructuras y alineación
En un programa en C, un registro natural es una struct. Es tentador enviarla tal cual: Serial1.write((uint8_t*)®istro, sizeof(registro)). Funciona en la mesa, entre dos placas idénticas, y falla en cuanto cambia algo, por tres motivos:
- Relleno (padding). Para que el procesador lea cada campo de una vez, el compilador coloca cada uno en una dirección múltiplo de su tamaño (alineación) y deja bytes vacíos en el medio y al final. La estructura
{ uint8_t id; uint32_t valor; uint16_t t; uint8_t estado; }suma 8 bytes de campos, pero ocupa 12 en un ESP32: 3 bytes de relleno después deid(para quevalorempiece en una dirección múltiplo de 4) y 1 al final (para completar un múltiplo de 4). Esos bytes llevan basura y viajan. - Plataformas distintas, tamaños distintos. Un AVR de 8 bits no alinea (la estructura mide 8), un ESP32 o un Cortex-M alinean hasta 4 (12) y un PC hasta 8. Un
intmide 2 bytes en un AVR y 4 en un ESP32; unlong, 4 u 8 según el sistema. El mismo código describe formatos distintos. - Orden de bytes. Una estructura copiada de memoria usa el orden del procesador. Un PC o un ESP32 son little-endian, pero un microcontrolador big-endian o una puerta de enlace de otra familia leerían todo al revés.
La solución es serializar campo por campo: una función que escribe cada campo, en el orden, el tamaño y el orden de bytes que fija el protocolo, y otra que hace el camino inverso. Ocupa unas líneas por campo, funciona en cualquier chip y deja el formato escrito en el código. Existen atajos que empeoran las cosas: __attribute__((packed)) quita el relleno, pero hace que el compilador genere accesos a campos desalineados, que son más lentos en algunos procesadores y provocan fallas en otros (ciertos Cortex-M0, por ejemplo); y no resuelve el orden de bytes ni los tamaños de los tipos. Dentro del programa se usan los tipos de tamaño fijo (uint8_t, int16_t, uint32_t), nunca int ni long en un formato externo.
La herramienta 3 calcula el diseño de una estructura para tres alineaciones y propone el orden de los campos de mayor a menor, que reduce el relleno: u8 u32 u16 u8 ocupa 12 bytes con alineación 4; u32 u16 u8 u8, 8. Pero el orden de la memoria y el del mensaje son independientes: el mensaje usa el orden que más convenga a la lectura y a la compatibilidad, no el del compilador.
06 · Cuando se lee a ojo
Texto: líneas, CSV y JSON
Un formato de texto es un acuerdo sobre cómo escribir los valores con caracteres. Los más simples son una línea con campos separados por un delimitador (coma, espacio, punto y coma) y terminada en un salto de línea. NMEA (tema 10) es así: $GPGGA,123519,4807.038,N,…*47, con la suma de comprobación (el XOR del tema 44) al final. El formato CSV es la misma idea: 7,23.5,55,3300. Es corto y fácil de producir con printf, pero tiene los problemas del texto:
- El separador decimal. En muchos países se escribe 23,5; en otros, 23.5. Si el delimitador es la coma, un valor con coma decimal rompe la línea. En protocolos de máquina se fija el punto, siempre, sin importar la configuración regional del equipo.
- El delimitador dentro de los datos: un texto con una coma o un salto de línea. Hay que escaparlo (comillas, barra invertida) o usar un delimitador que no pueda aparecer.
- El fin de línea:
\n(Linux),\r\n(Windows, la mayoría de los módulos AT) o\r. El receptor debe aceptar los tres, o el protocolo fijar uno. - La codificación de caracteres: ASCII usa 7 bits; los caracteres como ñ, é o ° necesitan UTF-8 (2 bytes) o una página de códigos, y dos equipos que no coinciden muestran «Â°C» en lugar de «°C».
- Sin nombres, sin tipos: «7,23.5,55,3300» no dice qué es cada cosa ni si 55 es entero o decimal.
JSON
JSON (JavaScript Object Notation) resuelve esas carencias con una sintaxis mínima para estructuras anidadas: objetos con pares nombre-valor entre llaves, listas entre corchetes y seis tipos de valor (número, texto, booleano, nulo, objeto y lista). El registro de antes es {"id":7,"t":23.5,"h":55,"bat":3300}. Es el formato de las interfaces web y de la mayor parte del tráfico de dispositivos conectados (HTTP y MQTT, tema 32). Sus ventajas son reales:
- Autodescriptivo: cada valor lleva su nombre. Un humano lo entiende, y un campo nuevo se ignora sin problema.
- Universal: todo lenguaje y todo entorno tienen un analizador. Un navegador lo lee directo.
- Flexible: campos opcionales, estructuras anidadas, listas de largo variable.
Y sus límites, que pesan en un microcontrolador. Es grande: los nombres de los campos, las comillas, las llaves y los números en decimal multiplican el tamaño (35 bytes contra 7 del mismo registro en binario). Es caro de procesar: analizarlo exige leer carácter por carácter, convertir cadenas a números y guardar la estructura en memoria; una librería como ArduinoJson lo hace bien, pero consume RAM proporcional al documento. No tiene tipo binario (los datos crudos se pasan a texto, en base 64, un tercio más grandes), ni distingue entero de decimal, y su manejo de números grandes depende de cada analizador. Y no tiene un esquema obligatorio: un campo que falta o con un tipo equivocado se descubre en el uso. Para enviar mediciones a la nube por Wi-Fi, JSON es excelente; para una trama de 20 bytes por una radio de baja potencia, no cabe.
07 · Estructura sin el peso
CBOR: JSON en binario
CBOR (Concise Binary Object Representation, RFC 8949) es un formato binario con el modelo de datos de JSON: números, textos, listas, mapas, booleanos y nulo, anidados como se quiera. Cada elemento empieza con un byte de cabecera que indica el tipo (los 3 bits altos) y un dato corto (los 5 bajos): un largo, o el valor mismo si es menor que 24. Para valores mayores, el dato corto vale 24, 25 o 26 y el valor sigue en 1, 2 o 4 bytes (el número 55 es 18 37; el 3300, 19 0C E4). Los tipos principales:
| Tipo | Qué es | Ejemplo |
|---|---|---|
| 0 | Entero sin signo | 07 = 7 · 18 37 = 55 · 19 03 E8 = 1000 |
| 1 | Entero negativo (guarda −1 − n) | 20 = −1 · 38 63 = −100 |
| 2 | Cadena de bytes (datos binarios) | 43 01 02 03 = 3 bytes |
| 3 | Texto UTF-8 | 62 69 64 = «id» |
| 4 | Lista | 83 01 02 03 = [1, 2, 3] |
| 5 | Mapa (pares clave-valor) | A1 61 61 01 = {"a": 1} |
| 6 | Etiqueta (da significado: fecha, número grande…) | C1 + número = una marca de tiempo Unix |
| 7 | Especiales: falso, verdadero, nulo, coma flotante | F4, F5, F6, FA + 4 bytes, FB + 8 bytes |
Sus virtudes para un microcontrolador: es compacto (el registro pasa de 35 a 23 bytes, y a menos si se usan claves numéricas o enteros escalados en lugar de decimales); es autodescriptivo, de modo que se puede decodificar sin conocer el esquema, y un campo nuevo se ignora; es fácil de codificar (el codificador de la sección 9 cabe en 30 líneas), y de procesar de forma secuencial, sin construir un árbol en memoria. Su tamaño tiene una lección importante en los números con decimales: 23.5 se representa exactamente en 32 bits, y entra como un FA más 4 bytes (5 en total); pero 23.51 no tiene representación exacta en 32 bits, y se manda como un FB más 8 bytes (9). Para datos de sensores, mandar el entero escalado (2351, 19 09 2F, 3 bytes) es más chico y exacto. Se usa en CoAP, en LwM2M, en los certificados de dispositivos y en los mensajes de seguridad del IoT.
Hay otros formatos de la misma idea: MessagePack (casi idéntico en espíritu y de uso muy amplio) y Protocol Buffers, que va por el otro camino: no es autodescriptivo, sino que se define un esquema en un archivo y se genera código para cada lenguaje; cada campo lleva un número de identificación en lugar de un nombre, lo que lo hace muy compacto y fácil de evolucionar, pero exige herramientas de generación.
| Formato | Tamaño | Legible | Esquema | Campos nuevos | Costo de proceso |
|---|---|---|---|---|---|
| Binario fijo | 7 bytes | No | Solo en la documentación | Rompe posiciones | Mínimo |
| CSV | 14 bytes | Sí, sin nombres | Implícito | Rompe posiciones | Medio |
| JSON | 35 bytes | Sí | Ninguno | Se ignoran | Alto |
| CBOR | 23 bytes | No (con una herramienta) | Ninguno | Se ignoran | Medio |
| Protocol Buffers | Cercano al binario fijo | No | Sí, obligatorio | Se ignoran | Bajo a medio |
08 · Crecer sin romper
Versiones y compatibilidad
Los dispositivos viven años en el campo, con versiones de firmware mezcladas. Un sistema con cien sensores no se actualiza de una vez: durante meses, los viejos y los nuevos conviven con el mismo receptor. Un formato que no preveía el cambio obliga a actualizar todo junto. Hay dos direcciones de compatibilidad:
- Hacia atrás: el receptor nuevo entiende los mensajes viejos.
- Hacia adelante: el receptor viejo tolera los mensajes nuevos (ignora lo que no conoce, en lugar de fallar o interpretarlos mal).
La compatibilidad se diseña desde el primer día, con algunas reglas:
- Un campo de versión al comienzo (un byte). El receptor lo lee primero y decide cómo interpretar el resto, o rechaza una versión que no conoce, con un error claro, en lugar de leer basura. Es la recomendación del tema 41.
- Agregar al final, nunca insertar ni reordenar. Un campo nuevo se pone después de los existentes; el receptor viejo lee los que conoce e ignora lo que sobra. Usa el largo del mensaje (que ya viaja en el paquete) para saber cuánto hay.
- No cambiar el significado de un campo existente. Si «temperatura» pasa de décimas a centésimas sin cambiar la versión, los receptores viejos leen valores diez veces mayores. Un cambio de unidad es un campo nuevo, con otro identificador.
- Reservar espacio: bits y bytes sin usar que se envían en cero y se ignoran en la recepción, para ocuparlos más tarde.
- Tipo-largo-valor (TLV): cada campo lleva un identificador, su largo y su valor. El receptor recorre la secuencia, atiende los identificadores que conoce y salta los que no, usando el largo. Permite campos opcionales, en cualquier orden, y agregar tipos sin afectar a nadie. Es la idea detrás de CBOR (con nombres), de Protocol Buffers (con números), de BLE (los datos de publicidad son TLV) y de muchos protocolos de red. Un ejemplo:
01 02 2E 09es «tipo 1 (temperatura), 2 bytes, 2350»; si llega un tipo desconocido,7F 02 AA BB, se salta por el largo.
El receptor, además, valida: el largo mínimo del mensaje para la versión que declara, los rangos de cada campo (una humedad de 317 % es imposible, y un CRC correcto no lo evita), los valores de los enumerados y los marcadores especiales («sin dato» = 0x7FFF). Es el control de la herramienta 5: ante un valor imposible, se descarta el mensaje y se cuenta como error, en lugar de usarlo. Por último, el formato se escribe: una tabla de campos con nombre, tipo, tamaño, orden de bytes, unidad, escala, rango y significado de cada valor, y vectores de prueba (un mensaje en hexadecimal con los valores que debe producir), que se verifican en los dos extremos. Es lo que evita la mayor parte de los problemas de integración.
09 · Con el microcontrolador
Serializar un registro: binario, JSON y CBOR
El programa siguiente define un registro de sensor y lo convierte a los tres formatos de la sección 4: un binario de 7 bytes con enteros escalados y orden little-endian, un JSON armado con snprintf y un CBOR codificado con un codificador de pocas líneas. Antes muestra por qué no se envía la estructura cruda, hace el camino inverso con validación de rangos y reproduce el error de leer con el orden de bytes equivocado. No necesita una segunda placa: todo se ve por el monitor serie.
// ---------- El registro (en memoria, con el relleno que ponga el compilador) ----------
struct Registro {
uint8_t id;
int16_t t; // temperatura en centésimas de grado
uint16_t h; // humedad en centésimas de por ciento
uint16_t bat; // batería en milivoltios
};
const uint8_t TAM_BINARIO = 7; // el tamaño del mensaje, fijado por el protocolo
// ---------- Enteros con el orden de bytes explícito ----------
void poner16(uint8_t *p, uint16_t v) { p[0] = v & 0xFF; p[1] = v >> 8; } // little-endian
uint16_t leer16(const uint8_t *p) { return p[0] | (p[1] << 8); }
uint16_t leer16BE(const uint8_t *p) { return (p[0] << 8) | p[1]; } // para mostrar el error de orden
// ---------- Binario: campo por campo ----------
uint8_t serializar(const Registro &r, uint8_t *p) {
p[0] = r.id;
poner16(p + 1, (uint16_t)r.t); // el complemento a dos viaja como un uint16
poner16(p + 3, r.h);
poner16(p + 5, r.bat);
return TAM_BINARIO;
}
bool deserializar(const uint8_t *p, uint8_t n, Registro &r) {
if (n < TAM_BINARIO) return false; // largo mínimo
Registro x;
x.id = p[0];
x.t = (int16_t)leer16(p + 1); // el cast extiende el signo
x.h = leer16(p + 3);
x.bat = leer16(p + 5);
if (x.t < -4000 || x.t > 8500) return false; // fuera del rango del sensor: -40 a 85 °C
if (x.h > 10000) return false; // más de 100 %
if (x.bat < 1800 || x.bat > 5500) return false; // fuera de 1,8 a 5,5 V
r = x;
return true;
}
// ---------- JSON con snprintf ----------
int aJson(const Registro &r, char *buf, size_t tam) {
int at = r.t < 0 ? -r.t : r.t;
return snprintf(buf, tam, "{\"id\":%u,\"t\":%s%d.%02d,\"h\":%u.%02u,\"bat\":%u}",
r.id, r.t < 0 ? "-" : "", at / 100, at % 100, r.h / 100, r.h % 100, r.bat);
}
// ---------- CBOR: un codificador mínimo ----------
uint8_t cabecera(uint8_t *p, uint8_t mayor, uint32_t n) {
if (n < 24) { p[0] = (mayor << 5) | n; return 1; }
if (n < 256) { p[0] = (mayor << 5) | 24; p[1] = n; return 2; }
if (n < 65536) { p[0] = (mayor << 5) | 25; p[1] = n >> 8; p[2] = n & 0xFF; return 3; }
p[0] = (mayor << 5) | 26; p[1] = n >> 24; p[2] = n >> 16; p[3] = n >> 8; p[4] = n; return 5;
}
uint8_t cborTexto(uint8_t *p, const char *s) {
uint8_t l = strlen(s), k = cabecera(p, 3, l);
memcpy(p + k, s, l);
return k + l;
}
uint8_t cborEntero(uint8_t *p, int32_t v) { return v >= 0 ? cabecera(p, 0, v) : cabecera(p, 1, -1 - v); }
uint8_t cborFloat(uint8_t *p, float f) { // coma flotante de 32 bits, byte alto primero
uint32_t u;
memcpy(&u, &f, 4);
p[0] = 0xFA; p[1] = u >> 24; p[2] = u >> 16; p[3] = u >> 8; p[4] = u;
return 5;
}
uint8_t aCbor(const Registro &r, uint8_t *p) {
uint8_t n = cabecera(p, 5, 4); // un mapa de 4 pares
n += cborTexto(p + n, "id"); n += cborEntero(p + n, r.id);
n += cborTexto(p + n, "t"); n += cborFloat(p + n, r.t / 100.0f);
n += cborTexto(p + n, "h"); n += cborFloat(p + n, r.h / 100.0f);
n += cborTexto(p + n, "bat"); n += cborEntero(p + n, r.bat);
return n;
}
void imprimir(const char *t, const uint8_t *p, uint8_t n) {
Serial.printf("%s (%u bytes):", t, n);
for (uint8_t i = 0; i < n; i++) Serial.printf(" %02X", p[i]);
Serial.println();
}
void setup() {
Serial.begin(115200);
Registro r;
memset(&r, 0, sizeof(r)); // el relleno en cero, para verlo
r.id = 7; r.t = 2350; r.h = 5500; r.bat = 3300;
Serial.printf("sizeof(Registro) = %u, y los campos suman %u\n", (unsigned)sizeof(Registro), TAM_BINARIO);
imprimir("la estructura cruda", (const uint8_t *)&r, sizeof(r)); // con un byte de relleno en el medio
uint8_t bin[16];
uint8_t n = serializar(r, bin);
imprimir("binario", bin, n);
Registro leido;
Serial.printf("leído: %s\n", deserializar(bin, n, leido) ? "válido" : "inválido");
Serial.printf("t = %.2f °C, h = %.2f %%, bat = %u mV\n", leido.t / 100.0, leido.h / 100.0, leido.bat);
int16_t tMal = (int16_t)leer16BE(bin + 1); // el receptor que lee al revés
Serial.printf("con el orden contrario: t = %.2f °C\n", tMal / 100.0);
char json[64];
int m = aJson(r, json, sizeof(json));
Serial.printf("JSON (%d bytes): %s\n", m, json);
uint8_t cbor[40];
imprimir("CBOR", cbor, aCbor(r, cbor));
}
void loop() {}
Qué hay que notar:
- La estructura cruda mide 8 bytes en un ESP32, uno más que el mensaje: el compilador coloca
ten la dirección 2 para alinearlo, y el byte de relleno (cero porque se inicializó) viaja. Conmemseta otro valor, o sin él, el relleno cambia entre ejecuciones y dos mensajes iguales dejan de serlo. El mensaje se arma conserializar, siempre de 7 bytes. - El orden se fija con desplazamientos (
poner16yleer16), no conmemcpy. El código es idéntico en un AVR, un ESP32 o una máquina big-endian, y el mensaje07 2E 09 7C 15 E4 0Ces el mismo. - El signo se recupera con el cast:
(int16_t)leer16(…). Sin él, una temperatura de −5,25 °C (−525 =0xFDF3) se leería como 64 011 centésimas y la validación de rango la rechazaría. - La recepción valida: el largo mínimo y los rangos de cada campo. Un mensaje con el orden de bytes equivocado produciría 117,85 °C y 317,65 %, y
deserializardevolveríafalse: un error que el CRC no puede ver. - El JSON lleva 39 bytes (
{"id":7,"t":23.50,"h":55.00,"bat":3300}), cuatro más que el de la herramienta 4 por los decimales fijos de%02d. Consnprintfse controla el tamaño del búfer, y el valor devuelto indica si el texto se cortó. Para documentos más complejos, ArduinoJson ofrece la serialización y el análisis, con el costo en memoria de una librería. - El CBOR lleva 26 bytes aquí porque
tyhvan comofloatde 32 bits (5 bytes cada uno):A4 62 69 64 07 61 74 FA 41 BC 00 00 61 68 FA 42 5C 00 00 63 62 61 74 19 0C E4. Con enteros escalados (2350y5500, 3 bytes cada uno) bajaría a 22; la herramienta 4 manda55como entero y da 23. Elfloatde la placa y el de la página son el mismo estándar, y por eso los bytes coinciden. - En un Arduino sin
printfenSerial, se reemplaza porSerial.print; en un AVR,snprintfno formateafloat, y por eso el JSON de arriba usa enteros.
10 · Analogía con programación
Formatos en Python
Python trae lo necesario para trabajar con todos los formatos del tema: struct para empaquetar binarios con un orden de bytes explícito, int.to_bytes y int.from_bytes, json para JSON y, con una función corta, CBOR. El modelo siguiente reproduce el registro, los dos órdenes de bytes, el relleno de las estructuras nativas, la coma flotante de 32 bits, los tres formatos y un lector de campos tipo-largo-valor que tolera tipos desconocidos.
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
def cabecera(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(v):
"""Codificador CBOR mínimo: enteros, decimales, textos y mapas."""
if isinstance(v, int):
return cabecera(0, v) if v >= 0 else cabecera(1, -1 - v)
if isinstance(v, float):
f32 = struct.pack(">f", v)
if struct.unpack(">f", f32)[0] == v: # ¿entra exacto en 32 bits?
return b"\xfa" + f32
return b"\xfb" + struct.pack(">d", v)
if isinstance(v, str):
b = v.encode("utf-8")
return cabecera(3, len(b)) + b
if isinstance(v, dict):
return cabecera(5, len(v)) + b"".join(cbor(k) + cbor(x) for k, x in v.items())
raise TypeError(type(v))
def tlv(tipo, valor):
return bytes([tipo, len(valor)]) + valor
def leer_tlv(datos):
conocidos, desconocidos, i = {}, [], 0
while i + 2 <= len(datos):
tipo, largo = datos[i], datos[i + 1]
valor = datos[i + 2:i + 2 + largo]
if len(valor) < largo:
break
if tipo == 1:
conocidos["t"] = int.from_bytes(valor, "little", signed=True) / 100
elif tipo == 2:
conocidos["h"] = int.from_bytes(valor, "little") / 100
elif tipo == 3:
conocidos["bat"] = int.from_bytes(valor, "little")
else:
desconocidos.append(tipo) # se salta por el largo
i += 2 + largo
return conocidos, desconocidos
def hexa(b):
return b.hex(" ").upper()
registro = {"id": 7, "t": 23.5, "h": 55, "bat": 3300}
le = struct.pack("<BhHH", 7, 2350, 5500, 3300)
be = struct.pack(">BhHH", 7, 2350, 5500, 3300)
print("little-endian:", hexa(le), "| big-endian:", hexa(be))
print("tamaño nativo (con relleno):", struct.calcsize("BhHH"), "| sin relleno:", struct.calcsize("<BhHH"))
print("leído bien:", struct.unpack("<BhHH", le))
print("con el orden contrario:", struct.unpack(">BhHH", le))
print("2350 en 16 bits, big-endian:", hexa((2350).to_bytes(2, "big")), "| -2:", hexa((-2).to_bytes(2, "big", signed=True)))
print("09 2E leído al revés:", int.from_bytes(b"\x09\x2e", "little"))
print("0.1 en float32:", hexa(struct.pack(">f", 0.1)), "→", struct.unpack(">f", struct.pack(">f", 0.1))[0])
print("23.5 en float32:", hexa(struct.pack(">f", 23.5)))
texto = json.dumps(registro, separators=(",", ":"))
csv = ",".join(str(v) for v in registro.values())
datos_cbor = cbor(registro)
print(f"binario {len(le)} bytes · CSV {len(csv)} · JSON {len(texto)} · CBOR {len(datos_cbor)}")
print("JSON:", texto)
print("CBOR:", hexa(datos_cbor))
print("CBOR con 23.51:", len(cbor({**registro, "t": 23.51})), "bytes")
print("CBOR de {'a': 1000}:", hexa(cbor({"a": 1000})))
mensaje = tlv(1, struct.pack("<h", 2350)) + tlv(2, struct.pack("<H", 5500)) + tlv(0x7F, b"\xaa\xbb") + tlv(3, struct.pack("<H", 3300))
print("TLV:", hexa(mensaje))
print("TLV leído:", leer_tlv(mensaje))
Resultado: el registro empaquetado en little-endian es 07 2E 09 7C 15 E4 0C y en big-endian 07 09 2E 15 7C 0C E4; el formato nativo ocupa 8 bytes (con un byte de relleno) y el explícito, 7. Leído bien da (7, 2350, 5500, 3300), y con el orden contrario, (7, 11785, 31765, 58380): 117,85 °C, 317,65 % y 58 380 mV. El 2350 en big-endian es 09 2E, el −2 es FF FE y 09 2E leído al revés es 11 785. El 0,1 en float de 32 bits es 3D CC CC CD y vuelve como 0,10000000149011612; el 23,5 es 41 BC 00 00. Los tamaños: binario 7 bytes, CSV 14, JSON 35 y CBOR 23. El JSON es {"id":7,"t":23.5,"h":55,"bat":3300} y el CBOR, A4 62 69 64 07 61 74 FA 41 BC 00 00 61 68 18 37 63 62 61 74 19 0C E4; con 23,51 el CBOR pasa a 27 bytes (la temperatura necesita 64 bits), y {'a': 1000} es A1 61 61 19 03 E8. El mensaje TLV es 01 02 2E 09 02 02 7C 15 7F 02 AA BB 03 02 E4 0C y al leerlo da ({'t': 23.5, 'h': 55.0, 'bat': 3300}, [127]): reconoce los tres campos y salta el tipo 127.
Dos lecciones del modelo. Primero, los mismos siete bytes, interpretados con los parámetros equivocados (el orden), producen valores imposibles pero perfectamente «válidos» para el decodificador: de ahí la validación de rangos. Segundo, un formato tipo-largo-valor permite que el receptor aprenda tipos nuevos sin cambiar su estructura, y que los viejos ignoren lo que no entienden: es lo que da compatibilidad hacia adelante.
11 · Exploración interactiva
Laboratorio: orden, números, estructuras, formatos y lectura
El laboratorio tiene cinco herramientas. La primera muestra un entero en los dos órdenes de bytes y lo que pasa al leerlo al revés. La segunda compara punto fijo y coma flotante. La tercera calcula el relleno de una estructura en tres plataformas. La cuarta codifica un registro en binario, CSV, JSON y CBOR y compara tamaños y tiempos. La quinta decodifica siete bytes con el orden elegido y revisa si los valores son plausibles.
1. Orden de bytes y complemento a dos
Escribí un entero.
2. Punto fijo y coma flotante
Escribí un valor.
3. Alineación de una estructura
Escribí los campos.
4. Un registro en cuatro formatos
Escribí los valores del registro.
5. Decodificar con el orden elegido
Escribí siete bytes.
En la herramienta 1, el valor negativo se representa en complemento a dos del ancho elegido. En la 2, el entero se redondea al más cercano y se satura en 16 bits con signo. En la 3, cada campo se alinea a su tamaño hasta el máximo de la plataforma y la estructura completa a su campo mayor. En la 4, el CBOR es un codificador real: los enteros usan la cabecera más corta y los decimales, 32 bits cuando son exactos y 64 si no; el tiempo agrega 7 bytes de paquete (tema 44) y 10 bits por byte. En la 5, el registro es un identificador, una temperatura con signo en centésimas, una humedad en centésimas y una batería en milivoltios.
Recorrido guiado
- En la herramienta 1, con 2350 y 16 bits:
0x092E, big-endian 09 2E, little-endian 2E 09, y leído al revés 11 785 (en rojo). Con −2:0xFFFE= 65 534 sin signo, FF FE y FE FF; leído al revés da −257. Con 1 000 000 en 32 bits: 00 0F 42 40 y 40 42 0F 00. Con 0x1234: 12 34 y 34 12. - En la herramienta 2, con 23,5 y escala 100: float 41 BC 00 00 (exacto), double 40 37 80 00 00 00 00 00, entero 2350 = 0x092E en 2 bytes y rango ±327,68. Con 0,1: 3D CC CC CD vuelve como 0,100000001; el entero, exacto. Con 327,68: no entra en 16 bits (se satura en 32 767), en rojo. Con 23,51: el float vuelve como 23,5100002 y el entero como 23,51.
- En la herramienta 3, con u8 u32 u16 u8 y ESP32: posiciones 0, 4, 8 y 10; 12 bytes contra 8 de campos, 4 de relleno (33 %), y ordenados de mayor a menor, 8. Con AVR: sin relleno (8). Con PC y u8 f64 u8: 24 bytes para 10 de campos (14 de relleno, 58 %). Probá u8 u16 u8 u32 f32 u8: 20 bytes contra 13.
- En la herramienta 4, con 7, 23,5, 55, 3300 y 9600 baudios: binario 7 bytes (07 2E 09 7C 15 E4 0C), CSV 14, JSON 35, CBOR 23 (A4 62 69 64 07 61 74 FA 41 BC 00 00 61 68 18 37 63 62 61 74 19 0C E4) y en el enlace 14,6 ms, 21,9 ms, 43,8 ms y 31,3 ms. Con 23,51: el CBOR pasa a 27 bytes, con un FB de 64 bits. Con −5,25 y 55,2: el binario lleva F3 FD (−525) y el CBOR, 30 bytes.
- En la herramienta 5, con 07 2E 09 7C 15 E4 0C en little-endian: 23,50 °C, 55,00 % y 3300 mV, «todos plausibles». Cambiando a big-endian: 117,85 °C, 317,65 % y 58 380 mV, en rojo, «sospechá del orden de bytes». Con 07 FF FF 7C 15 E4 0C: la temperatura es −0,01 °C (
0xFFFFcon signo), pero la humedad y la batería siguen imposibles.
Desafío: el medidor de energía que se actualiza por etapas
Una empresa tiene 300 medidores de energía que mandan por una radio (LoRa) un registro cada 15 minutos, de hasta 24 bytes por paquete. El registro actual, en JSON, mide 62 bytes y no cabe; se rediseña. Con las herramientas y lo visto: (1) Formato: los campos son identificador (2 bytes), energía acumulada (0 a 99 999 999 Wh), potencia instantánea (0 a 30 000 W), tensión (190 a 260 V, a décimas), estado (flags) y marca de tiempo. En binario: id u16 (2), energía u32 (4), potencia u16 (2), tensión u16 en décimas (2), estado u8 (1) y hora u32 en segundos Unix (4): 15 bytes, con 9 libres. En CBOR con claves de texto serían unos 50, con claves numéricas unos 30; en JSON, 62. El binario es el único que cabe con margen. (2) Orden y tipos: se fija little-endian y se escribe campo por campo (herramienta 3, no la estructura cruda); la energía es uint32_t y no float (un float de 32 bits pierde exactitud por encima de 16 777 216 Wh, y la energía acumulada la supera en meses). (3) Validación: el receptor rechaza tensiones fuera de 150 a 300 V o potencias mayores que 30 000 W, aunque el CRC sea correcto (herramienta 5). (4) Evolución: dentro de dos años se quiere agregar la corriente y la frecuencia. Se agrega un byte de versión adelante (16 bytes) y los campos nuevos al final, para que el firmware nuevo se despliegue de a poco: los receptores de la versión 1 leen los 15 bytes que conocen y los de la versión 2 aceptan paquetes de las dos. (5) Sin la versión, el cambio exigiría actualizar los 300 medidores y el servidor a la vez. (6) La puerta de enlace convierte el binario a JSON para el servidor web (tema 51) con las unidades en su lugar: tensión 230,5 V, no 2305. Es el contenido de los temas 41 a 46 en un mismo problema.
12 · Comprensión
Confusiones frecuentes
«Si el CRC es correcto, el dato es correcto»
El CRC garantiza que los bytes llegaron como salieron, no que se interpreten como se escribieron. Un orden de bytes equivocado, un campo con o sin signo o una escala distinta producen valores erróneos con un CRC perfecto. Por eso se validan los rangos.
«El orden de bytes lo decide el procesador, así que no hace falta pensar en él»
El procesador decide cómo guarda en memoria; el protocolo decide cómo se envía. Si el código copia memoria al cable, el orden del cable depende del chip. Se serializa con desplazamientos, en el orden que fija el protocolo, y funciona en cualquier chip.
«Puedo enviar la estructura tal cual con sizeof»
Con relleno, con tamaños de tipos distintos entre plataformas y con el orden del procesador. Funciona hasta que el otro extremo cambia de chip o de compilador. Se envía campo por campo y con tipos de tamaño fijo.
«Un float es más preciso que un entero escalado»
Un float de 32 bits tiene unas 7 cifras significativas y no representa 0,1 con exactitud. Un entero de 16 bits escalado por 100 representa exactamente cada centésima de un rango de ±327. Para magnitudes con rango y resolución conocidos, el entero es más exacto, más chico y más rápido.
«Los números negativos se mandan con un bit de signo»
Se mandan en complemento a dos: −1 es todo unos. El receptor tiene que saber que el campo tiene signo; leído sin signo, −2 °C (−200 centésimas) se vuelve 653,36 °C. Y al ensamblar bytes hay que extender el signo con el tipo correcto.
«JSON es siempre la mejor opción porque todo el mundo lo entiende»
Es la mejor para la comunicación con la web y la nube. En un enlace lento, con baterías o con tramas de 20 bytes, el registro ocupa cinco veces más y exige analizar texto. Se usa donde el volumen es chico y el tiempo de desarrollo cuenta; en el enlace corto, un binario compacto.
«CBOR es un JSON comprimido»
No comprime: codifica el mismo modelo de datos en binario, con cabeceras de un byte en lugar de comillas y llaves. El ahorro viene de eso, y de los enteros de 1 a 4 bytes en lugar de dígitos. No es legible a ojo, pero se decodifica sin esquema.
«Agregar un campo al final del mensaje no puede romper nada»
Rompe a los receptores que exigen un largo exacto o que usan el largo como validación. El receptor tiene que aceptar mensajes más largos que el que conoce (ignorando el sobrante), y el emisor tiene que indicar la versión. Eso se acuerda en el diseño, no después.
«El formato se entiende mirando el código, no hace falta documentarlo»
El código de un extremo no dice qué espera el otro. Una tabla con campo, tipo, tamaño, orden, unidad, escala y rango, y un mensaje de ejemplo en hexadecimal con los valores que debe dar, es lo que permite que dos personas (o la misma, dentro de un año) lo implementen igual.
13 · Práctica guiada
Predecí antes de comprobar
1. Orden de bytes
Escribí el valor 1000 en 16 bits y el valor 0x12345678 en 32 bits, en big-endian y en little-endian.
Ver solución
1000 = 0x03E8: big-endian 03 E8, little-endian E8 03. Para 0x12345678: big-endian 12 34 56 78, little-endian 78 56 34 12. Un receptor que lee E8 03 con el orden contrario obtiene 0xE803 = 59 395.
2. Complemento a dos
¿Cómo se envía −300 como entero de 16 bits? ¿Qué lee un receptor que lo interpreta sin signo?
Ver solución
65 536 − 300 = 65 236 = 0xFED4: FE D4 en big-endian y D4 FE en little-endian. Sin signo, el receptor lee 65 236; con el signo correcto, −300.
3. Punto fijo
Una temperatura de −12,34 °C se manda en centésimas, en un entero de 16 bits little-endian. ¿Qué bytes se envían? ¿Entraría hasta 40 °C si la escala fuera de milésimas?
Ver solución
−1234 = 65 536 − 1234 = 64 302 = 0xFB2E: little-endian 2E FB. Con escala de milésimas, el máximo de 16 bits con signo es 32 767, es decir, 32,767 °C: 40 °C (40 000) no entra. Habría que usar 32 bits o una escala de centésimas (hasta 327,67).
4. Relleno
La estructura { uint8_t a; uint16_t b; uint8_t c; uint32_t d; }, ¿cuánto mide en un ESP32 (alineación 4) y cuánto suman sus campos? ¿Cómo se reordena?
Ver solución
Posiciones: a en 0, b en 2 (alineado a 2), c en 4, d en 8: ocupa 12 bytes y los campos suman 8. Ordenada de mayor a menor (d, b, a, c): 4 + 2 + 1 + 1 = 8 bytes, sin relleno. Aun así el mensaje se arma campo por campo.
5. Tamaño y tiempo
El registro de la herramienta 4 ocupa 7 bytes en binario y 35 en JSON. Con 7 bytes de paquete a 9600 baudios, ¿cuánto tarda cada uno y cuántos registros por segundo caben en cada formato?
Ver solución
Binario: (7 + 7) × 10 / 9600 = 14,6 ms, o 68 registros por segundo. JSON: (35 + 7) × 10 / 9600 = 43,8 ms, o 22 por segundo. El texto cuesta tres veces el tiempo (y la energía) en el aire; en un enlace compartido por 20 sensores, la diferencia decide si el sondeo cabe en el ciclo.
6. CBOR a mano
Codificá en CBOR el mapa {"a": 1000}. ¿Cuántos bytes ocupa, y cuántos en JSON?
Ver solución
Un mapa de 1 par: A1. La clave, un texto de 1 carácter: 61 61. El valor 1000 = 0x03E8 no cabe en 5 bits ni en 1 byte, así que usa la cabecera de 2 bytes: 19 03 E8. Total: A1 61 61 19 03 E8, 6 bytes. El JSON {"a":1000} ocupa 10.
7. Un receptor que lee mal
Un receptor little-endian recibe 07 09 2E 15 7C 0C E4 y muestra 117,85 °C, 317,65 % y 58 380 mV. ¿Qué pasó y cómo se arregla?
Ver solución
El emisor envió big-endian (09 2E = 2350) y el receptor lo leyó como little-endian (0x2E09 = 11 785). Todos los valores son imposibles. Se arregla fijando un único orden en el protocolo (y en la tabla de campos) y corrigiendo el lado que no lo cumple. La validación de rangos permite detectarlo en el primer mensaje, en lugar de guardar datos erróneos durante días.
8. Agregar un campo
La versión 2 de un sensor agrega un campo de estado de 1 byte. ¿Cómo se diseña el mensaje para que los receptores de la versión 1 sigan funcionando?
Ver solución
El mensaje lleva un byte de versión al comienzo. El campo nuevo va al final, sin mover los anteriores ni cambiar su significado. El receptor de la versión 1 lee los 7 bytes que conoce e ignora lo que sobra (usa el largo de la trama para saber cuánto hay, sin exigirlo exacto). El de la versión 2 acepta las dos versiones: si el mensaje es corto, el estado se toma como «desconocido». Con TLV o CBOR el efecto es el mismo y no depende de las posiciones.
14 · Síntesis
Ideas para recordar
- Los bytes no traen etiqueta: su significado es un acuerdo entre emisor y receptor. Un error de formato no produce una falla de comunicación, produce un valor distinto del enviado con un CRC perfecto. El formato se documenta con una tabla de campos y se prueba con vectores de ejemplo.
- El binario es compacto y rápido pero ilegible y frágil ante los cambios; el texto es legible y flexible pero ocupa varias veces más y exige analizarlo. En enlaces lentos o con baterías, binario; hacia la web y la nube, JSON; entre los dos, CBOR.
- Un entero de varios bytes se envía en big-endian (el más significativo primero: redes, Modbus, CBOR) o en little-endian (BLE, USB, 1-Wire, el procesador). Se arma con desplazamientos, no copiando memoria, y los dos extremos acuerdan un orden. Los negativos van en complemento a dos y el receptor tiene que saber si el campo tiene signo.
- Para magnitudes con rango y resolución conocidos, el entero escalado (punto fijo) es exacto, chico y rápido; el float aproxima (0,1 no es exacto), ocupa más y necesita coma flotante. La escala y la unidad forman parte del formato.
- Una estructura de C no se envía cruda: el compilador agrega relleno de alineación (12 bytes en lugar de 8, en un ESP32), los tipos cambian de tamaño entre plataformas y el orden depende del chip. Se serializa campo por campo con tipos de tamaño fijo.
- CSV y las líneas de texto son simples pero ambiguos (separador decimal, delimitador dentro de los datos, fin de línea, codificación). JSON agrega nombres y estructura al costo de varias veces el tamaño. El mismo registro mide 7 bytes en binario, 14 en CSV, 35 en JSON y 23 en CBOR.
- CBOR tiene el modelo de JSON en binario: cada elemento empieza con un byte de cabecera (tipo en 3 bits y valor o largo en 5), se decodifica sin esquema y se ignoran los campos nuevos. Un decimal que no es exacto en 32 bits cuesta 9 bytes: conviene mandar el entero escalado.
- Los dispositivos viven años con versiones mezcladas. Se diseña con un byte de versión, campos nuevos al final, significados que no cambian, espacio reservado y, mejor, tipo-largo-valor. El receptor valida el largo mínimo y los rangos y descarta lo imposible, aunque el CRC sea correcto.
¿Listo para el siguiente tema?
Antes de avanzar, escribí en los dos órdenes de bytes la temperatura −5,25 °C en centésimas como entero de 16 bits; explicá por qué una estructura de C no se envía con sizeof y qué se hace en su lugar; y elegí un formato (binario, JSON o CBOR) para un sensor a pilas que manda 20 bytes por radio y para uno que envía mediciones por Wi-Fi a un servidor web, justificándolo. Si podés justificar las tres respuestas, entendés cómo se codifica un dato.
En el próximo tema, el taller de un protocolo con CRC y reintentos, se juntan los temas 41 a 46 en un mismo proyecto entre dos placas: una trama con delimitador, largo, tipo y CRC-16, confirmaciones y reintentos, una pérdida simulada para ver la recuperación y un registro de sensor serializado campo por campo. Es la práctica del bloque de protocolos.