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 Command Format
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.
All four values must be set. The interval time is sent in hexadecimal format, e.g., 600 seconds:
|
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.
Value2, Value3, and Value4 MUST NOT be sent. |
0x04 | Enable/Disable the Confirmed Uplink Mode |
Value1 is the new uplink mode.
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. |
Downlinks with commands must be sent over fPort 10 in this guide.
Sending LoRaWAN Downlinks to End Devices
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.
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
Figure 2: Convert dec to hex using RapidTables
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.
Figure 3: Paste the hex string into the Enqueue interface in ChirpStack
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.
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;
Change Data Rate For Uplinks: Command 0x02
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 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