Skip to Content

LoRaWAN

There is no “LoRaWAN” connector in the CMS, and none is needed. A LoRaWAN sensor does not speak IP: it transmits over radio, the gateway forwards the packet, and whoever decrypts, deduplicates and delivers the data is the Network Server — ChirpStack, The Things Stack (TTN), Actility, Loriot, Kerlink. The CMS talks to the Network Server, and all of them publish the data over MQTT or an HTTP webhook, which the CMS already receives.

Sensor ──radio──▶ Gateway ──▶ Network Server ──MQTT──▶ CMS (MQTT_TOPIC_SUBSCRIBER Collector) ▲ └──MQTT── CMS (MQTT_PUBLISH Delivery, downlink)
  1. Register the Network Server’s broker in External Connections, type MQTT_BROKER, and use it in the Application’s MQTT Keep Alive.
  2. Create an MQTT_TOPIC_SUBSCRIBER Collector on the uplink topic:
Network ServerUplink topicBroker
ChirpStack v4application/{applicationId}/device/+/event/upChirpStack’s own Mosquitto
The Things Stack v3v3/{app-id}@{tenant}/devices/+/up{region}.cloud.thethings.network:8883 (TLS), user app-id@tenant, password = API key

MQTT is the preferred path on the shop floor: the CMS opens the connection, so it works behind a firewall. An HTTP webhook from the Network Server to the dynamic Collector API also works, when the Network Server is in the cloud and the CMS is reachable from outside.

The payload

The sensor sends a few bytes. The Network Server delivers them in base64 — data in ChirpStack, frm_payload in TTS — and, when a codec is configured for the device model, already decoded too — object in ChirpStack, decoded_payload in TTS.

  • With a codec on the Network Server, the Transformer only maps fields. This is the simplest path: the manufacturer’s codec usually comes ready.
  • Without a codec, the Transformer decodes the bytes. The Transformer sandbox has no atob or Buffer, so base64 is decoded in the script itself — the repository’s example scenario (scripts/cenario-lorawan/transformador-normaliza-uplink.js) ships the decoder.

The Transformer goes on the Collector’s forwarding (or on the destination Delivery), not on the Collector: the Collector itself delivers the raw JSON. And the {{alias}} in an MQTT_PUBLISH Delivery’s topic reads the payload before the Transformer — to build a per-device topic, use the original JSON fields (deviceInfo.devEui in ChirpStack, end_device_ids.device_id in TTS).

Keep the radio metadata too: rssi, snr, the gateway that heard it and the fCnt. An fCnt that jumps is a lost packet; one that goes back to zero is a rejoin — both are field diagnostics that show up nowhere else.

To send a command to the sensor, the Delivery publishes to the Network Server’s downlink topic:

Network ServerTopicBody
ChirpStack v4application/{app}/device/{devEui}/command/down{ "devEui": "…", "fPort": 10, "confirmed": true, "data": "<base64>" }
The Things Stack v3v3/{app}@{tenant}/devices/{device-id}/down/push{ "downlinks": [{ "f_port": 10, "frm_payload": "<base64>", "confirmed": true }] }

The Delivery succeeds when the Network Server accepts the command, not when it reaches the sensor. A class A device only opens its receive window after transmitting: the downlink waits for the next uplink, which can take minutes or hours. To know it arrived, also subscribe to …/event/txack and …/event/ack (ChirpStack) or …/down/sent and …/down/ack (TTS) — ack only exists for downlinks with confirmed: true.

Testing without a LoRaWAN network

The LoRaWAN Simulator starts a broker inside the CMS and publishes to it what ChirpStack v4 or TTS v3 would publish, with sensors that send uplinks on their own and accept class A downlinks. The scripts/seed-cenario-lorawan.mjs script builds the full scenario on top of it: the LORAWANSIM Application, Collectors for both formats, the normalization Transformer and the LW_DOWN_CS and LW_DOWN_TTS downlink Deliveries, triggered by

curl -k -X POST https://localhost/api/LW_ENTREGA/LW_DOWN_CS \ -H 'Content-Type: application/json' -H 'x-api-token: <LORAWANSIM Application token>' \ -d '{"devEui":"70b3d5e75e000101","aplicacaoNs":"5f2b7c1e-8d4a-4b6f-9c3e-1a2b3c4d5e6f","intervaloSegundos":20}'