ChirpStack Payload Decoder: From Hex to Clean JSON

Write a ChirpStack v4 payload decoder: a codec on the device profile that turns LoRaWAN hex into clean JSON, with a worked Milesight example.

LoRaWAN
Architecture

The data flow

LoRaWAN devices send compact binary payloads like 0175640367AD00046873. A payload codec turns that hex into structured JSON that your dashboard, rules and alarms can actually use. Here is how to write, deploy and test one in ChirpStack v4.

Sensor / controller

Measures or controls in the field and sends LoRaWAN uplinks.

LoRaWAN gateway

Receives the radio packets and forwards them to the server.

ChirpStack

Network server: manages sessions and decodes the payload.

ThingsBoard / Grafana

Dashboards, alarms, rules and reports.

What a payload codec does

ChirpStack runs a small JavaScript function on every uplink. It receives the raw bytes and returns a JSON object. The decoded values are then available on the event stream (MQTT/HTTP) and can be forwarded to ThingsBoard, Grafana or any application. The same codec file also holds the downlink encoder for devices you control (relays, thermostats, valves).

Where the codec lives in ChirpStack v4

In ChirpStack v4 the codec sits on the Device Profile, not the device: Device Profiles, your profile, Codec, JavaScript functions. Every device using that profile inherits the decoder. This is why we model one clean profile per device type instead of pasting code per device.

The decoder function

ChirpStack v4 expects a decodeUplink(input) function. input.bytes is an array of byte values; you return { data: { ... } }. Milesight devices use a channel-based format, repeated [channel id][channel type][payload]:

function decodeUplink(input) {
  var bytes = input.bytes;
  var data = {};

  for (var i = 0; i < bytes.length; ) {
    var channel = bytes[i++];
    var type = bytes[i++];

    if (channel === 0x01 && type === 0x75) {          // battery (%)
      data.battery = bytes[i]; i += 1;
    } else if (channel === 0x03 && type === 0x67) {   // temperature, INT16 LE / 10
      data.temperature = readInt16LE(bytes, i) / 10; i += 2;
    } else if (channel === 0x04 && type === 0x68) {   // humidity, UINT8 / 2
      data.humidity = bytes[i] / 2; i += 1;
    } else {
      break;  // unknown channel, stop instead of mis-parsing
    }
  }
  return { data: data };
}

function readInt16LE(b, i) {
  var v = (b[i + 1] << 8) | b[i];
  return v > 0x7fff ? v - 0x10000 : v;
}

Test it before you trust it

Use Device Profiles, Codec, Test (or the device's Events tab on a live uplink). Paste a known payload and confirm the JSON:

Input  (hex): 0175640367AD00046873
Output (JSON): { "battery": 100, "temperature": 17.3, "humidity": 57.5 }

If a value looks wrong, it is almost always endianness (Milesight is little-endian) or a scaling factor (temperature divided by 10, humidity by 2).

Common pitfalls

  • Endianness: Milesight payloads are little-endian. Reading multi-byte values in the wrong order gives nonsense numbers.
  • Scaling factors: Temperature is divided by 10, humidity by 2. A missing divisor is the most common decode bug.
  • Unknown channels: Break on an unrecognised channel instead of mis-parsing the rest of the payload.
  • v3 to v4: v3 uses a different function signature and exposed decoded data as objectJSON. v4 exposes it directly as object. Migrate codecs and converters together.

From hex to dashboard

Once the codec returns clean JSON, ChirpStack publishes it over its integration. From there it flows into your dashboard, see ChirpStack to ThingsBoard. We write, deploy and operate the codec for every device we run as part of ChirpStack managed hosting.

Frequently asked questions

The logic is identical, but v3 uses a different function signature. We migrate v3 codecs to the v4 decodeUplink format as part of any upgrade.
Yes, any LoRaWAN device with a documented payload structure. We build the codec from the documented payload format and confirm it in your deployment.
Each device page in our Milesight hardware library ships a ChirpStack and ThingsBoard decoder plus a decoded example.
Yes. The codec file holds both decodeUplink and an encodeDownlink function, so devices you control (relays, valves, thermostats) use the same codec for commands.
Yes. The codec lives on the Device Profile, so every device that uses that profile inherits the same decoder. We model one clean profile per device type instead of pasting code per device.

Let's discuss your infrastructure. Digital and on-site.

Whether it's IoT platform development, hardware selection, managed hosting for ChirpStack, ThingsBoard, Grafana or NetBird VPN, or migration from a self-hosted setup - we'll find the right solution for your use case. Book a free 30-minute consultation, no commitment required.

Timo Wevelsiep

Your contact

Timo Wevelsiep

Founder, merkaio

15 minutes, no commitment, directly with Timo.

By submitting, you agree to our Privacy Policy.

merkaio is an independent integrator and is not affiliated with Milesight.