Sensor
/** * 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); }
This codec is sourced from TagoIO Github. All rights belong to TagoIO Github.
This codec is licensed under the GNU General Public License v3 (GPL v3). Modifications, if any, are clearly marked. You are free to use, modify, and distribute the codec under the terms of GPL v3.