How to determine correct format for downlink message

I have Chirpstack successfully configured with a few devices including a valve controller. To control the valve I can successfully send downlink messages via the Chirpstack UI using the device queue, this works well and the device manufacturer (Milesight) provides the different hex values needed for different commands.

I am now setting up Node-Red and want to create MQTT downlink messages to do the same.

What is the best way to discover the required format for the MQTT payload? Can I monitor the MQTT messages while I send one from Chirpstack to capture the format? Am I right that this is different depending on the device or is the download message just one encoded value?

Pointers to the best way to do this would be much appreciated.

Tim

Bottom of this link outlines the format and topic an MQTT downlink must be in. The only thing that changes between devices is the actual downlink message, which is the same hex code you put into the UI for a downlink, but it’s put into the “data” field in the mqtt message. Make sure the payload is in JSON format. If you are having any issues subscribing to the broker to see what your node-red outputs compared to the example is helpful, then looking at the logs for any errors.
https://www.chirpstack.io/docs/chirpstack/integrations/mqtt.html

Using that info I have made some progress, thank you. I tried another test by submitting the hex command to the device queue via the Chirpstack UI, as before that correctly operates the valve. I then tried to configure node-red to generate a similar command and watching the LoRaWAN frames I can see there are two differences:

Snippet from LoRaWAN frame for successful command via Chirpstack UI:

“payload”: {
“f_port”: 85,
“fhdr”: {
“devaddr”: “008ce744”,
“f_cnt”: 4959,
“f_ctrl”: {
“ack”: false,
“adr”: true,
“adr_ack_req”: false,
“class_b”: false,
“f_opts_len”: 0,
“f_pending”: false
},
“f_opts”:
},
“frm_payload”: “03010104c8537e0000”
}

Snippet from LoRaWAN frame generated by node-red:

“payload”: {
“f_port”: 1,
“fhdr”: {
“devaddr”: “008ce744”,
“f_cnt”: 4964,
“f_ctrl”: {
“ack”: true,
“adr”: true,
“adr_ack_req”: false,
“class_b”: false,
“f_opts_len”: 0,
“f_pending”: false
},
“f_opts”:
},
“frm_payload”: “fe1d0000”
}

In both cases my My MQTT messages are reaching the correct device so this is good. When I generate the command via Chirpstack UI it results in f_port = 85 but my node-red command is setting f_port = 1. Does that matter?

My main issue is that the hex command I am submitting is ‘fe1d0000’ but when that reaches the LoRaWAN frame the payload contains “03010104c8537e0000”. For the node-red version the raw hex command “fe1d0000” simply appears in the frame. Any ideas on what is happening here?

FPort = 0 → Indicates that the FRMPayload contains MAC commands only.
FPort = 1…223 → These are application specific, not LoRaWAN specific.
This means, that you have to check on which FPort your device is accepting downlink payloads(commands).
The FPort for UpLinks and DownLinks can be different.

It’s been a long time since I’ve played with MQTT downlinks but I believe that the data must be encoded in base64. As the comment on this line suggests:

“data”: “…” // base64 encoded data (plaintext, will be encrypted by ChirpStack)

Try - “data”: “/h0AAA==”

And yes the fPort does matter for most devices, if 85 works for the webUI to turn on/off your device, mimic that for the mqtt messages.

Just to confirm as well, you are posting these to the topic:

application/APPLICATION_ID/device/DEV_EUI/command/down

When I send the command via the Chirpstack UI device queue it looks like this and works correctly. What is confusing me slightly is that FPort here is 1 but when the command arrives at the device in the LoRaWAN frame it shows FPort=85 but works fine.

When I generate send the same command directly via the MQTT topic I set FPort =1 and when the command arrives at the device it still says FPort=1.

In node-red I am already encoding as Base64 using the below function. But when I look at the LoRaWAN frame that arrives at the device there is a difference between the command I send via Chirpstack UI and the one I am generating via node-red and submitting directly to MQTT.

If I send the hex command ‘ff1d2000’ via Chirpstack then the LoRaWAN frame shows “frm_payload”: “03010104c8537e0000” so I think Chirpstack is applying some other encoding that is important here?

If I send the hex command ‘ff1d2000’ via node-red then the LoRaWAN frame shows “frm_payload”: “fe1d0000”. So this has correctly been decoded from the Base64 string but doesn’t trigger the device.

Any Chirpstack experts know what is going on here? How do I encode my command so that it reaches the device in the right format?

let buff = Buffer.from(msg.payload, 'hex');
let base64data = buff.toString('base64');
msg.payload = {
    "devEui": "24e124460e220202",   
    "confirmed": true,                       
    "fPort": 1,                              
    "data": base64data
}
return msg;

When you trigger your node-red code, does the downlink appear in your queue for that device before it uplinks again? For example when I use the command:

mosquitto_pub -t "application/5fe1c19e-491a-4968-9a4a-622c073e4a0c/device/7894e80000054e0c/command/down" 
-m '{"devEui": "7894e80000054e0c", "confirmed": true, "fPort": 1, "data": "/x0gAA=="}'

I see the following in the queue:

Which reaches my device as expected when it next uplinks.

But once an uplink is received and the downlink is sent, in the LoRaWAN frames in the UI for the device it says:

frm_payload:“5b50c4cb” (and is different each time)

Which is expected because it has been encrypted. So in my test, everything works as expected and no further encoding is required beyond base64.

I am confused by several things you are saying:

Correctly? ff1d2000 and fe1d0000 are not the same.

regardless, if you are looking at the devices LoRaWAN frames, frm_payload should be encrypted and different with each message anyway. Which leads me to believe yours are not being encrypted, or you are looking in the wrong spot.

So I ask again - what topic are you posting these to? Are you bypassing Chirpstack entirely and posting directly to the gateway topics?

If that’s not it, share some photos of exactly what and where you are seeing the differences in packets between the two, and of the rest of your relevant node-red code. Then subscribe to your mqtt broker with the topic ‘#’ and show us what the node-red is outputting. My example above shows the MQTT downlink process works and that there is no extra encoding you need to do to the payload beyond base64, so clearly something in your process is incorrect.

Thanks for pointing that out, my mistake, just learning this stuff. Understood now that the contents of the LoRaWAN frame is encrypted each time. I have double checked the topic and is correct, see screenshot below.

I tried injecting into the queue like you suggest using this command:

mosquitto_pub -t "application/6f815454-25a3-4486-b8e6-5cd57a788c1f/device/24e124460e220202/command/down" -m '{"devEui": "24e124460e220202", "confirmed": true, "fPort": 1, "data": "/ZmYxZDIwMDA=="}'

ff1d2000 (hex) = ZmYxZDIwMDA= (Base64)

So I think I have the correct MQTT topic so that the message is directed to Chirpstack but it doesn’t appear in the device queue in the UI. It is a class C device but not sure that makes a difference?

Note when I submit the above command a new LoRaWAN frame does show up in the Chirpstack UI like before but the device does not respond.

I repeated the test by submitting the same command via the Chirpstack UI (works) and via MQTT (fails). Below are the LoRaWAN frames in each case. I still can’t figure out what I am doing wrong here, it must be that I am formatting the MQTT payload wrong I guess.

Success via Chirpstack UI

{
    "phy_payload": {
        "mhdr": {
            "m_type": "ConfirmedDataUp",
            "major": "LoRaWANR1"
        },
        "mic": [
            209,
            22,
            181,
            46
        ],
        "payload": {
            "f_port": 85,
            "fhdr": {
                "devaddr": "008ce744",
                "f_cnt": 5412,
                "f_ctrl": {
                    "ack": false,
                    "adr": true,
                    "adr_ack_req": false,
                    "class_b": false,
                    "f_opts_len": 0,
                    "f_pending": false
                },
                "f_opts": []
            },
            "frm_payload": "03010104c8cd880000"
        }
    },
    "rx_info": [
        {
            "channel": 7,
            "context": "63GuhA==",
            "crcStatus": "CRC_OK",
            "gatewayId": "24e124fffef8e260",
            "gwTime": "2025-04-30T22:52:05.734170+00:00",
            "location": {},
            "metadata": {
                "region_common_name": "AU915",
                "region_config_id": "au915_1"
            },
            "nsTime": "2025-04-30T22:52:32.331187998+00:00",
            "rfChain": 1,
            "rssi": -92,
            "snr": 8,
            "timeSinceGpsEpoch": "1430088743.734s",
            "uplinkId": 14795
        }
    ],
    "tx_info": {
        "frequency": 918200000,
        "modulation": {
            "lora": {
                "bandwidth": 125000,
                "codeRate": "CR_4_5",
                "spreadingFactor": 7
            }
        }
    }
}
````Preformatted text`

**Fails via MQTT**


{
“phy_payload”: {
“mhdr”: {
“m_type”: “ConfirmedDataUp”,
“major”: “LoRaWANR1”
},
“mic”: [
85,
30,
75,
126
],
“payload”: {
“f_port”: 85,
“fhdr”: {
“devaddr”: “008ce744”,
“f_cnt”: 5414,
“f_ctrl”: {
“ack”: false,
“adr”: true,
“adr_ack_req”: false,
“class_b”: false,
“f_opts_len”: 0,
“f_pending”: false
},
“f_opts”:
},
“frm_payload”: “03010004c8359b0000”
}
},
“rx_info”: [
{
“context”: “8hQyRg==”,
“crcStatus”: “CRC_OK”,
“gatewayId”: “24e124fffef8e260”,
“gwTime”: “2025-04-30T22:53:57.047035+00:00”,
“location”: {},
“metadata”: {
“region_common_name”: “AU915”,
“region_config_id”: “au915_1”
},
“nsTime”: “2025-04-30T22:54:23.652065625+00:00”,
“rssi”: -92,
“snr”: 7.800000190734863,
“timeSinceGpsEpoch”: “1430088855.047s”,
“uplinkId”: 10614
}
],
“tx_info”: {
“frequency”: 916800000,
“modulation”: {
“lora”: {
“bandwidth”: 125000,
“codeRate”: “CR_4_5”,
“spreadingFactor”: 7
}
}
}
}

The issue is that you are translating to base64 incorrectly.

You are encoding the ASCII string “ff1d2000” rather then the hex bytes 0xff, 0x1d, 0x20, 0x00. If you look at the ascii code for each letter in hex:

f = 0x66, f = 0x66, 1 = 0x31, d = 0x64, 2 = 0x32, 0 = 0x30, 0 = 0x30, 0 = 0x30

Which becomes /ZmYxZDIwMDA== In base 64.

So you are actually transmitting:

0x66, 0x66, 0x31, 0x64, 0x32, 0x30, 0x30, 0x30

rather then:

0xff, 0x1d, 0x20, 0x00

The proper base64 representation is:

/x0gAA==

Try sending that via MQTT, as I did in my example. Then adjust your node-red to match and it should work.

Success! Thank you so much for your patience.
I haven’t used node-red before but combined with Chirpstack it looks like a good tool for what I need to both collect data but also create flows and commands to devices. This is part of a farm irrigation system I am working on.

1 Like

Sounds awesome, good luck!