El Sensor de Casco Lansitec está basado en tecnologías GNSS, Bluetooth 5.0 y LoRaWAN. Permite seguimiento tanto en interiores como exteriores, ofreciendo funciones que simplifican la gestión en entornos industriales. Su acelerómetro integrado de 3 ejes detecta el estado de movimiento del dispositivo, ayudando a conservar la batería y mejorar la experiencia del usuario.
Las aplicaciones típicas incluyen gestión de visitantes, seguimiento de trabajadores de fábrica, monitoreo de trabajadores de construcción y rastreo de vehículos.
Requisitos
1. Configura el Gateway LoRaWAN
Aunque las imágenes de esta guía muestran un gateway LoRaWAN de RAK Wireless, el proceso de integración no se limita a dispositivos RAK. Puedes usar cualquier gateway LoRaWAN que soporte reenvío de paquetes a The Things Industries. El requisito clave es que el gateway esté correctamente configurado para reenviar datos a tu instancia de TTI. Mientras esa condición se cumpla, el resto de la integración —registro del dispositivo, decodificación y reenvío de datos a Ubidots— será igual.
El primer paso es configurar tu gateway LoRaWAN para reenviar datos a The Things Industries.
Inicia sesión en la interfaz web de tu Gateway.
Ve a Configuración de Red.
Activa el modo de Reenvío de Paquetes.
Introduce los parámetros de reenvío.
Haz clic en Save & Apply.
2. Registra el Gateway en The Things Industries (TTI)
Inicia sesión en la Consola de The Things Stack.
Presiona Registrar gateway.
Ingresa el Gateway EUI y los demás campos requeridos.
Haz clic en Registrar gateway.
Una vez guardado, tu gateway aparecerá como Desconectado hasta que reciba su primer paquete.
3. Agrega el dispositivo a TTI
Ve a Aplicaciones en TTI y selecciona tu aplicación.
Ve a la sección Dispositivos finales y haz clic en Registrar dispositivo final.
En JoinEUI, pega el App EUI del dispositivo (se encuentra en la hoja de datos o etiqueta).
Completa los campos restantes: DevEUI, AppKey, etc.
Haz clic en Registrar dispositivo final.
4. Agrega el Formateador de Payload en TTI
Para decodificar los datos de subida, se requiere un formateador personalizado en JavaScript:
1. Dentro del dispositivo registrado en TTI, ve a Payload Formatters.
2. Selecciona “Custom JavaScript Formatter” como tipo de formateador.
3. Pega el script decodificador del Sensor de Casco, por ejemplo:
// Decode uplink function.
//
// Input is an object with the following fields:
// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0]
// - fPort = Uplink fPort.
// - variables = Object containing the configured device variables.
//
// Output must be an object with the following fields:
// - data = Object representing the decoded payload.
// Helmet Sensor decoder
function decodeUplink(input) {
// bytes
var bytes = input.bytes;
// type
var uplinkType = (bytes[0] >> 4) & 0x0f;
switch (uplinkType) {
case 0x01:
return { data: decodeRegistration(bytes) };
case 0x02:
return { data: decodeHeartbeat(bytes) };
case 0x03:
return { data: decodeGNSSPosition(bytes) };
case 0x05:
return { data: decodeUUIDReport(bytes) };
case 0x07:
return { data: decodeBeacon(bytes) };
case 0x08:
return { data: decodeAlarm(bytes) };
case 0x0c:
return { data: decodeBracelet(bytes) };
default:
return null;
}
}
// type: 0x1 Registration
function decodeRegistration(bytes) {
var data = {};
data.type = "Registration";
// adr
data.adr = ((bytes[0] >> 3) & 0x1) == 0 ? "OFF" : "ON";
// power
data.power = ((bytes[2] >> 3) & 0x1f) + "dBm";
// dr
data.dr = (bytes[3] >> 4) & 0x0f;
// gnssEnable
data.gnssEnable = ((bytes[3] >> 3) & 0x01) == 0 ? "Disable" : "Enable";
// positionReportMode
var positionReportMode = (bytes[3] >> 1) & 0x03;
if (positionReportMode == 0) {
data.positionReportMode = "Period";
} else if (positionReportMode == 0) {
data.positionReportMode = "Autonomous";
} else if (positionReportMode == 0) {
data.positionReportMode = "On-Demand";
}
// bleEnable
data.bleEnable = (bytes[3] & 0x01) == 0 ? "Disable" : "Enable";
// blePositionReportInterval
data.blePositionReportInterval = (((bytes[4] << 8) & 0xff00) | (bytes[5] & 0xff)) * 5 + "s";
// gnssPositionReportInterval
data.gnssPositionReportInterval =(((bytes[6] << 8) & 0xff00) | (bytes[7] & 0xff)) * 5 + "s";
// heartbeatPeriod
data.heartbeatPeriod = (bytes[8] & 0xff) * 30 + "s";
// version
data.version = (bytes[9] & 0xff).toString(16).toUpperCase() + "." + (bytes[10] & 0xff).toString(16).toUpperCase();
// cfmmsg
data.cfmmsg = "1 Confirmed every " + (bytes[11] & 0xff) + " Heartbeat";
// hbCount
data.hbCount = "Disconnect Judgement " + (bytes[12] & 0xff);
// fallDetectFeatureThreshold
data.fallDetectFeatureThreshold = (bytes[13] & 0xff) * 0.5 + " meters";
return data;
}
// type: 0x2 Heartbeat
function decodeHeartbeat(bytes) {
var data = {};
// type
data.type = "Heartbeat";
// battery
data.battery = bytes[1] + "%";
// rssi
data.rssi = bytes[2] * -1 + "dBm";
// snr
data.snr = (((bytes[3] << 8) & 0xff00) | (bytes[4] & 0xff)) / 100 + "dB";
// bleReceivingNumber
data.bleReceivingNumber = bytes[5];
// gnssSearchingNumber
data.gnssSearchingNumber = bytes[6];
// chargeTime
data.chargeTime = bytes[7] * 30 + "s";
// wearTime
data.wearTime = bytes[8] * 30 + "s";
// moveState
data.moveState = (bytes[9] >> 4) & 0x0f;
// temperature
data.temperature = 0;
return data;
}
// type: 0x3 GNSSPosition
function decodeGNSSPosition(bytes) {
var data = {};
// type
data.type = "GNSSPosition";
// gnssState
data.gnssState = ((bytes[0] >> 3) & 0x01) == 0 ? "Success" : "Fail";
// wearState
data.wearState = (bytes[0] & 0x01) == 0 ? "Do not wear" : "Wear";
// pressure
let pressure = (bytes[1] << 24) | (bytes[2] << 16) | (bytes[3] << 8) | bytes[4];
data.pressure = pressure / 10 + "pa";
// longitude
let longitude = (bytes[5] << 24) | (bytes[6] << 16) | (bytes[7] << 8) | bytes[8];
data.longitude = hex2float(longitude);
// latitude
let latitude = (bytes[9] << 24) | (bytes[10] << 16) | (bytes[11] << 8) | bytes[12];
data.latitude = hex2float(latitude);
// time
let time = (bytes[13] << 24) | (bytes[14] << 16) | (bytes[15] << 8) | bytes[16];
data.time = timestampToTime((time + 8 * 60 * 60) * 1000);
return data;
}
// type: 0x5 UUIDReport
function decodeUUIDReport(bytes) {
var data = {};
// type
data.type = "UUIDReport";
// number
data.number = Math.floor((bytes.length - 1) / 17);
var beaconUUIDList = [];
for (let i = 0; i < data.number; i++) {
var beaconTypeId = bytes[1 + 17 * i] & 0x03;
if (beaconTypeId == 0x00) {
beaconTypeId = "PositionBeaconUUID";
} else if (beaconTypeId == 0x01) {
beaconTypeId = "AssetBeaconUUID";
} else if (beaconTypeId == 0x02) {
beaconTypeId = "SpecialBeaconUUID";
} else if (beaconTypeId == 0x03) {
beaconTypeId = "SearchBeaconUUID";
}
var beaconUUID = "";
for (let j = 0; j < 16; j++) {
beaconUUID += (bytes[2 + 17 * i + j] & 0xff)
.toString(16)
.toUpperCase()
.padStart(2, "0");
}
beaconUUIDList.push({ beaconTypeId, beaconUUID });
}
data.beaconUUIDList = beaconUUIDList;
return data;
}
// type: 0x7 Beacon
function decodeBeacon(bytes) {
var data = {};
data.type = "Beacon";
// wearState
data.wearState = (bytes[0] & 0x01) == 0 ? "Not wearing" : "Wearing";
// pressure
let pressure =
(bytes[1] << 24) | (bytes[2] << 16) | (bytes[3] << 8) | bytes[4];
data.pressure = pressure / 10 + "pa";
// numner
data.number = bytes[5] & 0x0f;
for (let i = 0; i < data.number; i++) {
var index = 7 + 5 * i;
var major = ((bytes[index] << 8) | bytes[index + 1])
.toString(16)
.toUpperCase()
.padStart(4, "0");
var minor = ((bytes[index + 2] << 8) | bytes[index + 3])
.toString(16)
.toUpperCase()
.padStart(4, "0");
var rssi = bytes[index + 4] - 256 + "dBm";
data["beacon" + (i + 1)] = major + minor;
data["rssi" + (i + 1)] = rssi;
}
return data;
}
// type: 0x8 Alarm
function decodeAlarm(bytes) {
var data = {};
data.type = "Alarm";
var alarmValue = bytes[1] & 0xff;
if (alarmValue == 1) {
data.alarm = "SOS";
} else if (alarmValue == 2) {
data.alarm = "Fall";
} else if (alarmValue == 3) {
data.alarm = "Special area";
} else if (alarmValue == 4) {
data.alarm = "Search";
}
return data;
}
// type: 0x0c Bracelet
function decodeBracelet(bytes) {
var data = {};
data.type = "Bracelet";
var number = bytes[1];
data.number = number;
var braceletList = [];
for (let i = 0; i < number; i++) {
var bracelet = {};
var mac = ""
for (let j = 0; j < 6; j++) {
mac += bytes[j + 2 + i * 21]
.toString(16)
.toUpperCase()
.padStart(2, "0");
}
bracelet.mac = mac;
bracelet.batteryLevel = bytes[8 + i * 21]; // 电量等级
bracelet.heartrate = bytes[9 + i * 21]; // 心率
bracelet.systolicPressure = bytes[10 + i * 21]; // 收缩压
bracelet.diastolicPressure = bytes[11 + i * 21]; // 舒张压
bracelet.wristTemperature = (((bytes[12 + i * 21] << 8) & 0xff00) | (bytes[13 + i * 21] & 0xff)) / 10 + "℃"; // 腕温
bracelet.bodyTemperature = (((bytes[14 + i * 21] << 8) & 0xff00) | (bytes[15 + i * 21] & 0xff)) / 10 + "℃"; // 体温
bracelet.stepNumber = ((bytes[16 + i * 21] << 16) & 0xff0000) | ((bytes[17 + i * 21] << 8) & 0xff00) | (bytes[18 + i * 21] & 0xff); // 步数
bracelet.wearStatus = ((bytes[19 + i * 21]) & 0x01) == 0 ? "Not wearing" : "Wearing";
bracelet.bluetoothBroadcastInterval = (bytes[20 + i * 21] & 0xff) + "s";
bracelet.samplingInterval = (bytes[21 + i * 21]& 0x7f) + "s";
bracelet.rssi = (bytes[22 + i * 21] - 256) + "dBm";
braceletList.push(bracelet);
}
data.braceletList = braceletList;
return data;
}
function hex2float(num) {
var sign = num & 0x80000000 ? -1 : 1;
var exponent = ((num >> 23) & 0xff) - 127;
var mantissa = 1 + (num & 0x7fffff) / 0x7fffff;
return sign * mantissa * Math.pow(2, exponent);
}
function timestampToTime(timestamp) {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hour = date.getHours().toString().padStart(2, "0");
const minute = date.getMinutes().toString().padStart(2, "0");
const second = date.getSeconds().toString().padStart(2, "0");
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
4. Haz clic en Save.
5. Crea un Webhook para Enviar Datos a Ubidots
Dentro de tu aplicación en TTI, haz clic en Integrations (Integraciones).
Luego selecciona Webhooks.
Haz clic en + Add webhook.
Escoge Ubidots de la lista de partners.
Completa los campos requeridos:
Webhook ID: Usa un nombre significativo en minúsculas (por ejemplo,
helmet_sensor
).Plugin ID: Es la parte final de tu URL del plugin Ubidots (lo que está después del último “/”). Por ejemplo, si tu endpoint es
https://dataplugin.ubidots.com/api/web-hook/lN4s2dlb4IgPgpp4Xeoq02stXcE=
,
tu plugin ID eslN4s2dlb4IgPgpp4Xeoq02stXcE=
.Ubidots Token: El token de tu cuenta Ubidots.
Desplázate hacia abajo y haz clic en Create Ubidots Webhook.
6. Crea el Plugin y el Decodificador en Ubidots
Ahora, configuremos el receptor en Ubidots.
Ve a Dev Center → Plugins.
Haz clic en el botón “+” y elige The Things Stack.
Llena los campos requeridos (tipo de dispositivo y token de Ubidots).
Una vez creado el plugin, edítalo y ve a su sección de Decoder.
Elimina el decodificador por defecto y pega este en su lugar:
// Ubidots decoder
async function formatPayload(args){
var ubidots_payload = {};
// Log received data for debugging purposes:
// console.log(JSON.stringify(args));
// Get RSSI and SNR variables using gateways data:
var gateways = args['uplink_message']['rx_metadata'];
for (const i in gateways) {
// Get gateway EUI and name
var gw = gateways[i];
var gw_eui = gw['gateway_ids']['eui'];
var gw_id = gw['gateway_ids']['gateway_id'];
// Build RSSI and SNR variables
ubidots_payload['rssi-' + gw_id] = {
"value": gw['rssi'],
"context": {
"channel_index": gw['channel_index'],
"channel_rssi": gw['channel_rssi'],
"gw_eui": gw_eui,
"gw_id": gw_id,
"uplink_token": gw['uplink_token']
}
}
ubidots_payload['snr-' + gw_id] = gw['snr'];
}
// Get Fcnt and Port variables:
ubidots_payload['f_cnt'] = args['uplink_message']['f_cnt'];
ubidots_payload['f_port'] = args['uplink_message']['f_port'];
// Get uplink's timestamp
ubidots_payload['timestamp'] = new Date(args['uplink_message']['received_at']).getTime();
// If you're already decoding in TTS using payload formatters,
// then uncomment the following line to use "uplink_message.decoded_payload".
// PROTIP: Make sure the incoming decoded payload is an Ubidots-compatible JSON (See https://ubidots.com/docs/hw/#sending-data)
var decoded_payload = args['uplink_message']['decoded_payload'];
// By default, this plugin uses "uplink_message.frm_payload" and sends it to the decoding function "decodeUplink".
// For more vendor-specific decoders, check out https://github.com/TheThingsNetwork/lorawan-devices/tree/master/vendor
// let bytes = Buffer.from(args['uplink_message']['frm_payload'], 'base64');
// var decoded_payload = decodeUplink(bytes)['data'];
// Merge decoded payload into Ubidots payload
// Object.assign(ubidots_payload, decoded_payload);
Object.keys(decoded_payload).forEach(key => {
const value = decoded_payload[key];
if (typeof value === 'number') {
ubidots_payload[key] = value; // 直接添加数字字段
} else if (!isNaN(parseFloat(value))) {
ubidots_payload[key] = parseFloat(value); // 将 "46%" 或 "10dB" 转为数字
} else {
// 字符串等非数值字段可以放在 context 中,例如 battery 状态
ubidots_payload[key] = {
value: 1,
context: { label: value }
};
}
});
return ubidots_payload
}
// function decodeUplink(bytes) {
// // Decoder for the RAK1906 WisBlock Environmental Sensor (https://store.rakwireless.com/products/rak1906-bme680-environment-sensor)
// var decoded = {};
// if (bytes[0] == 1) {
//
//
//
// // If received data is of Environment Monitoring type
// decoded.temperature = (bytes[1] << 8 | (bytes[2])) / 100;
// decoded.humidity = (bytes[3] << 8 | (bytes[4])) / 100;
// decoded.pressure = (bytes[8] | (bytes[7] << 8) | (bytes[6] << 16) | (bytes[5] << 24)) / 100;
// decoded.gas = bytes[12] | (bytes[11] << 8) | (bytes[10] << 16) | (bytes[9] << 24);
// }
// return {"data": decoded};
// }
module.exports = { formatPayload }Guarda los cambios haciendo clic en Guardar y publicar.
7. Visualiza los Datos en Ubidots
Después de completar los pasos anteriores, los datos comenzarán a aparecer en tu cuenta de Ubidots.
Ve a Dispositivos.
Busca tu Solar Bluetooth Gateway por nombre o ID.
Haz clic en el dispositivo para ver los datos entrantes.
Ahora puedes construir tableros, widgets y alertas utilizando estos datos.