micropython-serializacion-json-struct-binary

Serialización en MicroPython: JSON y struct

  • 5 min

La serialización es convertir datos en una secuencia que se pueda guardar o enviar.

Cuando programamos, trabajamos con objetos: listas, diccionarios, números flotantes… Pero cuando queremos guardar esos datos en un archivo o enviarlos por cable/radio a otro dispositivo, no podemos enviar “el objeto abstracto”. Tenemos que convertirlo a una secuencia lineal de bytes.

A este proceso de “aplanar” los datos se le llama Serialización.

En MicroPython, tenemos dos formas principales de hacerlo, y elegir la correcta define el rendimiento de tu proyecto:

  1. JSON: Legible por humanos, estándar web, pero “pesado”.
  2. Struct: Binario puro, ilegible para humanos, pero extremadamente compacto y rápido.

JSON: El estándar universal

Ya vimos en el capítulo anterior cómo guardar configuraciones en JSON. La librería ujson (o simplemente json) convierte diccionarios y listas de Python a una cadena de texto formateada.

Es perfecto para:

  • Intercambiar datos con servidores web (APIs REST).
  • Guardar configuración legible (config.json).
  • Enviar datos por MQTT donde el ancho de banda no es crítico.

Serializar y deserializar

import json

# Tenemos un diccionario con tipos mezclados
datos = {
    "sensor": "DHT22",
    "id": 12,
    "valores": [23.5, 60.2],
    "activo": True
}

# 1. Serializar a Texto (String)
mensaje_txt = json.dumps(datos)
print(f"Enviando: {mensaje_txt}")
# Salida: {"sensor": "DHT22", "id": 12, "valores": [23.5, 60.2], "activo": true}
# Ocupa unos ~80 bytes

# 2. Deserializar (Volver a objeto)
recibido = json.loads(mensaje_txt)
print(recibido["valores"][0]) # 23.5
Copied!

El coste del texto Fíjate que el número 23.5 en memoria ocupa 4 bytes (float). Pero en JSON se envía como los caracteres '2', '3', '.', '5', ocupando también 4 bytes… más las comillas, los corchetes y las claves {"valores":...}. Para enviar un dato pequeño, JSON añade mucha “basura” (overhead).

struct: datos binarios compactos

Imagina que quieres enviar la temperatura (23.5) y la humedad (60) por una red LoRaWAN o ESP-NOW, donde cada byte cuenta y el ancho de banda es oro.

Enviar el texto {"temp": 23.5, "hum": 60} consume unos 25 bytes. Si lo enviamos en binario puro (como lo hace C), solo ocuparía 5 o 6 bytes.

Para esto usamos el módulo ustruct (o struct). Este módulo nos permite empaquetar datos en bloques de bytes siguiendo el formato de C.

La cadena de formato

La gracia de struct está en definir cómo queremos empaquetar los datos usando unos caracteres especiales:

  • b / B: Byte con signo / sin signo (1 byte). (Para enteros pequeños 0-255).
  • h / H: Short integer (2 bytes). (Para enteros hasta 65535).
  • i / I: Integer (4 bytes). (Enteros grandes).
  • f: Float (4 bytes). (Decimales estándar).
  • s: String (Bytes fijos).

Empaquetar datos de sensores

Queremos enviar:

  1. ID del sensor (Entero positivo, max 255) -> 1 byte (B).
  2. Temperatura (Float) -> 4 bytes (f).
  3. Humedad (Entero) -> 2 bytes (h).

Total: 1 + 4 + 2 = 7 bytes. (Frente a los >25 del JSON).

import struct

# Datos a enviar
sensor_id = 101
temp = 23.45
hum = 60

# 1. Empaquetar (Pack)
# El '>' indica Big-Endian (estándar de red)
# 'B f h' es el formato: Byte, Float, Short
paquete_binario = struct.pack('>Bfh', sensor_id, temp, hum)

print(f"Bytes: {paquete_binario}")
print(f"Longitud: {len(paquete_binario)} bytes")
# Salida visual ilegible tipo: b'eA\xbb\x99\x9a<'
Copied!

Desempaquetar datos

El receptor (otro ESP32 o un servidor en Python) recibe ese chorro de bytes y, conociendo el formato, reconstruye los datos.

# Simulación de recepción
datos_recibidos = b'eA\xbb\x99\x9a<'

# 2. Desempaquetar (Unpack)
# Devuelve siempre una TUPLA
valores = struct.unpack('>Bfh', datos_recibidos)

print(f"ID: {valores[0]}")
print(f"Temp: {valores[1]:.2f}") # Los floats pierden algo de precisión minúscula
print(f"Hum: {valores[2]}")
Copied!

Endianness: el orden de los bytes

¿Te has fijado en el símbolo > al principio del formato? Define el Endianness (el orden de los bytes).

  • > Big Endian: El byte más significativo va primero. Es el estándar en redes (Internet, TCP/IP).
  • < Little Endian: El byte menos significativo va primero. Es el estándar en procesadores Intel y en muchos microcontroladores ARM/ESP32.

Idea importante: Si envías datos entre dos máquinas iguales (ESP32 a ESP32), da igual. Pero si envías datos por red o a un servidor distinto, usa siempre > (Big Endian) o ! (Network) para asegurar compatibilidad.

Reducir el tamaño con punto fijo

A veces, incluso 4 bytes para un float nos parece mucho. Un truco muy usado en IoT es multiplicar por 100 y enviar un entero.

  • Dato: 23.45
  • Multiplicado x100: 2345
  • Empaquetado como short (h): 2 bytes.

Al recibir, dividimos por 100. Hemos ahorrado la mitad del espacio sin perder precisión práctica.

JSON o struct

CaracterísticaJSON (ujson)Binario (ustruct)
LegibilidadHumana (Texto)Máquina (Bytes raros)
TamañoGrande (Verborrea)Mínimo (Compacto)
Velocidad CPULenta (Parsear texto)Rapidísima (Copia memoria)
Uso idealWeb, MQTT, Configuración, DebugLoRaWAN, ESP-NOW, Archivos grandes

Dominar struct es lo que te permite decir: “He creado un protocolo de comunicación propio optimizado”. Es una herramienta indispensable en el cinturón del desarrollador embebido.