Milesight EM300-TH Payload Decoder

Decode Milesight EM300-TH LoRaWAN payloads online: battery, temperature and humidity from the channel/type TLV format. Includes payload structure, verified examples and a decodeUplink codec for ChirpStack v4 and The Things Stack.

Type: Outdoor temperature & humidity sensor (IP67) LoRaWAN class: Class A FPort: 85 Payload: Variable (TLV) Endianness: Little-endian (values)

Example payloads

01756403671001046865 Battery 100 % · 27.2 °C · 50.5 %
0367 9BFF 0468 5C Freezer reading: −10.1 °C · 46 % (no battery field in this uplink)

Payload structure

Bytes Field Type / scale Description
x, x+1 Channel + type header uint8 + uint8 Every measurement starts with a channel byte and a type byte, followed by its value. Fields can appear in any order.
01 75 + 1 byte Battery uint8 → % Battery level in percent. Example: 0x64 = 100 %.
03 67 + 2 bytes Temperature int16 LE ÷ 10 → °C Little-endian and signed. Example: bytes 10 01 = 0x0110 = 272 → 27.2 °C; bytes 9B FF = −101 → −10.1 °C.
04 68 + 1 byte Humidity uint8 ÷ 2 → %RH Half-percent resolution. Example: 0x65 = 101 → 50.5 %.

Ready-to-use codec

ChirpStack v4 and The Things Stack share the same decodeUplink(input) signature: paste this into the device profile codec (ChirpStack) or the payload formatter (TTN).

function decodeUplink(input) {
  var b = input.bytes;
  var data = {};
  var i = 0;
  while (i < b.length - 1) {
    var ch = b[i], type = b[i + 1];
    i += 2;
    if (ch === 0x01 && type === 0x75) {          // battery, %
      data.battery_pct = b[i]; i += 1;
    } else if (ch === 0x03 && type === 0x67) {   // temperature, int16 LE, 0.1 C
      var t = b[i] | (b[i + 1] << 8);
      if (t >= 0x8000) t -= 0x10000;
      data.temperature_c = t / 10; i += 2;
    } else if (ch === 0x04 && type === 0x68) {   // humidity, 0.5 %
      data.humidity_pct = b[i] / 2; i += 1;
    } else {
      break; // unknown channel/type: stop to avoid misreading offsets
    }
  }
  return { data: data };
}

The Milesight EM300-TH reports on FPort 85 using Milesight’s channel/type TLV format: each measurement is announced by a channel byte and a type byte, followed by its value. Because of that, uplinks have variable length and fields can appear in any order — a robust decoder walks the buffer instead of using fixed offsets.

Points that commonly break third-party decoders:

  • Values are little-endian, unlike most LoRaWAN sensors. The temperature bytes 10 01 mean 0x0110 (272), not 0x1001 (4097).
  • Temperature is signed. Cold-chain deployments will produce negative values; two’s complement handling is required.
  • Unknown channels should stop the parser. Milesight devices can emit additional TLV fields (device info, history). Skipping bytes blindly after an unknown header corrupts every following field, so the codec on this page stops instead.

References

  • Milesight EM300 series payload documentation — milesight-iot.com
  • Milesight publishes official decoders on GitHub; this page adds the byte-level explanation and a minimal, readable implementation.