From c7430f56db8f0bdb6a40dfa3481507c5be5f2625 Mon Sep 17 00:00:00 2001
From: Maurice Makaay
Date: Thu, 7 Jan 2021 11:57:11 +0100
Subject: [PATCH] Initial import.
---
.gitignore | 6 +
.vscode/extensions.json | 7 +
include/README | 39 ++
.../Home_Assistant/Home_Assistant.ino | 218 +++++++
lib/Metriful/examples/IFTTT/IFTTT.ino | 190 ++++++
.../IoT_cloud_logging/IoT_cloud_logging.ino | 284 +++++++++
.../examples/cycle_readout/cycle_readout.ino | 111 ++++
.../graph_web_server/graph_web_server.ino | 366 +++++++++++
.../examples/interrupts/interrupts.ino | 132 ++++
.../on_demand_readout/on_demand_readout.ino | 105 +++
.../particle_sensor_toggle.ino | 166 +++++
.../simple_read_T_H/simple_read_T_H.ino | 150 +++++
.../simple_read_sound/simple_read_sound.ino | 98 +++
.../examples/web_server/web_server.ino | 380 +++++++++++
lib/Metriful/src/Metriful_sensor.cpp | 597 ++++++++++++++++++
lib/Metriful/src/Metriful_sensor.h | 175 +++++
lib/Metriful/src/WiFi_functions.cpp | 92 +++
lib/Metriful/src/WiFi_functions.h | 26 +
lib/Metriful/src/graph_web_page.h | 57 ++
lib/Metriful/src/graph_web_page.html | 333 ++++++++++
lib/Metriful/src/host_pin_definitions.h | 212 +++++++
lib/Metriful/src/sensor_constants.h | 208 ++++++
lib/README | 46 ++
platformio.ini | 31 +
src/config.h-example | 24 +
src/main.cpp | 178 ++++++
test/README | 11 +
27 files changed, 4242 insertions(+)
create mode 100644 .gitignore
create mode 100644 .vscode/extensions.json
create mode 100644 include/README
create mode 100644 lib/Metriful/examples/Home_Assistant/Home_Assistant.ino
create mode 100644 lib/Metriful/examples/IFTTT/IFTTT.ino
create mode 100644 lib/Metriful/examples/IoT_cloud_logging/IoT_cloud_logging.ino
create mode 100644 lib/Metriful/examples/cycle_readout/cycle_readout.ino
create mode 100644 lib/Metriful/examples/graph_web_server/graph_web_server.ino
create mode 100644 lib/Metriful/examples/interrupts/interrupts.ino
create mode 100644 lib/Metriful/examples/on_demand_readout/on_demand_readout.ino
create mode 100644 lib/Metriful/examples/particle_sensor_toggle/particle_sensor_toggle.ino
create mode 100644 lib/Metriful/examples/simple_read_T_H/simple_read_T_H.ino
create mode 100644 lib/Metriful/examples/simple_read_sound/simple_read_sound.ino
create mode 100644 lib/Metriful/examples/web_server/web_server.ino
create mode 100644 lib/Metriful/src/Metriful_sensor.cpp
create mode 100644 lib/Metriful/src/Metriful_sensor.h
create mode 100644 lib/Metriful/src/WiFi_functions.cpp
create mode 100644 lib/Metriful/src/WiFi_functions.h
create mode 100644 lib/Metriful/src/graph_web_page.h
create mode 100644 lib/Metriful/src/graph_web_page.html
create mode 100644 lib/Metriful/src/host_pin_definitions.h
create mode 100644 lib/Metriful/src/sensor_constants.h
create mode 100644 lib/README
create mode 100644 platformio.ini
create mode 100644 src/config.h-example
create mode 100644 src/main.cpp
create mode 100644 test/README
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"
+ ""
+ "