La primera vez que alguien abre el panel de su network server y ve llegar un uplink, suele llevarse una sorpresa: no hay ningún campo que diga “oxígeno disuelto: 5.2 mg/L”. Hay una cadena como 0D3401F40A8C. Ese fragmento ilegible es el payload, y convertirlo en un número con sentido es trabajo de un decoder. Entender cómo se arma y se lee un payload es tan importante como elegir el sensor correcto: un dato mal decodificado es indistinguible de un dato falso.
Qué es exactamente el payload
En una trama LoRaWAN, el payload (también llamado FRMPayload) es el bloque de bytes que va cifrado dentro del paquete, después del MHDR (encabezado) y antes del MIC (código de integridad). Es la parte que el nodo construye libremente: LoRaWAN no impone ningún formato de datos, solo transporta bytes. El protocolo se preocupa de que esos bytes lleguen cifrados con AES-128 y sin corromperse —algo que ya cubrimos en el artículo sobre seguridad y cifrado en LoRaWAN— pero no sabe, ni le importa, si el tercer byte es una temperatura o un identificador de batería. Ese significado lo define quien programa el firmware del nodo, y lo tiene que reconstruir quien escribe el decoder del lado del servidor.
Cuando el payload llega al network server (ChirpStack, The Things Stack u otro; ver qué es un network server LoRaWAN), se entrega descifrado como una cadena de bytes, normalmente representada en dos formatos intercambiables:
- Hexadecimal:
0D 34 01 F4 0A 8C— cada byte como dos caracteres hex. - Base64:
DTQB9AqM— la misma información codificada para viajar en JSON por HTTP/MQTT.
Ambas representan exactamente los mismos 6 bytes. El decoder trabaja sobre el array de bytes, sin importar en qué formato de texto haya llegado.
Por qué no se envía JSON directo
La tentación obvia sería que el nodo mande {"bateria":3.38,"oxigeno":5.0,"temperatura":27.0} y listo, sin necesidad de decoder. La razón por la que casi nadie hace esto en LoRaWAN es puramente de radio: cada carácter de texto pesa un byte, y el tamaño de payload está limitado por el Spreading Factor y el dwell time de 400 ms que rige en Ecuador bajo AU915 (lo explicamos con cifras en el artículo de duty cycle y Time on Air). Con SF10 u 11, el límite práctico de payload puede bajar a 50-115 bytes; ese JSON de ejemplo ya ocupa 47 caracteres solo para tres variables, sin contar timestamp ni ID de dispositivo.
Codificar los mismos tres valores en binario —como en el ejemplo de esta guía— toma 6 bytes. Es casi 8 veces más compacto, lo que se traduce directamente en menos tiempo en aire, menos consumo de batería y más margen para agregar variables sin acercarse al límite de dwell time.
Cómo se arma un payload binario
El firmware del nodo decide, campo por campo, cuántos bytes y qué escala usa cada variable. Las reglas más comunes:
- Enteros sin signo (
uint) para valores que nunca son negativos: voltaje de batería en mV, humedad relativa. - Enteros con signo (
int) para valores que sí pueden ser negativos: temperatura ambiental (importante en zonas altoandinas con heladas). - Escalado por un factor fijo (×10, ×100) para representar decimales sin usar bytes de punto flotante, que pesan más. Un
int16(2 bytes) alcanza para representar de -327.68 a 327.67 con dos decimales si se multiplica por 100 antes de enviar. - Orden de bytes (endianness): la mayoría de decoders en LoRaWAN usan big-endian (el byte más significativo primero), pero hay firmwares que usan little-endian. Es la causa número uno de payloads que “no cuadran”: si el decoder asume el orden equivocado, un valor de 500 puede leerse como 5, o como un número absurdo de miles.
Ejemplo real: un payload de 6 bytes
Tomemos el payload del inicio, típico de un Sensor HUB midiendo oxígeno disuelto en una piscina camaronera:
Bytes: 0D 34 01 F4 0A 8C
| Bytes | Campo | Cálculo | Resultado |
|---|---|---|---|
0D 34 |
Batería (uint16, mV) | 0x0D34 = 3.380 | 3,38 V |
01 F4 |
Oxígeno disuelto (uint16, ×100 mg/L) | 0x01F4 = 500 → 500/100 | 5,00 mg/L |
0A 8C |
Temperatura del agua (int16, ×100 °C) | 0x0A8C = 2.700 → 2.700/100 | 27,00 °C |
Un decoder en JavaScript (formato que usan tanto ChirpStack como The Things Stack para sus payload formatters) para este payload se ve así:
function decodeUplink(input) {
var bytes = input.bytes;
return {
data: {
bateria_v: ((bytes[0] << 8) | bytes[1]) / 1000,
oxigeno_mgl: ((bytes[2] << 8) | bytes[3]) / 100,
temperatura_c: ((bytes[4] << 8) | bytes[5]) / 100
}
};
}
Si la temperatura pudiera ser negativa (por ejemplo, en un sensor de suelo de altura), el campo de 2 bytes tendría que tratarse como int16 con signo, no uint16: se resta 65536 al resultado cuando el bit más significativo está en 1. Es un detalle que se olvida con frecuencia y produce lecturas de “-0,01 °C” convertidas en “655,35 °C” en el dashboard.
Formatos estándar: cuándo conviene no inventar el propio
Definir un payload propio da control total, pero obliga a mantener el decoder sincronizado con cada cambio de firmware. Para casos estándar existe Cayenne LPP (Low Power Payload), un formato abierto que varios network servers ya saben decodificar sin escribir código: cada variable se codifica como [canal][tipo][valor], con tipos predefinidos (temperatura = 2 bytes ×10, humedad relativa = 1 byte ×0,5, GPS = 9 bytes, etc.). Su ventaja es la compatibilidad inmediata con plataformas que integran Cayenne LPP de fábrica; su límite es que la precisión y el catálogo de tipos son fijos, así que para variables muy específicas (oxígeno disuelto, turbidez, conductividad) casi siempre termina siendo necesario un decoder a medida, como el del ejemplo anterior.
Dónde vive el decoder
El decoder no corre en el nodo ni en el gateway —esos solo transportan bytes—, sino en el network server o en la plataforma de aplicación que consume los datos:
- ChirpStack: el decoder se define por perfil de dispositivo, en JavaScript, y se ejecuta antes de reenviar el dato por la integración configurada (HTTP, MQTT, InfluxDB).
- The Things Stack (TTN): se llama payload formatter, también en JavaScript, con una función
decodeUplink(input)que recibebytesy elfPorty debe devolver un objetodata. - Yubox Cloud: aplica el decoder correspondiente al modelo de sensor antes de graficar el dato o dispararlo hacia una alarma o un webhook.
Tener el decoder en un solo lugar —no replicado en cada integración corriente abajo— evita el problema más común en despliegues grandes: alguien cambia la escala de un sensor en el firmware, actualiza el decoder en el network server, pero olvida que otro sistema aguas abajo tenía su propia copia del cálculo y sigue interpretando los bytes viejos.
Errores típicos al decodificar
- Confundir
fPortcon parte del payload. El puerto de aplicación (fPort) viaja fuera delFRMPayloady sirve para que el mismo dispositivo distinga tipos de mensaje (por ejemplo,fPort=1para telemetría normal yfPort=2para un evento de alarma), no para codificar datos. - No validar el largo del payload antes de leerlo. Un payload corrupto o de menos bytes de los esperados puede hacer que el decoder lea posiciones fuera de rango y devuelva basura silenciosa en vez de un error visible.
- Mezclar unidades entre firmware y decoder. Si el firmware cambia de mg/L a µg/L en una actualización y el decoder no se actualiza junto con él, los datos históricos y los nuevos quedan en escalas distintas sin que nada lo avise.
- Ignorar el signo en campos que sí pueden ser negativos, como ya se mencionó con la temperatura.
Conclusión
El payload es, literalmente, todo lo que un nodo LoRaWAN puede “decir”: unos pocos bytes que hay que diseñar con cuidado para caber dentro del límite de dwell time, y que solo cobran sentido cuando el decoder correcto —con el orden de bytes, el signo y la escala correctos— los convierte en la medición que un operador realmente necesita ver. Un buen payload no es el que cabe en menos bytes a cualquier costo, sino el que documenta claramente su estructura para que el decoder nunca quede desincronizado con el firmware.
¿Está diseñando el payload de un sensor propio o migrando un despliegue con decoders heredados y poco documentados? Escríbanos: ayudamos a definir la estructura de datos y a integrarla con ChirpStack, TTN o Yubox Cloud.