From 03209b8f9819ec4815531fc5476336dec3ed4187 Mon Sep 17 00:00:00 2001 From: Matthias Prinke Date: Wed, 10 Apr 2024 21:00:43 +0200 Subject: [PATCH] Imported from BresserWeatherSensorTTN --- scripts/datacake_decoder.js | 260 +++++++++++++++++++++ scripts/decoder_basic.js | 254 +++++++++++++++++++++ scripts/generate_decoder.py | 143 ++++++++++++ scripts/helium_decoder.js | 250 +++++++++++++++++++++ scripts/ttn_decoder_distance.js | 270 ++++++++++++++++++++++ scripts/ttn_decoder_fp.js | 236 +++++++++++++++++++ scripts/ttn_downlink_formatter.js | 238 ++++++++++++++++++++ scripts/ttn_uplink_formatter.js | 362 ++++++++++++++++++++++++++++++ 8 files changed, 2013 insertions(+) create mode 100644 scripts/datacake_decoder.js create mode 100644 scripts/decoder_basic.js create mode 100644 scripts/generate_decoder.py create mode 100644 scripts/helium_decoder.js create mode 100644 scripts/ttn_decoder_distance.js create mode 100644 scripts/ttn_decoder_fp.js create mode 100644 scripts/ttn_downlink_formatter.js create mode 100644 scripts/ttn_uplink_formatter.js diff --git a/scripts/datacake_decoder.js b/scripts/datacake_decoder.js new file mode 100644 index 0000000..d49c4c4 --- /dev/null +++ b/scripts/datacake_decoder.js @@ -0,0 +1,260 @@ +function Decoder(bytes, port) { + // bytes is of type Buffer + + // IMPORTANT: paste code from src/decoder.js here + var bytesToInt = function (bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << (x * 8)); + } + return i; + }; + + var unixtime = function (bytes) { + if (bytes.length !== unixtime.BYTES) { + throw new Error('Unix time must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + unixtime.BYTES = 4; + + var uint8 = function (bytes) { + if (bytes.length !== uint8.BYTES) { + throw new Error('int must have exactly 1 byte'); + } + return bytesToInt(bytes); + }; + uint8.BYTES = 1; + + var uint16 = function (bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + return bytesToInt(bytes); + }; + uint16.BYTES = 2; + + var uint16fp1 = function (bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + var res = bytesToInt(bytes) * 0.1; + return res.toFixed(1); + }; + uint16fp1.BYTES = 2; + + var uint32 = function (bytes) { + if (bytes.length !== uint32.BYTES) { + throw new Error('int must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + uint32.BYTES = 4; + + var latLng = function (bytes) { + if (bytes.length !== latLng.BYTES) { + throw new Error('Lat/Long must have exactly 8 bytes'); + } + + var lat = bytesToInt(bytes.slice(0, latLng.BYTES / 2)); + var lng = bytesToInt(bytes.slice(latLng.BYTES / 2, latLng.BYTES)); + + return [lat / 1e6, lng / 1e6]; + }; + latLng.BYTES = 8; + + var temperature = function (bytes) { + if (bytes.length !== temperature.BYTES) { + throw new Error('Temperature must have exactly 2 bytes'); + } + var isNegative = bytes[0] & 0x80; + var b = ('00000000' + Number(bytes[0]).toString(2)).slice(-8) + + ('00000000' + Number(bytes[1]).toString(2)).slice(-8); + if (isNegative) { + var arr = b.split('').map(function (x) { return !Number(x); }); + for (var i = arr.length - 1; i > 0; i--) { + arr[i] = !arr[i]; + if (arr[i]) { + break; + } + } + b = arr.map(Number).join(''); + } + var t = parseInt(b, 2); + if (isNegative) { + t = -t; + } + t = t / 1e2; + return t.toFixed(1); + }; + temperature.BYTES = 2; + + var humidity = function (bytes) { + if (bytes.length !== humidity.BYTES) { + throw new Error('Humidity must have exactly 2 bytes'); + } + + var h = bytesToInt(bytes); + return h / 1e2; + }; + humidity.BYTES = 2; + + // Based on https://stackoverflow.com/a/37471538 by Ilya Bursov + // quoted by Arjan here https://www.thethingsnetwork.org/forum/t/decode-float-sent-by-lopy-as-node/8757 + function rawfloat(bytes) { + if (bytes.length !== rawfloat.BYTES) { + throw new Error('Float must have exactly 4 bytes'); + } + // JavaScript bitwise operators yield a 32 bits integer, not a float. + // Assume LSB (least significant byte first). + var bits = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0]; + var sign = (bits >>> 31 === 0) ? 1.0 : -1.0; + var e = bits >>> 23 & 0xff; + var m = (e === 0) ? (bits & 0x7fffff) << 1 : (bits & 0x7fffff) | 0x800000; + var f = sign * m * Math.pow(2, e - 150); + return f.toFixed(1); + } + rawfloat.BYTES = 4; + + var bitmap_node = function (byte) { + if (byte.length !== bitmap_node.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + + return ['res7', 'res6', 'res5', 'res4', 'res3', 'res2', 'res1', 'res0'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_node.BYTES = 1; + + var bitmap_sensors = function (byte) { + if (byte.length !== bitmap_sensors.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + // Only Weather Sensor + //return ['res5', 'res4', 'res3', 'res2', 'res1', 'res0', 'dec_ok', 'batt_ok'] + // Weather Sensor + MiThermo (BLE) Sensor + //return ['res4', 'res3', 'res2', 'res1', 'res0', 'ble_ok', 'dec_ok', 'batt_ok'] + // Weather Sensor, Soil Sensor and MiThermo (BLE) Sensor + return ['res0', 'ble_ok', 'ls_dec_ok', 'ls_batt_ok', 's1_dec_ok', 's1_batt_ok', 'ws_dec_ok', 'ws_batt_ok'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_sensors.BYTES = 1; + + var decode = function (bytes, mask, names) { + + var maskLength = mask.reduce(function (prev, cur) { + return prev + cur.BYTES; + }, 0); + if (bytes.length < maskLength) { + throw new Error('Mask length is ' + maskLength + ' whereas input is ' + bytes.length); + } + + names = names || []; + var offset = 0; + return mask + .map(function (decodeFn) { + var current = bytes.slice(offset, offset += decodeFn.BYTES); + return decodeFn(current); + }) + .reduce(function (prev, cur, idx) { + prev[names[idx] || idx] = cur; + return prev; + }, {}); + }; + + if (typeof module === 'object' && typeof module.exports !== 'undefined') { + module.exports = { + unixtime: unixtime, + uint8: uint8, + uint16: uint16, + uint32: uint32, + temperature: temperature, + humidity: humidity, + latLng: latLng, + bitmap_node: bitmap_node, + bitmap_sensors: bitmap_sensors, + rawfloat: rawfloat, + uint16fp1: uint16fp1, + decode: decode + }; + } + + // Weather Sensor, Battery Voltage and Water Temperature (OneWire) + // return decode( + // bytes, + // [bitmap, temperature, uint8, uint16fp1, uint16fp1, uint16fp1, + // rawfloat, uint16, temperature], + // ['status', 'air_temp_c', 'humidity', 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + // 'rain_mm', 'battery_v', 'water_temp_c' ] + // ); + + // Weather Sensor, Supply Voltage, Battery Voltage, Water Temperature (OneWire), + // MiThermo Indoor Temperature+Humidity (BLE) + // return decode( + // bytes, + // [bitmap, temperature, uint8, uint16fp1, uint16fp1, uint16fp1, + // rawfloat, uint16, uint16, temperature, temperature, uint8 ], + // ['status', 'air_temp_c', 'humidity', 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + // 'rain_mm', 'supply_v', 'battery_v', 'water_temp_c', 'indoor_temp_c', 'indoor_humidity' ] + // ); + // Weather Sensor, Supply Voltage, Battery Voltage, Water Temperature (OneWire), + // MiThermo Indoor Temperature+Humidity (BLE), Bresser Soil Temperature+Moisture + // return decode( + // bytes, + // [bitmap, temperature, uint8, uint16fp1, uint16fp1, uint16fp1, + // rawfloat, uint16, uint16, temperature, temperature, uint8, + // temperature, uint8, rawfloat, rawfloat, rawfloat, rawfloat + // ], + // ['status', 'air_temp_c', 'humidity', 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + // 'rain_mm', 'supply_v', 'battery_v', 'water_temp_c', 'indoor_temp_c', 'indoor_humidity', + // 'soil_temp_c', 'soil_moisture', 'rain_hr', 'rain_day', 'rain_week', 'rain_mon' + // ] + // ); + + // Weather Sensor, Supply Voltage, Battery Voltage, Water Temperature (OneWire), + // MiThermo Indoor Temperature+Humidity (BLE), Bresser Soil Temperature+Moisture, + // Hourly (past 60 minutes)/Daily/Weekly/Monthly Rainfall + // return decode( + // bytes, + // [bitmap, temperature, uint8, uint16fp1, uint16fp1, uint16fp1, + // rawfloat, uint16, uint16, temperature, temperature, uint8, + // temperature, uint8, rawfloat, rawfloat, rawfloat, rawfloat + // ], + // ['status', 'air_temp_c', 'humidity', 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + // 'rain_mm', 'supply_v', 'battery_v', 'water_temp_c', 'indoor_temp_c', 'indoor_humidity', + // 'soil_temp_c', 'soil_moisture', 'rain_hr', 'rain_day', 'rain_week', 'rain_mon' + // ] + // ); + + // Weather Sensor, Supply Voltage, Battery Voltage, Water Temperature (OneWire), + // MiThermo Indoor Temperature+Humidity (BLE), Bresser Soil Temperature+Moisture, + // Hourly (past 60 minutes)/Daily/Weekly/Monthly Rainfall, Distance (or Fill Level) + return decode( + bytes, + [ bitmap_node, bitmap_sensors, temperature, uint8, + uint16fp1, uint16fp1, uint16fp1, + rawfloat, uint16, uint16, temperature, + temperature, uint8, temperature, uint8, + rawfloat, rawfloat, rawfloat, rawfloat, + unixtime, uint16, uint8 + ], + [ 'status_node', 'status', 'air_temp_c', 'humidity', + 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + 'rain_mm', 'supply_v', 'battery_v', 'water_temp_c', + 'indoor_temp_c', 'indoor_humidity', 'soil_temp_c', 'soil_moisture', + 'rain_hr', 'rain_day', 'rain_week', 'rain_mon', + 'lightning_time', 'lightning_count', 'lightning_distance_km' + ] + ); + +} diff --git a/scripts/decoder_basic.js b/scripts/decoder_basic.js new file mode 100644 index 0000000..d0090a8 --- /dev/null +++ b/scripts/decoder_basic.js @@ -0,0 +1,254 @@ +function ttn_decoder_fp(bytes) { + // bytes is of type Buffer + + + var bytesToInt = function (bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << (x * 8)); + } + return i; + }; + + var unixtime = function (bytes) { + if (bytes.length !== unixtime.BYTES) { + throw new Error('Unix time must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + unixtime.BYTES = 4; + + var uint8 = function (bytes) { + if (bytes.length !== uint8.BYTES) { + throw new Error('int must have exactly 1 byte'); + } + return bytesToInt(bytes); + }; + uint8.BYTES = 1; + + var uint16 = function (bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + return bytesToInt(bytes); + }; + uint16.BYTES = 2; + + var uint16fp1 = function (bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + var res = bytesToInt(bytes) * 0.1; + return res; + }; + uint16fp1.BYTES = 2; + + var uint32 = function (bytes) { + if (bytes.length !== uint32.BYTES) { + throw new Error('int must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + uint32.BYTES = 4; + + var latLng = function (bytes) { + if (bytes.length !== latLng.BYTES) { + throw new Error('Lat/Long must have exactly 8 bytes'); + } + + var lat = bytesToInt(bytes.slice(0, latLng.BYTES / 2)); + var lng = bytesToInt(bytes.slice(latLng.BYTES / 2, latLng.BYTES)); + + return [lat / 1e6, lng / 1e6]; + }; + latLng.BYTES = 8; + + var temperature = function (bytes) { + if (bytes.length !== temperature.BYTES) { + throw new Error('Temperature must have exactly 2 bytes'); + } + var isNegative = bytes[0] & 0x80; + var b = ('00000000' + Number(bytes[0]).toString(2)).slice(-8) + + ('00000000' + Number(bytes[1]).toString(2)).slice(-8); + if (isNegative) { + var arr = b.split('').map(function (x) { return !Number(x); }); + for (var i = arr.length - 1; i > 0; i--) { + arr[i] = !arr[i]; + if (arr[i]) { + break; + } + } + b = arr.map(Number).join(''); + } + var t = parseInt(b, 2); + if (isNegative) { + t = -t; + } + t = t / 1e2; + return t; + }; + temperature.BYTES = 2; + + var humidity = function (bytes) { + if (bytes.length !== humidity.BYTES) { + throw new Error('Humidity must have exactly 2 bytes'); + } + + var h = bytesToInt(bytes); + return h / 1e2; + }; + humidity.BYTES = 2; + + // Based on https://stackoverflow.com/a/37471538 by Ilya Bursov + // quoted by Arjan here https://www.thethingsnetwork.org/forum/t/decode-float-sent-by-lopy-as-node/8757 + function rawfloat(bytes) { + if (bytes.length !== rawfloat.BYTES) { + throw new Error('Float must have exactly 4 bytes'); + } + // JavaScript bitwise operators yield a 32 bits integer, not a float. + // Assume LSB (least significant byte first). + var bits = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0]; + var sign = (bits >>> 31 === 0) ? 1.0 : -1.0; + var e = bits >>> 23 & 0xff; + var m = (e === 0) ? (bits & 0x7fffff) << 1 : (bits & 0x7fffff) | 0x800000; + var f = sign * m * Math.pow(2, e - 150); + return f; + } + rawfloat.BYTES = 4; + + var bitmap_node = function (byte) { + if (byte.length !== bitmap_node.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + + return ['res7', 'res6', 'res5', 'res4', 'res3', 'res2', 'res1', 'res0'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_node.BYTES = 1; + + var bitmap_sensors = function (byte) { + if (byte.length !== bitmap_sensors.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + // Only Weather Sensor + //return ['res5', 'res4', 'res3', 'res2', 'res1', 'res0', 'dec_ok', 'batt_ok'] + // Weather Sensor + MiThermo (BLE) Sensor + //return ['res4', 'res3', 'res2', 'res1', 'res0', 'ble_ok', 'dec_ok', 'batt_ok'] + // Weather Sensor, Soil Sensor and MiThermo (BLE) Sensor + return ['res0', 'ble_ok', 'ls_dec_ok', 'ls_batt_ok', 's1_dec_ok', 's1_batt_ok', 'ws_dec_ok', 'ws_batt_ok'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_sensors.BYTES = 1; + + var decode = function (bytes, mask, names) { + + var maskLength = mask.reduce(function (prev, cur) { + return prev + cur.BYTES; + }, 0); + if (bytes.length < maskLength) { + throw new Error('Mask length is ' + maskLength + ' whereas input is ' + bytes.length); + } + + names = names || []; + var offset = 0; + return mask + .map(function (decodeFn) { + var current = bytes.slice(offset, offset += decodeFn.BYTES); + return decodeFn(current); + }) + .reduce(function (prev, cur, idx) { + prev[names[idx] || idx] = cur; + return prev; + }, {}); + }; + + if (typeof module === 'object' && typeof module.exports !== 'undefined') { + module.exports = { + unixtime: unixtime, + uint8: uint8, + uint16: uint16, + uint32: uint32, + temperature: temperature, + humidity: humidity, + latLng: latLng, + bitmap_node: bitmap_node, + bitmap_sensors: bitmap_sensors, + rawfloat: rawfloat, + uint16fp1: uint16fp1, + decode: decode + }; + } + + // SENSORID_EN defined + // -> #1: id uint32 + // + // unconditional + // -> #2: status bitmap + // + // unconditional + // -> #3: air_temp_c temperature + // + // unconditional + // -> #4: humidity uint8 + // + // ENCODE_AS_FLOAT defined + // -> #5: wind_gust_meter_sec rawfloat + // -> #6: wind_avg_meter_sec rawfloat + // -> #7: wind_direction_deg rawfloat + // + // ENCODE_AS_FLOAT not defined + // (uint16fp1: unsigned integer 16 bytes; fixed point with 1 decimal) + // -> #5: wind_gust_meter_sec uint16fp1 + // -> #6: wind_avg_meter_sec uint16fp1 + // -> #7: wind_direction_deg uint16fp1 + // + // unconditional + // -> #8: rain_mm rawfloat + // + // ADC_EN defined + // -> #9: supply_v uint16 + // + // ADC_EN defined and PIN_ADC3_IN defined + // -> #10: battery_v uint16 + // + // RAINDATA_EN defined + // -> #11: rain_hr rawfloat + // -> #12: rain_day rawfloat + // -> #13: rain_week rawfloat + // -> #14: rain_mon rawfloat + + // SensorID, Status, Weather Sensor, Supply Voltage, Battery Voltage, + // Hourly (past 60 minutes)/Daily/Weekly/Monthly Rainfall + return decode( + bytes, + [uint32, bitmap_node, bitmap_sensors, temperature, uint8, + uint16fp1, uint16fp1, uint16fp1, + rawfloat, uint16, uint16, rawfloat, rawfloat, rawfloat, rawfloat + ], + ['id', 'status_node', 'status', 'air_temp_c', 'humidity', + 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + 'rain_mm', 'supply_v', 'battery_v', 'rain_hr', 'rain_day', 'rain_week', 'rain_mon' + ] + ); +} + + +function decodeUplink(input) { + return { + data: { + bytes: ttn_decoder_fp(input.bytes) + }, + warnings: [], + errors: [] + }; +} diff --git a/scripts/generate_decoder.py b/scripts/generate_decoder.py new file mode 100644 index 0000000..a84dad9 --- /dev/null +++ b/scripts/generate_decoder.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 + +######################################################################################### +# Generate LoRaWAN Javascript payload decoder function from Arduino C++ header file +# +# Usage: +# g++ -dM -E BresserWeatherSensorTTNCfg.h | ~/generate_decoder.py +# or +# g++ -dM -D -E BresserWeatherSensorTTNCfg.h | ~/generate_decoder.py +# +# The C++ preprocessor is used to evaluate the defines in the header file. +# Its output is then passed to this script, which creates the decoder function call +# to be copied into the Javascript decoder in the LoRaWAN Network provider's +# web interface. +# +# created: 02/2023 +# +# MIT License +# Copyright (C) 02/2023 Matthias Prinke (https://github.com/matthias-bs) +# +# History: +# +# 20230221 Created +# 20230716 Added lightning sensor data, split status bitmap in status_node and status +# +# To Do: +# - +# +######################################################################################### +import sys +import re + +# List of known features +features = [ + 'SENSORID_EN', + 'ONEWIRE_EN', + 'SLEEP_EN', + 'THEENGSDECODER_EN', + 'RAINDATA_EN', + 'SOILSENSOR_EN', + 'MITHERMOMETER_EN', + 'DISTANCESENSOR_EN', + 'LIGHTNINGSENSOR_EN', + 'ADC_EN', + 'PIN_ADC_IN', + 'PIN_ADC0_IN', + 'PIN_ADC1_IN', + 'PIN_ADC2_IN', + 'PIN_ADC3_IN', +] + +# Javascript decoder generator +# +# The position in the dictionary below must match the order of assignments to the +# encoder object in doUplink() (BresserWeatherSensorTTN.ino). +# +# The position in the dictionary then defines the order in the output! +# +# Format: +# : {'cond': , 'type': } +# +# : JSON output key name +# : feature which must be present in input or '' if unconditional +# : decoder datatype (or rather name of decoding function) +generator = { + 'id' : {'cond': 'SENSORID_EN', 'type': 'uint32'}, + 'status_node' : {'cond': '', 'type': 'bitmap'}, + 'status' : {'cond': '', 'type': 'bitmap'}, + 'air_temp_c': {'cond': '', 'type': 'temperature'}, + 'humidity': {'cond': '', 'type': 'uint8'}, + 'wind_gust_meter_sec': {'cond': '', 'type': 'uint16fp1'}, + 'wind_avg_meter_sec': {'cond': '', 'type': 'uint16fp1'}, + 'wind_direction_deg': {'cond': '', 'type': 'uint16fp1'}, + 'rain_mm': {'cond': '', 'type': 'rawfloat'}, + 'supply_v': {'cond': 'ADC_EN', 'type': 'uint16'}, + 'battery_v': {'cond': 'PIN_ADC3_IN', 'type': 'uint16'}, + 'water_temp_c': {'cond': 'ONEWIRE_EN', 'type': 'temperature'}, + 'indoor_temp_c': {'cond': 'THEENGSDECODER_EN', 'type': 'temperature'}, + 'indoor_humidity': {'cond': 'THEENGSDECODER_EN', 'type': 'uint8'}, + 'indoor_temp_c': {'cond': 'MITHERMOMETER_EN', 'type': 'temperature'}, + 'indoor_humidity': {'cond': 'MITHERMOMETER_EN', 'type': 'uint8'}, + 'soil_temp_c': {'cond': 'SOILSENSOR_EN', 'type': 'temperature'}, + 'soil_moisture': {'cond': 'SOILSENSOR_EN', 'type': 'uint8'}, + 'rain_hr': {'cond': 'RAINDATA_EN', 'type': 'rawfloat'}, + 'rain_day': {'cond': 'RAINDATA_EN', 'type': 'rawfloat'}, + 'rain_week': {'cond': 'RAINDATA_EN', 'type': 'rawfloat'}, + 'rain_mon': {'cond': 'RAINDATA_EN', 'type': 'rawfloat'}, + 'adc0_v': {'cond': 'PIN_ADC0_IN', 'type': 'uint16'}, + 'adc1_v': {'cond': 'PIN_ADC1_IN', 'type': 'uint16'}, + 'adc2_v': {'cond': 'PIN_ADC2_IN', 'type': 'uint16'}, + 'distance_mm': {'cond': 'DISTANCESENSOR_EN', 'type': 'uint16'}, + 'lightning_time': {'cond': 'LIGHTNINGSENSOR_EN', 'type': 'unixtime'}, + 'lightning_count': {'cond': 'LIGHTNINGSENSOR_EN', 'type': 'uint16'}, + 'lightning_distance_km': {'cond': 'LIGHTNINGSENSOR_EN', 'type': 'uint8'}, +} + +header = ''' + return decode( + bytes, +'''[1:-1] + +types = ' ' * 8 + '[\n' + +keys = ' ' * 8 + '[\n' + +footer = ''' + ); +'''[1:-1] + +# Process input from C-preprocessor +list=[] +for line in sys.stdin: + line = line.removeprefix('#define') + + # remove leading whitespaces + line = line.lstrip() + + # remove trailing whitespaces + line = line.strip() + + # remove whitespaces followed by non-whitespaces at end of line + # e.g. "PIN_ADC0_IN A3" -> PIN_ADC0_IN + line = re.sub(r'\s+\S+$', '', line) + + # Add all known features from input to the list + if line in features: + list.append(line) + +# Print the list for debugging +#print(list) + +# Generate the output +for k,v in generator.items(): + if v['cond'] == '' or v['cond'] in list: + types += ' ' * 12 + v['type'] + ',\n' + keys += ' ' * 12 + k + ',\n' + +# Print the result +print(header) +print(types) +print(' ' * 8 + '],') +print(keys) +print(footer) diff --git a/scripts/helium_decoder.js b/scripts/helium_decoder.js new file mode 100644 index 0000000..1e9d124 --- /dev/null +++ b/scripts/helium_decoder.js @@ -0,0 +1,250 @@ +// Bresser Weather Sensor Decoder +function bws_decoder(bytes) { + // bytes is of type Buffer + + // IMPORTANT: paste code from src/decoder.js here + var bytesToInt = function (bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << (x * 8)); + } + return i; + }; + + var unixtime = function (bytes) { + if (bytes.length !== unixtime.BYTES) { + throw new Error('Unix time must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + unixtime.BYTES = 4; + + var uint8 = function (bytes) { + if (bytes.length !== uint8.BYTES) { + throw new Error('int must have exactly 1 byte'); + } + return bytesToInt(bytes); + }; + uint8.BYTES = 1; + + var uint16 = function (bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + return bytesToInt(bytes); + }; + uint16.BYTES = 2; + + var uint16fp1 = function (bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + var res = bytesToInt(bytes) * 0.1; + return res.toFixed(1); + }; + uint16fp1.BYTES = 2; + + var uint32 = function (bytes) { + if (bytes.length !== uint32.BYTES) { + throw new Error('int must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + uint32.BYTES = 4; + + var latLng = function (bytes) { + if (bytes.length !== latLng.BYTES) { + throw new Error('Lat/Long must have exactly 8 bytes'); + } + + var lat = bytesToInt(bytes.slice(0, latLng.BYTES / 2)); + var lng = bytesToInt(bytes.slice(latLng.BYTES / 2, latLng.BYTES)); + + return [lat / 1e6, lng / 1e6]; + }; + latLng.BYTES = 8; + + var temperature = function (bytes) { + if (bytes.length !== temperature.BYTES) { + throw new Error('Temperature must have exactly 2 bytes'); + } + var isNegative = bytes[0] & 0x80; + var b = ('00000000' + Number(bytes[0]).toString(2)).slice(-8) + + ('00000000' + Number(bytes[1]).toString(2)).slice(-8); + if (isNegative) { + var arr = b.split('').map(function (x) { return !Number(x); }); + for (var i = arr.length - 1; i > 0; i--) { + arr[i] = !arr[i]; + if (arr[i]) { + break; + } + } + b = arr.map(Number).join(''); + } + var t = parseInt(b, 2); + if (isNegative) { + t = -t; + } + t = t / 1e2; + return t.toFixed(1); + }; + temperature.BYTES = 2; + + var humidity = function (bytes) { + if (bytes.length !== humidity.BYTES) { + throw new Error('Humidity must have exactly 2 bytes'); + } + + var h = bytesToInt(bytes); + return h / 1e2; + }; + humidity.BYTES = 2; + + // Based on https://stackoverflow.com/a/37471538 by Ilya Bursov + // quoted by Arjan here https://www.thethingsnetwork.org/forum/t/decode-float-sent-by-lopy-as-node/8757 + function rawfloat(bytes) { + if (bytes.length !== rawfloat.BYTES) { + throw new Error('Float must have exactly 4 bytes'); + } + // JavaScript bitwise operators yield a 32 bits integer, not a float. + // Assume LSB (least significant byte first). + var bits = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0]; + var sign = (bits >>> 31 === 0) ? 1.0 : -1.0; + var e = bits >>> 23 & 0xff; + var m = (e === 0) ? (bits & 0x7fffff) << 1 : (bits & 0x7fffff) | 0x800000; + var f = sign * m * Math.pow(2, e - 150); + return f.toFixed(1); + } + rawfloat.BYTES = 4; + + var bitmap_node = function (byte) { + if (byte.length !== bitmap_node.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + + return ['res7', 'res6', 'res5', 'res4', 'res3', 'res2', 'res1', 'res0'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_node.BYTES = 1; + + var bitmap_sensors = function (byte) { + if (byte.length !== bitmap_sensors.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + // Only Weather Sensor + //return ['res5', 'res4', 'res3', 'res2', 'res1', 'res0', 'dec_ok', 'batt_ok'] + // Weather Sensor + MiThermo (BLE) Sensor + //return ['res4', 'res3', 'res2', 'res1', 'res0', 'ble_ok', 'dec_ok', 'batt_ok'] + // Weather Sensor, Soil Sensor and MiThermo (BLE) Sensor + return ['res0', 'ble_ok', 'ls_dec_ok', 'ls_batt_ok', 's1_dec_ok', 's1_batt_ok', 'ws_dec_ok', 'ws_batt_ok'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_sensors.BYTES = 1; + + var decode = function (bytes, mask, names) { + + var maskLength = mask.reduce(function (prev, cur) { + return prev + cur.BYTES; + }, 0); + if (bytes.length < maskLength) { + throw new Error('Mask length is ' + maskLength + ' whereas input is ' + bytes.length); + } + + names = names || []; + var offset = 0; + return mask + .map(function (decodeFn) { + var current = bytes.slice(offset, offset += decodeFn.BYTES); + return decodeFn(current); + }) + .reduce(function (prev, cur, idx) { + prev[names[idx] || idx] = cur; + return prev; + }, {}); + }; + + if (typeof module === 'object' && typeof module.exports !== 'undefined') { + module.exports = { + unixtime: unixtime, + uint8: uint8, + uint16: uint16, + uint32: uint32, + temperature: temperature, + humidity: humidity, + latLng: latLng, + bitmap_node: bitmap_node, + bitmap_sensors: bitmap_sensors, + rawfloat: rawfloat, + uint16fp1: uint16fp1, + decode: decode + }; + } + + // see assignment to 'bitmap' variable for status bit names + // return decode( + // bytes, + // [bitmap, temperature, uint8, uint16, uint16 ], // types + // ['status', 'air_temp_c', 'humidity', 'supply_v', 'battery_v' ] // JSON elements + // ); + return decode( + bytes, + [ bitmap_node, bitmap_sensors, temperature, uint8, + uint16fp1, uint16fp1, uint16fp1, + rawfloat, uint16, uint16, temperature, + temperature, uint8, temperature, uint8, + rawfloat, rawfloat, rawfloat, rawfloat, + unixtime, uint16, uint8 + ], + [ 'status_node', 'status', 'air_temp_c', 'humidity', + 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + 'rain_mm', 'supply_v', 'battery_v', 'water_temp_c', + 'indoor_temp_c', 'indoor_humidity', 'soil_temp_c', 'soil_moisture', + 'rain_hr', 'rain_day', 'rain_week', 'rain_mon', + 'lightning_time', 'lightning_count', 'lightning_distance_km' + ] + ); + +} + + +function Decoder(bytes, port, uplink_info) { + var decoded = {}; + + decoded = bws_decoder(bytes); + + /* + The uplink_info variable is an OPTIONAL third parameter that provides the following: + + uplink_info = { + type: "join", + uuid: , + id: , + name: , + dev_eui: , + app_eui: , + metadata: {...}, + fcnt: , + reported_at: , + port: , + devaddr: , + hotspots: {...}, + hold_time: + } + */ + + if (uplink_info) { + // do something with uplink_info fields + } + + return decoded; +} diff --git a/scripts/ttn_decoder_distance.js b/scripts/ttn_decoder_distance.js new file mode 100644 index 0000000..09b4cbf --- /dev/null +++ b/scripts/ttn_decoder_distance.js @@ -0,0 +1,270 @@ +function ttn_decoder_fp(bytes) { + // bytes is of type Buffer + + // IMPORTANT: paste code from src/decoder.js here + var bytesToInt = function (bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << (x * 8)); + } + return i; + }; + + var unixtime = function (bytes) { + if (bytes.length !== unixtime.BYTES) { + throw new Error('Unix time must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + unixtime.BYTES = 4; + + var uint8 = function (bytes) { + if (bytes.length !== uint8.BYTES) { + throw new Error('int must have exactly 1 byte'); + } + return bytesToInt(bytes); + }; + uint8.BYTES = 1; + + var uint16 = function (bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + return bytesToInt(bytes); + }; + uint16.BYTES = 2; + + var uint16fp1 = function (bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + var res = bytesToInt(bytes) * 0.1; + return res.toFixed(1); + }; + uint16fp1.BYTES = 2; + + var uint32 = function (bytes) { + if (bytes.length !== uint32.BYTES) { + throw new Error('int must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + uint32.BYTES = 4; + + var latLng = function (bytes) { + if (bytes.length !== latLng.BYTES) { + throw new Error('Lat/Long must have exactly 8 bytes'); + } + + var lat = bytesToInt(bytes.slice(0, latLng.BYTES / 2)); + var lng = bytesToInt(bytes.slice(latLng.BYTES / 2, latLng.BYTES)); + + return [lat / 1e6, lng / 1e6]; + }; + latLng.BYTES = 8; + + var temperature = function (bytes) { + if (bytes.length !== temperature.BYTES) { + throw new Error('Temperature must have exactly 2 bytes'); + } + var isNegative = bytes[0] & 0x80; + var b = ('00000000' + Number(bytes[0]).toString(2)).slice(-8) + + ('00000000' + Number(bytes[1]).toString(2)).slice(-8); + if (isNegative) { + var arr = b.split('').map(function (x) { return !Number(x); }); + for (var i = arr.length - 1; i > 0; i--) { + arr[i] = !arr[i]; + if (arr[i]) { + break; + } + } + b = arr.map(Number).join(''); + } + var t = parseInt(b, 2); + if (isNegative) { + t = -t; + } + t = t / 1e2; + return t.toFixed(1); + }; + temperature.BYTES = 2; + + var humidity = function (bytes) { + if (bytes.length !== humidity.BYTES) { + throw new Error('Humidity must have exactly 2 bytes'); + } + + var h = bytesToInt(bytes); + return h / 1e2; + }; + humidity.BYTES = 2; + + // Based on https://stackoverflow.com/a/37471538 by Ilya Bursov + // quoted by Arjan here https://www.thethingsnetwork.org/forum/t/decode-float-sent-by-lopy-as-node/8757 + function rawfloat(bytes) { + if (bytes.length !== rawfloat.BYTES) { + throw new Error('Float must have exactly 4 bytes'); + } + // JavaScript bitwise operators yield a 32 bits integer, not a float. + // Assume LSB (least significant byte first). + var bits = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0]; + var sign = (bits >>> 31 === 0) ? 1.0 : -1.0; + var e = bits >>> 23 & 0xff; + var m = (e === 0) ? (bits & 0x7fffff) << 1 : (bits & 0x7fffff) | 0x800000; + var f = sign * m * Math.pow(2, e - 150); + return f.toFixed(1); + } + rawfloat.BYTES = 4; + + var bitmap_node = function (byte) { + if (byte.length !== bitmap_node.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + + return ['res7', 'res6', 'res5', 'res4', 'res3', 'res2', 'res1', 'res0'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_node.BYTES = 1; + + var bitmap_sensors = function (byte) { + if (byte.length !== bitmap_sensors.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + // Only Weather Sensor + //return ['res5', 'res4', 'res3', 'res2', 'res1', 'res0', 'dec_ok', 'batt_ok'] + // Weather Sensor + MiThermo (BLE) Sensor + //return ['res4', 'res3', 'res2', 'res1', 'res0', 'ble_ok', 'dec_ok', 'batt_ok'] + // Weather Sensor, Soil Sensor, Lightning Sensor and MiThermo (BLE) Sensor + return ['res0', 'ble_ok', 'ls_dec_ok', 'ls_batt_ok', 's1_dec_ok', 's1_batt_ok', 'ws_dec_ok', 'ws_batt_ok'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_sensors.BYTES = 1; + + var decode = function (bytes, mask, names) { + + var maskLength = mask.reduce(function (prev, cur) { + return prev + cur.BYTES; + }, 0); + if (bytes.length < maskLength) { + throw new Error('Mask length is ' + maskLength + ' whereas input is ' + bytes.length); + } + + names = names || []; + var offset = 0; + return mask + .map(function (decodeFn) { + var current = bytes.slice(offset, offset += decodeFn.BYTES); + return decodeFn(current); + }) + .reduce(function (prev, cur, idx) { + prev[names[idx] || idx] = cur; + return prev; + }, {}); + }; + + if (typeof module === 'object' && typeof module.exports !== 'undefined') { + module.exports = { + unixtime: unixtime, + uint8: uint8, + uint16: uint16, + uint32: uint32, + temperature: temperature, + humidity: humidity, + latLng: latLng, + bitmap_node: bitmap_node, + bitmap_sensors: bitmap_sensors, + rawfloat: rawfloat, + uint16fp1: uint16fp1, + decode: decode + }; + } + + // Weather Sensor, Battery Voltage and Water Temperature (OneWire) + // return decode( + // bytes, + // [bitmap, temperature, uint8, uint16fp1, uint16fp1, uint16fp1, + // rawfloat, uint16, temperature], + // ['status', 'air_temp_c', 'humidity', 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + // 'rain_mm', 'battery_v', 'water_temp_c' ] + // ); + + // Weather Sensor, Supply Voltage, Battery Voltage, Water Temperature (OneWire), + // MiThermo Indoor Temperature+Humidity (BLE) + // return decode( + // bytes, + // [bitmap, temperature, uint8, uint16fp1, uint16fp1, uint16fp1, + // rawfloat, uint16, uint16, temperature, temperature, uint8 ], + // ['status', 'air_temp_c', 'humidity', 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + // 'rain_mm', 'supply_v', 'battery_v', 'water_temp_c', 'indoor_temp_c', 'indoor_humidity' ] + // ); + // Weather Sensor, Supply Voltage, Battery Voltage, Water Temperature (OneWire), + // MiThermo Indoor Temperature+Humidity (BLE), Bresser Soil Temperature+Moisture + // return decode( + // bytes, + // [bitmap, temperature, uint8, uint16fp1, uint16fp1, uint16fp1, + // rawfloat, uint16, uint16, temperature, temperature, uint8, + // temperature, uint8, rawfloat, rawfloat, rawfloat, rawfloat + // ], + // ['status', 'air_temp_c', 'humidity', 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + // 'rain_mm', 'supply_v', 'battery_v', 'water_temp_c', 'indoor_temp_c', 'indoor_humidity', + // 'soil_temp_c', 'soil_moisture', 'rain_hr', 'rain_day', 'rain_week', 'rain_mon' + // ] + // ); + + // Weather Sensor, Supply Voltage, Battery Voltage, Water Temperature (OneWire), + // MiThermo Indoor Temperature+Humidity (BLE), Bresser Soil Temperature+Moisture, + // Hourly (past 60 minutes)/Daily/Weekly/Monthly Rainfall + // return decode( + // bytes, + // [bitmap, temperature, uint8, uint16fp1, uint16fp1, uint16fp1, + // rawfloat, uint16, uint16, temperature, temperature, uint8, + // temperature, uint8, rawfloat, rawfloat, rawfloat, rawfloat + // ], + // ['status', 'air_temp_c', 'humidity', 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + // 'rain_mm', 'supply_v', 'battery_v', 'water_temp_c', 'indoor_temp_c', 'indoor_humidity', + // 'soil_temp_c', 'soil_moisture', 'rain_hr', 'rain_day', 'rain_week', 'rain_mon' + // ] + // ); + + // Weather Sensor, Supply Voltage, Battery Voltage, Water Temperature (OneWire), + // MiThermo Indoor Temperature+Humidity (BLE), Bresser Soil Temperature+Moisture, + // Hourly (past 60 minutes)/Daily/Weekly/Monthly Rainfall, Distance (or Fill Level) + return decode( + bytes, + [ bitmap_node, bitmap_sensors, temperature, uint8, + uint16fp1, uint16fp1, uint16fp1, + rawfloat, uint16, uint16, temperature, + temperature, uint8, temperature, uint8, + rawfloat, rawfloat, rawfloat, rawfloat, + uint16 + ], + [ 'status_node', 'status', 'air_temp_c', 'humidity', + 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + 'rain_mm', 'supply_v', 'battery_v', 'water_temp_c', + 'indoor_temp_c', 'indoor_humidity', 'soil_temp_c', 'soil_moisture', + 'rain_hr', 'rain_day', 'rain_week', 'rain_mon', + 'distance_mm' + ] + ); + +} + +function decodeUplink(input) { + return { + data: { + bytes: ttn_decoder_fp(input.bytes) + }, + warnings: [], + errors: [] + }; +} diff --git a/scripts/ttn_decoder_fp.js b/scripts/ttn_decoder_fp.js new file mode 100644 index 0000000..82c3595 --- /dev/null +++ b/scripts/ttn_decoder_fp.js @@ -0,0 +1,236 @@ +function ttn_decoder_fp(bytes) { + // bytes is of type Buffer + + // IMPORTANT: paste code from src/decoder.js here + var bytesToInt = function (bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << (x * 8)); + } + return i; + }; + + var unixtime = function (bytes) { + if (bytes.length !== unixtime.BYTES) { + throw new Error('Unix time must have exactly 4 bytes'); + } + //return bytesToInt(bytes); + dateObj = new Date(bytesToInt(bytes) * 1000); + return dateObj.toISOString(); + }; + unixtime.BYTES = 4; + + var uint8 = function (bytes) { + if (bytes.length !== uint8.BYTES) { + throw new Error('int must have exactly 1 byte'); + } + return bytesToInt(bytes); + }; + uint8.BYTES = 1; + + var uint16 = function (bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + return bytesToInt(bytes); + }; + uint16.BYTES = 2; + + var uint16fp1 = function (bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + var res = bytesToInt(bytes) * 0.1; + return res.toFixed(1); + }; + uint16fp1.BYTES = 2; + + var uint32 = function (bytes) { + if (bytes.length !== uint32.BYTES) { + throw new Error('int must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + uint32.BYTES = 4; + + var latLng = function (bytes) { + if (bytes.length !== latLng.BYTES) { + throw new Error('Lat/Long must have exactly 8 bytes'); + } + + var lat = bytesToInt(bytes.slice(0, latLng.BYTES / 2)); + var lng = bytesToInt(bytes.slice(latLng.BYTES / 2, latLng.BYTES)); + + return [lat / 1e6, lng / 1e6]; + }; + latLng.BYTES = 8; + + var temperature = function (bytes) { + if (bytes.length !== temperature.BYTES) { + throw new Error('Temperature must have exactly 2 bytes'); + } + var isNegative = bytes[0] & 0x80; + var b = ('00000000' + Number(bytes[0]).toString(2)).slice(-8) + + ('00000000' + Number(bytes[1]).toString(2)).slice(-8); + if (isNegative) { + var arr = b.split('').map(function (x) { return !Number(x); }); + for (var i = arr.length - 1; i > 0; i--) { + arr[i] = !arr[i]; + if (arr[i]) { + break; + } + } + b = arr.map(Number).join(''); + } + var t = parseInt(b, 2); + if (isNegative) { + t = -t; + } + t = t / 1e2; + return t.toFixed(1); + }; + temperature.BYTES = 2; + + var humidity = function (bytes) { + if (bytes.length !== humidity.BYTES) { + throw new Error('Humidity must have exactly 2 bytes'); + } + + var h = bytesToInt(bytes); + return h / 1e2; + }; + humidity.BYTES = 2; + + // Based on https://stackoverflow.com/a/37471538 by Ilya Bursov + // quoted by Arjan here https://www.thethingsnetwork.org/forum/t/decode-float-sent-by-lopy-as-node/8757 + function rawfloat(bytes) { + if (bytes.length !== rawfloat.BYTES) { + throw new Error('Float must have exactly 4 bytes'); + } + // JavaScript bitwise operators yield a 32 bits integer, not a float. + // Assume LSB (least significant byte first). + var bits = bytes[3] << 24 | bytes[2] << 16 | bytes[1] << 8 | bytes[0]; + var sign = (bits >>> 31 === 0) ? 1.0 : -1.0; + var e = bits >>> 23 & 0xff; + var m = (e === 0) ? (bits & 0x7fffff) << 1 : (bits & 0x7fffff) | 0x800000; + var f = sign * m * Math.pow(2, e - 150); + return f.toFixed(1); + } + rawfloat.BYTES = 4; + + var bitmap_node = function (byte) { + if (byte.length !== bitmap_node.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + + return ['res7', 'res6', 'res5', 'res4', 'res3', 'res2', 'res1', 'res0'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_node.BYTES = 1; + + var bitmap_sensors = function (byte) { + if (byte.length !== bitmap_sensors.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + // Only Weather Sensor + //return ['res5', 'res4', 'res3', 'res2', 'res1', 'res0', 'dec_ok', 'batt_ok'] + // Weather Sensor + MiThermo (BLE) Sensor + //return ['res4', 'res3', 'res2', 'res1', 'res0', 'ble_ok', 'dec_ok', 'batt_ok'] + // Weather Sensor, Soil Sensor and MiThermo (BLE) Sensor + return ['res0', 'ble_ok', 'ls_dec_ok', 'ls_batt_ok', 's1_dec_ok', 's1_batt_ok', 'ws_dec_ok', 'ws_batt_ok'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_sensors.BYTES = 1; + + var decode = function (bytes, mask, names) { + + var maskLength = mask.reduce(function (prev, cur) { + return prev + cur.BYTES; + }, 0); + if (bytes.length < maskLength) { + throw new Error('Mask length is ' + maskLength + ' whereas input is ' + bytes.length); + } + + names = names || []; + var offset = 0; + return mask + .map(function (decodeFn) { + var current = bytes.slice(offset, offset += decodeFn.BYTES); + return decodeFn(current); + }) + .reduce(function (prev, cur, idx) { + prev[names[idx] || idx] = cur; + return prev; + }, {}); + }; + + if (typeof module === 'object' && typeof module.exports !== 'undefined') { + module.exports = { + unixtime: unixtime, + uint8: uint8, + uint16: uint16, + uint32: uint32, + temperature: temperature, + humidity: humidity, + latLng: latLng, + bitmap_node: bitmap_node, + bitmap_sensors: bitmap_sensors, + rawfloat: rawfloat, + uint16fp1: uint16fp1, + decode: decode + }; + } + + // Weather Sensor, Battery Voltage and Water Temperature (OneWire) + // return decode( + // bytes, + // [bitmap, temperature, uint8, uint16fp1, uint16fp1, uint16fp1, + // rawfloat, uint16, temperature], + // ['status', 'air_temp_c', 'humidity', 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + // 'rain_mm', 'battery_v', 'water_temp_c' ] + // ); + + + // Weather Sensor, Supply Voltage, Battery Voltage, Water Temperature (OneWire), + // MiThermo Indoor Temperature+Humidity (BLE), Bresser Soil Temperature+Moisture, + // Hourly (past 60 minutes)/Daily/Weekly/Monthly Rainfall, [Distance (or Fill Level)] + // Bresser Lightning Sensor + return decode( + bytes, + [ bitmap_node, bitmap_sensors, temperature, uint8, + uint16fp1, uint16fp1, uint16fp1, + rawfloat, uint16, uint16, temperature, + temperature, uint8, temperature, uint8, + rawfloat, rawfloat, rawfloat, rawfloat, + unixtime, uint16, uint8 + ], + [ 'status_node', 'status', 'air_temp_c', 'humidity', + 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + 'rain_mm', 'supply_v', 'battery_v', 'water_temp_c', + 'indoor_temp_c', 'indoor_humidity', 'soil_temp_c', 'soil_moisture', + 'rain_hr', 'rain_day', 'rain_week', 'rain_mon', + 'lightning_time', 'lightning_events', 'lightning_distance_km' + ] + ); + +} + +function decodeUplink(input) { + return { + data: { + bytes: ttn_decoder_fp(input.bytes) + }, + warnings: [], + errors: [] + }; +} diff --git a/scripts/ttn_downlink_formatter.js b/scripts/ttn_downlink_formatter.js new file mode 100644 index 0000000..b829a2d --- /dev/null +++ b/scripts/ttn_downlink_formatter.js @@ -0,0 +1,238 @@ +/////////////////////////////////////////////////////////////////////////////// +// ttn_downlink_formatter.js +// +// Bresser 5-in-1/6-in-1 868 MHz Weather Sensor Radio Receiver +// based on ESP32 and RFM95W - +// sends data to a LoRaWAN network (e.g. The Things Network) +// +// This script allows to send downlink data to the The Things Network with +// commands, parameters and responses are encoded in JSON format. +// Commands shall be sent via FPort=1. +// +// Commands: +// ---------- +// {"cmd": "CMD_SET_WEATHERSENSOR_TIMEOUT", "timeout": } +// {"cmd": "CMD_SET_SLEEP_INTERVAL", "interval": } +// {"cmd": "CMD_SET_SLEEP_INTERVAL_LONG", "interval": } +// {"cmd": "CMD_GET_DATETIME"} +// {"cmd": "CMD_SET_DATETIME", "epoch": } +// {"cmd": "CMD_RESET_RAINGAUGE" [, "flags": ], +// {"cmd": "CMD_GET_CONFIG"} +// +// Responses: +// ----------- +// CMD_GET_CONFIG -> FPort=3: {"ws_timeout": , +// "sleep_interval": , +// "sleep_interval_long": } +// +// CMD_GET_DATETIME -> FPort=2: {"epoch": , "rtc_source":} +// +// : 0...255 +// : 0...65535 +// : unix epoch time, see https://www.epochconverter.com/ +// : 0...15 (1: hourly / 2: daily / 4: weekly / 8: monthly) +// : 0x00: GPS / 0x01: RTC / 0x02: LORA / 0x03: unsynched / 0x04: set (source unknown) +// +// +// Based on: +// --------- +// https://www.thethingsindustries.com/docs/integrations/payload-formatters/javascript/downlink/ +// +// created: 08/2023 +// +// +// MIT License +// +// Copyright (c) 2023 Matthias Prinke +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// +// History: +// 20230821 Created +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +// Commands +const cmd_code = new Map([ + ["CMD_SET_WEATHERSENSOR_TIMEOUT", 0xA0], + ["CMD_SET_SLEEP_INTERVAL", 0xA8], + ["CMD_SET_SLEEP_INTERVAL_LONG", 0xA9], + ["CMD_GET_DATETIME", 0x86], + ["CMD_SET_DATETIME", 0x88], + ["CMD_RESET_RAINGAUGE", 0xB0], + ["CMD_GET_CONFIG", 0xB1], +]); + +// Source of Real Time Clock setting +var rtc_source_code = { + 0x00: "GPS", + 0x01: "RTC", + 0x02: "LORA", + 0x03: "unsynched", + 0x04: "set (source unknown)" +}; + +function bytesToInt(bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << (x * 8)); + } + return i; +} + +// Big Endian +function bytesToIntBE(bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << ((bytes.length - 1 - x) * 8)); + } + return i; +} + +function uint8(bytes) { + if (bytes.length !== 1) { + throw new Error('uint8 must have exactly 1 byte'); + } + return bytesToInt(bytes); +} + +function uint16BE(bytes) { + if (bytes.length !== 2) { + throw new Error('uint16BE must have exactly 2 bytes'); + } + return bytesToIntBE(bytes); +} + +function uint32BE(bytes) { + if (bytes.length !== 4) { + throw new Error('uint32BE must have exactly 4 bytes'); + } + return bytesToIntBE(bytes); +} + +// Encode Downlink from JSON to bytes +function encodeDownlink(input) { + if ((input.data.cmd === "CMD_SET_SLEEP_INTERVAL") || + (input.data.cmd === "CMD_SET_SLEEP_INTERVAL_LONG")) { + + return { + bytes: [cmd_code.get(input.data.cmd), + input.data.interval >> 8, + input.data.interval & 0xFF + ], + fPort: 1, + warnings: [], + errors: [] + }; + } + else if (input.data.cmd === "CMD_SET_DATETIME") { + return { + bytes: [cmd_code.get(input.data.cmd), + input.data.epoch >> 24, + (input.data.epoch >> 16) & 0xFF, + (input.data.epoch >> 8) & 0xFF, + (input.data.epoch & 0xFF)], + fPort: 1, + warnings: [], + errors: [] + }; + } + else if ((input.data.cmd === "CMD_GET_CONFIG") || + (input.data.cmd === "CMD_GET_DATETIME")) { + return { + bytes: [cmd_code.get(input.data.cmd)], + fPort: 1, + warnings: [], + errors: [] + }; + } + else if (input.data.cmd === "CMD_SET_WEATHERSENSOR_TIMEOUT") { + return { + bytes: [cmd_code.get(input.data.cmd), input.data.timeout], + fPort: 1, + warnings: [], + errors: [] + }; + } else if (input.data.cmd === "CMD_RESET_RAINGAUGE") { + if (input.data.hasOwnProperty('flags')) { + return { + bytes: [cmd_code.get(input.data.cmd), input.data.flags], + fPort: 1, + warnings: [], + errors: [] + }; + } + else { + return { + bytes: [cmd_code.get(input.data.cmd)], + fPort: 1, + warnings: [], + errors: [] + }; + } + } else { + return { + bytes: [], + errors: ["unknown command"], + fPort: 1, + warnings: [] + }; + } +} + +// Decode Downlink from bytes to JSON +function decodeDownlink(input) { + switch (input.fPort) { + case 1: + for (const x of cmd_code.keys()) { + if (input.bytes[0] == cmd_code.get(x)) { + return { + cmd: x + }; + } + } + return { + cmd: [], + errors: ["unknown command"] + }; + case 2: + return { + data: { + unixtime: uint32BE(input.bytes.slice(0, 3)), + rtc_source: rtc_source_code[input.bytes[4]] + } + }; + case 3: + return { + data: { + ws_timeout: uint8(input.bytes[0]), + sleep_interval: uint16BE(input.bytes.slice(1, 2)), + sleep_interval_long: uint16BE(input.bytes.slice(3, 4)) + } + }; + default: + return { + errors: ["unknown FPort"] + }; + } +} diff --git a/scripts/ttn_uplink_formatter.js b/scripts/ttn_uplink_formatter.js new file mode 100644 index 0000000..17860c9 --- /dev/null +++ b/scripts/ttn_uplink_formatter.js @@ -0,0 +1,362 @@ + +/////////////////////////////////////////////////////////////////////////////// +// ttn_uplink_formatter.js +// +// Bresser 5-in-1/6-in-1 868 MHz Weather Sensor Radio Receiver +// based on ESP32 and RFM95W - +// sends data to a LoRaWAN network (e.g. The Things Network) +// +// This script allows to decode payload received from The Things Network - +// data (sent at fixed intervals) and responses (sent upon request) - +// from bytes to JSON. +// +// Commands: +// ---------- +// {"cmd": "CMD_SET_WEATHERSENSOR_TIMEOUT", "timeout": } +// {"cmd": "CMD_SET_SLEEP_INTERVAL", "interval": } +// {"cmd": "CMD_SET_SLEEP_INTERVAL_LONG", "interval": } +// {"cmd": "CMD_GET_DATETIME"} +// {"cmd": "CMD_SET_DATETIME", "epoch": } +// {"cmd": "CMD_RESET_RAINGAUGE" [, "flags": ], +// {"cmd": "CMD_GET_CONFIG"} +// +// Responses: +// ----------- +// CMD_GET_CONFIG -> FPort=3: {"ws_timeout": , +// "sleep_interval": , +// "sleep_interval_long": } +// +// CMD_GET_DATETIME -> FPort=2: {"epoch": , "rtc_source":} +// +// : 0...255 +// : 0...65535 +// : unix epoch time, see https://www.epochconverter.com/ +// : 0...15 (1: hourly / 2: daily / 4: weekly / 8: monthly) +// : 0x00: GPS / 0x01: RTC / 0x02: LORA / 0x03: unsynched / 0x04: set (source unknown) +// +// +// Based on: +// --------- +// ttn_decoder_fp.js +// +// created: 08/2023 +// +// +// MIT License +// +// Copyright (c) 2023 Matthias Prinke +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// +// History: +// 20230821 Created +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +function ttn_decoder(bytes, port) { + // bytes is of type Buffer + + var rtc_source_code = { + 0x00: "GPS", + 0x01: "RTC", + 0x02: "LORA", + 0x03: "unsynched", + 0x04: "set (source unknown)" + }; + + var rtc_source = function (bytes) { + if (bytes.length !== rtc_source.BYTES) { + throw new Error('rtc_source must have exactly 1 byte'); + } + return rtc_source_code[bytes[0]]; + }; + rtc_source.BYTES = 1; + + var bytesToInt = function(bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << (x * 8)); + } + return i; + }; + + // Big Endian + var bytesToIntBE = function(bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << ((bytes.length-1-x) * 8)); + } + return i; + }; + + var unixtime = function(bytes) { + if (bytes.length !== unixtime.BYTES) { + throw new Error('Unix time must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + unixtime.BYTES = 4; + + var uint8 = function(bytes) { + if (bytes.length !== uint8.BYTES) { + throw new Error('int must have exactly 1 byte'); + } + return bytesToInt(bytes); + }; + uint8.BYTES = 1; + + var uint16 = function(bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + return bytesToInt(bytes); + }; + uint16.BYTES = 2; + + var uint16fp1 = function(bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + var res = bytesToInt(bytes) * 0.1; + return res.toFixed(1); + }; + uint16fp1.BYTES = 2; + + var uint32 = function(bytes) { + if (bytes.length !== uint32.BYTES) { + throw new Error('int must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + uint32.BYTES = 4; + + var uint16BE = function(bytes) { + if (bytes.length !== uint16BE.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + return bytesToIntBE(bytes); + }; + uint16BE.BYTES = 2; + + var uint32BE = function(bytes) { + if (bytes.length !== uint32BE.BYTES) { + throw new Error('int must have exactly 4 bytes'); + } + return bytesToIntBE(bytes); + }; + uint32BE.BYTES = 4; + + var latLng = function(bytes) { + if (bytes.length !== latLng.BYTES) { + throw new Error('Lat/Long must have exactly 8 bytes'); + } + + var lat = bytesToInt(bytes.slice(0, latLng.BYTES / 2)); + var lng = bytesToInt(bytes.slice(latLng.BYTES / 2, latLng.BYTES)); + + return [lat / 1e6, lng / 1e6]; + }; + latLng.BYTES = 8; + + var temperature = function(bytes) { + if (bytes.length !== temperature.BYTES) { + throw new Error('Temperature must have exactly 2 bytes'); + } + var isNegative = bytes[0] & 0x80; + var b = ('00000000' + Number(bytes[0]).toString(2)).slice(-8) + + ('00000000' + Number(bytes[1]).toString(2)).slice(-8); + if (isNegative) { + var arr = b.split('').map(function(x) { return !Number(x); }); + for (var i = arr.length - 1; i > 0; i--) { + arr[i] = !arr[i]; + if (arr[i]) { + break; + } + } + b = arr.map(Number).join(''); + } + var t = parseInt(b, 2); + if (isNegative) { + t = -t; + } + t = t / 1e2; + return t.toFixed(1); + }; + temperature.BYTES = 2; + + var humidity = function(bytes) { + if (bytes.length !== humidity.BYTES) { + throw new Error('Humidity must have exactly 2 bytes'); + } + + var h = bytesToInt(bytes); + return h / 1e2; + }; + humidity.BYTES = 2; + + // Based on https://stackoverflow.com/a/37471538 by Ilya Bursov + // quoted by Arjan here https://www.thethingsnetwork.org/forum/t/decode-float-sent-by-lopy-as-node/8757 + function rawfloat(bytes) { + if (bytes.length !== rawfloat.BYTES) { + throw new Error('Float must have exactly 4 bytes'); + } + // JavaScript bitwise operators yield a 32 bits integer, not a float. + // Assume LSB (least significant byte first). + var bits = bytes[3]<<24 | bytes[2]<<16 | bytes[1]<<8 | bytes[0]; + var sign = (bits>>>31 === 0) ? 1.0 : -1.0; + var e = bits>>>23 & 0xff; + var m = (e === 0) ? (bits & 0x7fffff)<<1 : (bits & 0x7fffff) | 0x800000; + var f = sign * m * Math.pow(2, e - 150); + return f.toFixed(1); + } + rawfloat.BYTES = 4; + + var bitmap_node = function (byte) { + if (byte.length !== bitmap_node.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + + return ['res7', 'res6', 'res5', 'res4', 'res3', 'res2', 'res1', 'res0'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_node.BYTES = 1; + + var bitmap_sensors = function (byte) { + if (byte.length !== bitmap_sensors.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + // Only Weather Sensor + //return ['res5', 'res4', 'res3', 'res2', 'res1', 'res0', 'dec_ok', 'batt_ok'] + // Weather Sensor + MiThermo (BLE) Sensor + //return ['res4', 'res3', 'res2', 'res1', 'res0', 'ble_ok', 'dec_ok', 'batt_ok'] + // Weather Sensor, Soil Sensor and MiThermo (BLE) Sensor + return ['res0', 'ble_ok', 'ls_dec_ok', 'ls_batt_ok', 's1_dec_ok', 's1_batt_ok', 'ws_dec_ok', 'ws_batt_ok'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_sensors.BYTES = 1; + + var decode = function(bytes, mask, names) { + + var maskLength = mask.reduce(function(prev, cur) { + return prev + cur.BYTES; + }, 0); + if (bytes.length < maskLength) { + throw new Error('Mask length is ' + maskLength + ' whereas input is ' + bytes.length); + } + + names = names || []; + var offset = 0; + return mask + .map(function(decodeFn) { + var current = bytes.slice(offset, offset += decodeFn.BYTES); + return decodeFn(current); + }) + .reduce(function(prev, cur, idx) { + prev[names[idx] || idx] = cur; + return prev; + }, {}); + }; + + if (typeof module === 'object' && typeof module.exports !== 'undefined') { + module.exports = { + unixtime: unixtime, + uint8: uint8, + uint16: uint16, + uint32: uint32, + uint16BE: uint16BE, + uint32BE: uint32BE, + temperature: temperature, + humidity: humidity, + latLng: latLng, + bitmap_node: bitmap_node, + bitmap_sensors: bitmap_sensors, + rawfloat: rawfloat, + uint16fp1: uint16fp1, + rtc_source: rtc_source, + decode: decode + }; + } + + if (bytes.length === 1) { + return modbus(bytes); + } + + + if (port === 1) { + return decode( + bytes, + [ bitmap_node, bitmap_sensors, temperature, uint8, + uint16fp1, uint16fp1, uint16fp1, + rawfloat, uint16, temperature, + temperature, uint8, temperature, uint8, + rawfloat, rawfloat, rawfloat, rawfloat, + unixtime, uint16, uint8 + ], + [ 'status_node', 'status', 'air_temp_c', 'humidity', + 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', + 'rain_mm', 'supply_v', 'water_temp_c', + 'indoor_temp_c', 'indoor_humidity', 'soil_temp_c', 'soil_moisture', + 'rain_hr', 'rain_day', 'rain_week', 'rain_mon', + 'lightning_time', 'lightning_events', 'lightning_distance_km' + ] + ); + } else if (port === 2) { + return decode( + bytes, + [uint32BE, rtc_source + ], + ['unixtime', 'rtc_source' + ] + ); + } else if (port === 3) { + return decode( + bytes, + [ uint8, uint16BE, uint16BE + ], + ['ws_timeout', 'sleep_interval', 'sleep_interval_long' + ] + ); + } + +} + + +function decodeUplink(input) { + return { + data: { + bytes: ttn_decoder(input.bytes, input.fPort) + }, + warnings: [], + errors: [] + }; +}