En esta sección se describe la configuración del gateway utilizado en el proyecto VitiSensor, elemento clave para la comunicación entre los dispositivos desplegados en campo y las plataformas de visualización en la nube.
El gateway empleado en este proyecto es el Milesight UG65, un equipo diseñado para redes LoRaWAN que permite la recepción de datos de los nodos sensores y su posterior envío a servidores y aplicaciones externas.
A lo largo de este apartado se detallan los pasos necesarios para su puesta en marcha, incluyendo:
Configuración inicial del dispositivo
Conexión a red y acceso a la interfaz de gestión
Integración con la red LoRaWAN
Gestión de dispositivos y recepción de datos
Envío de información hacia plataformas como ThingSpeak
Aunque en el proyecto se ha utilizado el modelo Milesight UG65, los conceptos y procedimientos descritos son aplicables a otros gateways LoRaWAN disponibles en el mercado, por lo que esta guía puede servir como referencia para implementar soluciones similares con distintos equipos.
Este apartado tiene como finalidad facilitar la comprensión del sistema de comunicaciones y permitir la replicación completa del proyecto, reforzando así el objetivo de transferencia de conocimiento.
En este caso mostraremos la configuración de un Gateway Milesight UG65 con conexión WiFi podremos acceder a su configuración mediante la WiFi o cable LAN.
Utilizaremos la conexión cableada únicamente para administrar el Gateway, y la configuraremos como nos interese (Dirección IP, máscara de subred, puerta de enlace, y DNS). Lo realizaremos desde el menú Interface > Port. La dirección IP que indiquemos nos servirá para acceder a la configuración del Gateway posteriormente.
Ahora procedemos a configurar el adaptador WiFi para conectarnos a la red de forma inalámbrica y subir los datos medidos. En primer lugar iremos al menú Interface > WLAN para configura el Gateway como cliente WiFi e indicar a qué red nos queremos conectar. También indicaremos DHCP Client en el parámetro Protocol de modo que será la red a la que nos conectemos la que nos asigne dirección IP (IMPORTANTE: Para que esto funcione la red a la que nos vamos a conectar debe tener DHCP Server habilitado, de lo contrario tendremos que indicar la dirección IP y el resto de parámetros manualmente).
Quedaría indicar al Gateway que la conexión a través de la que se conectará a Internet para subir datos será la WiFi. Para ello iremos al menú Packet Forwarder > WAN Failover y pondremos como Main Interface la conexión WiFi.
Para configurar la parte de la comunicación LoRa, apenas hay que modificar nada de la configuración por defecto del Gateway. Dejaremos el menú Packet Forwarder > General como aparece en la imagen.
Y en el menú Network Server > General pondremos el Channel Plan EU868 que es el utilizado en Europa.
En el menú Network Server > Applications indicaremos el nombre que le queramos dar (Viti-Sensor en nuestro caso).
Como los dispositivos que vamos a usar son de clase A (optimizados específicamente para conseguir el mínimo consumo de energía posible) se lo indicaremos al Gateway, así como el estándar de conexión (OTAA) más recomendado para conectar y registrar de forma automática dispositivos IoT en una red LoRaWAN. Todo ello en el menú Network Server > Profiles.
Y ya, en el menú Network Server > Device, registraremos nuestros dispositivos a través del DevEUI propio de cada uno y la AppKey que debe coincidir con la que le pongamos al dispositivo y debe tener exactamente 32 caracteres hexadecimales.
Una vez realiza ya podremos ver la comunicación en el menú Network Server > Packets.
Ya nos estarían llegando datos de los sensores a través de los Arduino MKR WAN 1310, pero necesitamos darles formato para enviarlos a Thingspeak correctamente. Para ello utilizaremos Node-RED que ya está integrado en el Gateway. Primero lo habilitamos desde APP > Node-RED, y pulsaremos en “Launch”.
Para poder acceder a Node-RED hay que poner el mismo usuario y contraseña que para acceder al Gateway. Y una vez que nos aparezca la ventana de trabajo tendremos que crear el siguiente flujo.
Pasamos ahora a describir los nodos. En primer lugar colocaremos un nodo “LoRa Input” para indicar que los datos nos llegarán a través de esa conexión. No necesita ninguna configuración.
El siguiente será un nodo “function” que hemos llamado “Analizar datos” que nos servirá para detectar si el DevEUI del dispositivo que nos ha enviado los datos está registrado como válido, y para dar formato a los datos recibidos que posteriormente enviaremos a Thingspeak.
En la pestaña “Function” añadiremos el siguiente código:
// Configuración del mensaje para el debug
let DEBUG = msg.debug || true; // true para debug, false producción
// Obtener DevEUI y payload
let devEUI = (msg.deveui || "").toLowerCase();
let base64 = msg.payload;
let fcnt = msg.fcnt || 0;//Contador de envío. Al final no lo uso
//Comprobación de que el mensaje lleva DevEUI
if (!devEUI) {
if (DEBUG) node.warn("No se encontró DevEUI en el mensaje");
return null;
}
//Comprobación de que el mensaje no llega vacío
if (!base64) {
if (DEBUG) node.warn(`Payload vacío para sensor ${devEUI}`);
return null;
}
// Convertir Base64 a bytes.
let bytes;
try {
bytes = Array.from(Buffer.from(base64, 'base64'));
} catch (e) {
if (DEBUG) node.warn(`Base64 inválido para sensor ${devEUI}`);
return null;
}
// Convertir los datos recibidos en función del DevEUI
const decoders = {
//Vitisensor1
//Módulo 1
"<DevEUI Módulo1>": (b) => {
if (b.length < 18) return null;
return {
tempAMB1: ((b[0]<<8)|b[1])/100, //Se convierte a int
humAMB1: ((b[2]<<8)|b[3])/100, //Se convierte a int
radiacion1: ((b[4]<<8)|b[5])/100, //Se convierte a int
dirV1: (b[6]<<8)|b[7],
velocidadV1: (b[8]<<8)|b[9],
haylluvia1: (b[10]<<8)|b[11],
lluvia1h1: ((b[12]<<8)|b[13])/100, //Se convierte a int
lluvia24h1: ((b[14]<<8)|b[15])/100, //Se convierte a int
};
},
// Módulo 2
""<DevEUI Módulo2>"": (b) => {
if (b.length < 12) return null;
return {
HUMhoja21: ((b[0]<<8)|b[1])/100, //Se convierte a int
temperaturaSuelo21: ((b[2]<<8)|b[3])/100,//Se convierte a int
humedadSuelo21: ((b[4]<<8)|b[5])/100, //Se convierte a int
pHSuelo21: ((b[6]<<8)|b[7])/100, //Se convierte a int
ecSuelo21: (b[8]<<8)|b[9]
};
},
// Módulo 3
""<DevEUI Módulo3>"": (b) => {
if (b.length < 12) return null;
return {
HUMhoja31: ((b[0]<<8)|b[1])/100, //Se convierte a int
temperaturaSuelo31: ((b[2]<<8)|b[3])/100, //Se convierte a int
humedadSuelo31: ((b[4]<<8)|b[5])/100, //Se convierte a int
pHSuelo31: ((b[6]<<8)|b[7])/100, //Se convierte a int
ecSuelo31: (b[8]<<8)|b[9]
};
},
//Vitisensor2
// Módulo 1
""<DevEUI Módulo1>"": (b) => {
if (b.length < 18) return null;
return {
tempAMB2: ((b[0]<<8)|b[1])/100, //Se convierte a int
humAMB2: ((b[2]<<8)|b[3])/100, //Se convierte a int
radiacion2: ((b[4]<<8)|b[5])/100, //Se convierte a int
dirV2: (b[6]<<8)|b[7],
velocidadV2: (b[8]<<8)|b[9],
haylluvia2: (b[10]<<8)|b[11],
lluvia1h2: ((b[12]<<8)|b[13])/100, //Se convierte a int
lluvia24h2: ((b[14]<<8)|b[15])/100, //Se convierte a int
};
},
// Módulo 2
"<DevEUI Módulo2>": (b) => {
if (b.length < 12) return null;
return {
HUMhoja22: ((b[0]<<8)|b[1])/100, //Se convierte a int
temperaturaSuelo22: ((b[2]<<8)|b[3])/100, //Se convierte a int
humedadSuelo22: ((b[4]<<8)|b[5])/100, //Se convierte a int
pHSuelo22: ((b[6]<<8)|b[7])/100, //Se convierte a int
ecSuelo22: (b[8]<<8)|b[9]
};
},
// Módulo 3
"<DevEUI Módulo3>": (b) => {
if (b.length < 12) return null;
return {
HUMhoja32: ((b[0]<<8)|b[1])/100, //Se convierte a int
temperaturaSuelo32: ((b[2]<<8)|b[3])/100, //Se convierte a int
humedadSuelo32: ((b[4]<<8)|b[5])/100, //Se convierte a int
pHSuelo32: ((b[6]<<8)|b[7])/100, //Se convierte a int
ecSuelo32: (b[8]<<8)|b[9]
};
},
};
// Comprobar que el sensor es conocido
if (!decoders[devEUI]) {
if (DEBUG) node.warn(`Sensor desconocido: ${devEUI}`);
return null;
}
// Filtrar duplicados usando FCNT
let ultimoFCNT = flow.get(`fcnt_${devEUI}`) || -1;
if (fcnt === ultimoFCNT) {
if (DEBUG) node.warn(`Mensaje duplicado para ${devEUI} (FCNT=${fcnt}), ignorando`);
return null;
}
flow.set(`fcnt_${devEUI}`, fcnt);
// Decodificar valores
let result = decoders[devEUI](bytes);
if (!result) return null;
// Mapear a fields de ThingSpeak
let tsFields = {};
if (result.tempAMB1 !== undefined) tsFields.field1 = result.tempAMB1;
if (result.humAMB1 !== undefined) tsFields.field2 = result.humAMB1;
if (result.radiacion1 !== undefined) tsFields.field3 = result.radiacion1;
if (result.dirV1 !== undefined) tsFields.field4 = result.dirV1;
if (result.velocidadV1 !== undefined) tsFields.field5 = result.velocidadV1;
if (result.haylluvia1 !== undefined) tsFields.field6 = result.haylluvia1;
if (result.lluvia1h1 !== undefined) tsFields.field7 = result.lluvia1h1;
if (result.lluvia24h1 !== undefined) tsFields.field8 = result.lluvia24h1;
if (result.HUMhoja21 !== undefined) tsFields.field1 = result.HUMhoja21;
if (result.temperaturaSuelo21 !== undefined) tsFields.field2 = result. temperaturaSuelo21;
if (result.humedadSuelo21 !== undefined) tsFields.field3 = result. humedadSuelo21;
if (result.pHSuelo21 !== undefined) tsFields.field4 = result. pHSuelo21;
if (result.ecSuelo21 !== undefined) tsFields.field5 = result. ecSuelo21;
if (result.HUMhoja31 !== undefined) tsFields.field1 = result.HUMhoja31;
if (result.temperaturaSuelo31 !== undefined) tsFields.field2 = result. temperaturaSuelo31;
if (result.humedadSuelo31 !== undefined) tsFields.field3 = result. humedadSuelo31;
if (result.pHSuelo31 !== undefined) tsFields.field4 = result. pHSuelo31;
if (result.ecSuelo31 !== undefined) tsFields.field5 = result. ecSuelo31;
if (result.tempAMB2 !== undefined) tsFields.field1 = result.tempAMB2;
if (result.humAMB2 !== undefined) tsFields.field2 = result.humAMB2;
if (result.radiacion2 !== undefined) tsFields.field3 = result.radiacion2;
if (result.dirV2 !== undefined) tsFields.field4 = result.dirV2;
if (result.velocidadV2 !== undefined) tsFields.field5 = result.velocidadV2;
if (result.haylluvia2 !== undefined) tsFields.field6 = result.haylluvia2;
if (result.lluvia1h2 !== undefined) tsFields.field7 = result.lluvia1h2;
if (result.lluvia24h2 !== undefined) tsFields.field8 = result.lluvia24h2;
if (result.HUMhoja22 !== undefined) tsFields.field1 = result.HUMhoja22;
if (result.temperaturaSuelo22 !== undefined) tsFields.field2 = result. temperaturaSuelo22;
if (result.humedadSuelo22 !== undefined) tsFields.field3 = result. humedadSuelo22;
if (result.pHSuelo22 !== undefined) tsFields.field4 = result. pHSuelo22;
if (result.ecSuelo22 !== undefined) tsFields.field5 = result. ecSuelo22;
if (result.HUMhoja32 !== undefined) tsFields.field1 = result.HUMhoja32;
if (result.temperaturaSuelo32 !== undefined) tsFields.field2 = result. temperaturaSuelo32;
if (result.humedadSuelo32 !== undefined) tsFields.field3 = result. humedadSuelo32;
if (result.pHSuelo32 !== undefined) tsFields.field4 = result. pHSuelo32;
if (result.ecSuelo32 !== undefined) tsFields.field5 = result. ecSuelo32;
// Preparar salida
msg.devEUI = devEUI;
msg.tsFields = tsFields;
msg.payload = tsFields; // Listo para ThingSpeak
delete msg.tsFields; // Evita warnings
// Debug opcional
if (DEBUG && tsFields && Object.keys(tsFields).length > 0) {
let fieldDebug = Object.keys(tsFields).map(f => `${f}=${tsFields[f]}`).join(", ");
node.warn(`DevEUI: ${devEUI} → Fields: ${fieldDebug}`);
}
return msg;
De este punto salen dos nodos. Un nodo para el debug y otro nodo llamado “Preparar datos” donde añadiremos el siguiente código:
// Mapa de dispositivos → API Keys ThingSpeak
let apiKeys = {
"DevEUI Módulo1": "<Write API Key Thingspeak>",
"DevEUI Módulo2": "<Write API Key Thingspeak>",
"DevEUI Módulo3": "<Write API Key Thingspeak>"
};
// Obtener DevEUI
let devEUI = msg.devEUI;
// Validar
if (!devEUI || !apiKeys[devEUI]) {
node.warn("DevEUI sin canal asignado: " + devEUI);
return null;
}
// Obtener API key correspondiente
let apiKey = apiKeys[devEUI];
// Validar datos
if (!msg.payload || Object.keys(msg.payload).length === 0) {
node.warn("No hay datos para enviar");
return null;
}
// Construir payload
let payloadStr = `api_key=${apiKey}&` + Object.entries(msg.payload)
.map(([k,v]) => `${k}=${v}`)
.join('&');
// Preparar request
msg.payload = payloadStr;
msg.headers = {
'Content-Type': 'application/x-www-form-urlencoded'
};
return msg;
Y ya por último tendríamos el nodo “http request” llamado “Enviar a Thingspeak” en el que debemos indicar el método POST y la URL de la API de Thingspeak que es https://api.thingspeak.com/update.