WZ-IT Logo
How-toIoT

Milesight Sensor in ChirpStack: Set Up the Payload Decoder

Timo WevelsiepTimo WevelsiepUpdated: 30.06.2026

Editorial note: Versions, commands and prices may change. Please verify critical steps independently before production use. This guide does not replace individual consulting.

To turn the raw LoRaWAN bytes of a Milesight sensor into readable values such as temperature and humidity, you add a payload decoder in ChirpStack: grab the official JavaScript decoder file from the GitHub repo Milesight-IoT/SensorDecoders, open the matching device profile in ChirpStack, set the payload codec under the Codec tab to "JavaScript functions" and paste the decoder in. On the next uplink the decoded fields appear on the device. This guide walks through it step by step using the EM300-TH (temperature and humidity) as the example - fully self-hosted on your own ChirpStack instance, with no cloud lock-in and no per-device licences.

This guide assumes your gateway and ChirpStack are already running. If not, first set up ChirpStack and the LoRaWAN gateway. Reference version is ChirpStack v4 (currently 4.18, May 2026).

Why LoRaWAN payloads need decoding

LoRaWAN is a radio protocol with a tight time budget (duty cycle) and very small packets. So that a battery-powered sensor lasts for years and fits the narrow radio budget, it does not send plaintext JSON but the shortest possible byte sequence. A typical EM300-TH payload looks like this:

01 75 5A 03 67 EA 00 04 68 8F

ChirpStack uses the AppKey to decrypt the LoRaWAN security layer, but it does not know the vendor-specific format of this application data. Without a decoder the object field in the uplink event stays empty and you only see the Base64 or hex raw value. The payload codec is the translation rule: it reads the bytes according to the Milesight channel scheme (channel ID, channel type, data) and returns structured fields. For the layer model see our primer What is LoRaWAN.

Step 1: Get the official Milesight decoder

Milesight maintains all codecs centrally in the official GitHub repository Milesight-IoT/SensorDecoders (GPL-3.0 licence). The repo is organised by product series:

Series Example devices Typical values
em-series EM300-TH, EM300-MCS, EM500-CO2 temperature, humidity, CO2, level
am-series AM103, AM307, AM319 air quality (CO2, PM2.5, TVOC, lux)
vs-series VS121, VS133, VS373 people and occupancy counting
ws-series WS101, WS301, WS523 buttons, leak detection, smart plug

For the EM300-TH the decoder file lives at em-series/em300-th/em300-th-decoder.js. Each device folder also ships an encoder (for downlinks) and a *-codec.json in LoRaWAN device-repository format. Copy the content of em300-th-decoder.js to the clipboard. Important: always take the file for your exact model - the channel layout differs between series.

Step 2: Add the decoder as a payload codec in ChirpStack

In ChirpStack v4 the codec lives on the device profile, not on the individual device, so it applies to every sensor of the same type:

  1. In the web UI open Device profiles and pick your sensor's profile (or create it, see the gateway guide).
  2. Switch to the Codec tab.
  3. Set Payload codec to JavaScript functions.
  4. Paste the entire content of em300-th-decoder.js into the Codec functions field and save.

The Milesight file already ships the entry function that ChirpStack v4 expects. The decisive part is this wrapper at the end of the file:

// ChirpStack v4
function decodeUplink(input) {
    var decoded = milesightDeviceDecode(input.bytes);
    return { data: decoded };
}

ChirpStack hands the function an input object with bytes (a byte array), fPort and variables. The actual logic sits in milesightDeviceDecode(), which walks the channel/type bytes. The ChirpStack JavaScript environment is based on QuickJS (ES2020); the Node.js Buffer class is available. The same file also contains Decode(fPort, bytes) for ChirpStack v3 and Decoder(bytes, port) for The Things Network - for v4 only decodeUplink matters.

Save the profile and wait for the next uplink (EM300 sends every 10 minutes by default). Verify the result in two places:

  • Web UI: open the device, tab Events, and inspect the up event. The object field now holds the decoded values instead of just raw bytes.
  • MQTT: subscribe to the uplink topic and watch the object live:
mosquitto_sub -h SERVER-IP -t "application/+/device/+/event/up" -v

For the example payload the decoder returns this object:

{ "battery": 90, "temperature": 23.4, "humidity": 71.5 }

If you want to test without waiting for a real uplink, the Codec tab has no test input - instead run the file locally via Node.js or use the repo's own test setup. Once object is populated you can route the values over MQTT into a time-series database and a Grafana IoT dashboard.

Example: EM300-TH payload byte by byte

Milesight encodes each measurement as a block of channel ID + channel type + data (data little-endian). The EM300-TH uses three channels:

Bytes Channel ID Channel type Raw value Decoded
01 75 5A 0x01 battery 0x75 0x5A = 90 90 %
03 67 EA 00 0x03 temperature 0x67 INT16 LE 0x00EA = 234 23.4 °C
04 68 8F 0x04 humidity 0x68 0x8F = 143 71.5 %RH

That is how 01 75 5A 03 67 EA 00 04 68 8F becomes the JSON object above. Temperature is a signed 16-bit value divided by 10, humidity a single byte divided by 2. Other models use further channels - an AM319, for instance, also reports CO2, PM2.5, TVOC and illuminance. The matching decoder from the repo already knows these channels, so you never compute anything by hand.

Common pitfalls

  • object stays empty: the codec sits on the wrong device profile, or the device uses a different profile. Check the assignment.
  • Nonsense values: wrong decoder file (wrong model) or a failed join. With a wrong AppKey the application data stays encrypted.
  • Decoder does not refresh: after pasting, save the profile and wait for the next uplink - existing, already stored events are not re-decoded retroactively.
  • Wrong model within a series: EM300-TH, EM300-MCS and EM300-DI share channels but differ - always use the exact model file.

Next steps

With a payload codec in place, ChirpStack delivers clean, structured measurements - the basis for dashboards, alerts and analytics, fully sovereign on your own infrastructure. We can handle setup, decoder maintenance for mixed sensor fleets and operations end to end. Learn more on our pages for Milesight sensor integration and ChirpStack & LoRaWAN Network Server, in the overview of LoRaWAN solutions and in the WZ-IT IoT section. You can book a no-obligation intro call online.

You'd rather not run IoT yourself? WZ-IT handles setup, operations and maintenance – GDPR-compliant from Germany.

Frequently Asked Questions

Answers to the most important questions

For radio and battery reasons, LoRaWAN sensors send the shortest possible payloads as raw bytes (hex), for example '01 75 5A 03 67 EA 00 04 68 8F'. Without a decoder, ChirpStack only sees those bytes. A payload codec translates them according to the vendor channel scheme into readable fields such as battery, temperature and humidity.

Milesight maintains the official Milesight-IoT/SensorDecoders repository on GitHub (GPL-3.0). Decoders are organised by product series (am-series, em-series, vs-series and more). For the EM300-TH the JavaScript decoder lives at em-series/em300-th/em300-th-decoder.js.

ChirpStack v4 calls decodeUplink(input). The input object contains bytes (a byte array), fPort and variables. The Milesight decoder file already ships this function: it calls milesightDeviceDecode(input.bytes) and returns { data: decoded }. The same file also includes Decode() for ChirpStack v3 and Decoder() for The Things Network.

On the device profile, under the Codec tab. Set 'Payload codec' to 'JavaScript functions' and paste the entire content of the decoder file into the codec field. The codec then applies to every device that uses this profile.

By default the EM300 series uses application port (fPort) 85 and reports measurements every 10 minutes, with battery level every 6 hours. The Milesight decoder does not strictly switch on fPort; it parses the channel/type bytes of the payload.

Nothing. The decoders in the official GitHub repo are GPL-3.0 and free to use. ChirpStack itself is open source (MIT) with no licence or per-device fees. Costs only arise for hardware and operating your own infrastructure.

Check three things. First, that the codec sits on the correct device profile and the device uses that profile. Second, that you used the decoder file for your exact model. Third, that the join succeeded - with a wrong AppKey the application bytes stay encrypted and decode to nonsense.

Let's Talk About Your Idea

Whether a specific IT challenge or just an idea - we look forward to the exchange. In a brief conversation, we'll evaluate together if and how your project fits with WZ-IT.

E-Mail
[email protected]

Leading companies trust WZ-IT

  • Rekorder
  • Keymate
  • Führerscheinmacher
  • SolidProof
  • ARGE
  • Boese VA
  • NextGym
  • Maho Management
  • Golem.de
  • Millenium
  • Paritel
  • Yonju
  • EVADXB
  • Mr. Clipart
  • Aphy
  • Negosh
  • ABCO Water Systems
Timo Wevelsiep & Robin Zins - CEOs of WZ-IT

Timo Wevelsiep & Robin Zins

Managing Directors of WZ-IT

1/3 - Topic Selection33%

What is your inquiry about?

Select one or more areas where we can support you.