commit c7430f56db8f0bdb6a40dfa3481507c5be5f2625 Author: Maurice Makaay Date: Thu Jan 7 11:57:11 2021 +0100 Initial import. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f2b65d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +src/config.h diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..e80666b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ] +} diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/Metriful/examples/Home_Assistant/Home_Assistant.ino b/lib/Metriful/examples/Home_Assistant/Home_Assistant.ino new file mode 100644 index 0000000..651dbfe --- /dev/null +++ b/lib/Metriful/examples/Home_Assistant/Home_Assistant.ino @@ -0,0 +1,218 @@ +/* + Home_Assistant.ino + + Example code for sending environment data from the Metriful MS430 to + an installation of Home Assistant on your local WiFi network. + For more information, visit www.home-assistant.io + + This example is designed for the following WiFi enabled hosts: + * Arduino Nano 33 IoT + * Arduino MKR WiFi 1010 + * ESP8266 boards (e.g. Wemos D1, NodeMCU) + * ESP32 boards (e.g. DOIT DevKit v1) + + Data are sent at regular intervals over your WiFi network to Home + Assistant and can be viewed on the dashboard or used to control + home automation tasks. More setup information is provided in the + Readme and User Guide. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// How often to read and report the data (every 3, 100 or 300 seconds) +uint8_t cycle_period = CYCLE_PERIOD_100_S; + +// The details of the WiFi network: +char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name) +char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password + +// Home Assistant settings + +// You must have already installed Home Assistant on a computer on your +// network. Go to www.home-assistant.io for help on this. + +// Choose a unique name for this MS430 sensor board so you can identify it. +// Variables in HA will have names like: SENSOR_NAME.temperature, etc. +#define SENSOR_NAME "kitchen3" + +// Change this to the IP address of the computer running Home Assistant. +// You can find this from the admin interface of your router. +#define HOME_ASSISTANT_IP "192.168.43.144" + +// Security access token: the Readme and User Guide explain how to get this +#define LONG_LIVED_ACCESS_TOKEN "PASTE YOUR TOKEN HERE WITHIN QUOTES" + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +#if !defined(HAS_WIFI) +#error ("This example program has been created for specific WiFi enabled hosts only.") +#endif + +WiFiClient client; + +// Buffers for assembling http POST requests +char postBuffer[450] = {0}; +char fieldBuffer[70] = {0}; + +// Structs for data +AirData_t airData = {0}; +AirQualityData_t airQualityData = {0}; +LightData_t lightData = {0}; +ParticleData_t particleData = {0}; +SoundData_t soundData = {0}; + +// Define the display attributes of data sent to Home Assistant. +// The chosen name, unit and icon will appear in on the overview +// dashboard in Home Assistant. The icons can be chosen from +// https://cdn.materialdesignicons.com/5.3.45/ +// (remove the "mdi-" part from the icon name). +// The attribute fields are: {name, unit, icon, decimal places} +HA_Attributes_t pressure = {"Pressure","Pa","weather-cloudy",0}; +HA_Attributes_t humidity = {"Humidity","%","water-percent",1}; +HA_Attributes_t illuminance = {"Illuminance","lx","white-balance-sunny",2}; +HA_Attributes_t soundLevel = {"Sound level","dBA","microphone",1}; +HA_Attributes_t peakAmplitude = {"Sound peak","mPa","waveform",2}; +HA_Attributes_t AQI = {"Air Quality Index"," ","thought-bubble-outline",1}; +HA_Attributes_t AQ_assessment = {"Air quality assessment","","flower-tulip",0}; +#if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42) + HA_Attributes_t particulates = {"Particle concentration","ppL","chart-bubble",0}; +#else + HA_Attributes_t particulates = {"Particle concentration",SDS011_UNIT_SYMBOL,"chart-bubble",2}; +#endif +#ifdef USE_FAHRENHEIT + HA_Attributes_t temperature = {"Temperature",FAHRENHEIT_SYMBOL,"thermometer",1}; +#else + HA_Attributes_t temperature = {"Temperature",CELSIUS_SYMBOL,"thermometer",1}; +#endif + + +void setup() { + // Initialize the host's pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + connectToWiFi(SSID, password); + + // Apply settings to the MS430 and enter cycle mode + uint8_t particleSensorCode = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensorCode, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); +} + + +void loop() { + + // Wait for the next new data release, indicated by a falling edge on READY + while (!ready_assertion_event) { + yield(); + } + ready_assertion_event = false; + + // Read data from the MS430 into the data structs. + ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + + // Check that WiFi is still connected + uint8_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + // There is a problem with the WiFi connection: attempt to reconnect. + Serial.print("Wifi status: "); + Serial.println(interpret_WiFi_status(wifiStatus)); + connectToWiFi(SSID, password); + ready_assertion_event = false; + } + + uint8_t T_intPart = 0; + uint8_t T_fractionalPart = 0; + bool isPositive = true; + getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); + + // Send data to Home Assistant + sendNumericData(&temperature, (uint32_t) T_intPart, T_fractionalPart, isPositive); + sendNumericData(&pressure, (uint32_t) airData.P_Pa, 0, true); + sendNumericData(&humidity, (uint32_t) airData.H_pc_int, airData.H_pc_fr_1dp, true); + sendNumericData(&illuminance, (uint32_t) lightData.illum_lux_int, lightData.illum_lux_fr_2dp, true); + sendNumericData(&soundLevel, (uint32_t) soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp, true); + sendNumericData(&peakAmplitude, (uint32_t) soundData.peak_amp_mPa_int, + soundData.peak_amp_mPa_fr_2dp, true); + sendNumericData(&AQI, (uint32_t) airQualityData.AQI_int, airQualityData.AQI_fr_1dp, true); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + sendNumericData(&particulates, (uint32_t) particleData.concentration_int, + particleData.concentration_fr_2dp, true); + } + sendTextData(&AQ_assessment, interpret_AQI_value(airQualityData.AQI_int)); +} + +// Send numeric data with specified sign, integer and fractional parts +void sendNumericData(const HA_Attributes_t * attributes, uint32_t valueInteger, + uint8_t valueDecimal, bool isPositive) { + char valueText[20] = {0}; + const char * sign = isPositive ? "" : "-"; + switch (attributes->decimalPlaces) { + case 0: + default: + sprintf(valueText,"%s%" PRIu32, sign, valueInteger); + break; + case 1: + sprintf(valueText,"%s%" PRIu32 ".%u", sign, valueInteger, valueDecimal); + break; + case 2: + sprintf(valueText,"%s%" PRIu32 ".%02u", sign, valueInteger, valueDecimal); + break; + } + http_POST_Home_Assistant(attributes, valueText); +} + +// Send a text string: must have quotation marks added +void sendTextData(const HA_Attributes_t * attributes, const char * valueText) { + char quotedText[20] = {0}; + sprintf(quotedText,"\"%s\"", valueText); + http_POST_Home_Assistant(attributes, quotedText); +} + +// Send the data to Home Assistant as an HTTP POST request. +void http_POST_Home_Assistant(const HA_Attributes_t * attributes, const char * valueText) { + client.stop(); + if (client.connect(HOME_ASSISTANT_IP, 8123)) { + // Form the URL from the name but replace spaces with underscores + strcpy(fieldBuffer,attributes->name); + for (uint8_t i=0; iunit, attributes->name, attributes->icon); + + sprintf(fieldBuffer,"Content-Length: %u", strlen(postBuffer)); + client.println(fieldBuffer); + client.println(); + client.print(postBuffer); + } + else { + Serial.println("Client connection failed."); + } +} diff --git a/lib/Metriful/examples/IFTTT/IFTTT.ino b/lib/Metriful/examples/IFTTT/IFTTT.ino new file mode 100644 index 0000000..ff27276 --- /dev/null +++ b/lib/Metriful/examples/IFTTT/IFTTT.ino @@ -0,0 +1,190 @@ +/* + IFTTT.ino + + Example code for sending data from the Metriful MS430 to IFTTT.com + + This example is designed for the following WiFi enabled hosts: + * Arduino Nano 33 IoT + * Arduino MKR WiFi 1010 + * ESP8266 boards (e.g. Wemos D1, NodeMCU) + * ESP32 boards (e.g. DOIT DevKit v1) + + Environmental data values are periodically measured and compared with + a set of user-defined thresholds. If any values go outside the allowed + ranges, an HTTP POST request is sent to IFTTT.com, triggering an alert + email to your inbox, with customizable text. + This example requires a WiFi network and internet connection. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// The details of the WiFi network: +char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name) +char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password + +// Define the details of variables for monitoring. +// The seven fields are: +// {Name, measurement unit, high threshold, low threshold, +// initial inactive cycles (2), advice when high, advice when low} +ThresholdSetting_t humiditySetting = {"humidity","%",60,30,2, + "Reduce moisture sources.","Start the humidifier."}; +ThresholdSetting_t airQualitySetting = {"air quality index","",250,-1,2, + "Improve ventilation and reduce sources of VOCs.",""}; +// Change these values if Fahrenheit output temperature is selected in Metriful_sensor.h +ThresholdSetting_t temperatureSetting = {"temperature",CELSIUS_SYMBOL,24,18,2, + "Turn on the fan.","Turn on the heating."}; + +// An inactive period follows each alert, during which the same alert +// will not be generated again - this prevents too many emails/alerts. +// Choose the period as a number of readout cycles (each 5 minutes) +// e.g. for a 2 hour period, choose inactiveWaitCycles = 24 +uint16_t inactiveWaitCycles = 24; + +// IFTTT.com settings + +// You must set up a free account on IFTTT.com and create a Webhooks +// applet before using this example. This is explained further in the +// instructions in the GitHub Readme, or in the User Guide. + +#define WEBHOOKS_KEY "PASTE YOUR KEY HERE WITHIN QUOTES" +#define IFTTT_EVENT_NAME "PASTE YOUR EVENT NAME HERE WITHIN QUOTES" + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +#if !defined(HAS_WIFI) +#error ("This example program has been created for specific WiFi enabled hosts only.") +#endif + +// Measure the environment data every 300 seconds (5 minutes). This is +// adequate for long-term monitoring. +uint8_t cycle_period = CYCLE_PERIOD_300_S; + +WiFiClient client; + +// Buffers for assembling the http POST requests +char postBuffer[400] = {0}; +char fieldBuffer[120] = {0}; + +// Structs for data +AirData_t airData = {0}; +AirQualityData_t airQualityData = {0}; + + +void setup() { + // Initialize the host's pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + connectToWiFi(SSID, password); + + // Enter cycle mode + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); +} + + +void loop() { + + // Wait for the next new data release, indicated by a falling edge on READY + while (!ready_assertion_event) { + yield(); + } + ready_assertion_event = false; + + // Read the air data and air quality data + ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + + // Check that WiFi is still connected + uint8_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + // There is a problem with the WiFi connection: attempt to reconnect. + Serial.print("Wifi status: "); + Serial.println(interpret_WiFi_status(wifiStatus)); + connectToWiFi(SSID, password); + ready_assertion_event = false; + } + + // Process temperature value and convert if using Fahrenheit + float temperature = convertEncodedTemperatureToFloat(airData.T_C_int_with_sign, airData.T_C_fr_1dp); + #ifdef USE_FAHRENHEIT + temperature = convertCtoF(temperature); + #endif + + // Send an alert to IFTTT if a variable is outside the allowed range + // Just use the integer parts of values (ignore fractional parts) + checkData(&temperatureSetting, (int32_t) temperature); + checkData(&humiditySetting, (int32_t) airData.H_pc_int); + checkData(&airQualitySetting, (int32_t) airQualityData.AQI_int); +} + + +// Compare the measured value to the chosen thresholds and create an +// alert if the value is outside the allowed range. After triggering +// an alert, it cannot be re-triggered within the chosen number of cycles. +void checkData(ThresholdSetting_t * setting, int32_t value) { + + // Count down to when the monitoring is active again: + if (setting->inactiveCount > 0) { + setting->inactiveCount--; + } + + if ((value > setting->thresHigh) && (setting->inactiveCount == 0)) { + // The variable is above the high threshold + setting->inactiveCount = inactiveWaitCycles; + sendAlert(setting, value, true); + } + else if ((value < setting->thresLow) && (setting->inactiveCount == 0)) { + // The variable is below the low threshold + setting->inactiveCount = inactiveWaitCycles; + sendAlert(setting, value, false); + } +} + + +// Send an alert message to IFTTT.com as an HTTP POST request. +// isOverHighThres = true means (value > thresHigh) +// isOverHighThres = false means (value < thresLow) +void sendAlert(ThresholdSetting_t * setting, int32_t value, bool isOverHighThres) { + client.stop(); + if (client.connect("maker.ifttt.com", 80)) { + client.println("POST /trigger/" IFTTT_EVENT_NAME "/with/key/" WEBHOOKS_KEY " HTTP/1.1"); + client.println("Host: maker.ifttt.com"); + client.println("Content-Type: application/json"); + + sprintf(fieldBuffer,"The %s is too %s.", setting->variableName, + isOverHighThres ? "high" : "low"); + Serial.print("Sending new alert to IFTTT: "); + Serial.println(fieldBuffer); + + sprintf(postBuffer,"{\"value1\":\"%s\",", fieldBuffer); + + sprintf(fieldBuffer,"\"value2\":\"The measurement was %" PRId32 " %s\"", + value, setting->measurementUnit); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,",\"value3\":\"%s\"}", + isOverHighThres ? setting->adviceHigh : setting->adviceLow); + strcat(postBuffer, fieldBuffer); + + size_t len = strlen(postBuffer); + sprintf(fieldBuffer,"Content-Length: %u",len); + client.println(fieldBuffer); + client.println(); + client.print(postBuffer); + } + else { + Serial.println("Client connection failed."); + } +} diff --git a/lib/Metriful/examples/IoT_cloud_logging/IoT_cloud_logging.ino b/lib/Metriful/examples/IoT_cloud_logging/IoT_cloud_logging.ino new file mode 100644 index 0000000..60ecd79 --- /dev/null +++ b/lib/Metriful/examples/IoT_cloud_logging/IoT_cloud_logging.ino @@ -0,0 +1,284 @@ +/* + IoT_cloud_logging.ino + + Example IoT data logging code for the Metriful MS430. + + This example is designed for the following WiFi enabled hosts: + * Arduino Nano 33 IoT + * Arduino MKR WiFi 1010 + * ESP8266 boards (e.g. Wemos D1, NodeMCU) + * ESP32 boards (e.g. DOIT DevKit v1) + + Environmental data values are measured and logged to an internet + cloud account every 100 seconds, using a WiFi network. The example + gives the choice of using either the Tago.io or Thingspeak.com + clouds – both of these offer a free account for low data rates. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// How often to read and log data (every 100 or 300 seconds) +// Note: due to data rate limits on free cloud services, this should +// be set to 100 or 300 seconds, not 3 seconds. +uint8_t cycle_period = CYCLE_PERIOD_100_S; + +// The details of the WiFi network: +char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name) +char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password + +// IoT cloud settings +// This example uses the free IoT cloud hosting services provided +// by Tago.io or Thingspeak.com +// Other free cloud providers are available. +// An account must have been set up with the relevant cloud provider +// and a WiFi internet connection must exist. See the accompanying +// readme and User Guide for more information. + +// The chosen account's key/token must be put into the relevant define below. +#define TAGO_DEVICE_TOKEN_STRING "PASTE YOUR TOKEN HERE WITHIN QUOTES" +#define THINGSPEAK_API_KEY_STRING "PASTE YOUR API KEY HERE WITHIN QUOTES" + +// Choose which provider to use +bool useTagoCloud = true; +// To use the ThingSpeak cloud, set: useTagoCloud=false + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +#if !defined(HAS_WIFI) +#error ("This example program has been created for specific WiFi enabled hosts only.") +#endif + +WiFiClient client; + +// Buffers for assembling http POST requests +char postBuffer[450] = {0}; +char fieldBuffer[70] = {0}; + +// Structs for data +AirData_t airData = {0}; +AirQualityData_t airQualityData = {0}; +LightData_t lightData = {0}; +ParticleData_t particleData = {0}; +SoundData_t soundData = {0}; + +void setup() { + // Initialize the host's pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + connectToWiFi(SSID, password); + + // Apply chosen settings to the MS430 + uint8_t particleSensor = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); + + // Enter cycle mode + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); +} + + +void loop() { + + // Wait for the next new data release, indicated by a falling edge on READY + while (!ready_assertion_event) { + yield(); + } + ready_assertion_event = false; + + /* Read data from the MS430 into the data structs. + For each category of data (air, sound, etc.) a pointer to the data struct is + passed to the ReceiveI2C() function. The received byte sequence fills the + struct in the correct order so that each field within the struct receives + the value of an environmental quantity (temperature, sound level, etc.) + */ + + // Air data + // Choose output temperature unit (C or F) in Metriful_sensor.h + ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + + /* Air quality data + The initial self-calibration of the air quality data may take several + minutes to complete. During this time the accuracy parameter is zero + and the data values are not valid. + */ + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + + // Light data + ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + + // Sound data + ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + + /* Particle data + This requires the connection of a particulate sensor (invalid + values will be obtained if this sensor is not present). + Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h + Also note that, due to the low pass filtering used, the + particle data become valid after an initial initialization + period of approximately one minute. + */ + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + } + + // Check that WiFi is still connected + uint8_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + // There is a problem with the WiFi connection: attempt to reconnect. + Serial.print("Wifi status: "); + Serial.println(interpret_WiFi_status(wifiStatus)); + connectToWiFi(SSID, password); + ready_assertion_event = false; + } + + // Send data to the cloud + if (useTagoCloud) { + http_POST_data_Tago_cloud(); + } + else { + http_POST_data_Thingspeak_cloud(); + } +} + + +/* For both example cloud providers, the following quantities will be sent: +1 Temperature (C or F) +2 Pressure/Pa +3 Humidity/% +4 Air quality index +5 bVOC/ppm +6 SPL/dBA +7 Illuminance/lux +8 Particle concentration + + Additionally, for Tago, the following is sent: +9 Air Quality Assessment summary (Good, Bad, etc.) +10 Peak sound amplitude / mPa +*/ + +// Assemble the data into the required format, then send it to the +// Tago.io cloud as an HTTP POST request. +void http_POST_data_Tago_cloud(void) { + client.stop(); + if (client.connect("api.tago.io", 80)) { + client.println("POST /data HTTP/1.1"); + client.println("Host: api.tago.io"); + client.println("Content-Type: application/json"); + client.println("Device-Token: " TAGO_DEVICE_TOKEN_STRING); + + uint8_t T_intPart = 0; + uint8_t T_fractionalPart = 0; + bool isPositive = true; + getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); + sprintf(postBuffer,"[{\"variable\":\"temperature\",\"value\":%s%u.%u}", + isPositive?"":"-", T_intPart, T_fractionalPart); + + sprintf(fieldBuffer,",{\"variable\":\"pressure\",\"value\":%" PRIu32 "}", airData.P_Pa); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,",{\"variable\":\"humidity\",\"value\":%u.%u}", + airData.H_pc_int, airData.H_pc_fr_1dp); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,",{\"variable\":\"aqi\",\"value\":%u.%u}", + airQualityData.AQI_int, airQualityData.AQI_fr_1dp); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,",{\"variable\":\"aqi_string\",\"value\":\"%s\"}", + interpret_AQI_value(airQualityData.AQI_int)); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,",{\"variable\":\"bvoc\",\"value\":%u.%02u}", + airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,",{\"variable\":\"spl\",\"value\":%u.%u}", + soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,",{\"variable\":\"peak_amp\",\"value\":%u.%02u}", + soundData.peak_amp_mPa_int, soundData.peak_amp_mPa_fr_2dp); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,",{\"variable\":\"particulates\",\"value\":%u.%02u}", + particleData.concentration_int, particleData.concentration_fr_2dp); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,",{\"variable\":\"illuminance\",\"value\":%u.%02u}]", + lightData.illum_lux_int, lightData.illum_lux_fr_2dp); + strcat(postBuffer, fieldBuffer); + + size_t len = strlen(postBuffer); + sprintf(fieldBuffer,"Content-Length: %u",len); + client.println(fieldBuffer); + client.println(); + client.print(postBuffer); + } + else { + Serial.println("Client connection failed."); + } +} + + +// Assemble the data into the required format, then send it to the +// Thingspeak.com cloud as an HTTP POST request. +void http_POST_data_Thingspeak_cloud(void) { + client.stop(); + if (client.connect("api.thingspeak.com", 80)) { + client.println("POST /update HTTP/1.1"); + client.println("Host: api.thingspeak.com"); + client.println("Content-Type: application/x-www-form-urlencoded"); + + strcpy(postBuffer,"api_key=" THINGSPEAK_API_KEY_STRING); + + uint8_t T_intPart = 0; + uint8_t T_fractionalPart = 0; + bool isPositive = true; + getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); + sprintf(fieldBuffer,"&field1=%s%u.%u", isPositive?"":"-", T_intPart, T_fractionalPart); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,"&field2=%" PRIu32, airData.P_Pa); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,"&field3=%u.%u", airData.H_pc_int, airData.H_pc_fr_1dp); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,"&field4=%u.%u", airQualityData.AQI_int, airQualityData.AQI_fr_1dp); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,"&field5=%u.%02u", airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,"&field6=%u.%u", soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,"&field7=%u.%02u", lightData.illum_lux_int, lightData.illum_lux_fr_2dp); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,"&field8=%u.%02u", particleData.concentration_int, + particleData.concentration_fr_2dp); + strcat(postBuffer, fieldBuffer); + + size_t len = strlen(postBuffer); + sprintf(fieldBuffer,"Content-Length: %u",len); + client.println(fieldBuffer); + client.println(); + client.print(postBuffer); + } + else { + Serial.println("Client connection failed."); + } +} diff --git a/lib/Metriful/examples/cycle_readout/cycle_readout.ino b/lib/Metriful/examples/cycle_readout/cycle_readout.ino new file mode 100644 index 0000000..9f53595 --- /dev/null +++ b/lib/Metriful/examples/cycle_readout/cycle_readout.ino @@ -0,0 +1,111 @@ +/* + cycle_readout.ino + + Example code for using the Metriful MS430 in cycle mode. + + Continually measures and displays all environment data in + a repeating cycle. User can choose from a cycle time period + of 3, 100, or 300 seconds. View the output in the Serial Monitor. + + The measurements can be displayed as either labeled text, or as + simple columns of numbers. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// How often to read data (every 3, 100, or 300 seconds) +uint8_t cycle_period = CYCLE_PERIOD_3_S; + +// How to print the data over the serial port. If printDataAsColumns = true, +// data are columns of numbers, useful to copy/paste to a spreadsheet +// application. Otherwise, data are printed with explanatory labels and units. +bool printDataAsColumns = false; + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +// Structs for data +AirData_t airData = {0}; +AirQualityData_t airQualityData = {0}; +LightData_t lightData = {0}; +SoundData_t soundData = {0}; +ParticleData_t particleData = {0}; + + +void setup() { + // Initialize the host pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + // Apply chosen settings to the MS430 + uint8_t particleSensor = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); + + // Wait for the serial port to be ready, for displaying the output + while (!Serial) { + yield(); + } + + Serial.println("Entering cycle mode and waiting for data."); + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); +} + + +void loop() { + // Wait for the next new data release, indicated by a falling edge on READY + while (!ready_assertion_event) { + yield(); + } + ready_assertion_event = false; + + // Read data from the MS430 into the data structs. + + // Air data + // Choose output temperature unit (C or F) in Metriful_sensor.h + airData = getAirData(I2C_ADDRESS); + + /* Air quality data + The initial self-calibration of the air quality data may take several + minutes to complete. During this time the accuracy parameter is zero + and the data values are not valid. + */ + airQualityData = getAirQualityData(I2C_ADDRESS); + + // Light data + lightData = getLightData(I2C_ADDRESS); + + // Sound data + soundData = getSoundData(I2C_ADDRESS); + + /* Particle data + This requires the connection of a particulate sensor (invalid + values will be obtained if this sensor is not present). + Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h + Also note that, due to the low pass filtering used, the + particle data become valid after an initial initialization + period of approximately one minute. + */ + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + particleData = getParticleData(I2C_ADDRESS); + } + + // Print all data to the serial port + printAirData(&airData, printDataAsColumns); + printAirQualityData(&airQualityData, printDataAsColumns); + printLightData(&lightData, printDataAsColumns); + printSoundData(&soundData, printDataAsColumns); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR); + } + Serial.println(); +} diff --git a/lib/Metriful/examples/graph_web_server/graph_web_server.ino b/lib/Metriful/examples/graph_web_server/graph_web_server.ino new file mode 100644 index 0000000..5ff48a6 --- /dev/null +++ b/lib/Metriful/examples/graph_web_server/graph_web_server.ino @@ -0,0 +1,366 @@ +/* + graph_web_server.ino + + Serve a web page over a WiFi network, displaying graphs showing + environment data read from the Metriful MS430. A CSV data file is + also downloadable from the page. + + This example is designed for the following WiFi enabled hosts: + * Arduino Nano 33 IoT + * Arduino MKR WiFi 1010 + * ESP8266 boards (e.g. Wemos D1, NodeMCU) + * ESP32 boards (e.g. DOIT DevKit v1) + + The host can either connect to an existing WiFi network, or generate + its own for other devices to connect to (Access Point mode). + + The browser which views the web page uses the Plotly javascript + library to generate the graphs. This is automatically downloaded + over the internet, or can be cached for offline use. If it is not + available, graphs will not appear but text data and CSV downloads + should still work. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include +#include +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// Choose how often to read and update data (every 3, 100, or 300 seconds) +// 100 or 300 seconds are recommended for long-term monitoring. +uint8_t cycle_period = CYCLE_PERIOD_100_S; + +// The BUFFER_LENGTH parameter is the number of data points of each +// variable to store on the host. It is limited by the available host RAM. +#define BUFFER_LENGTH 576 +// Examples: +// For 16 hour graphs, choose 100 second cycle period and 576 buffer length +// For 24 hour graphs, choose 300 second cycle period and 288 buffer length + +// Choose whether to create a new WiFi network (host as Access Point), +// or connect to an existing WiFi network. +bool createWifiNetwork = true; +// If creating a WiFi network, a static (fixed) IP address ("theIP") is +// specified by the user. Otherwise, if connecting to an existing +// network, an IP address is automatically allocated and the serial +// output must be viewed at startup to see this allocated IP address. + +// Provide the SSID (name) and password for the WiFi network. Depending +// on the choice of createWifiNetwork, this is either created by the +// host (Access Point mode) or already exists. +// To avoid problems, do not create a network with the same SSID name +// as an already existing network. +char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name) +char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password; must be at least 8 characters + +// Choose a static IP address for the host, only used when generating +// a new WiFi network (createWifiNetwork = true). The served web +// page will be available at http:// +IPAddress theIP(192, 168, 12, 20); +// e.g. theIP(192, 168, 12, 20) means an IP of 192.168.12.20 +// and the web page will be at http://192.168.12.20 + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +#if !defined(HAS_WIFI) +#error ("This example program has been created for specific WiFi enabled hosts only.") +#endif + +WiFiServer server(80); +uint16_t dataPeriod_s; + +// Structs for data +AirData_F_t airDataF = {0}; +AirQualityData_F_t airQualityDataF = {0}; +LightData_F_t lightDataF = {0}; +ParticleData_F_t particleDataF = {0}; +SoundData_F_t soundDataF = {0}; + +const char * errorResponseHTTP = "HTTP/1.1 400 Bad Request\r\n\r\n"; + +const char * dataHeader = "HTTP/1.1 200 OK\r\n" + "Content-type: application/octet-stream\r\n" + "Connection: close\r\n\r\n"; + +uint16_t bufferLength = 0; +float temperature_buffer[BUFFER_LENGTH] = {0}; +float pressure_buffer[BUFFER_LENGTH] = {0}; +float humidity_buffer[BUFFER_LENGTH] = {0}; +float AQI_buffer[BUFFER_LENGTH] = {0}; +float bVOC_buffer[BUFFER_LENGTH] = {0}; +float SPL_buffer[BUFFER_LENGTH] = {0}; +float illuminance_buffer[BUFFER_LENGTH] = {0}; +float particle_buffer[BUFFER_LENGTH] = {0}; + + +void setup() { + // Initialize the host's pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + if (createWifiNetwork) { + // The host generates its own WiFi network ("Access Point") with + // a chosen static IP address + if (!createWiFiAP(SSID, password, theIP)) { + Serial.println("Failed to create access point."); + while (true) { + yield(); + } + } + } + else { + // The host connects to an existing Wifi network + + // Wait for the serial port to start because the user must be able + // to see the printed IP address in the serial monitor + while (!Serial) { + yield(); + } + + // Attempt to connect to the Wifi network and obtain the IP + // address. Because the address is not known before this point, + // a serial monitor must be used to display it to the user. + connectToWiFi(SSID, password); + theIP = WiFi.localIP(); + } + + // Print the IP address: use this address in a browser to view the + // generated web page + Serial.print("View your page at http://"); + Serial.println(theIP); + + // Start the web server + server.begin(); + + //////////////////////////////////////////////////////////////////// + + // Get time period value to send to web page + if (cycle_period == CYCLE_PERIOD_3_S) { + dataPeriod_s = 3; + } + else if (cycle_period == CYCLE_PERIOD_100_S) { + dataPeriod_s = 100; + } + else { // CYCLE_PERIOD_300_S + dataPeriod_s = 300; + } + + // Apply the chosen settings to the Metriful board + uint8_t particleSensor = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); +} + +void loop() { + + // Respond to the web page client requests while waiting for new data + while (!ready_assertion_event) { + handleClientRequests(); + yield(); + } + ready_assertion_event = false; + + // Read the new data and convert to float types: + airDataF = getAirDataF(I2C_ADDRESS); + airQualityDataF = getAirQualityDataF(I2C_ADDRESS); + lightDataF = getLightDataF(I2C_ADDRESS); + soundDataF = getSoundDataF(I2C_ADDRESS); + particleDataF = getParticleDataF(I2C_ADDRESS); + + // Save the data + updateDataBuffers(); + + // Check WiFi is still connected + if (!createWifiNetwork) { + uint8_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + // There is a problem with the WiFi connection: attempt to reconnect. + Serial.print("Wifi status: "); + Serial.println(interpret_WiFi_status(wifiStatus)); + connectToWiFi(SSID, password); + theIP = WiFi.localIP(); + Serial.print("View your page at http://"); + Serial.println(theIP); + ready_assertion_event = false; + } + } +} + +// Store the data, up to a maximum length of BUFFER_LENGTH, then start +// discarding the oldest data in a FIFO scheme ("First In First Out") +void updateDataBuffers(void) { + uint16_t position = 0; + if (bufferLength == BUFFER_LENGTH) { + // Buffers are full: shift all data values along, discarding the oldest + for (uint16_t i=0; i<(BUFFER_LENGTH-1); i++) { + temperature_buffer[i] = temperature_buffer[i+1]; + pressure_buffer[i] = pressure_buffer[i+1]; + humidity_buffer[i] = humidity_buffer[i+1]; + AQI_buffer[i] = AQI_buffer[i+1]; + bVOC_buffer[i] = bVOC_buffer[i+1]; + SPL_buffer[i] = SPL_buffer[i+1]; + illuminance_buffer[i] = illuminance_buffer[i+1]; + particle_buffer[i] = particle_buffer[i+1]; + } + position = BUFFER_LENGTH-1; + } + else { + // Buffers are not yet full; keep filling them + position = bufferLength; + bufferLength++; + } + + // Save the new data in the buffers + AQI_buffer[position] = airQualityDataF.AQI; + #ifdef USE_FAHRENHEIT + temperature_buffer[position] = convertCtoF(airDataF.T_C); + #else + temperature_buffer[position] = airDataF.T_C; + #endif + pressure_buffer[position] = (float) airDataF.P_Pa; + humidity_buffer[position] = airDataF.H_pc; + SPL_buffer[position] = soundDataF.SPL_dBA; + illuminance_buffer[position] = lightDataF.illum_lux; + bVOC_buffer[position] = airQualityDataF.bVOC; + particle_buffer[position] = particleDataF.concentration; +} + + +#define GET_REQUEST_STR "GET /" +#define URI_CHARS 2 +// Send either the web page or the data in response to HTTP requests. +void handleClientRequests(void) { + // Check for incoming client requests + WiFiClient client = server.available(); + if (client) { + + uint8_t requestCount = 0; + char requestBuffer[sizeof(GET_REQUEST_STR)] = {0}; + + uint8_t uriCount = 0; + char uriBuffer[URI_CHARS] = {0}; + + while (client.connected()) { + if (client.available()) { + char c = client.read(); + + if (requestCount < (sizeof(GET_REQUEST_STR)-1)) { + // Assemble the first part of the message containing the HTTP method (GET, POST etc) + requestBuffer[requestCount] = c; + requestCount++; + } + else if (uriCount < URI_CHARS) { + // Assemble the URI, up to a fixed number of characters + uriBuffer[uriCount] = c; + uriCount++; + } + else { + // Now use the assembled method and URI to decide how to respond + if (strcmp(requestBuffer, GET_REQUEST_STR) == 0) { + // It is a GET request (no other methods are supported). + // Now check for valid URIs. + if (uriBuffer[0] == ' ') { + // The web page is requested + sendData(&client, (const uint8_t *) graphWebPage, strlen(graphWebPage)); + break; + } + else if ((uriBuffer[0] == '1') && (uriBuffer[1] == ' ')) { + // A URI of '1' indicates a request of all buffered data + sendAllData(&client); + break; + } + else if ((uriBuffer[0] == '2') && (uriBuffer[1] == ' ')) { + // A URI of '2' indicates a request of the latest data only + sendLatestData(&client); + break; + } + } + // Reaching here means that the request is not supported or is incorrect + // (not a GET request, or not a valid URI) so send an error. + client.print(errorResponseHTTP); + break; + } + } + } + #ifndef ESP8266 + client.stop(); + #endif + } +} + +// Send all buffered data in the HTTP response. Binary format ("octet-stream") +// is used, and the receiving web page uses the known order of the data to +// decode and interpret it. +void sendAllData(WiFiClient * clientPtr) { + clientPtr->print(dataHeader); + // First send the time period, so the web page knows when to do the next request + clientPtr->write((const uint8_t *) &dataPeriod_s, sizeof(uint16_t)); + // Send temperature unit and particle sensor type, combined into one byte + uint8_t codeByte = (uint8_t) PARTICLE_SENSOR; + #ifdef USE_FAHRENHEIT + codeByte = codeByte | 0x10; + #endif + clientPtr->write((const uint8_t *) &codeByte, sizeof(uint8_t)); + // Send the length of the data buffers (the number of values of each variable) + clientPtr->write((const uint8_t *) &bufferLength, sizeof(uint16_t)); + // Send the data, unless none have been read yet: + if (bufferLength > 0) { + sendData(clientPtr, (const uint8_t *) AQI_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) temperature_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) pressure_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) humidity_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) SPL_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) illuminance_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) bVOC_buffer, bufferLength*sizeof(float)); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + sendData(clientPtr, (const uint8_t *) particle_buffer, bufferLength*sizeof(float)); + } + } +} + + +// Send just the most recent value of each variable (or no data if no values +// have been read yet) +void sendLatestData(WiFiClient * clientPtr) { + clientPtr->print(dataHeader); + if (bufferLength > 0) { + uint16_t bufferPosition = bufferLength-1; + clientPtr->write((const uint8_t *) &(AQI_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(temperature_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(pressure_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(humidity_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(SPL_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(illuminance_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(bVOC_buffer[bufferPosition]), sizeof(float)); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + clientPtr->write((const uint8_t *) &(particle_buffer[bufferPosition]), sizeof(float)); + } + } +} + + +// client.write() may fail with very large inputs, so split +// into several separate write() calls with a short delay between each. +#define MAX_DATA_BYTES 1000 +void sendData(WiFiClient * clientPtr, const uint8_t * dataPtr, size_t dataLength) { + while (dataLength > 0) { + size_t sendLength = dataLength; + if (sendLength > MAX_DATA_BYTES) { + sendLength = MAX_DATA_BYTES; + } + clientPtr->write(dataPtr, sendLength); + delay(10); + dataLength-=sendLength; + dataPtr+=sendLength; + } +} diff --git a/lib/Metriful/examples/interrupts/interrupts.ino b/lib/Metriful/examples/interrupts/interrupts.ino new file mode 100644 index 0000000..37bab65 --- /dev/null +++ b/lib/Metriful/examples/interrupts/interrupts.ino @@ -0,0 +1,132 @@ +/* + interrupts.ino + + Example code for using the Metriful MS430 interrupt outputs. + + Light and sound interrupts are configured and the program then + waits forever. When an interrupt occurs, a message prints over + the serial port, the interrupt is cleared (if set to latch type), + and the program returns to waiting. + View the output in the Serial Monitor. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// Light level interrupt settings + +bool enableLightInterrupts = true; +uint8_t light_int_type = LIGHT_INT_TYPE_LATCH; +// Choose the interrupt polarity: trigger when level rises above +// threshold (positive), or when level falls below threshold (negative). +uint8_t light_int_polarity = LIGHT_INT_POL_POSITIVE; +uint16_t light_int_thres_lux_i = 100; +uint8_t light_int_thres_lux_f2dp = 50; +// The interrupt threshold value in lux units can be fractional and is formed as: +// threshold = light_int_thres_lux_i + (light_int_thres_lux_f2dp/100) +// E.g. for a light threshold of 56.12 lux, set: +// light_int_thres_lux_i = 56 +// light_int_thres_lux_f2dp = 12 + + +// Sound level interrupt settings + +bool enableSoundInterrupts = true; +uint8_t sound_int_type = SOUND_INT_TYPE_LATCH; +uint16_t sound_thres_mPa = 100; + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +uint8_t transmit_buffer[1] = {0}; + + +void setup() { + // Initialize the host pins, set up the serial port and reset + SensorHardwareSetup(I2C_ADDRESS); + + // check that the chosen light threshold is a valid value + if (light_int_thres_lux_i > MAX_LUX_VALUE) { + Serial.println("The chosen light interrupt threshold exceeds the maximum allowed value."); + while (true) { + yield(); + } + } + + if ((!enableSoundInterrupts)&&(!enableLightInterrupts)) { + Serial.println("No interrupts have been selected."); + while (true) { + yield(); + } + } + + if (enableSoundInterrupts) { + // Set the interrupt type (latch or comparator) + transmit_buffer[0] = sound_int_type; + TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_TYPE_REG, transmit_buffer, 1); + + // Set the threshold + setSoundInterruptThreshold(I2C_ADDRESS, sound_thres_mPa); + + // Enable the interrupt + transmit_buffer[0] = ENABLED; + TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_ENABLE_REG, transmit_buffer, 1); + } + + if (enableLightInterrupts) { + // Set the interrupt type (latch or comparator) + transmit_buffer[0] = light_int_type; + TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_TYPE_REG, transmit_buffer, 1); + + // Set the threshold + setLightInterruptThreshold(I2C_ADDRESS, light_int_thres_lux_i, light_int_thres_lux_f2dp); + + // Set the interrupt polarity + transmit_buffer[0] = light_int_polarity; + TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_POLARITY_REG, transmit_buffer, 1); + + // Enable the interrupt + transmit_buffer[0] = ENABLED; + TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_ENABLE_REG, transmit_buffer, 1); + } + + // Wait for the serial port to be ready, for displaying the output + while (!Serial) { + yield(); + } + + Serial.println("Waiting for interrupts."); + Serial.println(); +} + + +void loop() { + + // Check whether a light interrupt has occurred + if ((digitalRead(L_INT_PIN) == LOW) && enableLightInterrupts) { + Serial.println("LIGHT INTERRUPT."); + if (light_int_type == LIGHT_INT_TYPE_LATCH) { + // Latch type interrupts remain set until cleared by command + TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_CLR_CMD, 0, 0); + } + } + + // Check whether a sound interrupt has occurred + if ((digitalRead(S_INT_PIN) == LOW) && enableSoundInterrupts) { + Serial.println("SOUND INTERRUPT."); + if (sound_int_type == SOUND_INT_TYPE_LATCH) { + // Latch type interrupts remain set until cleared by command + TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_CLR_CMD, 0, 0); + } + } + + delay(500); +} diff --git a/lib/Metriful/examples/on_demand_readout/on_demand_readout.ino b/lib/Metriful/examples/on_demand_readout/on_demand_readout.ino new file mode 100644 index 0000000..8509dde --- /dev/null +++ b/lib/Metriful/examples/on_demand_readout/on_demand_readout.ino @@ -0,0 +1,105 @@ +/* + on_demand_readout.ino + + Example code for using the Metriful MS430 in "on-demand" mode. + + Repeatedly measures and displays all environment data, with a pause + between measurements. Air quality data are unavailable in this mode + (instead see cycle_readout.ino). View output in the Serial Monitor. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// Pause (in milliseconds) between data measurements (note that the +// measurement itself takes 0.5 seconds) +uint32_t pause_ms = 4500; +// Choosing a pause of less than 2000 ms will cause inaccurate +// temperature, humidity and particle data. + +// How to print the data over the serial port. If printDataAsColumns = true, +// data are columns of numbers, useful to copy/paste to a spreadsheet +// application. Otherwise, data are printed with explanatory labels and units. +bool printDataAsColumns = false; + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +// Structs for data +AirData_t airData = {0}; +LightData_t lightData = {0}; +SoundData_t soundData = {0}; +ParticleData_t particleData = {0}; + + +void setup() { + // Initialize the host pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + uint8_t particleSensor = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); + + // Wait for the serial port to be ready, for displaying the output + while (!Serial) { + yield(); + } +} + + +void loop() { + + // Trigger a new measurement + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); + + // Wait for the measurement to finish, indicated by a falling edge on READY + while (!ready_assertion_event) { + yield(); + } + + // Read data from the MS430 into the data structs. + + // Air data + // Choose output temperature unit (C or F) in Metriful_sensor.h + airData = getAirData(I2C_ADDRESS); + + // Air quality data are not available with on demand measurements + + // Light data + lightData = getLightData(I2C_ADDRESS); + + // Sound data + soundData = getSoundData(I2C_ADDRESS); + + /* Particle data + This requires the connection of a particulate sensor (invalid + values will be obtained if this sensor is not present). + Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h + Also note that, due to the low pass filtering used, the + particle data become valid after an initial initialization + period of approximately one minute. + */ + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + particleData = getParticleData(I2C_ADDRESS); + } + + // Print all data to the serial port + printAirData(&airData, printDataAsColumns); + printLightData(&lightData, printDataAsColumns); + printSoundData(&soundData, printDataAsColumns); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR); + } + Serial.println(); + + // Wait for the chosen time period before repeating everything + delay(pause_ms); +} diff --git a/lib/Metriful/examples/particle_sensor_toggle/particle_sensor_toggle.ino b/lib/Metriful/examples/particle_sensor_toggle/particle_sensor_toggle.ino new file mode 100644 index 0000000..d9a8722 --- /dev/null +++ b/lib/Metriful/examples/particle_sensor_toggle/particle_sensor_toggle.ino @@ -0,0 +1,166 @@ +/* + particle_sensor_toggle.ino + + Optional advanced demo. This program shows how to generate an output + control signal from one of the host's pins, which can be used to turn + the particle sensor on and off. An external transistor circuit is + also needed - this will gate the sensor power supply according to + the control signal. Further details are given in the User Guide. + + The program continually measures and displays all environment data + in a repeating cycle. The user can view the output in the Serial + Monitor. After reading the data, the particle sensor is powered off + for a chosen number of cycles ("off_cycles"). It is then powered on + and read before being powered off again. Sound data are ignored + while the particle sensor is on, to avoid its fan noise. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// How often to read data; choose only 100 or 300 seconds for this demo +// because the sensor should be on for at least one minute before reading +// its data. +uint8_t cycle_period = CYCLE_PERIOD_100_S; + +// How to print the data over the serial port. If printDataAsColumns = true, +// data are columns of numbers, useful for transferring to a spreadsheet +// application. Otherwise, data are printed with explanatory labels and units. +bool printDataAsColumns = false; + +// Particle sensor power control options +uint8_t off_cycles = 2; // leave the sensor off for this many cycles between reads +uint8_t particle_sensor_control_pin = 10; // host pin number which outputs the control signal +bool particle_sensor_ON_state = true; +// particle_sensor_ON_state is the required polarity of the control +// signal; true means +V is output to turn the sensor on, while false +// means 0 V is output. Use true for 3.3 V hosts and false for 5 V hosts. + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +uint8_t transmit_buffer[1] = {0}; + +// Structs for data +AirData_t airData = {0}; +AirQualityData_t airQualityData = {0}; +LightData_t lightData = {0}; +SoundData_t soundData = {0}; +ParticleData_t particleData = {0}; + +bool particleSensorIsOn = false; +uint8_t particleSensor_count = 0; + + +void setup() { + // Initialize the host pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + // Set up the particle sensor control, and turn it off initially + pinMode(particle_sensor_control_pin, OUTPUT); + digitalWrite(particle_sensor_control_pin, !particle_sensor_ON_state); + particleSensorIsOn = false; + + // Apply chosen settings to the MS430 + transmit_buffer[0] = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); + transmit_buffer[0] = cycle_period; + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1); + + // Wait for the serial port to be ready, for displaying the output + while (!Serial) { + yield(); + } + + Serial.println("Entering cycle mode and waiting for data."); + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); +} + + +void loop() { + // Wait for the next new data release, indicated by a falling edge on READY + while (!ready_assertion_event) { + yield(); + } + ready_assertion_event = false; + + /* Read data from the MS430 into the data structs. + For each category of data (air, sound, etc.) a pointer to the data struct is + passed to the ReceiveI2C() function. The received byte sequence fills the data + struct in the correct order so that each field within the struct receives + the value of an environmental quantity (temperature, sound level, etc.) + */ + + // Air data + ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + + /* Air quality data + The initial self-calibration of the air quality data may take several + minutes to complete. During this time the accuracy parameter is zero + and the data values are not valid. + */ + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + + // Light data + ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + + // Sound data - only read when particle sensor is off + if (!particleSensorIsOn) { + ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + } + + /* Particle data + This requires the connection of a particulate sensor (invalid + values will be obtained if this sensor is not present). + Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h + Also note that, due to the low pass filtering used, the + particle data become valid after an initial initialization + period of approximately one minute. + */ + if (particleSensorIsOn) { + ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + } + + // Print all data to the serial port. The previous loop's particle or + // sound data will be printed if no reading was done on this loop. + printAirData(&airData, printDataAsColumns); + printAirQualityData(&airQualityData, printDataAsColumns); + printLightData(&lightData, printDataAsColumns); + printSoundData(&soundData, printDataAsColumns); + printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR); + Serial.println(); + + // Turn the particle sensor on/off if required + if (particleSensorIsOn) { + // Stop the particle detection on the MS430 + transmit_buffer[0] = OFF; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); + + // Turn off the hardware: + digitalWrite(particle_sensor_control_pin, !particle_sensor_ON_state); + particleSensorIsOn = false; + } + else { + particleSensor_count++; + if (particleSensor_count >= off_cycles) { + // Turn on the hardware: + digitalWrite(particle_sensor_control_pin, particle_sensor_ON_state); + + // Start the particle detection on the MS430 + transmit_buffer[0] = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); + + particleSensor_count = 0; + particleSensorIsOn = true; + } + } +} diff --git a/lib/Metriful/examples/simple_read_T_H/simple_read_T_H.ino b/lib/Metriful/examples/simple_read_T_H/simple_read_T_H.ino new file mode 100644 index 0000000..c2ff5eb --- /dev/null +++ b/lib/Metriful/examples/simple_read_T_H/simple_read_T_H.ino @@ -0,0 +1,150 @@ +/* + simple_read_T_H.ino + + Example code for using the Metriful MS430 to measure humidity + and temperature. + + Demonstrates multiple ways of reading and displaying the temperature + and humidity data. View the output in the Serial Monitor. The other + data can be measured and displayed in a similar way. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include + + +void setup() { + // Initialize the host pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + // Wait for the serial port to be ready, for displaying the output + while (!Serial) { + yield(); + } + + // Clear the global variable in preparation for waiting for READY assertion + ready_assertion_event = false; + + // Initiate an on-demand data measurement + TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); + + // Now wait for the ready signal before continuing + while (!ready_assertion_event) { + yield(); + } + + // We know that new data are ready to read. + + //////////////////////////////////////////////////////////////////// + + // There are different ways to read and display the data + + // 1. Simplest way: use the example float (_F) functions + + // Read the "air data" from the MS430. This includes temperature and + // humidity as well as pressure and gas sensor data. + AirData_F_t airDataF = getAirDataF(I2C_ADDRESS); + + // Print all of the air measurements to the serial monitor + printAirDataF(&airDataF); + // Fahrenheit temperature is printed if USE_FAHRENHEIT is defined + // in "Metriful_sensor.h" + + Serial.println("-----------------------------"); + + + // 2. After reading from the MS430, you can also access and print the + // float data directly from the struct: + Serial.print("The temperature is: "); + Serial.print(airDataF.T_C, 1); // print to 1 decimal place + Serial.println(" " CELSIUS_SYMBOL); + + // Optional: convert to Fahrenheit + float temperature_F = convertCtoF(airDataF.T_C); + + Serial.print("The temperature is: "); + Serial.print(temperature_F, 1); // print to 1 decimal place + Serial.println(" " FAHRENHEIT_SYMBOL); + + Serial.println("-----------------------------"); + + + // 3. If host resources are limited, avoid using floating point and + // instead use the integer versions (without "F" in the name) + AirData_t airData = getAirData(I2C_ADDRESS); + + // Print to the serial monitor + printAirData(&airData, false); + // If the second argument is "true", data are printed as columns. + // Fahrenheit temperature is printed if USE_FAHRENHEIT is defined + // in "Metriful_sensor.h" + + Serial.println("-----------------------------"); + + + // 4. Access and print integer data directly from the struct: + Serial.print("The humidity is: "); + Serial.print(airData.H_pc_int); // the integer part of the value + Serial.print("."); // the decimal point + Serial.print(airData.H_pc_fr_1dp); // the fractional part (1 decimal place) + Serial.println(" %"); + + Serial.println("-----------------------------"); + + + // 5. Advanced: read and decode only the humidity value from the MS430 + + // Read the raw humidity data + uint8_t receive_buffer[2] = {0}; + ReceiveI2C(I2C_ADDRESS, H_READ, receive_buffer, H_BYTES); + + // Decode the humidity: the first received byte is the integer part, the + // second received byte is the fractional part to one decimal place. + uint8_t humidity_integer = receive_buffer[0]; + uint8_t humidity_fraction = receive_buffer[1]; + // Print it: the units are percentage relative humidity. + Serial.print("Humidity = "); + Serial.print(humidity_integer); + Serial.print("."); + Serial.print(humidity_fraction); + Serial.println(" %"); + + Serial.println("-----------------------------"); + + + // 6. Advanced: read and decode only the temperature value from the MS430 + + // Read the raw temperature data + ReceiveI2C(I2C_ADDRESS, T_READ, receive_buffer, T_BYTES); + + // The temperature is encoded differently to allow negative values + + // Find the positive magnitude of the integer part of the temperature + // by doing a bitwise AND of the first received byte with TEMPERATURE_VALUE_MASK + uint8_t temperature_positive_integer = receive_buffer[0] & TEMPERATURE_VALUE_MASK; + + // The second received byte is the fractional part to one decimal place + uint8_t temperature_fraction = receive_buffer[1]; + + Serial.print("Temperature = "); + // If the most-significant bit of the first byte is a 1, the temperature + // is negative (below 0 C), otherwise it is positive + if ((receive_buffer[0] & TEMPERATURE_SIGN_MASK) != 0) { + // The bit is a 1: celsius temperature is negative + Serial.print("-"); + } + Serial.print(temperature_positive_integer); + Serial.print("."); + Serial.print(temperature_fraction); + Serial.println(" " CELSIUS_SYMBOL); + +} + +void loop() { + // There is no loop for this program. +} diff --git a/lib/Metriful/examples/simple_read_sound/simple_read_sound.ino b/lib/Metriful/examples/simple_read_sound/simple_read_sound.ino new file mode 100644 index 0000000..01d537a --- /dev/null +++ b/lib/Metriful/examples/simple_read_sound/simple_read_sound.ino @@ -0,0 +1,98 @@ +/* + simple_read_sound.ino + + Example code for using the Metriful MS430 to measure sound. + + Demonstrates multiple ways of reading and displaying the sound data. + View the output in the Serial Monitor. The other data can be measured + and displayed in a similar way. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include + + +void setup() { + // Initialize the host pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + // Wait for the serial port to be ready, for displaying the output + while (!Serial) { + yield(); + } + + //////////////////////////////////////////////////////////////////// + + // Wait for the microphone signal to stabilize (takes approximately 1.5 seconds). + // This only needs to be done once after the MS430 is powered-on or reset. + delay(1500); + + //////////////////////////////////////////////////////////////////// + + // Clear the global variable in preparation for waiting for READY assertion + ready_assertion_event = false; + + // Initiate an on-demand data measurement + TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); + + // Now wait for the ready signal (falling edge) before continuing + while (!ready_assertion_event) { + yield(); + } + + // We now know that newly measured data are ready to read. + + //////////////////////////////////////////////////////////////////// + + // There are multiple ways to read and display the data + + + // 1. Simplest way: use the example float (_F) functions + + // Read the sound data from the board + SoundData_F_t soundDataF = getSoundDataF(I2C_ADDRESS); + + // Print all of the sound measurements to the serial monitor + printSoundDataF(&soundDataF); + + Serial.println("-----------------------------"); + + + // 2. After reading from the MS430, you can also access and print the + // float data directly from the struct: + Serial.print("The sound pressure level is: "); + Serial.print(soundDataF.SPL_dBA, 1); // print to 1 decimal place + Serial.println(" dBA"); + + Serial.println("-----------------------------"); + + + // 3. If host resources are limited, avoid using floating point and + // instead use the integer versions (without "F" in the name) + SoundData_t soundData = getSoundData(I2C_ADDRESS); + + // Print to the serial monitor + printSoundData(&soundData, false); + // If the second argument is "true", data are printed as columns. + + Serial.println("-----------------------------"); + + + // 4. Access and print integer data directly from the struct: + Serial.print("The sound pressure level is: "); + Serial.print(soundData.SPL_dBA_int); // the integer part of the value + Serial.print("."); // the decimal point + Serial.print(soundData.SPL_dBA_fr_1dp); // the fractional part (1 decimal place) + Serial.println(" dBA"); + + Serial.println("-----------------------------"); +} + +void loop() { + // There is no loop for this program. +} diff --git a/lib/Metriful/examples/web_server/web_server.ino b/lib/Metriful/examples/web_server/web_server.ino new file mode 100644 index 0000000..1921ed6 --- /dev/null +++ b/lib/Metriful/examples/web_server/web_server.ino @@ -0,0 +1,380 @@ +/* + web_server.ino + + Example code for serving a web page over a WiFi network, displaying + environment data read from the Metriful MS430. + + This example is designed for the following WiFi enabled hosts: + * Arduino Nano 33 IoT + * Arduino MKR WiFi 1010 + * ESP8266 boards (e.g. Wemos D1, NodeMCU) + * ESP32 boards (e.g. DOIT DevKit v1) + + All environment data values are measured and displayed on a text + web page generated by the host, which acts as a simple web server. + + The host can either connect to an existing WiFi network, or generate + its own for other devices to connect to (Access Point mode). + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// Choose how often to read and update data (every 3, 100, or 300 seconds) +// The web page can be refreshed more often but the data will not change +uint8_t cycle_period = CYCLE_PERIOD_3_S; + +// Choose whether to create a new WiFi network (host as Access Point), +// or connect to an existing WiFi network. +bool createWifiNetwork = false; +// If creating a WiFi network, a static (fixed) IP address ("theIP") is +// specified by the user. Otherwise, if connecting to an existing +// network, an IP address is automatically allocated and the serial +// output must be viewed at startup to see this allocated IP address. + +// Provide the SSID (name) and password for the WiFi network. Depending +// on the choice of createWifiNetwork, this is either created by the +// host (Access Point mode) or already exists. +// To avoid problems, do not create a network with the same SSID name +// as an already existing network. +char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name) +char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password; must be at least 8 characters + +// Choose a static IP address for the host, only used when generating +// a new WiFi network (createWifiNetwork = true). The served web +// page will be available at http:// +IPAddress theIP(192, 168, 12, 20); +// e.g. theIP(192, 168, 12, 20) means an IP of 192.168.12.20 +// and the web page will be at http://192.168.12.20 + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +#if !defined(HAS_WIFI) +#error ("This example program has been created for specific WiFi enabled hosts only.") +#endif + +WiFiServer server(80); +uint16_t refreshPeriodSeconds; + +// Structs for data +AirData_t airData = {0}; +AirQualityData_t airQualityData = {0}; +LightData_t lightData = {0}; +ParticleData_t particleData = {0}; +SoundData_t soundData = {0}; + +// Storage for the web page text +char lineBuffer[100] = {0}; +char pageBuffer[2300] = {0}; + +void setup() { + // Initialize the host's pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + if (createWifiNetwork) { + // The host generates its own WiFi network ("Access Point") with + // a chosen static IP address + if (!createWiFiAP(SSID, password, theIP)) { + Serial.println("Failed to create access point."); + while (true) { + yield(); + } + } + } + else { + // The host connects to an existing Wifi network + + // Wait for the serial port to start because the user must be able + // to see the printed IP address in the serial monitor + while (!Serial) { + yield(); + } + + // Attempt to connect to the Wifi network and obtain the IP + // address. Because the address is not known before this point, + // a serial monitor must be used to display it to the user. + connectToWiFi(SSID, password); + theIP = WiFi.localIP(); + } + + // Print the IP address: use this address in a browser to view the + // generated web page + Serial.print("View your page at http://"); + Serial.println(theIP); + + // Start the web server + server.begin(); + + //////////////////////////////////////////////////////////////////// + + // Select how often to auto-refresh the web page. This should be done at + // least as often as new data are obtained. A more frequent refresh is + // best for long cycle periods because the page refresh is not + // synchronized with the cycle. Users can also manually refresh the page. + if (cycle_period == CYCLE_PERIOD_3_S) { + refreshPeriodSeconds = 3; + } + else if (cycle_period == CYCLE_PERIOD_100_S) { + refreshPeriodSeconds = 30; + } + else { // CYCLE_PERIOD_300_S + refreshPeriodSeconds = 50; + } + + // Apply the chosen settings to the Metriful board + uint8_t particleSensor = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); +} + +void loop() { + + // While waiting for the next data release, respond to client requests + // by serving the web page with the last available data. Initially the + // data will be all zero (until the first data readout has completed). + while (!ready_assertion_event) { + handleClientRequests(); + yield(); + } + ready_assertion_event = false; + + // new data are now ready + + /* Read data from the MS430 into the data structs. + For each category of data (air, sound, etc.) a pointer to the data struct is + passed to the ReceiveI2C() function. The received byte sequence fills the data + struct in the correct order so that each field within the struct receives + the value of an environmental quantity (temperature, sound level, etc.) + */ + + // Air data + // Choose output temperature unit (C or F) in Metriful_sensor.h + ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + + /* Air quality data + The initial self-calibration of the air quality data may take several + minutes to complete. During this time the accuracy parameter is zero + and the data values are not valid. + */ + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + + // Light data + ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + + // Sound data + ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + + /* Particle data + This requires the connection of a particulate sensor (invalid + values will be obtained if this sensor is not present). + Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h + Also note that, due to the low pass filtering used, the + particle data become valid after an initial initialization + period of approximately one minute. + */ + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + } + + // Create the web page ready for client requests + assembleWebPage(); + + // Check WiFi is still connected + if (!createWifiNetwork) { + uint8_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + // There is a problem with the WiFi connection: attempt to reconnect. + Serial.print("Wifi status: "); + Serial.println(interpret_WiFi_status(wifiStatus)); + connectToWiFi(SSID, password); + theIP = WiFi.localIP(); + Serial.print("View your page at http://"); + Serial.println(theIP); + ready_assertion_event = false; + } + } +} + + +void handleClientRequests(void) { + // Check for incoming client requests + WiFiClient client = server.available(); + if (client) { + bool blankLine = false; + while (client.connected()) { + if (client.available()) { + char c = client.read(); + if (c == '\n') { + // Two consecutive newline characters indicates the end of the client HTTP request + if (blankLine) { + // Send the page as a response + client.print(pageBuffer); + break; + } + else { + blankLine = true; + } + } + else if (c != '\r') { + // Carriage return (\r) is disregarded for blank line detection + blankLine = false; + } + } + } + delay(10); + // Close the connection: + client.stop(); + } +} + +// Create a simple text web page showing the environment data in +// separate category tables, using HTML and CSS +void assembleWebPage(void) { + sprintf(pageBuffer,"HTTP/1.1 200 OK\r\n" + "Content-type: text/html\r\n" + "Connection: close\r\n" + "Refresh: %u\r\n\r\n",refreshPeriodSeconds); + + strcat(pageBuffer,"" + "" + "Metriful Sensor Demo" + "" + "

Indoor Environment Data

"); + + ////////////////////////////////////// + + strcat(pageBuffer,"

Air Data

"); + + uint8_t T_intPart = 0; + uint8_t T_fractionalPart = 0; + bool isPositive = true; + const char * unit = getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); + sprintf(lineBuffer,"", + isPositive?"":"-", T_intPart, T_fractionalPart, unit); + strcat(pageBuffer,lineBuffer); + + sprintf(lineBuffer,"", airData.P_Pa); + strcat(pageBuffer,lineBuffer); + + sprintf(lineBuffer,"", + airData.H_pc_int, airData.H_pc_fr_1dp); + strcat(pageBuffer,lineBuffer); + + sprintf(lineBuffer,"" + "
Temperature%s%u.%u%s
Pressure%" PRIu32 "Pa
Humidity%u.%u%%
Gas Sensor Resistance%" PRIu32 "" OHM_SYMBOL "

", + airData.G_ohm); + strcat(pageBuffer,lineBuffer); + + ////////////////////////////////////// + + strcat(pageBuffer,"

Air Quality Data

"); + + if (airQualityData.AQI_accuracy == 0) { + sprintf(lineBuffer,"%s

",interpret_AQI_accuracy(airQualityData.AQI_accuracy)); + strcat(pageBuffer,lineBuffer); + } + else { + sprintf(lineBuffer,"", + airQualityData.AQI_int, airQualityData.AQI_fr_1dp); + strcat(pageBuffer,lineBuffer); + + sprintf(lineBuffer,"", + interpret_AQI_value(airQualityData.AQI_int)); + strcat(pageBuffer,lineBuffer); + + sprintf(lineBuffer,"", + airQualityData.CO2e_int, airQualityData.CO2e_fr_1dp); + strcat(pageBuffer,lineBuffer); + + sprintf(lineBuffer,"" + "
Air Quality Index%u.%u
Air Quality Summary%s
Estimated CO" SUBSCRIPT_2 "%u.%uppm
Equivalent Breath VOC%u.%02uppm

", + airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); + strcat(pageBuffer,lineBuffer); + } + + ////////////////////////////////////// + + strcat(pageBuffer,"

Sound Data

"); + + sprintf(lineBuffer,"" + "", + soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); + strcat(pageBuffer,lineBuffer); + + for (uint8_t i=0; i" + "", + i+1, sound_band_mids_Hz[i], soundData.SPL_bands_dB_int[i], soundData.SPL_bands_dB_fr_1dp[i]); + strcat(pageBuffer,lineBuffer); + } + + sprintf(lineBuffer,"" + "
A-weighted Sound Pressure Level%u.%udBA
Frequency Band %u (%u Hz) SPL%u.%udB
Peak Sound Amplitude%u.%02umPa

", + soundData.peak_amp_mPa_int, soundData.peak_amp_mPa_fr_2dp); + strcat(pageBuffer,lineBuffer); + + ////////////////////////////////////// + + strcat(pageBuffer,"

Light Data

"); + + sprintf(lineBuffer,"", + lightData.illum_lux_int, lightData.illum_lux_fr_2dp); + strcat(pageBuffer,lineBuffer); + + sprintf(lineBuffer,"" + "
Illuminance%u.%02ulux
White Light Level%u

", lightData.white); + strcat(pageBuffer,lineBuffer); + + ////////////////////////////////////// + + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + strcat(pageBuffer,"

Air Particulate Data

"); + + sprintf(lineBuffer,"", + particleData.duty_cycle_pc_int, particleData.duty_cycle_pc_fr_2dp); + strcat(pageBuffer,lineBuffer); + + char unitsBuffer[7] = {0}; + if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42) { + strcpy(unitsBuffer,"ppL"); + } + else if (PARTICLE_SENSOR == PARTICLE_SENSOR_SDS011) { + strcpy(unitsBuffer,SDS011_UNIT_SYMBOL); + } + else { + strcpy(unitsBuffer,"(?)"); + } + sprintf(lineBuffer,"" + "
Sensor Duty Cycle%u.%02u%%
Particle Concentration%u.%02u%s

", + particleData.concentration_int, particleData.concentration_fr_2dp, unitsBuffer); + strcat(pageBuffer,lineBuffer); + } + + ////////////////////////////////////// + + strcat(pageBuffer,""); +} diff --git a/lib/Metriful/src/Metriful_sensor.cpp b/lib/Metriful/src/Metriful_sensor.cpp new file mode 100644 index 0000000..bbec8fa --- /dev/null +++ b/lib/Metriful/src/Metriful_sensor.cpp @@ -0,0 +1,597 @@ +/* + Metriful_sensor.cpp + + This file defines functions which are used in the code examples. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include "Metriful_sensor.h" +#include "host_pin_definitions.h" + +// The Arduino Wire library has a limited internal buffer size: +#define ARDUINO_WIRE_BUFFER_LIMIT_BYTES 32 + +void SensorHardwareSetup(uint8_t i2c_7bit_address) { + + pinMode(LED_BUILTIN, OUTPUT); + + #ifdef ESP8266 + // Must specify the I2C pins + Wire.begin(SDA_PIN, SCL_PIN); + digitalWrite(LED_BUILTIN, HIGH); + #else + // Default I2C pins are used + Wire.begin(); + digitalWrite(LED_BUILTIN, LOW); + #endif + + Wire.setClock(I2C_CLK_FREQ_HZ); + + // READY, light interrupt and sound interrupt lines are digital inputs. + pinMode(READY_PIN, INPUT); + pinMode(L_INT_PIN, INPUT); + pinMode(S_INT_PIN, INPUT); + + // Set up interrupt monitoring of the READY signal, triggering on a falling edge + // event (high-to-low voltage change) indicating READY assertion. The + // function ready_ISR() will be called when this happens. + attachInterrupt(digitalPinToInterrupt(READY_PIN), ready_ISR, FALLING); + + // Start the serial port. + // Full settings are: 8 data bits, no parity, one stop bit + Serial.begin(SERIAL_BAUD_RATE); + + // Wait for the MS430 to finish power-on initialization: + while (digitalRead(READY_PIN) == HIGH) { + yield(); + } + + // Reset to clear any previous state: + TransmitI2C(i2c_7bit_address, RESET_CMD, 0, 0); + delay(5); + + // Wait for reset completion and entry to standby mode + while (digitalRead(READY_PIN) == HIGH) { + yield(); + } +} + +volatile bool ready_assertion_event = false; + +// This function is automatically called after a falling edge (assertion) of READY. +// The flag variable is set true - it must be set false again in the main program. +void ISR_ATTRIBUTE ready_ISR(void) { + ready_assertion_event = true; +} + +//////////////////////////////////////////////////////////////////////// + +// Functions to convert data from integer representation to floating-point representation. +// Floats are easy to use for writing programs but require greater memory and processing +// power resources, so may not always be appropriate. + +void convertAirDataF(const AirData_t * airData_in, AirData_F_t * airDataF_out) { + // Decode the signed value for T (in Celsius) + airDataF_out->T_C = convertEncodedTemperatureToFloat(airData_in->T_C_int_with_sign, + airData_in->T_C_fr_1dp); + airDataF_out->P_Pa = airData_in->P_Pa; + airDataF_out->H_pc = ((float) airData_in->H_pc_int) + (((float) airData_in->H_pc_fr_1dp)/10.0); + airDataF_out->G_Ohm = airData_in->G_ohm; +} + +void convertAirQualityDataF(const AirQualityData_t * airQualityData_in, + AirQualityData_F_t * airQualityDataF_out) { + airQualityDataF_out->AQI = ((float) airQualityData_in->AQI_int) + + (((float) airQualityData_in->AQI_fr_1dp)/10.0); + airQualityDataF_out->CO2e = ((float) airQualityData_in->CO2e_int) + + (((float) airQualityData_in->CO2e_fr_1dp)/10.0); + airQualityDataF_out->bVOC = ((float) airQualityData_in->bVOC_int) + + (((float) airQualityData_in->bVOC_fr_2dp)/100.0); + airQualityDataF_out->AQI_accuracy = airQualityData_in->AQI_accuracy; +} + +void convertLightDataF(const LightData_t * lightData_in, LightData_F_t * lightDataF_out) { + lightDataF_out->illum_lux = ((float) lightData_in->illum_lux_int) + + (((float) lightData_in->illum_lux_fr_2dp)/100.0); + lightDataF_out->white = lightData_in->white; +} + +void convertSoundDataF(const SoundData_t * soundData_in, SoundData_F_t * soundDataF_out) { + soundDataF_out->SPL_dBA = ((float) soundData_in->SPL_dBA_int) + + (((float) soundData_in->SPL_dBA_fr_1dp)/10.0); + for (uint16_t i=0; iSPL_bands_dB[i] = ((float) soundData_in->SPL_bands_dB_int[i]) + + (((float) soundData_in->SPL_bands_dB_fr_1dp[i])/10.0); + } + soundDataF_out->peakAmp_mPa = ((float) soundData_in->peak_amp_mPa_int) + + (((float) soundData_in->peak_amp_mPa_fr_2dp)/100.0); + soundDataF_out->stable = (soundData_in->stable == 1); +} + +void convertParticleDataF(const ParticleData_t * particleData_in, ParticleData_F_t * particleDataF_out) { + particleDataF_out->duty_cycle_pc = ((float) particleData_in->duty_cycle_pc_int) + + (((float) particleData_in->duty_cycle_pc_fr_2dp)/100.0); + particleDataF_out->concentration = ((float) particleData_in->concentration_int) + + (((float) particleData_in->concentration_fr_2dp)/100.0); + particleDataF_out->valid = (particleData_in->valid == 1); +} + +//////////////////////////////////////////////////////////////////////// + +// The following five functions print data (in floating-point +// representation) over the serial port as text + +void printAirDataF(const AirData_F_t * airDataF) { + Serial.print("Temperature = "); + #ifdef USE_FAHRENHEIT + float temperature_F = convertCtoF(airDataF->T_C); + Serial.print(temperature_F,1);Serial.println(" " FAHRENHEIT_SYMBOL); + #else + Serial.print(airDataF->T_C,1);Serial.println(" " CELSIUS_SYMBOL); + #endif + Serial.print("Pressure = ");Serial.print(airDataF->P_Pa);Serial.println(" Pa"); + Serial.print("Humidity = ");Serial.print(airDataF->H_pc,1);Serial.println(" %"); + Serial.print("Gas Sensor Resistance = ");Serial.print(airDataF->G_Ohm);Serial.println(" " OHM_SYMBOL); +} + +void printAirQualityDataF(const AirQualityData_F_t * airQualityDataF) { + if (airQualityDataF->AQI_accuracy > 0) { + Serial.print("Air Quality Index = ");Serial.print(airQualityDataF->AQI,1); + Serial.print(" ("); + Serial.print(interpret_AQI_value((uint16_t) airQualityDataF->AQI)); + Serial.println(")"); + Serial.print("Estimated CO" SUBSCRIPT_2 " = ");Serial.print(airQualityDataF->CO2e,1); + Serial.println(" ppm"); + Serial.print("Equivalent Breath VOC = ");Serial.print(airQualityDataF->bVOC,2); + Serial.println(" ppm"); + } + Serial.print("Air Quality Accuracy: "); + Serial.println(interpret_AQI_accuracy(airQualityDataF->AQI_accuracy)); +} + +void printLightDataF(const LightData_F_t * lightDataF) { + Serial.print("Illuminance = ");Serial.print(lightDataF->illum_lux,2);Serial.println(" lux"); + Serial.print("White Light Level = ");Serial.print(lightDataF->white);Serial.println(); +} + +void printSoundDataF(const SoundData_F_t * soundDataF) { + char strbuf[50] = {0}; + Serial.print("A-weighted Sound Pressure Level = "); + Serial.print(soundDataF->SPL_dBA,1);Serial.println(" dBA"); + for (uint16_t i=0; ivalid) { + Serial.println("Yes"); + } + else { + Serial.println("No (Initializing)"); + } +} + +//////////////////////////////////////////////////////////////////////// + +// The following five functions print data (in integer representation) over the serial port as text. +// printColumns determines the print format: +// choosing printColumns = false gives labeled values with measurement units +// choosing printColumns = true gives columns of numbers (convenient for spreadsheets). + +void printAirData(const AirData_t * airData, bool printColumns) { + char strbuf[50] = {0}; + + uint8_t T_intPart = 0; + uint8_t T_fractionalPart = 0; + bool isPositive = true; + const char * T_unit = getTemperature(airData, &T_intPart, &T_fractionalPart, &isPositive); + + if (printColumns) { + // Print: temperature, pressure/Pa, humidity/%, gas sensor resistance/ohm + sprintf(strbuf,"%s%u.%u %" PRIu32 " %u.%u %" PRIu32 " ",isPositive?"":"-", T_intPart, T_fractionalPart, + airData->P_Pa, airData->H_pc_int, airData->H_pc_fr_1dp, airData->G_ohm); + Serial.print(strbuf); + } + else { + sprintf(strbuf,"Temperature = %s%u.%u %s", isPositive?"":"-", T_intPart, T_fractionalPart, T_unit); + Serial.println(strbuf); + Serial.print("Pressure = ");Serial.print(airData->P_Pa);Serial.println(" Pa"); + sprintf(strbuf,"Humidity = %u.%u %%",airData->H_pc_int,airData->H_pc_fr_1dp); + Serial.println(strbuf); + Serial.print("Gas Sensor Resistance = ");Serial.print(airData->G_ohm);Serial.println(" " OHM_SYMBOL); + } +} + +void printAirQualityData(const AirQualityData_t * airQualityData, bool printColumns) { + char strbuf[50] = {0}; + if (printColumns) { + // Print: Air Quality Index, Estimated CO2/ppm, Equivalent breath VOC/ppm, Accuracy + sprintf(strbuf,"%u.%u %u.%u %u.%02u %u ",airQualityData->AQI_int, airQualityData->AQI_fr_1dp, + airQualityData->CO2e_int, airQualityData->CO2e_fr_1dp, + airQualityData->bVOC_int, airQualityData->bVOC_fr_2dp, airQualityData->AQI_accuracy); + Serial.print(strbuf); + } + else { + if (airQualityData->AQI_accuracy > 0) { + sprintf(strbuf,"Air Quality Index = %u.%u (%s)", + airQualityData->AQI_int, airQualityData->AQI_fr_1dp, interpret_AQI_value(airQualityData->AQI_int)); + Serial.println(strbuf); + sprintf(strbuf,"Estimated CO" SUBSCRIPT_2 " = %u.%u ppm", + airQualityData->CO2e_int, airQualityData->CO2e_fr_1dp); + Serial.println(strbuf); + sprintf(strbuf,"Equivalent Breath VOC = %u.%02u ppm", + airQualityData->bVOC_int, airQualityData->bVOC_fr_2dp); + Serial.println(strbuf); + } + Serial.print("Air Quality Accuracy: "); + Serial.println(interpret_AQI_accuracy(airQualityData->AQI_accuracy)); + } +} + +void printSoundData(const SoundData_t * soundData, bool printColumns) { + char strbuf[50] = {0}; + if (printColumns) { + // Print: Sound pressure level/dBA, Sound pressure level for frequency bands 1 to 6 (six columns), + // Peak sound amplitude/mPa, stability + sprintf(strbuf,"%u.%u ", soundData->SPL_dBA_int, soundData->SPL_dBA_fr_1dp); + Serial.print(strbuf); + for (uint16_t i=0; iSPL_bands_dB_int[i], soundData->SPL_bands_dB_fr_1dp[i]); + Serial.print(strbuf); + } + sprintf(strbuf,"%u.%02u %u ", soundData->peak_amp_mPa_int, + soundData->peak_amp_mPa_fr_2dp, soundData->stable); + Serial.print(strbuf); + } + else { + sprintf(strbuf,"A-weighted Sound Pressure Level = %u.%u dBA", + soundData->SPL_dBA_int, soundData->SPL_dBA_fr_1dp); + Serial.println(strbuf); + for (uint8_t i=0; iSPL_bands_dB_int[i], soundData->SPL_bands_dB_fr_1dp[i]); + Serial.println(strbuf); + } + sprintf(strbuf,"Peak Sound Amplitude = %u.%02u mPa", + soundData->peak_amp_mPa_int, soundData->peak_amp_mPa_fr_2dp); + Serial.println(strbuf); + } +} + +void printLightData(const LightData_t * lightData, bool printColumns) { + char strbuf[50] = {0}; + if (printColumns) { + // Print: illuminance/lux, white level + sprintf(strbuf,"%u.%02u %u ", lightData->illum_lux_int, lightData->illum_lux_fr_2dp, lightData->white); + Serial.print(strbuf); + } + else { + sprintf(strbuf,"Illuminance = %u.%02u lux", lightData->illum_lux_int, lightData->illum_lux_fr_2dp); + Serial.println(strbuf); + Serial.print("White Light Level = ");Serial.print(lightData->white);Serial.println(); + } +} + +void printParticleData(const ParticleData_t * particleData, bool printColumns, uint8_t particleSensor) { + char strbuf[50] = {0}; + if (printColumns) { + // Print: duty cycle/%, concentration + sprintf(strbuf,"%u.%02u %u.%02u %u ", particleData->duty_cycle_pc_int, + particleData->duty_cycle_pc_fr_2dp, particleData->concentration_int, + particleData->concentration_fr_2dp, particleData->valid); + Serial.print(strbuf); + } + else { + sprintf(strbuf,"Particle Duty Cycle = %u.%02u %%", + particleData->duty_cycle_pc_int, particleData->duty_cycle_pc_fr_2dp); + Serial.println(strbuf); + sprintf(strbuf,"Particle Concentration = %u.%02u ", + particleData->concentration_int, particleData->concentration_fr_2dp); + Serial.print(strbuf); + if (particleSensor == PARTICLE_SENSOR_PPD42) { + Serial.println("ppL"); + } + else if (particleSensor == PARTICLE_SENSOR_SDS011) { + Serial.println(SDS011_UNIT_SYMBOL); + } + else { + Serial.println("(?)"); + } + Serial.print("Particle data valid: "); + if (particleData->valid == 0) { + Serial.println("No (Initializing)"); + } + else { + Serial.println("Yes"); + } + } +} + +//////////////////////////////////////////////////////////////////////// + +// Send data to the Metriful MS430 using the I2C-compatible two wire interface. +// +// Returns true on success, false on failure. +// +// dev_addr_7bit = the 7-bit I2C address of the MS430 board. +// commandRegister = the settings register code or command code to be used. +// data = array containing the data to be sent; its length must be at least "data_length" bytes. +// data_length = the number of bytes from the "data" array to be sent. +// +bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length) { + + if (data_length > ARDUINO_WIRE_BUFFER_LIMIT_BYTES) { + // The Arduino Wire library has a limited internal buffer size + return false; + } + + Wire.beginTransmission(dev_addr_7bit); + uint8_t bytesWritten = Wire.write(commandRegister); + if (data_length > 0) { + bytesWritten += Wire.write(data, data_length); + } + if (bytesWritten != (data_length+1)) { + return false; + } + + return (Wire.endTransmission(true) == 0); +} + +// Read data from the Metriful MS430 using the I2C-compatible two wire interface. +// +// Returns true on success, false on failure. +// +// dev_addr_7bit = the 7-bit I2C address of the MS430 board. +// commandRegister = the settings register code or data location code to be used. +// data = array to store the received data; its length must be at least "data_length" bytes. +// data_length = the number of bytes to read. +// +bool ReceiveI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length) { + + if (data_length == 0) { + // Cannot do a zero byte read + return false; + } + + if (data_length > ARDUINO_WIRE_BUFFER_LIMIT_BYTES) { + // The Arduino Wire library has a limited internal buffer size + return false; + } + + Wire.beginTransmission(dev_addr_7bit); + Wire.write(commandRegister); + if (Wire.endTransmission(false) != 0) { + return false; + } + + if (Wire.requestFrom(dev_addr_7bit, data_length, (uint8_t) 1) != data_length) { + // Did not receive the expected number of bytes + return false; + } + + for (uint8_t i=0; i 0) { + data[i] = Wire.read(); + } + } + + return true; +} + +//////////////////////////////////////////////////////////////////////// + +// Provide a readable interpretation of the accuracy code for +// the air quality measurements (applies to all air quality data) +const char * interpret_AQI_accuracy(uint8_t AQI_accuracy_code) { + switch (AQI_accuracy_code) { + default: + case 0: + return "Not yet valid, self-calibration incomplete"; + case 1: + return "Low accuracy, self-calibration ongoing"; + case 2: + return "Medium accuracy, self-calibration ongoing"; + case 3: + return "High accuracy"; + } +} + +// Provide a readable interpretation of the AQI (air quality index) +const char * interpret_AQI_value(uint16_t AQI) { + if (AQI < 50) { + return "Good"; + } + else if (AQI < 100) { + return "Acceptable"; + } + else if (AQI < 150) { + return "Substandard"; + } + else if (AQI < 200) { + return "Poor"; + } + else if (AQI < 300) { + return "Bad"; + } + else { + return "Very bad"; + } +} + +// Set the threshold for triggering a sound interrupt. +// +// Returns true on success, false on failure. +// +// threshold_mPa = peak sound amplitude threshold in milliPascals, any 16-bit integer is allowed. +bool setSoundInterruptThreshold(uint8_t dev_addr_7bit, uint16_t threshold_mPa) { + uint8_t TXdata[SOUND_INTERRUPT_THRESHOLD_BYTES] = {0}; + TXdata[0] = (uint8_t) (threshold_mPa & 0x00FF); + TXdata[1] = (uint8_t) (threshold_mPa >> 8); + return TransmitI2C(dev_addr_7bit, SOUND_INTERRUPT_THRESHOLD_REG, TXdata, SOUND_INTERRUPT_THRESHOLD_BYTES); +} + +// Set the threshold for triggering a light interrupt. +// +// Returns true on success, false on failure. +// +// The threshold value in lux units can be fractional and is formed as: +// threshold = thres_lux_int + (thres_lux_fr_2dp/100) +// +// Threshold values exceeding MAX_LUX_VALUE will be limited to MAX_LUX_VALUE. +bool setLightInterruptThreshold(uint8_t dev_addr_7bit, uint16_t thres_lux_int, uint8_t thres_lux_fr_2dp) { + uint8_t TXdata[LIGHT_INTERRUPT_THRESHOLD_BYTES] = {0}; + TXdata[0] = (uint8_t) (thres_lux_int & 0x00FF); + TXdata[1] = (uint8_t) (thres_lux_int >> 8); + TXdata[2] = thres_lux_fr_2dp; + return TransmitI2C(dev_addr_7bit, LIGHT_INTERRUPT_THRESHOLD_REG, TXdata, LIGHT_INTERRUPT_THRESHOLD_BYTES); +} + +//////////////////////////////////////////////////////////////////////// + +// Convenience functions for reading data (integer representation) +// +// For each category of data (air, sound, etc.) a pointer to the data +// struct is passed to the ReceiveI2C() function. The received byte +// sequence fills the data struct in the correct order so that each +// field within the struct receives the value of an environmental data +// quantity (temperature, sound level, etc.) + +SoundData_t getSoundData(uint8_t i2c_7bit_address) { + SoundData_t soundData = {0}; + ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + return soundData; +} + +AirData_t getAirData(uint8_t i2c_7bit_address) { + AirData_t airData = {0}; + ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + return airData; +} + +LightData_t getLightData(uint8_t i2c_7bit_address) { + LightData_t lightData = {0}; + ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + return lightData; +} + +AirQualityData_t getAirQualityData(uint8_t i2c_7bit_address) { + AirQualityData_t airQualityData = {0}; + ReceiveI2C(i2c_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + return airQualityData; +} + +ParticleData_t getParticleData(uint8_t i2c_7bit_address) { + ParticleData_t particleData = {0}; + ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + return particleData; +} + +// Convenience functions for reading data (float representation) + +SoundData_F_t getSoundDataF(uint8_t i2c_7bit_address) { + SoundData_F_t soundDataF = {0}; + SoundData_t soundData = getSoundData(i2c_7bit_address); + convertSoundDataF(&soundData, &soundDataF); + return soundDataF; +} + +AirData_F_t getAirDataF(uint8_t i2c_7bit_address) { + AirData_F_t airDataF = {0}; + AirData_t airData = getAirData(i2c_7bit_address); + convertAirDataF(&airData, &airDataF); + return airDataF; +} + +LightData_F_t getLightDataF(uint8_t i2c_7bit_address) { + LightData_F_t lightDataF = {0}; + LightData_t lightData = getLightData(i2c_7bit_address); + convertLightDataF(&lightData, &lightDataF); + return lightDataF; +} + +AirQualityData_F_t getAirQualityDataF(uint8_t i2c_7bit_address) { + AirQualityData_F_t airQualityDataF = {0}; + AirQualityData_t airQualityData = getAirQualityData(i2c_7bit_address); + convertAirQualityDataF(&airQualityData, &airQualityDataF); + return airQualityDataF; +} + +ParticleData_F_t getParticleDataF(uint8_t i2c_7bit_address) { + ParticleData_F_t particleDataF = {0}; + ParticleData_t particleData = getParticleData(i2c_7bit_address); + convertParticleDataF(&particleData, &particleDataF); + return particleDataF; +} + +//////////////////////////////////////////////////////////////////////// + +// Functions to convert Celsius temperature to Fahrenheit, in float +// and integer formats + +float convertCtoF(float C) { + return ((C*1.8) + 32.0); +} + +// Convert Celsius to Fahrenheit in sign, integer and fractional parts +void convertCtoF_int(float C, uint8_t * F_int, uint8_t * F_fr_1dp, bool * isPositive) { + float F = convertCtoF(C); + bool isNegative = (F < 0.0); + if (isNegative) { + F = -F; + } + F += 0.05; + F_int[0] = (uint8_t) F; + F -= (float) F_int[0]; + F_fr_1dp[0] = (uint8_t) (F*10.0); + isPositive[0] = (!isNegative); +} + +// Decode and convert the temperature as read from the MS430 (integer +// representation) into a float value +float convertEncodedTemperatureToFloat(uint8_t T_C_int_with_sign, uint8_t T_C_fr_1dp) { + float temperature_C = ((float) (T_C_int_with_sign & TEMPERATURE_VALUE_MASK)) + + (((float) T_C_fr_1dp)/10.0); + if ((T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) { + // the most-significant bit is set, indicating that the temperature is negative + temperature_C = -temperature_C; + } + return temperature_C; +} + +// Obtain temperature, in chosen units (C or F), as sign, integer and fractional parts +const char * getTemperature(const AirData_t * pAirData, uint8_t * T_intPart, + uint8_t * T_fractionalPart, bool * isPositive) { + #ifdef USE_FAHRENHEIT + float temperature_C = convertEncodedTemperatureToFloat(pAirData->T_C_int_with_sign, + pAirData->T_C_fr_1dp); + convertCtoF_int(temperature_C, T_intPart, T_fractionalPart, isPositive); + return FAHRENHEIT_SYMBOL; + #else + isPositive[0] = ((pAirData->T_C_int_with_sign & TEMPERATURE_SIGN_MASK) == 0); + T_intPart[0] = pAirData->T_C_int_with_sign & TEMPERATURE_VALUE_MASK; + T_fractionalPart[0] = pAirData->T_C_fr_1dp; + return CELSIUS_SYMBOL; + #endif +} diff --git a/lib/Metriful/src/Metriful_sensor.h b/lib/Metriful/src/Metriful_sensor.h new file mode 100644 index 0000000..bd6690c --- /dev/null +++ b/lib/Metriful/src/Metriful_sensor.h @@ -0,0 +1,175 @@ +/* + Metriful_sensor.h + + This file declares functions and settings which are used in the code + examples. The function definitions are in file Metriful_sensor.cpp + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#ifndef METRIFUL_SENSOR_H +#define METRIFUL_SENSOR_H + +#include "Arduino.h" +#include +#include +#include +#include +#include +#include +#include "sensor_constants.h" +#include "host_pin_definitions.h" + +//////////////////////////////////////////////////////////////////////// + +// Choose to display output temperatures in Fahrenheit: +// un-comment the following line to use Fahrenheit +//#define USE_FAHRENHEIT + +// Specify which particle sensor is connected: +#define PARTICLE_SENSOR PARTICLE_SENSOR_PPD42 +// Define PARTICLE_SENSOR as: +// PARTICLE_SENSOR_PPD42 for the Shinyei PPD42 +// PARTICLE_SENSOR_SDS011 for the Nova SDS011 +// PARTICLE_SENSOR_OFF if no sensor is connected + +// The I2C address of the MS430 board. +#define I2C_ADDRESS I2C_ADDR_7BIT_SB_OPEN +// The default is I2C_ADDR_7BIT_SB_OPEN and must be changed to +// I2C_ADDR_7BIT_SB_CLOSED if the solder bridge SB1 on the board +// is soldered closed + +//////////////////////////////////////////////////////////////////////// + +#define I2C_CLK_FREQ_HZ 100000 +#define SERIAL_BAUD_RATE 9600 + +// Unicode symbol strings +#define CELSIUS_SYMBOL "\u00B0C" +#define FAHRENHEIT_SYMBOL "\u00B0F" +#define SDS011_UNIT_SYMBOL "\u00B5g/m\u00B3" +#define SUBSCRIPT_2 "\u2082" +#define OHM_SYMBOL "\u03A9" + +extern volatile bool ready_assertion_event; + +//////////////////////////////////////////////////////////////////////// + +// Data category structs containing floats. If floats are not wanted, +// use the integer-only struct versions in sensor_constants.h + +typedef struct { + float SPL_dBA; + float SPL_bands_dB[SOUND_FREQ_BANDS]; + float peakAmp_mPa; + bool stable; +} SoundData_F_t; + +typedef struct { + float T_C; + uint32_t P_Pa; + float H_pc; + uint32_t G_Ohm; +} AirData_F_t; + +typedef struct { + float AQI; + float CO2e; + float bVOC; + uint8_t AQI_accuracy; +} AirQualityData_F_t; + +typedef struct { + float illum_lux; + uint16_t white; +} LightData_F_t; + +typedef struct { + float duty_cycle_pc; + float concentration; + bool valid; +} ParticleData_F_t; + +//////////////////////////////////////////////////////////////////////// + +// Custom type used to select the particle sensor being used (if any) +typedef enum { + OFF = PARTICLE_SENSOR_OFF, + PPD42 = PARTICLE_SENSOR_PPD42, + SDS011 = PARTICLE_SENSOR_SDS011 +} ParticleSensor_t; + +// Struct used in the IFTTT example +typedef struct { + const char * variableName; + const char * measurementUnit; + int32_t thresHigh; + int32_t thresLow; + uint16_t inactiveCount; + const char * adviceHigh; + const char * adviceLow; +} ThresholdSetting_t; + +// Struct used in the Home Assistant example +typedef struct { + const char * name; + const char * unit; + const char * icon; + uint8_t decimalPlaces; +} HA_Attributes_t; + +//////////////////////////////////////////////////////////////////////// + +void SensorHardwareSetup(uint8_t i2c_7bit_address); +void ISR_ATTRIBUTE ready_ISR(void); + +bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length); +bool ReceiveI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length); + +const char * interpret_AQI_accuracy(uint8_t AQI_accuracy_code); +const char * interpret_AQI_value(uint16_t AQI); + +void convertAirDataF(const AirData_t * airData_in, AirData_F_t * airDataF_out); +void convertAirQualityDataF(const AirQualityData_t * airQualityData_in, + AirQualityData_F_t * airQualityDataF_out); +void convertLightDataF(const LightData_t * lightData_in, LightData_F_t * lightDataF_out); +void convertSoundDataF(const SoundData_t * soundData_in, SoundData_F_t * soundDataF_out); +void convertParticleDataF(const ParticleData_t * particleData_in, ParticleData_F_t * particleDataF_out); + +void printAirDataF(const AirData_F_t * airDataF); +void printAirQualityDataF(const AirQualityData_F_t * airQualityDataF); +void printLightDataF(const LightData_F_t * lightDataF); +void printSoundDataF(const SoundData_F_t * soundDataF); +void printParticleDataF(const ParticleData_F_t * particleDataF, uint8_t particleSensor); + +void printAirData(const AirData_t * airData, bool printColumns); +void printAirQualityData(const AirQualityData_t * airQualityData, bool printColumns); +void printLightData(const LightData_t * lightData, bool printColumns); +void printSoundData(const SoundData_t * soundData, bool printColumns); +void printParticleData(const ParticleData_t * particleData, bool printColumns, uint8_t particleSensor); + +bool setSoundInterruptThreshold(uint8_t dev_addr_7bit, uint16_t threshold_mPa); +bool setLightInterruptThreshold(uint8_t dev_addr_7bit, uint16_t thres_lux_int, uint8_t thres_lux_fr_2dp); + +SoundData_t getSoundData(uint8_t i2c_7bit_address); +AirData_t getAirData(uint8_t i2c_7bit_address); +LightData_t getLightData(uint8_t i2c_7bit_address); +AirQualityData_t getAirQualityData(uint8_t i2c_7bit_address); +ParticleData_t getParticleData(uint8_t i2c_7bit_address); + +SoundData_F_t getSoundDataF(uint8_t i2c_7bit_address); +AirData_F_t getAirDataF(uint8_t i2c_7bit_address); +LightData_F_t getLightDataF(uint8_t i2c_7bit_address); +AirQualityData_F_t getAirQualityDataF(uint8_t i2c_7bit_address); +ParticleData_F_t getParticleDataF(uint8_t i2c_7bit_address); + +float convertCtoF(float C); +void convertCtoF_int(float C, uint8_t * F_int, uint8_t * F_fr_1dp, bool * isPositive); +float convertEncodedTemperatureToFloat(uint8_t T_C_int_with_sign, uint8_t T_C_fr_1dp); +const char * getTemperature(const AirData_t * pAirData, uint8_t * T_intPart, + uint8_t * T_fractionalPart, bool * isPositive); +#endif diff --git a/lib/Metriful/src/WiFi_functions.cpp b/lib/Metriful/src/WiFi_functions.cpp new file mode 100644 index 0000000..9b887be --- /dev/null +++ b/lib/Metriful/src/WiFi_functions.cpp @@ -0,0 +1,92 @@ +/* + WiFi_functions.cpp + + This file defines functions used by examples connecting to, + or creating, a WiFi network. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include "host_pin_definitions.h" +#ifdef HAS_WIFI +#include "Arduino.h" +#include "WiFi_functions.h" + +// Repeatedly attempt to connect to the WiFi network using the input +// network name (SSID) and password. +void connectToWiFi(const char * SSID, const char * password) { + WiFi.disconnect(); + #if defined(ESP8266) || defined(ESP32) + WiFi.persistent(false); + WiFi.mode(WIFI_STA); + #endif + uint8_t wStatus = WL_DISCONNECTED; + while (wStatus != WL_CONNECTED) { + Serial.print("Attempting to connect to "); + Serial.println(SSID); + uint8_t statusChecks = 0; + WiFi.begin(SSID, password); + while ((wStatus != WL_CONNECTED) && (statusChecks < 8)) { + delay(1000); + Serial.print("."); + wStatus = WiFi.status(); + statusChecks++; + } + if (wStatus != WL_CONNECTED) { + Serial.println("Failed."); + WiFi.disconnect(); + delay(5000); + } + } + Serial.println("Connected."); +} + +// Configure the host as a WiFi access point, creating a WiFi network with +// specified network SSID (name), password and host IP address. +bool createWiFiAP(const char * SSID, const char * password, IPAddress hostIP) { + Serial.print("Creating access point named: "); + Serial.println(SSID); + #if defined(ESP8266) || defined(ESP32) + WiFi.persistent(false); + WiFi.mode(WIFI_AP); + IPAddress subnet(255,255,255,0); + bool success = WiFi.softAP(SSID, password); + delay(2000); + success = success && WiFi.softAPConfig(hostIP, hostIP, subnet); + #else + WiFi.config(hostIP); + bool success = (WiFi.beginAP(SSID, password) == WL_AP_LISTENING); + #endif + return success; +} + +// Provide a readable interpretation of the WiFi status. +// statusCode is the value returned by WiFi.status() +const char * interpret_WiFi_status(uint8_t statusCode) { + switch (statusCode) { + case WL_CONNECTED: + return "Connected"; + case WL_NO_SHIELD: + return "No shield"; + case WL_IDLE_STATUS: + return "Idle"; + case WL_NO_SSID_AVAIL: + return "No SSID available"; + case WL_SCAN_COMPLETED: + return "Scan completed"; + case WL_CONNECT_FAILED: + return "Connect failed"; + case WL_CONNECTION_LOST: + return "Connection lost"; + case WL_DISCONNECTED: + return "Disconnected"; + default: + return "Unknown"; + } +} + +#endif diff --git a/lib/Metriful/src/WiFi_functions.h b/lib/Metriful/src/WiFi_functions.h new file mode 100644 index 0000000..69d98fe --- /dev/null +++ b/lib/Metriful/src/WiFi_functions.h @@ -0,0 +1,26 @@ +/* + WiFi_functions.h + + This file declares functions used by examples connecting to, + or creating, a WiFi network. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include "host_pin_definitions.h" +#ifdef HAS_WIFI +#ifndef WIFI_FUNCTIONS_H +#define WIFI_FUNCTIONS_H + +#include + +void connectToWiFi(const char * SSID, const char * password); +bool createWiFiAP(const char * SSID, const char * password, IPAddress hostIP); +const char * interpret_WiFi_status(uint8_t statusCode); + +#endif +#endif diff --git a/lib/Metriful/src/graph_web_page.h b/lib/Metriful/src/graph_web_page.h new file mode 100644 index 0000000..a976491 --- /dev/null +++ b/lib/Metriful/src/graph_web_page.h @@ -0,0 +1,57 @@ +/* + graph_web_page.h + + This file contains the web page code which is used to display graphs + as part of the graph_web_server example. This is the code from the + file 'graph_web_page.html' (HTML/CSS/javascript), which has been + minified and put into a C character string. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#ifndef GRAPH_WEB_PAGE_H +#define GRAPH_WEB_PAGE_H + +const char * graphWebPage = "HTTP/1.1 200 OK\r\n" + "Content-type: text/html\r\n" + "Connection: close\r\n\r\n" +"" +"" +"" +"" +"" +"Indoor Environment Data" +"" +"" +"" +"" +"" +"

Indoor Environment Data

" +"
" +"
Incomplete load: please refresh the page.
" +"
" +"
" +"
" +"" +"
" +"
" +"

sensor.metriful.com

" +"
" +"" +"" +"" +""; + +#endif diff --git a/lib/Metriful/src/graph_web_page.html b/lib/Metriful/src/graph_web_page.html new file mode 100644 index 0000000..2259f91 --- /dev/null +++ b/lib/Metriful/src/graph_web_page.html @@ -0,0 +1,333 @@ + + + + + +Indoor Environment Data + + + + + +

Indoor Environment Data

+
+
Incomplete load: please refresh the page.
+
+
+
+ +
+
+

sensor.metriful.com

+
+ + + + diff --git a/lib/Metriful/src/host_pin_definitions.h b/lib/Metriful/src/host_pin_definitions.h new file mode 100644 index 0000000..eebef88 --- /dev/null +++ b/lib/Metriful/src/host_pin_definitions.h @@ -0,0 +1,212 @@ +/* + host_pin_definitions.h + + This file defines which host pins are used to interface to the + Metriful MS430 board. The relevant file section is selected + automatically when the board is chosen in the Arduino IDE. + + More detail is provided in the readme and User Guide. + + This file provides settings for the following host systems: + * Arduino Uno + * Arduino Nano 33 IoT + * Arduino Nano + * Arduino MKR WiFi 1010 + * ESP8266 (tested on NodeMCU and Wemos D1 Mini - other boards may require changes) + * ESP32 (tested on DOIT ESP32 DEVKIT V1 - other boards may require changes) + + The Metriful MS430 is compatible with many more development boards + than those listed. You can use this file as a guide to define the + necessary settings for other host systems. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#ifndef ARDUINO_PIN_DEFINITIONS_H +#define ARDUINO_PIN_DEFINITIONS_H + +#ifdef ARDUINO_AVR_UNO + + // Arduino Uno + + #define ISR_ATTRIBUTE + + #define READY_PIN 2 // Arduino digital pin 2 connects to RDY + #define L_INT_PIN 4 // Arduino digital pin 4 connects to LIT + #define S_INT_PIN 7 // Arduino digital pin 7 connects to SIT + /* Also make the following connections: + Arduino pins GND, SCL, SDA to MS430 pins GND, SCL, SDA + Arduino pin 5V to MS430 pins VPU and VIN + MS430 pin VDD is unused + + If a PPD42 particle sensor is used, connect the following: + Arduino pin 5V to PPD42 pin 3 + Arduino pin GND to PPD42 pin 1 + PPD42 pin 4 to MS430 pin PRT + + If an SDS011 particle sensor is used, connect the following: + Arduino pin 5V to SDS011 pin "5V" + Arduino pin GND to SDS011 pin "GND" + SDS011 pin "25um" to MS430 pin PRT + */ + +#elif defined ARDUINO_SAMD_NANO_33_IOT + + // Arduino Nano 33 IoT + + #include + #include + #define HAS_WIFI + #define ISR_ATTRIBUTE + + #define READY_PIN 11 // Arduino pin D11 connects to RDY + #define L_INT_PIN A1 // Arduino pin A1 connects to LIT + #define S_INT_PIN A2 // Arduino pin A2 connects to SIT + /* Also make the following connections: + Arduino pin GND to MS430 pin GND + Arduino pin 3.3V to MS430 pins VPU and VDD + Arduino pin A5 to MS430 pin SCL + Arduino pin A4 to MS430 pin SDA + MS430 pin VIN is unused + + If a PPD42 particle sensor is used, connect the following: + Arduino pin VUSB to PPD42 pin 3 + Arduino pin GND to PPD42 pin 1 + PPD42 pin 4 to MS430 pin PRT + + If an SDS011 particle sensor is used, connect the following: + Arduino pin VUSB to SDS011 pin "5V" + Arduino pin GND to SDS011 pin "GND" + SDS011 pin "25um" to MS430 pin PRT + + The solder bridge labeled "VUSB" on the underside of the Arduino + must be soldered closed to provide 5V to the PPD42/SDS011. + */ + +#elif defined ARDUINO_AVR_NANO + + // Arduino Nano + + #define ISR_ATTRIBUTE + + #define READY_PIN 2 // Arduino pin D2 connects to RDY + #define L_INT_PIN 4 // Arduino pin D4 connects to LIT + #define S_INT_PIN 7 // Arduino pin D7 connects to SIT + /* Also make the following connections: + Arduino pin GND to MS430 pin GND + Arduino pin A5 (SCL) to MS430 pin SCL + Arduino pin A4 (SDA) to MS430 pin SDA + Arduino pin 5V to MS430 pins VPU and VIN + MS430 pin VDD is unused + + If a PPD42 particle sensor is used, connect the following: + Arduino pin 5V to PPD42 pin 3 + Arduino pin GND to PPD42 pin 1 + PPD42 pin 4 to MS430 pin PRT + + If an SDS011 particle sensor is used, connect the following: + Arduino pin 5V to SDS011 pin "5V" + Arduino pin GND to SDS011 pin "GND" + SDS011 pin "25um" to MS430 pin PRT + */ + +#elif defined ARDUINO_SAMD_MKRWIFI1010 + + // Arduino MKR WiFi 1010 + + #include + #include + #define HAS_WIFI + #define ISR_ATTRIBUTE + + #define READY_PIN 0 // Arduino digital pin 0 connects to RDY + #define L_INT_PIN 4 // Arduino digital pin 4 connects to LIT + #define S_INT_PIN 5 // Arduino digital pin 5 connects to SIT + /* Also make the following connections: + Arduino pin GND to MS430 pin GND + Arduino pin D12 (SCL) to MS430 pin SCL + Arduino pin D11 (SDA) to MS430 pin SDA + Arduino pin VCC (3.3V) to MS430 pins VPU and VDD + MS430 pin VIN is unused + + If a PPD42 particle sensor is used, connect the following: + Arduino pin 5V to PPD42 pin 3 + Arduino pin GND to PPD42 pin 1 + PPD42 pin 4 to MS430 pin PRT + + If an SDS011 particle sensor is used, connect the following: + Arduino pin 5V to SDS011 pin "5V" + Arduino pin GND to SDS011 pin "GND" + SDS011 pin "25um" to MS430 pin PRT + */ + +#elif defined ESP8266 + + // The examples have been tested on NodeMCU and Wemos D1 Mini. + // Other ESP8266 boards may require changes. + + #include + #define HAS_WIFI + #define ISR_ATTRIBUTE ICACHE_RAM_ATTR + + #define SDA_PIN 5 // GPIO5 (labeled D1) connects to SDA + #define SCL_PIN 4 // GPIO4 (labeled D2) connects to SCL + #define READY_PIN 12 // GPIO12 (labeled D6) connects to RDY + #define L_INT_PIN 0 // GPIO0 (labeled D3) connects to LIT + #define S_INT_PIN 14 // GPIO14 (labeled D5) connects to SIT + /* Also make the following connections: + ESP8266 pin GND to MS430 pin GND + ESP8266 pin 3V3 to MS430 pins VPU and VDD + MS430 pin VIN is unused + + If a PPD42 particle sensor is used, also connect the following: + ESP8266 pin Vin (may be labeled Vin or 5V or VU) to PPD42 pin 3 + ESP8266 pin GND to PPD42 pin 1 + PPD42 pin 4 to MS430 pin PRT + + If an SDS011 particle sensor is used, connect the following: + ESP8266 pin Vin (may be labeled Vin or 5V or VU) to SDS011 pin "5V" + ESP8266 pin GND to SDS011 pin "GND" + SDS011 pin "25um" to MS430 pin PRT + */ + +#elif defined ESP32 + + // The examples have been tested on DOIT ESP32 DEVKIT V1 development board. + // Other ESP32 boards may require changes. + + #include + #define HAS_WIFI + #define ISR_ATTRIBUTE IRAM_ATTR + + #define READY_PIN 23 // Pin D23 connects to RDY + #define L_INT_PIN 18 // Pin D18 connects to LIT + #define S_INT_PIN 19 // Pin D19 connects to SIT + /* Also make the following connections: + ESP32 pin D21 to MS430 pin SDA + ESP32 pin D22 to MS430 pin SCL + ESP32 pin GND to MS430 pin GND + ESP32 pin 3V3 to MS430 pins VPU and VDD + MS430 pin VIN is unused + + If a PPD42 particle sensor is used, also connect the following: + ESP32 pin Vin to PPD42 pin 3 + ESP32 pin GND to PPD42 pin 1 + PPD42 pin 4 to MS430 pin PRT + + If an SDS011 particle sensor is used, connect the following: + ESP32 pin Vin to SDS011 pin "5V" + ESP32 pin GND to SDS011 pin "GND" + SDS011 pin "25um" to MS430 pin PRT + */ + +#else + #error ("Your development board is not directly supported") + // Please make a new section in this file to define the correct input/output pins +#endif + +#endif diff --git a/lib/Metriful/src/sensor_constants.h b/lib/Metriful/src/sensor_constants.h new file mode 100644 index 0000000..c93ecd7 --- /dev/null +++ b/lib/Metriful/src/sensor_constants.h @@ -0,0 +1,208 @@ +/* + sensor_constants.h + + This file defines constant values and data structures which are used + in the control of the Metriful MS430 board and the interpretation of + its output data. All values have been taken from the MS430 datasheet. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#ifndef SENSOR_CONSTANTS_H +#define SENSOR_CONSTANTS_H + +#include + +/////////////////////////////////////////////////////////// +// Register and command addresses: + +// Settings registers +#define PARTICLE_SENSOR_SELECT_REG 0x07 + +#define LIGHT_INTERRUPT_ENABLE_REG 0x81 +#define LIGHT_INTERRUPT_THRESHOLD_REG 0x82 +#define LIGHT_INTERRUPT_TYPE_REG 0x83 +#define LIGHT_INTERRUPT_POLARITY_REG 0x84 + +#define SOUND_INTERRUPT_ENABLE_REG 0x85 +#define SOUND_INTERRUPT_THRESHOLD_REG 0x86 +#define SOUND_INTERRUPT_TYPE_REG 0x87 + +#define CYCLE_TIME_PERIOD_REG 0x89 + +// Executable commands +#define ON_DEMAND_MEASURE_CMD 0xE1 +#define RESET_CMD 0xE2 +#define CYCLE_MODE_CMD 0xE4 +#define STANDBY_MODE_CMD 0xE5 +#define LIGHT_INTERRUPT_CLR_CMD 0xE6 +#define SOUND_INTERRUPT_CLR_CMD 0xE7 + +// Read the operational mode +#define OP_MODE_READ 0x8A + +// Read data for whole categories +#define AIR_DATA_READ 0x10 +#define AIR_QUALITY_DATA_READ 0x11 +#define LIGHT_DATA_READ 0x12 +#define SOUND_DATA_READ 0x13 +#define PARTICLE_DATA_READ 0x14 + +// Read individual data quantities +#define T_READ 0x21 +#define P_READ 0x22 +#define H_READ 0x23 +#define G_READ 0x24 + +#define AQI_READ 0x25 +#define CO2E_READ 0x26 +#define BVOC_READ 0x27 +#define AQI_ACCURACY_READ 0x28 + +#define ILLUMINANCE_READ 0x31 +#define WHITE_LIGHT_READ 0x32 + +#define SPL_READ 0x41 +#define SPL_BANDS_READ 0x42 +#define SOUND_PEAK_READ 0x43 +#define SOUND_STABLE_READ 0x44 + +#define DUTY_CYCLE_READ 0x51 +#define CONCENTRATION_READ 0x52 +#define PARTICLE_VALID_READ 0x53 + +/////////////////////////////////////////////////////////// + +// I2C address of sensor board: can select using solder bridge +#define I2C_ADDR_7BIT_SB_OPEN 0x71 // if solder bridge is left open +#define I2C_ADDR_7BIT_SB_CLOSED 0x70 // if solder bridge is soldered closed + +// Values for enabling/disabling of sensor functions +#define ENABLED 1 +#define DISABLED 0 + +// Device modes +#define STANDBY_MODE 0 +#define CYCLE_MODE 1 + +// Sizes of data expected when setting interrupt thresholds +#define LIGHT_INTERRUPT_THRESHOLD_BYTES 3 +#define SOUND_INTERRUPT_THRESHOLD_BYTES 2 + +// Frequency bands for sound level measurement +#define SOUND_FREQ_BANDS 6 +static const uint16_t sound_band_mids_Hz[SOUND_FREQ_BANDS] = {125, 250, 500, 1000, 2000, 4000}; +static const uint16_t sound_band_edges_Hz[SOUND_FREQ_BANDS+1] = {88, 177, 354, 707, 1414, 2828, 5657}; + +// Cycle mode time period +#define CYCLE_PERIOD_3_S 0 +#define CYCLE_PERIOD_100_S 1 +#define CYCLE_PERIOD_300_S 2 + +// Sound interrupt type: +#define SOUND_INT_TYPE_LATCH 0 +#define SOUND_INT_TYPE_COMP 1 + +// Maximum for illuminance measurement and threshold setting +#define MAX_LUX_VALUE 3774 + +// Light interrupt type: +#define LIGHT_INT_TYPE_LATCH 0 +#define LIGHT_INT_TYPE_COMP 1 + +// Light interrupt polarity: +#define LIGHT_INT_POL_POSITIVE 0 +#define LIGHT_INT_POL_NEGATIVE 1 + +// Decoding the temperature integer.fraction value format +#define TEMPERATURE_VALUE_MASK 0x7F +#define TEMPERATURE_SIGN_MASK 0x80 + +// Particle sensor module selection: +#define PARTICLE_SENSOR_OFF 0 +#define PARTICLE_SENSOR_PPD42 1 +#define PARTICLE_SENSOR_SDS011 2 + +/////////////////////////////////////////////////////////// + +// Structs for accessing individual data quantities after reading a category of data + +typedef struct __attribute__((packed)) { + uint8_t T_C_int_with_sign; + uint8_t T_C_fr_1dp; + uint32_t P_Pa; + uint8_t H_pc_int; + uint8_t H_pc_fr_1dp; + uint32_t G_ohm; +} AirData_t; + +typedef struct __attribute__((packed)) { + uint16_t AQI_int; + uint8_t AQI_fr_1dp; + uint16_t CO2e_int; + uint8_t CO2e_fr_1dp; + uint16_t bVOC_int; + uint8_t bVOC_fr_2dp; + uint8_t AQI_accuracy; +} AirQualityData_t; + +typedef struct __attribute__((packed)) { + uint16_t illum_lux_int; + uint8_t illum_lux_fr_2dp; + uint16_t white; +} LightData_t; + +typedef struct __attribute__((packed)) { + uint8_t SPL_dBA_int; + uint8_t SPL_dBA_fr_1dp; + uint8_t SPL_bands_dB_int[SOUND_FREQ_BANDS]; + uint8_t SPL_bands_dB_fr_1dp[SOUND_FREQ_BANDS]; + uint16_t peak_amp_mPa_int; + uint8_t peak_amp_mPa_fr_2dp; + uint8_t stable; +} SoundData_t; + +typedef struct __attribute__((packed)) { + uint8_t duty_cycle_pc_int; + uint8_t duty_cycle_pc_fr_2dp; + uint16_t concentration_int; + uint8_t concentration_fr_2dp; + uint8_t valid; +} ParticleData_t; + +/////////////////////////////////////////////////////////// + +// Byte lengths for each readable data quantity and data category + +#define T_BYTES 2 +#define P_BYTES 4 +#define H_BYTES 2 +#define G_BYTES 4 +#define AIR_DATA_BYTES sizeof(AirData_t) + +#define AQI_BYTES 3 +#define CO2E_BYTES 3 +#define BVOC_BYTES 3 +#define AQI_ACCURACY_BYTES 1 +#define AIR_QUALITY_DATA_BYTES sizeof(AirQualityData_t) + +#define ILLUMINANCE_BYTES 3 +#define WHITE_BYTES 2 +#define LIGHT_DATA_BYTES sizeof(LightData_t) + +#define SPL_BYTES 2 +#define SPL_BANDS_BYTES (2*SOUND_FREQ_BANDS) +#define SOUND_PEAK_BYTES 3 +#define SOUND_STABLE_BYTES 1 +#define SOUND_DATA_BYTES sizeof(SoundData_t) + +#define DUTY_CYCLE_BYTES 2 +#define CONCENTRATION_BYTES 3 +#define PARTICLE_VALID_BYTES 1 +#define PARTICLE_DATA_BYTES sizeof(ParticleData_t) + +#endif diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..e560ea9 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,31 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env] +monitor_speed = 9600 + +[env:nano_33_iot] +platform = atmelsam +board = nano_33_iot +framework = arduino + +lib_deps = + WiFiNINA + MQTT + +[env:d1_mini] +platform = espressif8266 +board = d1_mini +framework = arduino +upload_protocol = esptool + +lib_deps = + MQTT + diff --git a/src/config.h-example b/src/config.h-example new file mode 100644 index 0000000..d3f5cb0 --- /dev/null +++ b/src/config.h-example @@ -0,0 +1,24 @@ +#include + +// How often to read and report the data (every 3, 100 or 300 seconds) +#define CYCLE_PERIOD = CYCLE_PERIOD_3_S + +// The details of the WiFi network: +#define WIFI_SSID = "..." // network SSID (name, case sensitive) +#define WIFI_PASSWORD = "..." // network password + +// Home Assistant settings + +// You must have already installed Home Assistant on a computer on your +// network. Go to www.home-assistant.io for help on this. + +// Choose a unique name for this MS430 sensor board so you can identify it. +// Variables in HA will have names like: SENSOR_NAME.temperature, etc. +#define SENSOR_NAME "metriful_living_room" + +// Change this to the IP address of the computer running Home Assistant. +// You can find this from the admin interface of your router. +#define HOME_ASSISTANT_IP "..." + +// Security access token: the Readme and User Guide explain how to get this +#define LONG_LIVED_ACCESS_TOKEN "..." diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..3cd5736 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,178 @@ +#include +#include +#include +#include "config.h" + +#if !defined(HAS_WIFI) +#error ("This example program has been created for specific WiFi enabled hosts only.") +#endif + +WiFiClient client; + +// Buffers for assembling http POST requests +char postBuffer[450] = {0}; +char fieldBuffer[70] = {0}; + +// Structs for data +AirData_t airData = {0}; +AirQualityData_t airQualityData = {0}; +LightData_t lightData = {0}; +ParticleData_t particleData = {0}; +SoundData_t soundData = {0}; + +// Define the display attributes of data sent to Home Assistant. +// The chosen name, unit and icon will appear in on the overview +// dashboard in Home Assistant. The icons can be chosen from +// https://cdn.materialdesignicons.com/5.3.45/ +// (remove the "mdi-" part from the icon name). +// The attribute fields are: {name, unit, icon, decimal places} +HA_Attributes_t pressure = {"Pressure","Pa","weather-cloudy",0}; +HA_Attributes_t humidity = {"Humidity","%","water-percent",1}; +HA_Attributes_t illuminance = {"Illuminance","lx","white-balance-sunny",2}; +HA_Attributes_t soundLevel = {"Sound level","dBA","microphone",1}; +HA_Attributes_t peakAmplitude = {"Sound peak","mPa","waveform",2}; +HA_Attributes_t estimatedCO2 = {"Estimated CO2","ppm","chart-bubble",1}; +HA_Attributes_t equivalentBreathVOC = {"Equivalent breath VOC","ppm","chart-bubble",2}; +HA_Attributes_t AQI = {"Air Quality Index"," ","thought-bubble-outline",1}; +HA_Attributes_t AQ_assessment = {"Air quality assessment","","flower-tulip",0}; +HA_Attributes_t AQ_calibration = {"Air quality calibration","","flower-tulip",0}; +#if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42) + HA_Attributes_t particulates = {"Particle concentration","ppL","chart-bubble",0}; +#else + HA_Attributes_t particulates = {"Particle concentration",SDS011_UNIT_SYMBOL,"chart-bubble",2}; +#endif +#ifdef USE_FAHRENHEIT + HA_Attributes_t temperature = {"Temperature",FAHRENHEIT_SYMBOL,"thermometer",1}; +#else + HA_Attributes_t temperature = {"Temperature",CELSIUS_SYMBOL,"thermometer",1}; +#endif + + +void setup() { + // Initialize the host's pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + connectToWiFi(WIFI_SSID, WIFI_PASSWORD); + + // Apply settings to the MS430 and enter cycle mode + uint8_t particleSensorCode = PARTICLE_SENSOR; + uint8_t cycle_period = CYCLE_PERIOD; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensorCode, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); +} + +// Send the data to Home Assistant as an HTTP POST request. +void http_POST_Home_Assistant(const HA_Attributes_t * attributes, const char * valueText) { + client.stop(); + if (client.connect(HOME_ASSISTANT_IP, 8123)) { + // Form the URL from the name but replace spaces with underscores + strcpy(fieldBuffer,attributes->name); + for (uint8_t i=0; iunit, attributes->name, attributes->icon); + + sprintf(fieldBuffer,"Content-Length: %u", strlen(postBuffer)); + client.println(fieldBuffer); + client.println(); + client.print(postBuffer); + } + else { + Serial.println("Client connection failed."); + } +} + +// Send numeric data with specified sign, integer and fractional parts +void sendNumericData(const HA_Attributes_t * attributes, uint32_t valueInteger, + uint8_t valueDecimal, bool isPositive) { + char valueText[20] = {0}; + const char * sign = isPositive ? "" : "-"; + switch (attributes->decimalPlaces) { + case 0: + default: + sprintf(valueText,"%s%" PRIu32, sign, valueInteger); + break; + case 1: + sprintf(valueText,"%s%" PRIu32 ".%u", sign, valueInteger, valueDecimal); + break; + case 2: + sprintf(valueText,"%s%" PRIu32 ".%02u", sign, valueInteger, valueDecimal); + break; + } + http_POST_Home_Assistant(attributes, valueText); +} + +// Send a text string: must have quotation marks added +void sendTextData(const HA_Attributes_t * attributes, const char * valueText) { + char quotedText[20] = {0}; + sprintf(quotedText,"\"%s\"", valueText); + http_POST_Home_Assistant(attributes, quotedText); +} + +void loop() { + + // Wait for the next new data release, indicated by a falling edge on READY + Serial.println("Waiting for new data ..."); + while (!ready_assertion_event) { + yield(); + } + ready_assertion_event = false; + + // Read data from the MS430 into the data structs. + Serial.println("Reading data from sensors"); + ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + + // Check that WiFi is still connected + uint8_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + // There is a problem with the WiFi connection: attempt to reconnect. + Serial.print("Wifi status: "); + Serial.println(interpret_WiFi_status(wifiStatus)); + connectToWiFi(WIFI_SSID, WIFI_PASSWORD); + ready_assertion_event = false; + } + + uint8_t T_intPart = 0; + uint8_t T_fractionalPart = 0; + bool isPositive = true; + getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); + + // Send data to Home Assistant + Serial.println("Send data to Home Assistant"); + sendNumericData(&temperature, (uint32_t) T_intPart, T_fractionalPart, isPositive); + sendNumericData(&pressure, (uint32_t) airData.P_Pa, 0, true); + sendNumericData(&humidity, (uint32_t) airData.H_pc_int, airData.H_pc_fr_1dp, true); + sendNumericData(&illuminance, (uint32_t) lightData.illum_lux_int, lightData.illum_lux_fr_2dp, true); + sendNumericData(&soundLevel, (uint32_t) soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp, true); + sendNumericData(&peakAmplitude, (uint32_t) soundData.peak_amp_mPa_int, + soundData.peak_amp_mPa_fr_2dp, true); + sendNumericData(&AQI, (uint32_t) airQualityData.AQI_int, airQualityData.AQI_fr_1dp, true); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + sendNumericData(&particulates, (uint32_t) particleData.concentration_int, + particleData.concentration_fr_2dp, true); + } + sendTextData(&AQ_assessment, interpret_AQI_value(airQualityData.AQI_int)); + sendNumericData(&estimatedCO2, (uint32_t) airQualityData.CO2e_int, airQualityData.CO2e_fr_1dp, true); + sendNumericData(&equivalentBreathVOC, (uint32_t) airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp, true); + sendNumericData(&AQ_calibration, airQualityData.AQI_accuracy, 0, true); + + printAirData(&airData, false); + printParticleData(&particleData, false, PARTICLE_SENSOR); +} \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..b94d089 --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html