The Lansitec Solar Bluetooth Gateway enables remote Bluetooth beacon data collection over LoRaWAN networks. Designed for low-power environments, it uses a solar-powered battery to operate autonomously and transmit sensor data to Ubidots.
Requirements
1. Configure the LoRaWAN Gateway
Although the images in this guide feature a LoRaWAN gateway from RAK Wireless, the integration process is not limited to RAK devices. You can use any LoRaWAN gateway that supports packet forwarding to The Things Industries. The key requirement is that the gateway must be correctly configured to forward data to your TTI instance. As long as that condition is met, the rest of the integration, device registration, decoding, and data forwarding to Ubidots, remains the same. |
The first step is setting your LoRaWAN gateway to forward data to The Things Industries.
Log in to your gateway's web interface.
Go to Network Settings.
Enable Packet Forwarder Mode.
Input the forwarding parameters.
Click Save & Apply.
Your gateway is now ready to forward data packets.
2. Register the Gateway on The Things Industries (TTI)
Log in to The Things Stack Console.
Press Register gateway.
Enter the Gateway EUI and other required fields.
Click Register gateway.
Once saved, your gateway will show up as Disconnected until the first packet is received.
3. Add the Solar Bluetooth Gateway as a Device
Go to Applications in TTI and select your application.
Go to the End devices section and, once there, click Register end device.
In JoinEUI, paste the device’s App EUI (found in the datasheet or label).
Complete the remaining fields: DevEUI, AppKey, etc.
Click Register end device.
Your Solar Bluetooth Gateway is now listed as a device in TTI.
4. Add the Payload Formatter in TTI
To decode the uplink data, a custom JavaScript formatter is required:
Inside your registered device, go to Payload Formatters.
Select Custom JavaScript Formatter.
Paste the Solar Bluetooth Gateway script:
// 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.
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: decodeDeviceReportRule(bytes) };
case 0x05:
return { data: decodeWaterLevelDetection(bytes) };
case 0x08:
return { data: decodeDeviceType1(bytes) };
case 0x09:
return { data: decodeDeviceType2(bytes) };
case 0x0a:
return { data: decodeDeviceType3(bytes) };
case 0x0e:
return { data: decodeMultiDeviceTypeMessage(bytes) };
case 0x0f:
return { data: decodeAcknowledgment(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";
// mode
data.mode = bytes[0] & 0x07;
// loRaWANBand
var loRaWANBandValue = bytes[1];
if (loRaWANBandValue == 0x00) {
data.loRaWANBand = "KR920";
} else if (loRaWANBandValue == 0x01) {
data.loRaWANBand = "AU915";
} else if (loRaWANBandValue == 0x04) {
data.loRaWANBand = "CN470";
} else if (loRaWANBandValue == 0x08) {
data.loRaWANBand = "AS923";
} else if (loRaWANBandValue == 0x10) {
data.loRaWANBand = "EU433";
} else if (loRaWANBandValue == 0x20) {
data.loRaWANBand = "EU868";
} else if (loRaWANBandValue == 0x40) {
data.loRaWANBand = "US915";
}
// power
data.power = ((bytes[2] >> 3) & 0x1f) + "dBm";
// continuousBleReceiveEnable
data.continuousBleReceiveEnable = ((bytes[2] >> 1) & 0x1) == 0 ? "Disable" : "Enable";
// dr
data.dr = (bytes[3] >> 4) & 0x0f;
// positionReportInterval
data.positionReportInterval = (((bytes[4] << 8) & 0xff00) | (bytes[5] & 0xff)) * 5 + "s";
// heartbeatInterval
data.heartbeatInterval = bytes[6] * 30 + "s";
// bleReceivingDuration
data.bleReceivingDuration = bytes[7] + "s";
// networkReconnectionInterval
data.networkReconnectionInterval = bytes[8] * 5 + "min";
return data;
}
// type: 0x2 Heartbeat
function decodeHeartbeat(bytes) {
var data = {};
// type
data.type = "Heartbeat";
// battery
var batteryValue = bytes[1];
if (batteryValue > 100) {
data.battery = bytes[1] / 100 + 1.5 + "V";
} else {
data.battery = bytes[1] + "%";
}
// rssi
data.rssi = bytes[2] * -1 + "dBm";
// snr
data.snr = (((bytes[3] << 8) & 0xff00) | (bytes[4] & 0xff)) / 100 + "dB";
// version
data.version = ((bytes[5] << 8) & 0xff00) | (bytes[6] & 0xff);
// chargeState
var chargeStateValue = bytes[7] & 0xff;
if (chargeStateValue == 0x00) {
data.chargeState = "Not charging";
} else if (chargeStateValue == 0x50) {
data.chargeState = "Charging";
} else if (chargeStateValue == 0x60) {
data.chargeState = "Charging completed";
}
return data;
}
// type: 0x3 DeviceReportRule
function decodeDeviceReportRule(bytes) {
var data = {};
data.type = "DeviceReportRule";
data.deviceTypeQuantity = bytes[1] & 0xff;
data.deviceTypeId = (bytes[2] >> 4) & 0x0f;
data.filterAndDataBlockQuantity = bytes[2] & 0x0f;
var filterBlock = [];
var dataBlock = [];
var macBlock = [];
var index = 3;
for (let i = 0; i < data.filterAndDataBlockQuantity; i++) {
var ruleType = bytes[index++] & 0xff;
var startAddress = bytes[index++] & 0xff;
var endAddress = bytes[index++] & 0xff;
var filter = {};
if (ruleType == 1) {
filter.ruleType = "FilterBlock";
filter.startAddress = byteToHex(startAddress);
filter.endAddress = byteToHex(endAddress);
var len = endAddress - startAddress;
var filterValue = "";
for (let j = 0; j < len + 1; j++) {
filterValue += byteToHex(bytes[index + j]);
}
filter.value = filterValue;
index = index + (endAddress - startAddress + 1);
filterBlock.push(filter);
} else if (ruleType == 2) {
filter.ruleType = "DataBlock";
filter.startAddress = byteToHex(startAddress);
filter.endAddress = byteToHex(endAddress);
dataBlock.push(filter);
} else if (ruleType == 3) {
filter.ruleType = "MACBlock";
filter.startAddress = byteToHex(startAddress);
filter.endAddress = byteToHex(endAddress);
macBlock.push(filter);
}
}
data.filterBlock = filterBlock;
data.dataBlock = dataBlock;
data.macBlock = macBlock;
return data;
}
// type: 0x5 WaterLevelDetection
function decodeWaterLevelDetection(bytes) {
var data = {};
data.type = "WaterLevelDetection";
data.waterLevel = (((bytes[1] << 8) & 0xff00) | (bytes[2] & 0xff)) + "mm";
return data;
}
// type: 0x8 DeviceType1
function decodeDeviceType1(bytes) {
var data = {
type: "DeviceType1",
number: bytes[0] & 0x0f
};
var index = 1;
for (let i = 0; i < data.number; 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;
index = index + 5;
}
return data;
}
// type: 0x9 DeviceType2
function decodeDeviceType2(bytes) {
var data = {
type: "DeviceType2",
number: bytes[0] & 0x0f
};
var index = 1;
for (let i = 0; i < data.number; 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;
index = index + 5;
}
return data;
}
// type: 0xa DeviceType3
function decodeDeviceType3(bytes) {
var data = {
type: "DeviceType3",
number: bytes[0] & 0x0f
};
var index = 1;
for (let i = 0; i < data.number; 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;
index = index + 5;
}
return data;
}
// type: 0xe MultiDeviceTypeMessage
function decodeMultiDeviceTypeMessage(bytes) {
var data = {
type: "MultiDeviceTypeMessage",
number: bytes[0] & 0x0f
};
for (let i = 0; i < data.number; i++) {
var index = 1 + 6 * i;
var deviceTypeId = bytes[index];
var major = ((bytes[index + 1] << 8) | bytes[index + 2]).toString(16).toUpperCase().padStart(4, "0");
var minor = ((bytes[index + 3] << 8) | bytes[index + 4]).toString(16).toUpperCase().padStart(4, "0");
var rssi = bytes[index + 5] - 256 + "dBm";
data["deviceTypeId" + (i + 1)] = deviceTypeId;
data["beacon" + (i + 1)] = major + minor;
data["rssi" + (i + 1)] = rssi;
index = index + 6;
}
return data;
}
// type: 0xf Acknowledgment
function decodeAcknowledgment(bytes) {
var data = {};
data.type = "Acknowledgment";
data.result = (bytes[0] & 0x0f) == 0 ? "Success" : "Failure";
data.msgId = (bytes[1] & 0xff).toString(16).toUpperCase();
return data;
}
function byteToHex(str) {
return str.toString(16).toUpperCase().padStart(2, "0");
}
Click Save changes.
This formatter parses messages such as a heartbeat status, device registration, BLE readings, and more.
5. Create a Webhook to Send Data to Ubidots
Inside your TTI application, go to Webhooks.
Click + Add webhook.
Choose Ubidots from the partner list.
Fill in the required fields:
Webhook ID: lowercase name (e.g. solar_ble_gateway)
Plugin ID: The last part of your Ubidots plugin URL (after the final /). For example, if your endpoint’s URL is “
https://dataplugin.ubidots.com/api/web-hook/lN4s 2dlb4IgPgpp4Xeoq02stXcE=
” then your plugin ID is “lN4s2dlb4IgPgpp4Xeoq02stXcE=
”Ubidots Token: Your Ubidots account token
Scroll down and click Create Ubidots Webhook.
6. Create the Plugin and Decoder in Ubidots
Now, let’s set up the receiving end in Ubidots.
Go to Dev Center → Plugins.
Click the “+” button and choose The Things Stack.
Fill in the required fields (device type and Ubidots token).
Once the plugin is created, edit it and go to its Decoder section.
Delete the default decoder and paste this one instead:
// 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) {
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; // Direct numeric value
} else if (!isNaN(parseFloat(value))) {
ubidots_payload[key] = parseFloat(value); // e.g., "46%" or "10dB"
} else {
// Non-numeric string values (e.g., battery state)
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 };Save changes by clicking on Save & make live.
7. Visualize the Data in Ubidots
After completing the previous steps, data will start appearing in your Ubidots account.
Go to Devices.
Look for your Solar Bluetooth Gateway by name or ID.
Click the device to view incoming data.
You can now build dashboards, widgets, and alerts using this data.