diff --git a/.gitignore b/.gitignore index c0011a1..7a06e15 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ __vm # VS Code things .vscode + +# the build directory +build \ No newline at end of file diff --git a/README.md b/README.md index 19dad3c..6619524 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,15 @@ This repository is the top-level repository for the software. In order to build, - [Sketch Overview](#sketch-overview) - - [catena4450m101_sensor1](#catena4450m101_sensor1) - - [catena4410_sensor1](#catena4410_sensor1) - - [catena4410_test3](#catena4410_test3) - - [catena4410_test1, catena4410_test2](#catena4410_test1-catena4410_test2) - - [catena4450_test01](#catena4450_test01) - - [catena4450m101_sensor](#catena4450m101_sensor) - - [catena4450m102_pond](#catena4450m102_pond) + - [Full Sensor Programs](#full-sensor-programs) + - [catena4450m101_sensor1](#catena4450m101_sensor1) + - [catena4450m102_pond](#catena4450m102_pond) + - [catena4460_aqi](#catena4460_aqi) + - [catena4410_sensor1](#catena4410_sensor1) + - [Test programs](#test-programs) + - [catena4410_test3](#catena4410_test3) + - [catena4410_test1, catena4410_test2](#catena4410_test1-catena4410_test2) + - [catena4450_test01](#catena4450_test01) - [Extras](#extras) - [Required Board-Support Packages](#required-board-support-packages) - [Required Libraries](#required-libraries) @@ -29,79 +31,87 @@ This repository is the top-level repository for the software. In order to build, ## Sketch Overview -There are two kinds of sketches here: test programs (catena4410_test1, catene4410_test2, catena4410_test3, catena4450_test1), and full sensor programs (catena4410_sensor1, catena4420_test1, catena4450_sensor1). +There are two kinds of sketches here: test programs (catena4410_test1, catene4410_test2, catena4410_test3, catena4420_test1,catena4450_test1), and full sensor programs (catena4410_sensor1, catena4450m101_sensor1, catena4450m102_pond, catena4460_aqi). The sketches that use LoRaWAN take advantage of the [MCCI](http://www.mcci.com) [arduino-lorawan](https://github.com/mcci-catena/arduino-lorawan) library to cleanly separate the application from the network transport. The sensor sketches also use the RTCZero library to sleep the CPU. -### catena4450m101_sensor1 +### Full Sensor Programs + +#### catena4450m101_sensor1 This is the application written for the Catena 4450 power monitoring node used in the [Ithaca Power Project](https://ithaca-power.mcci.com). It uses FRAM-based provisioning (so there is no need to edit code to change LoRaWAN keys or other settings). -### catena4410_sensor1 +#### catena4450m102_pond + +This is the Tzu Chi University / Asolar Hualian research farm project sketch, upgraded for use with the Catena 4450. It uses the integrated FRAM for provisioning, auto-detects the attached sensors, and transmits data in format 0x15. + +#### catena4460_aqi + +This sketch collects and transmits air-quality information using the Bosch BME680 sensor on the MCCI Catena 4460. It transmits data in format 0x17. + +#### catena4410_sensor1 This sketch is the application written for the Tzu Chi University / Asolar Hualian research farm project. One firmware image is used for a variety of sensors. You can configure a given sensor as a general purpose device or as a specific subset, referencing back to the Atmel SAMD21 CPU's unique identifier. All provisioning is done at compile time, but the network keys and other sensitive information is placed in a special library that is outside the normal set of repositories. The sketch transmits data in format 0x11. -### catena4410_test3 +### Test programs + +#### catena4410_test3 This is the primary test app used when bringing up and provisioning Catena 4410 units for use with The Things Network. -### catena4410_test1, catena4410_test2 +#### catena4410_test1, catena4410_test2 These are simpler test programs. They were rarely used after test3 was ready, but they may be useful for test of future Catena 441x variants with different sensor configurations. -### catena4450_test01 +#### catena4450_test01 This is the primary (non-LoRaWAN) test sketch for the Catena 4450. -### catena4450m101_sensor - -This is the sketch used with the Catena 4450 for power sensing for the Ithaca Power project. It uses the integrated FRAM for provisioning, and transmits data in format 0x14. - -### catena4450m102_pond - -This is the Tzu Chi University / Asolar Hualian research farm project sketch, upgraded for use with the Catena 4450. It uses the integrated FRAM for provisioning, auto-detects the attached sensors, and transmits data in format 0x15. - ## Extras -The directory `extras` contains documenation and sample scripts for decoding the various formats. +The directory `extras` contains documentation and sample scripts for decoding the various formats. ## Required Board-Support Packages -All board support packages are maintained by MCCI. You should add the path to the reference Json file in your Arduino preferences. As of this writing, the file name to add to the list is: - -``` -https://github.com/mcci-catena/arduino-boards/raw/master/BoardManagerFiles/package_mcci_index.json -``` - -Check the [MCCI board support repository](https://github.com/mcci-catena/arduino-boards) for the latest information. +All board support packages are maintained by MCCI. You should add the path to the reference Json file in your Arduino preferences. See the README for [arduino-boards](https://github.com/mcci-catena/arduino-boards) for more informatin. ## Required Libraries A number of libraries are required by this code. `catena4450m101_sensor1` contains a Bash script [`git-boot.sh`](https://github.com/mcci-catena/Catena-Sketches/blob/master/catena4450m101_sensor/git-boot.sh) that can be used to download all the libraries, using a simple database stored in [`git-repos.dat`](https://github.com/mcci-catena/Catena-Sketches/blob/master/catena4450m101_sensor/git-repos.dat). -* [MCCI's Catena Platform library](https://github.com/mcci-catena/CatenaArduinoPlatform) provides an enhanced environment for portable sketch development. It includes an command-processing framework, an elaborate persistant storage framework, encoding libraries, support for storing the persistent data from the `arduino-lorawan` library, and so forth. -* [MCCI's Arduino-LoRaWAN library](https://github.com/mcci-catena/arduino-lorawan) is a structured wrapper for the Arduino LMIC library, with the neccessary hooks for interfacing to persistent storage. +* [MCCI's Catena Platform library](https://github.com/mcci-catena/Catena-Arduino-Platform) provides an enhanced environment for portable sketch development. It includes an command-processing framework, an elaborate persistent storage framework, encoding libraries, support for storing the persistent data from the `arduino-lorawan` library, and so forth. + +* [MCCI's Arduino-LoRaWAN library](https://github.com/mcci-catena/arduino-lorawan) is a structured wrapper for the Arduino LMIC library, with the necessary hooks for interfacing to persistent storage. + * [MCCI's Ardino LMIC library](https://github.com/mcci-catena/arduino-lmic) is MCCI's fork of [The Things Network New York Arduino LMIC code](https://github.com/things-nyc/arduino-lmic). + * [MCCI's ADK](https://github.com/mcci-catena/Catena-mcciadk) is MCCI's general-purpose cross-platform "XDK" library, ported to the Arduino environment. + * [MCCI's Fork of the SAMD RTCZero library](https://github.com/mcci-catena/RTCZero) has the somewhat more substantial changes needed to allow the various processor sleep modes to be accessed, and to allow for some debuggging of the sleep mode chosen. ## Libraries for sensor work * [MCCI's Adafruit BME280 library](https://github.com/mcci-catena/Adafruit_BME280_Library) is used to make temperature, humidity and barometric pressure measurements using the [Adafruit BME280 breakout board](https://www.adafruit.com/products/2652), which we connect via I2C. It's based on the Adafruit library, but updated so that temperature, humidity and pressure are all read at the same time, to avoid data instability. + * The [OneWire](https://github.com/mcci-catena/OneWire) and [Arduino Temperature Control Library](https://github.com/mcci-catena/Arduino-Temperature-Control-Library) are used for making measurements from Dallas-Semiconductor-based temperature sensors such as the [immersible sensor](https://www.adafruit.com/products/381) from Adafruit. + * The [Adafruit Sensor Library](https://github.com/mcci-catena/Adafruit_Sensor) and the [Adafruit TSL2561 Lux Sensor Library](https://github.com/mcci-catena/Adafruit_TSL2561) are used for making ambient light measurements with the Adafruit [TSL2561 Lux Sensor](https://www.adafruit.com/products/439) + * The [SHT1x library](https://github.com/mcci-catena/SHT1x) is used for measuring soil temperature and humidity using the Adafruit [SHT10 sensor](https://www.adafruit.com/products/1298). ## Related Work + * [MCCI's Fork of the Map The Things Arduino Sketch](https://github.com/mcci-catena/mapthethings-arduino) contains, on the MCCI-Catena branch, a port of that app supporting OTAA, using the Catena libraries. + * [MCCI's Catena Hardware Repository](https://github.com/mcci-catena/HW-Designs) contains hardware design information and schematics. ## Boilerplate + MCCI work is released under the MIT public license. All other work from contributors (repositories forked to the MCCI Catena [github page](https://github.com/mcci-catena/)) are licensed according to the terms of those modules. Support inquiries may be filed at [https:://portal.mcci.com](https:://portal.mcci.com) or as tickets on [github](https://github.com/mcci-catena). We are very busy, so we can't promise to help; but we'll do our best. @@ -114,4 +124,10 @@ Thanks to Amy Chen of Asolar, Josh Yu, and to Tzu-Chih University for funding th Further thanks to [Adafruit](https://www.adafruit.com/) for the wonderful Feather M0 LoRa platform, to [The Things Network](https://www.thethingsnetwork.org) for the LoRaWAN-based infrastructure, to [The Things Network New York](https://thethings.nyc) and [TTN Ithaca](https://ttni.tech) for the inspiration and support, and to the myriad people who have contributed to the Arduino and LoRaWAN infrastructure. -**MCCI** and **Catena** are registered trademarks of MCCI Corporation. \ No newline at end of file +MCCI and MCCI Catena are registered trademarks of MCCI Corporation. + +LoRa is a registered trademark of Semtech Corporation. + +LoRaWAN is a trademark of the LoRa Alliance. + +All other trademarks are the property of their respective owners. \ No newline at end of file diff --git a/catena4450m101_sensor/git-boot.sh b/catena4450m101_sensor/git-boot.sh index cffce77..03521ed 100755 --- a/catena4450m101_sensor/git-boot.sh +++ b/catena4450m101_sensor/git-boot.sh @@ -5,10 +5,10 @@ # Module: gitboot.sh # # Function: -# Load the repositories for building this sketch +# Install the libraries needed for building a given sketch. # # Copyright notice: -# This file copyright (C) 2017 by +# This file copyright (C) 2017-2018 by # # MCCI Corporation # 3520 Krums Corners Road @@ -72,26 +72,12 @@ else exit 1 fi -############################################################################## -# load the list of repos -############################################################################## - -### use a long quoted string to get the repositories -### into LIBRARY_REPOS. Multiple lines for readabilty. -LIBRARY_REPOS_DAT="${PDIR}/git-repos.dat" -if [ ! -f "${LIBRARY_REPOS_DAT}" ]; then - _fatal "can't find suitable git-repos.dat:" "${LIBRARY_REPOS_DAT}" -fi - -# parse the repo file, deleting comments -LIBRARY_REPOS=$(sed -e 's/#.*$//' ${PDIR}/git-repos.dat) - ############################################################################## # Scan the options ############################################################################## LIBRARY_ROOT="${LIBRARY_ROOT_DEFAULT}" -USAGE="${PNAME} -[D l* T u v]; ${PNAME} -H for help" +USAGE="${PNAME} -[D l* T u v] [datafile...]; ${PNAME} -H for help" #OPTDEBUG and OPTVERBOS are above OPTDRYRUN=0 @@ -130,8 +116,8 @@ Switches: Default is $LIBRARY_ROOT_DEFAULT. -S Skip repos that already exist; -nS means don't run if any repo already exist. - Only consulted if -nu. - -T Do a trial run (go through the motions + Only consulted if -nu. + -T Do a trial run (go through the motions but don't do anything). -u Do a git pull if repo already is found. -nu just skips the repository if it already @@ -139,6 +125,11 @@ Switches: -v turns on verbose mode; -nv is the default. -H prints this help message. +Data files: + The arguments specify repositories to be fetched, one repository + per line, in the form https://github.com/orgname/repo.git (or + similar). Blank lines and comments beginning with '#' are ignored. + . exit 0;; \?) echo "$USAGE" 1>&2 @@ -149,10 +140,31 @@ done #### get rid of scanned options #### shift `expr $OPTIND - 1` -if [ $# -ne 0 ]; then - _error "extra arguments: $@" +if [ $# -eq 0 ]; then + if [ -f "${PWD}/git-repos.dat" ]; then + LIBRARY_REPOS_DAT="${PWD}/git-repos.dat" + else + LIBRARY_REPOS_DAT="${PDIR}/git-repos.dat" + fi + _verbose "setting LIBRARY_REPOS_DAT to ${LIBRARY_REPOS_DAT}" + set -- "${LIBRARY_REPOS_DAT}" fi +############################################################################## +# load the list of repos +############################################################################## + +### use a long quoted string to get the repositories +### into LIBRARY_REPOS. Multiple lines for readabilty. +for LIBRARY_REPOS_DAT in "$@" ; do + if [ ! -f "${LIBRARY_REPOS_DAT}" ]; then + _fatal "can't find git-repos data file:" "${LIBRARY_REPOS_DAT}" + fi +done + +# parse the repo file, deleting comments and eliminating duplicates +LIBRARY_REPOS=$(sed -e 's/#.*$//' "$@" | LC_ALL=C sort -u) + #### make sure LIBRARY_ROOT is really a directory if [ ! -d "$LIBRARY_ROOT" ]; then _fatal "LIBRARY_ROOT: Can't find Arduino libraries:" "$LIBRARY_ROOT" diff --git a/catena4460_aqi/README.md b/catena4460_aqi/README.md new file mode 100644 index 0000000..ed11eec --- /dev/null +++ b/catena4460_aqi/README.md @@ -0,0 +1,153 @@ +# Catena4460-aqi example + +This sketch captures a simple air-quality index, based on the gas resistance returned by the BME680 that resides in the MCCI Catena 4460. It then uses LoRaWAN technology to transmit the captured data over a suitable network, such as The Things Network. + + + +- [Description](#description) + - [Prerequisites](#prerequisites) + - [Air Quality Index](#air-quality-index) + - [Known Unknowns](#known-unknowns) + - [Published Data about Air Conductivity and Pollution](#published-data-about-air-conductivity-and-pollution) + - [Data formats](#data-formats) +- [Configuration](#configuration) + - [Unattended mode](#unattended-mode) + - [Test mode](#test-mode) + - [Platform GUID](#platform-guid) +- [Boilerplate and acknowledgements](#boilerplate-and-acknowledgements) + - [Trademarks](#trademarks) + + + +## Description + +The application is very simple. It sets up the measurements, then periodically measures and transmits a collection of readings: + +* Temperature. +* Pressure. +* Relative Humidity. +* "Air Quality Index". This is an MCCI concept, and is different than the (closed source) IAQ offered by Bosch. +* System health indications (boot count, battery voltage). + +You must provision the Catena as normal, and set up your application to understand the data format. + +### Prerequisites + +- Make sure you're running the MCCI Board Support +Package for the Catena 4460. See https://github.com/mcci-catena/arduino-boards for information on how to set up the Arduino IDE. +- Select Catena 4460 as the target board in the Arduino IDE. +- Use `git-boot.sh` (elsewhere in the Catena-Sketches repository) to fetch the required libraries, by using the command: + + ```shell + cd catena4460_aqi + {path}/git-boot.sh -u ./git-repos.dat + ``` + + We recommend the `-u` switch because this will ensure that libraries will be updated if already present on your system. However, this may not be what you want. Use `{path}/git-boot.sh -H` to get help on the various options. + +- Make sure you have at least v0.9.0 of the `Catena-Arduino-Platform`; otherwise the sketch will not compile. + +### Air Quality Index + +This sketch measures gas resistance and computes an air quality index. The method chosen is not based on the proprietary Bosch method, but we chose to use a scale from 0 to 500, as they did. + +The AQI is a number ranging from 0..500, based on logarithmic gas conductivity (1/r), scaled according to the possible outputs of the BME680. The minimum possible output of the algorithm in the public code is mapped to 500; and the maximum possible output is mapped to 0, using a linear transformation of log(1/r). No claims about this index are made in terms of how this maps to pollution. However, [public data](https://forums.pimoroni.com/t/bme680-observed-gas-ohms-readings/6608) suggests the following table (descriptive tags taken from reference): + + Quality | R (ohms) | AQI +:-----------:|:-----------:|:--------: +"good" | >360k | < 160 +"average" | 184k - 360k | 160 - 190 +"little bad" | 93k - 180k | 190 - 220 +"bad" | 48k - 93k | 220 - 250 +"worse" | 24k - 48k | 250 - 280 +"very bad" | 12.5k - 24k | 280 - 310 +"can't see the exit" | < 12.5k | > 310 + +#### Known Unknowns + +Bosch measures gas resistance, but does not publish how this gas resistance converts to ohm-meters (the reference units for resistivity); they also do not publish the *resistance* values for their reference gases. They only publish "IAQ values", but do not disclose the algorithms. This prevents direct comparison of BME680 measurements with industry standard measurement of atmospheric conductivity. + +We hope to be able to research the actual response of the BME680 so that we can make better use of the published data. + +#### Published Data about Air Conductivity and Pollution + +We reviewed several papers, of which the following seemed the most important: + +1. Pawar et al, [Effect of relative humidity and sea level pressure on electrical conductivity of air over Indian Ocean][Pawar2009], [[Pawar2009]]". + +2. Kamalsi et al, [The Electrical Conductivity as an Index of Air Pollution in the Atmosphere][Kamalsi2011], [[Kamalsi2011]] + +3. US Environmental Protection Agency, [EPA-450/3-82-019: Measurement of Volatile Organic Compunds - Supplement 1][EPA1982], July 1982 [[EPA1982]]. + +[[Pawar2009]] was peer-reviewed, [[Kamalsi2011]] was not, but the results looked consistent. [[EPA1982]] is a good overview of laboratory measurement techniques for VOCs and is useful background. + +[[Pawar2009]] shows that relative humidity inversely affects conductivity, but the remarks, references, and data show that this is non-linear, increasing sharply when the RH > 75%. In other words, if RH > 75%, we should expect to measure a higher resistance than we would otherwise see, and therefore the AQI should be biased upwards. The current calculation doesn't include this bias. The published effect on conductivity is modest, about 5x; this translates to about 60 points on our AQI, or two levels. In other words, we might want to fit some non-linear curve to RH, so that above 75% increases AQI, and below 75% decreases, but less strongly. On the other hand, Section 4.1 concludes that the effect of RH is less at higher pollution levels. We conclude that we don't have enough information to know how to accommodate RH; rather RH and AQI should be reported together (as is done by this sketch). + +They also mention that small ion mobility decreases with increases in relative humidity; they believe that this is a smaller effect in their data, but it might be + +[[Pawar2009]] also shows that there is some contribution from atmospheric pressure. It is well known that higher pressure reduces small ion mobility, so it's likely that a given value of conductance indicates more small ions if pressure is higher. Again, however, we + +[Pawar2009]: https://doi.org/10.1029/2007JD009716 "Effect of relative humidity and sea level pressure on electrical conductivity of air over Indian Ocean" +[Kamalsi2011]: https://mts.intechopen.com/books/advanced-air-pollution/the-electrical-conductivity-as-an-index-of-air-pollution-in-the-atmosphere "The Electrical Conductivity as an Index of Air Pollution in the Atmosphere" +[EPA1982]: https://nepis.epa.gov/Exe/ZyNET.exe/2000MHV5.txt?ZyActionD=ZyDocument&Client=EPA&Index=2011%20Thru%202015%7C1995%20Thru%201999%7C1981%20Thru%201985%7C2006%20Thru%202010%7C1991%20Thru%201994%7C1976%20Thru%201980%7C2000%20Thru%202005%7C1986%20Thru%201990%7CPrior%20to%201976%7CHardcopy%20Publications&Docs=&Query=450382019&Time=&EndTime=&SearchMethod=2&TocRestrict=n&Toc=&TocEntry=&QField=&QFieldYear=&QFieldMonth=&QFieldDay=&UseQField=&IntQFieldOp=0&ExtQFieldOp=0&XmlQuery=&File=D%3A%5CZYFILES%5CINDEX%20DATA%5C81THRU85%5CTXT%5C00000005%5C2000MHV5.txt&User=ANONYMOUS&Password=anonymous&SortMethod=h%7C-&MaximumDocuments=15&FuzzyDegree=0&ImageQuality=r85g16/r85g16/x150y150g16/i500&Display=hpfr&DefSeekPage=x&SearchBack=ZyActionL&Back=ZyActionS&BackDesc=Results%20page&MaximumPages=1&ZyEntry=1&SeekPage=x "Measurement of Volatile Organic Compunds - Supplement 1, search link for retrieval" + +### Data formats + +It transmits a reading using `Catena_TxBuffer.h` format 0x17. This is similar to format 0x14, except that bit 0x20 of the flag byte indicates that a two-byte `uflt16` number is present representing the AQI. The AQI is a number ranging from 0..500, based on logarithmic gas conductivity (1/r), scaled according to the possible outputs of the BME680. It is divided by 512 prior to transmission in order to get into the [0,1) range limitation of the `uflt16` representation. So in the Javascript decoder, we multiply by 512 to recover the original data. + +Refer to [Understanding Catena Data Format 0x14](https://github.com/mcci-catena/Catena-Sketches/blob/master/extra/catena-message-0x14-format.md) for background information. + +## Configuration + +Most configuration is done via USB. + +### Unattended mode + +The Catena library defines operating flags bit 0 (mask 0x00000001) as "unattended mode". If set, the device is supposed to use low-power sleep; if clear, the device is presumably connected to a USB host, and so doesn't go to low-power sleep (so that the USB interface continues to work). + +**Note**: early versions of this sketch omitted this check. Verify in the code that your + +To enable unattended mode during configuration: + +``` +system configure operatingflags 1 +``` + +To disable: + +``` +system configure operatingflags 1 +``` + +### Test mode + +This sketch honors the manufacturing-test bit of operating flags (bit 1, mask 0x00000002). If bit 1 is set, it will continually measure (and print results). This will interfere with LoRa operations in test mode. + +To enable manufacturing test mode during configuration + +``` +system configure operatingflags 2 +``` + +To disable: + +``` +system configure operatingflags 2 +``` + +### Platform GUID + +Please set the platform GUID to `3037D9BE-8EBE-4AE7-970E-91915A2484F8` during configuration: + +``` +system configure platformguid 3037D9BE-8EBE-4AE7-970E-91915A2484F8 +``` + +## Boilerplate and acknowledgements + +### Trademarks + +- MCCI and MCCI Catena are registered trademarks of MCCI Corporation. +- LoRa is a registered trademark of Semtech Corporation. +- LoRaWAN is a trademark of the LoRa Alliance +- All other trademarks are properties of their respective owners. diff --git a/catena4460_aqi/catena4460_aqi.ino b/catena4460_aqi/catena4460_aqi.ino new file mode 100644 index 0000000..ddcb839 --- /dev/null +++ b/catena4460_aqi/catena4460_aqi.ino @@ -0,0 +1,533 @@ +/* catena4450_aqi.ino Sat Mar 31 2018 21:06:03 tmm */ + +/* + +Module: catena4450_aqi.ino + +Function: + Code for the air-quality sensor with Catena 4460. + +Version: + V0.1.1 Sat Mar 31 2018 21:06:03 tmm Edit level 1 + +Copyright notice: + This file copyright (C) 2017-2018 by + + MCCI Corporation + 3520 Krums Corners Road + Ithaca, NY 14850 + + An unpublished work. All rights reserved. All rights reserved. The license + file accompanying this file outlines the license granted. + +Author: + Terry Moore, MCCI Corporation & Suzen Filke December 201u + +Revision history: + 0.1.1 Sat Mar 31 2018 21:06:03 tmm + Adaptation for AQI. + +*/ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/****************************************************************************\ +| +| Manifest constants & typedefs. +| +| This is strictly for private types and constants which will not +| be exported. +| +\****************************************************************************/ + +using namespace McciCatena; + +/* how long do we wait between measurements (in seconds) */ +enum { + // set this to interval between measurements, in seconds + // Actual time will be a little longer because have to + // add measurement and broadcast time. + CATCFG_T_CYCLE = 30, // every 30 seconds + }; + +/* Additional timing parameters */ +enum { + CATCFG_T_WARMUP = 1, + CATCFG_T_SETTLE = 5, + CATCFG_T_INTERVAL = CATCFG_T_CYCLE - (CATCFG_T_WARMUP + + CATCFG_T_SETTLE), + }; + +// forwards +static void settleDoneCb(osjob_t *pSendJob); +static void warmupDoneCb(osjob_t *pSendJob); +static void txFailedDoneCb(osjob_t *pSendJob); +static void sleepDoneCb(osjob_t *pSendJob); +static Arduino_LoRaWAN::SendBufferCbFn sendBufferDoneCb; +void fillBuffer(TxBuffer_t &b); +void startSendingUplink(void); + +/****************************************************************************\ +| +| Read-only data. +| +| If program is to be ROM-able, these must all be tagged read-only +| using the ROM storage class; they may be global. +| +\****************************************************************************/ + +static const char sVersion[] = "0.1.1"; + +/****************************************************************************\ +| +| VARIABLES: +| +| If program is to be ROM-able, these must be initialized +| using the BSS keyword. (This allows for compilers that require +| every variable to have an initializer.) Note that only those +| variables owned by this module should be declared here, using the BSS +| keyword; this allows for linkers that dislike multiple declarations +| of objects. +| +\****************************************************************************/ + +// globals +Catena gCatena; + +// +// the LoRaWAN backhaul. Note that we use the +// Catena version so it can provide hardware-specific +// information to the base class. +// +Catena::LoRaWAN gLoRaWAN; + +// +// the LED +// +StatusLed gLed (Catena::PIN_STATUS_LED); + +// the RTC instance, used for sleeping +CatenaRTC gRtc; + +// The temperature/humidity sensor +Adafruit_BME680 bme; // The default initalizer creates an I2C connection +bool fBme; + +// The LUX sensor +BH1750 bh1750; +bool fLux; + +// the LMIC job that's used to synchronize us with the LMIC code +static osjob_t sensorJob; +void sensorJob_cb(osjob_t *pJob); + +/* + +Name: setup() + +Function: + Arduino setup function. + +Definition: + void setup( + void + ); + +Description: + This function is called by the Arduino framework after + basic framework has been initialized. We initialize the sensors + that are present on the platform, set up the LoRaWAN connection, + and (ultimately) return to the framework, which then calls loop() + forever. + +Returns: + No explicit result. + +*/ + +void setup(void) + { + gCatena.begin(); + + gCatena.SafePrintf("Catena 4460 sensor1 V%s\n", sVersion); + + gLed.begin(); + gCatena.registerObject(&gLed); + + // set up the RTC object + gRtc.begin(); + + gCatena.SafePrintf("LoRaWAN init: "); + if (!gLoRaWAN.begin(&gCatena)) + { + gCatena.SafePrintf("failed\n"); + gCatena.registerObject(&gLoRaWAN); + } + else + { + gCatena.SafePrintf("OK\n"); + gCatena.registerObject(&gLoRaWAN); + } + + Catena::UniqueID_string_t CpuIDstring; + + gCatena.SafePrintf("CPU Unique ID: %s\n", + gCatena.GetUniqueIDstring(&CpuIDstring) + ); + + /* find the platform */ + const Catena::EUI64_buffer_t *pSysEUI = gCatena.GetSysEUI(); + + uint32_t flags; + const CATENA_PLATFORM * const pPlatform = gCatena.GetPlatform(); + + if (pPlatform) + { + gCatena.SafePrintf("EUI64: "); + for (unsigned i = 0; i < sizeof(pSysEUI->b); ++i) + { + gCatena.SafePrintf("%s%02x", i == 0 ? "" : "-", pSysEUI->b[i]); + } + gCatena.SafePrintf("\n"); + flags = gCatena.GetPlatformFlags(); + gCatena.SafePrintf( + "Platform Flags: %#010x\n", + flags + ); + gCatena.SafePrintf( + "Operating Flags: %#010x\n", + gCatena.GetOperatingFlags() + ); + } + else + { + gCatena.SafePrintf("**** no platform, check provisioning ****\n"); + flags = 0; + } + + + /* initialize the lux sensor */ + if (flags & CatenaSamd21::fHasLuxRohm) + { + bh1750.begin(); + fLux = true; + bh1750.configure(BH1750_CONTINUOUS_HIGH_RES_MODE_2); + } + else + { + gCatena.SafePrintf("No Lux sensor found: check wiring and platform\n"); + fLux = false; + } + + /* initialize the BME680 */ + if (flags & Catena::fHasBme680 && + /* ! bme.begin(BME680_ADDRESS, Adafruit_BME680::OPERATING_MODE::Sleep) */ + ! bme.begin()) // the Adafruit BME680 lib doesn't have the MCCI low-power hacks + { + gCatena.SafePrintf("No BME680 found: check wiring and platform\n"); + fBme = false; + } + else + { + fBme = true; + } + + /* is it modded? */ + uint32_t modnumber = gCatena.PlatformFlags_GetModNumber(flags); + + if (modnumber != 0) + { + gCatena.SafePrintf("Catena 4460-M%u\n", modnumber); + } + else + { + gCatena.SafePrintf("No mods detected\n"); + } + + /* now, we kick off things by sending our first message */ + gLed.Set(LedPattern::Joining); + + /* warm up the BME680 by discarding a measurement */ + if (fBme) + (void)bme.readTemperature(); + + /* trigger a join by sending the first packet */ + startSendingUplink(); + } + +// The Arduino loop routine -- in our case, we just drive the other loops. +// If we try to do too much, we can break the LMIC radio. So the work is +// done by outcalls scheduled from the LMIC os loop. +void loop() + { + gCatena.poll(); + if (gCatena.GetOperatingFlags() & + static_cast(gCatena.OPERATING_FLAGS::fManufacturingTest)) + { + TxBuffer_t b; + fillBuffer(b); + } + } + +static uint16_t f2uflt16( + float f + ) + { + if (f < 0) + return 0; + else if (f >= 1.0) + return 0xFFFF; + else + { + int iExp; + float normalValue; + normalValue = frexpf(f, &iExp); + + // f is supposed to be in [0..1), so useful exp + // is [0..-15] + iExp += 15; + if (iExp < 0) + iExp = 0; + if (iExp > 15) + return 0xFFFF; + + + return (uint16_t)((iExp << 12u) + (unsigned) scalbnf(normalValue, 12)); + } + } + +void fillBuffer(TxBuffer_t &b) +{ + + b.begin(); + FlagsSensor5 flag; + + flag = FlagsSensor5(0); + + b.put(FormatSensor5); /* the flag for this record format */ + + /* capture the address of the flag byte */ + uint8_t * const pFlag = b.getp(); + + b.put(0x00); /* insert a byte. Value doesn't matter, will + || later be set to the final value of `flag` + */ + + // vBat is sent as 5000 * v + float vBat = gCatena.ReadVbat(); + gCatena.SafePrintf("vBat: %d mV\n", (int) (vBat * 1000.0f)); + b.putV(vBat); + flag |= FlagsSensor5::FlagVbat; + + uint32_t bootCount; + if (gCatena.getBootCount(bootCount)) + { + b.putBootCountLsb(bootCount); + flag |= FlagsSensor5::FlagBoot; + } + + if (fBme) + { + // bme.performReading() loads a consistent measurement into + // the bme. + // + // temperature is 2 bytes from -0x80.00 to +0x7F.FF degrees C + // pressure is 2 bytes, hPa * 10. + // humidity is one byte, where 0 == 0/256 and 0xFF == 255/256. + bme.performReading(); + gCatena.SafePrintf( + "BME680: T=%d P=%d RH=%d\n", + (int) (bme.temperature + 0.5), + (int) (bme.pressure + 0.5), + (int) (bme.humidity + 0.5) + ); + b.putT(bme.temperature); + b.putP(bme.pressure); + b.putRH(bme.humidity); + + flag |= FlagsSensor5::FlagTPH; + } + + if (fLux) + { + /* Get a new sensor event */ + uint16_t light; + + light = bh1750.readLightLevel(); + gCatena.SafePrintf("BH1750: %u lux\n", light); + b.putLux(light); + flag |= FlagsSensor5::FlagLux; + } + + if (fBme) + { + // compute the AQI (in 0..500/512). + // see README.md for a description. + constexpr float slope = 44.52282f / 512.0f; // constexpr moves calculation to compile-time + float const AQIsimple = (-logf(bme.gas_resistance) + 16.3787f) * slope; + + uint16_t const encodedAQI = f2uflt16(AQIsimple); + + gCatena.SafePrintf( + "BME680: Gas-Resistance=%d AQI=%d\n", + (int) (bme.gas_resistance + 0.5), + (int) (AQIsimple * 512.0 + 0.5) + ); + + b.put2u(encodedAQI); + flag |= FlagsSensor5::FlagAqi; + } + + *pFlag = uint8_t(flag); +} + + +void startSendingUplink(void) +{ + TxBuffer_t b; + LedPattern savedLed = gLed.Set(LedPattern::Measuring); + + fillBuffer(b); + + if (savedLed != LedPattern::Joining) + gLed.Set(LedPattern::Sending); + else + gLed.Set(LedPattern::Joining); + + gLoRaWAN.SendBuffer(b.getbase(), b.getn(), sendBufferDoneCb, NULL); +} + +static void +sendBufferDoneCb( + void *pContext, + bool fStatus + ) + { + osjobcb_t pFn; + + gLed.Set(LedPattern::Settling); + if (! fStatus) + { + gCatena.SafePrintf("send buffer failed\n"); + pFn = txFailedDoneCb; + } + else + { + pFn = settleDoneCb; + } + os_setTimedCallback( + &sensorJob, + os_getTime()+sec2osticks(CATCFG_T_SETTLE), + pFn + ); + } + +static void +txFailedDoneCb( + osjob_t *pSendJob + ) + { + gCatena.SafePrintf("not provisioned, idling\n"); + gLoRaWAN.Shutdown(); + gLed.Set(LedPattern::NotProvisioned); + } + + +// +// the following API is added to delay.c, .h in the BSP. It adjust millis() +// forward after a deep sleep. +// +// extern "C" { void adjust_millis_forward(unsigned); }; +// +// If you don't have it, make sure you're running the MCCI Board Support +// Package for the Catena 4460, https://github.com/mcci-catena/arduino-boards +// + +static void settleDoneCb( + osjob_t *pSendJob + ) + { + uint32_t startTime; + bool fDeepSleep; + + // if connected to USB, don't sleep + // ditto if we're monitoring pulses. + + // disable sleep if not unattended, or if USB active + if ((gCatena.GetOperatingFlags() & + static_cast(gCatena.OPERATING_FLAGS::fUnattended)) != 0) + fDeepSleep = true; + else if (! Serial.dtr()) + fDeepSleep = true; + else + fDeepSleep = false; + + /* if we can't sleep deeply, then simply schedule the sleepDoneCb */ + if (! fDeepSleep) + { + gLed.Set(LedPattern::Sleeping); + os_setTimedCallback( + &sensorJob, + os_getTime() + sec2osticks(CATCFG_T_INTERVAL), + sleepDoneCb + ); + return; + } + + /* + || ok... now it's time for a deep sleep. do the sleep here, since + || the Arduino loop won't do it. Note that nothing will get polled + || while we sleep + */ + gLed.Set(LedPattern::Off); + + startTime = millis(); + gRtc.SetAlarm(CATCFG_T_INTERVAL); + gRtc.SleepForAlarm( + gRtc.MATCH_HHMMSS, + gRtc.SleepMode::IdleCpuAhbApb + ); + + // add the number of ms that we were asleep to the millisecond timer. + // we don't need extreme accuracy. + adjust_millis_forward(CATCFG_T_INTERVAL * 1000); + + /* and now... we're fully awake. trigger another measurement */ + sleepDoneCb(pSendJob); + } + +static void sleepDoneCb( + osjob_t *pJob + ) + { + gLed.Set(LedPattern::WarmingUp); + + os_setTimedCallback( + &sensorJob, + os_getTime() + sec2osticks(CATCFG_T_WARMUP), + warmupDoneCb + ); + } + +static void warmupDoneCb( + osjob_t *pJob + ) + { + startSendingUplink(); + } \ No newline at end of file diff --git a/catena4460_aqi/git-repos.dat b/catena4460_aqi/git-repos.dat new file mode 100644 index 0000000..1815656 --- /dev/null +++ b/catena4460_aqi/git-repos.dat @@ -0,0 +1,13 @@ +# +# list of git repositories needed by this directory. +# Use git-boot.sh to be fetch them to the Arduino libraries directory. +# +https://github.com/mcci-catena/Adafruit_FRAM_I2C.git +https://github.com/mcci-catena/Catena-Arduino-Platform.git +https://github.com/mcci-catena/arduino-lorawan.git +https://github.com/mcci-catena/Catena-mcciadk.git +https://github.com/mcci-catena/arduino-lmic.git +https://github.com/mcci-catena/Adafruit_BME680.git +https://github.com/mcci-catena/Adafruit_Sensor.git +https://github.com/mcci-catena/RTCZero.git +https://github.com/mcci-catena/BH1750.git diff --git a/extra/catena-message-0x14-0x15-decoder-ttn.js b/extra/catena-message-0x14-0x15-decoder-ttn.js deleted file mode 100644 index 37cfe2b..0000000 --- a/extra/catena-message-0x14-0x15-decoder-ttn.js +++ /dev/null @@ -1,358 +0,0 @@ -// This function decodes the records (port 1, format 0x11, 0x14, 0x15) -// sent by the MCCI Catena 4410/4450/4551 soil/water and power applications. -// For use with console.thethingsnetwork.org -// 2017-09-19 add dewpoints. -// 2017-12-13 fix commments, fix negative soil/water temp, add test vectors. -// 2017-12-15 add format 0x11. - -// 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; -} - -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 == 0x14) { - // decode Catena 4450 M101 data - - // test vectors: - // 14 01 18 00 ==> vBat = 1.5 - // 14 01 F8 00 ==> vBat = -0.5 - // 14 05 F8 00 42 ==> boot: 66, vBat: -0.5 - // 14 0D F8 00 42 17 80 59 35 80 ==> adds one temp of 23.5, rh = 50, p = 913.48 - - // 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]; - 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 vRawBus = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - if (vRawBus & 0x8000) - vRawBus += -0x10000; - decoded.vBus = vRawBus / 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.error = "none"; - decoded.p = pRaw * 4 / 100.0; - decoded.rh = hRaw / 256 * 100; - decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); - } - - if (flags & 0x10) { - // we have lux - var luxRaw = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - decoded.lux = luxRaw; - } - - if (flags & 0x20) { - // watthour - var powerIn = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - var powerOut = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - decoded.powerUsedCount = powerIn; - decoded.powerSourcedCount = powerOut; - } - - if (flags & 0x40) { - // normalize floating pulses per hour - var floatIn = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - var floatOut = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - - var exp1 = floatIn >> 12; - var exp2 = floatOut >> 12; - var mant1 = (floatIn & 0xFFF) / 4096.0; - var mant2 = (floatOut & 0xFFF) / 4096.0; - var powerPerHourIn = mant1 * Math.pow(2, exp1 - 15) * 60 * 60 * 4; - var powerPerHourOut = mant2 * Math.pow(2, exp2 - 15) * 60 * 60 * 4; - decoded.powerUsedPerHour = powerPerHourIn; - decoded.powerSourcedPerHour = powerPerHourOut; - } - } else if (cmd == 0x15) { - // decode Catena 4450 M102 data - - // test vectors: - // 15 01 18 00 ==> vBat = 1.5 - // 15 01 F8 00 ==> vBat = -0.5 - // 15 05 F8 00 42 ==> boot: 66, vBat: -0.5 - // 15 0D F8 00 42 17 80 59 35 80 ==> adds one temp of 23.5, rh = 50, p = 913.48, tDewC = 12.5 - // 15 7D 44 60 0D 15 9D 5F CD C3 00 00 1C 11 14 46 E4 ==> - // { - // "boot": 13, - // "error": "none", - // "lux": 0, - // "p": 981, - // "rh": 76.171875, - // "rhSoil": 89.0625, - // "tDewC": 17.236466758309017, - // "tSoil": 20.2734375, - // "tSoilDew": 18.411840342527178, - // "tWater": 28.06640625, - // "tempC": 21.61328125, - // "vBat": 4.2734375, - // } - // 15 7D 43 72 07 17 A4 5F CB A7 01 DB 1C 01 16 AF C3 - // { - // "boot": 7, - // "error": "none", - // "lux": 475, - // "p": 980.92, - // "rh": 65.234375, - // "rhSoil": 76.171875, - // "tDewC": 16.732001483771757, - // "tSoil": 22.68359375, - // "tSoilDew": 18.271601276518467, - // "tWater": 28.00390625, - // "tempC": 23.640625, - // "vBat": 4.21533203125 - // } - // 15 7D 42 D4 21 F5 9B 5E 5F C1 00 00 01 C1 F9 1B EC - // { - // "boot": 33, - // "error": "none", - // "lux": 0, - // "p": 966.36, - // "rh": 75.390625, - // "rhSoil": 92.1875, - // "tDewC": -13.909882718758952, - // "tSoil": -6.89453125, - // "tSoilDew": -7.948780789914008, - // "tWater": 1.75390625, - // "tempC": -10.39453125, - // "vBat": 4.1767578125 - // } - // 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]; - 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 vRawBus = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - if (vRawBus & 0x8000) - vRawBus += -0x10000; - decoded.vBus = vRawBus / 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.error = "none"; - decoded.p = pRaw * 4 / 100.0; - decoded.rh = hRaw / 256 * 100; - decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); - } - - if (flags & 0x10) { - // we have lux - var luxRaw = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - decoded.lux = luxRaw; - } - - if (flags & 0x20) { - // onewire temperature - var tempRaw = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - if (tempRaw & 0x8000) - tempRaw = -0x10000 + tempRaw; - decoded.tWater = tempRaw / 256; - } - - if (flags & 0x40) { - // temperature followed by RH - var tempRaw = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - if (tempRaw & 0x8000) - tempRaw = -0x10000 + tempRaw; - var tempRH = bytes[i]; - i += 1; - decoded.tSoil = tempRaw / 256; - decoded.rhSoil = tempRH / 256 * 100; - decoded.tSoilDew = dewpoint(decoded.tSoil, decoded.rhSoil); - } - } else if (cmd == 0x11) { - // decode Catena 4410 sensor data - - // test vectors: - // 11 01 18 00 ==> vBat = 1.5 - // 11 01 F8 00 ==> vBat = -0.5 - // 11 05 F8 00 17 80 59 35 80 ==> adds one temp of 23.5, rh = 50, p = 913.48, tDewC = 12.5 - // 11 3D 44 60 15 9D 5F CD C3 00 00 1C 11 14 46 E4 ==> - // { - // "error": "none", - // "lux": 0, - // "p": 981, - // "rh": 76.171875, - // "rhSoil": 89.0625, - // "tDewC": 17.236466758309017, - // "tSoil": 20.2734375, - // "tSoilDew": 18.411840342527178, - // "tWater": 28.06640625, - // "tempC": 21.61328125, - // "vBat": 4.2734375, - // } - // 11 3D 43 72 17 A4 5F CB A7 01 DB 1C 01 16 AF C3 - // { - // "error": "none", - // "lux": 475, - // "p": 980.92, - // "rh": 65.234375, - // "rhSoil": 76.171875, - // "tDewC": 16.732001483771757, - // "tSoil": 22.68359375, - // "tSoilDew": 18.271601276518467, - // "tWater": 28.00390625, - // "tempC": 23.640625, - // "vBat": 4.21533203125 - // } - // 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]; - 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 vRawBus = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - if (vRawBus & 0x8000) - vRawBus += -0x10000; - decoded.vBus = vRawBus / 4096.0; - } - - if (flags & 0x4) { - // 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.error = "none"; - decoded.p = pRaw * 4 / 100.0; - decoded.rh = hRaw / 256 * 100; - decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); - } - - if (flags & 0x8) { - // we have lux - var luxRaw = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - decoded.lux = luxRaw; - } - - if (flags & 0x10) { - // onewire temperature - var tempRaw = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - decoded.tWater = tempRaw / 256; - } - - if (flags & 0x20) { - // temperature followed by RH - var tempRaw = (bytes[i] << 8) + bytes[i + 1]; - i += 2; - var tempRH = bytes[i]; - i += 1; - decoded.tSoil = tempRaw / 256; - decoded.rhSoil = tempRH / 256 * 100; - decoded.tSoilDew = dewpoint(decoded.tSoil, decoded.rhSoil); - } - } else { - // nothing - } - } - return decoded; -} \ No newline at end of file diff --git a/extra/catena-message-0x17-decoder-node-red.js b/extra/catena-message-0x17-decoder-node-red.js new file mode 100644 index 0000000..8cc4ea6 --- /dev/null +++ b/extra/catena-message-0x17-decoder-node-red.js @@ -0,0 +1,105 @@ +// JavaScript source code +// This Node-RED decoding function decodes the record sent by the Catena 4460 +// air-quality index application. + +var b = msg.payload; // pick up data for convenience; just saves typing. + +// an empty table to which we'll add result fields: +// +// result.vBat: the battery voltage (if present) +// result.vBus: the USB charger voltage (if provided) +// result.boot: the system boot counter, modulo 256 +// result.t: temperature in degrees C +// result.p: station pressure in hPa (millibars). Note that this is not +// adjusted for the height above sealevel so can't be directly compared +// to weather.gov "barometric pressure" +// result.rh: relative humidity (in %) +// result.lux: light level, in lux +// result.aqi: air quality index +var result = {}; + +// check the message type byte +if (b[0] != 0x17) { + // not one of ours: report an error, return without a value, + // so that Node-RED doesn't propagate the message any further. + node.error("not ours! " + b[0].toString()); + return; +} + +// i is used as the index into the message. Start with the flag byte. +var i = 1; +// fetch the bitmap. +var flags = b[i++]; + +if (flags & 0x1) { + // set vRaw to a uint16, and increment pointer + var vRaw = (b[i] << 8) + b[i + 1]; + i += 2; + // interpret uint16 as an int16 instead. + if (vRaw & 0x8000) + vRaw += -0x10000; + // scale and save in result. + result.vBat = vRaw / 4096.0; +} + +if (flags & 0x2) { + var vRaw = (b[i] << 8) + b[i + 1]; + i += 2; + if (vRaw & 0x8000) + vRaw += -0x10000; + result.vBus = vRaw / 4096.0; +} + +if (flags & 0x4) { + var iBoot = b[i]; + i += 1; + result.boot = iBoot; +} + +if (flags & 0x8) { + // we have temp, pressure, RH + var tRaw = (b[i] << 8) + b[i + 1]; + if (tRaw & 0x8000) + tRaw = -0x10000 + tRaw; + i += 2; + var pRaw = (b[i] << 8) + b[i + 1]; + i += 2; + var hRaw = b[i++]; + + result.t = tRaw / 256; + result.p = pRaw * 4 / 100.0; + result.rh = hRaw / 256 * 100; +} + +if (flags & 0x10) { + // we have lux + var luxRaw = (b[i] << 8) + b[i + 1]; + i += 2; + result.lux = luxRaw; +} + + +if (flags & 0x20) // get the air-quality index +{ + var rawUflt16 = (b[i] << 8) + b[i + 1]; + i += 2; + + var exp1 = rawUflt16 >> 12; + var mant1 = (rawUflt16 & 0xFFF) / 4096.0; + var f_unscaled = mant1 * Math.pow(2, exp1 - 15); + var aqi = f_unscaled * 512; + result.aqi = aqi; +} + +// now update msg with the new payload and new .local field +// the old msg.payload is overwritten. +msg.payload = result; +msg.local = + { + nodeType: "Catena 4460", + platformType: "Feather M0 LoRa", + radioType: "RF95", + applicationName: "Air Quality monitoring" + }; + +return msg; diff --git a/extra/catena-message-generic-decoder-ttn.js b/extra/catena-message-generic-decoder-ttn.js new file mode 100644 index 0000000..7dc8b7f --- /dev/null +++ b/extra/catena-message-generic-decoder-ttn.js @@ -0,0 +1,459 @@ +// This function decodes the records (port 1, format 0x11, 0x14, 0x15) +// sent by the MCCI Catena 4410/4450/4551 soil/water and power applications. +// For use with console.thethingsnetwork.org +// 2017-09-19 add dewpoints. +// 2017-12-13 fix commments, fix negative soil/water temp, add test vectors. +// 2017-12-15 add format 0x11. +// 2018-04-01 add format 0x17. + +// 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; +} + +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 == 0x14) { + // decode Catena 4450 M101 data + + // test vectors: + // 14 01 18 00 ==> vBat = 1.5 + // 14 01 F8 00 ==> vBat = -0.5 + // 14 05 F8 00 42 ==> boot: 66, vBat: -0.5 + // 14 0D F8 00 42 17 80 59 35 80 ==> adds one temp of 23.5, rh = 50, p = 913.48 + + // 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]; + 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 vRawBus = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (vRawBus & 0x8000) + vRawBus += -0x10000; + decoded.vBus = vRawBus / 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.error = "none"; + decoded.p = pRaw * 4 / 100.0; + decoded.rh = hRaw / 256 * 100; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + } + + if (flags & 0x10) { + // we have lux + var luxRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + decoded.lux = luxRaw; + } + + if (flags & 0x20) { + // watthour + var powerIn = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + var powerOut = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + decoded.powerUsedCount = powerIn; + decoded.powerSourcedCount = powerOut; + } + + if (flags & 0x40) { + // normalize floating pulses per hour + var floatIn = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + var floatOut = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + + var exp1 = floatIn >> 12; + var exp2 = floatOut >> 12; + var mant1 = (floatIn & 0xFFF) / 4096.0; + var mant2 = (floatOut & 0xFFF) / 4096.0; + var powerPerHourIn = mant1 * Math.pow(2, exp1 - 15) * 60 * 60 * 4; + var powerPerHourOut = mant2 * Math.pow(2, exp2 - 15) * 60 * 60 * 4; + decoded.powerUsedPerHour = powerPerHourIn; + decoded.powerSourcedPerHour = powerPerHourOut; + } + } else if (cmd == 0x15) { + // decode Catena 4450 M102 data + + // test vectors: + // 15 01 18 00 ==> vBat = 1.5 + // 15 01 F8 00 ==> vBat = -0.5 + // 15 05 F8 00 42 ==> boot: 66, vBat: -0.5 + // 15 0D F8 00 42 17 80 59 35 80 ==> adds one temp of 23.5, rh = 50, p = 913.48, tDewC = 12.5 + // 15 7D 44 60 0D 15 9D 5F CD C3 00 00 1C 11 14 46 E4 ==> + // { + // "boot": 13, + // "error": "none", + // "lux": 0, + // "p": 981, + // "rh": 76.171875, + // "rhSoil": 89.0625, + // "tDewC": 17.236466758309017, + // "tSoil": 20.2734375, + // "tSoilDew": 18.411840342527178, + // "tWater": 28.06640625, + // "tempC": 21.61328125, + // "vBat": 4.2734375, + // } + // 15 7D 43 72 07 17 A4 5F CB A7 01 DB 1C 01 16 AF C3 + // { + // "boot": 7, + // "error": "none", + // "lux": 475, + // "p": 980.92, + // "rh": 65.234375, + // "rhSoil": 76.171875, + // "tDewC": 16.732001483771757, + // "tSoil": 22.68359375, + // "tSoilDew": 18.271601276518467, + // "tWater": 28.00390625, + // "tempC": 23.640625, + // "vBat": 4.21533203125 + // } + // 15 7D 42 D4 21 F5 9B 5E 5F C1 00 00 01 C1 F9 1B EC + // { + // "boot": 33, + // "error": "none", + // "lux": 0, + // "p": 966.36, + // "rh": 75.390625, + // "rhSoil": 92.1875, + // "tDewC": -13.909882718758952, + // "tSoil": -6.89453125, + // "tSoilDew": -7.948780789914008, + // "tWater": 1.75390625, + // "tempC": -10.39453125, + // "vBat": 4.1767578125 + // } + // 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]; + 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 vRawBus = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (vRawBus & 0x8000) + vRawBus += -0x10000; + decoded.vBus = vRawBus / 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.error = "none"; + decoded.p = pRaw * 4 / 100.0; + decoded.rh = hRaw / 256 * 100; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + } + + if (flags & 0x10) { + // we have lux + var luxRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + decoded.lux = luxRaw; + } + + if (flags & 0x20) { + // onewire temperature + var tempRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (tempRaw & 0x8000) + tempRaw = -0x10000 + tempRaw; + decoded.tWater = tempRaw / 256; + } + + if (flags & 0x40) { + // temperature followed by RH + var tempRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (tempRaw & 0x8000) + tempRaw = -0x10000 + tempRaw; + var tempRH = bytes[i]; + i += 1; + decoded.tSoil = tempRaw / 256; + decoded.rhSoil = tempRH / 256 * 100; + decoded.tSoilDew = dewpoint(decoded.tSoil, decoded.rhSoil); + } + } else if (cmd == 0x11) { + // decode Catena 4410 sensor data + + // test vectors: + // 11 01 18 00 ==> vBat = 1.5 + // 11 01 F8 00 ==> vBat = -0.5 + // 11 05 F8 00 17 80 59 35 80 ==> adds one temp of 23.5, rh = 50, p = 913.48, tDewC = 12.5 + // 11 3D 44 60 15 9D 5F CD C3 00 00 1C 11 14 46 E4 ==> + // { + // "error": "none", + // "lux": 0, + // "p": 981, + // "rh": 76.171875, + // "rhSoil": 89.0625, + // "tDewC": 17.236466758309017, + // "tSoil": 20.2734375, + // "tSoilDew": 18.411840342527178, + // "tWater": 28.06640625, + // "tempC": 21.61328125, + // "vBat": 4.2734375, + // } + // 11 3D 43 72 17 A4 5F CB A7 01 DB 1C 01 16 AF C3 + // { + // "error": "none", + // "lux": 475, + // "p": 980.92, + // "rh": 65.234375, + // "rhSoil": 76.171875, + // "tDewC": 16.732001483771757, + // "tSoil": 22.68359375, + // "tSoilDew": 18.271601276518467, + // "tWater": 28.00390625, + // "tempC": 23.640625, + // "vBat": 4.21533203125 + // } + // 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]; + 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 vRawBus = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (vRawBus & 0x8000) + vRawBus += -0x10000; + decoded.vBus = vRawBus / 4096.0; + } + + if (flags & 0x4) { + // 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.error = "none"; + decoded.p = pRaw * 4 / 100.0; + decoded.rh = hRaw / 256 * 100; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + } + + if (flags & 0x8) { + // we have lux + var luxRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + decoded.lux = luxRaw; + } + + if (flags & 0x10) { + // onewire temperature + var tempRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + decoded.tWater = tempRaw / 256; + } + + if (flags & 0x20) { + // temperature followed by RH + var tempRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + var tempRH = bytes[i]; + i += 1; + decoded.tSoil = tempRaw / 256; + decoded.rhSoil = tempRH / 256 * 100; + decoded.tSoilDew = dewpoint(decoded.tSoil, decoded.rhSoil); + } + } else if (cmd == 0x17) { + // decode Catena 4460 AQI data + + // test vectors: + // 17 01 18 00 ==> vBat = 1.5 + // 17 01 F8 00 ==> vBat = -0.5 + // 17 05 F8 00 42 ==> boot: 66, vBat: -0.5 + // 17 0D F8 00 42 17 80 59 35 80 ==> adds one temp of 23.5, rh = 50, p = 913.48, tDewC = 12.5 + // 17 3D 44 60 0D 15 9D 5F CD C3 00 00 F9 07 ==> + // { + // "aqi": 288.875 + // "boot": 13, + // "error": "none", + // "lux": 0, + // "p": 981, + // "rh": 76.171875, + // "tDewC": 17.236466758309017, + // "tempC": 21.61328125, + // "vBat": 4.2734375, + // } + // 17 3D 43 72 07 17 A4 5F CB A7 01 DB E9 AF ==> + // { + // "aqi": 154.9375, + // "boot": 7, + // "error": "none", + // "lux": 475, + // "p": 980.92, + // "rh": 65.234375, + // "tDewC": 16.732001483771757, + // "tempC": 23.640625, + // "vBat": 4.21533203125 + // } + + // 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]; + 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 vRawBus = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + if (vRawBus & 0x8000) + vRawBus += -0x10000; + decoded.vBus = vRawBus / 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.error = "none"; + decoded.p = pRaw * 4 / 100.0; + decoded.rh = hRaw / 256 * 100; + decoded.tDewC = dewpoint(decoded.tempC, decoded.rh); + } + + if (flags & 0x10) { + // we have lux + var luxRaw = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + decoded.lux = luxRaw; + } + + if (flags & 0x20) { + var rawUflt16 = (bytes[i] << 8) + bytes[i + 1]; + i += 2; + + var exp1 = rawUflt16 >> 12; + var mant1 = (rawUflt16 & 0xFFF) / 4096.0; + var f_unscaled = mant1 * Math.pow(2, exp1 - 15); + var aqi = f_unscaled * 512; + + decoded.aqi = aqi; + } + } else { + // cmd value not recognized. + } + } + + // at this point, decoded has the real values. + return decoded; +} \ No newline at end of file