From 00200c90789d6cb1597371cec4875692ff64474c Mon Sep 17 00:00:00 2001
From: metriful
Date: Mon, 16 Nov 2020 18:26:04 +0000
Subject: [PATCH] v3.1.0
---
.../Home_Assistant/Home_Assistant.ino | 218 ++++++++
Arduino/Examples/IFTTT/IFTTT.ino | 190 +++++++
.../IoT_cloud_logging/IoT_cloud_logging.ino | 169 +++---
.../Examples/cycle_readout/cycle_readout.ino | 51 +-
.../graph_web_server/graph_web_server.ino | 366 +++++++++++++
Arduino/Examples/interrupts/interrupts.ino | 25 +-
.../on_demand_readout/on_demand_readout.ino | 48 +-
.../particle_sensor_toggle.ino | 50 +-
.../simple_read_T_H/simple_read_T_H.ino | 160 +++---
.../simple_read_sound/simple_read_sound.ino | 80 +--
Arduino/Examples/web_server/web_server.ino | 283 +++++-----
Arduino/Metriful_Sensor/Metriful_sensor.cpp | 291 ++++++----
Arduino/Metriful_Sensor/Metriful_sensor.h | 86 ++-
Arduino/Metriful_Sensor/WiFi_functions.cpp | 92 ++++
Arduino/Metriful_Sensor/WiFi_functions.h | 26 +
Arduino/Metriful_Sensor/graph_web_page.h | 57 ++
Arduino/Metriful_Sensor/graph_web_page.html | 333 ++++++++++++
.../Metriful_Sensor/host_pin_definitions.h | 150 +++---
CHANGELOG.md | 34 ++
Python/GraphViewer.py | 233 ++++++++
Python/Raspberry_Pi/Home_Assistant.py | 119 +++++
Python/Raspberry_Pi/IFTTT.py | 146 +++++
.../Raspberry_Pi}/IoT_cloud_logging.py | 49 +-
.../Raspberry_Pi}/cycle_readout.py | 35 +-
Python/Raspberry_Pi/graph_web_server.py | 136 +++++
.../Raspberry_Pi}/interrupts.py | 6 +-
.../Raspberry_Pi}/log_data_to_file.py | 53 +-
.../Raspberry_Pi}/on_demand_readout.py | 38 +-
.../Raspberry_Pi}/particle_sensor_toggle.py | 56 +-
.../Raspberry_Pi/sensor_package/__init__.py | 1 +
.../sensor_package/graph_web_page.html | 333 ++++++++++++
.../sensor_package}/sensor_constants.py | 0
.../sensor_package}/sensor_functions.py | 232 +++++---
Python/Raspberry_Pi/sensor_package/servers.py | 212 ++++++++
Python/Raspberry_Pi/simple_read_T_H.py | 99 ++++
.../Raspberry_Pi}/simple_read_sound.py | 39 +-
Python/Raspberry_Pi/web_server.py | 123 +++++
Python/graph_viewer_I2C.py | 159 ++++++
Python/graph_viewer_serial.py | 138 +++++
README.md | 498 +++++++++++++++---
Raspberry_Pi/simple_read_T_H.py | 100 ----
TROUBLESHOOTING.md | 92 ++++
User_guide.pdf | Bin 569590 -> 572639 bytes
pictures/graph_viewer.png | Bin 0 -> 60245 bytes
pictures/graph_web_server.png | Bin 0 -> 36333 bytes
pictures/group.png | Bin 0 -> 332355 bytes
pictures/home_assistant.png | Bin 0 -> 54783 bytes
pictures/tago.png | Bin 0 -> 50131 bytes
sensor_pcb.png | Bin 128106 -> 0 bytes
49 files changed, 4577 insertions(+), 1029 deletions(-)
create mode 100644 Arduino/Examples/Home_Assistant/Home_Assistant.ino
create mode 100644 Arduino/Examples/IFTTT/IFTTT.ino
create mode 100644 Arduino/Examples/graph_web_server/graph_web_server.ino
create mode 100644 Arduino/Metriful_Sensor/WiFi_functions.cpp
create mode 100644 Arduino/Metriful_Sensor/WiFi_functions.h
create mode 100644 Arduino/Metriful_Sensor/graph_web_page.h
create mode 100644 Arduino/Metriful_Sensor/graph_web_page.html
create mode 100644 CHANGELOG.md
create mode 100644 Python/GraphViewer.py
create mode 100644 Python/Raspberry_Pi/Home_Assistant.py
create mode 100644 Python/Raspberry_Pi/IFTTT.py
rename {Raspberry_Pi => Python/Raspberry_Pi}/IoT_cloud_logging.py (80%)
rename {Raspberry_Pi => Python/Raspberry_Pi}/cycle_readout.py (67%)
create mode 100644 Python/Raspberry_Pi/graph_web_server.py
rename {Raspberry_Pi => Python/Raspberry_Pi}/interrupts.py (95%)
rename {Raspberry_Pi => Python/Raspberry_Pi}/log_data_to_file.py (71%)
rename {Raspberry_Pi => Python/Raspberry_Pi}/on_demand_readout.py (66%)
rename {Raspberry_Pi => Python/Raspberry_Pi}/particle_sensor_toggle.py (70%)
create mode 100644 Python/Raspberry_Pi/sensor_package/__init__.py
create mode 100644 Python/Raspberry_Pi/sensor_package/graph_web_page.html
rename {Raspberry_Pi => Python/Raspberry_Pi/sensor_package}/sensor_constants.py (100%)
rename {Raspberry_Pi => Python/Raspberry_Pi/sensor_package}/sensor_functions.py (53%)
create mode 100644 Python/Raspberry_Pi/sensor_package/servers.py
create mode 100644 Python/Raspberry_Pi/simple_read_T_H.py
rename {Raspberry_Pi => Python/Raspberry_Pi}/simple_read_sound.py (57%)
create mode 100644 Python/Raspberry_Pi/web_server.py
create mode 100644 Python/graph_viewer_I2C.py
create mode 100644 Python/graph_viewer_serial.py
delete mode 100644 Raspberry_Pi/simple_read_T_H.py
create mode 100644 TROUBLESHOOTING.md
create mode 100644 pictures/graph_viewer.png
create mode 100644 pictures/graph_web_server.png
create mode 100644 pictures/group.png
create mode 100644 pictures/home_assistant.png
create mode 100644 pictures/tago.png
delete mode 100644 sensor_pcb.png
diff --git a/Arduino/Examples/Home_Assistant/Home_Assistant.ino b/Arduino/Examples/Home_Assistant/Home_Assistant.ino
new file mode 100644
index 0000000..651dbfe
--- /dev/null
+++ b/Arduino/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/Arduino/Examples/IFTTT/IFTTT.ino b/Arduino/Examples/IFTTT/IFTTT.ino
new file mode 100644
index 0000000..ff27276
--- /dev/null
+++ b/Arduino/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/Arduino/Examples/IoT_cloud_logging/IoT_cloud_logging.ino b/Arduino/Examples/IoT_cloud_logging/IoT_cloud_logging.ino
index bee5682..60ecd79 100644
--- a/Arduino/Examples/IoT_cloud_logging/IoT_cloud_logging.ino
+++ b/Arduino/Examples/IoT_cloud_logging/IoT_cloud_logging.ino
@@ -6,7 +6,8 @@
This example is designed for the following WiFi enabled hosts:
* Arduino Nano 33 IoT
* Arduino MKR WiFi 1010
- * NodeMCU ESP8266
+ * 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
@@ -21,24 +22,19 @@
*/
#include
+#include
//////////////////////////////////////////////////////////
// USER-EDITABLE SETTINGS
-// How often to read and log data (every 3, 100, 300 seconds)
+// 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 I2C address of the Metriful board
-uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN;
-
-// Which particle sensor is attached (PPD42, SDS011, or OFF)
-ParticleSensor_t particleSensor = OFF;
-
-// The details of the WiFi network to connect to:
+// 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
+char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password
// IoT cloud settings
// This example uses the free IoT cloud hosting services provided
@@ -49,7 +45,7 @@ char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password
// 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 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
@@ -59,7 +55,7 @@ bool useTagoCloud = true;
// END OF USER-EDITABLE SETTINGS
//////////////////////////////////////////////////////////
-#if !defined(ARDUINO_SAMD_NANO_33_IOT) && !defined(ARDUINO_SAMD_MKRWIFI1010) && !defined(ESP8266)
+#if !defined(HAS_WIFI)
#error ("This example program has been created for specific WiFi enabled hosts only.")
#endif
@@ -67,7 +63,7 @@ WiFiClient client;
// Buffers for assembling http POST requests
char postBuffer[450] = {0};
-char fieldBuffer[60] = {0};
+char fieldBuffer[70] = {0};
// Structs for data
AirData_t airData = {0};
@@ -76,36 +72,20 @@ LightData_t lightData = {0};
ParticleData_t particleData = {0};
SoundData_t soundData = {0};
-uint8_t transmit_buffer[1] = {0};
-
-
void setup() {
// Initialize the host's pins, set up the serial port and reset:
- SensorHardwareSetup(i2c_7bit_address);
+ SensorHardwareSetup(I2C_ADDRESS);
- // Attempt to connect to the Wifi network:
- Serial.print("Connecting to ");
- Serial.println(SSID);
- WiFi.begin(SSID, password);
- while (WiFi.status() != WL_CONNECTED) {
- delay(500);
- Serial.print(".");
- }
- Serial.println("Connected.");
-
- ////////////////////////////////////////////////////////////////////
+ connectToWiFi(SSID, password);
// Apply chosen settings to the MS430
- if (particleSensor != OFF) {
- transmit_buffer[0] = particleSensor;
- TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
- }
- transmit_buffer[0] = cycle_period;
- TransmitI2C(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1);
+ 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_7bit_address, CYCLE_MODE_CMD, 0, 0);
+ TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0);
}
@@ -125,48 +105,56 @@ void loop() {
*/
// Air data
- ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
+ // 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_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
+ ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
// Light data
- ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
+ ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
// Sound data
- ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
+ 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 (particleSensor != OFF) {
- ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
+ if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
+ ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
}
-
- if (WiFi.status() != WL_CONNECTED) {
- Serial.println("Wifi connection lost: attempting to reconnect.");
- WiFi.begin(SSID, password);
+
+ // 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 {
- if (useTagoCloud) {
- http_POST_data_Tago_cloud();
- }
- else {
- http_POST_data_Thingspeak_cloud();
- }
+ http_POST_data_Thingspeak_cloud();
}
}
+
/* For both example cloud providers, the following quantities will be sent:
-1 Temperature/C
+1 Temperature (C or F)
2 Pressure/Pa
3 Humidity/%
4 Air quality index
@@ -183,93 +171,86 @@ void loop() {
// 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_positive_integer = airData.T_C_int_with_sign & TEMPERATURE_VALUE_MASK;
- // If the most-significant bit is set, the temperature is negative (below 0 C)
- if ((airData.T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) {
- // The bit is set: celsius temperature is negative
- sprintf(postBuffer,"[{\"variable\":\"temperature\",\"value\":-%u.%u}",
- T_positive_integer, airData.T_C_fr_1dp);
- }
- else {
- // The bit is not set: celsius temperature is positive
- sprintf(postBuffer,"[{\"variable\":\"temperature\",\"value\":%u.%u}",
- T_positive_integer, airData.T_C_fr_1dp);
- }
- sprintf(fieldBuffer,",{\"variable\":\"pressure\",\"value\":%lu}", airData.P_Pa);
+ 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);
+ 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);
+ airQualityData.AQI_int, airQualityData.AQI_fr_1dp);
strcat(postBuffer, fieldBuffer);
sprintf(fieldBuffer,",{\"variable\":\"aqi_string\",\"value\":\"%s\"}",
- interpret_AQI_value(airQualityData.AQI_int));
+ interpret_AQI_value(airQualityData.AQI_int));
strcat(postBuffer, fieldBuffer);
sprintf(fieldBuffer,",{\"variable\":\"bvoc\",\"value\":%u.%02u}",
- airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp);
+ 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);
+ 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);
+ 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);
+ 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);
+ lightData.illum_lux_int, lightData.illum_lux_fr_2dp);
strcat(postBuffer, fieldBuffer);
- int len = strlen(postBuffer);
+ size_t len = strlen(postBuffer);
sprintf(fieldBuffer,"Content-Length: %u",len);
- client.println(fieldBuffer);
- client.println();
+ client.println(fieldBuffer);
+ client.println();
client.print(postBuffer);
}
else {
- Serial.println("Connection failed");
+ 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) {
- if (client.connect("api.thingspeak.com", 80)) {
+ 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");
- uint8_t T_positive_integer = airData.T_C_int_with_sign & TEMPERATURE_VALUE_MASK;
- // If the most-significant bit is set, the temperature is negative (below 0 C)
- if ((airData.T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) {
- // The bit is set: celsius temperature is negative
- sprintf(postBuffer,"api_key=" THINGSPEAK_API_KEY_STRING "&field1=-%u.%u",
- T_positive_integer, airData.T_C_fr_1dp);
- }
- else {
- // The bit is not set: celsius temperature is positive
- sprintf(postBuffer,"api_key=" THINGSPEAK_API_KEY_STRING "&field1=%u.%u",
- T_positive_integer, airData.T_C_fr_1dp);
- }
+ 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=%lu", airData.P_Pa);
+ sprintf(fieldBuffer,"&field2=%" PRIu32, airData.P_Pa);
strcat(postBuffer, fieldBuffer);
sprintf(fieldBuffer,"&field3=%u.%u", airData.H_pc_int, airData.H_pc_fr_1dp);
@@ -291,13 +272,13 @@ void http_POST_data_Thingspeak_cloud(void) {
particleData.concentration_fr_2dp);
strcat(postBuffer, fieldBuffer);
- int len = strlen(postBuffer);
+ size_t len = strlen(postBuffer);
sprintf(fieldBuffer,"Content-Length: %u",len);
client.println(fieldBuffer);
client.println();
client.print(postBuffer);
}
else {
- Serial.println("Connection failed");
+ Serial.println("Client connection failed.");
}
}
diff --git a/Arduino/Examples/cycle_readout/cycle_readout.ino b/Arduino/Examples/cycle_readout/cycle_readout.ino
index 40ddc38..9f53595 100644
--- a/Arduino/Examples/cycle_readout/cycle_readout.ino
+++ b/Arduino/Examples/cycle_readout/cycle_readout.ino
@@ -7,6 +7,9 @@
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
@@ -19,15 +22,9 @@
//////////////////////////////////////////////////////////
// USER-EDITABLE SETTINGS
-// How often to read data (every 3, 100, 300 seconds)
+// How often to read data (every 3, 100, or 300 seconds)
uint8_t cycle_period = CYCLE_PERIOD_3_S;
-// The I2C address of the Metriful board
-uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN;
-
-// Which particle sensor is attached (PPD42, SDS011, or OFF)
-ParticleSensor_t particleSensor = OFF;
-
// 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.
@@ -36,8 +33,6 @@ bool printDataAsColumns = false;
// END OF USER-EDITABLE SETTINGS
//////////////////////////////////////////////////////////
-uint8_t transmit_buffer[1] = {0};
-
// Structs for data
AirData_t airData = {0};
AirQualityData_t airQualityData = {0};
@@ -48,15 +43,12 @@ ParticleData_t particleData = {0};
void setup() {
// Initialize the host pins, set up the serial port and reset:
- SensorHardwareSetup(i2c_7bit_address);
+ SensorHardwareSetup(I2C_ADDRESS);
// Apply chosen settings to the MS430
- if (particleSensor != OFF) {
- transmit_buffer[0] = particleSensor;
- TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
- }
- transmit_buffer[0] = cycle_period;
- TransmitI2C(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1);
+ 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) {
@@ -65,7 +57,7 @@ void setup() {
Serial.println("Entering cycle mode and waiting for data.");
ready_assertion_event = false;
- TransmitI2C(i2c_7bit_address, CYCLE_MODE_CMD, 0, 0);
+ TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0);
}
@@ -76,38 +68,35 @@ void loop() {
}
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.)
- */
+ // Read data from the MS430 into the data structs.
// Air data
- ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
+ // 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.
*/
- ReceiveI2C(i2c_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
+ airQualityData = getAirQualityData(I2C_ADDRESS);
// Light data
- ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
+ lightData = getLightData(I2C_ADDRESS);
// Sound data
- ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
+ 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 (particleSensor != OFF) {
- ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
+ if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
+ particleData = getParticleData(I2C_ADDRESS);
}
// Print all data to the serial port
@@ -115,8 +104,8 @@ void loop() {
printAirQualityData(&airQualityData, printDataAsColumns);
printLightData(&lightData, printDataAsColumns);
printSoundData(&soundData, printDataAsColumns);
- if (particleSensor != OFF) {
- printParticleData(&particleData, printDataAsColumns, particleSensor);
+ if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
+ printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR);
}
Serial.println();
}
diff --git a/Arduino/Examples/graph_web_server/graph_web_server.ino b/Arduino/Examples/graph_web_server/graph_web_server.ino
new file mode 100644
index 0000000..5ff48a6
--- /dev/null
+++ b/Arduino/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/Arduino/Examples/interrupts/interrupts.ino b/Arduino/Examples/interrupts/interrupts.ino
index 03f1190..37bab65 100644
--- a/Arduino/Examples/interrupts/interrupts.ino
+++ b/Arduino/Examples/interrupts/interrupts.ino
@@ -4,7 +4,7 @@
Example code for using the Metriful MS430 interrupt outputs.
Light and sound interrupts are configured and the program then
- waits indefinitely. When an interrupt occurs, a message prints over
+ 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.
@@ -43,9 +43,6 @@ bool enableSoundInterrupts = true;
uint8_t sound_int_type = SOUND_INT_TYPE_LATCH;
uint16_t sound_thres_mPa = 100;
-// The I2C address of the Metriful board
-uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN;
-
// END OF USER-EDITABLE SETTINGS
//////////////////////////////////////////////////////////
@@ -54,7 +51,7 @@ uint8_t transmit_buffer[1] = {0};
void setup() {
// Initialize the host pins, set up the serial port and reset
- SensorHardwareSetup(i2c_7bit_address);
+ SensorHardwareSetup(I2C_ADDRESS);
// check that the chosen light threshold is a valid value
if (light_int_thres_lux_i > MAX_LUX_VALUE) {
@@ -74,31 +71,31 @@ void setup() {
if (enableSoundInterrupts) {
// Set the interrupt type (latch or comparator)
transmit_buffer[0] = sound_int_type;
- TransmitI2C(i2c_7bit_address, SOUND_INTERRUPT_TYPE_REG, transmit_buffer, 1);
+ TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_TYPE_REG, transmit_buffer, 1);
// Set the threshold
- setSoundInterruptThreshold(i2c_7bit_address, sound_thres_mPa);
+ setSoundInterruptThreshold(I2C_ADDRESS, sound_thres_mPa);
// Enable the interrupt
transmit_buffer[0] = ENABLED;
- TransmitI2C(i2c_7bit_address, SOUND_INTERRUPT_ENABLE_REG, transmit_buffer, 1);
+ 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_7bit_address, LIGHT_INTERRUPT_TYPE_REG, transmit_buffer, 1);
+ TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_TYPE_REG, transmit_buffer, 1);
// Set the threshold
- setLightInterruptThreshold(i2c_7bit_address, light_int_thres_lux_i, light_int_thres_lux_f2dp);
+ 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_7bit_address, LIGHT_INTERRUPT_POLARITY_REG, transmit_buffer, 1);
+ TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_POLARITY_REG, transmit_buffer, 1);
// Enable the interrupt
transmit_buffer[0] = ENABLED;
- TransmitI2C(i2c_7bit_address, LIGHT_INTERRUPT_ENABLE_REG, transmit_buffer, 1);
+ TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_ENABLE_REG, transmit_buffer, 1);
}
// Wait for the serial port to be ready, for displaying the output
@@ -118,7 +115,7 @@ void loop() {
Serial.println("LIGHT INTERRUPT.");
if (light_int_type == LIGHT_INT_TYPE_LATCH) {
// Latch type interrupts remain set until cleared by command
- TransmitI2C(i2c_7bit_address, LIGHT_INTERRUPT_CLR_CMD, 0, 0);
+ TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_CLR_CMD, 0, 0);
}
}
@@ -127,7 +124,7 @@ void loop() {
Serial.println("SOUND INTERRUPT.");
if (sound_int_type == SOUND_INT_TYPE_LATCH) {
// Latch type interrupts remain set until cleared by command
- TransmitI2C(i2c_7bit_address, SOUND_INTERRUPT_CLR_CMD, 0, 0);
+ TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_CLR_CMD, 0, 0);
}
}
diff --git a/Arduino/Examples/on_demand_readout/on_demand_readout.ino b/Arduino/Examples/on_demand_readout/on_demand_readout.ino
index abc71c3..8509dde 100644
--- a/Arduino/Examples/on_demand_readout/on_demand_readout.ino
+++ b/Arduino/Examples/on_demand_readout/on_demand_readout.ino
@@ -21,26 +21,18 @@
// Pause (in milliseconds) between data measurements (note that the
// measurement itself takes 0.5 seconds)
-uint32_t pause_ms = 3500;
+uint32_t pause_ms = 4500;
// Choosing a pause of less than 2000 ms will cause inaccurate
// temperature, humidity and particle data.
-// The I2C address of the Metriful board
-uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN;
-
-// Which particle sensor is attached (PPD42, SDS011, or OFF)
-ParticleSensor_t particleSensor = OFF;
-
// 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 = true;
+bool printDataAsColumns = false;
// END OF USER-EDITABLE SETTINGS
//////////////////////////////////////////////////////////
-uint8_t transmit_buffer[1] = {0};
-
// Structs for data
AirData_t airData = {0};
LightData_t lightData = {0};
@@ -50,12 +42,10 @@ ParticleData_t particleData = {0};
void setup() {
// Initialize the host pins, set up the serial port and reset:
- SensorHardwareSetup(i2c_7bit_address);
+ SensorHardwareSetup(I2C_ADDRESS);
- if (particleSensor != OFF) {
- transmit_buffer[0] = particleSensor;
- TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
- }
+ 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) {
@@ -68,50 +58,48 @@ void loop() {
// Trigger a new measurement
ready_assertion_event = false;
- TransmitI2C(i2c_7bit_address, ON_DEMAND_MEASURE_CMD, 0, 0);
+ 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.
- 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.)
- */
+
+ // Read data from the MS430 into the data structs.
// Air data
- ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
+ // 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
- ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
+ lightData = getLightData(I2C_ADDRESS);
// Sound data
- ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
+ 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 (particleSensor != OFF) {
- ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
+ 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 (particleSensor != OFF) {
- printParticleData(&particleData, printDataAsColumns, particleSensor);
+ 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/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino b/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino
index 064ce0a..d9a8722 100644
--- a/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino
+++ b/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino
@@ -5,14 +5,14 @@
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.
+ 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 fan noise.
+ 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
@@ -31,25 +31,18 @@
// its data.
uint8_t cycle_period = CYCLE_PERIOD_100_S;
-// The I2C address of the Metriful board
-uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN;
-
// 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 = true;
-
-// Which particle sensor is attached (PPD42, SDS011, or OFF)
-ParticleSensor_t particleSensor = SDS011;
+bool printDataAsColumns = false;
// Particle sensor power control options
-uint8_t off_cycles = 1; // leave the sensor off for this many cycles between reads
+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 (use this for
-// 3.3 V hosts). false means 0V is output to turn the sensor on (use
-// this for 5 V hosts). The User Guide gives example switching circuits.
+// 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
//////////////////////////////////////////////////////////
@@ -69,7 +62,7 @@ uint8_t particleSensor_count = 0;
void setup() {
// Initialize the host pins, set up the serial port and reset:
- SensorHardwareSetup(i2c_7bit_address);
+ SensorHardwareSetup(I2C_ADDRESS);
// Set up the particle sensor control, and turn it off initially
pinMode(particle_sensor_control_pin, OUTPUT);
@@ -77,12 +70,10 @@ void setup() {
particleSensorIsOn = false;
// Apply chosen settings to the MS430
- if (particleSensor != OFF) {
- transmit_buffer[0] = particleSensor;
- TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
- }
+ transmit_buffer[0] = PARTICLE_SENSOR;
+ TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
transmit_buffer[0] = cycle_period;
- TransmitI2C(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1);
+ TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1);
// Wait for the serial port to be ready, for displaying the output
while (!Serial) {
@@ -91,7 +82,7 @@ void setup() {
Serial.println("Entering cycle mode and waiting for data.");
ready_assertion_event = false;
- TransmitI2C(i2c_7bit_address, CYCLE_MODE_CMD, 0, 0);
+ TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0);
}
@@ -110,32 +101,33 @@ void loop() {
*/
// Air data
- ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
+ 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_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
+ ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
// Light data
- ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
+ 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_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
+ 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_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
+ 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
@@ -144,14 +136,14 @@ void loop() {
printAirQualityData(&airQualityData, printDataAsColumns);
printLightData(&lightData, printDataAsColumns);
printSoundData(&soundData, printDataAsColumns);
- printParticleData(&particleData, printDataAsColumns, particleSensor);
+ 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_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
+ TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
// Turn off the hardware:
digitalWrite(particle_sensor_control_pin, !particle_sensor_ON_state);
@@ -164,8 +156,8 @@ void loop() {
digitalWrite(particle_sensor_control_pin, particle_sensor_ON_state);
// Start the particle detection on the MS430
- transmit_buffer[0] = particleSensor;
- TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
+ transmit_buffer[0] = PARTICLE_SENSOR;
+ TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
particleSensor_count = 0;
particleSensorIsOn = true;
diff --git a/Arduino/Examples/simple_read_T_H/simple_read_T_H.ino b/Arduino/Examples/simple_read_T_H/simple_read_T_H.ino
index f1d9d97..c2ff5eb 100644
--- a/Arduino/Examples/simple_read_T_H/simple_read_T_H.ino
+++ b/Arduino/Examples/simple_read_T_H/simple_read_T_H.ino
@@ -4,10 +4,9 @@
Example code for using the Metriful MS430 to measure humidity
and temperature.
- Measures and displays the humidity and temperature, demonstrating
- the decoding of the signed temperature value. The data are also
- read out and displayed a second time, using a “data category read”
- of all Air measurement data. View the output in the Serial Monitor.
+ 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
@@ -18,24 +17,10 @@
#include
-//////////////////////////////////////////////////////////
-// USER-EDITABLE SETTINGS
-// The I2C address of the Metriful board
-uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN;
-
-// Whether to use floating point representation of numbers (uses more
-// host resources)
-bool useFloatingPoint = false;
-
-// END OF USER-EDITABLE SETTINGS
-//////////////////////////////////////////////////////////
-
-uint8_t receive_buffer[2] = {0};
-
-void setup() {
+void setup() {
// Initialize the host pins, set up the serial port and reset:
- SensorHardwareSetup(i2c_7bit_address);
+ SensorHardwareSetup(I2C_ADDRESS);
// Wait for the serial port to be ready, for displaying the output
while (!Serial) {
@@ -46,81 +31,118 @@ void setup() {
ready_assertion_event = false;
// Initiate an on-demand data measurement
- TransmitI2C(i2c_7bit_address, ON_DEMAND_MEASURE_CMD, 0, 0);
+ TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0);
// Now wait for the ready signal before continuing
while (!ready_assertion_event) {
yield();
}
-
- // We now know that data are ready to read.
+
+ // We know that new data are ready to read.
////////////////////////////////////////////////////////////////////
- // HUMIDITY
+ // 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("-----------------------------");
- // Read the humidity value from the sensor board
- ReceiveI2C(i2c_7bit_address, H_READ, receive_buffer, H_BYTES);
- // Decode the humidity: the first byte is the integer part, the
- // second byte is the fractional part to one decimal place.
+ // 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.print(humidity_integer);
+ Serial.print(".");
+ Serial.print(humidity_fraction);
+ Serial.println(" %");
+
+ Serial.println("-----------------------------");
+
- // TEMPERATURE
+ // 6. Advanced: read and decode only the temperature value from the MS430
- // Read the temperature value from the sensor board
- ReceiveI2C(i2c_7bit_address, T_READ, receive_buffer, T_BYTES);
+ // Read the raw temperature data
+ ReceiveI2C(I2C_ADDRESS, T_READ, receive_buffer, T_BYTES);
- // Decode and print the temperature:
+ // 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 byte with TEMPERATURE_VALUE_MASK
+ // 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 byte is the fractional part to one decimal place
+ // 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 is set, the temperature is negative (below 0 C)
- if ((receive_buffer[0] & TEMPERATURE_SIGN_MASK) == 0) {
- // The bit is not set: celsius temperature is positive
- Serial.print("+");
- }
- else {
- // The bit is set: celsius temperature is negative
+ // 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(" C");
-
- ////////////////////////////////////////////////////////////////////
-
- // AIR DATA
+ Serial.print(temperature_positive_integer);
+ Serial.print(".");
+ Serial.print(temperature_fraction);
+ Serial.println(" " CELSIUS_SYMBOL);
- // Rather than reading individual data values as shown above, whole
- // categories of data can be read in one I2C transaction
-
- // Read all Air data in one transaction, interpreting the received bytes as an AirData_t struct:
- AirData_t airData = {0};
- ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
-
- // Print the values over the serial port
- if (useFloatingPoint) {
- // Convert values where necessary to floating point representation:
- AirData_F_t airDataF = {0};
- convertAirDataF(&airData, &airDataF);
- printAirDataF(&airDataF);
- }
- else {
- // The data remain in integer representation
- printAirData(&airData, false);
- }
}
void loop() {
diff --git a/Arduino/Examples/simple_read_sound/simple_read_sound.ino b/Arduino/Examples/simple_read_sound/simple_read_sound.ino
index 3bb10df..01d537a 100644
--- a/Arduino/Examples/simple_read_sound/simple_read_sound.ino
+++ b/Arduino/Examples/simple_read_sound/simple_read_sound.ino
@@ -3,8 +3,9 @@
Example code for using the Metriful MS430 to measure sound.
- Measures and displays the sound data once.
- View the output in the Serial Monitor.
+ 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
@@ -15,24 +16,10 @@
#include
-//////////////////////////////////////////////////////////
-// USER-EDITABLE SETTINGS
-
-// The I2C address of the Metriful board
-uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN;
-
-// Whether to use floating point representation of numbers (uses more
-// host resources)
-bool useFloatingPoint = false;
-
-// END OF USER-EDITABLE SETTINGS
-//////////////////////////////////////////////////////////
-
-uint8_t receive_buffer[1] = {0};
void setup() {
// Initialize the host pins, set up the serial port and reset:
- SensorHardwareSetup(i2c_7bit_address);
+ SensorHardwareSetup(I2C_ADDRESS);
// Wait for the serial port to be ready, for displaying the output
while (!Serial) {
@@ -51,7 +38,7 @@ void setup() {
ready_assertion_event = false;
// Initiate an on-demand data measurement
- TransmitI2C(i2c_7bit_address, ON_DEMAND_MEASURE_CMD, 0, 0);
+ TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0);
// Now wait for the ready signal (falling edge) before continuing
while (!ready_assertion_event) {
@@ -61,22 +48,49 @@ void setup() {
// 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");
- // Read all sound data in one transaction, interpreting the received bytes as a SoundData_t struct:
- SoundData_t soundData = {0};
- ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
-
- // Print the values over the serial port
- if (useFloatingPoint) {
- // Convert values to floating point representation:
- SoundData_F_t soundDataF = {0};
- convertSoundDataF(&soundData, &soundDataF);
- printSoundDataF(&soundDataF);
- }
- else {
- // The data remain in integer representation
- printSoundData(&soundData, false);
- }
+ Serial.println("-----------------------------");
}
void loop() {
diff --git a/Arduino/Examples/web_server/web_server.ino b/Arduino/Examples/web_server/web_server.ino
index 5e6703c..1921ed6 100644
--- a/Arduino/Examples/web_server/web_server.ino
+++ b/Arduino/Examples/web_server/web_server.ino
@@ -2,15 +2,17 @@
web_server.ino
Example code for serving a web page over a WiFi network, displaying
- environment data from the Metriful MS430.
+ 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
- * NodeMCU ESP8266
-
+ * 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).
@@ -22,22 +24,18 @@
*/
#include
+#include
//////////////////////////////////////////////////////////
// USER-EDITABLE SETTINGS
-// Choose how often to read and display data (every 3, 100, 300 seconds)
+// 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;
-// The I2C address of the Metriful board
-uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN;
-
-// Which particle sensor is attached (PPD42, SDS011, or OFF)
-ParticleSensor_t particleSensor = OFF;
-
// Choose whether to create a new WiFi network (host as Access Point),
// or connect to an existing WiFi network.
-bool createWifiNetwork = true;
+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
@@ -46,10 +44,10 @@ bool createWifiNetwork = true;
// 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
+// 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 (at least 8 characters)
+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
@@ -61,7 +59,7 @@ IPAddress theIP(192, 168, 12, 20);
// END OF USER-EDITABLE SETTINGS
//////////////////////////////////////////////////////////
-#if !defined(ARDUINO_SAMD_NANO_33_IOT) && !defined(ARDUINO_SAMD_MKRWIFI1010) && !defined(ESP8266)
+#if !defined(HAS_WIFI)
#error ("This example program has been created for specific WiFi enabled hosts only.")
#endif
@@ -75,48 +73,23 @@ LightData_t lightData = {0};
ParticleData_t particleData = {0};
SoundData_t soundData = {0};
-// Buffer for commands (big enough to fit the largest send transaction):
-uint8_t transmit_buffer[1] = {0};
-
// Storage for the web page text
-char lineBuffer[128] = {0};
-char pageBuffer[2200] = {0};
+char lineBuffer[100] = {0};
+char pageBuffer[2300] = {0};
void setup() {
// Initialize the host's pins, set up the serial port and reset:
- SensorHardwareSetup(i2c_7bit_address);
+ SensorHardwareSetup(I2C_ADDRESS);
if (createWifiNetwork) {
- // The host generates its own WiFi network ("access point")
-
- #ifdef ESP8266
- // Set the chosen static IP address:
- WiFi.mode(WIFI_AP_STA);
- IPAddress subnet(255,255,255,0);
- WiFi.softAPConfig(theIP, theIP, subnet);
-
- Serial.print("Creating access point named: ");
- Serial.println(SSID);
- if (!WiFi.softAP(SSID, password)) {
- Serial.println("Failed to create access point.");
- while (true) {
- yield();
- }
+ // 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
- // Set the chosen static IP address:
- WiFi.config(theIP);
-
- Serial.print("Creating access point named: ");
- Serial.println(SSID);
-
- if (WiFi.beginAP(SSID, password) != WL_AP_LISTENING) {
- Serial.println("Failed to create access point.");
- while (true) {
- yield();
- }
- }
- #endif
+ }
}
else {
// The host connects to an existing Wifi network
@@ -126,25 +99,17 @@ void setup() {
while (!Serial) {
yield();
}
-
- // Attempt to connect to the Wifi network and obtain an IP
+
+ // 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.
- Serial.print("Connecting to ");
- Serial.println(SSID);
- WiFi.begin(SSID, password);
- while (WiFi.status() != WL_CONNECTED) {
- delay(500);
- Serial.print(".");
- }
- Serial.println("Connected.");
-
+ 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.print("View your page at http://");
Serial.println(theIP);
// Start the web server
@@ -152,10 +117,10 @@ void setup() {
////////////////////////////////////////////////////////////////////
- // Select how often to refresh the web page. This should be done at
+ // 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 access may be
- // out-of-step with the cycle.
+ // 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;
}
@@ -167,23 +132,18 @@ void setup() {
}
// Apply the chosen settings to the Metriful board
- if (particleSensor != OFF) {
- transmit_buffer[0] = particleSensor;
- TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
- }
- transmit_buffer[0] = cycle_period;
- TransmitI2C(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1);
-
- Serial.println("Entering cycle mode and waiting for data.");
+ 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_7bit_address, CYCLE_MODE_CMD, 0, 0);
+ 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 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();
@@ -192,7 +152,7 @@ void loop() {
// new data are now ready
- /* Read data the MS430 into the data structs.
+ /* 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
@@ -200,34 +160,51 @@ void loop() {
*/
// Air data
- ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
+ // 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_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
+ ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
// Light data
- ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
+ ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
// Sound data
- ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
+ 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 (particleSensor != OFF) {
- ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
+ 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;
+ }
+ }
}
@@ -235,7 +212,7 @@ void handleClientRequests(void) {
// Check for incoming client requests
WiFiClient client = server.available();
if (client) {
- bool blankLine = true;
+ bool blankLine = false;
while (client.connected()) {
if (client.available()) {
char c = client.read();
@@ -243,7 +220,7 @@ void handleClientRequests(void) {
// Two consecutive newline characters indicates the end of the client HTTP request
if (blankLine) {
// Send the page as a response
- client.println(pageBuffer);
+ client.print(pageBuffer);
break;
}
else {
@@ -262,128 +239,138 @@ void handleClientRequests(void) {
}
}
+// Create a simple text web page showing the environment data in
+// separate category tables, using HTML and CSS
void assembleWebPage(void) {
- strcpy(pageBuffer,"HTTP/1.1 200 OK\n" "Content-type:text/html\n" "Connection: close\n");
- sprintf(lineBuffer,"Refresh: %i\n\n\
- Metriful Sensor Demo",refreshPeriodSeconds);
- strcat(pageBuffer,lineBuffer);
- strcat(pageBuffer,"
");
- uint8_t T_positive_integer = airData.T_C_int_with_sign & TEMPERATURE_VALUE_MASK;
- // If the most-significant bit is set, the temperature is negative (below 0 C)
- if ((airData.T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) {
- // The bit is set: celsius temperature is negative
- sprintf(lineBuffer,"
Temperature
-%u.%u
C
",
- T_positive_integer, airData.T_C_fr_1dp);
- }
- else {
- // The bit is not set: celsius temperature is positive
- sprintf(lineBuffer,"
+
+
+
+
+
diff --git a/Arduino/Metriful_Sensor/host_pin_definitions.h b/Arduino/Metriful_Sensor/host_pin_definitions.h
index 95ba71c..eebef88 100644
--- a/Arduino/Metriful_Sensor/host_pin_definitions.h
+++ b/Arduino/Metriful_Sensor/host_pin_definitions.h
@@ -2,19 +2,21 @@
host_pin_definitions.h
This file defines which host pins are used to interface to the
- Metriful MS430 board and which library is used for I2C communications.
-
- The relevant file section is selected automatically when the board
- is chosen in the Arduino IDE.
+ Metriful MS430 board. The relevant file section is selected
+ automatically when the board is chosen in the Arduino IDE.
- This file can be used with the following host systems:
+ 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
- * NodeMCU ESP8266
+ * 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 just these five. You can use this file as a guide to define the
+ than those listed. You can use this file as a guide to define the
necessary settings for other host systems.
Copyright 2020 Metriful Ltd.
@@ -31,15 +33,12 @@
// Arduino Uno
- #include
- #define TheWire Wire
+ #define ISR_ATTRIBUTE
- // I2C pins are set by default so not defined here
#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
- /* In addition to these pins, the following I2C bus and power
- connections must be made to the Metriful MS430:
+ /* 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
@@ -53,8 +52,6 @@
Arduino pin 5V to SDS011 pin "5V"
Arduino pin GND to SDS011 pin "GND"
SDS011 pin "25um" to MS430 pin PRT
-
- For further details, see the readme and User Guide
*/
#elif defined ARDUINO_SAMD_NANO_33_IOT
@@ -63,22 +60,17 @@
#include
#include
-
- // The Wifi module uses the built-in hardware-supported I2C pins,
- // so a software I2C library must be used with pins SOFT_SDA and
- // SOFT_SCL. The examples use the SlowSoftWire library but
- // alternatives are available.
- #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
- #define SOFT_SDA A0 // Arduino pin A0 connects to SDA
- #define SOFT_SCL A3 // Arduino pin A3 connects to SCL
- /* In addition to these pins, the following I2C bus and power
- connections must be made to the Metriful MS430:
+ /* 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:
@@ -93,55 +85,51 @@
The solder bridge labeled "VUSB" on the underside of the Arduino
must be soldered closed to provide 5V to the PPD42/SDS011.
-
- For further details, see the readme and User Guide
*/
-
+
#elif defined ARDUINO_AVR_NANO
// Arduino Nano
- #include
- #define TheWire Wire
+ #define ISR_ATTRIBUTE
- // I2C pins (SDA = A4, SCL = A5) are set by default so not defined here
#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
- /* In addition to these pins, the following I2C bus and power
- connections must be made to the Metriful MS430:
- Arduino pins GND, A5 (SCL), A4 (SDA) to MS430 pins GND, SCL, SDA
+ /* 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
-
- For further details, see the readme and User Guide
*/
-
+
#elif defined ARDUINO_SAMD_MKRWIFI1010
// Arduino MKR WiFi 1010
+
#include
#include
- #include
- #define TheWire Wire
+ #define HAS_WIFI
+ #define ISR_ATTRIBUTE
- // I2C pins (SDA = D11, SCL = D12) are set by default so not defined here
#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
- /* In addition to these pins, the following I2C bus and power
- connections must be made to the Metriful MS430:
- Arduino pins GND, D12 (SCL), D11 (SDA) to MS430 pins GND, SCL, SDA
+ /* 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
@@ -154,43 +142,71 @@
Arduino pin 5V to SDS011 pin "5V"
Arduino pin GND to SDS011 pin "GND"
SDS011 pin "25um" to MS430 pin PRT
-
- For further details, see the readme and User Guide
*/
-
+
#elif defined ESP8266
- // NodeMCU ESP8266
+ // The examples have been tested on NodeMCU and Wemos D1 Mini.
+ // Other ESP8266 boards may require changes.
+
#include
- #include
- #define TheWire Wire
-
- #define SDA_PIN 5 // NodeMCU GPIO5 (labeled D1) connects to SDA
- #define SCL_PIN 4 // NodeMCU GPIO4 (labeled D2) connects to SCL
- #define READY_PIN 12 // NodeMCU GPIO12 (labeled D6) connects to RDY
- #define L_INT_PIN 0 // NodeMCU GPIO0 (labeled D3) connects to LIT
- #define S_INT_PIN 14 // NodeMCU GPIO14 (labeled D5) connects to SIT
- /* In addition to these pins, the following I2C bus and power
- connections must be made to the Metriful MS430:
- NodeMCU GND pin to MS430 pin GND
- NodeMCU pin 3V3 to MS430 pins VPU and VDD
+ #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:
- NodeMCU pin Vin to PPD42 pin 3
- NodeMCU pin GND to PPD42 pin 1
+ 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:
- NodeMCU pin Vin to SDS011 pin "5V"
- NodeMCU pin GND to SDS011 pin "GND"
+ 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
-
- For further details, see the readme and User Guide
*/
#else
- #error ("Your development board is not directly supported - edit this file to define the correct input/output pins.")
+ #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/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..72b9534
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,34 @@
+# Change Log
+All notable changes to the project software and documentation will be documented in this file.
+
+## [3.1.0] - 2020-11-16
+### Added
+- Fahrenheit temperature output.
+- Improved support for ESP8266.
+- Support for ESP32.
+- IFTTT example.
+- Home Assistant example.
+- Graph viewer software for cross-platform, real-time data monitoring.
+- Graph webpage server example using Plotly.js
+- Text webpage server added to Python examples (to match the one for Arduino).
+- Updated the User Guide (v2.1) for the new examples.
+
+### Changed
+- **All software changes are backwards-compatible so old programs should still work.**
+- The Raspberry Pi Python code now uses a package folder, so the import command has changed slightly.
+- Arduino Nano 33 IoT now uses hardware I2C port, so the previously-used software I2C library is no longer needed. This requires a re-wire of two pins for this host only.
+- The particle sensor being used (or not) is now set **once** rather than in each example code file - see readme for details.
+- The I2C address in the Arduino examples is now set **once** rather than in each example code file.
+- Small changes in most code files.
+
+
+## [3.0.1] - 2020-10-14
+### Fixed
+- Arduino IoT_cloud_logging HTTP request problem.
+
+
+## [3.0.0] - 2020-09-06
+### Changed
+- First release after hardware delivery.
+- Datasheet (v2.0), user guide (v2.0), readme and code comments were edited.
+
diff --git a/Python/GraphViewer.py b/Python/GraphViewer.py
new file mode 100644
index 0000000..469cfdd
--- /dev/null
+++ b/Python/GraphViewer.py
@@ -0,0 +1,233 @@
+# GraphViewer.py
+
+# This file defines a class for creating a graphical user interface,
+# to display graphs with real-time data updates.
+
+# A subclass must be derived from GraphViewer to implement the
+# method getDataFunction() and create a working example program. This
+# is done in "graph_viewer_serial.py" and "graph_viewer_I2C.py"
+
+# This is designed to run with Python 3 on multiple operating systems.
+# The readme and User Guide give instructions on installing the necessary
+# packages (pyqtgraph and PyQt5).
+
+# 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
+
+#########################################################
+
+import time
+from pyqtgraph.Qt import QtCore, QtGui
+import pyqtgraph as pg
+from collections import deque, OrderedDict
+
+
+class GraphViewer(QtGui.QMainWindow):
+ def __init__(self, data_buffer_length):
+ super(GraphViewer, self).__init__()
+
+ self.buffer_samples = data_buffer_length
+
+ # Define the number of graphs in the grid:
+ graphs_vertical = 2
+ graphs_horizontal = 2
+
+ # Appearance settings:
+ self.pen_style = pg.mkPen(color="y", width=2, style=QtCore.Qt.SolidLine)
+ self.title_color = "w"
+ self.title_size = "13pt"
+ self.axis_color = "w"
+ self.axis_label_style = {'color': '#FFF', 'font-size': '11pt'}
+ self.x_grid = False
+ self.y_grid = False
+
+ # Labels and measurement units for display
+ self.C_label = "\u00B0C"
+ self.F_label = "\u00B0F"
+ self.SDS_unit = "\u00B5g/m\u00B3"
+ self.PPD_unit = "ppL"
+ self.names_units = OrderedDict([('Temperature', self.C_label),
+ ('Pressure', 'Pa'),('Humidity', '%'),('Gas sensor resistance', "\u03A9"),
+ ('Air Quality Index', ''),('Estimated CO\u2082', 'ppm'),('Equivalent breath VOC', 'ppm'),
+ ('Air quality accuracy', ''),('Illuminance', 'lux'),('White light level', ''),
+ ('Sound pressure level', 'dBA'),('Band 1 SPL', 'dB'),('Band 2 SPL', 'dB'),
+ ('Band 3 SPL', 'dB'),('Band 4 SPL', 'dB'),('Band 5 SPL', 'dB'),
+ ('Band 6 SPL', 'dB'),('Peak sound amplitude', 'mPa'),('Microphone initialized', ''),
+ ('Particle sensor duty cycle', '%'),('Particle concentration', self.PPD_unit),
+ ('Particle data valid', '')])
+ self.decimal_places = [1,0,1,0,1,1,2,0,2,0,1,1,1,1,1,1,1,2,0,2,2,0]
+ self.sound_band_number = 6
+
+ # Construct the user interface
+ self.setWindowTitle('Waiting for data...')
+ self.widget = QtGui.QWidget()
+ self.setCentralWidget(self.widget)
+ self.widget.setLayout(QtGui.QGridLayout())
+ self.graph_var_numbers = []
+ self.selected_var_numbers = []
+ self.plot_items = []
+ self.plot_handles = []
+ self.combos = []
+ self.is_bar_chart = []
+ for nv in range(0,graphs_vertical):
+ for nh in range(0,graphs_horizontal):
+ GLW = pg.GraphicsLayoutWidget()
+ combo = pg.ComboBox()
+ self.combos.append(combo)
+ self.widget.layout().addWidget(combo, (2*nv), nh)
+ self.widget.layout().addWidget(GLW, (2*nv)+1, nh)
+ new_plot = GLW.addPlot()
+ self.plot_items.append(new_plot)
+ self.plot_handles.append(new_plot.plot(pen=self.pen_style,
+ symbol=None, axisItems={'bottom': pg.DateAxisItem()}))
+ self.formatPlotItem(new_plot)
+ self.is_bar_chart.append(False)
+ self.time_data = deque(maxlen=self.buffer_samples)
+
+
+ # Initialize and begin the periodic updating of the GUI
+ def start(self):
+ self.updateLoop()
+ self.show()
+
+
+ def setParticleUnits(self, name):
+ if (name == "SDS011"):
+ self.names_units['Particle concentration'] = self.SDS_unit
+ elif (name == "PPD42"):
+ self.names_units['Particle concentration'] = self.PPD_unit
+ elif (name is not None):
+ raise Exception("Particle sensor name must be 'SDS011' or 'PPD42', or None")
+
+
+ def useFahrenheitTemperatureUnits(self, use_fahrenheit):
+ if (use_fahrenheit):
+ self.names_units['Temperature'] = self.F_label
+ else:
+ self.names_units['Temperature'] = self.C_label
+
+
+ # Adjust plot appearance
+ def formatPlotItem(self, item):
+ item.setMenuEnabled(False)
+ item.showGrid(x=self.x_grid, y=self.y_grid)
+ item.getAxis("left").setPen(pg.mkPen(self.axis_color))
+ item.getAxis("bottom").setPen(pg.mkPen(self.axis_color))
+ item.getAxis("left").setStyle(tickLength=7)
+ item.getAxis("bottom").setStyle(tickLength=7)
+ item.setAxisItems({'bottom':pg.DateAxisItem()})
+
+
+ # Create and return a new function which will be called when one of
+ # the comboboxes is changed.
+ def funcCreator(self, graph_index, combo_handle):
+ def func():
+ self.selected_var_numbers[graph_index] = combo_handle.value()
+ return func
+
+
+ # Check for new data and redraw the graphs if data or combobox
+ # selections have changed
+ def updateLoop(self):
+ need_update = (self.graph_var_numbers != self.selected_var_numbers)
+ need_update = need_update or self.getDataFunction()
+ if (need_update):
+ self.updateGraphs()
+ # Call this function again in 20 ms
+ QtCore.QTimer.singleShot(20, self.updateLoop)
+
+
+ def getDataFunction(self):
+ # To be defined in subclass
+ pass
+
+
+ def createDataBuffer(self):
+ self.data_buffer = [deque(maxlen=self.buffer_samples) for i in range(0, len(self.indices))]
+
+
+ # Fill the ComboBoxes with the list of items and set the initial selected values
+ def initializeComboBoxes(self):
+ names = [list(self.names_units.keys())[j] for j in self.indices]
+ combo_items = dict(zip(names, [k for k in range(0,len(names))]))
+ combo_items['Sound frequency bands'] = len(combo_items)
+ for n,combo in enumerate(self.combos):
+ combo.setItems(combo_items)
+ start_index = n
+ if (n==0):
+ # Set first plot to be a bar chart
+ start_index = combo_items['Sound frequency bands']
+ self.selected_var_numbers.append(start_index)
+ combo.setValue(start_index)
+ combo.currentIndexChanged.connect(self.funcCreator(n, combo))
+ self.graph_var_numbers = self.selected_var_numbers.copy()
+
+
+ # Draw new data on the graphs and update the text label titles
+ def updateGraphs(self):
+ for n in range(0,len(self.plot_handles)):
+ self.graph_var_numbers[n] = self.selected_var_numbers[n]
+ if (self.graph_var_numbers[n] >= len(self.indices)):
+ # Bar chart of sound bands
+ if not (self.is_bar_chart[n] == True):
+ self.plot_items[n].removeItem(self.plot_handles[n])
+ self.plot_handles[n].deleteLater()
+ self.plot_handles[n] = pg.BarGraphItem(x=list(range(0,self.sound_band_number)),
+ height=[0]*self.sound_band_number, width=0.9, brush="r")
+ self.plot_items[n].addItem(self.plot_handles[n])
+ self.formatBarChart(self.plot_items[n])
+ self.is_bar_chart[n] = True
+ new_data = [self.data_buffer[i][-1] for i in range(self.band1_index,
+ self.band1_index+self.sound_band_number)]
+ self.plot_handles[n].setOpts(height=new_data)
+ else:
+ # Line graph of single variable
+ if not (self.is_bar_chart[n] == False):
+ self.plot_items[n].removeItem(self.plot_handles[n])
+ self.plot_handles[n].deleteLater()
+ self.plot_handles[n] = self.plot_items[n].plot(pen=self.pen_style, symbol=None)
+ self.adjustAxes(self.plot_items[n])
+ self.is_bar_chart[n] = False
+ ind = self.indices[self.graph_var_numbers[n]]
+ self.plot_items[n].setTitle(list(self.names_units.keys())[ind] +
+ " = {:.{dps}f} ".format(self.data_buffer[self.graph_var_numbers[n]][-1],
+ dps=self.decimal_places[ind]) + list(self.names_units.values())[ind],
+ color=self.title_color,size=self.title_size)
+ self.plot_handles[n].setData(self.time_data, self.data_buffer[self.graph_var_numbers[n]])
+
+
+ # Change axis settings
+ def adjustAxes(self, item):
+ item.getAxis("bottom").setTicks(None)
+ item.getAxis("left").setTicks(None)
+ item.getAxis("bottom").showLabel(False)
+ item.enableAutoRange(axis='x', enable=True)
+ item.enableAutoRange(axis='y', enable=True)
+
+
+ # Format the bar chart for displaying sound data for the six frequency bands
+ def formatBarChart(self, item):
+ frequency_midpoints = [125, 250, 500, 1000, 2000, 4000]
+ dB_labels = [20,30,40,50,60,70,80,90]
+ item.setTitle("Frequency band sound level / dB",color=self.title_color,size=self.title_size)
+ item.setLabel('bottom', text="Band center frequency / Hz", **self.axis_label_style)
+ # X axis ticks: set minor to same as major and label according to frequency
+ x_ticks = [[0]*len(frequency_midpoints),[0]*len(frequency_midpoints)]
+ for n,x in enumerate(frequency_midpoints):
+ x_ticks[0][n] = (n,str(x))
+ x_ticks[1][n] = x_ticks[0][n]
+ item.getAxis("bottom").setTicks(x_ticks)
+ item.setXRange(-0.5, 5.5, padding=0)
+ # Y axis ticks: set minor ticks to same as major
+ y_ticks = [[0]*len(dB_labels),[0]*len(dB_labels)]
+ for n,y in enumerate(dB_labels):
+ y_ticks[0][n] = (y,str(y))
+ y_ticks[1][n] = y_ticks[0][n]
+ item.getAxis("left").setTicks(y_ticks)
+ # fix Y axis limits, with margin:
+ item.setYRange(dB_labels[0], dB_labels[-1], padding=0.05)
+
+
diff --git a/Python/Raspberry_Pi/Home_Assistant.py b/Python/Raspberry_Pi/Home_Assistant.py
new file mode 100644
index 0000000..a84b8f4
--- /dev/null
+++ b/Python/Raspberry_Pi/Home_Assistant.py
@@ -0,0 +1,119 @@
+# Home_Assistant.py
+
+# Example code for sending environment data from the Metriful MS430 to
+# an installation of Home Assistant (www.home-assistant.io) on your
+# home network.
+# This example is designed to run with Python 3 on a Raspberry Pi.
+
+# Data are sent at regular intervals over your local network to Home
+# Assistant and can be viewed on the dashboard and 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
+
+import requests
+from sensor_package.sensor_functions import *
+
+#########################################################
+# USER-EDITABLE SETTINGS
+
+# How often to read and report the data (every 3, 100 or 300 seconds)
+cycle_period = CYCLE_PERIOD_100_S
+
+# 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.
+SENSOR_NAME = "kitchen3"
+
+# Specify the IP address of the computer running Home Assistant.
+# You can find this from the admin interface of your router.
+HOME_ASSISTANT_IP = "192.168.43.144"
+
+# Security access token: the Readme and User Guide explain how to get this
+LONG_LIVED_ACCESS_TOKEN = "PASTE YOUR TOKEN HERE WITHIN QUOTES"
+
+# END OF USER-EDITABLE SETTINGS
+#########################################################
+
+# Set up the GPIO and I2C communications bus
+(GPIO, I2C_bus) = SensorHardwareSetup()
+
+# Apply the settings to the MS430
+I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR])
+I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period])
+
+#########################################################
+
+print("Reporting data to Home Assistant. Press ctrl-c to exit.")
+
+# Enter cycle mode
+I2C_bus.write_byte(i2c_7bit_address, CYCLE_MODE_CMD)
+
+while (True):
+
+ # Wait for the next new data release, indicated by a falling edge on READY
+ while (not GPIO.event_detected(READY_pin)):
+ sleep(0.05)
+
+ # Now read all data from the MS430
+ air_data = get_air_data(I2C_bus)
+ air_quality_data = get_air_quality_data(I2C_bus)
+ light_data = get_light_data(I2C_bus)
+ sound_data = get_sound_data(I2C_bus)
+ particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR)
+
+ # Specify information needed by Home Assistant.
+ # Icons are chosen from https://cdn.materialdesignicons.com/5.3.45/
+ # (remove the "mdi-" part from the icon name).
+ pressure = dict(name='Pressure', data=air_data['P_Pa'], unit='Pa', icon='weather-cloudy', decimals=0)
+ humidity = dict(name='Humidity', data=air_data['H_pc'], unit='%', icon='water-percent', decimals=1)
+ temperature = dict(name='Temperature', data=air_data['T'], unit=air_data['T_unit'],
+ icon='thermometer', decimals=1)
+ illuminance = dict(name='Illuminance', data=light_data['illum_lux'], unit='lx',
+ icon='white-balance-sunny', decimals=2)
+ sound_level = dict(name='Sound level', data=sound_data['SPL_dBA'], unit='dBA',
+ icon='microphone', decimals=1)
+ sound_peak = dict(name='Sound peak', data=sound_data['peak_amp_mPa'], unit='mPa',
+ icon='waveform', decimals=2)
+ AQI = dict(name='Air Quality Index', data=air_quality_data['AQI'], unit=' ',
+ icon='thought-bubble-outline', decimals=1)
+ AQI_interpret = dict(name='Air quality assessment', data=interpret_AQI_value(air_quality_data['AQI']),
+ unit='', icon='flower-tulip', decimals=0)
+ particle = dict(name='Particle concentration', data=particle_data['concentration'],
+ unit=particle_data['conc_unit'], icon='chart-bubble', decimals=2)
+
+ # Send data to Home Assistant using HTTP POST requests
+ variables = [pressure, humidity, temperature, illuminance, sound_level, sound_peak, AQI, AQI_interpret]
+ if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF):
+ variables.append(particle)
+ try:
+ for v in variables:
+ url = ("http://" + HOME_ASSISTANT_IP + ":8123/api/states/" +
+ SENSOR_NAME + "." + v['name'].replace(' ','_'))
+ head = {"Content-type": "application/json","Authorization": "Bearer " + LONG_LIVED_ACCESS_TOKEN}
+ try:
+ valueStr = "{:.{dps}f}".format(v['data'], dps=v['decimals'])
+ except:
+ valueStr = v['data']
+ payload = {"state":valueStr, "attributes":{"unit_of_measurement":v['unit'],
+ "friendly_name":v['name'], "icon":"mdi:" + v['icon']}}
+ requests.post(url, json=payload, headers=head, timeout=2)
+ except Exception as e:
+ # An error has occurred, likely due to a lost network connection,
+ # and the post has failed.
+ # The program will retry with the next data release and will succeed
+ # if the network reconnects.
+ print("HTTP POST failed with the following error:")
+ print(repr(e))
+ print("The program will continue and retry on the next data output.")
+
+
diff --git a/Python/Raspberry_Pi/IFTTT.py b/Python/Raspberry_Pi/IFTTT.py
new file mode 100644
index 0000000..523f640
--- /dev/null
+++ b/Python/Raspberry_Pi/IFTTT.py
@@ -0,0 +1,146 @@
+# IFTTT.py
+
+# Example code for sending data from the Metriful MS430 to IFTTT.com
+# This example is designed to run with Python 3 on a Raspberry Pi.
+
+# 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.
+
+# 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
+
+import requests
+from sensor_package.sensor_functions import *
+
+#########################################################
+# USER-EDITABLE SETTINGS
+
+# IFTTT.com settings: WEBHOOKS_KEY and IFTTT_EVENT_NAME
+
+# 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 and in the User Guide.
+
+WEBHOOKS_KEY = "PASTE YOUR KEY HERE WITHIN QUOTES"
+IFTTT_EVENT_NAME = "PASTE YOUR EVENT NAME HERE WITHIN QUOTES"
+
+# 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 inactive_wait_cycles = 24
+inactive_wait_cycles = 24;
+
+# Define the details of the variables for monitoring:
+humidity = {'name':'humidity',
+ 'unit':"%",
+ 'decimal_places':1,
+ 'high_threshold':60,
+ 'low_threshold':30,
+ 'inactive_count':2,
+ 'high_advice':'Reduce moisture sources.',
+ 'low_advice':'Start the humidifier.'}
+
+air_quality_index = {'name':'air quality index',
+ 'unit':'',
+ 'decimal_places':1,
+ 'high_threshold':250,
+ 'low_threshold':-1,
+ 'inactive_count':2,
+ 'high_advice':'Improve ventilation.',
+ 'low_advice':''}
+
+# This example assumes that Celsius output temperature is selected. Edit
+# these values if Fahrenheit is selected in sensor_functions.py
+temperature = {'name':'temperature',
+ 'unit':CELSIUS_SYMBOL,
+ 'decimal_places':1,
+ 'high_threshold':23,
+ 'low_threshold':18,
+ 'inactive_count':2,
+ 'high_advice':'Turn on the fan.',
+ 'low_advice':'Turn on the heating.'}
+
+# END OF USER-EDITABLE SETTINGS
+#########################################################
+
+# Measure the environment data every 300 seconds (5 minutes). This is
+# adequate for long-term monitoring.
+cycle_period = CYCLE_PERIOD_300_S
+
+# IFTTT settings:
+IFTTT_url = "http://maker.ifttt.com/trigger/" + IFTTT_EVENT_NAME + "/with/key/" + WEBHOOKS_KEY
+IFTTT_header = {"Content-type": "application/json"}
+
+# Set up the GPIO and I2C communications bus
+(GPIO, I2C_bus) = SensorHardwareSetup()
+
+#########################################################
+
+print("Monitoring data. Press ctrl-c to exit.")
+
+# Enter cycle mode
+I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period])
+I2C_bus.write_byte(i2c_7bit_address, CYCLE_MODE_CMD)
+
+while (True):
+
+ # Wait for the next new data release, indicated by a falling edge on READY
+ while (not GPIO.event_detected(READY_pin)):
+ sleep(0.05)
+
+ # Read the air data and air quality data
+ air_data = get_air_data(I2C_bus)
+ air_quality_data = get_air_quality_data(I2C_bus)
+ temperature['data'] = air_data['T']
+ humidity['data'] = air_data['H_pc']
+ air_quality_index['data'] = air_quality_data['AQI']
+
+ # Check the new values and send an alert to IFTTT if a variable is
+ # outside its allowed range.
+ for v in [temperature, humidity, air_quality_index]:
+
+ if (v['inactive_count'] > 0):
+ # Count down to when the monitoring is active again
+ v['inactive_count']-=1
+
+ send_alert = False
+ if ((v['data'] > v['high_threshold']) and (v['inactive_count'] == 0)):
+ # The variable is above the high threshold: send an alert then
+ # ignore this variable for the next inactive_wait_cycles
+ v['inactive_count'] = inactive_wait_cycles
+ send_alert = True
+ threshold_description = 'high.'
+ advice = v['high_advice']
+ elif ((v['data'] < v['low_threshold']) and (v['inactive_count'] == 0)):
+ # The variable is below the low threshold: send an alert then
+ # ignore this variable for the next inactive_wait_cycles
+ v['inactive_count'] = inactive_wait_cycles
+ send_alert = True
+ threshold_description = 'low.'
+ advice = v['low_advice']
+
+ if send_alert:
+ # Send data using an HTTP POST request
+ try:
+ value1 = "The " + v['name'] + " is too " + threshold_description
+ print("Sending new alert to IFTTT: " + value1)
+ payload = {"value1":value1,
+ "value2":("The measurement was {:.{dps}f} ".format(v['data'],
+ dps=v['decimal_places']) + v['unit']),
+ "value3":advice}
+ requests.post(IFTTT_url, json=payload, headers=IFTTT_header, timeout=2)
+ except Exception as e:
+ # An error has occurred, likely due to a lost internet connection,
+ # and the post has failed. The program will continue and new
+ # alerts will succeed if the internet reconnects.
+ print("HTTP POST failed with the following error:")
+ print(repr(e))
+ print("The program will attempt to continue.")
+
diff --git a/Raspberry_Pi/IoT_cloud_logging.py b/Python/Raspberry_Pi/IoT_cloud_logging.py
similarity index 80%
rename from Raspberry_Pi/IoT_cloud_logging.py
rename to Python/Raspberry_Pi/IoT_cloud_logging.py
index 12910bf..553b272 100644
--- a/Raspberry_Pi/IoT_cloud_logging.py
+++ b/Python/Raspberry_Pi/IoT_cloud_logging.py
@@ -15,7 +15,7 @@
# https://github.com/metriful/sensor
import requests
-from sensor_functions import *
+from sensor_package.sensor_functions import *
#########################################################
# USER-EDITABLE SETTINGS
@@ -25,10 +25,8 @@
# be set to 100 or 300 seconds, not 3 seconds.
cycle_period = CYCLE_PERIOD_100_S
-# Which particle sensor, if any, is attached (PPD42, SDS011, or OFF)
-particleSensor = PARTICLE_SENSOR_OFF
-
# 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.
@@ -37,8 +35,7 @@
# readme and User Guide for more information.
# Choose which provider to use
-use_Tago_cloud = True
-# To use the ThingSpeak cloud, set: use_Tago_cloud=False
+use_Tago_cloud = True # set this False to use the Thingspeak cloud
# The chosen account's key/token must be inserted below.
if (use_Tago_cloud):
@@ -55,8 +52,7 @@
(GPIO, I2C_bus) = SensorHardwareSetup()
# Apply the chosen settings to the MS430
-if (particleSensor != PARTICLE_SENSOR_OFF):
- I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor])
+I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR])
I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period])
#########################################################
@@ -85,38 +81,35 @@
# Now read all data from the MS430
# Air data
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES)
- air_data = extractAirData(raw_data)
+ # Choose output temperature unit (C or F) in sensor_functions.py
+ air_data = get_air_data(I2C_bus)
# 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.
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_QUALITY_DATA_READ, AIR_QUALITY_DATA_BYTES)
- air_quality_data = extractAirQualityData(raw_data)
+ air_quality_data = get_air_quality_data(I2C_bus)
# Light data
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, LIGHT_DATA_READ, LIGHT_DATA_BYTES)
- light_data = extractLightData(raw_data)
-
+ light_data = get_light_data(I2C_bus)
+
# Sound data
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES)
- sound_data = extractSoundData(raw_data)
+ sound_data = get_sound_data(I2C_bus)
# Particle data
- # This requires the connection of a particulate sensor (invalid
+ # This requires the connection of a particulate sensor (zero/invalid
# values will be obtained if this sensor is not present).
+ # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py
# Also note that, due to the low pass filtering used, the
# particle data become valid after an initial initialization
# period of approximately one minute.
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, PARTICLE_DATA_READ, PARTICLE_DATA_BYTES)
- particle_data = extractParticleData(raw_data, particleSensor)
+ particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR)
# Assemble the data into the required format, then send it to the cloud
# as an HTTP POST request.
# For both example cloud providers, the following quantities will be sent:
- # 1 Temperature/C
+ # 1 Temperature (measurement unit is selected in sensor_functions.py)
# 2 Pressure/Pa
# 3 Humidity/%
# 4 Air quality index
@@ -125,14 +118,14 @@
# 7 Illuminance/lux
# 8 Particle concentration
- # Additionally, for Tago, the following is sent:
+ # Additionally, for Tago, the following are sent:
# 9 Air Quality Assessment summary (Good, Bad, etc.)
# 10 Peak sound amplitude / mPa
try:
if use_Tago_cloud:
payload = [0]*10;
- payload[0] = {"variable":"temperature","value":"{:.1f}".format(air_data['T_C'])}
+ payload[0] = {"variable":"temperature","value":"{:.1f}".format(air_data['T'])}
payload[1] = {"variable":"pressure","value":air_data['P_Pa']}
payload[2] = {"variable":"humidity","value":"{:.1f}".format(air_data['H_pc'])}
payload[3] = {"variable":"aqi","value":"{:.1f}".format(air_quality_data['AQI'])}
@@ -146,7 +139,7 @@
else:
# Use ThingSpeak.com cloud
payload = "api_key=" + THINGSPEAK_API_KEY_STRING
- payload += "&field1=" + "{:.1f}".format(air_data['T_C'])
+ payload += "&field1=" + "{:.1f}".format(air_data['T'])
payload += "&field2=" + str(air_data['P_Pa'])
payload += "&field3=" + "{:.1f}".format(air_data['H_pc'])
payload += "&field4=" + "{:.1f}".format(air_quality_data['AQI'])
@@ -156,12 +149,12 @@
payload += "&field8=" + "{:.2f}".format(particle_data['concentration'])
requests.post(thingspeak_url, data=payload, headers=thingspeak_header, timeout=2)
- except:
+ except Exception as e:
# An error has occurred, likely due to a lost internet connection,
# and the post has failed.
# The program will retry with the next data release and will succeed
# if the internet reconnects.
- print("HTTP POST failed.")
-
-
+ print("HTTP POST failed with the following error:")
+ print(repr(e))
+ print("The program will continue and retry on the next data output.")
diff --git a/Raspberry_Pi/cycle_readout.py b/Python/Raspberry_Pi/cycle_readout.py
similarity index 67%
rename from Raspberry_Pi/cycle_readout.py
rename to Python/Raspberry_Pi/cycle_readout.py
index d5a701e..5cc9268 100644
--- a/Raspberry_Pi/cycle_readout.py
+++ b/Python/Raspberry_Pi/cycle_readout.py
@@ -7,24 +7,23 @@
# repeating cycle. User can choose from a cycle time period
# of 3, 100, or 300 seconds.
+# 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
-from sensor_functions import *
+from sensor_package.sensor_functions import *
#########################################################
# USER-EDITABLE SETTINGS
-# How often to read data (every 3, 100, 300 seconds)
+# How often to read data (every 3, 100, or 300 seconds)
cycle_period = CYCLE_PERIOD_3_S
-# Which particle sensor, if any, is attached
-# (PARTICLE_SENSOR_X with X = PPD42, SDS011, or OFF)
-particleSensor = PARTICLE_SENSOR_OFF
-
# How to print the data: If print_data_as_columns = True,
# data are columns of numbers, useful to copy/paste to a spreadsheet
# application. Otherwise, data are printed with explanatory labels and units.
@@ -37,8 +36,7 @@
(GPIO, I2C_bus) = SensorHardwareSetup()
# Apply the chosen settings
-if (particleSensor != PARTICLE_SENSOR_OFF):
- I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor])
+I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR])
I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period])
#########################################################
@@ -56,37 +54,34 @@
# Now read and print all data
# Air data
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES)
- air_data = extractAirData(raw_data)
+ # Choose output temperature unit (C or F) in sensor_functions.py
+ air_data = get_air_data(I2C_bus)
writeAirData(None, air_data, print_data_as_columns)
# 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.
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_QUALITY_DATA_READ, AIR_QUALITY_DATA_BYTES)
- air_quality_data = extractAirQualityData(raw_data)
+ air_quality_data = get_air_quality_data(I2C_bus)
writeAirQualityData(None, air_quality_data, print_data_as_columns)
# Light data
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, LIGHT_DATA_READ, LIGHT_DATA_BYTES)
- light_data = extractLightData(raw_data)
+ light_data = get_light_data(I2C_bus)
writeLightData(None, light_data, print_data_as_columns)
# Sound data
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES)
- sound_data = extractSoundData(raw_data)
+ sound_data = get_sound_data(I2C_bus)
writeSoundData(None, sound_data, print_data_as_columns)
# Particle data
- # This requires the connection of a particulate sensor (invalid
+ # This requires the connection of a particulate sensor (zero/invalid
# values will be obtained if this sensor is not present).
+ # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py
# 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 (particleSensor != PARTICLE_SENSOR_OFF):
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, PARTICLE_DATA_READ, PARTICLE_DATA_BYTES)
- particle_data = extractParticleData(raw_data, particleSensor)
+ if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF):
+ particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR)
writeParticleData(None, particle_data, print_data_as_columns)
if print_data_as_columns:
diff --git a/Python/Raspberry_Pi/graph_web_server.py b/Python/Raspberry_Pi/graph_web_server.py
new file mode 100644
index 0000000..3357a55
--- /dev/null
+++ b/Python/Raspberry_Pi/graph_web_server.py
@@ -0,0 +1,136 @@
+# graph_web_server.py
+
+# Example code for serving a web page over a local network to display
+# graphs showing environment data read from the Metriful MS430. A CSV
+# data file is also downloadable from the page.
+# This example is designed to run with Python 3 on a Raspberry Pi.
+
+# The web page can be viewed from other devices connected to the same
+# network(s) as the host Raspberry Pi, including wired and wireless
+# networks.
+
+# 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.
+
+# NOTE: if you run, exit, then re-run this program, you may get an
+# "Address already in use" error. This ends after a short period: wait
+# one minute then retry.
+
+# 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
+
+import socketserver
+from sensor_package.servers import *
+from sensor_package.sensor_functions import *
+
+#########################################################
+# 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
+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.
+buffer_length = 864
+# 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
+
+
+# The web page address will be:
+# http://:8080 e.g. http://172.24.1.1:8080
+
+# Find your Raspberry Pi's IP address from the admin interface of your
+# router, or:
+# 1. Enter the command ifconfig in a terminal
+# 2. Each available network connection displays a block of output
+# 3. Ignore the "lo" output block
+# 4. The host's IP address on each network is displayed after "inet"
+#
+# Example - part of an output block showing the address 172.24.1.1
+#
+# wlan0: flags=4163 mtu 1500
+# inet 172.24.1.1 netmask 255.255.255.0 broadcast 172.24.1.255
+
+# END OF USER-EDITABLE SETTINGS
+#########################################################
+
+# Set up the GPIO and I2C communications bus
+(GPIO, I2C_bus) = SensorHardwareSetup()
+
+# Apply the chosen settings to the MS430
+I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR])
+I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period])
+
+# Get time period value to send to web page
+if (cycle_period == CYCLE_PERIOD_3_S):
+ GraphWebpageHandler.data_period_seconds = 3
+elif (cycle_period == CYCLE_PERIOD_100_S):
+ GraphWebpageHandler.data_period_seconds = 100
+else: # CYCLE_PERIOD_300_S
+ GraphWebpageHandler.data_period_seconds = 300
+
+# Set the number of each variable to be retained
+GraphWebpageHandler.set_buffer_length(buffer_length)
+
+# Set the webpage to use:
+GraphWebpageHandler.set_webpage_filename('sensor_package/graph_web_page.html')
+
+# Choose the TCP port number for the web page.
+port = 8080
+# The port can be any unused number from 1-65535 but values below 1024
+# require this program to be run as super-user as follows:
+# sudo python3 web_server.py
+# Port 80 is the default for HTTP, and with this value the port number
+# can be omitted from the web address. e.g. http://172.24.1.1
+
+print("Starting the web server. Your web page will be available at:")
+print("http://:" + str(port))
+print("Press ctrl-c to exit.")
+
+the_server = socketserver.TCPServer(("", port), GraphWebpageHandler)
+the_server.timeout = 0.1
+
+# Enter cycle mode to start periodic data output
+I2C_bus.write_byte(i2c_7bit_address, CYCLE_MODE_CMD)
+
+while (True):
+
+ # Respond to the web page client requests while waiting for new data
+ while (not GPIO.event_detected(READY_pin)):
+ the_server.handle_request()
+ sleep(0.05)
+
+ # Now read all data from the MS430 and pass to the web page
+
+ # Air data
+ GraphWebpageHandler.update_air_data(get_air_data(I2C_bus))
+
+ # 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.
+ GraphWebpageHandler.update_air_quality_data(get_air_quality_data(I2C_bus))
+
+ # Light data
+ GraphWebpageHandler.update_light_data(get_light_data(I2C_bus))
+
+ # Sound data
+ GraphWebpageHandler.update_sound_data(get_sound_data(I2C_bus))
+
+ # Particle data
+ # This requires the connection of a particulate sensor (invalid
+ # values will be obtained if this sensor is not present).
+ # 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):
+ GraphWebpageHandler.update_particle_data(get_particle_data(I2C_bus, PARTICLE_SENSOR))
+
diff --git a/Raspberry_Pi/interrupts.py b/Python/Raspberry_Pi/interrupts.py
similarity index 95%
rename from Raspberry_Pi/interrupts.py
rename to Python/Raspberry_Pi/interrupts.py
index 17204fd..c671d8a 100644
--- a/Raspberry_Pi/interrupts.py
+++ b/Python/Raspberry_Pi/interrupts.py
@@ -14,8 +14,7 @@
# For code examples, datasheet and user guide, visit
# https://github.com/metriful/sensor
-from time import sleep
-from sensor_functions import *
+from sensor_package.sensor_functions import *
#########################################################
# USER-EDITABLE SETTINGS
@@ -45,7 +44,8 @@
#########################################################
if ((light_thres_lux_i + (float(light_thres_lux_f2dp)/100.0)) > MAX_LUX_VALUE):
- raise Exception('The chosen light interrupt threshold exceeds the maximum allowed value.')
+ raise Exception("The chosen light interrupt threshold exceeds the "
+ "maximum allowed value of " + str(MAX_LUX_VALUE) + " lux")
# Set up the GPIO and I2C communications bus
(GPIO, I2C_bus) = SensorHardwareSetup()
diff --git a/Raspberry_Pi/log_data_to_file.py b/Python/Raspberry_Pi/log_data_to_file.py
similarity index 71%
rename from Raspberry_Pi/log_data_to_file.py
rename to Python/Raspberry_Pi/log_data_to_file.py
index 5021fc6..65ab8bd 100644
--- a/Raspberry_Pi/log_data_to_file.py
+++ b/Python/Raspberry_Pi/log_data_to_file.py
@@ -16,7 +16,7 @@
# https://github.com/metriful/sensor
import datetime
-from sensor_functions import *
+from sensor_package.sensor_functions import *
#########################################################
# USER-EDITABLE SETTINGS
@@ -28,15 +28,12 @@
# Number of lines of data to log in each file before starting a new file
# (required if log_to_file == True), and which directory to save them in.
-lines_per_file = 3000
+lines_per_file = 300
data_file_directory = "/home/pi/Desktop"
-# How often to measure and read data (every 3, 100, 300 seconds):
+# How often to measure and read data (every 3, 100, or 300 seconds):
cycle_period = CYCLE_PERIOD_3_S
-# Which particle sensor, if any, is attached (PPD42, SDS011, or OFF)
-particleSensor = PARTICLE_SENSOR_OFF
-
# END OF USER-EDITABLE SETTINGS
#########################################################
@@ -44,8 +41,7 @@
(GPIO, I2C_bus) = SensorHardwareSetup()
# Apply the chosen settings to the MS430
-if (particleSensor != PARTICLE_SENSOR_OFF):
- I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor])
+I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR])
I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period])
#########################################################
@@ -65,50 +61,41 @@
while (not GPIO.event_detected(READY_pin)):
sleep(0.05)
- # Air data:
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES)
- air_data = extractAirData(raw_data)
+ # Air data
+ # Choose output temperature unit (C or F) in sensor_functions.py
+ air_data = get_air_data(I2C_bus)
# 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.
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_QUALITY_DATA_READ, AIR_QUALITY_DATA_BYTES)
- air_quality_data = extractAirQualityData(raw_data)
-
- # Light data:
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, LIGHT_DATA_READ, LIGHT_DATA_BYTES)
- light_data = extractLightData(raw_data)
-
- # Sound data:
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES)
- sound_data = extractSoundData(raw_data)
+ air_quality_data = get_air_quality_data(I2C_bus)
+ # Light data
+ light_data = get_light_data(I2C_bus)
+
+ # Sound data
+ sound_data = get_sound_data(I2C_bus)
+
# Particle data
- # This requires the connection of a particulate sensor (invalid
+ # This requires the connection of a particulate sensor (zero/invalid
# values will be obtained if this sensor is not present).
+ # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py
# Also note that, due to the low pass filtering used, the
# particle data become valid after an initial initialization
# period of approximately one minute.
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, PARTICLE_DATA_READ, PARTICLE_DATA_BYTES)
- particle_data = extractParticleData(raw_data, particleSensor)
-
+ particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR)
+
if (print_to_screen):
# Display all data on screen as named quantities with units
print("")
print("------------------");
writeAirData(None, air_data, False)
- print("------------------");
writeAirQualityData(None, air_quality_data, False)
- print("------------------");
writeLightData(None, light_data, False)
- print("------------------");
writeSoundData(None, sound_data, False)
- print("------------------");
- if (particleSensor != PARTICLE_SENSOR_OFF):
+ if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF):
writeParticleData(None, particle_data, False)
- print("------------------");
-
if (log_to_file):
# Write the data as simple columns in a text file (without labels or
@@ -123,7 +110,7 @@
writeLightData(datafile, light_data, True)
# Sound data in columns 17 - 25
writeSoundData(datafile, sound_data, True)
- if (particleSensor != PARTICLE_SENSOR_OFF):
+ if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF):
# Particle data in columns 26 - 28
writeParticleData(datafile, particle_data, True)
datafile.write("\n")
diff --git a/Raspberry_Pi/on_demand_readout.py b/Python/Raspberry_Pi/on_demand_readout.py
similarity index 66%
rename from Raspberry_Pi/on_demand_readout.py
rename to Python/Raspberry_Pi/on_demand_readout.py
index f600f25..a4ac230 100644
--- a/Raspberry_Pi/on_demand_readout.py
+++ b/Python/Raspberry_Pi/on_demand_readout.py
@@ -4,8 +4,11 @@
# This example is designed to run with Python 3 on a Raspberry Pi.
# Repeatedly measures and displays all environment data, with a pause
-# between measurements. Air quality data are unavailable in this mode
-# (instead see cycle_readout.py).
+# of any chosen duration between measurements. Air quality data are
+# unavailable in this mode (instead use cycle_readout.py).
+
+# 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
@@ -13,7 +16,7 @@
# For code examples, datasheet and user guide, visit
# https://github.com/metriful/sensor
-from sensor_functions import *
+from sensor_package.sensor_functions import *
#########################################################
# USER-EDITABLE SETTINGS
@@ -24,10 +27,6 @@
# Choosing a pause of less than 2 seconds will cause inaccurate
# temperature, humidity and particle data.
-# Which particle sensor, if any, is attached
-# (PARTICLE_SENSOR_X with X = PPD42, SDS011, or OFF)
-particleSensor = PARTICLE_SENSOR_OFF
-
# How to print the data: If print_data_as_columns = True,
# data are columns of numbers, useful to copy/paste to a spreadsheet
# application. Otherwise, data are printed with explanatory labels and units.
@@ -39,9 +38,7 @@
# Set up the GPIO and I2C communications bus
(GPIO, I2C_bus) = SensorHardwareSetup()
-# Apply the chosen settings
-if (particleSensor != PARTICLE_SENSOR_OFF):
- I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor])
+I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR])
#########################################################
@@ -52,38 +49,37 @@
# Trigger a new measurement
I2C_bus.write_byte(i2c_7bit_address, ON_DEMAND_MEASURE_CMD)
- # Wait for the next new data release, indicated by a falling edge on READY
+ # Wait for the next new data release, indicated by a falling edge on READY.
+ # This will take 0.5 seconds.
while (not GPIO.event_detected(READY_pin)):
sleep(0.05)
# Now read and print all data
# Air data
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES)
- air_data = extractAirData(raw_data)
+ # Choose output temperature unit (C or F) in sensor_functions.py
+ air_data = get_air_data(I2C_bus)
writeAirData(None, air_data, print_data_as_columns)
# Air quality data are not available with on demand measurements
# Light data
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, LIGHT_DATA_READ, LIGHT_DATA_BYTES)
- light_data = extractLightData(raw_data)
+ light_data = get_light_data(I2C_bus)
writeLightData(None, light_data, print_data_as_columns)
# Sound data
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES)
- sound_data = extractSoundData(raw_data)
+ sound_data = get_sound_data(I2C_bus)
writeSoundData(None, sound_data, print_data_as_columns)
# Particle data
- # This requires the connection of a particulate sensor (invalid
+ # This requires the connection of a particulate sensor (zero/invalid
# values will be obtained if this sensor is not present).
+ # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py
# 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 (particleSensor != PARTICLE_SENSOR_OFF):
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, PARTICLE_DATA_READ, PARTICLE_DATA_BYTES)
- particle_data = extractParticleData(raw_data, particleSensor)
+ if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF):
+ particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR)
writeParticleData(None, particle_data, print_data_as_columns)
if print_data_as_columns:
diff --git a/Raspberry_Pi/particle_sensor_toggle.py b/Python/Raspberry_Pi/particle_sensor_toggle.py
similarity index 70%
rename from Raspberry_Pi/particle_sensor_toggle.py
rename to Python/Raspberry_Pi/particle_sensor_toggle.py
index fcb9e79..dbf4bb9 100644
--- a/Raspberry_Pi/particle_sensor_toggle.py
+++ b/Python/Raspberry_Pi/particle_sensor_toggle.py
@@ -4,7 +4,7 @@
# control signal from one of the Pi 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.
+# 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
@@ -19,7 +19,7 @@
# For code examples, datasheet and user guide, visit
# https://github.com/metriful/sensor
-from sensor_functions import *
+from sensor_package.sensor_functions import *
#########################################################
# USER-EDITABLE SETTINGS
@@ -29,17 +29,13 @@
# its data.
cycle_period = CYCLE_PERIOD_100_S
-# Which particle sensor, if any, is attached
-# (PARTICLE_SENSOR_X with X = PPD42, SDS011, or OFF)
-particleSensor = PARTICLE_SENSOR_SDS011
-
# How to print the data: If print_data_as_columns = True,
# data are columns of numbers, useful to copy/paste to a spreadsheet
# application. Otherwise, data are printed with explanatory labels and units.
print_data_as_columns = True
# Particle sensor power control options
-off_cycles = 1; # leave the sensor off for this many cycles between reads
+off_cycles = 2; # leave the sensor off for this many cycles between reads
particle_sensor_control_pin = 10; # Pi pin number which outputs the control signal
# END OF USER-EDITABLE SETTINGS
@@ -51,18 +47,17 @@
# Set up the particle sensor control, and turn it off initially
GPIO.setup(particle_sensor_control_pin, GPIO.OUT)
GPIO.output(particle_sensor_control_pin, 0)
-particleSensorIsOn = False
-particleSensor_count = 0
+particle_sensor_is_on = False
+particle_sensor_count = 0
# Apply the chosen settings
-if (particleSensor != PARTICLE_SENSOR_OFF):
- I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor])
+I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR])
I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period])
#########################################################
sound_data = extractSoundData([0]*SOUND_DATA_BYTES)
-particle_data = extractParticleData([0]*PARTICLE_DATA_BYTES, particleSensor)
+particle_data = extractParticleData([0]*PARTICLE_DATA_BYTES, PARTICLE_SENSOR)
print("Entering cycle mode and waiting for data. Press ctrl-c to exit.")
@@ -78,38 +73,33 @@
# sound data will be printed if no reading is done on this loop.
# Air data
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES)
- air_data = extractAirData(raw_data)
+ air_data = get_air_data(I2C_bus)
writeAirData(None, air_data, print_data_as_columns)
# 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.
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_QUALITY_DATA_READ, AIR_QUALITY_DATA_BYTES)
- air_quality_data = extractAirQualityData(raw_data)
+ air_quality_data = get_air_quality_data(I2C_bus)
writeAirQualityData(None, air_quality_data, print_data_as_columns)
# Light data
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, LIGHT_DATA_READ, LIGHT_DATA_BYTES)
- light_data = extractLightData(raw_data)
+ light_data = get_light_data(I2C_bus)
writeLightData(None, light_data, print_data_as_columns)
- # Sound data - only read when particle sensor is off
- if (not particleSensorIsOn):
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES)
- sound_data = extractSoundData(raw_data)
+ # Sound data - only read new data when particle sensor is off
+ if (not particle_sensor_is_on):
+ sound_data = get_sound_data(I2C_bus)
writeSoundData(None, sound_data, print_data_as_columns)
# Particle data
- # This requires the connection of a particulate sensor (invalid
+ # This requires the connection of a particulate sensor (zero/invalid
# values will be obtained if this sensor is not present).
# 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):
- raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, PARTICLE_DATA_READ, PARTICLE_DATA_BYTES)
- particle_data = extractParticleData(raw_data, particleSensor)
+ if (particle_sensor_is_on):
+ particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR)
writeParticleData(None, particle_data, print_data_as_columns)
if print_data_as_columns:
@@ -118,23 +108,23 @@
print("-------------------------------------------")
#Turn the particle sensor on/off if required
- if (particleSensorIsOn):
+ if (particle_sensor_is_on):
# Stop the particle detection on the MS430
I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR_OFF])
# Turn off the hardware:
GPIO.output(particle_sensor_control_pin, 0)
- particleSensorIsOn = False
+ particle_sensor_is_on = False
else:
- particleSensor_count += 1
- if (particleSensor_count >= off_cycles):
+ particle_sensor_count += 1
+ if (particle_sensor_count >= off_cycles):
# Turn on the hardware:
GPIO.output(particle_sensor_control_pin, 1)
# Start the particle detection on the MS430
- I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor])
+ I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR])
- particleSensor_count = 0
- particleSensorIsOn = True
+ particle_sensor_count = 0
+ particle_sensor_is_on = True
diff --git a/Python/Raspberry_Pi/sensor_package/__init__.py b/Python/Raspberry_Pi/sensor_package/__init__.py
new file mode 100644
index 0000000..d51ee2f
--- /dev/null
+++ b/Python/Raspberry_Pi/sensor_package/__init__.py
@@ -0,0 +1 @@
+# This file remains empty but is needed by Python to create a package
diff --git a/Python/Raspberry_Pi/sensor_package/graph_web_page.html b/Python/Raspberry_Pi/sensor_package/graph_web_page.html
new file mode 100644
index 0000000..2259f91
--- /dev/null
+++ b/Python/Raspberry_Pi/sensor_package/graph_web_page.html
@@ -0,0 +1,333 @@
+
+
+
+
+
+Indoor Environment Data
+
+
+
+
+
+
")
+
+ cls.the_web_page += ""
+
+
+##########################################################################################
+
+# A class for making a web page with graphs to display environment data, using
+# the Plotly.js libray, javascript, HTML and CSS. This is used in graph_web_server.py
+
+class GraphWebpageHandler(http.server.SimpleHTTPRequestHandler):
+ data_period_seconds = 3
+ error_response_HTTP = "HTTP/1.1 400 Bad Request\r\n\r\n"
+ data_header = ("HTTP/1.1 200 OK\r\n"
+ "Content-type: application/octet-stream\r\n"
+ "Connection: close\r\n\r\n")
+ page_header = ("HTTP/1.1 200 OK\r\n"
+ "Content-type: text/html\r\n"
+ "Connection: close\r\n\r\n")
+
+ # Respond to an HTTP GET request (no other methods are supported)
+ def do_GET(self):
+ if (self.path == '/'):
+ # The web page is requested
+ self.wfile.write(bytes(self.page_header, "utf8"))
+ with open(self.webpage_filename, 'rb') as fileObj:
+ for data in fileObj:
+ self.wfile.write(data)
+ fileObj.close()
+ elif (self.path == '/1'):
+ # A URI path of '1' indicates a request of all buffered data
+ self.send_all_data()
+ elif (self.path == '/2'):
+ # A URI path of '2' indicates a request of the latest data only
+ self.send_latest_data()
+ else:
+ # Path not recognized: send a standard error response
+ self.wfile.write(bytes(self.error_response_HTTP, "utf8"))
+
+
+ def send_all_data(self):
+ self.wfile.write(bytes(self.data_header, "utf8"))
+ # First send the time period, so the web page knows when to do the next request
+ self.wfile.write(struct.pack('H', self.data_period_seconds))
+ # Send temperature unit and particle sensor type, combined into one byte
+ codeByte = PARTICLE_SENSOR
+ if USE_FAHRENHEIT:
+ codeByte = codeByte | 0x10
+ self.wfile.write(struct.pack('B', codeByte))
+ # Send the length of the data buffers (the number of values of each variable)
+ self.wfile.write(struct.pack('H', len(self.temperature)))
+ # Send the data:
+ for p in [self.AQI, self.temperature, self.pressure, self.humidity,
+ self.SPL, self.illuminance, self.bVOC, self.particle]:
+ self.wfile.write(struct.pack(str(len(p)) + 'f', *p))
+
+
+ def send_latest_data(self):
+ self.wfile.write(bytes(self.data_header, "utf8"))
+ # Send the most recent value for each variable, if buffers are not empty
+ if (len(self.temperature) > 0):
+ data = [self.AQI[-1], self.temperature[-1], self.pressure[-1], self.humidity[-1],
+ self.SPL[-1], self.illuminance[-1], self.bVOC[-1]]
+ if (len(self.particle) > 0):
+ data.append(self.particle[-1])
+ self.wfile.write(struct.pack(str(len(data)) + 'f', *data))
+
+
+ @classmethod
+ def set_webpage_filename(self, filename):
+ self.webpage_filename = filename
+
+ @classmethod
+ def set_buffer_length(cls, buffer_length):
+ cls.temperature = deque(maxlen=buffer_length)
+ cls.pressure = deque(maxlen=buffer_length)
+ cls.humidity = deque(maxlen=buffer_length)
+ cls.AQI = deque(maxlen=buffer_length)
+ cls.bVOC = deque(maxlen=buffer_length)
+ cls.SPL = deque(maxlen=buffer_length)
+ cls.illuminance = deque(maxlen=buffer_length)
+ cls.particle = deque(maxlen=buffer_length)
+
+ @classmethod
+ def update_air_data(cls, air_data):
+ cls.temperature.append(air_data['T'])
+ cls.pressure.append(air_data['P_Pa'])
+ cls.humidity.append(air_data['H_pc'])
+
+ @classmethod
+ def update_air_quality_data(cls, air_quality_data):
+ cls.AQI.append(air_quality_data['AQI'])
+ cls.bVOC.append(air_quality_data['bVOC'])
+
+ @classmethod
+ def update_light_data(cls, light_data):
+ cls.illuminance.append(light_data['illum_lux'])
+
+ @classmethod
+ def update_sound_data(cls, sound_data):
+ cls.SPL.append(sound_data['SPL_dBA'])
+
+ @classmethod
+ def update_particle_data(cls, particle_data):
+ cls.particle.append(particle_data['concentration'])
+
diff --git a/Python/Raspberry_Pi/simple_read_T_H.py b/Python/Raspberry_Pi/simple_read_T_H.py
new file mode 100644
index 0000000..c2688f9
--- /dev/null
+++ b/Python/Raspberry_Pi/simple_read_T_H.py
@@ -0,0 +1,99 @@
+# simple_read_T_H.py
+
+# Example code for using the Metriful MS430 to measure humidity and
+# temperature.
+# This example is designed to run with Python 3 on a Raspberry Pi.
+
+# Demonstrates multiple ways of reading and displaying the temperature
+# and humidity data. View the output in the terminal. 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
+
+from sensor_package.sensor_functions import *
+
+# Set up the GPIO and I2C communications bus
+(GPIO, I2C_bus) = SensorHardwareSetup()
+
+# Initiate an on-demand data measurement
+I2C_bus.write_byte(i2c_7bit_address, ON_DEMAND_MEASURE_CMD)
+
+# Now wait for the ready signal (falling edge) before continuing
+while (not GPIO.event_detected(READY_pin)):
+ sleep(0.05)
+
+# New data are now ready to read.
+
+#########################################################
+
+# There are multiple ways to read and display the data
+
+
+# 1. Simplest way: use the example functions
+
+# Read all the "air data" from the MS430. This includes temperature and
+# humidity as well as pressure and gas sensor data. Return the data as
+# a data dictionary.
+air_data = get_air_data(I2C_bus)
+
+# Then print all the values onto the screen
+writeAirData(None, air_data, False)
+
+# Or you can use the values directly
+print("The temperature is: {:.1f} ".format(air_data['T_C']) + air_data['C_unit'])
+print("The humidity is: {:.1f} %".format(air_data['H_pc']))
+
+# Temperature can also be output in Fahrenheit units
+print("The temperature is: {:.1f} ".format(air_data['T_F']) + air_data['F_unit'])
+
+# The default temperature unit can be set in sensor_functions.py and used like:
+print("The temperature is: {:.1f} ".format(air_data['T']) + air_data['T_unit'])
+
+print("-----------------------------")
+
+# 2. Advanced: read and decode only the humidity value
+
+# Get the data from the MS430
+raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, H_READ, H_BYTES)
+
+# Decode the humidity: the first received byte is the integer part, the
+# second byte is the fractional part (to one decimal place).
+humidity = raw_data[0] + float(raw_data[1])/10.0
+
+# Print it: the units are percentage relative humidity.
+print("Humidity = {:.1f} %".format(humidity))
+
+print("-----------------------------")
+
+# 3. Advanced: read and decode only the temperature value (Celsius)
+
+# Get the data from the MS430
+raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, T_READ, T_BYTES)
+
+# 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
+temp_positive_integer = raw_data[0] & TEMPERATURE_VALUE_MASK
+
+# The second received byte is the fractional part to one decimal place
+temp_fraction = raw_data[1]
+
+# Combine to form a positive floating point number:
+temperature = temp_positive_integer + float(temp_fraction)/10.0
+
+# Now find the sign of the temperature: if the most-significant bit of
+# the first byte is a 1, the temperature is negative (below 0 C)
+if ((raw_data[0] & TEMPERATURE_SIGN_MASK) != 0):
+ # The bit is a 1: temperature is negative
+ temperature = -temperature
+
+# Print the temperature: the units are degrees Celsius.
+print("Temperature = {:.1f} ".format(temperature) + CELSIUS_SYMBOL)
+
+#########################################################
+
+GPIO.cleanup()
+
diff --git a/Raspberry_Pi/simple_read_sound.py b/Python/Raspberry_Pi/simple_read_sound.py
similarity index 57%
rename from Raspberry_Pi/simple_read_sound.py
rename to Python/Raspberry_Pi/simple_read_sound.py
index 1aadd3a..69d604a 100644
--- a/Raspberry_Pi/simple_read_sound.py
+++ b/Python/Raspberry_Pi/simple_read_sound.py
@@ -3,7 +3,9 @@
# Example code for using the Metriful MS430 to measure sound.
# This example is designed to run with Python 3 on a Raspberry Pi.
-# Measures and displays the sound data once.
+# Demonstrates multiple ways of reading and displaying the sound data.
+# View the output in the terminal. 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
@@ -11,8 +13,7 @@
# For code examples, datasheet and user guide, visit
# https://github.com/metriful/sensor
-from time import sleep
-from sensor_functions import *
+from sensor_package.sensor_functions import *
# Set up the GPIO and I2C communications bus
(GPIO, I2C_bus) = SensorHardwareSetup()
@@ -32,27 +33,35 @@
while (not GPIO.event_detected(READY_pin)):
sleep(0.05)
-# We now know that newly measured data are ready to read.
+# New data are now ready to read.
#########################################################
-# SOUND DATA
+# There are multiple ways to read and display the data
+
+
+# 1. Simplest way: use the example functions
+
+# Read all sound data from the MS430 and convert to a Python dictionary
+sound_data = get_sound_data(I2C_bus)
+
+# Then print all the values onto the screen
+writeSoundData(None, sound_data, False)
-# Read all sound data in one transaction
+# Or you can use the dictionary values directly, for example:
+print("The sound pressure level is: " + str(sound_data['SPL_dBA']) + " dBA")
+
+
+# 2. Read the raw data bytes from the MS430 using an I2C function
raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES)
-# Use the example function to decode the values and return then as a Python dictionary
+# Decode the values and return then as a Python dictionary
sound_data = extractSoundData(raw_data)
-# Print the values obtained
-print("A-weighted sound pressure level = {:.1f} dBA".format(sound_data['SPL_dBA']))
-for i in range(0,SOUND_FREQ_BANDS):
- print("Frequency band " + str(i+1) + " (" + str(sound_band_mids_Hz[i])
- + " Hz) SPL = {:.1f} dB".format(sound_data['SPL_bands_dB'][i]))
-print("Peak sound amplitude = {:.2f} mPa".format(sound_data['peak_amp_mPa']))
-
-# Or just use the following function for printing:
+# Print the dictionary values in the same ways as before
writeSoundData(None, sound_data, False)
+print("The sound pressure level is: " + str(sound_data['SPL_dBA']) + " dBA")
+
#########################################################
diff --git a/Python/Raspberry_Pi/web_server.py b/Python/Raspberry_Pi/web_server.py
new file mode 100644
index 0000000..a640b07
--- /dev/null
+++ b/Python/Raspberry_Pi/web_server.py
@@ -0,0 +1,123 @@
+# web_server.py
+
+# Example code for serving a text web page over a local network to
+# display environment data read from the Metriful MS430.
+# This example is designed to run with Python 3 on a Raspberry Pi.
+
+# All environment data values are measured and displayed on a text
+# web page generated by this program acting as a simple web server.
+# The web page can be viewed from other devices connected to the same
+# network(s) as the host Raspberry Pi, including wired and wireless
+# networks.
+
+# NOTE: if you run, exit, then re-run this program, you may get an
+# "Address already in use" error. This ends after a short period: wait
+# one minute then retry.
+
+# 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
+
+import socketserver
+from sensor_package.servers import *
+from sensor_package.sensor_functions import *
+
+#########################################################
+# 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
+cycle_period = CYCLE_PERIOD_3_S
+
+# The web page address will be:
+# http://:8080 e.g. http://172.24.1.1:8080
+
+# Find your Raspberry Pi's IP address from the admin interface of your
+# router, or:
+# 1. Enter the command ifconfig in a terminal
+# 2. Each available network connection displays a block of output
+# 3. Ignore the "lo" output block
+# 4. The host's IP address on each network is displayed after "inet"
+#
+# Example - part of an output block showing the address 172.24.1.1
+#
+# wlan0: flags=4163 mtu 1500
+# inet 172.24.1.1 netmask 255.255.255.0 broadcast 172.24.1.255
+
+# END OF USER-EDITABLE SETTINGS
+#########################################################
+
+# Set up the GPIO and I2C communications bus
+(GPIO, I2C_bus) = SensorHardwareSetup()
+
+# Apply the chosen settings to the MS430
+I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR])
+I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period])
+
+# Set the automatic refresh period of the web page. It should refresh
+# at least as often as new data are obtained. A more frequent refresh is
+# best for long cycle periods because the page access will be
+# out-of-step with the cycle. Users can also manually refresh the page.
+if (cycle_period == CYCLE_PERIOD_3_S):
+ SimpleWebpageHandler.refresh_period_seconds = 3
+elif (cycle_period == CYCLE_PERIOD_100_S):
+ SimpleWebpageHandler.refresh_period_seconds = 30
+else: # CYCLE_PERIOD_300_S
+ SimpleWebpageHandler.refresh_period_seconds = 50
+
+# Choose the TCP port number for the web page.
+port = 8080
+# The port can be any unused number from 1-65535 but values below 1024
+# require this program to be run as super-user as follows:
+# sudo python3 web_server.py
+# Port 80 is the default for HTTP, and with this value the port number
+# can be omitted from the web address. e.g. http://172.24.1.1
+
+print("Starting the web server. Your web page will be available at:")
+print("http://:" + str(port))
+print("Press ctrl-c to exit.")
+
+the_server = socketserver.TCPServer(("", port), SimpleWebpageHandler)
+the_server.timeout = 0.1
+
+# Enter cycle mode to start periodic data output
+I2C_bus.write_byte(i2c_7bit_address, CYCLE_MODE_CMD)
+
+while (True):
+
+ # While waiting for the next data release, respond to client requests
+ # by serving the web page with the last available data.
+ while (not GPIO.event_detected(READY_pin)):
+ the_server.handle_request()
+ sleep(0.05)
+
+ # Now read all data from the MS430 and pass to the web page
+
+ # Air data
+ SimpleWebpageHandler.air_data = get_air_data(I2C_bus)
+
+ # 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.
+ SimpleWebpageHandler.air_quality_data = get_air_quality_data(I2C_bus)
+
+ # Light data
+ SimpleWebpageHandler.light_data = get_light_data(I2C_bus)
+
+ # Sound data
+ SimpleWebpageHandler.sound_data = get_sound_data(I2C_bus)
+
+ # Particle data
+ # This requires the connection of a particulate sensor (invalid
+ # values will be obtained if this sensor is not present).
+ # 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):
+ SimpleWebpageHandler.particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR)
+
+ # Create the web page ready for client requests
+ SimpleWebpageHandler.assemble_web_page()
diff --git a/Python/graph_viewer_I2C.py b/Python/graph_viewer_I2C.py
new file mode 100644
index 0000000..9ad9235
--- /dev/null
+++ b/Python/graph_viewer_I2C.py
@@ -0,0 +1,159 @@
+# graph_viewer_I2C.py
+
+# NOTE on operating system/platform:
+
+# This example runs on Raspberry Pi only, and the MS430 sensor board
+# must be connected to the Raspberry Pi I2C/GPIO pins.
+
+# An alternate version, "graph_viewer_serial.py" runs on multiple operating
+# systems (including Windows and Linux) and uses serial over USB to get
+# data from the MS430 sensor via a microcontroller board (e.g. Arduino,
+# ESP8266, etc).
+
+#########################################################
+
+# This example displays a graphical user interface with real-time
+# updating graphs showing data from the MS430 sensor board.
+
+# Installation instructions for the necessary packages are 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
+
+import datetime
+from GraphViewer import *
+from Raspberry_Pi.sensor_package.sensor_functions import *
+
+#########################################################
+# USER-EDITABLE SETTINGS
+
+# Choose the delay between data measurements. This can be 3/100/300 seconds
+# in cycle mode, or any delay time in on-demand mode
+
+# Set cycle_period_code=None to use on-demand mode
+cycle_period_code = None # CYCLE_PERIOD_3_S, CYCLE_PERIOD_100_S, CYCLE_PERIOD_300_S, or None
+# OR:
+on_demand_delay_ms = 0 # Choose any number of milliseconds
+# This delay is in addition to the 0.5 second readout time
+
+# Temperature and particle data are less accurate if read more
+# frequently than every 2 seconds
+
+# Maximum number of values of each variable to store and display:
+data_buffer_length = 500
+
+# Specify the particle sensor model (PPD42/SDS011/none) and temperature
+# units (Celsuis/Fahrenheit) in Raspberry_Pi/sensor_functions.py
+
+# END OF USER-EDITABLE SETTINGS
+#########################################################
+
+class GraphViewerI2C(GraphViewer):
+ def __init__(self, buffer_length, cycle_period, OD_delay_ms):
+ super(GraphViewerI2C, self).__init__(buffer_length)
+ # Set up the I2C and the MS430 board
+ (self.GPIO, self.I2C_bus) = SensorHardwareSetup()
+ if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF):
+ self.I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR])
+ self.get_particle_data = True
+ if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42):
+ self.names_units['Particle concentration'] = self.PPD_unit
+ else:
+ self.names_units['Particle concentration'] = self.SDS_unit
+ else:
+ self.get_particle_data = False
+ if ((cycle_period is None) and (OD_delay_ms is None)):
+ raise Exception("Either cycle_period or OD_delay_ms must be specified")
+ # Set read mode for the MS430: cycle or on-demand
+ if (cycle_period is not None):
+ # Use cycle mode with 3/100/300 second delay periods
+ self.I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period])
+ self.I2C_bus.write_byte(i2c_7bit_address, CYCLE_MODE_CMD)
+ self.cycle_mode = True
+ else:
+ # Use on-demand mode with any chosen time delay between measurements
+ self.I2C_bus.write_byte(i2c_7bit_address, ON_DEMAND_MEASURE_CMD)
+ self.delaying = False
+ self.OD_delay_ms = OD_delay_ms
+ self.cycle_mode = False
+ if USE_FAHRENHEIT:
+ self.useFahrenheitTemperatureUnits(True)
+ # select data variables from name list
+ self.indices = list(range(0,4))
+ if (self.cycle_mode):
+ self.indices += list(range(4,8))
+ self.band1_index = 11
+ else:
+ self.band1_index = 7
+ self.indices += list(range(8,18))
+ if (self.get_particle_data):
+ self.indices += list(range(19,21))
+ self.createDataBuffer()
+ self.initializeComboBoxes()
+
+
+ # Check for new I2C data
+ def getDataFunction(self):
+ if (self.cycle_mode):
+ if GPIO.event_detected(READY_pin):
+ self.readData()
+ return True
+ else:
+ # On-demand mode
+ if (self.delaying):
+ time_now_ms = (datetime.datetime.now().timestamp())*1000
+ if ((time_now_ms-self.time_start_ms) >= self.OD_delay_ms):
+ # Trigger a new measurement
+ self.I2C_bus.write_byte(i2c_7bit_address, ON_DEMAND_MEASURE_CMD)
+ self.delaying = False
+ else:
+ if GPIO.event_detected(READY_pin):
+ self.readData()
+ self.delaying = True
+ self.time_start_ms = (datetime.datetime.now().timestamp())*1000
+ return True
+ return False
+
+
+ def readData(self):
+ self.setWindowTitle('Indoor Environment Data')
+ air_data = get_air_data(self.I2C_bus)
+ air_quality_data = get_air_quality_data(self.I2C_bus)
+ light_data = get_light_data(self.I2C_bus)
+ sound_data = get_sound_data(self.I2C_bus)
+ particle_data = get_particle_data(self.I2C_bus, PARTICLE_SENSOR)
+ self.putDataInBuffer(air_data, air_quality_data, light_data, sound_data, particle_data)
+
+
+ def appendData(self, start_index, data):
+ for i,v in enumerate(data):
+ self.data_buffer[start_index+i].append(v)
+ return (start_index + len(data))
+
+
+ # Store the data and also the time/date
+ def putDataInBuffer(self, air_data, air_quality_data, light_data, sound_data, particle_data):
+ i=0
+ i = self.appendData(i, [air_data['T'], air_data['P_Pa'], air_data['H_pc'], air_data['G_ohm']])
+ if (self.cycle_mode):
+ i = self.appendData(i, [air_quality_data['AQI'], air_quality_data['CO2e'],
+ air_quality_data['bVOC'], air_quality_data['AQI_accuracy']])
+ i = self.appendData(i, [light_data['illum_lux'], light_data['white']])
+ i = self.appendData(i, [sound_data['SPL_dBA']] +
+ [sound_data['SPL_bands_dB'][i] for i in range(0,self.sound_band_number)] +
+ [sound_data['peak_amp_mPa']])
+ if (self.get_particle_data):
+ i = self.appendData(i, [particle_data['duty_cycle_pc'], particle_data['concentration']])
+ self.time_data.append(datetime.datetime.now().timestamp())
+
+
+
+if __name__ == '__main__':
+ theApp = QtGui.QApplication([])
+ gv = GraphViewerI2C(data_buffer_length, cycle_period_code, on_demand_delay_ms)
+ gv.start()
+ theApp.exec_()
diff --git a/Python/graph_viewer_serial.py b/Python/graph_viewer_serial.py
new file mode 100644
index 0000000..556c45f
--- /dev/null
+++ b/Python/graph_viewer_serial.py
@@ -0,0 +1,138 @@
+# graph_viewer_serial.py
+
+# NOTE on operating system/platform:
+
+# This example runs on multiple operating systems (including Windows
+# and Linux) and uses serial over USB to get data from the MS430 sensor
+# via a microcontroller board (e.g. Arduino, ESP8266, etc).
+
+# An alternate version, "graph_viewer_I2C.py" is provided for the
+# Raspberry Pi, where the MS430 board is directly connected to the Pi
+# using the GPIO/I2C pins.
+
+#########################################################
+
+# This example displays a graphical user interface with real-time
+# updating graphs showing data from the MS430 sensor board.
+
+# Instructions (installation instructions are in the readme / User Guide)
+
+# 1) Program the microcontroller board with either "cycle_readout.ino"
+# or "on_demand_readout.ino", with printDataAsColumns = true
+
+# 2) Connect the microcontroller USB cable to your PC and close any
+# serial monitor software.
+
+# 3) Put the serial port name (system dependent) in the serial_port_name
+# parameter below.
+
+# 4) Run this program with python3
+
+# 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
+
+#########################################################
+# USER-EDITABLE SETTINGS
+
+# Which particle sensor is connected - this is used to select the
+# displayed measurement units (the microcontroller must also be
+# programmed to use the sensor)
+particle_sensor = "PPD42" # put here: "SDS011", "PPD42", or None
+
+# Choose which temperature label to use for display (NOTE: the actual
+# measurement unit depends on the microcontroller program).
+# Celsius is default.
+use_fahrenheit = False
+
+# Specify the serial port name on which the microcontroller is connected
+# e.g. on Windows this is usually a name like "COM1", on Linux it is
+# usually a path like "/dev/ttyACM0"
+# NOTE: close all other serial applications (e.g. Arduino Serial Monitor)
+serial_port_name = "/dev/ttyACM0"
+
+# Maximum number of values of each variable to store and display:
+data_buffer_length = 500
+
+# END OF USER-EDITABLE SETTINGS
+#########################################################
+
+import datetime
+import serial
+from GraphViewer import *
+
+class GraphViewerSerial(GraphViewer):
+ def __init__(self, buffer_length, serial_port):
+ super(GraphViewerSerial, self).__init__(buffer_length)
+ self.serial_port = serial.Serial(
+ port = serial_port,
+ baudrate = 9600,
+ parity=serial.PARITY_NONE,
+ stopbits=serial.STOPBITS_ONE,
+ bytesize=serial.EIGHTBITS,
+ timeout=0.02)
+ self.startup = True
+ self.initial_discard_lines = 2
+ self.line_count = 0
+ # There are 4 input cases resulting from the use of cycle_readout.ino and
+ # on_demand_readout.ino, with 15, 18, 19 and 22 data columns. These lists
+ # define which variables are present in each case:
+ self.col_indices = []
+ self.col_indices.append(list(range(0,4)) + list(range(8,19))) # no air quality or particle data
+ self.col_indices.append(list(range(0,4)) + list(range(8,22))) # no air quality data
+ self.col_indices.append(list(range(0,19))) # no particle data
+ self.col_indices.append(list(range(0,22))) # all data
+ self.sound_band1_index = [7, 7, 11, 11] # the index of 'Band 1 SPL' in the four lists
+
+
+ # Allow for initial corrupted serial data, incomplete lines or printed
+ # messages by discarding lines until a correct number of columns appears
+ def serialStartupCompleted(self, data_strings):
+ if (self.startup):
+ self.line_count+=1
+ if (self.line_count >= self.initial_discard_lines):
+ nc = len(data_strings)
+ for i in range(0,len(self.col_indices)):
+ if (nc == len(self.col_indices[i])):
+ self.startup = False
+ self.indices = self.col_indices[i]
+ self.band1_index = self.sound_band1_index[i]
+ self.createDataBuffer()
+ self.initializeComboBoxes()
+ self.setWindowTitle('Indoor Environment Data')
+ if (self.startup):
+ raise Exception('Unexpected number of data columns')
+ return (not self.startup)
+
+
+ # Check for new serial data
+ def getDataFunction(self):
+ response = self.serial_port.readline()
+ if (not ((response is None) or (len(response) == 0))):
+ # A complete line was received: convert it to string and split at spaces:
+ try:
+ data_strings = response.decode('utf-8').split()
+ if (self.serialStartupCompleted(data_strings)):
+ # Check number of values received; if incorrect, ignore the data
+ if (len(data_strings) == len(self.indices)):
+ # Convert strings to numbers and store the data
+ float_data = [float(i) for i in data_strings]
+ for i in range(0, len(self.indices)):
+ self.data_buffer[i].append(float_data[i])
+ self.time_data.append(datetime.datetime.now().timestamp())
+ return True
+ except:
+ pass
+ return False # no new data
+
+
+
+if __name__ == '__main__':
+ theApp = QtGui.QApplication([])
+ gv = GraphViewerSerial(data_buffer_length, serial_port_name)
+ gv.setParticleUnits(particle_sensor)
+ gv.useFahrenheitTemperatureUnits(use_fahrenheit)
+ gv.start()
+ theApp.exec_()
diff --git a/README.md b/README.md
index 3a18017..a890edf 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,46 @@
# Metriful MS430: Environment Sensor
-
+
-![](sensor_pcb.png)
+
-The Metriful MS430 is a low power, high accuracy, smart sensor cluster for indoor environment monitoring. It is operated via a simple I2C-compatible interface and measures eighteen variables including air quality, light and sound levels.
+[**Buy the hardware now - REDUCED PRICES.**](https://www.metriful.com/shop)
-This repository provides instructions and software examples for running the MS430 module with Raspberry Pi, Arduino and NodeMCU host systems. These are also useful starting points for development with other hosts.
+The Metriful MS430 is a low power, high accuracy, smart sensor cluster for indoor environment monitoring. It operates via a simple I2C-compatible interface and measures eighteen variables including air quality, light and sound levels.
-The code examples demonstrate various ways of using the module. This includes basic control/readout, saving data to files and IoT cloud storage. Detailed comments explain each part of the programs.
+This repository provides instructions and software examples for running the MS430 with **Raspberry Pi, Arduino, ESP8266** and **ESP32** host systems.
-The [**User Guide**](User_guide.pdf) covers hardware setup in more detail, gives an overview of the code examples and explains more about what the device measures.
+Code examples include interfaces to **IFTTT, Home Assistant** and **IoT cloud platforms**, as well as **real-time graph software, web servers** and **interrupt detection**.
+
+This readme provides a quick-start guide to running the examples on various host systems.
+
+The [**User Guide**](User_guide.pdf) gives an overview of the code examples and explains more about what the device measures.
The [**Datasheet**](Datasheet.pdf) is a detailed specification of the electrical and communications interfaces of the MS430.
-You can also [visit the product homepage.](https://www.sensor.metriful.com)
### Contents
-**[Handling precautions](#handling-precautions)**
-**[Use with Arduino](#use-with-arduino)**
-**[Use with Raspberry Pi](#use-with-raspberry-pi)**
-**[Use with NodeMCU](#use-with-nodemcu)**
-**[IoT cloud setup](#iot-cloud-setup)**
-**[License](#license)**
-**[Disclaimer](#disclaimer)**
+
+#### Hardware setup
+- **[Handling precautions](#handling-precautions)**
+- **[Arduino](#arduino)**
+- **[Raspberry Pi](#raspberry-pi)**
+- **[ESP8266](#esp8266)**
+- **[ESP32](#esp32)**
+- **[Particle sensor](#connecting-and-enabling-a-particle-sensor)**
+#### Code example setup
+- **[IoT cloud setup](#iot-cloud-setup)**
+- **[Graph web server](#graph-web-server)**
+- **[IFTTT example](#ifttt-example)**
+- **[Home Assistant example](#home-assistant-example)**
+- **[Graph viewer software](#graph-viewer-software)**
+- **[Fahrenheit temperatures](#fahrenheit-temperatures)**
+#### Other information
+- **[Case and enclosure ideas](#case-enclosure-and-mounting-ideas)**
+- **[Troubleshooting](#troubleshooting)**
+- **[Changelog](#changelog)**
+- **[License](#license)**
+- **[Disclaimer](#disclaimer)**
## Handling precautions
@@ -36,7 +53,7 @@ The MS430 can be damaged by static electricity discharges. Minimize this risk by
- Keep away from metal objects which could cause shorted connections
-## Use with Arduino
+## Arduino
All code examples in the Arduino folder run on the Arduino Nano 33 IoT and Arduino MKR WiFi 1010, while those not requiring a network connection also run on Arduino Uno and Nano.
@@ -46,17 +63,13 @@ Note that steps 1 and 2 are already complete if you have used Arduino before on
1. Download and install the [Arduino IDE](https://www.arduino.cc/en/main/software) on your computer.
2. Start the Arduino IDE for the first time. This will create a folder named **Arduino/libraries** in your user area (e.g. in the Documents folder on Windows computers).
-3. Download and unzip the [Sensor repository](https://www.github.com/metriful/sensor). From this, copy the folder **Metriful_Sensor** (located within the Arduino folder) into the Arduino libraries folder in your user area.
+3. Download and unzip the [Sensor repository](https://www.github.com/metriful/sensor). From this, copy the folder **Metriful_Sensor** (located within the Arduino folder) into the Arduino libraries folder in your user area. Delete any previous version you may have.
If using **Arduino Nano 33 IoT** or **Arduino MKR WiFi 1010**, also do the following:
-4. Download and install the SAMD board package: in the Arduino IDE menu, go to Tools > Board > Boards Manager (top of list). Search for and install **Arduino SAMD Boards (32-bits ARM Cortex-M0+)**
+4. Download and install the SAMD board package: in the Arduino IDE menu, go to Tools > Board > Boards Manager. Search for and install **Arduino SAMD Boards (32-bits ARM Cortex-M0+)**
5. Install the WiFiNINA package: in the Arduino IDE menu, go to Tools > Manage Libraries. Search for and install **WiFiNINA**.
-If using **Arduino Nano 33 IoT**, also do the following:
-
-6. Download the [SlowSoftWire library](https://github.com/felias-fogg/SlowSoftWire). Unzip it and move it into the Arduino libraries folder in your user area.
-7. Download the [SlowSoftI2CMaster library](https://github.com/felias-fogg/SlowSoftI2CMaster). Unzip it and move it into the Arduino libraries folder in your user area.
### Wiring for Arduino
@@ -66,27 +79,16 @@ If using **Arduino Nano 33 IoT**, also do the following:
| VDD | - | - | 3.3V | VCC |
| GND | GND | GND | GND | GND |
| VPU | IOREF | 5V | 3.3V | VCC |
-| SCL | SCL | A5 | A3 | SCL |
-| SDA | SDA | A4 | A0 | SDA |
+| SCL | SCL | A5 | A5 | SCL |
+| SDA | SDA | A4 | A4 | SDA |
| LIT | D4 | D4 | A1 | D4 |
| SIT | D7 | D7 | A2 | D5 |
| RDY | D2 | D2 | D11 | D0 |
-* MS430 pin VDD is not used with 5 V systems and VIN is not used with 3.3 V systems.
-
-* If using the PPD42 particle sensor, note that its pin numbering runs from right to left, and connect:
- * Arduino 5V pin to PPD42 pin 3
- * Arduino GND pin to PPD42 pin 1
- * PPD42 pin 4 to MS430 pin PRT
-
-* If using the SDS011 particle sensor, connect:
- * Arduino 5V pin to SDS011 pin "5V"
- * Arduino GND pin to SDS011 pin "GND"
- * SDS011 pin "25um" to MS430 pin PRT
-
-* To obtain 5V output on the Nano 33 IoT: the solder bridge labeled "VUSB" on the underside of the Arduino must be soldered closed, then use the VUSB pin.
-* To obtain a third 5V output on the Uno: use pin number 2 on the 6-pin ICSP header
-* With all hosts, VPU can be supplied from any host digital output pin set to a high voltage state. This can be useful for hosts without enough power output pins.
+* Arduino Nano 33 IoT used a software I2C library in previous code versions but now uses the hardware I2C pins.
+* MS430 pin VDD is not used with the 5V systems (Uno and Nano) and VIN is not used with the 3.3V systems (Nano 33 IoT and MKR WiFi 1010).
+* LIT/SIT connections are optional and only required if you are using light/sound interrupts.
+* VPU can be supplied from any spare host digital output pin set to a high voltage state. This can be useful for hosts without enough power output pins.
### To run an example program on Arduino
@@ -99,26 +101,32 @@ If using **Arduino Nano 33 IoT**, also do the following:
7. Go to Tools > Serial Monitor to view the output (ensure **9600 baud** is selected in the monitor).
-## Use with Raspberry Pi
+## Raspberry Pi
-The example programs for Raspberry Pi use Python 3 and are provided in the **Raspberry_Pi** folder.
+The example programs for Raspberry Pi use Python 3 and are provided in the **Raspberry_Pi** folder, within the **Python** folder.
### First time Raspberry Pi setup
-This setup assumes that you are using Raspbian Buster, which comes with all required Python packages already installed (the packages used are: **RPi.GPIO**, **smbus** and **requests**).
+This setup assumes that you are using Raspberry Pi OS. The standard OS version comes with all required Python packages already installed (except packages for the [Graph viewer software](#graph-viewer-software)). The **Lite** (command line) OS version requires package installation, as listed below.
+
+1. If you are using Raspberry Pi OS Lite (or get missing package errors), run the following commands to install the packages needed:
+ ```
+ sudo apt-get update
+ sudo apt install i2c-tools python3-smbus python3-rpi.gpio
+ ```
-1. Enable I2C on your Raspberry Pi using the raspi-config utility by opening a terminal and running:
+2. Enable I2C on your Raspberry Pi using the raspi-config utility by opening a terminal and running:
```
sudo raspi-config
```
Select **5 Interfacing Options** and then **P5 I2C**. A prompt will appear asking "Would you like the ARM I2C interface to be enabled?": select **Yes** and then exit the utility.
-2. Shut-down the Raspberry Pi and disconnect the power. Wire up the hardware as described in the following section. Double-check the wiring then restart the Pi.
-3. Check that the Metriful MS430 board can be detected by running:
+3. Shut-down the Raspberry Pi and disconnect the power. Wire up the hardware as described in the following section. Double-check the wiring then restart the Pi.
+4. Check that the Metriful MS430 board can be detected by running:
```
sudo i2cdetect -y 1
```
This should report the 7-bit address number **71**.
-4. Download and unzip this [Sensor repository](https://www.github.com/metriful/sensor). The Raspberry Pi examples are found within the folder named **Raspberry_Pi**.
+5. Download and unzip this [Sensor repository](https://www.github.com/metriful/sensor). The Raspberry Pi examples are found within the folder named **Raspberry_Pi**, inside the **Python** folder.
### Wiring for Raspberry Pi
@@ -134,17 +142,9 @@ This setup assumes that you are using Raspbian Buster, which comes with all requ
| SIT | 8 | GPIO 14 |
| RDY | 11 | GPIO 17 |
+* Raspberry Pi pin numbering is [shown here](https://www.raspberrypi.org/documentation/usage/gpio/README.md).
* MS430 pin VIN is not used.
-
-* If using the PPD42 particle sensor, note that its pin numbering runs from right to left, and connect:
- * Pi pin 2 (5V) to PPD42 pin 3
- * Pi pin 9 (Ground) to PPD42 pin 1
- * PPD42 pin 4 to MS430 pin PRT
-
-* If using the SDS011 particle sensor, connect:
- * Pi pin 2 (5V) to SDS011 pin "5V"
- * Pi pin 9 (Ground) to SDS011 pin "GND"
- * SDS011 pin "25um" to MS430 pin PRT
+* LIT/SIT connections are optional and only required if you are using light/sound interrupts.
### To run an example Raspberry Pi program:
@@ -156,26 +156,31 @@ This setup assumes that you are using Raspbian Buster, which comes with all requ
```
-## Use with NodeMCU
+## ESP8266
-All code examples in the Arduino folder run on the NodeMCU (ESP8266) and are programmed using the Arduino IDE.
+All code examples in the Arduino folder have been tested on NodeMCU and Wemos D1 Mini, and are programmed using the Arduino IDE.
-### First time NodeMCU setup
+Other ESP8266 development boards should also work but may use a different pinout and may therefore require edits to the host_pin_definitions.h file.
-Note that steps 1 and 2 are already complete if you have used Arduino before on your computer.
+Note that ESP8266 does not have a hardware I2C module, so any of the normal GPIO pins can be used for the I2C bus.
+
+### First time ESP8266 setup
+
+Note that steps 1 and 2 are already complete if you have used Arduino IDE before on your computer.
1. Download and install the [Arduino IDE](https://www.arduino.cc/en/main/software) on your computer.
2. Start the Arduino IDE for the first time. This will create a folder named **Arduino/libraries** in your user area (e.g. in the Documents folder on Windows computers).
-3. Download and unzip the [Sensor repository](https://www.github.com/metriful/sensor). From this, copy the folder **Metriful_Sensor** (located within the Arduino folder) into the Arduino libraries folder in your user area.
+3. Download and unzip the [Sensor repository](https://www.github.com/metriful/sensor). From this, copy the folder **Metriful_Sensor** (located within the Arduino folder) into the Arduino libraries folder in your user area. Remove any previous version you may have.
4. In the Arduino IDE menu, go to File > Preferences. In the box labeled "Additional Boards Manager URLs", paste the following link:
```
https://arduino.esp8266.com/stable/package_esp8266com_index.json
```
+ If there is already text in the box, place a comma and paste the new text after it.
5. In the Arduino IDE menu, go to Tools > Board > Boards Manager. Search for and install the package named **esp8266 by ESP8266 Community**.
-### Wiring for NodeMCU
+### Wiring for ESP8266
-| MS430 pin | NodeMCU |
+| MS430 pin | ESP8266 |
|:---------------:|:--------------------:|
| VIN | - |
| VDD | 3V3 |
@@ -187,32 +192,125 @@ Note that steps 1 and 2 are already complete if you have used Arduino before on
| SIT | D5 (GPIO 14) |
| RDY | D6 (GPIO 12) |
+* The **D** pin numbers refer to the usual pin labels printed on the development board. The GPIO numbers are the actual ESP8266 I/O identities.
* MS430 pin VIN is not used.
-
-* If using the PPD42 particle sensor, note that its pin numbering runs from right to left, and connect:
- * NodeMCU Vin (5V) pin to PPD42 pin 3
- * NodeMCU GND pin to PPD42 pin 1
- * PPD42 pin 4 to MS430 pin PRT
+* LIT/SIT connections are optional and only required if you are using light/sound interrupts.
+* VPU can be supplied from any spare host digital output pin set to a high voltage state. This can be useful for hosts without enough power output pins.
-* If using the SDS011 particle sensor, connect:
- * NodeMCU Vin (5V) pin to SDS011 pin "5V"
- * NodeMCU GND pin to SDS011 pin "GND"
- * SDS011 pin "25um" to MS430 pin PRT
+### To run an example program on ESP8266
+
+1. Wire the MS430 board to the ESP8266 as described in the previous section.
+2. Plug the ESP8266 board into your computer via USB.
+3. Start the Arduino IDE and open the chosen example code file, e.g. **simple_read_sound.ino**
+4. In the Arduino IDE menu, go to Tools > Port and select the port with the ESP8266 attached.
+5. Go to Tools > Board and select your development board, or "Generic ESP8266 Module", or experiment until you find one that works.
+6. Select Sketch > Upload and wait for upload confirmation.
+7. Go to Tools > Serial Monitor to view the output (ensure **9600 baud** is selected in the monitor).
-### To run an example program on NodeMCU
-1. Wire the MS430 board to the NodeMCU as described in the previous section.
-2. Plug the NodeMCU into your computer via USB.
+## ESP32
+
+All code examples in the Arduino folder have been tested on DOIT DevKit v1, and are programmed using the Arduino IDE.
+
+Other ESP32 development boards should also work but may use a different pinout and may therefore require edits to the host_pin_definitions.h file.
+
+### First time ESP32 setup
+
+Note that steps 1 and 2 are already complete if you have used Arduino IDE before on your computer.
+
+1. Download and install the [Arduino IDE](https://www.arduino.cc/en/main/software) on your computer.
+2. Start the Arduino IDE for the first time. This will create a folder named **Arduino/libraries** in your user area (e.g. in the Documents folder on Windows computers).
+3. Download and unzip the [Sensor repository](https://www.github.com/metriful/sensor). From this, copy the folder **Metriful_Sensor** (located within the Arduino folder) into the Arduino libraries folder in your user area. Remove any previous version you may have.
+4. In the Arduino IDE menu, go to File > Preferences. In the box labeled "Additional Boards Manager URLs", paste the following link:
+ ```
+ https://dl.espressif.com/dl/package_esp32_index.json
+ ```
+ If there is already text in the box, place a comma and paste the new text after it.
+5. In the Arduino IDE menu, go to Tools > Board > Boards Manager. Search for and install the package named **esp32 by Espressif Systems**.
+
+### Wiring for ESP32
+
+| MS430 pin | ESP32 |
+|:---------------:|:-------------:|
+| VIN | - |
+| VDD | 3V3 |
+| GND | GND |
+| VPU | 3V3 |
+| SCL | D22 |
+| SDA | D21 |
+| LIT | D18 |
+| SIT | D19 |
+| RDY | D23 |
+
+* MS430 pin VIN is not used.
+* LIT/SIT connections are optional and only required if you are using light/sound interrupts.
+* VPU can be supplied from any spare host digital output pin set to a high voltage state. This can be useful for hosts without enough power output pins.
+
+### To run an example program on ESP32
+
+1. Wire the MS430 board to the ESP32 as described in the previous section.
+2. Plug the ESP32 board into your computer via USB.
3. Start the Arduino IDE and open the chosen example code file, e.g. **simple_read_sound.ino**
-4. In the Arduino IDE menu, go to Tools > Port and select the port with the NodeMCU attached.
-5. Go to Tools > Board and select "Generic ESP8266 Module"
+4. In the Arduino IDE menu, go to Tools > Port and select the port with the ESP32 attached.
+5. Go to Tools > Board and select your development board, or experiment until you find one that works.
6. Select Sketch > Upload and wait for upload confirmation.
7. Go to Tools > Serial Monitor to view the output (ensure **9600 baud** is selected in the monitor).
+## Connecting and enabling a particle sensor
+
+The MS430 is compatible with two widely-available air particle sensors: the Shinyei PPD42 and the Nova SDS011. The particle sensor is optional and only a single sensor can be connected at any time.
+
+Both sensor models require three wire connections: **5V, GND, PRT** and a small edit to the example code.
+
+| | PPD42 pin number | SDS011 pin label |
+|:-----:|:----------------:|:----------------:|
+| 5V | 3 | 5V |
+| GND | 1 | GND |
+| PRT | 4 | 25um |
+
+* PRT is on the MS430 board
+* 5V and GND are supplied by the host system, or from a separate power supply. If using a separate power supply, the power supply GND must be connected to host GND.
+* The SDS011 pin labeled "25um" is the data output for 0.3 - 10 µm particles.
+
+5V is available from the hosts when they are powered from a USB power supply:
+
+| Host device | 5V pin name/number |
+|:---------------------:|:------------------:|
+| Raspberry Pi | Pin 2 |
+| Arduino Uno | 5V or IOREF (*) |
+| Arduino Nano | 5V |
+| Arduino Nano 33 IoT | VUSB (**) |
+| Arduino MKR WiFi 1010 | 5V |
+| ESP8266 | 5V or Vin or VU |
+| ESP32 | VIN |
+
+(*) To obtain a third 5V output from the **Uno**: use pin number 2 on the 6-pin ICSP header
+
+(**) To obtain 5V output on the **Nano 33 IoT**: the solder bridge labeled "VUSB" on the underside of the Arduino must be soldered closed, then use the VUSB pin.
+
+* **Raspberry Pi** pin 9 can be used as an extra GND connection.
+* Pin labels for ESP8266 and ESP32 may be different on some boards
+
+### Enable the particle sensor in the code examples
+
+* **Arduino**: in Metriful_sensor.h on the line:
+ ```
+ #define PARTICLE_SENSOR PARTICLE_SENSOR_OFF
+ ```
+ change ```PARTICLE_SENSOR_OFF``` to be either ```PARTICLE_SENSOR_PPD42``` or ```PARTICLE_SENSOR_SDS011```
+
+* **Raspberry Pi**: in /sensor_package/sensor_functions.py on the line:
+ ```
+ PARTICLE_SENSOR = PARTICLE_SENSOR_OFF
+ ```
+ change ```PARTICLE_SENSOR_OFF``` to be either ```PARTICLE_SENSOR_PPD42``` or ```PARTICLE_SENSOR_SDS011```
+
## IoT cloud setup
-The **IoT_cloud_logging** code example shows how to send data to an Internet of Things (IoT) cloud storage account. It can be used with Arduino Nano 33 IoT, MKR WiFi 1010, NodeMCU and Raspberry Pi host systems.
+
+
+The **IoT_cloud_logging** code example shows how to send data to an Internet of Things (IoT) cloud storage account. It can be used with Arduino Nano 33 IoT, MKR WiFi 1010, ESP8266, ESP32 and Raspberry Pi host systems.
IoT cloud hosting is available from many providers around the world. Some offer free accounts (with storage or access limits) for non-commercial purposes. The IoT cloud logging example gives a choice of two providers, [Tago.io](https://tago.io) and [Thingspeak.com](https://thingspeak.com). The following sections give a brief overview of how to set up free accounts with these providers: for further information see the relevant provider website.
@@ -267,10 +365,256 @@ The steps required to set up Thingspeak for the IoT cloud logging code example a
9. Go to the **API Keys** tab and copy the Write API Key (a sequence of letters and numbers).
10. Paste the API key into the Metriful IoT cloud logging example code as the variable **THINGSPEAK_API_KEY_STRING** and set the variable **useTagoCloud** as **false**.
+
+## Graph web server
+
+
+
+The **graph_web_server** example produces a web page with a set of graphs which display data stored on the host. The page can be accessed from other devices on your home network using their internet browsers. This is a good local alternative to using a cloud IoT service.
+
+When opened in a browser, the web page will attempt to run the [Plotly](https://plotly.com/javascript/) javascript library which is used to create the graphs. If there is no internet access, the browser may be able to use a cached copy if it previously accessed the page with internet access. Otherwise, the graphs will not load and you will only see text data.
+
+A button on the web page allows you to download the stored data as a CSV (comma separated value) text file, which can be opened with many spreadsheet applications.
+
+
+## IFTTT example
+
+The IFTTT example shows how to send data to [IFTTT.com](https://ifttt.com), which will trigger an email alert. It is compatible with IFTTT's free account.
+
+The host device monitors some of the environment data and sends the alert when the values go outside your chosen "safe" range. The emails you receive will look like:
+```
+The temperature is too high.
+The measurement was 25.5 °C on October 10, 2020 at 05.54PM.
+Turn on the fan.
+```
+You can customize all parts of this message.
+
+### Setup
+
+1. Go to [IFTTT.com](https://ifttt.com) and sign up for a free account.
+2. Click **Create** to start a new applet.
+3. Click **If This (Add)**, search for service **Webhooks** and click **Receive a web request**, then **Connect**.
+4. Enter an event name, e.g. **alert_event**, then click **Create trigger**.
+5. Click **Then That (Add)** and search for **email**.
+6. Choose either **Email** to get a new email for every alert, or choose **Email Digest** to get ONE email per day/week which contains all alerts. Then click **Connect**.
+7. Enter and validate your email address when prompted.
+8. Enter a Title/Subject for the emails, e.g. Environment data alert.
+9. Delete all text from the Body/Message text box and paste in the following:
+ ```
+ {{Value1}}