diff --git a/vendors/milesight/codecs/em500-pt100.js b/vendors/milesight/codecs/em500-pt100.js new file mode 100644 index 0000000..538029c --- /dev/null +++ b/vendors/milesight/codecs/em500-pt100.js @@ -0,0 +1,430 @@ +/** + * Payload Decoder + * + * Copyright 2025 Milesight IoT + * + * @product EM500-PT100 + */ +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 */ + +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 = readDeviceStatus(1); + i += 1; + } + + // BATTERY + else if (channel_id === 0x01 && channel_type === 0x75) { + decoded.battery = readUInt8(bytes[i]); + i += 1; + } + // TEMPERATURE + else if (channel_id === 0x03 && channel_type === 0x67) { + // °C + decoded.temperature = readInt16LE(bytes.slice(i, i + 2)) / 10; + i += 2; + } + // TEMPERATURE MUTATION ALARM + else if (channel_id === 0x83 && channel_type === 0xd7) { + decoded.temperature = readInt16LE(bytes.slice(i, i + 2)) / 10; + decoded.temperature_mutation = readInt16LE(bytes.slice(i + 2, i + 4)) / 10; + decoded.temperature_alarm = readTemperatureAlarm(bytes[i + 4]); + i += 5; + } + // MEASURING EQUIPMENT + else if (channel_id === 0xff && channel_type === 0x1b) { + decoded.measuring_equipment = {}; + decoded.measuring_equipment.rate = readMeasuringRate(bytes[i] & 0x07); + decoded.measuring_equipment.range_max = readInt16LE(bytes.slice(i + 1, i + 3)); + decoded.measuring_equipment.range_min = readInt16LE(bytes.slice(i + 3, i + 5)); + i += 5; + } + // HISTORY + else if (channel_id === 0x20 && channel_type === 0xce) { + var data = {}; + data.timestamp = readUInt32LE(bytes.slice(i, i + 4)); + data.temperature = readInt16LE(bytes.slice(i + 4, i + 6)) / 10; + i += 6; + 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 0x06: + var data = readUInt8(bytes[offset]); + var condition = data & 0x07; + var id = (data >> 3) & 0x07; + var enable = (data >> 6) & 0x01; + + // temperature alarm + if (id === 1) { + decoded.temperature_alarm_config = {}; + decoded.temperature_alarm_config.enable = readEnableStatus(enable); + decoded.temperature_alarm_config.condition = readConditionType(condition); + decoded.temperature_alarm_config.threshold_min = readInt16LE(bytes.slice(offset + 1, offset + 3)) / 10; + decoded.temperature_alarm_config.threshold_max = readInt16LE(bytes.slice(offset + 3, offset + 5)) / 10; + } + // temperature mutation alarm + else if (id === 2) { + decoded.temperature_mutation_alarm_config = {}; + decoded.temperature_mutation_alarm_config.enable = readEnableStatus(enable); + decoded.temperature_mutation_alarm_config.mutation = readInt16LE(bytes.slice(offset + 3, offset + 5)) / 10; + } + offset += 9; + 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 0x1c: + decoded.recollection_config = {}; + decoded.recollection_config.counts = readUInt8(bytes[offset]); + decoded.recollection_config.interval = readUInt8(bytes[offset + 1]); + offset += 2; + break; + case 0x27: + decoded.clear_history = readYesNoStatus(1); + offset += 1; + break; + case 0x28: + decoded.report_status = readYesNoStatus(1); + offset += 1; + break; + case 0x35: + decoded.d2d_key = readHexString(bytes.slice(offset, offset + 8)); + offset += 8; + break; + case 0x3b: + decoded.time_sync_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x4a: + decoded.sync_time = 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 interval_type = readUInt8(bytes[offset]); + switch (interval_type) { + case 0: + decoded.retransmit_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3)); + break; + case 1: + decoded.resend_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3)); + break; + } + offset += 3; + break; + case 0x84: + decoded.d2d_enable = readEnableStatus(bytes[offset]); + offset += 1; + break; + case 0x96: + var d2d_master_config = {}; + d2d_master_config.mode = readD2DMode(bytes[offset]); + d2d_master_config.enable = readEnableStatus(bytes[offset + 1]); + d2d_master_config.lora_uplink_enable = readEnableStatus(bytes[offset + 2]); + d2d_master_config.d2d_cmd = readD2DCommand(bytes.slice(offset + 3, offset + 5)); + offset += 8; + decoded.d2d_master_config = []; + decoded.d2d_master_config.push(d2d_master_config); + break; + case 0xf1: + decoded.temperature_calibration_settings = {}; + // skip 1 byte + decoded.temperature_calibration_settings.enable = readEnableStatus(bytes[offset + 1]); + decoded.temperature_calibration_settings.calibration_value = readInt16LE(bytes.slice(offset + 2, offset + 4)) / 10; + offset += 4; + break; + case 0xf2: + decoded.alarm_report_counts = readUInt16LE(bytes.slice(offset, offset + 2)); + offset += 2; + break; + case 0xf5: + decoded.alarm_release_enable = readEnableStatus(bytes[offset]); + offset += 1; + 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 readDeviceStatus(status) { + var status_map = { 0: "off", 1: "on" }; + return getValue(status_map, status); +} + +function readYesNoStatus(status) { + var status_map = { 0: "no", 1: "yes" }; + return getValue(status_map, status); +} + +function readEnableStatus(status) { + var status_map = { 0: "disable", 1: "enable" }; + 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 readMeasuringRate(rate) { + var rate_map = { 0: "0.01", 1: "0.1", 2: "1", 3: "10", 4: "100", 5: "1000" }; + return getValue(rate_map, rate); +} + +function readTemperatureAlarm(type) { + var type_map = { + 0: "threshold alarm", + 1: "threshold alarm release", + 2: "mutation alarm", + }; + return getValue(type_map, type); +} + +function readConditionType(condition) { + var condition_map = { 0: "disable", 1: "below", 2: "above", 3: "between", 4: "outside", 5: "mutation" }; + return getValue(condition_map, condition); +} + +function readD2DMode(mode) { + var mode_map = { 1: "threshold_alarm", 2: "threshold_alarm_release", 3: "mutation_alarm" }; + return getValue(mode_map, mode); +} + +function readD2DCommand(bytes) { + return ("0" + (bytes[1] & 0xff).toString(16)).slice(-2) + ("0" + (bytes[0] & 0xff).toString(16)).slice(-2); +} + +function readHexString(bytes) { + var temp = []; + for (var idx = 0; idx < bytes.length; idx++) { + temp.push(("0" + (bytes[idx] & 0xff).toString(16)).slice(-2)); + } + return temp.join(""); +} + +/* 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) >>> 0; +} + +function readInt32LE(bytes) { + var ref = readUInt32LE(bytes); + return ref > 0x7fffffff ? ref - 0x100000000 : ref; +} + +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; + }, + }); +//} diff --git a/vendors/milesight/codecs/test_decode_em500-pt100.json b/vendors/milesight/codecs/test_decode_em500-pt100.json new file mode 100644 index 0000000..5977db2 --- /dev/null +++ b/vendors/milesight/codecs/test_decode_em500-pt100.json @@ -0,0 +1,14 @@ +[ + { + "name": "Test decode Milesight EM500-PT100", + "input": { + "bytes": [1, 117, 100, 3, 103, 44, 1] + }, + "expected": { + "data": { + "battery": 100, + "temperature": 30 + } + } + } +] diff --git a/vendors/milesight/codecs/test_encode_em500-pt100.json b/vendors/milesight/codecs/test_encode_em500-pt100.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/vendors/milesight/codecs/test_encode_em500-pt100.json @@ -0,0 +1 @@ +[] diff --git a/vendors/milesight/devices/milesight-em500-pt100.toml b/vendors/milesight/devices/milesight-em500-pt100.toml new file mode 100644 index 0000000..b921e04 --- /dev/null +++ b/vendors/milesight/devices/milesight-em500-pt100.toml @@ -0,0 +1,18 @@ +[device] +id = "6520e342-96dd-4366-842d-63b5bd800acb" +name = "Milesight EM500-PT100" +description = "Industrial Temperature Sensor (PT100)" + +[[device.firmware]] +version = "1.0" +profiles = [ + "AS923-1_0_3.toml", + "AU915-1_0_3.toml", + "EU868-1_0_3.toml", + "US915-1_0_3.toml", +] +codec = "em500-pt100.js" + +[device.metadata] +product_url = "https://www.milesight.com/iot/product/lorawan-sensor/em500-pt100" +documentation_url = "https://www.milesight.com/iot/product/lorawan-sensor/em500-pt100" diff --git a/vendors/milesight/vendor.toml b/vendors/milesight/vendor.toml index 6929c60..923fe3e 100644 --- a/vendors/milesight/vendor.toml +++ b/vendors/milesight/vendor.toml @@ -12,6 +12,7 @@ devices = [ "milesight-em300-sld-zld.toml", "milesight-em310-tilt.toml", "milesight-em320-th.toml", + "milesight-em500-pt100.toml", ] [vendor.metadata]