TTN Forum: schedule downlink after uplink
data:privatelast updated:4 years ago
TTN Forum: schedule downlink after uplink

What's the problem?

If a LoRaWAN Class A downlink is scheduled while handling an uplink then it's often not transmitted after that very uplink, but scheduled for the next uplink. And then such pending downlink might be transmitted before it can be replaced during handling of the next uplink.

The button, motion sensor and LED on a The Things Node can easily show that often the downlink is delayed: using this example, a button press should yield a green LED, a timed uplink a red LED, while motion detection does not schedule any downlink at all. Downlinks are scheduled using schedule: 'replace' (which is also the default action). But this often happens too late as well, yielding an additional downlink rather than replacing a pending one.

See also My application’s downlink is always queued for next uplink in the TTN Forum, and a Python MQTT client for the same test with the same results.


Log in to Pipedream, create a copy of this workflow, and then:

  1. In the parameters of the decodeAndScheduleDownlink step, optionally enter a value for the Authorization header if such will be set in the TTN integration in Step 3. (Parameters are private to your copy. Alternatively, change the code to use environment variables, say const authorization = process.env.TTN_AUTH along with defining that variable.)

  2. Deploy the copied workflow to get a unique webhook URL.

  3. In TTN Console, set up the application's HTTP Integration to use that URL, and optionally set a value for the Authorization header.

  4. In the The Things Node, use the sketch below. (There is no need for a Decoder in the application's Payload Formats in TTN Console.)

After the device has joined and the LED is off, press its button while not moving the device. The LED will be blue while waiting for a downlink in 1 second (RX1) or 2 seconds (RX2). (It will shortly flash blue if the uplink is rejected immediately due to no_free_ch duty cycle limitations.) After a button press a downlink should set the LED to green, but you might only notice that after a next uplink, like after triggering the motion detection or when the device sends a timed uplink each 60 seconds. Such timed uplink should set the LED to red, which again you might only see after the next uplink. Make sure to alternate the trigger events (button, motion, timer) as repeated events of the same type will make it hard to tell if any received downlink was already pending or not. After the very first uplink one might also see the LED turn magenta, which is scheduled after the setup event.

Sketch for The Things Node

This is the default sketch for this device.

#include <TheThingsNode.h>

// Set your AppEUI and AppKey
const char *appEui = "70B3D00000000000";
const char *appKey = "00000000000000000000000000000000";

#define loraSerial Serial1
#define debugSerial Serial

// Use TTN_FP_EU868 or TTN_FP_US915
#define freqPlan TTN_FP_EU868

TheThingsNetwork ttn(loraSerial, debugSerial, freqPlan);
TheThingsNode *node;

#define PORT_SETUP 1
#define PORT_MOTION 3
#define PORT_BUTTON 4

Decoder payload function in TTN Console; not required for this demo.

function Decoder(bytes, port) {
  var events = {
    1: 'setup',
    2: 'interval',
    3: 'motion',
    4: 'button'
  return {
    event: events[port] || 'unknown',
    battery: bytes[0]<<8 | bytes[1],
    light: bytes[2]<<8 | bytes[3],
    // Sign-extend to 32 bits to support negative values, by shifting
    // 24 bits to the left (16 too far), followed by a sign-propagating
    // right shift of 16 bits, to effectively shift 8 bits:
    temperature: (bytes[4]<<24>>16 | bytes[5]) / 100

void setup()

  // Wait a maximum of 10s for Serial Monitor
  while (!debugSerial && millis() < 10000)

  // Config Node
  node = TheThingsNode::setup();
  node->configInterval(true, 60000);

  // Test sensors and set LED to GREEN if it works

  debugSerial.println("-- TTN: STATUS");


  debugSerial.println("-- TTN: JOIN");
  ttn.join(appEui, appKey);

  debugSerial.println("-- SEND: SETUP");

void loop()

void interval()

  debugSerial.println("-- SEND: INTERVAL");

void wake()
//  node->setColor(TTN_GREEN);

void sleep()

void onMotionStart()

  debugSerial.print("-- SEND: MOTION");

void onButtonRelease(unsigned long duration)

  debugSerial.print("-- SEND: BUTTON");


void sendData(uint8_t port)
  // Wake RN2483


  byte *bytes;
  byte payload[6];

  uint16_t battery = node->getBattery();
  bytes = (byte *)&battery;
  payload[0] = bytes[1];
  payload[1] = bytes[0];

  uint16_t light = node->getLight();
  bytes = (byte *)&light;
  payload[2] = bytes[1];
  payload[3] = bytes[0];

  int16_t temperature = round(node->getTemperatureAsFloat() * 100);
  bytes = (byte *)&temperature;
  payload[4] = bytes[1];
  payload[5] = bytes[0];

  ttn.sendBytes(payload, sizeof(payload), port);

  // Set RN2483 to sleep mode

  // This one is not optionnal, remove it
  // and say bye bye to RN2983 sleep mode

void onMessage(const uint8_t *payload, size_t size, port_t port)
  debugSerial.println("-- MESSAGE");
  debugSerial.print("Received " + String(size) + " bytes on port " + String(port) + ":");

  for (int i = 0; i < size; i++)
    debugSerial.print(" " + String(payload[i]));


  node->setRGB((payload[0] & 0b100) > 0, (payload[0] & 0b010) > 0, (payload[0] & 0b001) > 0);