diff --git a/package-lock.json b/package-lock.json index 91db08ce12..eb92fe0f2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/vendor/index.yaml b/vendor/index.yaml index 80cd8b4ba8..0258d31a85 100644 --- a/vendor/index.yaml +++ b/vendor/index.yaml @@ -587,6 +587,12 @@ vendors: name: PacketWorx Inc. vendorID: 314 + - id: pallidus + name: Pallidus + website: https://pallidus.io/ + ouis: + - 0002CC + - id: parametric name: Parametric Engineering GmbH vendorID: 315 diff --git a/vendor/mcci/catena4430.png b/vendor/mcci/catena4430.png index f2b252e047..9c1376ea12 100644 Binary files a/vendor/mcci/catena4430.png and b/vendor/mcci/catena4430.png differ diff --git a/vendor/mcci/catena4430.yaml b/vendor/mcci/catena4430.yaml index 8c1347d95d..babebc9b1b 100644 --- a/vendor/mcci/catena4430.yaml +++ b/vendor/mcci/catena4430.yaml @@ -46,6 +46,78 @@ firmwareVersions: lorawanCertified: false codec: codec-port2-fmt2 + - # Firmware version + version: '1.x.x' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v1 + US902-928: + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v1 + AU915-928: + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v1 + AS923: + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v1 + KR920-923: + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v1 + IN865-867: + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v1 + + - # Firmware version + version: '2.x.x' + numeric: 3 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '2.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v2 + US902-928: + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v2 + AU915-928: + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v2 + AS923: + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v2 + KR920-923: + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v2 + IN865-867: + id: catena4430-profile + lorawanCertified: false + codec: codec-animal-activity-v2 + # Sensors that this device features (optional) # Valid values are: # accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, @@ -66,9 +138,9 @@ sensors: # Dimensions in mm (optional) # Use width, height, length and/or diameter dimensions: - width: 23 - length: 72 - height: 25 + width: 58 + length: 84 + height: 30 ## # Weight in grams (optional) ## weight: 350 diff --git a/vendor/mcci/catena4460.yaml b/vendor/mcci/catena4460.yaml index 18d5ada73d..9be3dee47f 100644 --- a/vendor/mcci/catena4460.yaml +++ b/vendor/mcci/catena4460.yaml @@ -118,25 +118,3 @@ productURL: https://mcci.io/buy-catena4460 # Photos photos: main: catena4450.png -## other: -## - windsensor-package.jpg - -# Youtube or Vimeo Video (optional) -## videos: -## main: https://youtu.be/nG8MmaR5dsA - -# Regulatory compliances (optional) -## compliances: -## safety: -## - body: IEC -## norm: EN -## standard: 62368-1 -## radioEquipment: -## - body: ETSI -## norm: EN -## standard: 301 489-1 -## version: 2.2.0 -## - body: ETSI -## norm: EN -## standard: 301 489-3 -## version: 2.1.0 diff --git a/vendor/mcci/catena4610.yaml b/vendor/mcci/catena4610.yaml index 1a778bb09c..d9240e6d35 100644 --- a/vendor/mcci/catena4610.yaml +++ b/vendor/mcci/catena4610.yaml @@ -46,6 +46,42 @@ firmwareVersions: lorawanCertified: false codec: codec-catena-generic + - # Firmware version + version: '0.3.0' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: catena4610-profile + lorawanCertified: false + codec: codec-catena4610 + US902-928: + id: catena4610-profile + lorawanCertified: false + codec: codec-catena4610 + AU915-928: + id: catena4610-profile + lorawanCertified: false + codec: codec-catena4610 + AS923: + id: catena4610-profile + lorawanCertified: false + codec: codec-catena4610 + KR920-923: + id: catena4610-profile + lorawanCertified: false + codec: codec-catena4610 + IN865-867: + id: catena4610-profile + lorawanCertified: false + codec: codec-catena4610 + deviceType: devkit # Sensors that this device features (optional) diff --git a/vendor/mcci/catena4612.yaml b/vendor/mcci/catena4612.yaml index 18ac45aaa6..cb97d0576c 100644 --- a/vendor/mcci/catena4612.yaml +++ b/vendor/mcci/catena4612.yaml @@ -46,6 +46,42 @@ firmwareVersions: lorawanCertified: false codec: codec-catena-generic + - # Firmware version + version: '0.5.0' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: catena4612-profile + lorawanCertified: false + codec: codec-catena4612 + US902-928: + id: catena4612-profile + lorawanCertified: false + codec: codec-catena4612 + AU915-928: + id: catena4612-profile + lorawanCertified: false + codec: codec-catena4612 + AS923: + id: catena4612-profile + lorawanCertified: false + codec: codec-catena4612 + KR920-923: + id: catena4612-profile + lorawanCertified: false + codec: codec-catena4612 + IN865-867: + id: catena4612-profile + lorawanCertified: false + codec: codec-catena4612 + deviceType: devkit # Sensors that this device features (optional) diff --git a/vendor/mcci/catena4618.yaml b/vendor/mcci/catena4618.yaml index 25b4652787..eaa4b4e940 100644 --- a/vendor/mcci/catena4618.yaml +++ b/vendor/mcci/catena4618.yaml @@ -24,27 +24,27 @@ firmwareVersions: # unique ID of the profile (lowercase, alpha with dashes, max 36) id: catena4618-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618 US902-928: id: catena4618-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618 AU915-928: id: catena4618-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618 AS923: id: catena4618-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618 KR920-923: id: catena4618-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618 IN865-867: id: catena4618-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618 deviceType: devkit diff --git a/vendor/mcci/catena4618m201.yaml b/vendor/mcci/catena4618m201.yaml index 5444a84464..860d71f15a 100644 --- a/vendor/mcci/catena4618m201.yaml +++ b/vendor/mcci/catena4618m201.yaml @@ -24,27 +24,27 @@ firmwareVersions: # unique ID of the profile (lowercase, alpha with dashes, max 36) id: catena4618m201-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618m201 US902-928: id: catena4618m201-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618m201 AU915-928: id: catena4618m201-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618m201 AS923: id: catena4618m201-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618m201 KR920-923: id: catena4618m201-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618m201 IN865-867: id: catena4618m201-profile lorawanCertified: false - codec: codec-catena-generic + codec: codec-catena4618m201 deviceType: devkit diff --git a/vendor/mcci/catena4801.yaml b/vendor/mcci/catena4801.yaml index ff352fac00..65a0b09322 100644 --- a/vendor/mcci/catena4801.yaml +++ b/vendor/mcci/catena4801.yaml @@ -1,5 +1,5 @@ name: Catena® 4801 Modbus Node -description: The MCCI Catena® 4801 RS-485 Node for LoRaWAN® technology networks is a complete open-source single-board IoT device that can monitor and control remote Modbus, M-Bus and other industrial devices. It also allows connecting external sensors. Based on the Murata CMWX1ZZABZ-078, the Catena 4801 is a great platform for RS-485/Modbus based LoRaWAN investigation and deployment. +description: General purpose LPWAN building block, open source, user-programmable single-board end-device with RS-485 Modbus interface and support # Hardware versions (optional, used for revisions) # hardwareVersions: @@ -46,6 +46,42 @@ firmwareVersions: lorawanCertified: false codec: codec-catena-generic + - # Firmware version + version: '0.1.2' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: catena4801-profile + lorawanCertified: false + codec: codec-catena-generic + US902-928: + id: catena4801-profile + lorawanCertified: false + codec: codec-catena-generic + AU915-928: + id: catena4801-profile + lorawanCertified: false + codec: codec-catena-generic + AS923: + id: catena4801-profile + lorawanCertified: false + codec: codec-catena-generic + KR920-923: + id: catena4801-profile + lorawanCertified: false + codec: codec-catena-generic + IN865-867: + id: catena4801-profile + lorawanCertified: false + codec: codec-catena-generic + # Sensors that this device features (optional) # Valid values are: # accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, diff --git a/vendor/mcci/catena4802.yaml b/vendor/mcci/catena4802.yaml index 4cbfbbb54b..fbec1a4dc5 100644 --- a/vendor/mcci/catena4802.yaml +++ b/vendor/mcci/catena4802.yaml @@ -46,6 +46,42 @@ firmwareVersions: lorawanCertified: false codec: codec-catena-generic + - # Firmware version + version: '0.1.0' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: catena4802-profile + lorawanCertified: false + codec: codec-catena4802 + US902-928: + id: catena4802-profile + lorawanCertified: false + codec: codec-catena4802 + AU915-928: + id: catena4802-profile + lorawanCertified: false + codec: codec-catena4802 + AS923: + id: catena4802-profile + lorawanCertified: false + codec: codec-catena4802 + KR920-923: + id: catena4802-profile + lorawanCertified: false + codec: codec-catena4802 + IN865-867: + id: catena4802-profile + lorawanCertified: false + codec: codec-catena4802 + # Sensors that this device features (optional) # Valid values are: # accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, diff --git a/vendor/mcci/codec-animal-activity-v1.js b/vendor/mcci/codec-animal-activity-v1.js new file mode 100644 index 0000000000..baee42121c --- /dev/null +++ b/vendor/mcci/codec-animal-activity-v1.js @@ -0,0 +1,408 @@ +/* + +Name: codec-port2-port3-fmt22-fmt33.js + +Function: + Decode port 0x02 and 0x03 format 0x22 and 0x33 messages for TTN console. + +Copyright and License: + See accompanying LICENSE file at https://github.com/mcci-catena/MCCI-Catena-4430/ + +Author: + Terry Moore, MCCI Corporation August 2019 + +*/ + +// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) +// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html +// rearranged for efficiency and to deal sanely with very low (< 1%) RH +function dewpoint(t, rh) { + var c1 = 243.04; + var c2 = 17.625; + var h = rh / 100; + if (h <= 0.01) + h = 0.01; + else if (h > 1.0) + h = 1.0; + + var lnh = Math.log(h); + var tpc1 = t + c1; + var txc2 = t * c2; + var txc2_tpc1 = txc2 / tpc1; + + var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); + return tdew; +} + +/* + +Name: CalculateHeatIndex() + +Description: + Calculate the NWS heat index given dry-bulb T and RH + +Definition: + function CalculateHeatIndex(t, rh) -> value or null + +Description: + T is a Farentheit temperature in [76,120]; rh is a + relative humidity in [0,100]. The heat index is computed + and returned; or an error is returned. For consistency with + the other temperature, despite the heat index being defined + in Farenheit, we return in Celsius. + +Returns: + number => heat index in Farenheit. + null => error. + +References: + https://github.com/mcci-catena/heat-index/ + https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml + + Results was checked against the full chart at iweathernet.com: + https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png + + The MCCI-Catena heat-index site has a test js script to generate CSV to + match the chart, a spreadsheet that recreates the chart, and a + spreadsheet that compares results. + +*/ + +function CalculateHeatIndex(t, rh) { + var tRounded = Math.floor(t + 0.5); + + // return null outside the specified range of input parameters + if (tRounded < 76 || tRounded > 126) + return null; + if (rh < 0 || rh > 100) + return null; + + // according to the NWS, we try this first, and use it if we can + var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); + + // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 + // This is the same computation: + if ((tHeatEasy + t) < 160.0) + return (tHeatEasy - 32) * 5 / 9; + + // need to use the hard form, and possibly adjust. + var t2 = t * t; // t squared + var rh2 = rh * rh; // rh squared + var tResult = + -42.379 + + (2.04901523 * t) + + (10.14333127 * rh) + + (-0.22475541 * t * rh) + + (-0.00683783 * t2) + + (-0.05481717 * rh2) + + (0.00122874 * t2 * rh) + + (0.00085282 * t * rh2) + + (-0.00000199 * t2 * rh2); + + // these adjustments come from the NWA page, and are needed to + // match the reference table. + var tAdjust; + if (rh < 13.0 && 80.0 <= t && t <= 112.0) + tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); + else if (rh > 85.0 && 80.0 <= t && t <= 87.0) + tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); + else + tAdjust = 0; + + // apply the adjustment + tResult += tAdjust; + + // finally, the reference tables have no data above 183 (rounded), + // so filter out answers that we have no way to vouch for. + if (tResult >= 183.5) + return null; + else + return (tResult - 32) * 5 / 9; +} + +function DecodeU16(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + var result = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + return result; +} + +function DecodeUflt16(Parse) { + var rawUflt16 = DecodeU16(Parse); + var exp1 = rawUflt16 >> 12; + var mant1 = (rawUflt16 & 0xFFF) / 4096.0; + var f_unscaled = mant1 * Math.pow(2, exp1 - 15); + return f_unscaled; +} + +function DecodeSflt16(Parse) + { + var rawSflt16 = DecodeU16(Parse); + // rawSflt16 is the 2-byte number decoded from wherever; + // it's in range 0..0xFFFF + // bit 15 is the sign bit + // bits 14..11 are the exponent + // bits 10..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + // + // The result is a number in the open interval (-1.0, 1.0); + // + + // throw away high bits for repeatability. + rawSflt16 &= 0xFFFF; + + // special case minus zero: + if (rawSflt16 === 0x8000) + return -0.0; + + // extract the sign. + var sSign = ((rawSflt16 & 0x8000) !== 0) ? -1 : 1; + + // extract the exponent + var exp1 = (rawSflt16 >> 11) & 0xF; + + // extract the "mantissa" (the fractional part) + var mant1 = (rawSflt16 & 0x7FF) / 2048.0; + + // convert back to a floating point number. We hope + // that Math.pow(2, k) is handled efficiently by + // the JS interpreter! If this is time critical code, + // you can replace by a suitable shift and divide. + var f_unscaled = sSign * mant1 * Math.pow(2, exp1 - 15); + + return f_unscaled; + } + + +function DecodeLight(Parse) { + return DecodeUflt16(Parse); +} + +function DecodeActivity(Parse) { + return DecodeSflt16(Parse); +} + +function DecodeI16(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + var result = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + + // interpret uint16 as an int16 instead. + if (result & 0x8000) + result += -0x10000; + + return result; +} + +function DecodeU24(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 16) + (bytes[i + 1] << 8) + bytes[i + 2]; + Parse.i = i + 3; + + return result; +} + +function DecodeSflt24(Parse) + { + var rawSflt24 = DecodeU24(Parse); + // rawSflt24 is the 3-byte number decoded from wherever; + // it's in range 0..0xFFFFFF + // bit 23 is the sign bit + // bits 22..16 are the exponent + // bits 15..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + + // extract sign, exponent, mantissa + var bSign = (rawSflt24 & 0x800000) ? true : false; + var uExp = (rawSflt24 & 0x7F0000) >> 16; + var uMantissa = (rawSflt24 & 0x00FFFF); + + // if non-numeric, return appropriate result. + if (uExp === 0x7F) { + if (uMantissa === 0) + return bSign ? Number.NEGATIVE_INFINITY + : Number.POSITIVE_INFINITY; + else + return Number.NaN; + // else unless denormal, set the 1.0 bit + } else if (uExp !== 0) { + uMantissa += 0x010000; + } else { // denormal: exponent is the minimum + uExp = 1; + } + + // make a floating mantissa in [0,2); usually [1,2), but + // sometimes (0,1) for denormals, and exactly zero for zero. + var mantissa = uMantissa / 0x010000; + + // apply the exponent. + mantissa = Math.pow(2, uExp - 63) * mantissa; + + // apply sign and return result. + return bSign ? -mantissa : mantissa; + } + +function DecodeLux(Parse) { + return DecodeSflt24(Parse); +} + +function DecodeI32(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + // interpret uint16 as an int16 instead. + if (result & 0x80000000) + result += -0x100000000; + + return result; +} + +function DecodeU32(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + return result; +} + +function RemainingBytes(Parse) { + var i = Parse.i; + var nBytes = Parse.bytes.length; + + if (i < nBytes) + return (nBytes - i); + else + return 0; +} + +function DecodeV(Parse) { + return DecodeI16(Parse) / 4096.0; +} + +function Decoder(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + if (! (port === 2) && ! (port === 3)) + return null; + + var uFormat = bytes[0]; + if (! (uFormat === 0x22) && ! (uFormat === 0x33)) + return null; + + // an object to help us parse. + var Parse = {}; + Parse.bytes = bytes; + // i is used as the index into the message. Start with the time. + Parse.i = 1; + + // fetch time; convert to database time (which is UTC-like ignoring leap seconds) + decoded.time = new Date((DecodeU32(Parse) + /* gps epoch to posix */ 315964800 - /* leap seconds */ 17) * 1000); + + // fetch the bitmap. + var flags = bytes[Parse.i++]; + + if (flags & 0x1) { + decoded.Vbat = DecodeV(Parse); + } + + if (flags & 0x2) { + decoded.Vsys = DecodeV(Parse); + } + + if (flags & 0x4) { + decoded.Vbus = DecodeV(Parse); + } + + if (flags & 0x8) { + var iBoot = bytes[Parse.i++]; + decoded.boot = iBoot; + } + + if (flags & 0x10) { + if (uFormat === 0x22) { + // we have temp, pressure, RH + decoded.tempC = DecodeI16(Parse) / 256; + decoded.p = DecodeU16(Parse) * 4 / 100.0; + decoded.rh = DecodeU16(Parse) * 100 / 65535.0; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + var tHeat = CalculateHeatIndex(decoded.tempC * 1.8 + 32, decoded.rh); + if (tHeat !== null) + decoded.tHeatIndexC = tHeat; + } + else if (uFormat === 0x33 ) { + // we have temp, pressure, RH + decoded.tempC = DecodeI16(Parse) / 256; + decoded.rh = DecodeU16(Parse) * 100 / 65535.0; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + var tHeat = CalculateHeatIndex(decoded.tempC * 1.8 + 32, decoded.rh); + if (tHeat !== null) + decoded.tHeatIndexC = tHeat; + } + } + + if (flags & 0x20) { + if (uFormat === 0x22) { + // we have light + decoded.irradiance = {}; + decoded.irradiance.White = DecodeLight(Parse) * Math.pow(2.0, 24); + } + else if (uFormat === 0x33) { + // we have light + decoded.lux = DecodeLux(Parse); + } + } + + if (flags & 0x40) { + // we have gpio counts + decoded.pellets = []; + for (var i = 0; i < 2; ++i) { + decoded.pellets[i] = {}; + decoded.pellets[i].Total = DecodeU16(Parse); + decoded.pellets[i].Delta = bytes[Parse.i++]; + } + } + + if (flags & 0x80) { + // we have Activity + decoded.activity = []; + var i = 0; + while (RemainingBytes(Parse) >= 2) { + decoded.activity[i] = DecodeActivity(Parse); + ++i; + } + } + + if (port === 3) + decoded.NwTime = "set"; + + return decoded; +} + +// TTN V3 decoder +function decodeUplink(tInput) { + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; +} \ No newline at end of file diff --git a/vendor/mcci/codec-animal-activity-v1.yaml b/vendor/mcci/codec-animal-activity-v1.yaml new file mode 100644 index 0000000000..af5ed9f3ce --- /dev/null +++ b/vendor/mcci/codec-animal-activity-v1.yaml @@ -0,0 +1,51 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: codec-animal-activity-v1.js + # Examples (optional) + examples: + - description: Time 1255474907 Vbat 2 Vsys 3.3 Vbus 4.9 Boot 42 Env 30 1017.1 60 Light 1.23401e+06 Pellets 100 3 25 10 Activity [ 0.53 -1 1 -0.5 0.25 -0.3 ] . + input: + fPort: 2 + bytes: [34, 74, 213, 6, 219, 255, 32, 0, 52, 205, 78, 102, 42, 30, 0, 99, 84, 153, 153, 255, 255, 0, 100, 3, 0, 25, 10, 124, 61, 255, 255, 127, 255, 252, 0, 116, 0, 244, 205] + output: + data: + { + 'Vbat': 2, + 'Vbus': 4.89990234375, + 'Vsys': 3.300048828125, + 'activity': [0.52978515625, -0.99951171875, 0.99951171875, -0.5, 0.25, -0.300048828125], + 'boot': 42, + 'irradiance': { 'White': 16773120 }, + 'p': 1017.12, + 'pellets': [{ 'Delta': 3, 'Total': 100 }, { 'Delta': 10, 'Total': 25 }], + 'rh': 60, + 'tDewC': 21.390006900020513, + 'tHeatIndexC': 32.83203227777776, + 'tempC': 30, + 'time': '2019-10-18T23:01:30Z', + } +# Downlink encoder encodes JSON object into a binary data downlink (optional) +## downlinkEncoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## data: +## led: green +## output: +## bytes: [1] +## fPort: 2 +## # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +## downlinkDecoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## fPort: 2 +## bytes: [1] +## output: +## data: +## led: green diff --git a/vendor/mcci/codec-animal-activity-v2.js b/vendor/mcci/codec-animal-activity-v2.js new file mode 100644 index 0000000000..bc3a781efb --- /dev/null +++ b/vendor/mcci/codec-animal-activity-v2.js @@ -0,0 +1,578 @@ +/* + +Name: codec-animal-activity-v2.js + +Function: + Decode port 0x02/0x03/0x04 format 0x26/0x36 messages for TTN console. + +Copyright and License: + See accompanying LICENSE file at https://github.com/mcci-catena/MCCI-Catena-4430/ + +Author: + Dhinesh Kumar Pitchai, MCCI Corporation August 2022 + +*/ + +// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) +// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html +// rearranged for efficiency and to deal sanely with very low (< 1%) RH +function dewpoint(t, rh) { + var c1 = 243.04; + var c2 = 17.625; + var h = rh / 100; + if (h <= 0.01) + h = 0.01; + else if (h > 1.0) + h = 1.0; + + var lnh = Math.log(h); + var tpc1 = t + c1; + var txc2 = t * c2; + var txc2_tpc1 = txc2 / tpc1; + + var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); + return tdew; +} + +/* + +Name: CalculateHeatIndex() + +Description: + Calculate the NWS heat index given dry-bulb T and RH + +Definition: + function CalculateHeatIndex(t, rh) -> value or null + +Description: + T is a Farentheit temperature in [76,120]; rh is a + relative humidity in [0,100]. The heat index is computed + and returned; or an error is returned. For consistency with + the other temperature, despite the heat index being defined + in Farenheit, we return in Celsius. + +Returns: + number => heat index in Farenheit. + null => error. + +References: + https://github.com/mcci-catena/heat-index/ + https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml + + Results was checked against the full chart at iweathernet.com: + https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png + + The MCCI-Catena heat-index site has a test js script to generate CSV to + match the chart, a spreadsheet that recreates the chart, and a + spreadsheet that compares results. + +*/ + +function CalculateHeatIndex(t, rh) { + var tRounded = Math.floor(t + 0.5); + + // return null outside the specified range of input parameters + if (tRounded < 76 || tRounded > 126) + return null; + if (rh < 0 || rh > 100) + return null; + + // according to the NWS, we try this first, and use it if we can + var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); + + // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 + // This is the same computation: + if ((tHeatEasy + t) < 160.0) + return (tHeatEasy - 32) * 5 / 9; + + // need to use the hard form, and possibly adjust. + var t2 = t * t; // t squared + var rh2 = rh * rh; // rh squared + var tResult = + -42.379 + + (2.04901523 * t) + + (10.14333127 * rh) + + (-0.22475541 * t * rh) + + (-0.00683783 * t2) + + (-0.05481717 * rh2) + + (0.00122874 * t2 * rh) + + (0.00085282 * t * rh2) + + (-0.00000199 * t2 * rh2); + + // these adjustments come from the NWA page, and are needed to + // match the reference table. + var tAdjust; + if (rh < 13.0 && 80.0 <= t && t <= 112.0) + tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); + else if (rh > 85.0 && 80.0 <= t && t <= 87.0) + tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); + else + tAdjust = 0; + + // apply the adjustment + tResult += tAdjust; + + // finally, the reference tables have no data above 183 (rounded), + // so filter out answers that we have no way to vouch for. + if (tResult >= 183.5) + return null; + else + return (tResult - 32) * 5 / 9; +} + +function DecodeU16(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + var result = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + return result; +} + +function DecodeUflt16(Parse) { + var rawUflt16 = DecodeU16(Parse); + var exp1 = rawUflt16 >> 12; + var mant1 = (rawUflt16 & 0xFFF) / 4096.0; + var f_unscaled = mant1 * Math.pow(2, exp1 - 15); + return f_unscaled; +} + +function DecodeSflt16(Parse) + { + var rawSflt16 = DecodeU16(Parse); + // rawSflt16 is the 2-byte number decoded from wherever; + // it's in range 0..0xFFFF + // bit 15 is the sign bit + // bits 14..11 are the exponent + // bits 10..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + // + // The result is a number in the open interval (-1.0, 1.0); + // + + // throw away high bits for repeatability. + rawSflt16 &= 0xFFFF; + + // special case minus zero: + if (rawSflt16 === 0x8000) + return -0.0; + + // extract the sign. + var sSign = ((rawSflt16 & 0x8000) !== 0) ? -1 : 1; + + // extract the exponent + var exp1 = (rawSflt16 >> 11) & 0xF; + + // extract the "mantissa" (the fractional part) + var mant1 = (rawSflt16 & 0x7FF) / 2048.0; + + // convert back to a floating point number. We hope + // that Math.pow(2, k) is handled efficiently by + // the JS interpreter! If this is time critical code, + // you can replace by a suitable shift and divide. + var f_unscaled = sSign * mant1 * Math.pow(2, exp1 - 15); + + return f_unscaled; + } + + +function DecodeLight(Parse) { + return DecodeUflt16(Parse); +} + +function DecodeActivity(Parse) { + return DecodeSflt16(Parse); +} + +function DecodeI16(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + var result = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + + // interpret uint16 as an int16 instead. + if (result & 0x8000) + result += -0x10000; + + return result; +} + +function DecodeU24(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i] << 16) + (bytes[i + 1] << 8) + bytes[i + 2]; + Parse.i = i + 3; + + return result; +} + +function DecodeSflt24(Parse) + { + var rawSflt24 = DecodeU24(Parse); + // rawSflt24 is the 3-byte number decoded from wherever; + // it's in range 0..0xFFFFFF + // bit 23 is the sign bit + // bits 22..16 are the exponent + // bits 15..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + + // extract sign, exponent, mantissa + var bSign = (rawSflt24 & 0x800000) ? true : false; + var uExp = (rawSflt24 & 0x7F0000) >> 16; + var uMantissa = (rawSflt24 & 0x00FFFF); + + // if non-numeric, return appropriate result. + if (uExp === 0x7F) { + if (uMantissa === 0) + return bSign ? Number.NEGATIVE_INFINITY + : Number.POSITIVE_INFINITY; + else + return Number.NaN; + // else unless denormal, set the 1.0 bit + } else if (uExp !== 0) { + uMantissa += 0x010000; + } else { // denormal: exponent is the minimum + uExp = 1; + } + + // make a floating mantissa in [0,2); usually [1,2), but + // sometimes (0,1) for denormals, and exactly zero for zero. + var mantissa = uMantissa / 0x010000; + + // apply the exponent. + mantissa = Math.pow(2, uExp - 63) * mantissa; + + // apply sign and return result. + return bSign ? -mantissa : mantissa; + } + +function DecodeLux(Parse) { + return DecodeSflt24(Parse); +} + +function DecodeI32(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + // interpret uint16 as an int16 instead. + if (result & 0x80000000) + result += -0x100000000; + + return result; +} + +function DecodeU32(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + return result; +} + +function RemainingBytes(Parse) { + var i = Parse.i; + var nBytes = Parse.bytes.length; + + if (i < nBytes) + return (nBytes - i); + else + return 0; +} + +function DecodeV(Parse) { + return DecodeI16(Parse) / 4096.0; +} + +function DecodeDownlinkResponse(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + // an object to help us parse. + var Parse = {}; + Parse.bytes = bytes; + // i is used as the index into the message. Start with the flag byte. + Parse.i = 0; + + if (port === 1) + { + // SW version and HW details. + decoded.ResponseType = "Set Uplink Interval"; + // fetch the bitmap. + var response = bytes[Parse.i++]; + if (response === 0) + decoded.response = "Success"; + else if (response === 1) + decoded.response = "Invalid Length"; + else if (response === 2) + decoded.response = "Failure"; + else if (response === 3) + decoded.response = "Invalid Range"; + } + + else if (port === 2) + { + // SW version and HW details. + decoded.ResponseType = "SCD30 CO2 Calibration"; + // fetch the bitmap. + var response = bytes[Parse.i++]; + if (response === 0) + decoded.response = "Success"; + else if (response === 1) + decoded.response = "Invalid Length"; + else if (response === 2) + decoded.response = "Failure"; + else if (response === 3) + decoded.response = "Invalid Range"; + else if (response === 4) + decoded.response = "Sensor Not Connected"; + } + + else if (port === 3) + { + // fetch the bitmap. + var command = bytes[Parse.i++]; + + if (command === 0x02) { + // Reset do not send a reply back. + } + + else if (command === 0x03) { + // SW version and HW details. + decoded.ResponseType = "Device Version"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.ResponseError = "Success"; + else if (responseError === 1) + decoded.ResponseError = "Invalid Length"; + else if (responseError === 2) + decoded.ResponseError = "Failure"; + + var vMajor = bytes[Parse.i++]; + var vMinor = bytes[Parse.i++]; + var vPatch = bytes[Parse.i++]; + var vLocal = bytes[Parse.i++]; + decoded.AppVersion = "V" + vMajor + "." + vMinor + "." + vPatch + "." + vLocal; + + var Model = DecodeU16(Parse); + var Rev = bytes[Parse.i++]; + if (!(Model === 0)) + { + decoded.Model = Model; + if (Rev === 0) + decoded.Rev = "A"; + else if (Rev === 1) + decoded.Rev = "B"; + else if (Rev === 2) + decoded.Rev = "C"; + else if (Rev === 3) + decoded.Rev = "D"; + else if (Rev === 4) + decoded.Rev = "E"; + else if (Rev === 5) + decoded.Rev = "F"; + else if (Rev === 6) + decoded.Rev = "G"; + } + else if (Model === 0) + { + decoded.Model = 4610; + decoded.Rev = "Not Found"; + } + } + + else if (command === 0x04) { + // Reset/Set AppEUI. + decoded.ResponseType = "AppEUI Set"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.ResponseError = "Success"; + else if (responseError === 1) + decoded.ResponseError = "Invalid Length"; + else if (responseError === 2) + decoded.ResponseError = "Failure"; + } + + else if (command === 0x05) { + // Reset/Set AppKey. + decoded.ResponseType = "AppKey set"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.ResponseError = "Success"; + else if (responseError === 1) + decoded.ResponseError = "Invalid Length"; + else if (responseError === 2) + decoded.ResponseError = "Failure"; + } + + else if (command === 0x06) { + // Rejoin the network. + decoded.ResponseType = "Rejoin"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.ResponseError = "Success"; + else if (responseError === 1) + decoded.ResponseError = "Invalid Length"; + else if (responseError === 2) + decoded.ResponseError = "Failure"; + } + + else if (command === 0x07) { + // Uplink Interval for sensor data. + decoded.ResponseType = "Uplink Interval"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.ResponseError = "Success"; + else if (responseError === 1) + decoded.ResponseError = "Invalid Length"; + else if (responseError === 2) + decoded.ResponseError = "Failure"; + + decoded.UplinkInterval = DecodeU32(Parse); + } + else + return null; + } + + return decoded; +} + +function Decoder(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + if (! (port === 1) && ! (port === 2) && ! (port === 3) && ! (port === 4)) + return null; + + var uFormat = bytes[0]; + if (! (uFormat === 0x26) && ! (uFormat === 0x36) && ((port === 1) || (port === 2) || (port === 3))) + { + decoded = DecodeDownlinkResponse(bytes, port); + return decoded; + } + else if (! (uFormat === 0x26) && ! (uFormat === 0x36) && ! ((port === 1) || (port === 2) || (port === 3))) + return null; + + // an object to help us parse. + var Parse = {}; + Parse.bytes = bytes; + // i is used as the index into the message. Start with the time. + Parse.i = 1; + + // fetch time; convert to database time (which is UTC-like ignoring leap seconds) + decoded.time = new Date((DecodeU32(Parse) + /* gps epoch to posix */ 315964800 - /* leap seconds */ 17) * 1000); + + // fetch the bitmap. + var flags = bytes[Parse.i++]; + + if (flags & 0x1) { + decoded.Vbat = DecodeV(Parse); + } + + if (flags & 0x2) { + var vMajor = bytes[Parse.i++]; + var vMinor = bytes[Parse.i++]; + var vPatch = bytes[Parse.i++]; + var vLocal = bytes[Parse.i++]; + + decoded.version = vMajor + "." + vMinor + "." + vPatch + "." + vLocal; + } + + if (flags & 0x4) { + // we have CO2 ppm. + decoded.co2 = DecodeUflt16(Parse) * 40000; + } + + if (flags & 0x8) { + var iBoot = bytes[Parse.i++]; + decoded.boot = iBoot; + } + + if (flags & 0x10) { + if (uFormat === 0x26) { + // we have temp, pressure, RH + decoded.tempC = DecodeI16(Parse) / 256; + decoded.p = DecodeU16(Parse) * 4 / 100.0; + decoded.rh = DecodeU16(Parse) * 100 / 65535.0; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + var tHeat = CalculateHeatIndex(decoded.tempC * 1.8 + 32, decoded.rh); + if (tHeat !== null) + decoded.tHeatIndexC = tHeat; + } + else if (uFormat === 0x36) { + // we have temp, pressure, RH + decoded.tempC = DecodeI16(Parse) / 256; + decoded.rh = DecodeU16(Parse) * 100 / 65535.0; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + var tHeat = CalculateHeatIndex(decoded.tempC * 1.8 + 32, decoded.rh); + if (tHeat !== null) + decoded.tHeatIndexC = tHeat; + } + } + + if (flags & 0x20) { + if (uFormat === 0x26) { + // we have light + decoded.irradiance = {}; + decoded.irradiance.White = DecodeLight(Parse) * Math.pow(2.0, 24); + } + else if (uFormat === 0x36) { + // we have light + decoded.lux = DecodeLux(Parse); + } + } + + if (flags & 0x40) { + // we have gpio counts + decoded.pellets = []; + for (var i = 0; i < 2; ++i) { + decoded.pellets[i] = {}; + decoded.pellets[i].Total = DecodeU16(Parse); + decoded.pellets[i].Delta = bytes[Parse.i++]; + } + } + + if (flags & 0x80) { + // we have Activity + decoded.activity = []; + var i = 0; + while (RemainingBytes(Parse) >= 2) { + decoded.activity[i] = DecodeActivity(Parse); + ++i; + } + } + + if (port === 3) + decoded.NwTime = "set"; + + return decoded; +} + +// TTN V3 decoder +function decodeUplink(tInput) { + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; +} \ No newline at end of file diff --git a/vendor/mcci/codec-animal-activity-v2.yaml b/vendor/mcci/codec-animal-activity-v2.yaml new file mode 100644 index 0000000000..4a90873a29 --- /dev/null +++ b/vendor/mcci/codec-animal-activity-v2.yaml @@ -0,0 +1,81 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: codec-animal-activity-v2.js + # Examples (optional) + examples: + - description: Time 1742982409 Vbat 2 Version 2 5 2 0 CO2 800 Boot 45 Env 20 60 Light 1377 Pellets 100 3 25 10 Activity [ 0.53 -1 1 -0.5 0.25 -0.3 ] . + input: + fPort: 2 + bytes: + [ + 0x36, + 0x55, + 0x14, + 0xA6, + 0xF2, + 0xF9, + 0x3B, + 0x2C, + 0x09, + 0x16, + 0x16, + 0xAD, + 0x87, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDE, + 0x4B, + 0xEF, + 0x05, + 0xF7, + 0xF5, + 0xFE, + 0xEF, + 0xF4, + 0x32, + ] + output: + data: + { + 'Vbat': 3.6982421875, + 'activity': [-0.049163818359375, -0.2193603515625, -0.497314453125, -0.86669921875, -0.26220703125], + 'boot': 9, + 'lux': 0, + 'pellets': [{ 'Delta': 0, 'Total': 0 }, { 'Delta': 0, 'Total': 0 }], + 'rh': 67.78515297169452, + 'tDewC': 15.855392059081987, + 'tempC': 22.0859375, + 'time': '2025-03-31T00:40:01Z', + } +# Downlink encoder encodes JSON object into a binary data downlink (optional) +## downlinkEncoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## data: +## led: green +## output: +## bytes: [1] +## fPort: 2 +## # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +## downlinkDecoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## fPort: 2 +## bytes: [1] +## output: +## data: +## led: green diff --git a/vendor/mcci/codec-catena-generic.yaml b/vendor/mcci/codec-catena-generic.yaml index de3c595eec..5d50c9776f 100644 --- a/vendor/mcci/codec-catena-generic.yaml +++ b/vendor/mcci/codec-catena-generic.yaml @@ -10,27 +10,6 @@ uplinkDecoder: bytes: [0x14, 0x1D, 0x43, 0x23, 0x11, 0x19, 0x52, 0x5F, 0x97, 0xAE, 0x01, 0x02] output: data: { 'boot': 17, 'error': 'none', 'lux': 258, 'p': 978.84, 'rh': 67.96875, 'tDewC': 18.981996766825645, 'tempC': 25.3203125, 'vBat': 4.196044921875 } - - description: Sensor report format 0x14, with pulse/power input - input: - fPort: 1 - bytes: [0x14, 0x7F, 0x43, 0x23, 0x4F, 0x01, 0x11, 0x19, 0x52, 0x5F, 0x97, 0xAE, 0x03, 0x01, 0xC5, 0x50, 0x31, 0x24, 0xBF, 0x54, 0xD8, 0x39] - output: - data: - { - 'boot': 17, - 'error': 'none', - 'lux': 769, - 'p': 978.84, - 'powerSourcedCount': 12580, - 'powerSourcedPerHour': 1850.09765625, - 'powerUsedCount': 50512, - 'powerUsedPerHour': 862.20703125, - 'rh': 67.96875, - 'tDewC': 18.981996766825645, - 'tempC': 25.3203125, - 'vBat': 4.196044921875, - 'vBus': 4.937744140625, - } # Downlink encoder encodes JSON object into a binary data downlink (optional) ## downlinkEncoder: ## fileName: codec.js diff --git a/vendor/mcci/codec-catena4610.js b/vendor/mcci/codec-catena4610.js new file mode 100644 index 0000000000..c39508c23c --- /dev/null +++ b/vendor/mcci/codec-catena4610.js @@ -0,0 +1,378 @@ +/* + +Name: catena-message-port1-format-14-1A-decoder-ttn.js + +Function: + Decode port 0x01 format 0x14, 0x1A messages for TTN console. + +Copyright and License: + See accompanying LICENSE file at https://github.com/mcci-catena/catena4610_simple/ + +Author: + Dhinesh Kumar Pitchai, MCCI Corporation March 2024 + +*/ + +// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) +// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html +// rearranged for efficiency and to deal sanely with very low (< 1%) RH +function dewpoint(t, rh) { + var c1 = 243.04; + var c2 = 17.625; + var h = rh / 100; + if (h <= 0.01) + h = 0.01; + else if (h > 1.0) + h = 1.0; + + var lnh = Math.log(h); + var tpc1 = t + c1; + var txc2 = t * c2; + var txc2_tpc1 = txc2 / tpc1; + + var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); + return tdew; +} + +/* + +Name: CalculateHeatIndex() + +Description: + Calculate the NWS heat index given dry-bulb T and RH + +Definition: + function CalculateHeatIndex(t, rh) -> value or null + +Description: + T is a Farentheit temperature in [76,120]; rh is a + relative humidity in [0,100]. The heat index is computed + and returned; or an error is returned. For consistency with + the other temperature, despite the heat index being defined + in Farenheit, we return in Celsius. + +Returns: + number => heat index in Farenheit. + null => error. + +References: + https://github.com/mcci-catena/heat-index/ + https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml + + Results was checked against the full chart at iweathernet.com: + https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png + + The MCCI-Catena heat-index site has a test js script to generate CSV to + match the chart, a spreadsheet that recreates the chart, and a + spreadsheet that compares results. + +*/ + +function CalculateHeatIndex(t, rh) { + var tRounded = Math.floor(t + 0.5); + + // return null outside the specified range of input parameters + if (tRounded < 76 || tRounded > 126) + return null; + if (rh < 0 || rh > 100) + return null; + + // according to the NWS, we try this first, and use it if we can + var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); + + // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 + // This is the same computation: + if ((tHeatEasy + t) < 160.0) + return (tHeatEasy - 32) * 5 / 9; + + // need to use the hard form, and possibly adjust. + var t2 = t * t; // t squared + var rh2 = rh * rh; // rh squared + var tResult = + -42.379 + + (2.04901523 * t) + + (10.14333127 * rh) + + (-0.22475541 * t * rh) + + (-0.00683783 * t2) + + (-0.05481717 * rh2) + + (0.00122874 * t2 * rh) + + (0.00085282 * t * rh2) + + (-0.00000199 * t2 * rh2); + + // these adjustments come from the NWA page, and are needed to + // match the reference table. + var tAdjust; + if (rh < 13.0 && 80.0 <= t && t <= 112.0) + tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); + else if (rh > 85.0 && 80.0 <= t && t <= 87.0) + tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); + else + tAdjust = 0; + + // apply the adjustment + tResult += tAdjust; + + // finally, the reference tables have no data above 183 (rounded), + // so filter out answers that we have no way to vouch for. + if (tResult >= 183.5) + return null; + else + return (tResult - 32) * 5 / 9; +} + +function DecodeU16(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + var result = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + return result; +} + +function DecodeUflt16(Parse) { + var rawUflt16 = DecodeU16(Parse); + var exp1 = rawUflt16 >> 12; + var mant1 = (rawUflt16 & 0xFFF) / 4096.0; + var f_unscaled = mant1 * Math.pow(2, exp1 - 15); + return f_unscaled; +} + +function DecodeSflt16(Parse) + { + var rawSflt16 = DecodeU16(Parse); + // rawSflt16 is the 2-byte number decoded from wherever; + // it's in range 0..0xFFFF + // bit 15 is the sign bit + // bits 14..11 are the exponent + // bits 10..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + // + // The result is a number in the open interval (-1.0, 1.0); + // + + // throw away high bits for repeatability. + rawSflt16 &= 0xFFFF; + + // special case minus zero: + if (rawSflt16 === 0x8000) + return -0.0; + + // extract the sign. + var sSign = ((rawSflt16 & 0x8000) !== 0) ? -1 : 1; + + // extract the exponent + var exp1 = (rawSflt16 >> 11) & 0xF; + + // extract the "mantissa" (the fractional part) + var mant1 = (rawSflt16 & 0x7FF) / 2048.0; + + // convert back to a floating point number. We hope + // that Math.pow(2, k) is handled efficiently by + // the JS interpreter! If this is time critical code, + // you can replace by a suitable shift and divide. + var f_unscaled = sSign * mant1 * Math.pow(2, exp1 - 15); + + return f_unscaled; + } + + +function DecodeLight(Parse) { + return DecodeUflt16(Parse); +} + +function DecodeActivity(Parse) { + return DecodeSflt16(Parse); +} + +function DecodeI16(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + var result = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + + // interpret uint16 as an int16 instead. + if (result & 0x8000) + result += -0x10000; + + return result; +} + +function DecodeU24(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 16) + (bytes[i + 1] << 8) + bytes[i + 2]; + Parse.i = i + 3; + + return result; +} + +function DecodeSflt24(Parse) + { + var rawSflt24 = DecodeU24(Parse); + // rawSflt24 is the 3-byte number decoded from wherever; + // it's in range 0..0xFFFFFF + // bit 23 is the sign bit + // bits 22..16 are the exponent + // bits 15..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + + // extract sign, exponent, mantissa + var bSign = (rawSflt24 & 0x800000) ? true : false; + var uExp = (rawSflt24 & 0x7F0000) >> 16; + var uMantissa = (rawSflt24 & 0x00FFFF); + + // if non-numeric, return appropriate result. + if (uExp === 0x7F) { + if (uMantissa === 0) + return bSign ? Number.NEGATIVE_INFINITY + : Number.POSITIVE_INFINITY; + else + return Number.NaN; + // else unless denormal, set the 1.0 bit + } else if (uExp !== 0) { + uMantissa += 0x010000; + } else { // denormal: exponent is the minimum + uExp = 1; + } + + // make a floating mantissa in [0,2); usually [1,2), but + // sometimes (0,1) for denormals, and exactly zero for zero. + var mantissa = uMantissa / 0x010000; + + // apply the exponent. + mantissa = Math.pow(2, uExp - 63) * mantissa; + + // apply sign and return result. + return bSign ? -mantissa : mantissa; + } + +function DecodeLux(Parse) { + return DecodeSflt24(Parse); +} + +function DecodeI32(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + // interpret uint16 as an int16 instead. + if (result & 0x80000000) + result += -0x100000000; + + return result; +} + +function DecodeU32(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + return result; +} + +function RemainingBytes(Parse) { + var i = Parse.i; + var nBytes = Parse.bytes.length; + + if (i < nBytes) + return (nBytes - i); + else + return 0; +} + +function DecodeV(Parse) { + return DecodeI16(Parse) / 4096.0; +} + +function Decoder(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + if (! (port === 1)) + return null; + + var uFormat = bytes[0]; + if (! (uFormat === 0x14) && ! (uFormat === 0x1A)) + return null; + + // an object to help us parse. + var Parse = {}; + Parse.bytes = bytes; + // i is used as the index into the message. Start with the time. + Parse.i = 1; + + // fetch the bitmap. + var flags = bytes[Parse.i++]; + + if (flags & 0x1) { + decoded.Vbat = DecodeV(Parse); + } + + if (flags & 0x2) { + decoded.Vsys = DecodeV(Parse); + } + + if (flags & 0x4) { + var iBoot = bytes[Parse.i++]; + decoded.boot = iBoot; + } + + if (flags & 0x8) { + if (uFormat === 0x14) { + // we have temp, pressure, RH + decoded.tempC = DecodeI16(Parse) / 256; + decoded.p = DecodeU16(Parse) * 4 / 100.0; + decoded.rh = DecodeU16(Parse) * 100 / 65535.0; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + var tHeat = CalculateHeatIndex(decoded.tempC * 1.8 + 32, decoded.rh); + if (tHeat !== null) + decoded.tHeatIndexC = tHeat; + } + else if (uFormat === 0x1A) { + // we have temp, pressure, RH + decoded.tempC = DecodeI16(Parse) / 256; + decoded.rh = DecodeU16(Parse) * 100 / 65535.0; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + var tHeat = CalculateHeatIndex(decoded.tempC * 1.8 + 32, decoded.rh); + if (tHeat !== null) + decoded.tHeatIndexC = tHeat; + } + } + + if (flags & 0x10) { + if (uFormat === 0x14) { + // we have light + decoded.irradiance = {}; + decoded.irradiance.White = DecodeLight(Parse) * Math.pow(2.0, 24); + } + else if (uFormat === 0x1A) { + // we have light + decoded.lux = DecodeLux(Parse); + } + } + + return decoded; +} + +// TTN V3 decoder +function decodeUplink(tInput) { + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; +} \ No newline at end of file diff --git a/vendor/mcci/codec-catena4610.yaml b/vendor/mcci/codec-catena4610.yaml new file mode 100644 index 0000000000..24b9edabcb --- /dev/null +++ b/vendor/mcci/codec-catena4610.yaml @@ -0,0 +1,36 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: codec-catena4610.js + # Examples (optional) + examples: + - description: Sensor report format 0x14, no pulse/power input + input: + fPort: 1 + bytes: [0x1A, 0x1D, 0x28, 0x18, 0x11, 0x19, 0x20, 0x9F, 0x97, 0x47, 0x9E, 0xD0] + output: + data: { 'Vbat': 2.505859375, 'boot': 17, 'lux': 414.8125, 'rh': 62.34073395895323, 'tDewC': 17.41972264670249, 'tempC': 25.125, 'tHeatIndexC': 25.320841386705993 } +# Downlink encoder encodes JSON object into a binary data downlink (optional) +## downlinkEncoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## data: +## led: green +## output: +## bytes: [1] +## fPort: 2 +## # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +## downlinkDecoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## fPort: 2 +## bytes: [1] +## output: +## data: +## led: green diff --git a/vendor/mcci/codec-catena4612.js b/vendor/mcci/codec-catena4612.js new file mode 100644 index 0000000000..060cbbe539 --- /dev/null +++ b/vendor/mcci/codec-catena4612.js @@ -0,0 +1,285 @@ +/* + +Name: catena-message-port2-port4-decoder-ttn.js + +Function: + Decode MCCI port 0x02 and 0x04 messages for TTN console. + +Copyright and License: + See accompanying LICENSE file at https://github.com/mcci-catena/MCCI-Catena-PMS7003/ + +Author: + Pranau Ravi Kumar, MCCI Corporation February 2025 + +*/ + +// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) +// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html +// rearranged for efficiency and to deal sanely with very low (< 1%) RH +function dewpoint(t, rh) { + var c1 = 243.04; + var c2 = 17.625; + var h = rh / 100; + if (h <= 0.01) + h = 0.01; + else if (h > 1.0) + h = 1.0; + + var lnh = Math.log(h); + var tpc1 = t + c1; + var txc2 = t * c2; + var txc2_tpc1 = txc2 / tpc1; + + var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); + return tdew; +} + +/* + +Name: CalculateHeatIndex() + +Description: + Calculate the NWS heat index given dry-bulb T and RH + +Definition: + function CalculateHeatIndex(t, rh) -> value or null + +Description: + T is a Farentheit temperature in [76,120]; rh is a + relative humidity in [0,100]. The heat index is computed + and returned; or an error is returned. + +Returns: + number => heat index in Farenheit. + null => error. + +References: + https://github.com/mcci-catena/heat-index/ + https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml + + Results was checked against the full chart at iweathernet.com: + https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png + + The MCCI-Catena heat-index site has a test js script to generate CSV to + match the chart, a spreadsheet that recreates the chart, and a + spreadsheet that compares results. + +*/ + +function CalculateHeatIndex(t, rh) { + var tRounded = Math.floor(t + 0.5); + + // return null outside the specified range of input parameters + if (tRounded < 76 || tRounded > 126) + return null; + if (rh < 0 || rh > 100) + return null; + + // according to the NWS, we try this first, and use it if we can + var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); + + // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 + // This is the same computation: + if ((tHeatEasy + t) < 160.0) + return tHeatEasy; + + // need to use the hard form, and possibly adjust. + var t2 = t * t; // t squared + var rh2 = rh * rh; // rh squared + var tResult = + -42.379 + + (2.04901523 * t) + + (10.14333127 * rh) + + (-0.22475541 * t * rh) + + (-0.00683783 * t2) + + (-0.05481717 * rh2) + + (0.00122874 * t2 * rh) + + (0.00085282 * t * rh2) + + (-0.00000199 * t2 * rh2); + + // these adjustments come from the NWA page, and are needed to + // match the reference table. + var tAdjust; + if (rh < 13.0 && 80.0 <= t && t <= 112.0) + tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); + else if (rh > 85.0 && 80.0 <= t && t <= 87.0) + tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); + else + tAdjust = 0; + + // apply the adjustment + tResult += tAdjust; + + // finally, the reference tables have no data above 183 (rounded), + // so filter out answers that we have no way to vouch for. + if (tResult >= 183.5) + return null; + else + return tResult; +} + +/* + +Name: Decoder() + +Function: + Decode an MCCI Catena port-2 message for The Things Network console. + +Definition: + function Decoder(bytes, port) -> object + +Description: + This function decodes the message given by the byte array `bytes[]`, + and returns an object with values in engineering units. + +Returns: + Object, or null if the bytes could not be decoded. + +*/ + +function Decoder(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + if ((port === 2) || (port === 4)) { + // see catena-message-port2-format.md + // i is used as the index into the message. Start with the flag byte. + // note that there's no discriminator. + // test vectors are also available there. + var i = 0; + // fetch the bitmap. + var flags = bytes[i++]; + + if (flags & 0x1) { + // set vRaw to a uint16, and increment pointer + var vRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + // interpret uint16 as an int16 instead. + if (vRaw & 0x8000) + vRaw += -0x10000; + // scale and save in decoded. + decoded.vBat = vRaw / 4096.0; + } + + if (flags & 0x2) { + var VDDRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (VDDRaw & 0x8000) + VDDRaw += -0x10000; + decoded.VDD = VDDRaw / 4096.0; + } + + if (flags & 0x4) { + var iBoot = bytes[i]; + i += 1; + decoded.boot = iBoot; + } + + if (flags & 0x8) { + // we have temp, pressure, RH + var tRaw = (bytes[i] << 8) + bytes[i + 1]; + if (tRaw & 0x8000) + tRaw = -0x10000 + tRaw; + i += 2; + var pRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + var hRaw = bytes[i++]; + + decoded.tempC = tRaw / 256; + decoded.p = pRaw * 4 / 100.0; + decoded.rh = hRaw / 256 * 100; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + var tHeatIndexF = CalculateHeatIndex(decoded.tempC * 1.8 + 32, decoded.rh); + if (tHeatIndexF !== null) + decoded.tHeatIndexC = (tHeatIndexF - 32) / 1.8; + } + + if (flags & 0x10) { + if (port === 2) + { + // we have IR, White, UV -- units are C * W/m2, + // where C is a calibration constant. + var irradiance = { }; + decoded.irradiance = irradiance; + var lightRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + irradiance.IR = lightRaw; + lightRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + irradiance.White = lightRaw; + lightRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + irradiance.UV = lightRaw; + } + + else if (port === 4) + { + // we have light (lux) + var luxRaw = (bytes[i] << 16) + (bytes[i + 1] << 8) + bytes[i + 2]; + i = i + 3; + + // luxRaw is the 3-byte number decoded from wherever; + // it's in range 0..0xFFFFFF + // bit 23 is the sign bit + // bits 22..16 are the exponent + // bits 15..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + + // extract sign, exponent, mantissa + var bSign = (luxRaw & 0x800000) ? true : false; + var uExp = (luxRaw & 0x7F0000) >> 16; + var uMantissa = (luxRaw & 0x00FFFF); + + // if non-numeric, return appropriate result. + if (uExp === 0x7F) { + if (uMantissa === 0) + return bSign ? Number.NEGATIVE_INFINITY + : Number.POSITIVE_INFINITY; + else + return Number.NaN; + // else unless denormal, set the 1.0 bit + } else if (uExp !== 0) { + uMantissa += 0x010000; + } else { // denormal: exponent is the minimum + uExp = 1; + } + + // make a floating mantissa in [0,2); usually [1,2), but + // sometimes (0,1) for denormals, and exactly zero for zero. + var mantissa = uMantissa / 0x010000; + + // apply the exponent. + mantissa = Math.pow(2, uExp - 63) * mantissa; + + // apply sign and return result. + decoded.lux = bSign ? -mantissa : mantissa; + } + } + + if (flags & 0x20) { + var vRawBus = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (vRawBus & 0x8000) + vRawBus += -0x10000; + decoded.vBus = vRawBus / 4096.0; + } + } + + // at this point, decoded has the real values. + return decoded; +} + +// TTN V3 decoder +function decodeUplink(tInput) + { + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; + } diff --git a/vendor/mcci/codec-catena4612.yaml b/vendor/mcci/codec-catena4612.yaml new file mode 100644 index 0000000000..d19c95f9d6 --- /dev/null +++ b/vendor/mcci/codec-catena4612.yaml @@ -0,0 +1,47 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: codec-catena4612.js + # Examples (optional) + examples: + - description: Sensor report at port 2 for version 1, port 4 for version 2 boards + input: + fPort: 2 + bytes: [0x7D, 0x30, 0x23, 0x4F, 0x19, 0x20, 0x49, 0x82, 0x9F, 0x97, 0xAE, 0x03, 0x01, 0xC5, 0x50, 0x31, 0x24, 0xBF, 0x54, 0xC8, 0x39] + output: + data: + { + 'boot': 79, + 'irradiance': { 'IR': 38830, 'UV': 50512, 'White': 769 }, + 'p': 752.72, + 'rh': 62.109375, + 'tDewC': 17.360852159642555, + 'tHeatIndexC': 25.314800347222217, + 'tempC': 25.125, + 'vBat': 3.008544921875, + 'vBus': 3.0712890625, + } +# Downlink encoder encodes JSON object into a binary data downlink (optional) +## downlinkEncoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## data: +## led: green +## output: +## bytes: [1] +## fPort: 2 +## # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +## downlinkDecoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## fPort: 2 +## bytes: [1] +## output: +## data: +## led: green diff --git a/vendor/mcci/codec-catena4618.js b/vendor/mcci/codec-catena4618.js new file mode 100644 index 0000000000..5684fe58b9 --- /dev/null +++ b/vendor/mcci/codec-catena4618.js @@ -0,0 +1,294 @@ +/* + +Name: catena-message-port3-port6-decoder-ttn.js + +Function: + Decode MCCI port 0x03/0x06 messages for TTN console. Usually the generic decoder is more + convenient (catena-message-generic-decoder-ttn.js) + +Copyright and License: + See accompanying LICENSE file at https://github.com/mcci-catena/MCCI-Catena-PMS7003/ + +Author: + Pranau Ravi Kumar, MCCI Corporation February 2025 + +*/ + +// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) +// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html +// rearranged for efficiency and to deal sanely with very low (< 1%) RH +function dewpoint(t, rh) { + var c1 = 243.04; + var c2 = 17.625; + var h = rh / 100; + if (h <= 0.01) + h = 0.01; + else if (h > 1.0) + h = 1.0; + + var lnh = Math.log(h); + var tpc1 = t + c1; + var txc2 = t * c2; + var txc2_tpc1 = txc2 / tpc1; + + var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); + return tdew; +} + +/* + +Name: CalculateHeatIndex() + +Description: + Calculate the NWS heat index given dry-bulb T and RH + +Definition: + function CalculateHeatIndex(t, rh) -> value or null + +Description: + T is a Farentheit temperature in [76,120]; rh is a + relative humidity in [0,100]. The heat index is computed + and returned; or an error is returned. + +Returns: + number => heat index in Farenheit. + null => error. + +References: + https://github.com/mcci-catena/heat-index/ + https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml + + Results was checked against the full chart at iweathernet.com: + https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png + + The MCCI-Catena heat-index site has a test js script to generate CSV to + match the chart, a spreadsheet that recreates the chart, and a + spreadsheet that compares results. + +*/ + +function CalculateHeatIndex(t, rh) { + var tRounded = Math.floor(t + 0.5); + + // return null outside the specified range of input parameters + if (tRounded < 76 || tRounded > 126) + return null; + if (rh < 0 || rh > 100) + return null; + + // according to the NWS, we try this first, and use it if we can + var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); + + // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 + // This is the same computation: + if ((tHeatEasy + t) < 160.0) + return tHeatEasy; + + // need to use the hard form, and possibly adjust. + var t2 = t * t; // t squared + var rh2 = rh * rh; // rh squared + var tResult = + -42.379 + + (2.04901523 * t) + + (10.14333127 * rh) + + (-0.22475541 * t * rh) + + (-0.00683783 * t2) + + (-0.05481717 * rh2) + + (0.00122874 * t2 * rh) + + (0.00085282 * t * rh2) + + (-0.00000199 * t2 * rh2); + + // these adjustments come from the NWA page, and are needed to + // match the reference table. + var tAdjust; + if (rh < 13.0 && 80.0 <= t && t <= 112.0) + tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); + else if (rh > 85.0 && 80.0 <= t && t <= 87.0) + tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); + else + tAdjust = 0; + + // apply the adjustment + tResult += tAdjust; + + // finally, the reference tables have no data above 183 (rounded), + // so filter out answers that we have no way to vouch for. + if (tResult >= 183.5) + return null; + else + return tResult; +} + +function CalculateHeatIndexCelsius(t, rh) { + var result = CalculateHeatIndex(t, rh); + if (result !== null) { + // convert to celsius. + result = (result - 32) * 5 / 9; + } + return result; +} + +/* + +Name: Decoder() + +Function: + Decode an MCCI Catena port-3/6 message for The Things Network console. + +Definition: + function Decoder(bytes, port) -> object + +Description: + This function decodes the message given by the byte array `bytes[]`, + and returns an object with values in engineering units. + +Returns: + Object, or null if the bytes could not be decoded. + +*/ + +function Decoder(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + if (! (port === 3) && ! (port === 6)) + return null; + + // see catena-message-port3-port6-format.md + // i is used as the index into the message. Start with the flag byte. + // note that there's no discriminator. + // test vectors are also available there. + var i = 0; + // fetch the bitmap. + var flags = bytes[i++]; + + if (flags & 0x1) { + // set Vraw to a uint16, and increment pointer + var Vraw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + // interpret uint16 as an int16 instead. + if (Vraw & 0x8000) + Vraw += -0x10000; + // scale and save in result. + decoded.Vbat = Vraw / 4096.0; + } + + if (flags & 0x2) { + var Vraw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (Vraw & 0x8000) + Vraw += -0x10000; + decoded.VDD = Vraw / 4096.0; + } + + if (flags & 0x4) { + var iBoot = bytes[i]; + i += 1; + decoded.boot = iBoot; + } + + if (flags & 0x8) { + // we have temp, pressure, RH + var tRaw = (bytes[i] << 8) + bytes[i + 1]; + if (tRaw & 0x8000) + tRaw = -0x10000 + tRaw; + i += 2; + var rhRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + + decoded.t = tRaw / 256; + decoded.rh = rhRaw / 65535.0 * 100; + decoded.tDew = dewpoint(decoded.t, decoded.rh); + decoded.tHeatIndexC = CalculateHeatIndexCelsius(decoded.t, decoded.rh); + } + + if (flags & 0x10) { + if (port === 3) + { + // we have light irradiance info + var irradiance = {}; + decoded.irradiance = irradiance; + + var lightRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + irradiance.IR = lightRaw; + + lightRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + irradiance.White = lightRaw; + + lightRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + irradiance.UV = lightRaw; + } + + else if (port === 6) + { + // we have light (lux) + var luxRaw = (bytes[i] << 16) + (bytes[i + 1] << 8) + bytes[i + 2]; + i = i + 3; + + // luxRaw is the 3-byte number decoded from wherever; + // it's in range 0..0xFFFFFF + // bit 23 is the sign bit + // bits 22..16 are the exponent + // bits 15..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + + // extract sign, exponent, mantissa + var bSign = (luxRaw & 0x800000) ? true : false; + var uExp = (luxRaw & 0x7F0000) >> 16; + var uMantissa = (luxRaw & 0x00FFFF); + + // if non-numeric, return appropriate result. + if (uExp === 0x7F) { + if (uMantissa === 0) + return bSign ? Number.NEGATIVE_INFINITY + : Number.POSITIVE_INFINITY; + else + return Number.NaN; + // else unless denormal, set the 1.0 bit + } else if (uExp !== 0) { + uMantissa += 0x010000; + } else { // denormal: exponent is the minimum + uExp = 1; + } + + // make a floating mantissa in [0,2); usually [1,2), but + // sometimes (0,1) for denormals, and exactly zero for zero. + var mantissa = uMantissa / 0x010000; + + // apply the exponent. + mantissa = Math.pow(2, uExp - 63) * mantissa; + + // apply sign and return result. + decoded.lux = bSign ? -mantissa : mantissa; + } + } + + if (flags & 0x20) { + var Vraw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (Vraw & 0x8000) + Vraw += -0x10000; + decoded.Vbus = Vraw / 4096.0; + } + + // at this point, decoded has the real values. + return decoded; +} + +// TTN V3 decoder +function decodeUplink(tInput) + { + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; + } diff --git a/vendor/mcci/codec-catena4618.yaml b/vendor/mcci/codec-catena4618.yaml new file mode 100644 index 0000000000..4d4a84f75d --- /dev/null +++ b/vendor/mcci/codec-catena4618.yaml @@ -0,0 +1,36 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: codec-catena4618.js + # Examples (optional) + examples: + - description: Sensor report for catena 4618, no pulse/power input + input: + fPort: 6 + bytes: [0x1D, 0x28, 0x18, 0x11, 0x19, 0x20, 0x9F, 0x97, 0x47, 0x9E, 0xD0] + output: + data: { 'Vbat': 2.505859375, 'boot': 17, 'lux': 414.8125, 'rh': 62.34073395895323, 'tDew': 17.41972264670249, 't': 25.125, 'tHeatIndexC': null } +# Downlink encoder encodes JSON object into a binary data downlink (optional) +## downlinkEncoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## data: +## led: green +## output: +## bytes: [1] +## fPort: 2 +## # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +## downlinkDecoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## fPort: 2 +## bytes: [1] +## output: +## data: +## led: green diff --git a/vendor/mcci/codec-catena4618m201.js b/vendor/mcci/codec-catena4618m201.js new file mode 100644 index 0000000000..19545a5b52 --- /dev/null +++ b/vendor/mcci/codec-catena4618m201.js @@ -0,0 +1,294 @@ +/* + +Name: catena-message-port3-port6-decoder-ttn.js + +Function: + Decode MCCI port 0x03/0x06 messages for TTN console. Usually the generic decoder is more + convenient (catena-message-generic-decoder-ttn.js) + +Copyright and License: + See accompanying LICENSE file at https://github.com/mcci-catena/MCCI-Catena-PMS7003/ + +Author: + Terry Moore, MCCI Corporation July 2019 + +*/ + +// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) +// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html +// rearranged for efficiency and to deal sanely with very low (< 1%) RH +function dewpoint(t, rh) { + var c1 = 243.04; + var c2 = 17.625; + var h = rh / 100; + if (h <= 0.01) + h = 0.01; + else if (h > 1.0) + h = 1.0; + + var lnh = Math.log(h); + var tpc1 = t + c1; + var txc2 = t * c2; + var txc2_tpc1 = txc2 / tpc1; + + var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); + return tdew; +} + +/* + +Name: CalculateHeatIndex() + +Description: + Calculate the NWS heat index given dry-bulb T and RH + +Definition: + function CalculateHeatIndex(t, rh) -> value or null + +Description: + T is a Farentheit temperature in [76,120]; rh is a + relative humidity in [0,100]. The heat index is computed + and returned; or an error is returned. + +Returns: + number => heat index in Farenheit. + null => error. + +References: + https://github.com/mcci-catena/heat-index/ + https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml + + Results was checked against the full chart at iweathernet.com: + https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png + + The MCCI-Catena heat-index site has a test js script to generate CSV to + match the chart, a spreadsheet that recreates the chart, and a + spreadsheet that compares results. + +*/ + +function CalculateHeatIndex(t, rh) { + var tRounded = Math.floor(t + 0.5); + + // return null outside the specified range of input parameters + if (tRounded < 76 || tRounded > 126) + return null; + if (rh < 0 || rh > 100) + return null; + + // according to the NWS, we try this first, and use it if we can + var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); + + // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 + // This is the same computation: + if ((tHeatEasy + t) < 160.0) + return tHeatEasy; + + // need to use the hard form, and possibly adjust. + var t2 = t * t; // t squared + var rh2 = rh * rh; // rh squared + var tResult = + -42.379 + + (2.04901523 * t) + + (10.14333127 * rh) + + (-0.22475541 * t * rh) + + (-0.00683783 * t2) + + (-0.05481717 * rh2) + + (0.00122874 * t2 * rh) + + (0.00085282 * t * rh2) + + (-0.00000199 * t2 * rh2); + + // these adjustments come from the NWA page, and are needed to + // match the reference table. + var tAdjust; + if (rh < 13.0 && 80.0 <= t && t <= 112.0) + tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); + else if (rh > 85.0 && 80.0 <= t && t <= 87.0) + tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); + else + tAdjust = 0; + + // apply the adjustment + tResult += tAdjust; + + // finally, the reference tables have no data above 183 (rounded), + // so filter out answers that we have no way to vouch for. + if (tResult >= 183.5) + return null; + else + return tResult; +} + +function CalculateHeatIndexCelsius(t, rh) { + var result = CalculateHeatIndex(t, rh); + if (result !== null) { + // convert to celsius. + result = (result - 32) * 5 / 9; + } + return result; +} + +/* + +Name: Decoder() + +Function: + Decode an MCCI Catena port-3/6 message for The Things Network console. + +Definition: + function Decoder(bytes, port) -> object + +Description: + This function decodes the message given by the byte array `bytes[]`, + and returns an object with values in engineering units. + +Returns: + Object, or null if the bytes could not be decoded. + +*/ + +function Decoder(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + if (! (port === 3) && ! (port === 6)) + return null; + + // see catena-message-port3-port6-format.md + // i is used as the index into the message. Start with the flag byte. + // note that there's no discriminator. + // test vectors are also available there. + var i = 0; + // fetch the bitmap. + var flags = bytes[i++]; + + if (flags & 0x1) { + // set Vraw to a uint16, and increment pointer + var Vraw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + // interpret uint16 as an int16 instead. + if (Vraw & 0x8000) + Vraw += -0x10000; + // scale and save in result. + decoded.Vbat = Vraw / 4096.0; + } + + if (flags & 0x2) { + var Vraw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (Vraw & 0x8000) + Vraw += -0x10000; + decoded.VDD = Vraw / 4096.0; + } + + if (flags & 0x4) { + var iBoot = bytes[i]; + i += 1; + decoded.boot = iBoot; + } + + if (flags & 0x8) { + // we have temp, pressure, RH + var tRaw = (bytes[i] << 8) + bytes[i + 1]; + if (tRaw & 0x8000) + tRaw = -0x10000 + tRaw; + i += 2; + var rhRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + + decoded.t = tRaw / 256; + decoded.rh = rhRaw / 65535.0 * 100; + decoded.tDew = dewpoint(decoded.t, decoded.rh); + decoded.tHeatIndexC = CalculateHeatIndexCelsius(decoded.t, decoded.rh); + } + + if (flags & 0x10) { + if (port === 3) + { + // we have light irradiance info + var irradiance = {}; + decoded.irradiance = irradiance; + + var lightRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + irradiance.IR = lightRaw; + + lightRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + irradiance.White = lightRaw; + + lightRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + irradiance.UV = lightRaw; + } + + else if (port === 6) + { + // we have light (lux) + var luxRaw = (bytes[i] << 16) + (bytes[i + 1] << 8) + bytes[i + 2]; + i = i + 3; + + // luxRaw is the 3-byte number decoded from wherever; + // it's in range 0..0xFFFFFF + // bit 23 is the sign bit + // bits 22..16 are the exponent + // bits 15..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + + // extract sign, exponent, mantissa + var bSign = (luxRaw & 0x800000) ? true : false; + var uExp = (luxRaw & 0x7F0000) >> 16; + var uMantissa = (luxRaw & 0x00FFFF); + + // if non-numeric, return appropriate result. + if (uExp === 0x7F) { + if (uMantissa === 0) + return bSign ? Number.NEGATIVE_INFINITY + : Number.POSITIVE_INFINITY; + else + return Number.NaN; + // else unless denormal, set the 1.0 bit + } else if (uExp !== 0) { + uMantissa += 0x010000; + } else { // denormal: exponent is the minimum + uExp = 1; + } + + // make a floating mantissa in [0,2); usually [1,2), but + // sometimes (0,1) for denormals, and exactly zero for zero. + var mantissa = uMantissa / 0x010000; + + // apply the exponent. + mantissa = Math.pow(2, uExp - 63) * mantissa; + + // apply sign and return result. + decoded.lux = bSign ? -mantissa : mantissa; + } + } + + if (flags & 0x20) { + var Vraw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (Vraw & 0x8000) + Vraw += -0x10000; + decoded.Vbus = Vraw / 4096.0; + } + + // at this point, decoded has the real values. + return decoded; +} + +// TTN V3 decoder +function decodeUplink(tInput) + { + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; + } \ No newline at end of file diff --git a/vendor/mcci/codec-catena4618m201.yaml b/vendor/mcci/codec-catena4618m201.yaml new file mode 100644 index 0000000000..c8bb4aff26 --- /dev/null +++ b/vendor/mcci/codec-catena4618m201.yaml @@ -0,0 +1,36 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: codec-catena4618m201.js + # Examples (optional) + examples: + - description: Sensor report for catena 4618 m201 + input: + fPort: 6 + bytes: [0x1D, 0x28, 0x18, 0x11, 0x19, 0x20, 0x9F, 0x97, 0x47, 0x9E, 0xD0] + output: + data: { 'Vbat': 2.505859375, 'boot': 17, 'lux': 414.8125, 'rh': 62.34073395895323, 'tDew': 17.41972264670249, 't': 25.125, 'tHeatIndexC': null } +# Downlink encoder encodes JSON object into a binary data downlink (optional) +## downlinkEncoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## data: +## led: green +## output: +## bytes: [1] +## fPort: 2 +## # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +## downlinkDecoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## fPort: 2 +## bytes: [1] +## output: +## data: +## led: green diff --git a/vendor/mcci/codec-catena4802.js b/vendor/mcci/codec-catena4802.js new file mode 100644 index 0000000000..a6338d36c0 --- /dev/null +++ b/vendor/mcci/codec-catena4802.js @@ -0,0 +1,225 @@ +/* + +Name: catena-message-port4-decoder-ttn.js + +Function: + Decode MCCI port 0x03 messages for TTN console. Usually the generic decoder is more + convenient (catena-message-generic-decoder-ttn.js) + +Copyright and License: + See accompanying LICENSE file at https://github.com/mcci-catena/MCCI-Catena-PMS7003/ + +Author: + Terry Moore, MCCI Corporation August 2020 + +*/ + +// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) +// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html +// rearranged for efficiency and to deal sanely with very low (< 1%) RH +function dewpoint(t, rh) { + var c1 = 243.04; + var c2 = 17.625; + var h = rh / 100; + if (h <= 0.01) + h = 0.01; + else if (h > 1.0) + h = 1.0; + + var lnh = Math.log(h); + var tpc1 = t + c1; + var txc2 = t * c2; + var txc2_tpc1 = txc2 / tpc1; + + var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); + return tdew; +} + +/* +Name: CalculateHeatIndex() + +Description: + Calculate the NWS heat index given dry-bulb T and RH + +Definition: + function CalculateHeatIndex(t, rh) -> value or null + +Description: + T is a Farentheit temperature in [76,120]; rh is a + relative humidity in [0,100]. The heat index is computed + and returned; or an error is returned. + +Returns: + number => heat index in Farenheit. + null => error. + +References: + https://github.com/mcci-catena/heat-index/ + https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml + Results was checked against the full chart at iweathernet.com: + https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png + The MCCI-Catena heat-index site has a test js script to generate CSV to + match the chart, a spreadsheet that recreates the chart, and a + spreadsheet that compares results. + +*/ + +function CalculateHeatIndex(t, rh) { + var tRounded = Math.floor(t + 0.5); + + // return null outside the specified range of input parameters + if (tRounded < 76 || tRounded > 126) + return null; + if (rh < 0 || rh > 100) + return null; + + // according to the NWS, we try this first, and use it if we can + var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); + + // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 + // This is the same computation: + if ((tHeatEasy + t) < 160.0) + return tHeatEasy; + + // need to use the hard form, and possibly adjust. + var t2 = t * t; // t squared + var rh2 = rh * rh; // rh squared + var tResult = + -42.379 + + (2.04901523 * t) + + (10.14333127 * rh) + + (-0.22475541 * t * rh) + + (-0.00683783 * t2) + + (-0.05481717 * rh2) + + (0.00122874 * t2 * rh) + + (0.00085282 * t * rh2) + + (-0.00000199 * t2 * rh2); + + // these adjustments come from the NWA page, and are needed to + // match the reference table. + var tAdjust; + if (rh < 13.0 && 80.0 <= t && t <= 112.0) + tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); + else if (rh > 85.0 && 80.0 <= t && t <= 87.0) + tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); + else + tAdjust = 0; + + // apply the adjustment + tResult += tAdjust; + + // finally, the reference tables have no data above 183 (rounded), + // so filter out answers that we have no way to vouch for. + if (tResult >= 183.5) + return null; + else + return tResult; +} + +function CalculateHeatIndexCelsius(t, rh) { + var result = CalculateHeatIndex(t, rh); + if (result !== null) { + // convert to celsius. + result = (result - 32) * 5 / 9; + } + return result; +} + +/* + +Name: Decoder() + +Function: + Decode an MCCI Catena port-2 message for The Things Network console. + +Definition: + function Decoder(bytes, port) -> object + +Description: + This function decodes the message given by the byte array `bytes[]`, + and returns an object with values in engineering units. + +Returns: + Object, or null if the bytes could not be decoded. + +*/ + +function Decoder(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + if (! (port === 4)) + return null; + + // see catena-message-port4-format.md + // i is used as the index into the message. Start with the flag byte. + // note that there's no discriminator. + var i = 0; + // fetch the bitmap. + var flags = bytes[i++]; + + if (flags & 0x1) { + // set vRaw to a uint16, and increment pointer + var vRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + // interpret uint16 as an int16 instead. + if (vRaw & 0x8000) + vRaw += -0x10000; + // scale and save in decoded. + decoded.Vbat = vRaw / 4096.0; + } + + if (flags & 0x2) { + var VDDRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (VDDRaw & 0x8000) + VDDRaw += -0x10000; + // decoded.VDD = VDDRaw / 4096.0; + } + + if (flags & 0x4) { + var iBoot = bytes[i]; + i += 1; + decoded.boot = iBoot; + } + + if (flags & 0x8) { + // we have temp, RH (as u2) + var tRaw = (bytes[i] << 8) + bytes[i + 1]; + if (tRaw & 0x8000) + tRaw = -0x10000 + tRaw; + i += 2; + var rhRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + + decoded.tempC = tRaw / 256; + decoded.error = "none"; + decoded.rh = rhRaw / 65535 * 100; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + } + + // remaining bytes (if any) are modbus registers. + if (i < bytes.length - 1) { + // we have some number of Modbus registers + var registers = []; + decoded.registers = registers; + for (; i < bytes.length - 1; ) { + var value = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + registers[registers.length] = value; + } + } + + // at this point, decoded has the real values. + return decoded; +} + +// TTN V3 decoder +function decodeUplink(tInput) + { + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; + } \ No newline at end of file diff --git a/vendor/mcci/codec-catena4802.yaml b/vendor/mcci/codec-catena4802.yaml new file mode 100644 index 0000000000..40ed694aa7 --- /dev/null +++ b/vendor/mcci/codec-catena4802.yaml @@ -0,0 +1,36 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: codec-catena4802.js + # Examples (optional) + examples: + - description: Vbat 2.5 Boot 45 Env 16 60 . + input: + fPort: 4 + bytes: [13, 40, 24, 45, 16, 20, 153, 153] + output: + data: { 'Vbat': 2.505859375, 'boot': 45, 'error': 'none', 'rh': 60, 'tDewC': 8.311279180431027, 'tempC': 16.078125 } +# Downlink encoder encodes JSON object into a binary data downlink (optional) +## downlinkEncoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## data: +## led: green +## output: +## bytes: [1] +## fPort: 2 +## # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +## downlinkDecoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## fPort: 2 +## bytes: [1] +## output: +## data: +## led: green diff --git a/vendor/mcci/codec-model4811.js b/vendor/mcci/codec-model4811.js index d642926fd7..7e77bd8042 100644 --- a/vendor/mcci/codec-model4811.js +++ b/vendor/mcci/codec-model4811.js @@ -1,6 +1,40 @@ -// decode format 0x19 +/* -// test vectors: +Name: codec-model4811.js + +Function: + Decode messages from MCCI Model 4811 meter (long and short formats) + +Copyright and License: + This file is released under the MIT license: + + Copyright (c) 2019-2021, 2022 MCCI Corporation. + + 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. + + Author: + Terry Moore, MCCI Corporation August 2019 + + */ + +// test vectors (port 1): +// // 19 1B 35 34 0E 00 00 00 76 00 00 01 61 42 92 92 89 // { // "ModbusError": 0, @@ -30,6 +64,25 @@ // "mainDemand": 1.1754943508222875e-38, // "vBat": 3.3251953125 // } +// +// 198B351738000000060000001200000000000000000000000042EF59A542EF50010000000042EF6349 +// { +// "ModbusError": 0, +// "boot": 56, +// "energySourced": 1800, +// "energyUsed": 600, +// "mainDemand": 0, +// "quality": { +// "VoltageAB": 119.65625762939453, +// "VoltageBC": 0, +// "VoltageCA": 119.69391632080078, +// "VoltageLL": 119.67508697509766, +// "current1": 0, +// "current2": 0, +// "current3": 0 +// }, +// "vBat": 3.318115234375 +// } function u4toFloat32(bytes, i) { @@ -42,7 +95,7 @@ function u4toFloat32(bytes, i) var uMantissa = (u32 & 0x007FFFFF); // unless denormal, set the 1.0 bit - if (uExp != 0) + if (uExp !== 0) uMantissa += 0x00800000; else uExp += 1; @@ -75,36 +128,64 @@ function u4toInt32(bytes, i) return u32; } +function u3toFloat24(bytes, i) { + // pick up three bytes at index i into variable u32 + var u24 = (bytes[i] << 16) + (bytes[i + 1] << 8) + bytes[i + 2]; + + // extract sign, exponent, mantissa + var bSign = (u24 & 0x800000) ? true : false; + var uExp = (u24 & 0x7F0000) >> 16; + var uMantissa = (u24 & 0x00FFFF); + + // if non-numeric, return appropriate result. + if (uExp === 0x7F) { + if (uMantissa === 0) + return bSign ? Number.NEGATIVE_INFINITY + : Number.POSITIVE_INFINITY; + else + return Number.NaN; + // else unless denormal, set the 1.0 bit + } else if (uExp !== 0) { + uMantissa += 0x010000; + } else { // denormal: exponent is the minimum + uExp = 1; + } + + // make a floating mantissa in [0,2); usually [1,2), but + // sometimes (0,1) for denormals, and exactly zero for zero. + var mantissa = uMantissa / 0x010000; + + // apply the exponent. + mantissa = Math.pow(2, uExp - 63) * mantissa; + + // apply sign and return result. + return bSign ? -mantissa : mantissa; +} + function Decoder(bytes, port) { // Decode an uplink message from a buffer // (array) of bytes to an object of fields. var decoded = {}; - + if (port === 1) { cmd = bytes[0]; - if (cmd == 0x19) { + if (cmd === 0x19) { // i is used as the index into the message. Start with the flag byte. var i = 1; // fetch the bitmap. var flags = bytes[i++]; - + if (flags & 0x1) { - // set vRaw to a uint16, and increment pointer - var vRaw = (bytes[i] << 8) + bytes[i + 1]; + decoded.vBat = u2toInt16(bytes, i) / 4096.0; i += 2; - // interpret uint16 as an int16 instead. - if (vRaw & 0x8000) - vRaw += -0x10000; - // scale and save in decoded. - decoded.vBat = vRaw / 4096.0; } - + if (flags & 0x2) { var iBoot = bytes[i]; i += 1; decoded.boot = iBoot; } - + if (flags & 0x4) { // error code, as signed int. decoded.ModbusError = u2toInt16(bytes, i); @@ -112,17 +193,17 @@ function Decoder(bytes, port) { } else { decoded.ModbusError = 0; } - + if (flags & 0x08) { decoded.energyUsed = u4toInt32(bytes, i) * 100; decoded.energySourced = u4toInt32(bytes, i+4) * 100; i += 8; } - + if (flags & 0x10) { decoded.mainDemand = u4toFloat32(bytes, i); i += 4; - } else if ((flags & 0x04) == 0) { + } else if ((flags & 0x04) === 0) { decoded.mainDemand = 0; } @@ -131,21 +212,125 @@ function Decoder(bytes, port) { decoded.branchDemand = 0; i += 4; } - + if (flags & 0x40) { decoded.branchDemand = u4toFloat32(bytes, i); i += 4; } + + if (flags & 0x80) { + var quality = {}; + decoded.quality = quality; + quality.current1 = u4toFloat32(bytes, i); + quality.current2 = u4toFloat32(bytes, i + 4); + quality.current3 = u4toFloat32(bytes, i + 8); + quality.VoltageLL = u4toFloat32(bytes, i + 12); + quality.VoltageAB = u4toFloat32(bytes, i + 16); + quality.VoltageBC = u4toFloat32(bytes, i + 20); + quality.VoltageCA = u4toFloat32(bytes, i + 24); + i += 28; + } + + var flags2 = 0; + if (bytes.length > i) { + flags2 = bytes[i]; + ++i; + } + if (flags2 & 1) { + decoded.Ct2Used = u4toInt32(bytes, i) * 100; + i += 4; + } + if (flags2 & 2) { + decoded.Ct2Sourced = u4toInt32(bytes, i) * 100; + i += 4; + } + if (flags2 & 4) { + decoded.Ct2Demand = u4toFloat32(bytes, i); + i += 4; + } + if (flags2 & 8) { + decoded.Ct3Used = u4toInt32(bytes, i) * 100; + i += 4; + } + if (flags2 & 16) { + decoded.Ct3Sourced = u4toInt32(bytes, i) * 100; + i += 4; + } + if (flags2 & 32) { + decoded.Ct3Demand = u4toFloat32(bytes, i); + i += 4; + } + } + } + else if (port === 10) { + var i = 0; + decoded.vBat = u2toInt16(bytes, i) / 4096.0; + i += 2; + decoded.boot = bytes[i]; + i += 1; + if (bytes.length > i) { + decoded.ModbusError = u2toInt16(bytes, i); + i += 2; + } + if (bytes.length > i) { + var quality = {} + decoded.quality = quality; + quality.VoltageLL = u3toFloat24(bytes, i); + i += 3; + } + } + else if (port === 11) { + var i = 0; + decoded.energyUsed = u4toInt32(bytes, i) * 100; + i += 4; + decoded.energySourced = u4toInt32(bytes, i) * 100; + i += 4; + decoded.mainDemand = u3toFloat24(bytes, i); + i += 3; + } + else if (port === 12) { + var quality = {}; + decoded.quality = quality; + quality.current1 = u3toFloat24(bytes, 0); + quality.current2 = u3toFloat24(bytes, 3); + quality.current3 = u3toFloat24(bytes, 6); + } + else if (port === 13) { + var quality = {}; + decoded.quality = quality; + quality.VoltageAB = u3toFloat24(bytes, 0); + quality.VoltageBC = u3toFloat24(bytes, 3); + quality.VoltageCA = u3toFloat24(bytes, 6); + } + else if (port === 14) { + var i = 0; + decoded.branchEnergyUsed = u4toInt32(bytes, i) * 100; + i += 4; + decoded.branchDemand = 0; + if (bytes.length > i) { + decoded.branchDemand = u3toFloat24(bytes, i); + i += 3; } } + else if (port === 15) { + decoded.Ct2Used = u4toInt32(bytes, 0); + decoded.Ct2Sourced = u4toInt32(bytes, 4); + decoded.Ct2Demand = u3toFloat24(bytes, 8); + } + else if (port === 16) { + decoded.Ct3Used = u4toInt32(bytes, 0); + decoded.Ct3Sourced = u4toInt32(bytes, 4); + decoded.Ct3Demand = u3toFloat24(bytes, 8); + } + // at this point, decoded has the real values. return decoded; } // TTN V3 decoder function decodeUplink(tInput) { - var decoded = Decoder(tInput.bytes, tInput.fPort); - var result = {}; - result.data = decoded; - return result; -} + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; +} \ No newline at end of file diff --git a/vendor/mcci/codec-model4841.js b/vendor/mcci/codec-model4841.js index b8ab2fd038..1274cf87a5 100644 --- a/vendor/mcci/codec-model4841.js +++ b/vendor/mcci/codec-model4841.js @@ -1,6 +1,6 @@ /* -Name: catena-message-port1-format-20-decoder-ttn.js +Name: codec-model4841.js Function: Decode port 0x01 format 0x20 and 0x21 messages for TTN console. diff --git a/vendor/mcci/codec-model4931.js b/vendor/mcci/codec-model4931.js new file mode 100644 index 0000000000..7b1e2da4a2 --- /dev/null +++ b/vendor/mcci/codec-model4931.js @@ -0,0 +1,601 @@ +/* + +Name: codec-model433.js + +Function: + This function decodes the record (port 1, port4, format 0x37) sent by the + MCCI Model 4931 Maple Sugarbush Monitor application. + +Copyright and License: + See accompanying LICENSE file + +Author: + Pranau R, MCCI Corporation October 2024 + +*/ + +// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) +// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html +// rearranged for efficiency and to deal sanely with very low (< 1%) RH +function dewpoint(t, rh) + { + var c1 = 243.04; + var c2 = 17.625; + var h = rh / 100; + if (h <= 0.01) + h = 0.01; + else if (h > 1.0) + h = 1.0; + + var lnh = Math.log(h); + var tpc1 = t + c1; + var txc2 = t * c2; + var txc2_tpc1 = txc2 / tpc1; + + var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); + return tdew; + } + +/* + +Name: CalculateHeatIndex() + +Description: + Calculate the NWS heat index given dry-bulb T and RH + +Definition: + function CalculateHeatIndex(t, rh) -> value or null + +Description: + T is a Farentheit temperature in [76,120]; rh is a + relative humidity in [0,100]. The heat index is computed + and returned; or an error is returned. + +Returns: + number => heat index in Farenheit. + null => error. + +References: + https://github.com/mcci-catena/heat-index/ + https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml + + Results was checked against the full chart at iweathernet.com: + https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png + + The MCCI-Catena heat-index site has a test js script to generate CSV to + match the chart, a spreadsheet that recreates the chart, and a + spreadsheet that compares results. + +*/ + +function CalculateHeatIndex(t, rh) + { + var tRounded = Math.floor(t + 0.5); + + // return null outside the specified range of input parameters + if (tRounded < 76 || tRounded > 126) + return null; + if (rh < 0 || rh > 100) + return null; + + // according to the NWS, we try this first, and use it if we can + var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); + + // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 + // This is the same computation: + if ((tHeatEasy + t) < 160.0) + return tHeatEasy; + + // need to use the hard form, and possibly adjust. + var t2 = t * t; // t squared + var rh2 = rh * rh; // rh squared + var tResult = + -42.379 + + (2.04901523 * t) + + (10.14333127 * rh) + + (-0.22475541 * t * rh) + + (-0.00683783 * t2) + + (-0.05481717 * rh2) + + (0.00122874 * t2 * rh) + + (0.00085282 * t * rh2) + + (-0.00000199 * t2 * rh2); + + // these adjustments come from the NWA page, and are needed to + // match the reference table. + var tAdjust; + if (rh < 13.0 && 80.0 <= t && t <= 112.0) + tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); + else if (rh > 85.0 && 80.0 <= t && t <= 87.0) + tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); + else + tAdjust = 0; + + // apply the adjustment + tResult += tAdjust; + + // finally, the reference tables have no data above 183 (rounded), + // so filter out answers that we have no way to vouch for. + if (tResult >= 183.5) + return null; + else + return tResult; + } + +function CalculateHeatIndexCelsius(t, rh) + { + var result = CalculateHeatIndex(t, rh); + if (result !== null) + { + // convert to celsius. + result = (result - 32) * 5 / 9; + } + return result; + } + +function DecodeU16(Parse) + { + var i = Parse.i; + var bytes = Parse.bytes; + var raw = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + return raw; + } + +function DecodeI16(Parse) + { + var Vraw = DecodeU16(Parse); + + // interpret uint16 as an int16 instead. + if (Vraw & 0x8000) + Vraw += -0x10000; + + return Vraw; + } + +function DecodeU32(Parse) + { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + return result; + } + +function DecodeV(Parse) + { + return DecodeI16(Parse) / 4096.0; + } + +/* + +Name: DecodeDownlinkResponse(bytes) + +Function: + Decode the downlink response transmitted by device. + +Definition: + DecodeDownlinkResponse( + bytes + ); + +Description: + A function to decode the port 3 uplink data in a human readable way. + +Return: + Returns decoded data + +*/ + +function DecodeDownlinkResponse(bytes) + { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + // an object to help us parse. + var Parse = {}; + Parse.bytes = bytes; + // i is used as the index into the message. Start with the flag byte. + Parse.i = 0; + + // fetch the bitmap. + var command = bytes[Parse.i++]; + + if (command === 0x01) + { + // Reset device operating mode. + decoded.ResponseType = "Device Heater"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + + if (responseError === 0) + { + var heaterState = bytes[Parse.i++]; + if (heaterState === 0) + decoded.heaterState = "Heater OFF"; + else if (heaterState === 1) + decoded.heaterState = "Heater ON"; + else if (heaterState === 2) + decoded.heaterState = "Battery low, Heater OFF"; + } + } + + else if (command === 0x02) + { + // Reset do not send a reply back. + } + + else if (command === 0x03) + { + // SW version and HW details. + decoded.ResponseType = "Device Version"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + + var vMajor = bytes[Parse.i++]; + var vMinor = bytes[Parse.i++]; + var vPatch = bytes[Parse.i++]; + var vLocal = bytes[Parse.i++]; + decoded.AppVersion = "V" + vMajor + "." + vMinor + "." + vPatch + "." + vLocal; + + var Model = DecodeU16(Parse); + var Rev = bytes[Parse.i++]; + if (!(Model === 0)) + { + decoded.Model = Model; + if (Rev === 0) + decoded.Rev = "A"; + else if (Rev === 1) + decoded.Rev = "B"; + } + else if (Model === 0) + { + decoded.Model = 4931; + decoded.Rev = "Not Found"; + } + } + + else if (command === 0x04) + { + // Reset/Set AppEUI. + decoded.ResponseType = "AppEUI Set"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + } + + else if (command === 0x05) + { + // Reset/Set AppKey. + decoded.ResponseType = "AppKey set"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + } + + else if (command === 0x06) + { + // Rejoin the network. + decoded.ResponseType = "Rejoin"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + } + + else if (command === 0x07) + { + // Uplink Interval for sensor data. + decoded.ResponseType = "Uplink Interval"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + + decoded.UplinkInterval = DecodeU32(Parse); + } + + else if (command === 0x08) + { + // Data limit mode. + decoded.ResponseType = "Low Data Rate Interval"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + } + + else if (command === 0x09) + { + // Battery threshold for heater. + decoded.ResponseType = "Low Battery Threshold for Heater"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + } + + else if (command === 0x0A) + { + // Battery threshold for uplink interval. + decoded.ResponseType = "Low Battery Threshold for Uplink"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + } + + else if (command === 0x0B) + { + // Battery threshold for uplink interval. + decoded.ResponseType = "Reset Sap Total Count"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + } + + else if (command === 0x0C) + { + // Battery threshold for uplink interval. + decoded.ResponseType = "Reset Rain Total Count"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + } + + return decoded; + } + +/* + +Name: Decoder(bytes, port) + +Function: + Decode the transmitted uplink data. + +Definition: + Decoder( + bytes, + port + ); + +Description: + A function to decode the uplink data in a human readable way. + +Return: + Returns decoded data + +*/ + +function Decoder(bytes, port) + { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + if (port === 3) + { + decoded = DecodeDownlinkResponse(bytes); + return decoded; + } + + if (! (port === 1) && ! (port === 3) && ! (port === 4)) + return null; + + var uFormat = bytes[0]; + if (! (uFormat === 0x37)) + return null; + + // an object to help us parse. + var Parse = {}; + Parse.bytes = bytes; + // i is used as the index into the message. + Parse.i = 1; + + var sdCardStatus = bytes[Parse.i++]; + if (sdCardStatus === 0) + { + decoded.sdCardPresence = "SD card not detected"; + decoded.sdCardWorking = "SD card write failed"; + } + else if (sdCardStatus === 1) + { + decoded.sdCardPresence = "SD card is present"; + decoded.sdCardWorking = "SD card write failed"; + } + else if (sdCardStatus === 2) + { + decoded.sdCardPresence = "SD card not detected"; + decoded.sdCardWorking = "SD card write success"; + } + else if (sdCardStatus === 3) + { + decoded.sdCardPresence = "SD card is present"; + decoded.sdCardWorking = "SD card write success"; + } + else + { + // do nothing + } + + // fetch the bitmap. + var flags = bytes[Parse.i++]; + + if (flags & 0x1) + { + decoded.vBat = DecodeV(Parse); + } + + if (flags & 0x2) + { + decoded.vBus = DecodeV(Parse); + } + + if (flags & 0x4) + { + var iBoot = bytes[Parse.i++]; + decoded.boot = iBoot; + } + + if (flags & 0x8) + { + // we have temp, RH + decoded.t = DecodeI16(Parse) / 256; + decoded.rh = DecodeU16(Parse) * 100 / 65535.0; + decoded.tDew = dewpoint(decoded.t, decoded.rh); + decoded.tHeatIndexC = CalculateHeatIndexCelsius(decoded.t, decoded.rh); + } + + if (flags & 0x10) + { + decoded.p = DecodeU16(Parse) * 4 / 100.0; + } + + if (flags & 0x20) + { + // onewire temperature + decoded.tProbeOne = DecodeI16(Parse) / 256; + } + + if (flags & 0x40) + { + // onewire temperature + decoded.tProbeTwo = DecodeI16(Parse) / 256; + } + + if (flags & 0x80) + { + decoded.soil_1_TempC = DecodeI16(Parse) / 100.0; + decoded.soil_1_VMC = DecodeU16(Parse) / 100.0; + var sType = DecodeU16(Parse); + decoded.soil_1_Type = sType; + } + + // fetch the bitmap. + var flags2 = bytes[Parse.i++]; + + if (flags2 & 0x1) + { + decoded.soil_2_TempC = DecodeI16(Parse) / 100.0; + decoded.soil_2_VMC = DecodeU16(Parse) / 100.0; + var sType = DecodeU16(Parse); + decoded.soil_2_Type = sType; + } + + if (flags2 & 0x2) + { + decoded.p_2_mV = DecodeV(Parse); + decoded.p_2 = (750 * decoded.p_2_mV) - 1375; + } + + if (flags2 & 0x4) + { + decoded.p_1_mV = DecodeV(Parse); + decoded.p_1 = (750 * decoded.p_1_mV) - 1375; + } + + if (flags2 & 0x8) + { + // we have sap flow liters + var pulse_1 = (bytes[Parse.i] << 8) + bytes[Parse.i + 1]; + Parse.i += 2; + decoded.sap_1_GallonsPerTap = pulse_1; + + // normalize floating pulses per hour + var flowRateRaw_1 = (bytes[Parse.i] << 8) + bytes[Parse.i + 1]; + Parse.i += 2; + + var exp1 = flowRateRaw_1 >> 12; + var mant1 = (flowRateRaw_1 & 0xFFF) / 4096.0; + var pulsePerHour_1 = mant1 * Math.pow(2, exp1 - 15) * 60 * 60 * 4; + decoded.sap_1_GallonsPerTapPerHour = pulsePerHour_1; + } + + if (flags2 & 0x10) + { + // we have rain flow liters + var pulse_1 = (bytes[Parse.i] << 8) + bytes[Parse.i + 1]; + Parse.i += 2; + decoded.rainCount = pulse_1; + + // normalize floating pulses per hour + var flowRateRaw_1 = (bytes[Parse.i] << 8) + bytes[Parse.i + 1]; + Parse.i += 2; + + var exp1 = flowRateRaw_1 >> 12; + var mant1 = (flowRateRaw_1 & 0xFFF) / 4096.0; + var pulsePerHour_1 = mant1 * Math.pow(2, exp1 - 15) * 60 * 60 * 4; + decoded.rainPerHour = pulsePerHour_1; + } + + if (flags2 & 0x20) + { + // network timestamp + var timestamp = DecodeU32(Parse); + if (timestamp & 1) + decoded.timeType = "network time"; + else + decoded.timeType = "boot time"; + + timestamp = (timestamp >> 1) * 2; + decoded.timestamp = new Date((timestamp + /* gps epoch to posix */ 315964800 - /* leap seconds */ 17) * 1000); + } + + // at this point, decoded has the real values. + return decoded; + } + +// TTN V3 decoder +function decodeUplink(tInput) + { + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; + } \ No newline at end of file diff --git a/vendor/mcci/codec-model4931.yaml b/vendor/mcci/codec-model4931.yaml new file mode 100644 index 0000000000..85365dfbd0 --- /dev/null +++ b/vendor/mcci/codec-model4931.yaml @@ -0,0 +1,50 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: codec-model4931.js + # Examples (optional) + examples: + - description: Sdcardstatus 3 Vbat 3.7 Vbus 5 Boot 25 Env 25.5 60 Pressure 1013.25 TempOneC 22.3 TempTwoC 23.8 SoilOneData_t 30 SoilOneData_rh 50 SoilOneData_type 0 SoilTwoData_t 29 SoilTwoData_rh 45 SoilTwoData_type 0 TreepressureOne 2.5 TreepressureTwo 2.8 SapPulse 1742 48 RainPulse 394 7 Timestamp 1742982409 . + input: + fPort: 1 + bytes: [55, 51, 253, 59, 51, 80, 0, 25, 25, 128, 153, 153, 0, 253, 22, 77, 23, 205, 0, 30, 0, 50, 0, 0, 127, 0, 29, 0, 45, 0, 0, 40, 0, 44, 205, 6, 206, 0, 48, 1, 138, 0, 7, 103, 227, 205, 9] + output: + data: + { + 'boot': 80, + 'p': 1572.84, + 'rh': 9.961089494163424, + 'soil_1_TempC': 60.93, + 'soil_1_Type': 50, + 'soil_1_VMC': 0.3, + 't': 0.09765625, + 'tDew': -28.04838168345213, + 'tHeatIndexC': null, + 'tProbeOne': 0.98828125, + 'tProbeTwo': 22.30078125, + 'vBat': 3.699951171875, + } +# Downlink encoder encodes JSON object into a binary data downlink (optional) +## downlinkEncoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## data: +## led: green +## output: +## bytes: [1] +## fPort: 2 +## # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +## downlinkDecoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## fPort: 2 +## bytes: [1] +## output: +## data: +## led: green diff --git a/vendor/mcci/codec-model4933.js b/vendor/mcci/codec-model4933.js new file mode 100644 index 0000000000..efc6d16d8d --- /dev/null +++ b/vendor/mcci/codec-model4933.js @@ -0,0 +1,504 @@ +/* + +Name: codec-model433.js + +Function: + This function decodes the record (port 1, format 0x38) sent by the + MCCI Model 4933 multigas and environment sensor application. + +Copyright and License: + See accompanying LICENSE file + +Author: + Pranau Ravikumar, MCCI Corporation November 2024 + +*/ + +// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) +// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html +// rearranged for efficiency and to deal sanely with very low (< 1%) RH +function dewpoint(t, rh) + { + var c1 = 243.04; + var c2 = 17.625; + var h = rh / 100; + if (h <= 0.01) + h = 0.01; + else if (h > 1.0) + h = 1.0; + + var lnh = Math.log(h); + var tpc1 = t + c1; + var txc2 = t * c2; + var txc2_tpc1 = txc2 / tpc1; + + var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); + return tdew; + } + +/* + +Name: CalculateHeatIndex() + +Description: + Calculate the NWS heat index given dry-bulb T and RH + +Definition: + function CalculateHeatIndex(t, rh) -> value or null + +Description: + T is a Farentheit temperature in [76,120]; rh is a + relative humidity in [0,100]. The heat index is computed + and returned; or an error is returned. + +Returns: + number => heat index in Farenheit. + null => error. + +References: + https://github.com/mcci-catena/heat-index/ + https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml + + Results was checked against the full chart at iweathernet.com: + https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png + + The MCCI-Catena heat-index site has a test js script to generate CSV to + match the chart, a spreadsheet that recreates the chart, and a + spreadsheet that compares results. + +*/ + +function CalculateHeatIndex(t, rh) + { + var tRounded = Math.floor(t + 0.5); + + // return null outside the specified range of input parameters + if (tRounded < 76 || tRounded > 126) + return null; + if (rh < 0 || rh > 100) + return null; + + // according to the NWS, we try this first, and use it if we can + var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); + + // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 + // This is the same computation: + if ((tHeatEasy + t) < 160.0) + return tHeatEasy; + + // need to use the hard form, and possibly adjust. + var t2 = t * t; // t squared + var rh2 = rh * rh; // rh squared + var tResult = + -42.379 + + (2.04901523 * t) + + (10.14333127 * rh) + + (-0.22475541 * t * rh) + + (-0.00683783 * t2) + + (-0.05481717 * rh2) + + (0.00122874 * t2 * rh) + + (0.00085282 * t * rh2) + + (-0.00000199 * t2 * rh2); + + // these adjustments come from the NWA page, and are needed to + // match the reference table. + var tAdjust; + if (rh < 13.0 && 80.0 <= t && t <= 112.0) + tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); + else if (rh > 85.0 && 80.0 <= t && t <= 87.0) + tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); + else + tAdjust = 0; + + // apply the adjustment + tResult += tAdjust; + + // finally, the reference tables have no data above 183 (rounded), + // so filter out answers that we have no way to vouch for. + if (tResult >= 183.5) + return null; + else + return tResult; + } + +function CalculateHeatIndexCelsius(t, rh) + { + var result = CalculateHeatIndex(t, rh); + if (result !== null) { + // convert to celsius. + result = (result - 32) * 5 / 9; + } + return result; + } + +function DecodeU16(Parse) + { + var i = Parse.i; + var bytes = Parse.bytes; + var raw = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + return raw; + } + +function DecodeUflt16(Parse) + { + var rawUflt16 = DecodeU16(Parse); + var exp1 = rawUflt16 >> 12; + var mant1 = (rawUflt16 & 0xFFF) / 4096.0; + var f_unscaled = mant1 * Math.pow(2, exp1 - 15); + return f_unscaled; + } + +function DecodeU32(Parse) + { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + return result; + } + +function DecodePM(Parse) + { + return (DecodeUflt16(Parse) * 512); + } + +function DecodePC(Parse) + { + return DecodeU32(Parse); + } + +function DecodeSflt16(Parse) + { + var rawSflt16 = DecodeU16(Parse); + + // special case minus zero: + if (rawSflt16 == 0x8000) + return -0.0; + + // extract the sign. + var sSign = ((rawSflt16 & 0x8000) != 0) ? -1 : 1; + + // extract the exponent + var exp1 = (rawSflt16 >> 11) & 0xF; + + // extract the "mantissa" (the fractional part) + var mant1 = (rawSflt16 & 0x7FF) / 2048.0; + + // convert back to a floating point number. We hope + // that Math.pow(2, k) is handled efficiently by + // the JS interpreter! If this is time critical code, + // you can replace by a suitable shift and divide. + var f_unscaled = sSign * mant1 * Math.pow(2, exp1 - 15); + + return f_unscaled; + } + + +function DecodeI16(Parse) + { + var Vraw = DecodeU16(Parse); + + // interpret uint16 as an int16 instead. + if (Vraw & 0x8000) + Vraw += -0x10000; + + return Vraw; + } + +function DecodeI16(Parse) + { + var i = Parse.i; + var bytes = Parse.bytes; + var Vraw = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + + // interpret uint16 as an int16 instead. + if (Vraw & 0x8000) + Vraw += -0x10000; + + return Vraw; + } + +function DecodeV(Parse) + { + return DecodeI16(Parse) / 4096.0; + } + +/* + +Name: DecodeDownlinkResponse(bytes) + +Function: + Decode the downlink response transmitted by device. + +Definition: + DecodeDownlinkResponse( + bytes + ); + +Description: + A function to decode the port 3 uplink data in a human readable way. + +Return: + Returns decoded data + +*/ + +function DecodeDownlinkResponse(bytes) + { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + // an object to help us parse. + var Parse = {}; + Parse.bytes = bytes; + // i is used as the index into the message. Start with the flag byte. + Parse.i = 0; + + // fetch the bitmap. + var command = bytes[Parse.i++]; + + if (command === 0x02) + { + // Reset do not send a reply back. + } + + else if (command === 0x03) + { + // SW version and HW details. + decoded.ResponseType = "Device Version"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + + var vMajor = bytes[Parse.i++]; + var vMinor = bytes[Parse.i++]; + var vPatch = bytes[Parse.i++]; + var vLocal = bytes[Parse.i++]; + decoded.AppVersion = "V" + vMajor + "." + vMinor + "." + vPatch + "." + vLocal; + + var Model = DecodeU16(Parse); + var Rev = bytes[Parse.i++]; + if (!(Model === 0)) + { + decoded.Model = Model; + if (Rev === 0) + decoded.Rev = "A"; + else if (Rev === 1) + decoded.Rev = "B"; + } + else if (Model === 0) + { + decoded.Model = 4931; + decoded.Rev = "Not Found"; + } + } + + else if (command === 0x04) + { + // Reset/Set AppEUI. + decoded.ResponseType = "AppEUI Set"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + } + + else if (command === 0x05) + { + // Reset/Set AppKey. + decoded.ResponseType = "AppKey set"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + } + + else if (command === 0x06) + { + // Rejoin the network. + decoded.ResponseType = "Rejoin"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + } + + else if (command === 0x07) + { + // Uplink Interval for sensor data. + decoded.ResponseType = "Uplink Interval"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.Response = "Success"; + else if (responseError === 1) + decoded.Response = "Invalid Length"; + else if (responseError === 2) + decoded.Response = "Failure"; + + decoded.UplinkInterval = DecodeU32(Parse); + } + + return decoded; + } + +function Decoder(bytes, port) + { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + if (port === 3) + { + decoded = DecodeDownlinkResponse(bytes); + return decoded; + } + + if (! (port === 1) && ! (port === 3)) + return null; + + var uFormat = bytes[0]; + if (! (uFormat === 0x38)) + return null; + + // an object to help us parse. + var Parse = {}; + Parse.bytes = bytes; + // i is used as the index into the message. Start with the flag byte. + Parse.i = 1; + + // fetch the bitmap. + var flags = bytes[Parse.i++]; + + if (flags & 0x1) + { + // scale and save in decoded. + decoded.Vbat = DecodeV(Parse); + } + + if (flags & 0x2) + { + // scale and save in decoded. + decoded.Vbus = DecodeV(Parse); + } + + if (flags & 0x4) + { + var iBoot = bytes[Parse.i++]; + decoded.boot = iBoot; + } + + if (flags & 0x08) + { + // we have micro particles and their count + decoded.pm = {}; + decoded.pm["0.1"] = DecodePM(Parse); + decoded.pm["0.3"] = DecodePM(Parse); + decoded.pm["0.5"] = DecodePM(Parse); + decoded.pm["1.0"] = DecodePM(Parse); + decoded.pm["2.5"] = DecodePM(Parse); + decoded.pm["5.0"] = DecodePM(Parse); + decoded.pm["10"] = DecodePM(Parse); + + decoded.pc = {}; + decoded.pc["1.0"] = DecodePC(Parse); + decoded.pc["2.5"] = DecodePC(Parse); + decoded.pc["10"] = DecodePC(Parse); + } + + if (flags & 0x10) + { + // we have carbon monoxide + // decoded.vCO = DecodeV(Parse); + decoded.COppm = DecodeUflt16(Parse) * 1000; + } + + if (flags & 0x20) + { + // we have nitrogen-dioxide + // decoded.vNO2 = DecodeV(Parse); + decoded.NO2ppm = DecodeUflt16(Parse) * 10; + } + + if (flags & 0x40) + { + // we have ozone gas + // decoded.vO3 = DecodeV(Parse); + decoded.O3ppm = DecodeUflt16(Parse) * 30; + } + + if (flags & 0x80) + { + // we have sulphur-dioxide + // decoded.vSO2 = DecodeV(Parse); + decoded.SO2ppm = DecodeUflt16(Parse) * 30; + } + + decoded.vCO = DecodeV(Parse); + decoded.vNO2 = DecodeV(Parse); + decoded.vO3 = DecodeV(Parse); + decoded.vSO2 = DecodeV(Parse); + + // fetch the bitmap. + var flags2 = bytes[Parse.i++]; + + if (flags2 & 0x1) + { + // we have temp, RH + decoded.t = DecodeI16(Parse) / 256; + decoded.rh = DecodeU16(Parse) * 100 / 65535.0; + decoded.tDew = dewpoint(decoded.t, decoded.rh); + decoded.tHeatIndexC = CalculateHeatIndexCelsius(decoded.t, decoded.rh); + } + + if (flags2 & 0x2) + { + decoded.p = DecodeU16(Parse) * 4 / 100.0; + } + + if (flags2 & 0x4) + { + // network timestamp + var timestamp = DecodeU32(Parse); + if (timestamp & 1) + decoded.timeType = "network time"; + else + decoded.timeType = "boot time"; + + timestamp = (timestamp >> 1) * 2; + decoded.timestamp = new Date((timestamp + 17) * 1000); + } + + return decoded; + } + +// TTN V3 decoder +function decodeUplink(tInput) + { + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; + } \ No newline at end of file diff --git a/vendor/mcci/codec-model4933.yaml b/vendor/mcci/codec-model4933.yaml new file mode 100644 index 0000000000..0d1b8e2656 --- /dev/null +++ b/vendor/mcci/codec-model4933.yaml @@ -0,0 +1,113 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: codec-model4933.js + # Examples (optional) + examples: + - description: Vbat 2 Vbus 3.7 Boot 45 Particle pm0.1:51.0469 pm0.3:48.047 pm0.5:5.07845 pm1.0:4.04142 pm2.5:2.47152 pm5.0:1.04695 pm10.0:1.04695 pc1.0:5556697 pc2.5:1930124 pc10.0:1 Gases CO:120 NO2:3 O3:12 SO2:7 uVCO:675000 uVNO2:485000 uVO3:310000 uVSO2:725000 Env 30 60 Pressure 1027.8 Time 1742982409 . + input: + fPort: 1 + bytes: + [ + 56, + 255, + 32, + 0, + 59, + 51, + 45, + 204, + 195, + 204, + 3, + 154, + 40, + 152, + 21, + 137, + 227, + 120, + 96, + 120, + 96, + 0, + 84, + 201, + 217, + 0, + 29, + 115, + 140, + 0, + 0, + 0, + 1, + 207, + 92, + 233, + 154, + 236, + 205, + 222, + 239, + 10, + 205, + 7, + 195, + 4, + 246, + 11, + 154, + 0, + 30, + 0, + 153, + 153, + 100, + 95, + 103, + 227, + 205, + 9, + ] + output: + data: + { + 'COppm': 119.9951171875, + 'NO2ppm': 3.00048828125, + 'O3ppm': 12.000732421875, + 'SO2ppm': 7.0001220703125, + 'Vbat': 2, + 'Vbus': 3.699951171875, + 'boot': 45, + 'pc': { '2.5': 1930124, '1.0': 5556697, '10': 1 }, + 'pm': { '0.1': 51.046875, '0.3': 48.046875, '0.5': 5.078125, '1.0': 4.041015625, '2.5': 2.4716796875, '5.0': 1.046875, '10': 1.046875 }, + 'vCO': 0.675048828125, + 'vNO2': 0.485107421875, + 'vO3': 0.31005859375, + 'vSO2': 0.72509765625, + } +# Downlink encoder encodes JSON object into a binary data downlink (optional) +## downlinkEncoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## data: +## led: green +## output: +## bytes: [1] +## fPort: 2 +## # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +## downlinkDecoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## fPort: 2 +## bytes: [1] +## output: +## data: +## led: green diff --git a/vendor/mcci/codec-port2-fmt2.js b/vendor/mcci/codec-port2-fmt2.js index 5198a903c9..17889e2a3c 100644 --- a/vendor/mcci/codec-port2-fmt2.js +++ b/vendor/mcci/codec-port2-fmt2.js @@ -1,330 +1,330 @@ -/* - -Name: catena-message-port2-format-22-decoder-ttn.js - -Function: - Decode port 0x02 format 0x22 messages for TTN console. - -Copyright and License: - See accompanying LICENSE file at https://github.com/mcci-catena/MCCI-Catena-4430/ - -Author: - Terry Moore, MCCI Corporation August 2019 - -*/ - -// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) -// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html -// rearranged for efficiency and to deal sanely with very low (< 1%) RH -function dewpoint(t, rh) { - var c1 = 243.04; - var c2 = 17.625; - var h = rh / 100; - if (h <= 0.01) - h = 0.01; - else if (h > 1.0) - h = 1.0; - - var lnh = Math.log(h); - var tpc1 = t + c1; - var txc2 = t * c2; - var txc2_tpc1 = txc2 / tpc1; - - var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); - return tdew; - } - - /* - - Name: CalculateHeatIndex() - - Description: - Calculate the NWS heat index given dry-bulb T and RH - - Definition: - function CalculateHeatIndex(t, rh) -> value or null - - Description: - T is a Farentheit temperature in [76,120]; rh is a - relative humidity in [0,100]. The heat index is computed - and returned; or an error is returned. For consistency with - the other temperature, despite the heat index being defined - in Farenheit, we return in Celsius. - - Returns: - number => heat index in Farenheit. - null => error. - - References: - https://github.com/mcci-catena/heat-index/ - https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml - - Results was checked against the full chart at iweathernet.com: - https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png - - The MCCI-Catena heat-index site has a test js script to generate CSV to - match the chart, a spreadsheet that recreates the chart, and a - spreadsheet that compares results. - - */ - - function CalculateHeatIndex(t, rh) { - var tRounded = Math.floor(t + 0.5); - - // return null outside the specified range of input parameters - if (tRounded < 76 || tRounded > 126) - return null; - if (rh < 0 || rh > 100) - return null; - - // according to the NWS, we try this first, and use it if we can - var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); - - // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 - // This is the same computation: - if ((tHeatEasy + t) < 160.0) - return (tHeatEasy - 32) * 5 / 9; - - // need to use the hard form, and possibly adjust. - var t2 = t * t; // t squared - var rh2 = rh * rh; // rh squared - var tResult = - -42.379 + - (2.04901523 * t) + - (10.14333127 * rh) + - (-0.22475541 * t * rh) + - (-0.00683783 * t2) + - (-0.05481717 * rh2) + - (0.00122874 * t2 * rh) + - (0.00085282 * t * rh2) + - (-0.00000199 * t2 * rh2); - - // these adjustments come from the NWA page, and are needed to - // match the reference table. - var tAdjust; - if (rh < 13.0 && 80.0 <= t && t <= 112.0) - tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); - else if (rh > 85.0 && 80.0 <= t && t <= 87.0) - tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); - else - tAdjust = 0; - - // apply the adjustment - tResult += tAdjust; - - // finally, the reference tables have no data above 183 (rounded), - // so filter out answers that we have no way to vouch for. - if (tResult >= 183.5) - return null; - else - return (tResult - 32) * 5 / 9; - } - - function DecodeU16(Parse) { - var i = Parse.i; - var bytes = Parse.bytes; - var result = (bytes[i] << 8) + bytes[i + 1]; - Parse.i = i + 2; - return result; - } - - function DecodeUflt16(Parse) { - var rawUflt16 = DecodeU16(Parse); - var exp1 = rawUflt16 >> 12; - var mant1 = (rawUflt16 & 0xFFF) / 4096.0; - var f_unscaled = mant1 * Math.pow(2, exp1 - 15); - return f_unscaled; - } - - function DecodeSflt16(Parse) - { - var rawSflt16 = DecodeU16(Parse); - // rawSflt16 is the 2-byte number decoded from wherever; - // it's in range 0..0xFFFF - // bit 15 is the sign bit - // bits 14..11 are the exponent - // bits 10..0 are the the mantissa. Unlike IEEE format, - // the msb is explicit; this means that numbers - // might not be normalized, but makes coding for - // underflow easier. - // As with IEEE format, negative zero is possible, so - // we special-case that in hopes that JavaScript will - // also cooperate. - // - // The result is a number in the open interval (-1.0, 1.0); - // - - // throw away high bits for repeatability. - rawSflt16 &= 0xFFFF; - - // special case minus zero: - if (rawSflt16 === 0x8000) - return -0.0; - - // extract the sign. - var sSign = ((rawSflt16 & 0x8000) !== 0) ? -1 : 1; - - // extract the exponent - var exp1 = (rawSflt16 >> 11) & 0xF; - - // extract the "mantissa" (the fractional part) - var mant1 = (rawSflt16 & 0x7FF) / 2048.0; - - // convert back to a floating point number. We hope - // that Math.pow(2, k) is handled efficiently by - // the JS interpreter! If this is time critical code, - // you can replace by a suitable shift and divide. - var f_unscaled = sSign * mant1 * Math.pow(2, exp1 - 15); - - return f_unscaled; - } - - - function DecodeLight(Parse) { - return DecodeUflt16(Parse); - } - - function DecodeActivity(Parse) { - return DecodeSflt16(Parse); - } - - function DecodeI16(Parse) { - var i = Parse.i; - var bytes = Parse.bytes; - var result = (bytes[i] << 8) + bytes[i + 1]; - Parse.i = i + 2; - - // interpret uint16 as an int16 instead. - if (result & 0x8000) - result += -0x10000; - - return result; - } - - function DecodeI32(Parse) { - var i = Parse.i; - var bytes = Parse.bytes; - - var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; - Parse.i = i + 4; - - // interpret uint16 as an int16 instead. - if (result & 0x80000000) - result += -0x100000000; - - return result; - } - - function DecodeU32(Parse) { - var i = Parse.i; - var bytes = Parse.bytes; - - var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; - Parse.i = i + 4; - - return result; - } - - function RemainingBytes(Parse) { - var i = Parse.i; - var nBytes = Parse.bytes.length; - - if (i < nBytes) - return (nBytes - i); - else - return 0; - } - - function DecodeV(Parse) { - return DecodeI16(Parse) / 4096.0; - } - - function Decoder(bytes, port) { - // Decode an uplink message from a buffer - // (array) of bytes to an object of fields. - var decoded = {}; - - if (! (port === 2)) - return null; - - var uFormat = bytes[0]; - if (! (uFormat === 0x22)) - return null; - - // an object to help us parse. - var Parse = {}; - Parse.bytes = bytes; - // i is used as the index into the message. Start with the time. - Parse.i = 1; - - // fetch time, convert from GPS to ISO time assuming 17 leap seconds, - // and then convert to JSON format. - decoded.time = new Date((DecodeU32(Parse) + 315964800 - 17) * 1000).toJSON(); - - // fetch the bitmap. - var flags = bytes[Parse.i++]; - - if (flags & 0x1) { - decoded.Vbat = DecodeV(Parse); - } - - if (flags & 0x2) { - decoded.Vsys = DecodeV(Parse); - } - - if (flags & 0x4) { - decoded.Vbus = DecodeV(Parse); - } - - if (flags & 0x8) { - var iBoot = bytes[Parse.i++]; - decoded.boot = iBoot; - } - - if (flags & 0x10) { - // we have temp, pressure, RH - decoded.tempC = DecodeI16(Parse) / 256; - decoded.p = DecodeU16(Parse) * 4 / 100.0; - decoded.rh = DecodeU16(Parse) * 100 / 65535.0; - decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); - var tHeat = CalculateHeatIndex(decoded.tempC * 1.8 + 32, decoded.rh); - if (tHeat !== null) - decoded.tHeatIndexC = tHeat; - } - - if (flags & 0x20) { - // we have light - decoded.irradiance = {}; - decoded.irradiance.White = DecodeLight(Parse) * Math.pow(2.0, 24); - } - - if (flags & 0x40) { - // we have gpio counts - decoded.pellets = []; - for (var i = 0; i < 2; ++i) { - decoded.pellets[i] = {}; - decoded.pellets[i].Total = DecodeU16(Parse); - decoded.pellets[i].Delta = bytes[Parse.i++]; - } - } - - if (flags & 0x80) { - // we have Activity - decoded.activity = []; - var i = 0; - while (RemainingBytes(Parse) >= 2) { - decoded.activity[i] = DecodeActivity(Parse); - ++i; - } - } - - return decoded; - } - - // TTN V3 decoder - function decodeUplink(tInput) { - var decoded = Decoder(tInput.bytes, tInput.fPort); - var result = {}; - result.data = decoded; - return result; - } +/* + +Name: catena-message-port2-format-22-decoder-ttn.js + +Function: + Decode port 0x02 format 0x22 messages for TTN console. + +Copyright and License: + See accompanying LICENSE file at https://github.com/mcci-catena/MCCI-Catena-4430/ + +Author: + Terry Moore, MCCI Corporation August 2019 + +*/ + +// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) +// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html +// rearranged for efficiency and to deal sanely with very low (< 1%) RH +function dewpoint(t, rh) { + var c1 = 243.04; + var c2 = 17.625; + var h = rh / 100; + if (h <= 0.01) + h = 0.01; + else if (h > 1.0) + h = 1.0; + + var lnh = Math.log(h); + var tpc1 = t + c1; + var txc2 = t * c2; + var txc2_tpc1 = txc2 / tpc1; + + var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); + return tdew; + } + + /* + + Name: CalculateHeatIndex() + + Description: + Calculate the NWS heat index given dry-bulb T and RH + + Definition: + function CalculateHeatIndex(t, rh) -> value or null + + Description: + T is a Farentheit temperature in [76,120]; rh is a + relative humidity in [0,100]. The heat index is computed + and returned; or an error is returned. For consistency with + the other temperature, despite the heat index being defined + in Farenheit, we return in Celsius. + + Returns: + number => heat index in Farenheit. + null => error. + + References: + https://github.com/mcci-catena/heat-index/ + https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml + + Results was checked against the full chart at iweathernet.com: + https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png + + The MCCI-Catena heat-index site has a test js script to generate CSV to + match the chart, a spreadsheet that recreates the chart, and a + spreadsheet that compares results. + + */ + + function CalculateHeatIndex(t, rh) { + var tRounded = Math.floor(t + 0.5); + + // return null outside the specified range of input parameters + if (tRounded < 76 || tRounded > 126) + return null; + if (rh < 0 || rh > 100) + return null; + + // according to the NWS, we try this first, and use it if we can + var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); + + // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 + // This is the same computation: + if ((tHeatEasy + t) < 160.0) + return (tHeatEasy - 32) * 5 / 9; + + // need to use the hard form, and possibly adjust. + var t2 = t * t; // t squared + var rh2 = rh * rh; // rh squared + var tResult = + -42.379 + + (2.04901523 * t) + + (10.14333127 * rh) + + (-0.22475541 * t * rh) + + (-0.00683783 * t2) + + (-0.05481717 * rh2) + + (0.00122874 * t2 * rh) + + (0.00085282 * t * rh2) + + (-0.00000199 * t2 * rh2); + + // these adjustments come from the NWA page, and are needed to + // match the reference table. + var tAdjust; + if (rh < 13.0 && 80.0 <= t && t <= 112.0) + tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); + else if (rh > 85.0 && 80.0 <= t && t <= 87.0) + tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); + else + tAdjust = 0; + + // apply the adjustment + tResult += tAdjust; + + // finally, the reference tables have no data above 183 (rounded), + // so filter out answers that we have no way to vouch for. + if (tResult >= 183.5) + return null; + else + return (tResult - 32) * 5 / 9; + } + + function DecodeU16(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + var result = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + return result; + } + + function DecodeUflt16(Parse) { + var rawUflt16 = DecodeU16(Parse); + var exp1 = rawUflt16 >> 12; + var mant1 = (rawUflt16 & 0xFFF) / 4096.0; + var f_unscaled = mant1 * Math.pow(2, exp1 - 15); + return f_unscaled; + } + + function DecodeSflt16(Parse) + { + var rawSflt16 = DecodeU16(Parse); + // rawSflt16 is the 2-byte number decoded from wherever; + // it's in range 0..0xFFFF + // bit 15 is the sign bit + // bits 14..11 are the exponent + // bits 10..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + // + // The result is a number in the open interval (-1.0, 1.0); + // + + // throw away high bits for repeatability. + rawSflt16 &= 0xFFFF; + + // special case minus zero: + if (rawSflt16 === 0x8000) + return -0.0; + + // extract the sign. + var sSign = ((rawSflt16 & 0x8000) !== 0) ? -1 : 1; + + // extract the exponent + var exp1 = (rawSflt16 >> 11) & 0xF; + + // extract the "mantissa" (the fractional part) + var mant1 = (rawSflt16 & 0x7FF) / 2048.0; + + // convert back to a floating point number. We hope + // that Math.pow(2, k) is handled efficiently by + // the JS interpreter! If this is time critical code, + // you can replace by a suitable shift and divide. + var f_unscaled = sSign * mant1 * Math.pow(2, exp1 - 15); + + return f_unscaled; + } + + + function DecodeLight(Parse) { + return DecodeUflt16(Parse); + } + + function DecodeActivity(Parse) { + return DecodeSflt16(Parse); + } + + function DecodeI16(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + var result = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + + // interpret uint16 as an int16 instead. + if (result & 0x8000) + result += -0x10000; + + return result; + } + + function DecodeI32(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + // interpret uint16 as an int16 instead. + if (result & 0x80000000) + result += -0x100000000; + + return result; + } + + function DecodeU32(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + return result; + } + + function RemainingBytes(Parse) { + var i = Parse.i; + var nBytes = Parse.bytes.length; + + if (i < nBytes) + return (nBytes - i); + else + return 0; + } + + function DecodeV(Parse) { + return DecodeI16(Parse) / 4096.0; + } + + function Decoder(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + if (! (port === 2)) + return null; + + var uFormat = bytes[0]; + if (! (uFormat === 0x22)) + return null; + + // an object to help us parse. + var Parse = {}; + Parse.bytes = bytes; + // i is used as the index into the message. Start with the time. + Parse.i = 1; + + // fetch time, convert from GPS to ISO time assuming 17 leap seconds, + // and then convert to JSON format. + decoded.time = new Date((DecodeU32(Parse) + 315964800 - 17) * 1000).toJSON(); + + // fetch the bitmap. + var flags = bytes[Parse.i++]; + + if (flags & 0x1) { + decoded.Vbat = DecodeV(Parse); + } + + if (flags & 0x2) { + decoded.Vsys = DecodeV(Parse); + } + + if (flags & 0x4) { + decoded.Vbus = DecodeV(Parse); + } + + if (flags & 0x8) { + var iBoot = bytes[Parse.i++]; + decoded.boot = iBoot; + } + + if (flags & 0x10) { + // we have temp, pressure, RH + decoded.tempC = DecodeI16(Parse) / 256; + decoded.p = DecodeU16(Parse) * 4 / 100.0; + decoded.rh = DecodeU16(Parse) * 100 / 65535.0; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + var tHeat = CalculateHeatIndex(decoded.tempC * 1.8 + 32, decoded.rh); + if (tHeat !== null) + decoded.tHeatIndexC = tHeat; + } + + if (flags & 0x20) { + // we have light + decoded.irradiance = {}; + decoded.irradiance.White = DecodeLight(Parse) * Math.pow(2.0, 24); + } + + if (flags & 0x40) { + // we have gpio counts + decoded.pellets = []; + for (var i = 0; i < 2; ++i) { + decoded.pellets[i] = {}; + decoded.pellets[i].Total = DecodeU16(Parse); + decoded.pellets[i].Delta = bytes[Parse.i++]; + } + } + + if (flags & 0x80) { + // we have Activity + decoded.activity = []; + var i = 0; + while (RemainingBytes(Parse) >= 2) { + decoded.activity[i] = DecodeActivity(Parse); + ++i; + } + } + + return decoded; + } + + // TTN V3 decoder + function decodeUplink(tInput) { + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; + } diff --git a/vendor/mcci/index.yaml b/vendor/mcci/index.yaml index 2904a7f41e..e7d5366672 100644 --- a/vendor/mcci/index.yaml +++ b/vendor/mcci/index.yaml @@ -16,14 +16,5 @@ endDevices: - model4831 - model4832 - model4841 -profileIDs: - '3': - endDeviceID: 'model4841' - firmwareVersion: '2.0.2' - hardwareVersion: '' - region: 'IN865-867' - '8': - endDeviceID: 'model4811' - firmwareVersion: '1.4.0' - hardwareVersion: '' - region: 'IN865-867' + - model4931 + - model4933 diff --git a/vendor/mcci/model4811.yaml b/vendor/mcci/model4811.yaml index 8f900e495a..4807056b2c 100644 --- a/vendor/mcci/model4811.yaml +++ b/vendor/mcci/model4811.yaml @@ -46,6 +46,42 @@ firmwareVersions: lorawanCertified: false codec: codec-model4811 + - # Firmware version + version: '2.2.4' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: model4811-profile + lorawanCertified: false + codec: codec-model4811 + US902-928: + id: model4811-profile + lorawanCertified: false + codec: codec-model4811 + AU915-928: + id: model4811-profile + lorawanCertified: false + codec: codec-model4811 + AS923: + id: model4811-profile + lorawanCertified: false + codec: codec-model4811 + KR920-923: + id: model4811-profile + lorawanCertified: false + codec: codec-model4811 + IN865-867: + id: model4811-profile + lorawanCertified: false + codec: codec-model4811 + # Sensors that this device features (optional) # Valid values are: # accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, diff --git a/vendor/mcci/model4821.yaml b/vendor/mcci/model4821.yaml index b32a9b4d50..f4ab5758a9 100644 --- a/vendor/mcci/model4821.yaml +++ b/vendor/mcci/model4821.yaml @@ -46,6 +46,42 @@ firmwareVersions: lorawanCertified: false codec: codec-catena-generic + - # Firmware version + version: '0.4.0' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: model4821-profile + lorawanCertified: false + codec: codec-catena-generic + US902-928: + id: model4821-profile + lorawanCertified: false + codec: codec-catena-generic + AU915-928: + id: model4821-profile + lorawanCertified: false + codec: codec-catena-generic + AS923: + id: model4821-profile + lorawanCertified: false + codec: codec-catena-generic + KR920-923: + id: model4821-profile + lorawanCertified: false + codec: codec-catena-generic + IN865-867: + id: model4821-profile + lorawanCertified: false + codec: codec-catena-generic + # Sensors that this device features (optional) # Valid values are: # accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, diff --git a/vendor/mcci/model4822.yaml b/vendor/mcci/model4822.yaml index 266a3f9b42..46c6f04efc 100644 --- a/vendor/mcci/model4822.yaml +++ b/vendor/mcci/model4822.yaml @@ -46,6 +46,42 @@ firmwareVersions: lorawanCertified: false codec: codec-catena-generic + - # Firmware version + version: '0.4.1' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: model4822-profile + lorawanCertified: false + codec: codec-catena4618m201 + US902-928: + id: model4822-profile + lorawanCertified: false + codec: codec-catena4618m201 + AU915-928: + id: model4822-profile + lorawanCertified: false + codec: codec-catena4618m201 + AS923: + id: model4822-profile + lorawanCertified: false + codec: codec-catena4618m201 + KR920-923: + id: model4822-profile + lorawanCertified: false + codec: codec-catena4618m201 + IN865-867: + id: model4822-profile + lorawanCertified: false + codec: codec-catena4618m201 + # Sensors that this device features (optional) # Valid values are: # accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, diff --git a/vendor/mcci/model4823.yaml b/vendor/mcci/model4823.yaml index af02b42e19..f5ebd78505 100644 --- a/vendor/mcci/model4823.yaml +++ b/vendor/mcci/model4823.yaml @@ -46,6 +46,42 @@ firmwareVersions: lorawanCertified: false codec: codec-catena-generic + - # Firmware version + version: '0.4.1' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: model4823-profile + lorawanCertified: false + codec: codec-catena4618m201 + US902-928: + id: model4823-profile + lorawanCertified: false + codec: codec-catena4618m201 + AU915-928: + id: model4823-profile + lorawanCertified: false + codec: codec-catena4618m201 + AS923: + id: model4823-profile + lorawanCertified: false + codec: codec-catena4618m201 + KR920-923: + id: model4823-profile + lorawanCertified: false + codec: codec-catena4618m201 + IN865-867: + id: model4823-profile + lorawanCertified: false + codec: codec-catena4618m201 + # Sensors that this device features (optional) # Valid values are: # accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, diff --git a/vendor/mcci/model4831.yaml b/vendor/mcci/model4831.yaml index 5b1f542949..9841a16b48 100644 --- a/vendor/mcci/model4831.yaml +++ b/vendor/mcci/model4831.yaml @@ -46,6 +46,42 @@ firmwareVersions: lorawanCertified: false codec: codec-catena-generic + - # Firmware version + version: '0.4.1' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: model4831-profile + lorawanCertified: false + codec: codec-catena4618m201 + US902-928: + id: model4831-profile + lorawanCertified: false + codec: codec-catena4618m201 + AU915-928: + id: model4831-profile + lorawanCertified: false + codec: codec-catena4618m201 + AS923: + id: model4831-profile + lorawanCertified: false + codec: codec-catena4618m201 + KR920-923: + id: model4831-profile + lorawanCertified: false + codec: codec-catena4618m201 + IN865-867: + id: model4831-profile + lorawanCertified: false + codec: codec-catena4618m201 + # Sensors that this device features (optional) # Valid values are: # accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, @@ -113,7 +149,7 @@ dataSheetURL: https://cdn.shopify.com/s/files/1/0873/5104/files/model483x-datash # Photos photos: - main: model483x.png + main: model483x-closed.jpg other: - model483x-open.jpg # Youtube or Vimeo Video (optional) diff --git a/vendor/mcci/model4832.yaml b/vendor/mcci/model4832.yaml index 807a0c2d1e..f5fe0f3b74 100644 --- a/vendor/mcci/model4832.yaml +++ b/vendor/mcci/model4832.yaml @@ -46,6 +46,42 @@ firmwareVersions: lorawanCertified: false codec: codec-catena-generic + - # Firmware version + version: '0.4.1' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: model4832-profile + lorawanCertified: false + codec: codec-catena4618m201 + US902-928: + id: model4832-profile + lorawanCertified: false + codec: codec-catena4618m201 + AU915-928: + id: model4832-profile + lorawanCertified: false + codec: codec-catena4618m201 + AS923: + id: model4832-profile + lorawanCertified: false + codec: codec-catena4618m201 + KR920-923: + id: model4832-profile + lorawanCertified: false + codec: codec-catena4618m201 + IN865-867: + id: model4832-profile + lorawanCertified: false + codec: codec-catena4618m201 + # Sensors that this device features (optional) # Valid values are: # accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, @@ -113,7 +149,7 @@ dataSheetURL: https://cdn.shopify.com/s/files/1/0873/5104/files/model483x-datash # Photos photos: - main: model483x.png + main: model483x-closed.jpg other: - model483x-open.jpg # Youtube or Vimeo Video (optional) diff --git a/vendor/mcci/model483x-closed.jpg b/vendor/mcci/model483x-closed.jpg new file mode 100644 index 0000000000..d33e032015 Binary files /dev/null and b/vendor/mcci/model483x-closed.jpg differ diff --git a/vendor/mcci/model4841.yaml b/vendor/mcci/model4841.yaml index 5777169cc7..09502d68cd 100644 --- a/vendor/mcci/model4841.yaml +++ b/vendor/mcci/model4841.yaml @@ -1,5 +1,5 @@ name: Indoor Air Quality Sensor -description: The MCCI Indoor Air Quality Sensor consists of PM1.0, PM2.5, PM10, dust concentration, total volatile organic compounds, temperature, and humidity sensors. It transmits data using LoRaWAN® networks. +description: The MCCI Indoor Air Quality Sensor consists of PM1.0, PM2.5, PM10, dust concentration, total volatile organic compounds, temperature, and humidity sensors. It transmits data using LoRaWAN® networks. # Hardware versions (optional, used for revisions) # hardwareVersions: @@ -46,6 +46,42 @@ firmwareVersions: lorawanCertified: false codec: codec-model4841 + - # Firmware version + version: '1.4.0' + numeric: 2 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: model4841-profile + lorawanCertified: false + codec: codec-model4841 + US902-928: + id: model4841-profile + lorawanCertified: false + codec: codec-model4841 + AU915-928: + id: model4841-profile + lorawanCertified: false + codec: codec-model4841 + AS923: + id: model4841-profile + lorawanCertified: false + codec: codec-model4841 + KR920-923: + id: model4841-profile + lorawanCertified: false + codec: codec-model4841 + IN865-867: + id: model4841-profile + lorawanCertified: false + codec: codec-model4841 + # Sensors that this device features (optional) # Valid values are: # accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, diff --git a/vendor/mcci/model4931-profile.yaml b/vendor/mcci/model4931-profile.yaml new file mode 100644 index 0000000000..e0935863a3 --- /dev/null +++ b/vendor/mcci/model4931-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/mcci/model4931.png b/vendor/mcci/model4931.png new file mode 100644 index 0000000000..a0e3ba76e8 Binary files /dev/null and b/vendor/mcci/model4931.png differ diff --git a/vendor/mcci/model4931.yaml b/vendor/mcci/model4931.yaml new file mode 100644 index 0000000000..ec99ce03da --- /dev/null +++ b/vendor/mcci/model4931.yaml @@ -0,0 +1,118 @@ +name: Model 4931 Outdoor Sensor +description: Temperature, humidity, pressure, rainfall, soil temperature and moisture, tree temperature and pressure, sap flow + +# Hardware versions (optional, used for revisions) +# hardwareVersions: +# - version: 'A' +# numeric: 1 +# - version: 'A-1' +# numeric: 2 + +# Firmware versions (at least one mandatory) +firmwareVersions: + - # Firmware version + version: '1.3.1' + numeric: 1 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: model4931-profile + lorawanCertified: false + codec: codec-model4931 + US902-928: + id: model4931-profile + lorawanCertified: false + codec: codec-model4931 + AU915-928: + id: model4931-profile + lorawanCertified: false + codec: codec-model4931 + AS923: + id: model4931-profile + lorawanCertified: false + codec: codec-model4931 + KR920-923: + id: model4931-profile + lorawanCertified: false + codec: codec-model4931 + IN865-867: + id: model4931-profile + lorawanCertified: false + codec: codec-model4931 + +# Sensors that this device features (optional) +# Valid values are: +# accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, +# distance, dust, energy, gps, gyroscope, humidity, light, link, magnetometer, moisture, ph, pir, power, pressure, +# proximity, pulse count, pulse frequency, rainfall, rssi, snr, solar radiation, sound, temperature, time, tvoc, uv, +# velocity, vibration, voltage, water, wifi ssid, wind direction, wind speed. +sensors: + - temperature + - humidity + - pressure + - rainfall + - pulse count + - battery + +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 250 + length: 170 + height: 100 + +## # Weight in grams (optional) +## weight: 350 + +# Battery information (optional) +battery: + replaceable: true + type: Li-ion cylindrical (ICR18650) + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celcius) + temperature: + min: -20 + max: 70 + # Relative humidity (fraction of 1) + relativeHumidity: + min: 0 + max: 0.95 + +## # IP rating (optional) +## ipCode: IP64 + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +keyProvisioning: + - custom +##- join server + +# Key security (optional) +# Valid values are: none, read protected and secure element. +keySecurity: secure element + +# Product and data sheet URLs (optional) +productURL: https://mcci.io/m4931 +dataSheetURL: https://cdn.shopify.com/s/files/1/0873/5104/files/model483x-datasheet.pdf?v=1610937656 +## resellerURLs: +## - name: 'Reseller 1' +## region: +## - European Union +## url: https://example.org/reseller1 +## - name: 'Reseller 2' +## region: +## - United States +## - Canada +## url: https://example.org/reseller2 + +# Photos +photos: + main: model4931.png diff --git a/vendor/mcci/model4933-profile.yaml b/vendor/mcci/model4933-profile.yaml new file mode 100644 index 0000000000..e0935863a3 --- /dev/null +++ b/vendor/mcci/model4933-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/mcci/model4933.png b/vendor/mcci/model4933.png new file mode 100644 index 0000000000..7e256716a0 Binary files /dev/null and b/vendor/mcci/model4933.png differ diff --git a/vendor/mcci/model4933.yaml b/vendor/mcci/model4933.yaml new file mode 100644 index 0000000000..98ac929da8 --- /dev/null +++ b/vendor/mcci/model4933.yaml @@ -0,0 +1,118 @@ +name: Model 4933 Air Quality Gas Sensor +description: Temperature, humidity, pressure sensor, Gas sensors (CO, CO2, NO2, O3, SO2) and PM2.5 + +# Hardware versions (optional, used for revisions) +# hardwareVersions: +# - version: 'A' +# numeric: 1 +# - version: 'A-1' +# numeric: 2 + +# Firmware versions (at least one mandatory) +firmwareVersions: + - # Firmware version + version: '1.2.0' + numeric: 1 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '1.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: model4933-profile + lorawanCertified: false + codec: codec-model4933 + US902-928: + id: model4933-profile + lorawanCertified: false + codec: codec-model4933 + AU915-928: + id: model4933-profile + lorawanCertified: false + codec: codec-model4933 + AS923: + id: model4933-profile + lorawanCertified: false + codec: codec-model4933 + KR920-923: + id: model4933-profile + lorawanCertified: false + codec: codec-model4933 + IN865-867: + id: model4933-profile + lorawanCertified: false + codec: codec-model4933 + +# Sensors that this device features (optional) +# Valid values are: +# accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, +# distance, dust, energy, gps, gyroscope, humidity, light, link, magnetometer, moisture, ph, pir, power, pressure, +# proximity, pulse count, pulse frequency, rainfall, rssi, snr, solar radiation, sound, temperature, time, tvoc, uv, +# velocity, vibration, voltage, water, wifi ssid, wind direction, wind speed. +sensors: + - temperature + - humidity + - pressure + - co2 + - dust + - battery + +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 86 + length: 137 + height: 43 + +## # Weight in grams (optional) +## weight: 350 + +# Battery information (optional) +battery: + replaceable: true + type: Li-ion cylindrical (ICR18650) + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celcius) + temperature: + min: -20 + max: 70 + # Relative humidity (fraction of 1) + relativeHumidity: + min: 0 + max: 0.95 + +## # IP rating (optional) +## ipCode: IP64 + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +keyProvisioning: + - custom +##- join server + +# Key security (optional) +# Valid values are: none, read protected and secure element. +keySecurity: secure element + +# Product and data sheet URLs (optional) +productURL: https://mcci.io/m4933 +dataSheetURL: https://cdn.shopify.com/s/files/1/0873/5104/files/model483x-datasheet.pdf?v=1610937656 +## resellerURLs: +## - name: 'Reseller 1' +## region: +## - European Union +## url: https://example.org/reseller1 +## - name: 'Reseller 2' +## region: +## - United States +## - Canada +## url: https://example.org/reseller2 + +# Photos +photos: + main: model4933.png diff --git a/vendor/pallidus/codec-mr1.js b/vendor/pallidus/codec-mr1.js new file mode 100644 index 0000000000..b9acfdc2ec --- /dev/null +++ b/vendor/pallidus/codec-mr1.js @@ -0,0 +1,578 @@ +/* + +Name: codec-mr1.js + +Function: + Decode port 0x02/0x03/0x04 format 0x26/0x36 messages for TTN console. + +Copyright and License: + See accompanying LICENSE file at https://github.com/mcci-catena/MCCI-Catena-4430/ + +Author: + Dhinesh Kumar Pitchai, MCCI Corporation August 2022 + +*/ + +// calculate dewpoint (degrees C) given temperature (C) and relative humidity (0..100) +// from http://andrew.rsmas.miami.edu/bmcnoldy/Humidity.html +// rearranged for efficiency and to deal sanely with very low (< 1%) RH +function dewpoint(t, rh) { + var c1 = 243.04; + var c2 = 17.625; + var h = rh / 100; + if (h <= 0.01) + h = 0.01; + else if (h > 1.0) + h = 1.0; + + var lnh = Math.log(h); + var tpc1 = t + c1; + var txc2 = t * c2; + var txc2_tpc1 = txc2 / tpc1; + + var tdew = c1 * (lnh + txc2_tpc1) / (c2 - lnh - txc2_tpc1); + return tdew; +} + +/* + +Name: CalculateHeatIndex() + +Description: + Calculate the NWS heat index given dry-bulb T and RH + +Definition: + function CalculateHeatIndex(t, rh) -> value or null + +Description: + T is a Farentheit temperature in [76,120]; rh is a + relative humidity in [0,100]. The heat index is computed + and returned; or an error is returned. For consistency with + the other temperature, despite the heat index being defined + in Farenheit, we return in Celsius. + +Returns: + number => heat index in Farenheit. + null => error. + +References: + https://github.com/mcci-catena/heat-index/ + https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml + + Results was checked against the full chart at iweathernet.com: + https://www.iweathernet.com/wxnetcms/wp-content/uploads/2015/07/heat-index-chart-relative-humidity-2.png + + The MCCI-Catena heat-index site has a test js script to generate CSV to + match the chart, a spreadsheet that recreates the chart, and a + spreadsheet that compares results. + +*/ + +function CalculateHeatIndex(t, rh) { + var tRounded = Math.floor(t + 0.5); + + // return null outside the specified range of input parameters + if (tRounded < 76 || tRounded > 126) + return null; + if (rh < 0 || rh > 100) + return null; + + // according to the NWS, we try this first, and use it if we can + var tHeatEasy = 0.5 * (t + 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094)); + + // The NWS says we use tHeatEasy if (tHeatHeasy + t)/2 < 80.0 + // This is the same computation: + if ((tHeatEasy + t) < 160.0) + return (tHeatEasy - 32) * 5 / 9; + + // need to use the hard form, and possibly adjust. + var t2 = t * t; // t squared + var rh2 = rh * rh; // rh squared + var tResult = + -42.379 + + (2.04901523 * t) + + (10.14333127 * rh) + + (-0.22475541 * t * rh) + + (-0.00683783 * t2) + + (-0.05481717 * rh2) + + (0.00122874 * t2 * rh) + + (0.00085282 * t * rh2) + + (-0.00000199 * t2 * rh2); + + // these adjustments come from the NWA page, and are needed to + // match the reference table. + var tAdjust; + if (rh < 13.0 && 80.0 <= t && t <= 112.0) + tAdjust = -((13.0 - rh) / 4.0) * Math.sqrt((17.0 - Math.abs(t - 95.0)) / 17.0); + else if (rh > 85.0 && 80.0 <= t && t <= 87.0) + tAdjust = ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0); + else + tAdjust = 0; + + // apply the adjustment + tResult += tAdjust; + + // finally, the reference tables have no data above 183 (rounded), + // so filter out answers that we have no way to vouch for. + if (tResult >= 183.5) + return null; + else + return (tResult - 32) * 5 / 9; +} + +function DecodeU16(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + var result = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + return result; +} + +function DecodeUflt16(Parse) { + var rawUflt16 = DecodeU16(Parse); + var exp1 = rawUflt16 >> 12; + var mant1 = (rawUflt16 & 0xFFF) / 4096.0; + var f_unscaled = mant1 * Math.pow(2, exp1 - 15); + return f_unscaled; +} + +function DecodeSflt16(Parse) + { + var rawSflt16 = DecodeU16(Parse); + // rawSflt16 is the 2-byte number decoded from wherever; + // it's in range 0..0xFFFF + // bit 15 is the sign bit + // bits 14..11 are the exponent + // bits 10..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + // + // The result is a number in the open interval (-1.0, 1.0); + // + + // throw away high bits for repeatability. + rawSflt16 &= 0xFFFF; + + // special case minus zero: + if (rawSflt16 === 0x8000) + return -0.0; + + // extract the sign. + var sSign = ((rawSflt16 & 0x8000) !== 0) ? -1 : 1; + + // extract the exponent + var exp1 = (rawSflt16 >> 11) & 0xF; + + // extract the "mantissa" (the fractional part) + var mant1 = (rawSflt16 & 0x7FF) / 2048.0; + + // convert back to a floating point number. We hope + // that Math.pow(2, k) is handled efficiently by + // the JS interpreter! If this is time critical code, + // you can replace by a suitable shift and divide. + var f_unscaled = sSign * mant1 * Math.pow(2, exp1 - 15); + + return f_unscaled; + } + + +function DecodeLight(Parse) { + return DecodeUflt16(Parse); +} + +function DecodeActivity(Parse) { + return DecodeSflt16(Parse); +} + +function DecodeI16(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + var result = (bytes[i] << 8) + bytes[i + 1]; + Parse.i = i + 2; + + // interpret uint16 as an int16 instead. + if (result & 0x8000) + result += -0x10000; + + return result; +} + +function DecodeU24(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i] << 16) + (bytes[i + 1] << 8) + bytes[i + 2]; + Parse.i = i + 3; + + return result; +} + +function DecodeSflt24(Parse) + { + var rawSflt24 = DecodeU24(Parse); + // rawSflt24 is the 3-byte number decoded from wherever; + // it's in range 0..0xFFFFFF + // bit 23 is the sign bit + // bits 22..16 are the exponent + // bits 15..0 are the the mantissa. Unlike IEEE format, + // the msb is explicit; this means that numbers + // might not be normalized, but makes coding for + // underflow easier. + // As with IEEE format, negative zero is possible, so + // we special-case that in hopes that JavaScript will + // also cooperate. + + // extract sign, exponent, mantissa + var bSign = (rawSflt24 & 0x800000) ? true : false; + var uExp = (rawSflt24 & 0x7F0000) >> 16; + var uMantissa = (rawSflt24 & 0x00FFFF); + + // if non-numeric, return appropriate result. + if (uExp === 0x7F) { + if (uMantissa === 0) + return bSign ? Number.NEGATIVE_INFINITY + : Number.POSITIVE_INFINITY; + else + return Number.NaN; + // else unless denormal, set the 1.0 bit + } else if (uExp !== 0) { + uMantissa += 0x010000; + } else { // denormal: exponent is the minimum + uExp = 1; + } + + // make a floating mantissa in [0,2); usually [1,2), but + // sometimes (0,1) for denormals, and exactly zero for zero. + var mantissa = uMantissa / 0x010000; + + // apply the exponent. + mantissa = Math.pow(2, uExp - 63) * mantissa; + + // apply sign and return result. + return bSign ? -mantissa : mantissa; + } + +function DecodeLux(Parse) { + return DecodeSflt24(Parse); +} + +function DecodeI32(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + // interpret uint16 as an int16 instead. + if (result & 0x80000000) + result += -0x100000000; + + return result; +} + +function DecodeU32(Parse) { + var i = Parse.i; + var bytes = Parse.bytes; + + var result = (bytes[i + 0] << 24)+ (bytes[i + 1] << 16) + (bytes[i + 2] << 8) + bytes[i + 3]; + Parse.i = i + 4; + + return result; +} + +function RemainingBytes(Parse) { + var i = Parse.i; + var nBytes = Parse.bytes.length; + + if (i < nBytes) + return (nBytes - i); + else + return 0; +} + +function DecodeV(Parse) { + return DecodeI16(Parse) / 4096.0; +} + +function DecodeDownlinkResponse(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + // an object to help us parse. + var Parse = {}; + Parse.bytes = bytes; + // i is used as the index into the message. Start with the flag byte. + Parse.i = 0; + + if (port === 1) + { + // SW version and HW details. + decoded.ResponseType = "Set Uplink Interval"; + // fetch the bitmap. + var response = bytes[Parse.i++]; + if (response === 0) + decoded.response = "Success"; + else if (response === 1) + decoded.response = "Invalid Length"; + else if (response === 2) + decoded.response = "Failure"; + else if (response === 3) + decoded.response = "Invalid Range"; + } + + else if (port === 2) + { + // SW version and HW details. + decoded.ResponseType = "SCD30 CO2 Calibration"; + // fetch the bitmap. + var response = bytes[Parse.i++]; + if (response === 0) + decoded.response = "Success"; + else if (response === 1) + decoded.response = "Invalid Length"; + else if (response === 2) + decoded.response = "Failure"; + else if (response === 3) + decoded.response = "Invalid Range"; + else if (response === 4) + decoded.response = "Sensor Not Connected"; + } + + else if (port === 3) + { + // fetch the bitmap. + var command = bytes[Parse.i++]; + + if (command === 0x02) { + // Reset do not send a reply back. + } + + else if (command === 0x03) { + // SW version and HW details. + decoded.ResponseType = "Device Version"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.ResponseError = "Success"; + else if (responseError === 1) + decoded.ResponseError = "Invalid Length"; + else if (responseError === 2) + decoded.ResponseError = "Failure"; + + var vMajor = bytes[Parse.i++]; + var vMinor = bytes[Parse.i++]; + var vPatch = bytes[Parse.i++]; + var vLocal = bytes[Parse.i++]; + decoded.AppVersion = "V" + vMajor + "." + vMinor + "." + vPatch + "." + vLocal; + + var Model = DecodeU16(Parse); + var Rev = bytes[Parse.i++]; + if (!(Model === 0)) + { + decoded.Model = Model; + if (Rev === 0) + decoded.Rev = "A"; + else if (Rev === 1) + decoded.Rev = "B"; + else if (Rev === 2) + decoded.Rev = "C"; + else if (Rev === 3) + decoded.Rev = "D"; + else if (Rev === 4) + decoded.Rev = "E"; + else if (Rev === 5) + decoded.Rev = "F"; + else if (Rev === 6) + decoded.Rev = "G"; + } + else if (Model === 0) + { + decoded.Model = 4610; + decoded.Rev = "Not Found"; + } + } + + else if (command === 0x04) { + // Reset/Set AppEUI. + decoded.ResponseType = "AppEUI Set"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.ResponseError = "Success"; + else if (responseError === 1) + decoded.ResponseError = "Invalid Length"; + else if (responseError === 2) + decoded.ResponseError = "Failure"; + } + + else if (command === 0x05) { + // Reset/Set AppKey. + decoded.ResponseType = "AppKey set"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.ResponseError = "Success"; + else if (responseError === 1) + decoded.ResponseError = "Invalid Length"; + else if (responseError === 2) + decoded.ResponseError = "Failure"; + } + + else if (command === 0x06) { + // Rejoin the network. + decoded.ResponseType = "Rejoin"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.ResponseError = "Success"; + else if (responseError === 1) + decoded.ResponseError = "Invalid Length"; + else if (responseError === 2) + decoded.ResponseError = "Failure"; + } + + else if (command === 0x07) { + // Uplink Interval for sensor data. + decoded.ResponseType = "Uplink Interval"; + + var responseError = bytes[Parse.i++]; + if (responseError === 0) + decoded.ResponseError = "Success"; + else if (responseError === 1) + decoded.ResponseError = "Invalid Length"; + else if (responseError === 2) + decoded.ResponseError = "Failure"; + + decoded.UplinkInterval = DecodeU32(Parse); + } + else + return null; + } + + return decoded; +} + +function Decoder(bytes, port) { + // Decode an uplink message from a buffer + // (array) of bytes to an object of fields. + var decoded = {}; + + if (! (port === 1) && ! (port === 2) && ! (port === 3) && ! (port === 4)) + return null; + + var uFormat = bytes[0]; + if (! (uFormat === 0x26) && ! (uFormat === 0x36) && ((port === 1) || (port === 2) || (port === 3))) + { + decoded = DecodeDownlinkResponse(bytes, port); + return decoded; + } + else if (! (uFormat === 0x26) && ! (uFormat === 0x36) && ! ((port === 1) || (port === 2) || (port === 3))) + return null; + + // an object to help us parse. + var Parse = {}; + Parse.bytes = bytes; + // i is used as the index into the message. Start with the time. + Parse.i = 1; + + // fetch time; convert to database time (which is UTC-like ignoring leap seconds) + decoded.time = new Date((DecodeU32(Parse) + /* gps epoch to posix */ 315964800 - /* leap seconds */ 17) * 1000); + + // fetch the bitmap. + var flags = bytes[Parse.i++]; + + if (flags & 0x1) { + decoded.Vbat = DecodeV(Parse); + } + + if (flags & 0x2) { + var vMajor = bytes[Parse.i++]; + var vMinor = bytes[Parse.i++]; + var vPatch = bytes[Parse.i++]; + var vLocal = bytes[Parse.i++]; + + decoded.version = vMajor + "." + vMinor + "." + vPatch + "." + vLocal; + } + + if (flags & 0x4) { + // we have CO2 ppm. + decoded.co2 = DecodeUflt16(Parse) * 40000; + } + + if (flags & 0x8) { + var iBoot = bytes[Parse.i++]; + decoded.boot = iBoot; + } + + if (flags & 0x10) { + if (uFormat === 0x26) { + // we have temp, pressure, RH + decoded.tempC = DecodeI16(Parse) / 256; + decoded.p = DecodeU16(Parse) * 4 / 100.0; + decoded.rh = DecodeU16(Parse) * 100 / 65535.0; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + var tHeat = CalculateHeatIndex(decoded.tempC * 1.8 + 32, decoded.rh); + if (tHeat !== null) + decoded.tHeatIndexC = tHeat; + } + else if (uFormat === 0x36) { + // we have temp, pressure, RH + decoded.tempC = DecodeI16(Parse) / 256; + decoded.rh = DecodeU16(Parse) * 100 / 65535.0; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + var tHeat = CalculateHeatIndex(decoded.tempC * 1.8 + 32, decoded.rh); + if (tHeat !== null) + decoded.tHeatIndexC = tHeat; + } + } + + if (flags & 0x20) { + if (uFormat === 0x26) { + // we have light + decoded.irradiance = {}; + decoded.irradiance.White = DecodeLight(Parse) * Math.pow(2.0, 24); + } + else if (uFormat === 0x36) { + // we have light + decoded.lux = DecodeLux(Parse); + } + } + + if (flags & 0x40) { + // we have gpio counts + decoded.pellets = []; + for (var i = 0; i < 2; ++i) { + decoded.pellets[i] = {}; + decoded.pellets[i].Total = DecodeU16(Parse); + decoded.pellets[i].Delta = bytes[Parse.i++]; + } + } + + if (flags & 0x80) { + // we have Activity + decoded.activity = []; + var i = 0; + while (RemainingBytes(Parse) >= 2) { + decoded.activity[i] = DecodeActivity(Parse); + ++i; + } + } + + if (port === 3) + decoded.NwTime = "set"; + + return decoded; +} + +// TTN V3 decoder +function decodeUplink(tInput) { + var decoded = Decoder(tInput.bytes, tInput.fPort); + var result = {}; + result.data = decoded; + return result; +} \ No newline at end of file diff --git a/vendor/pallidus/codec-mr1.yaml b/vendor/pallidus/codec-mr1.yaml new file mode 100644 index 0000000000..8f44e271f2 --- /dev/null +++ b/vendor/pallidus/codec-mr1.yaml @@ -0,0 +1,81 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: codec-mr1.js + # Examples (optional) + examples: + - description: Time 1742982409 Vbat 2 Version 2 5 2 0 CO2 800 Boot 45 Env 20 60 Light 1377 Pellets 100 3 25 10 Activity [ 0.53 -1 1 -0.5 0.25 -0.3 ] . + input: + fPort: 2 + bytes: + [ + 0x36, + 0x55, + 0x14, + 0xA6, + 0xF2, + 0xF9, + 0x3B, + 0x2C, + 0x09, + 0x16, + 0x16, + 0xAD, + 0x87, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0xDE, + 0x4B, + 0xEF, + 0x05, + 0xF7, + 0xF5, + 0xFE, + 0xEF, + 0xF4, + 0x32, + ] + output: + data: + { + 'Vbat': 3.6982421875, + 'activity': [-0.049163818359375, -0.2193603515625, -0.497314453125, -0.86669921875, -0.26220703125], + 'boot': 9, + 'lux': 0, + 'pellets': [{ 'Delta': 0, 'Total': 0 }, { 'Delta': 0, 'Total': 0 }], + 'rh': 67.78515297169452, + 'tDewC': 15.855392059081987, + 'tempC': 22.0859375, + 'time': '2025-03-31T00:40:01Z', + } +# Downlink encoder encodes JSON object into a binary data downlink (optional) +## downlinkEncoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## data: +## led: green +## output: +## bytes: [1] +## fPort: 2 +## # Downlink decoder decodes the encoded downlink message (optional, must be symmetric with downlinkEncoder) +## downlinkDecoder: +## fileName: codec.js +## # Examples (optional) +## examples: +## - description: Turn green +## input: +## fPort: 2 +## bytes: [1] +## output: +## data: +## led: green diff --git a/vendor/pallidus/index.yaml b/vendor/pallidus/index.yaml new file mode 100644 index 0000000000..1f6f4dd24b --- /dev/null +++ b/vendor/pallidus/index.yaml @@ -0,0 +1,2 @@ +endDevices: + - mr1 diff --git a/vendor/pallidus/mr1-profile.yaml b/vendor/pallidus/mr1-profile.yaml new file mode 100644 index 0000000000..e0935863a3 --- /dev/null +++ b/vendor/pallidus/mr1-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/pallidus/mr1.png b/vendor/pallidus/mr1.png new file mode 100644 index 0000000000..9c1376ea12 Binary files /dev/null and b/vendor/pallidus/mr1.png differ diff --git a/vendor/pallidus/mr1.yaml b/vendor/pallidus/mr1.yaml new file mode 100644 index 0000000000..ac25470e5d --- /dev/null +++ b/vendor/pallidus/mr1.yaml @@ -0,0 +1,140 @@ +name: Animal Activity Sensor +description: The MR1 Activity Sensor Wing allows mounting Panasonic WL series PIR sensors and connecting additional external sensors. + +# Hardware versions (optional, used for revisions) +# hardwareVersions: +# - version: 'A' +# numeric: 1 +# - version: 'A-1' +# numeric: 2 + +# Firmware versions (at least one mandatory) +firmwareVersions: + - # Firmware version + version: '2.x.x' + numeric: 1 + # corresponding hardware versions (optional) + # hardwareVersions: + # - '2.0' + + # LoRaWAN device profiles per region + # defined regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870 + profiles: + EU863-870: + # unique ID of the profile (lowercase, alpha with dashes, max 36) + id: mr1-profile + lorawanCertified: false + codec: codec-mr1 + US902-928: + id: mr1-profile + lorawanCertified: false + codec: codec-mr1 + AU915-928: + id: mr1-profile + lorawanCertified: false + codec: codec-mr1 + AS923: + id: mr1-profile + lorawanCertified: false + codec: codec-mr1 + KR920-923: + id: mr1-profile + lorawanCertified: false + codec: codec-mr1 + IN865-867: + id: mr1-profile + lorawanCertified: false + codec: codec-mr1 + +# Sensors that this device features (optional) +# Valid values are: +# accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co2, current, digital input, +# distance, dust, energy, gps, gyroscope, humidity, light, link, magnetometer, moisture, ph, pir, power, pressure, +# proximity, pulse count, pulse frequency, rainfall, rssi, snr, solar radiation, sound, temperature, time, tvoc, uv, +# velocity, vibration, voltage, water, wifi ssid, wind direction, wind speed. +sensors: + - temperature + - humidity + - light + - pir + - digital input + - analog input + - battery + - barometer + - time + +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 58 + length: 84 + height: 30 + +## # Weight in grams (optional) +## weight: 350 + +# Battery information (optional) +battery: + replaceable: true + type: LiPo 3.7V + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celsius) + temperature: + min: -20 + max: 85 + # Relative humidity (fraction of 1) + relativeHumidity: + min: 0 + max: 0.90 + +## # IP rating (optional) +## ipCode: IP64 + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +keyProvisioning: + - custom +##- join server + +# Key security (optional) +# Valid values are: none, read protected and secure element. +keySecurity: none + +# Product and data sheet URLs (optional) +productURL: https://store.mcci.com/collections/pallidus +## dataSheetURL: https://example.org/wind-sensor/datasheet.pdf +## resellerURLs: +## - name: 'Reseller 1' +## region: +## - European Union +## url: https://example.org/reseller1 +## - name: 'Reseller 2' +## region: +## - United States +## - Canada +## url: https://example.org/reseller2 + +# Photos +photos: + main: mr1.png + +# Youtube or Vimeo Video (optional) +videos: + main: https://youtu.be/nG8MmaR5dsA +# Regulatory compliances (optional) +## compliances: +## safety: +## - body: IEC +## norm: EN +## standard: 62368-1 +## radioEquipment: +## - body: ETSI +## norm: EN +## standard: 301 489-1 +## version: 2.2.0 +## - body: ETSI +## norm: EN +## standard: 301 489-3 +## version: 2.1.0