Ir al contenido principal

Conectar el Sensor de Casco de Lansitec a Ubidots vía TTI

Configura el sensor de casco Lansitec y envía datos a Ubidots mediante LoRaWAN y The Things Industries (TTI).

Camilo Dávila avatar
Escrito por Camilo Dávila
Actualizado hoy

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.

  1. Inicia sesión en la interfaz web de tu Gateway.

  2. Ve a Configuración de Red.

  3. Activa el modo de Reenvío de Paquetes.

  4. Introduce los parámetros de reenvío.

  5. Haz clic en Save & Apply.


2. Registra el Gateway en The Things Industries (TTI)

  1. Inicia sesión en la Consola de The Things Stack.

  2. Presiona Registrar gateway.

  3. Ingresa el Gateway EUI y los demás campos requeridos.

  4. 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

  1. Ve a Aplicaciones en TTI y selecciona tu aplicación.

  2. Ve a la sección Dispositivos finales y haz clic en Registrar dispositivo final.

  3. En JoinEUI, pega el App EUI del dispositivo (se encuentra en la hoja de datos o etiqueta).

  4. Completa los campos restantes: DevEUI, AppKey, etc.

  5. 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

  1. Dentro de tu aplicación en TTI, haz clic en Integrations (Integraciones).

  2. Luego selecciona Webhooks.

  3. Haz clic en + Add webhook.

  4. Escoge Ubidots de la lista de partners.

  5. 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 es lN4s2dlb4IgPgpp4Xeoq02stXcE=.

    • Ubidots Token: El token de tu cuenta Ubidots.

  6. 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.

  1. Ve a Dev Center → Plugins.

  2. Haz clic en el botón “+” y elige The Things Stack.

  3. Llena los campos requeridos (tipo de dispositivo y token de Ubidots).

  4. Una vez creado el plugin, edítalo y ve a su sección de Decoder.

  5. 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 }
  6. 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.

  1. Ve a Dispositivos.

  2. Busca tu Solar Bluetooth Gateway por nombre o ID.

  3. Haz clic en el dispositivo para ver los datos entrantes.


Ahora puedes construir tableros, widgets y alertas utilizando estos datos.

¿Ha quedado contestada tu pregunta?