diff --git a/Arduino/Examples/Home_Assistant/Home_Assistant.ino b/Arduino/Examples/Home_Assistant/Home_Assistant.ino new file mode 100644 index 0000000..651dbfe --- /dev/null +++ b/Arduino/Examples/Home_Assistant/Home_Assistant.ino @@ -0,0 +1,218 @@ +/* + Home_Assistant.ino + + Example code for sending environment data from the Metriful MS430 to + an installation of Home Assistant on your local WiFi network. + For more information, visit www.home-assistant.io + + This example is designed for the following WiFi enabled hosts: + * Arduino Nano 33 IoT + * Arduino MKR WiFi 1010 + * ESP8266 boards (e.g. Wemos D1, NodeMCU) + * ESP32 boards (e.g. DOIT DevKit v1) + + Data are sent at regular intervals over your WiFi network to Home + Assistant and can be viewed on the dashboard or used to control + home automation tasks. More setup information is provided in the + Readme and User Guide. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// How often to read and report the data (every 3, 100 or 300 seconds) +uint8_t cycle_period = CYCLE_PERIOD_100_S; + +// The details of the WiFi network: +char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name) +char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password + +// Home Assistant settings + +// You must have already installed Home Assistant on a computer on your +// network. Go to www.home-assistant.io for help on this. + +// Choose a unique name for this MS430 sensor board so you can identify it. +// Variables in HA will have names like: SENSOR_NAME.temperature, etc. +#define SENSOR_NAME "kitchen3" + +// Change this to the IP address of the computer running Home Assistant. +// You can find this from the admin interface of your router. +#define HOME_ASSISTANT_IP "192.168.43.144" + +// Security access token: the Readme and User Guide explain how to get this +#define LONG_LIVED_ACCESS_TOKEN "PASTE YOUR TOKEN HERE WITHIN QUOTES" + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +#if !defined(HAS_WIFI) +#error ("This example program has been created for specific WiFi enabled hosts only.") +#endif + +WiFiClient client; + +// Buffers for assembling http POST requests +char postBuffer[450] = {0}; +char fieldBuffer[70] = {0}; + +// Structs for data +AirData_t airData = {0}; +AirQualityData_t airQualityData = {0}; +LightData_t lightData = {0}; +ParticleData_t particleData = {0}; +SoundData_t soundData = {0}; + +// Define the display attributes of data sent to Home Assistant. +// The chosen name, unit and icon will appear in on the overview +// dashboard in Home Assistant. The icons can be chosen from +// https://cdn.materialdesignicons.com/5.3.45/ +// (remove the "mdi-" part from the icon name). +// The attribute fields are: {name, unit, icon, decimal places} +HA_Attributes_t pressure = {"Pressure","Pa","weather-cloudy",0}; +HA_Attributes_t humidity = {"Humidity","%","water-percent",1}; +HA_Attributes_t illuminance = {"Illuminance","lx","white-balance-sunny",2}; +HA_Attributes_t soundLevel = {"Sound level","dBA","microphone",1}; +HA_Attributes_t peakAmplitude = {"Sound peak","mPa","waveform",2}; +HA_Attributes_t AQI = {"Air Quality Index"," ","thought-bubble-outline",1}; +HA_Attributes_t AQ_assessment = {"Air quality assessment","","flower-tulip",0}; +#if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42) + HA_Attributes_t particulates = {"Particle concentration","ppL","chart-bubble",0}; +#else + HA_Attributes_t particulates = {"Particle concentration",SDS011_UNIT_SYMBOL,"chart-bubble",2}; +#endif +#ifdef USE_FAHRENHEIT + HA_Attributes_t temperature = {"Temperature",FAHRENHEIT_SYMBOL,"thermometer",1}; +#else + HA_Attributes_t temperature = {"Temperature",CELSIUS_SYMBOL,"thermometer",1}; +#endif + + +void setup() { + // Initialize the host's pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + connectToWiFi(SSID, password); + + // Apply settings to the MS430 and enter cycle mode + uint8_t particleSensorCode = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensorCode, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); +} + + +void loop() { + + // Wait for the next new data release, indicated by a falling edge on READY + while (!ready_assertion_event) { + yield(); + } + ready_assertion_event = false; + + // Read data from the MS430 into the data structs. + ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + + // Check that WiFi is still connected + uint8_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + // There is a problem with the WiFi connection: attempt to reconnect. + Serial.print("Wifi status: "); + Serial.println(interpret_WiFi_status(wifiStatus)); + connectToWiFi(SSID, password); + ready_assertion_event = false; + } + + uint8_t T_intPart = 0; + uint8_t T_fractionalPart = 0; + bool isPositive = true; + getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); + + // Send data to Home Assistant + sendNumericData(&temperature, (uint32_t) T_intPart, T_fractionalPart, isPositive); + sendNumericData(&pressure, (uint32_t) airData.P_Pa, 0, true); + sendNumericData(&humidity, (uint32_t) airData.H_pc_int, airData.H_pc_fr_1dp, true); + sendNumericData(&illuminance, (uint32_t) lightData.illum_lux_int, lightData.illum_lux_fr_2dp, true); + sendNumericData(&soundLevel, (uint32_t) soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp, true); + sendNumericData(&peakAmplitude, (uint32_t) soundData.peak_amp_mPa_int, + soundData.peak_amp_mPa_fr_2dp, true); + sendNumericData(&AQI, (uint32_t) airQualityData.AQI_int, airQualityData.AQI_fr_1dp, true); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + sendNumericData(&particulates, (uint32_t) particleData.concentration_int, + particleData.concentration_fr_2dp, true); + } + sendTextData(&AQ_assessment, interpret_AQI_value(airQualityData.AQI_int)); +} + +// Send numeric data with specified sign, integer and fractional parts +void sendNumericData(const HA_Attributes_t * attributes, uint32_t valueInteger, + uint8_t valueDecimal, bool isPositive) { + char valueText[20] = {0}; + const char * sign = isPositive ? "" : "-"; + switch (attributes->decimalPlaces) { + case 0: + default: + sprintf(valueText,"%s%" PRIu32, sign, valueInteger); + break; + case 1: + sprintf(valueText,"%s%" PRIu32 ".%u", sign, valueInteger, valueDecimal); + break; + case 2: + sprintf(valueText,"%s%" PRIu32 ".%02u", sign, valueInteger, valueDecimal); + break; + } + http_POST_Home_Assistant(attributes, valueText); +} + +// Send a text string: must have quotation marks added +void sendTextData(const HA_Attributes_t * attributes, const char * valueText) { + char quotedText[20] = {0}; + sprintf(quotedText,"\"%s\"", valueText); + http_POST_Home_Assistant(attributes, quotedText); +} + +// Send the data to Home Assistant as an HTTP POST request. +void http_POST_Home_Assistant(const HA_Attributes_t * attributes, const char * valueText) { + client.stop(); + if (client.connect(HOME_ASSISTANT_IP, 8123)) { + // Form the URL from the name but replace spaces with underscores + strcpy(fieldBuffer,attributes->name); + for (uint8_t i=0; iunit, attributes->name, attributes->icon); + + sprintf(fieldBuffer,"Content-Length: %u", strlen(postBuffer)); + client.println(fieldBuffer); + client.println(); + client.print(postBuffer); + } + else { + Serial.println("Client connection failed."); + } +} diff --git a/Arduino/Examples/IFTTT/IFTTT.ino b/Arduino/Examples/IFTTT/IFTTT.ino new file mode 100644 index 0000000..ff27276 --- /dev/null +++ b/Arduino/Examples/IFTTT/IFTTT.ino @@ -0,0 +1,190 @@ +/* + IFTTT.ino + + Example code for sending data from the Metriful MS430 to IFTTT.com + + This example is designed for the following WiFi enabled hosts: + * Arduino Nano 33 IoT + * Arduino MKR WiFi 1010 + * ESP8266 boards (e.g. Wemos D1, NodeMCU) + * ESP32 boards (e.g. DOIT DevKit v1) + + Environmental data values are periodically measured and compared with + a set of user-defined thresholds. If any values go outside the allowed + ranges, an HTTP POST request is sent to IFTTT.com, triggering an alert + email to your inbox, with customizable text. + This example requires a WiFi network and internet connection. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// The details of the WiFi network: +char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name) +char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password + +// Define the details of variables for monitoring. +// The seven fields are: +// {Name, measurement unit, high threshold, low threshold, +// initial inactive cycles (2), advice when high, advice when low} +ThresholdSetting_t humiditySetting = {"humidity","%",60,30,2, + "Reduce moisture sources.","Start the humidifier."}; +ThresholdSetting_t airQualitySetting = {"air quality index","",250,-1,2, + "Improve ventilation and reduce sources of VOCs.",""}; +// Change these values if Fahrenheit output temperature is selected in Metriful_sensor.h +ThresholdSetting_t temperatureSetting = {"temperature",CELSIUS_SYMBOL,24,18,2, + "Turn on the fan.","Turn on the heating."}; + +// An inactive period follows each alert, during which the same alert +// will not be generated again - this prevents too many emails/alerts. +// Choose the period as a number of readout cycles (each 5 minutes) +// e.g. for a 2 hour period, choose inactiveWaitCycles = 24 +uint16_t inactiveWaitCycles = 24; + +// IFTTT.com settings + +// You must set up a free account on IFTTT.com and create a Webhooks +// applet before using this example. This is explained further in the +// instructions in the GitHub Readme, or in the User Guide. + +#define WEBHOOKS_KEY "PASTE YOUR KEY HERE WITHIN QUOTES" +#define IFTTT_EVENT_NAME "PASTE YOUR EVENT NAME HERE WITHIN QUOTES" + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +#if !defined(HAS_WIFI) +#error ("This example program has been created for specific WiFi enabled hosts only.") +#endif + +// Measure the environment data every 300 seconds (5 minutes). This is +// adequate for long-term monitoring. +uint8_t cycle_period = CYCLE_PERIOD_300_S; + +WiFiClient client; + +// Buffers for assembling the http POST requests +char postBuffer[400] = {0}; +char fieldBuffer[120] = {0}; + +// Structs for data +AirData_t airData = {0}; +AirQualityData_t airQualityData = {0}; + + +void setup() { + // Initialize the host's pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + connectToWiFi(SSID, password); + + // Enter cycle mode + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); +} + + +void loop() { + + // Wait for the next new data release, indicated by a falling edge on READY + while (!ready_assertion_event) { + yield(); + } + ready_assertion_event = false; + + // Read the air data and air quality data + ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + + // Check that WiFi is still connected + uint8_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + // There is a problem with the WiFi connection: attempt to reconnect. + Serial.print("Wifi status: "); + Serial.println(interpret_WiFi_status(wifiStatus)); + connectToWiFi(SSID, password); + ready_assertion_event = false; + } + + // Process temperature value and convert if using Fahrenheit + float temperature = convertEncodedTemperatureToFloat(airData.T_C_int_with_sign, airData.T_C_fr_1dp); + #ifdef USE_FAHRENHEIT + temperature = convertCtoF(temperature); + #endif + + // Send an alert to IFTTT if a variable is outside the allowed range + // Just use the integer parts of values (ignore fractional parts) + checkData(&temperatureSetting, (int32_t) temperature); + checkData(&humiditySetting, (int32_t) airData.H_pc_int); + checkData(&airQualitySetting, (int32_t) airQualityData.AQI_int); +} + + +// Compare the measured value to the chosen thresholds and create an +// alert if the value is outside the allowed range. After triggering +// an alert, it cannot be re-triggered within the chosen number of cycles. +void checkData(ThresholdSetting_t * setting, int32_t value) { + + // Count down to when the monitoring is active again: + if (setting->inactiveCount > 0) { + setting->inactiveCount--; + } + + if ((value > setting->thresHigh) && (setting->inactiveCount == 0)) { + // The variable is above the high threshold + setting->inactiveCount = inactiveWaitCycles; + sendAlert(setting, value, true); + } + else if ((value < setting->thresLow) && (setting->inactiveCount == 0)) { + // The variable is below the low threshold + setting->inactiveCount = inactiveWaitCycles; + sendAlert(setting, value, false); + } +} + + +// Send an alert message to IFTTT.com as an HTTP POST request. +// isOverHighThres = true means (value > thresHigh) +// isOverHighThres = false means (value < thresLow) +void sendAlert(ThresholdSetting_t * setting, int32_t value, bool isOverHighThres) { + client.stop(); + if (client.connect("maker.ifttt.com", 80)) { + client.println("POST /trigger/" IFTTT_EVENT_NAME "/with/key/" WEBHOOKS_KEY " HTTP/1.1"); + client.println("Host: maker.ifttt.com"); + client.println("Content-Type: application/json"); + + sprintf(fieldBuffer,"The %s is too %s.", setting->variableName, + isOverHighThres ? "high" : "low"); + Serial.print("Sending new alert to IFTTT: "); + Serial.println(fieldBuffer); + + sprintf(postBuffer,"{\"value1\":\"%s\",", fieldBuffer); + + sprintf(fieldBuffer,"\"value2\":\"The measurement was %" PRId32 " %s\"", + value, setting->measurementUnit); + strcat(postBuffer, fieldBuffer); + + sprintf(fieldBuffer,",\"value3\":\"%s\"}", + isOverHighThres ? setting->adviceHigh : setting->adviceLow); + strcat(postBuffer, fieldBuffer); + + size_t len = strlen(postBuffer); + sprintf(fieldBuffer,"Content-Length: %u",len); + client.println(fieldBuffer); + client.println(); + client.print(postBuffer); + } + else { + Serial.println("Client connection failed."); + } +} diff --git a/Arduino/Examples/IoT_cloud_logging/IoT_cloud_logging.ino b/Arduino/Examples/IoT_cloud_logging/IoT_cloud_logging.ino index bee5682..60ecd79 100644 --- a/Arduino/Examples/IoT_cloud_logging/IoT_cloud_logging.ino +++ b/Arduino/Examples/IoT_cloud_logging/IoT_cloud_logging.ino @@ -6,7 +6,8 @@ This example is designed for the following WiFi enabled hosts: * Arduino Nano 33 IoT * Arduino MKR WiFi 1010 - * NodeMCU ESP8266 + * ESP8266 boards (e.g. Wemos D1, NodeMCU) + * ESP32 boards (e.g. DOIT DevKit v1) Environmental data values are measured and logged to an internet cloud account every 100 seconds, using a WiFi network. The example @@ -21,24 +22,19 @@ */ #include +#include ////////////////////////////////////////////////////////// // USER-EDITABLE SETTINGS -// How often to read and log data (every 3, 100, 300 seconds) +// How often to read and log data (every 100 or 300 seconds) // Note: due to data rate limits on free cloud services, this should // be set to 100 or 300 seconds, not 3 seconds. uint8_t cycle_period = CYCLE_PERIOD_100_S; -// The I2C address of the Metriful board -uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN; - -// Which particle sensor is attached (PPD42, SDS011, or OFF) -ParticleSensor_t particleSensor = OFF; - -// The details of the WiFi network to connect to: +// The details of the WiFi network: char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name) -char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password +char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password // IoT cloud settings // This example uses the free IoT cloud hosting services provided @@ -49,7 +45,7 @@ char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password // readme and User Guide for more information. // The chosen account's key/token must be put into the relevant define below. -#define TAGO_DEVICE_TOKEN_STRING "PASTE YOUR TOKEN HERE WITHIN QUOTES" +#define TAGO_DEVICE_TOKEN_STRING "PASTE YOUR TOKEN HERE WITHIN QUOTES" #define THINGSPEAK_API_KEY_STRING "PASTE YOUR API KEY HERE WITHIN QUOTES" // Choose which provider to use @@ -59,7 +55,7 @@ bool useTagoCloud = true; // END OF USER-EDITABLE SETTINGS ////////////////////////////////////////////////////////// -#if !defined(ARDUINO_SAMD_NANO_33_IOT) && !defined(ARDUINO_SAMD_MKRWIFI1010) && !defined(ESP8266) +#if !defined(HAS_WIFI) #error ("This example program has been created for specific WiFi enabled hosts only.") #endif @@ -67,7 +63,7 @@ WiFiClient client; // Buffers for assembling http POST requests char postBuffer[450] = {0}; -char fieldBuffer[60] = {0}; +char fieldBuffer[70] = {0}; // Structs for data AirData_t airData = {0}; @@ -76,36 +72,20 @@ LightData_t lightData = {0}; ParticleData_t particleData = {0}; SoundData_t soundData = {0}; -uint8_t transmit_buffer[1] = {0}; - - void setup() { // Initialize the host's pins, set up the serial port and reset: - SensorHardwareSetup(i2c_7bit_address); + SensorHardwareSetup(I2C_ADDRESS); - // Attempt to connect to the Wifi network: - Serial.print("Connecting to "); - Serial.println(SSID); - WiFi.begin(SSID, password); - while (WiFi.status() != WL_CONNECTED) { - delay(500); - Serial.print("."); - } - Serial.println("Connected."); - - //////////////////////////////////////////////////////////////////// + connectToWiFi(SSID, password); // Apply chosen settings to the MS430 - if (particleSensor != OFF) { - transmit_buffer[0] = particleSensor; - TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); - } - transmit_buffer[0] = cycle_period; - TransmitI2C(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1); + uint8_t particleSensor = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); // Enter cycle mode ready_assertion_event = false; - TransmitI2C(i2c_7bit_address, CYCLE_MODE_CMD, 0, 0); + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); } @@ -125,48 +105,56 @@ void loop() { */ // Air data - ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + // Choose output temperature unit (C or F) in Metriful_sensor.h + ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); /* Air quality data The initial self-calibration of the air quality data may take several minutes to complete. During this time the accuracy parameter is zero and the data values are not valid. */ - ReceiveI2C(i2c_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); // Light data - ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); // Sound data - ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); /* Particle data This requires the connection of a particulate sensor (invalid values will be obtained if this sensor is not present). + Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h Also note that, due to the low pass filtering used, the particle data become valid after an initial initialization period of approximately one minute. */ - if (particleSensor != OFF) { - ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); } - - if (WiFi.status() != WL_CONNECTED) { - Serial.println("Wifi connection lost: attempting to reconnect."); - WiFi.begin(SSID, password); + + // Check that WiFi is still connected + uint8_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + // There is a problem with the WiFi connection: attempt to reconnect. + Serial.print("Wifi status: "); + Serial.println(interpret_WiFi_status(wifiStatus)); + connectToWiFi(SSID, password); + ready_assertion_event = false; + } + + // Send data to the cloud + if (useTagoCloud) { + http_POST_data_Tago_cloud(); } else { - if (useTagoCloud) { - http_POST_data_Tago_cloud(); - } - else { - http_POST_data_Thingspeak_cloud(); - } + http_POST_data_Thingspeak_cloud(); } } + /* For both example cloud providers, the following quantities will be sent: -1 Temperature/C +1 Temperature (C or F) 2 Pressure/Pa 3 Humidity/% 4 Air quality index @@ -183,93 +171,86 @@ void loop() { // Assemble the data into the required format, then send it to the // Tago.io cloud as an HTTP POST request. void http_POST_data_Tago_cloud(void) { + client.stop(); if (client.connect("api.tago.io", 80)) { client.println("POST /data HTTP/1.1"); client.println("Host: api.tago.io"); client.println("Content-Type: application/json"); client.println("Device-Token: " TAGO_DEVICE_TOKEN_STRING); - - uint8_t T_positive_integer = airData.T_C_int_with_sign & TEMPERATURE_VALUE_MASK; - // If the most-significant bit is set, the temperature is negative (below 0 C) - if ((airData.T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) { - // The bit is set: celsius temperature is negative - sprintf(postBuffer,"[{\"variable\":\"temperature\",\"value\":-%u.%u}", - T_positive_integer, airData.T_C_fr_1dp); - } - else { - // The bit is not set: celsius temperature is positive - sprintf(postBuffer,"[{\"variable\":\"temperature\",\"value\":%u.%u}", - T_positive_integer, airData.T_C_fr_1dp); - } - sprintf(fieldBuffer,",{\"variable\":\"pressure\",\"value\":%lu}", airData.P_Pa); + uint8_t T_intPart = 0; + uint8_t T_fractionalPart = 0; + bool isPositive = true; + getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); + sprintf(postBuffer,"[{\"variable\":\"temperature\",\"value\":%s%u.%u}", + isPositive?"":"-", T_intPart, T_fractionalPart); + + sprintf(fieldBuffer,",{\"variable\":\"pressure\",\"value\":%" PRIu32 "}", airData.P_Pa); strcat(postBuffer, fieldBuffer); sprintf(fieldBuffer,",{\"variable\":\"humidity\",\"value\":%u.%u}", - airData.H_pc_int, airData.H_pc_fr_1dp); + airData.H_pc_int, airData.H_pc_fr_1dp); strcat(postBuffer, fieldBuffer); sprintf(fieldBuffer,",{\"variable\":\"aqi\",\"value\":%u.%u}", - airQualityData.AQI_int, airQualityData.AQI_fr_1dp); + airQualityData.AQI_int, airQualityData.AQI_fr_1dp); strcat(postBuffer, fieldBuffer); sprintf(fieldBuffer,",{\"variable\":\"aqi_string\",\"value\":\"%s\"}", - interpret_AQI_value(airQualityData.AQI_int)); + interpret_AQI_value(airQualityData.AQI_int)); strcat(postBuffer, fieldBuffer); sprintf(fieldBuffer,",{\"variable\":\"bvoc\",\"value\":%u.%02u}", - airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); + airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); strcat(postBuffer, fieldBuffer); sprintf(fieldBuffer,",{\"variable\":\"spl\",\"value\":%u.%u}", - soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); + soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); strcat(postBuffer, fieldBuffer); sprintf(fieldBuffer,",{\"variable\":\"peak_amp\",\"value\":%u.%02u}", - soundData.peak_amp_mPa_int, soundData.peak_amp_mPa_fr_2dp); + soundData.peak_amp_mPa_int, soundData.peak_amp_mPa_fr_2dp); strcat(postBuffer, fieldBuffer); sprintf(fieldBuffer,",{\"variable\":\"particulates\",\"value\":%u.%02u}", - particleData.concentration_int, particleData.concentration_fr_2dp); + particleData.concentration_int, particleData.concentration_fr_2dp); strcat(postBuffer, fieldBuffer); sprintf(fieldBuffer,",{\"variable\":\"illuminance\",\"value\":%u.%02u}]", - lightData.illum_lux_int, lightData.illum_lux_fr_2dp); + lightData.illum_lux_int, lightData.illum_lux_fr_2dp); strcat(postBuffer, fieldBuffer); - int len = strlen(postBuffer); + size_t len = strlen(postBuffer); sprintf(fieldBuffer,"Content-Length: %u",len); - client.println(fieldBuffer); - client.println(); + client.println(fieldBuffer); + client.println(); client.print(postBuffer); } else { - Serial.println("Connection failed"); + Serial.println("Client connection failed."); } } + // Assemble the data into the required format, then send it to the // Thingspeak.com cloud as an HTTP POST request. void http_POST_data_Thingspeak_cloud(void) { - if (client.connect("api.thingspeak.com", 80)) { + client.stop(); + if (client.connect("api.thingspeak.com", 80)) { client.println("POST /update HTTP/1.1"); client.println("Host: api.thingspeak.com"); client.println("Content-Type: application/x-www-form-urlencoded"); - uint8_t T_positive_integer = airData.T_C_int_with_sign & TEMPERATURE_VALUE_MASK; - // If the most-significant bit is set, the temperature is negative (below 0 C) - if ((airData.T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) { - // The bit is set: celsius temperature is negative - sprintf(postBuffer,"api_key=" THINGSPEAK_API_KEY_STRING "&field1=-%u.%u", - T_positive_integer, airData.T_C_fr_1dp); - } - else { - // The bit is not set: celsius temperature is positive - sprintf(postBuffer,"api_key=" THINGSPEAK_API_KEY_STRING "&field1=%u.%u", - T_positive_integer, airData.T_C_fr_1dp); - } + strcpy(postBuffer,"api_key=" THINGSPEAK_API_KEY_STRING); + + uint8_t T_intPart = 0; + uint8_t T_fractionalPart = 0; + bool isPositive = true; + getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); + sprintf(fieldBuffer,"&field1=%s%u.%u", isPositive?"":"-", T_intPart, T_fractionalPart); + strcat(postBuffer, fieldBuffer); - sprintf(fieldBuffer,"&field2=%lu", airData.P_Pa); + sprintf(fieldBuffer,"&field2=%" PRIu32, airData.P_Pa); strcat(postBuffer, fieldBuffer); sprintf(fieldBuffer,"&field3=%u.%u", airData.H_pc_int, airData.H_pc_fr_1dp); @@ -291,13 +272,13 @@ void http_POST_data_Thingspeak_cloud(void) { particleData.concentration_fr_2dp); strcat(postBuffer, fieldBuffer); - int len = strlen(postBuffer); + size_t len = strlen(postBuffer); sprintf(fieldBuffer,"Content-Length: %u",len); client.println(fieldBuffer); client.println(); client.print(postBuffer); } else { - Serial.println("Connection failed"); + Serial.println("Client connection failed."); } } diff --git a/Arduino/Examples/cycle_readout/cycle_readout.ino b/Arduino/Examples/cycle_readout/cycle_readout.ino index 40ddc38..9f53595 100644 --- a/Arduino/Examples/cycle_readout/cycle_readout.ino +++ b/Arduino/Examples/cycle_readout/cycle_readout.ino @@ -7,6 +7,9 @@ a repeating cycle. User can choose from a cycle time period of 3, 100, or 300 seconds. View the output in the Serial Monitor. + The measurements can be displayed as either labeled text, or as + simple columns of numbers. + Copyright 2020 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt @@ -19,15 +22,9 @@ ////////////////////////////////////////////////////////// // USER-EDITABLE SETTINGS -// How often to read data (every 3, 100, 300 seconds) +// How often to read data (every 3, 100, or 300 seconds) uint8_t cycle_period = CYCLE_PERIOD_3_S; -// The I2C address of the Metriful board -uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN; - -// Which particle sensor is attached (PPD42, SDS011, or OFF) -ParticleSensor_t particleSensor = OFF; - // How to print the data over the serial port. If printDataAsColumns = true, // data are columns of numbers, useful to copy/paste to a spreadsheet // application. Otherwise, data are printed with explanatory labels and units. @@ -36,8 +33,6 @@ bool printDataAsColumns = false; // END OF USER-EDITABLE SETTINGS ////////////////////////////////////////////////////////// -uint8_t transmit_buffer[1] = {0}; - // Structs for data AirData_t airData = {0}; AirQualityData_t airQualityData = {0}; @@ -48,15 +43,12 @@ ParticleData_t particleData = {0}; void setup() { // Initialize the host pins, set up the serial port and reset: - SensorHardwareSetup(i2c_7bit_address); + SensorHardwareSetup(I2C_ADDRESS); // Apply chosen settings to the MS430 - if (particleSensor != OFF) { - transmit_buffer[0] = particleSensor; - TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); - } - transmit_buffer[0] = cycle_period; - TransmitI2C(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1); + uint8_t particleSensor = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); // Wait for the serial port to be ready, for displaying the output while (!Serial) { @@ -65,7 +57,7 @@ void setup() { Serial.println("Entering cycle mode and waiting for data."); ready_assertion_event = false; - TransmitI2C(i2c_7bit_address, CYCLE_MODE_CMD, 0, 0); + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); } @@ -76,38 +68,35 @@ void loop() { } ready_assertion_event = false; - /* Read data from the MS430 into the data structs. - For each category of data (air, sound, etc.) a pointer to the data struct is - passed to the ReceiveI2C() function. The received byte sequence fills the data - struct in the correct order so that each field within the struct receives - the value of an environmental quantity (temperature, sound level, etc.) - */ + // Read data from the MS430 into the data structs. // Air data - ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + // Choose output temperature unit (C or F) in Metriful_sensor.h + airData = getAirData(I2C_ADDRESS); /* Air quality data The initial self-calibration of the air quality data may take several minutes to complete. During this time the accuracy parameter is zero and the data values are not valid. */ - ReceiveI2C(i2c_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + airQualityData = getAirQualityData(I2C_ADDRESS); // Light data - ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + lightData = getLightData(I2C_ADDRESS); // Sound data - ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + soundData = getSoundData(I2C_ADDRESS); /* Particle data This requires the connection of a particulate sensor (invalid values will be obtained if this sensor is not present). + Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h Also note that, due to the low pass filtering used, the particle data become valid after an initial initialization period of approximately one minute. */ - if (particleSensor != OFF) { - ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + particleData = getParticleData(I2C_ADDRESS); } // Print all data to the serial port @@ -115,8 +104,8 @@ void loop() { printAirQualityData(&airQualityData, printDataAsColumns); printLightData(&lightData, printDataAsColumns); printSoundData(&soundData, printDataAsColumns); - if (particleSensor != OFF) { - printParticleData(&particleData, printDataAsColumns, particleSensor); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR); } Serial.println(); } diff --git a/Arduino/Examples/graph_web_server/graph_web_server.ino b/Arduino/Examples/graph_web_server/graph_web_server.ino new file mode 100644 index 0000000..5ff48a6 --- /dev/null +++ b/Arduino/Examples/graph_web_server/graph_web_server.ino @@ -0,0 +1,366 @@ +/* + graph_web_server.ino + + Serve a web page over a WiFi network, displaying graphs showing + environment data read from the Metriful MS430. A CSV data file is + also downloadable from the page. + + This example is designed for the following WiFi enabled hosts: + * Arduino Nano 33 IoT + * Arduino MKR WiFi 1010 + * ESP8266 boards (e.g. Wemos D1, NodeMCU) + * ESP32 boards (e.g. DOIT DevKit v1) + + The host can either connect to an existing WiFi network, or generate + its own for other devices to connect to (Access Point mode). + + The browser which views the web page uses the Plotly javascript + library to generate the graphs. This is automatically downloaded + over the internet, or can be cached for offline use. If it is not + available, graphs will not appear but text data and CSV downloads + should still work. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include +#include +#include + +////////////////////////////////////////////////////////// +// USER-EDITABLE SETTINGS + +// Choose how often to read and update data (every 3, 100, or 300 seconds) +// 100 or 300 seconds are recommended for long-term monitoring. +uint8_t cycle_period = CYCLE_PERIOD_100_S; + +// The BUFFER_LENGTH parameter is the number of data points of each +// variable to store on the host. It is limited by the available host RAM. +#define BUFFER_LENGTH 576 +// Examples: +// For 16 hour graphs, choose 100 second cycle period and 576 buffer length +// For 24 hour graphs, choose 300 second cycle period and 288 buffer length + +// Choose whether to create a new WiFi network (host as Access Point), +// or connect to an existing WiFi network. +bool createWifiNetwork = true; +// If creating a WiFi network, a static (fixed) IP address ("theIP") is +// specified by the user. Otherwise, if connecting to an existing +// network, an IP address is automatically allocated and the serial +// output must be viewed at startup to see this allocated IP address. + +// Provide the SSID (name) and password for the WiFi network. Depending +// on the choice of createWifiNetwork, this is either created by the +// host (Access Point mode) or already exists. +// To avoid problems, do not create a network with the same SSID name +// as an already existing network. +char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name) +char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password; must be at least 8 characters + +// Choose a static IP address for the host, only used when generating +// a new WiFi network (createWifiNetwork = true). The served web +// page will be available at http:// +IPAddress theIP(192, 168, 12, 20); +// e.g. theIP(192, 168, 12, 20) means an IP of 192.168.12.20 +// and the web page will be at http://192.168.12.20 + +// END OF USER-EDITABLE SETTINGS +////////////////////////////////////////////////////////// + +#if !defined(HAS_WIFI) +#error ("This example program has been created for specific WiFi enabled hosts only.") +#endif + +WiFiServer server(80); +uint16_t dataPeriod_s; + +// Structs for data +AirData_F_t airDataF = {0}; +AirQualityData_F_t airQualityDataF = {0}; +LightData_F_t lightDataF = {0}; +ParticleData_F_t particleDataF = {0}; +SoundData_F_t soundDataF = {0}; + +const char * errorResponseHTTP = "HTTP/1.1 400 Bad Request\r\n\r\n"; + +const char * dataHeader = "HTTP/1.1 200 OK\r\n" + "Content-type: application/octet-stream\r\n" + "Connection: close\r\n\r\n"; + +uint16_t bufferLength = 0; +float temperature_buffer[BUFFER_LENGTH] = {0}; +float pressure_buffer[BUFFER_LENGTH] = {0}; +float humidity_buffer[BUFFER_LENGTH] = {0}; +float AQI_buffer[BUFFER_LENGTH] = {0}; +float bVOC_buffer[BUFFER_LENGTH] = {0}; +float SPL_buffer[BUFFER_LENGTH] = {0}; +float illuminance_buffer[BUFFER_LENGTH] = {0}; +float particle_buffer[BUFFER_LENGTH] = {0}; + + +void setup() { + // Initialize the host's pins, set up the serial port and reset: + SensorHardwareSetup(I2C_ADDRESS); + + if (createWifiNetwork) { + // The host generates its own WiFi network ("Access Point") with + // a chosen static IP address + if (!createWiFiAP(SSID, password, theIP)) { + Serial.println("Failed to create access point."); + while (true) { + yield(); + } + } + } + else { + // The host connects to an existing Wifi network + + // Wait for the serial port to start because the user must be able + // to see the printed IP address in the serial monitor + while (!Serial) { + yield(); + } + + // Attempt to connect to the Wifi network and obtain the IP + // address. Because the address is not known before this point, + // a serial monitor must be used to display it to the user. + connectToWiFi(SSID, password); + theIP = WiFi.localIP(); + } + + // Print the IP address: use this address in a browser to view the + // generated web page + Serial.print("View your page at http://"); + Serial.println(theIP); + + // Start the web server + server.begin(); + + //////////////////////////////////////////////////////////////////// + + // Get time period value to send to web page + if (cycle_period == CYCLE_PERIOD_3_S) { + dataPeriod_s = 3; + } + else if (cycle_period == CYCLE_PERIOD_100_S) { + dataPeriod_s = 100; + } + else { // CYCLE_PERIOD_300_S + dataPeriod_s = 300; + } + + // Apply the chosen settings to the Metriful board + uint8_t particleSensor = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); + ready_assertion_event = false; + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); +} + +void loop() { + + // Respond to the web page client requests while waiting for new data + while (!ready_assertion_event) { + handleClientRequests(); + yield(); + } + ready_assertion_event = false; + + // Read the new data and convert to float types: + airDataF = getAirDataF(I2C_ADDRESS); + airQualityDataF = getAirQualityDataF(I2C_ADDRESS); + lightDataF = getLightDataF(I2C_ADDRESS); + soundDataF = getSoundDataF(I2C_ADDRESS); + particleDataF = getParticleDataF(I2C_ADDRESS); + + // Save the data + updateDataBuffers(); + + // Check WiFi is still connected + if (!createWifiNetwork) { + uint8_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + // There is a problem with the WiFi connection: attempt to reconnect. + Serial.print("Wifi status: "); + Serial.println(interpret_WiFi_status(wifiStatus)); + connectToWiFi(SSID, password); + theIP = WiFi.localIP(); + Serial.print("View your page at http://"); + Serial.println(theIP); + ready_assertion_event = false; + } + } +} + +// Store the data, up to a maximum length of BUFFER_LENGTH, then start +// discarding the oldest data in a FIFO scheme ("First In First Out") +void updateDataBuffers(void) { + uint16_t position = 0; + if (bufferLength == BUFFER_LENGTH) { + // Buffers are full: shift all data values along, discarding the oldest + for (uint16_t i=0; i<(BUFFER_LENGTH-1); i++) { + temperature_buffer[i] = temperature_buffer[i+1]; + pressure_buffer[i] = pressure_buffer[i+1]; + humidity_buffer[i] = humidity_buffer[i+1]; + AQI_buffer[i] = AQI_buffer[i+1]; + bVOC_buffer[i] = bVOC_buffer[i+1]; + SPL_buffer[i] = SPL_buffer[i+1]; + illuminance_buffer[i] = illuminance_buffer[i+1]; + particle_buffer[i] = particle_buffer[i+1]; + } + position = BUFFER_LENGTH-1; + } + else { + // Buffers are not yet full; keep filling them + position = bufferLength; + bufferLength++; + } + + // Save the new data in the buffers + AQI_buffer[position] = airQualityDataF.AQI; + #ifdef USE_FAHRENHEIT + temperature_buffer[position] = convertCtoF(airDataF.T_C); + #else + temperature_buffer[position] = airDataF.T_C; + #endif + pressure_buffer[position] = (float) airDataF.P_Pa; + humidity_buffer[position] = airDataF.H_pc; + SPL_buffer[position] = soundDataF.SPL_dBA; + illuminance_buffer[position] = lightDataF.illum_lux; + bVOC_buffer[position] = airQualityDataF.bVOC; + particle_buffer[position] = particleDataF.concentration; +} + + +#define GET_REQUEST_STR "GET /" +#define URI_CHARS 2 +// Send either the web page or the data in response to HTTP requests. +void handleClientRequests(void) { + // Check for incoming client requests + WiFiClient client = server.available(); + if (client) { + + uint8_t requestCount = 0; + char requestBuffer[sizeof(GET_REQUEST_STR)] = {0}; + + uint8_t uriCount = 0; + char uriBuffer[URI_CHARS] = {0}; + + while (client.connected()) { + if (client.available()) { + char c = client.read(); + + if (requestCount < (sizeof(GET_REQUEST_STR)-1)) { + // Assemble the first part of the message containing the HTTP method (GET, POST etc) + requestBuffer[requestCount] = c; + requestCount++; + } + else if (uriCount < URI_CHARS) { + // Assemble the URI, up to a fixed number of characters + uriBuffer[uriCount] = c; + uriCount++; + } + else { + // Now use the assembled method and URI to decide how to respond + if (strcmp(requestBuffer, GET_REQUEST_STR) == 0) { + // It is a GET request (no other methods are supported). + // Now check for valid URIs. + if (uriBuffer[0] == ' ') { + // The web page is requested + sendData(&client, (const uint8_t *) graphWebPage, strlen(graphWebPage)); + break; + } + else if ((uriBuffer[0] == '1') && (uriBuffer[1] == ' ')) { + // A URI of '1' indicates a request of all buffered data + sendAllData(&client); + break; + } + else if ((uriBuffer[0] == '2') && (uriBuffer[1] == ' ')) { + // A URI of '2' indicates a request of the latest data only + sendLatestData(&client); + break; + } + } + // Reaching here means that the request is not supported or is incorrect + // (not a GET request, or not a valid URI) so send an error. + client.print(errorResponseHTTP); + break; + } + } + } + #ifndef ESP8266 + client.stop(); + #endif + } +} + +// Send all buffered data in the HTTP response. Binary format ("octet-stream") +// is used, and the receiving web page uses the known order of the data to +// decode and interpret it. +void sendAllData(WiFiClient * clientPtr) { + clientPtr->print(dataHeader); + // First send the time period, so the web page knows when to do the next request + clientPtr->write((const uint8_t *) &dataPeriod_s, sizeof(uint16_t)); + // Send temperature unit and particle sensor type, combined into one byte + uint8_t codeByte = (uint8_t) PARTICLE_SENSOR; + #ifdef USE_FAHRENHEIT + codeByte = codeByte | 0x10; + #endif + clientPtr->write((const uint8_t *) &codeByte, sizeof(uint8_t)); + // Send the length of the data buffers (the number of values of each variable) + clientPtr->write((const uint8_t *) &bufferLength, sizeof(uint16_t)); + // Send the data, unless none have been read yet: + if (bufferLength > 0) { + sendData(clientPtr, (const uint8_t *) AQI_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) temperature_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) pressure_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) humidity_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) SPL_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) illuminance_buffer, bufferLength*sizeof(float)); + sendData(clientPtr, (const uint8_t *) bVOC_buffer, bufferLength*sizeof(float)); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + sendData(clientPtr, (const uint8_t *) particle_buffer, bufferLength*sizeof(float)); + } + } +} + + +// Send just the most recent value of each variable (or no data if no values +// have been read yet) +void sendLatestData(WiFiClient * clientPtr) { + clientPtr->print(dataHeader); + if (bufferLength > 0) { + uint16_t bufferPosition = bufferLength-1; + clientPtr->write((const uint8_t *) &(AQI_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(temperature_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(pressure_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(humidity_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(SPL_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(illuminance_buffer[bufferPosition]), sizeof(float)); + clientPtr->write((const uint8_t *) &(bVOC_buffer[bufferPosition]), sizeof(float)); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + clientPtr->write((const uint8_t *) &(particle_buffer[bufferPosition]), sizeof(float)); + } + } +} + + +// client.write() may fail with very large inputs, so split +// into several separate write() calls with a short delay between each. +#define MAX_DATA_BYTES 1000 +void sendData(WiFiClient * clientPtr, const uint8_t * dataPtr, size_t dataLength) { + while (dataLength > 0) { + size_t sendLength = dataLength; + if (sendLength > MAX_DATA_BYTES) { + sendLength = MAX_DATA_BYTES; + } + clientPtr->write(dataPtr, sendLength); + delay(10); + dataLength-=sendLength; + dataPtr+=sendLength; + } +} diff --git a/Arduino/Examples/interrupts/interrupts.ino b/Arduino/Examples/interrupts/interrupts.ino index 03f1190..37bab65 100644 --- a/Arduino/Examples/interrupts/interrupts.ino +++ b/Arduino/Examples/interrupts/interrupts.ino @@ -4,7 +4,7 @@ Example code for using the Metriful MS430 interrupt outputs. Light and sound interrupts are configured and the program then - waits indefinitely. When an interrupt occurs, a message prints over + waits forever. When an interrupt occurs, a message prints over the serial port, the interrupt is cleared (if set to latch type), and the program returns to waiting. View the output in the Serial Monitor. @@ -43,9 +43,6 @@ bool enableSoundInterrupts = true; uint8_t sound_int_type = SOUND_INT_TYPE_LATCH; uint16_t sound_thres_mPa = 100; -// The I2C address of the Metriful board -uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN; - // END OF USER-EDITABLE SETTINGS ////////////////////////////////////////////////////////// @@ -54,7 +51,7 @@ uint8_t transmit_buffer[1] = {0}; void setup() { // Initialize the host pins, set up the serial port and reset - SensorHardwareSetup(i2c_7bit_address); + SensorHardwareSetup(I2C_ADDRESS); // check that the chosen light threshold is a valid value if (light_int_thres_lux_i > MAX_LUX_VALUE) { @@ -74,31 +71,31 @@ void setup() { if (enableSoundInterrupts) { // Set the interrupt type (latch or comparator) transmit_buffer[0] = sound_int_type; - TransmitI2C(i2c_7bit_address, SOUND_INTERRUPT_TYPE_REG, transmit_buffer, 1); + TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_TYPE_REG, transmit_buffer, 1); // Set the threshold - setSoundInterruptThreshold(i2c_7bit_address, sound_thres_mPa); + setSoundInterruptThreshold(I2C_ADDRESS, sound_thres_mPa); // Enable the interrupt transmit_buffer[0] = ENABLED; - TransmitI2C(i2c_7bit_address, SOUND_INTERRUPT_ENABLE_REG, transmit_buffer, 1); + TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_ENABLE_REG, transmit_buffer, 1); } if (enableLightInterrupts) { // Set the interrupt type (latch or comparator) transmit_buffer[0] = light_int_type; - TransmitI2C(i2c_7bit_address, LIGHT_INTERRUPT_TYPE_REG, transmit_buffer, 1); + TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_TYPE_REG, transmit_buffer, 1); // Set the threshold - setLightInterruptThreshold(i2c_7bit_address, light_int_thres_lux_i, light_int_thres_lux_f2dp); + setLightInterruptThreshold(I2C_ADDRESS, light_int_thres_lux_i, light_int_thres_lux_f2dp); // Set the interrupt polarity transmit_buffer[0] = light_int_polarity; - TransmitI2C(i2c_7bit_address, LIGHT_INTERRUPT_POLARITY_REG, transmit_buffer, 1); + TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_POLARITY_REG, transmit_buffer, 1); // Enable the interrupt transmit_buffer[0] = ENABLED; - TransmitI2C(i2c_7bit_address, LIGHT_INTERRUPT_ENABLE_REG, transmit_buffer, 1); + TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_ENABLE_REG, transmit_buffer, 1); } // Wait for the serial port to be ready, for displaying the output @@ -118,7 +115,7 @@ void loop() { Serial.println("LIGHT INTERRUPT."); if (light_int_type == LIGHT_INT_TYPE_LATCH) { // Latch type interrupts remain set until cleared by command - TransmitI2C(i2c_7bit_address, LIGHT_INTERRUPT_CLR_CMD, 0, 0); + TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_CLR_CMD, 0, 0); } } @@ -127,7 +124,7 @@ void loop() { Serial.println("SOUND INTERRUPT."); if (sound_int_type == SOUND_INT_TYPE_LATCH) { // Latch type interrupts remain set until cleared by command - TransmitI2C(i2c_7bit_address, SOUND_INTERRUPT_CLR_CMD, 0, 0); + TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_CLR_CMD, 0, 0); } } diff --git a/Arduino/Examples/on_demand_readout/on_demand_readout.ino b/Arduino/Examples/on_demand_readout/on_demand_readout.ino index abc71c3..8509dde 100644 --- a/Arduino/Examples/on_demand_readout/on_demand_readout.ino +++ b/Arduino/Examples/on_demand_readout/on_demand_readout.ino @@ -21,26 +21,18 @@ // Pause (in milliseconds) between data measurements (note that the // measurement itself takes 0.5 seconds) -uint32_t pause_ms = 3500; +uint32_t pause_ms = 4500; // Choosing a pause of less than 2000 ms will cause inaccurate // temperature, humidity and particle data. -// The I2C address of the Metriful board -uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN; - -// Which particle sensor is attached (PPD42, SDS011, or OFF) -ParticleSensor_t particleSensor = OFF; - // How to print the data over the serial port. If printDataAsColumns = true, // data are columns of numbers, useful to copy/paste to a spreadsheet // application. Otherwise, data are printed with explanatory labels and units. -bool printDataAsColumns = true; +bool printDataAsColumns = false; // END OF USER-EDITABLE SETTINGS ////////////////////////////////////////////////////////// -uint8_t transmit_buffer[1] = {0}; - // Structs for data AirData_t airData = {0}; LightData_t lightData = {0}; @@ -50,12 +42,10 @@ ParticleData_t particleData = {0}; void setup() { // Initialize the host pins, set up the serial port and reset: - SensorHardwareSetup(i2c_7bit_address); + SensorHardwareSetup(I2C_ADDRESS); - if (particleSensor != OFF) { - transmit_buffer[0] = particleSensor; - TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); - } + uint8_t particleSensor = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); // Wait for the serial port to be ready, for displaying the output while (!Serial) { @@ -68,50 +58,48 @@ void loop() { // Trigger a new measurement ready_assertion_event = false; - TransmitI2C(i2c_7bit_address, ON_DEMAND_MEASURE_CMD, 0, 0); + TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); // Wait for the measurement to finish, indicated by a falling edge on READY while (!ready_assertion_event) { yield(); } - - /* Read data from the MS430 into the data structs. - For each category of data (air, sound, etc.) a pointer to the data struct is - passed to the ReceiveI2C() function. The received byte sequence fills the data - struct in the correct order so that each field within the struct receives - the value of an environmental quantity (temperature, sound level, etc.) - */ + + // Read data from the MS430 into the data structs. // Air data - ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + // Choose output temperature unit (C or F) in Metriful_sensor.h + airData = getAirData(I2C_ADDRESS); // Air quality data are not available with on demand measurements // Light data - ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + lightData = getLightData(I2C_ADDRESS); // Sound data - ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + soundData = getSoundData(I2C_ADDRESS); /* Particle data This requires the connection of a particulate sensor (invalid values will be obtained if this sensor is not present). + Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h Also note that, due to the low pass filtering used, the particle data become valid after an initial initialization period of approximately one minute. */ - if (particleSensor != OFF) { - ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + particleData = getParticleData(I2C_ADDRESS); } // Print all data to the serial port printAirData(&airData, printDataAsColumns); printLightData(&lightData, printDataAsColumns); printSoundData(&soundData, printDataAsColumns); - if (particleSensor != OFF) { - printParticleData(&particleData, printDataAsColumns, particleSensor); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR); } Serial.println(); + // Wait for the chosen time period before repeating everything delay(pause_ms); } diff --git a/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino b/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino index 064ce0a..d9a8722 100644 --- a/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino +++ b/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino @@ -5,14 +5,14 @@ control signal from one of the host's pins, which can be used to turn the particle sensor on and off. An external transistor circuit is also needed - this will gate the sensor power supply according to - the control signal. + the control signal. Further details are given in the User Guide. The program continually measures and displays all environment data in a repeating cycle. The user can view the output in the Serial Monitor. After reading the data, the particle sensor is powered off for a chosen number of cycles ("off_cycles"). It is then powered on and read before being powered off again. Sound data are ignored - while the particle sensor is on, to avoid fan noise. + while the particle sensor is on, to avoid its fan noise. Copyright 2020 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt @@ -31,25 +31,18 @@ // its data. uint8_t cycle_period = CYCLE_PERIOD_100_S; -// The I2C address of the Metriful board -uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN; - // How to print the data over the serial port. If printDataAsColumns = true, // data are columns of numbers, useful for transferring to a spreadsheet // application. Otherwise, data are printed with explanatory labels and units. -bool printDataAsColumns = true; - -// Which particle sensor is attached (PPD42, SDS011, or OFF) -ParticleSensor_t particleSensor = SDS011; +bool printDataAsColumns = false; // Particle sensor power control options -uint8_t off_cycles = 1; // leave the sensor off for this many cycles between reads +uint8_t off_cycles = 2; // leave the sensor off for this many cycles between reads uint8_t particle_sensor_control_pin = 10; // host pin number which outputs the control signal bool particle_sensor_ON_state = true; // particle_sensor_ON_state is the required polarity of the control -// signal; true means +V is output to turn the sensor on (use this for -// 3.3 V hosts). false means 0V is output to turn the sensor on (use -// this for 5 V hosts). The User Guide gives example switching circuits. +// signal; true means +V is output to turn the sensor on, while false +// means 0 V is output. Use true for 3.3 V hosts and false for 5 V hosts. // END OF USER-EDITABLE SETTINGS ////////////////////////////////////////////////////////// @@ -69,7 +62,7 @@ uint8_t particleSensor_count = 0; void setup() { // Initialize the host pins, set up the serial port and reset: - SensorHardwareSetup(i2c_7bit_address); + SensorHardwareSetup(I2C_ADDRESS); // Set up the particle sensor control, and turn it off initially pinMode(particle_sensor_control_pin, OUTPUT); @@ -77,12 +70,10 @@ void setup() { particleSensorIsOn = false; // Apply chosen settings to the MS430 - if (particleSensor != OFF) { - transmit_buffer[0] = particleSensor; - TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); - } + transmit_buffer[0] = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); transmit_buffer[0] = cycle_period; - TransmitI2C(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1); // Wait for the serial port to be ready, for displaying the output while (!Serial) { @@ -91,7 +82,7 @@ void setup() { Serial.println("Entering cycle mode and waiting for data."); ready_assertion_event = false; - TransmitI2C(i2c_7bit_address, CYCLE_MODE_CMD, 0, 0); + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); } @@ -110,32 +101,33 @@ void loop() { */ // Air data - ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); /* Air quality data The initial self-calibration of the air quality data may take several minutes to complete. During this time the accuracy parameter is zero and the data values are not valid. */ - ReceiveI2C(i2c_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); // Light data - ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); // Sound data - only read when particle sensor is off if (!particleSensorIsOn) { - ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); } /* Particle data This requires the connection of a particulate sensor (invalid values will be obtained if this sensor is not present). + Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h Also note that, due to the low pass filtering used, the particle data become valid after an initial initialization period of approximately one minute. */ if (particleSensorIsOn) { - ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); } // Print all data to the serial port. The previous loop's particle or @@ -144,14 +136,14 @@ void loop() { printAirQualityData(&airQualityData, printDataAsColumns); printLightData(&lightData, printDataAsColumns); printSoundData(&soundData, printDataAsColumns); - printParticleData(&particleData, printDataAsColumns, particleSensor); + printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR); Serial.println(); // Turn the particle sensor on/off if required if (particleSensorIsOn) { // Stop the particle detection on the MS430 transmit_buffer[0] = OFF; - TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); // Turn off the hardware: digitalWrite(particle_sensor_control_pin, !particle_sensor_ON_state); @@ -164,8 +156,8 @@ void loop() { digitalWrite(particle_sensor_control_pin, particle_sensor_ON_state); // Start the particle detection on the MS430 - transmit_buffer[0] = particleSensor; - TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); + transmit_buffer[0] = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); particleSensor_count = 0; particleSensorIsOn = true; diff --git a/Arduino/Examples/simple_read_T_H/simple_read_T_H.ino b/Arduino/Examples/simple_read_T_H/simple_read_T_H.ino index f1d9d97..c2ff5eb 100644 --- a/Arduino/Examples/simple_read_T_H/simple_read_T_H.ino +++ b/Arduino/Examples/simple_read_T_H/simple_read_T_H.ino @@ -4,10 +4,9 @@ Example code for using the Metriful MS430 to measure humidity and temperature. - Measures and displays the humidity and temperature, demonstrating - the decoding of the signed temperature value. The data are also - read out and displayed a second time, using a “data category read” - of all Air measurement data. View the output in the Serial Monitor. + Demonstrates multiple ways of reading and displaying the temperature + and humidity data. View the output in the Serial Monitor. The other + data can be measured and displayed in a similar way. Copyright 2020 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt @@ -18,24 +17,10 @@ #include -////////////////////////////////////////////////////////// -// USER-EDITABLE SETTINGS -// The I2C address of the Metriful board -uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN; - -// Whether to use floating point representation of numbers (uses more -// host resources) -bool useFloatingPoint = false; - -// END OF USER-EDITABLE SETTINGS -////////////////////////////////////////////////////////// - -uint8_t receive_buffer[2] = {0}; - -void setup() { +void setup() { // Initialize the host pins, set up the serial port and reset: - SensorHardwareSetup(i2c_7bit_address); + SensorHardwareSetup(I2C_ADDRESS); // Wait for the serial port to be ready, for displaying the output while (!Serial) { @@ -46,81 +31,118 @@ void setup() { ready_assertion_event = false; // Initiate an on-demand data measurement - TransmitI2C(i2c_7bit_address, ON_DEMAND_MEASURE_CMD, 0, 0); + TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); // Now wait for the ready signal before continuing while (!ready_assertion_event) { yield(); } - - // We now know that data are ready to read. + + // We know that new data are ready to read. //////////////////////////////////////////////////////////////////// - // HUMIDITY + // There are different ways to read and display the data + + // 1. Simplest way: use the example float (_F) functions + + // Read the "air data" from the MS430. This includes temperature and + // humidity as well as pressure and gas sensor data. + AirData_F_t airDataF = getAirDataF(I2C_ADDRESS); + + // Print all of the air measurements to the serial monitor + printAirDataF(&airDataF); + // Fahrenheit temperature is printed if USE_FAHRENHEIT is defined + // in "Metriful_sensor.h" + + Serial.println("-----------------------------"); - // Read the humidity value from the sensor board - ReceiveI2C(i2c_7bit_address, H_READ, receive_buffer, H_BYTES); - // Decode the humidity: the first byte is the integer part, the - // second byte is the fractional part to one decimal place. + // 2. After reading from the MS430, you can also access and print the + // float data directly from the struct: + Serial.print("The temperature is: "); + Serial.print(airDataF.T_C, 1); // print to 1 decimal place + Serial.println(" " CELSIUS_SYMBOL); + + // Optional: convert to Fahrenheit + float temperature_F = convertCtoF(airDataF.T_C); + + Serial.print("The temperature is: "); + Serial.print(temperature_F, 1); // print to 1 decimal place + Serial.println(" " FAHRENHEIT_SYMBOL); + + Serial.println("-----------------------------"); + + + // 3. If host resources are limited, avoid using floating point and + // instead use the integer versions (without "F" in the name) + AirData_t airData = getAirData(I2C_ADDRESS); + + // Print to the serial monitor + printAirData(&airData, false); + // If the second argument is "true", data are printed as columns. + // Fahrenheit temperature is printed if USE_FAHRENHEIT is defined + // in "Metriful_sensor.h" + + Serial.println("-----------------------------"); + + + // 4. Access and print integer data directly from the struct: + Serial.print("The humidity is: "); + Serial.print(airData.H_pc_int); // the integer part of the value + Serial.print("."); // the decimal point + Serial.print(airData.H_pc_fr_1dp); // the fractional part (1 decimal place) + Serial.println(" %"); + + Serial.println("-----------------------------"); + + + // 5. Advanced: read and decode only the humidity value from the MS430 + + // Read the raw humidity data + uint8_t receive_buffer[2] = {0}; + ReceiveI2C(I2C_ADDRESS, H_READ, receive_buffer, H_BYTES); + + // Decode the humidity: the first received byte is the integer part, the + // second received byte is the fractional part to one decimal place. uint8_t humidity_integer = receive_buffer[0]; uint8_t humidity_fraction = receive_buffer[1]; // Print it: the units are percentage relative humidity. Serial.print("Humidity = "); - Serial.print(humidity_integer);Serial.print(".");Serial.print(humidity_fraction);Serial.println(" %"); - - //////////////////////////////////////////////////////////////////// + Serial.print(humidity_integer); + Serial.print("."); + Serial.print(humidity_fraction); + Serial.println(" %"); + + Serial.println("-----------------------------"); + - // TEMPERATURE + // 6. Advanced: read and decode only the temperature value from the MS430 - // Read the temperature value from the sensor board - ReceiveI2C(i2c_7bit_address, T_READ, receive_buffer, T_BYTES); + // Read the raw temperature data + ReceiveI2C(I2C_ADDRESS, T_READ, receive_buffer, T_BYTES); - // Decode and print the temperature: + // The temperature is encoded differently to allow negative values - // Find the positive magnitude of the integer part of the temperature by - // doing a bitwise AND of the first byte with TEMPERATURE_VALUE_MASK + // Find the positive magnitude of the integer part of the temperature + // by doing a bitwise AND of the first received byte with TEMPERATURE_VALUE_MASK uint8_t temperature_positive_integer = receive_buffer[0] & TEMPERATURE_VALUE_MASK; - // The second byte is the fractional part to one decimal place + // The second received byte is the fractional part to one decimal place uint8_t temperature_fraction = receive_buffer[1]; Serial.print("Temperature = "); - // If the most-significant bit is set, the temperature is negative (below 0 C) - if ((receive_buffer[0] & TEMPERATURE_SIGN_MASK) == 0) { - // The bit is not set: celsius temperature is positive - Serial.print("+"); - } - else { - // The bit is set: celsius temperature is negative + // If the most-significant bit of the first byte is a 1, the temperature + // is negative (below 0 C), otherwise it is positive + if ((receive_buffer[0] & TEMPERATURE_SIGN_MASK) != 0) { + // The bit is a 1: celsius temperature is negative Serial.print("-"); } - Serial.print(temperature_positive_integer);Serial.print("."); - Serial.print(temperature_fraction);Serial.println(" C"); - - //////////////////////////////////////////////////////////////////// - - // AIR DATA + Serial.print(temperature_positive_integer); + Serial.print("."); + Serial.print(temperature_fraction); + Serial.println(" " CELSIUS_SYMBOL); - // Rather than reading individual data values as shown above, whole - // categories of data can be read in one I2C transaction - - // Read all Air data in one transaction, interpreting the received bytes as an AirData_t struct: - AirData_t airData = {0}; - ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); - - // Print the values over the serial port - if (useFloatingPoint) { - // Convert values where necessary to floating point representation: - AirData_F_t airDataF = {0}; - convertAirDataF(&airData, &airDataF); - printAirDataF(&airDataF); - } - else { - // The data remain in integer representation - printAirData(&airData, false); - } } void loop() { diff --git a/Arduino/Examples/simple_read_sound/simple_read_sound.ino b/Arduino/Examples/simple_read_sound/simple_read_sound.ino index 3bb10df..01d537a 100644 --- a/Arduino/Examples/simple_read_sound/simple_read_sound.ino +++ b/Arduino/Examples/simple_read_sound/simple_read_sound.ino @@ -3,8 +3,9 @@ Example code for using the Metriful MS430 to measure sound. - Measures and displays the sound data once. - View the output in the Serial Monitor. + Demonstrates multiple ways of reading and displaying the sound data. + View the output in the Serial Monitor. The other data can be measured + and displayed in a similar way. Copyright 2020 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt @@ -15,24 +16,10 @@ #include -////////////////////////////////////////////////////////// -// USER-EDITABLE SETTINGS - -// The I2C address of the Metriful board -uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN; - -// Whether to use floating point representation of numbers (uses more -// host resources) -bool useFloatingPoint = false; - -// END OF USER-EDITABLE SETTINGS -////////////////////////////////////////////////////////// - -uint8_t receive_buffer[1] = {0}; void setup() { // Initialize the host pins, set up the serial port and reset: - SensorHardwareSetup(i2c_7bit_address); + SensorHardwareSetup(I2C_ADDRESS); // Wait for the serial port to be ready, for displaying the output while (!Serial) { @@ -51,7 +38,7 @@ void setup() { ready_assertion_event = false; // Initiate an on-demand data measurement - TransmitI2C(i2c_7bit_address, ON_DEMAND_MEASURE_CMD, 0, 0); + TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); // Now wait for the ready signal (falling edge) before continuing while (!ready_assertion_event) { @@ -61,22 +48,49 @@ void setup() { // We now know that newly measured data are ready to read. //////////////////////////////////////////////////////////////////// + + // There are multiple ways to read and display the data + + + // 1. Simplest way: use the example float (_F) functions + + // Read the sound data from the board + SoundData_F_t soundDataF = getSoundDataF(I2C_ADDRESS); + + // Print all of the sound measurements to the serial monitor + printSoundDataF(&soundDataF); + + Serial.println("-----------------------------"); + + + // 2. After reading from the MS430, you can also access and print the + // float data directly from the struct: + Serial.print("The sound pressure level is: "); + Serial.print(soundDataF.SPL_dBA, 1); // print to 1 decimal place + Serial.println(" dBA"); + + Serial.println("-----------------------------"); + + + // 3. If host resources are limited, avoid using floating point and + // instead use the integer versions (without "F" in the name) + SoundData_t soundData = getSoundData(I2C_ADDRESS); + + // Print to the serial monitor + printSoundData(&soundData, false); + // If the second argument is "true", data are printed as columns. + + Serial.println("-----------------------------"); + + + // 4. Access and print integer data directly from the struct: + Serial.print("The sound pressure level is: "); + Serial.print(soundData.SPL_dBA_int); // the integer part of the value + Serial.print("."); // the decimal point + Serial.print(soundData.SPL_dBA_fr_1dp); // the fractional part (1 decimal place) + Serial.println(" dBA"); - // Read all sound data in one transaction, interpreting the received bytes as a SoundData_t struct: - SoundData_t soundData = {0}; - ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); - - // Print the values over the serial port - if (useFloatingPoint) { - // Convert values to floating point representation: - SoundData_F_t soundDataF = {0}; - convertSoundDataF(&soundData, &soundDataF); - printSoundDataF(&soundDataF); - } - else { - // The data remain in integer representation - printSoundData(&soundData, false); - } + Serial.println("-----------------------------"); } void loop() { diff --git a/Arduino/Examples/web_server/web_server.ino b/Arduino/Examples/web_server/web_server.ino index 5e6703c..1921ed6 100644 --- a/Arduino/Examples/web_server/web_server.ino +++ b/Arduino/Examples/web_server/web_server.ino @@ -2,15 +2,17 @@ web_server.ino Example code for serving a web page over a WiFi network, displaying - environment data from the Metriful MS430. + environment data read from the Metriful MS430. This example is designed for the following WiFi enabled hosts: * Arduino Nano 33 IoT * Arduino MKR WiFi 1010 - * NodeMCU ESP8266 - + * ESP8266 boards (e.g. Wemos D1, NodeMCU) + * ESP32 boards (e.g. DOIT DevKit v1) + All environment data values are measured and displayed on a text web page generated by the host, which acts as a simple web server. + The host can either connect to an existing WiFi network, or generate its own for other devices to connect to (Access Point mode). @@ -22,22 +24,18 @@ */ #include +#include ////////////////////////////////////////////////////////// // USER-EDITABLE SETTINGS -// Choose how often to read and display data (every 3, 100, 300 seconds) +// Choose how often to read and update data (every 3, 100, or 300 seconds) +// The web page can be refreshed more often but the data will not change uint8_t cycle_period = CYCLE_PERIOD_3_S; -// The I2C address of the Metriful board -uint8_t i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN; - -// Which particle sensor is attached (PPD42, SDS011, or OFF) -ParticleSensor_t particleSensor = OFF; - // Choose whether to create a new WiFi network (host as Access Point), // or connect to an existing WiFi network. -bool createWifiNetwork = true; +bool createWifiNetwork = false; // If creating a WiFi network, a static (fixed) IP address ("theIP") is // specified by the user. Otherwise, if connecting to an existing // network, an IP address is automatically allocated and the serial @@ -46,10 +44,10 @@ bool createWifiNetwork = true; // Provide the SSID (name) and password for the WiFi network. Depending // on the choice of createWifiNetwork, this is either created by the // host (Access Point mode) or already exists. -// To avoid problems, do not create a network with the same SSID +// To avoid problems, do not create a network with the same SSID name // as an already existing network. char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name) -char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password (at least 8 characters) +char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password; must be at least 8 characters // Choose a static IP address for the host, only used when generating // a new WiFi network (createWifiNetwork = true). The served web @@ -61,7 +59,7 @@ IPAddress theIP(192, 168, 12, 20); // END OF USER-EDITABLE SETTINGS ////////////////////////////////////////////////////////// -#if !defined(ARDUINO_SAMD_NANO_33_IOT) && !defined(ARDUINO_SAMD_MKRWIFI1010) && !defined(ESP8266) +#if !defined(HAS_WIFI) #error ("This example program has been created for specific WiFi enabled hosts only.") #endif @@ -75,48 +73,23 @@ LightData_t lightData = {0}; ParticleData_t particleData = {0}; SoundData_t soundData = {0}; -// Buffer for commands (big enough to fit the largest send transaction): -uint8_t transmit_buffer[1] = {0}; - // Storage for the web page text -char lineBuffer[128] = {0}; -char pageBuffer[2200] = {0}; +char lineBuffer[100] = {0}; +char pageBuffer[2300] = {0}; void setup() { // Initialize the host's pins, set up the serial port and reset: - SensorHardwareSetup(i2c_7bit_address); + SensorHardwareSetup(I2C_ADDRESS); if (createWifiNetwork) { - // The host generates its own WiFi network ("access point") - - #ifdef ESP8266 - // Set the chosen static IP address: - WiFi.mode(WIFI_AP_STA); - IPAddress subnet(255,255,255,0); - WiFi.softAPConfig(theIP, theIP, subnet); - - Serial.print("Creating access point named: "); - Serial.println(SSID); - if (!WiFi.softAP(SSID, password)) { - Serial.println("Failed to create access point."); - while (true) { - yield(); - } + // The host generates its own WiFi network ("Access Point") with + // a chosen static IP address + if (!createWiFiAP(SSID, password, theIP)) { + Serial.println("Failed to create access point."); + while (true) { + yield(); } - #else - // Set the chosen static IP address: - WiFi.config(theIP); - - Serial.print("Creating access point named: "); - Serial.println(SSID); - - if (WiFi.beginAP(SSID, password) != WL_AP_LISTENING) { - Serial.println("Failed to create access point."); - while (true) { - yield(); - } - } - #endif + } } else { // The host connects to an existing Wifi network @@ -126,25 +99,17 @@ void setup() { while (!Serial) { yield(); } - - // Attempt to connect to the Wifi network and obtain an IP + + // Attempt to connect to the Wifi network and obtain the IP // address. Because the address is not known before this point, // a serial monitor must be used to display it to the user. - Serial.print("Connecting to "); - Serial.println(SSID); - WiFi.begin(SSID, password); - while (WiFi.status() != WL_CONNECTED) { - delay(500); - Serial.print("."); - } - Serial.println("Connected."); - + connectToWiFi(SSID, password); theIP = WiFi.localIP(); } // Print the IP address: use this address in a browser to view the // generated web page - Serial.print("View your page at http://"); + Serial.print("View your page at http://"); Serial.println(theIP); // Start the web server @@ -152,10 +117,10 @@ void setup() { //////////////////////////////////////////////////////////////////// - // Select how often to refresh the web page. This should be done at + // Select how often to auto-refresh the web page. This should be done at // least as often as new data are obtained. A more frequent refresh is - // best for long cycle periods because the page access may be - // out-of-step with the cycle. + // best for long cycle periods because the page refresh is not + // synchronized with the cycle. Users can also manually refresh the page. if (cycle_period == CYCLE_PERIOD_3_S) { refreshPeriodSeconds = 3; } @@ -167,23 +132,18 @@ void setup() { } // Apply the chosen settings to the Metriful board - if (particleSensor != OFF) { - transmit_buffer[0] = particleSensor; - TransmitI2C(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); - } - transmit_buffer[0] = cycle_period; - TransmitI2C(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1); - - Serial.println("Entering cycle mode and waiting for data."); + uint8_t particleSensor = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); ready_assertion_event = false; - TransmitI2C(i2c_7bit_address, CYCLE_MODE_CMD, 0, 0); + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); } void loop() { - // While waiting for the next data release, - // respond to client requests by serving the web page with the last - // available data. Initially the data will be all zero (until the - // first data readout has completed). + + // While waiting for the next data release, respond to client requests + // by serving the web page with the last available data. Initially the + // data will be all zero (until the first data readout has completed). while (!ready_assertion_event) { handleClientRequests(); yield(); @@ -192,7 +152,7 @@ void loop() { // new data are now ready - /* Read data the MS430 into the data structs. + /* Read data from the MS430 into the data structs. For each category of data (air, sound, etc.) a pointer to the data struct is passed to the ReceiveI2C() function. The received byte sequence fills the data struct in the correct order so that each field within the struct receives @@ -200,34 +160,51 @@ void loop() { */ // Air data - ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + // Choose output temperature unit (C or F) in Metriful_sensor.h + ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); /* Air quality data The initial self-calibration of the air quality data may take several minutes to complete. During this time the accuracy parameter is zero and the data values are not valid. */ - ReceiveI2C(i2c_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); // Light data - ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); // Sound data - ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); /* Particle data This requires the connection of a particulate sensor (invalid values will be obtained if this sensor is not present). + Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h Also note that, due to the low pass filtering used, the particle data become valid after an initial initialization period of approximately one minute. */ - if (particleSensor != OFF) { - ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); } // Create the web page ready for client requests assembleWebPage(); + + // Check WiFi is still connected + if (!createWifiNetwork) { + uint8_t wifiStatus = WiFi.status(); + if (wifiStatus != WL_CONNECTED) { + // There is a problem with the WiFi connection: attempt to reconnect. + Serial.print("Wifi status: "); + Serial.println(interpret_WiFi_status(wifiStatus)); + connectToWiFi(SSID, password); + theIP = WiFi.localIP(); + Serial.print("View your page at http://"); + Serial.println(theIP); + ready_assertion_event = false; + } + } } @@ -235,7 +212,7 @@ void handleClientRequests(void) { // Check for incoming client requests WiFiClient client = server.available(); if (client) { - bool blankLine = true; + bool blankLine = false; while (client.connected()) { if (client.available()) { char c = client.read(); @@ -243,7 +220,7 @@ void handleClientRequests(void) { // Two consecutive newline characters indicates the end of the client HTTP request if (blankLine) { // Send the page as a response - client.println(pageBuffer); + client.print(pageBuffer); break; } else { @@ -262,128 +239,138 @@ void handleClientRequests(void) { } } +// Create a simple text web page showing the environment data in +// separate category tables, using HTML and CSS void assembleWebPage(void) { - strcpy(pageBuffer,"HTTP/1.1 200 OK\n" "Content-type:text/html\n" "Connection: close\n"); - sprintf(lineBuffer,"Refresh: %i\n\n\ - Metriful Sensor Demo",refreshPeriodSeconds); - strcat(pageBuffer,lineBuffer); - strcat(pageBuffer,"

Indoor Environment Data

"); + sprintf(pageBuffer,"HTTP/1.1 200 OK\r\n" + "Content-type: text/html\r\n" + "Connection: close\r\n" + "Refresh: %u\r\n\r\n",refreshPeriodSeconds); + + strcat(pageBuffer,"" + "" + "Metriful Sensor Demo" + "" + "

Indoor Environment Data

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

Air Data

"); + strcat(pageBuffer,"

Air Data

"); - uint8_t T_positive_integer = airData.T_C_int_with_sign & TEMPERATURE_VALUE_MASK; - // If the most-significant bit is set, the temperature is negative (below 0 C) - if ((airData.T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) { - // The bit is set: celsius temperature is negative - sprintf(lineBuffer,"", - T_positive_integer, airData.T_C_fr_1dp); - } - else { - // The bit is not set: celsius temperature is positive - sprintf(lineBuffer,"", - T_positive_integer, airData.T_C_fr_1dp); - } + uint8_t T_intPart = 0; + uint8_t T_fractionalPart = 0; + bool isPositive = true; + const char * unit = getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); + sprintf(lineBuffer,"", + isPositive?"":"-", T_intPart, T_fractionalPart, unit); strcat(pageBuffer,lineBuffer); - sprintf(lineBuffer,"", airData.P_Pa); + sprintf(lineBuffer,"", airData.P_Pa); strcat(pageBuffer,lineBuffer); - sprintf(lineBuffer,"", - airData.H_pc_int, airData.H_pc_fr_1dp); + sprintf(lineBuffer,"", + airData.H_pc_int, airData.H_pc_fr_1dp); strcat(pageBuffer,lineBuffer); - sprintf(lineBuffer,"
Temperature-%u.%uC
Temperature%u.%uC
Temperature%s%u.%u%s
Pressure%luPa
Pressure%" PRIu32 "Pa
Humidity%u.%u%%
Humidity%u.%u%%
Gas Sensor Resistance%luohm

", - airData.G_ohm); + sprintf(lineBuffer,"Gas Sensor Resistance" + "%" PRIu32 "" OHM_SYMBOL "

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

Air Quality Data

"); + strcat(pageBuffer,"

Air Quality Data

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

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

", + airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); strcat(pageBuffer,lineBuffer); } - sprintf(lineBuffer,"Measurement Accuracy%s

", - interpret_AQI_accuracy(airQualityData.AQI_accuracy)); - strcat(pageBuffer,lineBuffer); - ////////////////////////////////////// - strcat(pageBuffer,"

Sound Data

"); + strcat(pageBuffer,"

Sound Data

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

", - soundData.peak_amp_mPa_int, soundData.peak_amp_mPa_fr_2dp); + sprintf(lineBuffer,"Peak Sound Amplitude" + "%u.%02umPa

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

Light Data

"); + strcat(pageBuffer,"

Light Data

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

", lightData.white); + + sprintf(lineBuffer,"White Light Level%u" + "

", lightData.white); strcat(pageBuffer,lineBuffer); ////////////////////////////////////// - if (particleSensor != OFF) { - strcat(pageBuffer,"

Air Particulate Data

"); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + strcat(pageBuffer,"

Air Particulate Data

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

", - particleData.concentration_int, particleData.concentration_fr_2dp, unitsBuffer); + sprintf(lineBuffer,"Particle Concentration" + "%u.%02u%s

", + particleData.concentration_int, particleData.concentration_fr_2dp, unitsBuffer); strcat(pageBuffer,lineBuffer); } diff --git a/Arduino/Metriful_Sensor/Metriful_sensor.cpp b/Arduino/Metriful_Sensor/Metriful_sensor.cpp index 0fdbd74..bbec8fa 100644 --- a/Arduino/Metriful_Sensor/Metriful_sensor.cpp +++ b/Arduino/Metriful_Sensor/Metriful_sensor.cpp @@ -13,13 +13,6 @@ #include "Metriful_sensor.h" #include "host_pin_definitions.h" -#ifdef ARDUINO_SAMD_NANO_33_IOT - // The Arduino Nano 33 IoT wifi module prevents the use of the - // built-in hardware-supported I2C module, so a software I2C library - // must be used instead. SlowSoftWire is one of several libraries available. - SlowSoftWire TheWire(SOFT_SDA, SOFT_SCL, false); -#endif - // The Arduino Wire library has a limited internal buffer size: #define ARDUINO_WIRE_BUFFER_LIMIT_BYTES 32 @@ -29,14 +22,15 @@ void SensorHardwareSetup(uint8_t i2c_7bit_address) { #ifdef ESP8266 // Must specify the I2C pins - TheWire.begin(SDA_PIN, SCL_PIN); + Wire.begin(SDA_PIN, SCL_PIN); digitalWrite(LED_BUILTIN, HIGH); #else - TheWire.begin(); + // Default I2C pins are used + Wire.begin(); digitalWrite(LED_BUILTIN, LOW); #endif - TheWire.setClock(I2C_CLK_FREQ_HZ); + Wire.setClock(I2C_CLK_FREQ_HZ); // READY, light interrupt and sound interrupt lines are digital inputs. pinMode(READY_PIN, INPUT); @@ -71,15 +65,9 @@ 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. -#ifdef ESP8266 -void ICACHE_RAM_ATTR ready_ISR(void) { - ready_assertion_event = true; -} -#else -void ready_ISR(void) { +void ISR_ATTRIBUTE ready_ISR(void) { ready_assertion_event = true; } -#endif //////////////////////////////////////////////////////////////////////// @@ -88,17 +76,9 @@ void ready_ISR(void) { // 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 - float absoluteValue = ((float) (airData_in->T_C_int_with_sign & TEMPERATURE_VALUE_MASK)) + - (((float) airData_in->T_C_fr_1dp)/10.0); - if ((airData_in->T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) { - // the most-significant bit is set, indicating that the temperature is negative - airDataF_out->T_C = -absoluteValue; - } - else { - // temperature is positive - airDataF_out->T_C = absoluteValue; - } + // 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; @@ -143,21 +123,32 @@ void convertParticleDataF(const ParticleData_t * particleData_in, ParticleData_F //////////////////////////////////////////////////////////////////////// -// The following five functions print data (in floating-point representation) over the serial port as text +// 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 = ");Serial.print(airDataF->T_C,2);Serial.println(" C"); + 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,2);Serial.println(" %"); - Serial.print("Gas Sensor Resistance = ");Serial.print(airDataF->G_Ohm);Serial.println(" ohms"); + 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,2); - Serial.print(" (");Serial.print(interpret_AQI_value((uint16_t) airQualityDataF->AQI));Serial.println(")"); - Serial.print("Estimated CO2 = ");Serial.print(airQualityDataF->CO2e,2);Serial.println(" ppm"); - Serial.print("Equivalent Breath VOC = ");Serial.print(airQualityDataF->bVOC,2);Serial.println(" ppm"); + 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)); @@ -173,22 +164,22 @@ void printSoundDataF(const SoundData_F_t * soundDataF) { Serial.print("A-weighted Sound Pressure Level = "); Serial.print(soundDataF->SPL_dBA,1);Serial.println(" dBA"); for (uint16_t i=0; iT_C_int_with_sign & TEMPERATURE_VALUE_MASK; - if ((airData->T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) { - // the most-significant bit is set, indicating that the temperature is negative - sprintf(strbuf,"-%u.%u ",temp,airData->T_C_fr_1dp); - } - else { - sprintf(strbuf,"%u.%u ",temp,airData->T_C_fr_1dp); - } - Serial.print(strbuf); - sprintf(strbuf,"%lu %u.%u %lu ",airData->P_Pa, airData->H_pc_int, airData->H_pc_fr_1dp, airData->G_ohm); + // 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 { - uint8_t temp = airData->T_C_int_with_sign & TEMPERATURE_VALUE_MASK; - if ((airData->T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) { - // the most-significant bit is set, indicating that the temperature is negative - sprintf(strbuf,"Temperature = -%u.%u C",temp,airData->T_C_fr_1dp); - } - else { - // temperature is positive - sprintf(strbuf,"Temperature = %u.%u C",temp,airData->T_C_fr_1dp); - } + 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"); + Serial.print("Gas Sensor Resistance = ");Serial.print(airData->G_ohm);Serial.println(" " OHM_SYMBOL); } } @@ -255,9 +236,10 @@ void printAirQualityData(const AirQualityData_t * airQualityData, bool printColu 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)); + airQualityData->AQI_int, airQualityData->AQI_fr_1dp, interpret_AQI_value(airQualityData->AQI_int)); Serial.println(strbuf); - sprintf(strbuf,"Estimated CO2 = %u.%u ppm",airQualityData->CO2e_int, airQualityData->CO2e_fr_1dp); + 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); @@ -288,8 +270,8 @@ void printSoundData(const SoundData_t * soundData, bool printColumns) { soundData->SPL_dBA_int, soundData->SPL_dBA_fr_1dp); Serial.println(strbuf); for (uint8_t i=0; iSPL_bands_dB_int[i], soundData->SPL_bands_dB_fr_1dp[i]); + 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", @@ -312,8 +294,7 @@ void printLightData(const LightData_t * lightData, bool printColumns) { } } -void printParticleData(const ParticleData_t * particleData, bool printColumns, - ParticleSensor_t particleSensor) { +void printParticleData(const ParticleData_t * particleData, bool printColumns, uint8_t particleSensor) { char strbuf[50] = {0}; if (printColumns) { // Print: duty cycle/%, concentration @@ -329,14 +310,14 @@ void printParticleData(const ParticleData_t * particleData, bool printColumns, sprintf(strbuf,"Particle Concentration = %u.%02u ", particleData->concentration_int, particleData->concentration_fr_2dp); Serial.print(strbuf); - if (particleSensor == PPD42) { - Serial.println(" ppL"); + if (particleSensor == PARTICLE_SENSOR_PPD42) { + Serial.println("ppL"); } - else if (particleSensor == SDS011) { - Serial.println(" ug/m3"); + else if (particleSensor == PARTICLE_SENSOR_SDS011) { + Serial.println(SDS011_UNIT_SYMBOL); } else { - Serial.println(" (?)"); + Serial.println("(?)"); } Serial.print("Particle data valid: "); if (particleData->valid == 0) { @@ -366,16 +347,16 @@ bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], return false; } - TheWire.beginTransmission(dev_addr_7bit); - uint8_t bytesWritten = TheWire.write(commandRegister); + Wire.beginTransmission(dev_addr_7bit); + uint8_t bytesWritten = Wire.write(commandRegister); if (data_length > 0) { - bytesWritten += TheWire.write(data, data_length); + bytesWritten += Wire.write(data, data_length); } if (bytesWritten != (data_length+1)) { return false; } - return (TheWire.endTransmission(true) == 0); + return (Wire.endTransmission(true) == 0); } // Read data from the Metriful MS430 using the I2C-compatible two wire interface. @@ -386,33 +367,33 @@ bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], // 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; } - TheWire.beginTransmission(dev_addr_7bit); - TheWire.write(commandRegister); - if (TheWire.endTransmission(false) != 0) { + Wire.beginTransmission(dev_addr_7bit); + Wire.write(commandRegister); + if (Wire.endTransmission(false) != 0) { return false; - } + } - if (TheWire.requestFrom(dev_addr_7bit, data_length, (uint8_t) 1) != data_length) { + if (Wire.requestFrom(dev_addr_7bit, data_length, (uint8_t) 1) != data_length) { // Did not receive the expected number of bytes return false; } for (uint8_t i=0; i 0) { - data[i] = TheWire.read(); + if (Wire.available() > 0) { + data[i] = Wire.read(); } } @@ -427,13 +408,13 @@ const char * interpret_AQI_accuracy(uint8_t AQI_accuracy_code) { switch (AQI_accuracy_code) { default: case 0: - return "Not Yet Valid, Self-calibration Incomplete"; + return "Not yet valid, self-calibration incomplete"; case 1: - return "Low Accuracy, Self-calibration Ongoing"; + return "Low accuracy, self-calibration ongoing"; case 2: - return "Medium Accuracy, Self-calibration Ongoing"; + return "Medium accuracy, self-calibration ongoing"; case 3: - return "High Accuracy"; + return "High accuracy"; } } @@ -455,7 +436,7 @@ const char * interpret_AQI_value(uint16_t AQI) { return "Bad"; } else { - return "Very Bad"; + return "Very bad"; } } @@ -486,3 +467,131 @@ bool setLightInterruptThreshold(uint8_t dev_addr_7bit, uint16_t thres_lux_int, u TXdata[2] = thres_lux_fr_2dp; return TransmitI2C(dev_addr_7bit, LIGHT_INTERRUPT_THRESHOLD_REG, TXdata, LIGHT_INTERRUPT_THRESHOLD_BYTES); } + +//////////////////////////////////////////////////////////////////////// + +// Convenience functions for reading data (integer representation) +// +// For each category of data (air, sound, etc.) a pointer to the data +// struct is passed to the ReceiveI2C() function. The received byte +// sequence fills the data struct in the correct order so that each +// field within the struct receives the value of an environmental data +// quantity (temperature, sound level, etc.) + +SoundData_t getSoundData(uint8_t i2c_7bit_address) { + SoundData_t soundData = {0}; + ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + return soundData; +} + +AirData_t getAirData(uint8_t i2c_7bit_address) { + AirData_t airData = {0}; + ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + return airData; +} + +LightData_t getLightData(uint8_t i2c_7bit_address) { + LightData_t lightData = {0}; + ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + return lightData; +} + +AirQualityData_t getAirQualityData(uint8_t i2c_7bit_address) { + AirQualityData_t airQualityData = {0}; + ReceiveI2C(i2c_7bit_address, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + return airQualityData; +} + +ParticleData_t getParticleData(uint8_t i2c_7bit_address) { + ParticleData_t particleData = {0}; + ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + return particleData; +} + +// Convenience functions for reading data (float representation) + +SoundData_F_t getSoundDataF(uint8_t i2c_7bit_address) { + SoundData_F_t soundDataF = {0}; + SoundData_t soundData = getSoundData(i2c_7bit_address); + convertSoundDataF(&soundData, &soundDataF); + return soundDataF; +} + +AirData_F_t getAirDataF(uint8_t i2c_7bit_address) { + AirData_F_t airDataF = {0}; + AirData_t airData = getAirData(i2c_7bit_address); + convertAirDataF(&airData, &airDataF); + return airDataF; +} + +LightData_F_t getLightDataF(uint8_t i2c_7bit_address) { + LightData_F_t lightDataF = {0}; + LightData_t lightData = getLightData(i2c_7bit_address); + convertLightDataF(&lightData, &lightDataF); + return lightDataF; +} + +AirQualityData_F_t getAirQualityDataF(uint8_t i2c_7bit_address) { + AirQualityData_F_t airQualityDataF = {0}; + AirQualityData_t airQualityData = getAirQualityData(i2c_7bit_address); + convertAirQualityDataF(&airQualityData, &airQualityDataF); + return airQualityDataF; +} + +ParticleData_F_t getParticleDataF(uint8_t i2c_7bit_address) { + ParticleData_F_t particleDataF = {0}; + ParticleData_t particleData = getParticleData(i2c_7bit_address); + convertParticleDataF(&particleData, &particleDataF); + return particleDataF; +} + +//////////////////////////////////////////////////////////////////////// + +// Functions to convert Celsius temperature to Fahrenheit, in float +// and integer formats + +float convertCtoF(float C) { + return ((C*1.8) + 32.0); +} + +// Convert Celsius to Fahrenheit in sign, integer and fractional parts +void convertCtoF_int(float C, uint8_t * F_int, uint8_t * F_fr_1dp, bool * isPositive) { + float F = convertCtoF(C); + bool isNegative = (F < 0.0); + if (isNegative) { + F = -F; + } + F += 0.05; + F_int[0] = (uint8_t) F; + F -= (float) F_int[0]; + F_fr_1dp[0] = (uint8_t) (F*10.0); + isPositive[0] = (!isNegative); +} + +// Decode and convert the temperature as read from the MS430 (integer +// representation) into a float value +float convertEncodedTemperatureToFloat(uint8_t T_C_int_with_sign, uint8_t T_C_fr_1dp) { + float temperature_C = ((float) (T_C_int_with_sign & TEMPERATURE_VALUE_MASK)) + + (((float) T_C_fr_1dp)/10.0); + if ((T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) { + // the most-significant bit is set, indicating that the temperature is negative + temperature_C = -temperature_C; + } + return temperature_C; +} + +// Obtain temperature, in chosen units (C or F), as sign, integer and fractional parts +const char * getTemperature(const AirData_t * pAirData, uint8_t * T_intPart, + uint8_t * T_fractionalPart, bool * isPositive) { + #ifdef USE_FAHRENHEIT + float temperature_C = convertEncodedTemperatureToFloat(pAirData->T_C_int_with_sign, + pAirData->T_C_fr_1dp); + convertCtoF_int(temperature_C, T_intPart, T_fractionalPart, isPositive); + return FAHRENHEIT_SYMBOL; + #else + isPositive[0] = ((pAirData->T_C_int_with_sign & TEMPERATURE_SIGN_MASK) == 0); + T_intPart[0] = pAirData->T_C_int_with_sign & TEMPERATURE_VALUE_MASK; + T_fractionalPart[0] = pAirData->T_C_fr_1dp; + return CELSIUS_SYMBOL; + #endif +} diff --git a/Arduino/Metriful_Sensor/Metriful_sensor.h b/Arduino/Metriful_Sensor/Metriful_sensor.h index d5f825e..dd5c30a 100644 --- a/Arduino/Metriful_Sensor/Metriful_sensor.h +++ b/Arduino/Metriful_Sensor/Metriful_sensor.h @@ -1,8 +1,8 @@ /* Metriful_sensor.h - This file declares functions which are used in the code examples. - The function definitions are in file Metriful_sensor.cpp + 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 @@ -15,13 +15,46 @@ #define METRIFUL_SENSOR_H #include "Arduino.h" +#include #include +#include +#include +#include +#include #include "sensor_constants.h" #include "host_pin_definitions.h" +//////////////////////////////////////////////////////////////////////// + +// Choose to display output temperatures in Fahrenheit: +// un-comment the following line to use Fahrenheit +//#define USE_FAHRENHEIT + +// Specify which particle sensor is connected: +#define PARTICLE_SENSOR PARTICLE_SENSOR_OFF +// 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; //////////////////////////////////////////////////////////////////////// @@ -64,21 +97,35 @@ typedef struct { //////////////////////////////////////////////////////////////////////// // Custom type used to select the particle sensor being used (if any) - typedef enum { - OFF = PARTICLE_SENSOR_OFF, + 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); -#ifdef ESP8266 -void ICACHE_RAM_ATTR ready_ISR(void); -#else -void ready_ISR(void); -#endif +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); @@ -97,15 +144,32 @@ 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, ParticleSensor_t particleSensor); +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, ParticleSensor_t particleSensor); +void printParticleData(const ParticleData_t * particleData, bool printColumns, uint8_t particleSensor); bool setSoundInterruptThreshold(uint8_t dev_addr_7bit, uint16_t threshold_mPa); bool setLightInterruptThreshold(uint8_t dev_addr_7bit, uint16_t thres_lux_int, uint8_t thres_lux_fr_2dp); +SoundData_t getSoundData(uint8_t i2c_7bit_address); +AirData_t getAirData(uint8_t i2c_7bit_address); +LightData_t getLightData(uint8_t i2c_7bit_address); +AirQualityData_t getAirQualityData(uint8_t i2c_7bit_address); +ParticleData_t getParticleData(uint8_t i2c_7bit_address); + +SoundData_F_t getSoundDataF(uint8_t i2c_7bit_address); +AirData_F_t getAirDataF(uint8_t i2c_7bit_address); +LightData_F_t getLightDataF(uint8_t i2c_7bit_address); +AirQualityData_F_t getAirQualityDataF(uint8_t i2c_7bit_address); +ParticleData_F_t getParticleDataF(uint8_t i2c_7bit_address); + +float convertCtoF(float C); +void convertCtoF_int(float C, uint8_t * F_int, uint8_t * F_fr_1dp, bool * isPositive); +float convertEncodedTemperatureToFloat(uint8_t T_C_int_with_sign, uint8_t T_C_fr_1dp); +const char * getTemperature(const AirData_t * pAirData, uint8_t * T_intPart, + uint8_t * T_fractionalPart, bool * isPositive); #endif diff --git a/Arduino/Metriful_Sensor/WiFi_functions.cpp b/Arduino/Metriful_Sensor/WiFi_functions.cpp new file mode 100644 index 0000000..9b887be --- /dev/null +++ b/Arduino/Metriful_Sensor/WiFi_functions.cpp @@ -0,0 +1,92 @@ +/* + WiFi_functions.cpp + + This file defines functions used by examples connecting to, + or creating, a WiFi network. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include "host_pin_definitions.h" +#ifdef HAS_WIFI +#include "Arduino.h" +#include "WiFi_functions.h" + +// Repeatedly attempt to connect to the WiFi network using the input +// network name (SSID) and password. +void connectToWiFi(const char * SSID, const char * password) { + WiFi.disconnect(); + #if defined(ESP8266) || defined(ESP32) + WiFi.persistent(false); + WiFi.mode(WIFI_STA); + #endif + uint8_t wStatus = WL_DISCONNECTED; + while (wStatus != WL_CONNECTED) { + Serial.print("Attempting to connect to "); + Serial.println(SSID); + uint8_t statusChecks = 0; + WiFi.begin(SSID, password); + while ((wStatus != WL_CONNECTED) && (statusChecks < 8)) { + delay(1000); + Serial.print("."); + wStatus = WiFi.status(); + statusChecks++; + } + if (wStatus != WL_CONNECTED) { + Serial.println("Failed."); + WiFi.disconnect(); + delay(5000); + } + } + Serial.println("Connected."); +} + +// Configure the host as a WiFi access point, creating a WiFi network with +// specified network SSID (name), password and host IP address. +bool createWiFiAP(const char * SSID, const char * password, IPAddress hostIP) { + Serial.print("Creating access point named: "); + Serial.println(SSID); + #if defined(ESP8266) || defined(ESP32) + WiFi.persistent(false); + WiFi.mode(WIFI_AP); + IPAddress subnet(255,255,255,0); + bool success = WiFi.softAP(SSID, password); + delay(2000); + success = success && WiFi.softAPConfig(hostIP, hostIP, subnet); + #else + WiFi.config(hostIP); + bool success = (WiFi.beginAP(SSID, password) == WL_AP_LISTENING); + #endif + return success; +} + +// Provide a readable interpretation of the WiFi status. +// statusCode is the value returned by WiFi.status() +const char * interpret_WiFi_status(uint8_t statusCode) { + switch (statusCode) { + case WL_CONNECTED: + return "Connected"; + case WL_NO_SHIELD: + return "No shield"; + case WL_IDLE_STATUS: + return "Idle"; + case WL_NO_SSID_AVAIL: + return "No SSID available"; + case WL_SCAN_COMPLETED: + return "Scan completed"; + case WL_CONNECT_FAILED: + return "Connect failed"; + case WL_CONNECTION_LOST: + return "Connection lost"; + case WL_DISCONNECTED: + return "Disconnected"; + default: + return "Unknown"; + } +} + +#endif diff --git a/Arduino/Metriful_Sensor/WiFi_functions.h b/Arduino/Metriful_Sensor/WiFi_functions.h new file mode 100644 index 0000000..69d98fe --- /dev/null +++ b/Arduino/Metriful_Sensor/WiFi_functions.h @@ -0,0 +1,26 @@ +/* + WiFi_functions.h + + This file declares functions used by examples connecting to, + or creating, a WiFi network. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#include "host_pin_definitions.h" +#ifdef HAS_WIFI +#ifndef WIFI_FUNCTIONS_H +#define WIFI_FUNCTIONS_H + +#include + +void connectToWiFi(const char * SSID, const char * password); +bool createWiFiAP(const char * SSID, const char * password, IPAddress hostIP); +const char * interpret_WiFi_status(uint8_t statusCode); + +#endif +#endif diff --git a/Arduino/Metriful_Sensor/graph_web_page.h b/Arduino/Metriful_Sensor/graph_web_page.h new file mode 100644 index 0000000..a976491 --- /dev/null +++ b/Arduino/Metriful_Sensor/graph_web_page.h @@ -0,0 +1,57 @@ +/* + graph_web_page.h + + This file contains the web page code which is used to display graphs + as part of the graph_web_server example. This is the code from the + file 'graph_web_page.html' (HTML/CSS/javascript), which has been + minified and put into a C character string. + + Copyright 2020 Metriful Ltd. + Licensed under the MIT License - for further details see LICENSE.txt + + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor +*/ + +#ifndef GRAPH_WEB_PAGE_H +#define GRAPH_WEB_PAGE_H + +const char * graphWebPage = "HTTP/1.1 200 OK\r\n" + "Content-type: text/html\r\n" + "Connection: close\r\n\r\n" +"" +"" +"" +"" +"" +"Indoor Environment Data" +"" +"" +"" +"" +"" +"

Indoor Environment Data

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

sensor.metriful.com

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

Indoor Environment Data

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

sensor.metriful.com

+
+ + + + diff --git a/Arduino/Metriful_Sensor/host_pin_definitions.h b/Arduino/Metriful_Sensor/host_pin_definitions.h index 95ba71c..eebef88 100644 --- a/Arduino/Metriful_Sensor/host_pin_definitions.h +++ b/Arduino/Metriful_Sensor/host_pin_definitions.h @@ -2,19 +2,21 @@ host_pin_definitions.h This file defines which host pins are used to interface to the - Metriful MS430 board and which library is used for I2C communications. - - The relevant file section is selected automatically when the board - is chosen in the Arduino IDE. + Metriful MS430 board. The relevant file section is selected + automatically when the board is chosen in the Arduino IDE. - This file can be used with the following host systems: + More detail is provided in the readme and User Guide. + + This file provides settings for the following host systems: * Arduino Uno * Arduino Nano 33 IoT * Arduino Nano * Arduino MKR WiFi 1010 - * NodeMCU ESP8266 + * ESP8266 (tested on NodeMCU and Wemos D1 Mini - other boards may require changes) + * ESP32 (tested on DOIT ESP32 DEVKIT V1 - other boards may require changes) + The Metriful MS430 is compatible with many more development boards - than just these five. You can use this file as a guide to define the + than those listed. You can use this file as a guide to define the necessary settings for other host systems. Copyright 2020 Metriful Ltd. @@ -31,15 +33,12 @@ // Arduino Uno - #include - #define TheWire Wire + #define ISR_ATTRIBUTE - // I2C pins are set by default so not defined here #define READY_PIN 2 // Arduino digital pin 2 connects to RDY #define L_INT_PIN 4 // Arduino digital pin 4 connects to LIT #define S_INT_PIN 7 // Arduino digital pin 7 connects to SIT - /* In addition to these pins, the following I2C bus and power - connections must be made to the Metriful MS430: + /* Also make the following connections: Arduino pins GND, SCL, SDA to MS430 pins GND, SCL, SDA Arduino pin 5V to MS430 pins VPU and VIN MS430 pin VDD is unused @@ -53,8 +52,6 @@ Arduino pin 5V to SDS011 pin "5V" Arduino pin GND to SDS011 pin "GND" SDS011 pin "25um" to MS430 pin PRT - - For further details, see the readme and User Guide */ #elif defined ARDUINO_SAMD_NANO_33_IOT @@ -63,22 +60,17 @@ #include #include - - // The Wifi module uses the built-in hardware-supported I2C pins, - // so a software I2C library must be used with pins SOFT_SDA and - // SOFT_SCL. The examples use the SlowSoftWire library but - // alternatives are available. - #include + #define HAS_WIFI + #define ISR_ATTRIBUTE #define READY_PIN 11 // Arduino pin D11 connects to RDY #define L_INT_PIN A1 // Arduino pin A1 connects to LIT #define S_INT_PIN A2 // Arduino pin A2 connects to SIT - #define SOFT_SDA A0 // Arduino pin A0 connects to SDA - #define SOFT_SCL A3 // Arduino pin A3 connects to SCL - /* In addition to these pins, the following I2C bus and power - connections must be made to the Metriful MS430: + /* Also make the following connections: Arduino pin GND to MS430 pin GND Arduino pin 3.3V to MS430 pins VPU and VDD + Arduino pin A5 to MS430 pin SCL + Arduino pin A4 to MS430 pin SDA MS430 pin VIN is unused If a PPD42 particle sensor is used, connect the following: @@ -93,55 +85,51 @@ The solder bridge labeled "VUSB" on the underside of the Arduino must be soldered closed to provide 5V to the PPD42/SDS011. - - For further details, see the readme and User Guide */ - + #elif defined ARDUINO_AVR_NANO // Arduino Nano - #include - #define TheWire Wire + #define ISR_ATTRIBUTE - // I2C pins (SDA = A4, SCL = A5) are set by default so not defined here #define READY_PIN 2 // Arduino pin D2 connects to RDY #define L_INT_PIN 4 // Arduino pin D4 connects to LIT #define S_INT_PIN 7 // Arduino pin D7 connects to SIT - /* In addition to these pins, the following I2C bus and power - connections must be made to the Metriful MS430: - Arduino pins GND, A5 (SCL), A4 (SDA) to MS430 pins GND, SCL, SDA + /* Also make the following connections: + Arduino pin GND to MS430 pin GND + Arduino pin A5 (SCL) to MS430 pin SCL + Arduino pin A4 (SDA) to MS430 pin SDA Arduino pin 5V to MS430 pins VPU and VIN MS430 pin VDD is unused - + If a PPD42 particle sensor is used, connect the following: Arduino pin 5V to PPD42 pin 3 Arduino pin GND to PPD42 pin 1 PPD42 pin 4 to MS430 pin PRT - + If an SDS011 particle sensor is used, connect the following: Arduino pin 5V to SDS011 pin "5V" Arduino pin GND to SDS011 pin "GND" SDS011 pin "25um" to MS430 pin PRT - - For further details, see the readme and User Guide */ - + #elif defined ARDUINO_SAMD_MKRWIFI1010 // Arduino MKR WiFi 1010 + #include #include - #include - #define TheWire Wire + #define HAS_WIFI + #define ISR_ATTRIBUTE - // I2C pins (SDA = D11, SCL = D12) are set by default so not defined here #define READY_PIN 0 // Arduino digital pin 0 connects to RDY #define L_INT_PIN 4 // Arduino digital pin 4 connects to LIT #define S_INT_PIN 5 // Arduino digital pin 5 connects to SIT - /* In addition to these pins, the following I2C bus and power - connections must be made to the Metriful MS430: - Arduino pins GND, D12 (SCL), D11 (SDA) to MS430 pins GND, SCL, SDA + /* Also make the following connections: + Arduino pin GND to MS430 pin GND + Arduino pin D12 (SCL) to MS430 pin SCL + Arduino pin D11 (SDA) to MS430 pin SDA Arduino pin VCC (3.3V) to MS430 pins VPU and VDD MS430 pin VIN is unused @@ -154,43 +142,71 @@ Arduino pin 5V to SDS011 pin "5V" Arduino pin GND to SDS011 pin "GND" SDS011 pin "25um" to MS430 pin PRT - - For further details, see the readme and User Guide */ - + #elif defined ESP8266 - // NodeMCU ESP8266 + // The examples have been tested on NodeMCU and Wemos D1 Mini. + // Other ESP8266 boards may require changes. + #include - #include - #define TheWire Wire - - #define SDA_PIN 5 // NodeMCU GPIO5 (labeled D1) connects to SDA - #define SCL_PIN 4 // NodeMCU GPIO4 (labeled D2) connects to SCL - #define READY_PIN 12 // NodeMCU GPIO12 (labeled D6) connects to RDY - #define L_INT_PIN 0 // NodeMCU GPIO0 (labeled D3) connects to LIT - #define S_INT_PIN 14 // NodeMCU GPIO14 (labeled D5) connects to SIT - /* In addition to these pins, the following I2C bus and power - connections must be made to the Metriful MS430: - NodeMCU GND pin to MS430 pin GND - NodeMCU pin 3V3 to MS430 pins VPU and VDD + #define HAS_WIFI + #define ISR_ATTRIBUTE ICACHE_RAM_ATTR + + #define SDA_PIN 5 // GPIO5 (labeled D1) connects to SDA + #define SCL_PIN 4 // GPIO4 (labeled D2) connects to SCL + #define READY_PIN 12 // GPIO12 (labeled D6) connects to RDY + #define L_INT_PIN 0 // GPIO0 (labeled D3) connects to LIT + #define S_INT_PIN 14 // GPIO14 (labeled D5) connects to SIT + /* Also make the following connections: + ESP8266 pin GND to MS430 pin GND + ESP8266 pin 3V3 to MS430 pins VPU and VDD MS430 pin VIN is unused If a PPD42 particle sensor is used, also connect the following: - NodeMCU pin Vin to PPD42 pin 3 - NodeMCU pin GND to PPD42 pin 1 + ESP8266 pin Vin (may be labeled Vin or 5V or VU) to PPD42 pin 3 + ESP8266 pin GND to PPD42 pin 1 PPD42 pin 4 to MS430 pin PRT If an SDS011 particle sensor is used, connect the following: - NodeMCU pin Vin to SDS011 pin "5V" - NodeMCU pin GND to SDS011 pin "GND" + ESP8266 pin Vin (may be labeled Vin or 5V or VU) to SDS011 pin "5V" + ESP8266 pin GND to SDS011 pin "GND" + SDS011 pin "25um" to MS430 pin PRT + */ + +#elif defined ESP32 + + // The examples have been tested on DOIT ESP32 DEVKIT V1 development board. + // Other ESP32 boards may require changes. + + #include + #define HAS_WIFI + #define ISR_ATTRIBUTE IRAM_ATTR + + #define READY_PIN 23 // Pin D23 connects to RDY + #define L_INT_PIN 18 // Pin D18 connects to LIT + #define S_INT_PIN 19 // Pin D19 connects to SIT + /* Also make the following connections: + ESP32 pin D21 to MS430 pin SDA + ESP32 pin D22 to MS430 pin SCL + ESP32 pin GND to MS430 pin GND + ESP32 pin 3V3 to MS430 pins VPU and VDD + MS430 pin VIN is unused + + If a PPD42 particle sensor is used, also connect the following: + ESP32 pin Vin to PPD42 pin 3 + ESP32 pin GND to PPD42 pin 1 + PPD42 pin 4 to MS430 pin PRT + + If an SDS011 particle sensor is used, connect the following: + ESP32 pin Vin to SDS011 pin "5V" + ESP32 pin GND to SDS011 pin "GND" SDS011 pin "25um" to MS430 pin PRT - - For further details, see the readme and User Guide */ #else - #error ("Your development board is not directly supported - edit this file to define the correct input/output pins.") + #error ("Your development board is not directly supported") + // Please make a new section in this file to define the correct input/output pins #endif #endif diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..72b9534 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Change Log +All notable changes to the project software and documentation will be documented in this file. + +## [3.1.0] - 2020-11-16 +### Added +- Fahrenheit temperature output. +- Improved support for ESP8266. +- Support for ESP32. +- IFTTT example. +- Home Assistant example. +- Graph viewer software for cross-platform, real-time data monitoring. +- Graph webpage server example using Plotly.js +- Text webpage server added to Python examples (to match the one for Arduino). +- Updated the User Guide (v2.1) for the new examples. + +### Changed +- **All software changes are backwards-compatible so old programs should still work.** +- The Raspberry Pi Python code now uses a package folder, so the import command has changed slightly. +- Arduino Nano 33 IoT now uses hardware I2C port, so the previously-used software I2C library is no longer needed. This requires a re-wire of two pins for this host only. +- The particle sensor being used (or not) is now set **once** rather than in each example code file - see readme for details. +- The I2C address in the Arduino examples is now set **once** rather than in each example code file. +- Small changes in most code files. + + +## [3.0.1] - 2020-10-14 +### Fixed +- Arduino IoT_cloud_logging HTTP request problem. + + +## [3.0.0] - 2020-09-06 +### Changed +- First release after hardware delivery. +- Datasheet (v2.0), user guide (v2.0), readme and code comments were edited. + diff --git a/Python/GraphViewer.py b/Python/GraphViewer.py new file mode 100644 index 0000000..469cfdd --- /dev/null +++ b/Python/GraphViewer.py @@ -0,0 +1,233 @@ +# GraphViewer.py + +# This file defines a class for creating a graphical user interface, +# to display graphs with real-time data updates. + +# A subclass must be derived from GraphViewer to implement the +# method getDataFunction() and create a working example program. This +# is done in "graph_viewer_serial.py" and "graph_viewer_I2C.py" + +# This is designed to run with Python 3 on multiple operating systems. +# The readme and User Guide give instructions on installing the necessary +# packages (pyqtgraph and PyQt5). + +# Copyright 2020 Metriful Ltd. +# Licensed under the MIT License - for further details see LICENSE.txt + +# For code examples, datasheet and user guide, visit +# https://github.com/metriful/sensor + +######################################################### + +import time +from pyqtgraph.Qt import QtCore, QtGui +import pyqtgraph as pg +from collections import deque, OrderedDict + + +class GraphViewer(QtGui.QMainWindow): + def __init__(self, data_buffer_length): + super(GraphViewer, self).__init__() + + self.buffer_samples = data_buffer_length + + # Define the number of graphs in the grid: + graphs_vertical = 2 + graphs_horizontal = 2 + + # Appearance settings: + self.pen_style = pg.mkPen(color="y", width=2, style=QtCore.Qt.SolidLine) + self.title_color = "w" + self.title_size = "13pt" + self.axis_color = "w" + self.axis_label_style = {'color': '#FFF', 'font-size': '11pt'} + self.x_grid = False + self.y_grid = False + + # Labels and measurement units for display + self.C_label = "\u00B0C" + self.F_label = "\u00B0F" + self.SDS_unit = "\u00B5g/m\u00B3" + self.PPD_unit = "ppL" + self.names_units = OrderedDict([('Temperature', self.C_label), + ('Pressure', 'Pa'),('Humidity', '%'),('Gas sensor resistance', "\u03A9"), + ('Air Quality Index', ''),('Estimated CO\u2082', 'ppm'),('Equivalent breath VOC', 'ppm'), + ('Air quality accuracy', ''),('Illuminance', 'lux'),('White light level', ''), + ('Sound pressure level', 'dBA'),('Band 1 SPL', 'dB'),('Band 2 SPL', 'dB'), + ('Band 3 SPL', 'dB'),('Band 4 SPL', 'dB'),('Band 5 SPL', 'dB'), + ('Band 6 SPL', 'dB'),('Peak sound amplitude', 'mPa'),('Microphone initialized', ''), + ('Particle sensor duty cycle', '%'),('Particle concentration', self.PPD_unit), + ('Particle data valid', '')]) + self.decimal_places = [1,0,1,0,1,1,2,0,2,0,1,1,1,1,1,1,1,2,0,2,2,0] + self.sound_band_number = 6 + + # Construct the user interface + self.setWindowTitle('Waiting for data...') + self.widget = QtGui.QWidget() + self.setCentralWidget(self.widget) + self.widget.setLayout(QtGui.QGridLayout()) + self.graph_var_numbers = [] + self.selected_var_numbers = [] + self.plot_items = [] + self.plot_handles = [] + self.combos = [] + self.is_bar_chart = [] + for nv in range(0,graphs_vertical): + for nh in range(0,graphs_horizontal): + GLW = pg.GraphicsLayoutWidget() + combo = pg.ComboBox() + self.combos.append(combo) + self.widget.layout().addWidget(combo, (2*nv), nh) + self.widget.layout().addWidget(GLW, (2*nv)+1, nh) + new_plot = GLW.addPlot() + self.plot_items.append(new_plot) + self.plot_handles.append(new_plot.plot(pen=self.pen_style, + symbol=None, axisItems={'bottom': pg.DateAxisItem()})) + self.formatPlotItem(new_plot) + self.is_bar_chart.append(False) + self.time_data = deque(maxlen=self.buffer_samples) + + + # Initialize and begin the periodic updating of the GUI + def start(self): + self.updateLoop() + self.show() + + + def setParticleUnits(self, name): + if (name == "SDS011"): + self.names_units['Particle concentration'] = self.SDS_unit + elif (name == "PPD42"): + self.names_units['Particle concentration'] = self.PPD_unit + elif (name is not None): + raise Exception("Particle sensor name must be 'SDS011' or 'PPD42', or None") + + + def useFahrenheitTemperatureUnits(self, use_fahrenheit): + if (use_fahrenheit): + self.names_units['Temperature'] = self.F_label + else: + self.names_units['Temperature'] = self.C_label + + + # Adjust plot appearance + def formatPlotItem(self, item): + item.setMenuEnabled(False) + item.showGrid(x=self.x_grid, y=self.y_grid) + item.getAxis("left").setPen(pg.mkPen(self.axis_color)) + item.getAxis("bottom").setPen(pg.mkPen(self.axis_color)) + item.getAxis("left").setStyle(tickLength=7) + item.getAxis("bottom").setStyle(tickLength=7) + item.setAxisItems({'bottom':pg.DateAxisItem()}) + + + # Create and return a new function which will be called when one of + # the comboboxes is changed. + def funcCreator(self, graph_index, combo_handle): + def func(): + self.selected_var_numbers[graph_index] = combo_handle.value() + return func + + + # Check for new data and redraw the graphs if data or combobox + # selections have changed + def updateLoop(self): + need_update = (self.graph_var_numbers != self.selected_var_numbers) + need_update = need_update or self.getDataFunction() + if (need_update): + self.updateGraphs() + # Call this function again in 20 ms + QtCore.QTimer.singleShot(20, self.updateLoop) + + + def getDataFunction(self): + # To be defined in subclass + pass + + + def createDataBuffer(self): + self.data_buffer = [deque(maxlen=self.buffer_samples) for i in range(0, len(self.indices))] + + + # Fill the ComboBoxes with the list of items and set the initial selected values + def initializeComboBoxes(self): + names = [list(self.names_units.keys())[j] for j in self.indices] + combo_items = dict(zip(names, [k for k in range(0,len(names))])) + combo_items['Sound frequency bands'] = len(combo_items) + for n,combo in enumerate(self.combos): + combo.setItems(combo_items) + start_index = n + if (n==0): + # Set first plot to be a bar chart + start_index = combo_items['Sound frequency bands'] + self.selected_var_numbers.append(start_index) + combo.setValue(start_index) + combo.currentIndexChanged.connect(self.funcCreator(n, combo)) + self.graph_var_numbers = self.selected_var_numbers.copy() + + + # Draw new data on the graphs and update the text label titles + def updateGraphs(self): + for n in range(0,len(self.plot_handles)): + self.graph_var_numbers[n] = self.selected_var_numbers[n] + if (self.graph_var_numbers[n] >= len(self.indices)): + # Bar chart of sound bands + if not (self.is_bar_chart[n] == True): + self.plot_items[n].removeItem(self.plot_handles[n]) + self.plot_handles[n].deleteLater() + self.plot_handles[n] = pg.BarGraphItem(x=list(range(0,self.sound_band_number)), + height=[0]*self.sound_band_number, width=0.9, brush="r") + self.plot_items[n].addItem(self.plot_handles[n]) + self.formatBarChart(self.plot_items[n]) + self.is_bar_chart[n] = True + new_data = [self.data_buffer[i][-1] for i in range(self.band1_index, + self.band1_index+self.sound_band_number)] + self.plot_handles[n].setOpts(height=new_data) + else: + # Line graph of single variable + if not (self.is_bar_chart[n] == False): + self.plot_items[n].removeItem(self.plot_handles[n]) + self.plot_handles[n].deleteLater() + self.plot_handles[n] = self.plot_items[n].plot(pen=self.pen_style, symbol=None) + self.adjustAxes(self.plot_items[n]) + self.is_bar_chart[n] = False + ind = self.indices[self.graph_var_numbers[n]] + self.plot_items[n].setTitle(list(self.names_units.keys())[ind] + + " = {:.{dps}f} ".format(self.data_buffer[self.graph_var_numbers[n]][-1], + dps=self.decimal_places[ind]) + list(self.names_units.values())[ind], + color=self.title_color,size=self.title_size) + self.plot_handles[n].setData(self.time_data, self.data_buffer[self.graph_var_numbers[n]]) + + + # Change axis settings + def adjustAxes(self, item): + item.getAxis("bottom").setTicks(None) + item.getAxis("left").setTicks(None) + item.getAxis("bottom").showLabel(False) + item.enableAutoRange(axis='x', enable=True) + item.enableAutoRange(axis='y', enable=True) + + + # Format the bar chart for displaying sound data for the six frequency bands + def formatBarChart(self, item): + frequency_midpoints = [125, 250, 500, 1000, 2000, 4000] + dB_labels = [20,30,40,50,60,70,80,90] + item.setTitle("Frequency band sound level / dB",color=self.title_color,size=self.title_size) + item.setLabel('bottom', text="Band center frequency / Hz", **self.axis_label_style) + # X axis ticks: set minor to same as major and label according to frequency + x_ticks = [[0]*len(frequency_midpoints),[0]*len(frequency_midpoints)] + for n,x in enumerate(frequency_midpoints): + x_ticks[0][n] = (n,str(x)) + x_ticks[1][n] = x_ticks[0][n] + item.getAxis("bottom").setTicks(x_ticks) + item.setXRange(-0.5, 5.5, padding=0) + # Y axis ticks: set minor ticks to same as major + y_ticks = [[0]*len(dB_labels),[0]*len(dB_labels)] + for n,y in enumerate(dB_labels): + y_ticks[0][n] = (y,str(y)) + y_ticks[1][n] = y_ticks[0][n] + item.getAxis("left").setTicks(y_ticks) + # fix Y axis limits, with margin: + item.setYRange(dB_labels[0], dB_labels[-1], padding=0.05) + + diff --git a/Python/Raspberry_Pi/Home_Assistant.py b/Python/Raspberry_Pi/Home_Assistant.py new file mode 100644 index 0000000..a84b8f4 --- /dev/null +++ b/Python/Raspberry_Pi/Home_Assistant.py @@ -0,0 +1,119 @@ +# Home_Assistant.py + +# Example code for sending environment data from the Metriful MS430 to +# an installation of Home Assistant (www.home-assistant.io) on your +# home network. +# This example is designed to run with Python 3 on a Raspberry Pi. + +# Data are sent at regular intervals over your local network to Home +# Assistant and can be viewed on the dashboard and used to control +# home automation tasks. More setup information is provided in the +# Readme and User Guide. + +# Copyright 2020 Metriful Ltd. +# Licensed under the MIT License - for further details see LICENSE.txt + +# For code examples, datasheet and user guide, visit +# https://github.com/metriful/sensor + +import requests +from sensor_package.sensor_functions import * + +######################################################### +# USER-EDITABLE SETTINGS + +# How often to read and report the data (every 3, 100 or 300 seconds) +cycle_period = CYCLE_PERIOD_100_S + +# Home Assistant settings + +# You must have already installed Home Assistant on a computer on your +# network. Go to www.home-assistant.io for help on this. + +# Choose a unique name for this MS430 sensor board so you can identify it. +# Variables in HA will have names like: SENSOR_NAME.temperature, etc. +SENSOR_NAME = "kitchen3" + +# Specify the IP address of the computer running Home Assistant. +# You can find this from the admin interface of your router. +HOME_ASSISTANT_IP = "192.168.43.144" + +# Security access token: the Readme and User Guide explain how to get this +LONG_LIVED_ACCESS_TOKEN = "PASTE YOUR TOKEN HERE WITHIN QUOTES" + +# END OF USER-EDITABLE SETTINGS +######################################################### + +# Set up the GPIO and I2C communications bus +(GPIO, I2C_bus) = SensorHardwareSetup() + +# Apply the settings to the MS430 +I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR]) +I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period]) + +######################################################### + +print("Reporting data to Home Assistant. Press ctrl-c to exit.") + +# Enter cycle mode +I2C_bus.write_byte(i2c_7bit_address, CYCLE_MODE_CMD) + +while (True): + + # Wait for the next new data release, indicated by a falling edge on READY + while (not GPIO.event_detected(READY_pin)): + sleep(0.05) + + # Now read all data from the MS430 + air_data = get_air_data(I2C_bus) + air_quality_data = get_air_quality_data(I2C_bus) + light_data = get_light_data(I2C_bus) + sound_data = get_sound_data(I2C_bus) + particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR) + + # Specify information needed by Home Assistant. + # Icons are chosen from https://cdn.materialdesignicons.com/5.3.45/ + # (remove the "mdi-" part from the icon name). + pressure = dict(name='Pressure', data=air_data['P_Pa'], unit='Pa', icon='weather-cloudy', decimals=0) + humidity = dict(name='Humidity', data=air_data['H_pc'], unit='%', icon='water-percent', decimals=1) + temperature = dict(name='Temperature', data=air_data['T'], unit=air_data['T_unit'], + icon='thermometer', decimals=1) + illuminance = dict(name='Illuminance', data=light_data['illum_lux'], unit='lx', + icon='white-balance-sunny', decimals=2) + sound_level = dict(name='Sound level', data=sound_data['SPL_dBA'], unit='dBA', + icon='microphone', decimals=1) + sound_peak = dict(name='Sound peak', data=sound_data['peak_amp_mPa'], unit='mPa', + icon='waveform', decimals=2) + AQI = dict(name='Air Quality Index', data=air_quality_data['AQI'], unit=' ', + icon='thought-bubble-outline', decimals=1) + AQI_interpret = dict(name='Air quality assessment', data=interpret_AQI_value(air_quality_data['AQI']), + unit='', icon='flower-tulip', decimals=0) + particle = dict(name='Particle concentration', data=particle_data['concentration'], + unit=particle_data['conc_unit'], icon='chart-bubble', decimals=2) + + # Send data to Home Assistant using HTTP POST requests + variables = [pressure, humidity, temperature, illuminance, sound_level, sound_peak, AQI, AQI_interpret] + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF): + variables.append(particle) + try: + for v in variables: + url = ("http://" + HOME_ASSISTANT_IP + ":8123/api/states/" + + SENSOR_NAME + "." + v['name'].replace(' ','_')) + head = {"Content-type": "application/json","Authorization": "Bearer " + LONG_LIVED_ACCESS_TOKEN} + try: + valueStr = "{:.{dps}f}".format(v['data'], dps=v['decimals']) + except: + valueStr = v['data'] + payload = {"state":valueStr, "attributes":{"unit_of_measurement":v['unit'], + "friendly_name":v['name'], "icon":"mdi:" + v['icon']}} + requests.post(url, json=payload, headers=head, timeout=2) + except Exception as e: + # An error has occurred, likely due to a lost network connection, + # and the post has failed. + # The program will retry with the next data release and will succeed + # if the network reconnects. + print("HTTP POST failed with the following error:") + print(repr(e)) + print("The program will continue and retry on the next data output.") + + diff --git a/Python/Raspberry_Pi/IFTTT.py b/Python/Raspberry_Pi/IFTTT.py new file mode 100644 index 0000000..523f640 --- /dev/null +++ b/Python/Raspberry_Pi/IFTTT.py @@ -0,0 +1,146 @@ +# IFTTT.py + +# Example code for sending data from the Metriful MS430 to IFTTT.com +# This example is designed to run with Python 3 on a Raspberry Pi. + +# Environmental data values are periodically measured and compared with +# a set of user-defined thresholds. If any values go outside the allowed +# ranges, an HTTP POST request is sent to IFTTT.com, triggering an alert +# email to your inbox, with customizable text. + +# More setup information is provided in the readme and User Guide. + +# Copyright 2020 Metriful Ltd. +# Licensed under the MIT License - for further details see LICENSE.txt + +# For code examples, datasheet and user guide, visit +# https://github.com/metriful/sensor + +import requests +from sensor_package.sensor_functions import * + +######################################################### +# USER-EDITABLE SETTINGS + +# IFTTT.com settings: WEBHOOKS_KEY and IFTTT_EVENT_NAME + +# You must set up a free account on IFTTT.com and create a Webhooks +# applet before using this example. This is explained further in the +# instructions in the GitHub Readme and in the User Guide. + +WEBHOOKS_KEY = "PASTE YOUR KEY HERE WITHIN QUOTES" +IFTTT_EVENT_NAME = "PASTE YOUR EVENT NAME HERE WITHIN QUOTES" + +# An inactive period follows each alert, during which the same alert +# will not be generated again - this prevents too many emails/alerts. +# Choose the period as a number of readout cycles (each 5 minutes) +# e.g. for a 2 hour period, choose inactive_wait_cycles = 24 +inactive_wait_cycles = 24; + +# Define the details of the variables for monitoring: +humidity = {'name':'humidity', + 'unit':"%", + 'decimal_places':1, + 'high_threshold':60, + 'low_threshold':30, + 'inactive_count':2, + 'high_advice':'Reduce moisture sources.', + 'low_advice':'Start the humidifier.'} + +air_quality_index = {'name':'air quality index', + 'unit':'', + 'decimal_places':1, + 'high_threshold':250, + 'low_threshold':-1, + 'inactive_count':2, + 'high_advice':'Improve ventilation.', + 'low_advice':''} + +# This example assumes that Celsius output temperature is selected. Edit +# these values if Fahrenheit is selected in sensor_functions.py +temperature = {'name':'temperature', + 'unit':CELSIUS_SYMBOL, + 'decimal_places':1, + 'high_threshold':23, + 'low_threshold':18, + 'inactive_count':2, + 'high_advice':'Turn on the fan.', + 'low_advice':'Turn on the heating.'} + +# END OF USER-EDITABLE SETTINGS +######################################################### + +# Measure the environment data every 300 seconds (5 minutes). This is +# adequate for long-term monitoring. +cycle_period = CYCLE_PERIOD_300_S + +# IFTTT settings: +IFTTT_url = "http://maker.ifttt.com/trigger/" + IFTTT_EVENT_NAME + "/with/key/" + WEBHOOKS_KEY +IFTTT_header = {"Content-type": "application/json"} + +# Set up the GPIO and I2C communications bus +(GPIO, I2C_bus) = SensorHardwareSetup() + +######################################################### + +print("Monitoring data. Press ctrl-c to exit.") + +# Enter cycle mode +I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period]) +I2C_bus.write_byte(i2c_7bit_address, CYCLE_MODE_CMD) + +while (True): + + # Wait for the next new data release, indicated by a falling edge on READY + while (not GPIO.event_detected(READY_pin)): + sleep(0.05) + + # Read the air data and air quality data + air_data = get_air_data(I2C_bus) + air_quality_data = get_air_quality_data(I2C_bus) + temperature['data'] = air_data['T'] + humidity['data'] = air_data['H_pc'] + air_quality_index['data'] = air_quality_data['AQI'] + + # Check the new values and send an alert to IFTTT if a variable is + # outside its allowed range. + for v in [temperature, humidity, air_quality_index]: + + if (v['inactive_count'] > 0): + # Count down to when the monitoring is active again + v['inactive_count']-=1 + + send_alert = False + if ((v['data'] > v['high_threshold']) and (v['inactive_count'] == 0)): + # The variable is above the high threshold: send an alert then + # ignore this variable for the next inactive_wait_cycles + v['inactive_count'] = inactive_wait_cycles + send_alert = True + threshold_description = 'high.' + advice = v['high_advice'] + elif ((v['data'] < v['low_threshold']) and (v['inactive_count'] == 0)): + # The variable is below the low threshold: send an alert then + # ignore this variable for the next inactive_wait_cycles + v['inactive_count'] = inactive_wait_cycles + send_alert = True + threshold_description = 'low.' + advice = v['low_advice'] + + if send_alert: + # Send data using an HTTP POST request + try: + value1 = "The " + v['name'] + " is too " + threshold_description + print("Sending new alert to IFTTT: " + value1) + payload = {"value1":value1, + "value2":("The measurement was {:.{dps}f} ".format(v['data'], + dps=v['decimal_places']) + v['unit']), + "value3":advice} + requests.post(IFTTT_url, json=payload, headers=IFTTT_header, timeout=2) + except Exception as e: + # An error has occurred, likely due to a lost internet connection, + # and the post has failed. The program will continue and new + # alerts will succeed if the internet reconnects. + print("HTTP POST failed with the following error:") + print(repr(e)) + print("The program will attempt to continue.") + diff --git a/Raspberry_Pi/IoT_cloud_logging.py b/Python/Raspberry_Pi/IoT_cloud_logging.py similarity index 80% rename from Raspberry_Pi/IoT_cloud_logging.py rename to Python/Raspberry_Pi/IoT_cloud_logging.py index 12910bf..553b272 100644 --- a/Raspberry_Pi/IoT_cloud_logging.py +++ b/Python/Raspberry_Pi/IoT_cloud_logging.py @@ -15,7 +15,7 @@ # https://github.com/metriful/sensor import requests -from sensor_functions import * +from sensor_package.sensor_functions import * ######################################################### # USER-EDITABLE SETTINGS @@ -25,10 +25,8 @@ # be set to 100 or 300 seconds, not 3 seconds. cycle_period = CYCLE_PERIOD_100_S -# Which particle sensor, if any, is attached (PPD42, SDS011, or OFF) -particleSensor = PARTICLE_SENSOR_OFF - # IoT cloud settings. + # This example uses the free IoT cloud hosting services provided # by Tago.io or Thingspeak.com # Other free cloud providers are available. @@ -37,8 +35,7 @@ # readme and User Guide for more information. # Choose which provider to use -use_Tago_cloud = True -# To use the ThingSpeak cloud, set: use_Tago_cloud=False +use_Tago_cloud = True # set this False to use the Thingspeak cloud # The chosen account's key/token must be inserted below. if (use_Tago_cloud): @@ -55,8 +52,7 @@ (GPIO, I2C_bus) = SensorHardwareSetup() # Apply the chosen settings to the MS430 -if (particleSensor != PARTICLE_SENSOR_OFF): - I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor]) +I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR]) I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period]) ######################################################### @@ -85,38 +81,35 @@ # Now read all data from the MS430 # Air data - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES) - air_data = extractAirData(raw_data) + # Choose output temperature unit (C or F) in sensor_functions.py + air_data = get_air_data(I2C_bus) # Air quality data # The initial self-calibration of the air quality data may take several # minutes to complete. During this time the accuracy parameter is zero # and the data values are not valid. - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_QUALITY_DATA_READ, AIR_QUALITY_DATA_BYTES) - air_quality_data = extractAirQualityData(raw_data) + air_quality_data = get_air_quality_data(I2C_bus) # Light data - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, LIGHT_DATA_READ, LIGHT_DATA_BYTES) - light_data = extractLightData(raw_data) - + light_data = get_light_data(I2C_bus) + # Sound data - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES) - sound_data = extractSoundData(raw_data) + sound_data = get_sound_data(I2C_bus) # Particle data - # This requires the connection of a particulate sensor (invalid + # This requires the connection of a particulate sensor (zero/invalid # values will be obtained if this sensor is not present). + # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py # Also note that, due to the low pass filtering used, the # particle data become valid after an initial initialization # period of approximately one minute. - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, PARTICLE_DATA_READ, PARTICLE_DATA_BYTES) - particle_data = extractParticleData(raw_data, particleSensor) + particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR) # Assemble the data into the required format, then send it to the cloud # as an HTTP POST request. # For both example cloud providers, the following quantities will be sent: - # 1 Temperature/C + # 1 Temperature (measurement unit is selected in sensor_functions.py) # 2 Pressure/Pa # 3 Humidity/% # 4 Air quality index @@ -125,14 +118,14 @@ # 7 Illuminance/lux # 8 Particle concentration - # Additionally, for Tago, the following is sent: + # Additionally, for Tago, the following are sent: # 9 Air Quality Assessment summary (Good, Bad, etc.) # 10 Peak sound amplitude / mPa try: if use_Tago_cloud: payload = [0]*10; - payload[0] = {"variable":"temperature","value":"{:.1f}".format(air_data['T_C'])} + payload[0] = {"variable":"temperature","value":"{:.1f}".format(air_data['T'])} payload[1] = {"variable":"pressure","value":air_data['P_Pa']} payload[2] = {"variable":"humidity","value":"{:.1f}".format(air_data['H_pc'])} payload[3] = {"variable":"aqi","value":"{:.1f}".format(air_quality_data['AQI'])} @@ -146,7 +139,7 @@ else: # Use ThingSpeak.com cloud payload = "api_key=" + THINGSPEAK_API_KEY_STRING - payload += "&field1=" + "{:.1f}".format(air_data['T_C']) + payload += "&field1=" + "{:.1f}".format(air_data['T']) payload += "&field2=" + str(air_data['P_Pa']) payload += "&field3=" + "{:.1f}".format(air_data['H_pc']) payload += "&field4=" + "{:.1f}".format(air_quality_data['AQI']) @@ -156,12 +149,12 @@ payload += "&field8=" + "{:.2f}".format(particle_data['concentration']) requests.post(thingspeak_url, data=payload, headers=thingspeak_header, timeout=2) - except: + except Exception as e: # An error has occurred, likely due to a lost internet connection, # and the post has failed. # The program will retry with the next data release and will succeed # if the internet reconnects. - print("HTTP POST failed.") - - + print("HTTP POST failed with the following error:") + print(repr(e)) + print("The program will continue and retry on the next data output.") diff --git a/Raspberry_Pi/cycle_readout.py b/Python/Raspberry_Pi/cycle_readout.py similarity index 67% rename from Raspberry_Pi/cycle_readout.py rename to Python/Raspberry_Pi/cycle_readout.py index d5a701e..5cc9268 100644 --- a/Raspberry_Pi/cycle_readout.py +++ b/Python/Raspberry_Pi/cycle_readout.py @@ -7,24 +7,23 @@ # repeating cycle. User can choose from a cycle time period # of 3, 100, or 300 seconds. +# The measurements can be displayed as either labeled text, or as +# simple columns of numbers. + # Copyright 2020 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt # For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -from sensor_functions import * +from sensor_package.sensor_functions import * ######################################################### # USER-EDITABLE SETTINGS -# How often to read data (every 3, 100, 300 seconds) +# How often to read data (every 3, 100, or 300 seconds) cycle_period = CYCLE_PERIOD_3_S -# Which particle sensor, if any, is attached -# (PARTICLE_SENSOR_X with X = PPD42, SDS011, or OFF) -particleSensor = PARTICLE_SENSOR_OFF - # How to print the data: If print_data_as_columns = True, # data are columns of numbers, useful to copy/paste to a spreadsheet # application. Otherwise, data are printed with explanatory labels and units. @@ -37,8 +36,7 @@ (GPIO, I2C_bus) = SensorHardwareSetup() # Apply the chosen settings -if (particleSensor != PARTICLE_SENSOR_OFF): - I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor]) +I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR]) I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period]) ######################################################### @@ -56,37 +54,34 @@ # Now read and print all data # Air data - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES) - air_data = extractAirData(raw_data) + # Choose output temperature unit (C or F) in sensor_functions.py + air_data = get_air_data(I2C_bus) writeAirData(None, air_data, print_data_as_columns) # Air quality data # The initial self-calibration of the air quality data may take several # minutes to complete. During this time the accuracy parameter is zero # and the data values are not valid. - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_QUALITY_DATA_READ, AIR_QUALITY_DATA_BYTES) - air_quality_data = extractAirQualityData(raw_data) + air_quality_data = get_air_quality_data(I2C_bus) writeAirQualityData(None, air_quality_data, print_data_as_columns) # Light data - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, LIGHT_DATA_READ, LIGHT_DATA_BYTES) - light_data = extractLightData(raw_data) + light_data = get_light_data(I2C_bus) writeLightData(None, light_data, print_data_as_columns) # Sound data - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES) - sound_data = extractSoundData(raw_data) + sound_data = get_sound_data(I2C_bus) writeSoundData(None, sound_data, print_data_as_columns) # Particle data - # This requires the connection of a particulate sensor (invalid + # This requires the connection of a particulate sensor (zero/invalid # values will be obtained if this sensor is not present). + # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py # Also note that, due to the low pass filtering used, the # particle data become valid after an initial initialization # period of approximately one minute. - if (particleSensor != PARTICLE_SENSOR_OFF): - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, PARTICLE_DATA_READ, PARTICLE_DATA_BYTES) - particle_data = extractParticleData(raw_data, particleSensor) + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF): + particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR) writeParticleData(None, particle_data, print_data_as_columns) if print_data_as_columns: diff --git a/Python/Raspberry_Pi/graph_web_server.py b/Python/Raspberry_Pi/graph_web_server.py new file mode 100644 index 0000000..3357a55 --- /dev/null +++ b/Python/Raspberry_Pi/graph_web_server.py @@ -0,0 +1,136 @@ +# graph_web_server.py + +# Example code for serving a web page over a local network to display +# graphs showing environment data read from the Metriful MS430. A CSV +# data file is also downloadable from the page. +# This example is designed to run with Python 3 on a Raspberry Pi. + +# The web page can be viewed from other devices connected to the same +# network(s) as the host Raspberry Pi, including wired and wireless +# networks. + +# The browser which views the web page uses the Plotly javascript +# library to generate the graphs. This is automatically downloaded +# over the internet, or can be cached for offline use. If it is not +# available, graphs will not appear but text data and CSV downloads +# should still work. + +# NOTE: if you run, exit, then re-run this program, you may get an +# "Address already in use" error. This ends after a short period: wait +# one minute then retry. + +# Copyright 2020 Metriful Ltd. +# Licensed under the MIT License - for further details see LICENSE.txt + +# For code examples, datasheet and user guide, visit +# https://github.com/metriful/sensor + +import socketserver +from sensor_package.servers import * +from sensor_package.sensor_functions import * + +######################################################### +# USER-EDITABLE SETTINGS + +# Choose how often to read and update data (every 3, 100, or 300 seconds) +# The web page can be refreshed more often but the data will not change +cycle_period = CYCLE_PERIOD_100_S + +# The BUFFER_LENGTH parameter is the number of data points of each +# variable to store on the host. It is limited by the available host RAM. +buffer_length = 864 +# Examples: +# For 16 hour graphs, choose 100 second cycle period and 576 buffer length +# For 24 hour graphs, choose 300 second cycle period and 288 buffer length + + +# The web page address will be: +# http://:8080 e.g. http://172.24.1.1:8080 + +# Find your Raspberry Pi's IP address from the admin interface of your +# router, or: +# 1. Enter the command ifconfig in a terminal +# 2. Each available network connection displays a block of output +# 3. Ignore the "lo" output block +# 4. The host's IP address on each network is displayed after "inet" +# +# Example - part of an output block showing the address 172.24.1.1 +# +# wlan0: flags=4163 mtu 1500 +# inet 172.24.1.1 netmask 255.255.255.0 broadcast 172.24.1.255 + +# END OF USER-EDITABLE SETTINGS +######################################################### + +# Set up the GPIO and I2C communications bus +(GPIO, I2C_bus) = SensorHardwareSetup() + +# Apply the chosen settings to the MS430 +I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR]) +I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period]) + +# Get time period value to send to web page +if (cycle_period == CYCLE_PERIOD_3_S): + GraphWebpageHandler.data_period_seconds = 3 +elif (cycle_period == CYCLE_PERIOD_100_S): + GraphWebpageHandler.data_period_seconds = 100 +else: # CYCLE_PERIOD_300_S + GraphWebpageHandler.data_period_seconds = 300 + +# Set the number of each variable to be retained +GraphWebpageHandler.set_buffer_length(buffer_length) + +# Set the webpage to use: +GraphWebpageHandler.set_webpage_filename('sensor_package/graph_web_page.html') + +# Choose the TCP port number for the web page. +port = 8080 +# The port can be any unused number from 1-65535 but values below 1024 +# require this program to be run as super-user as follows: +# sudo python3 web_server.py +# Port 80 is the default for HTTP, and with this value the port number +# can be omitted from the web address. e.g. http://172.24.1.1 + +print("Starting the web server. Your web page will be available at:") +print("http://:" + str(port)) +print("Press ctrl-c to exit.") + +the_server = socketserver.TCPServer(("", port), GraphWebpageHandler) +the_server.timeout = 0.1 + +# Enter cycle mode to start periodic data output +I2C_bus.write_byte(i2c_7bit_address, CYCLE_MODE_CMD) + +while (True): + + # Respond to the web page client requests while waiting for new data + while (not GPIO.event_detected(READY_pin)): + the_server.handle_request() + sleep(0.05) + + # Now read all data from the MS430 and pass to the web page + + # Air data + GraphWebpageHandler.update_air_data(get_air_data(I2C_bus)) + + # Air quality data + # The initial self-calibration of the air quality data may take several + # minutes to complete. During this time the accuracy parameter is zero + # and the data values are not valid. + GraphWebpageHandler.update_air_quality_data(get_air_quality_data(I2C_bus)) + + # Light data + GraphWebpageHandler.update_light_data(get_light_data(I2C_bus)) + + # Sound data + GraphWebpageHandler.update_sound_data(get_sound_data(I2C_bus)) + + # Particle data + # This requires the connection of a particulate sensor (invalid + # values will be obtained if this sensor is not present). + # Also note that, due to the low pass filtering used, the + # particle data become valid after an initial initialization + # period of approximately one minute. + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF): + GraphWebpageHandler.update_particle_data(get_particle_data(I2C_bus, PARTICLE_SENSOR)) + diff --git a/Raspberry_Pi/interrupts.py b/Python/Raspberry_Pi/interrupts.py similarity index 95% rename from Raspberry_Pi/interrupts.py rename to Python/Raspberry_Pi/interrupts.py index 17204fd..c671d8a 100644 --- a/Raspberry_Pi/interrupts.py +++ b/Python/Raspberry_Pi/interrupts.py @@ -14,8 +14,7 @@ # For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -from time import sleep -from sensor_functions import * +from sensor_package.sensor_functions import * ######################################################### # USER-EDITABLE SETTINGS @@ -45,7 +44,8 @@ ######################################################### if ((light_thres_lux_i + (float(light_thres_lux_f2dp)/100.0)) > MAX_LUX_VALUE): - raise Exception('The chosen light interrupt threshold exceeds the maximum allowed value.') + raise Exception("The chosen light interrupt threshold exceeds the " + "maximum allowed value of " + str(MAX_LUX_VALUE) + " lux") # Set up the GPIO and I2C communications bus (GPIO, I2C_bus) = SensorHardwareSetup() diff --git a/Raspberry_Pi/log_data_to_file.py b/Python/Raspberry_Pi/log_data_to_file.py similarity index 71% rename from Raspberry_Pi/log_data_to_file.py rename to Python/Raspberry_Pi/log_data_to_file.py index 5021fc6..65ab8bd 100644 --- a/Raspberry_Pi/log_data_to_file.py +++ b/Python/Raspberry_Pi/log_data_to_file.py @@ -16,7 +16,7 @@ # https://github.com/metriful/sensor import datetime -from sensor_functions import * +from sensor_package.sensor_functions import * ######################################################### # USER-EDITABLE SETTINGS @@ -28,15 +28,12 @@ # Number of lines of data to log in each file before starting a new file # (required if log_to_file == True), and which directory to save them in. -lines_per_file = 3000 +lines_per_file = 300 data_file_directory = "/home/pi/Desktop" -# How often to measure and read data (every 3, 100, 300 seconds): +# How often to measure and read data (every 3, 100, or 300 seconds): cycle_period = CYCLE_PERIOD_3_S -# Which particle sensor, if any, is attached (PPD42, SDS011, or OFF) -particleSensor = PARTICLE_SENSOR_OFF - # END OF USER-EDITABLE SETTINGS ######################################################### @@ -44,8 +41,7 @@ (GPIO, I2C_bus) = SensorHardwareSetup() # Apply the chosen settings to the MS430 -if (particleSensor != PARTICLE_SENSOR_OFF): - I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor]) +I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR]) I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period]) ######################################################### @@ -65,50 +61,41 @@ while (not GPIO.event_detected(READY_pin)): sleep(0.05) - # Air data: - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES) - air_data = extractAirData(raw_data) + # Air data + # Choose output temperature unit (C or F) in sensor_functions.py + air_data = get_air_data(I2C_bus) # Air quality data # The initial self-calibration of the air quality data may take several # minutes to complete. During this time the accuracy parameter is zero # and the data values are not valid. - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_QUALITY_DATA_READ, AIR_QUALITY_DATA_BYTES) - air_quality_data = extractAirQualityData(raw_data) - - # Light data: - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, LIGHT_DATA_READ, LIGHT_DATA_BYTES) - light_data = extractLightData(raw_data) - - # Sound data: - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES) - sound_data = extractSoundData(raw_data) + air_quality_data = get_air_quality_data(I2C_bus) + # Light data + light_data = get_light_data(I2C_bus) + + # Sound data + sound_data = get_sound_data(I2C_bus) + # Particle data - # This requires the connection of a particulate sensor (invalid + # This requires the connection of a particulate sensor (zero/invalid # values will be obtained if this sensor is not present). + # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py # Also note that, due to the low pass filtering used, the # particle data become valid after an initial initialization # period of approximately one minute. - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, PARTICLE_DATA_READ, PARTICLE_DATA_BYTES) - particle_data = extractParticleData(raw_data, particleSensor) - + particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR) + if (print_to_screen): # Display all data on screen as named quantities with units print("") print("------------------"); writeAirData(None, air_data, False) - print("------------------"); writeAirQualityData(None, air_quality_data, False) - print("------------------"); writeLightData(None, light_data, False) - print("------------------"); writeSoundData(None, sound_data, False) - print("------------------"); - if (particleSensor != PARTICLE_SENSOR_OFF): + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF): writeParticleData(None, particle_data, False) - print("------------------"); - if (log_to_file): # Write the data as simple columns in a text file (without labels or @@ -123,7 +110,7 @@ writeLightData(datafile, light_data, True) # Sound data in columns 17 - 25 writeSoundData(datafile, sound_data, True) - if (particleSensor != PARTICLE_SENSOR_OFF): + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF): # Particle data in columns 26 - 28 writeParticleData(datafile, particle_data, True) datafile.write("\n") diff --git a/Raspberry_Pi/on_demand_readout.py b/Python/Raspberry_Pi/on_demand_readout.py similarity index 66% rename from Raspberry_Pi/on_demand_readout.py rename to Python/Raspberry_Pi/on_demand_readout.py index f600f25..a4ac230 100644 --- a/Raspberry_Pi/on_demand_readout.py +++ b/Python/Raspberry_Pi/on_demand_readout.py @@ -4,8 +4,11 @@ # This example is designed to run with Python 3 on a Raspberry Pi. # Repeatedly measures and displays all environment data, with a pause -# between measurements. Air quality data are unavailable in this mode -# (instead see cycle_readout.py). +# of any chosen duration between measurements. Air quality data are +# unavailable in this mode (instead use cycle_readout.py). + +# The measurements can be displayed as either labeled text, or as +# simple columns of numbers. # Copyright 2020 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt @@ -13,7 +16,7 @@ # For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -from sensor_functions import * +from sensor_package.sensor_functions import * ######################################################### # USER-EDITABLE SETTINGS @@ -24,10 +27,6 @@ # Choosing a pause of less than 2 seconds will cause inaccurate # temperature, humidity and particle data. -# Which particle sensor, if any, is attached -# (PARTICLE_SENSOR_X with X = PPD42, SDS011, or OFF) -particleSensor = PARTICLE_SENSOR_OFF - # How to print the data: If print_data_as_columns = True, # data are columns of numbers, useful to copy/paste to a spreadsheet # application. Otherwise, data are printed with explanatory labels and units. @@ -39,9 +38,7 @@ # Set up the GPIO and I2C communications bus (GPIO, I2C_bus) = SensorHardwareSetup() -# Apply the chosen settings -if (particleSensor != PARTICLE_SENSOR_OFF): - I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor]) +I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR]) ######################################################### @@ -52,38 +49,37 @@ # Trigger a new measurement I2C_bus.write_byte(i2c_7bit_address, ON_DEMAND_MEASURE_CMD) - # Wait for the next new data release, indicated by a falling edge on READY + # Wait for the next new data release, indicated by a falling edge on READY. + # This will take 0.5 seconds. while (not GPIO.event_detected(READY_pin)): sleep(0.05) # Now read and print all data # Air data - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES) - air_data = extractAirData(raw_data) + # Choose output temperature unit (C or F) in sensor_functions.py + air_data = get_air_data(I2C_bus) writeAirData(None, air_data, print_data_as_columns) # Air quality data are not available with on demand measurements # Light data - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, LIGHT_DATA_READ, LIGHT_DATA_BYTES) - light_data = extractLightData(raw_data) + light_data = get_light_data(I2C_bus) writeLightData(None, light_data, print_data_as_columns) # Sound data - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES) - sound_data = extractSoundData(raw_data) + sound_data = get_sound_data(I2C_bus) writeSoundData(None, sound_data, print_data_as_columns) # Particle data - # This requires the connection of a particulate sensor (invalid + # This requires the connection of a particulate sensor (zero/invalid # values will be obtained if this sensor is not present). + # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py # Also note that, due to the low pass filtering used, the # particle data become valid after an initial initialization # period of approximately one minute. - if (particleSensor != PARTICLE_SENSOR_OFF): - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, PARTICLE_DATA_READ, PARTICLE_DATA_BYTES) - particle_data = extractParticleData(raw_data, particleSensor) + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF): + particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR) writeParticleData(None, particle_data, print_data_as_columns) if print_data_as_columns: diff --git a/Raspberry_Pi/particle_sensor_toggle.py b/Python/Raspberry_Pi/particle_sensor_toggle.py similarity index 70% rename from Raspberry_Pi/particle_sensor_toggle.py rename to Python/Raspberry_Pi/particle_sensor_toggle.py index fcb9e79..dbf4bb9 100644 --- a/Raspberry_Pi/particle_sensor_toggle.py +++ b/Python/Raspberry_Pi/particle_sensor_toggle.py @@ -4,7 +4,7 @@ # control signal from one of the Pi pins, which can be used to turn # the particle sensor on and off. An external transistor circuit is # also needed - this will gate the sensor power supply according to -# the control signal. +# the control signal. Further details are given in the User Guide. # The program continually measures and displays all environment data # in a repeating cycle. The user can view the output in the Serial @@ -19,7 +19,7 @@ # For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -from sensor_functions import * +from sensor_package.sensor_functions import * ######################################################### # USER-EDITABLE SETTINGS @@ -29,17 +29,13 @@ # its data. cycle_period = CYCLE_PERIOD_100_S -# Which particle sensor, if any, is attached -# (PARTICLE_SENSOR_X with X = PPD42, SDS011, or OFF) -particleSensor = PARTICLE_SENSOR_SDS011 - # How to print the data: If print_data_as_columns = True, # data are columns of numbers, useful to copy/paste to a spreadsheet # application. Otherwise, data are printed with explanatory labels and units. print_data_as_columns = True # Particle sensor power control options -off_cycles = 1; # leave the sensor off for this many cycles between reads +off_cycles = 2; # leave the sensor off for this many cycles between reads particle_sensor_control_pin = 10; # Pi pin number which outputs the control signal # END OF USER-EDITABLE SETTINGS @@ -51,18 +47,17 @@ # Set up the particle sensor control, and turn it off initially GPIO.setup(particle_sensor_control_pin, GPIO.OUT) GPIO.output(particle_sensor_control_pin, 0) -particleSensorIsOn = False -particleSensor_count = 0 +particle_sensor_is_on = False +particle_sensor_count = 0 # Apply the chosen settings -if (particleSensor != PARTICLE_SENSOR_OFF): - I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor]) +I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR]) I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period]) ######################################################### sound_data = extractSoundData([0]*SOUND_DATA_BYTES) -particle_data = extractParticleData([0]*PARTICLE_DATA_BYTES, particleSensor) +particle_data = extractParticleData([0]*PARTICLE_DATA_BYTES, PARTICLE_SENSOR) print("Entering cycle mode and waiting for data. Press ctrl-c to exit.") @@ -78,38 +73,33 @@ # sound data will be printed if no reading is done on this loop. # Air data - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES) - air_data = extractAirData(raw_data) + air_data = get_air_data(I2C_bus) writeAirData(None, air_data, print_data_as_columns) # Air quality data # The initial self-calibration of the air quality data may take several # minutes to complete. During this time the accuracy parameter is zero # and the data values are not valid. - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_QUALITY_DATA_READ, AIR_QUALITY_DATA_BYTES) - air_quality_data = extractAirQualityData(raw_data) + air_quality_data = get_air_quality_data(I2C_bus) writeAirQualityData(None, air_quality_data, print_data_as_columns) # Light data - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, LIGHT_DATA_READ, LIGHT_DATA_BYTES) - light_data = extractLightData(raw_data) + light_data = get_light_data(I2C_bus) writeLightData(None, light_data, print_data_as_columns) - # Sound data - only read when particle sensor is off - if (not particleSensorIsOn): - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES) - sound_data = extractSoundData(raw_data) + # Sound data - only read new data when particle sensor is off + if (not particle_sensor_is_on): + sound_data = get_sound_data(I2C_bus) writeSoundData(None, sound_data, print_data_as_columns) # Particle data - # This requires the connection of a particulate sensor (invalid + # This requires the connection of a particulate sensor (zero/invalid # values will be obtained if this sensor is not present). # Also note that, due to the low pass filtering used, the # particle data become valid after an initial initialization # period of approximately one minute. - if (particleSensorIsOn): - raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, PARTICLE_DATA_READ, PARTICLE_DATA_BYTES) - particle_data = extractParticleData(raw_data, particleSensor) + if (particle_sensor_is_on): + particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR) writeParticleData(None, particle_data, print_data_as_columns) if print_data_as_columns: @@ -118,23 +108,23 @@ print("-------------------------------------------") #Turn the particle sensor on/off if required - if (particleSensorIsOn): + if (particle_sensor_is_on): # Stop the particle detection on the MS430 I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR_OFF]) # Turn off the hardware: GPIO.output(particle_sensor_control_pin, 0) - particleSensorIsOn = False + particle_sensor_is_on = False else: - particleSensor_count += 1 - if (particleSensor_count >= off_cycles): + particle_sensor_count += 1 + if (particle_sensor_count >= off_cycles): # Turn on the hardware: GPIO.output(particle_sensor_control_pin, 1) # Start the particle detection on the MS430 - I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [particleSensor]) + I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR]) - particleSensor_count = 0 - particleSensorIsOn = True + particle_sensor_count = 0 + particle_sensor_is_on = True diff --git a/Python/Raspberry_Pi/sensor_package/__init__.py b/Python/Raspberry_Pi/sensor_package/__init__.py new file mode 100644 index 0000000..d51ee2f --- /dev/null +++ b/Python/Raspberry_Pi/sensor_package/__init__.py @@ -0,0 +1 @@ +# This file remains empty but is needed by Python to create a package diff --git a/Python/Raspberry_Pi/sensor_package/graph_web_page.html b/Python/Raspberry_Pi/sensor_package/graph_web_page.html new file mode 100644 index 0000000..2259f91 --- /dev/null +++ b/Python/Raspberry_Pi/sensor_package/graph_web_page.html @@ -0,0 +1,333 @@ + + + + + +Indoor Environment Data + + + + + +

Indoor Environment Data

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

sensor.metriful.com

+
+ + + + diff --git a/Raspberry_Pi/sensor_constants.py b/Python/Raspberry_Pi/sensor_package/sensor_constants.py similarity index 100% rename from Raspberry_Pi/sensor_constants.py rename to Python/Raspberry_Pi/sensor_package/sensor_constants.py diff --git a/Raspberry_Pi/sensor_functions.py b/Python/Raspberry_Pi/sensor_package/sensor_functions.py similarity index 53% rename from Raspberry_Pi/sensor_functions.py rename to Python/Raspberry_Pi/sensor_package/sensor_functions.py index 021c26d..ab4ede8 100644 --- a/Raspberry_Pi/sensor_functions.py +++ b/Python/Raspberry_Pi/sensor_package/sensor_functions.py @@ -1,8 +1,11 @@ # sensor_functions.py -# This file defines functions and hardware pins which are used +# This file defines functions and hardware settings which are used # in the Metriful code examples on Raspberry Pi. +# Choose the preferred temperature measurement unit (Celsius or +# Fahrenheit) in this file, and select the optional particle sensor. + # Copyright 2020 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt @@ -15,12 +18,24 @@ import RPi.GPIO as GPIO import smbus import os -from sensor_constants import * +from .sensor_constants import * + +########################################################################################## + +# Choose to display output temperatures in Fahrenheit: +USE_FAHRENHEIT = False + +# Specify which particle sensor is connected: +PARTICLE_SENSOR = PARTICLE_SENSOR_OFF +# The possibilities are: +# PARTICLE_SENSOR_PPD42 for the Shinyei PPD42 +# PARTICLE_SENSOR_SDS011 for the Nova SDS011 +# PARTICLE_SENSOR_OFF if no sensor is connected ########################################################################################## # GPIO (input/output) header pin numbers: these must match the hardware wiring. -# The interrupt pins are not used in all examples. +# The light and sound interrupt pins are not used in all examples. light_int_pin = 7 # Raspberry Pi pin 7 connects to LIT sound_int_pin = 8 # Raspberry Pi pin 8 connects to SIT READY_pin = 11 # Raspberry Pi pin 11 connects to RDY @@ -49,6 +64,15 @@ ########################################################################################## +# Unicode symbol strings +CELSIUS_SYMBOL = "\u00B0C" +FAHRENHEIT_SYMBOL = "\u00B0F" +SDS011_CONC_SYMBOL = "\u00B5g/m\u00B3" # micrograms per cubic meter +SUBSCRIPT_2 = "\u2082" +OHM_SYMBOL = "\u03A9" + +########################################################################################## + def SensorHardwareSetup(): # Set up the Raspberry Pi GPIO GPIO.setwarnings(False) @@ -85,68 +109,111 @@ def SensorHardwareSetup(): def extractAirData(rawData): if (len(rawData) != AIR_DATA_BYTES): raise Exception('Incorrect number of Air Data bytes') - airData = {'T_C':0, 'P_Pa':0, 'H_pc':0, 'G_ohm':0} - airData['T_C'] = ((rawData[0] & TEMPERATURE_VALUE_MASK) + (float(rawData[1])/10.0)) + air_data = {'T_C':0, 'P_Pa':0, 'H_pc':0, 'G_ohm':0} + air_data['T_C'] = ((rawData[0] & TEMPERATURE_VALUE_MASK) + (float(rawData[1])/10.0)) if ((rawData[0] & TEMPERATURE_SIGN_MASK) != 0): # the most-significant bit is set, indicating that the temperature is negative - airData['T_C'] = -airData['T_C'] - airData['P_Pa'] = ((rawData[5] << 24) + (rawData[4] << 16) + (rawData[3] << 8) + rawData[2]) - airData['H_pc'] = rawData[6] + (float(rawData[7])/10.0) - airData['G_ohm'] = ((rawData[11] << 24) + (rawData[10] << 16) - + (rawData[9] << 8) + rawData[8]) - return airData + air_data['T_C'] = -air_data['T_C'] + air_data['T_F'] = convert_Celsius_to_Fahrenheit(air_data['T_C']) + air_data['P_Pa'] = ((rawData[5] << 24) + (rawData[4] << 16) + (rawData[3] << 8) + rawData[2]) + air_data['H_pc'] = rawData[6] + (float(rawData[7])/10.0) + air_data['G_ohm'] = ((rawData[11] << 24) + (rawData[10] << 16) + (rawData[9] << 8) + rawData[8]) + air_data['F_unit'] = FAHRENHEIT_SYMBOL + air_data['C_unit'] = CELSIUS_SYMBOL + if (USE_FAHRENHEIT): + air_data['T'] = air_data['T_F'] + air_data['T_unit'] = air_data['F_unit'] + else: + air_data['T'] = air_data['T_C'] + air_data['T_unit'] = air_data['C_unit'] + return air_data def extractAirQualityData(rawData): if (len(rawData) != AIR_QUALITY_DATA_BYTES): raise Exception('Incorrect number of Air Quality Data bytes') - airQualityData = {'AQI':0, 'CO2e':0, 'bVOC':0, 'AQI_accuracy':0} - airQualityData['AQI'] = rawData[0] + (rawData[1] << 8) + (float(rawData[2])/10.0) - airQualityData['CO2e'] = rawData[3] + (rawData[4] << 8) + (float(rawData[5])/10.0) - airQualityData['bVOC'] = rawData[6] + (rawData[7] << 8) + (float(rawData[8])/100.0) - airQualityData['AQI_accuracy'] = rawData[9] - return airQualityData + air_quality_data = {'AQI':0, 'CO2e':0, 'bVOC':0, 'AQI_accuracy':0} + air_quality_data['AQI'] = rawData[0] + (rawData[1] << 8) + (float(rawData[2])/10.0) + air_quality_data['CO2e'] = rawData[3] + (rawData[4] << 8) + (float(rawData[5])/10.0) + air_quality_data['bVOC'] = rawData[6] + (rawData[7] << 8) + (float(rawData[8])/100.0) + air_quality_data['AQI_accuracy'] = rawData[9] + return air_quality_data def extractLightData(rawData): if (len(rawData) != LIGHT_DATA_BYTES): raise Exception('Incorrect number of Light Data bytes supplied to function') - lightData = {'illum_lux':0, 'white':0} - lightData['illum_lux'] = rawData[0] + (rawData[1] << 8) + (float(rawData[2])/100.0) - lightData['white'] = rawData[3] + (rawData[4] << 8) - return lightData + light_data = {'illum_lux':0, 'white':0} + light_data['illum_lux'] = rawData[0] + (rawData[1] << 8) + (float(rawData[2])/100.0) + light_data['white'] = rawData[3] + (rawData[4] << 8) + return light_data def extractSoundData(rawData): if (len(rawData) != SOUND_DATA_BYTES): raise Exception('Incorrect number of Sound Data bytes supplied to function') - soundData = {'SPL_dBA':0, 'SPL_bands_dB':[0]*SOUND_FREQ_BANDS, 'peak_amp_mPa':0, 'stable':0} - soundData['SPL_dBA'] = rawData[0] + (float(rawData[1])/10.0) + sound_data = {'SPL_dBA':0, 'SPL_bands_dB':[0]*SOUND_FREQ_BANDS, 'peak_amp_mPa':0, 'stable':0} + sound_data['SPL_dBA'] = rawData[0] + (float(rawData[1])/10.0) j=2 for i in range(0,SOUND_FREQ_BANDS): - soundData['SPL_bands_dB'][i] = rawData[j] + (float(rawData[j+SOUND_FREQ_BANDS])/10.0) + sound_data['SPL_bands_dB'][i] = rawData[j] + (float(rawData[j+SOUND_FREQ_BANDS])/10.0) j+=1 j+=SOUND_FREQ_BANDS - soundData['peak_amp_mPa'] = rawData[j] + (rawData[j+1] << 8) + (float(rawData[j+2])/100.0) - soundData['stable'] = rawData[j+3] - return soundData + sound_data['peak_amp_mPa'] = rawData[j] + (rawData[j+1] << 8) + (float(rawData[j+2])/100.0) + sound_data['stable'] = rawData[j+3] + return sound_data def extractParticleData(rawData, particleSensor): + particle_data = {'duty_cycle_pc':0, 'concentration':0, 'conc_unit':"", 'valid':False} + if (particleSensor == PARTICLE_SENSOR_OFF): + return particle_data if (len(rawData) != PARTICLE_DATA_BYTES): raise Exception('Incorrect number of Particle Data bytes supplied to function') - particleData = {'duty_cycle_pc':0, 'concentration':0, 'conc_unit':"", 'valid':False} - particleData['duty_cycle_pc'] = rawData[0] + (float(rawData[1])/100.0) - particleData['concentration'] = rawData[2] + (rawData[3] << 8) + (float(rawData[4])/100.0) + particle_data['duty_cycle_pc'] = rawData[0] + (float(rawData[1])/100.0) + particle_data['concentration'] = rawData[2] + (rawData[3] << 8) + (float(rawData[4])/100.0) if (rawData[5] > 0): - particleData['valid'] = True + particle_data['valid'] = True if (particleSensor == PARTICLE_SENSOR_PPD42): - particleData['conc_unit'] = "ppL" + particle_data['conc_unit'] = "ppL" elif (particleSensor == PARTICLE_SENSOR_SDS011): - particleData['conc_unit'] = "ug/m3" - else: - particleData['conc_unit'] = "(?)" - return particleData + particle_data['conc_unit'] = SDS011_CONC_SYMBOL + return particle_data + +########################################################################################## + +# Convenience functions: each does a data category read from the MS430 +# and then converts the raw data into a python dictionary, which is +# returned from the function + +def get_air_data(I2C_bus): + raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES) + return extractAirData(raw_data) + +def get_air_quality_data(I2C_bus): + raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_QUALITY_DATA_READ, AIR_QUALITY_DATA_BYTES) + return extractAirQualityData(raw_data) + +def get_light_data(I2C_bus): + raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, LIGHT_DATA_READ, LIGHT_DATA_BYTES) + return extractLightData(raw_data) + +def get_sound_data(I2C_bus): + raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES) + return extractSoundData(raw_data) + +def get_particle_data(I2C_bus, particleSensor): + raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, PARTICLE_DATA_READ, PARTICLE_DATA_BYTES) + return extractParticleData(raw_data, particleSensor) + +########################################################################################## + +# Function to convert Celsius temperature to Fahrenheit. This is used +# just before outputting the temperature value, if the variable +# USE_FAHRENHEIT is True + +def convert_Celsius_to_Fahrenheit(temperature_C): + return ((temperature_C*1.8) + 32.0) ########################################################################################## @@ -154,13 +221,13 @@ def extractParticleData(rawData, particleSensor): # the air quality measurements (applies to all air quality data) def interpret_AQI_accuracy(AQI_accuracy_code): if (AQI_accuracy_code == 1): - return "Low Accuracy, Self-calibration Ongoing"; + return "Low accuracy, self-calibration ongoing"; elif (AQI_accuracy_code == 2): - return "Medium Accuracy, Self-calibration Ongoing"; + return "Medium accuracy, self-calibration ongoing"; elif (AQI_accuracy_code == 3): - return "High Accuracy"; + return "High accuracy"; else: - return "Not Yet Valid, Self-calibration Incomplete"; + return "Not yet valid, self-calibration incomplete"; # Provide a readable interpretation of the AQI (air quality index) @@ -176,7 +243,7 @@ def interpret_AQI_value(AQI): elif (AQI < 300): return "Bad" else: - return "Very Bad" + return "Very bad" ########################################################################################## @@ -196,91 +263,92 @@ def interpret_AQI_value(AQI): # Air data column order is: # Temperature/C, Pressure/Pa, Humidity/%RH, Gas sensor resistance/ohm -def writeAirData(textFileObject, airData, writeAsColumns): +def writeAirData(textFileObject, air_data, writeAsColumns): if (textFileObject is None): textFileObject = sys.stdout if (writeAsColumns): - textFileObject.write("{:.1f} ".format(airData['T_C'])) - textFileObject.write(str(airData['P_Pa']) + " ") - textFileObject.write("{:.1f} ".format(airData['H_pc'])) - textFileObject.write(str(airData['G_ohm']) + " ") + textFileObject.write("{:.1f} ".format(air_data['T'])) + textFileObject.write(str(air_data['P_Pa']) + " ") + textFileObject.write("{:.1f} ".format(air_data['H_pc'])) + textFileObject.write(str(air_data['G_ohm']) + " ") else: - textFileObject.write("Temperature = {:.1f} C\n".format(airData['T_C'])) - textFileObject.write("Pressure = " + str(airData['P_Pa']) + " Pa\n") - textFileObject.write("Humidity = {:.1f} %\n".format(airData['H_pc'])) - textFileObject.write("Gas Sensor Resistance = " + str(airData['G_ohm']) + " ohm\n") + textFileObject.write("Temperature = {:.1f} ".format(air_data['T']) + air_data['T_unit'] + "\n") + textFileObject.write("Pressure = " + str(air_data['P_Pa']) + " Pa\n") + textFileObject.write("Humidity = {:.1f} %\n".format(air_data['H_pc'])) + textFileObject.write("Gas Sensor Resistance = " + str(air_data['G_ohm']) + " " + OHM_SYMBOL + "\n") # Air quality data column order is: # Air Quality Index, Estimated CO2/ppm, Equivalent breath VOC/ppm, Accuracy -def writeAirQualityData(textFileObject, airQualityData, writeAsColumns): +def writeAirQualityData(textFileObject, air_quality_data, writeAsColumns): if (textFileObject is None): textFileObject = sys.stdout if (writeAsColumns): - textFileObject.write("{:.1f} ".format(airQualityData['AQI'])) - textFileObject.write("{:.1f} ".format(airQualityData['CO2e'])) - textFileObject.write("{:.2f} ".format(airQualityData['bVOC'])) - textFileObject.write(str(airQualityData['AQI_accuracy']) + " ") + textFileObject.write("{:.1f} ".format(air_quality_data['AQI'])) + textFileObject.write("{:.1f} ".format(air_quality_data['CO2e'])) + textFileObject.write("{:.2f} ".format(air_quality_data['bVOC'])) + textFileObject.write(str(air_quality_data['AQI_accuracy']) + " ") else: - if (airQualityData['AQI_accuracy'] > 0): - textFileObject.write("Air Quality Index = {:.1f}".format(airQualityData['AQI']) - + " (" + interpret_AQI_value(airQualityData['AQI']) + ")\n") - textFileObject.write("Estimated CO2 = {:.1f} ppm\n".format(airQualityData['CO2e'])) - textFileObject.write("Equivalent Breath VOC = {:.2f} ppm\n".format(airQualityData['bVOC'])) + if (air_quality_data['AQI_accuracy'] > 0): + textFileObject.write("Air Quality Index = {:.1f}".format(air_quality_data['AQI']) + + " (" + interpret_AQI_value(air_quality_data['AQI']) + ")\n") + textFileObject.write("Estimated CO" + SUBSCRIPT_2 + + " = {:.1f} ppm\n".format(air_quality_data['CO2e'])) + textFileObject.write("Equivalent Breath VOC = {:.2f} ppm\n".format(air_quality_data['bVOC'])) textFileObject.write("Air Quality Accuracy: " + - interpret_AQI_accuracy(airQualityData['AQI_accuracy']) + "\n") + interpret_AQI_accuracy(air_quality_data['AQI_accuracy']) + "\n") # Light data column order is: # Illuminance/lux, white light level -def writeLightData(textFileObject, lightData, writeAsColumns): +def writeLightData(textFileObject, light_data, writeAsColumns): if (textFileObject is None): textFileObject = sys.stdout if (writeAsColumns): - textFileObject.write("{:.2f} ".format(lightData['illum_lux'])) - textFileObject.write(str(lightData['white']) + " ") + textFileObject.write("{:.2f} ".format(light_data['illum_lux'])) + textFileObject.write(str(light_data['white']) + " ") else: - textFileObject.write("Illuminance = {:.2f} lux\n".format(lightData['illum_lux'])) - textFileObject.write("White Light Level = " + str(lightData['white']) + "\n") + textFileObject.write("Illuminance = {:.2f} lux\n".format(light_data['illum_lux'])) + textFileObject.write("White Light Level = " + str(light_data['white']) + "\n") # Sound data column order is: # Sound pressure level/dBA, Sound pressure level for frequency bands 1 to 6 (six columns), # Peak sound amplitude/mPa, stability -def writeSoundData(textFileObject, soundData, writeAsColumns): +def writeSoundData(textFileObject, sound_data, writeAsColumns): if (textFileObject is None): textFileObject = sys.stdout if (writeAsColumns): - textFileObject.write("{:.1f} ".format(soundData['SPL_dBA'])) + textFileObject.write("{:.1f} ".format(sound_data['SPL_dBA'])) for i in range(0,SOUND_FREQ_BANDS): - textFileObject.write("{:.1f} ".format(soundData['SPL_bands_dB'][i])) - textFileObject.write("{:.2f} ".format(soundData['peak_amp_mPa'])) - textFileObject.write(str(soundData['stable']) + " ") + textFileObject.write("{:.1f} ".format(sound_data['SPL_bands_dB'][i])) + textFileObject.write("{:.2f} ".format(sound_data['peak_amp_mPa'])) + textFileObject.write(str(sound_data['stable']) + " ") else: - textFileObject.write("A-weighted Sound Pressure Level = {:.1f} dBA\n".format(soundData['SPL_dBA'])) + textFileObject.write("A-weighted Sound Pressure Level = {:.1f} dBA\n".format(sound_data['SPL_dBA'])) for i in range(0,SOUND_FREQ_BANDS): textFileObject.write("Frequency Band " + str(i+1) + " (" + str(sound_band_mids_Hz[i]) - + " Hz) SPL = {:.1f} dB\n".format(soundData['SPL_bands_dB'][i])) - textFileObject.write("Peak Sound Amplitude = {:.2f} mPa\n".format(soundData['peak_amp_mPa'])) + + " Hz) SPL = {:.1f} dB\n".format(sound_data['SPL_bands_dB'][i])) + textFileObject.write("Peak Sound Amplitude = {:.2f} mPa\n".format(sound_data['peak_amp_mPa'])) # Particle data column order is: # Sensor duty cycle/%, particle concentration -def writeParticleData(textFileObject, particleData, writeAsColumns): +def writeParticleData(textFileObject, particle_data, writeAsColumns): if (textFileObject is None): textFileObject = sys.stdout if (writeAsColumns): - textFileObject.write("{:.2f} ".format(particleData['duty_cycle_pc'])) - textFileObject.write("{:.2f} ".format(particleData['concentration'])) - if (particleData['valid']): + textFileObject.write("{:.2f} ".format(particle_data['duty_cycle_pc'])) + textFileObject.write("{:.2f} ".format(particle_data['concentration'])) + if (particle_data['valid']): textFileObject.write("1 ") else: textFileObject.write("0 ") else: - textFileObject.write("Particle Sensor Duty Cycle = {:.2f} %\n".format(particleData['duty_cycle_pc'])) - textFileObject.write("Particle Concentration = {:.2f} ".format(particleData['concentration'])) - textFileObject.write(particleData['conc_unit'] + "\n") - if (particleData['valid'] == 0): + textFileObject.write("Particle Sensor Duty Cycle = {:.2f} %\n".format(particle_data['duty_cycle_pc'])) + textFileObject.write("Particle Concentration = {:.2f} ".format(particle_data['concentration'])) + textFileObject.write(particle_data['conc_unit'] + "\n") + if (particle_data['valid'] == 0): textFileObject.write("Particle data valid: No (Initializing)\n") else: textFileObject.write("Particle data valid: Yes\n") diff --git a/Python/Raspberry_Pi/sensor_package/servers.py b/Python/Raspberry_Pi/sensor_package/servers.py new file mode 100644 index 0000000..cede269 --- /dev/null +++ b/Python/Raspberry_Pi/sensor_package/servers.py @@ -0,0 +1,212 @@ +# servers.py + +# This file contains HTTP request handler classes which are used in the +# web_server.py and graph_web_server.py 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 + +import http.server +from collections import deque +import struct +from .sensor_functions import * + +########################################################################################## + +# A class for making a simple text web page showing the environment data in +# separate category tables, using HTML and CSS. This is used in web_server.py + +class SimpleWebpageHandler(http.server.SimpleHTTPRequestHandler): + the_web_page = "" + air_data = None + air_quality_data = None + sound_data = None + light_data = None + particle_data = None + refresh_period_seconds = 3 + + def do_GET(self): + self.wfile.write(bytes(self.the_web_page, "utf8")) + + @classmethod + def assemble_web_page(cls): + cls.the_web_page = ("HTTP/1.1 200 OK\r\n" + "Content-type:text/html\r\n" + "Connection: close\r\n" + "Refresh: {}\r\n\r\n".format(cls.refresh_period_seconds) + + "" + "Metriful Sensor Demo" + "" + "

Indoor Environment Data

") + + if (cls.air_data != None): + cls.the_web_page += "

Air Data

" + cls.the_web_page += ("") + cls.the_web_page += ("" + "".format(cls.air_data['P_Pa']) + + "".format(cls.air_data['H_pc']) + + "".format(cls.air_data['G_ohm']) + + "
Temperature" + "{:.1f}".format(cls.air_data['T']) + cls.air_data['T_unit'] + "
Pressure{}Pa
Humidity{:.1f}%
Gas Sensor Resistance{}" + OHM_SYMBOL + "

") + + if (cls.air_quality_data != None): + cls.the_web_page += "

Air Quality Data

" + if (cls.air_quality_data['AQI_accuracy'] == 0): + # values are not valid yet + cls.the_web_page += ("" + interpret_AQI_accuracy(cls.air_quality_data['AQI_accuracy']) + + "

") + else: + cls.the_web_page += ("" + "".format(cls.air_quality_data['AQI']) + + "" + "" + "".format(cls.air_quality_data['CO2e']) + + "
Air Quality Index{:.1f}
Air Quality Summary" + + interpret_AQI_value(cls.air_quality_data['AQI']) + "
Estimated CO" + SUBSCRIPT_2 + "{:.1f}ppm
Equivalent Breath VOC{:.2f}".format(cls.air_quality_data['bVOC']) + + "ppm

") + + if (cls.sound_data != None): + cls.the_web_page += ("

Sound Data

" + "".format(cls.sound_data['SPL_dBA'])) + for i in range(0,SOUND_FREQ_BANDS): + cls.the_web_page += ("".format(i+1, sound_band_mids_Hz[i]) + + "".format(cls.sound_data['SPL_bands_dB'][i])) + cls.the_web_page += ("" + "
A-weighted Sound Pressure Level" + "{:.1f}dBA
Frequency Band " + "{} ({} Hz) SPL{:.1f}dB
Peak Sound Amplitude{:.2f}mPa

".format(cls.sound_data['peak_amp_mPa'])) + + if (cls.light_data != None): + cls.the_web_page += ("

Light Data

" + "".format(cls.light_data['illum_lux']) + + "" + "".format(cls.light_data['white']) + + "
Illuminance{:.2f}lux
White Light Level{}

") + + if (cls.particle_data != None): + cls.the_web_page += ("

Air Particulate Data

" + "" + "".format(cls.particle_data['duty_cycle_pc']) + + "" + "
Sensor Duty Cycle{:.2f}%
Particle Concentration{:.2f}".format(cls.particle_data['concentration']) + + "" + cls.particle_data['conc_unit'] + "

") + + cls.the_web_page += "" + + +########################################################################################## + +# A class for making a web page with graphs to display environment data, using +# the Plotly.js libray, javascript, HTML and CSS. This is used in graph_web_server.py + +class GraphWebpageHandler(http.server.SimpleHTTPRequestHandler): + data_period_seconds = 3 + error_response_HTTP = "HTTP/1.1 400 Bad Request\r\n\r\n" + data_header = ("HTTP/1.1 200 OK\r\n" + "Content-type: application/octet-stream\r\n" + "Connection: close\r\n\r\n") + page_header = ("HTTP/1.1 200 OK\r\n" + "Content-type: text/html\r\n" + "Connection: close\r\n\r\n") + + # Respond to an HTTP GET request (no other methods are supported) + def do_GET(self): + if (self.path == '/'): + # The web page is requested + self.wfile.write(bytes(self.page_header, "utf8")) + with open(self.webpage_filename, 'rb') as fileObj: + for data in fileObj: + self.wfile.write(data) + fileObj.close() + elif (self.path == '/1'): + # A URI path of '1' indicates a request of all buffered data + self.send_all_data() + elif (self.path == '/2'): + # A URI path of '2' indicates a request of the latest data only + self.send_latest_data() + else: + # Path not recognized: send a standard error response + self.wfile.write(bytes(self.error_response_HTTP, "utf8")) + + + def send_all_data(self): + self.wfile.write(bytes(self.data_header, "utf8")) + # First send the time period, so the web page knows when to do the next request + self.wfile.write(struct.pack('H', self.data_period_seconds)) + # Send temperature unit and particle sensor type, combined into one byte + codeByte = PARTICLE_SENSOR + if USE_FAHRENHEIT: + codeByte = codeByte | 0x10 + self.wfile.write(struct.pack('B', codeByte)) + # Send the length of the data buffers (the number of values of each variable) + self.wfile.write(struct.pack('H', len(self.temperature))) + # Send the data: + for p in [self.AQI, self.temperature, self.pressure, self.humidity, + self.SPL, self.illuminance, self.bVOC, self.particle]: + self.wfile.write(struct.pack(str(len(p)) + 'f', *p)) + + + def send_latest_data(self): + self.wfile.write(bytes(self.data_header, "utf8")) + # Send the most recent value for each variable, if buffers are not empty + if (len(self.temperature) > 0): + data = [self.AQI[-1], self.temperature[-1], self.pressure[-1], self.humidity[-1], + self.SPL[-1], self.illuminance[-1], self.bVOC[-1]] + if (len(self.particle) > 0): + data.append(self.particle[-1]) + self.wfile.write(struct.pack(str(len(data)) + 'f', *data)) + + + @classmethod + def set_webpage_filename(self, filename): + self.webpage_filename = filename + + @classmethod + def set_buffer_length(cls, buffer_length): + cls.temperature = deque(maxlen=buffer_length) + cls.pressure = deque(maxlen=buffer_length) + cls.humidity = deque(maxlen=buffer_length) + cls.AQI = deque(maxlen=buffer_length) + cls.bVOC = deque(maxlen=buffer_length) + cls.SPL = deque(maxlen=buffer_length) + cls.illuminance = deque(maxlen=buffer_length) + cls.particle = deque(maxlen=buffer_length) + + @classmethod + def update_air_data(cls, air_data): + cls.temperature.append(air_data['T']) + cls.pressure.append(air_data['P_Pa']) + cls.humidity.append(air_data['H_pc']) + + @classmethod + def update_air_quality_data(cls, air_quality_data): + cls.AQI.append(air_quality_data['AQI']) + cls.bVOC.append(air_quality_data['bVOC']) + + @classmethod + def update_light_data(cls, light_data): + cls.illuminance.append(light_data['illum_lux']) + + @classmethod + def update_sound_data(cls, sound_data): + cls.SPL.append(sound_data['SPL_dBA']) + + @classmethod + def update_particle_data(cls, particle_data): + cls.particle.append(particle_data['concentration']) + diff --git a/Python/Raspberry_Pi/simple_read_T_H.py b/Python/Raspberry_Pi/simple_read_T_H.py new file mode 100644 index 0000000..c2688f9 --- /dev/null +++ b/Python/Raspberry_Pi/simple_read_T_H.py @@ -0,0 +1,99 @@ +# simple_read_T_H.py + +# Example code for using the Metriful MS430 to measure humidity and +# temperature. +# This example is designed to run with Python 3 on a Raspberry Pi. + +# Demonstrates multiple ways of reading and displaying the temperature +# and humidity data. View the output in the terminal. The other data +# can be measured and displayed in a similar way. + +# Copyright 2020 Metriful Ltd. +# Licensed under the MIT License - for further details see LICENSE.txt + +# For code examples, datasheet and user guide, visit +# https://github.com/metriful/sensor + +from sensor_package.sensor_functions import * + +# Set up the GPIO and I2C communications bus +(GPIO, I2C_bus) = SensorHardwareSetup() + +# Initiate an on-demand data measurement +I2C_bus.write_byte(i2c_7bit_address, ON_DEMAND_MEASURE_CMD) + +# Now wait for the ready signal (falling edge) before continuing +while (not GPIO.event_detected(READY_pin)): + sleep(0.05) + +# New data are now ready to read. + +######################################################### + +# There are multiple ways to read and display the data + + +# 1. Simplest way: use the example functions + +# Read all the "air data" from the MS430. This includes temperature and +# humidity as well as pressure and gas sensor data. Return the data as +# a data dictionary. +air_data = get_air_data(I2C_bus) + +# Then print all the values onto the screen +writeAirData(None, air_data, False) + +# Or you can use the values directly +print("The temperature is: {:.1f} ".format(air_data['T_C']) + air_data['C_unit']) +print("The humidity is: {:.1f} %".format(air_data['H_pc'])) + +# Temperature can also be output in Fahrenheit units +print("The temperature is: {:.1f} ".format(air_data['T_F']) + air_data['F_unit']) + +# The default temperature unit can be set in sensor_functions.py and used like: +print("The temperature is: {:.1f} ".format(air_data['T']) + air_data['T_unit']) + +print("-----------------------------") + +# 2. Advanced: read and decode only the humidity value + +# Get the data from the MS430 +raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, H_READ, H_BYTES) + +# Decode the humidity: the first received byte is the integer part, the +# second byte is the fractional part (to one decimal place). +humidity = raw_data[0] + float(raw_data[1])/10.0 + +# Print it: the units are percentage relative humidity. +print("Humidity = {:.1f} %".format(humidity)) + +print("-----------------------------") + +# 3. Advanced: read and decode only the temperature value (Celsius) + +# Get the data from the MS430 +raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, T_READ, T_BYTES) + +# Find the positive magnitude of the integer part of the temperature by +# doing a bitwise AND of the first received byte with TEMPERATURE_VALUE_MASK +temp_positive_integer = raw_data[0] & TEMPERATURE_VALUE_MASK + +# The second received byte is the fractional part to one decimal place +temp_fraction = raw_data[1] + +# Combine to form a positive floating point number: +temperature = temp_positive_integer + float(temp_fraction)/10.0 + +# Now find the sign of the temperature: if the most-significant bit of +# the first byte is a 1, the temperature is negative (below 0 C) +if ((raw_data[0] & TEMPERATURE_SIGN_MASK) != 0): + # The bit is a 1: temperature is negative + temperature = -temperature + +# Print the temperature: the units are degrees Celsius. +print("Temperature = {:.1f} ".format(temperature) + CELSIUS_SYMBOL) + +######################################################### + +GPIO.cleanup() + diff --git a/Raspberry_Pi/simple_read_sound.py b/Python/Raspberry_Pi/simple_read_sound.py similarity index 57% rename from Raspberry_Pi/simple_read_sound.py rename to Python/Raspberry_Pi/simple_read_sound.py index 1aadd3a..69d604a 100644 --- a/Raspberry_Pi/simple_read_sound.py +++ b/Python/Raspberry_Pi/simple_read_sound.py @@ -3,7 +3,9 @@ # Example code for using the Metriful MS430 to measure sound. # This example is designed to run with Python 3 on a Raspberry Pi. -# Measures and displays the sound data once. +# Demonstrates multiple ways of reading and displaying the sound data. +# View the output in the terminal. The other data can be measured +# and displayed in a similar way. # Copyright 2020 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt @@ -11,8 +13,7 @@ # For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -from time import sleep -from sensor_functions import * +from sensor_package.sensor_functions import * # Set up the GPIO and I2C communications bus (GPIO, I2C_bus) = SensorHardwareSetup() @@ -32,27 +33,35 @@ while (not GPIO.event_detected(READY_pin)): sleep(0.05) -# We now know that newly measured data are ready to read. +# New data are now ready to read. ######################################################### -# SOUND DATA +# There are multiple ways to read and display the data + + +# 1. Simplest way: use the example functions + +# Read all sound data from the MS430 and convert to a Python dictionary +sound_data = get_sound_data(I2C_bus) + +# Then print all the values onto the screen +writeSoundData(None, sound_data, False) -# Read all sound data in one transaction +# Or you can use the dictionary values directly, for example: +print("The sound pressure level is: " + str(sound_data['SPL_dBA']) + " dBA") + + +# 2. Read the raw data bytes from the MS430 using an I2C function raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, SOUND_DATA_READ, SOUND_DATA_BYTES) -# Use the example function to decode the values and return then as a Python dictionary +# Decode the values and return then as a Python dictionary sound_data = extractSoundData(raw_data) -# Print the values obtained -print("A-weighted sound pressure level = {:.1f} dBA".format(sound_data['SPL_dBA'])) -for i in range(0,SOUND_FREQ_BANDS): - print("Frequency band " + str(i+1) + " (" + str(sound_band_mids_Hz[i]) - + " Hz) SPL = {:.1f} dB".format(sound_data['SPL_bands_dB'][i])) -print("Peak sound amplitude = {:.2f} mPa".format(sound_data['peak_amp_mPa'])) - -# Or just use the following function for printing: +# Print the dictionary values in the same ways as before writeSoundData(None, sound_data, False) +print("The sound pressure level is: " + str(sound_data['SPL_dBA']) + " dBA") + ######################################################### diff --git a/Python/Raspberry_Pi/web_server.py b/Python/Raspberry_Pi/web_server.py new file mode 100644 index 0000000..a640b07 --- /dev/null +++ b/Python/Raspberry_Pi/web_server.py @@ -0,0 +1,123 @@ +# web_server.py + +# Example code for serving a text web page over a local network to +# display environment data read from the Metriful MS430. +# This example is designed to run with Python 3 on a Raspberry Pi. + +# All environment data values are measured and displayed on a text +# web page generated by this program acting as a simple web server. +# The web page can be viewed from other devices connected to the same +# network(s) as the host Raspberry Pi, including wired and wireless +# networks. + +# NOTE: if you run, exit, then re-run this program, you may get an +# "Address already in use" error. This ends after a short period: wait +# one minute then retry. + +# Copyright 2020 Metriful Ltd. +# Licensed under the MIT License - for further details see LICENSE.txt + +# For code examples, datasheet and user guide, visit +# https://github.com/metriful/sensor + +import socketserver +from sensor_package.servers import * +from sensor_package.sensor_functions import * + +######################################################### +# USER-EDITABLE SETTINGS + +# Choose how often to read and update data (every 3, 100, or 300 seconds) +# The web page can be refreshed more often but the data will not change +cycle_period = CYCLE_PERIOD_3_S + +# The web page address will be: +# http://:8080 e.g. http://172.24.1.1:8080 + +# Find your Raspberry Pi's IP address from the admin interface of your +# router, or: +# 1. Enter the command ifconfig in a terminal +# 2. Each available network connection displays a block of output +# 3. Ignore the "lo" output block +# 4. The host's IP address on each network is displayed after "inet" +# +# Example - part of an output block showing the address 172.24.1.1 +# +# wlan0: flags=4163 mtu 1500 +# inet 172.24.1.1 netmask 255.255.255.0 broadcast 172.24.1.255 + +# END OF USER-EDITABLE SETTINGS +######################################################### + +# Set up the GPIO and I2C communications bus +(GPIO, I2C_bus) = SensorHardwareSetup() + +# Apply the chosen settings to the MS430 +I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR]) +I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period]) + +# Set the automatic refresh period of the web page. It should refresh +# at least as often as new data are obtained. A more frequent refresh is +# best for long cycle periods because the page access will be +# out-of-step with the cycle. Users can also manually refresh the page. +if (cycle_period == CYCLE_PERIOD_3_S): + SimpleWebpageHandler.refresh_period_seconds = 3 +elif (cycle_period == CYCLE_PERIOD_100_S): + SimpleWebpageHandler.refresh_period_seconds = 30 +else: # CYCLE_PERIOD_300_S + SimpleWebpageHandler.refresh_period_seconds = 50 + +# Choose the TCP port number for the web page. +port = 8080 +# The port can be any unused number from 1-65535 but values below 1024 +# require this program to be run as super-user as follows: +# sudo python3 web_server.py +# Port 80 is the default for HTTP, and with this value the port number +# can be omitted from the web address. e.g. http://172.24.1.1 + +print("Starting the web server. Your web page will be available at:") +print("http://:" + str(port)) +print("Press ctrl-c to exit.") + +the_server = socketserver.TCPServer(("", port), SimpleWebpageHandler) +the_server.timeout = 0.1 + +# Enter cycle mode to start periodic data output +I2C_bus.write_byte(i2c_7bit_address, CYCLE_MODE_CMD) + +while (True): + + # While waiting for the next data release, respond to client requests + # by serving the web page with the last available data. + while (not GPIO.event_detected(READY_pin)): + the_server.handle_request() + sleep(0.05) + + # Now read all data from the MS430 and pass to the web page + + # Air data + SimpleWebpageHandler.air_data = get_air_data(I2C_bus) + + # Air quality data + # The initial self-calibration of the air quality data may take several + # minutes to complete. During this time the accuracy parameter is zero + # and the data values are not valid. + SimpleWebpageHandler.air_quality_data = get_air_quality_data(I2C_bus) + + # Light data + SimpleWebpageHandler.light_data = get_light_data(I2C_bus) + + # Sound data + SimpleWebpageHandler.sound_data = get_sound_data(I2C_bus) + + # Particle data + # This requires the connection of a particulate sensor (invalid + # values will be obtained if this sensor is not present). + # Also note that, due to the low pass filtering used, the + # particle data become valid after an initial initialization + # period of approximately one minute. + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF): + SimpleWebpageHandler.particle_data = get_particle_data(I2C_bus, PARTICLE_SENSOR) + + # Create the web page ready for client requests + SimpleWebpageHandler.assemble_web_page() diff --git a/Python/graph_viewer_I2C.py b/Python/graph_viewer_I2C.py new file mode 100644 index 0000000..9ad9235 --- /dev/null +++ b/Python/graph_viewer_I2C.py @@ -0,0 +1,159 @@ +# graph_viewer_I2C.py + +# NOTE on operating system/platform: + +# This example runs on Raspberry Pi only, and the MS430 sensor board +# must be connected to the Raspberry Pi I2C/GPIO pins. + +# An alternate version, "graph_viewer_serial.py" runs on multiple operating +# systems (including Windows and Linux) and uses serial over USB to get +# data from the MS430 sensor via a microcontroller board (e.g. Arduino, +# ESP8266, etc). + +######################################################### + +# This example displays a graphical user interface with real-time +# updating graphs showing data from the MS430 sensor board. + +# Installation instructions for the necessary packages are in the +# readme and User Guide. + +# Copyright 2020 Metriful Ltd. +# Licensed under the MIT License - for further details see LICENSE.txt + +# For code examples, datasheet and user guide, visit +# https://github.com/metriful/sensor + +import datetime +from GraphViewer import * +from Raspberry_Pi.sensor_package.sensor_functions import * + +######################################################### +# USER-EDITABLE SETTINGS + +# Choose the delay between data measurements. This can be 3/100/300 seconds +# in cycle mode, or any delay time in on-demand mode + +# Set cycle_period_code=None to use on-demand mode +cycle_period_code = None # CYCLE_PERIOD_3_S, CYCLE_PERIOD_100_S, CYCLE_PERIOD_300_S, or None +# OR: +on_demand_delay_ms = 0 # Choose any number of milliseconds +# This delay is in addition to the 0.5 second readout time + +# Temperature and particle data are less accurate if read more +# frequently than every 2 seconds + +# Maximum number of values of each variable to store and display: +data_buffer_length = 500 + +# Specify the particle sensor model (PPD42/SDS011/none) and temperature +# units (Celsuis/Fahrenheit) in Raspberry_Pi/sensor_functions.py + +# END OF USER-EDITABLE SETTINGS +######################################################### + +class GraphViewerI2C(GraphViewer): + def __init__(self, buffer_length, cycle_period, OD_delay_ms): + super(GraphViewerI2C, self).__init__(buffer_length) + # Set up the I2C and the MS430 board + (self.GPIO, self.I2C_bus) = SensorHardwareSetup() + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF): + self.I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR]) + self.get_particle_data = True + if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42): + self.names_units['Particle concentration'] = self.PPD_unit + else: + self.names_units['Particle concentration'] = self.SDS_unit + else: + self.get_particle_data = False + if ((cycle_period is None) and (OD_delay_ms is None)): + raise Exception("Either cycle_period or OD_delay_ms must be specified") + # Set read mode for the MS430: cycle or on-demand + if (cycle_period is not None): + # Use cycle mode with 3/100/300 second delay periods + self.I2C_bus.write_i2c_block_data(i2c_7bit_address, CYCLE_TIME_PERIOD_REG, [cycle_period]) + self.I2C_bus.write_byte(i2c_7bit_address, CYCLE_MODE_CMD) + self.cycle_mode = True + else: + # Use on-demand mode with any chosen time delay between measurements + self.I2C_bus.write_byte(i2c_7bit_address, ON_DEMAND_MEASURE_CMD) + self.delaying = False + self.OD_delay_ms = OD_delay_ms + self.cycle_mode = False + if USE_FAHRENHEIT: + self.useFahrenheitTemperatureUnits(True) + # select data variables from name list + self.indices = list(range(0,4)) + if (self.cycle_mode): + self.indices += list(range(4,8)) + self.band1_index = 11 + else: + self.band1_index = 7 + self.indices += list(range(8,18)) + if (self.get_particle_data): + self.indices += list(range(19,21)) + self.createDataBuffer() + self.initializeComboBoxes() + + + # Check for new I2C data + def getDataFunction(self): + if (self.cycle_mode): + if GPIO.event_detected(READY_pin): + self.readData() + return True + else: + # On-demand mode + if (self.delaying): + time_now_ms = (datetime.datetime.now().timestamp())*1000 + if ((time_now_ms-self.time_start_ms) >= self.OD_delay_ms): + # Trigger a new measurement + self.I2C_bus.write_byte(i2c_7bit_address, ON_DEMAND_MEASURE_CMD) + self.delaying = False + else: + if GPIO.event_detected(READY_pin): + self.readData() + self.delaying = True + self.time_start_ms = (datetime.datetime.now().timestamp())*1000 + return True + return False + + + def readData(self): + self.setWindowTitle('Indoor Environment Data') + air_data = get_air_data(self.I2C_bus) + air_quality_data = get_air_quality_data(self.I2C_bus) + light_data = get_light_data(self.I2C_bus) + sound_data = get_sound_data(self.I2C_bus) + particle_data = get_particle_data(self.I2C_bus, PARTICLE_SENSOR) + self.putDataInBuffer(air_data, air_quality_data, light_data, sound_data, particle_data) + + + def appendData(self, start_index, data): + for i,v in enumerate(data): + self.data_buffer[start_index+i].append(v) + return (start_index + len(data)) + + + # Store the data and also the time/date + def putDataInBuffer(self, air_data, air_quality_data, light_data, sound_data, particle_data): + i=0 + i = self.appendData(i, [air_data['T'], air_data['P_Pa'], air_data['H_pc'], air_data['G_ohm']]) + if (self.cycle_mode): + i = self.appendData(i, [air_quality_data['AQI'], air_quality_data['CO2e'], + air_quality_data['bVOC'], air_quality_data['AQI_accuracy']]) + i = self.appendData(i, [light_data['illum_lux'], light_data['white']]) + i = self.appendData(i, [sound_data['SPL_dBA']] + + [sound_data['SPL_bands_dB'][i] for i in range(0,self.sound_band_number)] + + [sound_data['peak_amp_mPa']]) + if (self.get_particle_data): + i = self.appendData(i, [particle_data['duty_cycle_pc'], particle_data['concentration']]) + self.time_data.append(datetime.datetime.now().timestamp()) + + + +if __name__ == '__main__': + theApp = QtGui.QApplication([]) + gv = GraphViewerI2C(data_buffer_length, cycle_period_code, on_demand_delay_ms) + gv.start() + theApp.exec_() diff --git a/Python/graph_viewer_serial.py b/Python/graph_viewer_serial.py new file mode 100644 index 0000000..556c45f --- /dev/null +++ b/Python/graph_viewer_serial.py @@ -0,0 +1,138 @@ +# graph_viewer_serial.py + +# NOTE on operating system/platform: + +# This example runs on multiple operating systems (including Windows +# and Linux) and uses serial over USB to get data from the MS430 sensor +# via a microcontroller board (e.g. Arduino, ESP8266, etc). + +# An alternate version, "graph_viewer_I2C.py" is provided for the +# Raspberry Pi, where the MS430 board is directly connected to the Pi +# using the GPIO/I2C pins. + +######################################################### + +# This example displays a graphical user interface with real-time +# updating graphs showing data from the MS430 sensor board. + +# Instructions (installation instructions are in the readme / User Guide) + +# 1) Program the microcontroller board with either "cycle_readout.ino" +# or "on_demand_readout.ino", with printDataAsColumns = true + +# 2) Connect the microcontroller USB cable to your PC and close any +# serial monitor software. + +# 3) Put the serial port name (system dependent) in the serial_port_name +# parameter below. + +# 4) Run this program with python3 + +# Copyright 2020 Metriful Ltd. +# Licensed under the MIT License - for further details see LICENSE.txt + +# For code examples, datasheet and user guide, visit +# https://github.com/metriful/sensor + +######################################################### +# USER-EDITABLE SETTINGS + +# Which particle sensor is connected - this is used to select the +# displayed measurement units (the microcontroller must also be +# programmed to use the sensor) +particle_sensor = "PPD42" # put here: "SDS011", "PPD42", or None + +# Choose which temperature label to use for display (NOTE: the actual +# measurement unit depends on the microcontroller program). +# Celsius is default. +use_fahrenheit = False + +# Specify the serial port name on which the microcontroller is connected +# e.g. on Windows this is usually a name like "COM1", on Linux it is +# usually a path like "/dev/ttyACM0" +# NOTE: close all other serial applications (e.g. Arduino Serial Monitor) +serial_port_name = "/dev/ttyACM0" + +# Maximum number of values of each variable to store and display: +data_buffer_length = 500 + +# END OF USER-EDITABLE SETTINGS +######################################################### + +import datetime +import serial +from GraphViewer import * + +class GraphViewerSerial(GraphViewer): + def __init__(self, buffer_length, serial_port): + super(GraphViewerSerial, self).__init__(buffer_length) + self.serial_port = serial.Serial( + port = serial_port, + baudrate = 9600, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + bytesize=serial.EIGHTBITS, + timeout=0.02) + self.startup = True + self.initial_discard_lines = 2 + self.line_count = 0 + # There are 4 input cases resulting from the use of cycle_readout.ino and + # on_demand_readout.ino, with 15, 18, 19 and 22 data columns. These lists + # define which variables are present in each case: + self.col_indices = [] + self.col_indices.append(list(range(0,4)) + list(range(8,19))) # no air quality or particle data + self.col_indices.append(list(range(0,4)) + list(range(8,22))) # no air quality data + self.col_indices.append(list(range(0,19))) # no particle data + self.col_indices.append(list(range(0,22))) # all data + self.sound_band1_index = [7, 7, 11, 11] # the index of 'Band 1 SPL' in the four lists + + + # Allow for initial corrupted serial data, incomplete lines or printed + # messages by discarding lines until a correct number of columns appears + def serialStartupCompleted(self, data_strings): + if (self.startup): + self.line_count+=1 + if (self.line_count >= self.initial_discard_lines): + nc = len(data_strings) + for i in range(0,len(self.col_indices)): + if (nc == len(self.col_indices[i])): + self.startup = False + self.indices = self.col_indices[i] + self.band1_index = self.sound_band1_index[i] + self.createDataBuffer() + self.initializeComboBoxes() + self.setWindowTitle('Indoor Environment Data') + if (self.startup): + raise Exception('Unexpected number of data columns') + return (not self.startup) + + + # Check for new serial data + def getDataFunction(self): + response = self.serial_port.readline() + if (not ((response is None) or (len(response) == 0))): + # A complete line was received: convert it to string and split at spaces: + try: + data_strings = response.decode('utf-8').split() + if (self.serialStartupCompleted(data_strings)): + # Check number of values received; if incorrect, ignore the data + if (len(data_strings) == len(self.indices)): + # Convert strings to numbers and store the data + float_data = [float(i) for i in data_strings] + for i in range(0, len(self.indices)): + self.data_buffer[i].append(float_data[i]) + self.time_data.append(datetime.datetime.now().timestamp()) + return True + except: + pass + return False # no new data + + + +if __name__ == '__main__': + theApp = QtGui.QApplication([]) + gv = GraphViewerSerial(data_buffer_length, serial_port_name) + gv.setParticleUnits(particle_sensor) + gv.useFahrenheitTemperatureUnits(use_fahrenheit) + gv.start() + theApp.exec_() diff --git a/README.md b/README.md index 3a18017..a890edf 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,46 @@ # Metriful MS430: Environment Sensor - + -![](sensor_pcb.png) +

-The Metriful MS430 is a low power, high accuracy, smart sensor cluster for indoor environment monitoring. It is operated via a simple I2C-compatible interface and measures eighteen variables including air quality, light and sound levels. +[**Buy the hardware now - REDUCED PRICES.**](https://www.metriful.com/shop) -This repository provides instructions and software examples for running the MS430 module with Raspberry Pi, Arduino and NodeMCU host systems. These are also useful starting points for development with other hosts. +The Metriful MS430 is a low power, high accuracy, smart sensor cluster for indoor environment monitoring. It operates via a simple I2C-compatible interface and measures eighteen variables including air quality, light and sound levels. -The code examples demonstrate various ways of using the module. This includes basic control/readout, saving data to files and IoT cloud storage. Detailed comments explain each part of the programs. +This repository provides instructions and software examples for running the MS430 with **Raspberry Pi, Arduino, ESP8266** and **ESP32** host systems. -The [**User Guide**](User_guide.pdf) covers hardware setup in more detail, gives an overview of the code examples and explains more about what the device measures. +Code examples include interfaces to **IFTTT, Home Assistant** and **IoT cloud platforms**, as well as **real-time graph software, web servers** and **interrupt detection**. + +This readme provides a quick-start guide to running the examples on various host systems. + +The [**User Guide**](User_guide.pdf) gives an overview of the code examples and explains more about what the device measures. The [**Datasheet**](Datasheet.pdf) is a detailed specification of the electrical and communications interfaces of the MS430. -You can also [visit the product homepage.](https://www.sensor.metriful.com) ### Contents -**[Handling precautions](#handling-precautions)**
-**[Use with Arduino](#use-with-arduino)**
-**[Use with Raspberry Pi](#use-with-raspberry-pi)**
-**[Use with NodeMCU](#use-with-nodemcu)**
-**[IoT cloud setup](#iot-cloud-setup)**
-**[License](#license)**
-**[Disclaimer](#disclaimer)**
+ +#### Hardware setup +- **[Handling precautions](#handling-precautions)**
+- **[Arduino](#arduino)**
+- **[Raspberry Pi](#raspberry-pi)**
+- **[ESP8266](#esp8266)**
+- **[ESP32](#esp32)**
+- **[Particle sensor](#connecting-and-enabling-a-particle-sensor)**
+#### Code example setup +- **[IoT cloud setup](#iot-cloud-setup)**
+- **[Graph web server](#graph-web-server)**
+- **[IFTTT example](#ifttt-example)**
+- **[Home Assistant example](#home-assistant-example)**
+- **[Graph viewer software](#graph-viewer-software)**
+- **[Fahrenheit temperatures](#fahrenheit-temperatures)**
+#### Other information +- **[Case and enclosure ideas](#case-enclosure-and-mounting-ideas)**
+- **[Troubleshooting](#troubleshooting)**
+- **[Changelog](#changelog)**
+- **[License](#license)**
+- **[Disclaimer](#disclaimer)**
## Handling precautions @@ -36,7 +53,7 @@ The MS430 can be damaged by static electricity discharges. Minimize this risk by - Keep away from metal objects which could cause shorted connections -## Use with Arduino +## Arduino All code examples in the Arduino folder run on the Arduino Nano 33 IoT and Arduino MKR WiFi 1010, while those not requiring a network connection also run on Arduino Uno and Nano. @@ -46,17 +63,13 @@ Note that steps 1 and 2 are already complete if you have used Arduino before on 1. Download and install the [Arduino IDE](https://www.arduino.cc/en/main/software) on your computer. 2. Start the Arduino IDE for the first time. This will create a folder named **Arduino/libraries** in your user area (e.g. in the Documents folder on Windows computers). -3. Download and unzip the [Sensor repository](https://www.github.com/metriful/sensor). From this, copy the folder **Metriful_Sensor** (located within the Arduino folder) into the Arduino libraries folder in your user area. +3. Download and unzip the [Sensor repository](https://www.github.com/metriful/sensor). From this, copy the folder **Metriful_Sensor** (located within the Arduino folder) into the Arduino libraries folder in your user area. Delete any previous version you may have. If using **Arduino Nano 33 IoT** or **Arduino MKR WiFi 1010**, also do the following: -4. Download and install the SAMD board package: in the Arduino IDE menu, go to Tools > Board > Boards Manager (top of list). Search for and install **Arduino SAMD Boards (32-bits ARM Cortex-M0+)** +4. Download and install the SAMD board package: in the Arduino IDE menu, go to Tools > Board > Boards Manager. Search for and install **Arduino SAMD Boards (32-bits ARM Cortex-M0+)** 5. Install the WiFiNINA package: in the Arduino IDE menu, go to Tools > Manage Libraries. Search for and install **WiFiNINA**. -If using **Arduino Nano 33 IoT**, also do the following: - -6. Download the [SlowSoftWire library](https://github.com/felias-fogg/SlowSoftWire). Unzip it and move it into the Arduino libraries folder in your user area. -7. Download the [SlowSoftI2CMaster library](https://github.com/felias-fogg/SlowSoftI2CMaster). Unzip it and move it into the Arduino libraries folder in your user area. ### Wiring for Arduino @@ -66,27 +79,16 @@ If using **Arduino Nano 33 IoT**, also do the following: | VDD | - | - | 3.3V | VCC | | GND | GND | GND | GND | GND | | VPU | IOREF | 5V | 3.3V | VCC | -| SCL | SCL | A5 | A3 | SCL | -| SDA | SDA | A4 | A0 | SDA | +| SCL | SCL | A5 | A5 | SCL | +| SDA | SDA | A4 | A4 | SDA | | LIT | D4 | D4 | A1 | D4 | | SIT | D7 | D7 | A2 | D5 | | RDY | D2 | D2 | D11 | D0 | -* MS430 pin VDD is not used with 5 V systems and VIN is not used with 3.3 V systems. - -* If using the PPD42 particle sensor, note that its pin numbering runs from right to left, and connect: - * Arduino 5V pin to PPD42 pin 3 - * Arduino GND pin to PPD42 pin 1 - * PPD42 pin 4 to MS430 pin PRT - -* If using the SDS011 particle sensor, connect: - * Arduino 5V pin to SDS011 pin "5V" - * Arduino GND pin to SDS011 pin "GND" - * SDS011 pin "25um" to MS430 pin PRT - -* To obtain 5V output on the Nano 33 IoT: the solder bridge labeled "VUSB" on the underside of the Arduino must be soldered closed, then use the VUSB pin. -* To obtain a third 5V output on the Uno: use pin number 2 on the 6-pin ICSP header -* With all hosts, VPU can be supplied from any host digital output pin set to a high voltage state. This can be useful for hosts without enough power output pins. +* Arduino Nano 33 IoT used a software I2C library in previous code versions but now uses the hardware I2C pins. +* MS430 pin VDD is not used with the 5V systems (Uno and Nano) and VIN is not used with the 3.3V systems (Nano 33 IoT and MKR WiFi 1010). +* LIT/SIT connections are optional and only required if you are using light/sound interrupts. +* VPU can be supplied from any spare host digital output pin set to a high voltage state. This can be useful for hosts without enough power output pins. ### To run an example program on Arduino @@ -99,26 +101,32 @@ If using **Arduino Nano 33 IoT**, also do the following: 7. Go to Tools > Serial Monitor to view the output (ensure **9600 baud** is selected in the monitor). -## Use with Raspberry Pi +## Raspberry Pi -The example programs for Raspberry Pi use Python 3 and are provided in the **Raspberry_Pi** folder. +The example programs for Raspberry Pi use Python 3 and are provided in the **Raspberry_Pi** folder, within the **Python** folder. ### First time Raspberry Pi setup -This setup assumes that you are using Raspbian Buster, which comes with all required Python packages already installed (the packages used are: **RPi.GPIO**, **smbus** and **requests**). +This setup assumes that you are using Raspberry Pi OS. The standard OS version comes with all required Python packages already installed (except packages for the [Graph viewer software](#graph-viewer-software)). The **Lite** (command line) OS version requires package installation, as listed below. + +1. If you are using Raspberry Pi OS Lite (or get missing package errors), run the following commands to install the packages needed: + ``` + sudo apt-get update + sudo apt install i2c-tools python3-smbus python3-rpi.gpio + ``` -1. Enable I2C on your Raspberry Pi using the raspi-config utility by opening a terminal and running: +2. Enable I2C on your Raspberry Pi using the raspi-config utility by opening a terminal and running: ``` sudo raspi-config ``` Select **5 Interfacing Options** and then **P5 I2C**. A prompt will appear asking "Would you like the ARM I2C interface to be enabled?": select **Yes** and then exit the utility. -2. Shut-down the Raspberry Pi and disconnect the power. Wire up the hardware as described in the following section. Double-check the wiring then restart the Pi. -3. Check that the Metriful MS430 board can be detected by running: +3. Shut-down the Raspberry Pi and disconnect the power. Wire up the hardware as described in the following section. Double-check the wiring then restart the Pi. +4. Check that the Metriful MS430 board can be detected by running: ``` sudo i2cdetect -y 1 ``` This should report the 7-bit address number **71**. -4. Download and unzip this [Sensor repository](https://www.github.com/metriful/sensor). The Raspberry Pi examples are found within the folder named **Raspberry_Pi**. +5. Download and unzip this [Sensor repository](https://www.github.com/metriful/sensor). The Raspberry Pi examples are found within the folder named **Raspberry_Pi**, inside the **Python** folder. ### Wiring for Raspberry Pi @@ -134,17 +142,9 @@ This setup assumes that you are using Raspbian Buster, which comes with all requ | SIT | 8 | GPIO 14 | | RDY | 11 | GPIO 17 | +* Raspberry Pi pin numbering is [shown here](https://www.raspberrypi.org/documentation/usage/gpio/README.md). * MS430 pin VIN is not used. - -* If using the PPD42 particle sensor, note that its pin numbering runs from right to left, and connect: - * Pi pin 2 (5V) to PPD42 pin 3 - * Pi pin 9 (Ground) to PPD42 pin 1 - * PPD42 pin 4 to MS430 pin PRT - -* If using the SDS011 particle sensor, connect: - * Pi pin 2 (5V) to SDS011 pin "5V" - * Pi pin 9 (Ground) to SDS011 pin "GND" - * SDS011 pin "25um" to MS430 pin PRT +* LIT/SIT connections are optional and only required if you are using light/sound interrupts. ### To run an example Raspberry Pi program: @@ -156,26 +156,31 @@ This setup assumes that you are using Raspbian Buster, which comes with all requ ``` -## Use with NodeMCU +## ESP8266 -All code examples in the Arduino folder run on the NodeMCU (ESP8266) and are programmed using the Arduino IDE. +All code examples in the Arduino folder have been tested on NodeMCU and Wemos D1 Mini, and are programmed using the Arduino IDE. -### First time NodeMCU setup +Other ESP8266 development boards should also work but may use a different pinout and may therefore require edits to the host_pin_definitions.h file. -Note that steps 1 and 2 are already complete if you have used Arduino before on your computer. +Note that ESP8266 does not have a hardware I2C module, so any of the normal GPIO pins can be used for the I2C bus. + +### First time ESP8266 setup + +Note that steps 1 and 2 are already complete if you have used Arduino IDE before on your computer. 1. Download and install the [Arduino IDE](https://www.arduino.cc/en/main/software) on your computer. 2. Start the Arduino IDE for the first time. This will create a folder named **Arduino/libraries** in your user area (e.g. in the Documents folder on Windows computers). -3. Download and unzip the [Sensor repository](https://www.github.com/metriful/sensor). From this, copy the folder **Metriful_Sensor** (located within the Arduino folder) into the Arduino libraries folder in your user area. +3. Download and unzip the [Sensor repository](https://www.github.com/metriful/sensor). From this, copy the folder **Metriful_Sensor** (located within the Arduino folder) into the Arduino libraries folder in your user area. Remove any previous version you may have. 4. In the Arduino IDE menu, go to File > Preferences. In the box labeled "Additional Boards Manager URLs", paste the following link: ``` https://arduino.esp8266.com/stable/package_esp8266com_index.json ``` + If there is already text in the box, place a comma and paste the new text after it. 5. In the Arduino IDE menu, go to Tools > Board > Boards Manager. Search for and install the package named **esp8266 by ESP8266 Community**. -### Wiring for NodeMCU +### Wiring for ESP8266 -| MS430 pin | NodeMCU | +| MS430 pin | ESP8266 | |:---------------:|:--------------------:| | VIN | - | | VDD | 3V3 | @@ -187,32 +192,125 @@ Note that steps 1 and 2 are already complete if you have used Arduino before on | SIT | D5 (GPIO 14) | | RDY | D6 (GPIO 12) | +* The **D** pin numbers refer to the usual pin labels printed on the development board. The GPIO numbers are the actual ESP8266 I/O identities. * MS430 pin VIN is not used. - -* If using the PPD42 particle sensor, note that its pin numbering runs from right to left, and connect: - * NodeMCU Vin (5V) pin to PPD42 pin 3 - * NodeMCU GND pin to PPD42 pin 1 - * PPD42 pin 4 to MS430 pin PRT +* LIT/SIT connections are optional and only required if you are using light/sound interrupts. +* VPU can be supplied from any spare host digital output pin set to a high voltage state. This can be useful for hosts without enough power output pins. -* If using the SDS011 particle sensor, connect: - * NodeMCU Vin (5V) pin to SDS011 pin "5V" - * NodeMCU GND pin to SDS011 pin "GND" - * SDS011 pin "25um" to MS430 pin PRT +### To run an example program on ESP8266 + +1. Wire the MS430 board to the ESP8266 as described in the previous section. +2. Plug the ESP8266 board into your computer via USB. +3. Start the Arduino IDE and open the chosen example code file, e.g. **simple_read_sound.ino** +4. In the Arduino IDE menu, go to Tools > Port and select the port with the ESP8266 attached. +5. Go to Tools > Board and select your development board, or "Generic ESP8266 Module", or experiment until you find one that works. +6. Select Sketch > Upload and wait for upload confirmation. +7. Go to Tools > Serial Monitor to view the output (ensure **9600 baud** is selected in the monitor). -### To run an example program on NodeMCU -1. Wire the MS430 board to the NodeMCU as described in the previous section. -2. Plug the NodeMCU into your computer via USB. +## ESP32 + +All code examples in the Arduino folder have been tested on DOIT DevKit v1, and are programmed using the Arduino IDE. + +Other ESP32 development boards should also work but may use a different pinout and may therefore require edits to the host_pin_definitions.h file. + +### First time ESP32 setup + +Note that steps 1 and 2 are already complete if you have used Arduino IDE before on your computer. + +1. Download and install the [Arduino IDE](https://www.arduino.cc/en/main/software) on your computer. +2. Start the Arduino IDE for the first time. This will create a folder named **Arduino/libraries** in your user area (e.g. in the Documents folder on Windows computers). +3. Download and unzip the [Sensor repository](https://www.github.com/metriful/sensor). From this, copy the folder **Metriful_Sensor** (located within the Arduino folder) into the Arduino libraries folder in your user area. Remove any previous version you may have. +4. In the Arduino IDE menu, go to File > Preferences. In the box labeled "Additional Boards Manager URLs", paste the following link: + ``` + https://dl.espressif.com/dl/package_esp32_index.json + ``` + If there is already text in the box, place a comma and paste the new text after it. +5. In the Arduino IDE menu, go to Tools > Board > Boards Manager. Search for and install the package named **esp32 by Espressif Systems**. + +### Wiring for ESP32 + +| MS430 pin | ESP32 | +|:---------------:|:-------------:| +| VIN | - | +| VDD | 3V3 | +| GND | GND | +| VPU | 3V3 | +| SCL | D22 | +| SDA | D21 | +| LIT | D18 | +| SIT | D19 | +| RDY | D23 | + +* MS430 pin VIN is not used. +* LIT/SIT connections are optional and only required if you are using light/sound interrupts. +* VPU can be supplied from any spare host digital output pin set to a high voltage state. This can be useful for hosts without enough power output pins. + +### To run an example program on ESP32 + +1. Wire the MS430 board to the ESP32 as described in the previous section. +2. Plug the ESP32 board into your computer via USB. 3. Start the Arduino IDE and open the chosen example code file, e.g. **simple_read_sound.ino** -4. In the Arduino IDE menu, go to Tools > Port and select the port with the NodeMCU attached. -5. Go to Tools > Board and select "Generic ESP8266 Module" +4. In the Arduino IDE menu, go to Tools > Port and select the port with the ESP32 attached. +5. Go to Tools > Board and select your development board, or experiment until you find one that works. 6. Select Sketch > Upload and wait for upload confirmation. 7. Go to Tools > Serial Monitor to view the output (ensure **9600 baud** is selected in the monitor). +## Connecting and enabling a particle sensor + +The MS430 is compatible with two widely-available air particle sensors: the Shinyei PPD42 and the Nova SDS011. The particle sensor is optional and only a single sensor can be connected at any time. + +Both sensor models require three wire connections: **5V, GND, PRT** and a small edit to the example code. + +| | PPD42 pin number | SDS011 pin label | +|:-----:|:----------------:|:----------------:| +| 5V | 3 | 5V | +| GND | 1 | GND | +| PRT | 4 | 25um | + +* PRT is on the MS430 board +* 5V and GND are supplied by the host system, or from a separate power supply. If using a separate power supply, the power supply GND must be connected to host GND. +* The SDS011 pin labeled "25um" is the data output for 0.3 - 10 µm particles. + +5V is available from the hosts when they are powered from a USB power supply: + +| Host device | 5V pin name/number | +|:---------------------:|:------------------:| +| Raspberry Pi | Pin 2 | +| Arduino Uno | 5V or IOREF (*) | +| Arduino Nano | 5V | +| Arduino Nano 33 IoT | VUSB (**) | +| Arduino MKR WiFi 1010 | 5V | +| ESP8266 | 5V or Vin or VU | +| ESP32 | VIN | + +(*) To obtain a third 5V output from the **Uno**: use pin number 2 on the 6-pin ICSP header + +(**) To obtain 5V output on the **Nano 33 IoT**: the solder bridge labeled "VUSB" on the underside of the Arduino must be soldered closed, then use the VUSB pin. + +* **Raspberry Pi** pin 9 can be used as an extra GND connection. +* Pin labels for ESP8266 and ESP32 may be different on some boards + +### Enable the particle sensor in the code examples + +* **Arduino**: in Metriful_sensor.h on the line: + ``` + #define PARTICLE_SENSOR PARTICLE_SENSOR_OFF + ``` + change ```PARTICLE_SENSOR_OFF``` to be either ```PARTICLE_SENSOR_PPD42``` or ```PARTICLE_SENSOR_SDS011``` + +* **Raspberry Pi**: in /sensor_package/sensor_functions.py on the line: + ``` + PARTICLE_SENSOR = PARTICLE_SENSOR_OFF + ``` + change ```PARTICLE_SENSOR_OFF``` to be either ```PARTICLE_SENSOR_PPD42``` or ```PARTICLE_SENSOR_SDS011``` + ## IoT cloud setup -The **IoT_cloud_logging** code example shows how to send data to an Internet of Things (IoT) cloud storage account. It can be used with Arduino Nano 33 IoT, MKR WiFi 1010, NodeMCU and Raspberry Pi host systems. +

+ +The **IoT_cloud_logging** code example shows how to send data to an Internet of Things (IoT) cloud storage account. It can be used with Arduino Nano 33 IoT, MKR WiFi 1010, ESP8266, ESP32 and Raspberry Pi host systems. IoT cloud hosting is available from many providers around the world. Some offer free accounts (with storage or access limits) for non-commercial purposes. The IoT cloud logging example gives a choice of two providers, [Tago.io](https://tago.io) and [Thingspeak.com](https://thingspeak.com). The following sections give a brief overview of how to set up free accounts with these providers: for further information see the relevant provider website. @@ -267,10 +365,256 @@ The steps required to set up Thingspeak for the IoT cloud logging code example a 9. Go to the **API Keys** tab and copy the Write API Key (a sequence of letters and numbers). 10. Paste the API key into the Metriful IoT cloud logging example code as the variable **THINGSPEAK_API_KEY_STRING** and set the variable **useTagoCloud** as **false**. + +## Graph web server + +

+ +The **graph_web_server** example produces a web page with a set of graphs which display data stored on the host. The page can be accessed from other devices on your home network using their internet browsers. This is a good local alternative to using a cloud IoT service. + +When opened in a browser, the web page will attempt to run the [Plotly](https://plotly.com/javascript/) javascript library which is used to create the graphs. If there is no internet access, the browser may be able to use a cached copy if it previously accessed the page with internet access. Otherwise, the graphs will not load and you will only see text data. + +A button on the web page allows you to download the stored data as a CSV (comma separated value) text file, which can be opened with many spreadsheet applications. + + +## IFTTT example + +The IFTTT example shows how to send data to [IFTTT.com](https://ifttt.com), which will trigger an email alert. It is compatible with IFTTT's free account. + +The host device monitors some of the environment data and sends the alert when the values go outside your chosen "safe" range. The emails you receive will look like: +``` +The temperature is too high. +The measurement was 25.5 °C on October 10, 2020 at 05.54PM. +Turn on the fan. +``` +You can customize all parts of this message. + +### Setup + +1. Go to [IFTTT.com](https://ifttt.com) and sign up for a free account. +2. Click **Create** to start a new applet. +3. Click **If This (Add)**, search for service **Webhooks** and click **Receive a web request**, then **Connect**. +4. Enter an event name, e.g. **alert_event**, then click **Create trigger**. +5. Click **Then That (Add)** and search for **email**. +6. Choose either **Email** to get a new email for every alert, or choose **Email Digest** to get ONE email per day/week which contains all alerts. Then click **Connect**. +7. Enter and validate your email address when prompted. +8. Enter a Title/Subject for the emails, e.g. Environment data alert. +9. Delete all text from the Body/Message text box and paste in the following: + ``` + {{Value1}}

+ {{Value2}} on {{OccurredAt}}

+ {{Value3}} + ``` +10. Click **Create action**, then **Continue**, then **Finish** to complete the applet setup. +11. Go to your IFTTT **Home** page and click on the applet you just created. +12. Click the triangle Webhooks icon, then **Documentation** at top right. +13. Copy the key (letter number sequence) that is displayed. +14. Edit the example code file **IFTTT**, pasting in this key and the event name (from step 4). +15. Run the program. + + +## Home Assistant example + +

+ +This code example shows how to send sensor data to an installation of [Home Assistant](https://www.home-assistant.io) on your home network. These instructions cover setup, creating an automation using your data, and removing your data. + +Note: this was tested on Home Assistant OS v0.117.2 + +### Setup + +1. Edit the program file "Home_Assistant": + * Choose a sensor name and insert the IP address of the computer running Home Assistant. + * Generate a "long lived access token": + * On the Home Assistant web interface, click your username at the bottom left corner to go to your profile page. + * At the bottom of the profile page, under **Long-Lived Access Tokens**, click **CREATE TOKEN** and copy the very long sequence of numbers/letters. + * Paste the token into the program file. +2. Run the program and wait a few minutes so that the first data have been sent. +3. Check that data are being received by Home Assistant: + * Go to the **Configuration** page of Home Assistant + * Click **Entities** in the component list + * There should be a series of entries with names like SENSOR_NAME.temperature, SENSOR_NAME.air_quality_index, etc. Where SENSOR_NAME is the name you chose in the program file. + + +### Display/view the data in Home Assistant + +* Add display cards to the **Overview** page dashboard - these can be text labels, gauges, graphs etc. + 1. Click the 3 dots at the top right corner and choose **Edit Dashboard** + 2. Add a card with the **+** symbol + 3. Choose **Entities**, **Gauge**, or **History graph** + 4. Add the variables using the entities dropdown list + +* You can also view data graphs on the **History** page. + +* If Home Assistant is rebooted, cards will show **Entity not available** (and the sensor will disappear from the entity list) until a new value is received. The data history will also reappear when this happens. + + +### Add automations using the sensor data + +This simple example shows how to generate a notification when the temperature goes above 22 °C. Much more advanced triggers and actions can be configured. + +1. On the Configuration page, go to: Automations and click the **+** to create a new Automation +2. Click **skip** on the "Create a new automation" window +3. On the "New Automation" page, click the 3 dots at the top right corner and choose **Edit as YAML** +4. Delete everything in the text box and replace with: + ``` + trigger: + - platform: numeric_state + entity_id: kitchen3.temperature + above: '22' + action: + - service: persistent_notification.create + data: + title: Kitchen temperature too high + message: 'The temperature is {{ states.kitchen3.temperature.state_with_unit }}' + ``` + Replace kitchen3 with your SENSOR_NAME chosen name and the other fields with your own values. +5. Click the save icon to finish. +6. Optional: click **execute** to test it (the action is forced to run without the trigger condition). +7. Optional: edit it further (e.g. name, description) via the UI on the Configuration > Automations page. + +### Removing entities and data from Home Assistant + +* To hide data from view on the Overview page dashboard, simply edit or delete the cards. +* To remove an entity and its data history from the system, follow this procedure: + +**Initial one-time setup** + +1. Install the Home Assistant SQLite add-on (via the Supervisor page > Add-on store > install "SQLite Web"). +2. On the Supervisor page: click the SQLite Web icon, go to the Configuration tab and change "read_only" to **false**, then save. + +**Entity removal** + +1. Go to the **SQLite Web** page in the Home Assistant menu. +2. Click **events**, then open the **Query** tab. +3. In the text box put: + ``` + delete from states where entity_id = "kitchen3.temperature"; + ``` + where **kitchen3.temperature** is the entity to remove. Or remove all entities with name beginning "kitchen3." using the % wildcard: + ``` + delete from states where entity_id like "kitchen3.%"; + ``` + (replace kitchen3 with your SENSOR_NAME name). +4. Click the **Execute** button. +5. In the text box put: + ``` + vacuum; + ``` +6. Click the **Execute** button. + + +## Graph viewer software + +

+ +The **graph viewer** uses a graphical interface to show environment data updating in real-time. It uses Python and runs on all major operating systems. + +Note that the graph viewer does not run on Raspberry Pi OS **Lite** because there is no desktop interface. + +There are two versions, to be used with Raspberry Pi or Arduino, provided in the Python folder. + +1. **graph_viewer_I2C.py** + + Runs only on Raspberry Pi and communicates directly with the MS430 board which is connected to the Pi GPIO pins. + +2. **graph_viewer_serial.py** + + Runs on multiple operating systems and uses serial over USB to get data from the MS430 sensor via a microcontroller board (e.g. Arduino, ESP8266, etc). + +### Package installation commands + +This assumes you have already installed Python3 and Pip3. + +**Windows** +``` +pip3 install pyqtgraph pyqt5 pyserial +``` + +**Linux, including Raspberry Pi** +``` +pip3 install pyqtgraph pyserial +sudo apt install python3-pyqt5 +``` + +**Extra steps for some Linux versions e.g. Ubuntu** +* Install pip3 by enabling the "Universe" software repository, then ```sudo apt install python3-pip``` +* Add the user to the **dialout** group for permission to use the serial port. + +### Running graph_viewer_I2C.py (Raspberry Pi) + +1. Follow the usual hardware setup for Raspberry Pi and check that the MS430 board is recognized by the Pi. +2. Run the program with: ```python3 graph_viewer_I2C.py``` + +### Running graph_viewer_serial.py (all operating systems) + +1. Follow the usual hardware setup for your microcontroller board. +2. Program the microcontroller board with either **cycle_readout.ino** or **on_demand_readout.ino**, with parameter ```printDataAsColumns = true``` +3. Connect the microcontroller USB cable to your computer and close all serial monitor software. +4. Edit the Python code file so that the particle sensor and temperature unit settings match those used on the microcontroller. +5. Put the serial port name (system dependent, e.g. COM1) in the **serial_port_name** parameter in the code file. +6. Run the program with: ```python3 graph_viewer_serial.py``` + + +## Fahrenheit temperatures + +The temperature is always measured by the MS430 as a Celsius value. The software output values (printed to screen/file, or sent over the network, etc) are by default in Celsius but will be converted to Fahrenheit if the following changes are made: + +* On Raspberry Pi + + In file **sensor_functions.py** set the following: + ``` + USE_FAHRENHEIT = True + ``` + +* On Arduino + + In file **Metriful_sensor.h** set the following: + ``` + #define USE_FAHRENHEIT + ``` + + +## Case, enclosure and mounting ideas + +The simplest arrangement is to leave the sensor board open to the air. Use bolts, PCB spacers/standoffs, adhesive foam pads, or hot glue to fix the board to a stand (e.g. a 6x4" picture frame). Wires can be hidden around the back to make it neater. + +### Enclosing the board in a box or case + +You can use a box or case with small holes for air and sound entry. Light entry can be through a transparent or open window hole. The following tips may help: + +* Fix the board as close as possible to the box wall but without having the sensors touching the wall. + +* Minimize the total air volume inside the box, so it exchanges quickly. + +* Provide some holes for air and sound entry – these do not have definite requirements. + +* If you use a particle sensor, its entry and exit ports need to be unobstructed and open to the surrounding air. So mount it against the box wall, with its ports lined up against holes in the box. + +* The light sensor requires a minimum hole size to have the correct exposure, with the diameter depending on sensor distance from the hole. For example, having the sensor 2 mm from the casing edge requires a hole of about 6 mm diameter. More of these values are given on page 18 of this document: +[https://www.vishay.com/docs/84367/designingveml6030.pdf](https://www.vishay.com/docs/84367/designingveml6030.pdf) + +* The host releases heat which can affect the temperature reading if they are both together in the same box. +If you get a small temperature offset, you can measure it with an accurate thermometer and subtract it in the software - this is what the sensor manufacturer recommends. + +* The SDS011 particle sensor has a powered fan which will affect the MS430 sound measurements. Reduce this by situating the SDS011 further away from the MS430, or with insulating material. Alternatively, you can "gate" power to the SDS011 to turn it on and off - the **particle_sensor_toggle** example and the User Guide give more information on this. + + +## Troubleshooting + +Having problems with the hardware or software? Please check [TROUBLESHOOTING](TROUBLESHOOTING.md) before opening a GitHub issue. + + +## Changelog + +Changes, fixes and additions in each software release version are listed in the [CHANGELOG](CHANGELOG.md) + + ## License See the [LICENSE](LICENSE.txt) file for software license rights and limitations (MIT). + ## Disclaimer This document and repository, and the products described herein, are subject to specific disclaimers set forth in the [DISCLAIMER](DISCLAIMER.txt) file. diff --git a/Raspberry_Pi/simple_read_T_H.py b/Raspberry_Pi/simple_read_T_H.py deleted file mode 100644 index aabbc50..0000000 --- a/Raspberry_Pi/simple_read_T_H.py +++ /dev/null @@ -1,100 +0,0 @@ -# simple_read_T_H.py - -# Example code for using the Metriful MS430 to measure humidity and -# temperature. -# This example is designed to run with Python 3 on a Raspberry Pi. - -# Measures and displays the humidity and temperature, demonstrating -# the decoding of the signed temperature value. The data are also -# read out and displayed a second time, using a "data category read" -# of all Air measurement data. - -# Copyright 2020 Metriful Ltd. -# Licensed under the MIT License - for further details see LICENSE.txt - -# For code examples, datasheet and user guide, visit -# https://github.com/metriful/sensor - -from sensor_functions import * - -# Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() - -# Initiate an on-demand data measurement -I2C_bus.write_byte(i2c_7bit_address, ON_DEMAND_MEASURE_CMD) - -# Now wait for the ready signal (falling edge) before continuing -while (not GPIO.event_detected(READY_pin)): - sleep(0.05) - -# We now know that newly measured data are ready to read. - -######################################################### - -# HUMIDITY - -# Read the humidity value from the MS430 -raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, H_READ, H_BYTES) - -# Decode the humidity: the first byte is the integer part, the -# second byte is the fractional part to one decimal place. -humidity_integer = raw_data[0] -humidity_fraction = raw_data[1] - -# Print it: the units are percentage relative humidity. -print("Humidity = " + str(humidity_integer) + "." + str(humidity_fraction) + " %") - -######################################################### - -# TEMPERATURE - -# Read the temperature value from the MS430 -raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, T_READ, T_BYTES) - -# Decode and print the temperature: - -# Find the positive magnitude of the integer part of the temperature by -# doing a bitwise AND of the first byte with TEMPERATURE_VALUE_MASK -temperature_positive_integer = raw_data[0] & TEMPERATURE_VALUE_MASK - -# The second byte is the fractional part to one decimal place -temperature_fraction = raw_data[1] - -# If the most-significant bit is set, the temperature is negative (below 0 C) -if ((raw_data[0] & TEMPERATURE_SIGN_MASK) == 0): - # Bit not set: temperature is positive - sign_string = "+" -else: - # Bit is set: temperature is negative - sign_string = "-" - -# Print the temperature: the units are degrees Celsius. -print("Temperature = " + sign_string + str(temperature_positive_integer) + "." - + str(temperature_fraction) + " C") - -######################################################### - -# AIR DATA - -# Rather than reading individual data values as shown above, whole -# categories of data can be read in one I2C transaction - -# Read all Air data in one transaction -raw_data = I2C_bus.read_i2c_block_data(i2c_7bit_address, AIR_DATA_READ, AIR_DATA_BYTES) - -# Use the example function to decode the values and return then as a Python dictionary -air_data = extractAirData(raw_data) - -# Print the values obtained -print("Temperature = {:.1f} C".format(air_data['T_C'])) -print("Pressure = " + str(air_data['P_Pa']) + " Pa") -print("Humidity = {:.1f} %".format(air_data['H_pc'])) -print("Gas sensor resistance = " + str(air_data['G_ohm']) + " ohm") - -# Or just use the following function for printing: -writeAirData(None, air_data, False) - -######################################################### - -GPIO.cleanup() - diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..0204a65 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,92 @@ +# Troubleshooting + + + +This file lists solutions for some common problems. Please check it before opening a GitHub issue. + +### Contents +**[Standard checks](#standard-checks)**
+**[ESP8266 problems](#esp8266-problems)**
+**[Arduino Nano 33 IoT problems](#arduino-nano-33-iot-problems)**
+**[WiFi connection problems](#wifi-connection-problems)**
+**[Particle sensor problems](#particle-sensor-problems)**
+**[Slow air quality accuracy change](#slow-air-quality-accuracy-change)**
+**[Temperature measurement is too high](#temperature-measurement-is-too-high)**
+ + +## Standard checks + +Most problems can be resolved by following these steps: + +1. Check that you can run a simple program on your host system **without the MS430 board** e.g. the blink demo on Arduino. +2. Ensure you have the most recent sensor code and instructions from our [GitHub repository](https://www.github.com/metriful/sensor) +3. Remove all wire connections and re-wire carefully. +4. If you have edited the code, go back to the original version and ensure it still works. + + +## ESP8266 problems + +There are many different development boards which use the ESP8266 module. Some may have different pin labels, or have different pin positions, so you may need to research your board or (rarely) edit the host_pin_definitions.h file. + +The ESP8266 does not have a hardware I2C module, so any of the normal GPIO pins can be used for the I2C bus. + + +## Arduino Nano 33 IoT problems + +The GitHub code releases **before v3.1.0** used a software I2C library for the Nano 33 IoT. The code now uses the hardware I2C module, with different pins - please follow the readme file to re-wire your setup. + + +## WiFi connection problems + +If WiFi connection never succeeds, check the following: +* SSID and password are correct +* The WiFi access point is functioning and within range +* WiFi library, Arduino board package, and board firmware versions are up-to-date and compatible +* Power supply is sufficient +* Arduino WiFi antenna is not damaged (very easy to damage on the Arduino Nano 33 IoT) + + +## Particle sensor problems + +### Measured value does not change +* Check wiring +* Check the input voltage - the "5V" must be 4.7-5.3 V +* If using a separate 5V source, the source GND must be connected to the host/MS430 GND. + +### Measured value fluctuates a lot +* This is typical for particle sensors, especially the PPD42 +* Check the input voltage - the "5V" must be 4.7-5.3 V +* Using a separate, regulated 5V supply can reduce the fluctuation. + + +## Slow air quality accuracy change + +The air quality measurement accuracy can be slow to increase, especially with new sensors, for two reasons: + +1. The air quality sensor uses a small internal heater - this will gradually evaporate off impurities when used after a period of storage. + +2. The analysis algorithm self-calibrates in response to a range of air qualities, which must be provided to the sensor after it starts monitoring. + +To speed up this process: + +* Run any code example which repeatedly measures the air quality, with 3 second cycle selected (e.g. cycle_readout, web_server, log_data_to_file) +* Keep it running as long as possible, ideally at least 48 hours +* If the accuracy is low (0 or 1) after running for an hour, expose the sensor to polluted air - a solvent vapor such as from a marker pen is ideal. + +In normal use the accuracy does not remain on 3 (highest) all the time but instead will periodically decrease/increase as calibration is ongoing. + + +## Temperature measurement is too high + +* The temperature sensor measurement may have a small offset compared to the true temperature. + +* The offset is different for each sensor but should remain mostly constant, so you can subtract it in your software after comparing with an accurate thermometer. + +* A larger offset will result if you put the sensor board in a case, especially if together with the host system. Hosts (and particle sensors) create heat, so should be separated or insulated from the sensor board. + + + +## Support + +If the information here does not help, please open a GitHub issue. + diff --git a/User_guide.pdf b/User_guide.pdf index e101da8..0da2eb9 100644 Binary files a/User_guide.pdf and b/User_guide.pdf differ diff --git a/pictures/graph_viewer.png b/pictures/graph_viewer.png new file mode 100644 index 0000000..32609e0 Binary files /dev/null and b/pictures/graph_viewer.png differ diff --git a/pictures/graph_web_server.png b/pictures/graph_web_server.png new file mode 100644 index 0000000..6c04885 Binary files /dev/null and b/pictures/graph_web_server.png differ diff --git a/pictures/group.png b/pictures/group.png new file mode 100644 index 0000000..dd11f9b Binary files /dev/null and b/pictures/group.png differ diff --git a/pictures/home_assistant.png b/pictures/home_assistant.png new file mode 100644 index 0000000..29280fa Binary files /dev/null and b/pictures/home_assistant.png differ diff --git a/pictures/tago.png b/pictures/tago.png new file mode 100644 index 0000000..64ead71 Binary files /dev/null and b/pictures/tago.png differ diff --git a/sensor_pcb.png b/sensor_pcb.png deleted file mode 100644 index 11eca80..0000000 Binary files a/sensor_pcb.png and /dev/null differ