Milesight EM310-UDL Payload Decoder

Decode Milesight EM310-UDL LoRaWAN payloads online: battery, ultrasonic distance and tilt position from the channel/type TLV format, plus the join-time device info frame. Includes payload structure, verified examples and a decodeUplink codec for ChirpStack v4 and The Things Stack.

Type: Ultrasonic distance / level sensor (IP67) LoRaWAN class: Class A FPort: 85 Payload: Variable (TLV) Endianness: Little-endian (values)

Example payloads

01755C03824408040001 Battery 92 % · distance 2116 mm · position: tilt (official Milesight test vector)
01756203824408040000 Battery 98 % · distance 2116 mm · position: normal (example from the user guide)
ff0bffff0101ff166713b31056670013ff090100ff0a0100ff0f00 Join-time device info: serial number, versions and Class A (from the user guide)

Payload structure

Bytes Field Type / scale Description
01 75 + 1 byte Battery uint8 → % Battery level in percent. Example: 0x5C = 92 %.
03 82 + 2 bytes Distance uint16 LE → mm Little-endian. Example: bytes 44 08 = 0x0844 = 2116 mm. Readings ≤ 30 mm are clamped to 30; a value of 0 means out of range (≥ 450 cm).
04 00 + 1 byte Device position uint8 enum 0 = normal (offset < 20°), 1 = tilt (offset ≥ 20°). Useful to detect a knocked-over or badly mounted sensor.
FF xx + n bytes Device info (join frame) TLV block Sent once when the device joins: protocol/hardware/software versions, serial number, LoRaWAN class, power-on event. This decoder parses them instead of failing.

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 = b[i]; i += 1;
    } else if (ch === 0x03 && type === 0x82) {   // distance, uint16 LE, mm
      data.distance = b[i] | (b[i + 1] << 8); i += 2;
    } else if (ch === 0x04 && type === 0x00) {   // device position
      data.position = b[i] === 0 ? "normal" : "tilt"; i += 1;
    } else {
      break; // device-info TLVs (0xFF ...) or unknown: stop safely
    }
  }
  return { data: data };
}

The Milesight EM310-UDL measures distance with an ultrasonic transducer and is widely used for tank level, waste-bin fill and silo monitoring. It reports on FPort 85 using Milesight’s channel/type TLV format: each field is announced by a channel byte and a type byte, followed by its value.

Details that matter when decoding it:

  • Distance is little-endian. The bytes 44 08 mean 0x0844 = 2116 mm, not 0x4408 = 17416.
  • Two special distance values. Readings at or below 30 mm are clamped to 30 (blind zone), and a distance of 0 means the target is out of range (beyond ~450 cm). Treat 0 as “no echo”, not as an empty tank.
  • The join frame is different. Right after joining the network the device sends a block of device-info TLVs on channel 0xFF (serial number, versions, class). A decoder that only expects sensor TLVs will corrupt every field — this page’s decoder parses them properly.
  • Position flag. Channel 0x04 reports whether the sensor is mounted level (normal) or tilted ≥ 20°, which invalidates distance readings.

References

  • Milesight EM310-UDL user guide (payload chapter, pages 14–16) — resource.milesight.com
  • Official decoder: github.com/Milesight-IoT/SensorDecoders, em-series/em310-udl