Manage downlinks to ChirpStack

Hi community.

In our company we use ChirpStack as the LNS for our networks. I’m trying to integrate some devices and I need to send downlinks to automate some processes for a model of sensors. This model requires sending a downlink with some data that I’m trying to calculate in Tago.

However, I read that ChirpStack downlinks are not integrated yet in the platform (How to integrate TagoIO with Chirpstack LoRaWAN), but the announcement is quite old by now.

Has anyone tried to use ChirpStack API from Tago? I think at the moment I need to send just a curl command (like this one HTTP examples - ChirpStack open-source LoRaWAN<sup>®</sup> Network Server). If I can’t do that natively on a Analysis script, can I hook it to another VPS and run the command from there?

(For context, I’m not exactly a programmer - my background is mainly on electronics, so maybe I’m missing something)

Hi Ignacio,
If I remember well, the Chirpstack runs at on-promise. The official supported is not yet available in this case because most on-promise would need to unblock the downlink requests by either TagoIO sending it through a fixed IP or by using a VPN.

If your solution doesn’t need any of these, you can just do a POST message to your endpoint as Chirpstack instructed.

The following analysis can do that, and it will work with the Dashboard from the Downlink tutorial. I already applied the JSON format from the documentation, but it may need some changes.

 ** Analysis Example
 ** Sending downlink using dashboard
 ** Using an Input Widget in the dashboard, you will be able to trigger a downlink to
 ** any LoraWaN network server.
 ** You can get the dashboard template to use here:
 ** Environment Variables
 ** In order to use this analysis, you must setup the Environment Variable table.
 ** account_token: Your account token. Check bellow how to get this.
 ** default_PORT: The default port to be used if not sent by the dashboard.
 ** device_id: The default device id to be used if not sent by the dashboard (OPTIONAL).
 ** payload: The default payload to be used if not sent by the dashboard (OPTIONAL).
 ** Steps to generate an account_token:
 ** 1 - Enter the following link:
 ** 2 - Select your Profile.
 ** 3 - Enter Tokens tab.
 ** 4 - Generate a new Token with Expires Never.
 ** 5 - Press the Copy Button and place at the Environment Variables tab of this analysis.
const { Analysis, Account, Utils } = require('@tago-io/sdk');
const axios        = require('axios');

async function init(context, scope) {
  if (!scope[0]) return context.log('This analysis must be triggered by a widget.');

  context.log('Downlink analysis started');
  // Get the environment variables.
  const env = Utils.envToJson(context.environment);
  if (!env.account_token) return context.log('Missing "account_token" environment variable');
  else if (env.account_token.length !== 36) return context.log('Invalid "account_token" in the environment variable');

  // Instance the Account class
  const account = new Account({ token: env.account_token });

  // Get the variables form_payload and form_port sent by the widget/dashboard.
  const payload = scope.find(x => x.variable === 'form_payload') || { value: env.payload, origin: env.device_id };
  const port = scope.find(x => x.variable === 'form_port') || { value: env.default_PORT };

  // Throw error if port or payload is not found in the scope or environment variable.
  if (!payload || !payload.value || !payload.origin) return context.log('Missing "form_payload" in the data scope.');
  else if (!port || !port.value) return context.log('Missing "form_port" in the data scope.');

  const device_id = payload.origin; // All variables that trigger the analysis have the "origin" parameter, with the TagoIO Device ID.
  if (!device_id) return context.log('Device ID <origin> not found in the variables sent by the widget/dashboard.');

  // Find the token containing the serial/device EUI of the device.
  const device_tokens = await account.devices.tokenList(device_id, { page: 1, fields: ['name', 'serie_number'], amount: 10 });
  const token = device_tokens.find(x => x.serie_number);
  if (!token) return context.log("Couldn't find a token with serial/EUI for this device");

  context.log('Trying to send the downlink');

  const axios_settings = {
    method: "POST",
    url: `http://localhost:8080/api/devices/${token.serie_number}/queue`,
    headers: {
      "Content-Type": "application/json",
      "Grpc-Metadata-Authorization": "Bearer <API TOKEN>"
    data: {
      "deviceQueueItem": {
        "confirmed": false,
        "data": `${payload.value}`,
        "fPort": Number(port.value)

  // Print the settings to the console, debug purpose.

  await axios(axios_settings)
   .then((result) => {
     context.log(`Downlink accepted with status ${result.status}`);
   .catch((error) => {
     context.log(`Downlink failed with status ${error.response.status}`);
     context.log( || JSON.stringify(error));

module.exports = new Analysis(init);
1 Like

Hi Vitor, thanks for your assistance as always :sweat_smile:

Correct - we run our LNS with a fixed IP (and also with a domain too). If I can do HTTP request with Axios then I’m settled.

Thank you.

1 Like