SODAQ Universal Tracker with T-Mobile NB-IoT and ThingsBoard
Introduction
Section titled “Introduction”SODAQ NB-IoT Trackers collect GPS location and sensor data and transmit it over the T-Mobile NB-IoT network. This guide walks through connecting a SODAQ NB-IoT Tracker to ThingsBoard PE using the IoT Creators integration, setting up a data converter, importing alarm rule chains, and visualizing data on a dashboard.
Prerequisites
Section titled “Prerequisites”- You have access to ThingsBoard PE or ThingsBoard Cloud with integration functionality enabled for your tenant.
- You have permissions to create integrations and data converters.
- You have an IoT Creators account with at least one project and a Starterkit / SIM card.
- SODAQ NB-IoT Tracker connected to the T-Mobile NB-IoT network
Step 1: Data Converter Configuration
Section titled “Step 1: Data Converter Configuration”The uplink data converter decodes the incoming hex-encoded payload from IoT Creators into the ThingsBoard telemetry format.
Input payload from IoT Creators:
{ "reports": [{ "serialNumber": "IMEI:0123456789", "timestamp": 1547035621977, "subscriptionId": "43524b52-b924-40f0-91f0-e7fa71dca87b", "resourcePath": "uplinkMsg/0/data", "value": "010145292a2bfbfc0000000000000000e6e3355c751a879de31e6535d10306005600d00402" }]}Decoded output:
{ "deviceName": "0123456789", "deviceType": "tracker", "telemetry": [{ "ts": 1547035622000, "values": { "batteryVoltage": 4.17, "temperature": 26, "latitude": 51.8233479, "longitude": 6.4042341, "altitude": 6, "speed": 86, "satellitesObserved": 208, "timeToFirstFix": 4 } }]}Key points:
- The IMEI from the incoming message becomes the device name in ThingsBoard.
- ThingsBoard automatically creates a device with type
trackerand a name equal to the IMEI. - Timestamp and sensor readings are decoded from the incoming hex string.
The following table shows each encoded field’s position and byte length in the hex string:
| Field | First byte | Byte length |
|---|---|---|
| ts | 16 | 4 |
| batteryVoltage | 20 | 1 |
| temperature | 21 | 1 |
| latitude | 22 | 4 |
| longitude | 26 | 4 |
| altitude | 30 | 2 |
| speed | 32 | 2 |
| satellitesObserved | 35 | 1 |
| timeToFirstFix | 36 | 1 |
Import Uplink Converter
Section titled “Import Uplink Converter”- Download sodaq_uplink_data_converter.json.
- Go to Integrations center ⇾ Data converters and click + Add data converter ⇾ Import converter.
- Drag and drop the downloaded
sodaq_uplink_data_converter.jsonfile. - Click Import.
Decoder function:
// Decode an IoT Creators uplink message.//// payload - incoming HTTP request body represented as an array of bytes.// IoT Creators sends a JSON object containing the "reports" array.// metadata - key-value pairs with additional integration metadata.
/** Decoder **/
// Convert the incoming byte array to a JSON object.var data = decodeToJson(payload);
// Each item in "reports" represents an uplink message received// from a registered IoT Creators device.var reports = data.reports;
// A valid uplink message must contain at least one report.if (reports == null || reports.length == 0) { raiseError("No reports found in the incoming payload.");}
// One converter execution must return data for one ThingsBoard entity.//// Use the IMEI from the first report as the ThingsBoard device name.// IoT Creators provides the serial number in the "IMEI:<value>" format,// therefore the "IMEI:" prefix is removed.var result = { deviceName: reports[0].serialNumber.replace("IMEI:", ""), deviceType: "SODAQ Universal Tracker",
// Each report will be added as a separate telemetry entry // with its own device timestamp. telemetry: []};
// Process all reports included in the IoT Creators request.for (var i = 0; i < reports.length; i++) { // The "value" field contains the tracker payload encoded // as a hexadecimal string. var value = reports[i].value;
// Decode the device timestamp from bytes 16-19. // The tracker stores multi-byte numeric values in little-endian order. // Convert seconds to milliseconds because ThingsBoard timestamps // are represented in milliseconds. var timestamp = Long.parseLong("" + parseHexToInt(value.substring(32, 40), false)) * 1000;
// Decode the battery voltage from byte 20. // The encoded value represents the voltage above 3 V // in increments of 0.01 V. var batteryVoltage = parseHexToInt(value.substring(40, 42), false) / 100.0 + 3.0;
// Decode the temperature from byte 21. var temperature = parseHexToInt(value.substring(42, 44), false);
// Decode latitude from bytes 22-25. // The raw integer is divided by 10,000,000 to obtain decimal degrees. var latitude = parseHexToInt(value.substring(44, 52), false) / 10000000.0;
// Decode longitude from bytes 26-29. // The raw integer is divided by 10,000,000 to obtain decimal degrees. var longitude = parseHexToInt(value.substring(52, 60), false) / 10000000.0;
// Decode altitude from bytes 30-31. var altitude = parseHexToInt(value.substring(60, 64), false);
// Decode speed from bytes 32-33. var speed = parseHexToInt(value.substring(64, 68), false);
// Decode the number of observed GNSS satellites from byte 34. var satellitesObserved = parseHexToInt(value.substring(68, 70), false);
// Decode the GNSS time to first fix from byte 35. var timeToFirstFix = parseHexToInt(value.substring(70, 72), false);
// Create the telemetry values associated with the decoded timestamp. var values = { batteryVoltage: batteryVoltage, temperature: temperature, speed: speed, satellitesObserved: satellitesObserved, timeToFirstFix: timeToFirstFix };
// Do not send location coordinates when the tracker reports zero, // because zero may indicate that no valid GNSS position is available. if (latitude != 0) { values.latitude = latitude; }
if (longitude != 0) { values.longitude = longitude; }
if (altitude != 0) { values.altitude = altitude; }
// Add the decoded report as timestamped telemetry. result.telemetry.push({ ts: timestamp, values: values });}
return result;// Decode an IoT Creators uplink message.//// payload - incoming HTTP request body represented as an array of bytes.// IoT Creators sends a JSON object containing the "reports" array.// metadata - key-value pairs with additional integration metadata.
/** Decoder **/
// Convert the incoming byte array to a JSON object.var data = decodeToJson(payload);
// Each item in "reports" represents an uplink message received// from a registered IoT Creators device.var reports = data.reports;
// A valid uplink message must contain at least one report.if (reports == null || reports.length === 0) { throw new Error("No reports found in the incoming payload.");}
// One converter execution must return data for one ThingsBoard entity.//// Use the IMEI from the first report as the ThingsBoard device name.// IoT Creators provides the serial number in the "IMEI:<value>" format,// therefore the "IMEI:" prefix is removed.var result = { deviceName: reports[0].serialNumber.replace("IMEI:", ""), deviceType: "SODAQ Universal Tracker",
// Each report will be added as a separate telemetry entry // with its own device timestamp. telemetry: []};
// Process all reports included in the IoT Creators request.for (var i = 0; i < reports.length; i++) { // The "value" field contains the tracker payload encoded // as a hexadecimal string. var value = reports[i].value;
// Decode the device timestamp from bytes 16-19. // The tracker stores multi-byte numeric values in little-endian order. // Convert seconds to milliseconds because ThingsBoard timestamps // are represented in milliseconds. var timestamp = littleEndianHexToInt(value.substring(32, 40)) * 1000;
// Decode the battery voltage from byte 20. // The encoded value represents the voltage above 3 V // in increments of 0.01 V. var batteryVoltage = littleEndianHexToInt(value.substring(40, 42)) / 100 + 3;
// Decode the temperature from byte 21. var temperature = littleEndianHexToInt(value.substring(42, 44));
// Decode latitude from bytes 22-25. // The raw integer is divided by 10,000,000 to obtain decimal degrees. var latitude = littleEndianHexToInt(value.substring(44, 52)) / 10000000;
// Decode longitude from bytes 26-29. // The raw integer is divided by 10,000,000 to obtain decimal degrees. var longitude = littleEndianHexToInt(value.substring(52, 60)) / 10000000;
// Decode altitude from bytes 30-31. var altitude = littleEndianHexToInt(value.substring(60, 64));
// Decode speed from bytes 32-33. var speed = littleEndianHexToInt(value.substring(64, 68));
// Decode the number of observed GNSS satellites from byte 34. var satellitesObserved = littleEndianHexToInt(value.substring(68, 70));
// Decode the GNSS time to first fix from byte 35. var timeToFirstFix = littleEndianHexToInt(value.substring(70, 72));
// Create the telemetry values associated with the decoded timestamp. var values = { batteryVoltage: batteryVoltage, temperature: temperature, speed: speed, satellitesObserved: satellitesObserved, timeToFirstFix: timeToFirstFix };
// Do not send location coordinates when the tracker reports zero, // because zero may indicate that no valid GNSS position is available. if (latitude !== 0) { values.latitude = latitude; }
if (longitude !== 0) { values.longitude = longitude; }
if (altitude !== 0) { values.altitude = altitude; }
// Add the decoded report as timestamped telemetry. result.telemetry.push({ ts: timestamp, values: values });}
/** Helper functions **/
// Convert a little-endian hexadecimal string to an integer.//// For example, "78563412" is converted to "12345678"// before it is parsed as a hexadecimal number.function littleEndianHexToInt(hex) { var bytes = hex.match(/../g);
if (bytes == null) { throw new Error("Invalid hexadecimal value: " + hex); }
return parseInt(bytes.reverse().join(""), 16);}
// Convert the incoming byte array to a UTF-8-compatible string.function decodeToString(payload) { return String.fromCharCode.apply(String, payload);}
// Parse the incoming JSON request body.function decodeToJson(payload) { return JSON.parse(decodeToString(payload));}
return result;Step 2: Integration Configuration
Section titled “Step 2: Integration Configuration”- Go to Integrations center ⇾ Integrations and click + Add integration.
- Basic settings:
- Set Integration type to iotcreators.com (T-Mobile – IoT CDP).
- Enter a Name, or keep the default
iotcreators.com (T-Mobile – IoT CDP) integration. - Leave Enable integration and Allow create devices or assets enabled so that devices are created automatically when data is received for the first time.
- Click Next.
- Uplink data converter:
- Select existing — choose the previously created
SODAQ Uplink data converterfrom the list. - Click Next.
- Select existing — choose the previously created
- Connection:
- Base URL — pre-filled with your ThingsBoard instance URL (for example,
https://thingsboard.cloud). - Copy the HTTP endpoint URL — you will paste this into the IoT Creators portal as the callback address so the platform forwards uplinks to ThingsBoard.
- Base URL — pre-filled with your ThingsBoard instance URL (for example,
- Click Add to finish creating the integration.
For more information about the IoT Creators integration in ThingsBoard, see the IoT Creators integration documentation.
Step 3: Post Telemetry and Verify the Integration
Section titled “Step 3: Post Telemetry and Verify the Integration”Before configuring IoT Creators, verify that ThingsBoard is correctly configured by sending a test message with cURL.
-
Download the test data file: telemetry-data.json.
-
Run the following command, replacing
$HTTP_ENDPOINT_URLwith the copied endpoint URL from the integration:Terminal window curl -v -X POST -d @telemetry-data.json $HTTP_ENDPOINT_URL --header "Content-Type:application/json"
A new device should be created in ThingsBoard:
Go to Entities ⇾ Devices — device 0123456789 is provisioned automatically on the first uplink. Open the Latest telemetry tab to verify decoded data such as location, battery voltage, and other sensor values.
Step 4: IoT Creators Callback Configuration
Section titled “Step 4: IoT Creators Callback Configuration”In the IoT Creators portal, configure the callback URL to the HTTP endpoint URL from Step 2. Use the image below as a reference:
Step 5: Check Integration Debug Events
Section titled “Step 5: Check Integration Debug Events”To trace the message through the integration, go to Integrations center ⇾ Integrations, click iotcreators.com (T-Mobile – IoT CDP) integration, open the Events tab, and set Event type to Debug. Click … in the Message column to see the reports received by the integration.
Step 6: Rule Chain Import
Section titled “Step 6: Rule Chain Import”This guide uses a modified Root Rule Chain and a new Tracker Alarms rule chain. The rule chain forwards telemetry to the Tracker Alarms chain, which evaluates individual alarm threshold parameters configured in the dashboard.
Download tracker-alarms.json and import it as the Tracker Alarms rule chain.
Step 7: Dashboard Import
Section titled “Step 7: Dashboard Import”Download sodaq-dashboard.json and import it in Dashboards. See the dashboard import instructions.
After importing the dashboard, navigate to the Tracker details state and set the alarm threshold values:
- Max Speed
- Min Voltage
- Min Temperature
- Max Temperature
Once the rule chains and dashboard are configured, trigger the device to post real data and verify that the integration and rule chains work as expected.
Advanced Configuration (Optional)
Section titled “Advanced Configuration (Optional)”The following sections explain how the rule chains work and how to recreate them from scratch. These steps are optional — beginners can skip this section.
Security
Section titled “Security”You can add additional HTTP headers with unique parameters to secure the integration. Add a custom header (like MY-INTEGRATION-AUTH-HEADER with a random string value) to both the integration configuration (Step 2) and the IoT Creators callback configuration (Step 4). The header values must match for data to flow correctly.
Message Flow
Section titled “Message Flow”The Tracker Alarms rule chain uses the following nodes:
- Node A: Originator attributes — adds Message Originator Attributes (client, shared, and server scope) and the latest telemetry values into message metadata.
- Nodes B, C, D, E: Filter Script — evaluate threshold conditions. Return
trueif the condition is met,falseotherwise. - Nodes F, H, J, L: Create alarm — create or update an alarm when the corresponding filter script returns
true. - Nodes G, I, K, M: Clear alarm — clear the alarm when the corresponding filter script returns
false. - Node O: Rule Chain — forwards incoming messages to the Tracker Alarms rule chain.
Create the Tracker Alarms Rule Chain
Section titled “Create the Tracker Alarms Rule Chain”-
Go to Rule Chains ⇾ Add new Rule Chain. Enter the name Tracker Alarms and click Add.
-
Open the rule chain and click Edit. Add 13 nodes as described below.
-
Add Node A: Originator attributes and connect it to the Input node. This node fetches shared scope attributes set from the dashboard.
Configure it with the following shared attributes:
maxTemperature,minTemperature,maxSpeed,minVoltage. Set the name to Fetch Limit telemetry. -
Add four Filter Script nodes (B, C, D, E) and connect each to Node A with relation type Success:
Node Name Script B Validate Max temperature return msg.temperature > metadata.shared_maxTemperature;C Validate Min temperature return msg.temperature < metadata.shared_minTemperature;D Validate Max speed return msg.speed > metadata.shared_maxSpeed;E Validate Min voltage return msg.batteryVoltage < metadata.shared_minVoltage; -
Add four Create alarm nodes (F, H, J, L) and connect each to the corresponding Filter Script node with relation type True:
Node Name Alarm type Details function F Max Temperature Max Temperature var details = {}; details.value = msg.temperature; if (metadata.prevAlarmDetails) { details = JSON.parse(metadata.prevAlarmDetails); } return details;H Min temperature Min temperature var details = {}; details.value = msg.temperature; if (metadata.prevAlarmDetails) { details = JSON.parse(metadata.prevAlarmDetails); } return details;J Max Speed Max Speed var details = {}; details.value = msg.speed; if (metadata.prevAlarmDetails) { details = JSON.parse(metadata.prevAlarmDetails); } return details;L Min Voltage Min Voltage var details = {}; details.value = msg.batteryVoltage; if (metadata.prevAlarmDetails) { details = JSON.parse(metadata.prevAlarmDetails); } return details; -
Add four Clear alarm nodes (G, I, K, M) and connect each to the corresponding Filter Script node with relation type False:
Node Name Alarm type Details function G Clear Max Temperature Max Temperature var details = {}; if (metadata.prevAlarmDetails) { details = JSON.parse(metadata.prevAlarmDetails); } details.clearedValue = msg.temperature; return details;I Clear Min temperature Min temperature var details = {}; if (metadata.prevAlarmDetails) { details = JSON.parse(metadata.prevAlarmDetails); } details.clearedValue = msg.temperature; return details;K Clear Max Speed Max Speed var details = {}; if (metadata.prevAlarmDetails) { details = JSON.parse(metadata.prevAlarmDetails); } details.clearedValue = msg.speed; return details;M Clear Min Voltage Min Voltage var details = {}; if (metadata.prevAlarmDetails) { details = JSON.parse(metadata.prevAlarmDetails); } details.clearedValue = msg.batteryVoltage; return details; -
The completed Tracker Alarms rule chain:
Modify the Root Rule Chain
Section titled “Modify the Root Rule Chain”Add two nodes to the existing Root Rule Chain:
-
Add a Filter Script node and connect it to the Save Timeseries node with relation type Success. Set the name to Tracker filter and enter the following script:
return metadata.deviceType === 'tracker'; -
Add a Rule Chain node and connect it to the Filter Script node with relation type True. Set the name to Tracker Alarms and point it to the Tracker Alarms rule chain.
-
The completed Root Rule Chain: