diff --git a/vendors/digital-matter/codecs/digital-matter-g62.js b/vendors/digital-matter/codecs/digital-matter-g62.js new file mode 100644 index 0000000..75de5ca --- /dev/null +++ b/vendors/digital-matter/codecs/digital-matter-g62.js @@ -0,0 +1,235 @@ +function decodeUplink(input) { + return { + data: Decoder(input.fPort, input.bytes) + }; +} + +function Decoder(port, bytes) +{ + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + let decoded = {}; + if (bytes === null) + return null; + + if (port === 1) + { + if ((bytes.length != 17) && (bytes.length < 19)) + return null; + + decoded._type = "full data"; + + switch (bytes[0] & 0x3) + { + case 0: decoded.tripType = "None"; break; + case 1: decoded.tripType = "Ignition"; break; + case 2: decoded.tripType = "Movement"; break; + case 3: decoded.tripType = "Run Detect"; break; + } + + decoded.latitudeDeg = (bytes[0] & 0xF0) + bytes[1] * 256 + + bytes[2] * 65536 + bytes[3] * 16777216; + if (decoded.latitudeDeg >= 0x80000000) // 2^31 + decoded.latitudeDeg -= 0x100000000; // 2^32 + decoded.latitudeDeg /= 1e7; + + decoded.longitudeDeg = (bytes[4] & 0xF0) + bytes[5] * 256 + + bytes[6] * 65536 + bytes[7] * 16777216; + if (decoded.longitudeDeg >= 0x80000000) // 2^31 + decoded.longitudeDeg -= 0x100000000; // 2^32 + decoded.longitudeDeg /= 1e7; + + decoded.vExtGood = ((bytes[0] & 0x4) !== 0) ? true : false; + decoded.gpsCurrent = ((bytes[0] & 0x8) !== 0) ? true : false; + + decoded.ignition = ((bytes[4] & 0x1) !== 0) ? true : false; + decoded.digIn1 = ((bytes[4] & 0x2) !== 0) ? true : false; + decoded.digIn2 = ((bytes[4] & 0x4) !== 0) ? true : false; + decoded.digOut = ((bytes[4] & 0x8) !== 0) ? true : false; + decoded.headingDeg = bytes[8] * 2; + decoded.speedKmph = bytes[9]; + decoded.batV = bytes[10] * 0.02; + + decoded.vExt = 0.001 * (bytes[11] + bytes[12] * 256); + decoded.vAin = 0.001 * (bytes[13] + bytes[14] * 256); + + decoded.tempC = bytes[15]; + if (decoded.tempC >= 0x80) // 2^7 + decoded.tempC -= 0x100; // 2^8 + decoded.gpsAccM = bytes[16]; + + if (bytes.length < 19) + { + decoded.timestamp = null; + decoded.time = null; + } + else + { + decoded.timestamp = bytes[17] + bytes[18] * 256; + decoded.time = ResolveTime(decoded.timestamp, new Date()) + if (decoded.time != null) + decoded.time = decoded.time.toISOString(); + } + + // Clean up the floats for display + decoded.latitudeDeg = parseFloat(decoded.latitudeDeg.toFixed(7)); + decoded.longitudeDeg = parseFloat(decoded.longitudeDeg.toFixed(7)); + decoded.batV = parseFloat(decoded.batV.toFixed(3)); + decoded.vExt = parseFloat(decoded.vExt.toFixed(3)); + decoded.vAin = parseFloat(decoded.vAin.toFixed(3)); + } + else if (port === 2) + { + if (bytes.length != 11) + return null; + + decoded._type = "data part 1"; + + switch (bytes[0] & 0x3) + { + case 0: decoded.tripType = "None"; break; + case 1: decoded.tripType = "Ignition"; break; + case 2: decoded.tripType = "Movement"; break; + case 3: decoded.tripType = "Run Detect"; break; + } + + decoded.latitudeDeg = (bytes[0] & 0xF0) + bytes[1] * 256 + + bytes[2] * 65536 + bytes[3] * 16777216; + if (decoded.latitudeDeg >= 0x80000000) // 2^31 + + decoded.latitudeDeg -= 0x100000000; // 2^32 + decoded.latitudeDeg /= 1e7; + + decoded.longitudeDeg = (bytes[4] & 0xF0) + bytes[5] * 256 + + bytes[6] * 65536 + bytes[7] * 16777216; + if (decoded.longitudeDeg >= 0x80000000) // 2^31 + decoded.longitudeDeg -= 0x100000000; // 2^32 + decoded.longitudeDeg /= 1e7; + + decoded.vExtGood = ((bytes[0] & 0x4) !== 0) ? true : false; + decoded.gpsCurrent = ((bytes[0] & 0x8) !== 0) ? true : false; + + decoded.ignition = ((bytes[4] & 0x1) !== 0) ? true : false; + decoded.digIn1 = ((bytes[4] & 0x2) !== 0) ? true : false; + decoded.digIn2 = ((bytes[4] & 0x4) !== 0) ? true : false; + decoded.digOut = ((bytes[4] & 0x8) !== 0) ? true : false; + decoded.headingDeg = bytes[8] * 2; + decoded.speedKmph = bytes[9]; + decoded.batV = bytes[10] * 0.02; + + // Clean up the floats for display + decoded.latitudeDeg = parseFloat(decoded.latitudeDeg.toFixed(7)); + decoded.longitudeDeg = parseFloat(decoded.longitudeDeg.toFixed(7)); + decoded.batV = parseFloat(decoded.batV.toFixed(3)); + } + else if (port === 3) + { + if ((bytes.length != 6) && (bytes.length < 8)) return null; + + decoded._type = "data part 2"; + decoded.vExt = 0.001 * (bytes[0] + bytes[1] * 256); + decoded.vAin = 0.001 * (bytes[2] + bytes[3] * 256); + + decoded.tempC = bytes[4]; + if (decoded.tempC >= 0x80) // 2^7 + decoded.tempC -= 0x100; // 2^8 + decoded.gpsAccM = bytes[5]; + + if (bytes.length < 8) + { + decoded.timestamp = null; + decoded.time = null; + } + else + { + decoded.timestamp = bytes[6] + bytes[7] * 256; + decoded.time = ResolveTime(decoded.timestamp, new Date()) + if (decoded.time != null) + decoded.time = decoded.time.toISOString(); + } + + // Clean up the floats for display + decoded.vExt = parseFloat(decoded.vExt.toFixed(3)); + decoded.vAin = parseFloat(decoded.vAin.toFixed(3)); + } + else if (port === 4) + { + if (bytes.length != 8) + return null; + + decoded._type = "odometer"; + let runtimeS = bytes[0] + bytes[1] * 256 + bytes[2] * 65536 + bytes[3] * 16777216; + decoded.runtime = Math.floor(runtimeS / 86400) + "d" + + Math.floor(runtimeS % 86400 / 3600) + "h" + + Math.floor(runtimeS % 3600 / 60) + "m" + (runtimeS % 60) + "s"; + decoded.distanceKm = 0.01 * (bytes[4] + bytes[5] * 256 + + bytes[6] * 65536 + bytes[7] * 16777216); + + // Clean up the floats for display + decoded.distanceKm = parseFloat(decoded.distanceKm.toFixed(2)); + } + else if (port === 5) + { + if (bytes.length != 3) + return null; + + decoded._type = "downlink ack"; + + decoded.sequence = (bytes[0] & 0x7F); + decoded.accepted = ((bytes[0] & 0x80) !== 0) ? true : false; + decoded.fwMaj = bytes[1]; + decoded.fwMin = bytes[2]; + } + return decoded; +} + +function ResolveTime(timestamp, approxReceptionTime) +{ + if (timestamp === 65535) return null; + + let approxUnixTime = Math.round(approxReceptionTime.getTime() / 1000); + + // Device supplies a unix time, modulo 65535. + // We're assuming that the packet arrived some time BEFORE refTime, + // and got delayed by network lag. So we'll resolve the timestamp to + // somewhere in the 18 hours before the reception time, rather than + // symetrically in a +- 9 hour window. + // Wind the reception time forward a bit, to tolerate clock errors. + let refTime = approxUnixTime + 1800; + + // refTime + // v + // [ | | | ] + // ^ ^ ^ ^ + // timestamp timestamp timestamp timestamp + // refTime + // v + // [ | | | ] + // ^ ^ ^ ^ + // timestamp timestamp timestamp timestamp + // We want the timestamp option immediately to the left of refTime. + let refTimeMultiple = Math.floor(refTime / 65535); + let refTimeModulo = refTime % 65535; + let closestUnixTime = 0; + if (refTimeModulo > timestamp) + closestUnixTime = refTimeMultiple * 65535 + timestamp; + else + closestUnixTime = (refTimeMultiple - 1) * 65535 + timestamp; + + return new Date(closestUnixTime * 1000); +} + +/** + * Encode downlink function. + * + * @param {object} input + * @param {object} input.data Object representing the payload that must be encoded. + * @param {Record} input.variables Object containing the configured device variables. + * + * @returns {{bytes: number[]}} Byte array containing the downlink payload. + */ +function encodeDownlink(input) { + return { + // bytes: [225, 230, 255, 0] + }; +} diff --git a/vendors/digital-matter/codecs/digital-matter-oyster3-yabby3.js b/vendors/digital-matter/codecs/digital-matter-oyster3-yabby3.js new file mode 100644 index 0000000..df6d398 --- /dev/null +++ b/vendors/digital-matter/codecs/digital-matter-oyster3-yabby3.js @@ -0,0 +1,113 @@ +function decodeUplink(input) { + let port = input.fPort; + let bytes = input.bytes; + let decoded = {}; + let location = {}; + let warnings = []; + + if (port === 1) { + decoded.type = "position"; + + decoded.inTrip = ((bytes[8] & 0x1) !== 0); + decoded.fixFailed = ((bytes[8] & 0x2) !== 0); + decoded.batV = Number((bytes[10] * 0.025).toFixed(2)); + + decoded.manDown = null; + + location.latitudeDeg = bytes[0] + + bytes[1] * 256 + + bytes[2] * 65536 + + bytes[3] * 16777216; + if (location.latitudeDeg >= 0x80000000) { // 2^31 + location.latitudeDeg -= 0x100000000; // 2^32 + } + location.latitudeDeg /= 1e7; + + location.longitudeDeg = bytes[4] + + bytes[5] * 256 + + bytes[6] * 65536 + + bytes[7] * 16777216; + if (location.longitudeDeg >= 0x80000000) { // 2^31 + location.longitudeDeg -= 0x100000000; // 2^32 + } + location.longitudeDeg /= 1e7; + + location.headingDeg = (bytes[8] >> 2) * 5.625; + location.speedKmph = bytes[9]; + + if (decoded.fixFailed) { + decoded.cached = location; + warnings.push("fix failed"); + } else { + decoded = Object.assign(decoded, location); + } + } + else if (port === 4) { + decoded.type = "position"; + + decoded.batV = Number((bytes[7] * 0.025).toFixed(2)); + + decoded.inTrip = ((bytes[8] & 0x1) !== 0); + decoded.fixFailed = ((bytes[8] & 0x2) !== 0); + decoded.manDown = ((bytes[8] & 0x4) !== 0); + + location.latitudeDeg = bytes[0] + + bytes[1] * 256 + + bytes[2] * 65536; + if (location.latitudeDeg >= 0x800000) { // 2^23 + location.latitudeDeg -= 0x1000000; // 2^24 + } + location.latitudeDeg *= 256e-7; + + location.longitudeDeg = bytes[3] + + bytes[4] * 256 + + bytes[5] * 65536; + if (location.longitudeDeg >= 0x800000) { // 2^23 + location.longitudeDeg -= 0x1000000; // 2^24 + } + location.longitudeDeg *= 256e-7; + + location.headingDeg = (bytes[6] & 0x7) * 45; + location.speedKmph = (bytes[6] >> 3) * 5; + + if (decoded.fixFailed) { + decoded.cached = location; + warnings.push("fix failed"); + } else { + decoded = Object.assign(decoded, location); + } + } + else if (port === 2) { + decoded.type = "downlink ack"; + + decoded.sequence = (bytes[0] & 0x7F); + decoded.accepted = ((bytes[0] & 0x80) !== 0); + decoded.fwMaj = bytes[1]; + decoded.fwMin = bytes[2]; + } + else if (port === 3) { + decoded.type = "stats"; + + decoded.initialBatV = Number((4.0 + 0.100 * (bytes[0] & 0xF)).toFixed(1)); + decoded.txCount = 32 * ((bytes[0] >> 4) + (bytes[1] & 0x7F) * 16); + decoded.tripCount = 32 * ((bytes[1] >> 7) + (bytes[2] & 0xFF) * 2 + + (bytes[3] & 0x0F) * 512); + decoded.gpsSuccesses = 32 * ((bytes[3] >> 4) + (bytes[4] & 0x3F) * 16); + decoded.gpsFails = 32 * ((bytes[4] >> 6) + (bytes[5] & 0x3F) * 4); + decoded.aveGpsFixS = 1 * ((bytes[5] >> 6) + (bytes[6] & 0x7F) * 4); + decoded.aveGpsFailS = 1 * ((bytes[6] >> 7) + (bytes[7] & 0xFF) * 2); + decoded.aveGpsFreshenS = 1 * ((bytes[7] >> 8) + (bytes[8] & 0xFF) * 1); + decoded.wakeupsPerTrip = 1 * ((bytes[8] >> 8) + (bytes[9] & 0x7F) * 1); + decoded.uptimeWeeks = 1 * ((bytes[9] >> 7) + (bytes[10] & 0xFF) * 2); + } + else { + return { + warnings: ['unknown FPort'], + }; + } + + return { + data: decoded, + warnings: warnings, + }; +} \ No newline at end of file diff --git a/vendors/digital-matter/codecs/test_decode_digital-matter-g62.json b/vendors/digital-matter/codecs/test_decode_digital-matter-g62.json new file mode 100644 index 0000000..83bdfca --- /dev/null +++ b/vendors/digital-matter/codecs/test_decode_digital-matter-g62.json @@ -0,0 +1,50 @@ +[ + { + "name": "Test 1 digital matter g62", + "input": { + "fPort": 0x01, + "bytes": [0xA0,0x26,0xF5,0xEC,0x16,0xA1,0x08,0x45,0x0A,0x12,0xCA,0xC9,0x33,0x00,0x00,0x17,0x1C,0x23,0x18] + }, + "expected": { + data: { + _type: 'full data', + tripType: 'None', + latitudeDeg: -31.9478112, + longitudeDeg: 115.8193424, + vExtGood: false, + gpsCurrent: false, + ignition: false, + digIn1: true, + digIn2: true, + digOut: false, + headingDeg: 20, + speedKmph: 18, + batV: 4.04, + vExt: 13.257, + vAin: 0, + tempC: 23, + gpsAccM: 28, + timestamp: 6179, + time: '2026-01-02T11:40:59.000Z' + } + } + }, + { + "name": "Test 2 digital matter g62", + "input": { + "fPort": 0x03, + "bytes": [0xA0,0x26,0xF5,0xEC,0x16,0xA1,0x08,0x45,0x0A,0x12,0xCA,0xC9,0x33,0x00,0x00,0x17,0x1C,0x23,0x18] + }, + "expected": { + data: { + _type: 'data part 2', + vExt: 9.888, + vAin: 60.661, + tempC: 22, + gpsAccM: 161, + timestamp: 17672, + time: '2026-01-02T14:52:32.000Z' + } + } + } +] \ No newline at end of file diff --git a/vendors/digital-matter/codecs/test_decode_digital-matter-oyster3-yabby3.json b/vendors/digital-matter/codecs/test_decode_digital-matter-oyster3-yabby3.json new file mode 100644 index 0000000..c6b52ef --- /dev/null +++ b/vendors/digital-matter/codecs/test_decode_digital-matter-oyster3-yabby3.json @@ -0,0 +1,84 @@ +[ + { + "name": "Test 1 digital matter oyster3 & yabby3", + "input": { + "fPort": 0x01, + "bytes": [0x53,0xAB,0x78,0x3C,0x04,0x21,0xF9,0x8E,0x94,0x0A,0xB3] + }, + "expected": { + data: { + type: "position", + inTrip: false, + fixFailed: false, + batV: 4.48, + manDown: null, + latitudeDeg: 101.4541139, + longitudeDeg: -189.6275708, + headingDeg: 208.125, + speedKmph: 10 + }, + warnings: [] + } + }, + { + "name": "Test 2 digital matter oyster3 & yabby3", + "input": { + "fPort": 0x02, + "bytes": [0x53,0xAB,0x78,0x3C,0x04,0x21,0xF9,0x8E,0x94,0x0A,0xB3] + }, + "expected": { + data: { + type: "downlink ack", + sequence: 83, + accepted: false, + fwMaj: 171, + fwMin: 120 + }, + warnings: [] + } + }, + { + "name": "Test 3 digital matter oyster3 & yabby3", + "input": { + "fPort": 0x03, + "bytes": [0x53,0xAB,0x78,0x3C,0x04,0x21,0xF9,0x8E,0x94,0x0A,0xB3] + }, + "expected": { + data: { + type: 'stats', + initialBatV: 4.3, + txCount: 22176, + tripCount: 204320, + gpsSuccesses: 2144, + gpsFails: 4224, + aveGpsFixS: 484, + aveGpsFailS: 285, + aveGpsFreshenS: 148, + wakeupsPerTrip: 10, + uptimeWeeks: 358 + }, + warnings: [] + } + }, + { + "name": "Test 4 digital matter oyster3 & yabby3", + "input": { + "fPort": 0x04, + "bytes": [0x53,0xAB,0x78,0x3C,0x04,0x21,0xF9,0x8E,0x94,0x0A,0xB3] + }, + "expected": { + data: { + type: 'position', + batV: 3.55, + inTrip: false, + fixFailed: false, + manDown: true, + latitudeDeg: 202.4493824, + longitudeDeg: 55.3925632, + headingDeg: 45, + speedKmph: 155 + }, + warnings: [] + } + }, +] \ No newline at end of file diff --git a/vendors/digital-matter/codecs/test_encode_digital-matter-g62.json b/vendors/digital-matter/codecs/test_encode_digital-matter-g62.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/digital-matter/codecs/test_encode_digital-matter-g62.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/digital-matter/codecs/test_encode_digital-matter-oyster3-yabby3.json b/vendors/digital-matter/codecs/test_encode_digital-matter-oyster3-yabby3.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/digital-matter/codecs/test_encode_digital-matter-oyster3-yabby3.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/digital-matter/devices/oyster3-lorawan.toml b/vendors/digital-matter/devices/oyster3-lorawan.toml new file mode 100644 index 0000000..f48d87c --- /dev/null +++ b/vendors/digital-matter/devices/oyster3-lorawan.toml @@ -0,0 +1,18 @@ +[device] +id = "92ac7616-1b96-4322-90e6-17a3a9b096c8" +name = "Oyster3 LoRaWAN" +description = "GPS tracker for high-precision outdoor asset tracking on LoRaWAN networks" + +[[device.firmware]] +version = "1.5" +profiles = [ + "AS923-1_0_3.toml", + "AU915-1_0_3.toml", + "EU868-1_0_3.toml", + "US915-1_0_3.toml", +] +codec = "digital-matter-oyster3-yabby3.js" + +[device.metadata] +product_url = "https://www.digitalmatter.com/devices/oyster-lorawan/" +documentation_url = "https://www.digitalmatter.com/hubfs/Datasheets%20for%20Website/Oyster3%20Lorawan%20Datasheet%20-%20Digital%20Matter.pdf" diff --git a/vendors/digital-matter/devices/yabby3-lorawan.toml b/vendors/digital-matter/devices/yabby3-lorawan.toml new file mode 100644 index 0000000..a0cfa02 --- /dev/null +++ b/vendors/digital-matter/devices/yabby3-lorawan.toml @@ -0,0 +1,18 @@ +[device] +id = "3b891c9e-1086-4be6-8c02-e3513836ce76" +name = "Yabby3 LoRaWAN" +description = "GPS battery-powered GPS tracker for outdoor asset tracking on LoRaWAN networks" + +[[device.firmware]] +version = "1.1" +profiles = [ + "AS923-1_0_3.toml", + "AU915-1_0_3.toml", + "EU868-1_0_3.toml", + "US915-1_0_3.toml", +] +codec = "digital-matter-oyster3-yabby3.js" + +[device.metadata] +product_url = "https://www.digitalmatter.com/devices/yabby3-for-lorawan/" +documentation_url = "https://support.digitalmatter.com/yabby3-lorawan%C2%AE/411-yabby-3-lorawan%C2%AE-configuration-and-usage-guide" diff --git a/vendors/digital-matter/profiles/AS923-1_0_3.toml b/vendors/digital-matter/profiles/AS923-1_0_3.toml new file mode 100644 index 0000000..d3a3b62 --- /dev/null +++ b/vendors/digital-matter/profiles/AS923-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "3ccc154f-dec5-42e9-8287-345c4bdb6132" +vendor_profile_id = 0 +region = "AS923" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 16 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/digital-matter/profiles/AU915-1_0_3.toml b/vendors/digital-matter/profiles/AU915-1_0_3.toml new file mode 100644 index 0000000..802535a --- /dev/null +++ b/vendors/digital-matter/profiles/AU915-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "f04483ea-bc56-4e34-8849-8a5e846bf58e" +vendor_profile_id = 0 +region = "AU915" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 27 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/digital-matter/profiles/EU868-1_0_3.toml b/vendors/digital-matter/profiles/EU868-1_0_3.toml new file mode 100644 index 0000000..af7a1a4 --- /dev/null +++ b/vendors/digital-matter/profiles/EU868-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "f7293b1a-6d02-401e-954f-0a891246474d" +vendor_profile_id = 0 +region = "EU868" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 16 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/digital-matter/profiles/US915-1_0_3.toml b/vendors/digital-matter/profiles/US915-1_0_3.toml new file mode 100644 index 0000000..a4f4afb --- /dev/null +++ b/vendors/digital-matter/profiles/US915-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "24cb395b-389f-4eb4-bfb7-86eea4da20b6" +vendor_profile_id = 0 +region = "US915" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 27 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/digital-matter/vendor.toml b/vendors/digital-matter/vendor.toml new file mode 100644 index 0000000..2796448 --- /dev/null +++ b/vendors/digital-matter/vendor.toml @@ -0,0 +1,9 @@ +[vendor] +id = "138736f1-c8cb-4949-8e8f-102f1b7ba7a8" +name = "Digital Matter" +vendor_id = 118 +ouis = ["70b3d5"] +devices = [] + +[vendor.metadata] +homepage = "https://www.digitalmatter.com/" diff --git a/vendors/seeed-technology-co-ltd/codecs/SenseCAP-S2100.js b/vendors/seeed-technology-co-ltd/codecs/SenseCAP-S2100.js new file mode 100644 index 0000000..f32e28d --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/SenseCAP-S2100.js @@ -0,0 +1,630 @@ +/** + * Decode uplink function + * + * @param {object} input + * @param {number[]} input.bytes Byte array containing the uplink payload, e.g. [255, 230, 255, 0] + * @param {number} input.fPort Uplink fPort. + * @param {Record} input.variables Object containing the configured device variables. + * + * @returns {{data: object}} Object representing the decoded payload. + */ +function decodeUplink (input, port) { + // data split + var bytes = input['bytes'] + // init + bytes = bytes2HexString(bytes) + .toLocaleUpperCase() + + let result = { + 'err': 0, 'payload': bytes, 'valid': true, messages: [] + } + let splitArray = dataSplit(bytes) + // data decoder + let decoderArray = [] + for (let i = 0; i < splitArray.length; i++) { + let item = splitArray[i] + let dataId = item.dataId + let dataValue = item.dataValue + let messages = dataIdAndDataValueJudge(dataId, dataValue) + decoderArray.push(messages) + } + result.messages = decoderArray + return { data: result } + } + + /** + * data splits + * @param bytes + * @returns {*[]} + */ + function dataSplit (bytes) { + let frameArray = [] + + for (let i = 0; i < bytes.length; i++) { + let remainingValue = bytes + let dataId = remainingValue.substring(0, 2) + let dataValue + let dataObj = {} + switch (dataId) { + case '01' : + case '20' : + case '21' : + case '30' : + case '31' : + case '33' : + case '40' : + case '41' : + case '42' : + case '43' : + case '44' : + case '45' : + dataValue = remainingValue.substring(2, 22) + bytes = remainingValue.substring(22) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '02': + dataValue = remainingValue.substring(2, 18) + bytes = remainingValue.substring(18) + dataObj = { + 'dataId': '02', 'dataValue': dataValue + } + break + case '03' : + case '06': + dataValue = remainingValue.substring(2, 4) + bytes = remainingValue.substring(4) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '05' : + case '34': + dataValue = bytes.substring(2, 10) + bytes = remainingValue.substring(10) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '04': + case '10': + case '32': + case '35': + case '36': + case '37': + case '38': + case '39': + dataValue = bytes.substring(2, 20) + bytes = remainingValue.substring(20) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + default: + dataValue = '9' + break + } + if (dataValue.length < 2) { + break + } + frameArray.push(dataObj) + } + return frameArray + } + + function dataIdAndDataValueJudge (dataId, dataValue) { + let messages = [] + switch (dataId) { + case '01': + let temperature = dataValue.substring(0, 4) + let humidity = dataValue.substring(4, 6) + let illumination = dataValue.substring(6, 14) + let uv = dataValue.substring(14, 16) + let windSpeed = dataValue.substring(16, 20) + messages = [{ + measurementValue: loraWANV2DataFormat(temperature, 10), measurementId: '4097', type: 'Air Temperature' + }, { + measurementValue: loraWANV2DataFormat(humidity), measurementId: '4098', type: 'Air Humidity' + }, { + measurementValue: loraWANV2DataFormat(illumination), measurementId: '4099', type: 'Light Intensity' + }, { + measurementValue: loraWANV2DataFormat(uv, 10), measurementId: '4190', type: 'UV Index' + }, { + measurementValue: loraWANV2DataFormat(windSpeed, 10), measurementId: '4105', type: 'Wind Speed' + }] + break + case '02': + let windDirection = dataValue.substring(0, 4) + let rainfall = dataValue.substring(4, 12) + let airPressure = dataValue.substring(12, 16) + messages = [{ + measurementValue: loraWANV2DataFormat(windDirection), measurementId: '4104', type: 'Wind Direction Sensor' + }, { + measurementValue: loraWANV2DataFormat(rainfall, 1000), measurementId: '4113', type: 'Rain Gauge' + }, { + + measurementValue: loraWANV2DataFormat(airPressure, 0.1), measurementId: '4101', type: 'Barometric Pressure' + }] + break + case '03': + let Electricity = dataValue + messages = [{ + 'Battery(%)': loraWANV2DataFormat(Electricity) + }] + break + case '04': + let electricityWhether = dataValue.substring(0, 2) + let hwv = dataValue.substring(2, 6) + let bdv = dataValue.substring(6, 10) + let sensorAcquisitionInterval = dataValue.substring(10, 14) + let gpsAcquisitionInterval = dataValue.substring(14, 18) + messages = [{ + 'Battery(%)': loraWANV2DataFormat(electricityWhether), + 'Hardware Version': `${loraWANV2DataFormat(hwv.substring(0, 2))}.${loraWANV2DataFormat(hwv.substring(2, 4))}`, + 'Firmware Version': `${loraWANV2DataFormat(bdv.substring(0, 2))}.${loraWANV2DataFormat(bdv.substring(2, 4))}`, + 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionInterval)) * 60, + 'gpsInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionInterval)) * 60 + }] + break + case '05': + let sensorAcquisitionIntervalFive = dataValue.substring(0, 4) + let gpsAcquisitionIntervalFive = dataValue.substring(4, 8) + messages = [{ + 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalFive)) * 60, + 'gpsInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalFive)) * 60 + }] + break + case '06': + let errorCode = dataValue + let descZh + switch (errorCode) { + case '00': + descZh = 'CCL_SENSOR_ERROR_NONE' + break + case '01': + descZh = 'CCL_SENSOR_NOT_FOUND' + break + case '02': + descZh = 'CCL_SENSOR_WAKEUP_ERROR' + break + case '03': + descZh = 'CCL_SENSOR_NOT_RESPONSE' + break + case '04': + descZh = 'CCL_SENSOR_DATA_EMPTY' + break + case '05': + descZh = 'CCL_SENSOR_DATA_HEAD_ERROR' + break + case '06': + descZh = 'CCL_SENSOR_DATA_CRC_ERROR' + break + case '07': + descZh = 'CCL_SENSOR_DATA_B1_NO_VALID' + break + case '08': + descZh = 'CCL_SENSOR_DATA_B2_NO_VALID' + break + case '09': + descZh = 'CCL_SENSOR_RANDOM_NOT_MATCH' + break + case '0A': + descZh = 'CCL_SENSOR_PUBKEY_SIGN_VERIFY_FAILED' + break + case '0B': + descZh = 'CCL_SENSOR_DATA_SIGN_VERIFY_FAILED' + break + case '0C': + descZh = 'CCL_SENSOR_DATA_VALUE_HI' + break + case '0D': + descZh = 'CCL_SENSOR_DATA_VALUE_LOW' + break + case '0E': + descZh = 'CCL_SENSOR_DATA_VALUE_MISSED' + break + case '0F': + descZh = 'CCL_SENSOR_ARG_INVAILD' + break + case '10': + descZh = 'CCL_SENSOR_RS485_MASTER_BUSY' + break + case '11': + descZh = 'CCL_SENSOR_RS485_REV_DATA_ERROR' + break + case '12': + descZh = 'CCL_SENSOR_RS485_REG_MISSED' + break + case '13': + descZh = 'CCL_SENSOR_RS485_FUN_EXE_ERROR' + break + case '14': + descZh = 'CCL_SENSOR_RS485_WRITE_STRATEGY_ERROR' + break + case '15': + descZh = 'CCL_SENSOR_CONFIG_ERROR' + break + case 'FF': + descZh = 'CCL_SENSOR_DATA_ERROR_UNKONW' + break + default: + descZh = 'CC_OTHER_FAILED' + break + } + messages = [{ + measurementId: '4101', type: 'sensor_error_event', errCode: errorCode, descZh + }] + break + case '10': + let statusValue = dataValue.substring(0, 2) + let { status, type } = loraWANV2BitDataFormat(statusValue) + let sensecapId = dataValue.substring(2) + messages = [{ + status: status, channelType: type, sensorEui: sensecapId + }] + break + case '20': + let initmeasurementId = 4175 + let sensor = [] + for (let i = 0; i < dataValue.length; i += 4) { + let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) + let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) + let aiHeadValues = `${modelId}.${detectionType}` + sensor.push({ + measurementValue: aiHeadValues, measurementId: initmeasurementId + }) + initmeasurementId++ + } + messages = sensor + break + case '21': + // Vision AI: + // AI 识别输出帧 + let tailValueArray = [] + let initTailmeasurementId = 4180 + for (let i = 0; i < dataValue.length; i += 4) { + let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) + let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) + let aiTailValues = `${modelId}.${detectionType}` + tailValueArray.push({ + measurementValue: aiTailValues, measurementId: initTailmeasurementId, type: `AI Detection ${i}` + }) + initTailmeasurementId++ + } + messages = tailValueArray + break + case '30': + case '31': + // 首帧或者首帧输出帧 + let channelInfoOne = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) + let dataOne = { + measurementValue: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), + measurementId: parseInt(channelInfoOne.one), + type: 'Measurement' + } + let dataTwo = { + measurementValue: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), + measurementId: parseInt(channelInfoOne.two), + type: 'Measurement' + } + let cacheArrayInfo = [] + if (parseInt(channelInfoOne.one)) { + cacheArrayInfo.push(dataOne) + } + if (parseInt(channelInfoOne.two)) { + cacheArrayInfo.push(dataTwo) + } + cacheArrayInfo.forEach(item => { + messages.push(item) + }) + break + case '32': + let channelInfoTwo = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) + let dataThree = { + measurementValue: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), + measurementId: parseInt(channelInfoTwo.one), + type: 'Measurement' + } + let dataFour = { + measurementValue: loraWANV2DataFormat(dataValue.substring(10, 18), 1000), + measurementId: parseInt(channelInfoTwo.two), + type: 'Measurement' + } + if (parseInt(channelInfoTwo.one)) { + messages.push(dataThree) + } + if (parseInt(channelInfoTwo.two)) { + messages.push(dataFour) + } + break + case '33': + let channelInfoThree = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) + let dataFive = { + measurementValue: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), + measurementId: parseInt(channelInfoThree.one), + type: 'Measurement' + } + let dataSix = { + measurementValue: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), + measurementId: parseInt(channelInfoThree.two), + type: 'Measurement' + } + if (parseInt(channelInfoThree.one)) { + messages.push(dataFive) + } + if (parseInt(channelInfoThree.two)) { + messages.push(dataSix) + } + + break + case '34': + let model = loraWANV2DataFormat(dataValue.substring(0, 2)) + let GPIOInput = loraWANV2DataFormat(dataValue.substring(2, 4)) + let simulationModel = loraWANV2DataFormat(dataValue.substring(4, 6)) + let simulationInterface = loraWANV2DataFormat(dataValue.substring(6, 8)) + messages = [{ + 'dataloggerProtocol': model, + 'dataloggerGPIOInput': GPIOInput, + 'dataloggerAnalogType': simulationModel, + 'dataloggerAnalogInterface': simulationInterface + }] + break + case '35': + case '36': + let channelTDOne = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) + let channelSortTDOne = 3920 + (parseInt(channelTDOne.one) - 1) * 2 + let channelSortTDTWO = 3921 + (parseInt(channelTDOne.one) - 1) * 2 + messages = [{ + [channelSortTDOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), + [channelSortTDTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) + }] + break + case '37': + let channelTDInfoTwo = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) + let channelSortOne = 3920 + (parseInt(channelTDInfoTwo.one) - 1) * 2 + let channelSortTWO = 3921 + (parseInt(channelTDInfoTwo.one) - 1) * 2 + messages = [{ + [channelSortOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), + [channelSortTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) + }] + break + case '38': + let channelTDInfoThree = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)) + let channelSortThreeOne = 3920 + (parseInt(channelTDInfoThree.one) - 1) * 2 + let channelSortThreeTWO = 3921 + (parseInt(channelTDInfoThree.one) - 1) * 2 + messages = [{ + [channelSortThreeOne]: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), + [channelSortThreeTWO]: loraWANV2DataFormat(dataValue.substring(10, 18), 1000) + }] + break + case '39': + let electricityWhetherTD = dataValue.substring(0, 2) + let hwvTD = dataValue.substring(2, 6) + let bdvTD = dataValue.substring(6, 10) + let sensorAcquisitionIntervalTD = dataValue.substring(10, 14) + let gpsAcquisitionIntervalTD = dataValue.substring(14, 18) + messages = [{ + 'Battery(%)': loraWANV2DataFormat(electricityWhetherTD), + 'Hardware Version': `${loraWANV2DataFormat(hwvTD.substring(0, 2))}.${loraWANV2DataFormat(hwvTD.substring(2, 4))}`, + 'Firmware Version': `${loraWANV2DataFormat(bdvTD.substring(0, 2))}.${loraWANV2DataFormat(bdvTD.substring(2, 4))}`, + 'measureInterval': parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalTD)) * 60, + 'thresholdMeasureInterval': parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalTD)) + }] + break + case '40': + case '41': + let lightIntensity = dataValue.substring(0, 4) + let loudness = dataValue.substring(4, 8) + // X + let accelerateX = dataValue.substring(8, 12) + // Y + let accelerateY = dataValue.substring(12, 16) + // Z + let accelerateZ = dataValue.substring(16, 20) + messages = [{ + measurementValue: loraWANV2DataFormat(lightIntensity), measurementId: '4193', type: 'Light Intensity' + }, { + measurementValue: loraWANV2DataFormat(loudness), measurementId: '4192', type: 'Sound Intensity' + }, { + + measurementValue: loraWANV2DataFormat(accelerateX, 100), measurementId: '4150', type: 'AccelerometerX' + }, { + + measurementValue: loraWANV2DataFormat(accelerateY, 100), measurementId: '4151', type: 'AccelerometerY' + }, { + + measurementValue: loraWANV2DataFormat(accelerateZ, 100), measurementId: '4152', type: 'AccelerometerZ' + }] + break + case '42': + let airTemperature = dataValue.substring(0, 4) + let AirHumidity = dataValue.substring(4, 8) + let tVOC = dataValue.substring(8, 12) + let CO2eq = dataValue.substring(12, 16) + let soilMoisture = dataValue.substring(16, 20) + messages = [{ + measurementValue: loraWANV2DataFormat(airTemperature, 100), measurementId: '4097', type: 'Air Temperature' + }, { + measurementValue: loraWANV2DataFormat(AirHumidity, 100), measurementId: '4098', type: 'Air Humidity' + }, { + measurementValue: loraWANV2DataFormat(tVOC), measurementId: '4195', type: 'Total Volatile Organic Compounds' + }, { + measurementValue: loraWANV2DataFormat(CO2eq), measurementId: '4100', type: 'CO2' + }, { + measurementValue: loraWANV2DataFormat(soilMoisture), measurementId: '4196', type: 'Soil moisture intensity' + }] + break + case '43': + case '44': + let headerDevKitValueArray = [] + let initDevkitmeasurementId = 4175 + for (let i = 0; i < dataValue.length; i += 4) { + let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) + let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) + let aiHeadValues = `${modelId}.${detectionType}` + headerDevKitValueArray.push({ + measurementValue: aiHeadValues, measurementId: initDevkitmeasurementId, type: `AI Detection ${i}` + }) + initDevkitmeasurementId++ + } + messages = headerDevKitValueArray + break + case '45': + let initTailDevKitmeasurementId = 4180 + for (let i = 0; i < dataValue.length; i += 4) { + let modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)) + let detectionType = loraWANV2DataFormat(dataValue.substring(i + 2, i + 4)) + let aiTailValues = `${modelId}.${detectionType}` + messages.push({ + measurementValue: aiTailValues, measurementId: initTailDevKitmeasurementId, type: `AI Detection ${i}` + }) + initTailDevKitmeasurementId++ + } + break + default: + break + } + return messages + } + + /** + * + * data formatting + * @param str + * @param divisor + * @returns {string|number} + */ + function loraWANV2DataFormat (str, divisor = 1) { + let strReverse = bigEndianTransform(str) + let str2 = toBinary(strReverse) + if (str2.substring(0, 1) === '1') { + let arr = str2.split('') + let reverseArr = arr.map((item) => { + if (parseInt(item) === 1) { + return 0 + } else { + return 1 + } + }) + str2 = parseInt(reverseArr.join(''), 2) + 1 + return parseFloat('-' + str2 / divisor) + } + return parseInt(str2, 2) / divisor + } + + /** + * Handling big-endian data formats + * @param data + * @returns {*[]} + */ + function bigEndianTransform (data) { + let dataArray = [] + for (let i = 0; i < data.length; i += 2) { + dataArray.push(data.substring(i, i + 2)) + } + // array of hex + return dataArray + } + + /** + * Convert to an 8-digit binary number with 0s in front of the number + * @param arr + * @returns {string} + */ + function toBinary (arr) { + let binaryData = arr.map((item) => { + let data = parseInt(item, 16) + .toString(2) + let dataLength = data.length + if (data.length !== 8) { + for (let i = 0; i < 8 - dataLength; i++) { + data = `0` + data + } + } + return data + }) + let ret = binaryData.toString() + .replace(/,/g, '') + return ret + } + + /** + * sensor + * @param str + * @returns {{channel: number, type: number, status: number}} + */ + function loraWANV2BitDataFormat (str) { + let strReverse = bigEndianTransform(str) + let str2 = toBinary(strReverse) + let channel = parseInt(str2.substring(0, 4), 2) + let status = parseInt(str2.substring(4, 5), 2) + let type = parseInt(str2.substring(5), 2) + return { channel, status, type } + } + + /** + * channel info + * @param str + * @returns {{channelTwo: number, channelOne: number}} + */ + function loraWANV2ChannelBitFormat (str) { + let strReverse = bigEndianTransform(str) + let str2 = toBinary(strReverse) + let one = parseInt(str2.substring(0, 4), 2) + let two = parseInt(str2.substring(4, 8), 2) + let resultInfo = { + one: one, two: two + } + return resultInfo + } + + /** + * data log status bit + * @param str + * @returns {{total: number, level: number, isTH: number}} + */ + function loraWANV2DataLogBitFormat (str) { + let strReverse = bigEndianTransform(str) + let str2 = toBinary(strReverse) + let isTH = parseInt(str2.substring(0, 1), 2) + let total = parseInt(str2.substring(1, 5), 2) + let left = parseInt(str2.substring(5), 2) + let resultInfo = { + isTH: isTH, total: total, left: left + } + return resultInfo + } + + function bytes2HexString (arrBytes) { + var str = '' + for (var i = 0; i < arrBytes.length; i++) { + var tmp + var num = arrBytes[i] + if (num < 0) { + tmp = (255 + num + 1).toString(16) + } else { + tmp = num.toString(16) + } + if (tmp.length === 1) { + tmp = '0' + tmp + } + str += tmp + } + return str + } + +/** + * Encode downlink function. + * + * @param {object} input + * @param {object} input.data Object representing the payload that must be encoded. + * @param {Record} input.variables Object containing the configured device variables. + * + * @returns {{bytes: number[]}} Byte array containing the downlink payload. + */ +function encodeDownlink(input) { + return { + // bytes: [225, 230, 255, 0] + }; +} diff --git a/vendors/seeed-technology-co-ltd/codecs/SenseCAP-T1000-AB.js b/vendors/seeed-technology-co-ltd/codecs/SenseCAP-T1000-AB.js new file mode 100644 index 0000000..ce4601a --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/SenseCAP-T1000-AB.js @@ -0,0 +1,930 @@ +/** + * Decode uplink function + * + * @param {object} input + * @param {number[]} input.bytes Byte array containing the uplink payload, e.g. [255, 230, 255, 0] + * @param {number} input.fPort Uplink fPort. + * @param {Record} input.variables Object containing the configured device variables. + * + * @returns {{data: object}} Object representing the decoded payload. + */ +function decodeUplink (input) { + const bytes = input['bytes'] + const fport = parseInt(input['fPort']) + const bytesString = bytes2HexString(bytes) + const originMessage = bytesString.toLocaleUpperCase() + const decoded = { + valid: true, + err: 0, + payload: bytesString, + messages: [] + } + if (fport === 199 || fport === 192) { + decoded.messages.push({fport: fport, payload: bytesString}) + return { data: decoded } + } + let measurement = messageAnalyzed(originMessage) + if (measurement.length === 0) { + decoded.valid = false + return { data: decoded } + } + + for (let message of measurement) { + if (message.length === 0) { + continue + } + let elements = [] + for (let element of message) { + if (element.errorCode) { + decoded.err = element.errorCode + decoded.errMessage = element.error + } else { + elements.push(element) + } + } + if (elements.length > 0) { + decoded.messages.push(elements) + } + } + return { data: decoded } +} + +function messageAnalyzed (messageValue) { + try { + let frames = unpack(messageValue) + let measurementResultArray = [] + for (let i = 0; i < frames.length; i++) { + let item = frames[i] + let dataId = item.dataId + let dataValue = item.dataValue + let measurementArray = deserialize(dataId, dataValue) + measurementResultArray.push(measurementArray) + } + return measurementResultArray + } catch (e) { + return e.toString() + } +} + +function unpack (messageValue) { + let frameArray = [] + for (let i = 0; i < messageValue.length; i++) { + let remainMessage = messageValue + let dataId = remainMessage.substring(0, 2).toUpperCase() + let dataValue + let dataObj = {} + let packageLen + switch (dataId) { + case '01': + dataValue = remainMessage.substring(2, 94) + messageValue = remainMessage.substring(94) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '02': + dataValue = remainMessage.substring(2, 32) + messageValue = remainMessage.substring(32) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '03': + dataValue = remainMessage.substring(2, 64) + messageValue = remainMessage.substring(64) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '04': + dataValue = remainMessage.substring(2, 20) + messageValue = remainMessage.substring(20) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '05': + dataValue = remainMessage.substring(2, 10) + messageValue = remainMessage.substring(10) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '06': + dataValue = remainMessage.substring(2, 44) + messageValue = remainMessage.substring(44) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '07': + dataValue = remainMessage.substring(2, 84) + messageValue = remainMessage.substring(84) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '08': + dataValue = remainMessage.substring(2, 70) + messageValue = remainMessage.substring(70) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '09': + dataValue = remainMessage.substring(2, 36) + messageValue = remainMessage.substring(36) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '0A': + dataValue = remainMessage.substring(2, 76) + messageValue = remainMessage.substring(76) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '0B': + dataValue = remainMessage.substring(2, 62) + messageValue = remainMessage.substring(62) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '0C': + break + case '0D': + dataValue = remainMessage.substring(2, 10) + messageValue = remainMessage.substring(10) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '0E': + packageLen = getInt(remainMessage.substring(8, 10)) * 2 + 10 + dataValue = remainMessage.substring(2, 8) + remainMessage.substring(10, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '0F': + dataValue = remainMessage.substring(2, 34) + messageValue = remainMessage.substring(34) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '10': + dataValue = remainMessage.substring(2, 26) + messageValue = remainMessage.substring(26) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '11': + dataValue = remainMessage.substring(2, 28) + messageValue = remainMessage.substring(28) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '1A': + dataValue = remainMessage.substring(2, 56) + messageValue = remainMessage.substring(56) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '1B': + dataValue = remainMessage.substring(2, 96) + messageValue = remainMessage.substring(96) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '1C': + dataValue = remainMessage.substring(2, 82) + messageValue = remainMessage.substring(82) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '1D': + dataValue = remainMessage.substring(2, 40) + messageValue = remainMessage.substring(40) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + default: + return frameArray + } + if (dataValue.length < 2) { + break + } + frameArray.push(dataObj) + } + return frameArray +} + +function deserialize (dataId, dataValue) { + let measurementArray = [] + let eventList = [] + let measurement = {} + let collectTime = 0 + let groupId = 0 + let shardFlag = {} + let payload = '' + let motionId = '' + switch (dataId) { + case '01': + measurementArray = getUpShortInfo(dataValue) + measurementArray.push(...getMotionSetting(dataValue.substring(30, 40))) + measurementArray.push(...getStaticSetting(dataValue.substring(40, 46))) + measurementArray.push(...getShockSetting(dataValue.substring(46, 52))) + measurementArray.push(...getTempSetting(dataValue.substring(52, 72))) + measurementArray.push(...getLightSetting(dataValue.substring(72, 92))) + break + case '02': + measurementArray = getUpShortInfo(dataValue) + break + case '03': + measurementArray.push(...getMotionSetting(dataValue.substring(0, 10))) + measurementArray.push(...getStaticSetting(dataValue.substring(10, 16))) + measurementArray.push(...getShockSetting(dataValue.substring(16, 22))) + measurementArray.push(...getTempSetting(dataValue.substring(22, 42))) + measurementArray.push(...getLightSetting(dataValue.substring(42, 62))) + break + case '04': + let interval = 0 + let workMode = getInt(dataValue.substring(0, 2)) + let heartbeatInterval = getMinsByMin(dataValue.substring(4, 8)) + let periodicInterval = getMinsByMin(dataValue.substring(8, 12)) + let eventInterval = getMinsByMin(dataValue.substring(12, 16)) + switch (workMode) { + case 0: + interval = heartbeatInterval + break + case 1: + interval = periodicInterval + break + case 2: + interval = eventInterval + break + } + measurementArray = [ + {measurementId: '3940', type: 'Work Mode', measurementValue: workMode}, + {measurementId: '3942', type: 'Heartbeat Interval', measurementValue: heartbeatInterval}, + {measurementId: '3943', type: 'Periodic Interval', measurementValue: periodicInterval}, + {measurementId: '3944', type: 'Event Interval', measurementValue: eventInterval}, + {measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(dataValue.substring(16, 18))}, + {measurementId: '3900', type: 'Uplink Interval', measurementValue: interval} + ] + break; + case '05': + measurementArray = [ + {measurementId: '3000', type: 'Battery', measurementValue: getBattery(dataValue.substring(0, 2))}, + {measurementId: '3940', type: 'Work Mode', measurementValue: getWorkingMode(dataValue.substring(2, 4))}, + {measurementId: '3965', type: 'Positioning Strategy', measurementValue: getPositioningStrategy(dataValue.substring(4, 6))}, + {measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(dataValue.substring(6, 8))} + ] + break + case '06': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + motionId = getMotionId(dataValue.substring(6, 8)) + measurementArray = [ + {measurementId: '4200', timestamp: collectTime, motionId: motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, + {measurementId: '4197', timestamp: collectTime, motionId: motionId, type: 'Longitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(16, 24), 1000000))}, + {measurementId: '4198', timestamp: collectTime, motionId: motionId, type: 'Latitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(24, 32), 1000000))}, + {measurementId: '4097', timestamp: collectTime, motionId: motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(32, 36), 10)}, + {measurementId: '4199', timestamp: collectTime, motionId: motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(36, 40))}, + {measurementId: '3000', timestamp: collectTime, motionId: motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(40, 42))} + ] + break + case '07': + eventList = getEventStatus(dataValue.substring(0, 6)) + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + motionId = getMotionId(dataValue.substring(6, 8)) + measurementArray = [ + {measurementId: '4200', timestamp: collectTime, motionId: motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, + {measurementId: '5001', timestamp: collectTime, motionId: motionId, type: 'Wi-Fi Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 72))}, + {measurementId: '4097', timestamp: collectTime, motionId: motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(72, 76), 10)}, + {measurementId: '4199', timestamp: collectTime, motionId: motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(76, 80))}, + {measurementId: '3000', timestamp: collectTime, motionId: motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(80, 82))} + ] + break + case '08': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + motionId = getMotionId(dataValue.substring(6, 8)) + measurementArray = [ + {measurementId: '4200', timestamp: collectTime, motionId: motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, + {measurementId: '5002', timestamp: collectTime, motionId: motionId, type: 'BLE Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 58))}, + {measurementId: '4097', timestamp: collectTime, motionId: motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(58, 62), 10)}, + {measurementId: '4199', timestamp: collectTime, motionId: motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(62, 66))}, + {measurementId: '3000', timestamp: collectTime, motionId: motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(66, 68))} + ] + break + case '09': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + motionId = getMotionId(dataValue.substring(6, 8)) + measurementArray = [ + {measurementId: '4200', timestamp: collectTime, motionId: motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, + {measurementId: '4197', timestamp: collectTime, motionId: motionId, type: 'Longitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(16, 24), 1000000))}, + {measurementId: '4198', timestamp: collectTime, motionId: motionId, type: 'Latitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(24, 32), 1000000))}, + {measurementId: '3000', timestamp: collectTime, motionId: motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(32, 34))} + ] + break + case '0A': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + motionId = getMotionId(dataValue.substring(6, 8)) + measurementArray = [ + {measurementId: '4200', timestamp: collectTime, motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, + {measurementId: '5001', timestamp: collectTime, motionId, type: 'Wi-Fi Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 72))}, + {measurementId: '3000', timestamp: collectTime, motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(72, 74))} + ] + break + case '0B': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + motionId = getMotionId(dataValue.substring(6, 8)) + measurementArray = [ + {measurementId: '4200', timestamp: collectTime, motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, + {measurementId: '5002', timestamp: collectTime, motionId, type: 'BLE Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 58))}, + {measurementId: '3000', timestamp: collectTime, motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(58, 60))}, + ] + break + case '0D': + let errorCode = getInt(dataValue) + let error = '' + switch (errorCode) { + case 1: + error = 'FAILED TO OBTAIN THE UTC TIMESTAMP' + break + case 2: + error = 'ALMANAC TOO OLD' + break + case 3: + error = 'DOPPLER ERROR' + break + } + measurementArray.push({errorCode, error}) + break + case '0E': + shardFlag = getShardFlag(dataValue.substring(0, 2)) + groupId = getInt(dataValue.substring(2, 6)) + payload = dataValue.substring(6) + measurement = { + measurementId: '6152', + groupId: groupId, + index: shardFlag.index, + count: shardFlag.count, + type: 'gnss-ng payload', + measurementValue: payload + } + measurementArray.push(measurement) + break + case '0F': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + shardFlag = getShardFlag(dataValue.substring(26, 28)) + groupId = getInt(dataValue.substring(28, 32)) + motionId = getMotionId(dataValue.substring(6, 8)) + measurementArray.push({ + measurementId: '4200', + timestamp: collectTime, + motionId, + groupId: groupId, + index: shardFlag.index, + count: shardFlag.count, + type: 'Event Status', + measurementValue: getEventStatus(dataValue.substring(0, 6)) + }) + measurementArray.push({ + measurementId: '4097', + timestamp: collectTime, + motionId, + groupId: groupId, + index: shardFlag.index, + count: shardFlag.count, + type: 'Air Temperature', + measurementValue: getSensorValue(dataValue.substring(16, 20), 10) + }) + measurementArray.push({ + measurementId: '4199', + timestamp: collectTime, + motionId, + groupId: groupId, + index: shardFlag.index, + count: shardFlag.count, + type: 'Light', + measurementValue: getSensorValue(dataValue.substring(20, 24)) + }) + measurementArray.push({ + measurementId: '3000', + timestamp: collectTime, + motionId, + groupId: groupId, + index: shardFlag.index, + count: shardFlag.count, + type: 'Battery', + measurementValue: getBattery(dataValue.substring(24, 26)) + }) + break + case '10': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + shardFlag = getShardFlag(dataValue.substring(18, 20)) + groupId = getInt(dataValue.substring(20, 24)) + motionId = getMotionId(dataValue.substring(6, 8)) + measurementArray.push({ + measurementId: '4200', + timestamp: collectTime, + motionId, + groupId: groupId, + index: shardFlag.index, + count: shardFlag.count, + type: 'Event Status', + measurementValue: getEventStatus(dataValue.substring(0, 6)) + }) + measurementArray.push({ + measurementId: '3000', + timestamp: collectTime, + motionId, + groupId: groupId, + index: shardFlag.index, + count: shardFlag.count, + type: 'Battery', + measurementValue: getBattery(dataValue.substring(16, 18)) + }) + break + case '11': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + measurementArray.push({ + measurementId: '3576', + timestamp: collectTime, + type: 'Positioning Status', + measurementValue: getPositingStatus(dataValue.substring(0, 2)) + }) + measurementArray.push({ + timestamp: collectTime, + measurementId: '4200', + type: 'Event Status', + measurementValue: getEventStatus(dataValue.substring(2, 8)) + }) + if (!isNaN(parseFloat(getSensorValue(dataValue.substring(16, 20), 10)))) { + measurementArray.push({ + timestamp: collectTime, + measurementId: '4097', + type: 'Air Temperature', + measurementValue: getSensorValue(dataValue.substring(16, 20), 10) + }) + } + if (!isNaN(parseFloat(getSensorValue(dataValue.substring(20, 24))))) { + measurementArray.push({ + timestamp: collectTime, + measurementId: '4199', + type: 'Light', + measurementValue: getSensorValue(dataValue.substring(20, 24)) + }) + } + measurementArray.push({ + timestamp: collectTime, + measurementId: '3000', + type: 'Battery', + measurementValue: getBattery(dataValue.substring(24, 26)) + }) + break + case '1A': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + motionId = getMotionId(dataValue.substring(6, 8)) + measurementArray = [ + {measurementId: '4200', timestamp: collectTime, motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, + {measurementId: '4197', timestamp: collectTime, motionId, type: 'Longitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(16, 24), 1000000))}, + {measurementId: '4198', timestamp: collectTime, motionId, type: 'Latitude', measurementValue: parseFloat(getSensorValue(dataValue.substring(24, 32), 1000000))}, + {measurementId: '4097', timestamp: collectTime, motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(32, 36), 10)}, + {measurementId: '4199', timestamp: collectTime, motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(36, 40))}, + {measurementId: '4210', timestamp: collectTime, motionId, type: 'AccelerometerX', measurementValue: getSensorValue(dataValue.substring(40, 44))}, + {measurementId: '4211', timestamp: collectTime, motionId, type: 'AccelerometerY', measurementValue: getSensorValue(dataValue.substring(44, 48))}, + {measurementId: '4212', timestamp: collectTime, motionId, type: 'AccelerometerZ', measurementValue: getSensorValue(dataValue.substring(48, 52))}, + {measurementId: '3000', timestamp: collectTime, motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(52, 54))}, + ] + break + case '1B': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + motionId = getMotionId(dataValue.substring(6, 8)) + measurementArray = [ + {measurementId: '4200', timestamp: collectTime, motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, + {measurementId: '5001', timestamp: collectTime, motionId, type: 'Wi-Fi Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 72))}, + {measurementId: '4097', timestamp: collectTime, motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(72, 76), 10)}, + {measurementId: '4199', timestamp: collectTime, motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(76, 80))}, + {measurementId: '4210', timestamp: collectTime, motionId, type: 'AccelerometerX', measurementValue: getSensorValue(dataValue.substring(80, 84))}, + {measurementId: '4211', timestamp: collectTime, motionId, type: 'AccelerometerY', measurementValue: getSensorValue(dataValue.substring(84, 88))}, + {measurementId: '4212', timestamp: collectTime, motionId, type: 'AccelerometerZ', measurementValue: getSensorValue(dataValue.substring(88, 92))}, + {measurementId: '3000', timestamp: collectTime, motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(92, 94))} + ] + break + case '1C': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + motionId = getMotionId(dataValue.substring(6, 8)) + measurementArray = [ + {measurementId: '4200', timestamp: collectTime, motionId, type: 'Event Status', measurementValue: getEventStatus(dataValue.substring(0, 6))}, + {measurementId: '5002', timestamp: collectTime, motionId, type: 'BLE Scan', measurementValue: getMacAndRssiObj(dataValue.substring(16, 58))}, + {measurementId: '4097', timestamp: collectTime, motionId, type: 'Air Temperature', measurementValue: getSensorValue(dataValue.substring(58, 62), 10)}, + {measurementId: '4199', timestamp: collectTime, motionId, type: 'Light', measurementValue: getSensorValue(dataValue.substring(62, 66))}, + {measurementId: '4210', timestamp: collectTime, motionId, type: 'AccelerometerX', measurementValue: getSensorValue(dataValue.substring(66, 70))}, + {measurementId: '4211', timestamp: collectTime, motionId, type: 'AccelerometerY', measurementValue: getSensorValue(dataValue.substring(70, 74))}, + {measurementId: '4212', timestamp: collectTime, motionId, type: 'AccelerometerZ', measurementValue: getSensorValue(dataValue.substring(74, 78))}, + {measurementId: '3000', timestamp: collectTime, motionId, type: 'Battery', measurementValue: getBattery(dataValue.substring(78, 80))} + ] + break + case '1D': + collectTime = getUTCTimestamp(dataValue.substring(8, 16)) + measurementArray.push({ + measurementId: '3576', + timestamp: collectTime, + type: 'Positioning Status', + measurementValue: getPositingStatus(dataValue.substring(0, 2)) + }) + measurementArray.push({ + timestamp: collectTime, + measurementId: '4200', + type: 'Event Status', + measurementValue: getEventStatus(dataValue.substring(2, 8)) + }) + if (!isNaN(parseFloat(getSensorValue(dataValue.substring(16, 20), 10)))) { + measurementArray.push({ + timestamp: collectTime, + measurementId: '4097', + type: 'Air Temperature', + measurementValue: getSensorValue(dataValue.substring(16, 20), 10) + }) + } + if (!isNaN(parseFloat(getSensorValue(dataValue.substring(20, 24))))) { + measurementArray.push({ + timestamp: collectTime, + measurementId: '4199', + type: 'Light', + measurementValue: getSensorValue(dataValue.substring(20, 24)) + }) + } + measurementArray.push({ + timestamp: collectTime, + measurementId: '4210', + type: 'AccelerometerX', + measurementValue: getSensorValue(dataValue.substring(24, 28)) + }) + measurementArray.push({ + timestamp: collectTime, + measurementId: '4211', + type: 'AccelerometerY', + measurementValue: getSensorValue(dataValue.substring(28, 32)) + }) + measurementArray.push({ + timestamp: collectTime, + measurementId: '4212', + type: 'AccelerometerZ', + measurementValue: getSensorValue(dataValue.substring(32, 36)) + }) + measurementArray.push({ + timestamp: collectTime, + measurementId: '3000', + type: 'Battery', + measurementValue: getBattery(dataValue.substring(36, 38)) + }) + break + } + return measurementArray +} + +function getMotionId (str) { + return getInt(str) +} + +function getPositingStatus (str) { + let status = getInt(str) + switch (status) { + case 0: + return {id:status, statusName:"locate successful."} + case 1: + return {id:status, statusName:"The GNSS scan timed out."} + case 2: + return {id:status, statusName:"The Wi-Fi scan timed out."} + case 3: + return {id:status, statusName:"The Wi-Fi + GNSS scan timed out."} + case 4: + return {id:status, statusName:"The GNSS + Wi-Fi scan timed out."} + case 5: + return {id:status, statusName:"The Bluetooth scan timed out."} + case 6: + return {id:status, statusName:"The Bluetooth + Wi-Fi scan timed out."} + case 7: + return {id:status, statusName:"The Bluetooth + GNSS scan timed out."} + case 8: + return {id:status, statusName:"The Bluetooth + Wi-Fi + GNSS scan timed out."} + case 9: + return {id:status, statusName:"Location Server failed to parse the GNSS location."} + case 10: + return {id:status, statusName:"Location Server failed to parse the Wi-Fi location."} + case 11: + return {id:status, statusName:"Location Server failed to parse the Bluetooth location."} + case 12: + return {id:status, statusName:"Failed to parse location due to the poor accuracy."} + case 13: + return {id:status, statusName:"Time synchronization failed."} + case 14: + return {id:status, statusName:"Failed due to the old Almanac."} + } + return getInt(str) +} + +function getUpShortInfo (messageValue) { + return [ + { + measurementId: '3000', type: 'Battery', measurementValue: getBattery(messageValue.substring(0, 2)) + }, { + measurementId: '3502', type: 'Firmware Version', measurementValue: getSoftVersion(messageValue.substring(2, 6)) + }, { + measurementId: '3001', type: 'Hardware Version', measurementValue: getHardVersion(messageValue.substring(6, 10)) + }, { + measurementId: '3940', type: 'Work Mode', measurementValue: getWorkingMode(messageValue.substring(10, 12)) + }, { + measurementId: '3965', type: 'Positioning Strategy', measurementValue: getPositioningStrategy(messageValue.substring(12, 14)) + }, { + measurementId: '3942', type: 'Heartbeat Interval', measurementValue: getMinsByMin(messageValue.substring(14, 18)) + }, { + measurementId: '3943', type: 'Periodic Interval', measurementValue: getMinsByMin(messageValue.substring(18, 22)) + }, { + measurementId: '3944', type: 'Event Interval', measurementValue: getMinsByMin(messageValue.substring(22, 26)) + }, { + measurementId: '3945', type: 'Sensor Enable', measurementValue: getInt(messageValue.substring(26, 28)) + }, { + measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(messageValue.substring(28, 30)) + } + ] +} + +function getMotionSetting (str) { + return [ + {measurementId: '3946', type: 'Motion Enable', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3947', type: 'Any Motion Threshold', measurementValue: getSensorValue(str.substring(2, 6), 1)}, + {measurementId: '3948', type: 'Motion Start Interval', measurementValue: getMinsByMin(str.substring(6, 10))}, + ] +} + +function getStaticSetting (str) { + return [ + {measurementId: '3949', type: 'Static Enable', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3950', type: 'Device Static Timeout', measurementValue: getMinsByMin(str.substring(2, 6))} + ] +} + +function getShockSetting (str) { + return [ + {measurementId: '3951', type: 'Shock Enable', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3952', type: 'Shock Threshold', measurementValue: getInt(str.substring(2, 6))} + ] +} + +function getTempSetting (str) { + return [ + {measurementId: '3953', type: 'Temp Enable', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3954', type: 'Event Temp Interval', measurementValue: getMinsByMin(str.substring(2, 6))}, + {measurementId: '3955', type: 'Event Temp Sample Interval', measurementValue: getSecondsByInt(str.substring(6, 10))}, + {measurementId: '3956', type: 'Temp ThMax', measurementValue: getSensorValue(str.substring(10, 14), 10)}, + {measurementId: '3957', type: 'Temp ThMin', measurementValue: getSensorValue(str.substring(14, 18), 10)}, + {measurementId: '3958', type: 'Temp Warning Type', measurementValue: getInt(str.substring(18, 20))} + ] +} + +function getLightSetting (str) { + return [ + {measurementId: '3959', type: 'Light Enable', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3960', type: 'Event Light Interval', measurementValue: getMinsByMin(str.substring(2, 6))}, + {measurementId: '3961', type: 'Event Light Sample Interval', measurementValue: getSecondsByInt(str.substring(6, 10))}, + {measurementId: '3962', type: 'Light ThMax', measurementValue: getSensorValue(str.substring(10, 14), 10)}, + {measurementId: '3963', type: 'Light ThMin', measurementValue: getSensorValue(str.substring(14, 18), 10)}, + {measurementId: '3964', type: 'Light Warning Type', measurementValue: getInt(str.substring(18, 20))} + ] +} + +function getShardFlag (str) { + let bitStr = getByteArray(str) + return { + count: parseInt(bitStr.substring(0, 4), 2), + index: parseInt(bitStr.substring(4), 2) + } +} + +function getBattery (batteryStr) { + return loraWANV2DataFormat(batteryStr) +} +function getSoftVersion (softVersion) { + return `${loraWANV2DataFormat(softVersion.substring(0, 2))}.${loraWANV2DataFormat(softVersion.substring(2, 4))}` +} +function getHardVersion (hardVersion) { + return `${loraWANV2DataFormat(hardVersion.substring(0, 2))}.${loraWANV2DataFormat(hardVersion.substring(2, 4))}` +} + +function getSecondsByInt (str) { + return getInt(str) +} + +function getMinsByMin (str) { + return getInt(str) +} + +function getSensorValue (str, dig) { + if (str === '8000') { + return null + } else { + return loraWANV2DataFormat(str, dig) + } +} + +function bytes2HexString (arrBytes) { + var str = '' + for (var i = 0; i < arrBytes.length; i++) { + var tmp + var num = arrBytes[i] + if (num < 0) { + tmp = (255 + num + 1).toString(16) + } else { + tmp = num.toString(16) + } + if (tmp.length === 1) { + tmp = '0' + tmp + } + str += tmp + } + return str +} +function loraWANV2DataFormat (str, divisor = 1) { + let strReverse = bigEndianTransform(str) + let str2 = toBinary(strReverse) + if (str2.substring(0, 1) === '1') { + let arr = str2.split('') + let reverseArr = arr.map((item) => { + if (parseInt(item) === 1) { + return 0 + } else { + return 1 + } + }) + str2 = parseInt(reverseArr.join(''), 2) + 1 + return parseFloat('-' + str2 / divisor) + } + return parseInt(str2, 2) / divisor +} + +function bigEndianTransform (data) { + let dataArray = [] + for (let i = 0; i < data.length; i += 2) { + dataArray.push(data.substring(i, i + 2)) + } + return dataArray +} + +function toBinary (arr) { + let binaryData = arr.map((item) => { + let data = parseInt(item, 16) + .toString(2) + let dataLength = data.length + if (data.length !== 8) { + for (let i = 0; i < 8 - dataLength; i++) { + data = `0` + data + } + } + return data + }) + return binaryData.toString().replace(/,/g, '') +} + +function getSOSMode (str) { + return loraWANV2DataFormat(str) +} + +function getMacAndRssiObj (pair) { + let pairs = [] + if (pair.length % 14 === 0) { + for (let i = 0; i < pair.length; i += 14) { + let mac = getMacAddress(pair.substring(i, i + 12)) + if (mac) { + let rssi = getInt8RSSI(pair.substring(i + 12, i + 14)) + pairs.push({mac: mac, rssi: rssi}) + } else { + continue + } + } + } + return pairs +} + +function getMacAddress (str) { + if (str.toLowerCase() === 'ffffffffffff') { + return null + } + let macArr = [] + for (let i = 1; i < str.length; i++) { + if (i % 2 === 1) { + macArr.push(str.substring(i - 1, i + 1)) + } + } + let mac = '' + for (let i = 0; i < macArr.length; i++) { + mac = mac + macArr[i] + if (i < macArr.length - 1) { + mac = mac + ':' + } + } + return mac +} + +function getInt8RSSI (str) { + return loraWANV2DataFormat(str) +} + +function getInt (str) { + return parseInt(str, 16) +} + +function getEventStatus (str) { + let bitStr = getByteArray(str) + let bitArr = [] + for (let i = 0; i < bitStr.length; i++) { + bitArr[i] = bitStr.substring(i, i + 1) + } + bitArr = bitArr.reverse() + let event = [] + for (let i = 0; i < bitArr.length; i++) { + if (bitArr[i] !== '1') { + continue + } + switch (i){ + case 0: + event.push({id:1, eventName:"Start moving event."}) + break + case 1: + event.push({id:2, eventName:"End movement event."}) + break + case 2: + event.push({id:3, eventName:"Motionless event."}) + break + case 3: + event.push({id:4, eventName:"Shock event."}) + break + case 4: + event.push({id:5, eventName:"Temperature event."}) + break + case 5: + event.push({id:6, eventName:"Light event."}) + break + case 6: + event.push({id:7, eventName:"SOS event."}) + break + case 7: + event.push({id:8, eventName:"Press once event."}) + break + } + } + return event +} + +function getByteArray (str) { + let bytes = [] + for (let i = 0; i < str.length; i += 2) { + bytes.push(str.substring(i, i + 2)) + } + return toBinary(bytes) +} + +function getWorkingMode (workingMode) { + return getInt(workingMode) +} + +function getPositioningStrategy (strategy) { + return getInt(strategy) +} + +function getUTCTimestamp(str){ + return parseInt(loraWANV2PositiveDataFormat(str)) * 1000 +} + +function loraWANV2PositiveDataFormat (str, divisor = 1) { + let strReverse = bigEndianTransform(str) + let str2 = toBinary(strReverse) + return parseInt(str2, 2) / divisor +} + +/** + * Encode downlink function. + * + * @param {object} input + * @param {object} input.data Object representing the payload that must be encoded. + * @param {Record} input.variables Object containing the configured device variables. + * + * @returns {{bytes: number[]}} Byte array containing the downlink payload. + */ +function encodeDownlink(input) { + return { + // bytes: [225, 230, 255, 0] + }; +} diff --git a/vendors/seeed-technology-co-ltd/codecs/SenseCAP-T1000-E.js b/vendors/seeed-technology-co-ltd/codecs/SenseCAP-T1000-E.js new file mode 100644 index 0000000..3ee5991 --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/SenseCAP-T1000-E.js @@ -0,0 +1,725 @@ +/** + * Decode uplink function + * + * @param {object} input + * @param {number[]} input.bytes Byte array containing the uplink payload, e.g. [255, 230, 255, 0] + * @param {number} input.fPort Uplink fPort. + * @param {Record} input.variables Object containing the configured device variables. + * + * @returns {{data: object}} Object representing the decoded payload. + */ +function decodeUplink (input) { + const bytes = input['bytes'] + const fport = parseInt(input['fPort']) + const bytesString = bytes2HexString(bytes) + const originMessage = bytesString.toLocaleUpperCase() + const decoded = { + valid: true, + err: 0, + payload: bytesString, + messages: [] + } + if (fport === 199 || fport === 192) { + decoded.messages.push({fport: fport, payload: bytesString}) + return { data: decoded } + } + let measurement = messageAnalyzed(originMessage) + if (measurement.length === 0) { + decoded.valid = false + return { data: decoded } + } + + for (let message of measurement) { + if (message.length === 0) { + continue + } + let elements = [] + for (let element of message) { + if (element.errorCode) { + decoded.err = element.errorCode + decoded.errMessage = element.error + } else { + elements.push(element) + } + } + if (elements.length > 0) { + decoded.messages.push(elements) + } + } + // decoded.messages = measurement + return { data: decoded } +} + +function messageAnalyzed (messageValue) { + try { + let frames = unpack(messageValue) + let measurementResultArray = [] + for (let i = 0; i < frames.length; i++) { + let item = frames[i] + let dataId = item.dataId + let dataValue = item.dataValue + let measurementArray = deserialize(dataId, dataValue) + measurementResultArray.push(measurementArray) + } + return measurementResultArray + } catch (e) { + return e.toString() + } +} + +function unpack (messageValue) { + let frameArray = [] + + for (let i = 0; i < messageValue.length; i++) { + let remainMessage = messageValue + let dataId = remainMessage.substring(0, 2).toUpperCase() + let dataValue + let dataObj = {} + let packageLen + switch (dataId) { + case '1E': + packageLen = 26 + if (remainMessage.length < packageLen) { + return frameArray + } + dataValue = remainMessage.substring(2, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '1F': + packageLen = 42 + if (remainMessage.length < packageLen) { + return frameArray + } + dataValue = remainMessage.substring(2, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '20': + scanCount = parseInt(remainMessage.substring(26, 28), 16) + packageLen = (21 + (scanCount - 1) * 7) * 2 + if (remainMessage.length < packageLen) { + return frameArray + } + dataValue = remainMessage.substring(2, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '21': + scanCount = parseInt(remainMessage.substring(26, 28), 16) + packageLen = (21 + (scanCount - 1) * 7) * 2 + if (remainMessage.length < packageLen) { + return frameArray + } + dataValue = remainMessage.substring(2, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '22': + packageLen = 30 + if (remainMessage.length < packageLen) { + return frameArray + } + dataValue = remainMessage.substring(2, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '23': + scanCount = parseInt(remainMessage.substring(14, 16), 16) + packageLen = (15 + (scanCount - 1) * 7) * 2 + if (remainMessage.length < packageLen) { + return frameArray + } + dataValue = remainMessage.substring(2, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '24': + scanCount = parseInt(remainMessage.substring(14, 16), 16) + packageLen = (15 + (scanCount - 1) * 7) * 2 + if (remainMessage.length < packageLen) { + return frameArray + } + dataValue = remainMessage.substring(2, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '25': + packageLen = 26 + if (remainMessage.length < packageLen) { + return frameArray + } + dataValue = remainMessage.substring(2, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + case '26': + packageLen = 14 + if (remainMessage.length < packageLen) { + return frameArray + } + dataValue = remainMessage.substring(2, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = { + 'dataId': dataId, 'dataValue': dataValue + } + break + default: + return frameArray + } + if (dataValue.length < 2) { + break + } + frameArray.push(dataObj) + } + return frameArray +} + +function deserialize (dataId, dataValue) { + let measurementArray = [] + let collectTime = 0 + let timestamp = parseInt((new Date()).getTime() / 1000) + switch (dataId) { + case '1E': + measurementArray = [ + { + measurementId: '3000', type: 'Battery', measurementValue: this.getBattery(dataValue.substring(0, 2)) + }, { + measurementId: '3502', type: 'Firmware Version', measurementValue: this.getSoftVersion(dataValue.substring(2, 6)) + }, { + measurementId: '3001', type: 'Hardware Version', measurementValue: this.getHardVersion(dataValue.substring(6, 10)) + }, { + measurementId: '3965', type: 'Positioning Strategy', measurementValue: this.getPositioningStrategy(dataValue.substring(10, 12)) + }, { + measurementId: '3900', type: 'Uplink Interval', measurementValue: this.getMinsByMin(dataValue.substring(12, 16))}, + { + measurementId: '3974', type: 'Accelerometer Enable', measurementValue: this.getInt(dataValue.substring(16, 18)) + }, { + measurementId: '3941', type: 'SOS Mode', measurementValue: this.getSOSMode(dataValue.substring(18, 20)) + }, { + measurementId: '3972', type: 'WI-FI Scan Limitation', measurementValue: this.getInt(dataValue.substring(20, 22)) + }, { + measurementId: '3973', type: 'Beacon Scan Limitation', measurementValue: this.getInt(dataValue.substring(22, 24)) + } + ] + break + case '1F': + collectTime = timestamp + measurementArray = this.getT1000EUplinkHeaderWithSensorAnd3Axis(dataValue, collectTime, 31) + measurementArray.push({ + measurementId: '4197', + measureTime: collectTime, + value: this.getSensorValue(dataValue.substring(24, 32), 1000000) + }) + measurementArray.push({ + measurementId: '4198', + measureTime: collectTime, + value: this.getSensorValue(dataValue.substring(32, 40), 1000000) + }) + break + case '20': + collectTime = timestamp + measurementArray = this.getT1000EUplinkHeaderWithSensorAnd3Axis(dataValue, collectTime, 32) + scanMax = this.getInt(dataValue.substring(24, 26)) + if (!scanMax || scanMax === 0) { + break + } + measurementArray.push({ + measurementId: '5001', + measureTime: collectTime, + value: this.getMacAndRssiObj(dataValue.substring(26)) + }) + break + case '21': + collectTime = timestamp + measurementArray = this.getT1000EUplinkHeaderWithSensorAnd3Axis(dataValue, collectTime, 33) + scanMax = this.getInt(dataValue.substring(24, 26)) + if (!scanMax || scanMax === 0) { + break + } + measurementArray.push({ + measurementId: '5002', + measureTime: collectTime, + value: this.getMacAndRssiObj(dataValue.substring(26)) + }) + break + case '22': + collectTime = timestamp + measurementArray = this.getT1000EUplinkHeaderWithSensor(dataValue, collectTime, 34) + measurementArray.push({ + measurementId: '4197', + measureTime: collectTime, + value: this.getSensorValue(dataValue.substring(12, 20), 1000000) + }) + measurementArray.push({ + measurementId: '4198', + measureTime: collectTime, + value: this.getSensorValue(dataValue.substring(20, 28), 1000000) + }) + break + case '23': + collectTime = timestamp + measurementArray = this.getT1000EUplinkHeaderWithSensor(dataValue, collectTime, 35) + scanMax = this.getInt(dataValue.substring(12, 14)) + if (!scanMax || scanMax === 0) { + break + } + measurementArray.push({ + measurementId: '5001', + measureTime: collectTime, + value: this.getMacAndRssiObj(dataValue.substring(14)) + }) + break + case '24': + collectTime = timestamp + measurementArray = this.getT1000EUplinkHeaderWithSensor(dataValue, collectTime, 36) + scanMax = this.getInt(dataValue.substring(12, 14)) + if (!scanMax || scanMax === 0) { + break + } + measurementArray.push({ + measurementId: '5002', + measureTime: collectTime, + value: this.getMacAndRssiObj(dataValue.substring(14)) + }) + break + case '25': + collectTime = timestamp + measurementArray = this.getT1000EUplinkHeaderWithSensorAnd3Axis(dataValue, collectTime, 37) + break + case '26': + collectTime = timestamp + measurementArray = this.getT1000EUplinkHeaderWithSensor(dataValue, collectTime, 38) + break + } + return measurementArray +} + +function getMotionId (str) { + return getInt(str) +} + +function getPositingStatus (str) { + let status = getInt(str) + switch (status) { + case 0: + return {id:status, statusName:"Positioning successful."} + case 1: + return {id:status, statusName:"The GNSS scan timed out and failed to obtain the location."} + case 2: + return {id:status, statusName:"The Wi-Fi scan timed out and failed to obtain the location."} + case 3: + return {id:status, statusName:"The Wi-Fi + GNSS scan timed out and failed to obtain the location."} + case 4: + return {id:status, statusName:"The GNSS + Wi-Fi scan timed out and failed to obtain the location."} + case 5: + return {id:status, statusName:"The Bluetooth scan timed out and failed to obtain the location."} + case 6: + return {id:status, statusName:"The Bluetooth + Wi-Fi scan timed out and failed to obtain the location."} + case 7: + return {id:status, statusName:"The Bluetooth + GNSS scan timed out and failed to obtain the location."} + case 8: + return {id:status, statusName:"The Bluetooth + Wi-Fi + GNSS scan timed out and failed to obtain the location."} + case 9: + return {id:status, statusName:"Location Server failed to parse the GNSS location."} + case 10: + return {id:status, statusName:"Location Server failed to parse the Wi-Fi location."} + case 11: + return {id:status, statusName:"Location Server failed to parse the Bluetooth location."} + case 12: + return {id:status, statusName:"Failed to parse the GNSS location due to the poor accuracy."} + case 13: + return {id:status, statusName:"Time synchronization failed."} + case 14: + return {id:status, statusName:"Failed to obtain location due to the old Almanac."} + } + return getInt(str) +} + +function getUpShortInfo (messageValue) { + return [ + { + measurementId: '3000', type: 'Battery', measurementValue: getBattery(messageValue.substring(0, 2)) + }, { + measurementId: '3502', type: 'Firmware Version', measurementValue: getSoftVersion(messageValue.substring(2, 6)) + }, { + measurementId: '3001', type: 'Hardware Version', measurementValue: getHardVersion(messageValue.substring(6, 10)) + }, { + measurementId: '3940', type: 'Work Mode', measurementValue: getWorkingMode(messageValue.substring(10, 12)) + }, { + measurementId: '3965', type: 'Positioning Strategy', measurementValue: getPositioningStrategy(messageValue.substring(12, 14)) + }, { + measurementId: '3942', type: 'Heartbeat Interval', measurementValue: getMinsByMin(messageValue.substring(14, 18)) + }, { + measurementId: '3943', type: 'Periodic Interval', measurementValue: getMinsByMin(messageValue.substring(18, 22)) + }, { + measurementId: '3944', type: 'Event Interval', measurementValue: getMinsByMin(messageValue.substring(22, 26)) + }, { + measurementId: '3945', type: 'Sensor Enable', measurementValue: getInt(messageValue.substring(26, 28)) + }, { + measurementId: '3941', type: 'SOS Mode', measurementValue: getSOSMode(messageValue.substring(28, 30)) + } + ] +} + +function getMotionSetting (str) { + return [ + {measurementId: '3946', type: 'Motion Enable', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3947', type: 'Any Motion Threshold', measurementValue: getSensorValue(str.substring(2, 6), 1)}, + {measurementId: '3948', type: 'Motion Start Interval', measurementValue: getMinsByMin(str.substring(6, 10))}, + ] +} + +function getStaticSetting (str) { + return [ + {measurementId: '3949', type: 'Static Enable', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3950', type: 'Device Static Timeout', measurementValue: getMinsByMin(str.substring(2, 6))} + ] +} + +function getShockSetting (str) { + return [ + {measurementId: '3951', type: 'Shock Enable', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3952', type: 'Shock Threshold', measurementValue: getInt(str.substring(2, 6))} + ] +} + +function getTempSetting (str) { + return [ + {measurementId: '3953', type: 'Temp Enable', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3954', type: 'Event Temp Interval', measurementValue: getMinsByMin(str.substring(2, 6))}, + {measurementId: '3955', type: 'Event Temp Sample Interval', measurementValue: getSecondsByInt(str.substring(6, 10))}, + {measurementId: '3956', type: 'Temp ThMax', measurementValue: getSensorValue(str.substring(10, 14), 10)}, + {measurementId: '3957', type: 'Temp ThMin', measurementValue: getSensorValue(str.substring(14, 18), 10)}, + {measurementId: '3958', type: 'Temp Warning Type', measurementValue: getInt(str.substring(18, 20))} + ] +} + +function getLightSetting (str) { + return [ + {measurementId: '3959', type: 'Light Enable', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3960', type: 'Event Light Interval', measurementValue: getMinsByMin(str.substring(2, 6))}, + {measurementId: '3961', type: 'Event Light Sample Interval', measurementValue: getSecondsByInt(str.substring(6, 10))}, + {measurementId: '3962', type: 'Light ThMax', measurementValue: getSensorValue(str.substring(10, 14), 10)}, + {measurementId: '3963', type: 'Light ThMin', measurementValue: getSensorValue(str.substring(14, 18), 10)}, + {measurementId: '3964', type: 'Light Warning Type', measurementValue: getInt(str.substring(18, 20))} + ] +} + +function getShardFlag (str) { + let bitStr = getByteArray(str) + return { + count: parseInt(bitStr.substring(0, 4), 2), + index: parseInt(bitStr.substring(4), 2) + } +} + +function getBattery (batteryStr) { + return loraWANV2DataFormat(batteryStr) +} +function getSoftVersion (softVersion) { + return `${loraWANV2DataFormat(softVersion.substring(0, 2))}.${loraWANV2DataFormat(softVersion.substring(2, 4))}` +} +function getHardVersion (hardVersion) { + return `${loraWANV2DataFormat(hardVersion.substring(0, 2))}.${loraWANV2DataFormat(hardVersion.substring(2, 4))}` +} + +function getSecondsByInt (str) { + return getInt(str) +} + +function getMinsByMin (str) { + return getInt(str) +} + +function getSensorValue (str, dig) { + if (str === '8000') { + return null + } else { + return loraWANV2DataFormat(str, dig) + } +} + +function bytes2HexString (arrBytes) { + var str = '' + for (var i = 0; i < arrBytes.length; i++) { + var tmp + var num = arrBytes[i] + if (num < 0) { + tmp = (255 + num + 1).toString(16) + } else { + tmp = num.toString(16) + } + if (tmp.length === 1) { + tmp = '0' + tmp + } + str += tmp + } + return str +} +function loraWANV2DataFormat (str, divisor = 1) { + let strReverse = bigEndianTransform(str) + let str2 = toBinary(strReverse) + if (str2.substring(0, 1) === '1') { + let arr = str2.split('') + let reverseArr = arr.map((item) => { + if (parseInt(item) === 1) { + return 0 + } else { + return 1 + } + }) + str2 = parseInt(reverseArr.join(''), 2) + 1 + return parseFloat('-' + str2 / divisor) + } + return parseInt(str2, 2) / divisor +} + +function bigEndianTransform (data) { + let dataArray = [] + for (let i = 0; i < data.length; i += 2) { + dataArray.push(data.substring(i, i + 2)) + } + return dataArray +} + +function toBinary (arr) { + let binaryData = arr.map((item) => { + let data = parseInt(item, 16) + .toString(2) + let dataLength = data.length + if (data.length !== 8) { + for (let i = 0; i < 8 - dataLength; i++) { + data = `0` + data + } + } + return data + }) + return binaryData.toString().replace(/,/g, '') +} + +function getSOSMode (str) { + return loraWANV2DataFormat(str) +} + +function getMacAndRssiObj (pair) { + let pairs = [] + if (pair.length % 14 === 0) { + for (let i = 0; i < pair.length; i += 14) { + let mac = getMacAddress(pair.substring(i, i + 12)) + if (mac) { + let rssi = getInt8RSSI(pair.substring(i + 12, i + 14)) + pairs.push({mac: mac, rssi: rssi}) + } else { + continue + } + } + } + return pairs +} + +function getMacAddress (str) { + if (str.toLowerCase() === 'ffffffffffff') { + return null + } + let macArr = [] + for (let i = 1; i < str.length; i++) { + if (i % 2 === 1) { + macArr.push(str.substring(i - 1, i + 1)) + } + } + let mac = '' + for (let i = 0; i < macArr.length; i++) { + mac = mac + macArr[i] + if (i < macArr.length - 1) { + mac = mac + ':' + } + } + return mac +} + +function getInt8RSSI (str) { + return loraWANV2DataFormat(str) +} + +function getInt (str) { + return parseInt(str, 16) +} + +function getEventStatus (str) { + // return getInt(str) + let bitStr = getByteArray(str) + let bitArr = [] + for (let i = 0; i < bitStr.length; i++) { + bitArr[i] = bitStr.substring(i, i + 1) + } + bitArr = bitArr.reverse() + let event = [] + for (let i = 0; i < bitArr.length; i++) { + if (bitArr[i] !== '1') { + continue + } + switch (i){ + case 0: + event.push({id:1, eventName:"Start moving event."}) + break + case 1: + event.push({id:2, eventName:"End movement event."}) + break + case 2: + event.push({id:3, eventName:"Motionless event."}) + break + case 3: + event.push({id:4, eventName:"Shock event."}) + break + case 4: + event.push({id:5, eventName:"Temperature event."}) + break + case 5: + event.push({id:6, eventName:"Light event."}) + break + case 6: + event.push({id:7, eventName:"SOS event."}) + break + case 7: + event.push({id:8, eventName:"Press once event."}) + break + } + } + return event +} + +function getByteArray (str) { + let bytes = [] + for (let i = 0; i < str.length; i += 2) { + bytes.push(str.substring(i, i + 2)) + } + return toBinary(bytes) +} + +function getWorkingMode (workingMode) { + return getInt(workingMode) +} + +function getPositioningStrategy (strategy) { + return getInt(strategy) +} + +function getUTCTimestamp(str){ + return parseInt(loraWANV2PositiveDataFormat(str)) * 1000 +} + +function loraWANV2PositiveDataFormat (str, divisor = 1) { + let strReverse = bigEndianTransform(str) + let str2 = toBinary(strReverse) + return parseInt(str2, 2) / divisor +} + +function getT1000EUplinkHeaderWithSensorAnd3Axis (dataValue, collectTime, dataId) { + let measurementArray = this.getT1000EUplinkHeaderWithSensor(dataValue, collectTime, dataId) + let value = this.getSignSensorValue(dataValue.substring(12, 16), 1) + if (value !== null) { + measurementArray.push({ + measurementId: '4210', + measureTime: collectTime, + value: value + }) + } + value = this.getSignSensorValue(dataValue.substring(16, 20), 1) + if (value !== null) { + measurementArray.push({ + measurementId: '4211', + measureTime: collectTime, + value: value + }) + } + value = this.getSignSensorValue(dataValue.substring(20, 24), 1) + if (value !== null) { + measurementArray.push({ + measurementId: '4212', + measureTime: collectTime, + value: value + }) + } + return measurementArray +} + +function getT1000EUplinkHeaderWithSensor (dataValue, collectTime, dataId) { + let measurementArray = this.getT1000EUplinkHeader(dataValue, collectTime, dataId) + measurementArray.push({ + measurementId: '4097', + measureTime: collectTime, + value: this.getSensorValue(dataValue.substring(4, 8), 10) + }) + measurementArray.push({ + measurementId: '4199', + measureTime: collectTime, + value: this.getSensorValue(dataValue.substring(8, 12)) + }) + return measurementArray +} + +function getT1000EUplinkHeader (dataValue, collectTime, dataId) { + let eventList = this.getEventStatus(dataValue.substring(0, 2)) + let measurementArray = [] + measurementArray.push({ + measurementId: '5003', + measureTime: collectTime, + value: eventList + }) + measurementArray.push({ + measurementId: '3000', + measureTime: collectTime, + value: this.getBattery(dataValue.substring(2, 4)) + }) + return measurementArray +} + +function getSignSensorValue (str, dig = 1) { + if (this.isNull(str)) { + return null + } + return this.loraWANV2DataFormat(str, dig) +} +function isNull (str) { + if (str.substring(0, 1) !== '8') { + return false + } + for (let i = 1; i < str.length; i++) { + if (str.substring(i, i + 1) !== '0') { + return false + } + } + return true +} + +/** + * Encode downlink function. + * + * @param {object} input + * @param {object} input.data Object representing the payload that must be encoded. + * @param {Record} input.variables Object containing the configured device variables. + * + * @returns {{bytes: number[]}} Byte array containing the downlink payload. + */ +function encodeDownlink(input) { + return { + // bytes: [225, 230, 255, 0] + }; +} diff --git a/vendors/seeed-technology-co-ltd/codecs/SenseCAP-T2000-AB.js b/vendors/seeed-technology-co-ltd/codecs/SenseCAP-T2000-AB.js new file mode 100644 index 0000000..342c9cd --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/SenseCAP-T2000-AB.js @@ -0,0 +1,729 @@ +/** + * Decode uplink function + * + * @param {object} input + * @param {number[]} input.bytes Byte array containing the uplink payload, e.g. [255, 230, 255, 0] + * @param {number} input.fPort Uplink fPort. + * @param {Record} input.variables Object containing the configured device variables. + * + * @returns {{data: object}} Object representing the decoded payload. + */ +function decodeUplink (input) { + const bytes = input['bytes'] + const bytesString = bytes2HexString(bytes) + const originMessage = bytesString.toLocaleUpperCase() + const decoded = { + valid: true, + err: 0, + payload: bytesString, + messages: [] + } + let measurement = messageAnalyzed(originMessage) + if (measurement.length === 0) { + decoded.valid = false + return { data: decoded } + } + + for (let message of measurement) { + if (message.length === 0) { + continue + } + let elements = [] + for (let element of message) { + if (element.errorCode) { + decoded.err = element.errorCode + decoded.errMessage = element.error + } else { + elements.push(element) + } + } + if (elements.length > 0) { + decoded.messages.push(elements) + } + } + return { data: decoded } +} + +function messageAnalyzed (messageValue) { + try { + let frames = unpack(messageValue) + let measurementResultArray = [] + for (let i = 0; i < frames.length; i++) { + let item = frames[i] + let dataId = item.dataId + let dataValue = item.dataValue + let measurementArray = deserialize(dataId, dataValue) + measurementResultArray.push(measurementArray) + } + return measurementResultArray + } catch (e) { + return e.toString() + } +} + +function unpack (messageValue) { + let frameArray = [] + const FIXED_LENGTH_PACKAGES = { + '27': 92, '28': 60, '29': 24, '2A': 12, '2B': 46, '2E': 34, '31': 30, '32': 18 + } + const DYNAMIC_LENGTH_PACKAGES = { + '2C': {minLen: 32, scanCountPos: [30, 32], baseLen: 23, itemLen: 7}, + '2D': {minLen: 32, scanCountPos: [30, 32], baseLen: 23, itemLen: 7}, + '2F': {minLen: 20, scanCountPos: [18, 20], baseLen: 17, itemLen: 7}, + '30': {minLen: 20, scanCountPos: [18, 20], baseLen: 17, itemLen: 7} + } + + for (let i = 0; i < messageValue.length; i++) { + let remainMessage = messageValue + let dataId = remainMessage.substring(0, 2).toUpperCase() + let dataValue + let dataObj = {} + + if (dataId === '0C') { + dataValue = '' + messageValue = remainMessage.substring(2) + dataObj = {'dataId': dataId, 'dataValue': ''} + } else if (dataId === '0D') { + dataValue = remainMessage.substring(2, 10) + messageValue = remainMessage.substring(10) + dataObj = {'dataId': dataId, 'dataValue': dataValue} + } else if (FIXED_LENGTH_PACKAGES[dataId]) { + let packageLen = FIXED_LENGTH_PACKAGES[dataId] + if (remainMessage.length < packageLen) { + return frameArray + } + dataValue = remainMessage.substring(2, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = {'dataId': dataId, 'dataValue': dataValue} + } else if (DYNAMIC_LENGTH_PACKAGES[dataId]) { + let config = DYNAMIC_LENGTH_PACKAGES[dataId] + if (remainMessage.length < config.minLen) { + return frameArray + } + let scanCount = parseInt(remainMessage.substring(config.scanCountPos[0], config.scanCountPos[1]), 16) + let packageLen = (config.baseLen + (scanCount - 1) * config.itemLen) * 2 + if (remainMessage.length < packageLen) { + return frameArray + } + dataValue = remainMessage.substring(2, packageLen) + messageValue = remainMessage.substring(packageLen) + dataObj = {'dataId': dataId, 'dataValue': dataValue} + } else { + return frameArray + } + + if (dataValue.length < 2 && dataObj.dataId !== '0C') { + break + } + frameArray.push(dataObj) + } + return frameArray +} + +function deserialize (dataId, dataValue) { + let measurementArray = [] + let eventList = [] + let timestamp = 0 + let value = null + let motionId = 0 + let scanMax = 0 + let interval = 0 + let workMode = 0 + let heartbeatInterval = 0 + let periodicInterval = 0 + let eventInterval = 0 + switch (dataId) { + case '0C': + measurementArray.push({type: "timeSync"}) + break + case '0D': + let errorCode = getInt(dataValue) + let error = '' + switch (errorCode) { + case 1: + error = 'FAILED TO OBTAIN THE UTC TIMESTAMP' + break + case 2: + error = 'ALMANAC TOO OLD' + break + case 3: + error = 'DOPPLER ERROR' + break + } + measurementArray.push({errorCode, error}) + break + case '27': + measurementArray.push(...getUpT2000(dataValue)) + break + case '28': + interval = 0 + workMode = getInt(dataValue.substring(0, 2)) + heartbeatInterval = getMinsByMin(dataValue.substring(4, 8)) + periodicInterval = getMinsByMin(dataValue.substring(8, 12)) + eventInterval = getMinsByMin(dataValue.substring(12, 16)) + switch (workMode) { + case 0: + interval = heartbeatInterval + break + case 1: + interval = periodicInterval + break + case 2: + interval = eventInterval + break + } + measurementArray = [ + { + measurementId: '3940', measurementValue: workMode + }, { + measurementId: '3965', measurementValue: getPositioningStrategy(dataValue.substring(2, 4)) + }, { + measurementId: '3942', measurementValue: heartbeatInterval + }, { + measurementId: '3943', measurementValue: periodicInterval + }, { + measurementId: '3944', measurementValue: eventInterval + }, { + measurementId: '3974', measurementValue: getInt(dataValue.substring(16, 18)) + }, { + measurementId: '3976', measurementValue: getInt(dataValue.substring(18, 20)) + }, { + measurementId: '3977', measurementValue: getInt(dataValue.substring(20, 22)) + }, { + measurementId: '3900', measurementValue: interval + }, { + measurementId: '3978', measurementValue: getInt(dataValue.substring(22, 24)) + }, { + measurementId: '3979', measurementValue: dataValue.substring(26, 26 + getInt(dataValue.substring(24, 26)) * 2) + } + ] + // measurementArray.push(measurement) + break + case '29': + measurementArray.push({ + measurementId: '3946', measurementValue: getInt(dataValue.substring(0, 2)) + }) + measurementArray.push({ + measurementId: '3947', measurementValue: getSensorValue(dataValue.substring(2, 6), 1) + }) + measurementArray.push({ + measurementId: '3948', measurementValue: getMinsByMin(dataValue.substring(6, 10)) + }) + measurementArray.push({ + measurementId: '3949', measurementValue: getInt(dataValue.substring(10, 12)) + }) + measurementArray.push({ + measurementId: '3950', measurementValue: getMinsByMin(dataValue.substring(12, 16)) + }) + measurementArray.push({ + measurementId: '3951', measurementValue: getInt(dataValue.substring(16, 18)) + }) + measurementArray.push({ + measurementId: '3952', measurementValue: getInt(dataValue.substring(18, 22)) + }) + break + case '2A': + measurementArray.push({ + measurementId: '3000', measurementValue: getBattery(dataValue.substring(0, 2)) + }) + measurementArray.push({ + measurementId: '3940', measurementValue: getWorkingMode(dataValue.substring(2, 4)) + }) + measurementArray.push({ + measurementId: '3965', measurementValue: getPositioningStrategy(dataValue.substring(4, 6)) + }) + measurementArray.push({ + measurementId: '3974', measurementValue: getInt(dataValue.substring(6, 8)) + }) + measurementArray.push({ + measurementId: '3976', measurementValue: getInt(dataValue.substring(8, 10)) + }) + break + case '2B': + eventList = getEventStatus(dataValue.substring(0, 4)) + motionId = getMotionId(dataValue.substring(4, 6)) + timestamp = getUTCTimestamp(dataValue.substring(6, 14)) + parseAccelerometerData(dataValue, 14, timestamp, motionId, measurementArray) + parseLocationData(dataValue, 26, timestamp, motionId, measurementArray) + parseBatteryData(dataValue, 42, timestamp, motionId, measurementArray) + parseEventData(eventList, timestamp, motionId, measurementArray) + break + case '2C': + case '2D': + eventList = getEventStatus(dataValue.substring(0, 4)) + motionId = getMotionId(dataValue.substring(4, 6)) + timestamp = getUTCTimestamp(dataValue.substring(6, 14)) + parseAccelerometerData(dataValue, 14, timestamp, motionId, measurementArray) + parseBatteryData(dataValue, 26, timestamp, motionId, measurementArray) + scanMax = getInt(dataValue.substring(28, 30)) + parseScanData(scanMax, dataValue, 30, dataId === '2C' ? '5001' : '5002', timestamp, motionId, measurementArray) + parseEventData(eventList, timestamp, motionId, measurementArray) + break + case '2E': + eventList = getEventStatus(dataValue.substring(0, 4)) + motionId = getMotionId(dataValue.substring(4, 6)) + timestamp = getUTCTimestamp(dataValue.substring(6, 14)) + parseLocationData(dataValue, 14, timestamp, motionId, measurementArray) + parseBatteryData(dataValue, 30, timestamp, motionId, measurementArray) + parseEventData(eventList, timestamp, motionId, measurementArray) + break + case '2F': + case '30': + eventList = getEventStatus(dataValue.substring(0, 4)) + motionId = getMotionId(dataValue.substring(4, 6)) + timestamp = getUTCTimestamp(dataValue.substring(6, 14)) + parseBatteryData(dataValue, 14, timestamp, motionId, measurementArray) + scanMax = getInt(dataValue.substring(16, 18)) + parseScanData(scanMax, dataValue, 18, dataId === '2F' ? '5001' : '5002', timestamp, motionId, measurementArray) + parseEventData(eventList, timestamp, motionId, measurementArray) + break + case '31': + eventList = getEventStatus(dataValue.substring(2, 6)) + timestamp = getUTCTimestamp(dataValue.substring(6, 14)) + measurementArray.push({ + measurementId: '3576', + timestamp, + motionId, + measurementValue: getPositingStatus(dataValue.substring(0, 2)) + }) + parseAccelerometerData(dataValue, 14, timestamp, motionId, measurementArray) + parseBatteryData(dataValue, 26, timestamp, motionId, measurementArray) + parseEventData(eventList, timestamp, motionId, measurementArray) + break + case '32': + eventList = getEventStatus(dataValue.substring(2, 6)) + timestamp = getUTCTimestamp(dataValue.substring(6, 14)) + measurementArray.push({ + measurementId: '3576', + timestamp, + motionId, + measurementValue: getPositingStatus(dataValue.substring(0, 2)) + }) + parseBatteryData(dataValue, 14, timestamp, motionId, measurementArray) + parseEventData(eventList, timestamp, motionId, measurementArray) + break + } + if (measurementArray.length > 0) { + for (let frag of measurementArray) { + if (frag.measurementId) { + frag.type = getTypeByMeasurementId(frag.measurementId) + } + } + } + return measurementArray +} + +function parseAccelerometerData (dataValue, startPos, timestamp, motionId, measurementArray) { + const accelerometerIds = ['4210', '4211', '4212'] + for (let i = 0; i < 3; i++) { + let value = getSignSensorValue(dataValue.substring(startPos + i * 4, startPos + (i + 1) * 4), 1) + if (value !== null) { + measurementArray.push({ + measurementId: accelerometerIds[i], + timestamp, + motionId, + measurementValue: value + }) + } + } +} + +function parseBatteryData (dataValue, pos, timestamp, motionId, measurementArray) { + measurementArray.push({ + measurementId: '3000', + timestamp, + motionId, + measurementValue: '' + getBattery(dataValue.substring(pos, pos + 2)) + }) +} + +function parseEventData (eventList, timestamp, motionId, measurementArray) { + if (eventList && eventList.length > 0) { + measurementArray.push({ + measurementId: '4200', + timestamp, + motionId, + measurementValue: eventList + }) + } +} + +function parseLocationData (dataValue, startPos, timestamp, motionId, measurementArray) { + measurementArray.push({ + measurementId: '4197', + timestamp, + motionId, + measurementValue: '' + getSensorValue(dataValue.substring(startPos, startPos + 8), 1000000) + }) + measurementArray.push({ + measurementId: '4198', + timestamp, + motionId, + measurementValue: '' + getSensorValue(dataValue.substring(startPos + 8, startPos + 16), 1000000) + }) +} + +function parseScanData (scanMax, dataValue, startPos, measurementId, timestamp, motionId, measurementArray) { + if (scanMax && scanMax > 0) { + measurementArray.push({ + measurementId, + timestamp, + motionId, + measurementValue: getMacAndRssiObj(dataValue.substring(startPos)) + }) + } +} + +function getTypeByMeasurementId (measurementId) { + const TYPE_MAP = { + '3000': 'Battery', + '3502': 'Firmware Version', + '3001': 'Hardware Version', + '3940': 'Work Mode', + '3965': 'Positioning Strategy', + '3942': 'Heartbeat Interval', + '3943': 'Periodic Interval', + '3944': 'Event Interval', + '3974': '3X Sensor Enable', + '3976': 'Anti-Theft', + '3977': 'GNSS Scan Timeout', + '3900': 'Uplink Interval', + '3978': 'BLE Scan Timeout', + '3979': 'UUID Filter', + '3946': 'Motion Enable', + '3947': 'Any Motion Threshold', + '3948': 'Motion Start Interval', + '3949': 'Static Enable', + '3950': 'Device Static Timeout', + '3951': 'Shock Enable', + '3952': 'Shock Threshold', + '4210': 'AccelerometerX', + '4211': 'AccelerometerY', + '4212': 'AccelerometerZ', + '4197': 'Longitude', + '4198': 'Latitude', + '4200': 'Event Status', + '5001': 'Wi-Fi Scan', + '5002': 'BLE Scan', + '3576': 'Positioning Status' + } + return TYPE_MAP[measurementId] || '' +} + +function getMotionId (str) { + return getInt(str) +} + +function getPositingStatus (str) { + let status = getInt(str) + const STATUS_MAP = { + 0: "locate successful.", + 1: "The GNSS scan timed out.", + 2: "The Wi-Fi scan timed out.", + 3: "The Wi-Fi + GNSS scan timed out.", + 4: "The GNSS + Wi-Fi scan timed out.", + 5: "The Bluetooth scan timed out.", + 6: "The Bluetooth + Wi-Fi scan timed out.", + 7: "The Bluetooth + GNSS scan timed out.", + 8: "The Bluetooth + Wi-Fi + GNSS scan timed out.", + 9: "Location Server failed to parse the GNSS location.", + 10: "Location Server failed to parse the Wi-Fi location.", + 11: "Location Server failed to parse the Bluetooth location.", + 12: "Failed to parse location due to the poor accuracy.", + 13: "Time synchronization failed.", + 14: "Failed due to the old Almanac.", + 15: "The GNSS +Bluetooth scan timed out." + } + if (STATUS_MAP[status] !== undefined) { + return {id: status, statusName: STATUS_MAP[status]} + } + return getInt(str) +} + +function getUpT2000 (messageValue) { + let interval = 0 + let workMode = getInt(messageValue.substring(10, 12)) + let heartbeatInterval = getMinsByMin(messageValue.substring(14, 18)) + let periodicInterval = getMinsByMin(messageValue.substring(18, 22)) + let eventInterval = getMinsByMin(messageValue.substring(22, 26)) + switch (workMode) { + case 0: + interval = heartbeatInterval + break + case 1: + interval = periodicInterval + break + case 2: + interval = eventInterval + break + } + let data = [ + { + measurementId: '3000', measurementValue: getBattery(messageValue.substring(0, 2)) + }, { + measurementId: '3502', measurementValue: getSoftVersion(messageValue.substring(2, 6)) + }, { + measurementId: '3001', measurementValue: getHardVersion(messageValue.substring(6, 10)) + }, { + measurementId: '3940', measurementValue: workMode + }, { + measurementId: '3965', measurementValue: getPositioningStrategy(messageValue.substring(12, 14)) + }, { + measurementId: '3942', measurementValue: heartbeatInterval + }, { + measurementId: '3943', measurementValue: periodicInterval + }, { + measurementId: '3944', measurementValue: eventInterval + }, { + measurementId: '3974', measurementValue: getInt(messageValue.substring(26, 28)) + }, { + measurementId: '3976', measurementValue: getInt(messageValue.substring(28, 30)) + }, { + measurementId: '3977', measurementValue: getInt(messageValue.substring(30, 32)) + }, { + measurementId: '3900', measurementValue: interval + }, { + measurementId: '3978', measurementValue: getInt(messageValue.substring(54, 56)) + }, { + measurementId: '3979', measurementValue: messageValue.substring(58, 58 + getInt(messageValue.substring(56, 58)) * 2) + } + ] + let motionSetting = getMotionSetting(messageValue.substring(32, 42)) + let staticsSetting = getStaticSetting(messageValue.substring(42, 48)) + let shockSetting = getShockSetting(messageValue.substring(48, 54)) + data = [...data, ...motionSetting, ...staticsSetting, ...shockSetting] + return data +} + +function getMotionSetting (str) { + return [ + {measurementId: '3946', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3947', measurementValue: getSensorValue(str.substring(2, 6), 1)}, + {measurementId: '3948', measurementValue: getMinsByMin(str.substring(6, 10))}, + ] +} + +function getStaticSetting (str) { + return [ + {measurementId: '3949', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3950', measurementValue: getMinsByMin(str.substring(2, 6))} + ] +} + +function getShockSetting (str) { + return [ + {measurementId: '3951', measurementValue: getInt(str.substring(0, 2))}, + {measurementId: '3952', measurementValue: getInt(str.substring(2, 6))} + ] +} + +function getBattery (batteryStr) { + return loraWANV2DataFormat(batteryStr) +} +function getSoftVersion (softVersion) { + return `${loraWANV2DataFormat(softVersion.substring(0, 2))}.${loraWANV2DataFormat(softVersion.substring(2, 4))}` +} +function getHardVersion (hardVersion) { + return `${loraWANV2DataFormat(hardVersion.substring(0, 2))}.${loraWANV2DataFormat(hardVersion.substring(2, 4))}` +} + +function getMinsByMin (str) { + return getInt(str) +} + +function getSensorValue (str, dig) { + if (str === '8000') { + return null + } else { + return loraWANV2DataFormat(str, dig) + } +} + +function isNull (str) { + if (str.substring(0, 1) !== '8') { + return false + } + for (let i = 1; i < str.length; i++) { + if (str.substring(i, i + 1) !== '0') { + return false + } + } + return true +} + +function getSignSensorValue (str, dig = 1) { + if (isNull(str)) { + return null + } + return loraWANV2DataFormat(str, dig) +} + +function bytes2HexString (arrBytes) { + var str = '' + for (var i = 0; i < arrBytes.length; i++) { + var tmp + var num = arrBytes[i] + if (num < 0) { + tmp = (255 + num + 1).toString(16) + } else { + tmp = num.toString(16) + } + if (tmp.length === 1) { + tmp = '0' + tmp + } + str += tmp + } + return str +} +function loraWANV2DataFormat (str, divisor = 1) { + let strReverse = bigEndianTransform(str) + let str2 = toBinary(strReverse) + if (str2.substring(0, 1) === '1') { + let arr = str2.split('') + let reverseArr = arr.map((item) => { + if (parseInt(item) === 1) { + return 0 + } else { + return 1 + } + }) + str2 = parseInt(reverseArr.join(''), 2) + 1 + return parseFloat('-' + str2 / divisor) + } + return parseInt(str2, 2) / divisor +} + +function bigEndianTransform (data) { + let dataArray = [] + for (let i = 0; i < data.length; i += 2) { + dataArray.push(data.substring(i, i + 2)) + } + return dataArray +} + +function toBinary (arr) { + let binaryData = arr.map((item) => { + let data = parseInt(item, 16) + .toString(2) + let dataLength = data.length + if (data.length !== 8) { + for (let i = 0; i < 8 - dataLength; i++) { + data = `0` + data + } + } + return data + }) + return binaryData.toString().replace(/,/g, '') +} + +function getMacAndRssiObj (pair) { + let pairs = [] + if (pair.length % 14 === 0) { + for (let i = 0; i < pair.length; i += 14) { + let mac = getMacAddress(pair.substring(i, i + 12)) + if (mac) { + let rssi = getInt8RSSI(pair.substring(i + 12, i + 14)) + pairs.push({mac: mac, rssi: rssi}) + } else { + continue + } + } + } + return pairs +} + +function getMacAddress (str) { + if (str.toLowerCase() === 'ffffffffffff') { + return null + } + let macArr = [] + for (let i = 1; i < str.length; i++) { + if (i % 2 === 1) { + macArr.push(str.substring(i - 1, i + 1)) + } + } + let mac = '' + for (let i = 0; i < macArr.length; i++) { + mac = mac + macArr[i] + if (i < macArr.length - 1) { + mac = mac + ':' + } + } + return mac +} + +function getInt8RSSI (str) { + return loraWANV2DataFormat(str) +} + +function getInt (str) { + return parseInt(str, 16) +} + +function getEventStatus (str) { + let bitStr = getByteArray(str) + let bitArr = [] + for (let i = 0; i < bitStr.length; i++) { + bitArr[i] = bitStr.substring(i, i + 1) + } + bitArr = bitArr.reverse() + const EVENT_MAP = { + 0: {id: 1, eventName: "Start moving event."}, + 1: {id: 2, eventName: "End movement event."}, + 2: {id: 3, eventName: "Motionless event."}, + 3: {id: 4, eventName: "Shock event."}, + 4: {id: 5, eventName: "Temperature event."}, + 5: {id: 6, eventName: "Light event."}, + 6: {id: 7, eventName: "SOS event."}, + 7: {id: 8, eventName: "Press once event."}, + 8: {id: 9, eventName: "disassembled event"} + } + let event = [] + for (let i = 0; i < bitArr.length; i++) { + if (bitArr[i] === '1' && EVENT_MAP[i]) { + event.push(EVENT_MAP[i]) + } + } + return event +} + +function getByteArray (str) { + let bytes = [] + for (let i = 0; i < str.length; i += 2) { + bytes.push(str.substring(i, i + 2)) + } + return toBinary(bytes) +} + +function getWorkingMode (workingMode) { + return getInt(workingMode) +} + +function getPositioningStrategy (strategy) { + return getInt(strategy) +} + +function getUTCTimestamp(str){ + return parseInt(loraWANV2PositiveDataFormat(str)) * 1000 +} + +function loraWANV2PositiveDataFormat (str, divisor = 1) { + let strReverse = bigEndianTransform(str) + let str2 = toBinary(strReverse) + return parseInt(str2, 2) / divisor +} + +/** + * Encode downlink function. + * + * @param {object} input + * @param {object} input.data Object representing the payload that must be encoded. + * @param {Record} input.variables Object containing the configured device variables. + * + * @returns {{bytes: number[]}} Byte array containing the downlink payload. + */ +function encodeDownlink(input) { + return { + // bytes: [225, 230, 255, 0] + }; +} diff --git a/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-S2100.json b/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-S2100.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-S2100.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-T1000-AB.json b/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-T1000-AB.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-T1000-AB.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-T1000-E.json b/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-T1000-E.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-T1000-E.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-T2000-AB.json b/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-T2000-AB.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/test_decode_SenseCAP-T2000-AB.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-S2100.json b/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-S2100.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-S2100.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-T1000-AB.json b/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-T1000-AB.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-T1000-AB.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-T1000-E.json b/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-T1000-E.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-T1000-E.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-T2000-AB.json b/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-T2000-AB.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/vendors/seeed-technology-co-ltd/codecs/test_encode_SenseCAP-T2000-AB.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-s2100.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-s2100.toml index 793e2b0..a00a21b 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-s2100.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-s2100.toml @@ -10,7 +10,10 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] +codec = "SenseCAP-S2100.js" [device.metadata] product_url = "https://www.seeedstudio.com/SenseCAP-S2100-LoRaWAN-Data-Logger-p-5361.html" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-s2101.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-s2101.toml index 7abd591..d2dc914 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-s2101.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-s2101.toml @@ -10,6 +10,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_LoRaWAN_V4_Decoder.js" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-s2102.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-s2102.toml index 8ec3fcc..dcd7ed6 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-s2102.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-s2102.toml @@ -10,6 +10,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_LoRaWAN_V4_Decoder.js" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-s2103.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-s2103.toml index ab5c2c4..ba1f75c 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-s2103.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-s2103.toml @@ -10,6 +10,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_LoRaWAN_V4_Decoder.js" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-s2104.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-s2104.toml index ab0729c..7b10e55 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-s2104.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-s2104.toml @@ -10,6 +10,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_LoRaWAN_V4_Decoder.js" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-s2105.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-s2105.toml index bc15f44..0b48171 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-s2105.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-s2105.toml @@ -10,6 +10,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_LoRaWAN_V4_Decoder.js" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-s2106.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-s2106.toml index c1b1a81..76bd049 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-s2106.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-s2106.toml @@ -10,6 +10,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_LoRaWAN_V4_Decoder.js" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-s2107.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-s2107.toml index 0f884e7..3fdc3ee 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-s2107.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-s2107.toml @@ -10,6 +10,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_LoRaWAN_V4_Decoder.js" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-s2120.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-s2120.toml index f57cd70..a4d1005 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-s2120.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-s2120.toml @@ -10,6 +10,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_S2120_Weather_Station.js" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-a.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-a.toml index a9e689a..0589759 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-a.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-a.toml @@ -20,6 +20,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_LoRaWAN_V4_Decoder.js" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-ab-mapper.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-ab-mapper.toml index 417ccea..313fdb4 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-ab-mapper.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-ab-mapper.toml @@ -20,6 +20,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_T1000_mapper.js" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-b.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-b.toml index fdba6f9..859e271 100644 --- a/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-b.toml +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-t1000-b.toml @@ -10,6 +10,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_LoRaWAN_V4_Decoder.js" @@ -20,6 +22,8 @@ profiles = [ "AU915-1_0_3.toml", "EU868-1_0_3.toml", "US915-1_0_3.toml", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", ] codec = "SenseCAP_LoRaWAN_V4_Decoder.js" diff --git a/vendors/seeed-technology-co-ltd/devices/sensecap-t2000.toml b/vendors/seeed-technology-co-ltd/devices/sensecap-t2000.toml new file mode 100644 index 0000000..4de57ab --- /dev/null +++ b/vendors/seeed-technology-co-ltd/devices/sensecap-t2000.toml @@ -0,0 +1,20 @@ +[device] +id = "dc48d269-c13c-4d63-b135-0e3881d543a8" +name = "SenseCAP T2000" +description = "Industrial-grade LoRaWAN® asset tracker, supports GNSS, Bluetooth and Wi-Fi positioning for reliable tracking across indoor and outdoor environments. It features IP67 protection, a built-in 3-axis accelerometer that detects the motion status, and an anti-tamper button that triggers a top-priority alarm if the device is removed. The T2000-A and T2000-B support long-lasting battery operation, while the solar-powered T2000-C with a rechargeable battery ensures continuous outdoor use, making the series ideal for long-term, maintenance-free asset tracking." + +[[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", + "IN865-1_0_3.toml", + "KR920-1_0_3.toml", +] +codec = "SenseCAP-T2000-AB.js" + +[device.metadata] +product_url = "https://www.seeedstudio.com/SenseCAP-Asset-Tracker-T2000-C-p-6582.html" +documentation_url = "https://wiki.seeedstudio.com/sensecap_t2000_tracker/" diff --git a/vendors/seeed-technology-co-ltd/profiles/IN865-1_0_3.toml b/vendors/seeed-technology-co-ltd/profiles/IN865-1_0_3.toml new file mode 100644 index 0000000..aa14e8d --- /dev/null +++ b/vendors/seeed-technology-co-ltd/profiles/IN865-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "a5230e01-1c9d-4a15-9234-a3cb9191433a" +vendor_profile_id = 0 +region = "IN865" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 21 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/seeed-technology-co-ltd/profiles/KR920-1_0_3.toml b/vendors/seeed-technology-co-ltd/profiles/KR920-1_0_3.toml new file mode 100644 index 0000000..96f4ae5 --- /dev/null +++ b/vendors/seeed-technology-co-ltd/profiles/KR920-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "074632e0-a6eb-4d1b-9f93-ae331df79d5e" +vendor_profile_id = 0 +region = "KR920" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 16 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/seeed-technology-co-ltd/vendor.toml b/vendors/seeed-technology-co-ltd/vendor.toml index 5e4a9ed..ea33c54 100644 --- a/vendors/seeed-technology-co-ltd/vendor.toml +++ b/vendors/seeed-technology-co-ltd/vendor.toml @@ -16,6 +16,8 @@ devices = [ "sensecap-s2120.toml", "sensecap-t1000-a.toml", "sensecap-t1000-b.toml", + "sensecap-t1000-ab-mapper.toml", + "sensecap-t2000.toml", ] [vendor.metadata]