Milesight UC501: Multi-interface SDI-12 LoRaWAN Controller

Milesight UC501 multi-interface LoRaWAN controller: SDI-12, RS485 Modbus, analog and GPIO inputs, own decoder framework and ChirpStack integration.

Milesight UC501
UC501Controller
LoRaWAN
Class A / Class C, OTAA
SDI-12
Up to multiple SDI-12 sensors
Fieldbus
RS485 Modbus RTU
Inputs
GPIO (DI/DO/pulse) + analog (4-20 mA / 0-10 V)
Enclosure
IP67, outdoor rated
Power
Battery, solar or external DC
Configuration
NFC / USB (Milesight ToolBox)
Capabilities

What the UC501 does

SDI-12 sensors

Weather, hydrological and agricultural probes read as ASCII strings.

RS485 Modbus RTU

Energy meters, PLCs and industrial Modbus sensors.

Analog inputs (AI)

4-20 mA / 0-10 V with min, max and average values.

GPIO

Digital input, digital output or pulse counter per channel.

History buffer

Stores readings locally and retransmits after a network gap.

Data into your dashboard

Integration

Sensor / controller

Measures or controls in the field and sends LoRaWAN uplinks.

LoRaWAN gateway

Receives the radio packets and forwards them to the server.

ChirpStack

Network server: manages sessions and decodes the payload.

ThingsBoard / Grafana

Dashboards, alarms, rules and reports.

ChirpStack v4 · decodeUplink
function decodeUplink(input) {
  var bytes = input.bytes;
  var data = { gpio: {}, analog: {}, sdi12: {}, modbus: {} };

  for (var i = 0; i < bytes.length; ) {
    var channel = bytes[i++];
    var type = bytes[i++];

    // Modbus: register id + package type, then 1/2/4 bytes by data type.
    // Checked before the 0xff device-info catch-all so the type byte 0x0e
    // is not swallowed by the generic 0xff branch (channel 0x80 or 0xff).
    if ((channel === 0x80 || channel === 0xff) && type === 0x0e) {
      var reg = bytes[i++] - 6;
      var dataType = bytes[i++] & 0x07;
      data.modbus["chn" + reg] = readModbusValue(bytes, i, dataType);
      i += modbusLen(dataType); continue;
    }

    // Device info (join / power-on): version, SN, class. Lengths are firmware-specific.
    if (channel === 0xff) { i += deviceInfoLen(type); continue; }

    // Battery (%)
    if (channel === 0x01 && type === 0x75) { data.battery = bytes[i]; i += 1; continue; }

    // GPIO as digital input / output: 1 byte (0/1)
    if ((channel === 0x03 || channel === 0x04) && (type === 0x00 || type === 0x01)) {
      data.gpio["ch" + channel] = bytes[i]; i += 1; continue;
    }

    // GPIO as pulse counter: UINT32 little-endian (4 bytes)
    if ((channel === 0x03 || channel === 0x04) && type === 0xc8) {
      data.gpio["ch" + channel] = readUInt32LE(bytes, i); i += 4; continue;
    }

    // Analog input: value + min + max + avg, four INT16 little-endian (8 bytes)
    if ((channel === 0x05 || channel === 0x06) && type === 0x02) {
      data.analog["ch" + channel] = {
        value: readInt16LE(bytes, i) / 1000,
        min: readInt16LE(bytes, i + 2) / 1000,
        max: readInt16LE(bytes, i + 4) / 1000,
        avg: readInt16LE(bytes, i + 6) / 1000
      };
      i += 8; continue;
    }

    // SDI-12: 1 byte sensor index + 36-byte ASCII string
    if (channel === 0x08 && type === 0xdb) {
      var idx = bytes[i++];
      data.sdi12["s" + (idx + 1)] = readString(bytes, i, 36); i += 36; continue;
    }

    break; // unknown channel: stop, payload layout is configuration-dependent
  }
  return { data: data };
}

function modbusLen(t) { return (t <= 1) ? 1 : (t <= 3) ? 2 : 4; }
function readModbusValue(b, i, t) {
  if (t <= 1) return b[i];
  if (t <= 3) return readUInt16LE(b, i);
  if (t === 5 || t === 7) return readFloatLE(b, i);
  return readUInt32LE(b, i);
}
function readInt16LE(b, i) {
  var v = (b[i + 1] << 8) | b[i];
  return v > 0x7fff ? v - 0x10000 : v;
}
function readUInt16LE(b, i) { return (b[i + 1] << 8) | b[i]; }
function readUInt32LE(b, i) {
  return ((b[i+3]<<24)|(b[i+2]<<16)|(b[i+1]<<8)|b[i]) >>> 0;
}
function readFloatLE(b, i) {
  var bits = readUInt32LE(b, i);
  var sign = (bits >>> 31) ? -1 : 1;
  var exp = (bits >>> 23) & 0xff;
  var frac = bits & 0x7fffff;
  if (exp === 0) return sign * frac * Math.pow(2, -149);
  if (exp === 0xff) return frac ? NaN : sign * Infinity;
  return sign * (1 + frac * Math.pow(2, -23)) * Math.pow(2, exp - 127);
}
function readString(b, i, len) {
  var s = "";
  for (var k = 0; k < len; k++) {
    if (b[i + k] === 0) break;
    s += String.fromCharCode(b[i + k]);
  }
  return s.replace(/[^\x20-\x7e]/g, "").trim();
}
function deviceInfoLen(type) {
  void type; // 0xFF segment lengths are firmware-specific; set per deployment
  return 1;
}

Implemented from the published Milesight byte specification (Communication Protocol / User Guide).

The UC501 payload is configuration-dependent: it reflects which interfaces are enabled (SDI-12, RS485 Modbus, analog, GPIO) and how the registers are mapped. SDI-12 readings arrive as a sensor index plus a 36-byte ASCII string that you parse per probe; Modbus values arrive as a register id plus a data type that sets the length (1/2/4 bytes). This is a framework implemented from the published Milesight byte specification, not a drop-in: we finalise the decoder against a real uplink from the deployment. Analog and float scaling differs between hardware/firmware revisions (for example /1000 vs /100), so confirm it against your unit.

From the field

Configuration & pitfalls

Interface selection

SDI-12, RS485, analog and GPIO are enabled in the ToolBox; the payload only carries the channels you turn on, which is why the decoder is per-deployment.

SDI-12 addressing

Each SDI-12 probe needs a unique bus address and a defined measurement command. Document the address-to-sensor map so the ASCII strings stay traceable.

Class C power

Class C keeps the receive window open for downlinks and draws more current; for SDI-12 field sites on battery or solar, Class A with scheduled reporting usually fits better.

History retransmission

The UC501 buffers readings locally and resends them after a network gap, so dashboards should de-duplicate history frames against live uplinks.

Your partner

How merkaio supports your UC501

From sourcing to day-to-day operation, all from one partner on our own European infrastructure.

Pre-staging & provisioning

We configure the UC501, set keys, intervals and alarms, and ship it ready to deploy.

Own decoder

Payload codec for ChirpStack v4 and ThingsBoard, implemented from the Milesight specification.

Dashboard integration

Data lands in your ThingsBoard or Grafana, with alarms and reports.

Operations & monitoring

We run the LoRaWAN stack and dashboards on European infrastructure, you just use the data.

Frequently asked questions

Yes. It is a standard LoRaWAN device, no Milesight gateway or cloud required. You add the codec to the device profile and provision it via OTAA.
Yes, implemented from the published Milesight byte specification. Because the payload depends on which interfaces you enable, we deliver a decoder framework and finalise it against a real uplink from your deployment.
The payload only contains the channels you activate (SDI-12, RS485 Modbus, analog, GPIO) and reflects your register and address mapping, so the decoder is built and validated for your exact configuration rather than shipped as a fixed drop-in.
SDI-12 for environmental probes, RS485 Modbus RTU, analog inputs for 4-20 mA or 0-10 V signals, and GPIO that can act as digital input, digital output or pulse counter.
Yes. It supports battery, solar and external DC power, and the IP67 enclosure suits outdoor field sites. On battery or solar use Class A with a scheduled reporting interval to extend runtime.
Each SDI-12 channel is sent as a sensor index plus a 36-byte ASCII string carrying the raw measurement. The decoder extracts the string per probe; you then parse the fields according to your sensor's SDI-12 response format.
From the same series

Related devices

Let's discuss your infrastructure. Digital and on-site.

Whether it's IoT platform development, hardware selection, managed hosting for ChirpStack, ThingsBoard, Grafana or NetBird VPN, or migration from a self-hosted setup - we'll find the right solution for your use case. Book a free 30-minute consultation, no commitment required.

Timo Wevelsiep

Your contact

Timo Wevelsiep

Founder, merkaio

15 minutes, no commitment, directly with Timo.

By submitting, you agree to our Privacy Policy.

Decoder for ChirpStack v4. merkaio is an independent integrator and is not affiliated with Milesight.