devicescript-mqtt-cliente-telemetria

Cliente MQTT y telemetría con DeviceScript

  • 5 min

MQTT es un protocolo ligero de mensajería basado en publicación y suscripción, pensado para intercambiar mensajes a través de un servidor central denominado broker.

Para el Internet de las Cosas, donde quizás queremos enviar la temperatura cada segundo o encender una luz instantáneamente, HTTP se queda corto. Aquí es donde entra MQTT (Message Queuing Telemetry Transport).

MQTT funciona como una radio.

  • No envías el mensaje a un destinatario específico.
  • Lo envías a un “canal” (Topic).
  • Quien quiera escuchar ese canal, se suscribe.
  • Un servidor central (Broker) se encarga de repartir el correo.

DeviceScript incluye un cliente MQTT en @devicescript/net que se conecta mediante TCP o TLS.

Configurar el broker

El cliente se crea con startMQTTClient. Para una prueba podemos indicar un broker público directamente; cualquier mensaje enviado será público, así que no incluyas información sensible.

  • Host: test.mosquitto.org
  • Protocol: tcp
  • Port: 1883

Seguridad: En un proyecto real, usa un broker privado con autenticación y, si la placa dispone de memoria suficiente, TLS.

Publicar telemetría

Vamos a publicar la temperatura medida por un ESP32.

Primero creamos el cliente y después llamamos a publish(tema, mensaje).

import * as ds from "@devicescript/core"
import { startMQTTClient } from "@devicescript/net"

const mqtt = await startMQTTClient({
    host: "broker.hivemq.com",
    proto: "tcp",
    port: 1883,
})

// Usamos un topic único para no mezclarlo con otros dispositivos.
// Estructura típica: casa / habitación / dispositivo / variable
const TOPIC_TELEMETRIA = "luisllamas/curso/esp32/estado"

console.log("Iniciando bucle MQTT...")

setInterval(async () => {
    // 1. Creamos los datos
    // Simulamos una temperatura (o leemos un sensor real AHT20 como vimos antes)
    const temperatura = 20 + Math.random() * 5
    
    // 2. Empaquetamos en JSON
    // MQTT envía texto o bytes. JSON es el estándar para datos estructurados.
    const payload = JSON.stringify({
        temp: temperatura,
        uptime: ds.millis(),
        device: "ESP32-01"
    })

    // 3. Publicamos
    try {
        await mqtt.publish(TOPIC_TELEMETRIA, payload)
        console.log(`Enviado: ${payload}`)
    } catch (e) {
        console.error("Error al publicar (¿Sin WiFi o Broker caído?)")
    }

}, 5000) // Enviamos cada 5 segundos
Copied!

Al ejecutar el ejemplo, un cliente MQTT del ordenador podrá recibir los datos cada cinco segundos.

El cliente intenta reconectarse cuando detecta que ha perdido la conectividad. Puedes llamar a stop() si necesitas cerrar el socket y detener los reintentos.

Recibir comandos

Ahora queremos lo contrario: controlar un LED desde fuera. Para ello, nos suscribimos a un topic.

Cuando llegue un mensaje a ese canal, DeviceScript ejecutará nuestra función (callback).

import { startMQTTClient } from "@devicescript/net"
import { startLightBulb } from "@devicescript/servers"
import { pins } from "@dsboard/esp32_c3_devkit_m"

const mqtt = await startMQTTClient({
    host: "broker.hivemq.com",
    proto: "tcp",
    port: 1883,
})

// Configuramos un LED
const led = startLightBulb({ pin: pins.GPIO2 })

// Topic de comandos
const TOPIC_COMANDOS = "luisllamas/curso/esp32/luz"

console.log(`Suscribiéndonos a ${TOPIC_COMANDOS}...`)

// Nos suscribimos. La función se ejecutará por cada mensaje recibido.
await mqtt.subscribe(TOPIC_COMANDOS, async (mensaje) => {
    // 'mensaje' es un objeto Buffer con los datos recibidos
    const texto = mensaje.content.toString()
    
    console.log(`Comando recibido: ${texto}`)

    // Lógica simple: "ON" / "OFF"
    if (texto === "ON") {
        await led.intensity.write(1)
    } else if (texto === "OFF") {
        await led.intensity.write(0)
    } else {
        // Si mandan un número (0-100), intentamos hacer dimming
        const brillo = parseFloat(texto)
        if (!isNaN(brillo)) {
            await led.intensity.write(brillo / 100)
        }
    }
})
Copied!

Calidad de servicio y retención

Aunque DeviceScript abstrae mucho, a veces necesitamos afinar. El método publish acepta opciones, aunque por defecto suele usar QoS 0 (Dispara y olvida).

La documentación archivada no promete todas las opciones de QoS y retención de un cliente MQTT completo. Comprueba la API de la versión que estés utilizando antes de basar en ellas un comportamiento crítico.

Ejemplo: termostato IoT

Vamos a juntar todo. Un dispositivo que:

  1. Lee temperatura (simulada o real).
  2. La envía por MQTT.
  3. Escucha un topic para saber la “temperatura objetivo” y enciende un relé si hace frío.
import { startMQTTClient } from "@devicescript/net"
import { startRelay } from "@devicescript/servers"
import { pins } from "@dsboard/esp32_c3_devkit_m"

const mqtt = await startMQTTClient({
    host: "broker.hivemq.com",
    proto: "tcp",
    port: 1883,
})

// Hardware
const calefaccion = startRelay({ pin: pins.GPIO10 })

// Estado
let temperaturaActual = 20
let temperaturaObjetivo = 22 // Valor por defecto

// Topics
const TOPIC_STATUS = "casa/salon/termostato/status"
const TOPIC_SET = "casa/salon/termostato/set"

// 1. Suscripción: Escuchar cambios de consigna
await mqtt.subscribe(TOPIC_SET, async (msg) => {
    const val = parseFloat(msg.content.toString())
    if (!isNaN(val)) {
        temperaturaObjetivo = val
        console.log(`Nueva temperatura objetivo: ${temperaturaObjetivo}`)
        // Forzamos una comprobación inmediata
        await comprobarTermostato()
    }
})

// 2. Lógica de control
async function comprobarTermostato() {
    // Histéresis simple
    if (temperaturaActual < temperaturaObjetivo - 0.5) {
        await calefaccion.enabled.write(true)
    } else if (temperaturaActual > temperaturaObjetivo + 0.5) {
        await calefaccion.enabled.write(false)
    }
}

// 3. Bucle principal (Lectura y Telemetría)
setInterval(async () => {
    // Simular lectura de sensor (sube y baja aleatoriamente)
    temperaturaActual += (Math.random() - 0.5)
    
    await comprobarTermostato()

    // Enviar estado
    const estado = {
        temp: temperaturaActual.toFixed(1),
        target: temperaturaObjetivo,
        calefaccion: await calefaccion.enabled.read()
    }
    
    await mqtt.publish(TOPIC_STATUS, JSON.stringify(estado))
}, 2000)
Copied!