|
| 1 | +/** |
| 2 | + * Payload Decoder |
| 3 | + * |
| 4 | + * Copyright 2025 Milesight IoT |
| 5 | + * |
| 6 | + * @product AM102L |
| 7 | + */ |
| 8 | +var RAW_VALUE = 0x00; |
| 9 | + |
| 10 | +/* eslint no-redeclare: "off" */ |
| 11 | +/* eslint-disable */ |
| 12 | +// Chirpstack v4 |
| 13 | +function decodeUplink(input) { |
| 14 | + var decoded = milesightDeviceDecode(input.bytes); |
| 15 | + return { data: decoded }; |
| 16 | +} |
| 17 | + |
| 18 | +// Chirpstack v3 |
| 19 | +function Decode(fPort, bytes) { |
| 20 | + return milesightDeviceDecode(bytes); |
| 21 | +} |
| 22 | + |
| 23 | +// The Things Network |
| 24 | +function Decoder(bytes, port) { |
| 25 | + return milesightDeviceDecode(bytes); |
| 26 | +} |
| 27 | +/* eslint-enable */ |
| 28 | + |
| 29 | +function milesightDeviceDecode(bytes) { |
| 30 | + var decoded = {}; |
| 31 | + |
| 32 | + for (var i = 0; i < bytes.length; ) { |
| 33 | + var channel_id = bytes[i++]; |
| 34 | + var channel_type = bytes[i++]; |
| 35 | + |
| 36 | + // IPSO VERSION |
| 37 | + if (channel_id === 0xff && channel_type === 0x01) { |
| 38 | + decoded.ipso_version = readProtocolVersion(bytes[i]); |
| 39 | + i += 1; |
| 40 | + } |
| 41 | + // HARDWARE VERSION |
| 42 | + else if (channel_id === 0xff && channel_type === 0x09) { |
| 43 | + decoded.hardware_version = readHardwareVersion(bytes.slice(i, i + 2)); |
| 44 | + i += 2; |
| 45 | + } |
| 46 | + // FIRMWARE VERSION |
| 47 | + else if (channel_id === 0xff && channel_type === 0x0a) { |
| 48 | + decoded.firmware_version = readFirmwareVersion(bytes.slice(i, i + 2)); |
| 49 | + i += 2; |
| 50 | + } |
| 51 | + // TSL VERSION |
| 52 | + else if (channel_id === 0xff && channel_type === 0xff) { |
| 53 | + decoded.tsl_version = readTslVersion(bytes.slice(i, i + 2)); |
| 54 | + i += 2; |
| 55 | + } |
| 56 | + // SERIAL NUMBER |
| 57 | + else if (channel_id === 0xff && channel_type === 0x16) { |
| 58 | + decoded.sn = readSerialNumber(bytes.slice(i, i + 8)); |
| 59 | + i += 8; |
| 60 | + } |
| 61 | + // LORAWAN CLASS TYPE |
| 62 | + else if (channel_id === 0xff && channel_type === 0x0f) { |
| 63 | + decoded.lorawan_class = readLoRaWANClass(bytes[i]); |
| 64 | + i += 1; |
| 65 | + } |
| 66 | + // RESET EVENT |
| 67 | + else if (channel_id === 0xff && channel_type === 0xfe) { |
| 68 | + decoded.reset_event = readResetEvent(1); |
| 69 | + i += 1; |
| 70 | + } |
| 71 | + // DEVICE STATUS |
| 72 | + else if (channel_id === 0xff && channel_type === 0x0b) { |
| 73 | + decoded.device_status = readDeviceStatus(1); |
| 74 | + i += 1; |
| 75 | + } |
| 76 | + |
| 77 | + // BATTERY |
| 78 | + else if (channel_id === 0x01 && channel_type === 0x75) { |
| 79 | + decoded.battery = readUInt8(bytes[i]); |
| 80 | + i += 1; |
| 81 | + } |
| 82 | + // TEMPERATURE |
| 83 | + else if (channel_id === 0x03 && channel_type === 0x67) { |
| 84 | + // °C |
| 85 | + decoded.temperature = readInt16LE(bytes.slice(i, i + 2)) / 10; |
| 86 | + i += 2; |
| 87 | + } |
| 88 | + // HUMIDITY |
| 89 | + else if (channel_id === 0x04 && channel_type === 0x68) { |
| 90 | + decoded.humidity = readUInt8(bytes[i]) / 2; |
| 91 | + i += 1; |
| 92 | + } |
| 93 | + // HISTORY DATA |
| 94 | + else if (channel_id === 0x20 && channel_type === 0xce) { |
| 95 | + var data = {}; |
| 96 | + data.timestamp = readUInt32LE(bytes.slice(i, i + 4)); |
| 97 | + data.temperature = readInt16LE(bytes.slice(i + 4, i + 6)) / 10; |
| 98 | + data.humidity = readUInt8(bytes[i + 6]) / 2; |
| 99 | + i += 7; |
| 100 | + decoded.history = decoded.history || []; |
| 101 | + decoded.history.push(data); |
| 102 | + } |
| 103 | + // SENSOR ENABLE |
| 104 | + else if (channel_id === 0xff && channel_type === 0x18) { |
| 105 | + // skip 1 byte |
| 106 | + var data = readUInt8(bytes[i + 1]); |
| 107 | + var sensor_bit_offset = { temperature: 0, humidity: 1 }; |
| 108 | + decoded.sensor_enable = {}; |
| 109 | + for (var key in sensor_bit_offset) { |
| 110 | + decoded.sensor_enable[key] = readEnableStatus((data >> sensor_bit_offset[key]) & 0x01); |
| 111 | + } |
| 112 | + i += 2; |
| 113 | + } |
| 114 | + // DOWNLINK RESPONSE |
| 115 | + else if (channel_id === 0xfe || channel_id === 0xff) { |
| 116 | + var result = handle_downlink_response(channel_type, bytes, i); |
| 117 | + decoded = Object.assign(decoded, result.data); |
| 118 | + i = result.offset; |
| 119 | + } else { |
| 120 | + break; |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + return decoded; |
| 125 | +} |
| 126 | + |
| 127 | +function handle_downlink_response(channel_type, bytes, offset) { |
| 128 | + var decoded = {}; |
| 129 | + |
| 130 | + switch (channel_type) { |
| 131 | + case 0x03: |
| 132 | + decoded.report_interval = readUInt16LE(bytes.slice(offset, offset + 2)); |
| 133 | + offset += 2; |
| 134 | + break; |
| 135 | + case 0x06: |
| 136 | + decoded.temperature_alarm_config = {}; |
| 137 | + var condition = readUInt8(bytes[offset]); |
| 138 | + decoded.temperature_alarm_config.condition = readMathCondition(condition & 0x07); |
| 139 | + decoded.temperature_alarm_config.threshold_min = readInt16LE(bytes.slice(offset + 1, offset + 3)) / 10; |
| 140 | + decoded.temperature_alarm_config.threshold_max = readInt16LE(bytes.slice(offset + 3, offset + 5)) / 10; |
| 141 | + // skip 4 bytes |
| 142 | + offset += 9; |
| 143 | + break; |
| 144 | + case 0x10: |
| 145 | + decoded.reboot = readYesNoStatus(1); |
| 146 | + offset += 1; |
| 147 | + break; |
| 148 | + case 0x11: |
| 149 | + decoded.timestamp = readUInt32LE(bytes.slice(offset, offset + 4)); |
| 150 | + offset += 4; |
| 151 | + break; |
| 152 | + case 0x17: |
| 153 | + decoded.time_zone = readTimeZone(readInt16LE(bytes.slice(offset, offset + 2))); |
| 154 | + offset += 2; |
| 155 | + break; |
| 156 | + case 0x27: |
| 157 | + decoded.clear_history = readYesNoStatus(1); |
| 158 | + offset += 1; |
| 159 | + break; |
| 160 | + case 0x2f: |
| 161 | + decoded.led_indicator_mode = readLedIndicatorStatus(bytes[offset]); |
| 162 | + offset += 1; |
| 163 | + break; |
| 164 | + case 0x3a: |
| 165 | + var num = readUInt8(bytes[offset]); |
| 166 | + offset += 1; |
| 167 | + for (var i = 0; i < num; i++) { |
| 168 | + var report_schedule_config = {}; |
| 169 | + report_schedule_config.start_time = readUInt8(bytes[offset]) / 10; |
| 170 | + report_schedule_config.end_time = readUInt8(bytes[offset + 1]) / 10; |
| 171 | + report_schedule_config.report_interval = readUInt16LE(bytes.slice(offset + 2, offset + 4)); |
| 172 | + // skip 1 byte |
| 173 | + report_schedule_config.collection_interval = readUInt8(bytes[offset + 5]); |
| 174 | + offset += 6; |
| 175 | + decoded.report_schedule_config = decoded.report_schedule_config || []; |
| 176 | + decoded.report_schedule_config.push(report_schedule_config); |
| 177 | + } |
| 178 | + break; |
| 179 | + case 0x3b: |
| 180 | + decoded.time_sync_enable = readEnableStatus(bytes[offset]); |
| 181 | + offset += 1; |
| 182 | + break; |
| 183 | + case 0x57: |
| 184 | + decoded.clear_report_schedule = readYesNoStatus(1); |
| 185 | + offset += 1; |
| 186 | + break; |
| 187 | + case 0x59: |
| 188 | + decoded.reset_battery = readYesNoStatus(1); |
| 189 | + offset += 1; |
| 190 | + break; |
| 191 | + case 0x68: |
| 192 | + decoded.history_enable = readEnableStatus(bytes[offset]); |
| 193 | + offset += 1; |
| 194 | + break; |
| 195 | + case 0x69: |
| 196 | + decoded.retransmit_enable = readEnableStatus(bytes[offset]); |
| 197 | + offset += 1; |
| 198 | + break; |
| 199 | + case 0x6a: |
| 200 | + var interval_type = readUInt8(bytes[offset]); |
| 201 | + if (interval_type === 0) { |
| 202 | + decoded.retransmit_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3)); |
| 203 | + } else if (interval_type === 1) { |
| 204 | + decoded.resend_interval = readUInt16LE(bytes.slice(offset + 1, offset + 3)); |
| 205 | + } |
| 206 | + offset += 3; |
| 207 | + break; |
| 208 | + default: |
| 209 | + throw new Error("unknown downlink response"); |
| 210 | + } |
| 211 | + |
| 212 | + return { data: decoded, offset: offset }; |
| 213 | +} |
| 214 | + |
| 215 | +function readProtocolVersion(bytes) { |
| 216 | + var major = (bytes & 0xf0) >> 4; |
| 217 | + var minor = bytes & 0x0f; |
| 218 | + return "v" + major + "." + minor; |
| 219 | +} |
| 220 | + |
| 221 | +function readHardwareVersion(bytes) { |
| 222 | + var major = (bytes[0] & 0xff).toString(16); |
| 223 | + var minor = (bytes[1] & 0xff) >> 4; |
| 224 | + return "v" + major + "." + minor; |
| 225 | +} |
| 226 | + |
| 227 | +function readFirmwareVersion(bytes) { |
| 228 | + var major = (bytes[0] & 0xff).toString(16); |
| 229 | + var minor = (bytes[1] & 0xff).toString(16); |
| 230 | + return "v" + major + "." + minor; |
| 231 | +} |
| 232 | + |
| 233 | +function readTslVersion(bytes) { |
| 234 | + var major = bytes[0] & 0xff; |
| 235 | + var minor = bytes[1] & 0xff; |
| 236 | + return "v" + major + "." + minor; |
| 237 | +} |
| 238 | + |
| 239 | +function readSerialNumber(bytes) { |
| 240 | + var temp = []; |
| 241 | + for (var idx = 0; idx < bytes.length; idx++) { |
| 242 | + temp.push(("0" + (bytes[idx] & 0xff).toString(16)).slice(-2)); |
| 243 | + } |
| 244 | + return temp.join(""); |
| 245 | +} |
| 246 | + |
| 247 | +function readLoRaWANClass(type) { |
| 248 | + var class_map = { |
| 249 | + 0: "Class A", |
| 250 | + 1: "Class B", |
| 251 | + 2: "Class C", |
| 252 | + 3: "Class CtoB", |
| 253 | + }; |
| 254 | + return getValue(class_map, type); |
| 255 | +} |
| 256 | + |
| 257 | +function readResetEvent(status) { |
| 258 | + var status_map = { 0: "normal", 1: "reset" }; |
| 259 | + return getValue(status_map, status); |
| 260 | +} |
| 261 | + |
| 262 | +function readDeviceStatus(status) { |
| 263 | + var status_map = { 0: "off", 1: "on" }; |
| 264 | + return getValue(status_map, status); |
| 265 | +} |
| 266 | + |
| 267 | +function readYesNoStatus(status) { |
| 268 | + var status_map = { 0: "no", 1: "yes" }; |
| 269 | + return getValue(status_map, status); |
| 270 | +} |
| 271 | + |
| 272 | +function readEnableStatus(status) { |
| 273 | + var status_map = { 0: "disable", 1: "enable" }; |
| 274 | + return getValue(status_map, status); |
| 275 | +} |
| 276 | + |
| 277 | +function readTimeZone(time_zone) { |
| 278 | + 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" }; |
| 279 | + return getValue(timezone_map, time_zone); |
| 280 | +} |
| 281 | + |
| 282 | +function readLedIndicatorStatus(status) { |
| 283 | + var status_map = { 0: "off", 2: "blink" }; |
| 284 | + return getValue(status_map, status); |
| 285 | +} |
| 286 | + |
| 287 | +function readMathCondition(type) { |
| 288 | + var condition_map = { 0: "disable", 1: "below", 2: "above", 3: "between", 4: "outside" }; |
| 289 | + return getValue(condition_map, type); |
| 290 | +} |
| 291 | + |
| 292 | +/* eslint-disable */ |
| 293 | +function readUInt8(bytes) { |
| 294 | + return bytes & 0xff; |
| 295 | +} |
| 296 | + |
| 297 | +function readInt8(bytes) { |
| 298 | + var ref = readUInt8(bytes); |
| 299 | + return ref > 0x7f ? ref - 0x100 : ref; |
| 300 | +} |
| 301 | + |
| 302 | +function readUInt16LE(bytes) { |
| 303 | + var value = (bytes[1] << 8) + bytes[0]; |
| 304 | + return value & 0xffff; |
| 305 | +} |
| 306 | + |
| 307 | +function readInt16LE(bytes) { |
| 308 | + var ref = readUInt16LE(bytes); |
| 309 | + return ref > 0x7fff ? ref - 0x10000 : ref; |
| 310 | +} |
| 311 | + |
| 312 | +function readUInt32LE(bytes) { |
| 313 | + var value = (bytes[3] << 24) + (bytes[2] << 16) + (bytes[1] << 8) + bytes[0]; |
| 314 | + return (value & 0xffffffff) >>> 0; |
| 315 | +} |
| 316 | + |
| 317 | +function readInt32LE(bytes) { |
| 318 | + var ref = readUInt32LE(bytes); |
| 319 | + return ref > 0x7fffffff ? ref - 0x100000000 : ref; |
| 320 | +} |
| 321 | + |
| 322 | +function getValue(map, key) { |
| 323 | + if (RAW_VALUE) return key; |
| 324 | + |
| 325 | + var value = map[key]; |
| 326 | + if (!value) value = "unknown"; |
| 327 | + return value; |
| 328 | +} |
| 329 | + |
| 330 | +//if (!Object.assign) { |
| 331 | +Object.defineProperty(Object, "assign", { |
| 332 | + enumerable: false, |
| 333 | + configurable: true, |
| 334 | + writable: true, |
| 335 | + value: function (target) { |
| 336 | + "use strict"; |
| 337 | + if (target == null) { |
| 338 | + throw new TypeError("Cannot convert first argument to object"); |
| 339 | + } |
| 340 | + |
| 341 | + var to = Object(target); |
| 342 | + for (var i = 1; i < arguments.length; i++) { |
| 343 | + var nextSource = arguments[i]; |
| 344 | + if (nextSource == null) { |
| 345 | + continue; |
| 346 | + } |
| 347 | + nextSource = Object(nextSource); |
| 348 | + |
| 349 | + var keysArray = Object.keys(Object(nextSource)); |
| 350 | + for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) { |
| 351 | + var nextKey = keysArray[nextIndex]; |
| 352 | + var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey); |
| 353 | + if (desc !== undefined && desc.enumerable) { |
| 354 | + // concat array |
| 355 | + if (Array.isArray(to[nextKey]) && Array.isArray(nextSource[nextKey])) { |
| 356 | + to[nextKey] = to[nextKey].concat(nextSource[nextKey]); |
| 357 | + } else { |
| 358 | + to[nextKey] = nextSource[nextKey]; |
| 359 | + } |
| 360 | + } |
| 361 | + } |
| 362 | + } |
| 363 | + return to; |
| 364 | + }, |
| 365 | +}); |
| 366 | +//} |
0 commit comments