The ioTracker 3, by ioThings, is an IoT device provided with LoRaWAN connectivity to achieve data transmission, as well as Wi-Fi and Bluetooth for configuration purposes. It has several built-in sensors to measure temperature, humidity, light, and various precision localization technologies including GPS, Galileo, and LoRa triangulation, among others.
Requirements
An ioTracker 3 device
An active ioTracker console account
An active account on The Things Network console
An Android device with Bluetooth
1. Configure the ioTracker
Enable Bluetooth on your Android device.
Launch the application store.
Search for “ioTracker Configurator” and install it.
Take your ioTracker 3, press and hold the button until the LED starts flashing blue, then briefly press and release. This will make the ioTracker be discoverable by your Android device’s Bluetooth interface.
Pro Tip: Do not release the button immediately on the second press, you must hold it for at least half a second.
Launch “IoTracker Configurator” app.
Tap on “LOGIN” and enter your ioTracker account credentials.
You’ll be able to see your device listed. Tap on it .
Click on the "LoRaWAN options.
Search for the pencil icon at the top right corner of the screen and click on it.
Copy the following info and make sure not to edit it. It will be needed in upcoming steps:
DevEUI
AppEUI
AppKey
Change the LoRaWAN region according to your needs by tapping on the dropdown menu and selecting the appropriate option. You cand find out what frequency band is allowed in your region here.
Click the “Update” button to save the LoRaWAN region settings.
Close the app.
Pro Tip: You can actually edit the AppEui and set any value you wish, however, this should match this setting when registering the device on the LNS.
2. Register the ioTracker on TTN LNS
Go to TTN's console and log in.
Go to the “Applications” tab and click on the “+ Add application” button.
Give your application a meaningful name, application ID, and description.
Click on “Create application”.
Click on “+ Add end device”.
Select “Enter end device specifics manually”.
Go to the “Frequency plan” drop-down menu and choose the frequency plan according to your region. In this case, the region we're located at allows the US915 band for LoRaWAN applications.
Go to “LoRaWAN version” and there choose “LoRaWAN specification 1.0.4” from the drop-down menu.
Go to “Regional Parameters version” drop-down menu and select “RP002 Regional Parameters 1.0.0”.
Scroll down to the “Provisioning information” section and set the “JoinEUI” value to that of the device’s AppEUI obtained in the first section. If you didn’t edit its value, it should be all zeroes. Then click the “Confirm” button once it’s enabled.
The following settings will become editable, do so in the following manner:
DevEUI: Set this value according to the device’s DevEUI obtained in the first section.
AppKey: Set this value according to the device’s AppKey obtained in the first section.
End device ID: Give this field a meaningful name that helps you identify it easily, such as “io-tracker-01”, “io-tracker-home” or anything that suits you.
Click on “Register end device”.
You’ll be prompted to a new page where the “end device’s ID” and the LoRaWAN configuration settings are displayed.
3. Set the Payload Formatter
Click on “Payload formatters” drop-down menu.
Click on “Uplink”.
Go to the “Formatter type” section and select “Custom Javascript formatter”.
Go to the “Formatter code” section, delete all the code there and paste the following:
Payload formatter code:
"use strict";
/* eslint no-bitwise: ["error", { "allow": ["&", "<<", ">>", "|"] }] */
/* eslint no-plusplus: "off" */
function Decoder(bytes) {
var decoded = {};
var index = 0;
function toSignedChar(byte) {
return (byte & 127) - (byte & 128);
}
function toSignedShort(byte1, byte2) {
var sign = byte1 & 1 << 7;
var x = (byte1 & 0xFF) << 8 | byte2 & 0xFF;
if (sign) {
return 0xFFFF0000 | x;
}
return x;
}
function toUnsignedShort(byte1, byte2) {
return (byte1 << 8) + byte2;
}
function toSignedInteger(byte1, byte2, byte3, byte4) {
return (byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4;
}
function bytesToHexString(bytes){
if (!bytes){
return null;
}
bytes = new Uint8Array(bytes);
var hexBytes = [];
for (var i = 0; i < bytes.length; ++i) {
var byteString = bytes[i].toString(16);
if (byteString.length < 2){
byteString = "0" + byteString;
}
hexBytes.push(byteString);
}
return hexBytes.join("");
}
function substring(source, offset, length) {
var buffer = new Uint8Array(length);
for(var i = 0; i < length; i++) {
buffer[i] = source[offset+i];
}
return bytesToHexString(buffer);
}
function parseBluetoothBeacons00() {
var beaconStatus = bytes[index++];
var beaconType = beaconStatus & 0x03;
var rssiRaw = beaconStatus >> 2;
var rssi = 27 - rssiRaw * 2;
var beacon = void 0;
switch (beaconType) {
case 0x00:
beacon = {
type: 'ibeacon',
rssi: rssi,
uuid: substring(bytes, index, 2),
major: substring(bytes, index + 2, 2),
minor: substring(bytes, index + 4, 2)
};
index += 6;
return beacon;
case 0x01:
beacon = {
type: 'eddystone',
rssi: rssi,
instance: substring(bytes, index, 6)
};
index += 6;
return beacon;
case 0x02:
beacon = {
type: 'altbeacon',
rssi: rssi,
id1: substring(bytes, index, 2),
id2: substring(bytes, index + 2, 2),
id3: substring(bytes, index + 4, 2)
};
index += 6;
return beacon;
case 0x03:
beacon = {
type: 'fullbeacon',
rssi: rssi,
id1: substring(bytes, index, 2),
id2: substring(bytes, index + 2, 2),
id3: substring(bytes, index + 4, 2)
};
index += 6;
return beacon;
default:
return null;
}
}
function parseBluetoothBeacons01() {
var beaconStatus = bytes[index++];
var beaconType = beaconStatus & 0x03;
var rssiRaw = beaconStatus >> 2;
var rssi = 27 - rssiRaw * 2;
var beacon = void 0;
switch (beaconType) {
case 0x00:
beacon = {
type: 'ibeacon',
rssi: rssi,
uuid: substring(bytes, index, 16),
major: substring(bytes, index + 16, 2),
minor: substring(bytes, index + 18, 2)
};
index += 20;
return beacon;
case 0x01:
beacon = {
type: 'eddystone',
rssi: rssi,
namespace: substring(bytes, index, 10),
instance: substring(bytes, index + 10, 6)
};
index += 16;
return beacon;
case 0x02:
beacon = {
type: 'altbeacon',
rssi: rssi,
id1: substring(bytes, index, 16),
id2: substring(bytes, index + 16, 2),
id3: substring(bytes, index + 18, 2)
};
index += 20;
return beacon;
case 0x03:
beacon = {
type: 'fullbeacon',
rssi: rssi,
id1: substring(bytes, index, 16),
id2: substring(bytes, index + 16, 2),
id3: substring(bytes, index + 18, 2)
};
index += 20;
return beacon;
default:
return null;
}
}
function parseBluetoothBeacons02() {
var beaconStatus = bytes[index++];
var beaconType = beaconStatus & 0x03;
var slotMatch = beaconStatus >> 2 & 0x07;
var rssiRaw = bytes[index++] & 63;
var rssi = 27 - rssiRaw * 2;
var beacon = void 0;
switch (beaconType) {
case 0x00:
beacon = {
type: 'ibeacon',
rssi: rssi,
slot: slotMatch,
major: substring(bytes, index, 2),
minor: substring(bytes, index + 2, 2)
};
index += 4;
return beacon;
case 0x01:
beacon = {
type: 'eddystone',
rssi: rssi,
slot: slotMatch,
instance: substring(bytes, index, 6)
};
index += 6;
return beacon;
case 0x02:
beacon = {
type: 'altbeacon',
rssi: rssi,
slot: slotMatch,
id2: substring(bytes, index, 2),
id3: substring(bytes, index + 2, 2)
};
index += 4;
return beacon;
case 0x03:
beacon = {
type: 'fullbeacon',
rssi: rssi,
slot: slotMatch,
id2: substring(bytes, index, 2),
id3: substring(bytes, index + 2, 2)
};
index += 6;
return beacon;
default:
return null;
}
}
var headerByte = bytes[index++];
decoded.uplinkReasonButton = !!(headerByte & 1);
decoded.uplinkReasonMovement = !!(headerByte & 2);
decoded.uplinkReasonGpio = !!(headerByte & 4);
decoded.containsGps = !!(headerByte & 8);
decoded.containsOnboardSensors = !!(headerByte & 16);
decoded.containsSpecial = !!(headerByte & 32);
decoded.crc = bytes[index++];
decoded.batteryLevel = bytes[index++];
if (decoded.containsOnboardSensors) {
var sensorContent = bytes[index++];
decoded.sensorContent = {
containsTemperature: !!(sensorContent & 1),
containsLight: !!(sensorContent & 2),
containsAccelerometerCurrent: !!(sensorContent & 4),
containsAccelerometerMax: !!(sensorContent & 8),
containsWifiPositioningData: !!(sensorContent & 16),
buttonEventInfo: !!(sensorContent & 32),
containsExternalSensors: !!(sensorContent & 64),
containsBluetoothData: false
};
var hasSecondSensorContent = !!(sensorContent & 128);
if (hasSecondSensorContent) {
var sensorContent2 = bytes[index++];
decoded.sensorContent.containsBluetoothData = !!(sensorContent2 & 1);
decoded.sensorContent.containsRelativeHumidity = !!(sensorContent2 & 2);
decoded.sensorContent.containsAirPressure = !!(sensorContent2 & 4);
decoded.sensorContent.containsManDown = !!(sensorContent2 & 8);
decoded.sensorContent.containsTilt = !!(sensorContent2 & 16);
}
if (decoded.sensorContent.containsTemperature) {
decoded.temperature = toSignedShort(bytes[index++], bytes[index++]) / 100;
}
if (decoded.sensorContent.containsLight) {
var value = (bytes[index++] << 8) + bytes[index++];
var exponent = value >> 12 & 0xFF;
decoded.lightIntensity = ((value & 0x0FFF) << exponent) / 100;
}
if (decoded.sensorContent.containsAccelerometerCurrent) {
decoded.accelerometer = {
x: toSignedShort(bytes[index++], bytes[index++]) / 1000,
y: toSignedShort(bytes[index++], bytes[index++]) / 1000,
z: toSignedShort(bytes[index++], bytes[index++]) / 1000
};
}
if (decoded.sensorContent.containsAccelerometerMax) {
decoded.maxAccelerationNew = toSignedShort(bytes[index++], bytes[index++]) / 1000;
decoded.maxAccelerationHistory = toSignedShort(bytes[index++], bytes[index++]) / 1000;
}
if (decoded.sensorContent.containsWifiPositioningData) {
var wifiInfo = bytes[index++];
var numAccessPoints = wifiInfo & 7;
var wifiStatus = ((wifiInfo & 8) >> 2) + ((wifiInfo & 16) >> 3);
var containsSignalStrength = wifiInfo & 32;
var wifiStatusDescription = void 0;
switch (wifiStatus) {
case 0:
wifiStatusDescription = 'success';
break;
case 1:
wifiStatusDescription = 'failed';
break;
case 2:
wifiStatusDescription = 'no_access_points';
break;
default:
wifiStatusDescription = "unknown (" + wifiStatus + ")";
}
decoded.wifiInfo = {
status: wifiStatusDescription,
statusCode: wifiStatus,
accessPoints: []
};
for (var i = 0; i < numAccessPoints; i++) {
var macAddress = [bytes[index++].toString(16), bytes[index++].toString(16), bytes[index++].toString(16), bytes[index++].toString(16), bytes[index++].toString(16), bytes[index++].toString(16)];
var signalStrength = void 0;
if (containsSignalStrength) {
signalStrength = toSignedChar(bytes[index++]);
} else {
signalStrength = null;
}
decoded.wifiInfo.accessPoints.push({
macAddress: macAddress.join(':'),
signalStrength: signalStrength
});
}
}
if (decoded.sensorContent.containsExternalSensors) {
var type = bytes[index++];
switch (type) {
case 0x0A:
decoded.externalSensor = {
type: 'battery',
batteryA: toUnsignedShort(bytes[index++], bytes[index++]),
batteryB: toUnsignedShort(bytes[index++], bytes[index++])
};
break;
case 0x65:
decoded.externalSensor = {
type: 'detectSwitch',
value: bytes[index++]
};
break;
}
}
if (decoded.sensorContent.containsBluetoothData) {
var bluetoothInfo = bytes[index++];
var numBeacons = bluetoothInfo & 7;
var bluetoothStatus = bluetoothInfo >> 3 & 0x03;
var addSlotInfo = bluetoothInfo >> 5 & 0x03;
var bluetoothStatusDescription = void 0;
switch (bluetoothStatus) {
case 0:
bluetoothStatusDescription = 'success';
break;
case 1:
bluetoothStatusDescription = 'failed';
break;
case 2:
bluetoothStatusDescription = 'no_access_points';
break;
default:
bluetoothStatusDescription = "unknown (" + bluetoothStatus + ")";
}
decoded.bluetoothInfo = {
status: bluetoothStatusDescription,
statusCode: bluetoothStatus,
addSlotInfo: addSlotInfo,
beacons: []
};
for (var _i = 0; _i < numBeacons; _i++) {
var beacon;
switch (addSlotInfo) {
case 0x00:
beacon = parseBluetoothBeacons00();
break;
case 0x01:
beacon = parseBluetoothBeacons01();
break;
case 0x02:
beacon = parseBluetoothBeacons02();
break;
default:
return {errors: ['Invalid addSlotInfo type']};
}
if (beacon === null) {
return {errors: ['Invalid beacon type']};
}
decoded.bluetoothInfo.beacons.push(beacon);
}
}
if (decoded.sensorContent.containsRelativeHumidity) {
decoded.relativeHumidity = toUnsignedShort(bytes[index++], bytes[index++]) / 100;
}
if (decoded.sensorContent.containsAirPressure) {
decoded.airPressure = (bytes[index++] << 16) + (bytes[index++] << 8) + bytes[index++];
}
if (decoded.sensorContent.containsManDown) {
var manDownData = (bytes[index++]);
var manDownState = (manDownData & 0x0f);
var manDownStateLabel;
switch(manDownState) {
case 0x00:
manDownStateLabel = 'ok';
break;
case 0x01:
manDownStateLabel = 'sleeping';
break;
case 0x02:
manDownStateLabel = 'preAlarm';
break;
case 0x03:
manDownStateLabel = 'alarm';
break;
default:
manDownStateLabel = manDownState+'';
break;
}
decoded.manDown = {
state: manDownStateLabel,
positionAlarm: !!(manDownData & 0x10),
movementAlarm: !!(manDownData & 0x20)
};
}
if (decoded.sensorContent.containsTilt) {
decoded.tilt = {
currentTilt: toUnsignedShort(bytes[index++], bytes[index++]) / 100,
currentDirection: Math.round(bytes[index++] * (360/255)),
maximumTiltHistory: toUnsignedShort(bytes[index++], bytes[index++]) / 100,
DirectionHistory: Math.round(bytes[index++] * (360/255)),
};
}
}
if (decoded.containsGps) {
decoded.gps = {};
decoded.gps.navStat = bytes[index++];
decoded.gps.latitude = toSignedInteger(
bytes[index++],
bytes[index++],
bytes[index++],
bytes[index++]
) / 10000000;
decoded.gps.longitude = toSignedInteger(
bytes[index++],
bytes[index++],
bytes[index++],
bytes[index++]
) / 10000000;
decoded.gps.altRef = toUnsignedShort(bytes[index++], bytes[index++]) / 10;
decoded.gps.hAcc = bytes[index++];
decoded.gps.vAcc = bytes[index++];
decoded.gps.sog = toUnsignedShort(bytes[index++], bytes[index++]) / 10;
decoded.gps.cog = toUnsignedShort(bytes[index++], bytes[index++]) / 10;
decoded.gps.hdop = bytes[index++] / 10;
decoded.gps.numSvs = bytes[index++];
}
return decoded;
}
// Export function (for implementations and for testing)
if (typeof module !== 'undefined' && module.exports !== null) {
module.exports = Decoder;
}
Click on “Save changes”
4. Configure the TTN Integration to Ubidots
Go to Ubidots and log into your account.
Head to the “Devices” → “Plugins” section.
Create a new plugin by clicking the "+" sign at the top right corner.
Fill in the corresponding fields, such as the plugin’s name, token, etc.
Once the plugin is created, head to its “Decoder” section.
Copy the “HTTPs Endpoint URL”.
Scroll down to the “Decoding Function” section.
Delete all the code there and paste the following:
function format_payload(args){
var payload = args["uplink_message"]["decoded_payload"];
var ubidotsPayload = {};
var batteryLevel = payload["batteryLevel"] * (100 / 254);
ubidotsPayload["batteryLevel"] = batteryLevel;
ubidotsPayload["lightIntensity"] = payload["lightIntensity"];
ubidotsPayload["temperature"] = payload["temperature"];
if(payload["containsGps"] === true){
var lat = payload["gps"]["latitude"];
var lng = payload["gps"]["longitude"];
ubidotsPayload["position"] = {
"value":1,
"context":{
"lat":lat,
"lng":lng,
}
};
}
if(payload["sensorContent"]["containsTilt"] === true)
{
var tilt = payload["tilt"]["currentTilt"];
ubidotsPayload["tilt"] = tilt;
}
return ubidotsPayload;
}
module.exports = { format_payload };
Click on the “SAVE & MAKE LIVE” button to save the changes.
Head back to the TTN console.
Click the “Integrations” drop-down menu.
Click “Webhooks”.
Search for Ubidots among the list of different platforms.
Fill these fields in the following way:
Webhook ID: Set a meaningful name, such as “ioTracker-integration” or anything that you consider right for your purposes.
Plugin ID: It's the last portion of your Ubidots plugin’s “HTTPs Endpoint URL” after the last “/”. I.e., if your endpoint’s URL is “https://dataplugin.ubidots.com/api/web-hook/lN4s2dlb4IgPgpp4Xeoq02stXcE=”, then your plugin’s ID should be “lN4s2dlb4IgPgpp4Xeoq02stXcE=”
Ubidots Token: Your Ubidots Token.
Click on “Create Ubidots Webhook”.
5. Visualize Data on Ubidots
Head back to Ubidots → “Devices” section and you’ll be able to see a newly created device whose name is the “Device ID” that you set on TTN LNS, in this case, “io-tracker3” is displayed.