Soldinamic WIKI / DOCS v1.0
Soldinamic Documentación API de Dispositivos Genéricos (MQTT)

API de Dispositivos Genéricos (MQTT)

La plataforma LumiotLoRaWAN cuenta con un puente MQTT (MQTT Bridge) diseñado para integrar dispositivos IoT genéricos (como ESP32, ESP8266, Raspberry Pi) que operan fuera de la red LoRaWAN nativa, pero que necesitan interactuar con los mismos Dashboards, reportes y lógica de facturación.

Esta guía te enseñará cómo conectar un ESP32 utilizando código Arduino para inyectar telemetría a la plataforma, basándonos en un hardware específico con sensores de temperatura, relés y LEDs indicadores.

1. Hardware de Referencia

El código de ejemplo está diseñado para un ESP32 con la siguiente configuración de pines:

  • DS18B20 (Sensor de Temperatura): Pin D13
  • Relé 1 (REL1): Pin D33
  • Relé 2 (REL2): Pin D32
  • LED Rojo (DIM): Pin D25
  • LED Verde (WIFI): Pin D26
  • LED Azul (MQTT): Pin D27

2. Credenciales y Conexión al Broker MQTT

El Broker MQTT Público de LumiotLoRaWAN opera bajo una conexión segura (TLS) y requiere autenticación básica (Usuario/Contraseña) o mediante tokens JWT.

  • Host: lumiotlorawan.online (o la IP pública de tu instalación)
  • Puerto Seguro (MQTTS): 8883
  • Usuario: Tu Username de dispositivo
  • Contraseña: Tu token de acceso o llave provista por la plataforma

3. Estructura de Tópicos MQTT

Para garantizar el aislamiento Multi-Tenant, todos los dispositivos deben publicar y suscribirse usando una estructura estricta de tópicos que incluye el identificador de tu Empresa (Tenant) y tu Filial (Subsidiary).

El dispositivo enviará sus datos publicando en este tópico:

text
lumiot/v1/{tenantId}/{subsidiaryId}/devices/{devEui}/tx

El payload enviado (Uplink) se empaqueta en formato JSON. En este ejemplo, enviaremos la temperatura en °C y °F, el estado de los relés, y otros datos complementarios para que haga match con la estructura requerida:

json
{
   "device": "1234567890ABCDEF",
   "sn": "1234567890ABCDEF",
   "temperature_c": 23.34,
   "temperature_f": 74.01,
   "relay1": false,
   "relay2": true,
   "dimmer": 50,
   "location": [-16.4897, -68.1193],
   "power": true,
   "state": "System OK",
   "counter": 9349
}

Para recibir comandos remotos (como apagar un relé) desde el Dashboard de LumiotLoRaWAN, tu ESP32 debe suscribirse a:

text
lumiot/v1/{tenantId}/{subsidiaryId}/devices/{devEui}/rx/downlink

LumiotLoRaWAN envía los comandos encapsulados en un JSON que simula la estructura de LoRaWAN. Llegará un mensaje como este:

json
{
  "payloadHex": "030100",
  "fPort": 2,
  "confirmed": true,
  "isEncrypted": false
}

¿Cómo llega y se decodifica este comando? El ESP32 intercepta este JSON, extrae la propiedad "payloadHex" y ejecuta la acción correspondiente. En nuestro ejemplo de código, asumimos una lógica de 3 bytes (Hexadecimal) para el control de relés: Control de Relés (Comando 03):

  • Byte 0 (03): Comando de control de relés.
  • Byte 1 (01 / 02): Cuál relé accionar (01 = Relé 1, 02 = Relé 2).
  • Byte 2 (00 / 01): Estado del relé (00 = Apagado, 01 = Encendido). Por lo tanto, "030100" apaga el Relé 1, y "030201" enciende el Relé 2.

Control de Dimmer PWM (Comando 02):

  • Byte 0 (02): Comando Dimmer.
  • Byte 1 (00): Canal objetivo (LED Rojo).
  • Valor (000 a 100): Porcentaje de intensidad (3 dígitos obligatorios). Por lo tanto, "0200100" enciende el LED Rojo al 100%, y "0200050" lo atenúa al 50%.

4. Ejemplo de Código para ESP32 (Arduino IDE)

El siguiente ejemplo utiliza las bibliotecas WiFi.h, PubSubClient.h, ArduinoJson.h (para decodificar el downlink) y DallasTemperature.h / OneWire.h (para el sensor de temperatura).

cpp
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <esp_task_wdt.h>

// ---------------- Configuracion WiFi ----------------
const char* ssid = "TU_RED_WIFI";
const char* password = "TU_PASSWORD_WIFI";

// ---------------- Configuracion MQTT ----------------
const char* mqtt_server = "mqtt.lumiotlorawan.online";
const int mqtt_port = 8883; // Puerto Seguro (MQTTS)
const char* mqtt_user = "TU_USUARIO_MQTT";
const char* mqtt_pass = "TU_PASSWORD_MQTT";

// Reemplaza con tus IDs reales sacados de la plataforma LumiotLoRaWAN
const char* tenantId = "TU-TENANT-ID-AQUI";
const char* subsidiaryId = "TU-SUBSIDIARY-ID-AQUI";
const char* devEui = "1234567890ABCDEF";
const char* sn = "1234567890ABCDEF"; // Serial Number para registro/asociación

// ------------------- Topics MQTT -------------------
String pubTopic;
String subTopic;
String statusTopic;

// ------------------- Pines -------------------
#define ONE_WIRE_BUS 13
#define RELAY1_PIN   33
#define RELAY2_PIN   32
#define LED_PWM_PIN   25  // Solo para PWM (Dimmer) - NO usar para indicadores
#define LED_STATUS_GREEN  26  // Indicador de sistema funcionando
#define LED_STATUS_BLUE   27  // Indicador de actividad MQTT

// ------------------- Constantes de Temporización -------------------
#define WIFI_TIMEOUT_MS    20000
#define MQTT_TIMEOUT_MS    10000
#define TELEMETRY_INTERVAL 60000
#define HEARTBEAT_INTERVAL 60000

// Patrones de LEDs indicadores
#define LED_GREEN_OFF_TIME    1000  // 1 segundo apagado
#define LED_GREEN_ON_TIME     500   // 0.5 segundos encendido
#define LED_BLUE_BLINK_TIME   200   // Azul parpadeo rápido para actividad

// ------------------- Constantes PWM -------------------
#define PWM_CHANNEL   0
#define PWM_FREQ      5000
#define PWM_RESOLUTION 8

// ------------------- Estados del Sistema -------------------
enum SystemState {
    STATE_INIT,
    STATE_WIFI_CONNECTING,
    STATE_MQTT_CONNECTING,
    STATE_RUNNING,
    STATE_ERROR
};

// ------------------- Objetos Globales -------------------
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
WiFiClientSecure espClient;
PubSubClient client(espClient);

// ------------------- Variables Globales -------------------
SystemState currentState = STATE_INIT;
int uplinkCounter = 0;
int currentDimmer = 100;
bool pwmInitialized = false;
unsigned long lastTelemetryTime = 0;
unsigned long lastHeartbeatTime = 0;
unsigned long systemStartTime = 0;
unsigned long lastGreenLedChange = 0;
bool greenLedState = false;
unsigned long lastBlueLedTime = 0;
bool blueLedState = false;
unsigned long lastWifiCheckTime = 0;

// ------------------- Prototipos -------------------
void publish_telemetry();
void publish_status(bool connected);
void callback(char* topic, byte* payload, unsigned int length);
void update_dimmer(int percent);
void check_watchdog();
void update_status_leds();

// ------------------- Implementación -------------------

void setup() {
    Serial.begin(115200);
    delay(100);
    
    Serial.println("\
╔════════════════════════════════════════╗");
    Serial.println("║     LUMIOT LORAWAN DEVICE v" FIRMWARE_VERSION "     ║");
    Serial.println("╚════════════════════════════════════════╝");
    
    // Inicializar topics
    pubTopic = String("lumiot/v1/") + tenantId + "/" + subsidiaryId + "/devices/" + devEui + "/rx";
    subTopic = String("lumiot/v1/") + tenantId + "/" + subsidiaryId + "/devices/" + devEui + "/rx/downlink";
    statusTopic = String("lumiot/v1/") + tenantId + "/" + subsidiaryId + "/devices/" + devEui + "/status";
    
    // Configurar Watchdog
    esp_task_wdt_init(WDT_TIMEOUT, true);
    esp_task_wdt_add(NULL);
    
    // Configurar pines
    pinMode(RELAY1_PIN, OUTPUT);
    pinMode(RELAY2_PIN, OUTPUT);
    pinMode(LED_STATUS_GREEN, OUTPUT);
    pinMode(LED_STATUS_BLUE, OUTPUT);
    
    // Estado inicial seguro
    digitalWrite(RELAY1_PIN, LOW);
    digitalWrite(RELAY2_PIN, LOW);
    digitalWrite(LED_STATUS_GREEN, LOW);
    digitalWrite(LED_STATUS_BLUE, LOW);
    
    // IMPORTANTE: Configurar PWM en LED_PWM_PIN (solo para dimmer)
    ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
    ledcAttachPin(LED_PWM_PIN, PWM_CHANNEL);
    ledcWrite(PWM_CHANNEL, 255);  // Iniciar al 100%
    pwmInitialized = true;
    currentDimmer = 100;
    
    Serial.println("\
=== Configuración de Pines ===");
    Serial.println("LED PWM (Dimmer): GPIO " + String(LED_PWM_PIN));
    Serial.println("LED Status Green: GPIO " + String(LED_STATUS_GREEN));
    Serial.println("LED Status Blue:  GPIO " + String(LED_STATUS_BLUE));
    Serial.println("Relé 1: GPIO " + String(RELAY1_PIN));
    Serial.println("Relé 2: GPIO " + String(RELAY2_PIN));
    Serial.println("================================\
");
    
    // Inicializar sensor de temperatura
    sensors.begin();
    sensors.setWaitForConversion(false);
    
    // Configurar cliente MQTT
    espClient.setInsecure();
    client.setBufferSize(1024);
    client.setKeepAlive(30);
    client.setServer(mqtt_server, mqtt_port);
    client.setCallback(callback);
    
    systemStartTime = millis();
    
    Serial.println("✓ Sistema inicializado correctamente");
    Serial.println("→ Conectando a WiFi...");
    
    currentState = STATE_WIFI_CONNECTING;
    WiFi.begin(ssid, password);
}

void loop() {
    unsigned long currentMillis = millis();
    
    // Reset Watchdog cada 5 segundos
    static unsigned long lastWatchdogReset = 0;
    if (currentMillis - lastWatchdogReset >= 5000) {
        esp_task_wdt_reset();
        lastWatchdogReset = currentMillis;
    }
    
    // Actualizar LEDs indicadores de estado
    update_status_leds();
    
    // Máquina de estados
    switch(currentState) {
        case STATE_WIFI_CONNECTING:
            if (WiFi.status() == WL_CONNECTED) {
                Serial.println("\
✓ WiFi conectado exitosamente");
                Serial.println("  IP: " + WiFi.localIP().toString());
                Serial.println("  RSSI: " + String(WiFi.RSSI()) + " dBm");
                Serial.println("\
→ Conectando a MQTT...");
                currentState = STATE_MQTT_CONNECTING;
            } else if (currentMillis > 30000) {
                Serial.println("✗ Error: No se pudo conectar WiFi");
                currentState = STATE_ERROR;
            }
            break;
            
        case STATE_MQTT_CONNECTING:
            if (!client.connected()) {
                if (WiFi.status() == WL_CONNECTED) {
                    Serial.print("→ Intentando conexión MQTT... ");
                    
                    String clientId = String(devEui) + "-" + String(random(0xffff), HEX);
                    String lwtMessage = "{\\"status\\": false}";
                    
                    if (client.connect(clientId.c_str(), mqtt_user, mqtt_pass, 
                                      statusTopic.c_str(), 1, true, lwtMessage.c_str())) {
                        Serial.println("✓ Conectado!");
                        publish_status(true);
                        
                        if (client.subscribe(subTopic.c_str())) {
                            Serial.println("  ✓ Suscrito a: " + subTopic);
                        }
                        
                        currentState = STATE_RUNNING;
                        Serial.println("\
✓ Sistema en modo RUNNING");
                        Serial.println("  LED Verde: 1s apagado / 0.5s encendido");
                        Serial.println("  LED Azul: Parpadea con actividad MQTT\
");
                        lastTelemetryTime = millis();
                        lastHeartbeatTime = millis();
                    } else {
                        Serial.print("✗ Falló (rc=");
                        Serial.print(client.state());
                        Serial.println(") reintentando en 2 seg...");
                        delay(2000);
                    }
                } else {
                    Serial.println("⚠ WiFi perdido, reiniciando...");
                    currentState = STATE_WIFI_CONNECTING;
                    WiFi.begin(ssid, password);
                }
            }
            break;
            
        case STATE_RUNNING:
            if (WiFi.status() != WL_CONNECTED) {
                Serial.println("\
⚠ WiFi desconectado");
                currentState = STATE_WIFI_CONNECTING;
                WiFi.begin(ssid, password);
                break;
            }
            
            if (!client.connected()) {
                Serial.println("\
⚠ MQTT desconectado");
                currentState = STATE_MQTT_CONNECTING;
                break;
            }
            
            client.loop();
            
            if (currentMillis - lastTelemetryTime >= TELEMETRY_INTERVAL) {
                publish_telemetry();
                lastTelemetryTime = currentMillis;
            }
            
            if (currentMillis - lastHeartbeatTime >= HEARTBEAT_INTERVAL) {
                publish_status(true);
                lastHeartbeatTime = currentMillis;
            }
            break;
            
        case STATE_ERROR:
            Serial.println("\
⚠ Estado de error - Reiniciando en 5 segundos...");
            // Parpadeo rápido del LED verde para indicar error
            for(int i = 0; i < 10; i++) {
                digitalWrite(LED_STATUS_GREEN, HIGH);
                delay(100);
                digitalWrite(LED_STATUS_GREEN, LOW);
                delay(100);
            }
            ESP.restart();
            break;
    }
    
    delay(10);
}

void update_status_leds() {
    unsigned long currentMillis = millis();
    
    switch(currentState) {
        case STATE_WIFI_CONNECTING:
            // LED verde parpadeo rápido indicando conexión WiFi
            if (currentMillis - lastGreenLedChange >= 200) {
                lastGreenLedChange = currentMillis;
                greenLedState = !greenLedState;
                digitalWrite(LED_STATUS_GREEN, greenLedState ? HIGH : LOW);
            }
            digitalWrite(LED_STATUS_BLUE, LOW);
            break;
            
        case STATE_MQTT_CONNECTING:
            // LED azul parpadeo rápido indicando conexión MQTT
            if (currentMillis - lastGreenLedChange >= 200) {
                lastGreenLedChange = currentMillis;
                blueLedState = !blueLedState;
                digitalWrite(LED_STATUS_BLUE, blueLedState ? HIGH : LOW);
            }
            digitalWrite(LED_STATUS_GREEN, LOW);
            break;
            
        case STATE_RUNNING:
            // LED verde: 1s apagado, 0.5s encendido (solo indicador de sistema vivo)
            if (greenLedState) {
                if (currentMillis - lastGreenLedChange >= LED_GREEN_ON_TIME) {
                    lastGreenLedChange = currentMillis;
                    greenLedState = false;
                    digitalWrite(LED_STATUS_GREEN, LOW);
                }
            } else {
                if (currentMillis - lastGreenLedChange >= LED_GREEN_OFF_TIME) {
                    lastGreenLedChange = currentMillis;
                    greenLedState = true;
                    digitalWrite(LED_STATUS_GREEN, HIGH);
                }
            }
            // El LED azul solo se activa durante actividad MQTT (en callback y publish)
            break;
            
        default:
            digitalWrite(LED_STATUS_GREEN, LOW);
            digitalWrite(LED_STATUS_BLUE, LOW);
            break;
    }
}

void publish_telemetry() {
    if (!client.connected()) {
        Serial.println("→ Telemetría diferida: MQTT desconectado");
        return;
    }
    
    // Indicador de actividad MQTT (LED azul)
    digitalWrite(LED_STATUS_BLUE, HIGH);
    
    sensors.requestTemperatures();
    float tempC = sensors.getTempCByIndex(0);
    float tempF = sensors.getTempFByIndex(0);
    
    bool r1State = digitalRead(RELAY1_PIN);
    bool r2State = digitalRead(RELAY2_PIN);
    
    uplinkCounter++;
    
    StaticJsonDocument<512> doc;
    doc["device"] = devEui;
    doc["sn"] = sn;
    doc["temperature_c"] = tempC;
    doc["temperature_f"] = tempF;
    doc["relay1"] = r1State;
    doc["relay2"] = r2State;
    doc["dimmer"] = currentDimmer;
    doc["uptime"] = (millis() - systemStartTime) / 1000;
    doc["free_heap"] = ESP.getFreeHeap();
    doc["wifi_rssi"] = WiFi.RSSI();
    doc["counter"] = uplinkCounter;
    
    JsonArray loc = doc.createNestedArray("location");
    loc.add(-16.4897);
    loc.add(-68.1193);
    
    String payload;
    serializeJson(doc, payload);
    
    Serial.println("\
--- Enviando Telemetría ---");
    Serial.print("Topico: "); Serial.println(pubTopic);
    Serial.print("Temp: "); Serial.print(tempC); Serial.println(" °C");
    Serial.print("Relé1: "); Serial.print(r1State ? "ON" : "OFF");
    Serial.print(" | Relé2: "); Serial.print(r2State ? "ON" : "OFF");
    Serial.print(" | Dimmer: "); Serial.println(currentDimmer);
    
    if (client.publish(pubTopic.c_str(), payload.c_str())) {
        Serial.println("✓ Publicación exitosa");
    } else {
        Serial.println("✗ Error al publicar");
    }
    Serial.println("-----------------------------\
");
    
    delay(100);  // Pequeña pausa para ver el LED
    digitalWrite(LED_STATUS_BLUE, LOW);
}

void publish_status(bool connected) {
    if (!client.connected()) return;
    
    StaticJsonDocument<256> doc;
    doc["status"] = connected;
    doc["uptime"] = (millis() - systemStartTime) / 1000;
    doc["heap"] = ESP.getFreeHeap();
    doc["wifi_rssi"] = WiFi.RSSI();
    
    String payload;
    serializeJson(doc, payload);
    client.publish(statusTopic.c_str(), payload.c_str(), true);
}

void callback(char* topic, byte* payload, unsigned int length) {
    // Activar LED azul por 200ms para indicar comando recibido
    digitalWrite(LED_STATUS_BLUE, HIGH);
    
    String message;
    for (unsigned int i = 0; i < length; i++) {
        message += (char)payload[i];
    }
    
    Serial.println("\
📨 Comando recibido:");
    Serial.println("  Topic: " + String(topic));
    Serial.println("  Data: " + message);
    
    StaticJsonDocument<512> doc;
    DeserializationError error = deserializeJson(doc, message);
    
    if (!error && doc.containsKey("payloadHex")) {
        String payloadHex = doc["payloadHex"].as<String>();
        
        if (payloadHex.length() >= 2) {
            String cmdType = payloadHex.substring(0, 2);
            
            if (cmdType == "03" && payloadHex.length() >= 6) {
                String relayId = payloadHex.substring(2, 4);
                int state = (int)strtol(payloadHex.substring(4, 6).c_str(), NULL, 16);
                
                if (relayId == "01") {
                    digitalWrite(RELAY1_PIN, state > 0 ? HIGH : LOW);
                    Serial.printf("  ✓ Relé 1: %s\
", state > 0 ? "ON" : "OFF");
                } else if (relayId == "02") {
                    digitalWrite(RELAY2_PIN, state > 0 ? HIGH : LOW);
                    Serial.printf("  ✓ Relé 2: %s\
", state > 0 ? "ON" : "OFF");
                }
            } 
            else if (cmdType == "02" && payloadHex.length() >= 6) {
                int percent = (int)strtol(payloadHex.substring(4, 6).c_str(), NULL, 16);
                update_dimmer(percent);
                Serial.printf("  ✓ Dimmer: %d%%\
", percent);
            }
        }
    }
    
    delay(200);  // Mantener LED azul encendido 200ms
    digitalWrite(LED_STATUS_BLUE, LOW);
}

void update_dimmer(int percent) {
    percent = constrain(percent, 0, 100);
    int pwmValue = map(percent, 0, 100, 0, 255);
    
    ledcWrite(PWM_CHANNEL, pwmValue);
    currentDimmer = percent;
    
    Serial.printf("  PWM actualizado: %d%% -> Valor: %d\
", percent, pwmValue);
}

5. Integración con el Motor LNS

Dado que LumiotLoRaWAN intercepta todos los mensajes MQTT y los inyecta en Kafka, no es necesario configurar decodificadores JS adicionales (Codecs) en la plataforma si el Payload ya es un JSON válido cuyas claves coinciden con las variables de tus Dashboards.

El JSON entrante pasará directamente hacia las bases de datos de series de tiempo (TimescaleDB) asociadas a tu Empresa (Tenant).

Busca temas, dispositivos, MQTT, gateways, API...
Vista ampliada