// Décodeur LoRaWAN officiel : Milesight UC50x Series LoRaWAN® Controller (Milesight) // Source : https://github.com/TheThingsNetwork/lorawan-devices/blob/master/vendor/milesight-iot/uc50x.js // The Things Network Device Repository, fichier publié par le constructeur, distribué sans modification. // Format : decodeUplink(input) (TTN v3 / ChirpStack v4). Hébergé par smartbuildings.fr. /** * Payload Decoder * * Copyright 2025 Milesight IoT * * @product UC50x */ var RAW_VALUE = 0x00; /* eslint no-redeclare: "off" */ /* eslint-disable */ // Chirpstack v4 function decodeUplink(input) { var decoded = milesightDeviceDecode(input.bytes); return { data: decoded }; } // Chirpstack v3 function Decode(fPort, bytes) { return milesightDeviceDecode(bytes); } // The Things Network function Decoder(bytes, port) { return milesightDeviceDecode(bytes); } /* eslint-enable */ var gpio_chns = [0x03, 0x04]; var adc_chns = [0x05, 0x06]; var adc_alarm_chns = [0x85, 0x86]; function milesightDeviceDecode(bytes) { var decoded = {}; for (var i = 0; i < bytes.length; ) { var channel_id = bytes[i++]; var channel_type = bytes[i++]; // IPSO VERSION if (channel_id === 0xff && channel_type === 0x01) { decoded.ipso_version = readProtocolVersion(bytes[i]); i += 1; } // HARDWARE VERSION else if (channel_id === 0xff && channel_type === 0x09) { decoded.hardware_version = readHardwareVersion(bytes.slice(i, i + 2)); i += 2; } // FIRMWARE VERSION else if (channel_id === 0xff && channel_type === 0x0a) { decoded.firmware_version = readFirmwareVersion(bytes.slice(i, i + 2)); i += 2; } // TSL VERSION else if (channel_id === 0xff && channel_type === 0xff) { decoded.tsl_version = readTslVersion(bytes.slice(i, i + 2)); i += 2; } // SERIAL NUMBER else if (channel_id === 0xff && channel_type === 0x16) { decoded.sn = readSerialNumber(bytes.slice(i, i + 8)); i += 8; } // LORAWAN CLASS TYPE else if (channel_id === 0xff && channel_type === 0x0f) { decoded.lorawan_class = readLoRaWANClass(bytes[i]); i += 1; } // RESET EVENT else if (channel_id === 0xff && channel_type === 0xfe) { decoded.reset_event = readResetEvent(1); i += 1; } // DEVICE STATUS else if (channel_id === 0xff && channel_type === 0x0b) { decoded.device_status = readOnOffStatus(1); i += 1; } // BATTERY else if (channel_id === 0x01 && channel_type === 0x75) { decoded.battery = readUInt8(bytes[i]); i += 1; } // GPIO (Input) else if (includes(gpio_chns, channel_id) && channel_type === 0x00) { var gpio_channel_name = "gpio_input_" + (channel_id - gpio_chns[0] + 1); decoded[gpio_channel_name] = readOnOffStatus(bytes[i]); i += 1; } // GPIO (Output) else if (includes(gpio_chns, channel_id) && channel_type === 0x01) { var gpio_channel_name = "gpio_output_" + (channel_id - gpio_chns[0] + 1); decoded[gpio_channel_name] = readOnOffStatus(bytes[i]); i += 1; } // GPIO (GPIO as PULSE COUNTER) else if (includes(gpio_chns, channel_id) && channel_type === 0xc8) { var gpio_channel_name = "gpio_counter_" + (channel_id - gpio_chns[0] + 1); decoded[gpio_channel_name] = readUInt32LE(bytes.slice(i, i + 4)); i += 4; } // ANALOG INPUT TYPE else if (channel_id === 0xff && channel_type === 0x14) { var channel = bytes[i]; var chn_name = "analog_input_" + (channel >>> 4) + "_type"; decoded[chn_name] = readAnalogInputType(channel & 0x0f); i += 1; } // ADC (UC50x v2) // firmware version 1.10 and below and UC50x V1, change 1000 to 100. else if (includes(adc_chns, channel_id) && channel_type === 0x02) { var adc_channel_name = "analog_input_" + (channel_id - adc_chns[0] + 1); decoded[adc_channel_name] = readInt16LE(bytes.slice(i, i + 2)) / 1000; decoded[adc_channel_name + "_min"] = readInt16LE(bytes.slice(i + 2, i + 4)) / 1000; decoded[adc_channel_name + "_max"] = readInt16LE(bytes.slice(i + 4, i + 6)) / 1000; decoded[adc_channel_name + "_avg"] = readInt16LE(bytes.slice(i + 6, i + 8)) / 1000; i += 8; } // ADC (UC50x v3) else if (includes(adc_chns, channel_id) && channel_type === 0xe2) { var adc_channel_name = "analog_input_" + (channel_id - adc_chns[0] + 1); decoded[adc_channel_name] = readFloat16LE(bytes.slice(i, i + 2)); decoded[adc_channel_name + "_min"] = readFloat16LE(bytes.slice(i + 2, i + 4)); decoded[adc_channel_name + "_max"] = readFloat16LE(bytes.slice(i + 4, i + 6)); decoded[adc_channel_name + "_avg"] = readFloat16LE(bytes.slice(i + 6, i + 8)); i += 8; } // SDI-12 else if (channel_id === 0x08 && channel_type === 0xdb) { var name = "sdi12_" + (bytes[i++] + 1); decoded[name] = readString(bytes.slice(i, i + 36)); i += 36; } // MODBUS else if ((channel_id === 0xff || channel_id === 0x80) && channel_type === 0x0e) { var modbus_chn_id = bytes[i++] - 6; var package_type = bytes[i++]; var data_type = package_type & 0x07; // 0x07 = 0b00000111 var chn = "modbus_chn_" + modbus_chn_id; switch (data_type) { case 0: case 1: decoded[chn] = readOnOffStatus(bytes[i]); i += 1; break; case 2: case 3: decoded[chn] = readUInt16LE(bytes.slice(i, i + 2)); i += 2; break; case 4: case 6: decoded[chn] = readUInt32LE(bytes.slice(i, i + 4)); i += 4; break; case 5: case 7: decoded[chn] = readFloatLE(bytes.slice(i, i + 4)); i += 4; break; } if (channel_id === 0x80) { decoded[chn + "_alarm"] = readAlarm(bytes[i++]); } } // MODBUS READ ERROR else if (channel_id === 0xff && channel_type === 0x15) { var modbus_error_chn_id = bytes[i] - 6; var channel_name = "modbus_chn_" + modbus_error_chn_id; decoded[channel_name + "_alarm"] = "read error"; i += 1; } // ADC alert (UC50x v3) else if (includes(adc_alarm_chns, channel_id) && channel_type === 0xe2) { var adc_channel_name = "analog_input_" + (channel_id - adc_alarm_chns[0] + 1); decoded[adc_channel_name] = readFloat16LE(bytes.slice(i, i + 2)); decoded[adc_channel_name + "_min"] = readFloat16LE(bytes.slice(i + 2, i + 4)); decoded[adc_channel_name + "_max"] = readFloat16LE(bytes.slice(i + 4, i + 6)); decoded[adc_channel_name + "_avg"] = readFloat16LE(bytes.slice(i + 6, i + 8)); i += 8; decoded[adc_channel_name + "_alarm"] = readAlarm(bytes[i++]); } // HISTORY DATA (GPIO / ADC) else if (channel_id === 0x20 && channel_type === 0xdc) { var timestamp = readUInt32LE(bytes.slice(i, i + 4)); var data = { timestamp: timestamp }; var gpio_1_type = readUInt8(bytes[i + 4]); if (gpio_1_type === 0x00) { data.gpio_input_1 = readOnOffStatus(readUInt32LE(bytes.slice(i + 5, i + 9))); } else if (gpio_1_type === 0x01) { data.gpio_output_1 = readOnOffStatus(readUInt32LE(bytes.slice(i + 5, i + 9))); } else if (gpio_1_type === 0x02) { data.gpio_counter_1 = readUInt32LE(bytes.slice(i + 5, i + 9)); } var gpio_2_type = readUInt8(bytes[i + 9]); if (gpio_2_type === 0x00) { data.gpio_input_2 = readOnOffStatus(readUInt32LE(bytes.slice(i + 10, i + 14))); } else if (gpio_2_type === 0x01) { data.gpio_output_2 = readOnOffStatus(readUInt32LE(bytes.slice(i + 10, i + 14))); } else if (gpio_2_type === 0x02) { data.gpio_counter_2 = readUInt32LE(bytes.slice(i + 10, i + 14)); } data.analog_input_1 = readInt32LE(bytes.slice(i + 14, i + 18)) / 1000; data.analog_input_2 = readInt32LE(bytes.slice(i + 18, i + 22)) / 1000; i += 22; decoded.history = decoded.history || []; decoded.history.push(data); } // HISTORY DATA (SDI-12) else if (channel_id === 0x20 && channel_type === 0xe0) { var timestamp = readUInt32LE(bytes.slice(i, i + 4)); var channel_mask = numToBits(readUInt16LE(bytes.slice(i + 4, i + 6)), 16); i += 6; var data = { timestamp: timestamp }; for (var j = 0; j < channel_mask.length; j++) { // skip if channel is not enabled if (channel_mask[j] === 0) continue; var name = "sdi12_" + (j + 1); data[name] = readString(bytes.slice(i, i + 36)); i += 36; } decoded.history = decoded.history || []; decoded.history.push(data); } // HISTORY DATA (MODBUS) else if (channel_id === 0x20 && channel_type === 0xdd) { var timestamp = readUInt32LE(bytes.slice(i, i + 4)); var channel_mask = numToBits(readUInt16LE(bytes.slice(i + 4, i + 6)), 16); i += 6; var data = { timestamp: timestamp }; for (var j = 0; j < channel_mask.length; j++) { // skip if channel is not enabled if (channel_mask[j] === 0) continue; var name = "modbus_chn_" + (j + 1); var type = bytes[i++] & 0x07; // 0x07 = 0b00000111 // 5 MB_REG_HOLD_FLOAT, 7 MB_REG_INPUT_FLOAT if (type === 5 || type === 7) { data[name] = readFloatLE(bytes.slice(i, i + 4)); } else { data[name] = readUInt32LE(bytes.slice(i, i + 4)); } i += 4; } decoded.history = decoded.history || []; decoded.history.push(data); } // DOWNLINK RESPONSE else if (channel_id === 0xfe || channel_id === 0xff) { var result = handle_downlink_response(channel_type, bytes, i); decoded = Object.assign(decoded, result.data); i = result.offset; } else { break; } } return decoded; } function handle_downlink_response(channel_type, bytes, offset) { var decoded = {}; switch (channel_type) { case 0x02: decoded.collection_interval = readUInt16LE(bytes.slice(offset, offset + 2)); offset += 2; break; case 0x03: decoded.report_interval = readUInt16LE(bytes.slice(offset, offset + 2)); offset += 2; break; case 0x10: decoded.reboot = readYesNoStatus(1); offset += 1; break; case 0x11: decoded.timestamp = readUInt32LE(bytes.slice(offset, offset + 4)); offset += 4; break; case 0x17: decoded.time_zone = readTimeZone(readInt16LE(bytes.slice(offset, offset + 2))); offset += 2; break; case 0x27: decoded.clear_history = readYesNoStatus(1); offset += 1; break; case 0x28: decoded.report_status = readYesNoStatus(1); offset += 1; break; case 0x68: decoded.history_enable = readEnableStatus(bytes[offset]); offset += 1; break; case 0x69: decoded.retransmit_enable = readEnableStatus(bytes[offset]); offset += 1; break; case 0x6a: var type = readUInt8(bytes[offset]); if (type === 0x00) { decoded.retransmit_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3)); } else if (type === 0x01) { decoded.resend_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3)); } offset += 3; break; default: throw new Error("unknown downlink response"); } return { data: decoded, offset: offset }; } function readProtocolVersion(bytes) { var major = (bytes & 0xf0) >> 4; var minor = bytes & 0x0f; return "v" + major + "." + minor; } function readHardwareVersion(bytes) { var major = (bytes[0] & 0xff).toString(16); var minor = (bytes[1] & 0xff) >> 4; return "v" + major + "." + minor; } function readFirmwareVersion(bytes) { var major = (bytes[0] & 0xff).toString(16); var minor = (bytes[1] & 0xff).toString(16); return "v" + major + "." + minor; } function readTslVersion(bytes) { var major = bytes[0] & 0xff; var minor = bytes[1] & 0xff; return "v" + major + "." + minor; } function readSerialNumber(bytes) { var temp = []; for (var idx = 0; idx < bytes.length; idx++) { temp.push(("0" + (bytes[idx] & 0xff).toString(16)).slice(-2)); } return temp.join(""); } function readLoRaWANClass(type) { var class_map = { 0: "Class A", 1: "Class B", 2: "Class C", 3: "Class CtoB", }; return getValue(class_map, type); } function readResetEvent(status) { var status_map = { 0: "normal", 1: "reset" }; return getValue(status_map, status); } function readOnOffStatus(status) { var status_map = { 0: "off", 1: "on" }; return getValue(status_map, status); } function readEnableStatus(status) { var status_map = { 0: "disable", 1: "enable" }; return getValue(status_map, status); } function readYesNoStatus(status) { var status_map = { 0: "no", 1: "yes" }; return getValue(status_map, status); } function readTimeZone(time_zone) { var timezone_map = { "-120": "UTC-12", "-110": "UTC-11", "-100": "UTC-10", "-95": "UTC-9:30", "-90": "UTC-9", "-80": "UTC-8", "-70": "UTC-7", "-60": "UTC-6", "-50": "UTC-5", "-40": "UTC-4", "-35": "UTC-3:30", "-30": "UTC-3", "-20": "UTC-2", "-10": "UTC-1", 0: "UTC", 10: "UTC+1", 20: "UTC+2", 30: "UTC+3", 35: "UTC+3:30", 40: "UTC+4", 45: "UTC+4:30", 50: "UTC+5", 55: "UTC+5:30", 57: "UTC+5:45", 60: "UTC+6", 65: "UTC+6:30", 70: "UTC+7", 80: "UTC+8", 90: "UTC+9", 95: "UTC+9:30", 100: "UTC+10", 105: "UTC+10:30", 110: "UTC+11", 120: "UTC+12", 127: "UTC+12:45", 130: "UTC+13", 140: "UTC+14" }; return getValue(timezone_map, time_zone); } function readAlarm(type) { var alarm_map = { 1: "threshold alarm", 2: "value change alarm" }; return getValue(alarm_map, type); } function readAnalogInputType(type) { var type_map = { 0: "current", 1: "voltage" }; return getValue(type_map, type); } function numToBits(num, bit_count) { var bits = []; for (var i = 0; i < bit_count; i++) { bits.push((num >> i) & 1); } return bits; } /* eslint-disable */ function readUInt8(bytes) { return bytes & 0xff; } function readInt8(bytes) { var ref = readUInt8(bytes); return ref > 0x7f ? ref - 0x100 : ref; } function readUInt16LE(bytes) { var value = (bytes[1] << 8) + bytes[0]; return value & 0xffff; } function readInt16LE(bytes) { var ref = readUInt16LE(bytes); return ref > 0x7fff ? ref - 0x10000 : ref; } function readUInt32LE(bytes) { var value = (bytes[3] << 24) + (bytes[2] << 16) + (bytes[1] << 8) + bytes[0]; return value & 0xffffffff; } function readInt32LE(bytes) { var ref = readUInt32LE(bytes); return ref > 0x7fffffff ? ref - 0x100000000 : ref; } function readFloat16LE(bytes) { var bits = (bytes[1] << 8) | bytes[0]; var sign = bits >>> 15 === 0 ? 1.0 : -1.0; var e = (bits >>> 10) & 0x1f; var m = e === 0 ? (bits & 0x3ff) << 1 : (bits & 0x3ff) | 0x400; var f = sign * m * Math.pow(2, e - 25); return f; } function readFloatLE(bytes) { var bits = (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0]; var sign = bits >>> 31 === 0 ? 1.0 : -1.0; var e = (bits >>> 23) & 0xff; var m = e === 0 ? (bits & 0x7fffff) << 1 : (bits & 0x7fffff) | 0x800000; var f = sign * m * Math.pow(2, e - 150); return f; } function readString(bytes) { var str = ""; for (var i = 0; i < bytes.length; i++) { if (bytes[i] === 0) { break; } str += String.fromCharCode(bytes[i]); } return str; } function includes(data, value) { var size = data.length; for (var i = 0; i < size; i++) { if (data[i] == value) { return true; } } return false; } function getValue(map, key) { if (RAW_VALUE) return key; var value = map[key]; if (!value) value = "unknown"; return value; } //if (!Object.assign) { Object.defineProperty(Object, "assign", { enumerable: false, configurable: true, writable: true, value: function (target) { "use strict"; if (target == null) { throw new TypeError("Cannot convert first argument to object"); } var to = Object(target); for (var i = 1; i < arguments.length; i++) { var nextSource = arguments[i]; if (nextSource == null) { continue; } nextSource = Object(nextSource); var keysArray = Object.keys(Object(nextSource)); for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { var nextKey = keysArray[nextIndex]; var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); if (desc !== undefined && desc.enumerable) { // concat array if (Array.isArray(to[nextKey]) && Array.isArray(nextSource[nextKey])) { to[nextKey] = to[nextKey].concat(nextSource[nextKey]); } else { to[nextKey] = nextSource[nextKey]; } } } } return to; }, }); //}