diff --git a/vendors/elv/codecs/elv-lw-gps2.js b/vendors/elv/codecs/elv-lw-gps2.js new file mode 100644 index 0000000..3af719e --- /dev/null +++ b/vendors/elv/codecs/elv-lw-gps2.js @@ -0,0 +1,173 @@ +/* + * ELV-LW-GPS2 - ChirpStack Codec + * Uplink parser: Original ELV V1.0.0 (with corrected TX-Reason off-by-one) + * Downlink encoder: JSON -> 5-byte config per ELVjournal 3/2025, Table 3 + */ + +// ============================================================ +// UPLINK +// ============================================================ + +var tx_reason = [ + "UNDEFINED_EVENT", // 0x00 + "TIMER_EVENT", // 0x01 + "USER_BUTTON_EVENT", // 0x02 + "GNSS_TIMEOUT_EVENT", // 0x03 + "HEARTBEAT_EVENT", // 0x04 + "INPUT_ONE_SHOT_EVENT", // 0x05 + "INPUT_CYCLIC_EVENT", // 0x06 + "MOTION_START_EVENT", // 0x07 + "MOTION_CYCLIC_EVENT", // 0x08 + "MOTION_STOP_EVENT", // 0x09 +]; + +function Decoder(bytes, port) { + var decoded = {}; + var index = 0; + + if (port === 10) { + if (bytes.length != 0) { + do { + switch (bytes[index]) { + case 0x01: // Application version + decoded.app_version = "V" + bytes[++index] + "." + bytes[++index] + "." + bytes[++index]; + break; + case 0x02: // Bootloader version + decoded.bl_version = "V" + bytes[++index] + "." + bytes[++index] + "." + bytes[++index]; + break; + case 0x03: // TX-Reason + index++; + if (bytes[index] >= tx_reason.length) { + decoded.tx_reason = "UNKNOWN_EVENT --> Please update your payload parser"; + } else { + decoded.tx_reason = tx_reason[bytes[index]]; + } + break; + case 0x04: // Supply voltage (mV) + decoded.supply_voltage = (bytes[++index] << 8) | bytes[++index]; + break; + case 0x0A: // Positioning Data (TTN Mapper conform) + decoded.latitude = parseFloat(bytes[++index] | (bytes[++index] << 8) | (bytes[++index] << 16) | (bytes[++index] << 24)) / 1000000; + decoded.longitude = parseFloat(bytes[++index] | (bytes[++index] << 8) | (bytes[++index] << 16) | (bytes[++index] << 24)) / 1000000; + decoded.altitude = bytes[++index] | (bytes[++index] << 8) | (bytes[++index] << 16) | (bytes[++index] << 24); + decoded.altitude = Number((decoded.altitude / 10000).toFixed(2)); + decoded.hdop = Number(parseFloat(String(bytes[++index]) + "." + String(bytes[++index] * 4).padStart(2, '0')).toFixed(2)); + break; + default: + decoded = {}; + decoded.parser_error = "Data Type Failure --> Please update your payload parser"; + break; + } + } + while ((++index < bytes.length) && ('parser_error' in decoded === false)); + } + } else { + decoded.parser_error = "Wrong Port Number"; + } + + return decoded; +} + +function decodeUplink(input) { + var decoded = Decoder(input.bytes, input.fPort); + if ('parser_error' in decoded) { + return { data: {}, errors: [decoded.parser_error] }; + } + return { data: decoded }; +} + +// ============================================================ +// DOWNLINK +// ============================================================ +/* + * Configuration according to ELVjournal 3/2025, Table 3. + * FPort 10. Always 5 bytes, fixed order. + * Value 0 in a field = parameter remains unchanged. + * + * input.data accepts the following fields (all optional): + * + * mode : "cyclic" | "contact" | "motion" + * -> Byte 0 (1 = cyclic, 2 = contact interface, 3 = motion) + * + * interval_s : number in seconds, multiple of 30, range 30..7650 + * -> Byte 1 (internal encoding = interval_s / 30) + * Factory default: 600 s (= 10 min) + * + * datarate : "DR0".."DR5" or number 0..5 + * -> Byte 2 (firmware value = DR number + 1) + * DR0=SF12 ... DR5=SF7; factory default DR3 + * + * sensitivity : "low" | "medium" | "high" + * -> Byte 3 (1/2/3); factory default "medium" + * + * low_power : "gnss_always_on" | "gnss_backup" + * -> Byte 4 (1/2); factory default "gnss_backup" + * + * Example: sensitivity only set to "high" + * { "sensitivity": "high" } -> [0, 0, 0, 3, 0] + */ + +var DL_MODE = { cyclic: 1, contact: 2, motion: 3 }; +var DL_SENSITIVITY = { low: 1, medium: 2, high: 3 }; +var DL_LOWPOWER = { gnss_always_on: 1, gnss_backup: 2 }; + +function encodeDownlink(input) { + var d = input.data || {}; + var errors = []; + var bytes = [0, 0, 0, 0, 0]; // Default: everything "no change" + + // --- Byte 0: Mode --- + if (d.mode !== undefined) { + if (DL_MODE[d.mode] !== undefined) { + bytes[0] = DL_MODE[d.mode]; + } else { + errors.push("invalid mode: '" + d.mode + "' (allowed: cyclic, contact, motion)"); + } + } + + // --- Byte 1: Time interval (seconds -> value/30) --- + if (d.interval_s !== undefined) { + var iv = Number(d.interval_s); + if (!Number.isInteger(iv) || iv % 30 !== 0 || iv < 30 || iv > 7650) { + errors.push("invalid interval_s: " + d.interval_s + " (multiple of 30, 30..7650 s)"); + } else { + bytes[1] = iv / 30; + } + } + + // --- Byte 2: Data rate (DR0..DR5 -> value+1) --- + if (d.datarate !== undefined) { + var dr = d.datarate; + if (typeof dr === "string" && /^DR[0-5]$/.test(dr)) { + dr = Number(dr.slice(2)); + } + if (Number.isInteger(dr) && dr >= 0 && dr <= 5) { + bytes[2] = dr + 1; + } else { + errors.push("invalid datarate: '" + d.datarate + "' (allowed: DR0..DR5 or 0..5)"); + } + } + + // --- Byte 3: Motion sensitivity --- + if (d.sensitivity !== undefined) { + if (DL_SENSITIVITY[d.sensitivity] !== undefined) { + bytes[3] = DL_SENSITIVITY[d.sensitivity]; + } else { + errors.push("invalid sensitivity: '" + d.sensitivity + "' (allowed: low, medium, high)"); + } + } + + // --- Byte 4: Low-power mode --- + if (d.low_power !== undefined) { + if (DL_LOWPOWER[d.low_power] !== undefined) { + bytes[4] = DL_LOWPOWER[d.low_power]; + } else { + errors.push("invalid low_power: '" + d.low_power + "' (allowed: gnss_always_on, gnss_backup)"); + } + } + + if (errors.length > 0) { + return { fPort: 10, bytes: [], errors: errors }; + } + return { fPort: 10, bytes: bytes }; +} diff --git a/vendors/elv/codecs/elv-lw-omo.js b/vendors/elv/codecs/elv-lw-omo.js new file mode 100644 index 0000000..7d7c3d1 --- /dev/null +++ b/vendors/elv/codecs/elv-lw-omo.js @@ -0,0 +1,201 @@ +/* + * ELV-LW-OMO - ChirpStack Codec + * Uplink parser: Original ELV V1.0.1 (unchanged) + * Downlink encoder: JSON -> [ID,value] tuples per ELVjournal 4/2023, Table 3 + */ + +// ============================================================ +// UPLINK (Original ELV parser, unchanged) +// ============================================================ + +function decodeUplink(input) { + var data = input.bytes; + var valid = true; + + if (typeof Decoder === "function") { data = Decoder(data, input.fPort); } + if (typeof Converter === "function") { data = Converter(data, input.fPort); } + if (typeof Validator === "function") { valid = Validator(data, input.fPort); } + + if (valid) { + return { data: data }; + } else { + return { data: {}, errors: ["Invalid data received"] }; + } +} + +var tx_reason = ["Undefined","Button Pressed","Heartbeat","Settings","Joined","Acceleration","Tilt","Ongoing Acceleration","Inactivity","Error"]; +var frame_type = ["Device_Info","Device_State","Acceleration_Data","Button_Pressed","Config_Data"]; +var device_modes = ["Acceleration","Tilt"]; + +function Decoder(bytes, port) { + var decoded = {}; + + if (port === 10) { + decoded.Supply_Voltage = bytes[0] * 10; + decoded.frame_type = frame_type[(bytes[1])]; + decoded.TX_Reason = tx_reason[(bytes[2])]; + + switch (decoded.frame_type) { + case "Device_Info": + decoded.Bootloader_Version = `${bytes[3]}.${bytes[4]}.${bytes[5]}`; + decoded.Firmware_Version = `${bytes[6]}.${bytes[7]}.${bytes[8]}`; + decoded.hw_revision = bytes[9] << 8 | bytes[10]; + break; + case "Device_State": + decoded.Accelerated = !!(bytes[3] & 0x1); + decoded.Tilt_Area_0 = !!(bytes[3] & 0x10); + decoded.Tilt_Area_1 = !!(bytes[3] & 0x20); + decoded.Tilt_Area_2 = !!(bytes[3] & 0x40); + decoded.Angle = bytes[4]; + decoded.Activation_count = (bytes[5] << 8 | bytes[6]); + break; + case "Acceleration_Data": + decoded.Accelerated = !!(bytes[3] & 0x1); + decoded.Tilt_Area_0 = !!(bytes[3] & 0x10); + decoded.Tilt_Area_1 = !!(bytes[3] & 0x20); + decoded.Tilt_Area_2 = !!(bytes[3] & 0x40); + decoded.Angle = bytes[4]; + break; + case "Button_Pressed": + decoded.Button_Count = bytes[3]; + break; + case "Config_Data": + decoded.device_mode = ""; + for (let i = 0; i < 8; i++) { + if ((bytes[3] >> i) & 1) { decoded.device_mode += device_modes[i]; } + } + decoded.sensor_threshold = bytes[4]; + decoded.range = bytes[5]; + decoded.alpha = bytes[6]; + decoded.beta = bytes[7]; + decoded.hysteresis = bytes[8]; + decoded.senc_cycle_minutes = bytes[9] * 6; + break; + } + } else { + decoded.parser_error = "Wrong Port Number"; + } + + return decoded; +} + +// ============================================================ +// DOWNLINK (ELVjournal 4/2023, Table 3) +// ============================================================ +/* + * Format: any number of [ID, value] tuples in sequence. + * ONLY the fields set in the JSON are sent. + * + * input.data accepts (all optional): + * + * device_mode : array of "acceleration" | "tilt" (bitwise combinable) + * or "disarmed" (disarm device) + * -> ID 0x00 (accel=1, tilt=2, both=3, disarmed=4) + * + * range_g : 2 | 4 | 8 | 16 (measurement range in g) + * -> ID 0x01 (2g=0, 4g=1, 8g=2, 16g=3) + * + * sensitivity : 1..255 (threshold = value * 0.008 g) + * -> ID 0x02 + * + * angle_alpha : 1..180 (trigger angle alpha, degrees) + * -> ID 0x03 + * + * angle_beta : 1..180 (trigger angle beta, degrees) + * -> ID 0x04 + * + * hysteresis : 0..180 (0 = disabled, otherwise degrees) + * -> ID 0x05 + * + * update_cycle_min : 0 or multiple of 6, 6..1530 (minutes) + * -> ID 0x06 (byte = minutes / 6; 0 = no cyclic uplinks) + * + * reference_vector_update : true (set current orientation as zero point) + * -> ID 0x07 0x00 + * + * request_config : true (device sends config uplink back) + * -> ID 0x08 0x00 + * + * Example orientation mode (matches PDF 00 02 03 46 04 96 05 03 06 0A): + * { "device_mode": ["tilt"], "angle_alpha": 70, "angle_beta": 150, + * "hysteresis": 3, "update_cycle_min": 60 } + */ + +var OMO_RANGE = { 2: 0, 4: 1, 8: 2, 16: 3 }; + +function encodeDownlink(input) { + var d = input.data || {}; + var errors = []; + var bytes = []; + + function push(id, val) { bytes.push(id, val); } + + // 0x00 Device mode + if (d.device_mode !== undefined) { + var modes = Array.isArray(d.device_mode) ? d.device_mode : [d.device_mode]; + var mv = 0, ok = true; + for (var i = 0; i < modes.length; i++) { + if (modes[i] === "acceleration") mv |= 0x01; + else if (modes[i] === "tilt") mv |= 0x02; + else if (modes[i] === "disarmed") mv = 0x04; + else { ok = false; errors.push("invalid device_mode: '" + modes[i] + "' (acceleration, tilt, disarmed)"); } + } + if (ok) push(0x00, mv); + } + + // 0x01 Measurement range + if (d.range_g !== undefined) { + if (OMO_RANGE[d.range_g] !== undefined) push(0x01, OMO_RANGE[d.range_g]); + else errors.push("invalid range_g: " + d.range_g + " (2, 4, 8, 16)"); + } + + // 0x02 Sensitivity (value * 0.008 g) + if (d.sensitivity !== undefined) { + var s = Number(d.sensitivity); + if (Number.isInteger(s) && s >= 1 && s <= 255) push(0x02, s); + else errors.push("invalid sensitivity: " + d.sensitivity + " (1..255)"); + } + + // 0x03 Trigger angle alpha + if (d.angle_alpha !== undefined) { + var a = Number(d.angle_alpha); + if (Number.isInteger(a) && a >= 1 && a <= 180) push(0x03, a); + else errors.push("invalid angle_alpha: " + d.angle_alpha + " (1..180)"); + } + + // 0x04 Trigger angle beta + if (d.angle_beta !== undefined) { + var b = Number(d.angle_beta); + if (Number.isInteger(b) && b >= 1 && b <= 180) push(0x04, b); + else errors.push("invalid angle_beta: " + d.angle_beta + " (1..180)"); + } + + // 0x05 Hysteresis + if (d.hysteresis !== undefined) { + var h = Number(d.hysteresis); + if (Number.isInteger(h) && h >= 0 && h <= 180) push(0x05, h); + else errors.push("invalid hysteresis: " + d.hysteresis + " (0..180)"); + } + + // 0x06 Update cycle (minutes / 6) + if (d.update_cycle_min !== undefined) { + var uc = Number(d.update_cycle_min); + if (uc === 0) push(0x06, 0); + else if (Number.isInteger(uc) && uc % 6 === 0 && uc >= 6 && uc <= 1530) push(0x06, uc / 6); + else errors.push("invalid update_cycle_min: " + d.update_cycle_min + " (0 or multiple of 6, 6..1530)"); + } + + // 0x07 Reference vector update + if (d.reference_vector_update === true) push(0x07, 0x00); + + // 0x08 Request configuration data + if (d.request_config === true) push(0x08, 0x00); + + if (errors.length > 0) { + return { fPort: 10, bytes: [], errors: errors }; + } + if (bytes.length === 0) { + return { fPort: 10, bytes: [], warnings: ["No valid configuration fields specified"] }; + } + return { fPort: 10, bytes: bytes }; +} diff --git a/vendors/elv/codecs/elv-modular-system.js b/vendors/elv/codecs/elv-modular-system.js new file mode 100644 index 0000000..0287d47 --- /dev/null +++ b/vendors/elv/codecs/elv-modular-system.js @@ -0,0 +1,894 @@ +/* + * ELV modular system Payload-Parser + * + * Version: V1.10.1 + * + * */ + +function decodeUplink(input) { + var data = input.bytes; + var valid = true; + + if (typeof Decoder === "function") { + data = Decoder(data, input.fPort); + } + + if (typeof Converter === "function") { + data = Converter(data, input.fPort); + } + + if (typeof Validator === "function") { + valid = Validator(data, input.fPort); + } + + if (valid) { + return { + data: data + }; + } else { + return { + data: {}, + errors: ["Invalid data received"] + }; + } +} + +var tx_reason = [ + "Timer_Event", // 0x00 + "User_Button_Event", // 0x01 + "App_Event", // 0x02 + "FUOTA_Event", // 0x03 + "Cyclic_Event", // 0x04 + "Timeout_Event" // 0x05 + ]; + +const PM_VM1_AS_PM = 0; // Selection value for using the ELV-PM-VM1. + // 0: ELV-PM-VM1 is used as an application module. + // 1: ELV-PM-VM1 is used as a pure power module. + +/* + * @brief Receives the bytes transmitted from a device of the ELV modular system + * @param bytes: Array with the data stream + * @param port: Used TTN/TTS data port + * @return Decoded data from a device of the ELV modular system + * */ +function Decoder(bytes, port) { + var decoded = {}; // Container with the decoded output + var index = 5; // Index variable for the application data in the bytes[] array + var Temp_Value = 0; // Variable for temporarily calculated values + var Datatype_usage = 0; // Variable counts the usage of the datatype temperatue (0x02) + + if (port === 10) { // The default port for app data + if (bytes.length >= 5) { // Minimum 5 Bytes for Header + + // Collecting header data + if (0xff === bytes[0]) { // + decoded.TX_Reason = "UNDEFINED_EVENT"; // + } else if (tx_reason.length <= bytes[0]) { // Verify that the TX_Reason value is within the available array elements + decoded.TX_Reason = "UNKNOWN_EVENT --> Please update your payload parser"; // The TX_Reason value is not within the available array elements. + } else { // The TX_Reason value is within the available array elements. + decoded.TX_Reason = tx_reason[bytes[0]]; // Read out the reason for sending + } + + decoded.Supply_Voltage = (bytes[3] << 8) | bytes[4]; + + // If the ELV-PM-VM1 is used as a pure power module. + if( PM_VM1_AS_PM === 1 ) + { + // Calculating the correct operating voltage + decoded.Supply_Voltage = Math.round( decoded.Supply_Voltage * 4.6 ); + } + + if (bytes.length >= 6) { // There is not only the header data + // Loop for collecting the application data + do { + switch (bytes[index]) { + case 0x00: // Binary Input + { + index++; // Set index to data value + if (bytes[index] & 0x02) { // if Input 1 enabled + if (bytes[index] & 0x01) { + decoded.Input_1 = "Active"; + } + else { + decoded.Input_1 = "Inactive"; + } + } + if (bytes[index] & 0x08) { // if Input 2 enabled + if (bytes[index] & 0x04) { + decoded.Input_2 = "Active"; + } + else { + decoded.Input_2 = "Inactive"; + } + } + if (bytes[index] & 0x20) { // if Input 3 enabled + if (bytes[index] & 0x10) { + decoded.Input_3 = "Active"; + } + else { + decoded.Input_3 = "Inactive"; + } + } + if (bytes[index] & 0x80) { // if Input 4 enabled + if (bytes[index] & 0x40) { + decoded.Input_4 = "Active"; + } + else { + decoded.Input_4 = "Inactive"; + } + } + break; + } + case 0x01: // Binary Output + { + index++; // Set index to data value + if (bytes[index] & 0x02) { + if (bytes[index] & 0x01) { + decoded.Output_1 = "Active"; + } + else { + decoded.Output_1 = "Inactive"; + } + } + if (bytes[index] & 0x08) { + if (bytes[index] & 0x04) { + decoded.Output_2 = "Active"; + } + else { + decoded.Output_2 = "Inactive"; + } + } + if (bytes[index] & 0x20) { + if (bytes[index] & 0x10) { + decoded.Output_3 = "Active"; + } + else { + decoded.Output_3 = "Inactive"; + } + } + if (bytes[index] & 0x80) { + if (bytes[index] & 0x40) { + decoded.Output_4 = "Active"; + } + else { + decoded.Output_4 = "Inactive"; + } + } + break; + } + case 0x02: // Temperature + { + var Temp_String = ""; + // + Datatype_usage++; + + // Get the 16 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + + switch (Temp_Value) { + case 0x8000: // Special value + Temp_String = "Unknown"; + break; + case 0x8001: // Special value + Temp_String = "Overflow"; + break; + case 0x8002: // Special value + Temp_String = "Underflow"; + break; + default: // Temperature value + // Convert to 16 bit signed value + if (Temp_Value > 0x7fff) { + Temp_Value -= 0x10000; + } + + Temp_Value *= 0.1; // Adjust the temperature resolution + Temp_String = String(Temp_Value.toFixed(1)); + break; + } + + switch (Datatype_usage) { + case 1: + // The first temperature element will be named with Temperature_Sensor + decoded.Temperature_Sensor = Temp_String; + break; + case 2: + // There is a second temperature element, so a new naming scheme is used for the elements. + decoded.Temperature_T1 = decoded.Temperature_Sensor; + // Delete the old element + delete decoded.Temperature_Sensor; + // The second temperature element will be the temperature T2 + decoded.Temperature_T2 = Temp_String; + break; + case 3: + // The third temperature element will be the temperature T3 + decoded.Temperature_T3 = Temp_String; + break; + default: + + break; + } + break; + } + case 0x03: // Temperature + Relative Humidity + { + // Get the 16 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + + switch (Temp_Value) { + case 0x8000: // Special value + decoded.TH_Sensor_Temperature = "Unknown"; + break; + case 0x8001: // Special value + decoded.TH_Sensor_Temperature = "Overflow"; + break; + case 0x8002: // Special value + decoded.TH_Sensor_Temperature = "Underflow"; + break; + default: // Temperature value + // Convert to 16 bit signed value + if (Temp_Value > 0x7fff) { + Temp_Value -= 0x10000; + } + Temp_Value *= 0.1; // Adjust the temperature resolution + decoded.TH_Sensor_Temperature = String(Temp_Value.toFixed(1)); + break; + } + + index++; // Set index to relative humidity data value + switch (bytes[index]) { + case 0xff: // Special value + decoded.TH_Sensor_Humidity = "Unknown"; + break; + case 0xfe: // Special value + decoded.TH_Sensor_Humidity = "Overflow"; + break; + case 0xfd: // Special value + decoded.TH_Sensor_Humidity = "Underflow"; + break; + default: // Relative humidity value + decoded.TH_Sensor_Humidity = String(bytes[index]); + break; + } + break; + } + case 0x04: // Positioning Data (TTN Mapper conform) + { + decoded.latitude = parseFloat( bytes[++index] | ( bytes[++index] << 8 ) | ( bytes[++index] << 16 ) | ( bytes[++index] << 24 ) ) / 1000000; + decoded.longitude = parseFloat( bytes[++index] | ( bytes[++index] << 8 ) | ( bytes[++index] << 16 ) | ( bytes[++index] << 24 ) ) / 1000000; + decoded.altitude = bytes[++index] | ( bytes[++index] << 8 ) | ( bytes[++index] << 16 ) | ( bytes[++index] << 24 ); + decoded.altitude = Number( ( decoded.altitude / 10000 ).toFixed( 2 ) ); + decoded.hdop = Number( parseFloat( String( bytes[++index] ) + "." + String( bytes[++index] * 4 ).padStart( 2, '0' ) ).toFixed( 2 ) ); + break; + } + case 0x05: // Time Value + { + // Get the 16 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + + switch (Temp_Value) { + case 0x3fff: // Special value + decoded.TimeValue_seconds = "Unknown"; + break; + case 0x3ffe: // Special value + decoded.TimeValue_seconds = "Overflow"; + break; + default: // Temperature value + switch (Temp_Value>>14) // Check time unit + { + case 0: //seconds + Temp_Value = (Temp_Value&0x3fff); + break; + case 1: //minutes + Temp_Value = (Temp_Value&0x3fff) * 60; + break; + case 2: //hours + Temp_Value = (Temp_Value&0x3fff) * 60 * 60; + break; + case 3: //days + Temp_Value = (Temp_Value&0x3fff) * 60 * 60 * 24; + break; + default: + Temp_Value = "Time Unit Error"; + break; + } + decoded.TimeValue_seconds = Temp_Value; + break; + } + break; + } + case 0x06: // Distance + { + // Get the 16 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + + decoded.Distance = Temp_Value; + break; + } + case 0x07: // Battery Indicator + { + index++; // Set index to battery indicator data value + switch ( bytes[index] ) + { + case 0x01: + { + decoded.Battery_Indicator = "Battery_LOW"; + } + break; + case 0x02: + { + decoded.Battery_Indicator = "Battery_OKAY"; + } + break; + case 0x03: + { + decoded.Battery_Indicator = "Battery_HIGH"; + } + break; + default: + { + decoded.Battery_Indicator = "Invalid_Value!"; + } + break; + } + break; + } + case 0x08: // Concentration + { + // Get the 16 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + + switch (Temp_Value) { + case 0x7fff: // Special value + decoded.Concentration = "Unknown"; + break; + case 0x7ffe: // Special value + decoded.Concentration = "Overflow"; + break; + case 0x7ffd: // Special value + decoded.Concentration = "SensorError"; + break; + case 0x7ffc: // Special value + decoded.Concentration = "CalibrationError"; + break; + case 0x7ffb: // reserved + case 0x7ffa: + case 0x7ff9: + case 0x7ff8: + case 0x7ff7: + case 0x7ff6: + case 0x7ff5: + case 0x7ff4: + case 0x7ff3: + case 0x7ff2: + case 0x7ff1: + case 0x7ff0: + decoded.Concentration = "reserved"; + break; + default: + // Convert to 16 bit signed value + if (Temp_Value > 0x7fff) { + Temp_Value -= 0x10000; + } + decoded.Concentration = Temp_Value; + break; + } + break; + } + case 0x0B: // Brightness in [lx] + { + // Get the 24 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 65536); + index++; // Set index to mid byte data value + Temp_Value += (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + + switch (Temp_Value) { + case 0xffffff: // Special value + decoded.Brightness = "Overflow"; + break; + default: // Brightness value + Temp_Value *= 0.01; // Adjust the Brightness resolution + decoded.Brightness = String(Temp_Value.toFixed(2)); + break; + } + break; + } + case 0x0C: // Acceleration Data + { + index++; // Set index to reason for sending data value + + if (bytes[index] & 0x80) { + decoded.In_Motion = "True"; + } + else { + decoded.In_Motion = "False"; + } + + if (bytes[index] & 0x08) { + decoded.Tilt_Area_2 = "True"; + } + else { + decoded.Tilt_Area_2= "False"; + } + + if (bytes[index] & 0x04) { + decoded.Tilt_Area_1 = "True"; + } + else { + decoded.Tilt_Area_1 = "False"; + } + + if (bytes[index] & 0x02) { + decoded.Tilt_Area_0 = "True"; + } + else { + decoded.Tilt_Area_0 = "False"; + } + + if (bytes[index] & 0x01) { + decoded.Acceleration = "True"; + } + else { + decoded.Acceleration = "False"; + } + + index++; // Set index to tilt angle data value + + decoded.Tilt_Angle = bytes[index]; + + break; + } + case 0x0D: // Voltage + Current + Power + { + index++; + const bitfield = bytes[index]; + for (let i = 0; i < 4; i++) { + if (bitfield & (1 << i)) { + // Get the 16 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + Temp_Value *= 0.001; // Adjust the resolution + switch (i) { + case 0: + decoded.Voltage = String(Temp_Value.toFixed(3)); + break; + case 1: + decoded.Voltage2 = String(Temp_Value.toFixed(3)); + break; + case 2: + decoded.Voltage3 = String(Temp_Value.toFixed(3)); + break; + case 3: + decoded.Voltage4 = String(Temp_Value.toFixed(3)); + break; + } + + // Get the 16 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + // Convert to 16 bit signed value + if (Temp_Value > 0x7fff) { + Temp_Value -= 0x10000; + } + Temp_Value *= 0.001; // Adjust the resolution + switch (i) { + case 0: + decoded.Current = String(Temp_Value.toFixed(3)); + break; + case 1: + decoded.Current2 = String(Temp_Value.toFixed(3)); + break; + case 2: + decoded.Current3 = String(Temp_Value.toFixed(3)); + break; + case 3: + decoded.Current4 = String(Temp_Value.toFixed(3)); + break; + } + + // Get the 16 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + switch (Temp_Value >> 14) { + default: + case 0: + Temp_Value = (Temp_Value & 0x3fff) * 0.001; // Adjust the resolution + break; + case 1: + Temp_Value = (Temp_Value & 0x3fff) * 0.01; // Adjust the resolution + break; + case 2: + Temp_Value = (Temp_Value & 0x3fff) * 0.1; // Adjust the resolution + break; + case 3: + Temp_Value = (Temp_Value & 0x3fff) * 1; // Adjust the resolution + break; + } + switch (i) { + case 0: + decoded.Power = String(Temp_Value.toFixed(3)); + break; + case 1: + decoded.Power2 = String(Temp_Value.toFixed(3)); + break; + case 2: + decoded.Power3 = String(Temp_Value.toFixed(3)); + break; + case 3: + decoded.Power4 = String(Temp_Value.toFixed(3)); + break; + } + } + } + break; + } + case 0x0E: // Pressure + { + // Get the 24 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 65536); + index++; // Set index to mid byte data value + Temp_Value += (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + + Temp_Value /= 10; // Adjust the resolution + decoded.Pressure = Temp_Value.toFixed(1); + break; + } + case 0x0F: // Error-Bitfield + { + index++; // Set index to data value + if (bytes[index]) + { + decoded.Error = ""; + if (bytes[index] & 0x01){ // if Input 1 enabled + decoded.Error += "Bit0 " ; + } + if (bytes[index] & 0x02){ // if Input 1 enabled + decoded.Error += "Bit1 " ; + } + if (bytes[index] & 0x04){ // if Input 1 enabled + decoded.Error += "Bit2 " ; + } + if (bytes[index] & 0x08){ // if Input 1 enabled + decoded.Error += "Bit3 " ; + } + if (bytes[index] & 0x10){ // if Input 1 enabled + decoded.Error += "Bit4 " ; + } + if (bytes[index] & 0x20){ // if Input 1 enabled + decoded.Error += "Bit5 " ; + } + if (bytes[index] & 0x40){ // if Input 1 enabled + decoded.Error += "Bit6 " ; + } + if (bytes[index] & 0x80){ // if Input 1 enabled + decoded.Error += "Bit7 " ; + } + } + else + { + decoded.Error = "None "; + } + break; + } + case 0x10: // Absolute angle + { + // Get the 8 bit value + index++; // Set index to data value + Temp_Value = bytes[index]; + switch ( bytes[index] ) + { + case 0xff: + { + decoded.Absolut_Angle = "Unknown"; + } + break; + default: + { + Temp_Value *= 2.5; // Multiply the value with the angle resolution of 2.5 ° + decoded.Absolut_Angle = String(Temp_Value.toFixed(1)); + } + break; + } + break; + } + case 0x11: // Speed + { + // Get the 16 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + + // Check the wind detection bit + if( Temp_Value & 0x0800 ) + { + decoded.Wind_Detection = "1"; + } + else + { + decoded.Wind_Detection = "0"; + } + + // Get the unsigned 11 bit speed value + Temp_Value &= 0x07ff; + switch ( Temp_Value ) + { + case 0x7ff: + { + decoded.Wind_Speed = "Unknown"; + } + break; + case 0x7fe: + { + decoded.Wind_Speed = "Overflow"; + } + break; + default: + { + Temp_Value *= 0.1; // Multiply the value with the speed resolution of 0.1 km/h + decoded.Wind_Speed = String(Temp_Value.toFixed(1)); + } + break; + } + break; + } + case 0x12: // Wind + { + // Get the 16 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + + // Check the variation range + switch ( ((Temp_Value & 0xf000) / 4096) ) + { + case 0xf: + { + decoded.Variation_Angle = "Unknown"; + } + break; + case 0xe: + { + decoded.Variation_Angle = "Overflow"; + } + break; + default: + { + decoded.Variation_Angle = String(((11.25 * (Temp_Value & 0xf000) / 4096)).toFixed(2)); + } + break; + + } + // Check the wind detection bit + if( Temp_Value & 0x0800 ) + { + decoded.Wind_Detection = "1"; + } + else + { + decoded.Wind_Detection = "0"; + } + + // Get the unsigned 11 bit speed value + Temp_Value &= 0x07ff; + switch ( Temp_Value ) + { + case 0x7ff: + { + decoded.Wind_Speed = "Unknown"; + } + break; + case 0x7fe: + { + decoded.Wind_Speed = "Overflow"; + } + break; + default: + { + Temp_Value *= 0.1; // Multiply the value with the speed resolution of 0.1 km/h + decoded.Wind_Speed = String(Temp_Value.toFixed(1)); + } + break; + } + + // Get the 8 bit value + index++; // Set index to data value + Temp_Value = bytes[index]; + switch ( bytes[index] ) + { + case 0xff: + { + decoded.Absolut_Angle = "Unknown"; + } + break; + default: + { + Temp_Value *= 2.5; // Multiply the value with the angle resolution of 2.5 ° + decoded.Absolut_Angle = String(Temp_Value.toFixed(1)); + } + break; + } + break; + } + case 0x13: // Rainfall + { + // Get the 16 bit value + index++; // Set index to high byte data value + Temp_Value = (bytes[index] * 256); + index++; // Set index to low byte data value + Temp_Value += bytes[index]; + + // Check the rain detection bit + if( Temp_Value & 0x8000 ) + { + decoded.Rain_Detection = "1"; + } + else + { + decoded.Rain_Detection = "0"; + } + + // Check the rain counter overflow bit + if( Temp_Value & 0x4000 ) + { + decoded.Rain_Counter_Overflow = "1"; + } + else + { + decoded.Rain_Counter_Overflow = "0"; + } + + // Get the unsigned 14 bit rain amount value + Temp_Value &= 0x3fff; + switch ( Temp_Value ) + { + case 0x3fff: + { + decoded.Rain_Amount = "Unknown"; + } + break; + default: + { + Temp_Value *= 0.1; // Multiply the value with the rainfall resolution of 0.1 l/m² + decoded.Rain_Amount = String(Temp_Value.toFixed(1)); + } + break; + } + break; + } + case 0x14: //6-Axis-Sensor + { + index++; + decoded.Acc_x = !!(bytes[index] & 0x01); + decoded.Acc_y = !!(bytes[index] & 0x02); + decoded.Acc_z = !!(bytes[index] & 0x04); + decoded.Gyr_x = !!(bytes[index] & 0x08); + decoded.Gyr_y = !!(bytes[index] & 0x10); + decoded.Gyr_z = !!(bytes[index] & 0x20); + break; + } + case 0x15: //Window-State + { + index++; + data = bytes[index]; + if (data < 100) + { + decoded.Window_State = data; + } + else if (data == 255) + { + decoded.Window_State = "Tilted" + } + else + { + decoded.Window_State = "Undefined" + } + break; + } + case 0x16: + { + index++; + decoded.Situation = bytes[index]; + break; + } + case 0x17: // UV-Index + { + decoded.UVI = bytes[++index]; + break; + } + case 0x18: // UV-A + { + decoded.UVA = ( bytes[++index] << 24 ) | ( bytes[++index] << 16 ) | ( bytes[++index] << 8 ) | bytes[++index]; + decoded.UVA /= 1000000; + + break; + } + case 0x19: // UV-B + { + decoded.UVB = ( bytes[++index] << 24 ) | ( bytes[++index] << 16 ) | ( bytes[++index] << 8 ) | bytes[++index]; + decoded.UVB /= 1000000; + + break; + } + case 0x1A: // UV-C + { + decoded.UVC = ( bytes[++index] << 24 ) | ( bytes[++index] << 16 ) | ( bytes[++index] << 8 ) | bytes[++index]; + decoded.UVC /= 1000000; + + break; + } + case 0x1B: // Irradiance + { + decoded.Irradiance = ( bytes[++index] << 8 ) | bytes[++index]; + + if( decoded.Irradiance == 0xFFFF ) + { + decoded.Irradiance = 0; + } + + decoded.Irradiance /= 10; + + break; + } + // case 0x??: // Further Data Type + // { + // . + // . + // . + // break; + // } + default: // There is something wrong with the data type value + { + // Removing all added properties from the "decoded" object with a deep clean + // https://stackoverflow.com/questions/19316857/removing-all-properties-from-a-object/19316873#19316873 + // Object.keys(decoded).forEach(function(key){ delete decoded[key]; }); + + // Clear all properties from the "decoded" object + decoded = {}; + + // Add error code propertiy to the "decoded" object + decoded.parser_error = "Data Type Failure --> Please update your payload parser"; + break; + } + } + } while ((++index < bytes.length) && ('parser_error' in decoded === false)); + } + } + else { + decoded.parser_error = "Not enough data"; + } + } + else { + decoded.parser_error = "Wrong Port Number"; + } + + return decoded; +} diff --git a/vendors/elv/codecs/test_decode_elv-lw-gps2.json b/vendors/elv/codecs/test_decode_elv-lw-gps2.json new file mode 100644 index 0000000..a8ca932 --- /dev/null +++ b/vendors/elv/codecs/test_decode_elv-lw-gps2.json @@ -0,0 +1,126 @@ +[ + { + "name": "Test decode application version", + "input": { + "fPort": 10, + "bytes": [1, 2, 3, 4] + }, + "expected": { + "data": { + "app_version": "V2.3.4" + } + } + }, + { + "name": "Test decode bootloader version", + "input": { + "fPort": 10, + "bytes": [2, 1, 0, 0] + }, + "expected": { + "data": { + "bl_version": "V1.0.0" + } + } + }, + { + "name": "Test decode known tx-reason", + "input": { + "fPort": 10, + "bytes": [3, 4] + }, + "expected": { + "data": { + "tx_reason": "HEARTBEAT_EVENT" + } + } + }, + { + "name": "Test decode unknown tx-reason", + "input": { + "fPort": 10, + "bytes": [3, 99] + }, + "expected": { + "data": { + "tx_reason": "UNKNOWN_EVENT --> Please update your payload parser" + } + } + }, + { + "name": "Test decode supply voltage", + "input": { + "fPort": 10, + "bytes": [4, 12, 192] + }, + "expected": { + "data": { + "supply_voltage": 3264 + } + } + }, + { + "name": "Test decode GPS position", + "input": { + "fPort": 10, + "bytes": [10, 64, 78, 222, 2, 63, 180, 150, 0, 68, 214, 18, 0, 1, 2] + }, + "expected": { + "data": { + "latitude": 48.123456, + "longitude": 9.876543, + "altitude": 123.45, + "hdop": 1.08 + } + } + }, + { + "name": "Test decode tx-reason, supply voltage and GPS position combined", + "input": { + "fPort": 10, + "bytes": [3, 4, 4, 12, 192, 10, 64, 78, 222, 2, 63, 180, 150, 0, 68, 214, 18, 0, 1, 2] + }, + "expected": { + "data": { + "tx_reason": "HEARTBEAT_EVENT", + "supply_voltage": 3264, + "latitude": 48.123456, + "longitude": 9.876543, + "altitude": 123.45, + "hdop": 1.08 + } + } + }, + { + "name": "Test decode empty payload", + "input": { + "fPort": 10, + "bytes": [] + }, + "expected": { + "data": {} + } + }, + { + "name": "Test decode unknown data type", + "input": { + "fPort": 10, + "bytes": [9, 9, 9] + }, + "expected": { + "data": {}, + "errors": ["Data Type Failure --> Please update your payload parser"] + } + }, + { + "name": "Test decode wrong port number", + "input": { + "fPort": 1, + "bytes": [4, 12, 192] + }, + "expected": { + "data": {}, + "errors": ["Wrong Port Number"] + } + } +] diff --git a/vendors/elv/codecs/test_decode_elv-lw-omo.json b/vendors/elv/codecs/test_decode_elv-lw-omo.json new file mode 100644 index 0000000..19ba531 --- /dev/null +++ b/vendors/elv/codecs/test_decode_elv-lw-omo.json @@ -0,0 +1,106 @@ +[ + { + "name": "Test decode device info frame", + "input": { + "fPort": 10, + "bytes": [33, 0, 4, 1, 2, 3, 4, 5, 6, 1, 2] + }, + "expected": { + "data": { + "Supply_Voltage": 330, + "frame_type": "Device_Info", + "TX_Reason": "Joined", + "Bootloader_Version": "1.2.3", + "Firmware_Version": "4.5.6", + "hw_revision": 258 + } + } + }, + { + "name": "Test decode device state frame", + "input": { + "fPort": 10, + "bytes": [25, 1, 6, 17, 45, 1, 44] + }, + "expected": { + "data": { + "Supply_Voltage": 250, + "frame_type": "Device_State", + "TX_Reason": "Tilt", + "Accelerated": true, + "Tilt_Area_0": true, + "Tilt_Area_1": false, + "Tilt_Area_2": false, + "Angle": 45, + "Activation_count": 300 + } + } + }, + { + "name": "Test decode acceleration data frame", + "input": { + "fPort": 10, + "bytes": [30, 2, 5, 33, 90] + }, + "expected": { + "data": { + "Supply_Voltage": 300, + "frame_type": "Acceleration_Data", + "TX_Reason": "Acceleration", + "Accelerated": true, + "Tilt_Area_0": false, + "Tilt_Area_1": true, + "Tilt_Area_2": false, + "Angle": 90 + } + } + }, + { + "name": "Test decode button pressed frame", + "input": { + "fPort": 10, + "bytes": [33, 3, 1, 7] + }, + "expected": { + "data": { + "Supply_Voltage": 330, + "frame_type": "Button_Pressed", + "TX_Reason": "Button Pressed", + "Button_Count": 7 + } + } + }, + { + "name": "Test decode config data frame", + "input": { + "fPort": 10, + "bytes": [33, 4, 3, 3, 50, 2, 70, 150, 3, 10] + }, + "expected": { + "data": { + "Supply_Voltage": 330, + "frame_type": "Config_Data", + "TX_Reason": "Settings", + "device_mode": "AccelerationTilt", + "sensor_threshold": 50, + "range": 2, + "alpha": 70, + "beta": 150, + "hysteresis": 3, + "senc_cycle_minutes": 60 + } + } + }, + { + "name": "Test decode wrong port number", + "input": { + "fPort": 1, + "bytes": [33, 4, 3, 3, 50, 2, 70, 150, 3, 10] + }, + "expected": { + "data": { + "parser_error": "Wrong Port Number" + } + } + } +] diff --git a/vendors/elv/codecs/test_decode_elv-modular-system.json b/vendors/elv/codecs/test_decode_elv-modular-system.json new file mode 100644 index 0000000..c357fa9 --- /dev/null +++ b/vendors/elv/codecs/test_decode_elv-modular-system.json @@ -0,0 +1,135 @@ +[ + { + "name": "Test decode header only (timer event)", + "input": { + "fPort": 10, + "bytes": [0, 0, 0, 12, 246] + }, + "expected": { + "data": { + "TX_Reason": "Timer_Event", + "Supply_Voltage": 3318 + } + } + }, + { + "name": "Test decode header only (undefined event)", + "input": { + "fPort": 10, + "bytes": [255, 0, 0, 12, 192] + }, + "expected": { + "data": { + "TX_Reason": "UNDEFINED_EVENT", + "Supply_Voltage": 3264 + } + } + }, + { + "name": "Test decode binary input (user button event)", + "input": { + "fPort": 10, + "bytes": [1, 0, 0, 12, 192, 0, 3] + }, + "expected": { + "data": { + "TX_Reason": "User_Button_Event", + "Supply_Voltage": 3264, + "Input_1": "Active" + } + } + }, + { + "name": "Test decode positive temperature", + "input": { + "fPort": 10, + "bytes": [0, 0, 0, 13, 136, 2, 0, 200] + }, + "expected": { + "data": { + "TX_Reason": "Timer_Event", + "Supply_Voltage": 3464, + "Temperature_Sensor": "20.0" + } + } + }, + { + "name": "Test decode negative temperature", + "input": { + "fPort": 10, + "bytes": [0, 0, 0, 13, 136, 2, 255, 56] + }, + "expected": { + "data": { + "TX_Reason": "Timer_Event", + "Supply_Voltage": 3464, + "Temperature_Sensor": "-20.0" + } + } + }, + { + "name": "Test decode temperature and relative humidity", + "input": { + "fPort": 10, + "bytes": [0, 0, 0, 13, 136, 3, 0, 200, 55] + }, + "expected": { + "data": { + "TX_Reason": "Timer_Event", + "Supply_Voltage": 3464, + "TH_Sensor_Temperature": "20.0", + "TH_Sensor_Humidity": "55" + } + } + }, + { + "name": "Test decode battery indicator (okay)", + "input": { + "fPort": 10, + "bytes": [0, 0, 0, 13, 136, 7, 2] + }, + "expected": { + "data": { + "TX_Reason": "Timer_Event", + "Supply_Voltage": 3464, + "Battery_Indicator": "Battery_OKAY" + } + } + }, + { + "name": "Test decode unknown data type", + "input": { + "fPort": 10, + "bytes": [0, 0, 0, 13, 136, 238] + }, + "expected": { + "data": { + "parser_error": "Data Type Failure --> Please update your payload parser" + } + } + }, + { + "name": "Test decode not enough data", + "input": { + "fPort": 10, + "bytes": [0, 0, 0, 12] + }, + "expected": { + "data": { + "parser_error": "Not enough data" + } + } + }, + { + "name": "Test decode wrong port number", + "input": { + "fPort": 1, + "bytes": [0, 0, 0, 12, 192] + }, + "expected": { + "data": { + "parser_error": "Wrong Port Number" + } + } + } +] diff --git a/vendors/elv/codecs/test_encode_elv-lw-gps2.json b/vendors/elv/codecs/test_encode_elv-lw-gps2.json new file mode 100644 index 0000000..d6d68e3 --- /dev/null +++ b/vendors/elv/codecs/test_encode_elv-lw-gps2.json @@ -0,0 +1,154 @@ +[ + { + "name": "Test encode empty config (no change)", + "input": { + "data": {} + }, + "expected": { + "fPort": 10, + "bytes": [0, 0, 0, 0, 0] + } + }, + { + "name": "Test encode mode only", + "input": { + "data": { "mode": "motion" } + }, + "expected": { + "fPort": 10, + "bytes": [3, 0, 0, 0, 0] + } + }, + { + "name": "Test encode interval only", + "input": { + "data": { "interval_s": 60 } + }, + "expected": { + "fPort": 10, + "bytes": [0, 2, 0, 0, 0] + } + }, + { + "name": "Test encode datarate as DR string", + "input": { + "data": { "datarate": "DR5" } + }, + "expected": { + "fPort": 10, + "bytes": [0, 0, 6, 0, 0] + } + }, + { + "name": "Test encode datarate as number", + "input": { + "data": { "datarate": 2 } + }, + "expected": { + "fPort": 10, + "bytes": [0, 0, 3, 0, 0] + } + }, + { + "name": "Test encode sensitivity only", + "input": { + "data": { "sensitivity": "high" } + }, + "expected": { + "fPort": 10, + "bytes": [0, 0, 0, 3, 0] + } + }, + { + "name": "Test encode low power mode only", + "input": { + "data": { "low_power": "gnss_always_on" } + }, + "expected": { + "fPort": 10, + "bytes": [0, 0, 0, 0, 1] + } + }, + { + "name": "Test encode all fields combined", + "input": { + "data": { + "mode": "cyclic", + "interval_s": 600, + "datarate": "DR3", + "sensitivity": "medium", + "low_power": "gnss_backup" + } + }, + "expected": { + "fPort": 10, + "bytes": [1, 20, 4, 2, 2] + } + }, + { + "name": "Test encode invalid mode", + "input": { + "data": { "mode": "invalid" } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid mode: 'invalid' (allowed: cyclic, contact, motion)"] + } + }, + { + "name": "Test encode invalid interval (not a multiple of 30)", + "input": { + "data": { "interval_s": 31 } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid interval_s: 31 (multiple of 30, 30..7650 s)"] + } + }, + { + "name": "Test encode invalid interval (out of range)", + "input": { + "data": { "interval_s": 7680 } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid interval_s: 7680 (multiple of 30, 30..7650 s)"] + } + }, + { + "name": "Test encode invalid datarate", + "input": { + "data": { "datarate": "DR9" } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid datarate: 'DR9' (allowed: DR0..DR5 or 0..5)"] + } + }, + { + "name": "Test encode invalid sensitivity", + "input": { + "data": { "sensitivity": "extreme" } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid sensitivity: 'extreme' (allowed: low, medium, high)"] + } + }, + { + "name": "Test encode invalid low power mode", + "input": { + "data": { "low_power": "off" } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid low_power: 'off' (allowed: gnss_always_on, gnss_backup)"] + } + } +] diff --git a/vendors/elv/codecs/test_encode_elv-lw-omo.json b/vendors/elv/codecs/test_encode_elv-lw-omo.json new file mode 100644 index 0000000..b98d762 --- /dev/null +++ b/vendors/elv/codecs/test_encode_elv-lw-omo.json @@ -0,0 +1,176 @@ +[ + { + "name": "Test encode orientation mode example (angle + hysteresis + update cycle)", + "input": { + "data": { + "device_mode": ["tilt"], + "angle_alpha": 70, + "angle_beta": 150, + "hysteresis": 3, + "update_cycle_min": 60 + } + }, + "expected": { + "fPort": 10, + "bytes": [0, 2, 3, 70, 4, 150, 5, 3, 6, 10] + } + }, + { + "name": "Test encode device mode acceleration only", + "input": { + "data": { "device_mode": "acceleration" } + }, + "expected": { + "fPort": 10, + "bytes": [0, 1] + } + }, + { + "name": "Test encode device mode acceleration and tilt combined", + "input": { + "data": { "device_mode": ["acceleration", "tilt"] } + }, + "expected": { + "fPort": 10, + "bytes": [0, 3] + } + }, + { + "name": "Test encode device mode disarmed", + "input": { + "data": { "device_mode": "disarmed" } + }, + "expected": { + "fPort": 10, + "bytes": [0, 4] + } + }, + { + "name": "Test encode measurement range", + "input": { + "data": { "range_g": 8 } + }, + "expected": { + "fPort": 10, + "bytes": [1, 2] + } + }, + { + "name": "Test encode sensitivity", + "input": { + "data": { "sensitivity": 100 } + }, + "expected": { + "fPort": 10, + "bytes": [2, 100] + } + }, + { + "name": "Test encode reference vector update", + "input": { + "data": { "reference_vector_update": true } + }, + "expected": { + "fPort": 10, + "bytes": [7, 0] + } + }, + { + "name": "Test encode request config", + "input": { + "data": { "request_config": true } + }, + "expected": { + "fPort": 10, + "bytes": [8, 0] + } + }, + { + "name": "Test encode invalid device mode", + "input": { + "data": { "device_mode": "foo" } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid device_mode: 'foo' (acceleration, tilt, disarmed)"] + } + }, + { + "name": "Test encode invalid measurement range", + "input": { + "data": { "range_g": 5 } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid range_g: 5 (2, 4, 8, 16)"] + } + }, + { + "name": "Test encode invalid sensitivity", + "input": { + "data": { "sensitivity": 0 } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid sensitivity: 0 (1..255)"] + } + }, + { + "name": "Test encode invalid angle alpha", + "input": { + "data": { "angle_alpha": 200 } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid angle_alpha: 200 (1..180)"] + } + }, + { + "name": "Test encode invalid angle beta", + "input": { + "data": { "angle_beta": 0 } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid angle_beta: 0 (1..180)"] + } + }, + { + "name": "Test encode invalid hysteresis", + "input": { + "data": { "hysteresis": 200 } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid hysteresis: 200 (0..180)"] + } + }, + { + "name": "Test encode invalid update cycle", + "input": { + "data": { "update_cycle_min": 7 } + }, + "expected": { + "fPort": 10, + "bytes": [], + "errors": ["invalid update_cycle_min: 7 (0 or multiple of 6, 6..1530)"] + } + }, + { + "name": "Test encode empty config", + "input": { + "data": {} + }, + "expected": { + "fPort": 10, + "bytes": [], + "warnings": ["No valid configuration fields specified"] + } + } +] diff --git a/vendors/elv/codecs/test_encode_elv-modular-system.json b/vendors/elv/codecs/test_encode_elv-modular-system.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/vendors/elv/codecs/test_encode_elv-modular-system.json @@ -0,0 +1 @@ +[] diff --git a/vendors/elv/devices/elv-lw-base.toml b/vendors/elv/devices/elv-lw-base.toml new file mode 100644 index 0000000..0d57ae3 --- /dev/null +++ b/vendors/elv/devices/elv-lw-base.toml @@ -0,0 +1,14 @@ +[device] +id = "f94abb37-2043-4f4f-bdaf-7d578e581617" +name = "ELV-LW-BASE" +description = "Base module of the ELV modular system for building LoRaWAN sensor/actuator nodes from application modules." + +[[device.firmware]] +version = "1.10.1" +profiles = [ + "EU868-1_0_3.toml", +] +codec = "elv-modular-system.js" + +[device.metadata] +product_url = "https://www.elv.de/" diff --git a/vendors/elv/devices/elv-lw-gps2.toml b/vendors/elv/devices/elv-lw-gps2.toml new file mode 100644 index 0000000..7549e40 --- /dev/null +++ b/vendors/elv/devices/elv-lw-gps2.toml @@ -0,0 +1,14 @@ +[device] +id = "6401d482-00b2-4ec4-bb52-6d6a896e20ee" +name = "ELV-LW-GPS2" +description = "GPS/GNSS module of the ELV modular system for position, motion and contact-interface based LoRaWAN tracking." + +[[device.firmware]] +version = "1.0.0" +profiles = [ + "EU868-1_0_3.toml", +] +codec = "elv-lw-gps2.js" + +[device.metadata] +product_url = "https://www.elv.de/" diff --git a/vendors/elv/devices/elv-lw-omo.toml b/vendors/elv/devices/elv-lw-omo.toml new file mode 100644 index 0000000..7e21d7b --- /dev/null +++ b/vendors/elv/devices/elv-lw-omo.toml @@ -0,0 +1,14 @@ +[device] +id = "667956ab-267c-4958-87d3-54d69402dc4e" +name = "ELV-LW-OMO" +description = "Orientation/motion module of the ELV modular system reporting acceleration, tilt and button events." + +[[device.firmware]] +version = "1.0.1" +profiles = [ + "EU868-1_0_3.toml", +] +codec = "elv-lw-omo.js" + +[device.metadata] +product_url = "https://www.elv.de/" diff --git a/vendors/elv/profiles/EU868-1_0_3.toml b/vendors/elv/profiles/EU868-1_0_3.toml new file mode 100644 index 0000000..9da22fb --- /dev/null +++ b/vendors/elv/profiles/EU868-1_0_3.toml @@ -0,0 +1,25 @@ +[profile] +id = "32caa6c1-87b5-45d5-bab9-311d74560085" +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 = 16 + +[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/elv/vendor.toml b/vendors/elv/vendor.toml new file mode 100644 index 0000000..5da1f69 --- /dev/null +++ b/vendors/elv/vendor.toml @@ -0,0 +1,8 @@ +[vendor] +id = "57b81b89-554d-457c-a6f7-126d8092ec8c" +name = "ELV" +vendor_id = 0 +ouis = [] + +[vendor.metadata] +homepage = "https://www.elv.de/"