The Things Stack Community Integration
The Things Stack Community Integration connects ThingsBoard to The Things Network (TTN), the public community LoRaWAN network that runs on The Things Stack. ThingsBoard connects to the MQTT server of your The Things Stack application as an MQTT client, subscribes to the uplinks of its devices, and passes each one to an uplink data converter, which turns it into telemetry and attributes. For downlink, ThingsBoard publishes messages to the downlink topic of the device on the same connection.
Use The Things Stack Community Integration when:
- Your LoRaWAN devices are registered in an application on The Things Network, for example on the
eu1,nam1, orau1cluster. - You want ThingsBoard to create a device for every end device of the application, with no extra setup per device.
- You need to send downlinks from ThingsBoard to those devices, for example when a shared attribute changes.
For a private The Things Industries deployment, use the The Things Stack Industries integration instead.
Architecture
Section titled “Architecture”- Connect and subscribe. The integration connects to the MQTT server of your cluster, for example
eu1.cloud.thethings.network:8883, with the application ID and an API key, and subscribes tov3/+/devices/+/up. - Receive data. A device sends an uplink through a gateway. The Things Stack publishes it as a JSON message to the uplink topic of the device.
- Decode the payload. The uplink converter takes the device payload and the network details out of the message, such as the DevEUI, the frame port, and the signal quality. The decoder turns the payload into telemetry and attributes. If Allow create devices or assets is on, ThingsBoard creates the device on its first uplink.
- Send a downlink (optional). The Rule Engine passes a message to the integration. The downlink converter encodes it, and ThingsBoard publishes it right away to
v3/{application ID}@ttn/devices/{device ID}/down/push. The Things Stack queues it and sends it to the device.
Prerequisites
Section titled “Prerequisites”- Access to ThingsBoard Cloud with integrations enabled for your tenant. ThingsBoard must be able to open an outbound connection to the MQTT server of The Things Stack, on port
8883. - Permission to create integrations and data converters, and to edit rule chains for downlink.
- A The Things Network account with access to The Things Stack Console.
- A LoRaWAN device and gateway are optional: this guide simulates uplinks in the console.
Prepare The Things Stack
Section titled “Prepare The Things Stack”In The Things Stack, devices belong to an application. The integration reads the uplinks of every device in one application, and authenticates with an API key of that application.
Here you create the application, its API key, and a test end device. Open the console of the cluster you use, for example Europe 1 (eu1), and keep the cluster ID: you need it for the integration.
Register an Application
Section titled “Register an Application”- Sign in to The Things Stack Console and select your cluster.
- Go to Applications and click + Add application.
- Enter an Application ID, for example
my-thingsboard. It must be unique on the cluster. - Click Create application. The Application overview page opens.
Create an API Key
Section titled “Create an API Key”ThingsBoard uses the API key as the MQTT password.
- In the application, go to API keys in the left menu and click + Add API key.
- Enter a Name, for example
ThingsBoard. - Select Grant individual rights and select:
- Read application traffic (uplink and downlink)
- Write downlink application traffic
- Click Create API key.
- Copy the key and store it securely. The console shows it only once.
Register an End Device
Section titled “Register an End Device”The guide registers a test device with ABP (Activation by Personalization), so you can simulate uplinks right away, without a join. For real devices, use OTAA (Over-the-Air Activation): see the The Things Stack device registration guide.
- In the application, go to End devices and click + Register end device.
- Under Input method, select Enter end device specifics manually.
- Select the Frequency plan, for example
Europe 863-870 MHz (SF9 for RX2 - recommended), the LoRaWAN version, for exampleLoRaWAN Specification 1.0.0, and the Regional Parameters version, for exampleTS002 Technical Specification 1.0.0. - Click Show advanced activation, LoRaWAN class and cluster settings and set Activation mode to Activation by personalization (ABP).
- Under Provisioning information, click Generate for DevEUI, Device address, AppSKey, and NwkSKey. ThingsBoard names the device after the DevEUI, so do not leave it empty.
- Enter the End device ID, for example
thermostat-a. ThingsBoard stores it as the device label and uses it to address downlinks. - Click Register end device.
Set Up The Things Stack Community Integration
Section titled “Set Up The Things Stack Community Integration”Create the uplink converter, then the integration with the MQTT credentials of your application.
Create the Uplink Data Converter
Section titled “Create the Uplink Data Converter”The Things Stack Community Integration uses the Typed uplink converter. ThingsBoard selects it for this integration type, and you cannot change it later. The converter takes the device payload and the network details out of The Things Stack message, then runs your decoder on the payload only. The device name, profile, and label come from patterns in the converter settings, not from the decoder. See Typed uplink converter.
Write the decoder yourself as below, or take a ready-made one for your device model from the Library.
-
Go to Integrations ⇾ Data converters and click + Add data converter ⇾ Create new converter.
-
Converter type — keep Uplink.
-
Integration type — select The Things Stack Community.
-
Keep Create new selected.
-
Name — enter a name, for example
TTN Uplink Converter. -
Main decoding configuration:
- Entity type — keep Device.
- Device name — keep
Device $eui.$euiis replaced with the DevEUI from the message, so the example device is namedDevice 70B3D57ED80054FE. You can use any key from Decoder Inputs as$key, for example$deviceId. - The editor holds the default decoder: the body of
function payloadDecoder(payload, metadata). Keep it. It reads a serial number, battery level, temperature, and saturation from fixed byte positions. Use the TBEL / JS toggle to switch to JavaScript.
/*** Decodes the incoming payload and returns a structured object containing telemetry data and attributes.** @param {byte[]} input - The raw payload received as an array of bytes.* @returns {Object} output - The structured output with decoded telemetry and attributes.*/function decodePayload(input) {// Initialize the output object with empty attributes and telemetry for clarity.var result = { attributes: {}, telemetry: {}};// Detect input type and normalize to byte arrayvar bytes;if (metadata.payloadFormat == 'JSON') {// payloadFormat is 'JSON' when The Things Network payload formatters are configured on the application or device level.// In that case, decoded_payload is a JSON object produced by your formatter.// This template assumes the formatter returns { "bytes": [...] } with the raw byte array.// If your formatter returns pre-decoded values in a different structure,// update this section to extract the values directly from 'parsed' instead.var parsed = decodeToJson(input);if (parsed.bytes == null) {throw new Error("Payload formatter output does not contain a 'bytes' field. " +"Update this decoder to match your formatter's output structure.");}bytes = parsed.bytes;} else {// payloadFormat is 'BINARY' when no payload formatter is configured — raw bytes from frm_payload are passed directly.bytes = input;}// Decode serial number (SN) from the first 4 bytes of the payload.// Press '?' icon in the top right corner to learn more about built in helper functions and capabilities.result.attributes.sn = parseBytesToInt(bytes, 0, 4);// Extract the timestamp from metadata (represented in milliseconds).var timestamp = metadata.ts; // ts is the timestamp parsed from the incoming message's time, or returns the current time if it cannot be parsed.// Combine the timestamp with decoded values and add it to the telemetry.result.telemetry = {ts: timestamp,values: {// Decode battery level from the 5th byte of the payload.battery: parseBytesToInt(bytes, 4, 1),// Decode temperature from the 6th and 7th bytes of the payload (divided by 100).temperature: parseBytesToInt(bytes, 5, 2) / 100.0,// Decode saturation from the 8th byte of the payload.saturation: parseBytesToInt(bytes, 7, 1)}};// Return the fully constructed output object.return result;// Same logic, less code:// return {// attributes: {// sn: parseBytesToInt(bytes, 0, 4)// },// telemetry: {// ts: convertDateToTimestamp(extractDateFromMetadata()),// values: {// battery: parseBytesToInt(bytes, 4, 1),// temperature: parseBytesToInt(bytes, 5, 2) / 100.0,// saturation: parseBytesToInt(bytes, 7, 1)// }// }// };}var result = decodePayload(payload);// Uncomment this code block to overwrite values set in the main configuration window. Useful if you extract device/asset/customer/group names from the payload;// result.type = 'DEVICE'; // Entity type allows you to choose type of created entity. Can be 'DEVICE' or 'ASSET'.// result.name = 'Temperature Sensor'; // Device or asset name (the value must be unique)// result.profile = 'IndustrialSensorProfile'; // Device or asset profile name.// result.customer = 'MyCustomer'; // If customer is not null - created entity will be assigned to customer with such name.// result.group = 'SensorsGroup'; // If group is not null - created entity will be added to the entity group with such name.// Return the final result object.return result;/*** Decodes the incoming payload and returns a structured object containing telemetry data and attributes.** @param {number[]} input - The raw payload received as an array of bytes.* @returns {Object} output - The structured output with decoded telemetry and attributes.*/function decodePayload(input) {// Initialize the output object with empty attributes and telemetry for clarity.var result = { attributes: {}, telemetry: {}};// Detect input type and normalize to byte array.var bytes;if (metadata.payloadFormat == 'JSON') {// payloadFormat is 'JSON' when The Things Network payload formatters are configured on the application or device level.// In that case, decoded_payload is a JSON object produced by your formatter.// This template assumes the formatter returns { "bytes": [...] } with the raw byte array.// If your formatter returns pre-decoded values in a different structure,// update this section to extract the values directly from 'parsed' instead.var parsed = decodeToJson(input);if (parsed.bytes == null) {throw new Error("Payload formatter output does not contain a 'bytes' field. " +"Update this decoder to match your formatter's output structure.");}bytes = parsed.bytes;} else {// payloadFormat is 'BINARY' when no payload formatter is configured — raw bytes from frm_payload are passed directly.bytes = input;}// Decode serial number (SN) from the first 4 bytes of the payload.// Press '?' icon in the top right corner to learn more about built in helper functions and capabilities.result.attributes.sn = parseBytesToInt(bytes, 0, 4);// Extract the timestamp from metadata (represented in milliseconds).var timestamp = metadata.ts; // ts is the timestamp parsed from the incoming message's time, or returns the current time if it cannot be parsed.// Initialize an object to store decoded key/value telemetry data.var values = {};// Decode battery level from the 5th byte of the payload.values.battery = parseBytesToInt(bytes, 4, 1);// Decode temperature from the 6th and 7th bytes of the payload (divided by 100).values.temperature = parseBytesToInt(bytes, 5, 2) / 100.0;// Decode saturation from the 8th byte of the payload.values.saturation = parseBytesToInt(bytes, 7, 1);// Combine the timestamp with values and add it to the telemetry.result.telemetry = {ts: timestamp,values: values};// Return the fully constructed output object.return result;// Same logic, less code:// return {// attributes: {// sn: parseBytesToInt(bytes, 0, 4)// },// telemetry: {// ts: convertDateToTimestamp(extractDateFromMetadata()),// values: {// battery: parseBytesToInt(bytes, 4, 1),// temperature: parseBytesToInt(bytes, 5, 2) / 100.0,// saturation: parseBytesToInt(bytes, 7, 1)// }// }// };}var result = decodePayload(payload);// Uncomment this code block to overwrite values set in the main configuration window. Useful if you extract device/asset/customer/group names from the payload;// result.type = 'DEVICE'; // Entity type allows you to choose type of created entity. Can be 'DEVICE' or 'ASSET'.// result.name = 'Temperature Sensor'; // Device or asset name (the value must be unique)// result.profile = 'IndustrialSensorProfile'; // Device or asset profile name.// result.customer = 'MyCustomer'; // If customer is not null - created entity will be assigned to customer with such name.// result.group = 'SensorsGroup'; // If group is not null - created entity will be added to the entity group with such name.// Return the final result object.return result;/*** Converts a byte array to a string and parses it as JSON.** @param {number[]} payload - The array of bytes.* @returns {Object} - The parsed JSON object.*/function decodeToJson(payload) {return JSON.parse(String.fromCharCode.apply(String, payload));}/*** Parse a slice of bytes from an array into an integer (big-endian).** @param {number[]} input - The array of bytes.* @param {number} offset - The starting index.* @param {number} length - The number of bytes to convert.* @returns {number} - The resulting integer.*/function parseBytesToInt(input, offset, length) {var result = 0;for (var i = offset; i < offset + length; i++) {result = (result << 8) | (input[i] & 0xFF);}return result;}The decoder expects this byte layout:
Bytes Key Stored as Value 0–3 snattribute Serial number 4 batterytelemetry Battery level 5–6 temperaturetelemetry Raw value ÷ 100 7 saturationtelemetry Saturation -
Advanced decoding parameters — keep the defaults:
- Device profile name —
$applicationId. The device gets a profile named after the application, for examplemy-thingsboard. - Device label —
$deviceId. The device label is the end device ID, for examplethermostat-a. The downlink in this guide reads it. - Customer name and Device group name — empty.
- Telemetry —
fCnt,data,decoded,latitude,longitude,altitude,rssi,snr, andchannel. - Attributes —
eui,devAddr,fPort,bandwidth,spreadingFactor,codeRate,frequency,brandId,modelId,hardwareVersion,firmwareVersion,attributes, andtenantId. - Update only keys list — the same keys as Attributes. They are stored only when their value changes.
The Telemetry and Attributes lists add network keys from Decoder Inputs to the decoder output. They do not filter it: every key the decoder returns is stored. A key that the message does not carry, or that the decoder already returns, is skipped.
- Device profile name —
-
Click Test payload decoder and check the result (see Test the Decoder).
-
Click Add.
To adapt:
- Byte layout — change the offset and length in each
parseBytesToInt()call, and rename or add keys to match your device. - Payload formatter — if The Things Stack decodes the payload,
metadata.payloadFormatisJSONandpayloadholds the formatter result. Read the values fromparsedinstead ofparsed.bytes, for examplevalues: { temperature: parsed.temperature }. - Device name — change the Device name pattern, for example to
$deviceId, or setresult.namein the decoder. - Network keys — remove keys from Telemetry or Attributes that you do not need, for example
data, which stores the raw payload as a Base64 string.
Use the Converters Library
The Converters library holds ready-made decoders for devices of vendors such as Milesight, Elsys, Decentlab, and Bosch.
- Go to Integrations ⇾ Data converters and click + Add data converter ⇾ Create new converter.
- Converter type — keep Uplink.
- Integration type — select The Things Stack Community.
- Click Library.
- Vendor — select the manufacturer of your device.
- Model — select the device model. ThingsBoard fills in the Name from the vendor and model, and loads the decoder.
- Review the decoder and the Advanced decoding parameters, then click Add.
A library decoder is written for one device model and its payload layout. Some library entries are written as Generic converters: they get the whole The Things Stack message as payload, parse it themselves, and show only Update only keys list in Advanced decoding parameters. Their test button is Test decoder function. To test such a decoder, paste the full uplink message from Decoder Inputs as the payload.
Decoder Inputs
The Things Stack publishes each uplink as a JSON message. This one comes from the example device, with the payload 00 BC 61 4E 5F 09 29 50:
{ "end_device_ids": { "device_id": "thermostat-a", "application_ids": { "application_id": "my-thingsboard" }, "dev_eui": "70B3D57ED80054FE", "dev_addr": "260B1234" }, "correlation_ids": ["as:up:01JB8Y2P3Q4R5S6T7V8W9X0YZA"], "received_at": "2026-10-22T09:15:30.512Z", "uplink_message": { "f_port": 1, "f_cnt": 12, "frm_payload": "ALxhTl8JKVA=", "rx_metadata": [ { "gateway_ids": { "gateway_id": "my-gateway" }, "rssi": -62, "channel_rssi": -62, "snr": 9.5 } ], "settings": { "data_rate": { "lora": { "bandwidth": 125000, "spreading_factor": 7 } }, "frequency": "868100000" }, "received_at": "2026-10-22T09:15:30.498Z" }}The converter passes the decoder two variables:
| Variable | Type | Content |
|---|---|---|
payload |
byte array | The device payload. Without a payload formatter, the Base64-decoded uplink_message.frm_payload. With a formatter, the JSON of uplink_message.decoded_payload. Empty if the message has neither. |
metadata |
object | The network keys below, plus payloadFormat, ts, integrationName, the MQTT topic, and the key–value pairs from the integration’s Metadata field. Integration keys replace network keys with the same name. |
Network keys that the converter reads from the message. A key is missing if its field is missing or null:
| Key | Source field | Example |
|---|---|---|
deviceId |
end_device_ids.device_id |
thermostat-a |
applicationId |
end_device_ids.application_ids.application_id |
my-thingsboard |
eui |
end_device_ids.dev_eui |
70B3D57ED80054FE |
joinEui |
end_device_ids.join_eui |
— |
devAddr |
end_device_ids.dev_addr |
260B1234 |
fPort |
uplink_message.f_port |
1 |
fCnt |
uplink_message.f_cnt |
12 |
data |
uplink_message.frm_payload |
ALxhTl8JKVA= |
decoded |
uplink_message.decoded_payload |
— |
rssi, snr |
The rx_metadata entry with the highest rssi |
-62, 9.5 |
bandwidth |
uplink_message.settings.data_rate.lora.bandwidth |
125000 |
spreadingFactor |
uplink_message.settings.data_rate.lora.spreading_factor |
7 |
frequency |
uplink_message.settings.frequency |
868100000 |
latitude, longitude, altitude |
uplink_message.locations.user |
— |
brandId, modelId, hardwareVersion, firmwareVersion |
uplink_message.version_ids |
— |
tenantId, netId, clusterId |
uplink_message.network_ids |
— |
receivedAt, uplinkMessageReceivedAt |
received_at, uplink_message.received_at |
2026-10-22T09:15:30.512Z |
simulated |
simulated |
true for console simulations |
The converter also reads correlationIds, rxMetadata, sessionKeyId, codeRate, dataRateIndex, consumedAirtime, attributes, and the other fields of uplink_message. Check the Metadata of a converter event for the full list.
payloadFormat—BINARYwhenpayloadholds the raw bytes,JSONwhen it holds the formatter result.ts— the time ofuplink_message.received_atin milliseconds, for example1792660530498. If it is missing,received_at, and if that is missing too, the current time.
Test the Decoder
- Click Test payload decoder below the editor.
- Click Updated RAW payload, paste the message from Decoder Inputs, and apply it. ThingsBoard unwraps it: the payload becomes
ALxhTl8JKVA=with Payload content type Binary (Base64), and Metadata fills with the network keys. - Click Test.
Decoder output shows the object returned by the decoder:
{ "attributes": { "sn": 12345678 }, "telemetry": { "ts": 1792660530498, "values": { "battery": 95, "temperature": 23.45, "saturation": 80 } }}Converter output shows the data ThingsBoard stores: the device name Device 70B3D57ED80054FE, the profile my-thingsboard, the label thermostat-a, the decoded keys, and the network keys from the Telemetry and Attributes lists, such as rssi, snr, fCnt, eui, and fPort. If the output is empty or shows an error, fix the decoder and test again.
Create the Integration
Section titled “Create the Integration”- Go to Integrations ⇾ Integrations and click + Add integration.
- Basic settings:
- Integration type — select The Things Stack Community.
- Name — keep the default
The Things Stack Community integrationor enter your own. - Enable integration and Allow create devices or assets — keep both on, so the device is created on its first uplink.
- Click Next.
- Uplink data converter:
- Click Select existing and choose
TTN Uplink Converter. - Alternatively, click Create new to write a decoder here, or Library to take one from the Converters library.
- Click Next.
- Click Select existing and choose
- Downlink data converter:
- Click Skip. Add a downlink converter later if you need to send downlinks.
- Connection:
- Host type — keep Region.
- Region — the ID of your cluster, for example
eu1. ThingsBoard adds.cloud.thethings.network, shown next to the field. - Port — keep
8883. - Username — the application ID followed by
@ttn, for examplemy-thingsboard@ttn. - Password — the API key.
- Keep Enable SSL and Use API v3 on.
- Leave the other parameters at their defaults. See Connection Settings for details.
- Optionally, click Check connection to test the connection to the MQTT server.
- Click Add.
The integration connects and subscribes right away.
Connection Settings
Section titled “Connection Settings”| Parameter | Default | Description |
|---|---|---|
| Host type | Region | Region builds the host from the cluster ID. Custom takes the full host name in Custom host, for example for a self-hosted The Things Stack. |
| Region | — | For Region. The cluster ID, for example eu1, nam1, or au1. The host is {Region}.cloud.thethings.network. Required. |
| Custom host | — | For Custom. The host name of the MQTT server. Required. On ThingsBoard Cloud, it must be a public address. |
| Port | 8883 |
Port of the MQTT server, from 1 to 65535. The Things Stack uses 8883 with TLS and 1883 without it. |
| Username and Password | — | In Credentials. The only credentials type is Basic. Username: {application ID}@ttn. Password: an API key of the application. Both are required. |
| Enable SSL | On | Connects over TLS. Keep it on with port 8883, and turn it off only for port 1883. |
| Use API v3 | On | Uses the topics of The Things Stack v3: uplinks on v3/+/devices/+/up, downlinks on v3/{Username}/devices/${devId}/down/push. Off switches to the legacy v2 topics +/devices/+/up and {Username}/devices/${devId}/down. |
| Topic filters | v3/+/devices/+/up, QoS 0 |
Read-only. Set by Use API v3. The + levels match every application and device that the API key can read. |
| Execute remotely | Off | Runs the integration as a separate process outside the ThingsBoard cluster. ThingsBoard generates an Integration key and Integration secret for it. See Remote Integration. |
| Protocol version | MQTT 3.1.1 |
In Advanced settings. MQTT version of the connection. MQTT 5 is not offered for this integration type. |
| Max bytes in message | 32368 |
In Advanced settings. Largest MQTT message the client accepts, in bytes. |
| Connection timeout (sec) | 10 |
In Advanced settings. Time to wait for the server to accept the connection, from 1 to 200 seconds. |
| Downlink topic pattern | v3/{Username}/devices/${devId}/down/push |
In Advanced settings. Read-only. Built from Username, for example v3/my-thingsboard@ttn/devices/${devId}/down/push. Each ${key} is replaced with the key value from the metadata of the downlink converter result. |
| Description | — | In Advanced settings. Free-text description. |
| Metadata | — | In Advanced settings. Key–value pairs added to the metadata of every message from this integration. |
The integration always connects with a clean session.
Test Uplink
Section titled “Test Uplink”Simulate an uplink in The Things Stack Console, then confirm that ThingsBoard decoded it and created the device.
Send a Test Uplink
Section titled “Send a Test Uplink”- In The Things Stack Console, open the application, go to End devices, and click
thermostat-a. - Open the Messaging tab and select Simulate uplink.
- Keep FPort at
1. - In Payload, enter
00BC614E5F092950. - Click Simulate uplink. A message confirms that the uplink was sent.
The payload holds the values that the decoder reads: sn = 12345678, battery = 95, temperature = 23.45, and saturation = 80.
Verify the Result
Section titled “Verify the Result”- Integration events — go to Integrations ⇾ Integrations, open the integration, and click the Events tab. The uplink appears as an event of type Uplink with status OK. Click
⋅⋅⋅in the Message column to see the MQTT topic, for examplev3/my-thingsboard@ttn/devices/thermostat-a/up, and the full message. - Converter events — open
TTN Uplink Converterin Integrations ⇾ Data converters and click the Events tab. In and Metadata show the message as it came from The Things Stack, before the converter unwrapped it. Out shows the decoder output only, without the network keys. - Device — go to Devices & assets ⇾ Devices. The integration has created the device
Device 70B3D57ED80054FE, named after your DevEUI, with the profilemy-thingsboardand the labelthermostat-a. Open it and check:- Latest telemetry —
battery,temperature, andsaturation, plus the network keys that the message carries, such asrssi,snr,fCnt, anddata. - Attributes —
sn,eui,devAddr,fPort, and the other network keys in Client attributes.
- Latest telemetry —
Configure Downlink
Section titled “Configure Downlink”ThingsBoard publishes each downlink right away to the downlink topic of the device, with QoS 1. Nothing is queued in ThingsBoard. The Things Stack adds the downlink to the device queue (/down/push) and sends it in a receive window after the next uplink of the device.
The downlink converter turns a Rule Engine message into a The Things Stack downlink message and returns the end device ID for the topic. In this guide, a change of a shared attribute sends the changed attributes to the device as JSON. For the full encoder reference, see Downlink data converter.
Create and Assign the Downlink Converter
Section titled “Create and Assign the Downlink Converter”- Open the integration in Integrations ⇾ Integrations and click Toggle edit mode (pencil icon).
- In the Downlink data converter field, click Create new.
- Enter a name, for example
TTN Downlink Converter. - Main encoding configuration opens in TBEL with a generic encoder that builds an MQTT
topicfrom the device name, not the message that The Things Stack needs. Replace it with the encoder below. For JavaScript, use the TBEL / JS toggle and the JavaScript tab. - To record downlink events, click the debug button next to the name, select All messages, and click Apply.
- Click Add.
- Click Apply changes.
Encoder used in this guide. It encodes the message as JSON, puts it in Base64 into frm_payload, and sends it on the port of the last uplink. The rule chain adds the end device ID to the metadata as devId, and the fPort client attribute as cs_fPort.
/** Encoder **/
// End device ID in The Things Stack, for example thermostat-a.// The rule chain adds it to the metadata from the device label.var devId = metadata.devId;if (devId == null || devId == "") { raiseError("Missing required metadata field: devId");}
// LoRaWAN port: the fPort client attribute, stored from the last uplink.if (metadata.cs_fPort == null) { raiseError("Missing required metadata field: cs_fPort");}var fPort = parseInt("" + metadata.cs_fPort);if (fPort < 1 || fPort > 223) { raiseError("Invalid LoRaWAN port. Expected a value from 1 to 223.");}
// The message as JSON, for example {"powerState":"on"}, in Base64.var frmPayload = bytesToBase64(stringToBytes(JSON.stringify(msg)));
var data = { downlinks: [ { f_port: fPort, frm_payload: frmPayload, confirmed: false, priority: "NORMAL" } ]};
return { contentType: "JSON", data: JSON.stringify(data), metadata: { // Replaces ${devId} in the downlink topic pattern. devId: devId }};/** Encoder **/
// UTF-8 string to Base64.function utf8ToBase64(value) { var binary = encodeURIComponent(value).replace(/%([0-9A-F]{2})/g, function (match, hex) { return String.fromCharCode(parseInt(hex, 16)); }); return btoa(binary);}
// End device ID in The Things Stack, for example thermostat-a.// The rule chain adds it to the metadata from the device label.var devId = metadata.devId;if (devId === undefined || devId === null || devId === '') { throw new Error('Missing required metadata field: devId');}
// LoRaWAN port: the fPort client attribute, stored from the last uplink.if (metadata.cs_fPort === undefined || metadata.cs_fPort === null) { throw new Error('Missing required metadata field: cs_fPort');}var fPort = parseInt('' + metadata.cs_fPort, 10);if (isNaN(fPort) || fPort < 1 || fPort > 223) { throw new Error('Invalid LoRaWAN port. Expected a value from 1 to 223.');}
// The message as JSON, for example {"powerState":"on"}, in Base64.var frmPayload = utf8ToBase64(JSON.stringify(msg));
var data = { downlinks: [ { f_port: fPort, frm_payload: frmPayload, confirmed: false, priority: 'NORMAL' } ]};
return { contentType: 'JSON', data: JSON.stringify(data), metadata: { // Replaces ${devId} in the downlink topic pattern. devId: String(devId) }};To check the encoder, click Test encoder function. Replace the Message with {"powerState": "on"}, add devId = thermostat-a and cs_fPort = 1 to Metadata, and click Test. The Output panel shows:
{ "contentType": "JSON", "data": "{\"downlinks\":[{\"f_port\":1,\"frm_payload\":\"eyJwb3dlclN0YXRlIjoib24ifQ==\",\"confirmed\":false,\"priority\":\"NORMAL\"}]}", "metadata": { "devId": "thermostat-a" }}The integration reads these fields from the encoder result:
| Field | Description |
|---|---|
contentType |
JSON, TEXT, or BINARY. JSON and TEXT publish data as UTF-8 text. BINARY decodes data from Base64 first. |
data |
The MQTT message, published as is. For The Things Stack, a downlinks array whose entries hold f_port, frm_payload (the device payload in Base64), and optionally confirmed and priority. See the The Things Stack MQTT server docs. An empty data sends nothing. |
metadata.devId |
Replaces ${devId} in Downlink topic pattern. Without it, the topic keeps the text ${devId}, and the downlink does not reach the device. Every other metadata key replaces its own ${key}. |
To set a fixed port, replace fPort with a number, for example f_port: 2. To send only one key, replace JSON.stringify(msg) with JSON.stringify({ powerState: msg.powerState }). For a device that expects bytes, build them instead of JSON, and encode them with bytesToBase64.
Configure the Root Rule Chain
Section titled “Configure the Root Rule Chain”The encoder needs the end device ID and the port. Add them to the metadata, then pass the message to the integration.
- Go to Data processing ⇾ Rule chains and open the Root Rule Chain.
- From the Enrichment section, drag an originator attributes node onto the canvas:
- Name — for example
Add fPort. - Client attributes —
fPort. The node adds it to the metadata ascs_fPort. - Add originator attributes to — keep Metadata.
- Click Add.
- Name — for example
- From the Enrichment section, drag an originator fields node onto the canvas:
- Name — for example
Add Device ID. - Under Originator fields mapping, set Source field to
Labeland Target key todevId. - Add mapped originator fields to — keep Metadata.
- Click Add.
- Name — for example
- From the Action section, drag an integration downlink node onto the canvas:
- Name — for example
Downlink to TTN. - Integration — select
The Things Stack Community integration. - Click Add.
- Name — for example
- Connect the message type switch node to
Add fPortwith the Attributes Updated relation. - Connect
Add fPorttoAdd Device IDwith the Success relation. - Connect
Add Device IDtoDownlink to TTNwith the Success relation. - Click Apply changes.
Use only Attributes Updated. The integration stores client attributes with every uplink, so a Post attributes relation would send a downlink after every uplink.
Test Downlink
Section titled “Test Downlink”- Go to Devices & assets ⇾ Devices and open the device created in Test Uplink.
- On the Attributes tab, select Shared attributes and click +.
- Enter Key
powerState, keep the String type, enter Valueon, and click Add.
Verify the Downlink
Section titled “Verify the Downlink”- Converter events — open
TTN Downlink Converterin Integrations ⇾ Data converters and click the Events tab. In shows the message{"powerState": "on"}withdevIdandcs_fPortin its metadata. Out shows thedownlinksdata anddevId. - Integration events — open the integration’s Events tab. Two events appear: Downlink: ATTRIBUTES_UPDATED for the message from the Rule Engine, and Downlink with status OK for the publish, with the topic, for example
v3/my-thingsboard@ttn/devices/thermostat-a/down/push, and the payload in its message. - The Things Stack — open the end device in the console and click Live data. The downlink appears in the list. Select it to see the payload and the port.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
| No events on the integration Events tab | Debug mode is off, or the integration is disabled. Debug mode records all events only for a limited time after you turn it on. | Turn on Enable integration and debug mode. Click Toggle edit mode and Apply changes to reconnect. |
Can't setup TheThingsNetwork integration. Required TheThingsNetwork Application Credentials values are missing! |
Username or Password is empty. | Enter the username and the API key. |
Failed to connect to MQTT broker at eu1.cloud.thethings.network:8883. |
The region or host is wrong, the port does not match Enable SSL, or a firewall blocks the connection. | Check the cluster ID of your application in the console address, for example eu1.cloud.thethings.network. Use port 8883 with Enable SSL on. Allow outbound TCP to the port. |
Failed to connect to MQTT broker at ... Result code is: ..., for example CONNECTION_REFUSED_NOT_AUTHORIZED |
The server rejected the credentials. The username is not {application ID}@ttn, or the API key is wrong, expired, or deleted. |
Enter the application ID, not your account name, followed by @ttn. Create a new API key and paste it as the Password. |
| Host must be in the public network. | The custom host is a private or local address, and the platform allows only public hosts. | Use a public address, or run the integration as a Remote Integration. |
| Connected, but no Uplink events | No uplinks reach the application, or the API key lacks the Read application traffic (uplink and downlink) right. | Check Live data of the application in the console. Check the rights of the API key. |
| Uplink events with status ERROR | The decoder failed, for example on a payload shorter than the layout it reads. | Check the Error column in the converter events and fix the decoder. |
Payload formatter output does not contain a 'bytes' field. |
A payload formatter is set in The Things Stack, and its result has no bytes key. |
Read the formatter values in the decoder (see To adapt), or remove the formatter. |
Device named Device $eui |
The message has no dev_eui, which is optional for ABP devices. |
Set a DevEUI on the end device, or change Device name to Device $deviceId. |
| Device not created | Allow create devices or assets is off. | Turn it on in the integration. |
| No downlink and no error | No downlink converter is assigned, or the rule chain does not route attribute updates. | Assign the converter and check the Attributes Updated relation. |
Downlink converter error Missing required metadata field: devId |
The device has no label, or the originator fields node is missing. | Set Device label to $deviceId in the uplink converter, or set the label by hand. Connect Add Device ID before the integration downlink node. |
Downlink converter error Missing required metadata field: cs_fPort |
The device has no fPort client attribute yet, or the originator attributes node is missing. |
Send an uplink first: the converter stores fPort with every uplink. Connect Add fPort after the message type switch. |
| Downlink event OK, but nothing in Live data | The topic does not match the device, or the API key lacks the Write downlink application traffic right. | Check the topic in the Downlink event: it must hold the end device ID. Check the rights of the API key. |
| Downlink in Live data, but the device does not react | The device listens on another port, or it is a class A device that has not sent an uplink since. | Set f_port to the port your device expects. Wait for the next uplink of the device. |
To read debug events, open the integration or converter, click Events, and select an event: In is the received data, Out is the converter result, and Error holds the error text.
See Also
Section titled “See Also”- Integrations overview — how integrations, converters, uplinks, and downlinks fit together.
- Uplink data converter — Typed and Generic converters, the Converters library, and the output format.
- Downlink data converter — encoder reference.
- The Things Stack Industries integration — connect to a private The Things Industries deployment.
- integration downlink node — route Rule Engine messages to an integration.
- originator attributes node — add attributes of the message originator to the metadata.
- originator fields node — add fields of the message originator, such as the label, to the metadata.
- message type switch node — route messages by type, for example attribute updates.
- Remote Integration — run the integration outside the ThingsBoard cluster.
- TBEL — built-in functions and syntax for converter scripts.
- The Things Stack MQTT server — topics, credentials, and the downlink message format.
- The Things Stack payload formatters — decode payloads in The Things Stack before they reach ThingsBoard.
Was this helpful?