Skip to main content
Skip to main content

LoRaWAN Payload Decoder: How to Integrate Any Sensor

November 3, 202614 min
Cover image for LoRaWAN Payload Decoder: How to Integrate Any Sensor

A LoRaWAN payload decoder turns the few bytes a sensor sends into values a platform can store, chart, and alarm on. It is where most integrations break.

The radio part of a LoRaWAN project gets all the attention: gateways, coverage, spreading factors. Then the first uplink arrives, and what shows up in the console is `AQDrOgzk`. Nobody can put that on a dashboard. Between that string and "23.5 °C, 58% humidity, battery at 3.3 V" there is a small piece of code, and that code decides whether adding a new sensor model takes an afternoon or two weeks.

This guide is for integrators who need to connect sensors from several manufacturers to one platform. It covers what the decoder does, how to read a payload specification, where the decoder should run, how to write and test one, how downlinks fit in, and how to keep decoders under control when the fleet grows. It assumes you know the basics of how LoRaWAN works.

What a LoRaWAN Payload Decoder Does

LoRaWAN sensors send binary payloads because every byte costs battery. In Europe, the slowest data rate allows 51 bytes of application payload, and the fastest allows 242. A JSON message such as `{"temperature":23.5,"humidity":58,"battery":3300}` takes 48 bytes. The same three values fit in six bytes of binary, and at spreading factor 12 the JSON version would spend almost twice as long on the air. That difference comes straight out of the battery, as our guide to LoRaWAN battery life explains.

So the device packs its readings into as few bytes as it can, and each manufacturer packs them in its own way. There is no common format. Two temperature sensors from two vendors will send different bytes for the same reading.

The decoder is the function that undoes the packing. It receives three things:

  • The bytes of the application payload, already decrypted by the network server.
  • The port (`fPort`), a number from 1 to 223 that the device uses to say what kind of message this is.
  • Some context, such as the device identifier and the time the gateway received the message.

It returns named values in engineering units: `temperature: 23.5`, `humidity: 58`, `battery_mv: 3300`. Some people call it a decoder, others a codec, a parser, or a payload formatter. They all mean the same function.

The opposite direction has its own function, the encoder, which turns a command such as "report every 30 minutes" into the bytes the device expects. A complete integration needs both, although most projects start with the decoder and add the encoder later.

Reading a Payload: Bytes, Ports, and Scaling

Before writing any code you need the payload specification from the manufacturer. It is usually a table in the user manual or a separate PDF. If the vendor cannot give you one, treat it as a warning about the support you will get later.

Take a simple temperature and humidity sensor that sends six bytes on port 2. A real uplink, in hexadecimal:

01 00 EB 3A 0C E4

Its specification reads like this:

OffsetBytesFieldTypeScalingThis message
01Message typeUnsignedNone01 = periodic report
12TemperatureSigned, big-endian0.1 °C00 EB = 235 = 23.5 °C
31HumidityUnsigned1%3A = 58%
42BatteryUnsigned, big-endian1 mV0C E4 = 3,300 mV

Four details in that table account for most decoding bugs.

Byte order. A two-byte value can arrive with the high byte first (big-endian) or the low byte first (little-endian). `00 EB` read the wrong way round is 60,160, not 235. Specifications do not always say which one they use, so check it against a reading you know.

Sign. Temperature goes below zero. A reading of -5.0 °C in this format is `FF CE`. A decoder that treats it as unsigned reports 6,548.6 °C. This bug survives every test done in a warm office and appears on the first cold night.

Scaling. Devices send integers. The specification tells you the factor: tenths of a degree, half percents, millivolts. Miss a factor of ten and the dashboard looks plausible but is wrong.

Bit fields. Many devices pack several flags into one byte: tamper, low battery, leak detected. You need masks and shifts to pull them apart, and the bit numbering in the manual does not always start where you expect.

The port matters as much as the bytes. Manufacturers use different ports for different message types: periodic data on one, alarms on another, configuration replies on a third. A decoder that ignores the port will sooner or later read a configuration reply as a temperature.

Where the Decoder Runs: Network Server or Platform

A LoRaWAN uplink passes through the gateway, the network server, and the application platform. The decoder can run in either of the last two, and the choice matters once you have more than one project.

Decoder in the network serverDecoder in the platform
What the platform receivesDecoded values, usually as JSONRaw bytes and port
Vendor decodersOften available from a device catalogYou write or adapt them
Fleet on two network serversTwo copies to maintainOne copy
Changing network serverDecoders must be movedNothing changes
Re-decoding old data after a fixOnly if the raw bytes were also forwardedPossible if the platform stores raw payloads

In the network server. The Things Stack calls them payload formatters: JavaScript functions attached to an application or to a single device. ChirpStack has an equivalent codec in the device profile. The advantage is the catalog. The community device repository holds decoders published by the manufacturers themselves, and for a listed device you may not need to write anything.

In the platform. The network server forwards the raw payload and the platform decodes it. This keeps every decoder in one place, next to the device model, the variables, and the alarm rules that depend on it. It is the better option when your devices are spread across more than one network server, or when you want to be able to change network provider without rewriting integrations.

There is also a standard that reduces the cost of the choice. The LoRa Alliance published the payload codec API (TS013), which defines the function names and the shape of the input and output: `decodeUplink`, `encodeDownlink`, and `decodeDownlink`. A decoder written to that interface moves between compatible network servers with little or no change.

A different approach is a self-describing format. Cayenne LPP prefixes each value with a channel and a type code, so one generic decoder handles any device that uses it. It costs two extra bytes per value, which is why battery-powered devices with tight payloads rarely use it.

For the role of each component in the path, see our guide to the LoRaWAN network server.

Writing a LoRaWAN Payload Decoder Step by Step

Here is a decoder for the six-byte sensor above, written to the TS013 interface that The Things Stack and ChirpStack both use.

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

if (input.fPort !== 2) {
return { errors: ["Unexpected port: " + input.fPort] };
}
if (bytes.length !== 6) {
return { errors: ["Expected 6 bytes, got " + bytes.length] };
}
if (bytes[0] !== 0x01) {
return { errors: ["Unknown message type: " + bytes[0]] };
}

var data = {
humidity: bytes[3],
battery_mv: (bytes[4] << 8) | bytes[5]
};
var warnings = [];

// Temperature: signed 16-bit, big-endian, tenths of a degree
var raw = (bytes[1] << 8) | bytes[2];
if (raw === 0x7FFF) {
warnings.push("Temperature probe fault");
} else {
if (raw & 0x8000) {
raw -= 0x10000;
}
data.temperature = raw / 10;
}

return { data: data, warnings: warnings };
}

It is short, and the order of the steps is the same for any device.

  1. Check the port first. If the message is not the type this branch handles, say so and stop.
  2. Check the length. A truncated or extended payload means a different firmware or a different message. Decoding it anyway produces values that look real.
  3. Decode each field with its type. Byte order, sign, and scaling, exactly as the specification states.
  4. Handle the sentinel values. Many sensors send a reserved number, such as `7F FF`, when a probe is disconnected. Without that check, the platform stores 3,276.7 °C and fires a high-temperature alarm.
  5. Return errors as errors. A decoder that returns zero or an empty object when it cannot decode hides the problem. An explicit error can be counted and alerted on.

Name the output fields the same way across device models. If one decoder returns `temperature` and another `temp_c`, every dashboard and rule downstream needs to know the difference. A short naming convention, agreed before the second device model arrives, saves that work.

Testing: The Bugs That Reach Production

A decoder that works with the sensor on your desk has been tested with one kind of message: room temperature, full battery, good signal, current firmware. The failures come from everything else.

Build a small set of test vectors for each device model and keep it with the decoder. Each vector is a port, a payload in hexadecimal, and the expected output.

CasePayloadExpected result
Normal reading01 00 EB 3A 0C E423.5 °C, 58%, 3,300 mV
Below zero01 FF CE 50 0C 1C-5.0 °C, 80%, 3,100 mV
Probe fault01 7F FF 00 0C E4No temperature, one warning
Wrong length01 00 EB 3AError
Unknown portAny payload on port 85Error

Besides the sign and the sentinel values, these are the cases that most often get through.

  • Firmware changes. A new firmware version adds a field or changes a scaling factor. If the payload carries a version byte or the length changes, the decoder can branch on it. If not, you need to know which devices run which firmware, which is one more reason to keep an inventory as part of IoT device management.
  • Buffered messages. Some devices store readings while they have no coverage and send them later with their own timestamps. A decoder that ignores the timestamp in the payload stamps old readings with the arrival time, and the chart shows a spike that never happened.
  • Counters that wrap. Pulse counters from water or energy meters are 16 or 32 bits wide and return to zero when they overflow. The decoder passes the value on, and something downstream has to handle the wrap.
  • Join and status messages. Many devices send a status frame after joining, on a different port, with firmware version and configuration. If it is not handled, it becomes the first decoding error of every device.

Test with payloads captured from the field. A week of real uplinks from a pilot contains cases that no specification mentions.

Downlinks: The Encoder Side

Sooner or later someone asks to change the reporting interval of 400 sensors without visiting them. That is a downlink, and it needs the reverse function.

The encoder receives a command in readable form and returns the bytes and the port. For the sensor above, the manual might say that the reporting interval is set by sending `02` followed by the interval in minutes as two bytes, on port 3:

function encodeDownlink(input) {
var minutes = input.data.report_interval_min;

if (minutes < 5 || minutes > 1440) {
return { errors: ["Interval out of range: " + minutes] };
}

return {
fPort: 3,
bytes: [0x02, (minutes >> 8) & 0xFF, minutes & 0xFF]
};
}

Three things are worth knowing before you promise remote configuration to a customer.

Class A devices only listen after they transmit. A battery-powered sensor opens two short receive windows after each uplink and sleeps the rest of the time. A downlink sent now waits in the network server until the next uplink, which may be an hour away.

Validate the range in the encoder. A reporting interval of one minute sent by mistake to a whole fleet empties batteries and can exceed the duty cycle limit. The encoder is the right place to refuse it.

Confirm that the change was applied. Most devices answer a configuration command with a reply on a specific port, or include the current interval in their status message. Decode that reply and show it. Without it, you only know the command was sent.

Decoders at Scale: Versions, Models, and Fleets

With three device models, decoders are a detail. With thirty, across several customers, they need the same care as any other code.

One decoder per device model. Attach the decoder to the model, not to each device. When a bug is fixed, it is fixed for every device of that model at once.

Keep decoders in version control. With their test vectors. A decoder edited directly in a web console, with no history, is how a working integration breaks without anyone knowing what changed.

Store the raw payload. If the platform keeps the original bytes and the port alongside the decoded values, a bug found three months later can be corrected by decoding again. If only the decoded values were stored, the wrong readings stay wrong.

Watch the error rate. Count decoding errors per device model. A sudden rise after a firmware campaign tells you the payload format changed before the customer notices missing data.

In Cloud Studio IoT, the decoder belongs to the device model. Many devices are supported natively and need no code. For any other device, the model carries a JavaScript script with two functions: `parseUplink`, which the platform runs for each payload it receives over LoRaWAN, MQTT, or HTTP, and `buildDownlink`, which turns a platform command into the payload the device expects. The script can read the payload as bytes, as text, or as JSON, and it has the LoRaWAN port available. If the data was already decoded in The Things Stack or ThingPark X, the script can read those values directly instead of decoding again. Each execution is atomic: if the script fails halfway, no value is updated. The details are in the documentation.

Because Cloud Studio IoT receives LoRaWAN data through external network servers, and can receive from several at once, an integrator with devices on two networks keeps one script per model. See our LoRaWAN solutions for the integrations available.

Conclusion

A LoRaWAN payload decoder is a few dozen lines of code, and it sits between every sensor and every value your customer sees. What to remember:

  • Sensors send binary because bytes cost battery. Each manufacturer uses its own format, and the decoder translates it.
  • Byte order, sign, scaling, and bit fields cause most bugs. The port tells you which message type you are reading.
  • The decoder can run in the network server or in the platform. Keeping it in the platform means one copy when the fleet spans several network servers.
  • Check port and length before decoding, handle sentinel values, and return explicit errors.
  • Keep test vectors with negative values, faults, and wrong lengths, and add payloads captured in the field.
  • Attach decoders to device models, keep them in version control, and store the raw payload so that old data can be decoded again.

If you are choosing sensors for a project, our overview of IoT sensor types and protocols is a good starting point, and the guide to IoT platform API integration covers what happens to the data after it is decoded. If you want to review how a specific device would fit, talk to the team.

Frequently asked questions

What is a LoRaWAN payload decoder?

It is the function that turns the bytes a sensor sends into named values in engineering units, such as temperature, humidity and battery voltage. It receives the payload, the port (fPort) and some context. It is also called a codec, a parser or a payload formatter.

Why do LoRaWAN sensors send binary instead of JSON?

Because every byte costs battery. Three values that take 48 bytes as JSON fit in six bytes of binary, and at spreading factor 12 the JSON version would spend almost twice as long on the air. In Europe the slowest data rate allows only 51 bytes of application payload.

Should the decoder run in the network server or in the platform?

Both work. In the network server you can often reuse decoders published by the manufacturers in the device catalog. In the platform you keep one copy when your devices are spread across several network servers, and you can change network provider without rewriting integrations.

What causes most payload decoding bugs?

Four details of the specification: byte order, sign, scaling and bit fields. A negative temperature read as unsigned shows thousands of degrees, and a missed factor of ten looks plausible but is wrong. Ignoring the port is the other common cause, because a configuration reply ends up read as a measurement.

What is the LoRaWAN payload codec API (TS013)?

It is a LoRa Alliance specification that defines the function names and the shape of the input and output: decodeUplink, encodeDownlink and decodeDownlink. A decoder written to that interface moves between compatible network servers with little or no change.

How do I test a payload decoder?

Keep a set of test vectors with each device model: a port, a payload in hexadecimal and the expected output. Include a normal reading, a value below zero, a probe fault, a wrong length and an unknown port, and add payloads captured from the field during the pilot.

Related Articles

Take the Next Step

Ready to Transform Your Business?

Contact us to discover how Cloud Studio IoT can help you achieve your goals.