diff --git a/biome.jsonc b/biome.jsonc index abb48b54..7f79cf3a 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -11,7 +11,7 @@ }, "organizeImports": { "enabled": true }, "linter": { - "ignore": ["node_modules"], + "ignore": ["node_modules", "./decoders/**/v1.0.0/*.ts", "./decoders/**/v1.0.0/payload.js"], "enabled": true, "rules": { "recommended": true, diff --git a/decoders/connector/robeau/robeau/assets/logo.png b/decoders/connector/robeau/robeau/assets/logo.png new file mode 100644 index 00000000..80c8afe5 Binary files /dev/null and b/decoders/connector/robeau/robeau/assets/logo.png differ diff --git a/decoders/connector/robeau/robeau/connector.jsonc b/decoders/connector/robeau/robeau/connector.jsonc new file mode 100644 index 00000000..1375c25e --- /dev/null +++ b/decoders/connector/robeau/robeau/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Robeau", + "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/robeau/robeau/description.md b/decoders/connector/robeau/robeau/description.md new file mode 100644 index 00000000..c27fdd10 --- /dev/null +++ b/decoders/connector/robeau/robeau/description.md @@ -0,0 +1 @@ +LoRaWAN device to measure water consumption and detect leaks \ No newline at end of file diff --git a/decoders/connector/robeau/robeau/v1.0.0/payload-config.jsonc b/decoders/connector/robeau/robeau/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..7b2141cd --- /dev/null +++ b/decoders/connector/robeau/robeau/v1.0.0/payload-config.jsonc @@ -0,0 +1,98 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "In-building water management solution composed with\n- one sensor using turbine to calculate flow and volume of water\n- one datalogger to transmit data with Lora protocole\nWith this IoT device, you can :\n- detect leaks in real time and receive alerts (emailor text message) in case of over-consumption or leak\n- monitor your water network and analyse your water consumption everywhere in your building and do predictive maintenance\n- save water and then save money on your water bill\n- conserve water and have an environmental impact", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [ + { + "type": "dropdown", + "label": "Sensor 1 type", + "name": "sensor_1", + "group": "advanced", + "options": [ + { + "is_default": true, + "label": "DN20", + "value": "1000" + }, + { + "is_default": false, + "label": "DN25", + "value": "336.89" + } + ] + }, + { + "name": "sensor_2", + "label": "Sensor 2 type", + "type": "dropdown", + "group": "advanced", + "options": [ + { + "is_default": true, + "label": "DN20", + "value": "1000" + }, + { + "is_default": false, + "label": "DN25", + "value": "336.89" + } + ] + }, + { + "name": "sensor_3", + "label": "Sensor 3 type", + "type": "dropdown", + "group": "advanced", + "options": [ + { + "is_default": true, + "label": "DN20", + "value": "1000" + }, + { + "is_default": false, + "label": "DN25", + "value": "336.89" + } + ] + }, + { + "name": "sensor_4", + "label": "Sensor 4 type", + "type": "dropdown", + "group": "advanced", + "options": [ + { + "is_default": true, + "label": "DN20", + "value": "1000" + }, + { + "is_default": false, + "label": "DN25", + "value": "336.89" + } + ] + } + ], + "networks": [ + "../../../../network/lorawan-actility/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-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-senet/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" + ] +} diff --git a/decoders/connector/robeau/robeau/v1.0.0/payload.js b/decoders/connector/robeau/robeau/v1.0.0/payload.js new file mode 100644 index 00000000..2b32f366 --- /dev/null +++ b/decoders/connector/robeau/robeau/v1.0.0/payload.js @@ -0,0 +1,121 @@ +/* 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 = []; + +/** + * 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 + */ +// const device = { params: [{ key: 'sensor_1', value: '1000' }, { key: 'sensor_2', value: '1000' }, { key: 'sensor_4', value: '1000' }] }; + +function parsePayload(payload_raw, port) { + try { + // if (port === 100) + const buffer = Buffer.from(payload_raw, 'hex'); + const data = []; + + const header_data = (`00000000${(parseInt(payload_raw.substr(0, 2), 16)).toString(2)}`).substr(-8); + const sensor = header_data.substr(2, 4); + console.log(sensor[3]); + + const battery_voltage = buffer.readUInt8(1); + + data.push({ + variable: 'battery_voltage', value: battery_voltage / 100, unit: 'v', + }); + + if (buffer.length > 2) { + const sensor_1_param = device.params.find(param => param.key === 'sensor_1'); + const sensor_1 = buffer.readUIntBE(2, 6); + if (sensor_1_param) { + data.push({ + variable: 'sensor_1', value: sensor_1 / +sensor_1_param.value, unit: 'L', + }); + } else { + data.push({ + variable: 'sensor_1', value: sensor_1, unit: 'L', metadata: { error: 'Missing sensor_1 parameter' }, + }); + } + } + + if (buffer.length > 8) { + const sensor_2_param = device.params.find(param => param.key === 'sensor_2'); + const sensor_2 = buffer.readUIntBE(8, 6); + if (sensor_2_param) { + data.push({ + variable: 'sensor_2', value: sensor_2 / +sensor_2_param.value, unit: 'L', + }); + } else { + data.push({ + variable: 'sensor_2', value: sensor_2, unit: 'L', metadata: { error: 'Missing sensor_2 parameter' }, + }); + } + } + + if (buffer.length > 14) { + const sensor_3_param = device.params.find(param => param.key === 'sensor_3'); + const sensor_3 = buffer.readUIntBE(14, 6); + if (sensor_3_param) { + data.push({ + variable: 'sensor_3', value: sensor_3 / +sensor_3_param.value, unit: 'L', + }); + } else { + data.push({ + variable: 'sensor_3', value: sensor_3, unit: 'L', metadata: { error: 'Missing sensor_3 parameter' }, + }); + } + } + + if (buffer.length > 20) { + const sensor_4_param = device.params.find(param => param.key === 'sensor_4'); + const sensor_4 = buffer.readUIntBE(20, 6); + if (sensor_4_param) { + data.push({ + variable: 'sensor_4', value: sensor_4 / +sensor_4_param.value, unit: 'L', + }); + } else { + data.push({ + variable: 'sensor_4', value: sensor_4, unit: 'L', metadata: { error: 'Missing sensor_4 parameter' }, + }); + } + } + + return data; + } catch (e) { + console.log(e); + // Return the variable parse_error for debugging. + return [{ variable: 'parse_error', value: e.message }]; + } +} + +// let payload = [{ variable: 'payload', value: 'C5FE000000001777' }]; +// 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'); +// const port = payload.find(x => x.variable === 'port' || x.variable === 'fport'); +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 }))); + } +} + +// console.log(payload); diff --git a/decoders/connector/tektelic/asset-tracker/assets/logo.png b/decoders/connector/tektelic/asset-tracker/assets/logo.png new file mode 100644 index 00000000..19327dfa Binary files /dev/null and b/decoders/connector/tektelic/asset-tracker/assets/logo.png differ diff --git a/decoders/connector/tektelic/asset-tracker/connector.jsonc b/decoders/connector/tektelic/asset-tracker/connector.jsonc new file mode 100644 index 00000000..6397ed5d --- /dev/null +++ b/decoders/connector/tektelic/asset-tracker/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Asset Tracker", + "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/tektelic/asset-tracker/description.md b/decoders/connector/tektelic/asset-tracker/description.md new file mode 100644 index 00000000..3e9a9cd7 --- /dev/null +++ b/decoders/connector/tektelic/asset-tracker/description.md @@ -0,0 +1 @@ +GPS-enabled real-time location with detection of motion over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/asset-tracker/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/asset-tracker/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..b0dcef87 --- /dev/null +++ b/decoders/connector/tektelic/asset-tracker/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "##\nThe Asset Tracker provides GPS-enabled real-time location of fixed and mobile assets over LoRaWAN™.\n\nIt also supports user configurable triggers that can be based on the detection of motion, a set g-force acceleration threshold or a time interval as the basis for location reporting. \n\nThe Tracker also comes with a user configurable multi-function button that can be configured to address different use case requirements.\n", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/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-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-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-ttittn-v3/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/tektelic/asset-tracker/v1.0.0/payload.js b/decoders/connector/tektelic/asset-tracker/v1.0.0/payload.js new file mode 100644 index 00000000..23f46cec --- /dev/null +++ b/decoders/connector/tektelic/asset-tracker/v1.0.0/payload.js @@ -0,0 +1,623 @@ +/* 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": "00 BA 70 00 00 00" }] +** +** The ignore_vars variable in this code should be used to ignore variables +** from the device that you don't want. +*/ + +// let payload = [{ variable: 'payload', value: '00 BA 70 00 00 00'.replace(/ /g, '') }]; + +// Add ignorable variables in this array. + +const ignore_vars = []; + + +function toTagoFormat(object_item, serie, prefix = '') { + const result = []; + + for (const key in object_item) { + if (ignore_vars.includes(key)) continue; + + + if (typeof object_item[key] === 'object') { + result.push({ + + variable: object_item[key].variable || `${prefix}${key}`.toLowerCase(), + + value: object_item[key].value, + + serie: object_item[key].serie || serie, + + metadata: object_item[key].metadata, + + location: object_item[key].location, + + unit: object_item[key].unit, + + }); + } else { + result.push({ + + variable: `${prefix}${key}`.toLowerCase(), + + value: object_item[key], + + serie, + + }); + } + } + + + return result; +} + +function Decoder(bytes, port) { // bytes - Array of bytes (signed) + function slice(a, f, t) { + const res = []; + for (let i = 0; i < t - f; i++) { + res[i] = a[f + i]; + } + return res; + } + + function extract_bytes(chunk, start_bit, end_bit) { + const total_bits = end_bit - start_bit + 1; + const total_bytes = total_bits % 8 === 0 ? to_uint(total_bits / 8) : to_uint(total_bits / 8) + 1; + const offset_in_byte = start_bit % 8; + const end_bit_chunk = total_bits % 8; + const arr = new Array(total_bytes); + for (byte = 0; byte < total_bytes; ++byte) { + const chunk_idx = to_uint(start_bit / 8) + byte; + let lo = chunk[chunk_idx] >> offset_in_byte; + let hi = 0; + if (byte < total_bytes - 1) { + hi = (chunk[chunk_idx + 1] & ((1 << offset_in_byte) - 1)) << (8 - offset_in_byte); + } else if (end_bit_chunk !== 0) { + // Truncate last bits + lo &= ((1 << end_bit_chunk) - 1); + } + arr[byte] = hi | lo; + } + return arr; + } + + function apply_data_type(bytes, data_type) { + output = 0; + if (data_type === 'unsigned') { + for (var i = 0; i < bytes.length; ++i) { + output = (to_uint(output << 8)) | bytes[i]; + } + return output; + } + + if (data_type === 'signed') { + for (var i = 0; i < bytes.length; ++i) { + output = (output << 8) | bytes[i]; + } + // Convert to signed, based on value size + if (output > Math.pow(2, 8 * bytes.length - 1)) { + output -= Math.pow(2, 8 * bytes.length); + } + return output; + } + if (data_type === 'bool') { + return !(bytes[0] === 0); + } + if (data_type === 'hexstring') { + return toHexString(bytes); + } + // Incorrect data type + return null; + } + + function decode_field(chunk, start_bit, end_bit, data_type) { + chunk_size = chunk.length; + if (end_bit >= chunk_size * 8) { + return null; // Error: exceeding boundaries of the chunk + } + if (end_bit < start_bit) { + return null; // Error: invalid input + } + arr = extract_bytes(chunk, start_bit, end_bit); + return apply_data_type(arr, data_type); + } + + decoded_data = {}; + decoder = []; + + if (port === 10) { + decoder = [ + { + key: [0x00, 0xBA], + fn(arg) { + decoded_data.battery_status_life = 2.5 + decode_field(arg, 0, 6, 'unsigned') * 0.01; + decoded_data.battery_status_eos_alert = decode_field(arg, 7, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x00, 0x04], + fn(arg) { + decoded_data.fsm_state = decode_field(arg, 0, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x00, 0x67], + fn(arg) { + decoded_data.mcu_temperature = decode_field(arg, 0, 15, 'signed') * 0.1; + return 2; + }, + }, + { + key: [0x00, 0x00], + fn(arg) { + decoded_data.acceleration_alarm = decode_field(arg, 0, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x00, 0x71], + fn(arg) { + decoded_data.acceleration_xaxis = decode_field(arg, 0, 15, 'signed') * 0.001; + decoded_data.acceleration_yaxis = decode_field(arg, 16, 31, 'signed') * 0.001; + decoded_data.acceleration_zaxis = decode_field(arg, 32, 47, 'signed') * 0.001; + return 6; + }, + }, + ]; + } + if (port === 100) { + decoder = [ + { + key: [0x00], + fn(arg) { + decoded_data.device_eui = decode_field(arg, 0, 63, 'hexstring'); + return 8; + }, + }, + { + key: [0x01], + fn(arg) { + decoded_data.app_eui = decode_field(arg, 0, 63, 'hexstring'); + return 8; + }, + }, + { + key: [0x02], + fn(arg) { + decoded_data.app_key = decode_field(arg, 0, 127, 'hexstring'); + return 16; + }, + }, + { + key: [0x03], + fn(arg) { + decoded_data.device_address = decode_field(arg, 0, 31, 'hexstring'); + return 4; + }, + }, + { + key: [0x04], + fn(arg) { + decoded_data.network_session_key = decode_field(arg, 0, 127, 'hexstring'); + return 16; + }, + }, + { + key: [0x05], + fn(arg) { + decoded_data.app_session_key = decode_field(arg, 0, 127, 'hexstring'); + return 16; + }, + }, + { + key: [0x10], + fn(arg) { + decoded_data.loramac_join_mode = decode_field(arg, 7, 7, 'unsigned'); + return 2; + }, + }, + { + key: [0x11], + fn(arg) { + decoded_data.loramac_opts_confirm_mode = decode_field(arg, 8, 8, 'unsigned'); + decoded_data.loramac_opts_sync_word = decode_field(arg, 9, 9, 'unsigned'); + decoded_data.loramac_opts_duty_cycle = decode_field(arg, 10, 10, 'unsigned'); + decoded_data.loramac_opts_adr = decode_field(arg, 11, 11, 'unsigned'); + return 2; + }, + }, + { + key: [0x12], + fn(arg) { + decoded_data.loramac_dr_tx_dr_number = decode_field(arg, 0, 3, 'unsigned'); + decoded_data.loramac_dr_tx_tx_power_number = decode_field(arg, 8, 11, 'unsigned'); + return 2; + }, + }, + { + key: [0x13], + fn(arg) { + decoded_data.loramac_rx2_frequency = decode_field(arg, 0, 31, 'unsigned'); + decoded_data.loramac_rx2_dr_number = decode_field(arg, 32, 39, 'unsigned'); + return 5; + }, + }, + { + key: [0x20], + fn(arg) { + decoded_data.seconds_per_core_tick = decode_field(arg, 0, 31, 'unsigned'); + return 4; + }, + }, + { + key: [0x21], + fn(arg) { + decoded_data.tick_per_battery = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x24], + fn(arg) { + decoded_data.tick_per_accelerometer = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x25], + fn(arg) { + decoded_data.tick_per_ble_default = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x26], + fn(arg) { + decoded_data.tick_per_ble_stillness = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x27], + fn(arg) { + decoded_data.tick_per_ble_mobility = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x28], + fn(arg) { + decoded_data.tick_per_temperature = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x2A], + fn(arg) { + decoded_data.mode_reed_event_type = decode_field(arg, 7, 7, 'unsigned'); + decoded_data.mode_battery_voltage_report = decode_field(arg, 8, 8, 'unsigned'); + decoded_data.mode_acceleration_vector_report = decode_field(arg, 9, 9, 'unsigned'); + decoded_data.mode_temperature_report = decode_field(arg, 10, 10, 'unsigned'); + decoded_data.mode_ble_report = decode_field(arg, 11, 11, 'unsigned'); + return 2; + }, + }, + { + key: [0x2B], + fn(arg) { + decoded_data.event_type1_m_value = decode_field(arg, 0, 3, 'unsigned'); + decoded_data.event_type1_n_value = decode_field(arg, 4, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x2C], + fn(arg) { + decoded_data.event_type2_t_value = decode_field(arg, 0, 3, 'unsigned'); + return 1; + }, + }, + { + key: [0x40], + fn(arg) { + decoded_data.accelerometer_xaxis_enabled = decode_field(arg, 0, 0, 'unsigned'); + decoded_data.accelerometer_yaxis_enabled = decode_field(arg, 1, 1, 'unsigned'); + decoded_data.accelerometer_zaxis_enabled = decode_field(arg, 2, 2, 'unsigned'); + return 1; + }, + }, + { + key: [0x41], + fn(arg) { + decoded_data.sensitivity_accelerometer_sample_rate = decode_field(arg, 0, 2, 'unsigned') * 1; + switch (decoded_data.sensitivity_accelerometer_sample_rate) { + case 1: + decoded_data.sensitivity_accelerometer_sample_rate = 1; + break; + case 2: + decoded_data.sensitivity_accelerometer_sample_rate = 10; + break; + case 3: + decoded_data.sensitivity_accelerometer_sample_rate = 25; + break; + case 4: + decoded_data.sensitivity_accelerometer_sample_rate = 50; + break; + case 5: + decoded_data.sensitivity_accelerometer_sample_rate = 100; + break; + case 6: + decoded_data.sensitivity_accelerometer_sample_rate = 200; + break; + case 7: + decoded_data.sensitivity_accelerometer_sample_rate = 400; + break; + default: // invalid value + decoded_data.sensitivity_accelerometer_sample_rate = 0; + break; + } + + decoded_data.sensitivity_accelerometer_measurement_range = decode_field(arg, 4, 5, 'unsigned') * 1; + switch (decoded_data.sensitivity_accelerometer_measurement_range) { + case 0: + decoded_data.sensitivity_accelerometer_measurement_range = 2; + break; + case 1: + decoded_data.sensitivity_accelerometer_measurement_range = 4; + break; + case 2: + decoded_data.sensitivity_accelerometer_measurement_range = 8; + break; + case 3: + decoded_data.sensitivity_accelerometer_measurement_range = 16; + break; + default: + decoded_data.sensitivity_accelerometer_measurement_range = 0; + } + return 1; + }, + }, + { + key: [0x42], + fn(arg) { + decoded_data.acceleration_alarm_threshold_count = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x43], + fn(arg) { + decoded_data.acceleration_alarm_threshold_period = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x44], + fn(arg) { + decoded_data.acceleration_alarm_threshold = decode_field(arg, 0, 15, 'unsigned') * 0.001; + return 2; + }, + }, + { + key: [0x45], + fn(arg) { + decoded_data.acceleration_alarm_grace_period = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x46], + fn(arg) { + decoded_data.accelerometer_tx_report_periodic_enabled = decode_field(arg, 0, 0, 'unsigned'); + decoded_data.accelerometer_tx_report_alarm_enabled = decode_field(arg, 1, 1, 'unsigned'); + return 1; + }, + }, + { + key: [0x50], + fn(arg) { + decoded_data.ble_mode = decode_field(arg, 7, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x51], + fn(arg) { + decoded_data.ble_scan_interval = decode_field(arg, 0, 15, 'unsigned') * 0.001; + return 2; + }, + }, + { + key: [0x52], + fn(arg) { + decoded_data.ble_scan_window = decode_field(arg, 0, 15, 'unsigned') * 0.001; + return 2; + }, + }, + { + key: [0x53], + fn(arg) { + decoded_data.ble_scan_duration = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x54], + fn(arg) { + decoded_data.ble_reported_devices = decode_field(arg, 0, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x60], + fn(arg) { + decoded_data.temperature_sample_period_idle = decode_field(arg, 0, 31, 'unsigned'); + return 4; + }, + }, + { + key: [0x61], + fn(arg) { + decoded_data.temperature_sample_period_active = decode_field(arg, 0, 31, 'unsigned'); + return 4; + }, + }, + { + key: [0x62], + fn(arg) { + decoded_data.temperature_threshold_high = decode_field(arg, 0, 7, 'unsigned'); + decoded_data.temperature_threshold_low = decode_field(arg, 8, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x63], + fn(arg) { + decoded_data.temperature_threshold_enabled = decode_field(arg, 0, 0, 'unsigned'); + return 1; + }, + }, + { + key: [0x71], + fn(arg) { + decoded_data.firmware_version_app_major_version = decode_field(arg, 0, 7, 'unsigned'); + decoded_data.firmware_version_app_minor_version = decode_field(arg, 8, 15, 'unsigned'); + decoded_data.firmware_version_app_revision = decode_field(arg, 16, 23, 'unsigned'); + decoded_data.firmware_version_loramac_major_version = decode_field(arg, 24, 31, 'unsigned'); + decoded_data.firmware_version_loramac_minor_version = decode_field(arg, 32, 39, 'unsigned'); + decoded_data.firmware_version_loramac_revision = decode_field(arg, 40, 47, 'unsigned'); + decoded_data.firmware_version_region = decode_field(arg, 48, 55, 'unsigned'); + return 7; + }, + }, + ]; + } + + if (port === 25) { + decoder = [ + { + key: [0x0A], + fn(arg) { + // RSSI to beacons + let count = 0; + for (let i = 0; i < arg.length * 8; i += 7 * 8) { + dev_id = decode_field(arg, i, i + 6 * 8 - 1, 'hexstring'); + decoded_data[dev_id] = decode_field(arg, i + 6 * 8, i + 7 * 8 - 1, 'signed'); + count += 7; + } + return count; + }, + }, + ]; + } + + bytes = convertToUint8Array(bytes); + decoded_data.raw = JSON.stringify(byteToArray(bytes)); + decoded_data.port = port; + + for (let bytes_left = bytes.length; bytes_left > 0;) { + let found = false; + for (let i = 0; i < decoder.length; i++) { + const item = decoder[i]; + const key = item.key; + const keylen = key.length; + header = slice(bytes, 0, keylen); + // Header in the data matches to what we expect + if (is_equal(header, key)) { + const f = item.fn; + consumed = f(slice(bytes, keylen, bytes.length)) + keylen; + bytes_left -= consumed; + bytes = slice(bytes, consumed, bytes.length); + found = true; + break; + } + } + if (found) { + continue; + } + // Unable to decode -- headers are not as expected, send raw payload to the application! + decoded_data = {}; + decoded_data.raw = JSON.stringify(byteToArray(bytes)); + decoded_data.port = port; + return decoded_data; + } + + // Converts value to unsigned + function to_uint(x) { + return x >>> 0; + } + + // Checks if two arrays are equal + function is_equal(arr1, arr2) { + if (arr1.length != arr2.length) { + return false; + } + for (let i = 0; i != arr1.length; i++) { + if (arr1[i] != arr2[i]) { + return false; + } + } + return true; + } + + function byteToArray(byteArray) { + arr = []; + for (let i = 0; i < byteArray.length; i++) { + arr.push(byteArray[i]); + } + return arr; + } + + function convertToUint8Array(byteArray) { + arr = []; + for (let i = 0; i < byteArray.length; i++) { + arr.push(to_uint(byteArray[i]) & 0xff); + } + return arr; + } + + function toHexString(byteArray) { + const arr = []; + for (let i = 0; i < byteArray.length; ++i) { + arr.push((`0${(byteArray[i] & 0xFF).toString(16)}`).slice(-2)); + } + return arr.join(''); + } + + return decoded_data; +} + + +// 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'); +const port = payload.find(x => x.variable === 'port' || x.variable === 'fport'); + + +if (payload_raw) { + // Get a unique serie for the incoming data. + const { value, time } = payload_raw; + let { serie } = payload_raw; + serie = new Date().getTime(); + + + // Parse the payload_raw to JSON format (it comes in a String format) + + if (value) { + payload = payload.concat(toTagoFormat(Decoder(Buffer.from(value.replace(/ /g, ''), 'hex'), Number(port.value)), serie)); + } +} diff --git a/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/assets/logo.png b/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/assets/logo.png new file mode 100644 index 00000000..e69de29b diff --git a/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/connector.jsonc b/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/connector.jsonc new file mode 100644 index 00000000..56f17306 --- /dev/null +++ b/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Breeze Indoor Air Quality & 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/tektelic/breeze-indoor-air-quality-co2-sensor/description.md b/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/description.md new file mode 100644 index 00000000..b84e9652 --- /dev/null +++ b/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/description.md @@ -0,0 +1 @@ +Non dispersive infrared (NDIR) CO2 sensor over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..46ab3687 --- /dev/null +++ b/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "The TEKTELIC BREEZE is a compact and versatile member of the Smart Room Sensor family, designed to enhance the monitoring of indoor environments. It comes in two variants: the standard BREEZE, which measures CO2 levels, temperature, humidity, and light; and the BREEZE-V, which includes all the functionalities of the standard model plus a PIR motion sensor for enhanced environment monitoring.\n\nKey features of the BREEZE series include: \n * user-configurable parameters and thresholds;\n * optimal Battery Life, boasting up to 5+ years;\n * it can be paired with an E-Ink display for easy viewing of measurements.\n\nThese sensors are particularly noted for their precision and reliability, enhanced by features such as automatic CO2 calibration and barometric pressure compensation, which adjust readings based on the deployment altitude to provide accurate and reliable data.\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-helium/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-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/v1.0.0/payload.js b/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..f7a77099 --- /dev/null +++ b/decoders/connector/tektelic/breeze-indoor-air-quality-co2-sensor/v1.0.0/payload.js @@ -0,0 +1,118 @@ +/* + * Smart Room Sensor (Gen 4) + */ +function signed_convert(val, bitwidth) { + const isnegative = val & (1 << (bitwidth - 1)); + const boundary = 1 << bitwidth; + const minval = -boundary; + const mask = boundary - 1; + return isnegative ? minval + (val & mask) : val; +} +function Decoder(bytes, port, string_find) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + // Device Info Not Repeated + const array_result = []; + if (port === 10) { + // CO2 Concentration(PressureCompensated) + if (string_find.search("0be4") !== -1) { + const result = string_find.search("0be4") / 2 + 2; + const co2_pressure_compensated = (bytes[result] << 8) | bytes[result + 1]; + array_result.push({ variable: "co2_pressure_compensated", value: co2_pressure_compensated, unit: "ppm" }); + } + if (string_find.search("0BE4") !== -1) { + const result = string_find.search("0BE4") / 2 + 2; + const co2_pressure_compensated = (bytes[result] << 8) | bytes[result + 1]; + array_result.push({ variable: "co2_pressure_compensated", value: co2_pressure_compensated, unit: "ppm" }); + } + // CO2 Concentration(Raw) + if (string_find.search("0ee4") !== -1) { + const result = string_find.search("0be4") / 2 + 2; + const co2_raw = (bytes[result] << 8) | bytes[result + 1]; + array_result.push({ variable: "co2_raw", value: co2_raw, unit: "ppm" }); + } + if (string_find.search("0EE4") !== -1) { + const result = string_find.search("0EE4") / 2 + 2; + const co2_raw = (bytes[result] << 8) | bytes[result + 1]; + array_result.push({ variable: "co2_raw", value: co2_raw, unit: "ppm" }); + } + // Atmospheric Pressure + if (string_find.search("0c73") !== -1) { + const result = string_find.search("0c73") / 2 + 2; + const atmospheric_pressure = ((bytes[result] << 8) | bytes[result + 1]) * 0.1; + array_result.push({ variable: "atmospheric_pressure", value: atmospheric_pressure, unit: "hPa" }); + } + if (string_find.search("0C73") !== -1) { + const result = string_find.search("0C73") / 2 + 2; + const atmospheric_pressure = ((bytes[result] << 8) | bytes[result + 1]) * 0.1; + array_result.push({ variable: "atmospheric_pressure", value: atmospheric_pressure, unit: "hPa" }); + } + // Motion (PIR) Event State + if (string_find.search("0a00") !== -1) { + const result = string_find.search("0a00") / 2 + 2; + const motion_event_state = bytes[result]; + array_result.push({ variable: "motion_event_state", value: motion_event_state }); + } + if (string_find.search("0A00") !== -1) { + const result = string_find.search("0A00") / 2 + 2; + const motion_event_state = bytes[result]; + array_result.push({ variable: "motion_event_state", value: motion_event_state }); + } + // Motion (PIR) Event Count + if (string_find.search("0d04") !== -1) { + const result = string_find.search("0d04") / 2 + 2; + const motion_event_count = (bytes[result] << 8) | bytes[result + 1]; + array_result.push({ variable: "motion_event_count", value: motion_event_count }); + } + if (string_find.search("0D04") !== -1) { + const result = string_find.search("0D04") / 2 + 2; + const motion_event_count = (bytes[result] << 8) | bytes[result + 1]; + array_result.push({ variable: "motion_event_count", value: motion_event_count }); + } + // Ambient Temperature + if (string_find.search("0367") !== -1) { + const result = string_find.search("0367") / 2 + 2; + const ambient_temperature = (bytes[result] << 8) | bytes[result + 1]; + const temperature = signed_convert(ambient_temperature, 16) * 0.1; + array_result.push({ variable: "temperature", value: temperature, unit: "°C" }); + } + // Ambient RH + if (string_find.search("0468") !== -1) { + const result = string_find.search("0468") / 2 + 2; + const relative_humidity = bytes[result] * 0.5; + array_result.push({ variable: "relative_humidity", value: relative_humidity, unit: "%" }); + } + // Ambient Light State + if (string_find.search("0200") !== -1) { + const result = string_find.search("0200") / 2 + 2; + const light_state = bytes[result]; + array_result.push({ variable: "light_state", value: light_state }); + } + + // Ambient Light Intensity + if (string_find.search("1002") !== -1) { + const result = string_find.search("1002") / 2 + 2; + const light_intensity = ((bytes[result] << 8) | bytes[result + 1]) * 0.1; + array_result.push({ variable: "light_intensity", value: light_intensity }); + } + return array_result; + } +} + +const payload_raw = payload.find((x) => x.variable === "payload_raw" || x.variable === "payload" || x.variable === "data" || x.variable === "payload_hex"); +const port = payload.find((x) => x.variable === "port" || x.variable === "fport" || x.variable === "FPort"); +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 string_find = payload_raw.value; + const payload_aux = Decoder(buffer, port.value, string_find); + 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/tektelic/clover-agriculture-sensor/assets/logo.png b/decoders/connector/tektelic/clover-agriculture-sensor/assets/logo.png new file mode 100644 index 00000000..864538a7 Binary files /dev/null and b/decoders/connector/tektelic/clover-agriculture-sensor/assets/logo.png differ diff --git a/decoders/connector/tektelic/clover-agriculture-sensor/connector.jsonc b/decoders/connector/tektelic/clover-agriculture-sensor/connector.jsonc new file mode 100644 index 00000000..a71bafa7 --- /dev/null +++ b/decoders/connector/tektelic/clover-agriculture-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Clover Agriculture 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/tektelic/clover-agriculture-sensor/description.md b/decoders/connector/tektelic/clover-agriculture-sensor/description.md new file mode 100644 index 00000000..ce586b52 --- /dev/null +++ b/decoders/connector/tektelic/clover-agriculture-sensor/description.md @@ -0,0 +1 @@ +Soil and ambient sensor for smart agriculture deployments over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/clover-agriculture-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/clover-agriculture-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..eec922ac --- /dev/null +++ b/decoders/connector/tektelic/clover-agriculture-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "The Tektelic CLOVER Agriculture Sensor provies a complete solution for the collection of key soil and environmental metrics \nfor crops such as soil moisture and temperature, air temperature and humidity, and outdoor light monitoring.\n\n• Ruggedized IP-67 enclosure for use in the most challenging outdoor environmental conditions\n• Integrated C-Cell LTC battery provides substantial battery life up to 10 years with a battery status indicator for easy reference\n• All Global ISM Bands\n• Soil Surface or Elevated Mounting Options\n• Internal or Optional External Antenna\n• LoRa device Class A (B)\n• RF power up to 23 dBm (200mW)\n\nThe CLOVER sensor is fully wireless, providing the flexibility to be deployed anywhere without the need for direct power access, and it includes a battery status indicator for maintenance alerts. It's designed to support multiple sensing functions, which not only monitor the basic environmental metrics but also detect changes in light and movement, crucial for comprehensive agricultural management.\n\n", + "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-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-kerlink/v1.0.0/payload.js", + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-senra/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-tektelic/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/tektelic/clover-agriculture-sensor/v1.0.0/payload.js b/decoders/connector/tektelic/clover-agriculture-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..c951fa71 --- /dev/null +++ b/decoders/connector/tektelic/clover-agriculture-sensor/v1.0.0/payload.js @@ -0,0 +1,146 @@ +function Decoder(bytes, port) { + const decoded = []; + if (port === 10) { + let soil_temp_cont = 0; + let soil_moist_cont = 0; + for (let i = 0; i < bytes.length; ) { + const channel = bytes[i++]; + const type = bytes[i++]; + + if (channel === 0x00 && type === 0xba) { + const byte = bytes.readUInt8(i++); + decoded.push({ variable: "battery_voltage", value: (byte & 0x7f) * 0.01 + 2.5, unit: "V" }); + decoded.push({ variable: "eos_alert", value: byte >> 7 }); + } else if (channel === 0x00 && type === 0xff) { + const byte = bytes.readUInt16BE(i); + i += 2; + decoded.push({ variable: "battery_voltage", value: byte * 0.01, unit: "V" }); + } else if (channel === 0x01 && type === 0x04) { + const byte = bytes.readUInt16BE(i); + decoded.push({ variable: "soil_moisture_raw", value: byte, unit: "kHz" }); + i += 2; + if (byte > 1402 && byte <= 1399) decoded.push({ variable: "soil_gwc", value: 0 }); + else if (byte > 1399 && byte <= 1396) decoded.push({ variable: "soil_gwc", value: 0.1 }); + else if (byte > 1396 && byte <= 1391) decoded.push({ variable: "soil_gwc", value: 0.2 }); + else if (byte > 1391 && byte <= 1386) decoded.push({ variable: "soil_gwc", value: 0.3 }); + else if (byte > 1386 && byte <= 1381) decoded.push({ variable: "soil_gwc", value: 0.4 }); + else if (byte > 1381 && byte <= 1376) decoded.push({ variable: "soil_gwc", value: 0.5 }); + else if (byte > 1376 && byte <= 1371) decoded.push({ variable: "soil_gwc", value: 0.6 }); + else if (byte > 1371 && byte <= 1366) decoded.push({ variable: "soil_gwc", value: 0.7 }); + else if (byte > 1366 && byte <= 1361) decoded.push({ variable: "soil_gwc", value: 0.8 }); + else if (byte > 1361 && byte <= 1356) decoded.push({ variable: "soil_gwc", value: 0.9 }); + else if (byte > 1356 && byte <= 1351) decoded.push({ variable: "soil_gwc", value: 1 }); + else if (byte > 1351 && byte <= 1346) decoded.push({ variable: "soil_gwc", value: 1.1 }); + else if (byte > 1346 && byte <= 1341) decoded.push({ variable: "soil_gwc", value: 1.2 }); + else if (byte > 1341 && byte <= 1322) decoded.push({ variable: "soil_gwc", value: 2 }); + } else if (channel === 0x02 && type === 0x02) { + const byte = bytes.readUInt16BE(i); + decoded.push({ variable: "soil_temp_raw", value: byte, unit: "mV" }); + decoded.push({ variable: "soil_temp", value: -32.46 * Math.log(byte) + 236.36, unit: "°C" }); + i += 2; + } else if (channel === 0x03 && type === 0x02) { + const byte = bytes.readUInt16BE(i); + decoded.push({ variable: "soil_temp_raw1", value: byte, unit: "mV" }); + decoded.push({ variable: "soil_temp1", value: -31.96 * Math.log(byte) + 213.25, unit: "°C" }); + i += 2; + soil_temp_cont += 1; + } else if (channel === 0x04 && type === 0x02) { + const byte = bytes.readUInt16BE(i); + decoded.push({ variable: "soil_temp_raw2", value: byte, unit: "mV" }); + decoded.push({ variable: "soil_temp2", value: -31.96 * Math.log(byte) + 213.25, unit: "°C" }); + i += 2; + soil_temp_cont += 2; + } else if (channel === 0x05 && type === 0x04) { + const byte = bytes.readUInt16BE(i); + decoded.push({ variable: "soil_moisture_raw1", value: byte, unit: "Hz" }); + i += 2; + if (byte < 293) decoded.push({ variable: "soil_water_tension1", value: 200, unit: "kPa" }); + else if (byte <= 485) decoded.push({ variable: "soil_water_tension1", value: 200 - (byte - 293) * 0.5208, unit: "kPa" }); + else if (byte <= 600) decoded.push({ variable: "soil_water_tension1", value: 100 - (byte - 485) * 0.2174, unit: "kPa" }); + else if (byte <= 770) decoded.push({ variable: "soil_water_tension1", value: 75 - (byte - 600) * 0.1176, unit: "kPa" }); + else if (byte <= 1110) decoded.push({ variable: "soil_water_tension1", value: 55 - (byte - 770) * 0.05884, unit: "kPa" }); + else if (byte <= 2820) decoded.push({ variable: "soil_water_tension1", value: 35 - (byte - 1110) * 0.0117, unit: "kPa" }); + else if (byte <= 4330) decoded.push({ variable: "soil_water_tension1", value: 15 - (byte - 2820) * 0.003974, unit: "kPa" }); + else if (byte <= 6430) decoded.push({ variable: "soil_water_tension1", value: 9 - (byte - 4330) * 0.004286, unit: "kPa" }); + else if (byte > 6430) decoded.push({ variable: "soil_water_tension1", value: 0, unit: "kPa" }); + // adjust with soil_temp1 after all variables are collected + soil_moist_cont += 1; + } else if (channel === 0x06 && type === 0x04) { + const byte = bytes.readUInt16BE(i); + decoded.push({ variable: "soil_moisture_raw2", value: byte, unit: "Hz" }); + i += 2; + if (byte < 293) decoded.push({ variable: "soil_water_tension2", value: 200, unit: "kPa" }); + else if (byte <= 485) decoded.push({ variable: "soil_water_tension2", value: 200 - (byte - 293) * 0.5208, unit: "kPa" }); + else if (byte <= 600) decoded.push({ variable: "soil_water_tension2", value: 100 - (byte - 485) * 0.2174, unit: "kPa" }); + else if (byte <= 770) decoded.push({ variable: "soil_water_tension2", value: 75 - (byte - 600) * 0.1176, unit: "kPa" }); + else if (byte <= 1110) decoded.push({ variable: "soil_water_tension2", value: 55 - (byte - 770) * 0.05884, unit: "kPa" }); + else if (byte <= 2820) decoded.push({ variable: "soil_water_tension2", value: 35 - (byte - 1110) * 0.0117, unit: "kPa" }); + else if (byte <= 4330) decoded.push({ variable: "soil_water_tension2", value: 15 - (byte - 2820) * 0.003974, unit: "kPa" }); + else if (byte <= 6430) decoded.push({ variable: "soil_water_tension2", value: 9 - (byte - 4330) * 0.004286, unit: "kPa" }); + else if (byte > 6430) decoded.push({ variable: "soil_water_tension2", value: 0, unit: "kPa" }); + // adjust with soil_temp2 after all variables are collected + soil_moist_cont += 2; + } else if (channel === 0x09 && type === 0x65) { + const byte = bytes.readUInt16BE(i); + decoded.push({ variable: "ambient_light", value: byte, unit: "lux" }); + i += 2; + } else if (channel === 0x09 && type === 0x00) { + const byte = bytes.readUInt8(i++); + decoded.push({ variable: "ambient_light_detected", value: byte === 0x00 ? "no" : "yes" }); + } else if (channel === 0x0a && type === 0x71) { + const byte_x = bytes.readInt16BE(i); + i += 2; + const byte_y = bytes.readInt16BE(i); + i += 2; + const byte_z = bytes.readInt16BE(i); + i += 2; + decoded.push({ variable: "acceleration_xaxis", value: byte_x * 0.001, unit: "g" }); + decoded.push({ variable: "acceleration_yaxis", value: byte_y * 0.001, unit: "g" }); + decoded.push({ variable: "acceleration_zaxis", value: byte_z * 0.001, unit: "g" }); + } else if (channel === 0x0a && type === 0x00) { + const byte = bytes.readUInt8(i++); + decoded.push({ variable: "orientation_alarm", value: byte === 0x00 ? "no" : "yes" }); + } else if (channel === 0x0b && type === 0x67) { + const byte = bytes.readInt16BE(i); + i += 2; + decoded.push({ variable: "ambient_temp", value: byte * 0.1, unit: "°C" }); + } else if (channel === 0x0b && type === 0x68) { + const byte = bytes.readUInt8(i++); + decoded.push({ variable: "ambient_hum", value: byte * 0.5, unit: "%" }); + } else if (channel === 0x0c && type === 0x67) { + const byte = bytes.readInt8(i++); + decoded.push({ variable: "mcu_temp", value: byte * 0.1, unit: "°C" }); + } + } + if (soil_moist_cont > 0 && soil_temp_cont > 0) { + const temp1 = decoded.find((x) => x.variable === "soil_temp1"); + const temp2 = decoded.find((x) => x.variable === "soil_temp2"); + const moist1 = decoded.find((x) => x.variable === "soil_water_tension1"); + const moist2 = decoded.find((x) => x.variable === "soil_water_tension2"); + + if (temp1.value && temp1.value !== 24.0 && moist1.value) { + decoded.push({ variable: "soil_water_tension1_adjusted", value: moist1.value * (1 - 0.019 * (temp1.value - 24)), unit: "kPa" }); + } + if (temp2.value && temp2.value !== 24.0 && moist2.value) { + decoded.push({ variable: "soil_water_tension2_adjusted", value: moist2.value * (1 - 0.019 * (temp2.value - 24)), unit: "kPa" }); + } + } + } + + return decoded; +} + +// let payload = [ +// { variable: "payload", value: "096500000b6700e10b6892" }, +// { variable: "port", value: "10" }, +// ]; + +const data = payload.find((x) => x.variable === "payload" || x.variable === "payload_raw" || x.variable === "data"); +const port = payload.find((x) => x.variable === "port" || x.variable === "fport"); +if (data) { + const serie = new Date().getTime(); + const bytes = Buffer.from(data.value, "hex"); + payload = payload.concat(Decoder(bytes, parseInt(port.value, 10))).map((x) => ({ ...x, serie })); +} + +// console.log(payload); diff --git a/decoders/connector/tektelic/cold-room-temperature-sensor/assets/logo.png b/decoders/connector/tektelic/cold-room-temperature-sensor/assets/logo.png new file mode 100644 index 00000000..897bea88 Binary files /dev/null and b/decoders/connector/tektelic/cold-room-temperature-sensor/assets/logo.png differ diff --git a/decoders/connector/tektelic/cold-room-temperature-sensor/connector.jsonc b/decoders/connector/tektelic/cold-room-temperature-sensor/connector.jsonc new file mode 100644 index 00000000..1c3322f5 --- /dev/null +++ b/decoders/connector/tektelic/cold-room-temperature-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Cold Room 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/tektelic/cold-room-temperature-sensor/description.md b/decoders/connector/tektelic/cold-room-temperature-sensor/description.md new file mode 100644 index 00000000..897b35f8 --- /dev/null +++ b/decoders/connector/tektelic/cold-room-temperature-sensor/description.md @@ -0,0 +1 @@ +Device for cold chain environment monitoring over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/cold-room-temperature-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/cold-room-temperature-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..d30012de --- /dev/null +++ b/decoders/connector/tektelic/cold-room-temperature-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "* 10 Year Battery Life\n* Restaurant and Food Service Cold Chain\n* Medical and Pharmaceutical Storage\n* Vaccine Storage\n* All Global ISM Bands\n* Small Form Factor for Diverse Deployments\n* Easily Integrated into LoRaWAN® Networks\n* Internal Antenna\n* -40° to +60°C operability ", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-actility/v1.0.0/payload.js", + "../../../../network/lorawan-everynet/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/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-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/tektelic/cold-room-temperature-sensor/v1.0.0/payload.js b/decoders/connector/tektelic/cold-room-temperature-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..989c7938 --- /dev/null +++ b/decoders/connector/tektelic/cold-room-temperature-sensor/v1.0.0/payload.js @@ -0,0 +1,623 @@ +/* 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": "00 BA 70 00 00 00" }] +** +** The ignore_vars variable in this code should be used to ignore variables +** from the device that you don't want. +*/ + +// let payload = [{ variable: 'payload', value: '00 BA 70 00 00 00'.replace(/ /g, '') }]; + +// Add ignorable variables in this array. + +const ignore_vars = []; + + +function toTagoFormat(object_item, serie, prefix = '') { + const result = []; + + for (const key in object_item) { + if (ignore_vars.includes(key)) continue; + + + if (typeof object_item[key] === 'object') { + result.push({ + + variable: object_item[key].variable || `${prefix}${key}`.toLowerCase(), + + value: object_item[key].value, + + serie: object_item[key].serie || serie, + + metadata: object_item[key].metadata, + + location: object_item[key].location, + + unit: object_item[key].unit, + + }); + } else { + result.push({ + + variable: `${prefix}${key}`.toLowerCase(), + + value: object_item[key], + + serie, + + }); + } + } + + + return result; +} + +function Decoder(bytes, port) { // bytes - Array of bytes (signed) + function slice(a, f, t) { + const res = []; + for (let i = 0; i < t - f; i++) { + res[i] = a[f + i]; + } + return res; + } + + function extract_bytes(chunk, start_bit, end_bit) { + const total_bits = end_bit - start_bit + 1; + const total_bytes = total_bits % 8 === 0 ? to_uint(total_bits / 8) : to_uint(total_bits / 8) + 1; + const offset_in_byte = start_bit % 8; + const end_bit_chunk = total_bits % 8; + const arr = new Array(total_bytes); + for (byte = 0; byte < total_bytes; ++byte) { + const chunk_idx = to_uint(start_bit / 8) + byte; + let lo = chunk[chunk_idx] >> offset_in_byte; + let hi = 0; + if (byte < total_bytes - 1) { + hi = (chunk[chunk_idx + 1] & ((1 << offset_in_byte) - 1)) << (8 - offset_in_byte); + } else if (end_bit_chunk !== 0) { + // Truncate last bits + lo &= ((1 << end_bit_chunk) - 1); + } + arr[byte] = hi | lo; + } + return arr; + } + + function apply_data_type(bytes, data_type) { + output = 0; + if (data_type === 'unsigned') { + for (var i = 0; i < bytes.length; ++i) { + output = (to_uint(output << 8)) | bytes[i]; + } + return output; + } + + if (data_type === 'signed') { + for (var i = 0; i < bytes.length; ++i) { + output = (output << 8) | bytes[i]; + } + // Convert to signed, based on value size + if (output > Math.pow(2, 8 * bytes.length - 1)) { + output -= Math.pow(2, 8 * bytes.length); + } + return output; + } + if (data_type === 'bool') { + return !(bytes[0] === 0); + } + if (data_type === 'hexstring') { + return toHexString(bytes); + } + // Incorrect data type + return null; + } + + function decode_field(chunk, start_bit, end_bit, data_type) { + chunk_size = chunk.length; + if (end_bit >= chunk_size * 8) { + return null; // Error: exceeding boundaries of the chunk + } + if (end_bit < start_bit) { + return null; // Error: invalid input + } + arr = extract_bytes(chunk, start_bit, end_bit); + return apply_data_type(arr, data_type); + } + + decoded_data = {}; + decoder = []; + + if (port === 10) { + decoder = [ + { + key: [0x00, 0xBA], + fn(arg) { + decoded_data.battery_status_life = 2.5 + decode_field(arg, 0, 6, 'unsigned') * 0.01; + decoded_data.battery_status_eos_alert = decode_field(arg, 7, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x00, 0x04], + fn(arg) { + decoded_data.fsm_state = decode_field(arg, 0, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x00, 0x67], + fn(arg) { + decoded_data.mcu_temperature = decode_field(arg, 0, 15, 'signed') * 0.1; + return 2; + }, + }, + { + key: [0x00, 0x00], + fn(arg) { + decoded_data.acceleration_alarm = decode_field(arg, 0, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x00, 0x71], + fn(arg) { + decoded_data.acceleration_xaxis = decode_field(arg, 0, 15, 'signed') * 0.001; + decoded_data.acceleration_yaxis = decode_field(arg, 16, 31, 'signed') * 0.001; + decoded_data.acceleration_zaxis = decode_field(arg, 32, 47, 'signed') * 0.001; + return 6; + }, + }, + ]; + } + if (port === 100) { + decoder = [ + { + key: [0x00], + fn(arg) { + decoded_data.device_eui = decode_field(arg, 0, 63, 'hexstring'); + return 8; + }, + }, + { + key: [0x01], + fn(arg) { + decoded_data.app_eui = decode_field(arg, 0, 63, 'hexstring'); + return 8; + }, + }, + { + key: [0x02], + fn(arg) { + decoded_data.app_key = decode_field(arg, 0, 127, 'hexstring'); + return 16; + }, + }, + { + key: [0x03], + fn(arg) { + decoded_data.device_address = decode_field(arg, 0, 31, 'hexstring'); + return 4; + }, + }, + { + key: [0x04], + fn(arg) { + decoded_data.network_session_key = decode_field(arg, 0, 127, 'hexstring'); + return 16; + }, + }, + { + key: [0x05], + fn(arg) { + decoded_data.app_session_key = decode_field(arg, 0, 127, 'hexstring'); + return 16; + }, + }, + { + key: [0x10], + fn(arg) { + decoded_data.loramac_join_mode = decode_field(arg, 7, 7, 'unsigned'); + return 2; + }, + }, + { + key: [0x11], + fn(arg) { + decoded_data.loramac_opts_confirm_mode = decode_field(arg, 8, 8, 'unsigned'); + decoded_data.loramac_opts_sync_word = decode_field(arg, 9, 9, 'unsigned'); + decoded_data.loramac_opts_duty_cycle = decode_field(arg, 10, 10, 'unsigned'); + decoded_data.loramac_opts_adr = decode_field(arg, 11, 11, 'unsigned'); + return 2; + }, + }, + { + key: [0x12], + fn(arg) { + decoded_data.loramac_dr_tx_dr_number = decode_field(arg, 0, 3, 'unsigned'); + decoded_data.loramac_dr_tx_tx_power_number = decode_field(arg, 8, 11, 'unsigned'); + return 2; + }, + }, + { + key: [0x13], + fn(arg) { + decoded_data.loramac_rx2_frequency = decode_field(arg, 0, 31, 'unsigned'); + decoded_data.loramac_rx2_dr_number = decode_field(arg, 32, 39, 'unsigned'); + return 5; + }, + }, + { + key: [0x20], + fn(arg) { + decoded_data.seconds_per_core_tick = decode_field(arg, 0, 31, 'unsigned'); + return 4; + }, + }, + { + key: [0x21], + fn(arg) { + decoded_data.tick_per_battery = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x24], + fn(arg) { + decoded_data.tick_per_accelerometer = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x25], + fn(arg) { + decoded_data.tick_per_ble_default = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x26], + fn(arg) { + decoded_data.tick_per_ble_stillness = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x27], + fn(arg) { + decoded_data.tick_per_ble_mobility = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x28], + fn(arg) { + decoded_data.tick_per_temperature = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x2A], + fn(arg) { + decoded_data.mode_reed_event_type = decode_field(arg, 7, 7, 'unsigned'); + decoded_data.mode_battery_voltage_report = decode_field(arg, 8, 8, 'unsigned'); + decoded_data.mode_acceleration_vector_report = decode_field(arg, 9, 9, 'unsigned'); + decoded_data.mode_temperature_report = decode_field(arg, 10, 10, 'unsigned'); + decoded_data.mode_ble_report = decode_field(arg, 11, 11, 'unsigned'); + return 2; + }, + }, + { + key: [0x2B], + fn(arg) { + decoded_data.event_type1_m_value = decode_field(arg, 0, 3, 'unsigned'); + decoded_data.event_type1_n_value = decode_field(arg, 4, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x2C], + fn(arg) { + decoded_data.event_type2_t_value = decode_field(arg, 0, 3, 'unsigned'); + return 1; + }, + }, + { + key: [0x40], + fn(arg) { + decoded_data.accelerometer_xaxis_enabled = decode_field(arg, 0, 0, 'unsigned'); + decoded_data.accelerometer_yaxis_enabled = decode_field(arg, 1, 1, 'unsigned'); + decoded_data.accelerometer_zaxis_enabled = decode_field(arg, 2, 2, 'unsigned'); + return 1; + }, + }, + { + key: [0x41], + fn(arg) { + decoded_data.sensitivity_accelerometer_sample_rate = decode_field(arg, 0, 2, 'unsigned') * 1; + switch (decoded_data.sensitivity_accelerometer_sample_rate) { + case 1: + decoded_data.sensitivity_accelerometer_sample_rate = 1; + break; + case 2: + decoded_data.sensitivity_accelerometer_sample_rate = 10; + break; + case 3: + decoded_data.sensitivity_accelerometer_sample_rate = 25; + break; + case 4: + decoded_data.sensitivity_accelerometer_sample_rate = 50; + break; + case 5: + decoded_data.sensitivity_accelerometer_sample_rate = 100; + break; + case 6: + decoded_data.sensitivity_accelerometer_sample_rate = 200; + break; + case 7: + decoded_data.sensitivity_accelerometer_sample_rate = 400; + break; + default: // invalid value + decoded_data.sensitivity_accelerometer_sample_rate = 0; + break; + } + + decoded_data.sensitivity_accelerometer_measurement_range = decode_field(arg, 4, 5, 'unsigned') * 1; + switch (decoded_data.sensitivity_accelerometer_measurement_range) { + case 0: + decoded_data.sensitivity_accelerometer_measurement_range = 2; + break; + case 1: + decoded_data.sensitivity_accelerometer_measurement_range = 4; + break; + case 2: + decoded_data.sensitivity_accelerometer_measurement_range = 8; + break; + case 3: + decoded_data.sensitivity_accelerometer_measurement_range = 16; + break; + default: + decoded_data.sensitivity_accelerometer_measurement_range = 0; + } + return 1; + }, + }, + { + key: [0x42], + fn(arg) { + decoded_data.acceleration_alarm_threshold_count = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x43], + fn(arg) { + decoded_data.acceleration_alarm_threshold_period = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x44], + fn(arg) { + decoded_data.acceleration_alarm_threshold = decode_field(arg, 0, 15, 'unsigned') * 0.001; + return 2; + }, + }, + { + key: [0x45], + fn(arg) { + decoded_data.acceleration_alarm_grace_period = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x46], + fn(arg) { + decoded_data.accelerometer_tx_report_periodic_enabled = decode_field(arg, 0, 0, 'unsigned'); + decoded_data.accelerometer_tx_report_alarm_enabled = decode_field(arg, 1, 1, 'unsigned'); + return 1; + }, + }, + { + key: [0x50], + fn(arg) { + decoded_data.ble_mode = decode_field(arg, 7, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x51], + fn(arg) { + decoded_data.ble_scan_interval = decode_field(arg, 0, 15, 'unsigned') * 0.001; + return 2; + }, + }, + { + key: [0x52], + fn(arg) { + decoded_data.ble_scan_window = decode_field(arg, 0, 15, 'unsigned') * 0.001; + return 2; + }, + }, + { + key: [0x53], + fn(arg) { + decoded_data.ble_scan_duration = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x54], + fn(arg) { + decoded_data.ble_reported_devices = decode_field(arg, 0, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x60], + fn(arg) { + decoded_data.temperature_sample_period_idle = decode_field(arg, 0, 31, 'unsigned'); + return 4; + }, + }, + { + key: [0x61], + fn(arg) { + decoded_data.temperature_sample_period_active = decode_field(arg, 0, 31, 'unsigned'); + return 4; + }, + }, + { + key: [0x62], + fn(arg) { + decoded_data.temperature_threshold_high = decode_field(arg, 0, 7, 'unsigned'); + decoded_data.temperature_threshold_low = decode_field(arg, 8, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x63], + fn(arg) { + decoded_data.temperature_threshold_enabled = decode_field(arg, 0, 0, 'unsigned'); + return 1; + }, + }, + { + key: [0x71], + fn(arg) { + decoded_data.firmware_version_app_major_version = decode_field(arg, 0, 7, 'unsigned'); + decoded_data.firmware_version_app_minor_version = decode_field(arg, 8, 15, 'unsigned'); + decoded_data.firmware_version_app_revision = decode_field(arg, 16, 23, 'unsigned'); + decoded_data.firmware_version_loramac_major_version = decode_field(arg, 24, 31, 'unsigned'); + decoded_data.firmware_version_loramac_minor_version = decode_field(arg, 32, 39, 'unsigned'); + decoded_data.firmware_version_loramac_revision = decode_field(arg, 40, 47, 'unsigned'); + decoded_data.firmware_version_region = decode_field(arg, 48, 55, 'unsigned'); + return 7; + }, + }, + ]; + } + + if (port === 25) { + decoder = [ + { + key: [0x0A], + fn(arg) { + // RSSI to beacons + let count = 0; + for (let i = 0; i < arg.length * 8; i += 7 * 8) { + dev_id = decode_field(arg, i, i + 6 * 8 - 1, 'hexstring'); + decoded_data[dev_id] = decode_field(arg, i + 6 * 8, i + 7 * 8 - 1, 'signed'); + count += 7; + } + return count; + }, + }, + ]; + } + + bytes = convertToUint8Array(bytes); + decoded_data.raw = JSON.stringify(byteToArray(bytes)); + decoded_data.port = port; + + for (let bytes_left = bytes.length; bytes_left > 0;) { + let found = false; + for (let i = 0; i < decoder.length; i++) { + const item = decoder[i]; + const key = item.key; + const keylen = key.length; + header = slice(bytes, 0, keylen); + // Header in the data matches to what we expect + if (is_equal(header, key)) { + const f = item.fn; + consumed = f(slice(bytes, keylen, bytes.length)) + keylen; + bytes_left -= consumed; + bytes = slice(bytes, consumed, bytes.length); + found = true; + break; + } + } + if (found) { + continue; + } + // Unable to decode -- headers are not as expected, send raw payload to the application! + decoded_data = {}; + decoded_data.raw = JSON.stringify(byteToArray(bytes)); + decoded_data.port = port; + return decoded_data; + } + + // Converts value to unsigned + function to_uint(x) { + return x >>> 0; + } + + // Checks if two arrays are equal + function is_equal(arr1, arr2) { + if (arr1.length != arr2.length) { + return false; + } + for (let i = 0; i != arr1.length; i++) { + if (arr1[i] != arr2[i]) { + return false; + } + } + return true; + } + + function byteToArray(byteArray) { + arr = []; + for (let i = 0; i < byteArray.length; i++) { + arr.push(byteArray[i]); + } + return arr; + } + + function convertToUint8Array(byteArray) { + arr = []; + for (let i = 0; i < byteArray.length; i++) { + arr.push(to_uint(byteArray[i]) & 0xff); + } + return arr; + } + + function toHexString(byteArray) { + const arr = []; + for (let i = 0; i < byteArray.length; ++i) { + arr.push((`0${(byteArray[i] & 0xFF).toString(16)}`).slice(-2)); + } + return arr.join(''); + } + + return decoded_data; +} + + +// 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'); +const port = payload.find(x => x.variable === 'port' || x.variable === 'fport'); + + +if (payload_raw) { + // Get a unique serie for the incoming data. + const { value, time } = payload_raw; + let { serie } = payload_raw; + serie = new Date().getTime(); + + + // Parse the payload_raw to JSON format (it comes in a String format) + + if (value) { + payload = payload.concat(toTagoFormat(Decoder(Buffer.from(value.replace(/ /g, ''), 'hex'), Number(port.value)), serie)); + } +} diff --git a/decoders/connector/tektelic/cold-room/assets/logo.png b/decoders/connector/tektelic/cold-room/assets/logo.png new file mode 100644 index 00000000..c41b4f6b Binary files /dev/null and b/decoders/connector/tektelic/cold-room/assets/logo.png differ diff --git a/decoders/connector/tektelic/cold-room/connector.jsonc b/decoders/connector/tektelic/cold-room/connector.jsonc new file mode 100644 index 00000000..9a9dbdfd --- /dev/null +++ b/decoders/connector/tektelic/cold-room/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Cold Room", + "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/tektelic/cold-room/description.md b/decoders/connector/tektelic/cold-room/description.md new file mode 100644 index 00000000..b50877a3 --- /dev/null +++ b/decoders/connector/tektelic/cold-room/description.md @@ -0,0 +1 @@ +Temperature and Humidity Sensor over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/cold-room/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/cold-room/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..ee2d9301 --- /dev/null +++ b/decoders/connector/tektelic/cold-room/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "TEKTELIC’s TUNDRA Sensor is the ideal solution for maintaining optimal and consistent temperatures within cold rooms and refrigerated areas. Applications include food and pharmaceutical storage, where safeguarding the integrity, quality, and safety of products is vital. The device can be implemented within cold storage such as fridges, coolers, cold rooms and even freezers with minimal impact on battery life or radio signal strength.\n\n**General Specifications**\n* Operational Temperature: -40°C to +60°C\n* Ingress Protection: IP67\n* Size: 47 x 43 x 36 mm\n* Battery: C Cell\n\n\n\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-swisscom/v1.0.0/payload.js", + "../../../../network/lorawan-senra/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/tektelic/cold-room/v1.0.0/payload.js b/decoders/connector/tektelic/cold-room/v1.0.0/payload.js new file mode 100644 index 00000000..b8e43cf4 --- /dev/null +++ b/decoders/connector/tektelic/cold-room/v1.0.0/payload.js @@ -0,0 +1,188 @@ +/* eslint-disable no-nested-ternary */ +/* eslint-disable no-bitwise */ +/* eslint-disable no-plusplus */ +function Decoder(bytes, port) { + const decoded = []; + if (port === 10) { + for (let i = 0; i < bytes.length; ) { + const channel = bytes[i++]; + const type = bytes[i++]; + // battery voltage + if (channel === 0x00 && type === 0xff) { + decoded.push({ variable: "battery_voltage", value: bytes.readInt16BE(i) * 0.01, unit: "V" }); + i += 2; + } + // external connector: digital input state + else if (channel === 0x0e && type === 0x00) { + if (bytes.readUInt8(i) !== 0xff && bytes.readUInt8(i) !== 0x00) { + return [{ variable: "parser_error", value: "Parser error: digital input state only accepts 0x00 or 0xFF" }]; + } + decoded.push({ variable: "extconnector_state", value: bytes[i++] === 0xff ? "open-circuited" : "short-circuited" }); + } + // external connector: digital input count + else if (channel === 0x0f && type === 0x04) { + decoded.push({ variable: "extconnector_count", value: bytes.readUInt16BE(i) }); + i += 2; + } + // mcu temperature + else if (channel === 0x0b && type === 0x67) { + decoded.push({ variable: "mcu_temperature", value: bytes.readInt16BE(i) * 0.1, unit: "°C" }); + i += 2; + } + // ambient temperature + else if (channel === 0x03 && type === 0x67) { + decoded.push({ variable: "ambient_temperature", value: bytes.readInt16BE(i) * 0.1, unit: "°C" }); + i += 2; + } + // ambient rh + else if (channel === 0x04 && type === 0x68) { + decoded.push({ variable: "relative_humidity", value: bytes.readUInt8(i++) * 0.5, unit: "%" }); + } + } + } else if (port === 100) { + for (let i = 0; i < bytes.length; ) { + const address = bytes[i++]; + + if (address === 0x10) { + decoded.push({ variable: "loramac_join_mode", value: bytes.readUInt16BE(i) >> 15 }); + i += 2; + } else if (address === 0x11) { + const opts = bytes.readUInt16BE(i); + decoded.push({ variable: "loramac_opts_confirm_mode", value: opts & 0x01 }); + decoded.push({ variable: "loramac_opts_sync_word", value: (opts >> 1) & 0x01 }); + decoded.push({ variable: "loramac_opts_duty_cycle", value: (opts >> 2) & 0x01 }); + decoded.push({ variable: "loramac_opts_adr", value: (opts >> 3) & 0x01 }); + i += 2; + } else if (address === 0x12) { + const dr_tx = bytes.readUInt16BE(i); + decoded.push({ variable: "loramac_dr_tx_dr_number", value: (dr_tx >> 8) & 0x0f }); + decoded.push({ variable: "loramac_dr_tx_tx_power_number", value: dr_tx & 0x0f }); + i += 2; + } else if (address === 0x13) { + const rx2 = bytes.slice(i, i + 5); + decoded.push({ variable: "loramac_rx2_frequency", value: rx2.readUInt32BE(0), unit: "Hz" }); + decoded.push({ variable: "loramac_rx2_dr_number", value: rx2.readUInt8(4) }); + i += 5; + } else if (address === 0x20) { + const ticks = bytes.readUInt32BE(i); + i += 4; + if ((ticks > 0 && ticks < 60) || ticks > 86400) { + continue; + } + decoded.push({ variable: "seconds_per_core_tick", value: ticks, unit: "sec" }); + } else if (address === 0x21) { + decoded.push({ variable: "tick_per_battery", value: bytes.readUInt16BE(i) }); + i += 2; + } else if (address === 0x22) { + decoded.push({ variable: "tick_per_ambient_temperature", value: bytes.readUInt16BE(i) }); + i += 2; + } else if (address === 0x23) { + decoded.push({ variable: "tick_per_relative_humidity", value: bytes.readUInt16BE(i) }); + i += 2; + } else if (address === 0x27) { + decoded.push({ variable: "tick_per_mcu_temperature", value: bytes.readUInt16BE(i) }); + i += 2; + } else if (address === 0x39) { + const idle = bytes.readUInt32BE(i); + i += 4; + if (idle < 30 || idle > 86400) { + continue; + } + decoded.push({ variable: "temperature_relative_humidity_sample_period_idle", value: idle, unit: "sec" }); + } else if (address === 0x3a) { + const active = bytes.readUInt32BE(i); + if (active < 30 || active > 86400) { + continue; + } + decoded.push({ variable: "temperature_relative_humidity_sample_period_active", value: active, unit: "sec" }); + i += 4; + } else if (address === 0x3b) { + const high = bytes.readInt8(i++); + const low = bytes.readInt8(i++); + if (high <= low) { + continue; + } + decoded.push({ variable: "ambient_temperature_threshold_high", value: high, unit: "°C" }); + decoded.push({ variable: "ambient_temperature_threshold_low", value: low, unit: "°C" }); + } else if (address === 0x3c) { + decoded.push({ variable: "ambient_temperature_threshold_enabled", value: bytes.readUInt8(i++) & 0x01 }); + } else if (address === 0x3d) { + const high = bytes.readUInt8(i++); + const low = bytes.readUInt8(i++); + if (high <= low) { + continue; + } + decoded.push({ variable: "relative_humidity_threshold_high", value: high, unit: "%" }); + decoded.push({ variable: "relative_humidity_threshold_low", value: low, unit: "%" }); + } else if (address === 0x3e) { + decoded.push({ variable: "relative_humidity_threshold_enabled", value: bytes.readUInt8(i++) & 0x01 }); + } else if (address === 0x40) { + const mcu_temperature_period = bytes.readUInt32BE(i); + i += 4; + if (mcu_temperature_period < 30 || mcu_temperature_period > 86400) { + continue; + } + decoded.push({ variable: "mcu_temperature_sample_period_idle", value: mcu_temperature_period, unit: "sec" }); + } else if (address === 0x41) { + const mcu_temperature_period = bytes.readUInt32BE(i); + i += 4; + if (mcu_temperature_period < 30 || mcu_temperature_period > 86400) { + continue; + } + decoded.push({ variable: "mcu_temperature_sample_period_active", value: mcu_temperature_period, unit: "sec" }); + } else if (address === 0x42) { + const high = bytes.readInt8(i++); + const low = bytes.readInt8(i++); + if (high <= low) { + continue; + } + decoded.push({ variable: "mcu_temperature_threshold_high", value: high, unit: "°C" }); + decoded.push({ variable: "mcu_temperature_threshold_low", value: low, unit: "°C" }); + } else if (address === 0x43) { + decoded.push({ variable: "mcu_temperature_threshold_enabled", value: bytes.readUInt8(i++) & 0x01 }); + } else if (address === 0x70) { + const flash = bytes.readUInt16BE(i); + decoded.push({ variable: "write_to_flash_app_configuration", value: (flash >> 14) & 0x01 }); + decoded.push({ variable: "write_to_flash_lora_configuration", value: (flash >> 13) & 0x01 }); + decoded.push({ variable: "write_to_flash_restart_sensor", value: flash & 0x01 }); + i += 2; + } else if (address === 0x71) { + decoded.push({ variable: "firmware_version_app_major_version", value: bytes.readUInt8(i++) }); + decoded.push({ variable: "firmware_version_app_minor_version", value: bytes.readUInt8(i++) }); + decoded.push({ variable: "firmware_version_app_revision", value: bytes.readUInt8(i++) }); + decoded.push({ variable: "firmware_version_loramac_major_version", value: bytes.readUInt8(i++) }); + decoded.push({ variable: "firmware_version_loramac_minor_version", value: bytes.readUInt8(i++) }); + decoded.push({ variable: "firmware_version_loramac_revision", value: bytes.readUInt8(i++) }); + decoded.push({ variable: "firmware_version_region", value: bytes.readUInt8(i++) }); + } else if (address === 0x72) { + const reset = bytes[i++]; + if (reset !== 0x0a && reset !== 0xb0 && reset !== 0xba) { + continue; + } + decoded.push({ + variable: "configuration_factory_reset", + value: `0x${reset.toString(16)}`, + }); + } + } + } + return decoded; +} +/* +let payload = [ + // { variable: "payload", value: "0367000a046828" }, + { variable: "payload", value: "3b46d8" }, + { variable: "port", value: 100 }, +]; +*/ +const data = payload.find((x) => x.variable === "payload_raw" || x.variable === "payload" || x.variable === "data"); +const port = payload.find((x) => x.variable === "port" || x.variable === "fport"); + +if (data && port) { + const buffer = Buffer.from(data.value, "hex"); + const serie = new Date().getTime(); + payload = Decoder(buffer, port.value); + payload = payload.map((x) => ({ ...x, serie })); +} + +// console.log(payload); diff --git a/decoders/connector/tektelic/finch-indoor-panic-button/assets/logo.png b/decoders/connector/tektelic/finch-indoor-panic-button/assets/logo.png new file mode 100644 index 00000000..1abd0d39 Binary files /dev/null and b/decoders/connector/tektelic/finch-indoor-panic-button/assets/logo.png differ diff --git a/decoders/connector/tektelic/finch-indoor-panic-button/connector.jsonc b/decoders/connector/tektelic/finch-indoor-panic-button/connector.jsonc new file mode 100644 index 00000000..1769f2ea --- /dev/null +++ b/decoders/connector/tektelic/finch-indoor-panic-button/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Finch Indoor Panic Button", + "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/tektelic/finch-indoor-panic-button/description.md b/decoders/connector/tektelic/finch-indoor-panic-button/description.md new file mode 100644 index 00000000..b51bfee5 --- /dev/null +++ b/decoders/connector/tektelic/finch-indoor-panic-button/description.md @@ -0,0 +1 @@ +Instant Panic Event Notification over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/finch-indoor-panic-button/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/finch-indoor-panic-button/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..de9ad3ac --- /dev/null +++ b/decoders/connector/tektelic/finch-indoor-panic-button/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "TEKTELIC’s SafeAlert Sensor is a compact, mobile, wireless BLE panic button that transmits an emergency signal from any location. SafeAlert is ideal for lone workers, seniors, and children to signal for help in an emergency situation. When triggered, this device instantly sends an SOS message containing location details to designated support staff or security personnel to ensure a rapid response in the event of a panic situation. This device combines the long-range, low-power communication capabilities of LoRaWAN™ with the universal availability and reliability of Bluetooth Low Energy (BLE) to provide real-time location information.\n\n**Specifications**\n* Operational Temperature: 0°C to +60°C\n* 5-year battery life\n* Ingress Protection: IP67\n* Size: 70 x 22 x 25 mm\n* Battery: AA LTC\n* RF Power: 15 dBm\n* RF Sensitivity: up to -137dBm (SF12, 125kHz)\n* ISM Band: All Global ISM Bands\n* Antenna: Internal\n* LoRa Class: Class A", + "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-ttittn-v3/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-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/tektelic/finch-indoor-panic-button/v1.0.0/payload.js b/decoders/connector/tektelic/finch-indoor-panic-button/v1.0.0/payload.js new file mode 100644 index 00000000..6599555d --- /dev/null +++ b/decoders/connector/tektelic/finch-indoor-panic-button/v1.0.0/payload.js @@ -0,0 +1,111 @@ +/** + * ## PORTS ## + * - 10: Sensor data from the MCU, battery gauge and accelerometer + * - 25: Report discovered BLE devices + * - 100: Response to configuration and control commands + */ + +function Decoder(bytes, port) { + const decoded = []; + if (port === 10) { + for (let i = 0; i < bytes.length; ) { + const channel = bytes[i++]; + const type = bytes[i++]; + + // battery status - 1byte unsigned + if (channel === 0x00 && type === 0xba) { + const value = bytes.readUInt8(i++); + decoded.push({ variable: "battery_status_life", value: 2.5 + (value & 0x7f) * 0.01, unit: "V" }); + } + // acceleration alarm status - 1byte unsigned + else if (channel === 0x00 && type === 0x00) { + const value = bytes.readUInt8(i++); + decoded.push({ variable: "acceleration_alarm", value: value === 0x00 ? 0 : 1 }); + } + // acceleration vector = 6bytes signed + else if (channel === 0x00 && type === 0x71) { + const valuex = bytes.readInt16BE(i); + i += 2; + const valuey = bytes.readInt16BE(i); + i += 2; + const valuez = bytes.readInt16BE(i); + i += 2; + decoded.push({ variable: "acceleration_vector_xaxis", value: valuex * 0.001, unit: "g" }); + decoded.push({ variable: "acceleration_vector_yaxis", value: valuey * 0.001, unit: "g" }); + decoded.push({ variable: "acceleration_vector_zaxis", value: valuez * 0.001, unit: "g" }); + } + // mcu temperature - 2b signed + else if (channel === 0x00 && type === 0x67) { + const value = bytes.readInt16BE(i); + i += 2; + decoded.push({ variable: "temperature", value: value * 0.1, unit: "°C" }); + } + } + } else if (port === 25) { + const type = bytes[0]; + switch (type) { + case 0x0a: + decoded.push({ variable: "message_type", value: "basic mode" }); + for (let i = 1, j = 1; i < bytes.length; j++) { + const bd_addr = bytes.readUIntBE(i, 6); + i += 6; + const rssi = bytes.readInt8(i++); + decoded.push({ variable: `bd_addr${j}`, value: bd_addr.toString(16).toUpperCase().padStart(12, "0") }); + decoded.push({ variable: `rssi${j}`, value: rssi }); + } + break; + case 0xb0: + decoded.push({ variable: "message_type", value: "whitelisting mode, range 0" }); + for (let i = 1, j = 1; i < bytes.length; j++) { + const bd_addr_lap = bytes.readUIntBE(i, 3); + i += 3; + const rssi = bytes.readInt8(i++); + decoded.push({ variable: `bd_addr_lap${j}`, value: bd_addr_lap.toString(16).toUpperCase().padStart(6, "0") }); + decoded.push({ variable: `rssi${j}`, value: rssi }); + } + break; + case 0xb1: + decoded.push({ variable: "message_type", value: "whitelisting mode, range 1" }); + for (let i = 1, j = 1; i < bytes.length; j++) { + const bd_addr_lap = bytes.readUIntBE(i, 3); + i += 3; + const rssi = bytes.readInt8(i++); + decoded.push({ variable: `bd_addr_lap${j}`, value: bd_addr_lap.toString(16).toUpperCase().padStart(6, "0") }); + decoded.push({ variable: `rssi${j}`, value: rssi }); + } + break; + case 0xb2: + decoded.push({ variable: "message_type", value: "whitelisting mode, range 2" }); + for (let i = 1, j = 1; i < bytes.length; j++) { + const bd_addr_lap = bytes.readUIntBE(i, 3); + i += 3; + const rssi = bytes.readInt8(i++); + decoded.push({ variable: `bd_addr_lap${j}`, value: bd_addr_lap.toString(16).toUpperCase().padStart(6, "0") }); + decoded.push({ variable: `rssi${j}`, value: rssi }); + } + break; + case 0xb3: + decoded.push({ variable: "message_type", value: "whitelisting mode, range 3" }); + for (let i = 1, j = 1; i < bytes.length; j++) { + const bd_addr_lap = bytes.readUIntBE(i, 3); + i += 3; + const rssi = bytes.readInt8(i++); + decoded.push({ variable: `bd_addr_lap${j}`, value: bd_addr_lap.toString(16).toUpperCase().padStart(6, "0") }); + decoded.push({ variable: `rssi${j}`, value: rssi }); + } + break; + default: + break; + } + } + return decoded; +} + +const data = payload.find((x) => x.variable === "payload" || x.variable === "payload_raw" || x.variable === "data"); +const port = payload.find((x) => x.variable === "port" || x.variable === "fport"); + +if (data) { + const serie = data.serie || new Date().getTime(); + const bytes = Buffer.from(data.value, "hex"); + payload = payload.concat(Decoder(bytes, Number(port.value))).map((x) => ({ ...x, serie })); +} diff --git a/decoders/connector/tektelic/flux-smart-ac-outlet/assets/logo.png b/decoders/connector/tektelic/flux-smart-ac-outlet/assets/logo.png new file mode 100644 index 00000000..2be6b40a Binary files /dev/null and b/decoders/connector/tektelic/flux-smart-ac-outlet/assets/logo.png differ diff --git a/decoders/connector/tektelic/flux-smart-ac-outlet/connector.jsonc b/decoders/connector/tektelic/flux-smart-ac-outlet/connector.jsonc new file mode 100644 index 00000000..7bfc8d3c --- /dev/null +++ b/decoders/connector/tektelic/flux-smart-ac-outlet/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic FLUX Smart AC Outlet", + "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/tektelic/flux-smart-ac-outlet/description.md b/decoders/connector/tektelic/flux-smart-ac-outlet/description.md new file mode 100644 index 00000000..d31077de --- /dev/null +++ b/decoders/connector/tektelic/flux-smart-ac-outlet/description.md @@ -0,0 +1 @@ +Electrical Outlet over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/flux-smart-ac-outlet/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/flux-smart-ac-outlet/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..67b5fecc --- /dev/null +++ b/decoders/connector/tektelic/flux-smart-ac-outlet/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "The TEKTELIC FLUX Smart AC Outlet is an advanced solution for Smart Home and Office automation. It allows for both manual and remote operation of a 120VAC, 60Hz switch via a LoRaWAN® network, offering significant improvements in power consumption management and safety. Features include tamper resistance, easy installation, and integration with comprehensive LoRaWAN® systems. Additionally, the FLUX enables real-time monitoring of energy usage, facilitating energy savings and enhanced security across various settings, making it an ideal choice for energy-conscious users looking to modernize their living or workspace.", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-everynet/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-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-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/tektelic/flux-smart-ac-outlet/v1.0.0/payload.js b/decoders/connector/tektelic/flux-smart-ac-outlet/v1.0.0/payload.js new file mode 100644 index 00000000..5541c604 --- /dev/null +++ b/decoders/connector/tektelic/flux-smart-ac-outlet/v1.0.0/payload.js @@ -0,0 +1,146 @@ +/* This is an example code for Everynet Parser. +** Everynet send several parameters to TagoIO. The job of this parse is to convert all these parameters into a TagoIO format. +** One of these parameters is the payload of your device. We find it too and apply the appropriate sensor parse. +** +** 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": "everynet_payload", "value": "{ \"params\": { \"payload\": \"0109611395\" } }" }] +** +** The ignore_vars variable in this code should be used to ignore variables +** from the device that you don't want. +*/ +// payload example from the documentation '00FE0001518000006A50000000'; | '0080CB750180415C0081FF'; +// Add ignorable variables in this array. +const ignore_vars = ['device_addr', 'port', 'duplicate', 'network', 'packet_hash', 'application', 'device', 'packet_id']; + + +/** + * Convert an object to TagoIO object format. + * Can be used in two ways: + * toTagoFormat({ myvariable: myvalue , anothervariable: anothervalue... }) + * toTagoFormat({ myvariable: { value: myvalue, unit: 'C', metadata: { color: 'green' }} , anothervariable: anothervalue... }) + * + * @param {Object} object_item Object containing key and value. + * @param {String} serie Serie for the variables + * @param {String} prefix Add a prefix to the variables name + */ +function toTagoFormat(object_item, serie, prefix = '') { + const result = []; + for (const key in object_item) { + if (ignore_vars.includes(key)) continue; + + if (typeof object_item[key] == 'object') { + result.push({ + variable: object_item[key].variable || `${prefix}${key}`, + value: object_item[key].value, + serie: object_item[key].serie || serie, + metadata: object_item[key].metadata, + location: object_item[key].location, + unit: object_item[key].unit, + }); + } else { + result.push({ + variable: `${prefix}${key}`, + value: object_item[key], + serie, + }); + } + } + + return result; +} + +/** + * In the solutions params is where usually latitude and longitude for your antenna signal comes from. + * @param {Object} solutions gateway object from everynet + * @param {String|Number} serie serie for the variables + */ +function transformSolutionParam(solutions, serie) { + let to_tago = []; + for (const s of solutions) { + let convert_json = {}; + convert_json.base_location = { value: `${s.lat}, ${s.lng}`, location: { lat: s.lat, lng: s.lng } }; + delete s.lat; + delete s.lng; + + convert_json = { ...convert_json, ...s }; + to_tago = to_tago.concat(toTagoFormat(convert_json, serie)); + } + + return to_tago; +} + +function parsePayload(payload_raw, serie) { + // If your device is sending something different than hex, like base64, just specify it bellow. + const buffer = Buffer.from(payload_raw, 'hex'); + + const data = {}; + let msg_byte; + let channel; + for (let x = 1; x < buffer.length;) { + switch (buffer[x]) { + case 254: // FE + msg_byte = Buffer.from(buffer.slice(x += 1, x += 4)); + data.elapsed_time = { value: msg_byte.readInt32BE(0), unit: 'seconds', serie }; + msg_byte = Buffer.from(buffer.slice(x, x += 4)); + data.energy_consumed = { value: msg_byte.readInt32BE(0), unit: 'W-h', serie }; + x += 1; + break; + case 0: // 00 + msg_byte = Buffer.from(buffer.slice(x += 1)); + data.energy_consumption_meter_status = { value: msg_byte.readUInt8(0) === 0 ? 'idde' : 'active', serie }; + x += 1; + break; + case 128: // 80 + channel = buffer[x - 1]; + msg_byte = Buffer.from(buffer.slice(x += 1, x += 2)); + if (channel === 0) { + data.real_power = { value: msg_byte.readInt16BE(0) * 0.1, unit: 'W', serie }; + } else { + data.apparent_power = { value: msg_byte.readInt16BE(0) * 0.1, unit: 'W', serie }; + } + x += 1; + break; + case 129: // 81 + msg_byte = Buffer.from(buffer.slice(x += 1)); + data.power_factor = { value: msg_byte.readUInt8(0) < 254 ? msg_byte.readUInt8(0) : 'N/A', unit: 'W', serie }; + x += 1; + break; + case 116: // 74 + msg_byte = Buffer.from(buffer.slice(x += 1, x += 2)); + data.voltmeter = { value: msg_byte.readInt16BE(0) * 0.1, unit: 'Vrms', serie }; + x += 1; + break; + case 117: // 75 + msg_byte = Buffer.from(buffer.slice(x += 1, x += 2)); + data.ammeter = { value: msg_byte.readInt16BE(0) * 0.1, unit: 'Arms', serie }; + x += 1; + break; + case 1: // 01 + msg_byte = Buffer.from(buffer.slice(x += 1)); + data.relay_status = { value: msg_byte.readUInt8(0) === 0 ? 'Open' : 'Closed', serie }; + x += 1; + break; + default: + } + } + + return data; +} + +const everynet_payload = payload.find(x => x.variable === 'payload' || x.variable === 'payload_raw' || x.variable === 'data'); +if (everynet_payload) { + // Get a unique serie for the incoming data. + const serie = everynet_payload.serie || new Date().getTime(); + let vars_to_tago = []; + try { + vars_to_tago = vars_to_tago.concat(toTagoFormat(parsePayload(everynet_payload.value, serie), serie)); + } catch (e) { + // Catch any error in the parse code and send to parse_error variable. + vars_to_tago = vars_to_tago.concat({ variable: 'parse_error', value: e.message || e }); + } + + payload = payload.concat(vars_to_tago); +} diff --git a/decoders/connector/tektelic/industrial-transceiver/assets/logo.png b/decoders/connector/tektelic/industrial-transceiver/assets/logo.png new file mode 100644 index 00000000..2f6e5e2a Binary files /dev/null and b/decoders/connector/tektelic/industrial-transceiver/assets/logo.png differ diff --git a/decoders/connector/tektelic/industrial-transceiver/connector.jsonc b/decoders/connector/tektelic/industrial-transceiver/connector.jsonc new file mode 100644 index 00000000..fb907c28 --- /dev/null +++ b/decoders/connector/tektelic/industrial-transceiver/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Industrial Transceiver", + "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/tektelic/industrial-transceiver/description.md b/decoders/connector/tektelic/industrial-transceiver/description.md new file mode 100644 index 00000000..62b63392 --- /dev/null +++ b/decoders/connector/tektelic/industrial-transceiver/description.md @@ -0,0 +1 @@ + Supports MODBUS® or CAN bus via serial (RS232, RS422, RS485) over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/industrial-transceiver/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/industrial-transceiver/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..56223a9f --- /dev/null +++ b/decoders/connector/tektelic/industrial-transceiver/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "##\nThe Industrial Transceiver and Sensor is used to interface automation and control instrumentation to a LoRaWAN™ network.\n\nIt provides a full suite of serial connectivity options including RS232, RS422 and RS485 to support applications such as MODBUS® or CAN bus over LoRa™.\n\nIt also includes three analog and digital inputs as well as two switched outputs that can be used to control industrial equipment. It comes in an IP67 rated enclosure to support most industrial installation requirements.\n\n", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-everynet/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-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-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/tektelic/industrial-transceiver/v1.0.0/payload.js b/decoders/connector/tektelic/industrial-transceiver/v1.0.0/payload.js new file mode 100644 index 00000000..23496031 --- /dev/null +++ b/decoders/connector/tektelic/industrial-transceiver/v1.0.0/payload.js @@ -0,0 +1,105 @@ +/* eslint-disable no-plusplus */ +const ignore_vars = []; + +/** + * 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 buffer = Buffer.from(payload_raw, "hex"); + + const data = []; + for (let i = 0; i < buffer.length; ) { + const channel = buffer[i++]; + const type = buffer[i++]; + + // battery voltage + if (channel === 0x00 && type === 0xff) { + data.push({ variable: "battery_voltage", value: buffer.readInt16BE(i) * 0.01, unit: "V" }); + i += 2; + } + // Output 1 + else if (channel === 0x01 && type === 0x01) { + const value = buffer[i++]; + if (value !== 0x00 && value !== 0xff) { + return [{ variable: "parser_error", value: "Parser Error: Output 1 value must be 0x00 or 0xFF" }]; + } + data.push({ variable: "output_1", value: value === 0xff ? "Closed" : "Opened" }); + } + // Output 2 + else if (channel === 0x02 && type === 0x01) { + const value = buffer[i++]; + if (value !== 0x00 && value !== 0xff) { + return [{ variable: "parser_error", value: "Parser Error: Output 2 value must be 0x00 or 0xFF" }]; + } + data.push({ variable: "output_2", value: value === 0xff ? "Closed" : "Opened" }); + } + // Temperature + else if (channel === 0x03 && type === 0x67) { + data.push({ variable: "temperature", value: buffer.readInt16BE(i) * 0.1, unit: "°C" }); + i += 2; + } + // Input 1 State + else if (channel === 0x05 && type === 0x00) { + const value = buffer[i++]; + if (value !== 0x00 && value !== 0x01) { + return [{ variable: "parser_error", value: "Parser Error: Input value must be 0x00 or 0x01" }]; + } + data.push({ variable: "input_1", value: value === 0x01 ? "Opened" : "Closed" }); + } + // Input 2 + else if (channel === 0x06 && type === 0x02) { + data.push({ variable: "input_2", value: buffer.readUInt16BE(i), unit: "uA" }); + i += 2; + } + // Input 3 + else if (channel === 0x07 && type === 0x02) { + data.push({ variable: "input_3", value: buffer.readUInt16BE(i), unit: "mV" }); + i += 2; + } + // Input 1 Count + else if (channel === 0x08 && type === 0x04) { + data.push({ variable: "input_1_count", value: buffer.readUInt16BE(i), unit: "count" }); + i += 2; + } + // MCU temperature + else if (channel === 0x09 && type === 0x67) { + data.push({ variable: "mcu_temperature", value: buffer.readInt16BE(i) * 0.1, unit: "°C" }); + i += 2; + } + } + return data; + } catch (e) { + // eslint-disable-next-line no-console + console.log(e); + // Return the variable parse_error for debugging. + return [{ variable: "parser_error", value: e.message }]; + } +} + +// let payload = [{ variable: "payload", value: "0500ff08040005" }]; + +// Remove unwanted variables. +payload = payload.filter((x) => !ignore_vars.includes(x.variable)); + +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 } = payload_raw; + + let { serie } = payload_raw; + + if (!serie) { + serie = new Date().getTime(); + } + + // Parse the payload_raw to JSON format (it comes in a String format) + if (value) { + payload = payload.concat(parsePayload(value)).map((x) => ({ ...x, serie })); + } +} + +// console.log(payload); diff --git a/decoders/connector/tektelic/mulch-temperature-sensor/assets/logo.png b/decoders/connector/tektelic/mulch-temperature-sensor/assets/logo.png new file mode 100644 index 00000000..9553b4ef Binary files /dev/null and b/decoders/connector/tektelic/mulch-temperature-sensor/assets/logo.png differ diff --git a/decoders/connector/tektelic/mulch-temperature-sensor/connector.jsonc b/decoders/connector/tektelic/mulch-temperature-sensor/connector.jsonc new file mode 100644 index 00000000..f61993b4 --- /dev/null +++ b/decoders/connector/tektelic/mulch-temperature-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Mulch 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/tektelic/mulch-temperature-sensor/description.md b/decoders/connector/tektelic/mulch-temperature-sensor/description.md new file mode 100644 index 00000000..ac963cd9 --- /dev/null +++ b/decoders/connector/tektelic/mulch-temperature-sensor/description.md @@ -0,0 +1 @@ +Mulch temperature monitoring sensor over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/mulch-temperature-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/mulch-temperature-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..12b6550c --- /dev/null +++ b/decoders/connector/tektelic/mulch-temperature-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "* Commercial Mulch/Compost Storage\n* Precision Agriculture\n* Hydroponics and Smart Greenhouses ", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-actility/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-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/tektelic/mulch-temperature-sensor/v1.0.0/payload.js b/decoders/connector/tektelic/mulch-temperature-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..6d02f7fb --- /dev/null +++ b/decoders/connector/tektelic/mulch-temperature-sensor/v1.0.0/payload.js @@ -0,0 +1,108 @@ +/* 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 = []; + +function toTagoFormat(object_item, serie, prefix = "") { + const result = []; + for (const key in object_item) { + if (ignore_vars.includes(key)) continue; + + if (typeof object_item[key] === "object") { + result.push({ + variable: object_item[key].variable || `${prefix}${key}`.toLowerCase(), + value: object_item[key].value, + serie: object_item[key].serie || serie, + metadata: object_item[key].metadata, + location: object_item[key].location, + unit: object_item[key].unit, + }); + } else { + result.push({ + variable: `${prefix}${key}`.toLowerCase(), + value: object_item[key], + serie, + }); + } + } + + return result; +} + +/** + * 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 Decoder(payload_raw) { + try { + // If your device is sending something different than hex, like base64, just specify it bellow. + const buffer = Buffer.from(payload_raw, "hex"); + const data = []; + for (let x = 1; x < buffer.length; x++) { + // console.log(buffer.slice(x, x+1).toString('hex')) + if (buffer[x] === 0x00) { + x += 2; + data.push({ variable: "battery_voltage", value: buffer.readInt16BE(x++) * 0.01, unit: "V" }); + } else if (buffer[x] === 0x01) { + x += 2; + data.push({ variable: "output_1", value: buffer.readInt8(x) === -1 ? "Closed" : "Opened" }); + } else if (buffer[x] === 0x02) { + x += 2; + data.push({ variable: "output_2", value: buffer.readInt8(x) === -1 ? "Closed" : "Opened" }); + } else if (buffer[x] === 0x03) { + x += 2; + data.push({ variable: "temperature", value: buffer.readInt16BE(x++) * 0.1, unit: "°C" }); + } else if (buffer[x] === 0x04) { + x += 2; + data.push({ variable: "humidity", value: buffer.readUInt8(x) / 2, unit: "% RH" }); + } else if (buffer[x] === 0x05) { + x += 2; + data.push({ variable: "input_1", value: buffer.readInt8(x) === -1 ? "Closed" : "Opened" }); + } else if (buffer[x] === 0x06) { + x += 2; + data.push({ variable: "input_2", value: buffer.readUInt16LE(x++), unit: "uA" }); + } else if (buffer[x] === 0x07) { + x += 2; + data.push({ variable: "input_3", value: buffer.readUInt16LE(x++), unit: "mV" }); + } else if (buffer[x] === 0x08) { + x += 2; + data.push({ variable: "input_1_count", value: buffer.readUInt16BE(x++), unit: "mV" }); + } + } + return data; + } catch (e) { + console.log(e); + // Return the variable parse_error for debugging. + return [{ variable: "parse_error", value: e.message }]; + } +} +// let payload = [{ variable: 'payload', value: '0104 68 2A 03 67 FF FF 00 FF 01 2C'.replace(/ /g, '') }]; +// let payload = [{ variable: "payload", value: "0104 68 14 05 00 FF 08 04 00 05".replace(/ /g, "") }]; + +// 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, time } = payload_raw; + let { serie } = payload_raw; + serie = new Date().getTime(); + + // Parse the payload_raw to JSON format (it comes in a String format) + + if (value) { + payload = payload.concat(toTagoFormat(Decoder(Buffer.from(value.replace(/ /g, ""), "hex")), serie)); + } +} \ No newline at end of file diff --git a/decoders/connector/tektelic/orca-industrial-gps-tracker/assets/logo.png b/decoders/connector/tektelic/orca-industrial-gps-tracker/assets/logo.png new file mode 100644 index 00000000..7894d910 Binary files /dev/null and b/decoders/connector/tektelic/orca-industrial-gps-tracker/assets/logo.png differ diff --git a/decoders/connector/tektelic/orca-industrial-gps-tracker/connector.jsonc b/decoders/connector/tektelic/orca-industrial-gps-tracker/connector.jsonc new file mode 100644 index 00000000..423d45b0 --- /dev/null +++ b/decoders/connector/tektelic/orca-industrial-gps-tracker/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Orca Industrial GPS Tracker", + "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/tektelic/orca-industrial-gps-tracker/description.md b/decoders/connector/tektelic/orca-industrial-gps-tracker/description.md new file mode 100644 index 00000000..fba3eaf7 --- /dev/null +++ b/decoders/connector/tektelic/orca-industrial-gps-tracker/description.md @@ -0,0 +1 @@ +GPS Tracker for Industrial Asset Management over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/orca-industrial-gps-tracker/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/orca-industrial-gps-tracker/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..939cad9d --- /dev/null +++ b/decoders/connector/tektelic/orca-industrial-gps-tracker/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "The TEKTELIC ORCA Industrial GPS Asset Tracker is a robust device designed for comprehensive asset management in industrial settings, both indoors and outdoors. \nKey Features of the Orca are:\n * Multi-Technology Integration: Utilizes LoRaWAN, GPS, and BLE for comprehensive tracking capabilities.\n * Operates effectively in extreme temperatures ranging from -40°C to +85°C, and boasts an IP67 rating for water and dust resistance.\n * Long Battery Life (up you 5 years): Ensures prolonged operation without frequent maintenance, suitable for remote and challenging environments.\n * Versatile Deployment: Ideal for tracking various assets like equipment, vehicles, and containers in diverse industrial contexts.\n * Real-Time Monitoring: Offers continuous location updates and status reports, enhancing asset security and operational efficiency.\n * Seamless Network Integration: Easily integrates with existing LoRaWAN networks, facilitating scalable and efficient asset management solutions.\n", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-actility/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-ttittn-v3/v1.0.0/payload.js", + "../../../../network/lorawan-helium/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/tektelic/orca-industrial-gps-tracker/v1.0.0/payload.js b/decoders/connector/tektelic/orca-industrial-gps-tracker/v1.0.0/payload.js new file mode 100644 index 00000000..d54dc2ba --- /dev/null +++ b/decoders/connector/tektelic/orca-industrial-gps-tracker/v1.0.0/payload.js @@ -0,0 +1,827 @@ +/* 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: '00 67 FF FF 01 BA 63 00 06 00 00 71 02 44 00 46 03 3E 00 88 3E 50 B0 BC 02 2D 60 08 2A'.replace(/ /g, '') }] + ** + ** The ignore_vars variable in this code should be used to ignore variables + ** from the device that you don't want. + */ + +// let payload = [ +// { +// id: "63e54c27ce6cbd0009d71abe", +// origin: "63c072304659fa0009e46eb5", +// serie: "1675971623799", +// time: "2023-02-09T19:40:23.805Z", +// value: 10, +// variable: "port", +// group: "1675971623799", +// }, +// { +// id: "63c0c6304659fa0009ebf840", +// origin: "63c072304659fa0009e46eb5", +// serie: "1673578032830", +// time: "2023-01-13T02:47:12.836Z", +// value: "00881baf0743f80faa01bd", +// variable: "payload", +// device: "63c072304659fa0009e46eb5", +// group: "1673578032830", +// }, +// ]; +const ignore_vars = []; + +let beacon_decoder = device.params.find((x) => x.key === "beacon_decoder"); +beacon_decoder = beacon_decoder && beacon_decoder.value === "simple" ? "simple" : null; + +function toTagoFormat(object_item, serie, prefix = "") { + const result = []; + for (const key in object_item) { + if (ignore_vars.includes(key)) continue; + + if (typeof object_item[key] === "object") { + result.push({ + variable: object_item[key].variable || `${prefix}${key}`.toLowerCase(), + value: object_item[key].value, + serie: object_item[key].serie || serie, + metadata: object_item[key].metadata, + location: object_item[key].location, + unit: object_item[key].unit, + }); + } else { + result.push({ + variable: `${prefix}${key}`.toLowerCase(), + value: object_item[key], + serie, + }); + } + } + + return result; +} + +function transformLatLngToLocation(fields, serie, prefix = "") { + if ((fields.latitude && fields.longitude) || (fields.lat && fields.lng)) { + const lat = fields.lat || fields.latitude; + const lng = fields.lng || fields.longitude; + + // Change to TagoIO format. + // Using variable "location". + const variable = { + variable: `${prefix}location`, + value: `${lat}, ${lng}`, + location: { lat, lng }, + serie, + }; + + delete fields.latitude; // remove latitude so it's not parsed later + delete fields.longitude; // remove latitude so it's not parsed later + delete fields.lat; // remove latitude so it's not parsed later + delete fields.lng; // remove latitude so it's not parsed later + + return variable; + } + return null; +} + +function Decoder(bytes, port) { + // bytes - Array of bytes + function slice(a, f, t) { + const res = []; + for (let i = 0; i < t - f; i++) { + res[i] = a[f + i]; + } + return res; + } + + function extract_bytes(chunk, start_bit, end_bit) { + const total_bits = end_bit - start_bit + 1; + const total_bytes = total_bits % 8 === 0 ? to_uint(total_bits / 8) : to_uint(total_bits / 8) + 1; + const offset_in_byte = start_bit % 8; + const end_bit_chunk = total_bits % 8; + const arr = new Array(total_bytes); + for (byte = 0; byte < total_bytes; ++byte) { + const chunk_idx = to_uint(start_bit / 8) + byte; + let lo = chunk[chunk_idx] >> offset_in_byte; + let hi = 0; + if (byte < total_bytes - 1) { + hi = (chunk[chunk_idx + 1] & ((1 << offset_in_byte) - 1)) << (8 - offset_in_byte); + } else if (end_bit_chunk !== 0) { + // Truncate last bits + lo &= (1 << end_bit_chunk) - 1; + } + arr[byte] = hi | lo; + } + return arr; + } + + function apply_data_type(bytes, data_type) { + output = 0; + if (data_type === "unsigned") { + for (var i = 0; i < bytes.length; ++i) { + output = to_uint(output << 8) | bytes[i]; + } + return output; + } + if (data_type === "signed") { + for (var i = 0; i < bytes.length; ++i) { + output = (output << 8) | bytes[i]; + } + // Convert to signed, based on value size + if (output > Math.pow(2, 8 * bytes.length - 1)) { + output -= Math.pow(2, 8 * bytes.length); + } + return output; + } + if (data_type === "bool") { + return !(bytes[0] === 0); + } + if (data_type === "hexstring") { + return toHexString(bytes); + } + // Incorrect data type + return null; + } + + function decode_field(chunk, start_bit, end_bit, data_type) { + chunk_size = chunk.length; + if (end_bit >= chunk_size * 8) { + return null; // Error: exceeding boundaries of the chunk + } + if (end_bit < start_bit) { + return null; // Error: invalid input + } + arr = extract_bytes(chunk, start_bit, end_bit); + return apply_data_type(arr, data_type); + } + + let decoded_data = {}; + let decoder = []; + + if (port === 10) { + decoder = [ + { + key: [0x00, 0xba], + fn(arg) { + decoded_data.battery1_status_life = 2.5 + decode_field(arg, 0, 6, "unsigned") * 0.01; + decoded_data.battery1_status_eos_alert = decode_field(arg, 7, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x01, 0xba], + fn(arg) { + decoded_data.battery2_status_life = 2.5 + decode_field(arg, 0, 6, "unsigned") * 0.01; + decoded_data.battery2_status_eos_alert = decode_field(arg, 7, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x02, 0xba], + fn(arg) { + decoded_data.battery3_status_life = 2.5 + decode_field(arg, 0, 6, "unsigned") * 0.01; + decoded_data.battery3_status_eos_alert = decode_field(arg, 7, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x00, 0x85], + fn(arg) { + const year = decode_field(arg, 0, 15, "unsigned"); + const month = decode_field(arg, 16, 23, "unsigned"); + const day = decode_field(arg, 24, 31, "unsigned"); + const hour = decode_field(arg, 32, 39, "unsigned"); + const minute = decode_field(arg, 40, 47, "unsigned"); + const second = decode_field(arg, 48, 55, "unsigned"); + decoded_data.timestamp = `${year}-${month}-${day} ${hour}:${minute}:${second}`; + + return 7; + }, + }, + { + key: [0x00, 0x92], + fn(arg) { + const ground_speed = decode_field(arg, 0, 15, "unsigned") * 0.1; + decoded_data.ground_speed = Math.round(ground_speed * 10) / 10; + return 2; + }, + }, + { + key: [0x00, 0x88], + fn(arg) { + decoded_data.latitude = decode_field(arg, 0, 23, "signed") * 0.0000125; + decoded_data.longitude = decode_field(arg, 24, 55, "signed") * 0.0000001; + decoded_data.altitude = decode_field(arg, 56, 71, "signed") * 0.5; + return 9; + }, + }, + { + key: [0x00, 0x04], + fn(arg) { + decoded_data.fsm_state = decode_field(arg, 0, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x00, 0x06], + fn(arg) { + decoded_data.fix_status_time = decode_field(arg, 0, 0, "unsigned"); + decoded_data.fix_status_position = decode_field(arg, 1, 1, "unsigned"); + return 1; + }, + }, + { + key: [0x01, 0x06], + fn(arg) { + decoded_data.geofence_status_one = decode_field(arg, 4, 7, "unsigned"); + decoded_data.geofence_status_two = decode_field(arg, 0, 3, "unsigned"); + decoded_data.geofence_status_three = decode_field(arg, 12, 15, "unsigned"); + decoded_data.geofence_status_four = decode_field(arg, 8, 11, "unsigned"); + return 2; + }, + }, + { + key: [0x00, 0x67], + fn(arg) { + decoded_data.mcu_temperature = decode_field(arg, 0, 15, "signed") * 0.1; + return 2; + }, + }, + { + key: [0x00, 0x00], + fn(arg) { + decoded_data.acceleration_alarm = decode_field(arg, 0, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x00, 0x71], + fn(arg) { + decoded_data.acceleration_xaxis = decode_field(arg, 0, 15, "signed") * 0.001; + decoded_data.acceleration_yaxis = decode_field(arg, 16, 31, "signed") * 0.001; + decoded_data.acceleration_zaxis = decode_field(arg, 32, 47, "signed") * 0.001; + return 6; + }, + }, + ]; + } + if (port === 100) { + decoder = [ + { + key: [0x00], + fn(arg) { + decoded_data.device_eui = decode_field(arg, 0, 63, "hexstring"); + return 8; + }, + }, + { + key: [0x01], + fn(arg) { + decoded_data.app_eui = decode_field(arg, 0, 63, "hexstring"); + return 8; + }, + }, + { + key: [0x02], + fn(arg) { + decoded_data.app_key = decode_field(arg, 0, 127, "hexstring"); + return 16; + }, + }, + { + key: [0x03], + fn(arg) { + decoded_data.device_address = decode_field(arg, 0, 31, "hexstring"); + return 4; + }, + }, + { + key: [0x04], + fn(arg) { + decoded_data.network_session_key = decode_field(arg, 0, 127, "hexstring"); + return 16; + }, + }, + { + key: [0x05], + fn(arg) { + decoded_data.app_session_key = decode_field(arg, 0, 127, "hexstring"); + return 16; + }, + }, + { + key: [0x10], + fn(arg) { + decoded_data.loramac_join_mode = decode_field(arg, 7, 7, "unsigned"); + return 2; + }, + }, + { + key: [0x11], + fn(arg) { + decoded_data.loramac_opts_confirm_mode = decode_field(arg, 8, 8, "unsigned"); + decoded_data.loramac_opts_sync_word = decode_field(arg, 9, 9, "unsigned"); + decoded_data.loramac_opts_duty_cycle = decode_field(arg, 10, 10, "unsigned"); + decoded_data.loramac_opts_adr = decode_field(arg, 11, 11, "unsigned"); + return 2; + }, + }, + { + key: [0x12], + fn(arg) { + decoded_data.loramac_dr_tx_dr_number = decode_field(arg, 0, 3, "unsigned"); + decoded_data.loramac_dr_tx_tx_power_number = decode_field(arg, 8, 11, "unsigned"); + return 2; + }, + }, + { + key: [0x13], + fn(arg) { + decoded_data.loramac_rx2_frequency = decode_field(arg, 0, 31, "unsigned"); + decoded_data.loramac_rx2_dr_number = decode_field(arg, 32, 39, "unsigned"); + return 5; + }, + }, + { + key: [0x20], + fn(arg) { + decoded_data.seconds_per_core_tick = decode_field(arg, 0, 31, "unsigned"); + return 4; + }, + }, + { + key: [0x21], + fn(arg) { + decoded_data.tick_per_battery = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x22], + fn(arg) { + decoded_data.tick_per_gps_stillness = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x23], + fn(arg) { + decoded_data.tick_per_gps_mobility = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x24], + fn(arg) { + decoded_data.tick_per_accelerometer = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x25], + fn(arg) { + decoded_data.tick_per_ble_default = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x26], + fn(arg) { + decoded_data.tick_per_ble_stillness = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x27], + fn(arg) { + decoded_data.tick_per_ble_mobility = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x28], + fn(arg) { + decoded_data.tick_per_temperature = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x2a], + fn(arg) { + decoded_data.mode_reed_event_type = decode_field(arg, 7, 7, "unsigned"); + decoded_data.mode_battery_voltage_report = decode_field(arg, 8, 8, "unsigned"); + decoded_data.mode_acceleration_vector_report = decode_field(arg, 9, 9, "unsigned"); + decoded_data.mode_temperature_report = decode_field(arg, 10, 10, "unsigned"); + decoded_data.mode_ble_report = decode_field(arg, 11, 11, "unsigned"); + return 2; + }, + }, + { + key: [0x2b], + fn(arg) { + decoded_data.event_type1_m_value = decode_field(arg, 0, 3, "unsigned"); + decoded_data.event_type1_n_value = decode_field(arg, 4, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x2c], + fn(arg) { + decoded_data.event_type2_t_value = decode_field(arg, 0, 3, "unsigned"); + return 1; + }, + }, + { + key: [0x30], + fn(arg) { + decoded_data.gps_enabled = decode_field(arg, 7, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x31], + fn(arg) { + decoded_data.speed_threshold_mobility = decode_field(arg, 0, 7, "unsigned") * 0.1; + decoded_data.speed_threshold_stillness = decode_field(arg, 8, 15, "unsigned") * 0.1; + return 2; + }, + }, + { + key: [0x32], + fn(arg) { + decoded_data.average_speed_count_mobility = decode_field(arg, 4, 7, "unsigned"); + decoded_data.average_speed_count_stillness = decode_field(arg, 0, 3, "unsigned"); + return 1; + }, + }, + { + key: [0x33], + fn(arg) { + decoded_data.tx_utc_report_enabled = decode_field(arg, 0, 0, "unsigned"); + decoded_data.tx_coordinate_report_enabled = decode_field(arg, 1, 1, "unsigned"); + decoded_data.tx_fsm_report_enabled = decode_field(arg, 2, 2, "unsigned"); + return 1; + }, + }, + { + key: [0x34], + fn(arg) { + decoded_data.geofence_definition1_latitude = decode_field(arg, 0, 23, "unsigned") * 0.0000125; + decoded_data.geofence_definition1_longitude = decode_field(arg, 24, 47, "unsigned") * 0.000025; + decoded_data.geofence_definition1_radius = decode_field(arg, 48, 63, "unsigned") * 10; + return 8; + }, + }, + { + key: [0x35], + fn(arg) { + decoded_data.geofence_definition2_latitude = decode_field(arg, 0, 23, "unsigned") * 0.0000125; + decoded_data.geofence_definition2_longitude = decode_field(arg, 24, 47, "unsigned") * 0.000025; + decoded_data.geofence_definition2_radius = decode_field(arg, 48, 63, "unsigned") * 10; + return 8; + }, + }, + { + key: [0x36], + fn(arg) { + decoded_data.geofence_definition3_latitude = decode_field(arg, 0, 23, "unsigned") * 0.0000125; + decoded_data.geofence_definition3_longitude = decode_field(arg, 24, 47, "unsigned") * 0.000025; + decoded_data.geofence_definition3_radius = decode_field(arg, 48, 63, "unsigned") * 10; + return 8; + }, + }, + { + key: [0x37], + fn(arg) { + decoded_data.geofence_definition4_latitude = decode_field(arg, 0, 23, "unsigned") * 0.0000125; + decoded_data.geofence_definition4_longitude = decode_field(arg, 24, 47, "unsigned") * 0.000025; + decoded_data.geofence_definition4_radius = decode_field(arg, 48, 63, "unsigned") * 10; + return 8; + }, + }, + { + key: [0x40], + fn(arg) { + decoded_data.accelerometer_xaxis_enabled = decode_field(arg, 0, 0, "unsigned"); + decoded_data.accelerometer_yaxis_enabled = decode_field(arg, 1, 1, "unsigned"); + decoded_data.accelerometer_zaxis_enabled = decode_field(arg, 2, 2, "unsigned"); + return 1; + }, + }, + { + key: [0x41], + fn(arg) { + // } + decoded_data.sensitivity_accelerometer_sample_rate = decode_field(arg, 0, 2, "unsigned") * 1; + switch (decoded_data.sensitivity_accelerometer_sample_rate) { + case 1: + decoded_data.sensitivity_accelerometer_sample_rate = 1; + break; + case 2: + decoded_data.sensitivity_accelerometer_sample_rate = 10; + break; + case 3: + decoded_data.sensitivity_accelerometer_sample_rate = 25; + break; + case 4: + decoded_data.sensitivity_accelerometer_sample_rate = 50; + break; + case 5: + decoded_data.sensitivity_accelerometer_sample_rate = 100; + break; + case 6: + decoded_data.sensitivity_accelerometer_sample_rate = 200; + break; + case 7: + decoded_data.sensitivity_accelerometer_sample_rate = 400; + break; + default: + // invalid value + decoded_data.sensitivity_accelerometer_sample_rate = 0; + break; + } + + decoded_data.sensitivity_accelerometer_measurement_range = decode_field(arg, 4, 5, "unsigned") * 1; + switch (decoded_data.sensitivity_accelerometer_measurement_range) { + case 0: + decoded_data.sensitivity_accelerometer_measurement_range = 2; + break; + case 1: + decoded_data.sensitivity_accelerometer_measurement_range = 4; + break; + case 2: + decoded_data.sensitivity_accelerometer_measurement_range = 8; + break; + case 3: + decoded_data.sensitivity_accelerometer_measurement_range = 16; + break; + default: + decoded_data.sensitivity_accelerometer_measurement_range = 0; + } + return 1; + }, + }, + { + key: [0x42], + fn(arg) { + decoded_data.acceleration_alarm_threshold_count = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x43], + fn(arg) { + decoded_data.acceleration_alarm_threshold_period = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x44], + fn(arg) { + decoded_data.acceleration_alarm_threshold = decode_field(arg, 0, 15, "unsigned") * 0.001; + return 2; + }, + }, + { + key: [0x45], + fn(arg) { + decoded_data.acceleration_alarm_grace_period = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x46], + fn(arg) { + decoded_data.accelerometer_tx_report_periodic_enabled = decode_field(arg, 0, 0, "unsigned"); + decoded_data.accelerometer_tx_report_alarm_enabled = decode_field(arg, 1, 1, "unsigned"); + return 1; + }, + }, + { + key: [0x50], + fn(arg) { + decoded_data.ble_mode = decode_field(arg, 7, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x51], + fn(arg) { + decoded_data.ble_scan_interval = decode_field(arg, 0, 15, "unsigned") * 0.001; + return 2; + }, + }, + { + key: [0x52], + fn(arg) { + decoded_data.ble_scan_window = decode_field(arg, 0, 15, "unsigned") * 0.001; + return 2; + }, + }, + { + key: [0x53], + fn(arg) { + decoded_data.ble_scan_duration = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x54], + fn(arg) { + decoded_data.ble_reported_devices = decode_field(arg, 0, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x60], + fn(arg) { + decoded_data.temperature_sample_period_idle = decode_field(arg, 0, 31, "unsigned"); + return 4; + }, + }, + { + key: [0x61], + fn(arg) { + decoded_data.temperature_sample_period_active = decode_field(arg, 0, 31, "unsigned"); + return 4; + }, + }, + { + key: [0x62], + fn(arg) { + decoded_data.temperature_threshold_high = decode_field(arg, 0, 7, "unsigned"); + decoded_data.temperature_threshold_low = decode_field(arg, 8, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x63], + fn(arg) { + decoded_data.temperature_threshold_enabled = decode_field(arg, 0, 0, "unsigned"); + return 1; + }, + }, + { + key: [0x71], + fn(arg) { + decoded_data.firmware_version_app_major_version = decode_field(arg, 0, 7, "unsigned"); + decoded_data.firmware_version_app_minor_version = decode_field(arg, 8, 15, "unsigned"); + decoded_data.firmware_version_app_revision = decode_field(arg, 16, 23, "unsigned"); + decoded_data.firmware_version_loramac_major_version = decode_field(arg, 24, 31, "unsigned"); + decoded_data.firmware_version_loramac_minor_version = decode_field(arg, 32, 39, "unsigned"); + decoded_data.firmware_version_loramac_revision = decode_field(arg, 40, 47, "unsigned"); + decoded_data.firmware_version_region = decode_field(arg, 48, 55, "unsigned"); + return 7; + }, + }, + ]; + } + + if (port === 25) { + decoder = [ + { + key: [0x0a], + fn(arg) { + // RSSI to beacons + let count = 0; + for (let i = 0; i < arg.length * 8; i += 7 * 8) { + dev_id = decode_field(arg, i, i + 6 * 8 - 1, "hexstring"); + if (beacon_decoder === "simple") { + dev_id = `${dev_id}_beacon`; + } + decoded_data[dev_id] = decode_field(arg, i + 6 * 8, i + 7 * 8 - 1, "signed"); + count += 7; + } + return count; + }, + }, + ]; + } + + bytes = convertToUint8Array(bytes); + + for (let bytes_left = bytes.length; bytes_left > 0; ) { + let found = false; + for (let i = 0; i < decoder.length; i++) { + const item = decoder[i]; + const { key } = item; + const keylen = key.length; + header = slice(bytes, 0, keylen); + // Header in the data matches to what we expect + if (is_equal(header, key)) { + const f = item.fn; + consumed = f(slice(bytes, keylen, bytes.length)) + keylen; + bytes_left -= consumed; + bytes = slice(bytes, consumed, bytes.length); + found = true; + break; + } + } + if (found) { + continue; + } + // Unable to decode -- headers are not as expected, send raw payload to the application! + decoded_data = {}; + decoded_data.raw = JSON.stringify(byteToArray(bytes)); + decoded_data.port = port; + return decoded_data; + } + + // Converts value to unsigned + function to_uint(x) { + return x >>> 0; + } + + // Checks if two arrays are equal + function is_equal(arr1, arr2) { + if (arr1.length != arr2.length) { + return false; + } + for (let i = 0; i != arr1.length; i++) { + if (arr1[i] != arr2[i]) { + return false; + } + } + return true; + } + + function byteToArray(byteArray) { + arr = []; + for (let i = 0; i < byteArray.length; i++) { + arr.push(byteArray[i]); + } + return arr; + } + + function convertToUint8Array(byteArray) { + arr = []; + for (let i = 0; i < byteArray.length; i++) { + arr.push(to_uint(byteArray[i]) & 0xff); + } + return arr; + } + + function toHexString(byteArray) { + const arr = []; + for (let i = 0; i < byteArray.length; ++i) { + arr.push(`0${(byteArray[i] & 0xff).toString(16)}`.slice(-2)); + } + return arr.join(""); + } + + return decoded_data; +} + +// 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" || x.variable === "payload_hex"); +const port = payload.find((x) => x.variable === "port" || x.variable === "fport" || x.variable === "FPort"); + +if (payload_raw) { + // Get a unique serie for the incoming data. + const { value, time } = payload_raw; + let { serie } = payload_raw; + serie = new Date().getTime(); + + // Parse the payload_raw to JSON format (it comes in a String format) + + if (value) { + let decoded = Decoder(Buffer.from(value.replace(/ /g, ""), "hex"), Number(port.value)); + + // Apply simplified beacon version; + const beacons = Object.keys(decoded).filter((x) => x.includes("_beacon")); + if (beacons.length) { + decoded.beacons = { + variable: "beacons", + value: beacons.map((x) => `${x.replace("_beacon", "")}: ${decoded[x]}`).join("; "), + metadata: beacons.reduce((final, x) => { + final[x.replace("_beacon", "")] = decoded[x]; + return final; + }, {}), + }; + + decoded = Object.keys(decoded).reduce((final, x) => { + if (!x.includes("_beacon")) { + final[x] = decoded[x]; + } + + return final; + }, {}); + } + + const loc = transformLatLngToLocation(decoded, serie); + if (loc) { + payload = payload.concat(loc); + } + + const timesTamp = decoded.timestamp; + // Parse the payload_raw to JSON format (it comes in a String format) + payload = payload.concat(toTagoFormat(decoded, serie)).map((x) => ({ ...x, serie, time: timesTamp })); + } +} diff --git a/decoders/connector/tektelic/panic-button-sensor/assets/logo.png b/decoders/connector/tektelic/panic-button-sensor/assets/logo.png new file mode 100644 index 00000000..a7fc9881 Binary files /dev/null and b/decoders/connector/tektelic/panic-button-sensor/assets/logo.png differ diff --git a/decoders/connector/tektelic/panic-button-sensor/connector.jsonc b/decoders/connector/tektelic/panic-button-sensor/connector.jsonc new file mode 100644 index 00000000..0eadfc5d --- /dev/null +++ b/decoders/connector/tektelic/panic-button-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Panic Button 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/tektelic/panic-button-sensor/description.md b/decoders/connector/tektelic/panic-button-sensor/description.md new file mode 100644 index 00000000..729797fd --- /dev/null +++ b/decoders/connector/tektelic/panic-button-sensor/description.md @@ -0,0 +1 @@ +Instant Panic Event Notification Trigger over BLE or LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/panic-button-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/panic-button-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..b93e2733 --- /dev/null +++ b/decoders/connector/tektelic/panic-button-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "* 10 Year Battery Life (2x Transmissions per Day)\n* Lone Worker, Senior Care, Children Monitoring\n* Bluetooth Low Energy Functionality\n* Data Transmission via LoRaWAN® and BLE\n* All Global ISM Bands\n* Small Form Factor for Diverse Deployments\n* Easily Integrated into LoRaWAN™ Networks\n* Internal Antenna\n* Sleek, Comfortable Design", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-actility/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-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-senra/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", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/tektelic/panic-button-sensor/v1.0.0/payload.js b/decoders/connector/tektelic/panic-button-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..989c7938 --- /dev/null +++ b/decoders/connector/tektelic/panic-button-sensor/v1.0.0/payload.js @@ -0,0 +1,623 @@ +/* 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": "00 BA 70 00 00 00" }] +** +** The ignore_vars variable in this code should be used to ignore variables +** from the device that you don't want. +*/ + +// let payload = [{ variable: 'payload', value: '00 BA 70 00 00 00'.replace(/ /g, '') }]; + +// Add ignorable variables in this array. + +const ignore_vars = []; + + +function toTagoFormat(object_item, serie, prefix = '') { + const result = []; + + for (const key in object_item) { + if (ignore_vars.includes(key)) continue; + + + if (typeof object_item[key] === 'object') { + result.push({ + + variable: object_item[key].variable || `${prefix}${key}`.toLowerCase(), + + value: object_item[key].value, + + serie: object_item[key].serie || serie, + + metadata: object_item[key].metadata, + + location: object_item[key].location, + + unit: object_item[key].unit, + + }); + } else { + result.push({ + + variable: `${prefix}${key}`.toLowerCase(), + + value: object_item[key], + + serie, + + }); + } + } + + + return result; +} + +function Decoder(bytes, port) { // bytes - Array of bytes (signed) + function slice(a, f, t) { + const res = []; + for (let i = 0; i < t - f; i++) { + res[i] = a[f + i]; + } + return res; + } + + function extract_bytes(chunk, start_bit, end_bit) { + const total_bits = end_bit - start_bit + 1; + const total_bytes = total_bits % 8 === 0 ? to_uint(total_bits / 8) : to_uint(total_bits / 8) + 1; + const offset_in_byte = start_bit % 8; + const end_bit_chunk = total_bits % 8; + const arr = new Array(total_bytes); + for (byte = 0; byte < total_bytes; ++byte) { + const chunk_idx = to_uint(start_bit / 8) + byte; + let lo = chunk[chunk_idx] >> offset_in_byte; + let hi = 0; + if (byte < total_bytes - 1) { + hi = (chunk[chunk_idx + 1] & ((1 << offset_in_byte) - 1)) << (8 - offset_in_byte); + } else if (end_bit_chunk !== 0) { + // Truncate last bits + lo &= ((1 << end_bit_chunk) - 1); + } + arr[byte] = hi | lo; + } + return arr; + } + + function apply_data_type(bytes, data_type) { + output = 0; + if (data_type === 'unsigned') { + for (var i = 0; i < bytes.length; ++i) { + output = (to_uint(output << 8)) | bytes[i]; + } + return output; + } + + if (data_type === 'signed') { + for (var i = 0; i < bytes.length; ++i) { + output = (output << 8) | bytes[i]; + } + // Convert to signed, based on value size + if (output > Math.pow(2, 8 * bytes.length - 1)) { + output -= Math.pow(2, 8 * bytes.length); + } + return output; + } + if (data_type === 'bool') { + return !(bytes[0] === 0); + } + if (data_type === 'hexstring') { + return toHexString(bytes); + } + // Incorrect data type + return null; + } + + function decode_field(chunk, start_bit, end_bit, data_type) { + chunk_size = chunk.length; + if (end_bit >= chunk_size * 8) { + return null; // Error: exceeding boundaries of the chunk + } + if (end_bit < start_bit) { + return null; // Error: invalid input + } + arr = extract_bytes(chunk, start_bit, end_bit); + return apply_data_type(arr, data_type); + } + + decoded_data = {}; + decoder = []; + + if (port === 10) { + decoder = [ + { + key: [0x00, 0xBA], + fn(arg) { + decoded_data.battery_status_life = 2.5 + decode_field(arg, 0, 6, 'unsigned') * 0.01; + decoded_data.battery_status_eos_alert = decode_field(arg, 7, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x00, 0x04], + fn(arg) { + decoded_data.fsm_state = decode_field(arg, 0, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x00, 0x67], + fn(arg) { + decoded_data.mcu_temperature = decode_field(arg, 0, 15, 'signed') * 0.1; + return 2; + }, + }, + { + key: [0x00, 0x00], + fn(arg) { + decoded_data.acceleration_alarm = decode_field(arg, 0, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x00, 0x71], + fn(arg) { + decoded_data.acceleration_xaxis = decode_field(arg, 0, 15, 'signed') * 0.001; + decoded_data.acceleration_yaxis = decode_field(arg, 16, 31, 'signed') * 0.001; + decoded_data.acceleration_zaxis = decode_field(arg, 32, 47, 'signed') * 0.001; + return 6; + }, + }, + ]; + } + if (port === 100) { + decoder = [ + { + key: [0x00], + fn(arg) { + decoded_data.device_eui = decode_field(arg, 0, 63, 'hexstring'); + return 8; + }, + }, + { + key: [0x01], + fn(arg) { + decoded_data.app_eui = decode_field(arg, 0, 63, 'hexstring'); + return 8; + }, + }, + { + key: [0x02], + fn(arg) { + decoded_data.app_key = decode_field(arg, 0, 127, 'hexstring'); + return 16; + }, + }, + { + key: [0x03], + fn(arg) { + decoded_data.device_address = decode_field(arg, 0, 31, 'hexstring'); + return 4; + }, + }, + { + key: [0x04], + fn(arg) { + decoded_data.network_session_key = decode_field(arg, 0, 127, 'hexstring'); + return 16; + }, + }, + { + key: [0x05], + fn(arg) { + decoded_data.app_session_key = decode_field(arg, 0, 127, 'hexstring'); + return 16; + }, + }, + { + key: [0x10], + fn(arg) { + decoded_data.loramac_join_mode = decode_field(arg, 7, 7, 'unsigned'); + return 2; + }, + }, + { + key: [0x11], + fn(arg) { + decoded_data.loramac_opts_confirm_mode = decode_field(arg, 8, 8, 'unsigned'); + decoded_data.loramac_opts_sync_word = decode_field(arg, 9, 9, 'unsigned'); + decoded_data.loramac_opts_duty_cycle = decode_field(arg, 10, 10, 'unsigned'); + decoded_data.loramac_opts_adr = decode_field(arg, 11, 11, 'unsigned'); + return 2; + }, + }, + { + key: [0x12], + fn(arg) { + decoded_data.loramac_dr_tx_dr_number = decode_field(arg, 0, 3, 'unsigned'); + decoded_data.loramac_dr_tx_tx_power_number = decode_field(arg, 8, 11, 'unsigned'); + return 2; + }, + }, + { + key: [0x13], + fn(arg) { + decoded_data.loramac_rx2_frequency = decode_field(arg, 0, 31, 'unsigned'); + decoded_data.loramac_rx2_dr_number = decode_field(arg, 32, 39, 'unsigned'); + return 5; + }, + }, + { + key: [0x20], + fn(arg) { + decoded_data.seconds_per_core_tick = decode_field(arg, 0, 31, 'unsigned'); + return 4; + }, + }, + { + key: [0x21], + fn(arg) { + decoded_data.tick_per_battery = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x24], + fn(arg) { + decoded_data.tick_per_accelerometer = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x25], + fn(arg) { + decoded_data.tick_per_ble_default = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x26], + fn(arg) { + decoded_data.tick_per_ble_stillness = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x27], + fn(arg) { + decoded_data.tick_per_ble_mobility = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x28], + fn(arg) { + decoded_data.tick_per_temperature = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x2A], + fn(arg) { + decoded_data.mode_reed_event_type = decode_field(arg, 7, 7, 'unsigned'); + decoded_data.mode_battery_voltage_report = decode_field(arg, 8, 8, 'unsigned'); + decoded_data.mode_acceleration_vector_report = decode_field(arg, 9, 9, 'unsigned'); + decoded_data.mode_temperature_report = decode_field(arg, 10, 10, 'unsigned'); + decoded_data.mode_ble_report = decode_field(arg, 11, 11, 'unsigned'); + return 2; + }, + }, + { + key: [0x2B], + fn(arg) { + decoded_data.event_type1_m_value = decode_field(arg, 0, 3, 'unsigned'); + decoded_data.event_type1_n_value = decode_field(arg, 4, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x2C], + fn(arg) { + decoded_data.event_type2_t_value = decode_field(arg, 0, 3, 'unsigned'); + return 1; + }, + }, + { + key: [0x40], + fn(arg) { + decoded_data.accelerometer_xaxis_enabled = decode_field(arg, 0, 0, 'unsigned'); + decoded_data.accelerometer_yaxis_enabled = decode_field(arg, 1, 1, 'unsigned'); + decoded_data.accelerometer_zaxis_enabled = decode_field(arg, 2, 2, 'unsigned'); + return 1; + }, + }, + { + key: [0x41], + fn(arg) { + decoded_data.sensitivity_accelerometer_sample_rate = decode_field(arg, 0, 2, 'unsigned') * 1; + switch (decoded_data.sensitivity_accelerometer_sample_rate) { + case 1: + decoded_data.sensitivity_accelerometer_sample_rate = 1; + break; + case 2: + decoded_data.sensitivity_accelerometer_sample_rate = 10; + break; + case 3: + decoded_data.sensitivity_accelerometer_sample_rate = 25; + break; + case 4: + decoded_data.sensitivity_accelerometer_sample_rate = 50; + break; + case 5: + decoded_data.sensitivity_accelerometer_sample_rate = 100; + break; + case 6: + decoded_data.sensitivity_accelerometer_sample_rate = 200; + break; + case 7: + decoded_data.sensitivity_accelerometer_sample_rate = 400; + break; + default: // invalid value + decoded_data.sensitivity_accelerometer_sample_rate = 0; + break; + } + + decoded_data.sensitivity_accelerometer_measurement_range = decode_field(arg, 4, 5, 'unsigned') * 1; + switch (decoded_data.sensitivity_accelerometer_measurement_range) { + case 0: + decoded_data.sensitivity_accelerometer_measurement_range = 2; + break; + case 1: + decoded_data.sensitivity_accelerometer_measurement_range = 4; + break; + case 2: + decoded_data.sensitivity_accelerometer_measurement_range = 8; + break; + case 3: + decoded_data.sensitivity_accelerometer_measurement_range = 16; + break; + default: + decoded_data.sensitivity_accelerometer_measurement_range = 0; + } + return 1; + }, + }, + { + key: [0x42], + fn(arg) { + decoded_data.acceleration_alarm_threshold_count = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x43], + fn(arg) { + decoded_data.acceleration_alarm_threshold_period = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x44], + fn(arg) { + decoded_data.acceleration_alarm_threshold = decode_field(arg, 0, 15, 'unsigned') * 0.001; + return 2; + }, + }, + { + key: [0x45], + fn(arg) { + decoded_data.acceleration_alarm_grace_period = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x46], + fn(arg) { + decoded_data.accelerometer_tx_report_periodic_enabled = decode_field(arg, 0, 0, 'unsigned'); + decoded_data.accelerometer_tx_report_alarm_enabled = decode_field(arg, 1, 1, 'unsigned'); + return 1; + }, + }, + { + key: [0x50], + fn(arg) { + decoded_data.ble_mode = decode_field(arg, 7, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x51], + fn(arg) { + decoded_data.ble_scan_interval = decode_field(arg, 0, 15, 'unsigned') * 0.001; + return 2; + }, + }, + { + key: [0x52], + fn(arg) { + decoded_data.ble_scan_window = decode_field(arg, 0, 15, 'unsigned') * 0.001; + return 2; + }, + }, + { + key: [0x53], + fn(arg) { + decoded_data.ble_scan_duration = decode_field(arg, 0, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x54], + fn(arg) { + decoded_data.ble_reported_devices = decode_field(arg, 0, 7, 'unsigned'); + return 1; + }, + }, + { + key: [0x60], + fn(arg) { + decoded_data.temperature_sample_period_idle = decode_field(arg, 0, 31, 'unsigned'); + return 4; + }, + }, + { + key: [0x61], + fn(arg) { + decoded_data.temperature_sample_period_active = decode_field(arg, 0, 31, 'unsigned'); + return 4; + }, + }, + { + key: [0x62], + fn(arg) { + decoded_data.temperature_threshold_high = decode_field(arg, 0, 7, 'unsigned'); + decoded_data.temperature_threshold_low = decode_field(arg, 8, 15, 'unsigned'); + return 2; + }, + }, + { + key: [0x63], + fn(arg) { + decoded_data.temperature_threshold_enabled = decode_field(arg, 0, 0, 'unsigned'); + return 1; + }, + }, + { + key: [0x71], + fn(arg) { + decoded_data.firmware_version_app_major_version = decode_field(arg, 0, 7, 'unsigned'); + decoded_data.firmware_version_app_minor_version = decode_field(arg, 8, 15, 'unsigned'); + decoded_data.firmware_version_app_revision = decode_field(arg, 16, 23, 'unsigned'); + decoded_data.firmware_version_loramac_major_version = decode_field(arg, 24, 31, 'unsigned'); + decoded_data.firmware_version_loramac_minor_version = decode_field(arg, 32, 39, 'unsigned'); + decoded_data.firmware_version_loramac_revision = decode_field(arg, 40, 47, 'unsigned'); + decoded_data.firmware_version_region = decode_field(arg, 48, 55, 'unsigned'); + return 7; + }, + }, + ]; + } + + if (port === 25) { + decoder = [ + { + key: [0x0A], + fn(arg) { + // RSSI to beacons + let count = 0; + for (let i = 0; i < arg.length * 8; i += 7 * 8) { + dev_id = decode_field(arg, i, i + 6 * 8 - 1, 'hexstring'); + decoded_data[dev_id] = decode_field(arg, i + 6 * 8, i + 7 * 8 - 1, 'signed'); + count += 7; + } + return count; + }, + }, + ]; + } + + bytes = convertToUint8Array(bytes); + decoded_data.raw = JSON.stringify(byteToArray(bytes)); + decoded_data.port = port; + + for (let bytes_left = bytes.length; bytes_left > 0;) { + let found = false; + for (let i = 0; i < decoder.length; i++) { + const item = decoder[i]; + const key = item.key; + const keylen = key.length; + header = slice(bytes, 0, keylen); + // Header in the data matches to what we expect + if (is_equal(header, key)) { + const f = item.fn; + consumed = f(slice(bytes, keylen, bytes.length)) + keylen; + bytes_left -= consumed; + bytes = slice(bytes, consumed, bytes.length); + found = true; + break; + } + } + if (found) { + continue; + } + // Unable to decode -- headers are not as expected, send raw payload to the application! + decoded_data = {}; + decoded_data.raw = JSON.stringify(byteToArray(bytes)); + decoded_data.port = port; + return decoded_data; + } + + // Converts value to unsigned + function to_uint(x) { + return x >>> 0; + } + + // Checks if two arrays are equal + function is_equal(arr1, arr2) { + if (arr1.length != arr2.length) { + return false; + } + for (let i = 0; i != arr1.length; i++) { + if (arr1[i] != arr2[i]) { + return false; + } + } + return true; + } + + function byteToArray(byteArray) { + arr = []; + for (let i = 0; i < byteArray.length; i++) { + arr.push(byteArray[i]); + } + return arr; + } + + function convertToUint8Array(byteArray) { + arr = []; + for (let i = 0; i < byteArray.length; i++) { + arr.push(to_uint(byteArray[i]) & 0xff); + } + return arr; + } + + function toHexString(byteArray) { + const arr = []; + for (let i = 0; i < byteArray.length; ++i) { + arr.push((`0${(byteArray[i] & 0xFF).toString(16)}`).slice(-2)); + } + return arr.join(''); + } + + return decoded_data; +} + + +// 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'); +const port = payload.find(x => x.variable === 'port' || x.variable === 'fport'); + + +if (payload_raw) { + // Get a unique serie for the incoming data. + const { value, time } = payload_raw; + let { serie } = payload_raw; + serie = new Date().getTime(); + + + // Parse the payload_raw to JSON format (it comes in a String format) + + if (value) { + payload = payload.concat(toTagoFormat(Decoder(Buffer.from(value.replace(/ /g, ''), 'hex'), Number(port.value)), serie)); + } +} diff --git a/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/assets/logo.png b/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/assets/logo.png new file mode 100644 index 00000000..6b487fcc Binary files /dev/null and b/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/assets/logo.png differ diff --git a/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/connector.jsonc b/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/connector.jsonc new file mode 100644 index 00000000..1b4bc233 --- /dev/null +++ b/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Sparrow Enterprise Asset Tracker", + "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/tektelic/sparrow-enterprise-asset-tracker/description.md b/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/description.md new file mode 100644 index 00000000..1968dc5d --- /dev/null +++ b/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/description.md @@ -0,0 +1 @@ +Indoor Asset Tracking over BLE and LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..aa113112 --- /dev/null +++ b/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/v1.0.0/payload-config.jsonc @@ -0,0 +1,52 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "The TEKTELIC SPARROW is a highly versatile asset tracker that combines the long-range, low-power capabilities of LoRaWAN® with the widespread availability and reliability of Bluetooth Low Energy (BLE). This dual functionality allows the SPARROW to serve as both a tracker and a BLE beacon, making it suitable for a wide range of asset tracking applications in various environments like offices, retail spaces, and manufacturing facilities.\n\nKey features of the SPARROW include:\n * 5 Year Battery Life - Tracker Mode\n * 16 Month Battery Life - Beacon Mode\n * Accelerometer\n * Bluetooth Low Energy Functionality\n * Data Transmission via LoRaWAN™ and BLE\n * All Global ISM Bands\n * Small Form Factor for Diverse Deployments\n * Easily Integrated into LoRaWAN™ Networks\n * Internal Antenna.\n\nThe SPARROW can be configured to increase its reporting frequency when the tracked asset is in motion, which is particularly useful for dynamic asset management. Additionally, it integrates seamlessly with TEKTELIC's Locus application for asset visualization, and it offers an API for integration with third-party real-time location tracking applications. This level of flexibility makes SPARROW an ideal solution for enhancing operational efficiency, reducing asset loss, and improving inventory management across multiple sectors.\n", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [ + { + "default": "", + "group": "main", + "label": "Beacon decoder type", + "name": "beacon_decoder", + "type": "dropdown", + "options": [ + { + "is_default": false, + "label": "One variable with all beacons", + "value": "simple" + }, + { + "is_default": true, + "label": "Split beacon in different variables", + "value": "splitted" + } + ] + }, + { + "default": "2.5", + "group": "main", + "label": "Battery offset", + "name": "battery_offset", + "type": "number" + } + ], + "networks": [ + "../../../../network/lorawan-tektelic/v1.0.0/payload.js", + "../../../../network/lorawan-citykinect/v1.0.0/payload.js", + "../../../../network/lorawan-actility/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-ttittn-v3/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", + "../../../../network/lorawan-swisscom/v1.0.0/payload.js" + ] +} diff --git a/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/v1.0.0/payload.js b/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/v1.0.0/payload.js new file mode 100644 index 00000000..1af9113c --- /dev/null +++ b/decoders/connector/tektelic/sparrow-enterprise-asset-tracker/v1.0.0/payload.js @@ -0,0 +1,710 @@ +/* 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": "00 BA 70 00 00 00" }] + ** + ** The ignore_vars variable in this code should be used to ignore variables + ** from the device that you don't want. + */ + +// let payload = [{ variable: 'payload', value: '00 BA 70 00 00 00'.replace(/ /g, '') }]; + +// Add ignorable variables in this array. +const ignore_vars = []; +let battery_offset = device.params.find((x) => x.key === "battery_offset"); +battery_offset = battery_offset ? Number(battery_offset.value) : 2.5; + +let beacon_decoder = device.params.find((x) => x.key === "beacon_decoder"); +beacon_decoder = beacon_decoder && beacon_decoder.value === "simple" ? "simple" : null; + +function toTagoFormat(object_item, serie, prefix = "") { + const result = []; + + for (const key in object_item) { + if (ignore_vars.includes(key)) continue; + if (object_item[key] === undefined || object_item[key] === null) { + continue; + } + if (typeof object_item[key] === "object") { + result.push({ + variable: object_item[key].variable || `${prefix}${key}`.toLowerCase(), + + value: object_item[key].value, + + serie: object_item[key].serie || serie, + + metadata: object_item[key].metadata, + + location: object_item[key].location, + + unit: object_item[key].unit, + }); + } else { + result.push({ + variable: `${prefix}${key}`.toLowerCase(), + + value: object_item[key], + + serie, + }); + } + } + + return result; +} + +function Decoder(bytes, port) { + // bytes - Array of bytes (signed) + function slice(a, f, t) { + const res = []; + for (let i = 0; i < t - f; i++) { + res[i] = a[f + i]; + } + return res; + } + + function extract_bytes(chunk, start_bit, end_bit) { + const total_bits = end_bit - start_bit + 1; + const total_bytes = total_bits % 8 === 0 ? to_uint(total_bits / 8) : to_uint(total_bits / 8) + 1; + const offset_in_byte = start_bit % 8; + const end_bit_chunk = total_bits % 8; + const arr = new Array(total_bytes); + for (byte = 0; byte < total_bytes; ++byte) { + const chunk_idx = to_uint(start_bit / 8) + byte; + let lo = chunk[chunk_idx] >> offset_in_byte; + let hi = 0; + if (byte < total_bytes - 1) { + hi = (chunk[chunk_idx + 1] & ((1 << offset_in_byte) - 1)) << (8 - offset_in_byte); + } else if (end_bit_chunk !== 0) { + // Truncate last bits + lo &= (1 << end_bit_chunk) - 1; + } + arr[byte] = hi | lo; + } + return arr; + } + + function apply_data_type(bytes, data_type) { + output = 0; + if (data_type === "unsigned") { + for (var i = 0; i < bytes.length; ++i) { + output = to_uint(output << 8) | bytes[i]; + } + return output; + } + + if (data_type === "signed") { + for (var i = 0; i < bytes.length; ++i) { + output = (output << 8) | bytes[i]; + } + // Convert to signed, based on value size + if (output > Math.pow(2, 8 * bytes.length - 1)) { + output -= Math.pow(2, 8 * bytes.length); + } + return output; + } + if (data_type === "bool") { + return !(bytes[0] === 0); + } + if (data_type === "hexstring") { + return toHexString(bytes); + } + // Incorrect data type + return null; + } + + function decode_field(chunk, start_bit, end_bit, data_type) { + chunk_size = chunk.length; + if (end_bit >= chunk_size * 8) { + return null; // Error: exceeding boundaries of the chunk + } + if (end_bit < start_bit) { + return null; // Error: invalid input + } + arr = extract_bytes(chunk, start_bit, end_bit); + return apply_data_type(arr, data_type); + } + + decoded_data = {}; + decoder = []; + + if (port === 10) { + decoder = [ + { + key: [0x00, 0xd3], + fn(arg) { + decoded_data.battery_status_life = { value: decode_field(arg, 0, 6, "unsigned"), "unit": "%" }; + return 1; + }, + }, + { + key: [0x00, 0xba], + fn(arg) { + decoded_data.battery_status_life = { value: battery_offset + decode_field(arg, 0, 6, "unsigned") * 0.01, unit: "V" }; + decoded_data.battery_status_eos_alert = decode_field(arg, 7, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x00, 0x04], + fn(arg) { + decoded_data.fsm_state = decode_field(arg, 0, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x00, 0x67], + fn(arg) { + decoded_data.mcu_temperature = decode_field(arg, 0, 15, "signed") * 0.1; + return 2; + }, + }, + { + key: [0x00, 0x00], + fn(arg) { + decoded_data.acceleration_alarm = decode_field(arg, 0, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x00, 0x71], + fn(arg) { + decoded_data.acceleration_xaxis = decode_field(arg, 0, 15, "signed") * 0.001; + decoded_data.acceleration_yaxis = decode_field(arg, 16, 31, "signed") * 0.001; + decoded_data.acceleration_zaxis = decode_field(arg, 32, 47, "signed") * 0.001; + return 6; + }, + }, + ]; + } + if (port === 100) { + decoder = [ + { + key: [0x00], + fn(arg) { + decoded_data.device_eui = decode_field(arg, 0, 63, "hexstring"); + return 8; + }, + }, + { + key: [0x01], + fn(arg) { + decoded_data.app_eui = decode_field(arg, 0, 63, "hexstring"); + return 8; + }, + }, + { + key: [0x02], + fn(arg) { + decoded_data.app_key = decode_field(arg, 0, 127, "hexstring"); + return 16; + }, + }, + { + key: [0x03], + fn(arg) { + decoded_data.device_address = decode_field(arg, 0, 31, "hexstring"); + return 4; + }, + }, + { + key: [0x04], + fn(arg) { + decoded_data.network_session_key = decode_field(arg, 0, 127, "hexstring"); + return 16; + }, + }, + { + key: [0x05], + fn(arg) { + decoded_data.app_session_key = decode_field(arg, 0, 127, "hexstring"); + return 16; + }, + }, + { + key: [0x10], + fn(arg) { + decoded_data.loramac_join_mode = decode_field(arg, 7, 7, "unsigned"); + return 2; + }, + }, + { + key: [0x11], + fn(arg) { + decoded_data.loramac_opts_confirm_mode = decode_field(arg, 8, 8, "unsigned"); + decoded_data.loramac_opts_sync_word = decode_field(arg, 9, 9, "unsigned"); + decoded_data.loramac_opts_duty_cycle = decode_field(arg, 10, 10, "unsigned"); + decoded_data.loramac_opts_adr = decode_field(arg, 11, 11, "unsigned"); + return 2; + }, + }, + { + key: [0x12], + fn(arg) { + decoded_data.loramac_dr_tx_dr_number = decode_field(arg, 0, 3, "unsigned"); + decoded_data.loramac_dr_tx_tx_power_number = decode_field(arg, 8, 11, "unsigned"); + return 2; + }, + }, + { + key: [0x13], + fn(arg) { + decoded_data.loramac_rx2_frequency = decode_field(arg, 0, 31, "unsigned"); + decoded_data.loramac_rx2_dr_number = decode_field(arg, 32, 39, "unsigned"); + return 5; + }, + }, + { + key: [0x20], + fn(arg) { + decoded_data.seconds_per_core_tick = decode_field(arg, 0, 31, "unsigned"); + return 4; + }, + }, + { + key: [0x21], + fn(arg) { + decoded_data.tick_per_battery = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x24], + fn(arg) { + decoded_data.tick_per_accelerometer = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x25], + fn(arg) { + decoded_data.tick_per_ble_default = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x26], + fn(arg) { + decoded_data.tick_per_ble_stillness = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x27], + fn(arg) { + decoded_data.tick_per_ble_mobility = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x28], + fn(arg) { + decoded_data.tick_per_temperature = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x2a], + fn(arg) { + decoded_data.mode_reed_event_type = decode_field(arg, 7, 7, "unsigned"); + decoded_data.mode_battery_voltage_report = decode_field(arg, 8, 8, "unsigned"); + decoded_data.mode_acceleration_vector_report = decode_field(arg, 9, 9, "unsigned"); + decoded_data.mode_temperature_report = decode_field(arg, 10, 10, "unsigned"); + decoded_data.mode_ble_report = decode_field(arg, 11, 11, "unsigned"); + return 2; + }, + }, + { + key: [0x2b], + fn(arg) { + decoded_data.event_type1_m_value = decode_field(arg, 0, 3, "unsigned"); + decoded_data.event_type1_n_value = decode_field(arg, 4, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x2c], + fn(arg) { + decoded_data.event_type2_t_value = decode_field(arg, 0, 3, "unsigned"); + return 1; + }, + }, + { + key: [0x40], + fn(arg) { + decoded_data.accelerometer_xaxis_enabled = decode_field(arg, 0, 0, "unsigned"); + decoded_data.accelerometer_yaxis_enabled = decode_field(arg, 1, 1, "unsigned"); + decoded_data.accelerometer_zaxis_enabled = decode_field(arg, 2, 2, "unsigned"); + return 1; + }, + }, + { + key: [0x41], + fn(arg) { + decoded_data.sensitivity_accelerometer_sample_rate = decode_field(arg, 0, 2, "unsigned") * 1; + switch (decoded_data.sensitivity_accelerometer_sample_rate) { + case 1: + decoded_data.sensitivity_accelerometer_sample_rate = 1; + break; + case 2: + decoded_data.sensitivity_accelerometer_sample_rate = 10; + break; + case 3: + decoded_data.sensitivity_accelerometer_sample_rate = 25; + break; + case 4: + decoded_data.sensitivity_accelerometer_sample_rate = 50; + break; + case 5: + decoded_data.sensitivity_accelerometer_sample_rate = 100; + break; + case 6: + decoded_data.sensitivity_accelerometer_sample_rate = 200; + break; + case 7: + decoded_data.sensitivity_accelerometer_sample_rate = 400; + break; + default: + // invalid value + decoded_data.sensitivity_accelerometer_sample_rate = 0; + break; + } + + decoded_data.sensitivity_accelerometer_measurement_range = decode_field(arg, 4, 5, "unsigned") * 1; + switch (decoded_data.sensitivity_accelerometer_measurement_range) { + case 0: + decoded_data.sensitivity_accelerometer_measurement_range = 2; + break; + case 1: + decoded_data.sensitivity_accelerometer_measurement_range = 4; + break; + case 2: + decoded_data.sensitivity_accelerometer_measurement_range = 8; + break; + case 3: + decoded_data.sensitivity_accelerometer_measurement_range = 16; + break; + default: + decoded_data.sensitivity_accelerometer_measurement_range = 0; + } + return 1; + }, + }, + { + key: [0x42], + fn(arg) { + decoded_data.acceleration_alarm_threshold_count = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x43], + fn(arg) { + decoded_data.acceleration_alarm_threshold_period = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x44], + fn(arg) { + decoded_data.acceleration_alarm_threshold = decode_field(arg, 0, 15, "unsigned") * 0.001; + return 2; + }, + }, + { + key: [0x45], + fn(arg) { + decoded_data.acceleration_alarm_grace_period = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x46], + fn(arg) { + decoded_data.accelerometer_tx_report_periodic_enabled = decode_field(arg, 0, 0, "unsigned"); + decoded_data.accelerometer_tx_report_alarm_enabled = decode_field(arg, 1, 1, "unsigned"); + return 1; + }, + }, + { + key: [0x50], + fn(arg) { + decoded_data.ble_mode = decode_field(arg, 7, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x51], + fn(arg) { + decoded_data.ble_scan_interval = decode_field(arg, 0, 15, "unsigned") * 0.001; + return 2; + }, + }, + { + key: [0x52], + fn(arg) { + decoded_data.ble_scan_window = decode_field(arg, 0, 15, "unsigned") * 0.001; + return 2; + }, + }, + { + key: [0x53], + fn(arg) { + decoded_data.ble_scan_duration = decode_field(arg, 0, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x54], + fn(arg) { + decoded_data.ble_reported_devices = decode_field(arg, 0, 7, "unsigned"); + return 1; + }, + }, + { + key: [0x60], + fn(arg) { + decoded_data.temperature_sample_period_idle = decode_field(arg, 0, 31, "unsigned"); + return 4; + }, + }, + { + key: [0x61], + fn(arg) { + decoded_data.temperature_sample_period_active = decode_field(arg, 0, 31, "unsigned"); + return 4; + }, + }, + { + key: [0x62], + fn(arg) { + decoded_data.temperature_threshold_high = decode_field(arg, 0, 7, "unsigned"); + decoded_data.temperature_threshold_low = decode_field(arg, 8, 15, "unsigned"); + return 2; + }, + }, + { + key: [0x63], + fn(arg) { + decoded_data.temperature_threshold_enabled = decode_field(arg, 0, 0, "unsigned"); + return 1; + }, + }, + { + key: [0x71], + fn(arg) { + decoded_data.firmware_version_app_major_version = decode_field(arg, 0, 7, "unsigned"); + decoded_data.firmware_version_app_minor_version = decode_field(arg, 8, 15, "unsigned"); + decoded_data.firmware_version_app_revision = decode_field(arg, 16, 23, "unsigned"); + decoded_data.firmware_version_loramac_major_version = decode_field(arg, 24, 31, "unsigned"); + decoded_data.firmware_version_loramac_minor_version = decode_field(arg, 32, 39, "unsigned"); + decoded_data.firmware_version_loramac_revision = decode_field(arg, 40, 47, "unsigned"); + decoded_data.firmware_version_region = decode_field(arg, 48, 55, "unsigned"); + return 7; + }, + }, + ]; + } + + if (port === 25) { + decoder = [ + { + key: [0x0a], + fn(arg) { + // RSSI to beacons + let count = 0; + for (let i = 0; i < arg.length * 8; i += 7 * 8) { + dev_id = decode_field(arg, i, i + 6 * 8 - 1, "hexstring"); + if (beacon_decoder === "simple") { + dev_id = `${dev_id}_beacon`; + } + decoded_data[dev_id] = decode_field(arg, i + 6 * 8, i + 7 * 8 - 1, "signed"); + count += 7; + } + return count; + }, + }, + { + key: [0xb0], + fn(arg) { + // RSSI to beacons + let count = 0; + for (let i = 0; i < arg.length * 8; i += 4 * 8) { + dev_id = decode_field(arg, i, i + 3 * 8 - 1, "hexstring"); + if (beacon_decoder === "simple") { + dev_id = `${dev_id}_beacon`; + } + decoded_data[dev_id] = decode_field(arg, i + 3 * 8, i + 4 * 8 - 1, "signed"); + count += 4; + } + return count; + }, + }, + { + key: [0xb1], + fn(arg) { + // RSSI to beacons + let count = 0; + for (let i = 0; i < arg.length * 8; i += 4 * 8) { + dev_id = decode_field(arg, i, i + 3 * 8 - 1, "hexstring"); + if (beacon_decoder === "simple") { + dev_id = `${dev_id}_beacon`; + } + decoded_data[dev_id] = decode_field(arg, i + 3 * 8, i + 4 * 8 - 1, "signed"); + count += 4; + } + return count; + }, + }, + { + key: [0xb2], + fn(arg) { + // RSSI to beacons + let count = 0; + for (let i = 0; i < arg.length * 8; i += 4 * 8) { + dev_id = decode_field(arg, i, i + 3 * 8 - 1, "hexstring"); + if (beacon_decoder === "simple") { + dev_id = `${dev_id}_beacon`; + } + decoded_data[dev_id] = decode_field(arg, i + 3 * 8, i + 4 * 8 - 1, "signed"); + count += 4; + } + return count; + }, + }, + { + key: [0xb3], + fn(arg) { + // RSSI to beacons + let count = 0; + for (let i = 0; i < arg.length * 8; i += 4 * 8) { + dev_id = decode_field(arg, i, i + 3 * 8 - 1, "hexstring"); + if (beacon_decoder === "simple") { + dev_id = `${dev_id}_beacon`; + } + decoded_data[dev_id] = decode_field(arg, i + 3 * 8, i + 4 * 8 - 1, "signed"); + count += 4; + } + return count; + }, + }, + ]; + } + + bytes = convertToUint8Array(bytes); + + for (let bytes_left = bytes.length; bytes_left > 0; ) { + let found = false; + for (let i = 0; i < decoder.length; i++) { + const item = decoder[i]; + const { key } = item; + const keylen = key.length; + header = slice(bytes, 0, keylen); + // Header in the data matches to what we expect + if (is_equal(header, key)) { + const f = item.fn; + consumed = f(slice(bytes, keylen, bytes.length)) + keylen; + bytes_left -= consumed; + bytes = slice(bytes, consumed, bytes.length); + found = true; + break; + } + } + if (found) { + continue; + } + // Unable to decode -- headers are not as expected, send raw payload to the application! + // decoded_data = {}; + return decoded_data; + } + + // Converts value to unsigned + function to_uint(x) { + return x >>> 0; + } + + // Checks if two arrays are equal + function is_equal(arr1, arr2) { + if (arr1.length != arr2.length) { + return false; + } + for (let i = 0; i != arr1.length; i++) { + if (arr1[i] != arr2[i]) { + return false; + } + } + return true; + } + + function byteToArray(byteArray) { + arr = []; + for (let i = 0; i < byteArray.length; i++) { + arr.push(byteArray[i]); + } + return arr; + } + + function convertToUint8Array(byteArray) { + arr = []; + for (let i = 0; i < byteArray.length; i++) { + arr.push(to_uint(byteArray[i]) & 0xff); + } + return arr; + } + + function toHexString(byteArray) { + const arr = []; + for (let i = 0; i < byteArray.length; ++i) { + arr.push(`0${(byteArray[i] & 0xff).toString(16)}`.slice(-2)); + } + return arr.join(""); + } + + return decoded_data; +} + +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"); +const port = payload.find((x) => x.variable === "port" || x.variable === "fport"); + +if (payload_raw) { + // Get a unique serie for the incoming data. + const { value, time } = payload_raw; + let { serie } = payload_raw; + serie = new Date().getTime(); + + if (value) { + let decoded = Decoder(Buffer.from(value.replace(/ /g, ""), "hex"), Number(port.value)); + + // Apply simplified beacon version; + const beacons = Object.keys(decoded).filter((x) => x.includes("_beacon")); + if (beacons.length) { + decoded.beacons = { + variable: "beacons", + value: beacons.map((x) => `${x.replace("_beacon", "")}`).join("; "), + metadata: beacons.reduce((final, x) => { + final[x.replace("_beacon", "")] = decoded[x]; + return final; + }, {}), + }; + + decoded = Object.keys(decoded).reduce((final, x) => { + if (!x.includes("_beacon")) { + final[x] = decoded[x]; + } + + return final; + }, {}); + } + + // Parse the payload_raw to JSON format (it comes in a String format) + payload = payload.concat(toTagoFormat(decoded, serie)).map((x) => ({ ...x, serie })); + } +} \ No newline at end of file diff --git a/decoders/connector/tektelic/tundra-sensor/assets/logo.png b/decoders/connector/tektelic/tundra-sensor/assets/logo.png new file mode 100644 index 00000000..c41b4f6b Binary files /dev/null and b/decoders/connector/tektelic/tundra-sensor/assets/logo.png differ diff --git a/decoders/connector/tektelic/tundra-sensor/connector.jsonc b/decoders/connector/tektelic/tundra-sensor/connector.jsonc new file mode 100644 index 00000000..1c55ad74 --- /dev/null +++ b/decoders/connector/tektelic/tundra-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Tundra 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/tektelic/tundra-sensor/description.md b/decoders/connector/tektelic/tundra-sensor/description.md new file mode 100644 index 00000000..1b8482c5 --- /dev/null +++ b/decoders/connector/tektelic/tundra-sensor/description.md @@ -0,0 +1 @@ +Temperature and Humidity Sensor for cold chain monitoring over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/tundra-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/tundra-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..728f354d --- /dev/null +++ b/decoders/connector/tektelic/tundra-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "Tektelic’s Tundra Sensor is the Lorawan solution for maintaining optimal and consistent temperatures within cold rooms and refrigerated areas. Applications include food and pharmaceutical storage, where safeguarding the integrity, quality, and safety of products is vital. The device can be implemented within cold storage such as fridges, coolers, cold rooms and even freezers with minimal impact on battery life or radio signal strength.\n\nFeatures:\n* 15 Year Battery Life\n* Ability to store up to 3000 data entries for 125 days\n* Restaurant and Food Service Cold Chain\n* Medical and Pharmaceutical Storage\n* Vaccine Storage\n* All Global ISM Bands\n* Small Form Factor for Diverse Deployments\n* Internal Antenna\n* -40° to +85°C operability\n* Temperature and Humidity Monitoring", + "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-helium/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-brdot-/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/tektelic/tundra-sensor/v1.0.0/payload.js b/decoders/connector/tektelic/tundra-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..871e86ef --- /dev/null +++ b/decoders/connector/tektelic/tundra-sensor/v1.0.0/payload.js @@ -0,0 +1,145 @@ +/* eslint-disable no-plusplus */ +/* eslint-disable prettier/prettier */ +/* eslint-disable no-bitwise */ +/* + * Tundra sensor + */ + +function signed_convert(val, bitwidth) { + const isnegative = val & (1 << (bitwidth - 1)); + const boundary = 1 << bitwidth; + const minval = -boundary; + const mask = boundary - 1; + return isnegative ? minval + (val & mask) : val; +} + +function Decoder(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + // Device Info Not Repeated + const array_result = []; + if (port === 10) { + for (let i = 0; i < bytes.length; ) { + const channel = bytes[i++]; + const type = bytes[i++]; + + // battery voltage + if (channel === 0x00 && type === 0xff) { + let battery_voltage = (bytes[i] << 8) | bytes[i + 1]; + battery_voltage = signed_convert(battery_voltage, 16); + array_result.push({ variable: "battery_voltage", value: Number((battery_voltage * 0.01).toFixed(2)), unit: "V" }); + i += 2; + } + // mcu temperature + if (channel === 0x0b && type === 0x67) { + let mcu_temperature = (bytes[i] << 8) | bytes[i + 1]; + mcu_temperature = signed_convert(mcu_temperature, 16); + array_result.push({ variable: "mcu_temperature", value: Number((mcu_temperature * 0.1).toFixed(2)), unit: "°C" }); + i += 2; + } + + if (channel === 0x03 && type === 0x67) { + let ambient_temperature = (bytes[i] << 8) | bytes[i + 1]; + ambient_temperature = signed_convert(ambient_temperature, 16); + if (ambient_temperature === 65535) { + ambient_temperature = -1; + } + array_result.push({ variable: "ambient_temperature", value: Number((ambient_temperature * 0.1).toFixed(2)), unit: "°C" }); + i += 2; + } + // impact alarm + if (channel === 0x0c && type === 0x00) { + let impact_alarm = bytes[i]; + if (impact_alarm === 0xff) { + impact_alarm = "Impact alarm active"; + } + if (impact_alarm === 0x00) { + impact_alarm = "Impact alarm inactive"; + } + array_result.push({ variable: "impact_alarm", value: impact_alarm }); + i += 1; + } + // Acceleration Magnitude + if (channel === 0x05 && type === 0x02) { + const acceleration_magnitude = (bytes[i] << 8) | bytes[i + 1]; + array_result.push({ variable: "acceleration_magnitude", value: acceleration_magnitude, unit: "g" }); + i += 2; + } + // Acceleration Vector + if (channel === 0x07 && type === 0x71) { + const acceleration_vector_x = (bytes[i] << 8) | bytes[i + 1]; + array_result.push({ variable: "acceleration_x_axis", value: acceleration_vector_x, unit: "g" }); + const acceleration_vector_y = (bytes[i + 2] << 8) | bytes[i + 3]; + array_result.push({ variable: "acceleration_y_axis", value: acceleration_vector_y, unit: "g" }); + const acceleration_vector_z = (bytes[i + 4] << 8) | bytes[i + 5]; + array_result.push({ variable: "acceleration_z_axis", value: acceleration_vector_z, unit: "g" }); + i += 6; + } + } + return array_result; + } + if (port === 32) { + let flag_entry = 0; + for (let i = 0; i < bytes.length; ) { + if (flag_entry === 0) { + flag_entry = 1; + const tag_entry = (bytes[0] << 8) | bytes[i + 1]; + array_result.push({ variable: "tag_entry", value: tag_entry }); + i += 2; + } + const channel = bytes[i++]; + const type = bytes[i++]; + if (channel === 0x03 && type === 0x67) { + let ambient_temperature = (bytes[i] << 8) | bytes[i + 1]; + ambient_temperature = signed_convert(ambient_temperature, 16); + if (ambient_temperature === 65535) { + ambient_temperature = -1; + } + array_result.push({ variable: "ambient_temperature", value: Number((ambient_temperature * 0.1).toFixed(2)), unit: "°C" }); + i += 2; + } + if (channel === 0x04 && type === 0x68) { + const ambient_rh = bytes[i]; + array_result.push({ variable: "ambient_rh", value: Number((ambient_rh * 0.5).toFixed(2)), unit: "%" }); + i += 1; + } + } + flag_entry = 0; + return array_result; + } + if (port === 33) { + for (let i = 0; i < bytes.length; ) { + const channel = bytes[i++]; + const type = bytes[i++]; + if (channel === 0x03 && type === 0x67) { + let ambient_temperature = (bytes[i] << 8) | bytes[i + 1]; + ambient_temperature = signed_convert(ambient_temperature, 16); + array_result.push({ variable: "ambient_temperature", value: Number((ambient_temperature * 0.1).toFixed(2)), unit: "°C" }); + i += 2; + } + if (channel === 0x04 && type === 0x68) { + const ambient_rh = bytes[i]; + array_result.push({ variable: "ambient_rh", value: Number((ambient_rh * 0.5).toFixed(2)), unit: "%" }); + i += 1; + } + } + return array_result; + } +} + +const payload_raw = payload.find((x) => x.variable === "payload_raw" || x.variable === "payload" || x.variable === "data" || x.variable === "payload_hex"); +const port = payload.find((x) => x.variable === "port" || x.variable === "fport" || x.variable === "FPort"); +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 = Decoder(buffer, port.value); + 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/tektelic/vivid-smart-room-occupancy-sensor/assets/logo.png b/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/assets/logo.png new file mode 100644 index 00000000..5dea83d1 Binary files /dev/null and b/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/assets/logo.png differ diff --git a/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/connector.jsonc b/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/connector.jsonc new file mode 100644 index 00000000..9a39d882 --- /dev/null +++ b/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/connector.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "../../../../schema/connector.json", + "name": "Tektelic Vivid Smart Room & Occupancy 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/tektelic/vivid-smart-room-occupancy-sensor/description.md b/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/description.md new file mode 100644 index 00000000..ec14ec2c --- /dev/null +++ b/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/description.md @@ -0,0 +1 @@ +All-in-one Home monitoring Sensor over LoRaWAN™ \ No newline at end of file diff --git a/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/v1.0.0/payload-config.jsonc b/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/v1.0.0/payload-config.jsonc new file mode 100644 index 00000000..868268a5 --- /dev/null +++ b/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/v1.0.0/payload-config.jsonc @@ -0,0 +1,25 @@ +{ + "$schema": "../../../../../schema/connector_details.json", + "description": "../description.md", + "install_text": "##\nThe All-In-One Smart Home Sensor packs a huge amount of functionality into a small form factor. \n\nIt is an ideal tool for measuring and reporting temperature, humidity and light intensity, detecting motion, shock and water leaks.\n\nAvailable in three different packaging options that include either an external contact for pulse reading, or a PIR lens for motion detection.\n##\nApplications\n* Movement Detection (Doors, Drawers)\n* G-Force Measurement (Settable Trigger)\n* Motion Detection (PIR) - Optional\n* On / Off External Contact - Optional\n* On / Off Internal Magnetic Switch\n* Pulse Reading (Water, Gas, other metering)\n* Light Detection\n* Temperature Measurement\n* Humidity Measurement\n* Moisture / Leak Detection - Optional", + "install_end_text": "", + "device_annotation": "", + "device_parameters": [], + "networks": [ + "../../../../network/lorawan-everynet/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-helium/v1.0.0/payload.js", + "../../../../network/lorawan-orbiwise/v1.0.0/payload.js", + "../../../../network/lorawan-senet/v1.0.0/payload.js", + "../../../../network/lorawan-brdot-/v1.0.0/payload.js", + "../../../../network/lorawan-chirpstack/v1.0.0/payload.js", + "../../../../network/lorawan-loriot-/v1.0.0/payload.js", + "../../../../network/lorawan-machineq/v1.0.0/payload.js", + "../../../../network/lorawan-senra/v1.0.0/payload.js" + ] +} \ No newline at end of file diff --git a/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/v1.0.0/payload.js b/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/v1.0.0/payload.js new file mode 100644 index 00000000..de27655e --- /dev/null +++ b/decoders/connector/tektelic/vivid-smart-room-occupancy-sensor/v1.0.0/payload.js @@ -0,0 +1,238 @@ +/* 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 = []; + +/** + * Convert an object to TagoIO object format. + * Can be used in two ways: + * toTagoFormat({ myvariable: myvalue , anothervariable: anothervalue... }) + * toTagoFormat({ myvariable: { value: myvalue, unit: 'C', metadata: { color: 'green' }} , anothervariable: anothervalue... }) + * + * @param {Object} object_item Object containing key and value. + * @param {String} serie Serie for the variables + * @param {String} prefix Add a prefix to the variables name + */ +function toTagoFormat(object_item, serie, prefix = '') { + const result = []; + for (const key in object_item) { + if (ignore_vars.includes(key)) continue; + + if (typeof object_item[key] === 'object') { + result.push({ + variable: object_item[key].variable || `${prefix}${key}`, + value: object_item[key].value, + serie: object_item[key].serie || serie, + metadata: object_item[key].metadata, + location: object_item[key].location, + unit: object_item[key].unit, + }); + } else { + result.push({ + variable: `${prefix}${key}`, + value: object_item[key], + serie, + }); + } + } + + return result; +} + +function parsePayload(payload_raw) { + // If your device is sending something different than hex, like base64, just specify it bellow. + const bytes = Buffer.from(payload_raw, 'hex'); + const params = { + // battery_voltage: null, + // reed_state: null, + // light_detected: null, + // temperature: null, + // humidity: null, + // impact_magnitude: null, + // break_in: null, + // acceleration_x: null, + // acceleration_y: null, + // acceleration_z: null, + // reed_count: null, + // moisture: null, + // activity: null, + // mcu_temperature: null, + // impact_alarm: null, + // activity_count: null, + // external_input: null, + // external_input_count: null, + }; + + for (let i = 0; i < bytes.length; i++) { + // Handle battery voltage + if (bytes[i] === 0x00 && bytes[i + 1] === 0xFF) { + params.battery_voltage = 0.01 * ((bytes[i + 2] << 8) | bytes[i + 3]); + i += 3; + } + + // Handle reed switch state + if (bytes[i] === 0x01 && bytes[i + 1] === 0x00) { + if (bytes[i + 2] === 0x00) { + params.input = -1; + } else if (bytes[i + 2] === 0xFF) { + params.input = 0; + } + i += 2; + } + + // Handle light detection + if (bytes[i] === 0x02 && bytes[i + 1] === 0x00) { + if (bytes[i + 2] === 0x00) { + params.light_detected = 0; + } else if (bytes[i + 2] === 0xFF) { + params.light_detected = 1; + } + i += 2; + } + + // Handle temperature + if (bytes[i] === 0x03 && bytes[i + 1] === 0x67) { + // Sign-extend to 32 bits to support negative values, by shifting 24 bits + // (16 too far) to the left, followed by a sign-propagating right shift: + params.temperature = (bytes[i + 2] << 24 >> 16 | bytes[i + 3]) / 10; + i += 3; + } + + // Handle humidity + if (bytes[i] === 0x04 && bytes[i + 1] === 0x68) { + params.humidity = 0.5 * bytes[i + 2]; + i += 2; + } + + // Handle impact magnitude + if (bytes[i] === 0x05 && bytes[i + 1] === 0x02) { + // Sign-extend to 32 bits to support negative values, by shifting 24 bits + // (16 too far) to the left, followed by a sign-propagating right shift: + params.impact_magnitude = (bytes[i + 2] << 24 >> 16 | bytes[i + 3]) / 1000; + i += 3; + } + + // Handle break-in + if (bytes[i] === 0x06 && bytes[i + 1] === 0x00) { + if (bytes[i + 2] === 0x00) { + params.break_in = false; + } else if (bytes[i + 2] === 0xFF) { + params.break_in = true; + } + i += 2; + } + + // Handle accelerometer data + if (bytes[i] === 0x07 && bytes[i + 1] === 0x71) { + // Sign-extend to 32 bits to support negative values, by shifting 24 bits + // (16 too far) to the left, followed by a sign-propagating right shift: + params.acceleration_x = (bytes[i + 2] << 24 >> 16 | bytes[i + 3]) / 1000; + params.acceleration_y = (bytes[i + 4] << 24 >> 16 | bytes[i + 5]) / 1000; + params.acceleration_z = (bytes[i + 6] << 24 >> 16 | bytes[i + 7]) / 1000; + i += 7; + } + + // Handle reed switch count + if (bytes[i] === 0x08 && bytes[i + 1] === 0x04) { + params.input_count = (bytes[i + 2] << 8) | bytes[i + 3]; + i += 3; + } + + // Handle moisture + if (bytes[i] === 0x09 && bytes[i + 1] === 0x00) { + i += 1; + // check data + if (bytes[i + 1] === 0x00) { + params.moisture = false; + i += 1; + } else if (bytes[i + 1] === 0xFF) { + params.moisture = true; + i += 1; + } + } + + // Handle PIR activity + // check the channel and type + if (bytes[i] === 0x0A && bytes[i + 1] === 0x00) { + i += 1; + // check data + if (bytes[i + 1] === 0x00) { + params.motion_detected = 0; + i += 1; + } else if (bytes[i + 1] === 0xFF) { + params.motion_detected = -1; + i += 1; + } + } + + // Handle temperature + if (bytes[i] === 0x0B && bytes[i + 1] === 0x67) { + // Sign-extend to 32 bits to support negative values, by shifting 24 bits + // (16 too far) to the left, followed by a sign-propagating right shift: + params.mcu_temperature = (bytes[i + 2] << 24 >> 16 | bytes[i + 3]) / 10; + i += 3; + } + + // Handle impact alarm + if (bytes[i] === 0x0C && bytes[i + 1] === 0x00) { + if (bytes[i + 2] === 0x00) { + params.impact_alarm = false; + } else if (bytes[i + 2] === 0xFF) { + params.impact_alarm = true; + } + i += 2; + } + + // Handle motion (PIR activity) event count + if (bytes[i] === 0x0D && bytes[i + 1] === 0x04) { + params.motion_count = (bytes[i + 2] << 8) | bytes[i + 3]; + i += 3; + } + + // Handle external input state + if (bytes[i] === 0x0E && bytes[i + 1] === 0x00) { + if (bytes[i + 2] === 0x00) { + params.external_input = true; + } else if (bytes[i + 2] === 0xFF) { + params.external_input = false; + } + i += 2; + } + + // Handle external input count + if (bytes[i] === 0x0F && bytes[i + 1] === 0x04) { + params.external_input_count = (bytes[i + 2] << 8) | bytes[i + 3]; + i += 3; + } + } + + return params; +} + +// let payload = [{ variable: 'payload', value: '036700f204686000ff0129', serie: 122 }]; +// 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 data = payload.find(x => x.variable === 'payload_raw' || x.variable === 'payload' || x.variable === 'data'); +if (data) { + // Get a unique serie for the incoming data. + const { value, serie } = data; + + // Parse the payload_raw to JSON format (it comes in a String format) + if (value) { + payload = payload.concat(toTagoFormat(parsePayload(value), serie)); + } +} +// console.log(payload); diff --git a/schema/connector_details.json b/schema/connector_details.json index d204009b..a1217854 100644 --- a/schema/connector_details.json +++ b/schema/connector_details.json @@ -24,7 +24,7 @@ "type": "array", "description": "List of device parameters.", "items": { - "type": "string" + "type": "object" } }, "networks": { @@ -52,4 +52,4 @@ }, "required": ["description", "networks"], "additionalProperties": false -} \ No newline at end of file +}