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.19.2, September 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:
- In the web UI open Device profiles and pick your sensor's profile (or create it, see the gateway guide).
- Switch to the Codec tab.
- Set Payload codec to JavaScript functions.
- Paste the entire content of
em300-th-decoder.jsinto 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 top 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.
Step 3: Check the uplink and see the values
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
upevent. Theobjectfield now holds the decoded values instead of just raw bytes. - MQTT: subscribe to the uplink topic and watch the
objectlive:
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
objectstays 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.











