Initial import.

This commit is contained in:
Maurice Makaay 2021-01-07 11:57:11 +01:00
commit c7430f56db
27 changed files with 4242 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
src/config.h

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
]
}

39
include/README Normal file
View File

@ -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

View File

@ -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.");
}
}

View File

@ -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.");
}
}

View File

@ -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.");
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}
}
}

View File

@ -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.
}

View File

@ -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.
}

View File

@ -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>");
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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

46
lib/README Normal file
View File

@ -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

31
platformio.ini Normal file
View File

@ -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

24
src/config.h-example Normal file
View File

@ -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 "..."

178
src/main.cpp Normal file
View File

@ -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);
}

11
test/README Normal file
View File

@ -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