Milesight UC100: Modbus RS485 to LoRaWAN Converter

Milesight UC100 Modbus-to-LoRaWAN converter: read up to 32 Modbus RTU devices over RS485, own ChirpStack decoder framework and downlink control.

Milesight UC100
UC100Controller
LoRaWAN
Class A, OTAA
Fieldbus
RS485 Modbus RTU
Modbus devices
Up to 32 RTU slaves polled
History buffer
Up to 800 records, retransmission
Power
5 V DC (USB) or 8 to 28 V DC
Configuration
USB + Milesight ToolBox
Mounting
DIN rail or wall mount
Capabilities

What the UC100 does

Modbus channels

Up to 32 register maps: coils, discrete, input and holding registers.

Data types

INT16, UINT16, INT32, UINT32, float and on/off, all byte orders.

History records

Stores up to 800 sets locally and retransmits after an outage.

Read-error flags

Per-channel alarm when a Modbus slave fails to respond.

Modbus transparent mode

Pass raw Modbus frames over LoRaWAN for custom registers.

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 = { modbus: {}, info: {} };

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

    // Modbus channel value (ff 19): channel id (1) + data length (1)
    //                              + data def (1) + value
    if (channel === 0xff && type === 0x19) {
      var chId = bytes[i++] + 1;            // 1-based channel index
      i++;                                  // skip data length byte
      var dataDef = bytes[i++];
      var sign = (dataDef >>> 7) & 0x01;    // high bit = signed
      var dataType = dataDef & 0x7f;        // low 7 bits = register type
      var read = readModbusValue(bytes, i, dataType, sign);
      data.modbus["chn" + chId] = read.value;
      i += read.size;
      continue;
    }

    // Modbus read error (ff 15): channel id (1)
    if (channel === 0xff && type === 0x15) {
      var errId = bytes[i++] + 1;
      data.modbus["chn" + errId + "_error"] = true;
      continue;
    }

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

    // Unknown / config-specific segment: stop to avoid misreads
    break;
  }
  return { data: data };
}

function readModbusValue(b, i, dataType, sign) {
  // 0,1 coil/discrete -> 1 byte on/off; 2,3 16-bit; 4,6 32-bit;
  // 5,7 float; 8-11 16-bit value carried in a 4-byte slot
  if (dataType <= 1) {
    return { value: b[i] ? 1 : 0, size: 1 };
  }
  if (dataType === 2 || dataType === 3) {
    return { value: sign ? readInt16LE(b, i) : readUInt16LE(b, i), size: 2 };
  }
  if (dataType === 5 || dataType === 7) {
    return { value: readFloatLE(b, i), size: 4 };
  }
  if (dataType >= 8 && dataType <= 11) {
    return { value: sign ? readInt16LE(b, i) : readUInt16LE(b, i), size: 4 };
  }
  return { value: sign ? readInt32LE(b, i) : readUInt32LE(b, i), size: 4 };
}

function readUInt16LE(b, i) { return (b[i + 1] << 8) | b[i]; }
function readInt16LE(b, i) {
  var v = readUInt16LE(b, i);
  return v > 0x7fff ? v - 0x10000 : v;
}
function readUInt32LE(b, i) {
  return ((b[i + 3] << 24) | (b[i + 2] << 16) | (b[i + 1] << 8) | b[i]) >>> 0;
}
function readInt32LE(b, i) {
  var v = readUInt32LE(b, i);
  return v > 0x7fffffff ? v - 0x100000000 : v;
}
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 deviceInfoLen(type) {
  // 0xFF segment lengths are firmware-specific; tune per deployment
  if (type === 0x16) { return 8; } // serial number
  if (type === 0x09 || type === 0x0a || type === 0xff) { return 2; }
  return 1;
}

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

The UC100 payload is configuration-dependent: each Modbus channel carries a channel id, a data length and a data-type byte (high bit signed, low 7 bits register type), then the value. The same data type covers coils, 16/32-bit integers and floats in any byte order. This is a framework, not a drop-in, since the register-to-channel mapping is set per deployment. As a Class A device the UC100 also accepts downlinks (after an uplink) to write registers or trigger Modbus commands. Implemented from the published Milesight byte specification.

From the field

Configuration & pitfalls

Power and class

The UC100 is a Class A device powered over USB (5 V) or an 8 to 28 V DC terminal. Plan a stable supply on the RS485 cabinet rather than battery.

RS485 wiring

Match baud rate, parity and slave addresses to the field devices, and add termination on long or fast bus runs.

Modbus poll interval

Choose a realistic polling interval for the number of registers, or uplinks queue up and drain battery on downstream nodes.

History and retransmission

The 800-record buffer fills history-mode uplinks after an outage. Dashboard rules must handle batched timestamped records, not only live values.

Your partner

How merkaio supports your UC100

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

Pre-staging & provisioning

We configure the UC100, 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

It is a compact Modbus RS485 to LoRaWAN converter. It polls up to 32 Modbus RTU slaves, such as energy meters and PLCs, and forwards the readings over LoRaWAN, so wired field devices reach a dashboard without local backhaul.
Yes. It is a standard LoRaWAN Class A device, no Milesight gateway or cloud required. You add the codec to the device profile and provision it via OTAA.
Each Modbus channel carries a data-type byte that tells the decoder whether the value is a coil, a 16/32-bit integer or a float. Which registers map to which channel is set during configuration, so the decoder is a framework we build and validate against a real uplink from your deployment, not a fixed drop-in.
Up to 32 Modbus RTU slaves over a single RS485 bus, with configurable registers per device. It also supports Modbus transparent transmission for custom registers.
Yes. It stores up to 800 sets of historical records locally and retransmits them once the LoRaWAN link is back, so data is not lost during a network outage.
Yes. As a Class A device it accepts downlinks in the receive window after an uplink, so you can write Modbus registers or trigger actions. We build and validate the commands for your setup.
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.