Skip to content

ChirpStack v4 / TTN v3 ↔ ThinkLink Integration Bridge (BizCode 90004)

A generic MQTT forwarding bridge that connects ChirpStack v4 or TTN v3 LoRaWAN devices to the ThinkLink platform bidirectionally, with no changes required on the device side.


1. Overview

This bridge is a generic MQTT forwarding bridge for operators who already run LoRaWAN devices on ChirpStack v4 or TTN v3 (The Things Stack) and want to bring device data into the ThinkLink IoT platform. It consists of four ThinkLink forwarders, each handling one direction for one NS (uplink: device → platform; downlink: platform → device). Uplink messages from both NS types are normalized into ThinkLink's AS standard format at the forwarding layer, then decoded by the platform's thing model and written to telemetry storage. Downlink RPCs issued by the platform are forwarded to the corresponding NS's downlink queue endpoint. Adding new devices requires no script changes — simply pre-create the device entity in ThinkLink and bind it to a thing model.


2. Architecture & Data Flow

┌─────────────────────────────────────────────────────────┐
│ External NS MQTT Broker                                  │
│ ChirpStack: application/{appId}/device/{devEui}/event/up │
│ TTN:        v3/{appId}@{tenantId}/devices/{devId}/up    │
└────────────────────┬────────────────────────────────────┘
                     │ Subscribe (source broker)

         ┌───────────────────────┐
         │   ThinkLink Forwarder  │
         │   forwardScript()      │
         │  (chirpstack-up or     │
         │   ttn-up)              │
         │  reads tenant from     │
         │  org_params            │
         │  normalizes to AS fmt  │
         └───────────┬───────────┘
                     │ Publish (target broker)

        ┌────────────────────────────────┐
        │ ThinkLink AS MQTT Broker        │
        │ /v32/{tenant}/as/up/data/{eui} │
        └────────────────────────────────┘


        Platform thing model (payload_parser_type=CHIRPSTACK) decodes
        → telemetry stored / triggers / alerts fired

Hop-by-hop notes:

  • External NS Broker → Forwarder: The forwarder subscribes to the NS uplink topic using wildcards (+) to match all applications and devices, receiving the raw uplink JSON.
  • Forwarder processing: Extracts EUI, fPort, base64 payload, and gateway RSSI/SNR/timestamp from the raw message; normalizes them into ThinkLink AS uplink format; reads org_params['lns.tenant'] to construct the target topic.
  • ThinkLink AS Broker → Platform: The platform consumes the AS MQTT message, matches the device by EUI, invokes the thing model codec (payload_parser_type=CHIRPSTACK) to decode the payload, and writes telemetry and fires triggers/alerts.
        Platform RPC / scheduled task / manual downlink


        ┌────────────────────────────────┐
        │ ThinkLink AS MQTT Broker        │
        │ /v32/{tenant}/as/dn/data/{eui} │
        └────────────────────────────────┘
                     │ Subscribe (source broker = AS)

         ┌───────────────────────┐
         │   ThinkLink Forwarder  │
         │   forwardScript()      │
         │  (chirpstack-down or   │
         │   ttn-down)            │
         │  extracts eui from     │
         │  topic trailing segment│
         │  builds NS downlink    │
         │  queue message body    │
         └───────────┬───────────┘
                     │ Publish (target = NS broker)

┌──────────────────────────────────────────────────────────┐
│ External NS MQTT Broker                                   │
│ ChirpStack: application/{appId}/device/{devEui}/command/down │
│ TTN:        v3/{appId}@{tenantId}/devices/{devId}/down/push  │
└──────────────────────────────────────────────────────────┘

Hop-by-hop notes:

  • Platform → AS Broker: When the platform issues a downlink RPC, it publishes the downlink message to the AS downlink topic, with the device EUI as the trailing segment.
  • Forwarder processing: Extracts the EUI from the topic's trailing segment; reads fPort, payload, and confirmed flag from msg.userdata; builds the NS-specific downlink queue message body; reads appId/tenantId from org_params to construct the target topic.
  • AS Broker → External NS: The NS receives the queued downlink and transmits it to the device at the next available downlink window (Class A) or immediately (Class C).

Single-NS convention (current release): Both chirpstack-down and ttn-down subscribe to the same AS downlink topic /v32/{tenant}/as/dn/data/+. In this release, one ThinkLink tenant connects to only one NS — enable only the *-down forwarder for the NS in use, and keep the other disabled, to prevent the same downlink from being dispatched by both NS simultaneously. Multi-NS support (per-device NS routing) is planned for a future release.


3. Prerequisites

Before deploying this bridge, confirm the following:

  1. Devices pre-created in ThinkLink: Every LoRa device must have a device entity in ThinkLink identified by its EUI. The EUI must match the NS side (case-insensitive; the bridge normalizes to lowercase).
  2. Thing model type is CHIRPSTACK: The telemetry thing model bound to the device must have payload_parser_type set to CHIRPSTACK, with a codec implementing the standard function signature Decode(fPort, bytes) — ThinkLink uses this to decode the normalized uplink payload.
  3. NS MQTT Broker reachable: The ChirpStack or TTN MQTT broker address, port, and credentials must be known, and the server hosting ThinkLink must be able to reach the broker over the network.
  4. ThinkLink AS Broker credentials: Obtain the internal AS MQTT broker connection credentials from the platform system configuration (the address is typically an internal hostname or IP).

Forwarder subscribe topic (on ChirpStack broker):

application/{appId}/device/{devEui}/event/up

Deployed with the wildcard pattern application/+/device/+/event/up to match all devices.

Field mapping:

ThinkLink AS normalized fieldChirpStack v4 raw fieldNotes
euimsg.deviceInfo.devEuiConverted to lowercase
userdata.portmsg.fPortRequired; frame dropped if missing
userdata.seqnomsg.fCntFrame counter
userdata.payloadmsg.database64; required; frame dropped if missing
gwrx[0].euimsg.rxInfo[0].gatewayIdConverted to lowercase
gwrx[0].rssimsg.rxInfo[0].rssidBm
gwrx[0].lsnrmsg.rxInfo[0].snrdB
gwrx[0].timemsg.rxInfo[0].timeISO 8601

Forwarder subscribe topic (on TTN broker):

v3/{appId}@{tenantId}/devices/{devId}/up

Deployed with the wildcard pattern v3/+/devices/+/up.

Field mapping:

ThinkLink AS normalized fieldTTN v3 raw fieldNotes
euimsg.end_device_ids.dev_euiConverted to lowercase
userdata.portmsg.uplink_message.f_portRequired
userdata.seqnomsg.uplink_message.f_cntFrame counter
userdata.payloadmsg.uplink_message.frm_payloadbase64; required
gwrx[0].euimsg.uplink_message.rx_metadata[0].gateway_ids.euiConverted to lowercase
gwrx[0].rssimsg.uplink_message.rx_metadata[0].rssidBm
gwrx[0].lsnrmsg.uplink_message.rx_metadata[0].snrdB
gwrx[0].timemsg.uplink_message.rx_metadata[0].timeISO 8601

If the NS does not include gateway receive metadata (rxInfo / rx_metadata is absent or empty), gwrx will be an empty array []. This does not affect telemetry storage — the platform AS accepts an empty gwrx.

After processing, messages from both NS types are published in the following format:

Publish topic: /v32/{tenant}/as/up/data/{eui} ({eui} in lowercase)

json
{
  "if": "loraWAN",
  "type": "data",
  "version": "3.0",
  "moteeui": "6353012b00021678",
  "gwrx": [
    {
      "eui": "a840411e5f8e7200",
      "rssi": -85,
      "lsnr": 7.5,
      "time": "2026-06-14T08:00:00Z"
    }
  ],
  "userdata": {
    "adr": false,
    "port": 1,
    "class": "ClassA",
    "seqno": 123,
    "payload": "AQIDBA==",
    "confirmed": false
  }
}

Required fields: Both userdata.payload (base64) and userdata.port must be present and non-empty. If either is missing, the forwarder returns null and the frame is silently discarded (not forwarded, no error).


When the platform issues an RPC or manual downlink, ThinkLink AS broker publishes to:

AS downlink entry topic: /v32/{tenant}/as/dn/data/{eui}

The forwarder reads the following fields from the message:

FieldMeaning
msg.userdata.portLoRa fPort; required
msg.userdata.payloadbase64 downlink payload; required
msg.userdata.confirmedWhether a confirmed frame is requested (optional; defaults to false)

5.1 Forward to ChirpStack v4

Publish topic: application/{appId}/device/{eui}/command/down

appId is read from org_params['lns.chirpstack.appId'].

json
{
  "devEui": "6353012b00021678",
  "confirmed": false,
  "fPort": 1,
  "data": "AQIDBA=="
}

5.2 Forward to TTN v3

Publish topic: v3/{appId}@{tenantId}/devices/{devId}/down/push

appId is read from org_params['lns.ttn.appId']; tenantId from org_params['lns.ttn.tenantId'].

json
{
  "downlinks": [
    {
      "f_port": 1,
      "frm_payload": "AQIDBA==",
      "priority": "NORMAL",
      "confirmed": false
    }
  ]
}

TTN downlink device_id mapping: TTN downlink topics require a string devId (not a bare EUI). The forwarder resolves devId as follows:

  1. First checks org_params['lns.ttn.deviceMap'][eui] — an operator-maintained explicit mapping.
  2. If the map is empty or the EUI has no entry, falls back to using the EUI itself as devId (works when the TTN device id matches the EUI, which is a common convention).

6. Configuration (org_params)

The tenant administrator maintains the following five keys under System Settings → Server Configuration → Organization Parameters:

KeyPurposeExample
lns.tenantThinkLink tenant code; used to construct AS topics /v32/{tenant}/as/...demo
lns.chirpstack.appIdChirpStack application id (integer string)1
lns.ttn.appIdTTN application idmy-app
lns.ttn.tenantIdTTN tenant id (The Things Stack multi-tenancy identifier)ttn
lns.ttn.deviceMapJSON object {"eui": "ttn-device-id"}; use {} if TTN device id matches the EUI{"a1b2c3d4e5f60708":"my-sensor-01"}

Security: These are operational settings. Do not commit them to any code repository. Broker credentials and API keys are maintained separately in the platform's Broker connection settings and must not be stored in org_params.


7. Deployment Steps

Step 1: Create 3 MQTT Broker Connections

Platform path: System Settings → Server Configuration → Broker Connection Management

1a. ThinkLink AS Broker (internal)

ParameterDescription
Connection typeAS (platform built-in AS broker)
Address / PortSee System Settings → Server Configuration → Internal MQTT for the actual address and port
Username / PasswordSame as above; use the platform's internal MQTT credentials
RoleTarget for uplink forwarders; source for downlink forwarders

1b. ChirpStack Broker

ParameterDescription
Connection typeCustomize (external broker)
Address / PortFrom ChirpStack integrations page → MQTT integration → Server (e.g. mqtt://chirpstack.example.com:1883)
Username / PasswordCredentials configured in ChirpStack MQTT integration
TLSMatch the ChirpStack deployment; enable TLS in production
RoleSource for chirpstack-up; target for chirpstack-down

1c. TTN Broker

ParameterDescription
Connection typeCustomize (external broker)
Address / PortFrom TTN Console → Application → Integrations → MQTT (e.g. mqtts://eu1.cloud.thethings.network:8883)
Username / PasswordTTN MQTT credentials (username format {appId}@{tenantId}, password is an API Key)
TLSTTN cloud service uses TLS by default
RoleSource for ttn-up; target for ttn-down

Step 2: Create 4 Forwarders

Platform path: Advanced Features → Forwarder → Add

Create one forwarder per row below. Paste the full content of the corresponding forwarder script into the script field.

2a. chirpstack-up (ChirpStack uplink)

ParameterValue
Namechirpstack-up
Source BrokerChirpStack broker (Step 1b)
Subscribe Topicapplication/+/device/+/event/up
Target BrokerThinkLink AS broker (Step 1a)

2b. ttn-up (TTN uplink)

ParameterValue
Namettn-up
Source BrokerTTN broker (Step 1c)
Subscribe Topicv3/+/devices/+/up
Target BrokerThinkLink AS broker (Step 1a)

2c. chirpstack-down (ChirpStack downlink)

ParameterValue
Namechirpstack-down
Source BrokerThinkLink AS broker (Step 1a)
Subscribe Topic/v32/{tenant}/as/dn/data/+ (replace {tenant} with the actual tenant code; must match lns.tenant)
Target BrokerChirpStack broker (Step 1b)

2d. ttn-down (TTN downlink)

ParameterValue
Namettn-down
Source BrokerThinkLink AS broker (Step 1a)
Subscribe Topic/v32/{tenant}/as/dn/data/+ (same replacement as above)
Target BrokerTTN broker (Step 1c)

Single-NS convention: chirpstack-down and ttn-down subscribe to the same AS downlink topic. Enable only the *-down forwarder for the NS in use; keep the other disabled. Having both enabled simultaneously causes the same downlink to be dispatched to both NS.

Step 3: Fill in org_params

Platform path: System Settings → Server Configuration → Organization Parameters

Maintain the following keys in the target tenant's organization parameters (only fill keys for the NS you are using):

KeyDescriptionExample (replace with real values)
lns.tenantThinkLink tenant codedemo
lns.chirpstack.appIdChirpStack application id1
lns.ttn.appIdTTN application idmy-app
lns.ttn.tenantIdTTN tenant idttn
lns.ttn.deviceMap{"eui":"ttn-device-id"} map; use {} if device id matches EUI{"a1b2c3d4e5f60708":"my-sensor-01"}

For each LoRa device to be connected through this bridge:

  1. Create a device entity in ThinkLink using the device's EUI (case-insensitive; the bridge normalizes to lowercase).
  2. Bind a telemetry thing model with payload_parser_type set to CHIRPSTACK and a codec implementing Decode(fPort, bytes).
  3. Confirm the EUI is stored as lowercase on the platform side, or that the platform's device lookup is case-insensitive.

Step 5: End-to-End Testing

Uplink verification:

  1. Send an uplink frame from the ChirpStack / TTN console or a simulator (valid fPort + payload).
  2. In ThinkLink platform → Device Details → Telemetry, confirm data has been stored.
  3. If telemetry is empty, check the forwarder run log (Advanced Features → Forwarder → Run Log). A null return means a required field was missing and the frame was silently discarded. Verify lns.tenant, device pre-creation, and thing model type.

Downlink verification:

  1. Manually issue an RPC or downlink command to the target device from the ThinkLink platform.
  2. In ChirpStack → Device → LoRaWAN Frames, or TTN Console → Live Data, confirm the downlink was queued (command/down / down/push received).
  3. If the downlink did not reach the NS, verify that {tenant} in the downlink forwarder's subscribe topic has been replaced with the real tenant code, and that the relevant org_params (appId, tenantId) are filled in.

8. Troubleshooting

SymptomLikely CauseHow to Investigate
Uplink telemetry not storedlns.tenant does not match the actual tenant codeCheck org_params and forwarder run log
Uplink telemetry not storedDevice not pre-created or EUI mismatchInspect the device list in ThinkLink
Uplink telemetry not storedThing model not set to CHIRPSTACK typeCheck the thing model bound to the device
Downlink not received by NS{tenant} placeholder not replaced in downlink forwarder's subscribe topicCheck forwarder configuration
Downlink not received by NSappId / tenantId org_params not filled inCheck org_params configuration page
TTN downlink topic errorlns.ttn.deviceMap missing and TTN device id differs from EUIAdd the EUI → device id mapping to lns.ttn.deviceMap
Both NS receive the same downlinkchirpstack-down and ttn-down are both enabledDisable the forwarder for the NS not in use (single-NS convention)