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)Uplink — MQTT Collector
- Register the Network Server’s broker in External Connections,
type
MQTT_BROKER, and use it in the Application’s MQTT Keep Alive. - Create an
MQTT_TOPIC_SUBSCRIBERCollector on the uplink topic:
| Network Server | Uplink topic | Broker |
|---|---|---|
| ChirpStack v4 | application/{applicationId}/device/+/event/up | ChirpStack’s own Mosquitto |
| The Things Stack v3 | v3/{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
atoborBuffer, 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.
Downlink — MQTT_PUBLISH Delivery
To send a command to the sensor, the Delivery publishes to the Network Server’s downlink topic:
| Network Server | Topic | Body |
|---|---|---|
| ChirpStack v4 | application/{app}/device/{devEui}/command/down | { "devEui": "…", "fPort": 10, "confirmed": true, "data": "<base64>" } |
| The Things Stack v3 | v3/{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}'