Initial import.
This commit is contained in:
commit
c7430f56db
|
@ -0,0 +1,6 @@
|
||||||
|
.pio
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
|
src/config.h
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
|
||||||
|
This directory is intended for project header files.
|
||||||
|
|
||||||
|
A header file is a file containing C declarations and macro definitions
|
||||||
|
to be shared between several project source files. You request the use of a
|
||||||
|
header file in your project source file (C, C++, etc) located in `src` folder
|
||||||
|
by including it, with the C preprocessing directive `#include'.
|
||||||
|
|
||||||
|
```src/main.c
|
||||||
|
|
||||||
|
#include "header.h"
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Including a header file produces the same results as copying the header file
|
||||||
|
into each source file that needs it. Such copying would be time-consuming
|
||||||
|
and error-prone. With a header file, the related declarations appear
|
||||||
|
in only one place. If they need to be changed, they can be changed in one
|
||||||
|
place, and programs that include the header file will automatically use the
|
||||||
|
new version when next recompiled. The header file eliminates the labor of
|
||||||
|
finding and changing all the copies as well as the risk that a failure to
|
||||||
|
find one copy will result in inconsistencies within a program.
|
||||||
|
|
||||||
|
In C, the usual convention is to give header files names that end with `.h'.
|
||||||
|
It is most portable to use only letters, digits, dashes, and underscores in
|
||||||
|
header file names, and at most one dot.
|
||||||
|
|
||||||
|
Read more about using header files in official GCC documentation:
|
||||||
|
|
||||||
|
* Include Syntax
|
||||||
|
* Include Operation
|
||||||
|
* Once-Only Headers
|
||||||
|
* Computed Includes
|
||||||
|
|
||||||
|
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
|
@ -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 <Metriful_sensor.h>
|
||||||
|
#include <WiFi_functions.h>
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// 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; i<strlen(fieldBuffer); i++) {
|
||||||
|
if (fieldBuffer[i] == ' ') {
|
||||||
|
fieldBuffer[i] = '_';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sprintf(postBuffer,"POST /api/states/" SENSOR_NAME ".%s HTTP/1.1", fieldBuffer);
|
||||||
|
client.println(postBuffer);
|
||||||
|
client.println("Host: " HOME_ASSISTANT_IP ":8123");
|
||||||
|
client.println("Content-Type: application/json");
|
||||||
|
client.println("Authorization: Bearer " LONG_LIVED_ACCESS_TOKEN);
|
||||||
|
|
||||||
|
// Assemble the JSON content string:
|
||||||
|
sprintf(postBuffer,"{\"state\":%s,\"attributes\":{\"unit_of_measurement\""
|
||||||
|
":\"%s\",\"friendly_name\":\"%s\",\"icon\":\"mdi:%s\"}}",
|
||||||
|
valueText, attributes->unit, 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.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <Metriful_sensor.h>
|
||||||
|
#include <WiFi_functions.h>
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// 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.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,284 @@
|
||||||
|
/*
|
||||||
|
IoT_cloud_logging.ino
|
||||||
|
|
||||||
|
Example IoT data logging code for the Metriful MS430.
|
||||||
|
|
||||||
|
This example is designed for the following WiFi enabled hosts:
|
||||||
|
* Arduino Nano 33 IoT
|
||||||
|
* Arduino MKR WiFi 1010
|
||||||
|
* ESP8266 boards (e.g. Wemos D1, NodeMCU)
|
||||||
|
* ESP32 boards (e.g. DOIT DevKit v1)
|
||||||
|
|
||||||
|
Environmental data values are measured and logged to an internet
|
||||||
|
cloud account every 100 seconds, using a WiFi network. The example
|
||||||
|
gives the choice of using either the Tago.io or Thingspeak.com
|
||||||
|
clouds – both of these offer a free account for low data rates.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Metriful_sensor.h>
|
||||||
|
#include <WiFi_functions.h>
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// USER-EDITABLE SETTINGS
|
||||||
|
|
||||||
|
// How often to read and log data (every 100 or 300 seconds)
|
||||||
|
// Note: due to data rate limits on free cloud services, this should
|
||||||
|
// be set to 100 or 300 seconds, not 3 seconds.
|
||||||
|
uint8_t cycle_period = CYCLE_PERIOD_100_S;
|
||||||
|
|
||||||
|
// The details of the WiFi network:
|
||||||
|
char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name)
|
||||||
|
char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password
|
||||||
|
|
||||||
|
// IoT cloud settings
|
||||||
|
// This example uses the free IoT cloud hosting services provided
|
||||||
|
// by Tago.io or Thingspeak.com
|
||||||
|
// Other free cloud providers are available.
|
||||||
|
// An account must have been set up with the relevant cloud provider
|
||||||
|
// and a WiFi internet connection must exist. See the accompanying
|
||||||
|
// readme and User Guide for more information.
|
||||||
|
|
||||||
|
// The chosen account's key/token must be put into the relevant define below.
|
||||||
|
#define TAGO_DEVICE_TOKEN_STRING "PASTE YOUR TOKEN HERE WITHIN QUOTES"
|
||||||
|
#define THINGSPEAK_API_KEY_STRING "PASTE YOUR API KEY HERE WITHIN QUOTES"
|
||||||
|
|
||||||
|
// Choose which provider to use
|
||||||
|
bool useTagoCloud = true;
|
||||||
|
// To use the ThingSpeak cloud, set: useTagoCloud=false
|
||||||
|
|
||||||
|
// END OF USER-EDITABLE SETTINGS
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#if !defined(HAS_WIFI)
|
||||||
|
#error ("This example program has been created for specific WiFi enabled hosts only.")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
WiFiClient client;
|
||||||
|
|
||||||
|
// Buffers for assembling http POST requests
|
||||||
|
char postBuffer[450] = {0};
|
||||||
|
char fieldBuffer[70] = {0};
|
||||||
|
|
||||||
|
// Structs for data
|
||||||
|
AirData_t airData = {0};
|
||||||
|
AirQualityData_t airQualityData = {0};
|
||||||
|
LightData_t lightData = {0};
|
||||||
|
ParticleData_t particleData = {0};
|
||||||
|
SoundData_t soundData = {0};
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Initialize the host's pins, set up the serial port and reset:
|
||||||
|
SensorHardwareSetup(I2C_ADDRESS);
|
||||||
|
|
||||||
|
connectToWiFi(SSID, password);
|
||||||
|
|
||||||
|
// Apply chosen settings to the MS430
|
||||||
|
uint8_t particleSensor = PARTICLE_SENSOR;
|
||||||
|
TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1);
|
||||||
|
TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1);
|
||||||
|
|
||||||
|
// Enter cycle mode
|
||||||
|
ready_assertion_event = false;
|
||||||
|
TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
|
||||||
|
// Wait for the next new data release, indicated by a falling edge on READY
|
||||||
|
while (!ready_assertion_event) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
ready_assertion_event = false;
|
||||||
|
|
||||||
|
/* Read data from the MS430 into the data structs.
|
||||||
|
For each category of data (air, sound, etc.) a pointer to the data struct is
|
||||||
|
passed to the ReceiveI2C() function. The received byte sequence fills the
|
||||||
|
struct in the correct order so that each field within the struct receives
|
||||||
|
the value of an environmental quantity (temperature, sound level, etc.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Air data
|
||||||
|
// Choose output temperature unit (C or F) in Metriful_sensor.h
|
||||||
|
ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
|
||||||
|
|
||||||
|
/* Air quality data
|
||||||
|
The initial self-calibration of the air quality data may take several
|
||||||
|
minutes to complete. During this time the accuracy parameter is zero
|
||||||
|
and the data values are not valid.
|
||||||
|
*/
|
||||||
|
ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
|
||||||
|
|
||||||
|
// Light data
|
||||||
|
ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
|
||||||
|
|
||||||
|
// Sound data
|
||||||
|
ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
|
||||||
|
|
||||||
|
/* Particle data
|
||||||
|
This requires the connection of a particulate sensor (invalid
|
||||||
|
values will be obtained if this sensor is not present).
|
||||||
|
Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h
|
||||||
|
Also note that, due to the low pass filtering used, the
|
||||||
|
particle data become valid after an initial initialization
|
||||||
|
period of approximately one minute.
|
||||||
|
*/
|
||||||
|
if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
|
||||||
|
ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that WiFi is still connected
|
||||||
|
uint8_t wifiStatus = WiFi.status();
|
||||||
|
if (wifiStatus != WL_CONNECTED) {
|
||||||
|
// There is a problem with the WiFi connection: attempt to reconnect.
|
||||||
|
Serial.print("Wifi status: ");
|
||||||
|
Serial.println(interpret_WiFi_status(wifiStatus));
|
||||||
|
connectToWiFi(SSID, password);
|
||||||
|
ready_assertion_event = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send data to the cloud
|
||||||
|
if (useTagoCloud) {
|
||||||
|
http_POST_data_Tago_cloud();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
http_POST_data_Thingspeak_cloud();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* For both example cloud providers, the following quantities will be sent:
|
||||||
|
1 Temperature (C or F)
|
||||||
|
2 Pressure/Pa
|
||||||
|
3 Humidity/%
|
||||||
|
4 Air quality index
|
||||||
|
5 bVOC/ppm
|
||||||
|
6 SPL/dBA
|
||||||
|
7 Illuminance/lux
|
||||||
|
8 Particle concentration
|
||||||
|
|
||||||
|
Additionally, for Tago, the following is sent:
|
||||||
|
9 Air Quality Assessment summary (Good, Bad, etc.)
|
||||||
|
10 Peak sound amplitude / mPa
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Assemble the data into the required format, then send it to the
|
||||||
|
// Tago.io cloud as an HTTP POST request.
|
||||||
|
void http_POST_data_Tago_cloud(void) {
|
||||||
|
client.stop();
|
||||||
|
if (client.connect("api.tago.io", 80)) {
|
||||||
|
client.println("POST /data HTTP/1.1");
|
||||||
|
client.println("Host: api.tago.io");
|
||||||
|
client.println("Content-Type: application/json");
|
||||||
|
client.println("Device-Token: " TAGO_DEVICE_TOKEN_STRING);
|
||||||
|
|
||||||
|
uint8_t T_intPart = 0;
|
||||||
|
uint8_t T_fractionalPart = 0;
|
||||||
|
bool isPositive = true;
|
||||||
|
getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive);
|
||||||
|
sprintf(postBuffer,"[{\"variable\":\"temperature\",\"value\":%s%u.%u}",
|
||||||
|
isPositive?"":"-", T_intPart, T_fractionalPart);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,",{\"variable\":\"pressure\",\"value\":%" PRIu32 "}", airData.P_Pa);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,",{\"variable\":\"humidity\",\"value\":%u.%u}",
|
||||||
|
airData.H_pc_int, airData.H_pc_fr_1dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,",{\"variable\":\"aqi\",\"value\":%u.%u}",
|
||||||
|
airQualityData.AQI_int, airQualityData.AQI_fr_1dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,",{\"variable\":\"aqi_string\",\"value\":\"%s\"}",
|
||||||
|
interpret_AQI_value(airQualityData.AQI_int));
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,",{\"variable\":\"bvoc\",\"value\":%u.%02u}",
|
||||||
|
airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,",{\"variable\":\"spl\",\"value\":%u.%u}",
|
||||||
|
soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,",{\"variable\":\"peak_amp\",\"value\":%u.%02u}",
|
||||||
|
soundData.peak_amp_mPa_int, soundData.peak_amp_mPa_fr_2dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,",{\"variable\":\"particulates\",\"value\":%u.%02u}",
|
||||||
|
particleData.concentration_int, particleData.concentration_fr_2dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,",{\"variable\":\"illuminance\",\"value\":%u.%02u}]",
|
||||||
|
lightData.illum_lux_int, lightData.illum_lux_fr_2dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
size_t len = strlen(postBuffer);
|
||||||
|
sprintf(fieldBuffer,"Content-Length: %u",len);
|
||||||
|
client.println(fieldBuffer);
|
||||||
|
client.println();
|
||||||
|
client.print(postBuffer);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Serial.println("Client connection failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Assemble the data into the required format, then send it to the
|
||||||
|
// Thingspeak.com cloud as an HTTP POST request.
|
||||||
|
void http_POST_data_Thingspeak_cloud(void) {
|
||||||
|
client.stop();
|
||||||
|
if (client.connect("api.thingspeak.com", 80)) {
|
||||||
|
client.println("POST /update HTTP/1.1");
|
||||||
|
client.println("Host: api.thingspeak.com");
|
||||||
|
client.println("Content-Type: application/x-www-form-urlencoded");
|
||||||
|
|
||||||
|
strcpy(postBuffer,"api_key=" THINGSPEAK_API_KEY_STRING);
|
||||||
|
|
||||||
|
uint8_t T_intPart = 0;
|
||||||
|
uint8_t T_fractionalPart = 0;
|
||||||
|
bool isPositive = true;
|
||||||
|
getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive);
|
||||||
|
sprintf(fieldBuffer,"&field1=%s%u.%u", isPositive?"":"-", T_intPart, T_fractionalPart);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,"&field2=%" PRIu32, airData.P_Pa);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,"&field3=%u.%u", airData.H_pc_int, airData.H_pc_fr_1dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,"&field4=%u.%u", airQualityData.AQI_int, airQualityData.AQI_fr_1dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,"&field5=%u.%02u", airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,"&field6=%u.%u", soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,"&field7=%u.%02u", lightData.illum_lux_int, lightData.illum_lux_fr_2dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,"&field8=%u.%02u", particleData.concentration_int,
|
||||||
|
particleData.concentration_fr_2dp);
|
||||||
|
strcat(postBuffer, fieldBuffer);
|
||||||
|
|
||||||
|
size_t len = strlen(postBuffer);
|
||||||
|
sprintf(fieldBuffer,"Content-Length: %u",len);
|
||||||
|
client.println(fieldBuffer);
|
||||||
|
client.println();
|
||||||
|
client.print(postBuffer);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Serial.println("Client connection failed.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
cycle_readout.ino
|
||||||
|
|
||||||
|
Example code for using the Metriful MS430 in cycle mode.
|
||||||
|
|
||||||
|
Continually measures and displays all environment data in
|
||||||
|
a repeating cycle. User can choose from a cycle time period
|
||||||
|
of 3, 100, or 300 seconds. View the output in the Serial Monitor.
|
||||||
|
|
||||||
|
The measurements can be displayed as either labeled text, or as
|
||||||
|
simple columns of numbers.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Metriful_sensor.h>
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// USER-EDITABLE SETTINGS
|
||||||
|
|
||||||
|
// How often to read data (every 3, 100, or 300 seconds)
|
||||||
|
uint8_t cycle_period = CYCLE_PERIOD_3_S;
|
||||||
|
|
||||||
|
// How to print the data over the serial port. If printDataAsColumns = true,
|
||||||
|
// data are columns of numbers, useful to copy/paste to a spreadsheet
|
||||||
|
// application. Otherwise, data are printed with explanatory labels and units.
|
||||||
|
bool printDataAsColumns = false;
|
||||||
|
|
||||||
|
// END OF USER-EDITABLE SETTINGS
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Structs for data
|
||||||
|
AirData_t airData = {0};
|
||||||
|
AirQualityData_t airQualityData = {0};
|
||||||
|
LightData_t lightData = {0};
|
||||||
|
SoundData_t soundData = {0};
|
||||||
|
ParticleData_t particleData = {0};
|
||||||
|
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Initialize the host pins, set up the serial port and reset:
|
||||||
|
SensorHardwareSetup(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Apply chosen settings to the MS430
|
||||||
|
uint8_t particleSensor = PARTICLE_SENSOR;
|
||||||
|
TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1);
|
||||||
|
TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1);
|
||||||
|
|
||||||
|
// Wait for the serial port to be ready, for displaying the output
|
||||||
|
while (!Serial) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("Entering cycle mode and waiting for data.");
|
||||||
|
ready_assertion_event = false;
|
||||||
|
TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// Wait for the next new data release, indicated by a falling edge on READY
|
||||||
|
while (!ready_assertion_event) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
ready_assertion_event = false;
|
||||||
|
|
||||||
|
// Read data from the MS430 into the data structs.
|
||||||
|
|
||||||
|
// Air data
|
||||||
|
// Choose output temperature unit (C or F) in Metriful_sensor.h
|
||||||
|
airData = getAirData(I2C_ADDRESS);
|
||||||
|
|
||||||
|
/* Air quality data
|
||||||
|
The initial self-calibration of the air quality data may take several
|
||||||
|
minutes to complete. During this time the accuracy parameter is zero
|
||||||
|
and the data values are not valid.
|
||||||
|
*/
|
||||||
|
airQualityData = getAirQualityData(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Light data
|
||||||
|
lightData = getLightData(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Sound data
|
||||||
|
soundData = getSoundData(I2C_ADDRESS);
|
||||||
|
|
||||||
|
/* Particle data
|
||||||
|
This requires the connection of a particulate sensor (invalid
|
||||||
|
values will be obtained if this sensor is not present).
|
||||||
|
Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h
|
||||||
|
Also note that, due to the low pass filtering used, the
|
||||||
|
particle data become valid after an initial initialization
|
||||||
|
period of approximately one minute.
|
||||||
|
*/
|
||||||
|
if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
|
||||||
|
particleData = getParticleData(I2C_ADDRESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print all data to the serial port
|
||||||
|
printAirData(&airData, printDataAsColumns);
|
||||||
|
printAirQualityData(&airQualityData, printDataAsColumns);
|
||||||
|
printLightData(&lightData, printDataAsColumns);
|
||||||
|
printSoundData(&soundData, printDataAsColumns);
|
||||||
|
if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
|
||||||
|
printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR);
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
}
|
|
@ -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 <Metriful_sensor.h>
|
||||||
|
#include <WiFi_functions.h>
|
||||||
|
#include <graph_web_page.h>
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// 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://<IP address here>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
interrupts.ino
|
||||||
|
|
||||||
|
Example code for using the Metriful MS430 interrupt outputs.
|
||||||
|
|
||||||
|
Light and sound interrupts are configured and the program then
|
||||||
|
waits forever. When an interrupt occurs, a message prints over
|
||||||
|
the serial port, the interrupt is cleared (if set to latch type),
|
||||||
|
and the program returns to waiting.
|
||||||
|
View the output in the Serial Monitor.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Metriful_sensor.h>
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// USER-EDITABLE SETTINGS
|
||||||
|
|
||||||
|
// Light level interrupt settings
|
||||||
|
|
||||||
|
bool enableLightInterrupts = true;
|
||||||
|
uint8_t light_int_type = LIGHT_INT_TYPE_LATCH;
|
||||||
|
// Choose the interrupt polarity: trigger when level rises above
|
||||||
|
// threshold (positive), or when level falls below threshold (negative).
|
||||||
|
uint8_t light_int_polarity = LIGHT_INT_POL_POSITIVE;
|
||||||
|
uint16_t light_int_thres_lux_i = 100;
|
||||||
|
uint8_t light_int_thres_lux_f2dp = 50;
|
||||||
|
// The interrupt threshold value in lux units can be fractional and is formed as:
|
||||||
|
// threshold = light_int_thres_lux_i + (light_int_thres_lux_f2dp/100)
|
||||||
|
// E.g. for a light threshold of 56.12 lux, set:
|
||||||
|
// light_int_thres_lux_i = 56
|
||||||
|
// light_int_thres_lux_f2dp = 12
|
||||||
|
|
||||||
|
|
||||||
|
// Sound level interrupt settings
|
||||||
|
|
||||||
|
bool enableSoundInterrupts = true;
|
||||||
|
uint8_t sound_int_type = SOUND_INT_TYPE_LATCH;
|
||||||
|
uint16_t sound_thres_mPa = 100;
|
||||||
|
|
||||||
|
// END OF USER-EDITABLE SETTINGS
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
uint8_t transmit_buffer[1] = {0};
|
||||||
|
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Initialize the host pins, set up the serial port and reset
|
||||||
|
SensorHardwareSetup(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// check that the chosen light threshold is a valid value
|
||||||
|
if (light_int_thres_lux_i > MAX_LUX_VALUE) {
|
||||||
|
Serial.println("The chosen light interrupt threshold exceeds the maximum allowed value.");
|
||||||
|
while (true) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((!enableSoundInterrupts)&&(!enableLightInterrupts)) {
|
||||||
|
Serial.println("No interrupts have been selected.");
|
||||||
|
while (true) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableSoundInterrupts) {
|
||||||
|
// Set the interrupt type (latch or comparator)
|
||||||
|
transmit_buffer[0] = sound_int_type;
|
||||||
|
TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_TYPE_REG, transmit_buffer, 1);
|
||||||
|
|
||||||
|
// Set the threshold
|
||||||
|
setSoundInterruptThreshold(I2C_ADDRESS, sound_thres_mPa);
|
||||||
|
|
||||||
|
// Enable the interrupt
|
||||||
|
transmit_buffer[0] = ENABLED;
|
||||||
|
TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_ENABLE_REG, transmit_buffer, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableLightInterrupts) {
|
||||||
|
// Set the interrupt type (latch or comparator)
|
||||||
|
transmit_buffer[0] = light_int_type;
|
||||||
|
TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_TYPE_REG, transmit_buffer, 1);
|
||||||
|
|
||||||
|
// Set the threshold
|
||||||
|
setLightInterruptThreshold(I2C_ADDRESS, light_int_thres_lux_i, light_int_thres_lux_f2dp);
|
||||||
|
|
||||||
|
// Set the interrupt polarity
|
||||||
|
transmit_buffer[0] = light_int_polarity;
|
||||||
|
TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_POLARITY_REG, transmit_buffer, 1);
|
||||||
|
|
||||||
|
// Enable the interrupt
|
||||||
|
transmit_buffer[0] = ENABLED;
|
||||||
|
TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_ENABLE_REG, transmit_buffer, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the serial port to be ready, for displaying the output
|
||||||
|
while (!Serial) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("Waiting for interrupts.");
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
|
||||||
|
// Check whether a light interrupt has occurred
|
||||||
|
if ((digitalRead(L_INT_PIN) == LOW) && enableLightInterrupts) {
|
||||||
|
Serial.println("LIGHT INTERRUPT.");
|
||||||
|
if (light_int_type == LIGHT_INT_TYPE_LATCH) {
|
||||||
|
// Latch type interrupts remain set until cleared by command
|
||||||
|
TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_CLR_CMD, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether a sound interrupt has occurred
|
||||||
|
if ((digitalRead(S_INT_PIN) == LOW) && enableSoundInterrupts) {
|
||||||
|
Serial.println("SOUND INTERRUPT.");
|
||||||
|
if (sound_int_type == SOUND_INT_TYPE_LATCH) {
|
||||||
|
// Latch type interrupts remain set until cleared by command
|
||||||
|
TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_CLR_CMD, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(500);
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
on_demand_readout.ino
|
||||||
|
|
||||||
|
Example code for using the Metriful MS430 in "on-demand" mode.
|
||||||
|
|
||||||
|
Repeatedly measures and displays all environment data, with a pause
|
||||||
|
between measurements. Air quality data are unavailable in this mode
|
||||||
|
(instead see cycle_readout.ino). View output in the Serial Monitor.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Metriful_sensor.h>
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// USER-EDITABLE SETTINGS
|
||||||
|
|
||||||
|
// Pause (in milliseconds) between data measurements (note that the
|
||||||
|
// measurement itself takes 0.5 seconds)
|
||||||
|
uint32_t pause_ms = 4500;
|
||||||
|
// Choosing a pause of less than 2000 ms will cause inaccurate
|
||||||
|
// temperature, humidity and particle data.
|
||||||
|
|
||||||
|
// How to print the data over the serial port. If printDataAsColumns = true,
|
||||||
|
// data are columns of numbers, useful to copy/paste to a spreadsheet
|
||||||
|
// application. Otherwise, data are printed with explanatory labels and units.
|
||||||
|
bool printDataAsColumns = false;
|
||||||
|
|
||||||
|
// END OF USER-EDITABLE SETTINGS
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Structs for data
|
||||||
|
AirData_t airData = {0};
|
||||||
|
LightData_t lightData = {0};
|
||||||
|
SoundData_t soundData = {0};
|
||||||
|
ParticleData_t particleData = {0};
|
||||||
|
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Initialize the host pins, set up the serial port and reset:
|
||||||
|
SensorHardwareSetup(I2C_ADDRESS);
|
||||||
|
|
||||||
|
uint8_t particleSensor = PARTICLE_SENSOR;
|
||||||
|
TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1);
|
||||||
|
|
||||||
|
// Wait for the serial port to be ready, for displaying the output
|
||||||
|
while (!Serial) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
|
||||||
|
// Trigger a new measurement
|
||||||
|
ready_assertion_event = false;
|
||||||
|
TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0);
|
||||||
|
|
||||||
|
// Wait for the measurement to finish, indicated by a falling edge on READY
|
||||||
|
while (!ready_assertion_event) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read data from the MS430 into the data structs.
|
||||||
|
|
||||||
|
// Air data
|
||||||
|
// Choose output temperature unit (C or F) in Metriful_sensor.h
|
||||||
|
airData = getAirData(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Air quality data are not available with on demand measurements
|
||||||
|
|
||||||
|
// Light data
|
||||||
|
lightData = getLightData(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Sound data
|
||||||
|
soundData = getSoundData(I2C_ADDRESS);
|
||||||
|
|
||||||
|
/* Particle data
|
||||||
|
This requires the connection of a particulate sensor (invalid
|
||||||
|
values will be obtained if this sensor is not present).
|
||||||
|
Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h
|
||||||
|
Also note that, due to the low pass filtering used, the
|
||||||
|
particle data become valid after an initial initialization
|
||||||
|
period of approximately one minute.
|
||||||
|
*/
|
||||||
|
if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
|
||||||
|
particleData = getParticleData(I2C_ADDRESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print all data to the serial port
|
||||||
|
printAirData(&airData, printDataAsColumns);
|
||||||
|
printLightData(&lightData, printDataAsColumns);
|
||||||
|
printSoundData(&soundData, printDataAsColumns);
|
||||||
|
if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
|
||||||
|
printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR);
|
||||||
|
}
|
||||||
|
Serial.println();
|
||||||
|
|
||||||
|
// Wait for the chosen time period before repeating everything
|
||||||
|
delay(pause_ms);
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
/*
|
||||||
|
particle_sensor_toggle.ino
|
||||||
|
|
||||||
|
Optional advanced demo. This program shows how to generate an output
|
||||||
|
control signal from one of the host's pins, which can be used to turn
|
||||||
|
the particle sensor on and off. An external transistor circuit is
|
||||||
|
also needed - this will gate the sensor power supply according to
|
||||||
|
the control signal. Further details are given in the User Guide.
|
||||||
|
|
||||||
|
The program continually measures and displays all environment data
|
||||||
|
in a repeating cycle. The user can view the output in the Serial
|
||||||
|
Monitor. After reading the data, the particle sensor is powered off
|
||||||
|
for a chosen number of cycles ("off_cycles"). It is then powered on
|
||||||
|
and read before being powered off again. Sound data are ignored
|
||||||
|
while the particle sensor is on, to avoid its fan noise.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Metriful_sensor.h>
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// USER-EDITABLE SETTINGS
|
||||||
|
|
||||||
|
// How often to read data; choose only 100 or 300 seconds for this demo
|
||||||
|
// because the sensor should be on for at least one minute before reading
|
||||||
|
// its data.
|
||||||
|
uint8_t cycle_period = CYCLE_PERIOD_100_S;
|
||||||
|
|
||||||
|
// How to print the data over the serial port. If printDataAsColumns = true,
|
||||||
|
// data are columns of numbers, useful for transferring to a spreadsheet
|
||||||
|
// application. Otherwise, data are printed with explanatory labels and units.
|
||||||
|
bool printDataAsColumns = false;
|
||||||
|
|
||||||
|
// Particle sensor power control options
|
||||||
|
uint8_t off_cycles = 2; // leave the sensor off for this many cycles between reads
|
||||||
|
uint8_t particle_sensor_control_pin = 10; // host pin number which outputs the control signal
|
||||||
|
bool particle_sensor_ON_state = true;
|
||||||
|
// particle_sensor_ON_state is the required polarity of the control
|
||||||
|
// signal; true means +V is output to turn the sensor on, while false
|
||||||
|
// means 0 V is output. Use true for 3.3 V hosts and false for 5 V hosts.
|
||||||
|
|
||||||
|
// END OF USER-EDITABLE SETTINGS
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
uint8_t transmit_buffer[1] = {0};
|
||||||
|
|
||||||
|
// Structs for data
|
||||||
|
AirData_t airData = {0};
|
||||||
|
AirQualityData_t airQualityData = {0};
|
||||||
|
LightData_t lightData = {0};
|
||||||
|
SoundData_t soundData = {0};
|
||||||
|
ParticleData_t particleData = {0};
|
||||||
|
|
||||||
|
bool particleSensorIsOn = false;
|
||||||
|
uint8_t particleSensor_count = 0;
|
||||||
|
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Initialize the host pins, set up the serial port and reset:
|
||||||
|
SensorHardwareSetup(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Set up the particle sensor control, and turn it off initially
|
||||||
|
pinMode(particle_sensor_control_pin, OUTPUT);
|
||||||
|
digitalWrite(particle_sensor_control_pin, !particle_sensor_ON_state);
|
||||||
|
particleSensorIsOn = false;
|
||||||
|
|
||||||
|
// Apply chosen settings to the MS430
|
||||||
|
transmit_buffer[0] = PARTICLE_SENSOR;
|
||||||
|
TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
|
||||||
|
transmit_buffer[0] = cycle_period;
|
||||||
|
TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1);
|
||||||
|
|
||||||
|
// Wait for the serial port to be ready, for displaying the output
|
||||||
|
while (!Serial) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("Entering cycle mode and waiting for data.");
|
||||||
|
ready_assertion_event = false;
|
||||||
|
TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// Wait for the next new data release, indicated by a falling edge on READY
|
||||||
|
while (!ready_assertion_event) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
ready_assertion_event = false;
|
||||||
|
|
||||||
|
/* Read data from the MS430 into the data structs.
|
||||||
|
For each category of data (air, sound, etc.) a pointer to the data struct is
|
||||||
|
passed to the ReceiveI2C() function. The received byte sequence fills the data
|
||||||
|
struct in the correct order so that each field within the struct receives
|
||||||
|
the value of an environmental quantity (temperature, sound level, etc.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Air data
|
||||||
|
ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
|
||||||
|
|
||||||
|
/* Air quality data
|
||||||
|
The initial self-calibration of the air quality data may take several
|
||||||
|
minutes to complete. During this time the accuracy parameter is zero
|
||||||
|
and the data values are not valid.
|
||||||
|
*/
|
||||||
|
ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
|
||||||
|
|
||||||
|
// Light data
|
||||||
|
ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
|
||||||
|
|
||||||
|
// Sound data - only read when particle sensor is off
|
||||||
|
if (!particleSensorIsOn) {
|
||||||
|
ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Particle data
|
||||||
|
This requires the connection of a particulate sensor (invalid
|
||||||
|
values will be obtained if this sensor is not present).
|
||||||
|
Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h
|
||||||
|
Also note that, due to the low pass filtering used, the
|
||||||
|
particle data become valid after an initial initialization
|
||||||
|
period of approximately one minute.
|
||||||
|
*/
|
||||||
|
if (particleSensorIsOn) {
|
||||||
|
ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print all data to the serial port. The previous loop's particle or
|
||||||
|
// sound data will be printed if no reading was done on this loop.
|
||||||
|
printAirData(&airData, printDataAsColumns);
|
||||||
|
printAirQualityData(&airQualityData, printDataAsColumns);
|
||||||
|
printLightData(&lightData, printDataAsColumns);
|
||||||
|
printSoundData(&soundData, printDataAsColumns);
|
||||||
|
printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR);
|
||||||
|
Serial.println();
|
||||||
|
|
||||||
|
// Turn the particle sensor on/off if required
|
||||||
|
if (particleSensorIsOn) {
|
||||||
|
// Stop the particle detection on the MS430
|
||||||
|
transmit_buffer[0] = OFF;
|
||||||
|
TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
|
||||||
|
|
||||||
|
// Turn off the hardware:
|
||||||
|
digitalWrite(particle_sensor_control_pin, !particle_sensor_ON_state);
|
||||||
|
particleSensorIsOn = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
particleSensor_count++;
|
||||||
|
if (particleSensor_count >= off_cycles) {
|
||||||
|
// Turn on the hardware:
|
||||||
|
digitalWrite(particle_sensor_control_pin, particle_sensor_ON_state);
|
||||||
|
|
||||||
|
// Start the particle detection on the MS430
|
||||||
|
transmit_buffer[0] = PARTICLE_SENSOR;
|
||||||
|
TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1);
|
||||||
|
|
||||||
|
particleSensor_count = 0;
|
||||||
|
particleSensorIsOn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
simple_read_T_H.ino
|
||||||
|
|
||||||
|
Example code for using the Metriful MS430 to measure humidity
|
||||||
|
and temperature.
|
||||||
|
|
||||||
|
Demonstrates multiple ways of reading and displaying the temperature
|
||||||
|
and humidity data. View the output in the Serial Monitor. The other
|
||||||
|
data can be measured and displayed in a similar way.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Metriful_sensor.h>
|
||||||
|
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Initialize the host pins, set up the serial port and reset:
|
||||||
|
SensorHardwareSetup(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Wait for the serial port to be ready, for displaying the output
|
||||||
|
while (!Serial) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the global variable in preparation for waiting for READY assertion
|
||||||
|
ready_assertion_event = false;
|
||||||
|
|
||||||
|
// Initiate an on-demand data measurement
|
||||||
|
TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0);
|
||||||
|
|
||||||
|
// Now wait for the ready signal before continuing
|
||||||
|
while (!ready_assertion_event) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We know that new data are ready to read.
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// There are different ways to read and display the data
|
||||||
|
|
||||||
|
// 1. Simplest way: use the example float (_F) functions
|
||||||
|
|
||||||
|
// Read the "air data" from the MS430. This includes temperature and
|
||||||
|
// humidity as well as pressure and gas sensor data.
|
||||||
|
AirData_F_t airDataF = getAirDataF(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Print all of the air measurements to the serial monitor
|
||||||
|
printAirDataF(&airDataF);
|
||||||
|
// Fahrenheit temperature is printed if USE_FAHRENHEIT is defined
|
||||||
|
// in "Metriful_sensor.h"
|
||||||
|
|
||||||
|
Serial.println("-----------------------------");
|
||||||
|
|
||||||
|
|
||||||
|
// 2. After reading from the MS430, you can also access and print the
|
||||||
|
// float data directly from the struct:
|
||||||
|
Serial.print("The temperature is: ");
|
||||||
|
Serial.print(airDataF.T_C, 1); // print to 1 decimal place
|
||||||
|
Serial.println(" " CELSIUS_SYMBOL);
|
||||||
|
|
||||||
|
// Optional: convert to Fahrenheit
|
||||||
|
float temperature_F = convertCtoF(airDataF.T_C);
|
||||||
|
|
||||||
|
Serial.print("The temperature is: ");
|
||||||
|
Serial.print(temperature_F, 1); // print to 1 decimal place
|
||||||
|
Serial.println(" " FAHRENHEIT_SYMBOL);
|
||||||
|
|
||||||
|
Serial.println("-----------------------------");
|
||||||
|
|
||||||
|
|
||||||
|
// 3. If host resources are limited, avoid using floating point and
|
||||||
|
// instead use the integer versions (without "F" in the name)
|
||||||
|
AirData_t airData = getAirData(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Print to the serial monitor
|
||||||
|
printAirData(&airData, false);
|
||||||
|
// If the second argument is "true", data are printed as columns.
|
||||||
|
// Fahrenheit temperature is printed if USE_FAHRENHEIT is defined
|
||||||
|
// in "Metriful_sensor.h"
|
||||||
|
|
||||||
|
Serial.println("-----------------------------");
|
||||||
|
|
||||||
|
|
||||||
|
// 4. Access and print integer data directly from the struct:
|
||||||
|
Serial.print("The humidity is: ");
|
||||||
|
Serial.print(airData.H_pc_int); // the integer part of the value
|
||||||
|
Serial.print("."); // the decimal point
|
||||||
|
Serial.print(airData.H_pc_fr_1dp); // the fractional part (1 decimal place)
|
||||||
|
Serial.println(" %");
|
||||||
|
|
||||||
|
Serial.println("-----------------------------");
|
||||||
|
|
||||||
|
|
||||||
|
// 5. Advanced: read and decode only the humidity value from the MS430
|
||||||
|
|
||||||
|
// Read the raw humidity data
|
||||||
|
uint8_t receive_buffer[2] = {0};
|
||||||
|
ReceiveI2C(I2C_ADDRESS, H_READ, receive_buffer, H_BYTES);
|
||||||
|
|
||||||
|
// Decode the humidity: the first received byte is the integer part, the
|
||||||
|
// second received byte is the fractional part to one decimal place.
|
||||||
|
uint8_t humidity_integer = receive_buffer[0];
|
||||||
|
uint8_t humidity_fraction = receive_buffer[1];
|
||||||
|
// Print it: the units are percentage relative humidity.
|
||||||
|
Serial.print("Humidity = ");
|
||||||
|
Serial.print(humidity_integer);
|
||||||
|
Serial.print(".");
|
||||||
|
Serial.print(humidity_fraction);
|
||||||
|
Serial.println(" %");
|
||||||
|
|
||||||
|
Serial.println("-----------------------------");
|
||||||
|
|
||||||
|
|
||||||
|
// 6. Advanced: read and decode only the temperature value from the MS430
|
||||||
|
|
||||||
|
// Read the raw temperature data
|
||||||
|
ReceiveI2C(I2C_ADDRESS, T_READ, receive_buffer, T_BYTES);
|
||||||
|
|
||||||
|
// The temperature is encoded differently to allow negative values
|
||||||
|
|
||||||
|
// Find the positive magnitude of the integer part of the temperature
|
||||||
|
// by doing a bitwise AND of the first received byte with TEMPERATURE_VALUE_MASK
|
||||||
|
uint8_t temperature_positive_integer = receive_buffer[0] & TEMPERATURE_VALUE_MASK;
|
||||||
|
|
||||||
|
// The second received byte is the fractional part to one decimal place
|
||||||
|
uint8_t temperature_fraction = receive_buffer[1];
|
||||||
|
|
||||||
|
Serial.print("Temperature = ");
|
||||||
|
// If the most-significant bit of the first byte is a 1, the temperature
|
||||||
|
// is negative (below 0 C), otherwise it is positive
|
||||||
|
if ((receive_buffer[0] & TEMPERATURE_SIGN_MASK) != 0) {
|
||||||
|
// The bit is a 1: celsius temperature is negative
|
||||||
|
Serial.print("-");
|
||||||
|
}
|
||||||
|
Serial.print(temperature_positive_integer);
|
||||||
|
Serial.print(".");
|
||||||
|
Serial.print(temperature_fraction);
|
||||||
|
Serial.println(" " CELSIUS_SYMBOL);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// There is no loop for this program.
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
simple_read_sound.ino
|
||||||
|
|
||||||
|
Example code for using the Metriful MS430 to measure sound.
|
||||||
|
|
||||||
|
Demonstrates multiple ways of reading and displaying the sound data.
|
||||||
|
View the output in the Serial Monitor. The other data can be measured
|
||||||
|
and displayed in a similar way.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Metriful_sensor.h>
|
||||||
|
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Initialize the host pins, set up the serial port and reset:
|
||||||
|
SensorHardwareSetup(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Wait for the serial port to be ready, for displaying the output
|
||||||
|
while (!Serial) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Wait for the microphone signal to stabilize (takes approximately 1.5 seconds).
|
||||||
|
// This only needs to be done once after the MS430 is powered-on or reset.
|
||||||
|
delay(1500);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Clear the global variable in preparation for waiting for READY assertion
|
||||||
|
ready_assertion_event = false;
|
||||||
|
|
||||||
|
// Initiate an on-demand data measurement
|
||||||
|
TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0);
|
||||||
|
|
||||||
|
// Now wait for the ready signal (falling edge) before continuing
|
||||||
|
while (!ready_assertion_event) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We now know that newly measured data are ready to read.
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// There are multiple ways to read and display the data
|
||||||
|
|
||||||
|
|
||||||
|
// 1. Simplest way: use the example float (_F) functions
|
||||||
|
|
||||||
|
// Read the sound data from the board
|
||||||
|
SoundData_F_t soundDataF = getSoundDataF(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Print all of the sound measurements to the serial monitor
|
||||||
|
printSoundDataF(&soundDataF);
|
||||||
|
|
||||||
|
Serial.println("-----------------------------");
|
||||||
|
|
||||||
|
|
||||||
|
// 2. After reading from the MS430, you can also access and print the
|
||||||
|
// float data directly from the struct:
|
||||||
|
Serial.print("The sound pressure level is: ");
|
||||||
|
Serial.print(soundDataF.SPL_dBA, 1); // print to 1 decimal place
|
||||||
|
Serial.println(" dBA");
|
||||||
|
|
||||||
|
Serial.println("-----------------------------");
|
||||||
|
|
||||||
|
|
||||||
|
// 3. If host resources are limited, avoid using floating point and
|
||||||
|
// instead use the integer versions (without "F" in the name)
|
||||||
|
SoundData_t soundData = getSoundData(I2C_ADDRESS);
|
||||||
|
|
||||||
|
// Print to the serial monitor
|
||||||
|
printSoundData(&soundData, false);
|
||||||
|
// If the second argument is "true", data are printed as columns.
|
||||||
|
|
||||||
|
Serial.println("-----------------------------");
|
||||||
|
|
||||||
|
|
||||||
|
// 4. Access and print integer data directly from the struct:
|
||||||
|
Serial.print("The sound pressure level is: ");
|
||||||
|
Serial.print(soundData.SPL_dBA_int); // the integer part of the value
|
||||||
|
Serial.print("."); // the decimal point
|
||||||
|
Serial.print(soundData.SPL_dBA_fr_1dp); // the fractional part (1 decimal place)
|
||||||
|
Serial.println(" dBA");
|
||||||
|
|
||||||
|
Serial.println("-----------------------------");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// There is no loop for this program.
|
||||||
|
}
|
|
@ -0,0 +1,380 @@
|
||||||
|
/*
|
||||||
|
web_server.ino
|
||||||
|
|
||||||
|
Example code for serving a web page over a WiFi network, displaying
|
||||||
|
environment data read from the Metriful MS430.
|
||||||
|
|
||||||
|
This example is designed for the following WiFi enabled hosts:
|
||||||
|
* Arduino Nano 33 IoT
|
||||||
|
* Arduino MKR WiFi 1010
|
||||||
|
* ESP8266 boards (e.g. Wemos D1, NodeMCU)
|
||||||
|
* ESP32 boards (e.g. DOIT DevKit v1)
|
||||||
|
|
||||||
|
All environment data values are measured and displayed on a text
|
||||||
|
web page generated by the host, which acts as a simple web server.
|
||||||
|
|
||||||
|
The host can either connect to an existing WiFi network, or generate
|
||||||
|
its own for other devices to connect to (Access Point mode).
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Metriful_sensor.h>
|
||||||
|
#include <WiFi_functions.h>
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// USER-EDITABLE SETTINGS
|
||||||
|
|
||||||
|
// Choose how often to read and update data (every 3, 100, or 300 seconds)
|
||||||
|
// The web page can be refreshed more often but the data will not change
|
||||||
|
uint8_t cycle_period = CYCLE_PERIOD_3_S;
|
||||||
|
|
||||||
|
// Choose whether to create a new WiFi network (host as Access Point),
|
||||||
|
// or connect to an existing WiFi network.
|
||||||
|
bool createWifiNetwork = false;
|
||||||
|
// If creating a WiFi network, a static (fixed) IP address ("theIP") is
|
||||||
|
// specified by the user. Otherwise, if connecting to an existing
|
||||||
|
// network, an IP address is automatically allocated and the serial
|
||||||
|
// output must be viewed at startup to see this allocated IP address.
|
||||||
|
|
||||||
|
// Provide the SSID (name) and password for the WiFi network. Depending
|
||||||
|
// on the choice of createWifiNetwork, this is either created by the
|
||||||
|
// host (Access Point mode) or already exists.
|
||||||
|
// To avoid problems, do not create a network with the same SSID name
|
||||||
|
// as an already existing network.
|
||||||
|
char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name)
|
||||||
|
char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password; must be at least 8 characters
|
||||||
|
|
||||||
|
// Choose a static IP address for the host, only used when generating
|
||||||
|
// a new WiFi network (createWifiNetwork = true). The served web
|
||||||
|
// page will be available at http://<IP address here>
|
||||||
|
IPAddress theIP(192, 168, 12, 20);
|
||||||
|
// e.g. theIP(192, 168, 12, 20) means an IP of 192.168.12.20
|
||||||
|
// and the web page will be at http://192.168.12.20
|
||||||
|
|
||||||
|
// END OF USER-EDITABLE SETTINGS
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#if !defined(HAS_WIFI)
|
||||||
|
#error ("This example program has been created for specific WiFi enabled hosts only.")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
WiFiServer server(80);
|
||||||
|
uint16_t refreshPeriodSeconds;
|
||||||
|
|
||||||
|
// Structs for data
|
||||||
|
AirData_t airData = {0};
|
||||||
|
AirQualityData_t airQualityData = {0};
|
||||||
|
LightData_t lightData = {0};
|
||||||
|
ParticleData_t particleData = {0};
|
||||||
|
SoundData_t soundData = {0};
|
||||||
|
|
||||||
|
// Storage for the web page text
|
||||||
|
char lineBuffer[100] = {0};
|
||||||
|
char pageBuffer[2300] = {0};
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Initialize the host's pins, set up the serial port and reset:
|
||||||
|
SensorHardwareSetup(I2C_ADDRESS);
|
||||||
|
|
||||||
|
if (createWifiNetwork) {
|
||||||
|
// The host generates its own WiFi network ("Access Point") with
|
||||||
|
// a chosen static IP address
|
||||||
|
if (!createWiFiAP(SSID, password, theIP)) {
|
||||||
|
Serial.println("Failed to create access point.");
|
||||||
|
while (true) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// The host connects to an existing Wifi network
|
||||||
|
|
||||||
|
// Wait for the serial port to start because the user must be able
|
||||||
|
// to see the printed IP address in the serial monitor
|
||||||
|
while (!Serial) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to connect to the Wifi network and obtain the IP
|
||||||
|
// address. Because the address is not known before this point,
|
||||||
|
// a serial monitor must be used to display it to the user.
|
||||||
|
connectToWiFi(SSID, password);
|
||||||
|
theIP = WiFi.localIP();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the IP address: use this address in a browser to view the
|
||||||
|
// generated web page
|
||||||
|
Serial.print("View your page at http://");
|
||||||
|
Serial.println(theIP);
|
||||||
|
|
||||||
|
// Start the web server
|
||||||
|
server.begin();
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Select how often to auto-refresh the web page. This should be done at
|
||||||
|
// least as often as new data are obtained. A more frequent refresh is
|
||||||
|
// best for long cycle periods because the page refresh is not
|
||||||
|
// synchronized with the cycle. Users can also manually refresh the page.
|
||||||
|
if (cycle_period == CYCLE_PERIOD_3_S) {
|
||||||
|
refreshPeriodSeconds = 3;
|
||||||
|
}
|
||||||
|
else if (cycle_period == CYCLE_PERIOD_100_S) {
|
||||||
|
refreshPeriodSeconds = 30;
|
||||||
|
}
|
||||||
|
else { // CYCLE_PERIOD_300_S
|
||||||
|
refreshPeriodSeconds = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the chosen settings to the Metriful board
|
||||||
|
uint8_t particleSensor = PARTICLE_SENSOR;
|
||||||
|
TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1);
|
||||||
|
TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1);
|
||||||
|
ready_assertion_event = false;
|
||||||
|
TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
|
||||||
|
// While waiting for the next data release, respond to client requests
|
||||||
|
// by serving the web page with the last available data. Initially the
|
||||||
|
// data will be all zero (until the first data readout has completed).
|
||||||
|
while (!ready_assertion_event) {
|
||||||
|
handleClientRequests();
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
ready_assertion_event = false;
|
||||||
|
|
||||||
|
// new data are now ready
|
||||||
|
|
||||||
|
/* Read data from the MS430 into the data structs.
|
||||||
|
For each category of data (air, sound, etc.) a pointer to the data struct is
|
||||||
|
passed to the ReceiveI2C() function. The received byte sequence fills the data
|
||||||
|
struct in the correct order so that each field within the struct receives
|
||||||
|
the value of an environmental quantity (temperature, sound level, etc.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Air data
|
||||||
|
// Choose output temperature unit (C or F) in Metriful_sensor.h
|
||||||
|
ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
|
||||||
|
|
||||||
|
/* Air quality data
|
||||||
|
The initial self-calibration of the air quality data may take several
|
||||||
|
minutes to complete. During this time the accuracy parameter is zero
|
||||||
|
and the data values are not valid.
|
||||||
|
*/
|
||||||
|
ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
|
||||||
|
|
||||||
|
// Light data
|
||||||
|
ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
|
||||||
|
|
||||||
|
// Sound data
|
||||||
|
ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
|
||||||
|
|
||||||
|
/* Particle data
|
||||||
|
This requires the connection of a particulate sensor (invalid
|
||||||
|
values will be obtained if this sensor is not present).
|
||||||
|
Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h
|
||||||
|
Also note that, due to the low pass filtering used, the
|
||||||
|
particle data become valid after an initial initialization
|
||||||
|
period of approximately one minute.
|
||||||
|
*/
|
||||||
|
if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
|
||||||
|
ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the web page ready for client requests
|
||||||
|
assembleWebPage();
|
||||||
|
|
||||||
|
// Check WiFi is still connected
|
||||||
|
if (!createWifiNetwork) {
|
||||||
|
uint8_t wifiStatus = WiFi.status();
|
||||||
|
if (wifiStatus != WL_CONNECTED) {
|
||||||
|
// There is a problem with the WiFi connection: attempt to reconnect.
|
||||||
|
Serial.print("Wifi status: ");
|
||||||
|
Serial.println(interpret_WiFi_status(wifiStatus));
|
||||||
|
connectToWiFi(SSID, password);
|
||||||
|
theIP = WiFi.localIP();
|
||||||
|
Serial.print("View your page at http://");
|
||||||
|
Serial.println(theIP);
|
||||||
|
ready_assertion_event = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void handleClientRequests(void) {
|
||||||
|
// Check for incoming client requests
|
||||||
|
WiFiClient client = server.available();
|
||||||
|
if (client) {
|
||||||
|
bool blankLine = false;
|
||||||
|
while (client.connected()) {
|
||||||
|
if (client.available()) {
|
||||||
|
char c = client.read();
|
||||||
|
if (c == '\n') {
|
||||||
|
// Two consecutive newline characters indicates the end of the client HTTP request
|
||||||
|
if (blankLine) {
|
||||||
|
// Send the page as a response
|
||||||
|
client.print(pageBuffer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
blankLine = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (c != '\r') {
|
||||||
|
// Carriage return (\r) is disregarded for blank line detection
|
||||||
|
blankLine = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(10);
|
||||||
|
// Close the connection:
|
||||||
|
client.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a simple text web page showing the environment data in
|
||||||
|
// separate category tables, using HTML and CSS
|
||||||
|
void assembleWebPage(void) {
|
||||||
|
sprintf(pageBuffer,"HTTP/1.1 200 OK\r\n"
|
||||||
|
"Content-type: text/html\r\n"
|
||||||
|
"Connection: close\r\n"
|
||||||
|
"Refresh: %u\r\n\r\n",refreshPeriodSeconds);
|
||||||
|
|
||||||
|
strcat(pageBuffer,"<!DOCTYPE HTML><html><head>"
|
||||||
|
"<meta charset='UTF-8'>"
|
||||||
|
"<title>Metriful Sensor Demo</title>"
|
||||||
|
"<style>"
|
||||||
|
"h1{font-size: 3vw;}"
|
||||||
|
"h2{font-size: 2vw; margin-top: 4vw;}"
|
||||||
|
"a{padding: 1vw; font-size: 2vw;}"
|
||||||
|
"table,th,td{font-size: 2vw;}"
|
||||||
|
"body{padding: 0vw 2vw;}"
|
||||||
|
"th,td{padding: 0.05vw 1vw; text-align: left;}"
|
||||||
|
"#v1{text-align: right; width: 10vw;}"
|
||||||
|
"#v2{text-align: right; width: 13vw;}"
|
||||||
|
"#v3{text-align: right; width: 10vw;}"
|
||||||
|
"#v4{text-align: right; width: 10vw;}"
|
||||||
|
"#v5{text-align: right; width: 11vw;}"
|
||||||
|
"</style></head>"
|
||||||
|
"<body><h1>Indoor Environment Data</h1>");
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
|
||||||
|
strcat(pageBuffer,"<p><h2>Air Data</h2><table>");
|
||||||
|
|
||||||
|
uint8_t T_intPart = 0;
|
||||||
|
uint8_t T_fractionalPart = 0;
|
||||||
|
bool isPositive = true;
|
||||||
|
const char * unit = getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive);
|
||||||
|
sprintf(lineBuffer,"<tr><td>Temperature</td><td id='v1'>%s%u.%u</td><td>%s</td></tr>",
|
||||||
|
isPositive?"":"-", T_intPart, T_fractionalPart, unit);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
sprintf(lineBuffer,"<tr><td>Pressure</td><td id='v1'>%" PRIu32 "</td><td>Pa</td></tr>", airData.P_Pa);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
sprintf(lineBuffer,"<tr><td>Humidity</td><td id='v1'>%u.%u</td><td>%%</td></tr>",
|
||||||
|
airData.H_pc_int, airData.H_pc_fr_1dp);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
sprintf(lineBuffer,"<tr><td>Gas Sensor Resistance</td>"
|
||||||
|
"<td id='v1'>%" PRIu32 "</td><td>" OHM_SYMBOL "</td></tr></table></p>",
|
||||||
|
airData.G_ohm);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
|
||||||
|
strcat(pageBuffer,"<p><h2>Air Quality Data</h2>");
|
||||||
|
|
||||||
|
if (airQualityData.AQI_accuracy == 0) {
|
||||||
|
sprintf(lineBuffer,"<a>%s</a></p>",interpret_AQI_accuracy(airQualityData.AQI_accuracy));
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sprintf(lineBuffer,"<table><tr><td>Air Quality Index</td><td id='v2'>%u.%u</td><td></td></tr>",
|
||||||
|
airQualityData.AQI_int, airQualityData.AQI_fr_1dp);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
sprintf(lineBuffer,"<tr><td>Air Quality Summary</td><td id='v2'>%s</td><td></td></tr>",
|
||||||
|
interpret_AQI_value(airQualityData.AQI_int));
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
sprintf(lineBuffer,"<tr><td>Estimated CO" SUBSCRIPT_2 "</td><td id='v2'>%u.%u</td><td>ppm</td></tr>",
|
||||||
|
airQualityData.CO2e_int, airQualityData.CO2e_fr_1dp);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
sprintf(lineBuffer,"<tr><td>Equivalent Breath VOC</td>"
|
||||||
|
"<td id='v2'>%u.%02u</td><td>ppm</td></tr></table></p>",
|
||||||
|
airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
|
||||||
|
strcat(pageBuffer,"<p><h2>Sound Data</h2><table>");
|
||||||
|
|
||||||
|
sprintf(lineBuffer,"<tr><td>A-weighted Sound Pressure Level</td>"
|
||||||
|
"<td id='v3'>%u.%u</td><td>dBA</td></tr>",
|
||||||
|
soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
for (uint8_t i=0; i<SOUND_FREQ_BANDS; i++) {
|
||||||
|
sprintf(lineBuffer,"<tr><td>Frequency Band %u (%u Hz) SPL</td>"
|
||||||
|
"<td id='v3'>%u.%u</td><td>dB</td></tr>",
|
||||||
|
i+1, sound_band_mids_Hz[i], soundData.SPL_bands_dB_int[i], soundData.SPL_bands_dB_fr_1dp[i]);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
sprintf(lineBuffer,"<tr><td>Peak Sound Amplitude</td>"
|
||||||
|
"<td id='v3'>%u.%02u</td><td>mPa</td></tr></table></p>",
|
||||||
|
soundData.peak_amp_mPa_int, soundData.peak_amp_mPa_fr_2dp);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
|
||||||
|
strcat(pageBuffer,"<p><h2>Light Data</h2><table>");
|
||||||
|
|
||||||
|
sprintf(lineBuffer,"<tr><td>Illuminance</td><td id='v4'>%u.%02u</td><td>lux</td></tr>",
|
||||||
|
lightData.illum_lux_int, lightData.illum_lux_fr_2dp);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
sprintf(lineBuffer,"<tr><td>White Light Level</td><td id='v4'>%u</td><td></td></tr>"
|
||||||
|
"</table></p>", lightData.white);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
|
||||||
|
if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
|
||||||
|
strcat(pageBuffer,"<p><h2>Air Particulate Data</h2><table>");
|
||||||
|
|
||||||
|
sprintf(lineBuffer,"<tr><td>Sensor Duty Cycle</td><td id='v5'>%u.%02u</td><td>%%</td></tr>",
|
||||||
|
particleData.duty_cycle_pc_int, particleData.duty_cycle_pc_fr_2dp);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
|
||||||
|
char unitsBuffer[7] = {0};
|
||||||
|
if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42) {
|
||||||
|
strcpy(unitsBuffer,"ppL");
|
||||||
|
}
|
||||||
|
else if (PARTICLE_SENSOR == PARTICLE_SENSOR_SDS011) {
|
||||||
|
strcpy(unitsBuffer,SDS011_UNIT_SYMBOL);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
strcpy(unitsBuffer,"(?)");
|
||||||
|
}
|
||||||
|
sprintf(lineBuffer,"<tr><td>Particle Concentration</td>"
|
||||||
|
"<td id='v5'>%u.%02u</td><td>%s</td></tr></table></p>",
|
||||||
|
particleData.concentration_int, particleData.concentration_fr_2dp, unitsBuffer);
|
||||||
|
strcat(pageBuffer,lineBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////
|
||||||
|
|
||||||
|
strcat(pageBuffer,"</body></html>");
|
||||||
|
}
|
|
@ -0,0 +1,597 @@
|
||||||
|
/*
|
||||||
|
Metriful_sensor.cpp
|
||||||
|
|
||||||
|
This file defines functions which are used in the code examples.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Metriful_sensor.h"
|
||||||
|
#include "host_pin_definitions.h"
|
||||||
|
|
||||||
|
// The Arduino Wire library has a limited internal buffer size:
|
||||||
|
#define ARDUINO_WIRE_BUFFER_LIMIT_BYTES 32
|
||||||
|
|
||||||
|
void SensorHardwareSetup(uint8_t i2c_7bit_address) {
|
||||||
|
|
||||||
|
pinMode(LED_BUILTIN, OUTPUT);
|
||||||
|
|
||||||
|
#ifdef ESP8266
|
||||||
|
// Must specify the I2C pins
|
||||||
|
Wire.begin(SDA_PIN, SCL_PIN);
|
||||||
|
digitalWrite(LED_BUILTIN, HIGH);
|
||||||
|
#else
|
||||||
|
// Default I2C pins are used
|
||||||
|
Wire.begin();
|
||||||
|
digitalWrite(LED_BUILTIN, LOW);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Wire.setClock(I2C_CLK_FREQ_HZ);
|
||||||
|
|
||||||
|
// READY, light interrupt and sound interrupt lines are digital inputs.
|
||||||
|
pinMode(READY_PIN, INPUT);
|
||||||
|
pinMode(L_INT_PIN, INPUT);
|
||||||
|
pinMode(S_INT_PIN, INPUT);
|
||||||
|
|
||||||
|
// Set up interrupt monitoring of the READY signal, triggering on a falling edge
|
||||||
|
// event (high-to-low voltage change) indicating READY assertion. The
|
||||||
|
// function ready_ISR() will be called when this happens.
|
||||||
|
attachInterrupt(digitalPinToInterrupt(READY_PIN), ready_ISR, FALLING);
|
||||||
|
|
||||||
|
// Start the serial port.
|
||||||
|
// Full settings are: 8 data bits, no parity, one stop bit
|
||||||
|
Serial.begin(SERIAL_BAUD_RATE);
|
||||||
|
|
||||||
|
// Wait for the MS430 to finish power-on initialization:
|
||||||
|
while (digitalRead(READY_PIN) == HIGH) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset to clear any previous state:
|
||||||
|
TransmitI2C(i2c_7bit_address, RESET_CMD, 0, 0);
|
||||||
|
delay(5);
|
||||||
|
|
||||||
|
// Wait for reset completion and entry to standby mode
|
||||||
|
while (digitalRead(READY_PIN) == HIGH) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volatile bool ready_assertion_event = false;
|
||||||
|
|
||||||
|
// This function is automatically called after a falling edge (assertion) of READY.
|
||||||
|
// The flag variable is set true - it must be set false again in the main program.
|
||||||
|
void ISR_ATTRIBUTE ready_ISR(void) {
|
||||||
|
ready_assertion_event = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Functions to convert data from integer representation to floating-point representation.
|
||||||
|
// Floats are easy to use for writing programs but require greater memory and processing
|
||||||
|
// power resources, so may not always be appropriate.
|
||||||
|
|
||||||
|
void convertAirDataF(const AirData_t * airData_in, AirData_F_t * airDataF_out) {
|
||||||
|
// Decode the signed value for T (in Celsius)
|
||||||
|
airDataF_out->T_C = convertEncodedTemperatureToFloat(airData_in->T_C_int_with_sign,
|
||||||
|
airData_in->T_C_fr_1dp);
|
||||||
|
airDataF_out->P_Pa = airData_in->P_Pa;
|
||||||
|
airDataF_out->H_pc = ((float) airData_in->H_pc_int) + (((float) airData_in->H_pc_fr_1dp)/10.0);
|
||||||
|
airDataF_out->G_Ohm = airData_in->G_ohm;
|
||||||
|
}
|
||||||
|
|
||||||
|
void convertAirQualityDataF(const AirQualityData_t * airQualityData_in,
|
||||||
|
AirQualityData_F_t * airQualityDataF_out) {
|
||||||
|
airQualityDataF_out->AQI = ((float) airQualityData_in->AQI_int) +
|
||||||
|
(((float) airQualityData_in->AQI_fr_1dp)/10.0);
|
||||||
|
airQualityDataF_out->CO2e = ((float) airQualityData_in->CO2e_int) +
|
||||||
|
(((float) airQualityData_in->CO2e_fr_1dp)/10.0);
|
||||||
|
airQualityDataF_out->bVOC = ((float) airQualityData_in->bVOC_int) +
|
||||||
|
(((float) airQualityData_in->bVOC_fr_2dp)/100.0);
|
||||||
|
airQualityDataF_out->AQI_accuracy = airQualityData_in->AQI_accuracy;
|
||||||
|
}
|
||||||
|
|
||||||
|
void convertLightDataF(const LightData_t * lightData_in, LightData_F_t * lightDataF_out) {
|
||||||
|
lightDataF_out->illum_lux = ((float) lightData_in->illum_lux_int) +
|
||||||
|
(((float) lightData_in->illum_lux_fr_2dp)/100.0);
|
||||||
|
lightDataF_out->white = lightData_in->white;
|
||||||
|
}
|
||||||
|
|
||||||
|
void convertSoundDataF(const SoundData_t * soundData_in, SoundData_F_t * soundDataF_out) {
|
||||||
|
soundDataF_out->SPL_dBA = ((float) soundData_in->SPL_dBA_int) +
|
||||||
|
(((float) soundData_in->SPL_dBA_fr_1dp)/10.0);
|
||||||
|
for (uint16_t i=0; i<SOUND_FREQ_BANDS; i++) {
|
||||||
|
soundDataF_out->SPL_bands_dB[i] = ((float) soundData_in->SPL_bands_dB_int[i]) +
|
||||||
|
(((float) soundData_in->SPL_bands_dB_fr_1dp[i])/10.0);
|
||||||
|
}
|
||||||
|
soundDataF_out->peakAmp_mPa = ((float) soundData_in->peak_amp_mPa_int) +
|
||||||
|
(((float) soundData_in->peak_amp_mPa_fr_2dp)/100.0);
|
||||||
|
soundDataF_out->stable = (soundData_in->stable == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void convertParticleDataF(const ParticleData_t * particleData_in, ParticleData_F_t * particleDataF_out) {
|
||||||
|
particleDataF_out->duty_cycle_pc = ((float) particleData_in->duty_cycle_pc_int) +
|
||||||
|
(((float) particleData_in->duty_cycle_pc_fr_2dp)/100.0);
|
||||||
|
particleDataF_out->concentration = ((float) particleData_in->concentration_int) +
|
||||||
|
(((float) particleData_in->concentration_fr_2dp)/100.0);
|
||||||
|
particleDataF_out->valid = (particleData_in->valid == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// The following five functions print data (in floating-point
|
||||||
|
// representation) over the serial port as text
|
||||||
|
|
||||||
|
void printAirDataF(const AirData_F_t * airDataF) {
|
||||||
|
Serial.print("Temperature = ");
|
||||||
|
#ifdef USE_FAHRENHEIT
|
||||||
|
float temperature_F = convertCtoF(airDataF->T_C);
|
||||||
|
Serial.print(temperature_F,1);Serial.println(" " FAHRENHEIT_SYMBOL);
|
||||||
|
#else
|
||||||
|
Serial.print(airDataF->T_C,1);Serial.println(" " CELSIUS_SYMBOL);
|
||||||
|
#endif
|
||||||
|
Serial.print("Pressure = ");Serial.print(airDataF->P_Pa);Serial.println(" Pa");
|
||||||
|
Serial.print("Humidity = ");Serial.print(airDataF->H_pc,1);Serial.println(" %");
|
||||||
|
Serial.print("Gas Sensor Resistance = ");Serial.print(airDataF->G_Ohm);Serial.println(" " OHM_SYMBOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void printAirQualityDataF(const AirQualityData_F_t * airQualityDataF) {
|
||||||
|
if (airQualityDataF->AQI_accuracy > 0) {
|
||||||
|
Serial.print("Air Quality Index = ");Serial.print(airQualityDataF->AQI,1);
|
||||||
|
Serial.print(" (");
|
||||||
|
Serial.print(interpret_AQI_value((uint16_t) airQualityDataF->AQI));
|
||||||
|
Serial.println(")");
|
||||||
|
Serial.print("Estimated CO" SUBSCRIPT_2 " = ");Serial.print(airQualityDataF->CO2e,1);
|
||||||
|
Serial.println(" ppm");
|
||||||
|
Serial.print("Equivalent Breath VOC = ");Serial.print(airQualityDataF->bVOC,2);
|
||||||
|
Serial.println(" ppm");
|
||||||
|
}
|
||||||
|
Serial.print("Air Quality Accuracy: ");
|
||||||
|
Serial.println(interpret_AQI_accuracy(airQualityDataF->AQI_accuracy));
|
||||||
|
}
|
||||||
|
|
||||||
|
void printLightDataF(const LightData_F_t * lightDataF) {
|
||||||
|
Serial.print("Illuminance = ");Serial.print(lightDataF->illum_lux,2);Serial.println(" lux");
|
||||||
|
Serial.print("White Light Level = ");Serial.print(lightDataF->white);Serial.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
void printSoundDataF(const SoundData_F_t * soundDataF) {
|
||||||
|
char strbuf[50] = {0};
|
||||||
|
Serial.print("A-weighted Sound Pressure Level = ");
|
||||||
|
Serial.print(soundDataF->SPL_dBA,1);Serial.println(" dBA");
|
||||||
|
for (uint16_t i=0; i<SOUND_FREQ_BANDS; i++) {
|
||||||
|
sprintf(strbuf,"Frequency Band %u (%u Hz) SPL = ", i+1, sound_band_mids_Hz[i]);
|
||||||
|
Serial.print(strbuf);
|
||||||
|
Serial.print(soundDataF->SPL_bands_dB[i],1);Serial.println(" dB");
|
||||||
|
}
|
||||||
|
Serial.print("Peak Sound Amplitude = ");Serial.print(soundDataF->peakAmp_mPa,2);Serial.println(" mPa");
|
||||||
|
}
|
||||||
|
|
||||||
|
void printParticleDataF(const ParticleData_F_t * particleDataF, uint8_t particleSensor) {
|
||||||
|
Serial.print("Particle Duty Cycle = ");Serial.print(particleDataF->duty_cycle_pc,2);Serial.println(" %");
|
||||||
|
Serial.print("Particle Concentration = ");
|
||||||
|
Serial.print(particleDataF->concentration,2);
|
||||||
|
if (particleSensor == PARTICLE_SENSOR_PPD42) {
|
||||||
|
Serial.println(" ppL");
|
||||||
|
}
|
||||||
|
else if (particleSensor == PARTICLE_SENSOR_SDS011) {
|
||||||
|
Serial.println(" " SDS011_UNIT_SYMBOL);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Serial.println(" (?)");
|
||||||
|
}
|
||||||
|
Serial.print("Particle data valid: ");
|
||||||
|
if (particleDataF->valid) {
|
||||||
|
Serial.println("Yes");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Serial.println("No (Initializing)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// The following five functions print data (in integer representation) over the serial port as text.
|
||||||
|
// printColumns determines the print format:
|
||||||
|
// choosing printColumns = false gives labeled values with measurement units
|
||||||
|
// choosing printColumns = true gives columns of numbers (convenient for spreadsheets).
|
||||||
|
|
||||||
|
void printAirData(const AirData_t * airData, bool printColumns) {
|
||||||
|
char strbuf[50] = {0};
|
||||||
|
|
||||||
|
uint8_t T_intPart = 0;
|
||||||
|
uint8_t T_fractionalPart = 0;
|
||||||
|
bool isPositive = true;
|
||||||
|
const char * T_unit = getTemperature(airData, &T_intPart, &T_fractionalPart, &isPositive);
|
||||||
|
|
||||||
|
if (printColumns) {
|
||||||
|
// Print: temperature, pressure/Pa, humidity/%, gas sensor resistance/ohm
|
||||||
|
sprintf(strbuf,"%s%u.%u %" PRIu32 " %u.%u %" PRIu32 " ",isPositive?"":"-", T_intPart, T_fractionalPart,
|
||||||
|
airData->P_Pa, airData->H_pc_int, airData->H_pc_fr_1dp, airData->G_ohm);
|
||||||
|
Serial.print(strbuf);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sprintf(strbuf,"Temperature = %s%u.%u %s", isPositive?"":"-", T_intPart, T_fractionalPart, T_unit);
|
||||||
|
Serial.println(strbuf);
|
||||||
|
Serial.print("Pressure = ");Serial.print(airData->P_Pa);Serial.println(" Pa");
|
||||||
|
sprintf(strbuf,"Humidity = %u.%u %%",airData->H_pc_int,airData->H_pc_fr_1dp);
|
||||||
|
Serial.println(strbuf);
|
||||||
|
Serial.print("Gas Sensor Resistance = ");Serial.print(airData->G_ohm);Serial.println(" " OHM_SYMBOL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void printAirQualityData(const AirQualityData_t * airQualityData, bool printColumns) {
|
||||||
|
char strbuf[50] = {0};
|
||||||
|
if (printColumns) {
|
||||||
|
// Print: Air Quality Index, Estimated CO2/ppm, Equivalent breath VOC/ppm, Accuracy
|
||||||
|
sprintf(strbuf,"%u.%u %u.%u %u.%02u %u ",airQualityData->AQI_int, airQualityData->AQI_fr_1dp,
|
||||||
|
airQualityData->CO2e_int, airQualityData->CO2e_fr_1dp,
|
||||||
|
airQualityData->bVOC_int, airQualityData->bVOC_fr_2dp, airQualityData->AQI_accuracy);
|
||||||
|
Serial.print(strbuf);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (airQualityData->AQI_accuracy > 0) {
|
||||||
|
sprintf(strbuf,"Air Quality Index = %u.%u (%s)",
|
||||||
|
airQualityData->AQI_int, airQualityData->AQI_fr_1dp, interpret_AQI_value(airQualityData->AQI_int));
|
||||||
|
Serial.println(strbuf);
|
||||||
|
sprintf(strbuf,"Estimated CO" SUBSCRIPT_2 " = %u.%u ppm",
|
||||||
|
airQualityData->CO2e_int, airQualityData->CO2e_fr_1dp);
|
||||||
|
Serial.println(strbuf);
|
||||||
|
sprintf(strbuf,"Equivalent Breath VOC = %u.%02u ppm",
|
||||||
|
airQualityData->bVOC_int, airQualityData->bVOC_fr_2dp);
|
||||||
|
Serial.println(strbuf);
|
||||||
|
}
|
||||||
|
Serial.print("Air Quality Accuracy: ");
|
||||||
|
Serial.println(interpret_AQI_accuracy(airQualityData->AQI_accuracy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void printSoundData(const SoundData_t * soundData, bool printColumns) {
|
||||||
|
char strbuf[50] = {0};
|
||||||
|
if (printColumns) {
|
||||||
|
// Print: Sound pressure level/dBA, Sound pressure level for frequency bands 1 to 6 (six columns),
|
||||||
|
// Peak sound amplitude/mPa, stability
|
||||||
|
sprintf(strbuf,"%u.%u ", soundData->SPL_dBA_int, soundData->SPL_dBA_fr_1dp);
|
||||||
|
Serial.print(strbuf);
|
||||||
|
for (uint16_t i=0; i<SOUND_FREQ_BANDS; i++) {
|
||||||
|
sprintf(strbuf,"%u.%u ", soundData->SPL_bands_dB_int[i], soundData->SPL_bands_dB_fr_1dp[i]);
|
||||||
|
Serial.print(strbuf);
|
||||||
|
}
|
||||||
|
sprintf(strbuf,"%u.%02u %u ", soundData->peak_amp_mPa_int,
|
||||||
|
soundData->peak_amp_mPa_fr_2dp, soundData->stable);
|
||||||
|
Serial.print(strbuf);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sprintf(strbuf,"A-weighted Sound Pressure Level = %u.%u dBA",
|
||||||
|
soundData->SPL_dBA_int, soundData->SPL_dBA_fr_1dp);
|
||||||
|
Serial.println(strbuf);
|
||||||
|
for (uint8_t i=0; i<SOUND_FREQ_BANDS; i++) {
|
||||||
|
sprintf(strbuf,"Frequency Band %u (%u Hz) SPL = %u.%u dB",
|
||||||
|
i+1, sound_band_mids_Hz[i], soundData->SPL_bands_dB_int[i], soundData->SPL_bands_dB_fr_1dp[i]);
|
||||||
|
Serial.println(strbuf);
|
||||||
|
}
|
||||||
|
sprintf(strbuf,"Peak Sound Amplitude = %u.%02u mPa",
|
||||||
|
soundData->peak_amp_mPa_int, soundData->peak_amp_mPa_fr_2dp);
|
||||||
|
Serial.println(strbuf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void printLightData(const LightData_t * lightData, bool printColumns) {
|
||||||
|
char strbuf[50] = {0};
|
||||||
|
if (printColumns) {
|
||||||
|
// Print: illuminance/lux, white level
|
||||||
|
sprintf(strbuf,"%u.%02u %u ", lightData->illum_lux_int, lightData->illum_lux_fr_2dp, lightData->white);
|
||||||
|
Serial.print(strbuf);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sprintf(strbuf,"Illuminance = %u.%02u lux", lightData->illum_lux_int, lightData->illum_lux_fr_2dp);
|
||||||
|
Serial.println(strbuf);
|
||||||
|
Serial.print("White Light Level = ");Serial.print(lightData->white);Serial.println();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void printParticleData(const ParticleData_t * particleData, bool printColumns, uint8_t particleSensor) {
|
||||||
|
char strbuf[50] = {0};
|
||||||
|
if (printColumns) {
|
||||||
|
// Print: duty cycle/%, concentration
|
||||||
|
sprintf(strbuf,"%u.%02u %u.%02u %u ", particleData->duty_cycle_pc_int,
|
||||||
|
particleData->duty_cycle_pc_fr_2dp, particleData->concentration_int,
|
||||||
|
particleData->concentration_fr_2dp, particleData->valid);
|
||||||
|
Serial.print(strbuf);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sprintf(strbuf,"Particle Duty Cycle = %u.%02u %%",
|
||||||
|
particleData->duty_cycle_pc_int, particleData->duty_cycle_pc_fr_2dp);
|
||||||
|
Serial.println(strbuf);
|
||||||
|
sprintf(strbuf,"Particle Concentration = %u.%02u ",
|
||||||
|
particleData->concentration_int, particleData->concentration_fr_2dp);
|
||||||
|
Serial.print(strbuf);
|
||||||
|
if (particleSensor == PARTICLE_SENSOR_PPD42) {
|
||||||
|
Serial.println("ppL");
|
||||||
|
}
|
||||||
|
else if (particleSensor == PARTICLE_SENSOR_SDS011) {
|
||||||
|
Serial.println(SDS011_UNIT_SYMBOL);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Serial.println("(?)");
|
||||||
|
}
|
||||||
|
Serial.print("Particle data valid: ");
|
||||||
|
if (particleData->valid == 0) {
|
||||||
|
Serial.println("No (Initializing)");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Serial.println("Yes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Send data to the Metriful MS430 using the I2C-compatible two wire interface.
|
||||||
|
//
|
||||||
|
// Returns true on success, false on failure.
|
||||||
|
//
|
||||||
|
// dev_addr_7bit = the 7-bit I2C address of the MS430 board.
|
||||||
|
// commandRegister = the settings register code or command code to be used.
|
||||||
|
// data = array containing the data to be sent; its length must be at least "data_length" bytes.
|
||||||
|
// data_length = the number of bytes from the "data" array to be sent.
|
||||||
|
//
|
||||||
|
bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length) {
|
||||||
|
|
||||||
|
if (data_length > ARDUINO_WIRE_BUFFER_LIMIT_BYTES) {
|
||||||
|
// The Arduino Wire library has a limited internal buffer size
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Wire.beginTransmission(dev_addr_7bit);
|
||||||
|
uint8_t bytesWritten = Wire.write(commandRegister);
|
||||||
|
if (data_length > 0) {
|
||||||
|
bytesWritten += Wire.write(data, data_length);
|
||||||
|
}
|
||||||
|
if (bytesWritten != (data_length+1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Wire.endTransmission(true) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read data from the Metriful MS430 using the I2C-compatible two wire interface.
|
||||||
|
//
|
||||||
|
// Returns true on success, false on failure.
|
||||||
|
//
|
||||||
|
// dev_addr_7bit = the 7-bit I2C address of the MS430 board.
|
||||||
|
// commandRegister = the settings register code or data location code to be used.
|
||||||
|
// data = array to store the received data; its length must be at least "data_length" bytes.
|
||||||
|
// data_length = the number of bytes to read.
|
||||||
|
//
|
||||||
|
bool ReceiveI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length) {
|
||||||
|
|
||||||
|
if (data_length == 0) {
|
||||||
|
// Cannot do a zero byte read
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data_length > ARDUINO_WIRE_BUFFER_LIMIT_BYTES) {
|
||||||
|
// The Arduino Wire library has a limited internal buffer size
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Wire.beginTransmission(dev_addr_7bit);
|
||||||
|
Wire.write(commandRegister);
|
||||||
|
if (Wire.endTransmission(false) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Wire.requestFrom(dev_addr_7bit, data_length, (uint8_t) 1) != data_length) {
|
||||||
|
// Did not receive the expected number of bytes
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t i=0; i<data_length; i++) {
|
||||||
|
if (Wire.available() > 0) {
|
||||||
|
data[i] = Wire.read();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Provide a readable interpretation of the accuracy code for
|
||||||
|
// the air quality measurements (applies to all air quality data)
|
||||||
|
const char * interpret_AQI_accuracy(uint8_t AQI_accuracy_code) {
|
||||||
|
switch (AQI_accuracy_code) {
|
||||||
|
default:
|
||||||
|
case 0:
|
||||||
|
return "Not yet valid, self-calibration incomplete";
|
||||||
|
case 1:
|
||||||
|
return "Low accuracy, self-calibration ongoing";
|
||||||
|
case 2:
|
||||||
|
return "Medium accuracy, self-calibration ongoing";
|
||||||
|
case 3:
|
||||||
|
return "High accuracy";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide a readable interpretation of the AQI (air quality index)
|
||||||
|
const char * interpret_AQI_value(uint16_t AQI) {
|
||||||
|
if (AQI < 50) {
|
||||||
|
return "Good";
|
||||||
|
}
|
||||||
|
else if (AQI < 100) {
|
||||||
|
return "Acceptable";
|
||||||
|
}
|
||||||
|
else if (AQI < 150) {
|
||||||
|
return "Substandard";
|
||||||
|
}
|
||||||
|
else if (AQI < 200) {
|
||||||
|
return "Poor";
|
||||||
|
}
|
||||||
|
else if (AQI < 300) {
|
||||||
|
return "Bad";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "Very bad";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the threshold for triggering a sound interrupt.
|
||||||
|
//
|
||||||
|
// Returns true on success, false on failure.
|
||||||
|
//
|
||||||
|
// threshold_mPa = peak sound amplitude threshold in milliPascals, any 16-bit integer is allowed.
|
||||||
|
bool setSoundInterruptThreshold(uint8_t dev_addr_7bit, uint16_t threshold_mPa) {
|
||||||
|
uint8_t TXdata[SOUND_INTERRUPT_THRESHOLD_BYTES] = {0};
|
||||||
|
TXdata[0] = (uint8_t) (threshold_mPa & 0x00FF);
|
||||||
|
TXdata[1] = (uint8_t) (threshold_mPa >> 8);
|
||||||
|
return TransmitI2C(dev_addr_7bit, SOUND_INTERRUPT_THRESHOLD_REG, TXdata, SOUND_INTERRUPT_THRESHOLD_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the threshold for triggering a light interrupt.
|
||||||
|
//
|
||||||
|
// Returns true on success, false on failure.
|
||||||
|
//
|
||||||
|
// The threshold value in lux units can be fractional and is formed as:
|
||||||
|
// threshold = thres_lux_int + (thres_lux_fr_2dp/100)
|
||||||
|
//
|
||||||
|
// Threshold values exceeding MAX_LUX_VALUE will be limited to MAX_LUX_VALUE.
|
||||||
|
bool setLightInterruptThreshold(uint8_t dev_addr_7bit, uint16_t thres_lux_int, uint8_t thres_lux_fr_2dp) {
|
||||||
|
uint8_t TXdata[LIGHT_INTERRUPT_THRESHOLD_BYTES] = {0};
|
||||||
|
TXdata[0] = (uint8_t) (thres_lux_int & 0x00FF);
|
||||||
|
TXdata[1] = (uint8_t) (thres_lux_int >> 8);
|
||||||
|
TXdata[2] = thres_lux_fr_2dp;
|
||||||
|
return TransmitI2C(dev_addr_7bit, LIGHT_INTERRUPT_THRESHOLD_REG, TXdata, LIGHT_INTERRUPT_THRESHOLD_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Convenience functions for reading data (integer representation)
|
||||||
|
//
|
||||||
|
// For each category of data (air, sound, etc.) a pointer to the data
|
||||||
|
// struct is passed to the ReceiveI2C() function. The received byte
|
||||||
|
// sequence fills the data struct in the correct order so that each
|
||||||
|
// field within the struct receives the value of an environmental data
|
||||||
|
// quantity (temperature, sound level, etc.)
|
||||||
|
|
||||||
|
SoundData_t getSoundData(uint8_t i2c_7bit_address) {
|
||||||
|
SoundData_t soundData = {0};
|
||||||
|
ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
|
||||||
|
return soundData;
|
||||||
|
}
|
||||||
|
|
||||||
|
AirData_t getAirData(uint8_t i2c_7bit_address) {
|
||||||
|
AirData_t airData = {0};
|
||||||
|
ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
|
||||||
|
return airData;
|
||||||
|
}
|
||||||
|
|
||||||
|
LightData_t getLightData(uint8_t i2c_7bit_address) {
|
||||||
|
LightData_t lightData = {0};
|
||||||
|
ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
|
||||||
|
return lightData;
|
||||||
|
}
|
||||||
|
|
||||||
|
AirQualityData_t getAirQualityData(uint8_t i2c_7bit_address) {
|
||||||
|
AirQualityData_t airQualityData = {0};
|
||||||
|
ReceiveI2C(i2c_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
|
||||||
|
return airQualityData;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParticleData_t getParticleData(uint8_t i2c_7bit_address) {
|
||||||
|
ParticleData_t particleData = {0};
|
||||||
|
ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
|
||||||
|
return particleData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience functions for reading data (float representation)
|
||||||
|
|
||||||
|
SoundData_F_t getSoundDataF(uint8_t i2c_7bit_address) {
|
||||||
|
SoundData_F_t soundDataF = {0};
|
||||||
|
SoundData_t soundData = getSoundData(i2c_7bit_address);
|
||||||
|
convertSoundDataF(&soundData, &soundDataF);
|
||||||
|
return soundDataF;
|
||||||
|
}
|
||||||
|
|
||||||
|
AirData_F_t getAirDataF(uint8_t i2c_7bit_address) {
|
||||||
|
AirData_F_t airDataF = {0};
|
||||||
|
AirData_t airData = getAirData(i2c_7bit_address);
|
||||||
|
convertAirDataF(&airData, &airDataF);
|
||||||
|
return airDataF;
|
||||||
|
}
|
||||||
|
|
||||||
|
LightData_F_t getLightDataF(uint8_t i2c_7bit_address) {
|
||||||
|
LightData_F_t lightDataF = {0};
|
||||||
|
LightData_t lightData = getLightData(i2c_7bit_address);
|
||||||
|
convertLightDataF(&lightData, &lightDataF);
|
||||||
|
return lightDataF;
|
||||||
|
}
|
||||||
|
|
||||||
|
AirQualityData_F_t getAirQualityDataF(uint8_t i2c_7bit_address) {
|
||||||
|
AirQualityData_F_t airQualityDataF = {0};
|
||||||
|
AirQualityData_t airQualityData = getAirQualityData(i2c_7bit_address);
|
||||||
|
convertAirQualityDataF(&airQualityData, &airQualityDataF);
|
||||||
|
return airQualityDataF;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParticleData_F_t getParticleDataF(uint8_t i2c_7bit_address) {
|
||||||
|
ParticleData_F_t particleDataF = {0};
|
||||||
|
ParticleData_t particleData = getParticleData(i2c_7bit_address);
|
||||||
|
convertParticleDataF(&particleData, &particleDataF);
|
||||||
|
return particleDataF;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Functions to convert Celsius temperature to Fahrenheit, in float
|
||||||
|
// and integer formats
|
||||||
|
|
||||||
|
float convertCtoF(float C) {
|
||||||
|
return ((C*1.8) + 32.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Celsius to Fahrenheit in sign, integer and fractional parts
|
||||||
|
void convertCtoF_int(float C, uint8_t * F_int, uint8_t * F_fr_1dp, bool * isPositive) {
|
||||||
|
float F = convertCtoF(C);
|
||||||
|
bool isNegative = (F < 0.0);
|
||||||
|
if (isNegative) {
|
||||||
|
F = -F;
|
||||||
|
}
|
||||||
|
F += 0.05;
|
||||||
|
F_int[0] = (uint8_t) F;
|
||||||
|
F -= (float) F_int[0];
|
||||||
|
F_fr_1dp[0] = (uint8_t) (F*10.0);
|
||||||
|
isPositive[0] = (!isNegative);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode and convert the temperature as read from the MS430 (integer
|
||||||
|
// representation) into a float value
|
||||||
|
float convertEncodedTemperatureToFloat(uint8_t T_C_int_with_sign, uint8_t T_C_fr_1dp) {
|
||||||
|
float temperature_C = ((float) (T_C_int_with_sign & TEMPERATURE_VALUE_MASK)) +
|
||||||
|
(((float) T_C_fr_1dp)/10.0);
|
||||||
|
if ((T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) {
|
||||||
|
// the most-significant bit is set, indicating that the temperature is negative
|
||||||
|
temperature_C = -temperature_C;
|
||||||
|
}
|
||||||
|
return temperature_C;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain temperature, in chosen units (C or F), as sign, integer and fractional parts
|
||||||
|
const char * getTemperature(const AirData_t * pAirData, uint8_t * T_intPart,
|
||||||
|
uint8_t * T_fractionalPart, bool * isPositive) {
|
||||||
|
#ifdef USE_FAHRENHEIT
|
||||||
|
float temperature_C = convertEncodedTemperatureToFloat(pAirData->T_C_int_with_sign,
|
||||||
|
pAirData->T_C_fr_1dp);
|
||||||
|
convertCtoF_int(temperature_C, T_intPart, T_fractionalPart, isPositive);
|
||||||
|
return FAHRENHEIT_SYMBOL;
|
||||||
|
#else
|
||||||
|
isPositive[0] = ((pAirData->T_C_int_with_sign & TEMPERATURE_SIGN_MASK) == 0);
|
||||||
|
T_intPart[0] = pAirData->T_C_int_with_sign & TEMPERATURE_VALUE_MASK;
|
||||||
|
T_fractionalPart[0] = pAirData->T_C_fr_1dp;
|
||||||
|
return CELSIUS_SYMBOL;
|
||||||
|
#endif
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
Metriful_sensor.h
|
||||||
|
|
||||||
|
This file declares functions and settings which are used in the code
|
||||||
|
examples. The function definitions are in file Metriful_sensor.cpp
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef METRIFUL_SENSOR_H
|
||||||
|
#define METRIFUL_SENSOR_H
|
||||||
|
|
||||||
|
#include "Arduino.h"
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "sensor_constants.h"
|
||||||
|
#include "host_pin_definitions.h"
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Choose to display output temperatures in Fahrenheit:
|
||||||
|
// un-comment the following line to use Fahrenheit
|
||||||
|
//#define USE_FAHRENHEIT
|
||||||
|
|
||||||
|
// Specify which particle sensor is connected:
|
||||||
|
#define PARTICLE_SENSOR PARTICLE_SENSOR_PPD42
|
||||||
|
// Define PARTICLE_SENSOR as:
|
||||||
|
// PARTICLE_SENSOR_PPD42 for the Shinyei PPD42
|
||||||
|
// PARTICLE_SENSOR_SDS011 for the Nova SDS011
|
||||||
|
// PARTICLE_SENSOR_OFF if no sensor is connected
|
||||||
|
|
||||||
|
// The I2C address of the MS430 board.
|
||||||
|
#define I2C_ADDRESS I2C_ADDR_7BIT_SB_OPEN
|
||||||
|
// The default is I2C_ADDR_7BIT_SB_OPEN and must be changed to
|
||||||
|
// I2C_ADDR_7BIT_SB_CLOSED if the solder bridge SB1 on the board
|
||||||
|
// is soldered closed
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#define I2C_CLK_FREQ_HZ 100000
|
||||||
|
#define SERIAL_BAUD_RATE 9600
|
||||||
|
|
||||||
|
// Unicode symbol strings
|
||||||
|
#define CELSIUS_SYMBOL "\u00B0C"
|
||||||
|
#define FAHRENHEIT_SYMBOL "\u00B0F"
|
||||||
|
#define SDS011_UNIT_SYMBOL "\u00B5g/m\u00B3"
|
||||||
|
#define SUBSCRIPT_2 "\u2082"
|
||||||
|
#define OHM_SYMBOL "\u03A9"
|
||||||
|
|
||||||
|
extern volatile bool ready_assertion_event;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Data category structs containing floats. If floats are not wanted,
|
||||||
|
// use the integer-only struct versions in sensor_constants.h
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float SPL_dBA;
|
||||||
|
float SPL_bands_dB[SOUND_FREQ_BANDS];
|
||||||
|
float peakAmp_mPa;
|
||||||
|
bool stable;
|
||||||
|
} SoundData_F_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float T_C;
|
||||||
|
uint32_t P_Pa;
|
||||||
|
float H_pc;
|
||||||
|
uint32_t G_Ohm;
|
||||||
|
} AirData_F_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float AQI;
|
||||||
|
float CO2e;
|
||||||
|
float bVOC;
|
||||||
|
uint8_t AQI_accuracy;
|
||||||
|
} AirQualityData_F_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float illum_lux;
|
||||||
|
uint16_t white;
|
||||||
|
} LightData_F_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
float duty_cycle_pc;
|
||||||
|
float concentration;
|
||||||
|
bool valid;
|
||||||
|
} ParticleData_F_t;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Custom type used to select the particle sensor being used (if any)
|
||||||
|
typedef enum {
|
||||||
|
OFF = PARTICLE_SENSOR_OFF,
|
||||||
|
PPD42 = PARTICLE_SENSOR_PPD42,
|
||||||
|
SDS011 = PARTICLE_SENSOR_SDS011
|
||||||
|
} ParticleSensor_t;
|
||||||
|
|
||||||
|
// Struct used in the IFTTT example
|
||||||
|
typedef struct {
|
||||||
|
const char * variableName;
|
||||||
|
const char * measurementUnit;
|
||||||
|
int32_t thresHigh;
|
||||||
|
int32_t thresLow;
|
||||||
|
uint16_t inactiveCount;
|
||||||
|
const char * adviceHigh;
|
||||||
|
const char * adviceLow;
|
||||||
|
} ThresholdSetting_t;
|
||||||
|
|
||||||
|
// Struct used in the Home Assistant example
|
||||||
|
typedef struct {
|
||||||
|
const char * name;
|
||||||
|
const char * unit;
|
||||||
|
const char * icon;
|
||||||
|
uint8_t decimalPlaces;
|
||||||
|
} HA_Attributes_t;
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
void SensorHardwareSetup(uint8_t i2c_7bit_address);
|
||||||
|
void ISR_ATTRIBUTE ready_ISR(void);
|
||||||
|
|
||||||
|
bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length);
|
||||||
|
bool ReceiveI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length);
|
||||||
|
|
||||||
|
const char * interpret_AQI_accuracy(uint8_t AQI_accuracy_code);
|
||||||
|
const char * interpret_AQI_value(uint16_t AQI);
|
||||||
|
|
||||||
|
void convertAirDataF(const AirData_t * airData_in, AirData_F_t * airDataF_out);
|
||||||
|
void convertAirQualityDataF(const AirQualityData_t * airQualityData_in,
|
||||||
|
AirQualityData_F_t * airQualityDataF_out);
|
||||||
|
void convertLightDataF(const LightData_t * lightData_in, LightData_F_t * lightDataF_out);
|
||||||
|
void convertSoundDataF(const SoundData_t * soundData_in, SoundData_F_t * soundDataF_out);
|
||||||
|
void convertParticleDataF(const ParticleData_t * particleData_in, ParticleData_F_t * particleDataF_out);
|
||||||
|
|
||||||
|
void printAirDataF(const AirData_F_t * airDataF);
|
||||||
|
void printAirQualityDataF(const AirQualityData_F_t * airQualityDataF);
|
||||||
|
void printLightDataF(const LightData_F_t * lightDataF);
|
||||||
|
void printSoundDataF(const SoundData_F_t * soundDataF);
|
||||||
|
void printParticleDataF(const ParticleData_F_t * particleDataF, uint8_t particleSensor);
|
||||||
|
|
||||||
|
void printAirData(const AirData_t * airData, bool printColumns);
|
||||||
|
void printAirQualityData(const AirQualityData_t * airQualityData, bool printColumns);
|
||||||
|
void printLightData(const LightData_t * lightData, bool printColumns);
|
||||||
|
void printSoundData(const SoundData_t * soundData, bool printColumns);
|
||||||
|
void printParticleData(const ParticleData_t * particleData, bool printColumns, uint8_t particleSensor);
|
||||||
|
|
||||||
|
bool setSoundInterruptThreshold(uint8_t dev_addr_7bit, uint16_t threshold_mPa);
|
||||||
|
bool setLightInterruptThreshold(uint8_t dev_addr_7bit, uint16_t thres_lux_int, uint8_t thres_lux_fr_2dp);
|
||||||
|
|
||||||
|
SoundData_t getSoundData(uint8_t i2c_7bit_address);
|
||||||
|
AirData_t getAirData(uint8_t i2c_7bit_address);
|
||||||
|
LightData_t getLightData(uint8_t i2c_7bit_address);
|
||||||
|
AirQualityData_t getAirQualityData(uint8_t i2c_7bit_address);
|
||||||
|
ParticleData_t getParticleData(uint8_t i2c_7bit_address);
|
||||||
|
|
||||||
|
SoundData_F_t getSoundDataF(uint8_t i2c_7bit_address);
|
||||||
|
AirData_F_t getAirDataF(uint8_t i2c_7bit_address);
|
||||||
|
LightData_F_t getLightDataF(uint8_t i2c_7bit_address);
|
||||||
|
AirQualityData_F_t getAirQualityDataF(uint8_t i2c_7bit_address);
|
||||||
|
ParticleData_F_t getParticleDataF(uint8_t i2c_7bit_address);
|
||||||
|
|
||||||
|
float convertCtoF(float C);
|
||||||
|
void convertCtoF_int(float C, uint8_t * F_int, uint8_t * F_fr_1dp, bool * isPositive);
|
||||||
|
float convertEncodedTemperatureToFloat(uint8_t T_C_int_with_sign, uint8_t T_C_fr_1dp);
|
||||||
|
const char * getTemperature(const AirData_t * pAirData, uint8_t * T_intPart,
|
||||||
|
uint8_t * T_fractionalPart, bool * isPositive);
|
||||||
|
#endif
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
WiFi_functions.cpp
|
||||||
|
|
||||||
|
This file defines functions used by examples connecting to,
|
||||||
|
or creating, a WiFi network.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "host_pin_definitions.h"
|
||||||
|
#ifdef HAS_WIFI
|
||||||
|
#include "Arduino.h"
|
||||||
|
#include "WiFi_functions.h"
|
||||||
|
|
||||||
|
// Repeatedly attempt to connect to the WiFi network using the input
|
||||||
|
// network name (SSID) and password.
|
||||||
|
void connectToWiFi(const char * SSID, const char * password) {
|
||||||
|
WiFi.disconnect();
|
||||||
|
#if defined(ESP8266) || defined(ESP32)
|
||||||
|
WiFi.persistent(false);
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
#endif
|
||||||
|
uint8_t wStatus = WL_DISCONNECTED;
|
||||||
|
while (wStatus != WL_CONNECTED) {
|
||||||
|
Serial.print("Attempting to connect to ");
|
||||||
|
Serial.println(SSID);
|
||||||
|
uint8_t statusChecks = 0;
|
||||||
|
WiFi.begin(SSID, password);
|
||||||
|
while ((wStatus != WL_CONNECTED) && (statusChecks < 8)) {
|
||||||
|
delay(1000);
|
||||||
|
Serial.print(".");
|
||||||
|
wStatus = WiFi.status();
|
||||||
|
statusChecks++;
|
||||||
|
}
|
||||||
|
if (wStatus != WL_CONNECTED) {
|
||||||
|
Serial.println("Failed.");
|
||||||
|
WiFi.disconnect();
|
||||||
|
delay(5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Serial.println("Connected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the host as a WiFi access point, creating a WiFi network with
|
||||||
|
// specified network SSID (name), password and host IP address.
|
||||||
|
bool createWiFiAP(const char * SSID, const char * password, IPAddress hostIP) {
|
||||||
|
Serial.print("Creating access point named: ");
|
||||||
|
Serial.println(SSID);
|
||||||
|
#if defined(ESP8266) || defined(ESP32)
|
||||||
|
WiFi.persistent(false);
|
||||||
|
WiFi.mode(WIFI_AP);
|
||||||
|
IPAddress subnet(255,255,255,0);
|
||||||
|
bool success = WiFi.softAP(SSID, password);
|
||||||
|
delay(2000);
|
||||||
|
success = success && WiFi.softAPConfig(hostIP, hostIP, subnet);
|
||||||
|
#else
|
||||||
|
WiFi.config(hostIP);
|
||||||
|
bool success = (WiFi.beginAP(SSID, password) == WL_AP_LISTENING);
|
||||||
|
#endif
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide a readable interpretation of the WiFi status.
|
||||||
|
// statusCode is the value returned by WiFi.status()
|
||||||
|
const char * interpret_WiFi_status(uint8_t statusCode) {
|
||||||
|
switch (statusCode) {
|
||||||
|
case WL_CONNECTED:
|
||||||
|
return "Connected";
|
||||||
|
case WL_NO_SHIELD:
|
||||||
|
return "No shield";
|
||||||
|
case WL_IDLE_STATUS:
|
||||||
|
return "Idle";
|
||||||
|
case WL_NO_SSID_AVAIL:
|
||||||
|
return "No SSID available";
|
||||||
|
case WL_SCAN_COMPLETED:
|
||||||
|
return "Scan completed";
|
||||||
|
case WL_CONNECT_FAILED:
|
||||||
|
return "Connect failed";
|
||||||
|
case WL_CONNECTION_LOST:
|
||||||
|
return "Connection lost";
|
||||||
|
case WL_DISCONNECTED:
|
||||||
|
return "Disconnected";
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
WiFi_functions.h
|
||||||
|
|
||||||
|
This file declares functions used by examples connecting to,
|
||||||
|
or creating, a WiFi network.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "host_pin_definitions.h"
|
||||||
|
#ifdef HAS_WIFI
|
||||||
|
#ifndef WIFI_FUNCTIONS_H
|
||||||
|
#define WIFI_FUNCTIONS_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
void connectToWiFi(const char * SSID, const char * password);
|
||||||
|
bool createWiFiAP(const char * SSID, const char * password, IPAddress hostIP);
|
||||||
|
const char * interpret_WiFi_status(uint8_t statusCode);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
#endif
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,333 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<!--Copyright 2020 Metriful Ltd. Licensed under the MIT License.-->
|
||||||
|
<head>
|
||||||
|
<meta charset='UTF-8'>
|
||||||
|
<title>Indoor Environment Data</title>
|
||||||
|
<script src='https://cdn.plot.ly/plotly-1.56.0.min.js' charset='utf-8'></script>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1'>
|
||||||
|
<style>
|
||||||
|
.tx {
|
||||||
|
font-family: Verdana, sans-serif;
|
||||||
|
text-align:center;
|
||||||
|
font-weight:normal;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style='background-color:#ededed;' onload='plotBufferedData()'>
|
||||||
|
<h3 class='tx'>Indoor Environment Data</h3>
|
||||||
|
<div id='tdata' class='tx'></div>
|
||||||
|
<div id='error' class='tx'>Incomplete load: please refresh the page.</div>
|
||||||
|
<div id='grid' style='display: flex;'></div>
|
||||||
|
<br>
|
||||||
|
<div class='tx'>
|
||||||
|
<button type='button' onclick='makeCSVfile()'>Download CSV data</button>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<p class='tx'><a href='https://www.sensor.metriful.com'>sensor.metriful.com</a></p>
|
||||||
|
<br>
|
||||||
|
<a id='lnk' href='' style='visibility:hidden;'></a>
|
||||||
|
<script>
|
||||||
|
var max_data_length = 1000;
|
||||||
|
var x_values = [];
|
||||||
|
var data = [];
|
||||||
|
var names = ['Air Quality Index','Temperature','Pressure','Humidity',
|
||||||
|
'Sound Level','Illuminance','Breath VOC','Particulates'];
|
||||||
|
var units = new Map([['AQI',''],['T','\u00B0C'],['P','Pa'], ['H','%'],
|
||||||
|
['SPL','dBA'],['lux','lux'],['bVOC','ppm'],['part','\u00B5g/m\u00B3']]);
|
||||||
|
var titles = []
|
||||||
|
const decimalPlaces = [1,1,0,1,1,2,2,2];
|
||||||
|
const AQI_position = 0;
|
||||||
|
var Ngraphs = 0;
|
||||||
|
var isMobile = false;
|
||||||
|
var doPlot = true;
|
||||||
|
var includeParticles = true;
|
||||||
|
var delay_ms = 0;
|
||||||
|
var errorString = 'Incomplete load: please refresh the page.';
|
||||||
|
|
||||||
|
// Put a leading zero on a string to get correct time and date format
|
||||||
|
function padString(s) {
|
||||||
|
if (s.length == 1) {
|
||||||
|
return ('0' + s);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get time as a string in the format HH:MM:SS
|
||||||
|
function makeTimeString(dateNum) {
|
||||||
|
d = new Date(dateNum);
|
||||||
|
return (padString(d.getHours().toString())
|
||||||
|
+ ':' + padString(d.getMinutes().toString())
|
||||||
|
+ ':' + padString(d.getSeconds().toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get time and date as a string in the format YYYY-mm-DD HH:MM:SS
|
||||||
|
function makeTimeDateString(dateNum) {
|
||||||
|
d = new Date(dateNum);
|
||||||
|
return (d.getFullYear().toString()
|
||||||
|
+ '-' + padString((d.getMonth()+1).toString())
|
||||||
|
+ '-' + padString(d.getDate().toString())
|
||||||
|
+ ' ' + makeTimeString(dateNum));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make graphs using Plotly
|
||||||
|
function plotGraph(plotname, i) {
|
||||||
|
P = document.getElementById(plotname);
|
||||||
|
Plotly.newPlot(P, [{
|
||||||
|
x: x_values,
|
||||||
|
y: data[i],
|
||||||
|
mode: 'lines'}], {
|
||||||
|
title: {
|
||||||
|
text: titles[i],
|
||||||
|
font: {
|
||||||
|
family: 'verdana, sans-serif',
|
||||||
|
size: 15},
|
||||||
|
xref: 'paper',
|
||||||
|
x: (isMobile ? 0 : 0.5),
|
||||||
|
yref: 'paper',
|
||||||
|
y: 1,
|
||||||
|
yanchor:'bottom',
|
||||||
|
pad: {b:15}},
|
||||||
|
plot_bgcolor:'#f5f6f7',
|
||||||
|
paper_bgcolor:'#ededed',
|
||||||
|
margin: {
|
||||||
|
l: 60,
|
||||||
|
r: 30,
|
||||||
|
b: 0,
|
||||||
|
t: 40},
|
||||||
|
xaxis: {
|
||||||
|
nticks: (isMobile ? 3 : 7),
|
||||||
|
showline: true,
|
||||||
|
automargin: true,
|
||||||
|
mirror: 'ticks',
|
||||||
|
linewidth: 1},
|
||||||
|
yaxis: {
|
||||||
|
automargin: true,
|
||||||
|
showline: true,
|
||||||
|
mirror: 'ticks',
|
||||||
|
linewidth: 1},
|
||||||
|
autosize: true},
|
||||||
|
{responsive: true, displaylogo:false,
|
||||||
|
modeBarButtonsToRemove: ['toggleSpikelines',
|
||||||
|
'hoverClosestCartesian','hoverCompareCartesian','zoomIn2d',
|
||||||
|
'zoomOut2d','autoScale2d']});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide a text interpretation of the air quality index value
|
||||||
|
function interpretAQI(AQI) {
|
||||||
|
if (AQI < 50) {
|
||||||
|
return 'Good';
|
||||||
|
}
|
||||||
|
else if (AQI < 100) {
|
||||||
|
return 'Acceptable';
|
||||||
|
}
|
||||||
|
else if (AQI < 150) {
|
||||||
|
return 'Substandard';
|
||||||
|
}
|
||||||
|
else if (AQI < 200) {
|
||||||
|
return 'Poor';
|
||||||
|
}
|
||||||
|
else if (AQI < 300) {
|
||||||
|
return 'Bad';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 'Very bad';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display data as text (if graphs cannot be created)
|
||||||
|
function showTextData() {
|
||||||
|
var j = x_values.length - 1;
|
||||||
|
const dn = Date.now();
|
||||||
|
const d = new Date(dn);
|
||||||
|
var t = '<br>' + makeTimeString(dn) + ' ' + d.toDateString() + '<br><br>';
|
||||||
|
t += 'Air Quality: ' + interpretAQI(data[AQI_position][j]) + '<br><br>';
|
||||||
|
for (var i=0; i<Ngraphs; i++) {
|
||||||
|
t += names[i] + ': ' + data[i][j].toFixed(decimalPlaces[i]) + ' '
|
||||||
|
+ units.get(Array.from(units.keys())[i]) + '<br><br>';
|
||||||
|
}
|
||||||
|
document.getElementById('tdata').innerHTML = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do a GET request for all of the buffered data and generate the graphs
|
||||||
|
function plotBufferedData() {
|
||||||
|
document.getElementById('error').innerHTML = '';
|
||||||
|
// Check whether plotly library could be loaded:
|
||||||
|
doPlot = !(typeof(Plotly) == 'undefined');
|
||||||
|
var xmlhttp = new XMLHttpRequest();
|
||||||
|
xmlhttp.onreadystatechange=function() {
|
||||||
|
if (xmlhttp.readyState==4 && xmlhttp.status==200) {
|
||||||
|
const body = xmlhttp.response;
|
||||||
|
if (body.byteLength < 5) {
|
||||||
|
// The correct response should have at least 5 bytes
|
||||||
|
document.getElementById('error').innerHTML = errorString;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Read the time interval between new data
|
||||||
|
delay_ms = (new Uint16Array(body.slice(0, 2)))[0]*1000;
|
||||||
|
const codeByte = (new Uint8Array(body.slice(2, 3)))[0];
|
||||||
|
Ngraphs = units.size; // number of graphs to plot
|
||||||
|
if ((codeByte & 0x0F) == 0x00) {
|
||||||
|
// There is no particle sensor, so omit one graph
|
||||||
|
Ngraphs-=1;
|
||||||
|
includeParticles = false;
|
||||||
|
}
|
||||||
|
else if ((codeByte & 0x0F) == 0x01) {
|
||||||
|
// A PPD42 sensor is used: change the units (default is for SDS011)
|
||||||
|
units.set('part','ppL');
|
||||||
|
}
|
||||||
|
if ((codeByte & 0x10) != 0) {
|
||||||
|
// Change temperature units to Fahrenheit (default is Celsius)
|
||||||
|
units.set('T','\u00B0F');
|
||||||
|
}
|
||||||
|
for (var i=0; i<Ngraphs; i++) {
|
||||||
|
var u = units.get(Array.from(units.keys())[i]);
|
||||||
|
if (u === '') {
|
||||||
|
titles.push(names[i]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
titles.push(names[i] + ' / ' + u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const buflen = (new Uint16Array(body.slice(3, 5)))[0];
|
||||||
|
// Check length of remaining data:
|
||||||
|
var expBytes = 5 + (Ngraphs*4*buflen);
|
||||||
|
if (expBytes != body.byteLength) {
|
||||||
|
document.getElementById('error').innerHTML = errorString;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Extract and decode data, starting at byte 5
|
||||||
|
const view = new DataView(body, 5);
|
||||||
|
var byteOffset = 0;
|
||||||
|
for (var i = 0; i < Ngraphs; i++) {
|
||||||
|
data.push([]);
|
||||||
|
for (var v = 0; v < buflen; v++) {
|
||||||
|
data[i].push(view.getFloat32(byteOffset, true));
|
||||||
|
byteOffset+=4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Create approximate time data for each point, based on current
|
||||||
|
// time and known cycle delay time
|
||||||
|
var val = Date.now();
|
||||||
|
x_values = new Array(buflen);
|
||||||
|
for (var i=buflen; i>0; i--) {
|
||||||
|
x_values[i-1] = makeTimeDateString(val);
|
||||||
|
val = val - delay_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buflen > max_data_length) {
|
||||||
|
max_data_length = buflen;
|
||||||
|
}
|
||||||
|
// Create the plots, formatting appropriately for mobile or desktop
|
||||||
|
isMobile = isMobileDevice();
|
||||||
|
if (doPlot) {
|
||||||
|
var w_pc = isMobile ? 100 : 50;
|
||||||
|
var h_vh = isMobile ? 33.3 : 50;
|
||||||
|
var colTxt = "<div class='column' style='flex: " + w_pc.toString() + "%'>";
|
||||||
|
var mainTxt = colTxt;
|
||||||
|
for (var i=0; i<Ngraphs; i++) {
|
||||||
|
if ((!isMobile) && (i == Math.ceil(Ngraphs/2))) {
|
||||||
|
mainTxt += "</div>" + colTxt;
|
||||||
|
}
|
||||||
|
mainTxt += "<div style='height:" + h_vh.toString() + "vh'><div id='plot"
|
||||||
|
+ i.toString() + "' style='height:90%'></div></div>";
|
||||||
|
}
|
||||||
|
mainTxt += "</div>";
|
||||||
|
document.getElementById('grid').innerHTML = mainTxt;
|
||||||
|
|
||||||
|
for (var i=0; i<Ngraphs; i++) {
|
||||||
|
plotGraph('plot' + i.toString(), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Plotly could not be loaded - show text data instead
|
||||||
|
showTextData();
|
||||||
|
document.getElementById('error').innerHTML = '<br>Graphs are not displayed because the Plotly.js library could not be loaded.<br>Connect to the internet, or cache the script for offline use.<br><br>';
|
||||||
|
}
|
||||||
|
// Schedule the data update so that the page will keep showing new data
|
||||||
|
setTimeout(getLatestData, delay_ms);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xmlhttp.open('GET','/1',true);
|
||||||
|
xmlhttp.responseType = 'arraybuffer';
|
||||||
|
xmlhttp.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do a GET request for just the last value of each variable, then plot it.
|
||||||
|
// This function runs periodically, at the same interval as data are read
|
||||||
|
// on the MS430.
|
||||||
|
// NOTE: if a 3 second cycle is used, most browsers will NOT call the function
|
||||||
|
// every 3 seconds unless the browser window is "in focus" (in view of the
|
||||||
|
// user and selected). If the window is minimized or in a background tab,
|
||||||
|
// the delay between function calls is often greater.
|
||||||
|
function getLatestData() {
|
||||||
|
var xmlhttp = new XMLHttpRequest();
|
||||||
|
xmlhttp.onreadystatechange=function() {
|
||||||
|
if (xmlhttp.readyState==4 && xmlhttp.status==200) {
|
||||||
|
const d = new Float32Array(xmlhttp.response);
|
||||||
|
// Only attempt data extraction if the data length is as expected:
|
||||||
|
if (d.length == Ngraphs) {
|
||||||
|
for (var i = 0; i < Ngraphs; i++) {
|
||||||
|
if (x_values.length == max_data_length) {
|
||||||
|
data[i].shift();
|
||||||
|
}
|
||||||
|
data[i].push(d[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x_values.length == max_data_length) {
|
||||||
|
x_values.shift();
|
||||||
|
}
|
||||||
|
x_values.push(makeTimeDateString(Date.now()));
|
||||||
|
|
||||||
|
if (doPlot) {
|
||||||
|
for (var i=0; i<Ngraphs; i++) {
|
||||||
|
plotGraph('plot' + i.toString(), i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Graphs are not being plotted because the Plotly library could not
|
||||||
|
// be loaded, so display data as text instead.
|
||||||
|
showTextData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Reschedule this function to run again after the cycle period time.
|
||||||
|
setTimeout(getLatestData, delay_ms);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xmlhttp.open('GET','/2',true);
|
||||||
|
xmlhttp.responseType = 'arraybuffer';
|
||||||
|
xmlhttp.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect whether the browser is on a mobile device (tablet does not qualify)
|
||||||
|
// If mobile, show graphs in a single column. Otherwise, use two columns.
|
||||||
|
function isMobileDevice() {
|
||||||
|
let result = false;
|
||||||
|
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) result = true;})(navigator.userAgent||navigator.vendor||window.opera);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a "comma separated values" file containing all data and start the download.
|
||||||
|
// This file can be opened with most spreadsheet software and text editors.
|
||||||
|
function makeCSVfile() {
|
||||||
|
var csvData = '"Time and Date"';
|
||||||
|
for (var i=0; i<Ngraphs; i++) {
|
||||||
|
csvData += ',"' + titles[i] + '"';
|
||||||
|
}
|
||||||
|
csvData += '\r\n';
|
||||||
|
for (var n=0; n<x_values.length; n++) {
|
||||||
|
csvData += '"' + x_values[n] + '"';
|
||||||
|
for (var i=0; i<Ngraphs; i++) {
|
||||||
|
csvData += ',"' + data[i][n].toFixed(decimalPlaces[i]) + '"';
|
||||||
|
}
|
||||||
|
csvData += '\r\n';
|
||||||
|
}
|
||||||
|
var f = document.getElementById('lnk');
|
||||||
|
URL.revokeObjectURL(f.href);
|
||||||
|
f.href = URL.createObjectURL(new Blob([csvData],{type:'text/csv;charset=utf-8;'}));
|
||||||
|
f.download='data.csv';
|
||||||
|
f.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,212 @@
|
||||||
|
/*
|
||||||
|
host_pin_definitions.h
|
||||||
|
|
||||||
|
This file defines which host pins are used to interface to the
|
||||||
|
Metriful MS430 board. The relevant file section is selected
|
||||||
|
automatically when the board is chosen in the Arduino IDE.
|
||||||
|
|
||||||
|
More detail is provided in the readme and User Guide.
|
||||||
|
|
||||||
|
This file provides settings for the following host systems:
|
||||||
|
* Arduino Uno
|
||||||
|
* Arduino Nano 33 IoT
|
||||||
|
* Arduino Nano
|
||||||
|
* Arduino MKR WiFi 1010
|
||||||
|
* ESP8266 (tested on NodeMCU and Wemos D1 Mini - other boards may require changes)
|
||||||
|
* ESP32 (tested on DOIT ESP32 DEVKIT V1 - other boards may require changes)
|
||||||
|
|
||||||
|
The Metriful MS430 is compatible with many more development boards
|
||||||
|
than those listed. You can use this file as a guide to define the
|
||||||
|
necessary settings for other host systems.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ARDUINO_PIN_DEFINITIONS_H
|
||||||
|
#define ARDUINO_PIN_DEFINITIONS_H
|
||||||
|
|
||||||
|
#ifdef ARDUINO_AVR_UNO
|
||||||
|
|
||||||
|
// Arduino Uno
|
||||||
|
|
||||||
|
#define ISR_ATTRIBUTE
|
||||||
|
|
||||||
|
#define READY_PIN 2 // Arduino digital pin 2 connects to RDY
|
||||||
|
#define L_INT_PIN 4 // Arduino digital pin 4 connects to LIT
|
||||||
|
#define S_INT_PIN 7 // Arduino digital pin 7 connects to SIT
|
||||||
|
/* Also make the following connections:
|
||||||
|
Arduino pins GND, SCL, SDA to MS430 pins GND, SCL, SDA
|
||||||
|
Arduino pin 5V to MS430 pins VPU and VIN
|
||||||
|
MS430 pin VDD is unused
|
||||||
|
|
||||||
|
If a PPD42 particle sensor is used, connect the following:
|
||||||
|
Arduino pin 5V to PPD42 pin 3
|
||||||
|
Arduino pin GND to PPD42 pin 1
|
||||||
|
PPD42 pin 4 to MS430 pin PRT
|
||||||
|
|
||||||
|
If an SDS011 particle sensor is used, connect the following:
|
||||||
|
Arduino pin 5V to SDS011 pin "5V"
|
||||||
|
Arduino pin GND to SDS011 pin "GND"
|
||||||
|
SDS011 pin "25um" to MS430 pin PRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#elif defined ARDUINO_SAMD_NANO_33_IOT
|
||||||
|
|
||||||
|
// Arduino Nano 33 IoT
|
||||||
|
|
||||||
|
#include <SPI.h>
|
||||||
|
#include <WiFiNINA.h>
|
||||||
|
#define HAS_WIFI
|
||||||
|
#define ISR_ATTRIBUTE
|
||||||
|
|
||||||
|
#define READY_PIN 11 // Arduino pin D11 connects to RDY
|
||||||
|
#define L_INT_PIN A1 // Arduino pin A1 connects to LIT
|
||||||
|
#define S_INT_PIN A2 // Arduino pin A2 connects to SIT
|
||||||
|
/* Also make the following connections:
|
||||||
|
Arduino pin GND to MS430 pin GND
|
||||||
|
Arduino pin 3.3V to MS430 pins VPU and VDD
|
||||||
|
Arduino pin A5 to MS430 pin SCL
|
||||||
|
Arduino pin A4 to MS430 pin SDA
|
||||||
|
MS430 pin VIN is unused
|
||||||
|
|
||||||
|
If a PPD42 particle sensor is used, connect the following:
|
||||||
|
Arduino pin VUSB to PPD42 pin 3
|
||||||
|
Arduino pin GND to PPD42 pin 1
|
||||||
|
PPD42 pin 4 to MS430 pin PRT
|
||||||
|
|
||||||
|
If an SDS011 particle sensor is used, connect the following:
|
||||||
|
Arduino pin VUSB to SDS011 pin "5V"
|
||||||
|
Arduino pin GND to SDS011 pin "GND"
|
||||||
|
SDS011 pin "25um" to MS430 pin PRT
|
||||||
|
|
||||||
|
The solder bridge labeled "VUSB" on the underside of the Arduino
|
||||||
|
must be soldered closed to provide 5V to the PPD42/SDS011.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#elif defined ARDUINO_AVR_NANO
|
||||||
|
|
||||||
|
// Arduino Nano
|
||||||
|
|
||||||
|
#define ISR_ATTRIBUTE
|
||||||
|
|
||||||
|
#define READY_PIN 2 // Arduino pin D2 connects to RDY
|
||||||
|
#define L_INT_PIN 4 // Arduino pin D4 connects to LIT
|
||||||
|
#define S_INT_PIN 7 // Arduino pin D7 connects to SIT
|
||||||
|
/* Also make the following connections:
|
||||||
|
Arduino pin GND to MS430 pin GND
|
||||||
|
Arduino pin A5 (SCL) to MS430 pin SCL
|
||||||
|
Arduino pin A4 (SDA) to MS430 pin SDA
|
||||||
|
Arduino pin 5V to MS430 pins VPU and VIN
|
||||||
|
MS430 pin VDD is unused
|
||||||
|
|
||||||
|
If a PPD42 particle sensor is used, connect the following:
|
||||||
|
Arduino pin 5V to PPD42 pin 3
|
||||||
|
Arduino pin GND to PPD42 pin 1
|
||||||
|
PPD42 pin 4 to MS430 pin PRT
|
||||||
|
|
||||||
|
If an SDS011 particle sensor is used, connect the following:
|
||||||
|
Arduino pin 5V to SDS011 pin "5V"
|
||||||
|
Arduino pin GND to SDS011 pin "GND"
|
||||||
|
SDS011 pin "25um" to MS430 pin PRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#elif defined ARDUINO_SAMD_MKRWIFI1010
|
||||||
|
|
||||||
|
// Arduino MKR WiFi 1010
|
||||||
|
|
||||||
|
#include <SPI.h>
|
||||||
|
#include <WiFiNINA.h>
|
||||||
|
#define HAS_WIFI
|
||||||
|
#define ISR_ATTRIBUTE
|
||||||
|
|
||||||
|
#define READY_PIN 0 // Arduino digital pin 0 connects to RDY
|
||||||
|
#define L_INT_PIN 4 // Arduino digital pin 4 connects to LIT
|
||||||
|
#define S_INT_PIN 5 // Arduino digital pin 5 connects to SIT
|
||||||
|
/* Also make the following connections:
|
||||||
|
Arduino pin GND to MS430 pin GND
|
||||||
|
Arduino pin D12 (SCL) to MS430 pin SCL
|
||||||
|
Arduino pin D11 (SDA) to MS430 pin SDA
|
||||||
|
Arduino pin VCC (3.3V) to MS430 pins VPU and VDD
|
||||||
|
MS430 pin VIN is unused
|
||||||
|
|
||||||
|
If a PPD42 particle sensor is used, connect the following:
|
||||||
|
Arduino pin 5V to PPD42 pin 3
|
||||||
|
Arduino pin GND to PPD42 pin 1
|
||||||
|
PPD42 pin 4 to MS430 pin PRT
|
||||||
|
|
||||||
|
If an SDS011 particle sensor is used, connect the following:
|
||||||
|
Arduino pin 5V to SDS011 pin "5V"
|
||||||
|
Arduino pin GND to SDS011 pin "GND"
|
||||||
|
SDS011 pin "25um" to MS430 pin PRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#elif defined ESP8266
|
||||||
|
|
||||||
|
// The examples have been tested on NodeMCU and Wemos D1 Mini.
|
||||||
|
// Other ESP8266 boards may require changes.
|
||||||
|
|
||||||
|
#include <ESP8266WiFi.h>
|
||||||
|
#define HAS_WIFI
|
||||||
|
#define ISR_ATTRIBUTE ICACHE_RAM_ATTR
|
||||||
|
|
||||||
|
#define SDA_PIN 5 // GPIO5 (labeled D1) connects to SDA
|
||||||
|
#define SCL_PIN 4 // GPIO4 (labeled D2) connects to SCL
|
||||||
|
#define READY_PIN 12 // GPIO12 (labeled D6) connects to RDY
|
||||||
|
#define L_INT_PIN 0 // GPIO0 (labeled D3) connects to LIT
|
||||||
|
#define S_INT_PIN 14 // GPIO14 (labeled D5) connects to SIT
|
||||||
|
/* Also make the following connections:
|
||||||
|
ESP8266 pin GND to MS430 pin GND
|
||||||
|
ESP8266 pin 3V3 to MS430 pins VPU and VDD
|
||||||
|
MS430 pin VIN is unused
|
||||||
|
|
||||||
|
If a PPD42 particle sensor is used, also connect the following:
|
||||||
|
ESP8266 pin Vin (may be labeled Vin or 5V or VU) to PPD42 pin 3
|
||||||
|
ESP8266 pin GND to PPD42 pin 1
|
||||||
|
PPD42 pin 4 to MS430 pin PRT
|
||||||
|
|
||||||
|
If an SDS011 particle sensor is used, connect the following:
|
||||||
|
ESP8266 pin Vin (may be labeled Vin or 5V or VU) to SDS011 pin "5V"
|
||||||
|
ESP8266 pin GND to SDS011 pin "GND"
|
||||||
|
SDS011 pin "25um" to MS430 pin PRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#elif defined ESP32
|
||||||
|
|
||||||
|
// The examples have been tested on DOIT ESP32 DEVKIT V1 development board.
|
||||||
|
// Other ESP32 boards may require changes.
|
||||||
|
|
||||||
|
#include <WiFi.h>
|
||||||
|
#define HAS_WIFI
|
||||||
|
#define ISR_ATTRIBUTE IRAM_ATTR
|
||||||
|
|
||||||
|
#define READY_PIN 23 // Pin D23 connects to RDY
|
||||||
|
#define L_INT_PIN 18 // Pin D18 connects to LIT
|
||||||
|
#define S_INT_PIN 19 // Pin D19 connects to SIT
|
||||||
|
/* Also make the following connections:
|
||||||
|
ESP32 pin D21 to MS430 pin SDA
|
||||||
|
ESP32 pin D22 to MS430 pin SCL
|
||||||
|
ESP32 pin GND to MS430 pin GND
|
||||||
|
ESP32 pin 3V3 to MS430 pins VPU and VDD
|
||||||
|
MS430 pin VIN is unused
|
||||||
|
|
||||||
|
If a PPD42 particle sensor is used, also connect the following:
|
||||||
|
ESP32 pin Vin to PPD42 pin 3
|
||||||
|
ESP32 pin GND to PPD42 pin 1
|
||||||
|
PPD42 pin 4 to MS430 pin PRT
|
||||||
|
|
||||||
|
If an SDS011 particle sensor is used, connect the following:
|
||||||
|
ESP32 pin Vin to SDS011 pin "5V"
|
||||||
|
ESP32 pin GND to SDS011 pin "GND"
|
||||||
|
SDS011 pin "25um" to MS430 pin PRT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#else
|
||||||
|
#error ("Your development board is not directly supported")
|
||||||
|
// Please make a new section in this file to define the correct input/output pins
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,208 @@
|
||||||
|
/*
|
||||||
|
sensor_constants.h
|
||||||
|
|
||||||
|
This file defines constant values and data structures which are used
|
||||||
|
in the control of the Metriful MS430 board and the interpretation of
|
||||||
|
its output data. All values have been taken from the MS430 datasheet.
|
||||||
|
|
||||||
|
Copyright 2020 Metriful Ltd.
|
||||||
|
Licensed under the MIT License - for further details see LICENSE.txt
|
||||||
|
|
||||||
|
For code examples, datasheet and user guide, visit
|
||||||
|
https://github.com/metriful/sensor
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SENSOR_CONSTANTS_H
|
||||||
|
#define SENSOR_CONSTANTS_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////
|
||||||
|
// Register and command addresses:
|
||||||
|
|
||||||
|
// Settings registers
|
||||||
|
#define PARTICLE_SENSOR_SELECT_REG 0x07
|
||||||
|
|
||||||
|
#define LIGHT_INTERRUPT_ENABLE_REG 0x81
|
||||||
|
#define LIGHT_INTERRUPT_THRESHOLD_REG 0x82
|
||||||
|
#define LIGHT_INTERRUPT_TYPE_REG 0x83
|
||||||
|
#define LIGHT_INTERRUPT_POLARITY_REG 0x84
|
||||||
|
|
||||||
|
#define SOUND_INTERRUPT_ENABLE_REG 0x85
|
||||||
|
#define SOUND_INTERRUPT_THRESHOLD_REG 0x86
|
||||||
|
#define SOUND_INTERRUPT_TYPE_REG 0x87
|
||||||
|
|
||||||
|
#define CYCLE_TIME_PERIOD_REG 0x89
|
||||||
|
|
||||||
|
// Executable commands
|
||||||
|
#define ON_DEMAND_MEASURE_CMD 0xE1
|
||||||
|
#define RESET_CMD 0xE2
|
||||||
|
#define CYCLE_MODE_CMD 0xE4
|
||||||
|
#define STANDBY_MODE_CMD 0xE5
|
||||||
|
#define LIGHT_INTERRUPT_CLR_CMD 0xE6
|
||||||
|
#define SOUND_INTERRUPT_CLR_CMD 0xE7
|
||||||
|
|
||||||
|
// Read the operational mode
|
||||||
|
#define OP_MODE_READ 0x8A
|
||||||
|
|
||||||
|
// Read data for whole categories
|
||||||
|
#define AIR_DATA_READ 0x10
|
||||||
|
#define AIR_QUALITY_DATA_READ 0x11
|
||||||
|
#define LIGHT_DATA_READ 0x12
|
||||||
|
#define SOUND_DATA_READ 0x13
|
||||||
|
#define PARTICLE_DATA_READ 0x14
|
||||||
|
|
||||||
|
// Read individual data quantities
|
||||||
|
#define T_READ 0x21
|
||||||
|
#define P_READ 0x22
|
||||||
|
#define H_READ 0x23
|
||||||
|
#define G_READ 0x24
|
||||||
|
|
||||||
|
#define AQI_READ 0x25
|
||||||
|
#define CO2E_READ 0x26
|
||||||
|
#define BVOC_READ 0x27
|
||||||
|
#define AQI_ACCURACY_READ 0x28
|
||||||
|
|
||||||
|
#define ILLUMINANCE_READ 0x31
|
||||||
|
#define WHITE_LIGHT_READ 0x32
|
||||||
|
|
||||||
|
#define SPL_READ 0x41
|
||||||
|
#define SPL_BANDS_READ 0x42
|
||||||
|
#define SOUND_PEAK_READ 0x43
|
||||||
|
#define SOUND_STABLE_READ 0x44
|
||||||
|
|
||||||
|
#define DUTY_CYCLE_READ 0x51
|
||||||
|
#define CONCENTRATION_READ 0x52
|
||||||
|
#define PARTICLE_VALID_READ 0x53
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// I2C address of sensor board: can select using solder bridge
|
||||||
|
#define I2C_ADDR_7BIT_SB_OPEN 0x71 // if solder bridge is left open
|
||||||
|
#define I2C_ADDR_7BIT_SB_CLOSED 0x70 // if solder bridge is soldered closed
|
||||||
|
|
||||||
|
// Values for enabling/disabling of sensor functions
|
||||||
|
#define ENABLED 1
|
||||||
|
#define DISABLED 0
|
||||||
|
|
||||||
|
// Device modes
|
||||||
|
#define STANDBY_MODE 0
|
||||||
|
#define CYCLE_MODE 1
|
||||||
|
|
||||||
|
// Sizes of data expected when setting interrupt thresholds
|
||||||
|
#define LIGHT_INTERRUPT_THRESHOLD_BYTES 3
|
||||||
|
#define SOUND_INTERRUPT_THRESHOLD_BYTES 2
|
||||||
|
|
||||||
|
// Frequency bands for sound level measurement
|
||||||
|
#define SOUND_FREQ_BANDS 6
|
||||||
|
static const uint16_t sound_band_mids_Hz[SOUND_FREQ_BANDS] = {125, 250, 500, 1000, 2000, 4000};
|
||||||
|
static const uint16_t sound_band_edges_Hz[SOUND_FREQ_BANDS+1] = {88, 177, 354, 707, 1414, 2828, 5657};
|
||||||
|
|
||||||
|
// Cycle mode time period
|
||||||
|
#define CYCLE_PERIOD_3_S 0
|
||||||
|
#define CYCLE_PERIOD_100_S 1
|
||||||
|
#define CYCLE_PERIOD_300_S 2
|
||||||
|
|
||||||
|
// Sound interrupt type:
|
||||||
|
#define SOUND_INT_TYPE_LATCH 0
|
||||||
|
#define SOUND_INT_TYPE_COMP 1
|
||||||
|
|
||||||
|
// Maximum for illuminance measurement and threshold setting
|
||||||
|
#define MAX_LUX_VALUE 3774
|
||||||
|
|
||||||
|
// Light interrupt type:
|
||||||
|
#define LIGHT_INT_TYPE_LATCH 0
|
||||||
|
#define LIGHT_INT_TYPE_COMP 1
|
||||||
|
|
||||||
|
// Light interrupt polarity:
|
||||||
|
#define LIGHT_INT_POL_POSITIVE 0
|
||||||
|
#define LIGHT_INT_POL_NEGATIVE 1
|
||||||
|
|
||||||
|
// Decoding the temperature integer.fraction value format
|
||||||
|
#define TEMPERATURE_VALUE_MASK 0x7F
|
||||||
|
#define TEMPERATURE_SIGN_MASK 0x80
|
||||||
|
|
||||||
|
// Particle sensor module selection:
|
||||||
|
#define PARTICLE_SENSOR_OFF 0
|
||||||
|
#define PARTICLE_SENSOR_PPD42 1
|
||||||
|
#define PARTICLE_SENSOR_SDS011 2
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Structs for accessing individual data quantities after reading a category of data
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t T_C_int_with_sign;
|
||||||
|
uint8_t T_C_fr_1dp;
|
||||||
|
uint32_t P_Pa;
|
||||||
|
uint8_t H_pc_int;
|
||||||
|
uint8_t H_pc_fr_1dp;
|
||||||
|
uint32_t G_ohm;
|
||||||
|
} AirData_t;
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint16_t AQI_int;
|
||||||
|
uint8_t AQI_fr_1dp;
|
||||||
|
uint16_t CO2e_int;
|
||||||
|
uint8_t CO2e_fr_1dp;
|
||||||
|
uint16_t bVOC_int;
|
||||||
|
uint8_t bVOC_fr_2dp;
|
||||||
|
uint8_t AQI_accuracy;
|
||||||
|
} AirQualityData_t;
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint16_t illum_lux_int;
|
||||||
|
uint8_t illum_lux_fr_2dp;
|
||||||
|
uint16_t white;
|
||||||
|
} LightData_t;
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t SPL_dBA_int;
|
||||||
|
uint8_t SPL_dBA_fr_1dp;
|
||||||
|
uint8_t SPL_bands_dB_int[SOUND_FREQ_BANDS];
|
||||||
|
uint8_t SPL_bands_dB_fr_1dp[SOUND_FREQ_BANDS];
|
||||||
|
uint16_t peak_amp_mPa_int;
|
||||||
|
uint8_t peak_amp_mPa_fr_2dp;
|
||||||
|
uint8_t stable;
|
||||||
|
} SoundData_t;
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t duty_cycle_pc_int;
|
||||||
|
uint8_t duty_cycle_pc_fr_2dp;
|
||||||
|
uint16_t concentration_int;
|
||||||
|
uint8_t concentration_fr_2dp;
|
||||||
|
uint8_t valid;
|
||||||
|
} ParticleData_t;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Byte lengths for each readable data quantity and data category
|
||||||
|
|
||||||
|
#define T_BYTES 2
|
||||||
|
#define P_BYTES 4
|
||||||
|
#define H_BYTES 2
|
||||||
|
#define G_BYTES 4
|
||||||
|
#define AIR_DATA_BYTES sizeof(AirData_t)
|
||||||
|
|
||||||
|
#define AQI_BYTES 3
|
||||||
|
#define CO2E_BYTES 3
|
||||||
|
#define BVOC_BYTES 3
|
||||||
|
#define AQI_ACCURACY_BYTES 1
|
||||||
|
#define AIR_QUALITY_DATA_BYTES sizeof(AirQualityData_t)
|
||||||
|
|
||||||
|
#define ILLUMINANCE_BYTES 3
|
||||||
|
#define WHITE_BYTES 2
|
||||||
|
#define LIGHT_DATA_BYTES sizeof(LightData_t)
|
||||||
|
|
||||||
|
#define SPL_BYTES 2
|
||||||
|
#define SPL_BANDS_BYTES (2*SOUND_FREQ_BANDS)
|
||||||
|
#define SOUND_PEAK_BYTES 3
|
||||||
|
#define SOUND_STABLE_BYTES 1
|
||||||
|
#define SOUND_DATA_BYTES sizeof(SoundData_t)
|
||||||
|
|
||||||
|
#define DUTY_CYCLE_BYTES 2
|
||||||
|
#define CONCENTRATION_BYTES 3
|
||||||
|
#define PARTICLE_VALID_BYTES 1
|
||||||
|
#define PARTICLE_DATA_BYTES sizeof(ParticleData_t)
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,46 @@
|
||||||
|
|
||||||
|
This directory is intended for project specific (private) libraries.
|
||||||
|
PlatformIO will compile them to static libraries and link into executable file.
|
||||||
|
|
||||||
|
The source code of each library should be placed in a an own separate directory
|
||||||
|
("lib/your_library_name/[here are source files]").
|
||||||
|
|
||||||
|
For example, see a structure of the following two libraries `Foo` and `Bar`:
|
||||||
|
|
||||||
|
|--lib
|
||||||
|
| |
|
||||||
|
| |--Bar
|
||||||
|
| | |--docs
|
||||||
|
| | |--examples
|
||||||
|
| | |--src
|
||||||
|
| | |- Bar.c
|
||||||
|
| | |- Bar.h
|
||||||
|
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||||
|
| |
|
||||||
|
| |--Foo
|
||||||
|
| | |- Foo.c
|
||||||
|
| | |- Foo.h
|
||||||
|
| |
|
||||||
|
| |- README --> THIS FILE
|
||||||
|
|
|
||||||
|
|- platformio.ini
|
||||||
|
|--src
|
||||||
|
|- main.c
|
||||||
|
|
||||||
|
and a contents of `src/main.c`:
|
||||||
|
```
|
||||||
|
#include <Foo.h>
|
||||||
|
#include <Bar.h>
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
PlatformIO Library Dependency Finder will find automatically dependent
|
||||||
|
libraries scanning project source files.
|
||||||
|
|
||||||
|
More information about PlatformIO Library Dependency Finder
|
||||||
|
- https://docs.platformio.org/page/librarymanager/ldf.html
|
|
@ -0,0 +1,31 @@
|
||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env]
|
||||||
|
monitor_speed = 9600
|
||||||
|
|
||||||
|
[env:nano_33_iot]
|
||||||
|
platform = atmelsam
|
||||||
|
board = nano_33_iot
|
||||||
|
framework = arduino
|
||||||
|
|
||||||
|
lib_deps =
|
||||||
|
WiFiNINA
|
||||||
|
MQTT
|
||||||
|
|
||||||
|
[env:d1_mini]
|
||||||
|
platform = espressif8266
|
||||||
|
board = d1_mini
|
||||||
|
framework = arduino
|
||||||
|
upload_protocol = esptool
|
||||||
|
|
||||||
|
lib_deps =
|
||||||
|
MQTT
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
#include <Metriful_sensor.h>
|
||||||
|
|
||||||
|
// How often to read and report the data (every 3, 100 or 300 seconds)
|
||||||
|
#define CYCLE_PERIOD = CYCLE_PERIOD_3_S
|
||||||
|
|
||||||
|
// The details of the WiFi network:
|
||||||
|
#define WIFI_SSID = "..." // network SSID (name, case sensitive)
|
||||||
|
#define WIFI_PASSWORD = "..." // network password
|
||||||
|
|
||||||
|
// Home Assistant settings
|
||||||
|
|
||||||
|
// You must have already installed Home Assistant on a computer on your
|
||||||
|
// network. Go to www.home-assistant.io for help on this.
|
||||||
|
|
||||||
|
// Choose a unique name for this MS430 sensor board so you can identify it.
|
||||||
|
// Variables in HA will have names like: SENSOR_NAME.temperature, etc.
|
||||||
|
#define SENSOR_NAME "metriful_living_room"
|
||||||
|
|
||||||
|
// Change this to the IP address of the computer running Home Assistant.
|
||||||
|
// You can find this from the admin interface of your router.
|
||||||
|
#define HOME_ASSISTANT_IP "..."
|
||||||
|
|
||||||
|
// Security access token: the Readme and User Guide explain how to get this
|
||||||
|
#define LONG_LIVED_ACCESS_TOKEN "..."
|
|
@ -0,0 +1,178 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Metriful_sensor.h>
|
||||||
|
#include <WiFi_functions.h>
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#if !defined(HAS_WIFI)
|
||||||
|
#error ("This example program has been created for specific WiFi enabled hosts only.")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
WiFiClient client;
|
||||||
|
|
||||||
|
// Buffers for assembling http POST requests
|
||||||
|
char postBuffer[450] = {0};
|
||||||
|
char fieldBuffer[70] = {0};
|
||||||
|
|
||||||
|
// Structs for data
|
||||||
|
AirData_t airData = {0};
|
||||||
|
AirQualityData_t airQualityData = {0};
|
||||||
|
LightData_t lightData = {0};
|
||||||
|
ParticleData_t particleData = {0};
|
||||||
|
SoundData_t soundData = {0};
|
||||||
|
|
||||||
|
// Define the display attributes of data sent to Home Assistant.
|
||||||
|
// The chosen name, unit and icon will appear in on the overview
|
||||||
|
// dashboard in Home Assistant. The icons can be chosen from
|
||||||
|
// https://cdn.materialdesignicons.com/5.3.45/
|
||||||
|
// (remove the "mdi-" part from the icon name).
|
||||||
|
// The attribute fields are: {name, unit, icon, decimal places}
|
||||||
|
HA_Attributes_t pressure = {"Pressure","Pa","weather-cloudy",0};
|
||||||
|
HA_Attributes_t humidity = {"Humidity","%","water-percent",1};
|
||||||
|
HA_Attributes_t illuminance = {"Illuminance","lx","white-balance-sunny",2};
|
||||||
|
HA_Attributes_t soundLevel = {"Sound level","dBA","microphone",1};
|
||||||
|
HA_Attributes_t peakAmplitude = {"Sound peak","mPa","waveform",2};
|
||||||
|
HA_Attributes_t estimatedCO2 = {"Estimated CO2","ppm","chart-bubble",1};
|
||||||
|
HA_Attributes_t equivalentBreathVOC = {"Equivalent breath VOC","ppm","chart-bubble",2};
|
||||||
|
HA_Attributes_t AQI = {"Air Quality Index"," ","thought-bubble-outline",1};
|
||||||
|
HA_Attributes_t AQ_assessment = {"Air quality assessment","","flower-tulip",0};
|
||||||
|
HA_Attributes_t AQ_calibration = {"Air quality calibration","","flower-tulip",0};
|
||||||
|
#if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42)
|
||||||
|
HA_Attributes_t particulates = {"Particle concentration","ppL","chart-bubble",0};
|
||||||
|
#else
|
||||||
|
HA_Attributes_t particulates = {"Particle concentration",SDS011_UNIT_SYMBOL,"chart-bubble",2};
|
||||||
|
#endif
|
||||||
|
#ifdef USE_FAHRENHEIT
|
||||||
|
HA_Attributes_t temperature = {"Temperature",FAHRENHEIT_SYMBOL,"thermometer",1};
|
||||||
|
#else
|
||||||
|
HA_Attributes_t temperature = {"Temperature",CELSIUS_SYMBOL,"thermometer",1};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
// Initialize the host's pins, set up the serial port and reset:
|
||||||
|
SensorHardwareSetup(I2C_ADDRESS);
|
||||||
|
|
||||||
|
connectToWiFi(WIFI_SSID, WIFI_PASSWORD);
|
||||||
|
|
||||||
|
// Apply settings to the MS430 and enter cycle mode
|
||||||
|
uint8_t particleSensorCode = PARTICLE_SENSOR;
|
||||||
|
uint8_t cycle_period = CYCLE_PERIOD;
|
||||||
|
TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensorCode, 1);
|
||||||
|
TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1);
|
||||||
|
ready_assertion_event = false;
|
||||||
|
TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the data to Home Assistant as an HTTP POST request.
|
||||||
|
void http_POST_Home_Assistant(const HA_Attributes_t * attributes, const char * valueText) {
|
||||||
|
client.stop();
|
||||||
|
if (client.connect(HOME_ASSISTANT_IP, 8123)) {
|
||||||
|
// Form the URL from the name but replace spaces with underscores
|
||||||
|
strcpy(fieldBuffer,attributes->name);
|
||||||
|
for (uint8_t i=0; i<strlen(fieldBuffer); i++) {
|
||||||
|
if (fieldBuffer[i] == ' ') {
|
||||||
|
fieldBuffer[i] = '_';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sprintf(postBuffer,"POST /api/states/" SENSOR_NAME ".%s HTTP/1.1", fieldBuffer);
|
||||||
|
client.println(postBuffer);
|
||||||
|
client.println("Host: " HOME_ASSISTANT_IP ":8123");
|
||||||
|
client.println("Content-Type: application/json");
|
||||||
|
client.println("Authorization: Bearer " LONG_LIVED_ACCESS_TOKEN);
|
||||||
|
|
||||||
|
// Assemble the JSON content string:
|
||||||
|
sprintf(postBuffer,"{\"state\":%s,\"attributes\":{\"unit_of_measurement\""
|
||||||
|
":\"%s\",\"friendly_name\":\"%s\",\"icon\":\"mdi:%s\"}}",
|
||||||
|
valueText, attributes->unit, attributes->name, attributes->icon);
|
||||||
|
|
||||||
|
sprintf(fieldBuffer,"Content-Length: %u", strlen(postBuffer));
|
||||||
|
client.println(fieldBuffer);
|
||||||
|
client.println();
|
||||||
|
client.print(postBuffer);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Serial.println("Client connection failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send numeric data with specified sign, integer and fractional parts
|
||||||
|
void sendNumericData(const HA_Attributes_t * attributes, uint32_t valueInteger,
|
||||||
|
uint8_t valueDecimal, bool isPositive) {
|
||||||
|
char valueText[20] = {0};
|
||||||
|
const char * sign = isPositive ? "" : "-";
|
||||||
|
switch (attributes->decimalPlaces) {
|
||||||
|
case 0:
|
||||||
|
default:
|
||||||
|
sprintf(valueText,"%s%" PRIu32, sign, valueInteger);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
sprintf(valueText,"%s%" PRIu32 ".%u", sign, valueInteger, valueDecimal);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
sprintf(valueText,"%s%" PRIu32 ".%02u", sign, valueInteger, valueDecimal);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
http_POST_Home_Assistant(attributes, valueText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a text string: must have quotation marks added
|
||||||
|
void sendTextData(const HA_Attributes_t * attributes, const char * valueText) {
|
||||||
|
char quotedText[20] = {0};
|
||||||
|
sprintf(quotedText,"\"%s\"", valueText);
|
||||||
|
http_POST_Home_Assistant(attributes, quotedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
|
||||||
|
// Wait for the next new data release, indicated by a falling edge on READY
|
||||||
|
Serial.println("Waiting for new data ...");
|
||||||
|
while (!ready_assertion_event) {
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
ready_assertion_event = false;
|
||||||
|
|
||||||
|
// Read data from the MS430 into the data structs.
|
||||||
|
Serial.println("Reading data from sensors");
|
||||||
|
ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES);
|
||||||
|
ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES);
|
||||||
|
ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES);
|
||||||
|
ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES);
|
||||||
|
ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES);
|
||||||
|
|
||||||
|
// Check that WiFi is still connected
|
||||||
|
uint8_t wifiStatus = WiFi.status();
|
||||||
|
if (wifiStatus != WL_CONNECTED) {
|
||||||
|
// There is a problem with the WiFi connection: attempt to reconnect.
|
||||||
|
Serial.print("Wifi status: ");
|
||||||
|
Serial.println(interpret_WiFi_status(wifiStatus));
|
||||||
|
connectToWiFi(WIFI_SSID, WIFI_PASSWORD);
|
||||||
|
ready_assertion_event = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t T_intPart = 0;
|
||||||
|
uint8_t T_fractionalPart = 0;
|
||||||
|
bool isPositive = true;
|
||||||
|
getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive);
|
||||||
|
|
||||||
|
// Send data to Home Assistant
|
||||||
|
Serial.println("Send data to Home Assistant");
|
||||||
|
sendNumericData(&temperature, (uint32_t) T_intPart, T_fractionalPart, isPositive);
|
||||||
|
sendNumericData(&pressure, (uint32_t) airData.P_Pa, 0, true);
|
||||||
|
sendNumericData(&humidity, (uint32_t) airData.H_pc_int, airData.H_pc_fr_1dp, true);
|
||||||
|
sendNumericData(&illuminance, (uint32_t) lightData.illum_lux_int, lightData.illum_lux_fr_2dp, true);
|
||||||
|
sendNumericData(&soundLevel, (uint32_t) soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp, true);
|
||||||
|
sendNumericData(&peakAmplitude, (uint32_t) soundData.peak_amp_mPa_int,
|
||||||
|
soundData.peak_amp_mPa_fr_2dp, true);
|
||||||
|
sendNumericData(&AQI, (uint32_t) airQualityData.AQI_int, airQualityData.AQI_fr_1dp, true);
|
||||||
|
if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) {
|
||||||
|
sendNumericData(&particulates, (uint32_t) particleData.concentration_int,
|
||||||
|
particleData.concentration_fr_2dp, true);
|
||||||
|
}
|
||||||
|
sendTextData(&AQ_assessment, interpret_AQI_value(airQualityData.AQI_int));
|
||||||
|
sendNumericData(&estimatedCO2, (uint32_t) airQualityData.CO2e_int, airQualityData.CO2e_fr_1dp, true);
|
||||||
|
sendNumericData(&equivalentBreathVOC, (uint32_t) airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp, true);
|
||||||
|
sendNumericData(&AQ_calibration, airQualityData.AQI_accuracy, 0, true);
|
||||||
|
|
||||||
|
printAirData(&airData, false);
|
||||||
|
printParticleData(&particleData, false, PARTICLE_SENSOR);
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
This directory is intended for PlatformIO Unit Testing and project tests.
|
||||||
|
|
||||||
|
Unit Testing is a software testing method by which individual units of
|
||||||
|
source code, sets of one or more MCU program modules together with associated
|
||||||
|
control data, usage procedures, and operating procedures, are tested to
|
||||||
|
determine whether they are fit for use. Unit testing finds problems early
|
||||||
|
in the development cycle.
|
||||||
|
|
||||||
|
More information about PlatformIO Unit Testing:
|
||||||
|
- https://docs.platformio.org/page/plus/unit-testing.html
|
Loading…
Reference in New Issue