Payload Decoding in IoT Systems: ChirpStack & Milesight VS373
In IoT systems, sensors typically send their data in compact, binary form – as byte sequences that are not directly understandable to humans or many applications. Payload decoding means converting this raw data into meaningful values.
In this article, we explain why decoding is necessary, how it works technically, and show a comprehensive practical example with ChirpStack and the Milesight VS373 fall detection sensor.
Table of Contents
- 1) What is Payload Decoding and Why Does it Matter?
- 2) How Does Payload Decoding Work Technically?
- 3) Payload Decoding with ChirpStack
- 4) The Milesight VS373 – How it Works and Data Format
- 5) VS373 Example Payloads Decoded
- 6) Why Decoding Matters for Grafana, ThingsBoard & Co.
- 7) Best Practices for Decoder Development
- 8) Conclusion
- Bring Your IoT Project to Life
- References
1) What is Payload Decoding and Why Does it Matter?
With LoRaWAN and other LPWAN technologies, every bit counts. Radio bandwidth is limited, so manufacturers encode measurements and status information efficiently in bytes rather than sending clear-text JSON or text data.
Without decoding, downstream systems would only receive cryptic hex or Base64 strings from which no direct information can be extracted. Only decoding translates the raw sensor data into understandable, usable information – e.g., temperature values in degrees Celsius or alarm messages like "motion detected".
A simple example:
A temperature sensor might send the value 23.5 °C as byte 0xEB. Without context, EB (235 decimal) is meaningless. A decoder, however, knows that it must divide this value by 10 to get 23.5 °C.
This conversion is essential for making device data usable for users and downstream platforms.
2) How Does Payload Decoding Work Technically?
In practice, decoding is often implemented through small decoder functions, typically in JavaScript. Many IoT network servers or cloud platforms (such as The Things Network, ChirpStack, Datacake, etc.) offer the ability to execute custom code whenever a device data packet arrives.
This function reads the byte array and produces a structured JSON object with named fields. The JSON then contains, for example, "temperature": 23.5 instead of a raw hex string.
The Channel/Type Concept
Many manufacturers – including Milesight – use a channel/type concept: Each data element in the payload begins with one byte for the channel and one byte for the data type. This is followed by several data bytes depending on the type.
Example Decoder in JavaScript
// Example decoder function
function Decoder(bytes, port) {
var decoded = {};
if (port === 1) {
// Temperature is encoded as integer in hundredths of °C:
var rawTemp = (bytes[0] << 8) | bytes[1]; // Combine two bytes to 16-bit int
decoded.temperature = rawTemp / 100.0; // Convert to degrees Celsius
}
return decoded;
}
In this example, a payload 0x09 0x3B (hex) would be interpreted by the decoder as follows: The two bytes together yield the value 0x093B (2363 decimal). Dividing by 100 results in 23.63 °C. The decoder result would then be:
{ "temperature": 23.63 }
Typically, the device manufacturer provides the necessary information: which bytes represent which measurements, what conversion factors apply, or which bits represent individual status flags. Often, ready-made example decoders exist – for instance, Milesight maintains a public repository with payload codecs for all its sensors.
3) Payload Decoding with ChirpStack
ChirpStack is an open-source LoRaWAN network server that directly supports integrated decoding. For each device (or device type), you can store a codec function in the Device Profile.
Codec Configuration
Through the web interface, you'll find a "Codec" tab in the device profile where you can either choose predefined codecs or enter your own JavaScript functions.
ChirpStack offers Cayenne LPP as a standard codec – selecting this will automatically decode payloads according to the Cayenne Low Power Protocol. For proprietary formats (like most Milesight devices), select "Custom JavaScript functions" and insert the decoder code there.
ChirpStack V3 vs. V4
When integrating Milesight example decoders, note a small difference:
- ChirpStack V3 expected a function with the header
function Decode(fPort, bytes) - ChirpStack V4 has been unified with The Things Network – here a function
decodeUplink(input)is typically used that returns an object
The principle remains the same: you adjust the function name and header if necessary, while the decoder content (byte processing) can be taken from the manufacturer's repository.
Verification in the Event Log
Once the decoder function is stored and saved in the device profile, ChirpStack automatically applies it to every received packet from the device. In the Live Event Log of the ChirpStack console, you can verify if decoding works – the decoded data appears there under the key object in the JSON.
Important: The raw payload is still preserved and stored together with the decoded values. No information is lost.
4) The Milesight VS373 – How it Works and Data Format
As a practical example, let's look at the Milesight VS373, an IoT sensor with complex payloads.
What is the VS373?
The VS373 is a radar fall detection sensor that uses 60 GHz millimeter-wave radar to detect falls. It is primarily used in elderly care, smart home, and healthcare environments to immediately detect falls or emergencies and trigger alarms.
Thanks to AI-based point cloud analysis, the sensor can detect falls with up to 99% accuracy – contactlessly and anonymously, as no camera is used.
Detected Events and States
The VS373 can do much more than just generate fall alarms:
| Event | Description |
|---|---|
| Motion/Presence Detection | Detects whether a person is present (Occupied) or absent (Vacant). Up to 6 sub-regions can be defined. |
| Fall Detection (Fall Alarm) | Detects severe falls and immediately sends an alarm. |
| Motionless Alarm | Alarm for unusually long motionlessness (may indicate unconsciousness). |
| Dwell Alarm | Alarm when a person stays too long in an area. |
| Out-of-Bed Alarm | Detects when a person leaves the bed – important in care environments. |
| Lying Alarm | Detects that a person is lying down (possibly on the floor after a fall). |
| Respiratory/Vital Status | Detects breathing movements, reports tachypnea (rapid breathing) or bradycardia. |
VS373 Payload Structure
The VS373 uses the typical Milesight format with the channel/type concept. For example, it uses channel 0x06 together with type 0xFB to send an alarm report. This is followed by 5 bytes of value data:
| Byte | Meaning |
|---|---|
| Bytes 1-2 | Alarm ID (0–9999, to distinguish individual alarm events) |
| Byte 3 | Alarm type code |
| Byte 4 | Alarm status |
| Byte 5 | Sub-region ID (or 0xFF if not relevant) |
Alarm Type Codes
| Code | Meaning |
|---|---|
0x00 |
Fall (fall alarm) |
0x01 |
Motionless |
0x02 |
Dwell |
0x03 |
Out-of-Bed |
0x04 |
Occupied (area occupied) |
0x05 |
Vacant (area empty) |
0x06 |
Bradycardia (slow breathing/pulse) |
0x07 |
Tachypnea (rapid breathing) |
0x08 |
Lying (person lying detected) |
Alarm Status Codes
| Code | Meaning |
|---|---|
0x01 |
Alarm (newly triggered) |
0x02 |
Resolved |
0x03 |
Ignore |
0x04 |
Report Respiratory Status |
5) VS373 Example Payloads Decoded
Let's illustrate decoding with two concrete examples:
Example 1: Fall Alarm
Payload (Hex): 06 FB 05 00 00 01 FF
Decoded:
| Field | Value | Explanation |
|---|---|---|
| Channel/Type | 06 FB |
Alarm data record |
| Alarm ID | 05 00 → 5 |
Unique ID of this alarm |
| Alarm Type | 00 |
Fall |
| Status | 01 |
Alarm active |
| Sub-region | FF |
Not applicable |
As JSON:
{
"alarm_id": 5,
"alarm_type": "Fall",
"status": "Alarm",
"zone": null
}
In a dashboard, this payload would appear as the message "Fall detected (ID 5)".
Example 2: Out-of-Bed Alarm
Payload (Hex): 06 FB 02 00 03 01 01
Decoded:
| Field | Value | Explanation |
|---|---|---|
| Channel/Type | 06 FB |
Alarm data record |
| Alarm ID | 02 00 → 2 |
Unique ID of this alarm |
| Alarm Type | 03 |
Out-of-Bed |
| Status | 01 |
Alarm active |
| Sub-region | 01 |
Zone 1 (e.g., Bed 1) |
As JSON:
{
"alarm_id": 2,
"alarm_type": "Out-of-Bed",
"status": "Alarm",
"zone": 1
}
A care dashboard could display: "Alert: Person has left Bed 1".
6) Why Decoding Matters for Grafana, ThingsBoard & Co.
Once the payload is decoded by ChirpStack, the data can be passed to visualization and IoT platforms – e.g., Grafana, ThingsBoard, Node-RED, Datacake, or custom web applications.
ThingsBoard Integration Requires Decoding
ChirpStack offers direct ThingsBoard integration, but this requires that payloads are already decoded. The ChirpStack documentation states:
"Before this integration is able to write data to ThingsBoard, the uplink payloads must be decoded. The payload codec can be configured per device profile."
Without a decoder, ThingsBoard would only receive a Base64-encoded payload string that it can't use. With a decoder, it receives ready-to-use key-value pairs (e.g., alarm_type = "Fall", status = "Alarm") that it stores directly as telemetry.
Grafana and Time-Series Databases
Suppose ChirpStack writes sensor data to an InfluxDB or TimescaleDB for Grafana visualizations. With decoding, ChirpStack can write the decoded fields as separate values:
status_code=1, alarm_type="OutOfBed", zone=1
In Grafana, these fields can then be plotted or used for alerts:
- Graph of fall events over time
- Live alarm panel showing if an alarm is currently active
- Notifications for critical events
Without decoding, the database would only contain a field payload_raw = "06FB0500..." – which would need complicated queries to decipher.
In short: Decoding forms the bridge between devices and IoT applications.
7) Best Practices for Decoder Development
Creating payload decoders requires care. Here are our recommendations:
Study Manufacturer Documentation
The foundation of every decoder is the official protocol description. User manuals and Communication Protocol Guides contain tables of the payload structure. These should be thoroughly understood to cover all edge cases.
Use and Adapt Example Code
Many manufacturers (Milesight, Dragino, Netvox, etc.) provide example decoder functions. It's wise not to start from scratch but to use these templates and adapt them to your own platform.
Consistent Structure for Similar Sensors
If you have multiple devices with similar payload logic, keep the decoder code consistent. Milesight sensors all use the Channel-Type format – you can reuse byte interpretation functions.
Test Extensively
Before production use, the decoder should be tested with known example data. Manufacturers often provide example payloads in documentation.
Handle Edge Cases
IoT payloads often have special cases:
- Ambiguous flags
- Optional fields
- Endianness (Little vs. Big Endian)
- Negative values (Signed vs. Unsigned)
A good decoder handles these things and logs unknown combinations.
Comment and Document the Code
Every mapping (e.g., 0x03 -> Out-of-Bed) and every conversion formula should be documented. This allows team members to extend or debug the code later.
Version Control
Treat decoder code like production software code. Use Git to track changes – this way you can trace historical versions if an update has unexpected effects.
Integrate Decoding Early
It's most effective to perform decoding directly in the LoRaWAN network server. Centralized decoding saves time and avoids errors because not every downstream system needs to implement its own parsing rules.
8) Conclusion
Payload decoding is an invisible but crucial component of modern IoT solutions. It translates the language of devices (bits and bytes) into the language of applications (numbers and meanings).
Using ChirpStack and the Milesight VS373 sensor, we've seen how important and versatile this process is. From the first moment a LoRaWAN packet arrives to the visualization in Grafana or the alarm notification in ThingsBoard – the decoder ensures that the right information arrives.
For IoT integrators, this means: You need to know both the network platform and the device itself well. But once you understand the structure, a few dozen lines of JavaScript can turn raw data into something valuable – namely transparent IoT data that delivers real value.
Bring Your IoT Project to Life
Have an idea for connected sensors? merkaio guides you from requirements analysis to ongoing operations – technology selection, architecture, implementation, and operations.
References
- Concepts of LoRaWAN Payload Decoders – Datacake Docs
- ChirpStack V3 Integration – Milesight Support
- Device Profiles – ChirpStack Documentation
- ThingsBoard Integration – ChirpStack Documentation
- Milesight VS373 Radar Fall Detection Sensor – MCCI
- VS373 User Guide – Milesight
- Milesight Sensor Decoders – GitHub
Frequently Asked Questions
Why is payload decoding necessary for IoT sensors?▼
Where does payload decoding typically take place?▼
Does ChirpStack support custom payload decoding?▼
Does Milesight provide decoders for its sensors?▼
Why is correct decoding important for platforms like Grafana or ThingsBoard?▼
Does merkaio help with creating and maintaining payload decoders?▼
Does merkaio also support complete IoT integration?▼
For which use cases is this type of integration particularly relevant?▼
Ready to build your IoT project?
From idea to production – we guide your IoT journey.