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.
- 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)
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.
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.
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.
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.
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
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.
Your contact
Timo Wevelsiep
Founder, merkaio
15 minutes, no commitment, directly with Timo.
Decoder for ChirpStack v4. merkaio is an independent integrator and is not affiliated with Milesight.