diff --git a/vendors/elsys/codecs/elsys.js b/vendors/elsys/codecs/elsys.js new file mode 100644 index 0000000..614810d --- /dev/null +++ b/vendors/elsys/codecs/elsys.js @@ -0,0 +1,211 @@ +/* + ELSYS payload decoder for ChirpStack v4. + Based on the official Elsys payload decoder. + www.elsys.se + peter@elsys.se + + Adapted for chirpstack-device-profiles decodeUplink/encodeDownlink format. +*/ + +var TYPE_TEMP = 0x01; // temp 2 bytes -3276.8°C to 3276.7°C +var TYPE_RH = 0x02; // Humidity 1 byte 0-100% +var TYPE_ACC = 0x03; // Acceleration 3 bytes X,Y,Z -128 to 127 +/-63=1G +var TYPE_LIGHT = 0x04; // Light 2 bytes 0-65535 Lux +var TYPE_MOTION = 0x05; // No of motion 1 byte 0-255 +var TYPE_CO2 = 0x06; // CO2 2 bytes 0-65535 ppm +var TYPE_VDD = 0x07; // Battery mV 2 bytes 0-65535 +var TYPE_ANALOG1 = 0x08; // Analog input 1 2 bytes 0-65535 mV +var TYPE_GPS = 0x09; // GPS 6 bytes lat(3),long(3) +var TYPE_PULSE1 = 0x0A; // Pulse input 1 relative 2 bytes +var TYPE_PULSE1_ABS = 0x0B; // Pulse input 1 absolute 4 bytes +var TYPE_EXT_TEMP1 = 0x0C; // External temp 1 2 bytes -3276.8°C to 3276.7°C +var TYPE_EXT_DIGITAL = 0x0D; // Digital input 1 byte +var TYPE_EXT_DISTANCE = 0x0E; // Distance sensor 2 bytes 0-65535 mm +var TYPE_ACC_MOTION = 0x0F; // Acc motion 1 byte 0-255 +var TYPE_IR_TEMP = 0x10; // IR temperature 4 bytes internal(2),external(2) +var TYPE_OCCUPANCY = 0x11; // Occupancy 1 byte +var TYPE_WATERLEAK = 0x12; // Water leak 1 byte 0-255 +var TYPE_GRIDEYE = 0x13; // Grideye data 65 bytes +var TYPE_PRESSURE = 0x14; // Pressure 4 bytes (hPa) +var TYPE_SOUND = 0x15; // Sound 2 bytes (peak,avg) +var TYPE_PULSE2 = 0x16; // Pulse input 2 relative 2 bytes +var TYPE_PULSE2_ABS = 0x17; // Pulse input 2 absolute 4 bytes +var TYPE_ANALOG2 = 0x18; // Analog input 2 2 bytes 0-65535 mV +var TYPE_EXT_TEMP2 = 0x19; // External temp 2 2 bytes -3276.8°C to 3276.7°C +var TYPE_EXT_DIGITAL2 = 0x1A; // Digital input 2 1 byte +var TYPE_EXT_ANALOG_UV= 0x1B; // Analog UV 4 bytes + +function bin16dec(bin) { + var num = bin & 0xFFFF; + if (0x8000 & num) + num = -(0x010000 - num); + return num; +} + +function bin8dec(bin) { + var num = bin & 0xFF; + if (0x80 & num) + num = -(0x0100 - num); + return num; +} + +function DecodeElsysPayload(data) { + var obj = {}; + for (var i = 0; i < data.length; i++) { + switch (data[i]) { + case TYPE_TEMP: + var temp = (data[i + 1] << 8) | (data[i + 2]); + temp = bin16dec(temp); + obj.temperature = temp / 10; + i += 2; + break; + case TYPE_RH: + obj.humidity = data[i + 1]; + i += 1; + break; + case TYPE_ACC: + obj.x = bin8dec(data[i + 1]); + obj.y = bin8dec(data[i + 2]); + obj.z = bin8dec(data[i + 3]); + i += 3; + break; + case TYPE_LIGHT: + obj.light = (data[i + 1] << 8) | (data[i + 2]); + i += 2; + break; + case TYPE_MOTION: + obj.motion = data[i + 1]; + i += 1; + break; + case TYPE_CO2: + obj.co2 = (data[i + 1] << 8) | (data[i + 2]); + i += 2; + break; + case TYPE_VDD: + obj.vdd = (data[i + 1] << 8) | (data[i + 2]); + i += 2; + break; + case TYPE_ANALOG1: + obj.analog1 = (data[i + 1] << 8) | (data[i + 2]); + i += 2; + break; + case TYPE_GPS: + obj.lat = (data[i + 1] << 16) | (data[i + 2] << 8) | (data[i + 3]); + obj.long = (data[i + 4] << 16) | (data[i + 5] << 8) | (data[i + 6]); + i += 6; + break; + case TYPE_PULSE1: + obj.pulse1 = (data[i + 1] << 8) | (data[i + 2]); + i += 2; + break; + case TYPE_PULSE1_ABS: + obj.pulseAbs = (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4]); + i += 4; + break; + case TYPE_EXT_TEMP1: + var temp = (data[i + 1] << 8) | (data[i + 2]); + temp = bin16dec(temp); + obj.externalTemperature = temp / 10; + i += 2; + break; + case TYPE_EXT_DIGITAL: + obj.digital = data[i + 1]; + i += 1; + break; + case TYPE_EXT_DISTANCE: + obj.distance = (data[i + 1] << 8) | (data[i + 2]); + i += 2; + break; + case TYPE_ACC_MOTION: + obj.accMotion = data[i + 1]; + i += 1; + break; + case TYPE_IR_TEMP: + var iTemp = (data[i + 1] << 8) | (data[i + 2]); + iTemp = bin16dec(iTemp); + var eTemp = (data[i + 3] << 8) | (data[i + 4]); + eTemp = bin16dec(eTemp); + obj.irInternalTemperature = iTemp / 10; + obj.irExternalTemperature = eTemp / 10; + i += 4; + break; + case TYPE_OCCUPANCY: + obj.occupancy = data[i + 1]; + i += 1; + break; + case TYPE_WATERLEAK: + obj.waterleak = data[i + 1]; + i += 1; + break; + case TYPE_GRIDEYE: + var ref = data[i + 1]; + i++; + obj.grideye = []; + for (var j = 0; j < 64; j++) { + obj.grideye[j] = ref + (data[1 + i + j] / 10.0); + } + i += 64; + break; + case TYPE_PRESSURE: + var press = (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4]); + obj.pressure = press / 1000; + i += 4; + break; + case TYPE_SOUND: + obj.soundPeak = data[i + 1]; + obj.soundAvg = data[i + 2]; + i += 2; + break; + case TYPE_PULSE2: + obj.pulse2 = (data[i + 1] << 8) | (data[i + 2]); + i += 2; + break; + case TYPE_PULSE2_ABS: + obj.pulseAbs2 = (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4]); + i += 4; + break; + case TYPE_ANALOG2: + obj.analog2 = (data[i + 1] << 8) | (data[i + 2]); + i += 2; + break; + case TYPE_EXT_TEMP2: + var temp = (data[i + 1] << 8) | (data[i + 2]); + temp = bin16dec(temp); + if (typeof obj.externalTemperature2 === "number") { + obj.externalTemperature2 = [obj.externalTemperature2]; + } + if (typeof obj.externalTemperature2 === "object") { + obj.externalTemperature2.push(temp / 10); + } else { + obj.externalTemperature2 = temp / 10; + } + i += 2; + break; + case TYPE_EXT_DIGITAL2: + obj.digital2 = data[i + 1]; + i += 1; + break; + case TYPE_EXT_ANALOG_UV: + obj.analogUv = (data[i + 1] << 24) | (data[i + 2] << 16) | (data[i + 3] << 8) | (data[i + 4]); + i += 4; + break; + default: + i = data.length; + break; + } + } + return obj; +} + +// ChirpStack v4 / LoRa Alliance TS013 codec API +function decodeUplink(input) { + return { + data: DecodeElsysPayload(input.bytes), + }; +} + +function encodeDownlink(input) { + return { + bytes: [], + }; +} diff --git a/vendors/elsys/codecs/test_decode_elsys.json b/vendors/elsys/codecs/test_decode_elsys.json new file mode 100644 index 0000000..9ce063e --- /dev/null +++ b/vendors/elsys/codecs/test_decode_elsys.json @@ -0,0 +1,105 @@ +[ + { + "name": "Test decode temperature and humidity", + "input": { + "fPort": 5, + "bytes": [1, 0, 234, 2, 55] + }, + "expected": { + "data": { + "temperature": 23.4, + "humidity": 55 + } + } + }, + { + "name": "Test decode temperature, humidity and VDD", + "input": { + "fPort": 5, + "bytes": [1, 0, 198, 2, 42, 7, 14, 10] + }, + "expected": { + "data": { + "temperature": 19.8, + "humidity": 42, + "vdd": 3594 + } + } + }, + { + "name": "Test decode negative temperature", + "input": { + "fPort": 5, + "bytes": [1, 255, 206, 2, 80] + }, + "expected": { + "data": { + "temperature": -5.0, + "humidity": 80 + } + } + }, + { + "name": "Test decode external temperature (DS18B20)", + "input": { + "fPort": 5, + "bytes": [1, 0, 200, 12, 0, 150] + }, + "expected": { + "data": { + "temperature": 20.0, + "externalTemperature": 15.0 + } + } + }, + { + "name": "Test decode analog input", + "input": { + "fPort": 5, + "bytes": [1, 0, 180, 8, 19, 136] + }, + "expected": { + "data": { + "temperature": 18.0, + "analog1": 5000 + } + } + }, + { + "name": "Test decode pressure", + "input": { + "fPort": 5, + "bytes": [20, 0, 15, 92, 168] + }, + "expected": { + "data": { + "pressure": 1006.76 + } + } + }, + { + "name": "Test decode digital input", + "input": { + "fPort": 5, + "bytes": [1, 0, 210, 13, 1] + }, + "expected": { + "data": { + "temperature": 21.0, + "digital": 1 + } + } + }, + { + "name": "Test decode pulse counter", + "input": { + "fPort": 5, + "bytes": [10, 0, 42] + }, + "expected": { + "data": { + "pulse1": 42 + } + } + } +] diff --git a/vendors/elsys/codecs/test_encode_elsys.json b/vendors/elsys/codecs/test_encode_elsys.json new file mode 100644 index 0000000..8e1a761 --- /dev/null +++ b/vendors/elsys/codecs/test_encode_elsys.json @@ -0,0 +1,11 @@ +[ + { + "name": "Test encode empty (no downlink encoding supported)", + "input": { + "data": {} + }, + "expected": { + "bytes": [] + } + } +] diff --git a/vendors/elsys/devices/elt-2-hp.toml b/vendors/elsys/devices/elt-2-hp.toml new file mode 100644 index 0000000..a3bbc30 --- /dev/null +++ b/vendors/elsys/devices/elt-2-hp.toml @@ -0,0 +1,15 @@ +[device] +id = "b2c3d4e5-6f7a-8b9c-0d1e-f2a3b4c5d6e7" +name = "ELT-2 HP" +description = "Multi-sensor with 2 external sensor inputs. Internal temperature, humidity, accelerometer and barometric pressure. External inputs support analog 0-10V, digital, pulse counter, DS18B20, ultrasonic distance, and water leak sensors. IP67 rated with external antenna." + +[[device.firmware]] +version = "1.0" +profiles = [ + "EU868-1_0_3.toml", +] +codec = "elsys.js" + +[device.metadata] +product_url = "https://www.elsys.se/shop/product/elt2hp/" +documentation_url = "https://elsys.se/public/documents/Sensor_payload.pdf" diff --git a/vendors/elsys/profiles/EU868-1_0_3.toml b/vendors/elsys/profiles/EU868-1_0_3.toml new file mode 100644 index 0000000..c72c6f0 --- /dev/null +++ b/vendors/elsys/profiles/EU868-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "c3d4e5f6-7a8b-9c0d-1e2f-a3b4c5d6e7f8" +vendor_profile_id = 0 +region = "EU868" +mac_version = "1.0.3" +reg_params_revision = "A" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 14 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/elsys/vendor.toml b/vendors/elsys/vendor.toml new file mode 100644 index 0000000..ed87fe8 --- /dev/null +++ b/vendors/elsys/vendor.toml @@ -0,0 +1,10 @@ +[vendor] +id = "a1b2c3d4-5e6f-7a8b-9c0d-e1f2a3b4c5d6" +name = "Elsys" +ouis = ["fc0fee"] +devices = [ + "elt-2-hp.toml", +] + +[vendor.metadata] +homepage = "https://www.elsys.se/"