how to use downlinks from the lorawan® server to change device settings with rui3

Some applications require dynamically changing LoRaWAN device settings from the network. This guide shows how to use downlink messages from the LNS to update the following parameters:

  • Sending interval: ATC+SENDINT
  • Data rate: AT+DR
  • Adaptive data rate (ADR): AT+ADR
  • Confirmed packet mode: AT+CFM
  • Device reboot: ATZ

Downlink commands follow a fixed byte structure:

<Marker1><Marker2><Command><Value1><Value2><Value3><Value4>

Where:

  • <Marker1>: 0XAA
  • <Marker2>: 0X55
    • The markers are used to distinguish between command downlinks and other downlinks.
  • <Command>: The ID of the command to be executed.
  • <Value1> to <Value4>: Command-specific parameters.
Command ID Command Content of Value1 to Value4
0x01 Change the send interval

Value1 to Value4 contain the new send interval in seconds.

  • Value1 is the most significant byte (MSB)
  • Value4 is the least significant byte (LSB).

All four values must be set.

The interval time is sent in hexadecimal format, e.g., 600 seconds:

  • Value1 = 0x00
  • Value2 = 0x00
  • Value3 = 0x02
  • Value4 = 0x58
0x02 Change the data rate Value1 sets the new data rate (0–7). Value2, Value3, and Value4 MUST NOT be sent.
0x03 Enable/Disable the Adaptive Data Rate (ADR) setting

Value1 is the new adaptive data rate setting.

  • Value1 = 0 → ADR off
  • Value1 = 1 → ADR on

Value2, Value3, and Value4 MUST NOT be sent.

0x04 Enable/Disable the Confirmed Uplink Mode

Value1 is the new uplink mode.

  • Value1 = 0 → CFM off
  • Value1 = 1 → CFM on

Value2, Value3, and Value4 MUST NOT be sent.

0x05 Restart the device This command uses no values. Value1, Value2, Value3, and Value4 MUST NOT be sent.

 

‼️
IMPORTANT

Downlinks with commands must be sent over fPort 10 in this guide.

There are several ways to send downlink messages to a LoRaWAN end device, depending on the Network Server (LNS) being used. Some LNS platforms offer direct user interfaces, while others support MQTT or API-based downlink delivery.

This example uses the Enqueue feature in ChirpStack V4 to demonstrate how to send downlink commands.


To access the Enqueue interface, ensure that the end device is registered in an application on the ChirpStack LNS.

1. Log in to the ChirpStack Web UI.

2. Navigate to the Application.

3. Select the Device within the application.

4. Open the Queue tab.

chirpstack-enqueue-overview.png

Figure 1: ChickStack Queue tab

 

Downlink payloads can be submitted in any of the following formats:

  • Plain hexadecimal string
  • Base64-decoded string
  • JSON-formatted data

Creating Commands as Hex Strings

The simplest way to create a downlink command is by using the hex format. Below are example hex strings for each supported command:

Command Hex String
Change DR to 5 AA550205
Change DR to 3 AA550203
Enable ADR AA550301
Disable ADR AA550300
Enable Confirmed Uplink Mode AA550401
Disable Confirmed Uplink Mode AA550400
Restart device AA5505

 

To change the send interval, first convert the new interval (in seconds) into a 32-bit hexadecimal value.

For example, to set the interval to 600 seconds:

  • Convert 600 to a hex: 0258 (use an online tool like RapidTables for the conversion.)
  • Pad it to 32 bits (8 characters): 00000258
  • Build the full hex command: AA550100000258

rapidtables-convert-dec-to-hex.png

Figure 2: Convert dec to hex using RapidTables

🗒️
NOTE

Make sure to set the fPort to 10 in ChirpStack. Otherwise, the end device will ignore any command sent on a different port.

 

Paste the hex string into ChirpStack's Enqueue interface and click Enqueue to add it to the device's downlink queue.

Once added, it will appear in the queue and remain there until transmitted.

chirpstack-enter-command.png

Figure 3: Paste the hex string into the Enqueue interface in ChirpStack

‼️
IMPORTANT

The timing of the downlink transmission depends on the LoRaWAN class of the device.

Device Class Downlink Send Time
Class A Sent after the next uplink received from the end node
Class B Sent in the next available RX window of the end node
Class C Sent immediately

Once the downlink has been transmitted, it will disappear from the ChirpStack queue.

The example code will execute the received downlink and report through its debug interface. Since the device used in this example is configured as a Class A device, it will only receive the downlink after it sends an uplink.

device-received-downlink.png

Figure 4: Received downlink example code

 

Code Sections

This section focuses on the RX callback function, which handles incoming downlink commands. All other logic follows the standard RUI3 Low-Power example and is not covered in detail here.

LoRa/LoRaWAN Callbacks

The system uses various callbacks to handle key LoRa/LoRaWAN events. This example implements three main callbacks: join, send, and receive events.

Join Callback

  • Definition: Triggered when a join request succeeds or fails.
  • Parameter: int32_t status
  • Behavior:
    • Called once after all join attempts have completed (not on each retry).
    • Status == 0: join successful
    • Status != 0: join failed
    • If the join fails, you can initiate a new join request from within this callback.

Send (TX Finished) Callback

  • Definition: Triggered when the transmission of a LoRaWAN packet completes.
  • Parameter: int32_t status
  • Behavior:
    • Indicates whether the transmission was successful or failed.
    • Status == 0: join successful
    • Status != 0: join failed
    • The status is derived from the underlying LoRaMAC stack. In most cases, it’s sufficient to check for zero (success) or non-zero (failure).
    • This callback wakes the device from sleep.
    • When using loop(), call sleep mode only after this callback has executed.

Receive (RX) Callback

  • Definition: Triggered when a downlink message is received. This is where the payload is validated and the appropriate command is processed.
  • Parameter: A pointer to a structure containing metadata about the received data, including a pointer to the payload and its length.
  • Behavior:
    • Verifies the downlink was received on fPort 10.
    • Parses the command ID and checks payload length
    • Interprets the data to execute the corresponding command.

The structure has the following content:

typedef struct SERVICE_LORA_RECEIVE
{
 uint8_t Port; // Application port (aka fPort)
 uint8_t RxDatarate; // Downlink datarate
 uint8_t *Buffer; // Pointer to the received data stream
 uint8_t BufferSize; // Size of the received data stream
 int16_t Rssi; // Rssi of the received packet
 int8_t Snr; // Snr of the received packet
 uint32_t DownLinkCounter; // The downlink counter value for the received frame
} SERVICE_LORA_RECEIVE_T;

In the callback, the received data is checked for the correct fPort and the expected markers:

void receiveCallback(SERVICE_LORA_RECEIVE_T *data)
{
    // debug output removed

    uint32_t rcvd_value = 0;

    // Check downlink for commands
    if ((data->Buffer[0] == 0xAA) && (data->Buffer[1] == 0x55) && (data->Port == 10))
    {

If the received data is a valid command and was received on the correct fPort, the command is processed in a switch function:

   // Got a command
   switch (data->Buffer[2])
   {

Change Send Interval: Command 0x01

case 0x01: // Change Send Interval

Check the length of the command for correctness. Discard the command if the length is incorrect:

          if (data->BufferSize != 7) // Check command length
          {
               MYLOG("RX_CB", "Send interval wrong size");
               AT_PRINTF("+EVT:PARAM_ERROR");
          }

Calculate the new send interval from the received values:

          // Get new value
          rcvd_value = (uint32_t)(data->Buffer[3]) << 24;
          rcvd_value += (uint32_t)(data->Buffer[4]) << 16;
          rcvd_value += (uint32_t)(data->Buffer[5]) << 8;
          rcvd_value += (uint32_t)(data->Buffer[6]) << 0;

Set the new send interval and save the new value in flash memory:

          g_send_repeat_time = rcvd_value * 1000;

          MYLOG("RX_CB", "New interval %ld", g_send_repeat_time);
          // Stop the timer
          api.system.timer.stop(RAK_TIMER_0);
          if (g_send_repeat_time != 0)
          {
              // Restart the timer
              api.system.timer.start(RAK_TIMER_0, g_send_repeat_time, NULL);
          }
          // Save custom settings
          save_at_setting();
          break;
      case 0x02: // Change DR

Check the length of the command for correctness. Discard the command if the length is incorrect:

         if (data->BufferSize != 4) // Check command length
         {
             MYLOG("RX_CB", "DR wrong size");
             AT_PRINTF("+EVT:PARAM_ERROR");
         }

Get the new data rate from the received values and check for a valid value:

         // Get new value
         rcvd_value = data->Buffer[3];
         if ((rcvd_value < 0) || (rcvd_value > 7))
         {
             MYLOG("RX_CB", "DR wrong value");
             AT_PRINTF("+EVT:PARAM_ERROR");
         }

Set the new data rate. The new data rate is automatically saved in flash memory by RUI3:

         // Set new DR value
         api.lorawan.dr.set(rcvd_value);
         break;

Enable/Disable ADR: Command 0x03

      case 0x03: // Enable/Disable ADR

Check the length of the command for correctness. Discard the command if the length is incorrect:

          if (data->BufferSize != 4) // Check command length
          {
              MYLOG("RX_CB", "ADR wrong size");
              AT_PRINTF("+EVT:PARAM_ERROR");
          }

Get the new ADR setting from the received values and check for a valid value:

          // Get new value
rcvd_value = data->Buffer[3];
if ((rcvd_value < 0) || (rcvd_value > 1))
{
MYLOG("RX_CB", "ADR wrong value");
AT_PRINTF("+EVT:PARAM_ERROR");
}

Set the ADR setting. The new ADR setting is automatically saved in flash memory by RUI3:

          // Set new ADR value
api.lorawan.adr.set(rcvd_value);
break;

Enable/Disable Confirmed Packet Mode: Command 0x04

      case 0x04: // Enable/Disable confirmed mode

Check the length of the command for correctness. Discard the command if the length is incorrect:

          if (data->BufferSize != 4) // Check command length
{
MYLOG("RX_CB", "CFM wrong size");
AT_PRINTF("+EVT:PARAM_ERROR");
}

Get the new confirmed mode setting from the received values and check for a valid value:

          // Get new value
rcvd_value = data->Buffer[3];
if ((rcvd_value < 0) || (rcvd_value > 1))
{
MYLOG("RX_CB", "CFM wrong value");
AT_PRINTF("+EVT:PARAM_ERROR");
}

Set the confirmed mode setting. The new setting is automatically saved in flash memory by RUI3:

          // Set new CFM value
api.lorawan.cfm.set(rcvd_value);
break;

Restart End Node: Command 0x05

      case 0x05: // Restart device

Check the length of the command for correctness. Discard the command if the length is incorrect:

         if (data->BufferSize != 3) // Check command length
{
MYLOG("RX_CB", "Reboot wrong size");
AT_PRINTF("+EVT:PARAM_ERROR");
}

Perform the restart. No data values are used with this command:

         // Restart device (no data value for this command)
delay(100);
api.system.reboot();
break;

Additional commands can be added before the end of the function:

     // Add more commands here
default:
AT_PRINTF("+EVT:AT_ERROR");
break;
}
}
tx_active = false;
}

Final Thoughts

You’ve now seen how LoRaWAN downlink commands can be used to remotely adjust device parameters such as send interval, data rate, ADR, confirmed mode, and reboot control. Use this implementation as a baseline for your remote configuration logic.

If you have questions or run into issues with the example code, visit the RAKwireless Forum for support.

 


bernd-giesecke.png

Bernd Giesecke

Bernd is an Electronics Engineer and Product Manager at RAKwireless with 23 years of experience in industrial and automotive hardware and software R&D. He has been supporting the Arduino open-source community since 2014.

 


Changelog

  • Version 1 - How to Setup a Water Supply Monitoring System with Sensor Hub
    • Author: Bernd Gieseck
    • Reviewer: Karla Jimenez
    • Date Published: 06/11/2025

 

 

Updated