diff --git a/vendor/ellenex/index.yaml b/vendor/ellenex/index.yaml index 6e28ea5fc1..7ae10336e5 100644 --- a/vendor/ellenex/index.yaml +++ b/vendor/ellenex/index.yaml @@ -16,6 +16,7 @@ endDevices: - plm2-l - tts2-l - fms2-l + - pls2-l-v6 #- rs1-l #- rm1-l #- csd2-l diff --git a/vendor/ellenex/pls2-l-v6-codec.yaml b/vendor/ellenex/pls2-l-v6-codec.yaml new file mode 100644 index 0000000000..45f23160f7 --- /dev/null +++ b/vendor/ellenex/pls2-l-v6-codec.yaml @@ -0,0 +1,193 @@ +# Uplink decoder decodes CBOR data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: pls2-l-v6.js + examples: + - description: payload BF614CFA3FCEC8C86176190CF8FF -> Level(m) 1.6155023574829102 m, Battery_Voltage(mv) 3320mV + input: + fPort: 15 + bytes: [0xBF, 0x61, 0x4C, 0xFA, 0x3F, 0xCE, 0xC8, 0xC8, 0x61, 0x76, 0x19, 0x0C, 0xF8, 0xFF] + output: + data: + Level(m): 1.6155023574829102 + Battery_Voltage(mv): 3320 + +# Downlink encoder encodes JSON object into a binary data downlink (optional) +downlinkEncoder: + fileName: encoder.js + examples: + - description: Change sampling rate - sample every 180 minutes (3hrs) + input: + command: 1 + data: + unit: 'minute' + time: 180 + output: + bytes: [0x10, 0x01, 0x00, 0xB4] + fPort: 5 + - description: Change sampling rate - sample every 180 seconds (3mins) + input: + command: 1 + data: + unit: 'second' + time: 180 + output: + bytes: [0x10, 0x00, 0x00, 0xB4] + fPort: 5 + - description: Change sampling rate - wrong time unit + input: + command: 1 + data: + unit: 'seconds' + time: 59 + output: + errors: + - 'Invalid time unit: must be either "minute" or "second"' + - description: Change sampling rate - shorter than minimum interval in minutes + input: + command: 1 + data: + unit: 'minute' + time: 0.6 + output: + errors: + - 'Invalid sampling interval: minimum is 60 seconds (i.e. 1 minute)' + - description: Change sampling rate - shorter than minimum interval in seconds + input: + command: 1 + data: + unit: 'second' + time: 59 + output: + errors: + - 'Invalid sampling interval: minimum is 60 seconds (i.e. 1 minute)' + - description: Change sampling rate - non-string input for unit + input: + command: 1 + data: + unit: 60 + time: 59 + output: + errors: + - 'Missing required field or invalid input: unit' + - description: Change sampling rate - non-numeric input for time + input: + command: 1 + data: + unit: 'second' + time: 'second' + output: + errors: + - 'Missing required field or invalid input: time' + - description: Change sampling rate - missing input for unit + input: + command: 1 + data: + time: 180 + output: + errors: + - 'Missing required field or invalid input: unit' + - description: Change sampling rate - missing input for time + input: + command: 1 + data: + unit: 'second' + output: + errors: + - 'Missing required field or invalid input: time' + - description: Enable confirmation + input: + command: 2 + data: + confirmation: true + output: + bytes: [0x07, 0x01] + fPort: 5 + - description: Disable confirmation + input: + command: 2 + data: + confirmation: false + output: + bytes: [0x07, 0x00] + fPort: 5 + - description: Enable/Disable confirmation with non-boolean value + input: + command: 2 + data: + confirmation: 'confirmation' + output: + errors: + - 'Missing required field or invalid input: confirmation' + - description: Enable/Disable confirmation with missing confirmation input + input: + command: 2 + data: + confirm: true + output: + errors: + - 'Missing required field or invalid input: confirmation' + - description: Reset device + input: + command: 3 + data: + reset: true + output: + bytes: [0xFF, 0x00] + fPort: 5 + - description: Reset device with false + input: + command: 3 + data: + reset: false + output: + errors: + - 'Missing required field or invalid input: reset' + - description: Reset device with non-boolean value + input: + command: 3 + data: + reset: 'reset' + output: + errors: + - 'Missing required field or invalid input: reset' + - description: Reset device with missing reset input + input: + command: 3 + data: + confirmation: 'reset' + output: + errors: + - 'Missing required field or invalid input: reset' + - description: Change periodic auto-reset settings - device automatically reset after sending 3000 samples + input: + command: 4 + data: + count: 3000 + output: + bytes: [0x16, 0x0B, 0xB8] + fPort: 5 + - description: Change periodic auto-reset settings - disable autoreset + input: + command: 4 + data: + count: 0 + output: + bytes: [0x16, 0x00, 0x00] + fPort: 5 + - description: Change periodic auto-reset settings - non-numeric input + input: + command: 4 + data: + count: '0' + output: + errors: + - 'Missing required field or invalid input: count' + - description: Change periodic auto-reset settings - missing input + input: + command: 4 + data: + confirmation: true + output: + errors: + - 'Missing required field or invalid input: count' diff --git a/vendor/ellenex/pls2-l-v6-profile.yaml b/vendor/ellenex/pls2-l-v6-profile.yaml new file mode 100644 index 0000000000..e0935863a3 --- /dev/null +++ b/vendor/ellenex/pls2-l-v6-profile.yaml @@ -0,0 +1,47 @@ +# LoRaWAN MAC version: 1.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4 or 1.1 +macVersion: 1.0.3 +# LoRaWAN Regional Parameters version. Values depend on the LoRaWAN version: +# 1.0: TS001-1.0 +# 1.0.1: TS001-1.0.1 +# 1.0.2: RP001-1.0.2 or RP001-1.0.2-RevB +# 1.0.3: RP001-1.0.3-RevA +# 1.0.4: RP002-1.0.0 or RP002-1.0.1 +# 1.1: RP001-1.1-RevA or RP001-1.1-RevB +regionalParametersVersion: RP001-1.0.3-RevA + +# Whether the end device supports join (OTAA) or not (ABP) +supportsJoin: true +# If your device is an ABP device (supportsJoin is false), uncomment the following fields: +# RX1 delay +#rx1Delay: 5 +# RX1 data rate offset +#rx1DataRateOffset: 0 +# RX2 data rate index +#rx2DataRateIndex: 0 +# RX2 frequency (MHz) +#rx2Frequency: 869.525 +# Factory preset frequencies (MHz) +#factoryPresetFrequencies: [868.1, 868.3, 868.5, 867.1, 867.3, 867.5, 867.7, 867.9] + +# Maximum EIRP +maxEIRP: 16 +# Whether the end device supports 32-bit frame counters +supports32bitFCnt: true + +# Whether the end device supports class B +supportsClassB: false +# If your device supports class B, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classBTimeout: 60 +# Ping slot period (seconds) +#pingSlotPeriod: 128 +# Ping slot data rate index +#pingSlotDataRateIndex: 0 +# Ping slot frequency (MHz). Set to 0 if the band supports ping slot frequency hopping. +#pingSlotFrequency: 869.525 + +# Whether the end device supports class C +supportsClassC: false +# If your device supports class C, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classCTimeout: 60 diff --git a/vendor/ellenex/pls2-l-v6.js b/vendor/ellenex/pls2-l-v6.js new file mode 100644 index 0000000000..7cbaa37014 --- /dev/null +++ b/vendor/ellenex/pls2-l-v6.js @@ -0,0 +1,158 @@ + +function decodeUtf8(bytes) { + let s = ''; + for (let i = 0; i < bytes.length; i++) { + s += String.fromCharCode(bytes[i]); + } + return s; +} + +function bytesToFloat16(bytes) { + const half = (bytes[0] << 8) | bytes[1]; + const exp = (half & 0x7c00) >> 10; + const frac = half & 0x03ff; + let val; + if (exp === 0) { + val = (frac / 1024) * Math.pow(2, -14); + } else if (exp === 31) { + val = frac ? NaN : Infinity; + } else { + val = (1 + frac / 1024) * Math.pow(2, exp - 15); + } + return half & 0x8000 ? -val : val; +} + +function bytesToFloat32(bytes) { + const dv = new DataView(new ArrayBuffer(4)); + for (let i = 0; i < 4; i++) dv.setUint8(i, bytes[i]); + return dv.getFloat32(0, false); +} + +function bytesToFloat64(bytes) { + const dv = new DataView(new ArrayBuffer(8)); + for (let i = 0; i < 8; i++) dv.setUint8(i, bytes[i]); + return dv.getFloat64(0, false); +} + +function decodeCBOR(buf) { + let i = 0; + function readByte() { + return buf[i++]; + } + function readN(n) { + const s = buf.slice(i, i + n); + i += n; + return s; + } + + function readLength(ai) { + if (ai < 24) return ai; + if (ai === 24) return readByte(); + if (ai === 25) { + const b = readN(2); + return (b[0] << 8) | b[1]; + } + if (ai === 26) { + const b = readN(4); + return (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]; + } + if (ai === 27) { + const b = readN(8); + return Number( + (BigInt(b[0]) << 56n) | (BigInt(b[1]) << 48n) | (BigInt(b[2]) << 40n) | (BigInt(b[3]) << 32n) | (BigInt(b[4]) << 24n) | (BigInt(b[5]) << 16n) | (BigInt(b[6]) << 8n) | BigInt(b[7]), + ); + } + if (ai === 31) return -1; // indefinite + throw new Error('Unsupported length encoding'); + } + + function parseItem() { + const initial = readByte(); + const major = initial >> 5; + const ai = initial & 0x1f; + + switch (major) { + case 0: + return readLength(ai); + case 1: { + const n = readLength(ai); + return -1 - n; + } + case 2: { + const len = readLength(ai); + return readN(len); + } + case 3: { + const len = readLength(ai); + return decodeUtf8(readN(len)); + } + case 4: { + const len = readLength(ai); + const arr = []; + if (len === -1) { + while (buf[i] !== 0xff) arr.push(parseItem()); + i++; + } else { + for (let k = 0; k < len; k++) arr.push(parseItem()); + } + return arr; + } + case 5: { + const len = readLength(ai); + const obj = {}; + if (len === -1) { + while (buf[i] !== 0xff) { + obj[parseItem()] = parseItem(); + } + i++; + } else { + for (let k = 0; k < len; k++) obj[parseItem()] = parseItem(); + } + return obj; + } + case 7: + if (ai === 20) return false; + if (ai === 21) return true; + if (ai === 22) return null; + if (ai === 23) return undefined; + if (ai === 25) return bytesToFloat16(readN(2)); + if (ai === 26) return bytesToFloat32(readN(4)); + if (ai === 27) return bytesToFloat64(readN(8)); + if (ai === 31) return null; + return ai; + default: + throw new Error('Unsupported major type: ' + major); + } + } + + return parseItem(); +} + +// --- mapping for your device --- +const SENSOR_MAP = { + L: { name: 'Level(m)', transform: (v) => Number(v) }, + v: { name: 'Battery_Voltage(mv)', transform: (v) => Number(v) }, +}; + +function mapCbor(obj) { + if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) return obj; + const out = {}; + for (const k in obj) { + if (SENSOR_MAP[k]) { + out[SENSOR_MAP[k].name] = SENSOR_MAP[k].transform(obj[k]); + } else { + out[k] = obj[k]; + } + } + return out; +} + +// --- TTN entry point --- +function decodeUplink(input) { + try { + const parsed = decodeCBOR(input.bytes); + return { data: mapCbor(parsed) }; + } catch (e) { + return { data: {} }; + } +} diff --git a/vendor/ellenex/pls2-l-v6.yaml b/vendor/ellenex/pls2-l-v6.yaml new file mode 100644 index 0000000000..e93443765f --- /dev/null +++ b/vendor/ellenex/pls2-l-v6.yaml @@ -0,0 +1,176 @@ +name: PLS2-L-V6- Level Transmitter +description: LoRaWAN Operated Low Power Level Transmitter for Liquid Media + +# Hardware versions (optional, use when you have revisions) +hardwareVersions: + - version: '1.0' + numeric: 1 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '1.0' + numeric: 1 + # Corresponding hardware versions (optional) + hardwareVersions: + - '1.0' + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, + # RU864-870 + profiles: + AS923: + # Optional identifier of the vendor of the profile. + # vendorID: example + # Identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) + id: pls2-l-v6-profile + lorawanCertified: true + codec: pls2-l-v6-codec + US902-928: + id: pls2-l-v6-profile + lorawanCertified: true + codec: pls2-l-v6-codec + EU863-870: + id: pls2-l-v6-profile + lorawanCertified: true + codec: pls2-l-v6-codec + AU915-928: + id: pls2-l-v6-profile + lorawanCertified: true + codec: pls2-l-v6-codec + +# Sensors that this device features (optional) +# Valid values are: +# 4-20 ma, accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co, co2, conductivity, +# current, digital input, dissolved oxygen, distance, dust, energy, gps, gyroscope, h2s, humidity, iaq, level, light, +# lightning, link, magnetometer, moisture, motion, no, no2, o3, particulate matter, ph, pir, pm2.5, pm10, potentiometer, +# power, precipitation, pressure, proximity, pulse count, pulse frequency, radar, rainfall, rssi, smart valve, snr, so2, +# solar radiation, sound, strain, surface temperature, temperature, tilt, time, tvoc, uv, vapor pressure, velocity, +# vibration, voltage, water potential, water, weight, wifi ssid, wind direction, wind speed. +sensors: + - level + +# Additional radios that this device has (optional) +# Valid values are: ble, nfc, wifi, cellular. +additionalRadios: + - nfc + - wifi + +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +# Used length and diameter of larger part of C Model +dimensions: + length: 155 + diameter: 56 + +# Weight in grams (optional) +weight: 1500 + +# Battery information (optional) +battery: + replaceable: true + type: 3.6V Lithium C Type + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celsius) + temperature: + min: -20 + max: 85 + +# IP rating (optional) +ipCode: IP65 + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +# keyProvisioning: + +# Key programming (optional) +# Valid values are: bluetooth, nfc, wifi, serial (when the user has a serial interface to set the keys) +# and firmware (when the user should change the firmware to set the keys). +keyProgramming: + - nfc + - wifi + - serial + +# Key security (optional) +# Valid values are: none, read protected and secure element. +# keySecurity: secure element + +# Firmware programming (optional) +# Valid values are: serial (when the user has a serial interface to update the firmware), fuota lorawan (when the device +# supports LoRaWAN FUOTA via standard interfaces) and fuota other (other wireless update mechanism). +firmwareProgramming: + - serial + - fuota lorawan + - fuota other + +# Product and data sheet URLs (optional) +productURL: https://www.ellenex.com/ellenex-lorawan-products +dataSheetURL: https://83a2d02d-d78a-4920-905c-42da0f69388a.filesusr.com/ugd/b0de6e_ac75ab8464c541a1b8058b0351cfb5fe.pdf + +# Commercial information + +# Photos +photos: + main: pls2-l.png + +# Youtube or Vimeo Video (optional) +videos: + main: https://www.youtube.com/watch?v=sQFsiFGDLkQ&t=56s + +# Regulatory compliances (optional) +compliances: + safety: + - body: IEC + norm: EN + standard: 62368-1 + - body: IEC + norm: AU + standard: 62368-1 + - body: IEC + norm: NZ + standard: 62368-1 + - body: IEC + norm: USA + standard: 62368-1 + - body: IEC + norm: EN + standard: 61010-1 + - body: IEC + norm: AU + standard: 61010-1 + - body: IEC + norm: NZ + standard: 61010-1 + - body: IEC + norm: USA + standard: 61010-1 + - body: IEC + norm: EN + standard: 61326-1 + - body: IEC + norm: EN + standard: '62311' + - body: IEC + norm: EN + standard: '60529' + radioEquipment: + - body: ETSI + norm: EN + standard: 301 489-1 + - body: ETSI + norm: EN + standard: 301 489-3 + - body: ETSI + norm: EN + standard: 301 489-17 + - body: ETSI + norm: EN + standard: 300 220 + - body: ETSI + norm: EN + standard: 300 328 + - body: ETSI + norm: EN + standard: 300 330