Protocolo MQTT de Dispositivos
El transporte recomendado para la ingesta de dispositivos: MQTT 3.1.1/5 estándar sobre TLS, soporte de doble credencial y firma HMAC por mensaje.
Conexión y autenticación
- TLS es obligatorio - el broker solo acepta
mqtts://en el puerto de dispositivos. - El
usernamede MQTT DEBE ser el id corto del dispositivo que se muestra en el panel de credenciales, yclientidDEBE ser igual a ese mismo usuario - ambos se validan en el broker. - Dos opciones de contraseña, ambas funcionan con el mismo usuario: una contraseña MQTT dedicada (prefijo
dvp_), o tu Device API Key existente (prefijodvk_) para firmware que no se puede reconfigurar. La que uses para conectarte es también tu clave de firma HMAC - ver más abajo. - Active
clean_start=true(una sesión persistente no aporta nada a un dispositivo, y descartarla evita que se acumule una cola de comandos mientras el dispositivo está desconectado). Como la sesión es limpia, vuelva a suscribirse a su topiccmden cada conexión: hacerlo desde su callback de conexión cubre las reconexiones sin esfuerzo. - QoS 1 en todos los topics. QoS 2 no es compatible. Los mensajes retenidos (retained) están deshabilitados en toda la plataforma.
- Keepalive: proponga
30segundos en el CONNECT, que es lo que usan todos los ejemplos de IoT Manager. Es lo máximo que su cliente puede permanecer en silencio antes de que el broker corte la conexión; si no publica nada en esa ventana, envíe un PINGREQ. Un valor menor detecta antes una conexión TCP muerta a cambio de más despertares de radio; uno mucho mayor deja que un dispositivo caído de la red parezca conectado durante minutos. - El keepalive no es presencia: aparecer como desconectado en el panel depende de un tiempo de espera distinto, basado en la telemetría (más abajo), así que un dispositivo que mantiene la conexión abierta pero deja de publicar igual pasa a desconectado.
mqtts://mqtt.luminatti.online:8883
username: a1b2c3d4e5f6a1b2
password: dvp_Kx9mQ2... (or your dvk_ device api key)
clientid: a1b2c3d4e5f6a1b2 (same as username)
Espacio de nombres de topics
Prefijo iot/v1. Todo topic tiene como raíz el topic_id corto de tu dispositivo - el broker rechaza los intentos de publish/subscribe fuera de estas rutas exactas.
| Topic | Dirección | QoS |
|---|---|---|
| iot/v1/<topic_id>/telemetry | dispositivo → backend (una lectura o un lote) | 1 |
| iot/v1/<topic_id>/cmd | backend → dispositivo (subscribe) | 1 |
| iot/v1/<topic_id>/cmd/ack | dispositivo → backend | 1 |
<topic_id> viene de tu panel de credenciales - un id opaco y corto (12 caracteres), independiente de los ids de proyecto/dispositivo. A diferencia de tu usuario, se puede rotar por separado desde el panel de credenciales sin afectar tu contraseña ni tu clave de firma. Un dispositivo solo puede publicar en sus propios topics de telemetría/ack y suscribirse a su propio topic cmd - cualquier otro topic es denegado por la ACL en el momento del CONNECT.
No existe un topic de presencia/status - de todas formas, un dispositivo nunca puede anunciar de forma confiable que se está desconectando. En su lugar, un dispositivo se marca offline una vez que no ha enviado nada durante 120 segundos (configurable por dispositivo desde el dashboard, mínimo 30).
Firma HMAC del payload (LM1)
Todo payload de telemetría y command-ack está firmado - no existe un modo sin firmar. La clave de firma es la misma contraseña con la que te conectaste: tu contraseña MQTT dvp_, o tu API key dvk_ si usaste esa en su lugar. Nada extra que guardar - el mismo secreto que autentica tu conexión también firma tus frames.
Forma del frame: LM1.<mac>.<ts>.<body>. mac es el HMAC-SHA256 en hexadecimal minúscula sobre los bytes exactos topic + "\n" + ts + "\n" + body (no una estructura JSON re-serializada o reordenada) - constrúyelo con sprintf + HMAC, no requiere una librería JSON del lado del firmware.
ts es un timestamp Unix en segundos. Un frame se rechaza si ts tiene más de 3600s de antigüedad o está más de 300s en el futuro (sincroniza el reloj de tu dispositivo con NTP).
El campo sequence dentro del payload de telemetría debe incrementarse estrictamente por dispositivo - una secuencia repetida o que no aumenta se descarta en silencio, aunque la firma y el timestamp sean válidos.
topic + "\n" + ts + "\n" + body
topic: iot/v1/demo00000042/telemetry
key: dvp_0000000000000000000000000000
ts: 1735689600
body: {"sequence":1,"event_id":"vector-1","values":{"x":{"value":1}}}
frame: LM1.67a3a4b8cf766ec815d5ce261c51f85664a3e44f956654a5616c654050b94377.1735689600.{"sequence":1,"event_id":"vector-1","values":{"x":{"value":1}}}
Telemetría - medición individual
Este transporte no tiene ACK por mensaje: el PUBACK de QoS 1 es la única confirmación de entrega, y significa recepción por el broker, no aceptación por el backend; use viajes de ida y vuelta de cmd/ack o el panel para confirmar la ingesta.
values asocia una clave de señal a un objeto de valor. Un value puede ser number, an integer, a boolean or a string, nada más. unit es una cadena libre (vacía por defecto) y quality es uno de valid / stale / uncertain / error (valid por defecto).
timestamp es la hora observada por el dispositivo (UTC, ISO-8601); el servidor registra aparte su propia hora de recepción, así que la telemetría almacenada en buffer sigue quedando en el momento correcto. event_id es una clave de idempotencia opcional. sequence es obligatorio en la práctica: vea la sección de firma más arriba.
iot/v1//telemetry
{
"timestamp": "2026-08-22T17:00:00Z",
"sequence": 42,
"event_id": "optional-idempotency-key",
"values": {
"temperature": {"value": 24.7, "unit": "C", "quality": "valid"},
"humidity": {"value": 61.2, "unit": "%", "quality": "valid"}
},
"metadata": {}
}
Telemetría - lote
Se publica en el mismo topic telemetry que una medición individual: el backend los distingue por la clave messages de nivel superior, no por el topic. Cada elemento se valida exactamente igual que un mensaje de telemetría individual, incluida su propia comprobación de sequence.
sequence debe crecer estrictamente DENTRO del lote además de entre lotes: el backend recorre la lista en orden y descarta el lote ENTERO en el primer valor que no crece.
iot/v1//telemetry
{
"messages": [
{ "timestamp": "...", "sequence": 41, "event_id": "...", "values": {"...": {"value": 1}} },
{ "timestamp": "...", "sequence": 42, "event_id": "...", "values": {"...": {"value": 2}} }
]
}
Comandos
Suscríbete a tu topic cmd al conectarte. Todo comando que el backend publique ahí está firmado con LM1 usando tu propia contraseña de conexión, igual que tus propios frames salientes - verifícalo de la misma manera antes de actuar sobre él.
command es la clave (key) del datastream objetivo (por ejemplo, "relay1"), nunca un verbo - y kind se deriva del lado del servidor a partir de ese datastream, siempre uno de exactamente action / boolean / number / string / enum. Un kind de "action" nunca lleva un campo value; cualquier otro kind requiere uno del tipo correspondiente.
Responde en cmd/ack con el mismo command_id, firmado. status debe ser exactamente la cadena "ok" para indicar éxito; cualquier otro valor se trata como un error del dispositivo y error se lee como la razón del fallo. Un comando que no recibe ack dentro del timeout configurado se marca como timed out del lado del servidor.
{"type": "COMMAND", "command_id": "...", "command": "gate1", "kind": "action"}
{"type": "COMMAND", "command_id": "...", "command": "relay1", "kind": "boolean", "value": true}
{"command_id": "...", "status": "ok", "value": true}
{"command_id": "...", "status": "error", "error": "actuator_fault"}
Presencia: timeout de desconexión
Aquí no hay nada que publicar - un dispositivo no anuncia su propia presencia. El dashboard marca un dispositivo como offline una vez que no ha recibido telemetría durante el timeout de desconexión configurado para ese dispositivo (por defecto 120s, ajustable por dispositivo, mínimo 30s). No existe una vía más rápida que esa: no hay topic de status, no hay Last Will, no hay nada que el dispositivo deba configurar.
Límites de mensaje
| Límite | Aplica a |
|---|---|
| 50 | claves de señal en un mismo mapa values |
| 100 | mensajes en un mismo lote ({"messages": [...]}) |
| 65536 | bytes para el frame publicado completo: incluye el prefijo de firma LM1, no solo el cuerpo |
El tope de 64 KB se comprueba antes incluso de verificar la firma, así que un frame demasiado grande se descarta sin diagnóstico alguno. Divida un backlog por BYTES además de por cantidad de mensajes: 100 lecturas pequeñas caben de sobra, 100 lecturas anchas de varias señales quizá no. Aparte, su plan limita cuántas claves de señal DISTINTAS puede usar un dispositivo (20 en todos los planes hoy): consulte <a href="/docs/guides#rate-limits">Guías</a>.
Comportamiento de rechazo
- No existe un frame de ERROR en este transporte - un mensaje rechazado (firma inválida, timestamp vencido, secuencia repetida, payload demasiado grande, JSON malformado) simplemente se descarta después del propio PUBACK del broker, que solo confirma la recepción del lado del broker, no la aceptación del backend.
- Usa una ida y vuelta de
cmd/ack, o la vista de telemetría en vivo del dashboard, para confirmar de forma positiva que tu dispositivo está siendo aceptado - no asumas que un PUBACK significa que fue ingerido. - Una credencial revocada o regenerada se expulsa (kick) del broker de inmediato - reconéctate con la nueva credencial desde el panel.
Límites de tasa
Los mismos límites por dispositivo y por nivel (tier) que cualquier otro transporte de ingesta, medidos con el timestamp firmado de tu payload y no con el momento de llegada - una ráfaga de backlog entregada tras una reconexión se acepta siempre que los timestamps de los payloads estén correctamente espaciados. Consulta la tabla completa en la página <a href="/docs/guides#rate-limits">Guías</a>.