diff --git a/decoders/connector/rak/2171-wisnode-trackit/assets/logo.png b/decoders/connector/rak/2171-wisnode-trackit/assets/logo.png new file mode 100644 index 00000000..50f7dd73 Binary files /dev/null and b/decoders/connector/rak/2171-wisnode-trackit/assets/logo.png differ diff --git a/decoders/connector/rak/2171-wisnode-trackit/connector.jsonc b/decoders/connector/rak/2171-wisnode-trackit/connector.jsonc new file mode 100644 index 00000000..126befa8 --- /dev/null +++ b/decoders/connector/rak/2171-wisnode-trackit/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "RAKWireless 2171 WisNode TrackIt", + "images": { + "logo": "./assets/logo.png" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.js", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/connector/rak/2171-wisnode-trackit/description.md b/decoders/connector/rak/2171-wisnode-trackit/description.md new file mode 100644 index 00000000..0f8c7519 --- /dev/null +++ b/decoders/connector/rak/2171-wisnode-trackit/description.md @@ -0,0 +1 @@ +The RAK2171 is a GPS tracking device over LoRaWAN \ No newline at end of file diff --git a/decoders/connector/rak/2171-wisnode-trackit/v1.0.0/payload-config.jsonc b/decoders/connector/rak/2171-wisnode-trackit/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..988183a9 --- /dev/null +++ b/decoders/connector/rak/2171-wisnode-trackit/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "WisNode TrackIt (RAK2171) is RAKwireless LoRaWAN GPS tracking device. It comes in a small form factor with rechargeable battery and tracking \r\n\r\n\r\n**Features:**\r\n\r\n* Built-in LoRaWAN, Bluetooth, and GPS\r\n* Built-in battery: 400 mAh\r\n* Charger with a magnetic plate\r\n* Tracker Dimensions: 42x42x18 mm\r\n* Tracker weight: 25 g\r\n* Built-in accelerometer\r\n* Operating temperature: -20° C to +60° C", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-kerlink/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-senra/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/rak/2171-wisnode-trackit/v1.0.0/payload.js b/decoders/connector/rak/2171-wisnode-trackit/v1.0.0/payload.js new file mode 100644 index 00000000..6a0cfacb --- /dev/null +++ b/decoders/connector/rak/2171-wisnode-trackit/v1.0.0/payload.js @@ -0,0 +1,119 @@ +/* eslint-disable no-plusplus */ +/* eslint-disable no-redeclare */ +/* eslint-disable vars-on-top */ +/* eslint-disable no-var */ +/* eslint-disable default-case */ +/* eslint-disable prefer-destructuring */ + +function ToTagoFormat(object_item, serie, prefix = "") { + const result = []; + for (const key in object_item) { + if (typeof object_item[key] === "object") { + result.push({ + variable: (object_item[key].MessageType || `${prefix}${key}`).toLowerCase(), + value: `${object_item[key].lat}, ${object_item[key].lng}`, + serie: object_item[key].serie || serie, + metadata: object_item[key].metadata, + unit: object_item[key].unit, + location: { lat: Number(object_item[key].lat), lng: Number(object_item[key].lng) }, + }); + } else { + result.push({ + variable: `${prefix}${key}`.toLowerCase(), + value: object_item[key], + serie, + }); + } + } + + return result; +} +function Decoder(bytes) { + const decoded = {}; + + // adjust time zone, here Asia/Manila = +8H + const my_time_zone = 8 * 60 * 60; + + decoded.num = bytes[1]; + decoded.app_id = (bytes[2] << 24) | (bytes[3] << 16) | (bytes[4] << 8) | bytes[5]; + decoded.dev_id = (bytes[6] << 24) | (bytes[7] << 16) | (bytes[8] << 8) | bytes[9]; + switch (bytes[0]) { + case 0xca: // No Location fix + decoded.acc = 0; + decoded.fix = 0; + decoded.batt = bytes[10]; + decoded.time = (bytes[11] << 24) | (bytes[12] << 16) | (bytes[13] << 8) | bytes[14]; + // adjust time zone + decoded.time += my_time_zone; + var dev_date = new Date(decoded.time * 1000); + decoded.time_stamp = `${dev_date.getHours()}:${dev_date.getMinutes()}`; + decoded.date_stamp = `${dev_date.getDate()}.${dev_date.getMonth() + 1}.${dev_date.getFullYear()}`; + decoded.time = String((bytes[11] << 24) | (bytes[12] << 16) | (bytes[13] << 8) | bytes[14]); + decoded.stat = bytes[15] & 0x03; + decoded.gps = bytes[15] & 0x0c; + break; + case 0xcb: // Location fix + decoded.fix = 1; + decoded.batt = bytes[20]; + decoded.time = (bytes[21] << 24) | (bytes[22] << 16) | (bytes[23] << 8) | bytes[24]; + // adjust time zone + decoded.time += my_time_zone; + var dev_date = new Date(decoded.time * 1000); + decoded.time_stamp = `${dev_date.getHours()}:${dev_date.getMinutes()}`; + decoded.date_stamp = `${dev_date.getDate()}.${dev_date.getMonth() + 1}.${dev_date.getFullYear()}`; + decoded.time = String((bytes[21] << 24) | (bytes[22] << 16) | (bytes[23] << 8) | bytes[24]); + decoded.stat = bytes[25] & 0x03; + decoded.gps = bytes[25] & 0x0c; + decoded.location = { + lat: (((bytes[14] << 24) | (bytes[15] << 16) | (bytes[16] << 8) | bytes[17]) * 0.000001).toFixed(6), + lng: (((bytes[10] << 24) | (bytes[11] << 16) | (bytes[12] << 8) | bytes[13]) * 0.000001).toFixed(6), + }; + decoded.acc = bytes[18]; + decoded.gps_start = bytes[19]; + break; + case 0xcc: // SOS + decoded.sos = 1; + decoded.location = { + lat: (((bytes[14] << 24) | (bytes[15] << 16) | (bytes[16] << 8) | bytes[17]) * 0.000001).toFixed(6), + lng: (((bytes[10] << 24) | (bytes[11] << 16) | (bytes[12] << 8) | bytes[13]) * 0.000001).toFixed(6), + }; + if (bytes.length > 18) { + let i; + for (i = 18; i < 28; i++) { + decoded.name += bytes[i].toString(); + } + for (i = 28; i < 40; i++) { + decoded.country += bytes[i].toString(); + } + for (i = 39; i < 50; i++) { + decoded.phone += bytes[i].toString(); + } + } + break; + case 0xcd: + decoded.sos = 0; + break; + case 0xce: + decoded.alarm = 0x01; + decoded.alarm_lvl = bytes[10]; + break; + } + return decoded; +} + +// let payload = [{ variable: "payload", value: "cb0e000000b9000000ce00d740e60289738b3b043762e27c6409" }]; +const payload_raw = payload.find((x) => x.variable === "payload_raw" || x.variable === "payload" || x.variable === "data"); +if (payload_raw) { + try { + // Convert the data from Hex to Javascript Buffer. + const buffer = Buffer.from(payload_raw.value, "hex"); + const serie = new Date().getTime(); + const payload_aux = ToTagoFormat(Decoder(buffer)); + payload = payload.concat(payload_aux.map((x) => ({ ...x, serie }))); + } catch (e) { + // Print the error to the Live Inspector. + console.error(e); + // Return the variable parse_error for debugging. + payload = [{ variable: "parse_error", value: e.message }]; + } +} diff --git a/decoders/connector/risinghf/rhf1s001/assets/logo.png b/decoders/connector/risinghf/rhf1s001/assets/logo.png new file mode 100644 index 00000000..85458866 Binary files /dev/null and b/decoders/connector/risinghf/rhf1s001/assets/logo.png differ diff --git a/decoders/connector/risinghf/rhf1s001/connector.jsonc b/decoders/connector/risinghf/rhf1s001/connector.jsonc new file mode 100644 index 00000000..5cd192f6 --- /dev/null +++ b/decoders/connector/risinghf/rhf1s001/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "RisingHF RHF1S001", + "images": { + "logo": "./assets/logo.png" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.js", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/connector/risinghf/rhf1s001/description.md b/decoders/connector/risinghf/rhf1s001/description.md new file mode 100644 index 00000000..322417d0 --- /dev/null +++ b/decoders/connector/risinghf/rhf1s001/description.md @@ -0,0 +1 @@ +Outdoor IP64 Temperature and Humidity sensor from RisingHF \ No newline at end of file diff --git a/decoders/connector/risinghf/rhf1s001/v1.0.0/payload-config.jsonc b/decoders/connector/risinghf/rhf1s001/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..caef249f --- /dev/null +++ b/decoders/connector/risinghf/rhf1s001/v1.0.0/payload-config.jsonc @@ -0,0 +1,23 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "Outdoor IP64 Temperature and Humidity LoRaWAN sensor RHF1S001 offers cost effective LoRaWAN end node solution for a variety of applications. \n\nIP64 enclosure concept and water splash proof unique sensor design are ideal for outdoor use and installation in low accessibility locations.\n##\n* Powered by lithium thionyl chloride battery: 5 years of operation for 1 uplink 2 minutes.\n* Extended industrial operating temperature: -40°C to +85°C. ?\n* Outdoor use: IP64 enclosure.\n* ±5% RH typically from 20% RH to 80% RH at 25°C.\n* ±0.5°C typically from +5°C to +60°C.\n* LoRaWAN compatible: Class A, uplink rate programmable from 2 minutes to 24 hours.\n", + "install_end_text": "", + "device_annotation": "Install the dashboard template: https://admin.tago.io/template/5c155bec93d838001dcc246b", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-kerlink/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/risinghf/rhf1s001/v1.0.0/payload.js b/decoders/connector/risinghf/rhf1s001/v1.0.0/payload.js new file mode 100644 index 00000000..6f08ec07 --- /dev/null +++ b/decoders/connector/risinghf/rhf1s001/v1.0.0/payload.js @@ -0,0 +1,77 @@ +/* This is an generic payload parser example. +** The code find the payload variable and parse it if exists. +** +** IMPORTANT: In most case, you will only need to edit the parsePayload function. +** +** Testing: +** You can do manual tests to this parse by using the Device Emulator. Copy and Paste the following code: +** [{ "variable": "payload", "value": "0109611395" }] +** +** The ignore_vars variable in this code should be used to ignore variables +** from the device that you don't want. +*/ +// Add ignorable variables in this array. +const ignore_vars = ['fcnt', 'EUI', 'port', 'ts', 'freq', 'dr', 'cmd', 'ack']; + +/** + * This is the main function to parse the payload. Everything else doesn't require your attention. + * @param {String} payload_raw + * @returns {Object} containing key and value to TagoIO + */ +function parsePayload(payload_raw) { + try { + // If your device is sending something different than hex, like base64, just specify it bellow. + const bytes = Buffer.from(payload_raw, 'hex'); + + const obj = {}; + + // temp + const tempEncoded = (bytes[2] << 8) | (bytes[1]); + const tempDecoded = (tempEncoded * 175.72 / 65536) - 46.85; + obj.temp = tempDecoded.toFixed(2); + + // humidity + const humEncoded = (bytes[3]); + const humDecoded = (humEncoded * 125 / 256) - 6; + obj.hum = humDecoded.toFixed(2); + + // period + const periodEncoded = (bytes[5] << 8) | (bytes[4]); + const periodDecoded = (periodEncoded * 2); + obj.period = `${periodDecoded}`; + + // battery + const batteryEncoded = (bytes[8]); + const batteryDecoded = (batteryEncoded + 150) * 0.01; + obj.battery = batteryDecoded.toFixed(2); + console.log(obj); + const data = [ + { variable: 'temperature', value: Number(obj.temp), unit: '°C' }, + { variable: 'humidity', value: Number(obj.hum), unit: '%' }, + { variable: 'period', value: Number(obj.period), unit: 'sec' }, + { variable: 'battery', value: Number(obj.battery), unit: 'V' }, + ]; + return data; + } catch (e) { + console.log(e); + // Return the variable parse_error for debugging. + return [{ variable: 'parse_error', value: e.message }]; + } +} + +// Remove unwanted variables. +payload = payload.filter(x => !ignore_vars.includes(x.variable)); + +// Payload is an environment variable. Is where what is being inserted to your device comes in. +// Payload always is an array of objects. [ { variable, value...}, {variable, value...} ...] +const payload_raw = payload.find(x => x.variable === 'payload_raw' || x.variable === 'payload' || x.variable === 'data'); +if (payload_raw) { + // Get a unique serie for the incoming data. + const { value, serie, time } = payload_raw; + + // Parse the payload_raw to JSON format (it comes in a String format) + if (value) { + payload = payload.concat(parsePayload(value).map(x => ({ ...x, serie, time: x.time || time }))); + } +} + diff --git a/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/assets/logo.png b/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/assets/logo.png new file mode 100644 index 00000000..7f8bf8b4 Binary files /dev/null and b/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/assets/logo.png differ diff --git a/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/connector.jsonc b/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/connector.jsonc new file mode 100644 index 00000000..2691b366 --- /dev/null +++ b/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Seeed SenseCap Air Temperature and Humidity Sensor", + "images": { + "logo": "./assets/logo.png" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.js", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/description.md b/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/description.md new file mode 100644 index 00000000..7de8a082 --- /dev/null +++ b/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/description.md @@ -0,0 +1 @@ +Temperature and Humidity sensor over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..eb564fd2 --- /dev/null +++ b/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "SenseCAP Wireless Air Temperature & Humidity Sensor measures temperature and humidity in the atmosphere at the range of -40℃ to 85℃ and 0 to 100 %RH (non-condensing) respectively. With a high-precision measurement chip, this sensor features stability and reliability, making it widely applicable in industrial environmental sensing scenarios.\n\nThis device incorporates a built-in LoRa transmitter based on SX1276 for long-range transmission, a 2-in-1 sensor, and a custom battery. It is specifically designed and optimized for use cases powering end devices by batteries for years. To minimize the power consumption, the device wakes up, transmits the collected temperature and humidity data to the gateway, and then goes back to sleep.\n\n**Air Temperature Sensor Specifications**\n* range: -40 ℃ to +85 ℃\n* accuracy: ±0.2 ℃\n* Resolution: 0.1 ℃\n* Drift: <0.03 ℃/year\n\n**Air Humidity Sensor Specifications**\n* range: 0 to 100 %RH (non-condensing)\n* accuracy: ±1.5 %RH\n* Resolution: \t1 %RH\n* Drift: <0.25 %RH/year\n\nDocumentation:\nhttps://sensecap-docs.seeed.cc/pdf/SenseCAP%20LoRaWAN%20Sensor%20User%20Manual-V1.1.pdf", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/v1.0.0/payload.js", + "../../../../network/lorawan-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-kerlink/v1.0.0/payload.js", + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-senra/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/v1.0.0/payload.js b/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..1fa88729 --- /dev/null +++ b/decoders/connector/seeed/sensecap-air-temperature-and-humidity-sensor/v1.0.0/payload.js @@ -0,0 +1,448 @@ +/** + * SenseCAP & TTN Converter + * + * @since 1.0 + * @return Object + * @param Boolean valid Indicates whether the payload is a valid payload. + * @param String err The reason for the payload to be invalid. 0 means valid, minus means invalid. + * @param String payload Hexadecimal string, to show the payload. + * @param Array messages One or more messages are parsed according to payload. + * type // Enum: + * // - "report_telemetry" + * // - "upload_battery" + * // - "upload_interval" + * // - "upload_version" + * // - "upload_sensor_id" + * // - "report_remove_sensor" + * // - "unknown_message" + * + * + * + * + * @sample-1 + * var sample = Decoder(["00", "00", "00", "01", "01", "00", "01", "00", "07", "00", "64", "00", "3C", "00", "01", "20", "01", "00", "00", "00", "00", "28", "90"], null); + * { + * valid: true, + * err: 0, + * payload: '0000000101000100070064003C00012001000000002890', + * messages: [ + * { type: 'upload_version', + * hardwareVersion: '1.0', + * softwareVersion: '1.1' }, + * { type: 'upload_battery', battery: 100 }, + * { type: 'upload_interval', interval: 3600 }, + * { type: 'report_remove_sensor', channel: 1 } + * ] + * } + * @sample-2 + * var sample = Decoder(["01", "01", "10", "98", "53", "00", "00", "01", "02", "10", "A8", "7A", "00", "00", "AF", "51"], null); + * { + * valid: true, + * err: 0, + * payload: '01011098530000010210A87A0000AF51', + * messages: [ + * { type: 'report_telemetry', + * measurementId: 4097, + * measurementValue: 21.4 }, + * { type: 'report_telemetry', + * measurementId: 4098, + * measurementValue: 31.4 } + * ] + * } + * @sample-3 + * var sample = Decoder(["01", "01", "00", "01", "01", "00", "01", "01", "02", "00", "6A", "01", "00", "15", "01", "03", "00", "30", "F1", "F7", "2C", "01", "04", "00", "09", "0C", "13", "14", "01", "05", "00", "7F", "4D", "00", "00", "01", "06", "00", "00", "00", "00", "00", "4C", "BE"], null); + * { + * valid: true, + * err: 0, + * payload: '010100010100010102006A01001501030030F1F72C010400090C13140105007F4D0000010600000000004CBE', + * messages: [ + * { type: 'upload_sensor_id', sensorId: '2CF7F1301500016A', channel: 1 } + * ] + * } + */ + +// util +function toBinary(arr) { + const binaryData = []; + // eslint-disable-next-line no-plusplus + for (let forArr = 0; forArr < arr.length; forArr++) { + const item = arr[forArr]; + let data = parseInt(item, 16).toString(2); + const dataLength = data.length; + if (data.length !== 8) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 8 - dataLength; i++) { + data = `0${data}`; + } + } + binaryData.push(data); + } + return binaryData.toString().replace(/,/g, ""); +} + +function crc16Check(data) { + return true; +} + +// util +function bytes2HexString(arrBytes) { + let str = ""; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < arrBytes.length; i++) { + let tmp; + const 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; +} + +// util +function divideBy7Bytes(str) { + const frameArray = []; + for (let i = 0; i < str.length - 4; i += 14) { + const data = str.substring(i, i + 14); + frameArray.push(data); + } + return frameArray; +} + +// util +function littleEndianTransform(data) { + const dataArray = []; + for (let i = 0; i < data.length; i += 2) { + dataArray.push(data.substring(i, i + 2)); + } + dataArray.reverse(); + return dataArray; +} + +// util +function strTo10SysNub(str) { + const arr = littleEndianTransform(str); + return parseInt(arr.toString().replace(/,/g, ""), 16); +} + +// util +function checkDataIdIsMeasureUpload(dataId) { + return parseInt(dataId, 10) > 4096; +} + +// configurable. +function isSpecialDataId(dataID) { + switch (dataID) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 7: + case 0x120: + return true; + default: + return false; + } +} + +// configurable +function ttnDataSpecialFormat(dataId, str) { + const strReverse = littleEndianTransform(str); + if (dataId === 2 || dataId === 3) { + return strReverse.join(""); + } + + // handle unsigned number + const str2 = toBinary(strReverse); + + const dataArray = []; + switch (dataId) { + case 0: // DATA_BOARD_VERSION + case 1: // DATA_SENSOR_VERSION + // Using point segmentation + for (let k = 0; k < str2.length; k += 16) { + let tmp146 = str2.substring(k, k + 16); + tmp146 = `${parseInt(tmp146.substring(0, 8), 2) || 0}.${ + parseInt(tmp146.substring(8, 16), 2) || 0 + }`; + dataArray.push(tmp146); + } + return dataArray.join(","); + case 4: + for (let i = 0; i < str2.length; i += 8) { + let item = parseInt(str2.substring(i, i + 8), 2); + if (item < 10) { + item = `0${item.toString()}`; + } else { + item = item.toString(); + } + dataArray.push(item); + } + return dataArray.join(""); + case 7: + // battery && interval + return { + interval: parseInt(str2.substr(0, 16), 2), + power: parseInt(str2.substr(-16, 16), 2), + }; + default: + return []; + } +} + +// util +function ttnDataFormat(str) { + const strReverse = littleEndianTransform(str); + let str2 = toBinary(strReverse); + if (str2.substring(0, 1) === "1") { + const arr = str2.split(""); + const reverseArr = []; + // eslint-disable-next-line no-plusplus + for (let forArr = 0; forArr < arr.length; forArr++) { + const item = arr[forArr]; + if (parseInt(item, 2) === 1) { + reverseArr.push(0); + } else { + reverseArr.push(1); + } + } + str2 = parseInt(reverseArr.join(""), 2) + 1; + return `-${str2 / 1000}`; + } + return parseInt(str2, 2) / 1000; +} + +// util +function sensorAttrForVersion(dataValue) { + const dataValueSplitArray = dataValue.split(","); + return { + ver_hardware: dataValueSplitArray[0], + ver_software: dataValueSplitArray[1], + }; +} + +/** + * Entry, decoder.js + */ +function Decoder(bytes) { + // init + const bytesString = bytes2HexString(bytes).toLocaleUpperCase(); + const decoded = { + // valid + valid: true, + err: 0, + // bytes + payload: bytesString, + // messages array + messages: [], + }; + + // Cache sensor id + let sensorEuiLowBytes; + let sensorEuiHighBytes; + const frameArray = divideBy7Bytes(bytesString); + const id_soil = (bytes[0] << 8) | bytes[1]; + if (id_soil === 3088) { + decoded.messages.push({ ec_id: "100C" }); + + decoded.messages.push({ + ec_Value: + (bytes[2] | (bytes[3] << 8) | (bytes[4] << 16) | (bytes[5] << 24)) / + 1000, + }); + return decoded; + } + // CRC check + if (!crc16Check(bytesString)) { + decoded.valid = false; + decoded.err = -1; // "crc check fail." + return decoded; + } + + // Length Check + if ((bytesString.length / 2 - 2) % 7 !== 0) { + decoded.valid = false; + decoded.err = -2; // "length check fail." + return decoded; + } + + // Handle each frame + // eslint-disable-next-line no-plusplus + for (let forFrame = 0; forFrame < frameArray.length; forFrame++) { + const frame = frameArray[forFrame]; + // Extract key parameters + // const channel = strTo10SysNub(frame.substring(0, 2)); + const dataID = strTo10SysNub(frame.substring(2, 6)); + const dataValue = frame.substring(6, 14); + const realDataValue = isSpecialDataId(dataID) + ? ttnDataSpecialFormat(dataID, dataValue) + : ttnDataFormat(dataValue); + // eslint-disable-next-line no-console + // console.log(dataID, dataValue, realDataValue); + + if (checkDataIdIsMeasureUpload(dataID)) { + // if telemetry. + if (dataID === 4097) + decoded.messages.push({ temperature: realDataValue }); + if (dataID === 4108 || dataID === 4111) + decoded.messages.push({ soil_ec: realDataValue }); + else if (dataID === 4098) + decoded.messages.push({ humidity: realDataValue }); + else if (dataID === 4100) decoded.messages.push({ co2: realDataValue }); + else if (dataID === 4102 || dataID === 4112) + decoded.messages.push({ soil_temperature: realDataValue }); + else if (dataID === 4103 || dataID === 4110) + decoded.messages.push({ soil_moisture: realDataValue }); + else if (dataID === 4099) + decoded.messages.push({ ligh_itensity: realDataValue }); + else if (dataID === 4101) + decoded.messages.push({ barometric_pressure: realDataValue }); + decoded.messages.push({ + type: "report_telemetry", + // measurementId: dataID, + // measurementValue: realDataValue, + }); + } else if (isSpecialDataId(dataID) || dataID === 5 || dataID === 6) { + // if special order, except "report_sensor_id". + switch (dataID) { + case 0x00: + // node version + decoded.messages.push({ + type: "upload_version", + hardwareVersion: sensorAttrForVersion(realDataValue).ver_hardware, + softwareVersion: sensorAttrForVersion(realDataValue).ver_software, + }); + break; + case 1: + // sensor version + break; + case 2: + // sensor eui, low bytes + sensorEuiLowBytes = realDataValue; + break; + case 3: + // sensor eui, high bytes + sensorEuiHighBytes = realDataValue; + break; + case 7: + // battery power && interval + decoded.messages.push( + { type: "upload_battery", battery: realDataValue.power }, + { + type: "upload_interval", + interval: parseInt(realDataValue.interval, 10) * 60, + } + ); + break; + case 0x120: + // remove sensor + decoded.messages.push({ + type: "report_remove_sensor", + channel: 1, + }); + break; + default: + break; + } + } else { + decoded.messages.push({ + type: "unknown_message", + dataID, + dataValue, + }); + } + } + + // if the complete id received, as "upload_sensor_id" + if (sensorEuiHighBytes && sensorEuiLowBytes) { + decoded.messages.unshift({ + type: "upload_sensor_id", + channel: 1, + sensorId: (sensorEuiHighBytes + sensorEuiLowBytes).toUpperCase(), + }); + } + + // return + return decoded; +} + +function ToTagoFormat(object_item, serie, prefix = "") { + const result = []; + const messages = []; + // eslint-disable-next-line guard-for-in + for (let i = 0; i < object_item.messages.length; i += 1) { + if (typeof object_item.messages[i] === "object") { + // eslint-disable-next-line guard-for-in + for (const item in object_item.messages[i]) { + let data_to_send = { + variable: item.toLowerCase(), + value: + typeof object_item.messages[i][item] === "string" + ? object_item.messages[i][item].toLowerCase() + : object_item.messages[i][item], + serie, + }; + if (item === "temperature") data_to_send.unit = "°C"; + else if (item === "humidity") data_to_send.unit = "%"; + else if (item === "co2") data_to_send.unit = "ppm"; + else if (item === "soil_temperature") data_to_send.unit = "°C"; + else if (item === "soil_moisture") data_to_send.unit = "%"; + else if (item === "ligh_itensity") data_to_send.unit = "lux"; + else if (item === "barometric_pressure") data_to_send.unit = "pa"; + else if (item === "soil_ec") data_to_send.unit = "dS/m"; + messages.push(data_to_send); + } + } + } + delete object_item.messages; + for (const key in object_item) { + if (typeof object_item[key] === "object") { + result.push({ + variable: ( + object_item[key].MessageType || `${prefix}${key}` + ).toLowerCase(), + value: + // eslint-disable-next-line no-nested-ternary + typeof object_item[key].value === "string" + ? object_item[key].value.toLowerCase() + : object_item[key].value || + typeof object_item[key].Value === "string" + ? object_item[key].Value.toLowerCase() + : object_item[key].Value, + serie: object_item[key].serie || serie, + metadata: object_item[key].metadata, + unit: object_item[key].unit, + location: object_item[key].location, + }); + } else { + result.push({ + variable: `${prefix}${key}`.toLowerCase(), + value: + typeof object_item[key] === "string" + ? object_item[key].toLowerCase() + : object_item[key], + serie, + }); + } + } + return result.concat(messages); +} + +const data = payload.find( + (x) => + x.variable === "payload_raw" || + x.variable === "payload" || + x.variable === "data" +); + +if (data) { + const buffer = Buffer.from(data.value, "hex"); + const serie = new Date().getTime(); + payload = ToTagoFormat(Decoder(buffer), serie); +} diff --git a/decoders/connector/seeed/sensecap-barometric-pressure-sensor/assets/logo.png b/decoders/connector/seeed/sensecap-barometric-pressure-sensor/assets/logo.png new file mode 100644 index 00000000..25f8dc16 Binary files /dev/null and b/decoders/connector/seeed/sensecap-barometric-pressure-sensor/assets/logo.png differ diff --git a/decoders/connector/seeed/sensecap-barometric-pressure-sensor/connector.jsonc b/decoders/connector/seeed/sensecap-barometric-pressure-sensor/connector.jsonc new file mode 100644 index 00000000..6baaee44 --- /dev/null +++ b/decoders/connector/seeed/sensecap-barometric-pressure-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Seeed SenseCap Barometric Pressure Sensor", + "images": { + "logo": "./assets/logo.png" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.js", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/connector/seeed/sensecap-barometric-pressure-sensor/description.md b/decoders/connector/seeed/sensecap-barometric-pressure-sensor/description.md new file mode 100644 index 00000000..d2dbc565 --- /dev/null +++ b/decoders/connector/seeed/sensecap-barometric-pressure-sensor/description.md @@ -0,0 +1 @@ +Barometric Pressure sensor over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-barometric-pressure-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/seeed/sensecap-barometric-pressure-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..546685cf --- /dev/null +++ b/decoders/connector/seeed/sensecap-barometric-pressure-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "SenseCAP Wireless Barometric Pressure Sensor measures atmospheric pressure in the range of 300~1100 hPa. Featuring high-precision, stability, and high EMC robustness, this sensor is suitable for industrial applications such as weather stations, outdoor farms, tea plantations, greenhouses, and more. \n\n \nThis device incorporates a built-in LoRa transmitter based on SX1276 for long-range transmission, a barometric sensor, and a custom battery. It is specifically designed and optimized for use cases powering end devices by batteries for years. To minimize the power consumption, the device wakes up, transmits the collected air pressure data to the gateway, and then goes back to sleep.\n\n**Barometric Pressure Sensor Specifications**\n* Range: 300 to 1100 hPa\n* Resolution: 1 Pa\n* Relative Accuracy: ±0.12 hPa (condition: 700 to 900 hPa, 25 to 40 ℃)\n* Absolute Accuracy: ±1.7 hPa (condition: 300 to 1100 hPa, -20 to 0 ℃)\n* Absolute Accuracy: ±1.0 hPa (condition: 300 to 1100 hPa, 0 to 65 ℃)\n* Temperature Coefficient Offset: 1.5 Pa/K (condition: 900 hPa, 25 to 40 ℃)\n* Drift: ±1.0 hPa/year", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/v1.0.0/payload.js", + "../../../../network/lorawan-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-kerlink/v1.0.0/payload.js", + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-senra/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-barometric-pressure-sensor/v1.0.0/payload.js b/decoders/connector/seeed/sensecap-barometric-pressure-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..1fa88729 --- /dev/null +++ b/decoders/connector/seeed/sensecap-barometric-pressure-sensor/v1.0.0/payload.js @@ -0,0 +1,448 @@ +/** + * SenseCAP & TTN Converter + * + * @since 1.0 + * @return Object + * @param Boolean valid Indicates whether the payload is a valid payload. + * @param String err The reason for the payload to be invalid. 0 means valid, minus means invalid. + * @param String payload Hexadecimal string, to show the payload. + * @param Array messages One or more messages are parsed according to payload. + * type // Enum: + * // - "report_telemetry" + * // - "upload_battery" + * // - "upload_interval" + * // - "upload_version" + * // - "upload_sensor_id" + * // - "report_remove_sensor" + * // - "unknown_message" + * + * + * + * + * @sample-1 + * var sample = Decoder(["00", "00", "00", "01", "01", "00", "01", "00", "07", "00", "64", "00", "3C", "00", "01", "20", "01", "00", "00", "00", "00", "28", "90"], null); + * { + * valid: true, + * err: 0, + * payload: '0000000101000100070064003C00012001000000002890', + * messages: [ + * { type: 'upload_version', + * hardwareVersion: '1.0', + * softwareVersion: '1.1' }, + * { type: 'upload_battery', battery: 100 }, + * { type: 'upload_interval', interval: 3600 }, + * { type: 'report_remove_sensor', channel: 1 } + * ] + * } + * @sample-2 + * var sample = Decoder(["01", "01", "10", "98", "53", "00", "00", "01", "02", "10", "A8", "7A", "00", "00", "AF", "51"], null); + * { + * valid: true, + * err: 0, + * payload: '01011098530000010210A87A0000AF51', + * messages: [ + * { type: 'report_telemetry', + * measurementId: 4097, + * measurementValue: 21.4 }, + * { type: 'report_telemetry', + * measurementId: 4098, + * measurementValue: 31.4 } + * ] + * } + * @sample-3 + * var sample = Decoder(["01", "01", "00", "01", "01", "00", "01", "01", "02", "00", "6A", "01", "00", "15", "01", "03", "00", "30", "F1", "F7", "2C", "01", "04", "00", "09", "0C", "13", "14", "01", "05", "00", "7F", "4D", "00", "00", "01", "06", "00", "00", "00", "00", "00", "4C", "BE"], null); + * { + * valid: true, + * err: 0, + * payload: '010100010100010102006A01001501030030F1F72C010400090C13140105007F4D0000010600000000004CBE', + * messages: [ + * { type: 'upload_sensor_id', sensorId: '2CF7F1301500016A', channel: 1 } + * ] + * } + */ + +// util +function toBinary(arr) { + const binaryData = []; + // eslint-disable-next-line no-plusplus + for (let forArr = 0; forArr < arr.length; forArr++) { + const item = arr[forArr]; + let data = parseInt(item, 16).toString(2); + const dataLength = data.length; + if (data.length !== 8) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 8 - dataLength; i++) { + data = `0${data}`; + } + } + binaryData.push(data); + } + return binaryData.toString().replace(/,/g, ""); +} + +function crc16Check(data) { + return true; +} + +// util +function bytes2HexString(arrBytes) { + let str = ""; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < arrBytes.length; i++) { + let tmp; + const 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; +} + +// util +function divideBy7Bytes(str) { + const frameArray = []; + for (let i = 0; i < str.length - 4; i += 14) { + const data = str.substring(i, i + 14); + frameArray.push(data); + } + return frameArray; +} + +// util +function littleEndianTransform(data) { + const dataArray = []; + for (let i = 0; i < data.length; i += 2) { + dataArray.push(data.substring(i, i + 2)); + } + dataArray.reverse(); + return dataArray; +} + +// util +function strTo10SysNub(str) { + const arr = littleEndianTransform(str); + return parseInt(arr.toString().replace(/,/g, ""), 16); +} + +// util +function checkDataIdIsMeasureUpload(dataId) { + return parseInt(dataId, 10) > 4096; +} + +// configurable. +function isSpecialDataId(dataID) { + switch (dataID) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 7: + case 0x120: + return true; + default: + return false; + } +} + +// configurable +function ttnDataSpecialFormat(dataId, str) { + const strReverse = littleEndianTransform(str); + if (dataId === 2 || dataId === 3) { + return strReverse.join(""); + } + + // handle unsigned number + const str2 = toBinary(strReverse); + + const dataArray = []; + switch (dataId) { + case 0: // DATA_BOARD_VERSION + case 1: // DATA_SENSOR_VERSION + // Using point segmentation + for (let k = 0; k < str2.length; k += 16) { + let tmp146 = str2.substring(k, k + 16); + tmp146 = `${parseInt(tmp146.substring(0, 8), 2) || 0}.${ + parseInt(tmp146.substring(8, 16), 2) || 0 + }`; + dataArray.push(tmp146); + } + return dataArray.join(","); + case 4: + for (let i = 0; i < str2.length; i += 8) { + let item = parseInt(str2.substring(i, i + 8), 2); + if (item < 10) { + item = `0${item.toString()}`; + } else { + item = item.toString(); + } + dataArray.push(item); + } + return dataArray.join(""); + case 7: + // battery && interval + return { + interval: parseInt(str2.substr(0, 16), 2), + power: parseInt(str2.substr(-16, 16), 2), + }; + default: + return []; + } +} + +// util +function ttnDataFormat(str) { + const strReverse = littleEndianTransform(str); + let str2 = toBinary(strReverse); + if (str2.substring(0, 1) === "1") { + const arr = str2.split(""); + const reverseArr = []; + // eslint-disable-next-line no-plusplus + for (let forArr = 0; forArr < arr.length; forArr++) { + const item = arr[forArr]; + if (parseInt(item, 2) === 1) { + reverseArr.push(0); + } else { + reverseArr.push(1); + } + } + str2 = parseInt(reverseArr.join(""), 2) + 1; + return `-${str2 / 1000}`; + } + return parseInt(str2, 2) / 1000; +} + +// util +function sensorAttrForVersion(dataValue) { + const dataValueSplitArray = dataValue.split(","); + return { + ver_hardware: dataValueSplitArray[0], + ver_software: dataValueSplitArray[1], + }; +} + +/** + * Entry, decoder.js + */ +function Decoder(bytes) { + // init + const bytesString = bytes2HexString(bytes).toLocaleUpperCase(); + const decoded = { + // valid + valid: true, + err: 0, + // bytes + payload: bytesString, + // messages array + messages: [], + }; + + // Cache sensor id + let sensorEuiLowBytes; + let sensorEuiHighBytes; + const frameArray = divideBy7Bytes(bytesString); + const id_soil = (bytes[0] << 8) | bytes[1]; + if (id_soil === 3088) { + decoded.messages.push({ ec_id: "100C" }); + + decoded.messages.push({ + ec_Value: + (bytes[2] | (bytes[3] << 8) | (bytes[4] << 16) | (bytes[5] << 24)) / + 1000, + }); + return decoded; + } + // CRC check + if (!crc16Check(bytesString)) { + decoded.valid = false; + decoded.err = -1; // "crc check fail." + return decoded; + } + + // Length Check + if ((bytesString.length / 2 - 2) % 7 !== 0) { + decoded.valid = false; + decoded.err = -2; // "length check fail." + return decoded; + } + + // Handle each frame + // eslint-disable-next-line no-plusplus + for (let forFrame = 0; forFrame < frameArray.length; forFrame++) { + const frame = frameArray[forFrame]; + // Extract key parameters + // const channel = strTo10SysNub(frame.substring(0, 2)); + const dataID = strTo10SysNub(frame.substring(2, 6)); + const dataValue = frame.substring(6, 14); + const realDataValue = isSpecialDataId(dataID) + ? ttnDataSpecialFormat(dataID, dataValue) + : ttnDataFormat(dataValue); + // eslint-disable-next-line no-console + // console.log(dataID, dataValue, realDataValue); + + if (checkDataIdIsMeasureUpload(dataID)) { + // if telemetry. + if (dataID === 4097) + decoded.messages.push({ temperature: realDataValue }); + if (dataID === 4108 || dataID === 4111) + decoded.messages.push({ soil_ec: realDataValue }); + else if (dataID === 4098) + decoded.messages.push({ humidity: realDataValue }); + else if (dataID === 4100) decoded.messages.push({ co2: realDataValue }); + else if (dataID === 4102 || dataID === 4112) + decoded.messages.push({ soil_temperature: realDataValue }); + else if (dataID === 4103 || dataID === 4110) + decoded.messages.push({ soil_moisture: realDataValue }); + else if (dataID === 4099) + decoded.messages.push({ ligh_itensity: realDataValue }); + else if (dataID === 4101) + decoded.messages.push({ barometric_pressure: realDataValue }); + decoded.messages.push({ + type: "report_telemetry", + // measurementId: dataID, + // measurementValue: realDataValue, + }); + } else if (isSpecialDataId(dataID) || dataID === 5 || dataID === 6) { + // if special order, except "report_sensor_id". + switch (dataID) { + case 0x00: + // node version + decoded.messages.push({ + type: "upload_version", + hardwareVersion: sensorAttrForVersion(realDataValue).ver_hardware, + softwareVersion: sensorAttrForVersion(realDataValue).ver_software, + }); + break; + case 1: + // sensor version + break; + case 2: + // sensor eui, low bytes + sensorEuiLowBytes = realDataValue; + break; + case 3: + // sensor eui, high bytes + sensorEuiHighBytes = realDataValue; + break; + case 7: + // battery power && interval + decoded.messages.push( + { type: "upload_battery", battery: realDataValue.power }, + { + type: "upload_interval", + interval: parseInt(realDataValue.interval, 10) * 60, + } + ); + break; + case 0x120: + // remove sensor + decoded.messages.push({ + type: "report_remove_sensor", + channel: 1, + }); + break; + default: + break; + } + } else { + decoded.messages.push({ + type: "unknown_message", + dataID, + dataValue, + }); + } + } + + // if the complete id received, as "upload_sensor_id" + if (sensorEuiHighBytes && sensorEuiLowBytes) { + decoded.messages.unshift({ + type: "upload_sensor_id", + channel: 1, + sensorId: (sensorEuiHighBytes + sensorEuiLowBytes).toUpperCase(), + }); + } + + // return + return decoded; +} + +function ToTagoFormat(object_item, serie, prefix = "") { + const result = []; + const messages = []; + // eslint-disable-next-line guard-for-in + for (let i = 0; i < object_item.messages.length; i += 1) { + if (typeof object_item.messages[i] === "object") { + // eslint-disable-next-line guard-for-in + for (const item in object_item.messages[i]) { + let data_to_send = { + variable: item.toLowerCase(), + value: + typeof object_item.messages[i][item] === "string" + ? object_item.messages[i][item].toLowerCase() + : object_item.messages[i][item], + serie, + }; + if (item === "temperature") data_to_send.unit = "°C"; + else if (item === "humidity") data_to_send.unit = "%"; + else if (item === "co2") data_to_send.unit = "ppm"; + else if (item === "soil_temperature") data_to_send.unit = "°C"; + else if (item === "soil_moisture") data_to_send.unit = "%"; + else if (item === "ligh_itensity") data_to_send.unit = "lux"; + else if (item === "barometric_pressure") data_to_send.unit = "pa"; + else if (item === "soil_ec") data_to_send.unit = "dS/m"; + messages.push(data_to_send); + } + } + } + delete object_item.messages; + for (const key in object_item) { + if (typeof object_item[key] === "object") { + result.push({ + variable: ( + object_item[key].MessageType || `${prefix}${key}` + ).toLowerCase(), + value: + // eslint-disable-next-line no-nested-ternary + typeof object_item[key].value === "string" + ? object_item[key].value.toLowerCase() + : object_item[key].value || + typeof object_item[key].Value === "string" + ? object_item[key].Value.toLowerCase() + : object_item[key].Value, + serie: object_item[key].serie || serie, + metadata: object_item[key].metadata, + unit: object_item[key].unit, + location: object_item[key].location, + }); + } else { + result.push({ + variable: `${prefix}${key}`.toLowerCase(), + value: + typeof object_item[key] === "string" + ? object_item[key].toLowerCase() + : object_item[key], + serie, + }); + } + } + return result.concat(messages); +} + +const data = payload.find( + (x) => + x.variable === "payload_raw" || + x.variable === "payload" || + x.variable === "data" +); + +if (data) { + const buffer = Buffer.from(data.value, "hex"); + const serie = new Date().getTime(); + payload = ToTagoFormat(Decoder(buffer), serie); +} diff --git a/decoders/connector/seeed/sensecap-co2-sensor/assets/logo.png b/decoders/connector/seeed/sensecap-co2-sensor/assets/logo.png new file mode 100644 index 00000000..afef6a1c Binary files /dev/null and b/decoders/connector/seeed/sensecap-co2-sensor/assets/logo.png differ diff --git a/decoders/connector/seeed/sensecap-co2-sensor/connector.jsonc b/decoders/connector/seeed/sensecap-co2-sensor/connector.jsonc new file mode 100644 index 00000000..00c2a82a --- /dev/null +++ b/decoders/connector/seeed/sensecap-co2-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Seeed SenseCap CO2 Sensor", + "images": { + "logo": "./assets/logo.png" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.js", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/connector/seeed/sensecap-co2-sensor/description.md b/decoders/connector/seeed/sensecap-co2-sensor/description.md new file mode 100644 index 00000000..cf774edc --- /dev/null +++ b/decoders/connector/seeed/sensecap-co2-sensor/description.md @@ -0,0 +1 @@ +CO2 sensor over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-co2-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/seeed/sensecap-co2-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..35f54391 --- /dev/null +++ b/decoders/connector/seeed/sensecap-co2-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "SenseCAP Wireless CO2 Sensor measures the level of carbon dioxide (CO2) gas at the range of 0 ~ 40000 ppm in the atmosphere, applicable for both indoor and outdoor environments. It is perfect for monitoring CO2 ppm in outdoor farms, weather stations, greenhouses, industrial campuses, factories, schools, office buildings, hotels, hospitals, transportation stations, and anywhere data of CO2 emission is needed.\n\nThis device incorporates a built-in LoRa transmitter based on SX1276 for long-range transmission, an NDIR CO2 sensor, and a custom battery. It is specifically designed and optimized for use cases powering end devices by batteries for years. To minimize the power consumption, the device wakes up, transmits the collected CO2 data to the gateway, and then goes back to sleep.\n\n\n**CO2 Sensor Specifications**\n* Range: 0 - 40000 ppm\n* Accuracy: ±(30 ppm + 3 %MV) (condition: 400 to 10000ppm)\n* Resolution: 1 ppm\n* Temperature Stability: ±2.5 ppm/℃ (condition: T = 0 to 50 ℃)\n", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/v1.0.0/payload.js", + "../../../../network/lorawan-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-kerlink/v1.0.0/payload.js", + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-senra/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-co2-sensor/v1.0.0/payload.js b/decoders/connector/seeed/sensecap-co2-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..1fa88729 --- /dev/null +++ b/decoders/connector/seeed/sensecap-co2-sensor/v1.0.0/payload.js @@ -0,0 +1,448 @@ +/** + * SenseCAP & TTN Converter + * + * @since 1.0 + * @return Object + * @param Boolean valid Indicates whether the payload is a valid payload. + * @param String err The reason for the payload to be invalid. 0 means valid, minus means invalid. + * @param String payload Hexadecimal string, to show the payload. + * @param Array messages One or more messages are parsed according to payload. + * type // Enum: + * // - "report_telemetry" + * // - "upload_battery" + * // - "upload_interval" + * // - "upload_version" + * // - "upload_sensor_id" + * // - "report_remove_sensor" + * // - "unknown_message" + * + * + * + * + * @sample-1 + * var sample = Decoder(["00", "00", "00", "01", "01", "00", "01", "00", "07", "00", "64", "00", "3C", "00", "01", "20", "01", "00", "00", "00", "00", "28", "90"], null); + * { + * valid: true, + * err: 0, + * payload: '0000000101000100070064003C00012001000000002890', + * messages: [ + * { type: 'upload_version', + * hardwareVersion: '1.0', + * softwareVersion: '1.1' }, + * { type: 'upload_battery', battery: 100 }, + * { type: 'upload_interval', interval: 3600 }, + * { type: 'report_remove_sensor', channel: 1 } + * ] + * } + * @sample-2 + * var sample = Decoder(["01", "01", "10", "98", "53", "00", "00", "01", "02", "10", "A8", "7A", "00", "00", "AF", "51"], null); + * { + * valid: true, + * err: 0, + * payload: '01011098530000010210A87A0000AF51', + * messages: [ + * { type: 'report_telemetry', + * measurementId: 4097, + * measurementValue: 21.4 }, + * { type: 'report_telemetry', + * measurementId: 4098, + * measurementValue: 31.4 } + * ] + * } + * @sample-3 + * var sample = Decoder(["01", "01", "00", "01", "01", "00", "01", "01", "02", "00", "6A", "01", "00", "15", "01", "03", "00", "30", "F1", "F7", "2C", "01", "04", "00", "09", "0C", "13", "14", "01", "05", "00", "7F", "4D", "00", "00", "01", "06", "00", "00", "00", "00", "00", "4C", "BE"], null); + * { + * valid: true, + * err: 0, + * payload: '010100010100010102006A01001501030030F1F72C010400090C13140105007F4D0000010600000000004CBE', + * messages: [ + * { type: 'upload_sensor_id', sensorId: '2CF7F1301500016A', channel: 1 } + * ] + * } + */ + +// util +function toBinary(arr) { + const binaryData = []; + // eslint-disable-next-line no-plusplus + for (let forArr = 0; forArr < arr.length; forArr++) { + const item = arr[forArr]; + let data = parseInt(item, 16).toString(2); + const dataLength = data.length; + if (data.length !== 8) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 8 - dataLength; i++) { + data = `0${data}`; + } + } + binaryData.push(data); + } + return binaryData.toString().replace(/,/g, ""); +} + +function crc16Check(data) { + return true; +} + +// util +function bytes2HexString(arrBytes) { + let str = ""; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < arrBytes.length; i++) { + let tmp; + const 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; +} + +// util +function divideBy7Bytes(str) { + const frameArray = []; + for (let i = 0; i < str.length - 4; i += 14) { + const data = str.substring(i, i + 14); + frameArray.push(data); + } + return frameArray; +} + +// util +function littleEndianTransform(data) { + const dataArray = []; + for (let i = 0; i < data.length; i += 2) { + dataArray.push(data.substring(i, i + 2)); + } + dataArray.reverse(); + return dataArray; +} + +// util +function strTo10SysNub(str) { + const arr = littleEndianTransform(str); + return parseInt(arr.toString().replace(/,/g, ""), 16); +} + +// util +function checkDataIdIsMeasureUpload(dataId) { + return parseInt(dataId, 10) > 4096; +} + +// configurable. +function isSpecialDataId(dataID) { + switch (dataID) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 7: + case 0x120: + return true; + default: + return false; + } +} + +// configurable +function ttnDataSpecialFormat(dataId, str) { + const strReverse = littleEndianTransform(str); + if (dataId === 2 || dataId === 3) { + return strReverse.join(""); + } + + // handle unsigned number + const str2 = toBinary(strReverse); + + const dataArray = []; + switch (dataId) { + case 0: // DATA_BOARD_VERSION + case 1: // DATA_SENSOR_VERSION + // Using point segmentation + for (let k = 0; k < str2.length; k += 16) { + let tmp146 = str2.substring(k, k + 16); + tmp146 = `${parseInt(tmp146.substring(0, 8), 2) || 0}.${ + parseInt(tmp146.substring(8, 16), 2) || 0 + }`; + dataArray.push(tmp146); + } + return dataArray.join(","); + case 4: + for (let i = 0; i < str2.length; i += 8) { + let item = parseInt(str2.substring(i, i + 8), 2); + if (item < 10) { + item = `0${item.toString()}`; + } else { + item = item.toString(); + } + dataArray.push(item); + } + return dataArray.join(""); + case 7: + // battery && interval + return { + interval: parseInt(str2.substr(0, 16), 2), + power: parseInt(str2.substr(-16, 16), 2), + }; + default: + return []; + } +} + +// util +function ttnDataFormat(str) { + const strReverse = littleEndianTransform(str); + let str2 = toBinary(strReverse); + if (str2.substring(0, 1) === "1") { + const arr = str2.split(""); + const reverseArr = []; + // eslint-disable-next-line no-plusplus + for (let forArr = 0; forArr < arr.length; forArr++) { + const item = arr[forArr]; + if (parseInt(item, 2) === 1) { + reverseArr.push(0); + } else { + reverseArr.push(1); + } + } + str2 = parseInt(reverseArr.join(""), 2) + 1; + return `-${str2 / 1000}`; + } + return parseInt(str2, 2) / 1000; +} + +// util +function sensorAttrForVersion(dataValue) { + const dataValueSplitArray = dataValue.split(","); + return { + ver_hardware: dataValueSplitArray[0], + ver_software: dataValueSplitArray[1], + }; +} + +/** + * Entry, decoder.js + */ +function Decoder(bytes) { + // init + const bytesString = bytes2HexString(bytes).toLocaleUpperCase(); + const decoded = { + // valid + valid: true, + err: 0, + // bytes + payload: bytesString, + // messages array + messages: [], + }; + + // Cache sensor id + let sensorEuiLowBytes; + let sensorEuiHighBytes; + const frameArray = divideBy7Bytes(bytesString); + const id_soil = (bytes[0] << 8) | bytes[1]; + if (id_soil === 3088) { + decoded.messages.push({ ec_id: "100C" }); + + decoded.messages.push({ + ec_Value: + (bytes[2] | (bytes[3] << 8) | (bytes[4] << 16) | (bytes[5] << 24)) / + 1000, + }); + return decoded; + } + // CRC check + if (!crc16Check(bytesString)) { + decoded.valid = false; + decoded.err = -1; // "crc check fail." + return decoded; + } + + // Length Check + if ((bytesString.length / 2 - 2) % 7 !== 0) { + decoded.valid = false; + decoded.err = -2; // "length check fail." + return decoded; + } + + // Handle each frame + // eslint-disable-next-line no-plusplus + for (let forFrame = 0; forFrame < frameArray.length; forFrame++) { + const frame = frameArray[forFrame]; + // Extract key parameters + // const channel = strTo10SysNub(frame.substring(0, 2)); + const dataID = strTo10SysNub(frame.substring(2, 6)); + const dataValue = frame.substring(6, 14); + const realDataValue = isSpecialDataId(dataID) + ? ttnDataSpecialFormat(dataID, dataValue) + : ttnDataFormat(dataValue); + // eslint-disable-next-line no-console + // console.log(dataID, dataValue, realDataValue); + + if (checkDataIdIsMeasureUpload(dataID)) { + // if telemetry. + if (dataID === 4097) + decoded.messages.push({ temperature: realDataValue }); + if (dataID === 4108 || dataID === 4111) + decoded.messages.push({ soil_ec: realDataValue }); + else if (dataID === 4098) + decoded.messages.push({ humidity: realDataValue }); + else if (dataID === 4100) decoded.messages.push({ co2: realDataValue }); + else if (dataID === 4102 || dataID === 4112) + decoded.messages.push({ soil_temperature: realDataValue }); + else if (dataID === 4103 || dataID === 4110) + decoded.messages.push({ soil_moisture: realDataValue }); + else if (dataID === 4099) + decoded.messages.push({ ligh_itensity: realDataValue }); + else if (dataID === 4101) + decoded.messages.push({ barometric_pressure: realDataValue }); + decoded.messages.push({ + type: "report_telemetry", + // measurementId: dataID, + // measurementValue: realDataValue, + }); + } else if (isSpecialDataId(dataID) || dataID === 5 || dataID === 6) { + // if special order, except "report_sensor_id". + switch (dataID) { + case 0x00: + // node version + decoded.messages.push({ + type: "upload_version", + hardwareVersion: sensorAttrForVersion(realDataValue).ver_hardware, + softwareVersion: sensorAttrForVersion(realDataValue).ver_software, + }); + break; + case 1: + // sensor version + break; + case 2: + // sensor eui, low bytes + sensorEuiLowBytes = realDataValue; + break; + case 3: + // sensor eui, high bytes + sensorEuiHighBytes = realDataValue; + break; + case 7: + // battery power && interval + decoded.messages.push( + { type: "upload_battery", battery: realDataValue.power }, + { + type: "upload_interval", + interval: parseInt(realDataValue.interval, 10) * 60, + } + ); + break; + case 0x120: + // remove sensor + decoded.messages.push({ + type: "report_remove_sensor", + channel: 1, + }); + break; + default: + break; + } + } else { + decoded.messages.push({ + type: "unknown_message", + dataID, + dataValue, + }); + } + } + + // if the complete id received, as "upload_sensor_id" + if (sensorEuiHighBytes && sensorEuiLowBytes) { + decoded.messages.unshift({ + type: "upload_sensor_id", + channel: 1, + sensorId: (sensorEuiHighBytes + sensorEuiLowBytes).toUpperCase(), + }); + } + + // return + return decoded; +} + +function ToTagoFormat(object_item, serie, prefix = "") { + const result = []; + const messages = []; + // eslint-disable-next-line guard-for-in + for (let i = 0; i < object_item.messages.length; i += 1) { + if (typeof object_item.messages[i] === "object") { + // eslint-disable-next-line guard-for-in + for (const item in object_item.messages[i]) { + let data_to_send = { + variable: item.toLowerCase(), + value: + typeof object_item.messages[i][item] === "string" + ? object_item.messages[i][item].toLowerCase() + : object_item.messages[i][item], + serie, + }; + if (item === "temperature") data_to_send.unit = "°C"; + else if (item === "humidity") data_to_send.unit = "%"; + else if (item === "co2") data_to_send.unit = "ppm"; + else if (item === "soil_temperature") data_to_send.unit = "°C"; + else if (item === "soil_moisture") data_to_send.unit = "%"; + else if (item === "ligh_itensity") data_to_send.unit = "lux"; + else if (item === "barometric_pressure") data_to_send.unit = "pa"; + else if (item === "soil_ec") data_to_send.unit = "dS/m"; + messages.push(data_to_send); + } + } + } + delete object_item.messages; + for (const key in object_item) { + if (typeof object_item[key] === "object") { + result.push({ + variable: ( + object_item[key].MessageType || `${prefix}${key}` + ).toLowerCase(), + value: + // eslint-disable-next-line no-nested-ternary + typeof object_item[key].value === "string" + ? object_item[key].value.toLowerCase() + : object_item[key].value || + typeof object_item[key].Value === "string" + ? object_item[key].Value.toLowerCase() + : object_item[key].Value, + serie: object_item[key].serie || serie, + metadata: object_item[key].metadata, + unit: object_item[key].unit, + location: object_item[key].location, + }); + } else { + result.push({ + variable: `${prefix}${key}`.toLowerCase(), + value: + typeof object_item[key] === "string" + ? object_item[key].toLowerCase() + : object_item[key], + serie, + }); + } + } + return result.concat(messages); +} + +const data = payload.find( + (x) => + x.variable === "payload_raw" || + x.variable === "payload" || + x.variable === "data" +); + +if (data) { + const buffer = Buffer.from(data.value, "hex"); + const serie = new Date().getTime(); + payload = ToTagoFormat(Decoder(buffer), serie); +} diff --git a/decoders/connector/seeed/sensecap-light-intensity-sensor/assets/logo.png b/decoders/connector/seeed/sensecap-light-intensity-sensor/assets/logo.png new file mode 100644 index 00000000..80db7807 Binary files /dev/null and b/decoders/connector/seeed/sensecap-light-intensity-sensor/assets/logo.png differ diff --git a/decoders/connector/seeed/sensecap-light-intensity-sensor/connector.jsonc b/decoders/connector/seeed/sensecap-light-intensity-sensor/connector.jsonc new file mode 100644 index 00000000..ba10bc6b --- /dev/null +++ b/decoders/connector/seeed/sensecap-light-intensity-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Seeed SenseCap Light Intensity Sensor", + "images": { + "logo": "./assets/logo.png" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.js", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/connector/seeed/sensecap-light-intensity-sensor/description.md b/decoders/connector/seeed/sensecap-light-intensity-sensor/description.md new file mode 100644 index 00000000..fe62948d --- /dev/null +++ b/decoders/connector/seeed/sensecap-light-intensity-sensor/description.md @@ -0,0 +1 @@ +Light Intensity Sensor over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-light-intensity-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/seeed/sensecap-light-intensity-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..c073ee89 --- /dev/null +++ b/decoders/connector/seeed/sensecap-light-intensity-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "SenseCAP Wireless Light Intensity Sensor measures the intensity of light in lux from 0 - 188000 lux, which is designed for outdoor use.\n\n \nThis battery-powered device incorporates a built-in LoRa transmitter based on SX1276 for long-range transmission, a digital light sensor, and a custom battery. It is specifically designed and optimized for use cases powering end devices by batteries for years. To minimize the power consumption, the device wakes up, transmits the collected light data to the gateway, and then goes back to sleep.\n\n\n**Light Intensity Sensor Specifications**\n* Range: 0 to 188000 Lux\n* Sensitivity: 0.045 Lux/LSB\n* Resolution: 0.045 Lux\n", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/v1.0.0/payload.js", + "../../../../network/lorawan-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-kerlink/v1.0.0/payload.js", + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-senra/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-light-intensity-sensor/v1.0.0/payload.js b/decoders/connector/seeed/sensecap-light-intensity-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..1fa88729 --- /dev/null +++ b/decoders/connector/seeed/sensecap-light-intensity-sensor/v1.0.0/payload.js @@ -0,0 +1,448 @@ +/** + * SenseCAP & TTN Converter + * + * @since 1.0 + * @return Object + * @param Boolean valid Indicates whether the payload is a valid payload. + * @param String err The reason for the payload to be invalid. 0 means valid, minus means invalid. + * @param String payload Hexadecimal string, to show the payload. + * @param Array messages One or more messages are parsed according to payload. + * type // Enum: + * // - "report_telemetry" + * // - "upload_battery" + * // - "upload_interval" + * // - "upload_version" + * // - "upload_sensor_id" + * // - "report_remove_sensor" + * // - "unknown_message" + * + * + * + * + * @sample-1 + * var sample = Decoder(["00", "00", "00", "01", "01", "00", "01", "00", "07", "00", "64", "00", "3C", "00", "01", "20", "01", "00", "00", "00", "00", "28", "90"], null); + * { + * valid: true, + * err: 0, + * payload: '0000000101000100070064003C00012001000000002890', + * messages: [ + * { type: 'upload_version', + * hardwareVersion: '1.0', + * softwareVersion: '1.1' }, + * { type: 'upload_battery', battery: 100 }, + * { type: 'upload_interval', interval: 3600 }, + * { type: 'report_remove_sensor', channel: 1 } + * ] + * } + * @sample-2 + * var sample = Decoder(["01", "01", "10", "98", "53", "00", "00", "01", "02", "10", "A8", "7A", "00", "00", "AF", "51"], null); + * { + * valid: true, + * err: 0, + * payload: '01011098530000010210A87A0000AF51', + * messages: [ + * { type: 'report_telemetry', + * measurementId: 4097, + * measurementValue: 21.4 }, + * { type: 'report_telemetry', + * measurementId: 4098, + * measurementValue: 31.4 } + * ] + * } + * @sample-3 + * var sample = Decoder(["01", "01", "00", "01", "01", "00", "01", "01", "02", "00", "6A", "01", "00", "15", "01", "03", "00", "30", "F1", "F7", "2C", "01", "04", "00", "09", "0C", "13", "14", "01", "05", "00", "7F", "4D", "00", "00", "01", "06", "00", "00", "00", "00", "00", "4C", "BE"], null); + * { + * valid: true, + * err: 0, + * payload: '010100010100010102006A01001501030030F1F72C010400090C13140105007F4D0000010600000000004CBE', + * messages: [ + * { type: 'upload_sensor_id', sensorId: '2CF7F1301500016A', channel: 1 } + * ] + * } + */ + +// util +function toBinary(arr) { + const binaryData = []; + // eslint-disable-next-line no-plusplus + for (let forArr = 0; forArr < arr.length; forArr++) { + const item = arr[forArr]; + let data = parseInt(item, 16).toString(2); + const dataLength = data.length; + if (data.length !== 8) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 8 - dataLength; i++) { + data = `0${data}`; + } + } + binaryData.push(data); + } + return binaryData.toString().replace(/,/g, ""); +} + +function crc16Check(data) { + return true; +} + +// util +function bytes2HexString(arrBytes) { + let str = ""; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < arrBytes.length; i++) { + let tmp; + const 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; +} + +// util +function divideBy7Bytes(str) { + const frameArray = []; + for (let i = 0; i < str.length - 4; i += 14) { + const data = str.substring(i, i + 14); + frameArray.push(data); + } + return frameArray; +} + +// util +function littleEndianTransform(data) { + const dataArray = []; + for (let i = 0; i < data.length; i += 2) { + dataArray.push(data.substring(i, i + 2)); + } + dataArray.reverse(); + return dataArray; +} + +// util +function strTo10SysNub(str) { + const arr = littleEndianTransform(str); + return parseInt(arr.toString().replace(/,/g, ""), 16); +} + +// util +function checkDataIdIsMeasureUpload(dataId) { + return parseInt(dataId, 10) > 4096; +} + +// configurable. +function isSpecialDataId(dataID) { + switch (dataID) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 7: + case 0x120: + return true; + default: + return false; + } +} + +// configurable +function ttnDataSpecialFormat(dataId, str) { + const strReverse = littleEndianTransform(str); + if (dataId === 2 || dataId === 3) { + return strReverse.join(""); + } + + // handle unsigned number + const str2 = toBinary(strReverse); + + const dataArray = []; + switch (dataId) { + case 0: // DATA_BOARD_VERSION + case 1: // DATA_SENSOR_VERSION + // Using point segmentation + for (let k = 0; k < str2.length; k += 16) { + let tmp146 = str2.substring(k, k + 16); + tmp146 = `${parseInt(tmp146.substring(0, 8), 2) || 0}.${ + parseInt(tmp146.substring(8, 16), 2) || 0 + }`; + dataArray.push(tmp146); + } + return dataArray.join(","); + case 4: + for (let i = 0; i < str2.length; i += 8) { + let item = parseInt(str2.substring(i, i + 8), 2); + if (item < 10) { + item = `0${item.toString()}`; + } else { + item = item.toString(); + } + dataArray.push(item); + } + return dataArray.join(""); + case 7: + // battery && interval + return { + interval: parseInt(str2.substr(0, 16), 2), + power: parseInt(str2.substr(-16, 16), 2), + }; + default: + return []; + } +} + +// util +function ttnDataFormat(str) { + const strReverse = littleEndianTransform(str); + let str2 = toBinary(strReverse); + if (str2.substring(0, 1) === "1") { + const arr = str2.split(""); + const reverseArr = []; + // eslint-disable-next-line no-plusplus + for (let forArr = 0; forArr < arr.length; forArr++) { + const item = arr[forArr]; + if (parseInt(item, 2) === 1) { + reverseArr.push(0); + } else { + reverseArr.push(1); + } + } + str2 = parseInt(reverseArr.join(""), 2) + 1; + return `-${str2 / 1000}`; + } + return parseInt(str2, 2) / 1000; +} + +// util +function sensorAttrForVersion(dataValue) { + const dataValueSplitArray = dataValue.split(","); + return { + ver_hardware: dataValueSplitArray[0], + ver_software: dataValueSplitArray[1], + }; +} + +/** + * Entry, decoder.js + */ +function Decoder(bytes) { + // init + const bytesString = bytes2HexString(bytes).toLocaleUpperCase(); + const decoded = { + // valid + valid: true, + err: 0, + // bytes + payload: bytesString, + // messages array + messages: [], + }; + + // Cache sensor id + let sensorEuiLowBytes; + let sensorEuiHighBytes; + const frameArray = divideBy7Bytes(bytesString); + const id_soil = (bytes[0] << 8) | bytes[1]; + if (id_soil === 3088) { + decoded.messages.push({ ec_id: "100C" }); + + decoded.messages.push({ + ec_Value: + (bytes[2] | (bytes[3] << 8) | (bytes[4] << 16) | (bytes[5] << 24)) / + 1000, + }); + return decoded; + } + // CRC check + if (!crc16Check(bytesString)) { + decoded.valid = false; + decoded.err = -1; // "crc check fail." + return decoded; + } + + // Length Check + if ((bytesString.length / 2 - 2) % 7 !== 0) { + decoded.valid = false; + decoded.err = -2; // "length check fail." + return decoded; + } + + // Handle each frame + // eslint-disable-next-line no-plusplus + for (let forFrame = 0; forFrame < frameArray.length; forFrame++) { + const frame = frameArray[forFrame]; + // Extract key parameters + // const channel = strTo10SysNub(frame.substring(0, 2)); + const dataID = strTo10SysNub(frame.substring(2, 6)); + const dataValue = frame.substring(6, 14); + const realDataValue = isSpecialDataId(dataID) + ? ttnDataSpecialFormat(dataID, dataValue) + : ttnDataFormat(dataValue); + // eslint-disable-next-line no-console + // console.log(dataID, dataValue, realDataValue); + + if (checkDataIdIsMeasureUpload(dataID)) { + // if telemetry. + if (dataID === 4097) + decoded.messages.push({ temperature: realDataValue }); + if (dataID === 4108 || dataID === 4111) + decoded.messages.push({ soil_ec: realDataValue }); + else if (dataID === 4098) + decoded.messages.push({ humidity: realDataValue }); + else if (dataID === 4100) decoded.messages.push({ co2: realDataValue }); + else if (dataID === 4102 || dataID === 4112) + decoded.messages.push({ soil_temperature: realDataValue }); + else if (dataID === 4103 || dataID === 4110) + decoded.messages.push({ soil_moisture: realDataValue }); + else if (dataID === 4099) + decoded.messages.push({ ligh_itensity: realDataValue }); + else if (dataID === 4101) + decoded.messages.push({ barometric_pressure: realDataValue }); + decoded.messages.push({ + type: "report_telemetry", + // measurementId: dataID, + // measurementValue: realDataValue, + }); + } else if (isSpecialDataId(dataID) || dataID === 5 || dataID === 6) { + // if special order, except "report_sensor_id". + switch (dataID) { + case 0x00: + // node version + decoded.messages.push({ + type: "upload_version", + hardwareVersion: sensorAttrForVersion(realDataValue).ver_hardware, + softwareVersion: sensorAttrForVersion(realDataValue).ver_software, + }); + break; + case 1: + // sensor version + break; + case 2: + // sensor eui, low bytes + sensorEuiLowBytes = realDataValue; + break; + case 3: + // sensor eui, high bytes + sensorEuiHighBytes = realDataValue; + break; + case 7: + // battery power && interval + decoded.messages.push( + { type: "upload_battery", battery: realDataValue.power }, + { + type: "upload_interval", + interval: parseInt(realDataValue.interval, 10) * 60, + } + ); + break; + case 0x120: + // remove sensor + decoded.messages.push({ + type: "report_remove_sensor", + channel: 1, + }); + break; + default: + break; + } + } else { + decoded.messages.push({ + type: "unknown_message", + dataID, + dataValue, + }); + } + } + + // if the complete id received, as "upload_sensor_id" + if (sensorEuiHighBytes && sensorEuiLowBytes) { + decoded.messages.unshift({ + type: "upload_sensor_id", + channel: 1, + sensorId: (sensorEuiHighBytes + sensorEuiLowBytes).toUpperCase(), + }); + } + + // return + return decoded; +} + +function ToTagoFormat(object_item, serie, prefix = "") { + const result = []; + const messages = []; + // eslint-disable-next-line guard-for-in + for (let i = 0; i < object_item.messages.length; i += 1) { + if (typeof object_item.messages[i] === "object") { + // eslint-disable-next-line guard-for-in + for (const item in object_item.messages[i]) { + let data_to_send = { + variable: item.toLowerCase(), + value: + typeof object_item.messages[i][item] === "string" + ? object_item.messages[i][item].toLowerCase() + : object_item.messages[i][item], + serie, + }; + if (item === "temperature") data_to_send.unit = "°C"; + else if (item === "humidity") data_to_send.unit = "%"; + else if (item === "co2") data_to_send.unit = "ppm"; + else if (item === "soil_temperature") data_to_send.unit = "°C"; + else if (item === "soil_moisture") data_to_send.unit = "%"; + else if (item === "ligh_itensity") data_to_send.unit = "lux"; + else if (item === "barometric_pressure") data_to_send.unit = "pa"; + else if (item === "soil_ec") data_to_send.unit = "dS/m"; + messages.push(data_to_send); + } + } + } + delete object_item.messages; + for (const key in object_item) { + if (typeof object_item[key] === "object") { + result.push({ + variable: ( + object_item[key].MessageType || `${prefix}${key}` + ).toLowerCase(), + value: + // eslint-disable-next-line no-nested-ternary + typeof object_item[key].value === "string" + ? object_item[key].value.toLowerCase() + : object_item[key].value || + typeof object_item[key].Value === "string" + ? object_item[key].Value.toLowerCase() + : object_item[key].Value, + serie: object_item[key].serie || serie, + metadata: object_item[key].metadata, + unit: object_item[key].unit, + location: object_item[key].location, + }); + } else { + result.push({ + variable: `${prefix}${key}`.toLowerCase(), + value: + typeof object_item[key] === "string" + ? object_item[key].toLowerCase() + : object_item[key], + serie, + }); + } + } + return result.concat(messages); +} + +const data = payload.find( + (x) => + x.variable === "payload_raw" || + x.variable === "payload" || + x.variable === "data" +); + +if (data) { + const buffer = Buffer.from(data.value, "hex"); + const serie = new Date().getTime(); + payload = ToTagoFormat(Decoder(buffer), serie); +} diff --git a/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/assets/logo.png b/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/assets/logo.png new file mode 100644 index 00000000..4c365a67 Binary files /dev/null and b/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/assets/logo.png differ diff --git a/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/connector.jsonc b/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/connector.jsonc new file mode 100644 index 00000000..856034d9 --- /dev/null +++ b/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Seeed SenseCap S2100 LoRaWAN Data Logger", + "images": { + "logo": "./assets/logo.png" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.js", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/description.md b/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/description.md new file mode 100644 index 00000000..9b96f552 --- /dev/null +++ b/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/description.md @@ -0,0 +1 @@ +The SenseCAP S2100 the sensor, collects the Data, and uploads it to the LoRaWAN gateway \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/v1.0.0/payload-config.jsonc b/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..03e14952 --- /dev/null +++ b/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "The SenseCAP S2100 Data Logger connects the sensor, collects the Data, and uploads it to the LoRaWAN gateway. It can support various sensor types such as Analog/RS485/GPIO.\n\n**Features:**\n\n* Strong Compatibility with different Sensors: supports all MODBUS-RTU RS485/Analog(4~20mA/0~10V) / GPIO(level/pulse) sensors.\n\n* Long Range & replaceable Battery powered: With LoRaWAN® wireless transmission, S2100 supports 19Ah built-in battery and external 12V DC to supply devices, and ultra-wide-transmission range of 2km in urban scenes and 10km in line of sight scenes.\n\n* Designed to Use in Harsh Environments: -40℃ ~ 85℃ operating temperature and IP66 rated enclosure, suitable for outdoor use, high UV exposure, heavy rain, dusty conditions, etc.\n\n* Remote configuration and management: Seeed provides SenseCAP Mate APP and SenseCAP Portal, allows users to remotely manage data and configure.", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/v1.0.0/payload.js", + "../../../../network/lorawan-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-kerlink/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-senra/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/v1.0.0/payload.js b/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/v1.0.0/payload.js new file mode 100644 index 00000000..8a5e4615 --- /dev/null +++ b/decoders/connector/seeed/sensecap-s2100-lorawan-data-logger/v1.0.0/payload.js @@ -0,0 +1,885 @@ +/* eslint-disable no-throw-literal */ +/* eslint-disable vars-on-top */ +/* eslint-disable no-var */ +/* eslint-disable radix */ +/* eslint-disable no-case-declarations */ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-plusplus */ +/* eslint-disable no-use-before-define */ +/** + * Entry, decoder.js + */ +function decodeUplink(input, port) { + // init + var bytes = bytes2HexString(input).toLocaleUpperCase(); + + const result = { + err: 0, + payload: bytes, + valid: true, + messages: [], + }; + const splitArray = dataSplit(bytes); + // data decoder + const decoderArray = []; + for (let i = 0; i < splitArray.length; i++) { + const item = splitArray[i]; + const { dataId } = item; + const { dataValue } = item; + const messages = dataIdAndDataValueJudge(dataId, dataValue); + decoderArray.push(messages); + } + result.messages = decoderArray; + return { data: result }; +} + +/** + * data splits + * @param bytes + * @returns {*[]} + */ +function dataSplit(bytes) { + const frameArray = []; + + for (let i = 0; i < bytes.length; i++) { + const remainingValue = bytes; + const 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, + dataValue, + }; + break; + case "02": + dataValue = remainingValue.substring(2, 18); + bytes = remainingValue.substring(18); + dataObj = { + dataId: "02", + dataValue, + }; + break; + case "03": + case "06": + dataValue = remainingValue.substring(2, 4); + bytes = remainingValue.substring(4); + dataObj = { + dataId, + dataValue, + }; + break; + case "05": + case "34": + dataValue = bytes.substring(2, 10); + bytes = remainingValue.substring(10); + dataObj = { + dataId, + 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, + 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": + const temperature = dataValue.substring(0, 4); + const humidity = dataValue.substring(4, 6); + const illumination = dataValue.substring(6, 14); + const uv = dataValue.substring(14, 16); + const windSpeed = dataValue.substring(16, 20); + messages = [ + { + variable: "4097", + value: loraWANV2DataFormat(temperature, 10), + metadata: { + type: "Air Temperature", + }, + }, + { + variable: "4098", + value: loraWANV2DataFormat(humidity), + metadata: { + type: "Air Humidity", + }, + }, + { + variable: "4099", + value: loraWANV2DataFormat(illumination), + metadata: { + type: "Light Intensity", + }, + }, + { + variable: "4190", + value: loraWANV2DataFormat(uv, 10), + metadata: { + type: "UV Index", + }, + }, + { + variable: "4105", + value: loraWANV2DataFormat(windSpeed, 10), + metadata: { + type: "Wind Speed", + }, + }, + ]; + break; + case "02": + const windDirection = dataValue.substring(0, 4); + const rainfall = dataValue.substring(4, 12); + const airPressure = dataValue.substring(12, 16); + messages = [ + { + variable: "4104", + value: loraWANV2DataFormat(windDirection), + metadata: { + type: "Wind Direction Sensor", + }, + }, + { + variable: "4113", + value: loraWANV2DataFormat(rainfall, 1000), + metadata: { + type: "Rain Gauge", + }, + }, + { + variable: "4101", + value: loraWANV2DataFormat(airPressure, 0.1), + metadata: { + type: "Barometric Pressure", + }, + }, + ]; + break; + case "03": + const Electricity = dataValue; + messages = [ + { + variable: "battery", + value: loraWANV2DataFormat(Electricity), + unit: "%", + }, + ]; + break; + case "04": + const electricityWhether = dataValue.substring(0, 2); + const hwv = dataValue.substring(2, 6); + const bdv = dataValue.substring(6, 10); + const sensorAcquisitionInterval = dataValue.substring(10, 14); + const gpsAcquisitionInterval = dataValue.substring(14, 18); + messages = [ + { + variable: "battery", + value: loraWANV2DataFormat(electricityWhether), + unit: "%", + }, + { + variable: "hardware_version", + value: `${loraWANV2DataFormat( + hwv.substring(0, 2) + )}.${loraWANV2DataFormat(hwv.substring(2, 4))}`, + }, + { + variable: "firmware_version", + value: `${loraWANV2DataFormat( + bdv.substring(0, 2) + )}.${loraWANV2DataFormat(bdv.substring(2, 4))}`, + }, + { + variable: "measure_interval", + value: parseInt(loraWANV2DataFormat(sensorAcquisitionInterval)) * 60, + }, + { + variable: "gps_interval", + value: parseInt(loraWANV2DataFormat(gpsAcquisitionInterval)) * 60, + }, + ]; + break; + case "05": + const sensorAcquisitionIntervalFive = dataValue.substring(0, 4); + const gpsAcquisitionIntervalFive = dataValue.substring(4, 8); + messages = [ + { + variable: "measure_interval", + value: + parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalFive)) * 60, + }, + { + variable: "gps_interval", + value: parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalFive)) * 60, + }, + ]; + break; + case "06": + const 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 = [ + { + variable: "4101", + value: descZh, + metadata: { + type: "sensor_error_event", + }, + }, + ]; + break; + case "10": + const statusValue = dataValue.substring(0, 2); + const { status, type } = loraWANV2BitDataFormat(statusValue); + const sensecapId = dataValue.substring(2); + messages = [ + { + variable: "case10", + value: "case10", + metadata: { + status, + channelType: type, + sensorEui: sensecapId, + }, + }, + ]; + break; + case "20": + let initmeasurementId = 4175; + const sensor = []; + for (let i = 0; i < dataValue.length; i += 4) { + const modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)); + const detectionType = loraWANV2DataFormat( + dataValue.substring(i + 2, i + 4) + ); + const aiHeadValues = `${modelId}.${detectionType}`; + sensor.push({ + variable: String(initmeasurementId), + value: aiHeadValues, + }); + initmeasurementId++; + } + messages = sensor; + break; + case "21": + // Vision AI: + // AI 识别输出帧 + const tailValueArray = []; + let initTailmeasurementId = 4180; + for (let i = 0; i < dataValue.length; i += 4) { + const modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)); + const detectionType = loraWANV2DataFormat( + dataValue.substring(i + 2, i + 4) + ); + const aiTailValues = `${modelId}.${detectionType}`; + tailValueArray.push({ + variable: String(initTailmeasurementId), + value: aiTailValues, + metadata: { + type: `AI Detection ${i}`, + }, + }); + initTailmeasurementId++; + } + messages = tailValueArray; + break; + case "30": + case "31": + // 首帧或者首帧输出帧 + const channelInfoOne = loraWANV2ChannelBitFormat( + dataValue.substring(0, 2) + ); + const dataOne = { + variable: String(parseInt(channelInfoOne.one)), + value: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), + metadata: { + type: "Measurement", + }, + }; + const dataTwo = { + variable: String(parseInt(channelInfoOne.two)), + value: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), + metadata: { + type: "Measurement", + }, + }; + const cacheArrayInfo = []; + if (parseInt(channelInfoOne.one)) { + cacheArrayInfo.push(dataOne); + } + if (parseInt(channelInfoOne.two)) { + cacheArrayInfo.push(dataTwo); + } + cacheArrayInfo.forEach((item) => { + messages.push(item); + }); + break; + case "32": + const channelInfoTwo = loraWANV2ChannelBitFormat( + dataValue.substring(0, 2) + ); + const dataThree = { + variable: String(parseInt(channelInfoTwo.one)), + value: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), + metadata: { + type: "Measurement", + }, + }; + const dataFour = { + variable: String(parseInt(channelInfoTwo.two)), + value: loraWANV2DataFormat(dataValue.substring(10, 18), 1000), + metadata: { + type: "Measurement", + }, + }; + if (parseInt(channelInfoTwo.one)) { + messages.push(dataThree); + } + if (parseInt(channelInfoTwo.two)) { + messages.push(dataFour); + } + break; + case "33": + const channelInfoThree = loraWANV2ChannelBitFormat( + dataValue.substring(0, 2) + ); + const dataFive = { + variable: String(parseInt(channelInfoThree.one)), + value: loraWANV2DataFormat(dataValue.substring(4, 12), 1000), + metadata: { + type: "Measurement", + }, + }; + const dataSix = { + variable: String(parseInt(channelInfoThree.two)), + value: loraWANV2DataFormat(dataValue.substring(12, 20), 1000), + metadata: { + type: "Measurement", + }, + }; + if (parseInt(channelInfoThree.one)) { + messages.push(dataFive); + } + if (parseInt(channelInfoThree.two)) { + messages.push(dataSix); + } + + break; + case "34": + const model = loraWANV2DataFormat(dataValue.substring(0, 2)); + const GPIOInput = loraWANV2DataFormat(dataValue.substring(2, 4)); + const simulationModel = loraWANV2DataFormat(dataValue.substring(4, 6)); + const simulationInterface = loraWANV2DataFormat( + dataValue.substring(6, 8) + ); + messages = [ + { + variable: "datalogger", + value: "datalogger", + metadata: { + dataloggerProtocol: model, + dataloggerGPIOInput: GPIOInput, + dataloggerAnalogType: simulationModel, + dataloggerAnalogInterface: simulationInterface, + }, + }, + ]; + break; + case "35": + case "36": + const channelTDOne = loraWANV2ChannelBitFormat(dataValue.substring(0, 2)); + const channelSortTDOne = 3920 + (parseInt(channelTDOne.one) - 1) * 2; + const channelSortTDTWO = 3921 + (parseInt(channelTDOne.one) - 1) * 2; + messages = [ + { + variable: String([channelSortTDOne]), + value: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), + }, + { + variable: String([channelSortTDTWO]), + value: loraWANV2DataFormat(dataValue.substring(10, 18), 1000), + }, + ]; + break; + case "37": + const channelTDInfoTwo = loraWANV2ChannelBitFormat( + dataValue.substring(0, 2) + ); + const channelSortOne = 3920 + (parseInt(channelTDInfoTwo.one) - 1) * 2; + const channelSortTWO = 3921 + (parseInt(channelTDInfoTwo.one) - 1) * 2; + messages = [ + { + variable: String([channelSortOne]), + value: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), + }, + { + variable: String([channelSortTWO]), + value: loraWANV2DataFormat(dataValue.substring(10, 18), 1000), + }, + ]; + break; + case "38": + const channelTDInfoThree = loraWANV2ChannelBitFormat( + dataValue.substring(0, 2) + ); + const channelSortThreeOne = + 3920 + (parseInt(channelTDInfoThree.one) - 1) * 2; + const channelSortThreeTWO = + 3921 + (parseInt(channelTDInfoThree.one) - 1) * 2; + messages = [ + { + variable: String([channelSortThreeOne]), + value: loraWANV2DataFormat(dataValue.substring(2, 10), 1000), + }, + { + variable: String([channelSortThreeTWO]), + value: loraWANV2DataFormat(dataValue.substring(10, 18), 1000), + }, + ]; + break; + case "39": + const electricityWhetherTD = dataValue.substring(0, 2); + const hwvTD = dataValue.substring(2, 6); + const bdvTD = dataValue.substring(6, 10); + const sensorAcquisitionIntervalTD = dataValue.substring(10, 14); + const gpsAcquisitionIntervalTD = dataValue.substring(14, 18); + + messages = [ + { + variable: "battery", + value: loraWANV2DataFormat(electricityWhetherTD), + unit: "%", + }, + { + variable: "hardware_version", + value: `${loraWANV2DataFormat( + hwvTD.substring(0, 2) + )}.${loraWANV2DataFormat(hwvTD.substring(2, 4))}`, + }, + { + variable: "firmware_version", + value: `${loraWANV2DataFormat( + bdvTD.substring(0, 2) + )}.${loraWANV2DataFormat(bdvTD.substring(2, 4))}`, + }, + { + variable: "measure_interval", + value: + parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalTD)) * 60, + }, + { + variable: "thresholdMeasureInterval", + value: parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalTD)), + }, + ]; + break; + case "40": + case "41": + const lightIntensity = dataValue.substring(0, 4); + const loudness = dataValue.substring(4, 8); + // X + const accelerateX = dataValue.substring(8, 12); + // Y + const accelerateY = dataValue.substring(12, 16); + // Z + const accelerateZ = dataValue.substring(16, 20); + messages = [ + { + variable: "4193", + value: loraWANV2DataFormat(lightIntensity), + metadata: { + type: "Light Intensity", + }, + }, + { + variable: "4192", + value: loraWANV2DataFormat(loudness), + metadata: { + type: "Sound Intensity", + }, + }, + { + variable: "4150", + value: loraWANV2DataFormat(accelerateX, 100), + metadata: { + type: "AccelerometerX", + }, + }, + { + variable: "4151", + value: loraWANV2DataFormat(accelerateY, 100), + metadata: { + type: "AccelerometerY", + }, + }, + { + variable: "4152", + value: loraWANV2DataFormat(accelerateZ, 100), + metadata: { + type: "AccelerometerZ", + }, + }, + ]; + break; + case "42": + const airTemperature = dataValue.substring(0, 4); + const AirHumidity = dataValue.substring(4, 8); + const tVOC = dataValue.substring(8, 12); + const CO2eq = dataValue.substring(12, 16); + const soilMoisture = dataValue.substring(16, 20); + messages = [ + { + variable: "4097", + value: loraWANV2DataFormat(airTemperature, 100), + metadata: { + type: "Air Temperature", + }, + }, + { + variable: "4098", + value: loraWANV2DataFormat(AirHumidity, 100), + metadata: { + type: "Air Humidity", + }, + }, + { + variable: "4195", + value: loraWANV2DataFormat(tVOC), + metadata: { + type: "Total Volatile Organic Compounds", + }, + }, + { + variable: "4100", + value: loraWANV2DataFormat(CO2eq), + metadata: { + type: "CO2", + }, + }, + { + variable: "4196", + value: loraWANV2DataFormat(soilMoisture), + metadata: { + type: "Soil moisture intensity", + }, + }, + ]; + break; + case "43": + case "44": + const headerDevKitValueArray = []; + let initDevkitmeasurementId = 4175; + for (let i = 0; i < dataValue.length; i += 4) { + const modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)); + const detectionType = loraWANV2DataFormat( + dataValue.substring(i + 2, i + 4) + ); + const aiHeadValues = `${modelId}.${detectionType}`; + headerDevKitValueArray.push({ + variable: String(initDevkitmeasurementId), + value: aiHeadValues, + metadata: { + type: `AI Detection ${i}`, + }, + }); + initDevkitmeasurementId++; + } + messages = headerDevKitValueArray; + break; + case "45": + let initTailDevKitmeasurementId = 4180; + for (let i = 0; i < dataValue.length; i += 4) { + const modelId = loraWANV2DataFormat(dataValue.substring(i, i + 2)); + const detectionType = loraWANV2DataFormat( + dataValue.substring(i + 2, i + 4) + ); + const aiTailValues = `${modelId}.${detectionType}`; + messages.push({ + variable: String(initTailDevKitmeasurementId), + value: aiTailValues, + metadata: { + type: `AI Detection ${i}`, + }, + }); + initTailDevKitmeasurementId++; + } + break; + default: + break; + } + return messages; +} + +/** + * + * data formatting + * @param str + * @param divisor + * @returns {string|number} + */ +function loraWANV2DataFormat(str, divisor = 1) { + const strReverse = bigEndianTransform(str); + let str2 = toBinary(strReverse); + if (str2.substring(0, 1) === "1") { + const arr = str2.split(""); + const reverseArr = arr.map((item) => { + if (parseInt(item) === 1) { + return 0; + } + return 1; + }); + str2 = parseInt(reverseArr.join(""), 2) + 1; + return `-${str2 / divisor}`; + } + return parseInt(str2, 2) / divisor; +} + +/** + * Handling big-endian data formats + * @param data + * @returns {*[]} + */ +function bigEndianTransform(data) { + const 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) { + const binaryData = arr.map((item) => { + let data = parseInt(item, 16).toString(2); + const dataLength = data.length; + if (data.length !== 8) { + for (let i = 0; i < 8 - dataLength; i++) { + data = `0${data}`; + } + } + return data; + }); + const ret = binaryData.toString().replace(/,/g, ""); + return ret; +} + +/** + * sensor + * @param str + * @returns {{channel: number, type: number, status: number}} + */ +function loraWANV2BitDataFormat(str) { + const strReverse = bigEndianTransform(str); + const str2 = toBinary(strReverse); + const channel = parseInt(str2.substring(0, 4), 2); + const status = parseInt(str2.substring(4, 5), 2); + const type = parseInt(str2.substring(5), 2); + return { channel, status, type }; +} + +/** + * channel info + * @param str + * @returns {{channelTwo: number, channelOne: number}} + */ +function loraWANV2ChannelBitFormat(str) { + const strReverse = bigEndianTransform(str); + const str2 = toBinary(strReverse); + const one = parseInt(str2.substring(0, 4), 2); + const two = parseInt(str2.substring(4, 8), 2); + const resultInfo = { + one, + two, + }; + return resultInfo; +} + +/** + * data log status bit + * @param str + * @returns {{total: number, level: number, isTH: number}} + */ +function loraWANV2DataLogBitFormat(str) { + const strReverse = bigEndianTransform(str); + const str2 = toBinary(strReverse); + const isTH = parseInt(str2.substring(0, 1), 2); + const total = parseInt(str2.substring(1, 5), 2); + const left = parseInt(str2.substring(5), 2); + const resultInfo = { + isTH, + total, + 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; +} + +function toTagoFormat(result) { + if (!result.data.messages.length) { + return "Payload is not valid"; + } + + console.log("result", result.data.messages); + const arrayToTago = []; + result.data.messages.forEach((messageArray) => { + messageArray.forEach((item) => { + arrayToTago.push(item); + }); + }); + + console.log("arrayToTago", arrayToTago); + return arrayToTago; +} + +const payload_raw = payload.find((x) => x.variable === "payload"); + +if (payload_raw) { + try { + // Convert the data from Hex to Javascript Buffer. + const buffer = Buffer.from(payload_raw.value, "hex"); + const payload_aux = toTagoFormat(decodeUplink(buffer)); + // verify if payload_aux is of type array + payload = payload.concat(payload_aux.map((x) => ({ ...x }))); + } catch (e) { + // Print the error to the Live Inspector. + console.error(e); + // Return the variable parse_error for debugging. + payload = [{ variable: "parse_error", value: e.message }]; + } +} + +console.log("payload", payload); diff --git a/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/assets/logo.png b/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/assets/logo.png new file mode 100644 index 00000000..fa83d63b Binary files /dev/null and b/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/assets/logo.png differ diff --git a/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/connector.jsonc b/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/connector.jsonc new file mode 100644 index 00000000..3dcfe7ae --- /dev/null +++ b/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Seeed SenseCap S2120 8-in-1 Weather Sensor", + "images": { + "logo": "./assets/logo.png" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.js", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/description.md b/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/description.md new file mode 100644 index 00000000..7f7fbfcd --- /dev/null +++ b/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/description.md @@ -0,0 +1 @@ +SenseCAP LoRaWAN S2120 Weather Station provides you with hyperlocal weather \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..a05fcc24 --- /dev/null +++ b/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "SenseCAP LoRaWAN S2120 Weather Station collects and uploads air temperature, humidity, wind speed/direction, rainfall, light intensity, UV index, and barometric pressure data supported by worldwide LoRaWAN networks. The S2120 weather station is suitable for applications in gardens, agriculture, meteorology, urban environmental monitoring, and other scenarios. It also enables low maintenance cost for its ultra-low power consumption, reliable performance, built-in Bluetooth for OTA configuration and remote device management.", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/v1.0.0/payload.js", + "../../../../network/lorawan-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-kerlink/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-senra/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/v1.0.0/payload.js b/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..deb05a11 --- /dev/null +++ b/decoders/connector/seeed/sensecap-s2120-8-in-1-weather-sensor/v1.0.0/payload.js @@ -0,0 +1,496 @@ +/* eslint-disable no-throw-literal */ +/* eslint-disable vars-on-top */ +/* eslint-disable no-var */ +/* eslint-disable radix */ +/* eslint-disable no-case-declarations */ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-plusplus */ +/* eslint-disable no-use-before-define */ +/** + * Entry, decoder.js + */ +function decodeUplink(input, port) { + // init + var bytes = bytes2HexString(input).toLocaleUpperCase(); + + const result = { + err: 0, + payload: bytes, + valid: true, + messages: [], + }; + const splitArray = dataSplit(bytes); + // data decoder + const decoderArray = []; + for (let i = 0; i < splitArray.length; i++) { + const item = splitArray[i]; + const { dataId } = item; + const { dataValue } = item; + const messages = dataIdAndDataValueJudge(dataId, dataValue); + decoderArray.push(messages); + } + result.messages = decoderArray; + console.log(JSON.stringify(result.messages)); + return { data: result }; +} + +/** + * data splits + * @param bytes + * @returns {*[]} + */ +function dataSplit(bytes) { + const frameArray = []; + + for (let i = 0; i < bytes.length; i++) { + const remainingValue = bytes; + const 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, + dataValue, + }; + break; + case "02": + dataValue = remainingValue.substring(2, 18); + bytes = remainingValue.substring(18); + dataObj = { + dataId: "02", + dataValue, + }; + break; + case "03": + case "06": + dataValue = remainingValue.substring(2, 4); + bytes = remainingValue.substring(4); + dataObj = { + dataId, + dataValue, + }; + break; + case "05": + case "34": + dataValue = bytes.substring(2, 10); + bytes = remainingValue.substring(10); + dataObj = { + dataId, + 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, + 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": + const temperature = dataValue.substring(0, 4); + const humidity = dataValue.substring(4, 6); + const illumination = dataValue.substring(6, 14); + const uv = dataValue.substring(14, 16); + const 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": + const windDirection = dataValue.substring(0, 4); + const rainfall = dataValue.substring(4, 12); + const 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": + const Electricity = dataValue; + messages = [ + { + "Battery(%)": loraWANV2DataFormat(Electricity), + }, + ]; + break; + case "04": + const electricityWhether = dataValue.substring(0, 2); + const hwv = dataValue.substring(2, 6); + const bdv = dataValue.substring(6, 10); + const sensorAcquisitionInterval = dataValue.substring(10, 14); + const 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": + const sensorAcquisitionIntervalFive = dataValue.substring(0, 4); + const gpsAcquisitionIntervalFive = dataValue.substring(4, 8); + messages = [ + { + measureInterval: + parseInt(loraWANV2DataFormat(sensorAcquisitionIntervalFive)) * 60, + gpsInterval: + parseInt(loraWANV2DataFormat(gpsAcquisitionIntervalFive)) * 60, + }, + ]; + break; + case "06": + const 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": + const statusValue = dataValue.substring(0, 2); + const { status, type } = loraWANV2BitDataFormat(statusValue); + const sensecapId = dataValue.substring(2); + messages = [ + { + status, + channelType: type, + sensorEui: sensecapId, + }, + ]; + break; + default: + break; + } + return messages; +} + +/** + * + * data formatting + * @param str + * @param divisor + * @returns {string|number} + */ +function loraWANV2DataFormat(str, divisor = 1) { + const strReverse = bigEndianTransform(str); + let str2 = toBinary(strReverse); + if (str2.substring(0, 1) === "1") { + const arr = str2.split(""); + const reverseArr = arr.map((item) => { + if (parseInt(item) === 1) { + return 0; + } + return 1; + }); + str2 = parseInt(reverseArr.join(""), 2) + 1; + return `-${str2 / divisor}`; + } + return parseInt(str2, 2) / divisor; +} + +/** + * Handling big-endian data formats + * @param data + * @returns {*[]} + */ +function bigEndianTransform(data) { + const 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) { + const binaryData = arr.map((item) => { + let data = parseInt(item, 16).toString(2); + const dataLength = data.length; + if (data.length !== 8) { + for (let i = 0; i < 8 - dataLength; i++) { + data = `0${data}`; + } + } + return data; + }); + const ret = binaryData.toString().replace(/,/g, ""); + return ret; +} + +/** + * sensor + * @param str + * @returns {{channel: number, type: number, status: number}} + */ +function loraWANV2BitDataFormat(str) { + const strReverse = bigEndianTransform(str); + const str2 = toBinary(strReverse); + const channel = parseInt(str2.substring(0, 4), 2); + const status = parseInt(str2.substring(4, 5), 2); + const type = parseInt(str2.substring(5), 2); + return { channel, status, type }; +} + +/** + * channel info + * @param str + * @returns {{channelTwo: number, channelOne: number}} + */ +function loraWANV2ChannelBitFormat(str) { + const strReverse = bigEndianTransform(str); + const str2 = toBinary(strReverse); + const one = parseInt(str2.substring(0, 4), 2); + const two = parseInt(str2.substring(4, 8), 2); + const resultInfo = { + one, + two, + }; + return resultInfo; +} + +/** + * data log status bit + * @param str + * @returns {{total: number, level: number, isTH: number}} + */ +function loraWANV2DataLogBitFormat(str) { + const strReverse = bigEndianTransform(str); + const str2 = toBinary(strReverse); + const isTH = parseInt(str2.substring(0, 1), 2); + const total = parseInt(str2.substring(1, 5), 2); + const left = parseInt(str2.substring(5), 2); + const resultInfo = { + isTH, + total, + 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; +} + +function toTagoFormat(result) { + if (!result.data.messages.length) { + return "Payload is not valid"; + } + const group = String(new Date().getTime()); + const arrayToTago = []; + console.log(result.data); + + for (const messages of result.data.messages) { + for (const x of messages) { + console.log(x); + arrayToTago.push({ + variable: String(x.type).toLowerCase().replace(/\s/g, "_"), + value: x.measurementValue, + group, + }); + } + } + + return arrayToTago; +} + +const payload_raw = payload.find( + (x) => + x.variable === "payload_raw" || + x.variable === "payload" || + x.variable === "data" +); +if (payload_raw) { + try { + // Convert the data from Hex to Javascript Buffer. + const buffer = Buffer.from(payload_raw.value, "hex"); + const payload_aux = toTagoFormat(decodeUplink(buffer)); + payload = payload.concat(payload_aux.map((x) => ({ ...x }))); + } catch (e) { + // Print the error to the Live Inspector. + console.error(e); + // Return the variable parse_error for debugging. + payload = [{ variable: "parse_error", value: e.message }]; + } +} + +// console.log(JSON.stringify(payload)); diff --git a/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/assets/logo.png b/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/assets/logo.png new file mode 100644 index 00000000..5519e8f9 Binary files /dev/null and b/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/assets/logo.png differ diff --git a/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/connector.jsonc b/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/connector.jsonc new file mode 100644 index 00000000..d3a4dda3 --- /dev/null +++ b/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Seeed SenseCap Soil Moisture and Temperature Sensor", + "images": { + "logo": "./assets/logo.png" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.js", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/description.md b/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/description.md new file mode 100644 index 00000000..2db29189 --- /dev/null +++ b/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/description.md @@ -0,0 +1 @@ +Soil Moisture and Temperature sensor over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..f1d9a764 --- /dev/null +++ b/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "SenseCAP Wireless Soil Moisture & Temperature Sensor measures soil volumetric water content (VWC) and soil temperature at the range of 0 ~ 100% (m³/m³) and -30 ~ 70℃ respectively. With the high-quality soil moisture and temperature probe, this sensor features high precision and sensitivity regardless of soil variability, making it widely applicable in industrial IoT (IIoT) scenarios such as water-saving irrigation, outdoor fields, greenhouses, and more. \n\n \nThis device incorporates a built-in LoRa transmitter based on SX1276 for long-range transmission, a 2-in-1 sensor, and a custom battery. It is specifically designed and optimized for user cases powering end devices by batteries for years.To minimize the power consumption, the device wakes up, transmits the collected soil moisture and temperature data to the gateway, and then goes back to sleep.\n\n**Soil Temperature Sensor Specifications**\n* Range:\t-30 ℃ to +70 ℃\n* Accuracy:\t±0.5 ℃\n* Resolution:\t0.1 ℃\n\n**Soil Moisture Sensor Specifications**\n* Range:\tFrom completely dry to fully saturated (from 0% to 100% of saturation)\n* Accuracy:\t±2% ( 0 to 50 %) ; ±3% ( 50 to 100 %) \n* Resolution:\t0.03 %(0 to 50%); 1%(50 to 100%)", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/v1.0.0/payload.js", + "../../../../network/lorawan-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-kerlink/v1.0.0/payload.js", + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-senra/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/v1.0.0/payload.js b/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..1fa88729 --- /dev/null +++ b/decoders/connector/seeed/sensecap-soil-moisture-and-temperature-sensor/v1.0.0/payload.js @@ -0,0 +1,448 @@ +/** + * SenseCAP & TTN Converter + * + * @since 1.0 + * @return Object + * @param Boolean valid Indicates whether the payload is a valid payload. + * @param String err The reason for the payload to be invalid. 0 means valid, minus means invalid. + * @param String payload Hexadecimal string, to show the payload. + * @param Array messages One or more messages are parsed according to payload. + * type // Enum: + * // - "report_telemetry" + * // - "upload_battery" + * // - "upload_interval" + * // - "upload_version" + * // - "upload_sensor_id" + * // - "report_remove_sensor" + * // - "unknown_message" + * + * + * + * + * @sample-1 + * var sample = Decoder(["00", "00", "00", "01", "01", "00", "01", "00", "07", "00", "64", "00", "3C", "00", "01", "20", "01", "00", "00", "00", "00", "28", "90"], null); + * { + * valid: true, + * err: 0, + * payload: '0000000101000100070064003C00012001000000002890', + * messages: [ + * { type: 'upload_version', + * hardwareVersion: '1.0', + * softwareVersion: '1.1' }, + * { type: 'upload_battery', battery: 100 }, + * { type: 'upload_interval', interval: 3600 }, + * { type: 'report_remove_sensor', channel: 1 } + * ] + * } + * @sample-2 + * var sample = Decoder(["01", "01", "10", "98", "53", "00", "00", "01", "02", "10", "A8", "7A", "00", "00", "AF", "51"], null); + * { + * valid: true, + * err: 0, + * payload: '01011098530000010210A87A0000AF51', + * messages: [ + * { type: 'report_telemetry', + * measurementId: 4097, + * measurementValue: 21.4 }, + * { type: 'report_telemetry', + * measurementId: 4098, + * measurementValue: 31.4 } + * ] + * } + * @sample-3 + * var sample = Decoder(["01", "01", "00", "01", "01", "00", "01", "01", "02", "00", "6A", "01", "00", "15", "01", "03", "00", "30", "F1", "F7", "2C", "01", "04", "00", "09", "0C", "13", "14", "01", "05", "00", "7F", "4D", "00", "00", "01", "06", "00", "00", "00", "00", "00", "4C", "BE"], null); + * { + * valid: true, + * err: 0, + * payload: '010100010100010102006A01001501030030F1F72C010400090C13140105007F4D0000010600000000004CBE', + * messages: [ + * { type: 'upload_sensor_id', sensorId: '2CF7F1301500016A', channel: 1 } + * ] + * } + */ + +// util +function toBinary(arr) { + const binaryData = []; + // eslint-disable-next-line no-plusplus + for (let forArr = 0; forArr < arr.length; forArr++) { + const item = arr[forArr]; + let data = parseInt(item, 16).toString(2); + const dataLength = data.length; + if (data.length !== 8) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 8 - dataLength; i++) { + data = `0${data}`; + } + } + binaryData.push(data); + } + return binaryData.toString().replace(/,/g, ""); +} + +function crc16Check(data) { + return true; +} + +// util +function bytes2HexString(arrBytes) { + let str = ""; + // eslint-disable-next-line no-plusplus + for (let i = 0; i < arrBytes.length; i++) { + let tmp; + const 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; +} + +// util +function divideBy7Bytes(str) { + const frameArray = []; + for (let i = 0; i < str.length - 4; i += 14) { + const data = str.substring(i, i + 14); + frameArray.push(data); + } + return frameArray; +} + +// util +function littleEndianTransform(data) { + const dataArray = []; + for (let i = 0; i < data.length; i += 2) { + dataArray.push(data.substring(i, i + 2)); + } + dataArray.reverse(); + return dataArray; +} + +// util +function strTo10SysNub(str) { + const arr = littleEndianTransform(str); + return parseInt(arr.toString().replace(/,/g, ""), 16); +} + +// util +function checkDataIdIsMeasureUpload(dataId) { + return parseInt(dataId, 10) > 4096; +} + +// configurable. +function isSpecialDataId(dataID) { + switch (dataID) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 7: + case 0x120: + return true; + default: + return false; + } +} + +// configurable +function ttnDataSpecialFormat(dataId, str) { + const strReverse = littleEndianTransform(str); + if (dataId === 2 || dataId === 3) { + return strReverse.join(""); + } + + // handle unsigned number + const str2 = toBinary(strReverse); + + const dataArray = []; + switch (dataId) { + case 0: // DATA_BOARD_VERSION + case 1: // DATA_SENSOR_VERSION + // Using point segmentation + for (let k = 0; k < str2.length; k += 16) { + let tmp146 = str2.substring(k, k + 16); + tmp146 = `${parseInt(tmp146.substring(0, 8), 2) || 0}.${ + parseInt(tmp146.substring(8, 16), 2) || 0 + }`; + dataArray.push(tmp146); + } + return dataArray.join(","); + case 4: + for (let i = 0; i < str2.length; i += 8) { + let item = parseInt(str2.substring(i, i + 8), 2); + if (item < 10) { + item = `0${item.toString()}`; + } else { + item = item.toString(); + } + dataArray.push(item); + } + return dataArray.join(""); + case 7: + // battery && interval + return { + interval: parseInt(str2.substr(0, 16), 2), + power: parseInt(str2.substr(-16, 16), 2), + }; + default: + return []; + } +} + +// util +function ttnDataFormat(str) { + const strReverse = littleEndianTransform(str); + let str2 = toBinary(strReverse); + if (str2.substring(0, 1) === "1") { + const arr = str2.split(""); + const reverseArr = []; + // eslint-disable-next-line no-plusplus + for (let forArr = 0; forArr < arr.length; forArr++) { + const item = arr[forArr]; + if (parseInt(item, 2) === 1) { + reverseArr.push(0); + } else { + reverseArr.push(1); + } + } + str2 = parseInt(reverseArr.join(""), 2) + 1; + return `-${str2 / 1000}`; + } + return parseInt(str2, 2) / 1000; +} + +// util +function sensorAttrForVersion(dataValue) { + const dataValueSplitArray = dataValue.split(","); + return { + ver_hardware: dataValueSplitArray[0], + ver_software: dataValueSplitArray[1], + }; +} + +/** + * Entry, decoder.js + */ +function Decoder(bytes) { + // init + const bytesString = bytes2HexString(bytes).toLocaleUpperCase(); + const decoded = { + // valid + valid: true, + err: 0, + // bytes + payload: bytesString, + // messages array + messages: [], + }; + + // Cache sensor id + let sensorEuiLowBytes; + let sensorEuiHighBytes; + const frameArray = divideBy7Bytes(bytesString); + const id_soil = (bytes[0] << 8) | bytes[1]; + if (id_soil === 3088) { + decoded.messages.push({ ec_id: "100C" }); + + decoded.messages.push({ + ec_Value: + (bytes[2] | (bytes[3] << 8) | (bytes[4] << 16) | (bytes[5] << 24)) / + 1000, + }); + return decoded; + } + // CRC check + if (!crc16Check(bytesString)) { + decoded.valid = false; + decoded.err = -1; // "crc check fail." + return decoded; + } + + // Length Check + if ((bytesString.length / 2 - 2) % 7 !== 0) { + decoded.valid = false; + decoded.err = -2; // "length check fail." + return decoded; + } + + // Handle each frame + // eslint-disable-next-line no-plusplus + for (let forFrame = 0; forFrame < frameArray.length; forFrame++) { + const frame = frameArray[forFrame]; + // Extract key parameters + // const channel = strTo10SysNub(frame.substring(0, 2)); + const dataID = strTo10SysNub(frame.substring(2, 6)); + const dataValue = frame.substring(6, 14); + const realDataValue = isSpecialDataId(dataID) + ? ttnDataSpecialFormat(dataID, dataValue) + : ttnDataFormat(dataValue); + // eslint-disable-next-line no-console + // console.log(dataID, dataValue, realDataValue); + + if (checkDataIdIsMeasureUpload(dataID)) { + // if telemetry. + if (dataID === 4097) + decoded.messages.push({ temperature: realDataValue }); + if (dataID === 4108 || dataID === 4111) + decoded.messages.push({ soil_ec: realDataValue }); + else if (dataID === 4098) + decoded.messages.push({ humidity: realDataValue }); + else if (dataID === 4100) decoded.messages.push({ co2: realDataValue }); + else if (dataID === 4102 || dataID === 4112) + decoded.messages.push({ soil_temperature: realDataValue }); + else if (dataID === 4103 || dataID === 4110) + decoded.messages.push({ soil_moisture: realDataValue }); + else if (dataID === 4099) + decoded.messages.push({ ligh_itensity: realDataValue }); + else if (dataID === 4101) + decoded.messages.push({ barometric_pressure: realDataValue }); + decoded.messages.push({ + type: "report_telemetry", + // measurementId: dataID, + // measurementValue: realDataValue, + }); + } else if (isSpecialDataId(dataID) || dataID === 5 || dataID === 6) { + // if special order, except "report_sensor_id". + switch (dataID) { + case 0x00: + // node version + decoded.messages.push({ + type: "upload_version", + hardwareVersion: sensorAttrForVersion(realDataValue).ver_hardware, + softwareVersion: sensorAttrForVersion(realDataValue).ver_software, + }); + break; + case 1: + // sensor version + break; + case 2: + // sensor eui, low bytes + sensorEuiLowBytes = realDataValue; + break; + case 3: + // sensor eui, high bytes + sensorEuiHighBytes = realDataValue; + break; + case 7: + // battery power && interval + decoded.messages.push( + { type: "upload_battery", battery: realDataValue.power }, + { + type: "upload_interval", + interval: parseInt(realDataValue.interval, 10) * 60, + } + ); + break; + case 0x120: + // remove sensor + decoded.messages.push({ + type: "report_remove_sensor", + channel: 1, + }); + break; + default: + break; + } + } else { + decoded.messages.push({ + type: "unknown_message", + dataID, + dataValue, + }); + } + } + + // if the complete id received, as "upload_sensor_id" + if (sensorEuiHighBytes && sensorEuiLowBytes) { + decoded.messages.unshift({ + type: "upload_sensor_id", + channel: 1, + sensorId: (sensorEuiHighBytes + sensorEuiLowBytes).toUpperCase(), + }); + } + + // return + return decoded; +} + +function ToTagoFormat(object_item, serie, prefix = "") { + const result = []; + const messages = []; + // eslint-disable-next-line guard-for-in + for (let i = 0; i < object_item.messages.length; i += 1) { + if (typeof object_item.messages[i] === "object") { + // eslint-disable-next-line guard-for-in + for (const item in object_item.messages[i]) { + let data_to_send = { + variable: item.toLowerCase(), + value: + typeof object_item.messages[i][item] === "string" + ? object_item.messages[i][item].toLowerCase() + : object_item.messages[i][item], + serie, + }; + if (item === "temperature") data_to_send.unit = "°C"; + else if (item === "humidity") data_to_send.unit = "%"; + else if (item === "co2") data_to_send.unit = "ppm"; + else if (item === "soil_temperature") data_to_send.unit = "°C"; + else if (item === "soil_moisture") data_to_send.unit = "%"; + else if (item === "ligh_itensity") data_to_send.unit = "lux"; + else if (item === "barometric_pressure") data_to_send.unit = "pa"; + else if (item === "soil_ec") data_to_send.unit = "dS/m"; + messages.push(data_to_send); + } + } + } + delete object_item.messages; + for (const key in object_item) { + if (typeof object_item[key] === "object") { + result.push({ + variable: ( + object_item[key].MessageType || `${prefix}${key}` + ).toLowerCase(), + value: + // eslint-disable-next-line no-nested-ternary + typeof object_item[key].value === "string" + ? object_item[key].value.toLowerCase() + : object_item[key].value || + typeof object_item[key].Value === "string" + ? object_item[key].Value.toLowerCase() + : object_item[key].Value, + serie: object_item[key].serie || serie, + metadata: object_item[key].metadata, + unit: object_item[key].unit, + location: object_item[key].location, + }); + } else { + result.push({ + variable: `${prefix}${key}`.toLowerCase(), + value: + typeof object_item[key] === "string" + ? object_item[key].toLowerCase() + : object_item[key], + serie, + }); + } + } + return result.concat(messages); +} + +const data = payload.find( + (x) => + x.variable === "payload_raw" || + x.variable === "payload" || + x.variable === "data" +); + +if (data) { + const buffer = Buffer.from(data.value, "hex"); + const serie = new Date().getTime(); + payload = ToTagoFormat(Decoder(buffer), serie); +} diff --git a/decoders/connector/seeed/sensecap-t1000-ab/assets/logo.png b/decoders/connector/seeed/sensecap-t1000-ab/assets/logo.png new file mode 100644 index 00000000..29a689bc Binary files /dev/null and b/decoders/connector/seeed/sensecap-t1000-ab/assets/logo.png differ diff --git a/decoders/connector/seeed/sensecap-t1000-ab/connector.jsonc b/decoders/connector/seeed/sensecap-t1000-ab/connector.jsonc new file mode 100644 index 00000000..917b4300 --- /dev/null +++ b/decoders/connector/seeed/sensecap-t1000-ab/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Seeed SenseCap T1000-A/B", + "images": { + "logo": "./assets/logo.png" + }, + "versions": { + "v1.0.0": { + "src": "./v1.0.0/payload.js", + "manifest": "./v1.0.0/payload-config.jsonc" + } + } +} diff --git a/decoders/connector/seeed/sensecap-t1000-ab/description.md b/decoders/connector/seeed/sensecap-t1000-ab/description.md new file mode 100644 index 00000000..0d938013 --- /dev/null +++ b/decoders/connector/seeed/sensecap-t1000-ab/description.md @@ -0,0 +1 @@ +LoRaWAN® card size tracker for INDOOR & OUTDOOR positioning. \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-t1000-ab/v1.0.0/payload-config.jsonc b/decoders/connector/seeed/sensecap-t1000-ab/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..8aa06299 --- /dev/null +++ b/decoders/connector/seeed/sensecap-t1000-ab/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "SenseCAP T1000 is a compact LoRaWAN® tracker that utilizes GNSS/Wi-Fi/Bluetooth for precise indoor & outdoor location tracking. It boasts self-geo-adaptive capabilities, local data storage, and an impressive months of battery life. Additionally, it is equipped with temperature, light, and motion sensors, making it ideal for a variety of location-based applications.\n\n* Outdoor & Indoor Tracking\n\n* Cross-Regional Adaptability\n\n* Offline Data Storage\n\n* Temp, Light, Motion Sensors\n\n* Proof Of Location\n\n* Months Of Battery Life", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/v1.0.0/payload.js", + "../../../../network/lorawan-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-kerlink/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-senra/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/seeed/sensecap-t1000-ab/v1.0.0/payload.js b/decoders/connector/seeed/sensecap-t1000-ab/v1.0.0/payload.js new file mode 100644 index 00000000..77a77f13 --- /dev/null +++ b/decoders/connector/seeed/sensecap-t1000-ab/v1.0.0/payload.js @@ -0,0 +1,788 @@ +/* + * TagoIO Decoders - (https://tago.io/) + * ------------------- + * Generated by :: rafaeltelessepulveda + * Generated at :: Thu Jul 06 2023 19:30:58 GMT+0000 (Horário Universal Coordenado) + * Machine :: MacBook-Air-de-Rafael.local - Node.js v18.15.0 + * ------------------- +*/ + +/* eslint-disable no-case-declarations */ /* eslint-disable no-var */ /* eslint-disable unicorn/prefer-string-slice */ /* eslint-disable @typescript-eslint/restrict-plus-operands */ /* eslint-disable @typescript-eslint/no-unused-vars */ // @ts-nocheck +// let payload = [ +// { variable: "payload", value: "0d00000000" }, +// { variable: "port", value: 5 }, +// ]; +// const device = { +// id: "", +// params: [{ key: "beacon_decoder", value: "simple" }], +// }; +function decodeUplink(input) { + var bytes = input; + var bytesString = bytes2HexString(bytes).toLocaleUpperCase(); + var decoded = { + valid: true, + err: 0, + payload: bytesString, + messages: [] + }; + const measurement = messageAnalyzed(bytesString); + decoded.messages = measurement; + return { + data: decoded + }; +} +function messageAnalyzed(messageValue) { + try { + const frames = unpack(messageValue); + const measurementResultArray = []; + for(let i = 0; i < frames.length; i++){ + const item = frames[i]; + const dataId = item.dataId; + const dataValue = item.dataValue; + const measurementArray = deserialize(dataId, dataValue); + measurementResultArray.push(measurementArray); + } + return measurementResultArray; + } catch (error) { + return error.toString(); + } +} +function unpack(messageValue) { + const frameArray = []; + for(let i = 0; i < messageValue.length; i++){ + const remainMessage = messageValue; + const dataId = remainMessage.substring(0, 2).toUpperCase(); + let dataValue; + let dataObj = {}; + let packageLen; + switch(dataId){ + case "01": + packageLen = 94; + if (remainMessage.length < packageLen) { + return frameArray; + } + dataValue = remainMessage.substring(2, packageLen); + messageValue = remainMessage.substring(packageLen); + dataObj = { + dataId: dataId, + dataValue: dataValue + }; + break; + case "02": + packageLen = 32; + if (remainMessage.length < packageLen) { + return frameArray; + } + dataValue = remainMessage.substring(2, packageLen); + messageValue = remainMessage.substring(packageLen); + dataObj = { + dataId: dataId, + dataValue: dataValue + }; + break; + case "03": + packageLen = 64; + if (remainMessage.length < packageLen) { + return frameArray; + } + break; + case "04": + packageLen = 20; + if (remainMessage.length < packageLen) { + return frameArray; + } + dataValue = remainMessage.substring(2, packageLen); + messageValue = remainMessage.substring(packageLen); + dataObj = { + dataId: dataId, + dataValue: dataValue + }; + break; + case "05": + packageLen = 10; + if (remainMessage.length < packageLen) { + return frameArray; + } + dataValue = remainMessage.substring(2, packageLen); + messageValue = remainMessage.substring(packageLen); + dataObj = { + dataId: dataId, + dataValue: dataValue + }; + break; + case "06": + packageLen = 44; + if (remainMessage.length < packageLen) { + return frameArray; + } + dataValue = remainMessage.substring(2, packageLen); + messageValue = remainMessage.substring(packageLen); + dataObj = { + dataId: dataId, + dataValue: dataValue + }; + break; + case "07": + packageLen = 84; + if (remainMessage.length < packageLen) { + return frameArray; + } + dataValue = remainMessage.substring(2, packageLen); + messageValue = remainMessage.substring(packageLen); + dataObj = { + dataId: dataId, + dataValue: dataValue + }; + break; + case "08": + packageLen = 70; + if (remainMessage.length < packageLen) { + return frameArray; + } + dataValue = remainMessage.substring(2, packageLen); + messageValue = remainMessage.substring(packageLen); + dataObj = { + dataId: dataId, + dataValue: dataValue + }; + break; + case "09": + packageLen = 36; + if (remainMessage.length < packageLen) { + return frameArray; + } + dataValue = remainMessage.substring(2, packageLen); + messageValue = remainMessage.substring(packageLen); + dataObj = { + dataId: dataId, + dataValue: dataValue + }; + break; + case "0A": + packageLen = 76; + if (remainMessage.length < packageLen) { + return frameArray; + } + dataValue = remainMessage.substring(2, packageLen); + messageValue = remainMessage.substring(packageLen); + dataObj = { + dataId: dataId, + dataValue: dataValue + }; + break; + case "0B": + packageLen = 62; + if (remainMessage.length < packageLen) { + return frameArray; + } + dataValue = remainMessage.substring(2, packageLen); + messageValue = remainMessage.substring(packageLen); + dataObj = { + dataId: dataId, + dataValue: dataValue + }; + break; + case "0C": + packageLen = 2; + if (remainMessage.length < packageLen) { + return frameArray; + } + break; + case "0D": + packageLen = 10; + 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 eventList = []; + let collectTime = 0; + switch(dataId){ + case "01": + measurementArray = getUpShortInfo(dataValue); + break; + case "02": + measurementArray = getUpShortInfo(dataValue); + break; + case "03": + break; + case "04": + measurementArray = [ + { + measurementId: "3940", + type: "Work Mode", + measurementValue: getWorkingMode(dataValue.substring(0, 2)) + }, + { + measurementId: "3942", + type: "Heartbeat Interval", + measurementValue: getOneWeekInterval(dataValue.substring(4, 8)) + }, + { + measurementId: "3943", + type: "Periodic Interval", + measurementValue: getOneWeekInterval(dataValue.substring(8, 12)) + }, + { + measurementId: "3944", + type: "Event Interval", + measurementValue: getOneWeekInterval(dataValue.substring(12, 16)) + }, + { + measurementId: "3941", + type: "SOS Mode", + measurementValue: getSOSMode(dataValue.substring(16, 18)) + } + ]; + 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: "3941", + type: "SOS Mode", + measurementValue: getSOSMode(dataValue.substring(6, 8)) + } + ]; + break; + case "06": + eventList = getEventStatus(dataValue.substring(0, 6)); + collectTime = getUTCTimestamp(dataValue.substring(8, 16)); + measurementArray = [ + { + measurementId: "4200", + type: "SOS Event", + measurementValue: eventList[6] + }, + { + measurementId: "4197", + type: "Longitude", + measurementValue: getSensorValue(dataValue.substring(16, 24), 1_000_000) + }, + { + measurementId: "4198", + type: "Latitude", + measurementValue: getSensorValue(dataValue.substring(24, 32), 1_000_000) + }, + { + measurementId: "4097", + type: "Air Temperature", + measurementValue: getSensorValue(dataValue.substring(32, 36), 10) + }, + { + measurementId: "4199", + type: "Light", + measurementValue: getSensorValue(dataValue.substring(36, 40)) + }, + { + measurementId: "3000", + type: "Battery", + measurementValue: getBattery(dataValue.substring(40, 42)) + }, + { + type: "Timestamp", + measurementValue: collectTime + } + ]; + break; + case "07": + eventList = getEventStatus(dataValue.substring(0, 6)); + collectTime = getUTCTimestamp(dataValue.substring(8, 16)); + measurementArray = [ + { + measurementId: "4200", + type: "SOS Event", + measurementValue: eventList[6] + }, + { + measurementId: "5001", + type: "WiFi Scan", + measurementValue: getMacAndRssiObj(dataValue.substring(16, 72)) + }, + { + measurementId: "4097", + type: "Air Temperature", + measurementValue: getSensorValue(dataValue.substring(72, 76), 10) + }, + { + measurementId: "4199", + type: "Light", + measurementValue: getSensorValue(dataValue.substring(76, 80)) + }, + { + measurementId: "3000", + type: "Battery", + measurementValue: getBattery(dataValue.substring(80, 82)) + }, + { + type: "Timestamp", + measurementValue: collectTime + } + ]; + break; + case "08": + eventList = getEventStatus(dataValue.substring(0, 6)); + collectTime = getUTCTimestamp(dataValue.substring(8, 16)); + measurementArray = [ + { + measurementId: "4200", + type: "SOS Event", + measurementValue: eventList[6] + }, + { + measurementId: "5002", + type: "BLE Scan", + measurementValue: getMacAndRssiObj(dataValue.substring(16, 58)) + }, + { + measurementId: "4097", + type: "Air Temperature", + measurementValue: getSensorValue(dataValue.substring(58, 62), 10) + }, + { + measurementId: "4199", + type: "Light", + measurementValue: getSensorValue(dataValue.substring(62, 66)) + }, + { + measurementId: "3000", + type: "Battery", + measurementValue: getBattery(dataValue.substring(66, 68)) + }, + { + type: "Timestamp", + measurementValue: collectTime + } + ]; + break; + case "09": + eventList = getEventStatus(dataValue.substring(0, 6)); + collectTime = getUTCTimestamp(dataValue.substring(8, 16)); + measurementArray = [ + { + measurementId: "4200", + type: "SOS Event", + measurementValue: eventList[6] + }, + { + measurementId: "4197", + type: "Longitude", + measurementValue: getSensorValue(dataValue.substring(16, 24), 1_000_000) + }, + { + measurementId: "4198", + type: "Latitude", + measurementValue: getSensorValue(dataValue.substring(24, 32), 1_000_000) + }, + { + measurementId: "3000", + type: "Battery", + measurementValue: getBattery(dataValue.substring(32, 34)) + }, + { + type: "Timestamp", + measurementValue: collectTime + } + ]; + break; + case "0A": + eventList = getEventStatus(dataValue.substring(0, 6)); + collectTime = getUTCTimestamp(dataValue.substring(8, 16)); + measurementArray = [ + { + measurementId: "4200", + type: "SOS Event", + measurementValue: eventList[6] + }, + { + measurementId: "5001", + type: "WiFi Scan", + measurementValue: getMacAndRssiObj(dataValue.substring(16, 72)) + }, + { + measurementId: "3000", + type: "Battery", + measurementValue: getBattery(dataValue.substring(72, 74)) + }, + { + type: "Timestamp", + measurementValue: collectTime + } + ]; + break; + case "0B": + eventList = getEventStatus(dataValue.substring(0, 6)); + collectTime = getUTCTimestamp(dataValue.substring(8, 16)); + measurementArray = [ + { + measurementId: "4200", + type: "SOS Event", + measurementValue: eventList[6] + }, + { + measurementId: "5002", + type: "BLE Scan", + measurementValue: getMacAndRssiObj(dataValue.substring(16, 58)) + }, + { + measurementId: "3000", + type: "Battery", + measurementValue: getBattery(dataValue.substring(58, 60)) + }, + { + type: "Timestamp", + measurementValue: collectTime + } + ]; + break; + case "0D": + const errorCode = getInt(dataValue); + let error = ""; + switch(errorCode){ + case 0: + error = "THE GNSS SCAN TIME OUT"; + break; + case 1: + error = "THE WIFI SCAN TIME OUT"; + break; + case 2: + error = "THE WIFI GNSS SCAN TIME OUT"; + break; + case 3: + error = "THE GNSS WIFI SCAN TIME OUT"; + break; + case 4: + error = "THE BEACON SCAN TIME OUT"; + break; + case 5: + error = "THE BEACON WIFI SCAN TIME OUT"; + break; + case 6: + error = "THE BEACON GNSS SCAN TIME OUT"; + break; + case 7: + error = "THE BEACON WIFI GNSS SCAN TIME OUT"; + break; + case 8: + error = "FAILED TO OBTAIN THE UTC TIMESTAMP"; + break; + } + measurementArray.push({ + errorCode, + error + }); + } + return measurementArray; +} +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: "3942", + type: "Heartbeat Interval", + measurementValue: getOneWeekInterval(messageValue.substring(14, 18)) + }, + { + measurementId: "3943", + type: "Periodic Interval", + measurementValue: getOneWeekInterval(messageValue.substring(18, 22)) + }, + { + measurementId: "3944", + type: "Event Interval", + measurementValue: getOneWeekInterval(messageValue.substring(22, 26)) + }, + { + measurementId: "3941", + type: "SOS Mode", + measurementValue: getSOSMode(messageValue.substring(28, 30)) + } + ]; +} +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 getOneWeekInterval(str) { + return loraWANV2DataFormat(str) * 60; +} +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) { + const strReverse = bigEndianTransform(str); + let str2 = toBinary(strReverse); + if (str2.substring(0, 1) === "1") { + const arr = str2.split(""); + const reverseArr = arr.map((item)=>{ + if (parseInt(item) === 1) { + return 0; + } else { + return 1; + } + }); + str2 = parseInt(reverseArr.join(""), 2) + 1; + return "-" + str2 / divisor; + } + return parseInt(str2, 2) / divisor; +} +function bigEndianTransform(data) { + const dataArray = []; + for(let i = 0; i < data.length; i += 2){ + dataArray.push(data.substring(i, i + 2)); + } + return dataArray; +} +function toBinary(arr) { + const binaryData = arr.map((item)=>{ + let data = parseInt(item, 16).toString(2); + const dataLength = data.length; + if (data.length !== 8) { + for(let i = 0; i < 8 - dataLength; i++){ + data = `0` + data; + } + } + return data; + }); + return binaryData.toString().replaceAll(",", ""); +} +function getSOSMode(str) { + return loraWANV2DataFormat(str); +} +function getMacAndRssiObj(pair) { + const pairs = []; + if (pair.length % 14 === 0) { + for(let i = 0; i < pair.length; i += 14){ + const mac = getMacAddress(pair.substring(i, i + 12)); + if (mac) { + const 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; + } + const 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 i1 = 0; i1 < macArr.length; i1++){ + mac = mac + macArr[i1]; + } + return mac; +} +function getInt8RSSI(str) { + return loraWANV2DataFormat(str); +} +function getInt(str) { + return parseInt(str); +} +/** + * 1.MOVING_STARTING + * 2.MOVING_END + * 3.DEVICE_STATIC + * 4.SHOCK_EVENT + * 5.TEMP_EVENT + * 6.LIGHTING_EVENT + * 7.SOS_EVENT + * 8.CUSTOMER_EVENT + * */ function getEventStatus(str) { + const bitStr = getByteArray(str); + const event = []; + for(let i = bitStr.length; i >= 0; i--){ + if (i === 0) { + event[i] = bitStr.substring(0); + } else { + event[i] = bitStr.substring(i - 1, i); + } + } + return event.reverse(); +} +function getByteArray(str) { + const 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 getUTCTimestamp(str) { + return parseInt(loraWANV2PositiveDataFormat(str)) * 1000; +} +function loraWANV2PositiveDataFormat(str, divisor = 1) { + const strReverse = bigEndianTransform(str); + const str2 = toBinary(strReverse); + return parseInt(str2, 2) / divisor; +} +function toTagoFormat(data, beacon_decoder, group) { + const result = []; + for (const item of data){ + let variableName = item.type?.toLowerCase()?.replaceAll(" ", "_"); + if (item.error) { + variableName = "error_code"; + } + let variableValue = item.measurementValue || item.error; + if (variableName === "wifi_scan" || variableName === "ble_scan") { + if (beacon_decoder === "simple") { + let metadata = {}; + variableValue = ""; + for (const element of item.measurementValue){ + const macValue = element.mac; + const rssi = element.rssi; + variableValue = variableValue.concat(macValue, "; "); + metadata = { + ...metadata, + [macValue]: Number(rssi) + }; + } + const obj = { + variable: variableName, + value: variableValue, + metadata: metadata, + group + }; + result.push(obj); + } else if (beacon_decoder === "splitted") { + for (const element1 of item.measurementValue){ + const macValue1 = element1.mac; + const rssi1 = element1.rssi; + const obj1 = { + variable: macValue1, + value: rssi1 + }; + result.push(obj1); + } + } else { + throw "beacon_decoder not found!"; + } + } else if (variableName === "latitude") { + const latitude = data.find((x)=>x.type === "Latitude").measurementValue; + const longitude = data.find((x)=>x.type === "Longitude").measurementValue; + const obj2 = { + variable: "location", + group, + value: `${latitude},${longitude}`, + location: { + lat: Number(latitude), + lng: Number(longitude) + } + }; + result.push(obj2); + } else if (!(variableName === "latitude" || variableName === "longitude")) { + const obj3 = { + variable: variableName, + value: variableValue, + group + }; + result.push(obj3); + } + } + return result; +} +const payload_t1000 = payload.find((x)=>x.variable === "payload_raw" || x.variable === "payload" || x.variable === "data"); +const beacon_decoder = device.params.find((x)=>x.key === "beacon_decoder"); +if (payload_t1000) { + try { + const group = String(payload_t1000.group || payload_t1000.serie || Date.now()); + const buffer = Buffer.from(payload_t1000.value, "hex"); + const data = decodeUplink(buffer); + payload = payload.concat(toTagoFormat(data.data.messages[0], beacon_decoder.value, group)); + } catch (error) { + console.error(error); + payload = [ + { + variable: "parse_error", + value: error.message + } + ]; + } +} +console.log(JSON.stringify(payload)); + + +//#sourceMappingURL=data:application/json;charset=utf-8;base64,IntcInZlcnNpb25cIjozLFwic291cmNlc1wiOltdLFwibmFtZXNcIjpbXSxcIm1hcHBpbmdzXCI6XCJcIixcImZpbGVcIjpcInN0ZG91dFwifSI= \ No newline at end of file