Soldinamic WIKI / DOCS v1.0
Soldinamic Documentación Generic Devices API (MQTT)

Generic Devices API (MQTT)

The LumiotLoRaWAN platform features an MQTT Bridge designed to integrate generic IoT devices (like ESP32, ESP8266, Raspberry Pi) that operate outside the native LoRaWAN network, but need to interact with the same Dashboards, reports, and billing logic.

This guide will teach you how to connect an ESP32 using Arduino code to inject telemetry into the platform, based on a specific hardware setup with temperature sensors, relays, and indicator LEDs.

1. Reference Hardware

The example code is designed for an ESP32 with the following pin configuration:

  • DS18B20 (Temperature Sensor): Pin D13
  • Relay 1 (REL1): Pin D33
  • Relay 2 (REL2): Pin D32
  • Red LED (DIM): Pin D25
  • Green LED (WIFI): Pin D26
  • Blue LED (MQTT): Pin D27

2. Credentials and Connection to the MQTT Broker

The LumiotLoRaWAN Public MQTT Broker operates under a secure connection (TLS) and requires basic authentication (Username/Password) or via JWT tokens.

  • Host: lumiotlorawan.online (or the public IP of your installation)
  • Secure Port (MQTTS): 8883
  • User: Your device Username
  • Password: Your access token or key provided by the platform

3. MQTT Topic Structure

To guarantee Multi-Tenant isolation, all devices must publish and subscribe using a strict topic structure that includes your Company (Tenant) and Subsidiary identifiers.

The device will send its data by publishing to this topic:

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

The payload sent (Uplink) is packaged in JSON format. In this example, we will send the temperature in °C and °F, the relay statuses, and other complementary data to match the required structure:

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
}

To receive remote commands (like turning off a relay) from the LumiotLoRaWAN Dashboard, your ESP32 must subscribe to:

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

LumiotLoRaWAN sends the commands encapsulated in a JSON that simulates the LoRaWAN structure. A message like this will arrive:

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

How is this command received and decoded? The ESP32 intercepts this JSON, extracts the "payloadHex" property and executes the corresponding action. In our example code, we assume a 3-byte (Hexadecimal) logic for relay control: Relay Control (Command 03):

  • Byte 0 (03): Relay control command.
  • Byte 1 (01 / 02): Which relay to trigger (01 = Relay 1, 02 = Relay 2).
  • Byte 2 (00 / 01): Relay state (00 = Off, 01 = On). Therefore, "030100" turns off Relay 1, and "030201" turns on Relay 2.

PWM Dimmer Control (Command 02):

  • Byte 0 (02): Dimmer command.
  • Byte 1 (00): Target channel (Red LED).
  • Value (000 to 100): Intensity percentage (3 digits required). Therefore, "0200100" turns on the Red LED at 100%, and "0200050" dims it to 50%.

4. Example Code for ESP32 (Arduino IDE)

The following example uses the libraries WiFi.h, PubSubClient.h, ArduinoJson.h (for downlink decoding) and DallasTemperature.h / OneWire.h (for the temperature sensor).

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. Integration with the LNS Engine

Since LumiotLoRaWAN intercepts all MQTT messages and injects them into Kafka, there is no need to configure additional JS decoders (Codecs) on the platform if the Payload is already a valid JSON whose keys match your Dashboard variables.

The incoming JSON will pass directly to the time-series databases (TimescaleDB) associated with your Company (Tenant).

Search for topics, devices, MQTT, gateways, API...
Vista ampliada