diff --git a/Arduino/Examples/Home_Assistant/Home_Assistant.ino b/Arduino/Examples/Home_Assistant/Home_Assistant.ino index 651dbfe..46d716a 100644 --- a/Arduino/Examples/Home_Assistant/Home_Assistant.ino +++ b/Arduino/Examples/Home_Assistant/Home_Assistant.ino @@ -1,25 +1,27 @@ /* 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 + + An example of using HTTP POST requests to send environment data + from the Metriful MS430 to an installation of Home Assistant on + your local WiFi network. This does not use ESPHome. 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. + * Raspberry Pi Pico W + The non-Arduino boards can also use the separate ESPHome method. + + 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. - Copyright 2020 Metriful Ltd. + Copyright 2020-2023 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt - For code examples, datasheet and user guide, visit + For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ @@ -32,9 +34,9 @@ // 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 +// The details of your WiFi network: +const char * SSID = "PUT WIFI NETWORK NAME HERE"; // network SSID (name) +const char * password = "PUT WIFI PASSWORD HERE"; // Home Assistant settings @@ -47,9 +49,9 @@ char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password // 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" +#define HOME_ASSISTANT_IP "192.168.1.152" -// Security access token: the Readme and User Guide explain how to get this +// Security access token: the Readme explains how to get this #define LONG_LIVED_ACCESS_TOKEN "PASTE YOUR TOKEN HERE WITHIN QUOTES" // END OF USER-EDITABLE SETTINGS @@ -62,8 +64,8 @@ char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password WiFiClient client; // Buffers for assembling http POST requests -char postBuffer[450] = {0}; -char fieldBuffer[70] = {0}; +char postBuffer[500] = {0}; +char fieldBuffer[200] = {0}; // Structs for data AirData_t airData = {0}; @@ -72,32 +74,42 @@ LightData_t lightData = {0}; ParticleData_t particleData = {0}; SoundData_t soundData = {0}; -// Define the display attributes of data sent to Home Assistant. +// Define the display attributes of data to send 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). +// dashboard in Home Assistant. The icons can be chosen at +// https://pictogrammers.com/library/mdi/ // 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}; +HA_Attributes_t pressure = {"Air pressure", "Pa", "weather-partly-rainy", 0}; +HA_Attributes_t humidity = {"Humidity", "%", "cloud-percent", 1}; +HA_Attributes_t illuminance = {"Illuminance", "lux", "white-balance-sunny", 2}; +HA_Attributes_t white_light = {"White light level", "", "circle-outline", 0}; +HA_Attributes_t soundLevel = {"Sound pressure level", "dBA", "microphone", 1}; +HA_Attributes_t peakAmplitude = {"Peak sound amplitude", "mPa", "waveform", 2}; +HA_Attributes_t AQI = {"Air Quality Index", "", "flower-tulip-outline", 1}; +HA_Attributes_t AQ_assessment = {"Air quality", "", "flower-tulip-outline", 0}; +HA_Attributes_t AQ_accuracy = {"Air quality accuracy", "", "magnify", 0}; +HA_Attributes_t gas_resistance = {"Gas sensor resistance", OHM_SYMBOL, "scent", 0}; #if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42) - HA_Attributes_t particulates = {"Particle concentration","ppL","chart-bubble",0}; + HA_Attributes_t particulates = {"Particle concentration", "ppL", "chart-bubble", 0}; #else - HA_Attributes_t particulates = {"Particle concentration",SDS011_UNIT_SYMBOL,"chart-bubble",2}; + 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}; + HA_Attributes_t temperature = {"Temperature", FAHRENHEIT_SYMBOL, "thermometer", 1}; #else - HA_Attributes_t temperature = {"Temperature",CELSIUS_SYMBOL,"thermometer",1}; + HA_Attributes_t temperature = {"Temperature", CELSIUS_SYMBOL, "thermometer", 1}; #endif +HA_Attributes_t soundBands[SOUND_FREQ_BANDS] = {{"SPL at 125 Hz", "dB", "sine-wave", 1}, + {"SPL at 250 Hz", "dB", "sine-wave", 1}, + {"SPL at 500 Hz", "dB", "sine-wave", 1}, + {"SPL at 1000 Hz", "dB", "sine-wave", 1}, + {"SPL at 2000 Hz", "dB", "sine-wave", 1}, + {"SPL at 4000 Hz", "dB", "sine-wave", 1}}; -void setup() { +void setup() +{ // Initialize the host's pins, set up the serial port and reset: SensorHardwareSetup(I2C_ADDRESS); @@ -112,24 +124,27 @@ void setup() { } -void loop() { - +void loop() +{ // Wait for the next new data release, indicated by a falling edge on READY - while (!ready_assertion_event) { + 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, 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) { + 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)); @@ -146,73 +161,93 @@ void loop() { 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(&gas_resistance, airData.G_ohm, 0, true); + sendNumericData(&illuminance, (uint32_t) lightData.illum_lux_int, + lightData.illum_lux_fr_2dp, true); + sendNumericData(&white_light, (uint32_t) lightData.white, 0, 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) { + 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)); + sendTextData(&AQ_accuracy, interpret_AQI_accuracy_brief(airQualityData.AQI_accuracy)); + for (uint8_t i = 0; i < SOUND_FREQ_BANDS; i++) + { + sendNumericData(&soundBands[i], (uint32_t) soundData.SPL_bands_dB_int[i], + soundData.SPL_bands_dB_fr_1dp[i], true); + } } // 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) { + uint8_t valueDecimal, bool isPositive) +{ char valueText[20] = {0}; const char * sign = isPositive ? "" : "-"; - switch (attributes->decimalPlaces) { + switch (attributes->decimalPlaces) + { case 0: default: - sprintf(valueText,"%s%" PRIu32, sign, valueInteger); + snprintf(valueText, sizeof valueText, "%s%" PRIu32, sign, valueInteger); break; case 1: - sprintf(valueText,"%s%" PRIu32 ".%u", sign, valueInteger, valueDecimal); + snprintf(valueText, sizeof valueText, "%s%" PRIu32 ".%u", sign, + valueInteger, valueDecimal); break; case 2: - sprintf(valueText,"%s%" PRIu32 ".%02u", sign, valueInteger, valueDecimal); + snprintf(valueText, sizeof 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) { +// 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); + snprintf(quotedText, sizeof 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) { +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; iname); + for (uint8_t i = 0; i < strlen(fieldBuffer); i++) + { + if (fieldBuffer[i] == ' ') + { fieldBuffer[i] = '_'; } } - sprintf(postBuffer,"POST /api/states/" SENSOR_NAME ".%s HTTP/1.1", fieldBuffer); + snprintf(postBuffer, sizeof postBuffer, "POST /api/states/%s HTTP/1.1", fieldBuffer); client.println(postBuffer); client.println("Host: " HOME_ASSISTANT_IP ":8123"); client.println("Content-Type: application/json"); client.println("Authorization: Bearer " LONG_LIVED_ACCESS_TOKEN); - + // Assemble the JSON content string: - sprintf(postBuffer,"{\"state\":%s,\"attributes\":{\"unit_of_measurement\"" - ":\"%s\",\"friendly_name\":\"%s\",\"icon\":\"mdi:%s\"}}", - valueText, attributes->unit, attributes->name, attributes->icon); - - sprintf(fieldBuffer,"Content-Length: %u", strlen(postBuffer)); + snprintf(postBuffer, sizeof postBuffer, "{\"state\":%s,\"attributes\":" + "{\"unit_of_measurement\":\"%s\",\"friendly_name\":\"%s\",\"icon\":\"mdi:%s\"}}", + valueText, attributes->unit, attributes->name, attributes->icon); + + snprintf(fieldBuffer, sizeof fieldBuffer, "Content-Length: %u", strlen(postBuffer)); client.println(fieldBuffer); client.println(); client.print(postBuffer); } - else { + else + { Serial.println("Client connection failed."); } } diff --git a/Arduino/Examples/IFTTT/IFTTT.ino b/Arduino/Examples/IFTTT/IFTTT.ino index ff27276..51cb5c6 100644 --- a/Arduino/Examples/IFTTT/IFTTT.ino +++ b/Arduino/Examples/IFTTT/IFTTT.ino @@ -1,60 +1,62 @@ /* IFTTT.ino - - Example code for sending data from the Metriful MS430 to IFTTT.com - + + 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) - + * Raspberry Pi Pico W + 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. + email to your inbox, with customizable text. This example requires a WiFi network and internet connection. - Copyright 2020 Metriful Ltd. + Copyright 2020-2023 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt - For code examples, datasheet and user guide, visit + For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ #include #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 +// The details of the WiFi network that we will connect to: +const char * SSID = "PUT WIFI NETWORK NAME HERE"; // network SSID (name) +const char * password = "PUT WIFI PASSWORD HERE"; // 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.",""}; +ThresholdSetting_t humiditySetting = {"humidity", "%", 60, 30, 2, + "Reduce moisture sources.", "Start the humidifier."}; +ThresholdSetting_t airQualitySetting = {"air quality index", "", 150, -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."}; +ThresholdSetting_t temperatureSetting = {"temperature", CELSIUS_SYMBOL, 28, 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; +// e.g. for a 1 hour period, choose inactiveWaitCycles = 12 +uint16_t inactiveWaitCycles = 12; // 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. +// readme. #define WEBHOOKS_KEY "PASTE YOUR KEY HERE WITHIN QUOTES" #define IFTTT_EVENT_NAME "PASTE YOUR EVENT NAME HERE WITHIN QUOTES" @@ -67,7 +69,7 @@ uint16_t inactiveWaitCycles = 24; #endif // Measure the environment data every 300 seconds (5 minutes). This is -// adequate for long-term monitoring. +// recommended for long-term monitoring. uint8_t cycle_period = CYCLE_PERIOD_300_S; WiFiClient client; @@ -81,7 +83,8 @@ AirData_t airData = {0}; AirQualityData_t airQualityData = {0}; -void setup() { +void setup() +{ // Initialize the host's pins, set up the serial port and reset: SensorHardwareSetup(I2C_ADDRESS); @@ -94,21 +97,24 @@ void setup() { } -void loop() { - +void loop() +{ // Wait for the next new data release, indicated by a falling edge on READY - while (!ready_assertion_event) { + 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); + 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) { + 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)); @@ -117,7 +123,8 @@ void loop() { } // Process temperature value and convert if using Fahrenheit - float temperature = convertEncodedTemperatureToFloat(airData.T_C_int_with_sign, airData.T_C_fr_1dp); + float temperature = convertEncodedTemperatureToFloat(airData.T_C_int_with_sign, + airData.T_C_fr_1dp); #ifdef USE_FAHRENHEIT temperature = convertCtoF(temperature); #endif @@ -133,58 +140,69 @@ void loop() { // 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) { - +void checkData(ThresholdSetting_t * setting, int32_t value) +{ // Count down to when the monitoring is active again: - if (setting->inactiveCount > 0) { + if (setting->inactiveCount > 0) + { setting->inactiveCount--; } - if ((value > setting->thresHigh) && (setting->inactiveCount == 0)) { + 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)) { + else if ((value < setting->thresLow) && (setting->inactiveCount == 0)) + { // The variable is below the low threshold setting->inactiveCount = inactiveWaitCycles; sendAlert(setting, value, false); } } +// Create part of the HTTP response in fieldBuffer and append +// it to postBuffer. +void appendResponse(const char * format, ...) +{ + va_list args; + va_start(args, format); + vsnprintf(fieldBuffer, sizeof fieldBuffer, format, args); + va_end(args); + strncat(postBuffer, fieldBuffer, (sizeof postBuffer) - strlen(postBuffer) - 1); +} // 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) { +void sendAlert(ThresholdSetting_t * setting, int32_t value, bool isOverHighThres) +{ client.stop(); - if (client.connect("maker.ifttt.com", 80)) { + 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"); + snprintf(fieldBuffer, sizeof fieldBuffer, "The %s is too %s.", + setting->variableName, isOverHighThres ? "high" : "low"); + snprintf(postBuffer, sizeof postBuffer, "{\"value1\":\"%s\",", fieldBuffer); 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); + appendResponse("\"value2\":\"The measurement was %" PRId32 " %s\"", + value, setting->measurementUnit); + appendResponse(",\"value3\":\"%s\"}", + isOverHighThres ? setting->adviceHigh : setting->adviceLow); - size_t len = strlen(postBuffer); - sprintf(fieldBuffer,"Content-Length: %u",len); + snprintf(fieldBuffer, sizeof fieldBuffer, "Content-Length: %u", strlen(postBuffer)); client.println(fieldBuffer); client.println(); client.print(postBuffer); } - else { + 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 60ecd79..9aa4787 100644 --- a/Arduino/Examples/IoT_cloud_logging/IoT_cloud_logging.ino +++ b/Arduino/Examples/IoT_cloud_logging/IoT_cloud_logging.ino @@ -1,28 +1,30 @@ /* IoT_cloud_logging.ino - - Example IoT data logging code for the Metriful MS430. - + + Example IoT data logging code for the Metriful MS430. + This example is designed for the following WiFi enabled hosts: * Arduino Nano 33 IoT * Arduino MKR WiFi 1010 * ESP8266 boards (e.g. Wemos D1, NodeMCU) * ESP32 boards (e.g. DOIT DevKit v1) - - Environmental data values are measured and logged to an internet - cloud account every 100 seconds, using a WiFi network. The example - gives the choice of using either the Tago.io or Thingspeak.com + * Raspberry Pi Pico W + + Environmental data values are measured and logged to an internet + cloud account every 100 seconds, using a WiFi network. The example + gives the choice of using either the Tago.io or Thingspeak.com clouds – both of these offer a free account for low data rates. - Copyright 2020 Metriful Ltd. + Copyright 2020-2023 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt - For code examples, datasheet and user guide, visit + For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ #include #include +#include ////////////////////////////////////////////////////////// // USER-EDITABLE SETTINGS @@ -32,26 +34,26 @@ // be set to 100 or 300 seconds, not 3 seconds. uint8_t cycle_period = CYCLE_PERIOD_100_S; -// The details of the WiFi network: -char SSID[] = "PUT WIFI NETWORK NAME HERE IN QUOTES"; // network SSID (name) -char password[] = "PUT WIFI PASSWORD HERE IN QUOTES"; // network password +// The details of the WiFi network that we will connect to: +const char * SSID = "PUT WIFI NETWORK NAME HERE"; // network SSID (name) +const char * password = "PUT WIFI PASSWORD HERE"; // IoT cloud settings // This example uses the free IoT cloud hosting services provided // by Tago.io or Thingspeak.com -// Other free cloud providers are available. // An account must have been set up with the relevant cloud provider -// and a WiFi internet connection must exist. See the accompanying -// readme and User Guide for more information. - -// The chosen account's key/token must be put into the relevant define below. -#define TAGO_DEVICE_TOKEN_STRING "PASTE YOUR TOKEN HERE WITHIN QUOTES" -#define THINGSPEAK_API_KEY_STRING "PASTE YOUR API KEY HERE WITHIN QUOTES" +// and a WiFi internet connection must exist. See the readme for +// more information. // Choose which provider to use bool useTagoCloud = true; // To use the ThingSpeak cloud, set: useTagoCloud=false +// 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" +// or +#define THINGSPEAK_API_KEY_STRING "PASTE YOUR API KEY HERE WITHIN QUOTES" + // END OF USER-EDITABLE SETTINGS ////////////////////////////////////////////////////////// @@ -62,8 +64,11 @@ bool useTagoCloud = true; WiFiClient client; // Buffers for assembling http POST requests -char postBuffer[450] = {0}; +char postBuffer[600] = {0}; char fieldBuffer[70] = {0}; +char valueBuffer[20] = {0}; + +typedef enum {FIRST, LAST, OTHER} FirstLast; // Structs for data AirData_t airData = {0}; @@ -72,7 +77,8 @@ LightData_t lightData = {0}; ParticleData_t particleData = {0}; SoundData_t soundData = {0}; -void setup() { +void setup() +{ // Initialize the host's pins, set up the serial port and reset: SensorHardwareSetup(I2C_ADDRESS); @@ -89,23 +95,19 @@ void setup() { } -void loop() { - +void loop() +{ // Wait for the next new data release, indicated by a falling edge on READY - while (!ready_assertion_event) { + while (!ready_assertion_event) + { yield(); } ready_assertion_event = false; - /* Read data from the MS430 into the data structs. - For each category of data (air, sound, etc.) a pointer to the data struct is - passed to the ReceiveI2C() function. The received byte sequence fills the - struct in the correct order so that each field within the struct receives - the value of an environmental quantity (temperature, sound level, etc.) - */ + // Read data from the MS430 into the data structs. // Air data - // Choose output temperature unit (C or F) in Metriful_sensor.h + // You can enable Fahrenheit temperature unit in Metriful_sensor.h ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); /* Air quality data @@ -113,7 +115,8 @@ void loop() { minutes to complete. During this time the accuracy parameter is zero and the data values are not valid. */ - ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, + AIR_QUALITY_DATA_BYTES); // Light data ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); @@ -129,13 +132,15 @@ void loop() { particle data become valid after an initial initialization period of approximately one minute. */ - if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) + { ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); } // Check that WiFi is still connected uint8_t wifiStatus = WiFi.status(); - if (wifiStatus != WL_CONNECTED) { + 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)); @@ -144,10 +149,12 @@ void loop() { } // Send data to the cloud - if (useTagoCloud) { + if (useTagoCloud) + { http_POST_data_Tago_cloud(); } - else { + else + { http_POST_data_Thingspeak_cloud(); } } @@ -163,122 +170,125 @@ void loop() { 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 */ +// Add the field for a single data variable to the Tago HTTP response in +// the postBuffer. +void addTagoVariable(FirstLast firstLast, const char * variableName, + const char * valueFormat, ...) +{ + va_list args; + va_start(args, valueFormat); + vsnprintf(valueBuffer, sizeof valueBuffer, valueFormat, args); + va_end(args); + const char * fieldFormat; + switch (firstLast) + { + case FIRST: + postBuffer[0] = 0; + fieldFormat = "[{\"variable\":\"%s\",\"value\":%s},"; + break; + case LAST: + fieldFormat = "{\"variable\":\"%s\",\"value\":%s}]"; + break; + case OTHER: default: + fieldFormat = "{\"variable\":\"%s\",\"value\":%s},"; + } + snprintf(fieldBuffer, sizeof fieldBuffer, fieldFormat, variableName, valueBuffer); + strncat(postBuffer, fieldBuffer, (sizeof postBuffer) - strlen(postBuffer) - 1); +} + // 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) { +void http_POST_data_Tago_cloud(void) +{ client.stop(); - if (client.connect("api.tago.io", 80)) { + if (client.connect("api.tago.io", 80)) + { client.println("POST /data HTTP/1.1"); client.println("Host: api.tago.io"); client.println("Content-Type: application/json"); client.println("Device-Token: " TAGO_DEVICE_TOKEN_STRING); - + uint8_t T_intPart = 0; uint8_t T_fractionalPart = 0; bool isPositive = true; getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); - sprintf(postBuffer,"[{\"variable\":\"temperature\",\"value\":%s%u.%u}", - isPositive?"":"-", T_intPart, T_fractionalPart); - - sprintf(fieldBuffer,",{\"variable\":\"pressure\",\"value\":%" PRIu32 "}", airData.P_Pa); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,",{\"variable\":\"humidity\",\"value\":%u.%u}", - airData.H_pc_int, airData.H_pc_fr_1dp); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,",{\"variable\":\"aqi\",\"value\":%u.%u}", - airQualityData.AQI_int, airQualityData.AQI_fr_1dp); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,",{\"variable\":\"aqi_string\",\"value\":\"%s\"}", - interpret_AQI_value(airQualityData.AQI_int)); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,",{\"variable\":\"bvoc\",\"value\":%u.%02u}", - airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,",{\"variable\":\"spl\",\"value\":%u.%u}", - soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); - strcat(postBuffer, fieldBuffer); - sprintf(fieldBuffer,",{\"variable\":\"peak_amp\",\"value\":%u.%02u}", - soundData.peak_amp_mPa_int, soundData.peak_amp_mPa_fr_2dp); - strcat(postBuffer, fieldBuffer); + addTagoVariable(FIRST, "temperature", "%s%u.%u", isPositive ? "" : "-", T_intPart, + T_fractionalPart); + addTagoVariable(OTHER, "pressure", "%" PRIu32, airData.P_Pa); + addTagoVariable(OTHER, "humidity", "%u.%u", airData.H_pc_int, airData.H_pc_fr_1dp); + addTagoVariable(OTHER, "aqi", "%u.%u", airQualityData.AQI_int, airQualityData.AQI_fr_1dp); + addTagoVariable(OTHER, "aqi_string", "\"%s\"", interpret_AQI_value(airQualityData.AQI_int)); + addTagoVariable(OTHER, "bvoc", "%u.%02u", airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); + addTagoVariable(OTHER, "spl", "%u.%u", soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); + addTagoVariable(OTHER, "peak_amp", "%u.%02u", soundData.peak_amp_mPa_int, + soundData.peak_amp_mPa_fr_2dp); + addTagoVariable(OTHER, "particulates", "%u.%02u", particleData.concentration_int, + particleData.concentration_fr_2dp); + addTagoVariable(LAST, "illuminance", "%u.%02u", lightData.illum_lux_int, + lightData.illum_lux_fr_2dp); - sprintf(fieldBuffer,",{\"variable\":\"particulates\",\"value\":%u.%02u}", - particleData.concentration_int, particleData.concentration_fr_2dp); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,",{\"variable\":\"illuminance\",\"value\":%u.%02u}]", - lightData.illum_lux_int, lightData.illum_lux_fr_2dp); - strcat(postBuffer, fieldBuffer); - - size_t len = strlen(postBuffer); - sprintf(fieldBuffer,"Content-Length: %u",len); + snprintf(fieldBuffer, sizeof fieldBuffer, "Content-Length: %u", strlen(postBuffer)); client.println(fieldBuffer); client.println(); client.print(postBuffer); } - else { + else + { Serial.println("Client connection failed."); } } +// Add the field for a single data variable to the Thingspeak HTTP +// response in the postBuffer. +void addThingspeakVariable(uint8_t fieldNumber, const char * valueFormat, ...) +{ + va_list args; + va_start(args, valueFormat); + vsnprintf(valueBuffer, sizeof valueBuffer, valueFormat, args); + va_end(args); + snprintf(fieldBuffer, sizeof fieldBuffer, "&field%u=%s", fieldNumber, valueBuffer); + strncat(postBuffer, fieldBuffer, (sizeof postBuffer) - strlen(postBuffer) - 1); +} // 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) { +void http_POST_data_Thingspeak_cloud(void) +{ client.stop(); - if (client.connect("api.thingspeak.com", 80)) { + if (client.connect("api.thingspeak.com", 80)) + { client.println("POST /update HTTP/1.1"); client.println("Host: api.thingspeak.com"); client.println("Content-Type: application/x-www-form-urlencoded"); - - strcpy(postBuffer,"api_key=" THINGSPEAK_API_KEY_STRING); - + uint8_t T_intPart = 0; uint8_t T_fractionalPart = 0; bool isPositive = true; getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); - sprintf(fieldBuffer,"&field1=%s%u.%u", isPositive?"":"-", T_intPart, T_fractionalPart); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,"&field2=%" PRIu32, airData.P_Pa); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,"&field3=%u.%u", airData.H_pc_int, airData.H_pc_fr_1dp); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,"&field4=%u.%u", airQualityData.AQI_int, airQualityData.AQI_fr_1dp); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,"&field5=%u.%02u", airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,"&field6=%u.%u", soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,"&field7=%u.%02u", lightData.illum_lux_int, lightData.illum_lux_fr_2dp); - strcat(postBuffer, fieldBuffer); - - sprintf(fieldBuffer,"&field8=%u.%02u", particleData.concentration_int, - particleData.concentration_fr_2dp); - strcat(postBuffer, fieldBuffer); - size_t len = strlen(postBuffer); - sprintf(fieldBuffer,"Content-Length: %u",len); + snprintf(postBuffer, sizeof postBuffer, "%s", "api_key=" THINGSPEAK_API_KEY_STRING); + addThingspeakVariable(1, "%s%u.%u", isPositive ? "" : "-", T_intPart, T_fractionalPart); + addThingspeakVariable(2, "%" PRIu32, airData.P_Pa); + addThingspeakVariable(3, "%u.%u", airData.H_pc_int, airData.H_pc_fr_1dp); + addThingspeakVariable(4, "%u.%u", airQualityData.AQI_int, airQualityData.AQI_fr_1dp); + addThingspeakVariable(5, "%u.%02u", airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); + addThingspeakVariable(6, "%u.%u", soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); + addThingspeakVariable(7, "%u.%02u", lightData.illum_lux_int, lightData.illum_lux_fr_2dp); + addThingspeakVariable(8, "%u.%02u", particleData.concentration_int, + particleData.concentration_fr_2dp); + snprintf(fieldBuffer, sizeof fieldBuffer, "Content-Length: %u", strlen(postBuffer)); client.println(fieldBuffer); client.println(); client.print(postBuffer); } - else { + else + { Serial.println("Client connection failed."); } } diff --git a/Arduino/Examples/cycle_readout/cycle_readout.ino b/Arduino/Examples/cycle_readout/cycle_readout.ino index 9f53595..d18d66f 100644 --- a/Arduino/Examples/cycle_readout/cycle_readout.ino +++ b/Arduino/Examples/cycle_readout/cycle_readout.ino @@ -1,19 +1,19 @@ /* cycle_readout.ino - Example code for using the Metriful MS430 in cycle mode. - - Continually measures and displays all environment data in - a repeating cycle. User can choose from a cycle time period + Example code for using the Metriful MS430 in cycle mode. + + Continually measures and displays all environment data in + a repeating cycle. User can choose from a cycle time period of 3, 100, or 300 seconds. View the output in the Serial Monitor. - The measurements can be displayed as either labeled text, or as + The measurements can be displayed as either labeled text, or as simple columns of numbers. - Copyright 2020 Metriful Ltd. + Copyright 2020-2023 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt - For code examples, datasheet and user guide, visit + For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ @@ -25,9 +25,10 @@ // How often to read data (every 3, 100, or 300 seconds) uint8_t cycle_period = CYCLE_PERIOD_3_S; -// How to print the data over the serial port. If printDataAsColumns = true, -// data are columns of numbers, useful to copy/paste to a spreadsheet -// application. Otherwise, data are printed with explanatory labels and units. +// How to print the data over the serial port. +// If printDataAsColumns = true, data are columns of numbers, useful +// to copy/paste to a spreadsheet application. Otherwise, data are +// printed with explanatory labels and units. bool printDataAsColumns = false; // END OF USER-EDITABLE SETTINGS @@ -41,19 +42,21 @@ SoundData_t soundData = {0}; ParticleData_t particleData = {0}; -void setup() { +void setup() +{ // Initialize the host pins, set up the serial port and reset: - SensorHardwareSetup(I2C_ADDRESS); - + SensorHardwareSetup(I2C_ADDRESS); + // Apply chosen settings to the MS430 uint8_t particleSensor = PARTICLE_SENSOR; TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); // Wait for the serial port to be ready, for displaying the output - while (!Serial) { + while (!Serial) + { yield(); - } + } Serial.println("Entering cycle mode and waiting for data."); ready_assertion_event = false; @@ -61,9 +64,11 @@ void setup() { } -void loop() { +void loop() +{ // Wait for the next new data release, indicated by a falling edge on READY - while (!ready_assertion_event) { + while (!ready_assertion_event) + { yield(); } ready_assertion_event = false; @@ -95,7 +100,8 @@ void loop() { particle data become valid after an initial initialization period of approximately one minute. */ - if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) + { particleData = getParticleData(I2C_ADDRESS); } @@ -104,7 +110,8 @@ void loop() { printAirQualityData(&airQualityData, printDataAsColumns); printLightData(&lightData, printDataAsColumns); printSoundData(&soundData, printDataAsColumns); - if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + 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 index 5ff48a6..fce1acf 100644 --- a/Arduino/Examples/graph_web_server/graph_web_server.ino +++ b/Arduino/Examples/graph_web_server/graph_web_server.ino @@ -4,26 +4,27 @@ 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 + * Raspberry Pi Pico W + + 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. + + 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 + will still work. + + Copyright 2020-2023 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt - For code examples, datasheet and user guide, visit + For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ @@ -36,30 +37,33 @@ // 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; +uint8_t cycle_period = CYCLE_PERIOD_3_S; -// The BUFFER_LENGTH parameter is the number of data points of each +// The DATA_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 +#define DATA_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. +bool createWifiNetwork = false; +// If creating a WiFi network, you must choose a static (fixed) +// IP address ("theIP"). +// Otherwise, if connecting to an existing network, an IP address is +// automatically allocated. This program displays the IP address on +// the serial monitor, or you can find it on your router software. // 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 +// Also note: some boards (e.g. Pico and ESP8266) may not restart after +// being programmed in Access Point mode and require a power cycle. +const char * SSID = "PUT WIFI NETWORK NAME HERE"; // network SSID (name) +const char * password = "PUT WIFI PASSWORD HERE"; // 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 @@ -85,57 +89,32 @@ 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() { +float temperature_buffer[DATA_BUFFER_LENGTH] = {0}; +float pressure_buffer[DATA_BUFFER_LENGTH] = {0}; +float humidity_buffer[DATA_BUFFER_LENGTH] = {0}; +float AQI_buffer[DATA_BUFFER_LENGTH] = {0}; +float bVOC_buffer[DATA_BUFFER_LENGTH] = {0}; +float SPL_buffer[DATA_BUFFER_LENGTH] = {0}; +float illuminance_buffer[DATA_BUFFER_LENGTH] = {0}; +float particle_buffer[DATA_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) { + // Wait for serial to start functioning correctly: + delay(2000); + + if (!wifiCreateOrConnect(createWifiNetwork, true, SSID, password, theIP)) + { + Serial.println("Failed to set up WiFi."); + while (true) + { 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(); @@ -143,13 +122,16 @@ void setup() { //////////////////////////////////////////////////////////////////// // Get time period value to send to web page - if (cycle_period == CYCLE_PERIOD_3_S) { + if (cycle_period == CYCLE_PERIOD_3_S) + { dataPeriod_s = 3; } - else if (cycle_period == CYCLE_PERIOD_100_S) { + else if (cycle_period == CYCLE_PERIOD_100_S) + { dataPeriod_s = 100; } - else { // CYCLE_PERIOD_300_S + else + { // CYCLE_PERIOD_300_S dataPeriod_s = 300; } @@ -161,10 +143,11 @@ void setup() { TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); } -void loop() { - +void loop() +{ // Respond to the web page client requests while waiting for new data - while (!ready_assertion_event) { + while (!ready_assertion_event) + { handleClientRequests(); yield(); } @@ -181,28 +164,33 @@ void loop() { updateDataBuffers(); // Check WiFi is still connected - if (!createWifiNetwork) { + if (!createWifiNetwork) + { uint8_t wifiStatus = WiFi.status(); - if (wifiStatus != WL_CONNECTED) { + 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.print("Reconnected. 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 +// Store the data, up to a maximum length of DATA_BUFFER_LENGTH, then start // discarding the oldest data in a FIFO scheme ("First In First Out") -void updateDataBuffers(void) { +void updateDataBuffers(void) +{ uint16_t position = 0; - if (bufferLength == BUFFER_LENGTH) { + if (bufferLength == DATA_BUFFER_LENGTH) + { // Buffers are full: shift all data values along, discarding the oldest - for (uint16_t i=0; i<(BUFFER_LENGTH-1); i++) { + for (uint16_t i = 0; i < (DATA_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]; @@ -212,9 +200,10 @@ void updateDataBuffers(void) { illuminance_buffer[i] = illuminance_buffer[i+1]; particle_buffer[i] = particle_buffer[i+1]; } - position = BUFFER_LENGTH-1; + position = DATA_BUFFER_LENGTH - 1; } - else { + else + { // Buffers are not yet full; keep filling them position = bufferLength; bufferLength++; @@ -239,82 +228,99 @@ void updateDataBuffers(void) { #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) { +void handleClientRequests(void) +{ // Check for incoming client requests - WiFiClient client = server.available(); - if (client) { - + WiFiClient client = getClient(&server); + 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()) { + 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) + 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) { + else if (uriCount < URI_CHARS) + { // Assemble the URI, up to a fixed number of characters uriBuffer[uriCount] = c; uriCount++; } - else { + else + { // Now use the assembled method and URI to decide how to respond - if (strcmp(requestBuffer, GET_REQUEST_STR) == 0) { + 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] == ' ') { + if (uriBuffer[0] == ' ') + { // The web page is requested + client.write(pageHeader, strlen(pageHeader)); sendData(&client, (const uint8_t *) graphWebPage, strlen(graphWebPage)); break; } - else if ((uriBuffer[0] == '1') && (uriBuffer[1] == ' ')) { + 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] == ' ')) { + 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. + // 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 + #if !defined(ESP8266) && !defined(ARDUINO_ARCH_RP2040) client.stop(); #endif } } -// Send all buffered data in the HTTP response. Binary format ("octet-stream") +// 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) { +void sendAllData(WiFiClient * clientPtr) +{ clientPtr->print(dataHeader); - // First send the time period, so the web page knows when to do the next request + // First send the time period clientPtr->write((const uint8_t *) &dataPeriod_s, sizeof(uint16_t)); - // Send temperature unit and particle sensor type, combined into one byte + // Send particle sensor type uint8_t codeByte = (uint8_t) PARTICLE_SENSOR; + clientPtr->write((const uint8_t *) &codeByte, sizeof(uint8_t)); + // Send temperature unit + codeByte = 0; #ifdef USE_FAHRENHEIT - codeByte = codeByte | 0x10; + codeByte = 1; #endif clientPtr->write((const uint8_t *) &codeByte, sizeof(uint8_t)); - // Send the length of the data buffers (the number of values of each variable) + // Send the length of the data buffers (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) { + 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)); @@ -331,10 +337,12 @@ void sendAllData(WiFiClient * clientPtr) { // Send just the most recent value of each variable (or no data if no values // have been read yet) -void sendLatestData(WiFiClient * clientPtr) { +void sendLatestData(WiFiClient * clientPtr) +{ clientPtr->print(dataHeader); - if (bufferLength > 0) { - uint16_t bufferPosition = bufferLength-1; + 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)); @@ -342,7 +350,8 @@ void sendLatestData(WiFiClient * clientPtr) { 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) { + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) + { clientPtr->write((const uint8_t *) &(particle_buffer[bufferPosition]), sizeof(float)); } } @@ -352,15 +361,18 @@ void sendLatestData(WiFiClient * clientPtr) { // 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) { +void sendData(WiFiClient * clientPtr, const uint8_t * dataPtr, size_t dataLength) +{ + while (dataLength > 0) + { size_t sendLength = dataLength; - if (sendLength > MAX_DATA_BYTES) { + if (sendLength > MAX_DATA_BYTES) + { sendLength = MAX_DATA_BYTES; } clientPtr->write(dataPtr, sendLength); delay(10); - dataLength-=sendLength; - dataPtr+=sendLength; + dataLength -= sendLength; + dataPtr += sendLength; } } diff --git a/Arduino/Examples/interrupts/interrupts.ino b/Arduino/Examples/interrupts/interrupts.ino index 37bab65..d87f3b5 100644 --- a/Arduino/Examples/interrupts/interrupts.ino +++ b/Arduino/Examples/interrupts/interrupts.ino @@ -1,18 +1,18 @@ /* interrupts.ino - Example code for using the Metriful MS430 interrupt outputs. - - Light and sound interrupts are configured and the program then + Example code for using the Metriful MS430 interrupt outputs. + + Light and sound interrupts are configured and the program then waits forever. When an interrupt occurs, a message prints over - the serial port, the interrupt is cleared (if set to latch type), - and the program returns to waiting. + the serial port, the interrupt is cleared (if set to latch type), + and the program returns to waiting. View the output in the Serial Monitor. - Copyright 2020 Metriful Ltd. + Copyright 2020-2023 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt - For code examples, datasheet and user guide, visit + For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ @@ -36,7 +36,6 @@ uint8_t light_int_thres_lux_f2dp = 50; // light_int_thres_lux_i = 56 // light_int_thres_lux_f2dp = 12 - // Sound level interrupt settings bool enableSoundInterrupts = true; @@ -48,27 +47,32 @@ uint16_t sound_thres_mPa = 100; uint8_t transmit_buffer[1] = {0}; - -void setup() { +void setup() +{ // Initialize the host pins, set up the serial port and reset SensorHardwareSetup(I2C_ADDRESS); // check that the chosen light threshold is a valid value - if (light_int_thres_lux_i > MAX_LUX_VALUE) { + if (light_int_thres_lux_i > MAX_LUX_VALUE) + { Serial.println("The chosen light interrupt threshold exceeds the maximum allowed value."); - while (true) { + while (true) + { yield(); } } - if ((!enableSoundInterrupts)&&(!enableLightInterrupts)) { + if ((!enableSoundInterrupts) && (!enableLightInterrupts)) + { Serial.println("No interrupts have been selected."); - while (true) { + while (true) + { yield(); } } - if (enableSoundInterrupts) { + if (enableSoundInterrupts) + { // Set the interrupt type (latch or comparator) transmit_buffer[0] = sound_int_type; TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_TYPE_REG, transmit_buffer, 1); @@ -81,7 +85,8 @@ void setup() { TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_ENABLE_REG, transmit_buffer, 1); } - if (enableLightInterrupts) { + if (enableLightInterrupts) + { // Set the interrupt type (latch or comparator) transmit_buffer[0] = light_int_type; TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_TYPE_REG, transmit_buffer, 1); @@ -99,7 +104,8 @@ void setup() { } // Wait for the serial port to be ready, for displaying the output - while (!Serial) { + while (!Serial) + { yield(); } @@ -108,21 +114,26 @@ void setup() { } -void loop() { +void loop() +{ // Check whether a light interrupt has occurred - if ((digitalRead(L_INT_PIN) == LOW) && enableLightInterrupts) { + if ((digitalRead(L_INT_PIN) == LOW) && enableLightInterrupts) + { Serial.println("LIGHT INTERRUPT."); - if (light_int_type == LIGHT_INT_TYPE_LATCH) { + if (light_int_type == LIGHT_INT_TYPE_LATCH) + { // Latch type interrupts remain set until cleared by command TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_CLR_CMD, 0, 0); } } // Check whether a sound interrupt has occurred - if ((digitalRead(S_INT_PIN) == LOW) && enableSoundInterrupts) { + if ((digitalRead(S_INT_PIN) == LOW) && enableSoundInterrupts) + { Serial.println("SOUND INTERRUPT."); - if (sound_int_type == SOUND_INT_TYPE_LATCH) { + if (sound_int_type == SOUND_INT_TYPE_LATCH) + { // Latch type interrupts remain set until cleared by command TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_CLR_CMD, 0, 0); } diff --git a/Arduino/Examples/on_demand_readout/on_demand_readout.ino b/Arduino/Examples/on_demand_readout/on_demand_readout.ino index 8509dde..5b4b28b 100644 --- a/Arduino/Examples/on_demand_readout/on_demand_readout.ino +++ b/Arduino/Examples/on_demand_readout/on_demand_readout.ino @@ -1,16 +1,16 @@ /* on_demand_readout.ino - Example code for using the Metriful MS430 in "on-demand" mode. - + Example code for using the Metriful MS430 in "on-demand" mode. + Repeatedly measures and displays all environment data, with a pause - between measurements. Air quality data are unavailable in this mode + between measurements. Air quality data are unavailable in this mode (instead see cycle_readout.ino). View output in the Serial Monitor. - Copyright 2020 Metriful Ltd. + Copyright 2020-2023 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt - For code examples, datasheet and user guide, visit + For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ @@ -21,8 +21,8 @@ // Pause (in milliseconds) between data measurements (note that the // measurement itself takes 0.5 seconds) -uint32_t pause_ms = 4500; -// Choosing a pause of less than 2000 ms will cause inaccurate +uint32_t pause_ms = 2500; +// Choosing a pause of less than about 2000 ms will cause inaccurate // temperature, humidity and particle data. // How to print the data over the serial port. If printDataAsColumns = true, @@ -40,7 +40,8 @@ SoundData_t soundData = {0}; ParticleData_t particleData = {0}; -void setup() { +void setup() +{ // Initialize the host pins, set up the serial port and reset: SensorHardwareSetup(I2C_ADDRESS); @@ -48,20 +49,22 @@ void setup() { TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); // Wait for the serial port to be ready, for displaying the output - while (!Serial) { + while (!Serial) + { yield(); } } -void loop() { - +void loop() +{ // Trigger a new measurement ready_assertion_event = false; TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); // Wait for the measurement to finish, indicated by a falling edge on READY - while (!ready_assertion_event) { + while (!ready_assertion_event) + { yield(); } @@ -87,7 +90,8 @@ void loop() { particle data become valid after an initial initialization period of approximately one minute. */ - if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) + { particleData = getParticleData(I2C_ADDRESS); } @@ -95,7 +99,8 @@ void loop() { printAirData(&airData, printDataAsColumns); printLightData(&lightData, printDataAsColumns); printSoundData(&soundData, printDataAsColumns); - if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) + { printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR); } Serial.println(); diff --git a/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino b/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino index d9a8722..005eea2 100644 --- a/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino +++ b/Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino @@ -1,23 +1,23 @@ /* particle_sensor_toggle.ino - Optional advanced demo. This program shows how to generate an output - control signal from one of the host's pins, which can be used to turn + Optional advanced demo. This program shows how to generate an output + control signal from one of the host's pins, which can be used to turn the particle sensor on and off. An external transistor circuit is - also needed - this will gate the sensor power supply according to + also needed - this will gate the sensor power supply according to the control signal. Further details are given in the User Guide. - + The program continually measures and displays all environment data - in a repeating cycle. The user can view the output in the Serial - Monitor. After reading the data, the particle sensor is powered off - for a chosen number of cycles ("off_cycles"). It is then powered on - and read before being powered off again. Sound data are ignored + in a repeating cycle. The user can view the output in the Serial + Monitor. After reading the data, the particle sensor is powered off + for a chosen number of cycles ("off_cycles"). It is then powered on + and read before being powered off again. Sound data are ignored while the particle sensor is on, to avoid its fan noise. - Copyright 2020 Metriful Ltd. + Copyright 2020-2023 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt - For code examples, datasheet and user guide, visit + For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ @@ -60,7 +60,8 @@ bool particleSensorIsOn = false; uint8_t particleSensor_count = 0; -void setup() { +void setup() +{ // Initialize the host pins, set up the serial port and reset: SensorHardwareSetup(I2C_ADDRESS); @@ -76,7 +77,8 @@ void setup() { TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1); // Wait for the serial port to be ready, for displaying the output - while (!Serial) { + while (!Serial) + { yield(); } @@ -86,9 +88,11 @@ void setup() { } -void loop() { +void loop() +{ // Wait for the next new data release, indicated by a falling edge on READY - while (!ready_assertion_event) { + while (!ready_assertion_event) + { yield(); } ready_assertion_event = false; @@ -108,13 +112,15 @@ void loop() { minutes to complete. During this time the accuracy parameter is zero and the data values are not valid. */ - ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, + AIR_QUALITY_DATA_BYTES); // Light data ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); // Sound data - only read when particle sensor is off - if (!particleSensorIsOn) { + if (!particleSensorIsOn) + { ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); } @@ -126,7 +132,8 @@ void loop() { particle data become valid after an initial initialization period of approximately one minute. */ - if (particleSensorIsOn) { + if (particleSensorIsOn) + { ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); } @@ -140,7 +147,8 @@ void loop() { Serial.println(); // Turn the particle sensor on/off if required - if (particleSensorIsOn) { + if (particleSensorIsOn) + { // Stop the particle detection on the MS430 transmit_buffer[0] = OFF; TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); @@ -149,9 +157,11 @@ void loop() { digitalWrite(particle_sensor_control_pin, !particle_sensor_ON_state); particleSensorIsOn = false; } - else { + else + { particleSensor_count++; - if (particleSensor_count >= off_cycles) { + if (particleSensor_count >= off_cycles) + { // Turn on the hardware: digitalWrite(particle_sensor_control_pin, particle_sensor_ON_state); 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 c2ff5eb..2d0cb8e 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 @@ -1,29 +1,31 @@ /* simple_read_T_H.ino - Example code for using the Metriful MS430 to measure humidity - and temperature. - - Demonstrates multiple ways of reading and displaying the temperature - and humidity data. View the output in the Serial Monitor. The other + Example code for using the Metriful MS430 to measure humidity + and temperature. + + Demonstrates multiple ways of reading and displaying the temperature + and humidity data. View the output in the Serial Monitor. The other data can be measured and displayed in a similar way. - Copyright 2020 Metriful Ltd. + Copyright 2020-2023 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt - For code examples, datasheet and user guide, visit + For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ #include -void setup() { +void setup() +{ // Initialize the host pins, set up the serial port and reset: SensorHardwareSetup(I2C_ADDRESS); // Wait for the serial port to be ready, for displaying the output - while (!Serial) { + while (!Serial) + { yield(); } @@ -34,7 +36,8 @@ void setup() { TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); // Now wait for the ready signal before continuing - while (!ready_assertion_event) { + while (!ready_assertion_event) + { yield(); } @@ -59,7 +62,7 @@ void setup() { // 2. After reading from the MS430, you can also access and print the - // float data directly from the struct: + // float data directly from the float struct: Serial.print("The temperature is: "); Serial.print(airDataF.T_C, 1); // print to 1 decimal place Serial.println(" " CELSIUS_SYMBOL); @@ -87,7 +90,7 @@ void setup() { Serial.println("-----------------------------"); - // 4. Access and print integer data directly from the struct: + // 4. Access and print integer data directly from the integer struct: Serial.print("The humidity is: "); Serial.print(airData.H_pc_int); // the integer part of the value Serial.print("."); // the decimal point @@ -134,7 +137,8 @@ void setup() { Serial.print("Temperature = "); // If the most-significant bit of the first byte is a 1, the temperature // is negative (below 0 C), otherwise it is positive - if ((receive_buffer[0] & TEMPERATURE_SIGN_MASK) != 0) { + if ((receive_buffer[0] & TEMPERATURE_SIGN_MASK) != 0) + { // The bit is a 1: celsius temperature is negative Serial.print("-"); } @@ -145,6 +149,7 @@ void setup() { } -void loop() { +void loop() +{ // There is no loop for this program. } diff --git a/Arduino/Examples/simple_read_sound/simple_read_sound.ino b/Arduino/Examples/simple_read_sound/simple_read_sound.ino index 01d537a..d4969bf 100644 --- a/Arduino/Examples/simple_read_sound/simple_read_sound.ino +++ b/Arduino/Examples/simple_read_sound/simple_read_sound.ino @@ -1,28 +1,30 @@ /* simple_read_sound.ino - Example code for using the Metriful MS430 to measure sound. - - Demonstrates multiple ways of reading and displaying the sound data. + Example code for using the Metriful MS430 to measure sound. + + Demonstrates multiple ways of reading and displaying the sound data. View the output in the Serial Monitor. The other data can be measured and displayed in a similar way. - Copyright 2020 Metriful Ltd. + Copyright 2020-2023 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt - For code examples, datasheet and user guide, visit + For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ #include -void setup() { +void setup() +{ // Initialize the host pins, set up the serial port and reset: SensorHardwareSetup(I2C_ADDRESS); // Wait for the serial port to be ready, for displaying the output - while (!Serial) { + while (!Serial) + { yield(); } @@ -41,7 +43,8 @@ void setup() { TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); // Now wait for the ready signal (falling edge) before continuing - while (!ready_assertion_event) { + while (!ready_assertion_event) + { yield(); } @@ -64,7 +67,7 @@ void setup() { // 2. After reading from the MS430, you can also access and print the - // float data directly from the struct: + // float data directly from the float struct: Serial.print("The sound pressure level is: "); Serial.print(soundDataF.SPL_dBA, 1); // print to 1 decimal place Serial.println(" dBA"); @@ -83,7 +86,7 @@ void setup() { Serial.println("-----------------------------"); - // 4. Access and print integer data directly from the struct: + // 4. Access and print integer data directly from the integer 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 @@ -93,6 +96,7 @@ void setup() { Serial.println("-----------------------------"); } -void loop() { +void loop() +{ // There is no loop for this program. } diff --git a/Arduino/Examples/web_server/web_server.ino b/Arduino/Examples/web_server/web_server.ino index 1921ed6..ba30db7 100644 --- a/Arduino/Examples/web_server/web_server.ino +++ b/Arduino/Examples/web_server/web_server.ino @@ -3,51 +3,58 @@ Example code for serving a web page over a WiFi network, displaying environment data read from the Metriful MS430. - + This example is designed for the following WiFi enabled hosts: * Arduino Nano 33 IoT * Arduino MKR WiFi 1010 * ESP8266 boards (e.g. Wemos D1, NodeMCU) * ESP32 boards (e.g. DOIT DevKit v1) + * Raspberry Pi Pico W + + All environment data values are measured and displayed on a text + web page generated by the host, which acts as a simple web server. - 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 + The host can either connect to an existing WiFi network, or generate its own for other devices to connect to (Access Point mode). - Copyright 2020 Metriful Ltd. + Copyright 2020-2023 Metriful Ltd. Licensed under the MIT License - for further details see LICENSE.txt - For code examples, datasheet and user guide, visit + For code examples, datasheet and user guide, visit https://github.com/metriful/sensor */ #include #include +#include +#include ////////////////////////////////////////////////////////// // USER-EDITABLE SETTINGS // Choose how often to read and update data (every 3, 100, or 300 seconds) -// The web page can be refreshed more often but the data will not change +// The web page can be refreshed more often but the data will not change. +// 100 or 300 seconds are recommended for long-term monitoring. uint8_t cycle_period = CYCLE_PERIOD_3_S; // Choose whether to create a new WiFi network (host as Access Point), // or connect to an existing WiFi network. bool createWifiNetwork = false; -// If creating a WiFi network, a static (fixed) IP address ("theIP") is -// specified by the user. Otherwise, if connecting to an existing -// network, an IP address is automatically allocated and the serial -// output must be viewed at startup to see this allocated IP address. +// If creating a WiFi network, you must choose a static (fixed) +// IP address ("theIP"). +// Otherwise, if connecting to an existing network, an IP address is +// automatically allocated. This program displays the IP address on +// the serial monitor, or you can find it on your router software. // 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 +// Also note: some boards (e.g. Pico and ESP8266) may not restart after +// being programmed in Access Point mode and require a power cycle. +const char * SSID = "PUT WIFI NETWORK NAME HERE"; // network SSID (name) +const char * password = "PUT WIFI PASSWORD HERE"; // 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 @@ -74,43 +81,25 @@ ParticleData_t particleData = {0}; SoundData_t soundData = {0}; // Storage for the web page text -char lineBuffer[100] = {0}; -char pageBuffer[2300] = {0}; +char valueBuffer[20] = {0}; +char lineBuffer[150] = {0}; +char pageBuffer[3000] = {0}; -void setup() { +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) { + SensorHardwareSetup(I2C_ADDRESS); + // Wait for serial to start functioning correctly: + delay(2000); + + if (!wifiCreateOrConnect(createWifiNetwork, true, SSID, password, theIP)) + { + Serial.println("Failed to set up WiFi."); + while (true) + { 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(); @@ -121,13 +110,16 @@ void setup() { // least as often as new data are obtained. A more frequent refresh is // best for long cycle periods because the page refresh is not // synchronized with the cycle. Users can also manually refresh the page. - if (cycle_period == CYCLE_PERIOD_3_S) { + if (cycle_period == CYCLE_PERIOD_3_S) + { refreshPeriodSeconds = 3; } - else if (cycle_period == CYCLE_PERIOD_100_S) { + else if (cycle_period == CYCLE_PERIOD_100_S) + { refreshPeriodSeconds = 30; } - else { // CYCLE_PERIOD_300_S + else + { // CYCLE_PERIOD_300_S refreshPeriodSeconds = 50; } @@ -139,25 +131,20 @@ void setup() { TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); } -void loop() { - +void loop() +{ // While waiting for the next data release, respond to client requests // by serving the web page with the last available data. Initially the // data will be all zero (until the first data readout has completed). - while (!ready_assertion_event) { + while (!ready_assertion_event) + { handleClientRequests(); yield(); } ready_assertion_event = false; - // new data are now ready - - /* Read data from the MS430 into the data structs. - For each category of data (air, sound, etc.) a pointer to the data struct is - passed to the ReceiveI2C() function. The received byte sequence fills the data - struct in the correct order so that each field within the struct receives - the value of an environmental quantity (temperature, sound level, etc.) - */ + // New data are now ready. + // Read data from the MS430 into the data structs. // Air data // Choose output temperature unit (C or F) in Metriful_sensor.h @@ -168,7 +155,8 @@ void loop() { minutes to complete. During this time the accuracy parameter is zero and the data values are not valid. */ - ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, AIR_QUALITY_DATA_BYTES); + ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, + AIR_QUALITY_DATA_BYTES); // Light data ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); @@ -184,7 +172,8 @@ void loop() { particle data become valid after an initial initialization period of approximately one minute. */ - if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) + { ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); } @@ -192,15 +181,17 @@ void loop() { assembleWebPage(); // Check WiFi is still connected - if (!createWifiNetwork) { + if (!createWifiNetwork) + { uint8_t wifiStatus = WiFi.status(); - if (wifiStatus != WL_CONNECTED) { + 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.print("Reconnected. View your page at http://"); Serial.println(theIP); ready_assertion_event = false; } @@ -208,26 +199,33 @@ void loop() { } -void handleClientRequests(void) { +void handleClientRequests(void) +{ // Check for incoming client requests - WiFiClient client = server.available(); - if (client) { + WiFiClient client = getClient(&server); + if (client) + { bool blankLine = false; - while (client.connected()) { - if (client.available()) { + while (client.connected()) + { + if (client.available()) + { char c = client.read(); if (c == '\n') { - // Two consecutive newline characters indicates the end of the client HTTP request - if (blankLine) { + // Two consecutive newline characters indicates the end of the HTTP request + if (blankLine) + { // Send the page as a response client.print(pageBuffer); break; } - else { + else + { blankLine = true; } } - else if (c != '\r') { + else if (c != '\r') + { // Carriage return (\r) is disregarded for blank line detection blankLine = false; } @@ -239,142 +237,116 @@ void handleClientRequests(void) { } } +// Start an HTML table for data display. +void startTable(const char * tableName) +{ + snprintf(lineBuffer, sizeof lineBuffer, tableStart, tableName); + strncat(pageBuffer, lineBuffer, (sizeof pageBuffer) - strlen(pageBuffer) - 1); +} + +// Add an HTML table row to the web page buffer, to display +// data for one variable. +void addTableRow(const char * dataName, const uint8_t styleNumber, + const char * unit, const char * valueFormat, ...) +{ + va_list args; + va_start(args, valueFormat); + vsnprintf(valueBuffer, sizeof valueBuffer, valueFormat, args); + va_end(args); + snprintf(lineBuffer, sizeof lineBuffer, tableRow, dataName, styleNumber, valueBuffer, unit); + strncat(pageBuffer, lineBuffer, (sizeof pageBuffer) - strlen(pageBuffer) - 1); +} + // Create a simple text web page showing the environment data in // separate category tables, using HTML and CSS -void assembleWebPage(void) { - sprintf(pageBuffer,"HTTP/1.1 200 OK\r\n" - "Content-type: text/html\r\n" - "Connection: close\r\n" - "Refresh: %u\r\n\r\n",refreshPeriodSeconds); - - strcat(pageBuffer,"" - "" - "Metriful Sensor Demo" - "" - "

Indoor Environment Data

"); +void assembleWebPage(void) +{ + snprintf(pageBuffer, sizeof pageBuffer, responseHeader, refreshPeriodSeconds); + strncat(pageBuffer, pageStart, (sizeof pageBuffer) - strlen(pageBuffer) - 1); ////////////////////////////////////// - strcat(pageBuffer,"

Air Data

"); - 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); - strcat(pageBuffer,lineBuffer); - - sprintf(lineBuffer,"", - airData.H_pc_int, airData.H_pc_fr_1dp); - strcat(pageBuffer,lineBuffer); - - sprintf(lineBuffer,"" - "
Temperature%s%u.%u%s
Pressure%" PRIu32 "Pa
Humidity%u.%u%%
Gas Sensor Resistance%" PRIu32 "" OHM_SYMBOL "

", - airData.G_ohm); - strcat(pageBuffer,lineBuffer); + + startTable("Air Data"); + addTableRow("Temperature", 1, unit, "%s%u.%u", + isPositive ? "" : "-", T_intPart, T_fractionalPart); + addTableRow("Pressure", 1, "Pa", "%" PRIu32, airData.P_Pa); + addTableRow("Humidity", 1, "%", "%u.%u", airData.H_pc_int, airData.H_pc_fr_1dp); + addTableRow("Gas Sensor Resistance", 1, OHM_SYMBOL, "%" PRIu32, airData.G_ohm); + strncat(pageBuffer, tableEnd, (sizeof pageBuffer) - strlen(pageBuffer) - 1); ////////////////////////////////////// - - strcat(pageBuffer,"

Air Quality Data

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

",interpret_AQI_accuracy(airQualityData.AQI_accuracy)); - strcat(pageBuffer,lineBuffer); + + if (airQualityData.AQI_accuracy == 0) + { + snprintf(lineBuffer, sizeof lineBuffer, "

Air Quality Data

%s

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

", - airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); - strcat(pageBuffer,lineBuffer); + else + { + startTable("Air Quality Data"); + addTableRow("Air Quality Index", 2, "", "%u.%u", + airQualityData.AQI_int, airQualityData.AQI_fr_1dp); + addTableRow("Air Quality Summary", 2, "", "%s", + interpret_AQI_value(airQualityData.AQI_int)); + addTableRow("Estimated CO" SUBSCRIPT_2, 2, "ppm", "%u.%u", + airQualityData.CO2e_int, airQualityData.CO2e_fr_1dp); + addTableRow("Equivalent Breath VOC", 2, "ppm", "%u.%02u", + airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); + strncat(pageBuffer, tableEnd, (sizeof pageBuffer) - strlen(pageBuffer) - 1); } ////////////////////////////////////// - - strcat(pageBuffer,"

Sound Data

"); - - sprintf(lineBuffer,"" - "", - soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); - strcat(pageBuffer,lineBuffer); - - for (uint8_t i=0; i" - "", - i+1, sound_band_mids_Hz[i], soundData.SPL_bands_dB_int[i], soundData.SPL_bands_dB_fr_1dp[i]); - strcat(pageBuffer,lineBuffer); + + startTable("Sound Data"); + addTableRow("A-weighted Sound Pressure Level", 3, "dBA", "%u.%u", + soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); + + for (uint8_t i=0; i" + "", + i+1, sound_band_mids_Hz[i], soundData.SPL_bands_dB_int[i], + soundData.SPL_bands_dB_fr_1dp[i]); + strncat(pageBuffer, lineBuffer, (sizeof pageBuffer) - strlen(pageBuffer) - 1); } - - sprintf(lineBuffer,"" - "
A-weighted Sound Pressure Level%u.%udBA
Frequency Band %u (%u 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); - strcat(pageBuffer,lineBuffer); + + addTableRow("Peak Sound Amplitude", 3, "mPa", "%u.%02u", + soundData.peak_amp_mPa_int, soundData.peak_amp_mPa_fr_2dp); + strncat(pageBuffer, tableEnd, (sizeof pageBuffer) - strlen(pageBuffer) - 1); ////////////////////////////////////// - - strcat(pageBuffer,"

Light Data

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

", lightData.white); - strcat(pageBuffer,lineBuffer); - + + startTable("Light Data"); + addTableRow("Illuminance", 4, "lux", "%u.%02u", + lightData.illum_lux_int, lightData.illum_lux_fr_2dp); + addTableRow("White Light Level", 4, "", "%u", lightData.white); + strncat(pageBuffer, tableEnd, (sizeof pageBuffer) - strlen(pageBuffer) - 1); + ////////////////////////////////////// - - if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) { - strcat(pageBuffer,"

Air Particulate Data

"); - - sprintf(lineBuffer,"", - particleData.duty_cycle_pc_int, particleData.duty_cycle_pc_fr_2dp); - strcat(pageBuffer,lineBuffer); - - char unitsBuffer[7] = {0}; - if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42) { - strcpy(unitsBuffer,"ppL"); - } - else if (PARTICLE_SENSOR == PARTICLE_SENSOR_SDS011) { - strcpy(unitsBuffer,SDS011_UNIT_SYMBOL); - } - else { - strcpy(unitsBuffer,"(?)"); + + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) + { + const char * particleUnit = SDS011_UNIT_SYMBOL; + if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42) + { + particleUnit = "ppL"; } - sprintf(lineBuffer,"" - "
Sensor Duty Cycle%u.%02u%%
Particle Concentration%u.%02u%s

", - particleData.concentration_int, particleData.concentration_fr_2dp, unitsBuffer); - strcat(pageBuffer,lineBuffer); + startTable("Air Particulate Data"); + addTableRow("Sensor Duty Cycle", 5, "%", "%u.%02u", + particleData.duty_cycle_pc_int, particleData.duty_cycle_pc_fr_2dp); + addTableRow("Particle Concentration", 5, particleUnit, "%u.%02u", + particleData.concentration_int, particleData.concentration_fr_2dp); + strncat(pageBuffer, tableEnd, (sizeof pageBuffer) - strlen(pageBuffer) - 1); } ////////////////////////////////////// - - strcat(pageBuffer,""); + + strncat(pageBuffer, pageEnd, (sizeof pageBuffer) - strlen(pageBuffer) - 1); } diff --git a/Arduino/Metriful_Sensor/ESPHome_patch.yaml b/Arduino/Metriful_Sensor/ESPHome_patch.yaml new file mode 100644 index 0000000..554116b --- /dev/null +++ b/Arduino/Metriful_Sensor/ESPHome_patch.yaml @@ -0,0 +1,39 @@ +# Use the MS430 with ESPHome by adding this file to your yaml +# device configuration file. + +# Replace the "esphome:" section in your wizard-generated yaml file +# with the entire contents of this file and then edit the +# three substitutions. + +# "device_name" identifies the microcontroller board. + +# "friendly_name" is prepended to each sensor name and displayed by +# default on dashboard cards in Home Assistant. These default names +# can be overridden manually with new names in Home Assistant. + +substitutions: + device_name: put_your_device_name_here + friendly_name: " " # Prefer a space here to leave the sensor names unchanged + temperature_offset: "0.0" # Optional adjustment for temperature sensor + +# The optional particle sensor is configured in Metriful_sensor.h +# Temperature is always sent to Home Assistant as Celsius and can +# be displayed there as Fahrenheit if required. + +# The file below this line should not require any edits. +############################################################################### + +esphome: + name: ${device_name} + friendly_name: ${friendly_name} + includes: + - sensor_constants.h + - host_pin_definitions.h + - Metriful_sensor.h + - MS430_ESPHome.h + - Metriful_sensor.cpp + libraries: + - Wire + +# MS430 configuration +<<: !include MS430_ESPHome.yaml diff --git a/Arduino/Metriful_Sensor/MS430_ESPHome.h b/Arduino/Metriful_Sensor/MS430_ESPHome.h new file mode 100644 index 0000000..5c5d934 --- /dev/null +++ b/Arduino/Metriful_Sensor/MS430_ESPHome.h @@ -0,0 +1,142 @@ +/* + MS430_ESPHome.h + + This file creates an interface so that the MS430 Arduino code can + be used as a custom sensor within ESPHome. + + Suitable for ESP8266, ESP32 and Raspberry Pi Pico W. + + Copyright 2023 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 "esphome.h" +#include +#include "Metriful_sensor.h" +#include "host_pin_definitions.h" +#include "sensor_constants.h" + +// Choose time interval for reading data (every 3, 100, or 300 seconds) +// 100 or 300 seconds are recommended to avoid self-heating. +#define CYCLE_PERIOD CYCLE_PERIOD_100_S + +////////////////////////////////////////////////////////////// + +extern bool enableSerial; + +class MS430 : public Component +{ + public: + Sensor * temperature_s = new Sensor(); + Sensor * pressure_s = new Sensor(); + Sensor * humidity_s = new Sensor(); + Sensor * particle_duty_s = new Sensor(); + Sensor * particle_conc_s = new Sensor(); + Sensor * gas_s = new Sensor(); + Sensor * aqi_s = new Sensor(); + Sensor * CO2e_s = new Sensor(); + Sensor * bVOC_s = new Sensor(); + Sensor * aqi_acc_s = new Sensor(); + Sensor * illuminance_s = new Sensor(); + Sensor * w_light_s = new Sensor(); + Sensor * sound_spl_s = new Sensor(); + Sensor * sound_peak_s = new Sensor(); + Sensor * sound_bands_s[SOUND_FREQ_BANDS] = {0}; + AirData_F_t airDataF = {0}; + AirQualityData_F_t airQualityDataF = {0}; + LightData_F_t lightDataF = {0}; + SoundData_F_t soundDataF = {0}; + ParticleData_F_t particleDataF = {0}; + bool firstOutput = true; + bool firstAQIoutput = true; + bool AQIinitialized = false; + + MS430() + { + for (uint8_t i = 0; i < SOUND_FREQ_BANDS; i++) + { + sound_bands_s[i] = new Sensor(); + } + } + + float get_setup_priority() const override + { + return esphome::setup_priority::BUS; + } + + // Initialize the I2C bus and the MS430 board + void setup() override + { + enableSerial = false; + SensorHardwareSetup(I2C_ADDRESS); + uint8_t particleSensor = PARTICLE_SENSOR; + TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); + uint8_t cyclePeriod = CYCLE_PERIOD; + TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cyclePeriod, 1); + TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); + } + + void loop() override + { + if (ready_assertion_event) + { + output(); + ready_assertion_event = false; + } + } + + // Read data and send to Home Assistant + void output() + { + airDataF = getAirDataF(I2C_ADDRESS); + airQualityDataF = getAirQualityDataF(I2C_ADDRESS); + lightDataF = getLightDataF(I2C_ADDRESS); + soundDataF = getSoundDataF(I2C_ADDRESS); + if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) + { + particleDataF = getParticleDataF(I2C_ADDRESS); + particle_duty_s->publish_state(particleDataF.duty_cycle_pc); + particle_conc_s->publish_state(particleDataF.concentration); + } + temperature_s->publish_state(airDataF.T_C); + pressure_s->publish_state(airDataF.P_Pa); + humidity_s->publish_state(airDataF.H_pc); + gas_s->publish_state(airDataF.G_Ohm); + + // Only publish air quality values when the algorithm has + // initialized, and send initial dummy values to force update. + if (firstOutput) + { + aqi_acc_s->publish_state(-1.0); + firstOutput = false; + } + aqi_acc_s->publish_state(airQualityDataF.AQI_accuracy); + if (airQualityDataF.AQI_accuracy > 0) + { + AQIinitialized = true; + } + if (AQIinitialized) + { + if (firstAQIoutput) + { + aqi_s->publish_state(-1.0); + firstAQIoutput = false; + } + aqi_s->publish_state(airQualityDataF.AQI); + CO2e_s->publish_state(airQualityDataF.CO2e); + bVOC_s->publish_state(airQualityDataF.bVOC); + } + // + illuminance_s->publish_state(lightDataF.illum_lux); + w_light_s->publish_state(lightDataF.white); + // + sound_spl_s->publish_state(soundDataF.SPL_dBA); + sound_peak_s->publish_state(soundDataF.peakAmp_mPa); + for (uint8_t i = 0; i < SOUND_FREQ_BANDS; i++) { + sound_bands_s[i]->publish_state(soundDataF.SPL_bands_dB[i]); + } + } +}; diff --git a/Arduino/Metriful_Sensor/MS430_ESPHome.yaml b/Arduino/Metriful_Sensor/MS430_ESPHome.yaml new file mode 100644 index 0000000..759239d --- /dev/null +++ b/Arduino/Metriful_Sensor/MS430_ESPHome.yaml @@ -0,0 +1,237 @@ +# ESPHome configuration for the MS430. + +# Copyright 2023 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 + +# This file must be included in another yaml configuration file, in which +# "device_name" and "temperature_offset" are defined as substitutions. + +######################################################################### + +sensor: +- platform: custom + lambda: |- + auto my_ms430 = new MS430(); + App.register_component(my_ms430); + return { + my_ms430->temperature_s, + my_ms430->pressure_s, + my_ms430->humidity_s, + my_ms430->gas_s, + my_ms430->particle_duty_s, + my_ms430->particle_conc_s, + my_ms430->aqi_s, + my_ms430->CO2e_s, + my_ms430->bVOC_s, + my_ms430->aqi_acc_s, + my_ms430->illuminance_s, + my_ms430->w_light_s, + my_ms430->sound_spl_s, + my_ms430->sound_peak_s, + my_ms430->sound_bands_s[0], + my_ms430->sound_bands_s[1], + my_ms430->sound_bands_s[2], + my_ms430->sound_bands_s[3], + my_ms430->sound_bands_s[4], + my_ms430->sound_bands_s[5] + }; + sensors: + - name: "Temperature" + unit_of_measurement: °C + accuracy_decimals: 1 + icon: "mdi:thermometer" + device_class: "temperature" + state_class: "measurement" + filters: + - offset: ${temperature_offset} + - name: "Air pressure" + unit_of_measurement: Pa + accuracy_decimals: 0 + icon: "mdi:weather-partly-rainy" + device_class: "atmospheric_pressure" + state_class: "measurement" + - name: "Humidity" + unit_of_measurement: "%" + accuracy_decimals: 1 + icon: "mdi:cloud-percent" + device_class: "humidity" + state_class: "measurement" + - name: "Gas sensor resistance" + unit_of_measurement: "Ω" + accuracy_decimals: 0 + icon: "mdi:scent" + device_class: "aqi" + state_class: "measurement" + - name: "Particle sensor duty cycle" + unit_of_measurement: "%" + accuracy_decimals: 2 + icon: "mdi:square-wave" + device_class: "pm25" + state_class: "measurement" + - name: "Particle concentration" + unit_of_measurement: "μg/m³" + accuracy_decimals: 2 + icon: "mdi:chart-bubble" + device_class: "pm25" + state_class: "measurement" + - name: "Air quality index" + unit_of_measurement: "" + accuracy_decimals: 1 + icon: "mdi:flower-tulip-outline" + device_class: "aqi" + state_class: "measurement" + on_value_range: + - above: -0.5 + below: 50 + then: + - text_sensor.template.publish: + id: aqi_text + state: "Good" + - above: 50 + below: 100 + then: + - text_sensor.template.publish: + id: aqi_text + state: "Acceptable" + - above: 100 + below: 150 + then: + - text_sensor.template.publish: + id: aqi_text + state: "Substandard" + - above: 150 + below: 200 + then: + - text_sensor.template.publish: + id: aqi_text + state: "Poor" + - above: 200 + below: 300 + then: + - text_sensor.template.publish: + id: aqi_text + state: "Bad" + - above: 300 + then: + - text_sensor.template.publish: + id: aqi_text + state: "Very bad" + - name: "Estimated CO₂" + unit_of_measurement: "ppm" + accuracy_decimals: 1 + icon: "mdi:molecule-co2" + device_class: "carbon_dioxide" + state_class: "measurement" + - name: "Equivalent breath VOC" + unit_of_measurement: "ppm" + accuracy_decimals: 2 + icon: "mdi:account-voice" + device_class: "volatile_organic_compounds_parts" + state_class: "measurement" + - name: "Air quality accuracy" + unit_of_measurement: "" + accuracy_decimals: 0 + device_class: "" + state_class: "measurement" + internal: true + on_value_range: + - above: -0.5 + below: 0.5 + then: + - text_sensor.template.publish: + id: aqi_acc_text + state: "Not yet valid" + - above: 0.5 + below: 1.5 + then: + - text_sensor.template.publish: + id: aqi_acc_text + state: "Low" + - above: 1.5 + below: 2.5 + then: + - text_sensor.template.publish: + id: aqi_acc_text + state: "Medium" + - above: 2.5 + then: + - text_sensor.template.publish: + id: aqi_acc_text + state: "High" + - name: "Illuminance" + unit_of_measurement: "lux" + accuracy_decimals: 2 + icon: "mdi:white-balance-sunny" + device_class: "illuminance" + state_class: "measurement" + - name: "White light level" + unit_of_measurement: "" + accuracy_decimals: 0 + icon: "mdi:circle-outline" + device_class: "illuminance" + state_class: "measurement" + - name: "Sound pressure level" + unit_of_measurement: "dBA" + accuracy_decimals: 1 + icon: "mdi:microphone" + device_class: "sound_pressure" + state_class: "measurement" + - name: "Peak sound amplitude" + unit_of_measurement: "mPa" + accuracy_decimals: 2 + icon: "mdi:waveform" + device_class: "sound_pressure" + state_class: "measurement" + - name: "SPL at 125 Hz" + unit_of_measurement: "dB" + accuracy_decimals: 1 + icon: "mdi:sine-wave" + device_class: "sound_pressure" + state_class: "measurement" + - name: "SPL at 250 Hz" + unit_of_measurement: "dB" + accuracy_decimals: 1 + icon: "mdi:sine-wave" + device_class: "sound_pressure" + state_class: "measurement" + - name: "SPL at 500 Hz" + unit_of_measurement: "dB" + accuracy_decimals: 1 + icon: "mdi:sine-wave" + device_class: "sound_pressure" + state_class: "measurement" + - name: "SPL at 1000 Hz" + unit_of_measurement: "dB" + accuracy_decimals: 1 + icon: "mdi:sine-wave" + device_class: "sound_pressure" + state_class: "measurement" + - name: "SPL at 2000 Hz" + unit_of_measurement: "dB" + accuracy_decimals: 1 + icon: "mdi:sine-wave" + device_class: "sound_pressure" + state_class: "measurement" + - name: "SPL at 4000 Hz" + unit_of_measurement: "dB" + accuracy_decimals: 1 + icon: "mdi:sine-wave" + device_class: "sound_pressure" + state_class: "measurement" + +# Must add device_name into sensor names here, else difficult to +# identify them if multiple MS430s are present in one HA installation. +# The displayed name can be overridden in HA to hide this name on +# the dashboard cards. +text_sensor: + - platform: template + name: "Air quality accuracy ${device_name}" + icon: "mdi:magnify" + id: aqi_acc_text + - platform: template + name: "Air quality ${device_name}" + icon: "mdi:flower-tulip-outline" + id: aqi_text diff --git a/Arduino/Metriful_Sensor/Metriful_sensor.cpp b/Arduino/Metriful_Sensor/Metriful_sensor.cpp index bbec8fa..5a010f7 100644 --- a/Arduino/Metriful_Sensor/Metriful_sensor.cpp +++ b/Arduino/Metriful_Sensor/Metriful_sensor.cpp @@ -1,33 +1,45 @@ -/* - Metriful_sensor.cpp +/* + Metriful_sensor.cpp - This file defines functions which are used in the code examples. + This file defines functions which are used in the code examples. - Copyright 2020 Metriful Ltd. - Licensed under the MIT License - for further details see LICENSE.txt + Copyright 2020-2023 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 + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor */ #include "Metriful_sensor.h" #include "host_pin_definitions.h" - -// The Arduino Wire library has a limited internal buffer size: -#define ARDUINO_WIRE_BUFFER_LIMIT_BYTES 32 - -void SensorHardwareSetup(uint8_t i2c_7bit_address) { - - pinMode(LED_BUILTIN, OUTPUT); +#include + +char strbuf[100] = {0}; +bool enableSerial = true; + +void SensorHardwareSetup(uint8_t i2c_7bit_address) +{ + // Turn off the LED (there may also be a power LED which you can cover or remove) + #ifdef LED_BUILTIN + pinMode(LED_BUILTIN, OUTPUT); + #ifdef ESP8266 + digitalWrite(LED_BUILTIN, HIGH); + #else + digitalWrite(LED_BUILTIN, LOW); + #endif + #endif #ifdef ESP8266 // Must specify the I2C pins - Wire.begin(SDA_PIN, SCL_PIN); - digitalWrite(LED_BUILTIN, HIGH); + Wire.begin(SDA_PIN, SCL_PIN); + #elif defined ARDUINO_ARCH_RP2040 + // Must specify the I2C pins + Wire.setSDA(SDA_PIN); + Wire.setSCL(SCL_PIN); + Wire.begin(); #else // Default I2C pins are used - Wire.begin(); - digitalWrite(LED_BUILTIN, LOW); + Wire.begin(); #endif Wire.setClock(I2C_CLK_FREQ_HZ); @@ -37,87 +49,117 @@ void SensorHardwareSetup(uint8_t i2c_7bit_address) { pinMode(L_INT_PIN, INPUT); pinMode(S_INT_PIN, INPUT); - // Set up interrupt monitoring of the READY signal, triggering on a falling edge - // event (high-to-low voltage change) indicating READY assertion. The - // function ready_ISR() will be called when this happens. + // Set up interrupt monitoring of the READY signal, triggering on a + // falling edge event (high-to-low voltage change) indicating READY + // assertion. The function ready_ISR() will be called when this happens. attachInterrupt(digitalPinToInterrupt(READY_PIN), ready_ISR, FALLING); - // Start the serial port. - // Full settings are: 8 data bits, no parity, one stop bit - Serial.begin(SERIAL_BAUD_RATE); + if (enableSerial) + { + // Start the serial port. + // Full settings are: 8 data bits, no parity, one stop bit + Serial.begin(SERIAL_BAUD_RATE); + } // Wait for the MS430 to finish power-on initialization: - while (digitalRead(READY_PIN) == HIGH) { + while (digitalRead(READY_PIN) == HIGH) + { yield(); } - + // Reset to clear any previous state: TransmitI2C(i2c_7bit_address, RESET_CMD, 0, 0); delay(5); // Wait for reset completion and entry to standby mode - while (digitalRead(READY_PIN) == HIGH) { + while (digitalRead(READY_PIN) == HIGH) + { yield(); } } volatile bool ready_assertion_event = false; -// This function is automatically called after a falling edge (assertion) of READY. -// The flag variable is set true - it must be set false again in the main program. -void ISR_ATTRIBUTE ready_ISR(void) { +// This function is automatically called after a falling edge +// (assertion) of READY and the flag variable is set true - it +// must be set false again in the main program. +void ISR_ATTRIBUTE ready_ISR(void) +{ ready_assertion_event = true; } +// Format and print a string to the serial port. Equivalent to +// printf() which is not available on Arduino. Limited to sizeof strbuf. +void s_printf(const char * format, ...) +{ + va_list args; + va_start(args, format); + vsnprintf(strbuf, sizeof strbuf, format, args); + va_end(args); + Serial.print(strbuf); +} + //////////////////////////////////////////////////////////////////////// -// Functions to convert data from integer representation to floating-point representation. -// Floats are easy to use for writing programs but require greater memory and processing -// power resources, so may not always be appropriate. +// Functions to convert data from integer representation to floating-point +// representation. Floats are easy to use for writing programs but require +// greater memory and processing power resources, so may not always be +// appropriate. -void convertAirDataF(const AirData_t * airData_in, AirData_F_t * airDataF_out) { +void convertAirDataF(const AirData_t * airData_in, AirData_F_t * airDataF_out) +{ // Decode the signed value for T (in Celsius) - airDataF_out->T_C = convertEncodedTemperatureToFloat(airData_in->T_C_int_with_sign, - airData_in->T_C_fr_1dp); + airDataF_out->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->H_pc = ((float) airData_in->H_pc_int) + + (((float) airData_in->H_pc_fr_1dp) / 10.0f); airDataF_out->G_Ohm = airData_in->G_ohm; } void convertAirQualityDataF(const AirQualityData_t * airQualityData_in, - AirQualityData_F_t * airQualityDataF_out) { + AirQualityData_F_t * airQualityDataF_out) +{ airQualityDataF_out->AQI = ((float) airQualityData_in->AQI_int) + - (((float) airQualityData_in->AQI_fr_1dp)/10.0); + (((float) airQualityData_in->AQI_fr_1dp) / 10.0f); airQualityDataF_out->CO2e = ((float) airQualityData_in->CO2e_int) + - (((float) airQualityData_in->CO2e_fr_1dp)/10.0); + (((float) airQualityData_in->CO2e_fr_1dp) / 10.0f); airQualityDataF_out->bVOC = ((float) airQualityData_in->bVOC_int) + - (((float) airQualityData_in->bVOC_fr_2dp)/100.0); + (((float) airQualityData_in->bVOC_fr_2dp) / 100.0f); airQualityDataF_out->AQI_accuracy = airQualityData_in->AQI_accuracy; } -void convertLightDataF(const LightData_t * lightData_in, LightData_F_t * lightDataF_out) { - lightDataF_out->illum_lux = ((float) lightData_in->illum_lux_int) + - (((float) lightData_in->illum_lux_fr_2dp)/100.0); +void convertLightDataF(const LightData_t * lightData_in, + LightData_F_t * lightDataF_out) +{ + lightDataF_out->illum_lux = ((float) lightData_in->illum_lux_int) + + (((float) lightData_in->illum_lux_fr_2dp) / 100.0f); lightDataF_out->white = lightData_in->white; } -void convertSoundDataF(const SoundData_t * soundData_in, SoundData_F_t * soundDataF_out) { - soundDataF_out->SPL_dBA = ((float) soundData_in->SPL_dBA_int) + - (((float) soundData_in->SPL_dBA_fr_1dp)/10.0); - for (uint16_t i=0; iSPL_bands_dB[i] = ((float) soundData_in->SPL_bands_dB_int[i]) + - (((float) soundData_in->SPL_bands_dB_fr_1dp[i])/10.0); +void convertSoundDataF(const SoundData_t * soundData_in, + SoundData_F_t * soundDataF_out) +{ + soundDataF_out->SPL_dBA = ((float) soundData_in->SPL_dBA_int) + + (((float) soundData_in->SPL_dBA_fr_1dp) / 10.0f); + for (uint16_t i = 0; i < SOUND_FREQ_BANDS; i++) + { + soundDataF_out->SPL_bands_dB[i] = ((float) soundData_in->SPL_bands_dB_int[i]) + + (((float) soundData_in->SPL_bands_dB_fr_1dp[i]) / 10.0f); } - soundDataF_out->peakAmp_mPa = ((float) soundData_in->peak_amp_mPa_int) + - (((float) soundData_in->peak_amp_mPa_fr_2dp)/100.0); + soundDataF_out->peakAmp_mPa = ((float) soundData_in->peak_amp_mPa_int) + + (((float) soundData_in->peak_amp_mPa_fr_2dp) / 100.0f); soundDataF_out->stable = (soundData_in->stable == 1); } -void convertParticleDataF(const ParticleData_t * particleData_in, ParticleData_F_t * particleDataF_out) { - particleDataF_out->duty_cycle_pc = ((float) particleData_in->duty_cycle_pc_int) + - (((float) particleData_in->duty_cycle_pc_fr_2dp)/100.0); - particleDataF_out->concentration = ((float) particleData_in->concentration_int) + - (((float) particleData_in->concentration_fr_2dp)/100.0); +void convertParticleDataF(const ParticleData_t * particleData_in, + ParticleData_F_t * particleDataF_out) +{ + particleDataF_out->duty_cycle_pc = ((float) particleData_in->duty_cycle_pc_int) + + (((float) particleData_in->duty_cycle_pc_fr_2dp) / 100.0f); + particleDataF_out->concentration = ((float) particleData_in->concentration_int) + + (((float) particleData_in->concentration_fr_2dp) / 100.0f); particleDataF_out->valid = (particleData_in->valid == 1); } @@ -126,204 +168,243 @@ 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 -void printAirDataF(const AirData_F_t * airDataF) { +void printAirDataF(const AirData_F_t * airDataF) +{ Serial.print("Temperature = "); #ifdef USE_FAHRENHEIT float temperature_F = convertCtoF(airDataF->T_C); - Serial.print(temperature_F,1);Serial.println(" " FAHRENHEIT_SYMBOL); + Serial.print(temperature_F, 1); + Serial.println(" " FAHRENHEIT_SYMBOL); #else - Serial.print(airDataF->T_C,1);Serial.println(" " CELSIUS_SYMBOL); + Serial.print(airDataF->T_C, 1); + Serial.println(" " CELSIUS_SYMBOL); #endif - Serial.print("Pressure = ");Serial.print(airDataF->P_Pa);Serial.println(" Pa"); - Serial.print("Humidity = ");Serial.print(airDataF->H_pc,1);Serial.println(" %"); - Serial.print("Gas Sensor Resistance = ");Serial.print(airDataF->G_Ohm);Serial.println(" " OHM_SYMBOL); -} - -void printAirQualityDataF(const AirQualityData_F_t * airQualityDataF) { - if (airQualityDataF->AQI_accuracy > 0) { - Serial.print("Air Quality Index = ");Serial.print(airQualityDataF->AQI,1); + Serial.print("Pressure = "); + Serial.print(airDataF->P_Pa); Serial.println(" Pa"); + Serial.print("Humidity = "); + Serial.print(airDataF->H_pc, 1); Serial.println(" %"); + Serial.print("Gas Sensor Resistance = "); + Serial.print(airDataF->G_Ohm); Serial.println(" " OHM_SYMBOL); +} + +void printAirQualityDataF(const AirQualityData_F_t * airQualityDataF) +{ + if (airQualityDataF->AQI_accuracy > 0) + { + Serial.print("Air Quality Index = "); + Serial.print(airQualityDataF->AQI, 1); Serial.print(" ("); Serial.print(interpret_AQI_value((uint16_t) airQualityDataF->AQI)); Serial.println(")"); - Serial.print("Estimated CO" SUBSCRIPT_2 " = ");Serial.print(airQualityDataF->CO2e,1); + Serial.print("Estimated CO" SUBSCRIPT_2 " = "); + Serial.print(airQualityDataF->CO2e, 1); Serial.println(" ppm"); - Serial.print("Equivalent Breath VOC = ");Serial.print(airQualityDataF->bVOC,2); + Serial.print("Equivalent Breath VOC = "); + Serial.print(airQualityDataF->bVOC, 2); Serial.println(" ppm"); } Serial.print("Air Quality Accuracy: "); Serial.println(interpret_AQI_accuracy(airQualityDataF->AQI_accuracy)); } -void printLightDataF(const LightData_F_t * lightDataF) { - Serial.print("Illuminance = ");Serial.print(lightDataF->illum_lux,2);Serial.println(" lux"); - Serial.print("White Light Level = ");Serial.print(lightDataF->white);Serial.println(); +void printLightDataF(const LightData_F_t * lightDataF) +{ + Serial.print("Illuminance = "); + Serial.print(lightDataF->illum_lux, 2); Serial.println(" lux"); + Serial.print("White Light Level = "); + Serial.println(lightDataF->white); } -void printSoundDataF(const SoundData_F_t * soundDataF) { - char strbuf[50] = {0}; +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; iSPL_dBA, 1); Serial.println(" dBA"); + for (uint16_t i = 0; i < SOUND_FREQ_BANDS; i++) + { + s_printf("Frequency Band %u (%u Hz) SPL = ", i + 1, + sound_band_mids_Hz[i]); + Serial.print(soundDataF->SPL_bands_dB[i], 1); Serial.println(" dB"); + } + Serial.print("Peak Sound Amplitude = "); + Serial.print(soundDataF->peakAmp_mPa, 2); Serial.println(" mPa"); +} + +void printParticleDataF(const ParticleData_F_t * particleDataF, + uint8_t particleSensor) +{ + Serial.print("Particle Duty Cycle = "); + Serial.print(particleDataF->duty_cycle_pc, 2); Serial.println(" %"); Serial.print("Particle Concentration = "); - Serial.print(particleDataF->concentration,2); - if (particleSensor == PARTICLE_SENSOR_PPD42) { + Serial.print(particleDataF->concentration, 2); + if (particleSensor == PARTICLE_SENSOR_PPD42) + { Serial.println(" ppL"); } - else if (particleSensor == PARTICLE_SENSOR_SDS011) { + else if (particleSensor == PARTICLE_SENSOR_SDS011) + { Serial.println(" " SDS011_UNIT_SYMBOL); } - else { + else + { Serial.println(" (?)"); } Serial.print("Particle data valid: "); - if (particleDataF->valid) { + if (particleDataF->valid) + { Serial.println("Yes"); } - else { + else + { Serial.println("No (Initializing)"); } } //////////////////////////////////////////////////////////////////////// -// The following five functions print data (in integer representation) over the serial port as text. -// printColumns determines the print format: -// choosing printColumns = false gives labeled values with measurement units -// choosing printColumns = true gives columns of numbers (convenient for spreadsheets). +// The following five functions print data (in integer representation) +// over the serial port as text. "printColumns" determines the print format: +// - printColumns = false gives labeled values with measurement units +// - printColumns = true gives columns of numbers (good for spreadsheets). -void printAirData(const AirData_t * airData, bool printColumns) { - char strbuf[50] = {0}; - +void printAirData(const AirData_t * airData, bool printColumns) +{ uint8_t T_intPart = 0; uint8_t T_fractionalPart = 0; bool isPositive = true; - const char * T_unit = getTemperature(airData, &T_intPart, &T_fractionalPart, &isPositive); - - if (printColumns) { - // Print: temperature, pressure/Pa, humidity/%, gas sensor resistance/ohm - sprintf(strbuf,"%s%u.%u %" PRIu32 " %u.%u %" PRIu32 " ",isPositive?"":"-", T_intPart, T_fractionalPart, - airData->P_Pa, airData->H_pc_int, airData->H_pc_fr_1dp, airData->G_ohm); - Serial.print(strbuf); - } - else { - sprintf(strbuf,"Temperature = %s%u.%u %s", isPositive?"":"-", T_intPart, T_fractionalPart, T_unit); - Serial.println(strbuf); - Serial.print("Pressure = ");Serial.print(airData->P_Pa);Serial.println(" Pa"); - sprintf(strbuf,"Humidity = %u.%u %%",airData->H_pc_int,airData->H_pc_fr_1dp); - Serial.println(strbuf); - Serial.print("Gas Sensor Resistance = ");Serial.print(airData->G_ohm);Serial.println(" " OHM_SYMBOL); - } -} - -void printAirQualityData(const AirQualityData_t * airQualityData, bool printColumns) { - char strbuf[50] = {0}; - if (printColumns) { - // Print: Air Quality Index, Estimated CO2/ppm, Equivalent breath VOC/ppm, Accuracy - sprintf(strbuf,"%u.%u %u.%u %u.%02u %u ",airQualityData->AQI_int, airQualityData->AQI_fr_1dp, - airQualityData->CO2e_int, airQualityData->CO2e_fr_1dp, - airQualityData->bVOC_int, airQualityData->bVOC_fr_2dp, airQualityData->AQI_accuracy); - Serial.print(strbuf); - } - else { - if (airQualityData->AQI_accuracy > 0) { - sprintf(strbuf,"Air Quality Index = %u.%u (%s)", - airQualityData->AQI_int, airQualityData->AQI_fr_1dp, interpret_AQI_value(airQualityData->AQI_int)); - Serial.println(strbuf); - sprintf(strbuf,"Estimated CO" SUBSCRIPT_2 " = %u.%u ppm", - airQualityData->CO2e_int, airQualityData->CO2e_fr_1dp); - Serial.println(strbuf); - sprintf(strbuf,"Equivalent Breath VOC = %u.%02u ppm", - airQualityData->bVOC_int, airQualityData->bVOC_fr_2dp); - Serial.println(strbuf); + const char * T_unit = getTemperature(airData, &T_intPart, + &T_fractionalPart, &isPositive); + if (printColumns) + { + // temperature, pressure/Pa, humidity/%, gas sensor resistance/ohm + s_printf("%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); + } + else + { + s_printf("Temperature = %s%u.%u %s\n", + isPositive ? "" : "-", T_intPart, T_fractionalPart, T_unit); + s_printf("Pressure = %" PRIu32 " Pa\n", airData->P_Pa); + s_printf("Humidity = %u.%u %%\n", + airData->H_pc_int, airData->H_pc_fr_1dp); + s_printf("Gas Sensor Resistance = %" PRIu32 " " OHM_SYMBOL "\n", + airData->G_ohm); + } +} + +void printAirQualityData(const AirQualityData_t * airQualityData, + bool printColumns) +{ + if (printColumns) + { + // Print: Air Quality Index, Estimated CO2/ppm, + // Equivalent breath VOC/ppm, Accuracy + s_printf("%u.%u %u.%u %u.%02u %u ", airQualityData->AQI_int, + airQualityData->AQI_fr_1dp, airQualityData->CO2e_int, + airQualityData->CO2e_fr_1dp, airQualityData->bVOC_int, + airQualityData->bVOC_fr_2dp, airQualityData->AQI_accuracy); + } + else + { + if (airQualityData->AQI_accuracy > 0) + { + s_printf("Air Quality Index = %u.%u (%s)\n", + airQualityData->AQI_int, airQualityData->AQI_fr_1dp, + interpret_AQI_value(airQualityData->AQI_int)); + s_printf("Estimated CO" SUBSCRIPT_2 " = %u.%u ppm\n", + airQualityData->CO2e_int, airQualityData->CO2e_fr_1dp); + s_printf("Equivalent Breath VOC = %u.%02u ppm\n", + airQualityData->bVOC_int, airQualityData->bVOC_fr_2dp); } - Serial.print("Air Quality Accuracy: "); - Serial.println(interpret_AQI_accuracy(airQualityData->AQI_accuracy)); - } -} - -void printSoundData(const SoundData_t * soundData, bool printColumns) { - char strbuf[50] = {0}; - if (printColumns) { - // Print: Sound pressure level/dBA, Sound pressure level for frequency bands 1 to 6 (six columns), - // Peak sound amplitude/mPa, stability - sprintf(strbuf,"%u.%u ", soundData->SPL_dBA_int, soundData->SPL_dBA_fr_1dp); - Serial.print(strbuf); - for (uint16_t i=0; iSPL_bands_dB_int[i], soundData->SPL_bands_dB_fr_1dp[i]); - Serial.print(strbuf); + s_printf("Air Quality Accuracy: %s\n", + interpret_AQI_accuracy(airQualityData->AQI_accuracy)); + } +} + +void printSoundData(const SoundData_t * soundData, bool printColumns) +{ + if (printColumns) + { + // Print: + // Sound pressure level/dBA, + // Sound pressure level for frequency bands 1 to 6 (six columns), + // Peak sound amplitude/mPa, stability + s_printf("%u.%u ", soundData->SPL_dBA_int, soundData->SPL_dBA_fr_1dp); + for (uint16_t i = 0; i < SOUND_FREQ_BANDS; i++) + { + s_printf("%u.%u ", soundData->SPL_bands_dB_int[i], + soundData->SPL_bands_dB_fr_1dp[i]); } - sprintf(strbuf,"%u.%02u %u ", soundData->peak_amp_mPa_int, - soundData->peak_amp_mPa_fr_2dp, soundData->stable); - Serial.print(strbuf); - } - else { - sprintf(strbuf,"A-weighted Sound Pressure Level = %u.%u dBA", - soundData->SPL_dBA_int, soundData->SPL_dBA_fr_1dp); - Serial.println(strbuf); - for (uint8_t i=0; iSPL_bands_dB_int[i], soundData->SPL_bands_dB_fr_1dp[i]); - Serial.println(strbuf); + s_printf("%u.%02u %u ", soundData->peak_amp_mPa_int, + soundData->peak_amp_mPa_fr_2dp, soundData->stable); + } + else + { + s_printf("A-weighted Sound Pressure Level = %u.%u dBA\n", + soundData->SPL_dBA_int, soundData->SPL_dBA_fr_1dp); + for (uint8_t i = 0; i < SOUND_FREQ_BANDS; i++) + { + s_printf("Frequency Band %u (%u Hz) SPL = %u.%u dB\n", i + 1, + sound_band_mids_Hz[i], soundData->SPL_bands_dB_int[i], + soundData->SPL_bands_dB_fr_1dp[i]); } - sprintf(strbuf,"Peak Sound Amplitude = %u.%02u mPa", - soundData->peak_amp_mPa_int, soundData->peak_amp_mPa_fr_2dp); - Serial.println(strbuf); + s_printf("Peak Sound Amplitude = %u.%02u mPa\n", + soundData->peak_amp_mPa_int, soundData->peak_amp_mPa_fr_2dp); } } -void printLightData(const LightData_t * lightData, bool printColumns) { - char strbuf[50] = {0}; - if (printColumns) { +void printLightData(const LightData_t * lightData, bool printColumns) +{ + if (printColumns) + { // Print: illuminance/lux, white level - sprintf(strbuf,"%u.%02u %u ", lightData->illum_lux_int, lightData->illum_lux_fr_2dp, lightData->white); - Serial.print(strbuf); + s_printf("%u.%02u %u ", lightData->illum_lux_int, + lightData->illum_lux_fr_2dp, lightData->white); } - else { - sprintf(strbuf,"Illuminance = %u.%02u lux", lightData->illum_lux_int, lightData->illum_lux_fr_2dp); - Serial.println(strbuf); - Serial.print("White Light Level = ");Serial.print(lightData->white);Serial.println(); + else + { + s_printf("Illuminance = %u.%02u lux\n", + lightData->illum_lux_int, lightData->illum_lux_fr_2dp); + s_printf("White Light Level = %u\n", lightData->white); } } -void printParticleData(const ParticleData_t * particleData, bool printColumns, uint8_t particleSensor) { - char strbuf[50] = {0}; - if (printColumns) { +void printParticleData(const ParticleData_t * particleData, + bool printColumns, uint8_t particleSensor) + { + if (printColumns) + { // Print: duty cycle/%, concentration - sprintf(strbuf,"%u.%02u %u.%02u %u ", particleData->duty_cycle_pc_int, - particleData->duty_cycle_pc_fr_2dp, particleData->concentration_int, - particleData->concentration_fr_2dp, particleData->valid); - Serial.print(strbuf); - } - else { - sprintf(strbuf,"Particle Duty Cycle = %u.%02u %%", - particleData->duty_cycle_pc_int, particleData->duty_cycle_pc_fr_2dp); - Serial.println(strbuf); - sprintf(strbuf,"Particle Concentration = %u.%02u ", - particleData->concentration_int, particleData->concentration_fr_2dp); - Serial.print(strbuf); - if (particleSensor == PARTICLE_SENSOR_PPD42) { - Serial.println("ppL"); - } - else if (particleSensor == PARTICLE_SENSOR_SDS011) { - Serial.println(SDS011_UNIT_SYMBOL); + s_printf("%u.%02u %u.%02u %u ", particleData->duty_cycle_pc_int, + particleData->duty_cycle_pc_fr_2dp, + particleData->concentration_int, + particleData->concentration_fr_2dp, particleData->valid); + } + else + { + s_printf("Particle Duty Cycle = %u.%02u %%\n", + particleData->duty_cycle_pc_int, + particleData->duty_cycle_pc_fr_2dp); + const char * particleUnit = "(?)"; + if (particleSensor == PARTICLE_SENSOR_PPD42) + { + particleUnit = "ppL"; } - else { - Serial.println("(?)"); + else if (particleSensor == PARTICLE_SENSOR_SDS011) + { + particleUnit = SDS011_UNIT_SYMBOL; } + s_printf("Particle Concentration = %u.%02u %s\n", + particleData->concentration_int, + particleData->concentration_fr_2dp, particleUnit); + Serial.print("Particle data valid: "); - if (particleData->valid == 0) { + if (particleData->valid == 0) + { Serial.println("No (Initializing)"); } - else { + else + { Serial.println("Yes"); } } @@ -331,68 +412,80 @@ void printParticleData(const ParticleData_t * particleData, bool printColumns, u //////////////////////////////////////////////////////////////////////// -// Send data to the Metriful MS430 using the I2C-compatible two wire interface. +// Send data to the Metriful MS430 using the I2C ("two wire") interface. // // Returns true on success, false on failure. // // dev_addr_7bit = the 7-bit I2C address of the MS430 board. -// commandRegister = the settings register code or command code to be used. -// data = array containing the data to be sent; its length must be at least "data_length" bytes. -// data_length = the number of bytes from the "data" array to be sent. +// commandRegister = the settings register/command code to be used. +// data = array containing the data to be sent. +// data_length = the number of bytes from the "data" array to be sent. // -bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length) { - - if (data_length > ARDUINO_WIRE_BUFFER_LIMIT_BYTES) { +bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, + const uint8_t * data, uint8_t data_length) +{ + if (data_length > ARDUINO_WIRE_BUFFER_LIMIT_BYTES) + { // The Arduino Wire library has a limited internal buffer size return false; } Wire.beginTransmission(dev_addr_7bit); uint8_t bytesWritten = Wire.write(commandRegister); - if (data_length > 0) { + if (data_length > 0) + { bytesWritten += Wire.write(data, data_length); } - if (bytesWritten != (data_length+1)) { + if (bytesWritten != (data_length + 1)) + { return false; } return (Wire.endTransmission(true) == 0); } -// Read data from the Metriful MS430 using the I2C-compatible two wire interface. +// Read data from the Metriful MS430 using the I2C ("two wire") interface. // // Returns true on success, false on failure. // // dev_addr_7bit = the 7-bit I2C address of the MS430 board. -// commandRegister = the settings register code or data location code to be used. -// data = array to store the received data; its length must be at least "data_length" bytes. +// commandRegister = the settings register/data code to be used. +// data = array to store the received data. // 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) { +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) { + if (data_length > ARDUINO_WIRE_BUFFER_LIMIT_BYTES) + { // The Arduino Wire library has a limited internal buffer size return false; } Wire.beginTransmission(dev_addr_7bit); Wire.write(commandRegister); - if (Wire.endTransmission(false) != 0) { + if (Wire.endTransmission(false) != 0) + { return false; } - if (Wire.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) { + for (uint8_t i = 0; i < data_length; i++) + { + if (Wire.available() > 0) + { data[i] = Wire.read(); } } @@ -400,12 +493,14 @@ bool ReceiveI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], return true; } -//////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////// // Provide a readable interpretation of the accuracy code for // the air quality measurements (applies to all air quality data) -const char * interpret_AQI_accuracy(uint8_t AQI_accuracy_code) { - switch (AQI_accuracy_code) { +const char * interpret_AQI_accuracy(uint8_t AQI_accuracy_code) +{ + switch (AQI_accuracy_code) + { default: case 0: return "Not yet valid, self-calibration incomplete"; @@ -418,8 +513,25 @@ const char * interpret_AQI_accuracy(uint8_t AQI_accuracy_code) { } } +const char * interpret_AQI_accuracy_brief(uint8_t AQI_accuracy_code) +{ + switch (AQI_accuracy_code) + { + default: + case 0: + return "Not yet valid"; + case 1: + return "Low"; + case 2: + return "Medium"; + case 3: + return "High"; + } +} + // Provide a readable interpretation of the AQI (air quality index) -const char * interpret_AQI_value(uint16_t AQI) { +const char * interpret_AQI_value(uint16_t AQI) +{ if (AQI < 50) { return "Good"; } @@ -444,12 +556,15 @@ const char * interpret_AQI_value(uint16_t AQI) { // // Returns true on success, false on failure. // -// threshold_mPa = peak sound amplitude threshold in milliPascals, any 16-bit integer is allowed. -bool setSoundInterruptThreshold(uint8_t dev_addr_7bit, uint16_t threshold_mPa) { +// threshold_mPa = peak sound amplitude threshold in milliPascals +bool setSoundInterruptThreshold(uint8_t dev_addr_7bit, + uint16_t threshold_mPa) +{ uint8_t TXdata[SOUND_INTERRUPT_THRESHOLD_BYTES] = {0}; TXdata[0] = (uint8_t) (threshold_mPa & 0x00FF); TXdata[1] = (uint8_t) (threshold_mPa >> 8); - return TransmitI2C(dev_addr_7bit, SOUND_INTERRUPT_THRESHOLD_REG, TXdata, SOUND_INTERRUPT_THRESHOLD_BYTES); + return TransmitI2C(dev_addr_7bit, SOUND_INTERRUPT_THRESHOLD_REG, + TXdata, SOUND_INTERRUPT_THRESHOLD_BYTES); } // Set the threshold for triggering a light interrupt. @@ -459,13 +574,17 @@ bool setSoundInterruptThreshold(uint8_t dev_addr_7bit, uint16_t threshold_mPa) { // The threshold value in lux units can be fractional and is formed as: // threshold = thres_lux_int + (thres_lux_fr_2dp/100) // -// Threshold values exceeding MAX_LUX_VALUE will be limited to MAX_LUX_VALUE. -bool setLightInterruptThreshold(uint8_t dev_addr_7bit, uint16_t thres_lux_int, uint8_t thres_lux_fr_2dp) { +// Threshold values are limited to MAX_LUX_VALUE. +bool setLightInterruptThreshold(uint8_t dev_addr_7bit, + uint16_t thres_lux_int, + uint8_t thres_lux_fr_2dp) +{ uint8_t TXdata[LIGHT_INTERRUPT_THRESHOLD_BYTES] = {0}; TXdata[0] = (uint8_t) (thres_lux_int & 0x00FF); TXdata[1] = (uint8_t) (thres_lux_int >> 8); TXdata[2] = thres_lux_fr_2dp; - return TransmitI2C(dev_addr_7bit, LIGHT_INTERRUPT_THRESHOLD_REG, TXdata, LIGHT_INTERRUPT_THRESHOLD_BYTES); + return TransmitI2C(dev_addr_7bit, LIGHT_INTERRUPT_THRESHOLD_REG, + TXdata, LIGHT_INTERRUPT_THRESHOLD_BYTES); } //////////////////////////////////////////////////////////////////////// @@ -478,67 +597,82 @@ bool setLightInterruptThreshold(uint8_t dev_addr_7bit, uint16_t thres_lux_int, u // 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 getSoundData(uint8_t i2c_7bit_address) +{ SoundData_t soundData = {0}; - ReceiveI2C(i2c_7bit_address, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); + 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 getAirData(uint8_t i2c_7bit_address) +{ AirData_t airData = {0}; - ReceiveI2C(i2c_7bit_address, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); + 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 getLightData(uint8_t i2c_7bit_address) +{ LightData_t lightData = {0}; - ReceiveI2C(i2c_7bit_address, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); + 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 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); + 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 getParticleData(uint8_t i2c_7bit_address) +{ ParticleData_t particleData = {0}; - ReceiveI2C(i2c_7bit_address, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); + 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 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 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 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 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 getParticleDataF(uint8_t i2c_7bit_address) +{ ParticleData_F_t particleDataF = {0}; ParticleData_t particleData = getParticleData(i2c_7bit_address); convertParticleDataF(&particleData, &particleDataF); @@ -550,46 +684,58 @@ ParticleData_F_t getParticleDataF(uint8_t i2c_7bit_address) { // Functions to convert Celsius temperature to Fahrenheit, in float // and integer formats -float convertCtoF(float C) { - return ((C*1.8) + 32.0); +float convertCtoF(float C) +{ + return ((C * 1.8f) + 32.0f); } // 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) { +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) { + bool isNegative = (F < 0.0f); + if (isNegative) + { F = -F; } - F += 0.05; + F += 0.05f; F_int[0] = (uint8_t) F; F -= (float) F_int[0]; - F_fr_1dp[0] = (uint8_t) (F*10.0); + F_fr_1dp[0] = (uint8_t) (F * 10.0f); isPositive[0] = (!isNegative); } -// Decode and convert the temperature as read from the MS430 (integer +// 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 +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.0f); + if ((T_C_int_with_sign & TEMPERATURE_SIGN_MASK) != 0) + { + // The most-significant bit is set, which indicates 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 +// 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) { + uint8_t * T_fractionalPart, bool * isPositive) +{ #ifdef USE_FAHRENHEIT - float temperature_C = convertEncodedTemperatureToFloat(pAirData->T_C_int_with_sign, - pAirData->T_C_fr_1dp); + 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); + 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; diff --git a/Arduino/Metriful_Sensor/Metriful_sensor.h b/Arduino/Metriful_Sensor/Metriful_sensor.h index dd5c30a..e439523 100644 --- a/Arduino/Metriful_Sensor/Metriful_sensor.h +++ b/Arduino/Metriful_Sensor/Metriful_sensor.h @@ -1,14 +1,14 @@ -/* - Metriful_sensor.h +/* + Metriful_sensor.h - This file declares functions and settings which are used in the code - examples. The function definitions are in file Metriful_sensor.cpp + 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 + Copyright 2020-2023 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 + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor */ #ifndef METRIFUL_SENSOR_H @@ -24,10 +24,8 @@ #include "sensor_constants.h" #include "host_pin_definitions.h" -//////////////////////////////////////////////////////////////////////// - -// Choose to display output temperatures in Fahrenheit: -// un-comment the following line to use Fahrenheit +// Un-comment the following line to display temperatures in Fahrenheit +// else they will be in Celsius //#define USE_FAHRENHEIT // Specify which particle sensor is connected: @@ -43,68 +41,78 @@ // I2C_ADDR_7BIT_SB_CLOSED if the solder bridge SB1 on the board // is soldered closed -//////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////// + +// The Arduino Wire library has a limited internal buffer size: +#define ARDUINO_WIRE_BUFFER_LIMIT_BYTES 32 #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" +#define CELSIUS_SYMBOL "°C" +#define FAHRENHEIT_SYMBOL "°F" +#define SDS011_UNIT_SYMBOL "µg/m³" +#define SUBSCRIPT_2 "₂" +#define OHM_SYMBOL "Ω" extern volatile bool ready_assertion_event; -//////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////// // Data category structs containing floats. If floats are not wanted, // use the integer-only struct versions in sensor_constants.h -typedef struct { +typedef struct +{ float SPL_dBA; float SPL_bands_dB[SOUND_FREQ_BANDS]; float peakAmp_mPa; bool stable; } SoundData_F_t; -typedef struct { +typedef struct +{ float T_C; uint32_t P_Pa; float H_pc; uint32_t G_Ohm; } AirData_F_t; -typedef struct { +typedef struct +{ float AQI; float CO2e; float bVOC; uint8_t AQI_accuracy; } AirQualityData_F_t; -typedef struct { +typedef struct +{ float illum_lux; uint16_t white; } LightData_F_t; -typedef struct { +typedef struct +{ float duty_cycle_pc; float concentration; bool valid; } ParticleData_F_t; -//////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////// // Custom type used to select the particle sensor being used (if any) -typedef enum { +typedef enum +{ OFF = PARTICLE_SENSOR_OFF, PPD42 = PARTICLE_SENSOR_PPD42, SDS011 = PARTICLE_SENSOR_SDS011 } ParticleSensor_t; // Struct used in the IFTTT example -typedef struct { +typedef struct +{ const char * variableName; const char * measurementUnit; int32_t thresHigh; @@ -115,45 +123,58 @@ typedef struct { } ThresholdSetting_t; // Struct used in the Home Assistant example -typedef struct { +typedef struct +{ const char * name; const char * unit; const char * icon; uint8_t decimalPlaces; } HA_Attributes_t; -//////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////// void SensorHardwareSetup(uint8_t i2c_7bit_address); void ISR_ATTRIBUTE ready_ISR(void); -bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length); -bool ReceiveI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, uint8_t data[], uint8_t data_length); +bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, + const uint8_t * data, uint8_t data_length); +bool ReceiveI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, + uint8_t data[], uint8_t data_length); const char * interpret_AQI_accuracy(uint8_t AQI_accuracy_code); +const char * interpret_AQI_accuracy_brief(uint8_t AQI_accuracy_code); const char * interpret_AQI_value(uint16_t AQI); +void s_printf(const char * format, ...); + void convertAirDataF(const AirData_t * airData_in, AirData_F_t * airDataF_out); void convertAirQualityDataF(const AirQualityData_t * airQualityData_in, AirQualityData_F_t * airQualityDataF_out); -void convertLightDataF(const LightData_t * lightData_in, LightData_F_t * lightDataF_out); -void convertSoundDataF(const SoundData_t * soundData_in, SoundData_F_t * soundDataF_out); -void convertParticleDataF(const ParticleData_t * particleData_in, ParticleData_F_t * particleDataF_out); +void convertLightDataF(const LightData_t * lightData_in, + LightData_F_t * lightDataF_out); +void convertSoundDataF(const SoundData_t * soundData_in, + SoundData_F_t * soundDataF_out); +void convertParticleDataF(const ParticleData_t * particleData_in, + ParticleData_F_t * particleDataF_out); void printAirDataF(const AirData_F_t * airDataF); void printAirQualityDataF(const AirQualityData_F_t * airQualityDataF); void printLightDataF(const LightData_F_t * lightDataF); void printSoundDataF(const SoundData_F_t * soundDataF); -void printParticleDataF(const ParticleData_F_t * particleDataF, uint8_t particleSensor); +void 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 printAirQualityData(const AirQualityData_t * airQualityData, + bool printColumns); void printLightData(const LightData_t * lightData, bool printColumns); void printSoundData(const SoundData_t * soundData, bool printColumns); -void printParticleData(const ParticleData_t * particleData, bool printColumns, uint8_t particleSensor); +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); +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); @@ -168,8 +189,10 @@ 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); +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); + uint8_t * T_fractionalPart, bool * isPositive); #endif diff --git a/Arduino/Metriful_Sensor/WiFi_functions.cpp b/Arduino/Metriful_Sensor/WiFi_functions.cpp index 9b887be..e1a7e33 100644 --- a/Arduino/Metriful_Sensor/WiFi_functions.cpp +++ b/Arduino/Metriful_Sensor/WiFi_functions.cpp @@ -1,14 +1,14 @@ -/* - WiFi_functions.cpp +/* + 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 + This file defines functions used by examples connecting to, + or creating, a WiFi network. - For code examples, datasheet and user guide, visit - https://github.com/metriful/sensor + Copyright 2020-2023 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" @@ -18,25 +18,29 @@ // Repeatedly attempt to connect to the WiFi network using the input // network name (SSID) and password. -void connectToWiFi(const char * SSID, const char * password) { +void connectToWiFi(const char * SSID, const char * password) +{ WiFi.disconnect(); - #if defined(ESP8266) || defined(ESP32) + #if defined(ESP8266) || defined(ESP32) || defined(ARDUINO_ARCH_RP2040) WiFi.persistent(false); WiFi.mode(WIFI_STA); #endif uint8_t wStatus = WL_DISCONNECTED; - while (wStatus != WL_CONNECTED) { + 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)) { + while ((wStatus != WL_CONNECTED) && (statusChecks < 8)) + { delay(1000); Serial.print("."); wStatus = WiFi.status(); statusChecks++; } - if (wStatus != WL_CONNECTED) { + if (wStatus != WL_CONNECTED) + { Serial.println("Failed."); WiFi.disconnect(); delay(5000); @@ -45,18 +49,19 @@ void connectToWiFi(const char * SSID, const char * password) { 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) { +// 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) + #if defined(ESP8266) || defined(ESP32) || defined(ARDUINO_ARCH_RP2040) 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); + IPAddress subnet(255, 255, 255, 0); + bool success = WiFi.softAPConfig(hostIP, hostIP, subnet); + success = success && WiFi.softAP(SSID, password); #else WiFi.config(hostIP); bool success = (WiFi.beginAP(SSID, password) == WL_AP_LISTENING); @@ -66,27 +71,103 @@ bool createWiFiAP(const char * SSID, const char * password, IPAddress hostIP) { // 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) { +const char * interpret_WiFi_status(uint8_t statusCode) +{ + switch (statusCode) + { case WL_CONNECTED: return "Connected"; case WL_NO_SHIELD: - return "No shield"; + return "No shield/module"; case WL_IDLE_STATUS: - return "Idle"; + return "Idle (temporary)"; case WL_NO_SSID_AVAIL: return "No SSID available"; case WL_SCAN_COMPLETED: return "Scan completed"; case WL_CONNECT_FAILED: - return "Connect failed"; + return "Connection failed"; case WL_CONNECTION_LOST: return "Connection lost"; case WL_DISCONNECTED: return "Disconnected"; + #if !defined(ESP8266) && !defined(ESP32) + case WL_AP_CONNECTED: + return "AP connected"; + case WL_AP_LISTENING: + return "AP listening"; + #endif default: return "Unknown"; } } +// Get the IP address of the host. +// We need this function because the different board types +// do not have a consistent WiFi API. +IPAddress getIPaddress(bool isAccessPoint) +{ + if (isAccessPoint) + { + #if defined(ESP8266) || defined(ESP32) || defined(ARDUINO_ARCH_RP2040) + return WiFi.softAPIP(); + #else + return WiFi.localIP(); + #endif + } + else + { + return WiFi.localIP(); + } +} + +// Either: connect to a wifi network, or create a new wifi network +// and assign the specified host IP address. +bool wifiCreateOrConnect(bool createWifiNetwork, bool waitForSerial, + const char * SSID, const char * password, + IPAddress hostIP) +{ + if (createWifiNetwork) + { + // The host generates its own WiFi network ("Access Point") with + // a chosen static IP address + if (!createWiFiAP(SSID, password, hostIP)) + { + return false; + } + } + 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 (waitForSerial && (!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); + } + + // 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(getIPaddress(createWifiNetwork)); + return true; +} + + +WiFiClient getClient(WiFiServer * server) +{ + #ifdef ESP8266 + return server->accept(); + #else + return server->available(); + #endif +} + #endif diff --git a/Arduino/Metriful_Sensor/WiFi_functions.h b/Arduino/Metriful_Sensor/WiFi_functions.h index 69d98fe..455a8b2 100644 --- a/Arduino/Metriful_Sensor/WiFi_functions.h +++ b/Arduino/Metriful_Sensor/WiFi_functions.h @@ -1,14 +1,14 @@ -/* - WiFi_functions.h +/* + 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 + This file declares functions used by examples connecting to, + or creating, a WiFi network. - For code examples, datasheet and user guide, visit - https://github.com/metriful/sensor + Copyright 2020-2023 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" @@ -19,8 +19,14 @@ #include void connectToWiFi(const char * SSID, const char * password); -bool createWiFiAP(const char * SSID, const char * password, IPAddress hostIP); +bool createWiFiAP(const char * SSID, const char * password, + IPAddress hostIP); const char * interpret_WiFi_status(uint8_t statusCode); +IPAddress getIPaddress(bool isAccessPoint); +bool wifiCreateOrConnect(bool createWifiNetwork, bool waitForSerial, + const char * SSID, const char * password, + IPAddress hostIP); +WiFiClient getClient(WiFiServer * server); #endif #endif diff --git a/Arduino/Metriful_Sensor/graph_web_page.h b/Arduino/Metriful_Sensor/graph_web_page.h index a976491..4bc400f 100644 --- a/Arduino/Metriful_Sensor/graph_web_page.h +++ b/Arduino/Metriful_Sensor/graph_web_page.h @@ -1,57 +1,480 @@ /* - graph_web_page.h + 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. + This file contains the web page code which is used to display graphs + as part of the graph_web_server example. + This uses HTML, CSS, javascript inside a C character string. - Copyright 2020 Metriful Ltd. - Licensed under the MIT License - for further details see LICENSE.txt + Copyright 2020-2023 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 + 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

" -"
" -"" -"" -"" -""; +#define QUOTE(...) #__VA_ARGS__ + +// This is the HTTP response header for the web page. +const char * pageHeader = "HTTP/1.1 200 OK\r\n" + "Content-type: text/html\r\n" + "Connection: close\r\n\r\n"; + +// HTTP response header for error. +const char * errorResponseHTTP = "HTTP/1.1 400 Bad Request\r\n\r\n"; + +// HTTP response header for data transfer. +const char * dataHeader = "HTTP/1.1 200 OK\r\n" + "Content-type: application/octet-stream\r\n" + "Connection: close\r\n\r\n"; + +// This is the full web page text. +const char * graphWebPage = QUOTE( + + + + + 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 deleted file mode 100644 index 2259f91..0000000 --- a/Arduino/Metriful_Sensor/graph_web_page.html +++ /dev/null @@ -1,333 +0,0 @@ - - - - - -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 eebef88..44bc4c8 100644 --- a/Arduino/Metriful_Sensor/host_pin_definitions.h +++ b/Arduino/Metriful_Sensor/host_pin_definitions.h @@ -1,29 +1,30 @@ -/* - host_pin_definitions.h +/* + host_pin_definitions.h - This file defines which host pins are used to interface to the - Metriful MS430 board. The relevant file section is selected - automatically when the board is chosen in the Arduino IDE. + This file defines which host pins are used to interface to the + Metriful MS430 board. The relevant file section is selected + automatically when the board is chosen in the Arduino IDE. - More detail is provided in the readme and User Guide. + More detail is provided in the readme. - This file provides settings for the following host systems: - * Arduino Uno - * Arduino Nano 33 IoT - * Arduino Nano - * Arduino MKR WiFi 1010 - * ESP8266 (tested on NodeMCU and Wemos D1 Mini - other boards may require changes) - * ESP32 (tested on DOIT ESP32 DEVKIT V1 - other boards may require changes) + This file provides settings for the following host systems: + * Arduino Uno + * Arduino Nano 33 IoT + * Arduino Nano + * Arduino MKR WiFi 1010 + * ESP8266 (tested on NodeMCU and Wemos D1 Mini - other boards may require changes) + * ESP32 (tested on DOIT ESP32 DEVKIT V1 - other boards may require changes) + * Raspberry Pi Pico (including WiFi version) - The Metriful MS430 is compatible with many more development boards - than those listed. You can use this file as a guide to define the - necessary settings for other host systems. + The Metriful MS430 is compatible with many more development boards + than those listed. You can use this file as a guide to define the + necessary settings for other host systems. - Copyright 2020 Metriful Ltd. - Licensed under the MIT License - for further details see LICENSE.txt + Copyright 2020-2023 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 + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor */ #ifndef ARDUINO_PIN_DEFINITIONS_H @@ -57,7 +58,7 @@ #elif defined ARDUINO_SAMD_NANO_33_IOT // Arduino Nano 33 IoT - + #include #include #define HAS_WIFI @@ -151,10 +152,10 @@ #include #define HAS_WIFI - #define ISR_ATTRIBUTE ICACHE_RAM_ATTR + #define ISR_ATTRIBUTE IRAM_ATTR - #define SDA_PIN 5 // GPIO5 (labeled D1) connects to SDA - #define SCL_PIN 4 // GPIO4 (labeled D2) connects to SCL + #define SDA_PIN 4 // GPIO4 (labeled D2) connects to SDA + #define SCL_PIN 5 // GPIO5 (labeled D1) 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 @@ -204,9 +205,42 @@ SDS011 pin "25um" to MS430 pin PRT */ +#elif defined ARDUINO_ARCH_RP2040 + + // The examples have been tested on the official Raspberry Pi Pico and + // Pico W development boards. Other Pico/RP2040 boards may require changes. + + #ifdef ARDUINO_RASPBERRY_PI_PICO_W + #include + #define HAS_WIFI + #endif + + #define ISR_ATTRIBUTE + #define SDA_PIN 20 // GP20 (Pin 26) connects to SDA + #define SCL_PIN 21 // GP21 (Pin 27) connects to SCL + #define READY_PIN 28 // GP28 (Pin 34) connects to RDY + #define L_INT_PIN 26 // GP26 (Pin 31) connects to LIT + #define S_INT_PIN 27 // GP27 (Pin 32) connects to SIT + /* Also make the following connections: + MS430 pin GND to any of the Pico GND pins + Pico pin 36 to MS430 pins VPU and VDD + MS430 pin VIN is unused + + If a PPD42 particle sensor is used, also connect the following: + Pico pin 40 to PPD42 pin 3 + PPD42 pin 1 to any of the Pico GND pins + PPD42 pin 4 to MS430 pin PRT + + If an SDS011 particle sensor is used, connect the following: + Pico pin 40 to SDS011 pin "5V" + SDS011 pin "GND" to any of the Pico GND pins + SDS011 pin "25um" to MS430 pin PRT + */ + #else - #error ("Your development board is not directly supported") - // Please make a new section in this file to define the correct input/output pins + #error ("Your development board is not yet supported.") + // Please make a new section in this file to define + // the correct input/output pins. #endif #endif diff --git a/Arduino/Metriful_Sensor/sensor_constants.h b/Arduino/Metriful_Sensor/sensor_constants.h index c93ecd7..0943e87 100644 --- a/Arduino/Metriful_Sensor/sensor_constants.h +++ b/Arduino/Metriful_Sensor/sensor_constants.h @@ -1,15 +1,15 @@ /* - sensor_constants.h + sensor_constants.h - This file defines constant values and data structures which are used - in the control of the Metriful MS430 board and the interpretation of - its output data. All values have been taken from the MS430 datasheet. + This file defines constant values and data structures which are used + in the control of the Metriful MS430 board and the interpretation of + its output data. All values have been taken from the MS430 datasheet. - Copyright 2020 Metriful Ltd. - Licensed under the MIT License - for further details see LICENSE.txt + Copyright 2020-2023 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 + For code examples, datasheet and user guide, visit + https://github.com/metriful/sensor */ #ifndef SENSOR_CONSTANTS_H @@ -95,8 +95,10 @@ // Frequency bands for sound level measurement #define SOUND_FREQ_BANDS 6 -static const uint16_t sound_band_mids_Hz[SOUND_FREQ_BANDS] = {125, 250, 500, 1000, 2000, 4000}; -static const uint16_t sound_band_edges_Hz[SOUND_FREQ_BANDS+1] = {88, 177, 354, 707, 1414, 2828, 5657}; +static const uint16_t sound_band_mids_Hz[SOUND_FREQ_BANDS] = + {125, 250, 500, 1000, 2000, 4000}; +static const uint16_t sound_band_edges_Hz[SOUND_FREQ_BANDS+1] = + {88, 177, 354, 707, 1414, 2828, 5657}; // Cycle mode time period #define CYCLE_PERIOD_3_S 0 @@ -129,9 +131,11 @@ static const uint16_t sound_band_edges_Hz[SOUND_FREQ_BANDS+1] = {88, 177, 354, 7 /////////////////////////////////////////////////////////// -// Structs for accessing individual data quantities after reading a category of data +// Structs for accessing individual data quantities after +// reading a category of data. -typedef struct __attribute__((packed)) { +typedef struct __attribute__((packed)) +{ uint8_t T_C_int_with_sign; uint8_t T_C_fr_1dp; uint32_t P_Pa; @@ -140,7 +144,8 @@ typedef struct __attribute__((packed)) { uint32_t G_ohm; } AirData_t; -typedef struct __attribute__((packed)) { +typedef struct __attribute__((packed)) +{ uint16_t AQI_int; uint8_t AQI_fr_1dp; uint16_t CO2e_int; @@ -150,13 +155,15 @@ typedef struct __attribute__((packed)) { uint8_t AQI_accuracy; } AirQualityData_t; -typedef struct __attribute__((packed)) { +typedef struct __attribute__((packed)) +{ uint16_t illum_lux_int; uint8_t illum_lux_fr_2dp; uint16_t white; } LightData_t; -typedef struct __attribute__((packed)) { +typedef struct __attribute__((packed)) +{ uint8_t SPL_dBA_int; uint8_t SPL_dBA_fr_1dp; uint8_t SPL_bands_dB_int[SOUND_FREQ_BANDS]; @@ -166,7 +173,8 @@ typedef struct __attribute__((packed)) { uint8_t stable; } SoundData_t; -typedef struct __attribute__((packed)) { +typedef struct __attribute__((packed)) +{ uint8_t duty_cycle_pc_int; uint8_t duty_cycle_pc_fr_2dp; uint16_t concentration_int; diff --git a/Arduino/Metriful_Sensor/text_web_page.h b/Arduino/Metriful_Sensor/text_web_page.h new file mode 100644 index 0000000..a22a6ae --- /dev/null +++ b/Arduino/Metriful_Sensor/text_web_page.h @@ -0,0 +1,85 @@ +/* + text_web_page.h + + This file contains parts of the web page code which is used in the + web_server example. + + Copyright 2020-2023 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 TEXT_WEB_PAGE_H +#define TEXT_WEB_PAGE_H + +#define QUOTE(...) #__VA_ARGS__ + +// This is the HTTP response header. Variable = refresh time in seconds. +const char * responseHeader = "HTTP/1.1 200 OK\r\n" + "Content-type: text/html\r\n" + "Connection: close\r\n" + "Refresh: %u\r\n\r\n"; + +// This is the web page up to the start of the table data. No variables. +const char * pageStart = QUOTE( + + + + + + Metriful Sensor Demo + + + +

Indoor Environment Data

+); + +// Start of a data table. Variable = title. +const char * tableStart = QUOTE( +

+

%s

+ +); + +// A table row. +// Variables = data name, class number, value, unit +const char * tableRow = QUOTE( + + + + + +); + +// End of a data table. No variables. +const char * tableEnd = QUOTE( +
%s%s%s
+

+); + +// End of the web page. No variables. +const char * pageEnd = QUOTE( +

+ sensor.metriful.com +

+ + +); + +#endif diff --git a/CHANGELOG.md b/CHANGELOG.md index 72b9534..39c971d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Change Log All notable changes to the project software and documentation will be documented in this file. +## [3.2.0] - 2023-11-19 +### Added +- Support for Raspberry Pi Pico (W) +- ESPHome support for Home Assistant + +### Changed +- Swap SDA and SCL pin definitions for ESP8266 in host_pin_definitions.h to match the standard defaults. +- Improve web page formatting and introduce jinja2 templating in Python. +- Improve string formatting in Arduino code. + ## [3.1.0] - 2020-11-16 ### Added - Fahrenheit temperature output. diff --git a/DISCLAIMER.txt b/DISCLAIMER.txt index 90c4842..ef0d9bd 100644 --- a/DISCLAIMER.txt +++ b/DISCLAIMER.txt @@ -15,9 +15,6 @@ Metriful products are not designed for use in medical, life-saving, safety, or l No license, express or implied, by estoppel or otherwise, to any intellectual property rights is granted by this document or by any conduct of Metriful. Product names and markings noted herein may be trademarks of their respective owners. -Copyright 2020 Metriful Ltd. - - diff --git a/LICENSE.txt b/LICENSE.txt index 7a7ccb2..868455a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright 2020 Metriful Ltd. +Copyright 2020-2023 Metriful Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Python/GraphViewer.py b/Python/GraphViewer.py index 469cfdd..60973f8 100644 --- a/Python/GraphViewer.py +++ b/Python/GraphViewer.py @@ -1,233 +1,282 @@ -# GraphViewer.py +"""Real-time graphical display of MS430 data. -# This file defines a class for creating a graphical user interface, -# to display graphs with real-time data updates. +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" +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). +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. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor ######################################################### -import time -from pyqtgraph.Qt import QtCore, QtGui +from PyQt5 import QtCore +from PyQt5.QtWidgets import QMainWindow, QWidget, QGridLayout 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) - - +from collections import deque +import Raspberry_Pi.sensor_package.sensor_constants as const + + +class GraphViewer(QMainWindow): + """Real-time graphical display of MS430 data.""" + + def __init__(self, data_buffer_length, + particle_sensor_type, use_fahrenheit): + """Set up a grid layout of data graphs. + + data_buffer_length: number of data points stored/displayed + """ + super().__init__() + + self.buffer_samples = data_buffer_length + + # graphs_vertical, graphs_horizontal: grid size + self.graphs_vertical = 2 + self.graphs_horizontal = 2 + + # Time delay (milliseconds) between graph updates + self.update_time_period_ms = 20 + + # Appearance settings: + self.pen_style = pg.mkPen(color="y", width=2, + style=QtCore.Qt.PenStyle.SolidLine) + self.title_color = "w" + self.title_size = "13pt" + self.axis_color = "w" + self.axis_label_style = {'color': 'w', 'font-size': '11pt'} + self.x_grid = False + self.y_grid = False + self.data_names_units = {'Temperature': '', + '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': '', + 'Particle data valid': ''} + self.sound_band_name = 'Sound frequency bands' + self.setParticleUnits(particle_sensor_type) + self.setTemperatureUnits(use_fahrenheit) + 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 = len(const.sound_band_mids_Hz) + # displayed_combo_index = the combobox index of the variable + # currently displayed on each graph. + # selected_combo_index = the combobox index of the variable + # selected by each combobox menu. + self.displayed_combo_index = [] + self.selected_combo_index = [] + self.time_data = deque(maxlen=self.buffer_samples) + self.setWindowTitle('Waiting for data...') + self.createUI() + + def createUI(self): + """Construct the user interface.""" + self.widget = QWidget() + self.setCentralWidget(self.widget) + self.widget.setLayout(QGridLayout()) + self.plot_items = [] + self.plot_handles = [] + self.combos = [] + self.is_bar_chart = [] + for nv in range(self.graphs_vertical): + for nh in range(self.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) + + def setDataRequired(self, air_quality_data, particle_data, flag_data): + """Indicate which variables from the name list will be available.""" + self.data_name_index = list(range(0, 4)) + if air_quality_data: + self.data_name_index += list(range(4, 8)) + self.band1_index = 11 + else: + self.band1_index = 7 + self.data_name_index += list(range(8, 18)) + if flag_data: + self.data_name_index.append(18) + if particle_data: + self.data_name_index += list(range(19, 21)) + if flag_data: + self.data_name_index.append(21) + self.createDataBuffer() + self.initializeComboBoxes() + + def start(self): + """Begin the periodic updating of the GUI.""" + self.updateLoop() + self.show() + + def setParticleUnits(self, particle_sensor_type): + """Set the particulate unit, depending on hardware (if any).""" + if particle_sensor_type == const.PARTICLE_SENSOR_SDS011: + self.data_names_units['Particle concentration'] = const.SDS011_CONC_SYMBOL + elif particle_sensor_type == const.PARTICLE_SENSOR_PPD42: + self.data_names_units['Particle concentration'] = "ppL" + elif particle_sensor_type != const.PARTICLE_SENSOR_OFF: + raise ValueError("Particle sensor type not recognized") + + def setTemperatureUnits(self, use_fahrenheit): + """Set either C or F for temperature display.""" + if use_fahrenheit: + self.data_names_units['Temperature'] = const.FAHRENHEIT_SYMBOL + else: + self.data_names_units['Temperature'] = const.CELSIUS_SYMBOL + + def formatPlotItem(self, item): + """Adjust plot appearance.""" + 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()}) + + def funcCreator(self, graph_index, combo_handle): + """Create functions to be executed on combobox change action.""" + def func(): + self.selected_combo_index[graph_index] = combo_handle.value() + return func + + def updateLoop(self): + """Check for new data and redraw the graphs if anything changed.""" + need_update = self.displayed_combo_index != self.selected_combo_index + need_update = need_update or self.getDataFunction() + if (need_update): + self.updateGraphs() + QtCore.QTimer.singleShot(self.update_time_period_ms, self.updateLoop) + + def getDataFunction(self): + """Obtain new data (hardware-dependent) and put in data_buffer. + + Returns True if new data were obtained, else returns False. + """ + raise NotImplementedError("Override this method in a derived class") + + def createDataBuffer(self): + """Store data for each graph in a deque.""" + self.data_buffer = [deque(maxlen=self.buffer_samples) + for i in range(len(self.data_name_index))] + + def initializeComboBoxes(self): + """Fill the ComboBoxes and set the initial selected values.""" + names = [list(self.data_names_units.keys())[j] + for j in self.data_name_index] + combo_items = dict(zip(names, [k for k in range(len(names))])) + combo_items[self.sound_band_name] = 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[self.sound_band_name] + self.selected_combo_index.append(start_index) + combo.setValue(start_index) + combo.currentIndexChanged.connect(self.funcCreator(n, combo)) + self.displayed_combo_index = self.selected_combo_index.copy() + + def updateGraphs(self): + """Draw new data on the graphs and update the text label titles.""" + for n in range(len(self.plot_handles)): + self.displayed_combo_index[n] = self.selected_combo_index[n] + bar_chart = (self.displayed_combo_index[n] + >= len(self.data_name_index)) + if bar_chart != self.is_bar_chart[n]: + # Chart type has just changed: initialize new type: + self.changeChartType(n, bar_chart) + if bar_chart: + 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 + ind = self.data_name_index[self.displayed_combo_index[n]] + self.plot_items[n].setTitle( + list(self.data_names_units.keys())[ind] + + " = {:.{dps}f} ".format( + self.data_buffer[self.displayed_combo_index[n]][-1], + dps=self.decimal_places[ind]) + + list(self.data_names_units.values())[ind], + color=self.title_color, size=self.title_size) + self.plot_handles[n].setData( + self.time_data, + self.data_buffer[self.displayed_combo_index[n]]) + + def changeChartType(self, plot_index, is_bar_chart): + """Switch between bar chart (for sound frequencies) and line graph.""" + self.plot_items[plot_index].removeItem(self.plot_handles[plot_index]) + self.plot_handles[plot_index].deleteLater() + self.is_bar_chart[plot_index] = is_bar_chart + if is_bar_chart: + self.plot_handles[plot_index] = pg.BarGraphItem( + x=list(range(self.sound_band_number)), + height=[0]*self.sound_band_number, + width=0.9, brush="r") + self.plot_items[plot_index].addItem(self.plot_handles[plot_index]) + self.formatBarChart(self.plot_items[plot_index]) + else: # Line graph of single variable + self.plot_handles[plot_index] = self.plot_items[plot_index].plot( + pen=self.pen_style, symbol=None) + self.adjustAxes(self.plot_items[plot_index]) + + def adjustAxes(self, item): + """Format the line graph axis settings.""" + 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) + + def formatBarChart(self, item): + """Format the bar chart for the sound frequency bands.""" + 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 = [[], []] + for n, x in enumerate(const.sound_band_mids_Hz): + x_ticks[0].append((n, str(x))) + x_ticks[1].append(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 = [[], []] + for n, y in enumerate(dB_labels): + y_ticks[0].append((y, str(y))) + y_ticks[1].append(y_ticks[0][n]) + item.getAxis("left").setTicks(y_ticks) + 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 index a84b8f4..c330ec4 100644 --- a/Python/Raspberry_Pi/Home_Assistant.py +++ b/Python/Raspberry_Pi/Home_Assistant.py @@ -1,40 +1,42 @@ -# Home_Assistant.py +"""Example of sending data from the Metriful MS430 to Home Assistant. -# 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. +This example is designed to run with Python 3 on a Raspberry Pi and +requires an installation of Home Assistant (www.home-assistant.io) +on your home network. -# 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. +Data are sent at regular intervals over your local network, can be +viewed on the Home Assistant dashboard, and can be used to control +home automation tasks. More setup information is provided in the +Readme and User Guide. +""" -# Copyright 2020 Metriful Ltd. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor import requests -from sensor_package.sensor_functions import * +import time +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const ######################################################### # USER-EDITABLE SETTINGS # How often to read and report the data (every 3, 100 or 300 seconds) -cycle_period = CYCLE_PERIOD_100_S +cycle_period = const.CYCLE_PERIOD_100_S # Home Assistant settings -# You must have already installed Home Assistant on a computer on your +# 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. +# 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" @@ -45,75 +47,85 @@ ######################################################### # Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() +(GPIO, I2C_bus) = sensor.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]) +I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, + const.PARTICLE_SENSOR_SELECT_REG, + [sensor.PARTICLE_SENSOR]) +I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, + const.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) +I2C_bus.write_byte(sensor.i2c_7bit_address, const.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.") - - + # Wait for the next new data release, indicated by a falling edge on READY + while (not GPIO.event_detected(sensor.READY_pin)): + time.sleep(0.05) + + # Now read all data from the MS430 + air_data = sensor.get_air_data(I2C_bus) + air_quality_data = sensor.get_air_quality_data(I2C_bus) + light_data = sensor.get_light_data(I2C_bus) + sound_data = sensor.get_sound_data(I2C_bus) + particle_data = sensor.get_particle_data(I2C_bus, sensor.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=sensor.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 sensor.PARTICLE_SENSOR != const.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 Exception: + 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 index 523f640..bb02fb4 100644 --- a/Python/Raspberry_Pi/IFTTT.py +++ b/Python/Raspberry_Pi/IFTTT.py @@ -1,146 +1,153 @@ -# IFTTT.py +"""Example code for sending data from the Metriful MS430 to IFTTT.com. -# 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. +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. +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. +More setup information is provided in the readme and User Guide. +""" -# Copyright 2020 Metriful Ltd. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor import requests -from sensor_package.sensor_functions import * +import time +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const ######################################################### # 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 +# 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 +# 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; +# Choose the period as a number of readout cycles (each 5 minutes) +# e.g. for a 1 hour period, choose inactive_wait_cycles = 12 +inactive_wait_cycles = 12 # 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.'} +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': ''} + +# Change the following values if Fahrenheit output +# temperature is selected in sensor_functions.py +temperature = {'name': 'temperature', + 'unit': const.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 +# Measure the environment data every 300 seconds (5 minutes). +# This is suitable for long-term monitoring. +cycle_period = const.CYCLE_PERIOD_300_S # IFTTT settings: -IFTTT_url = "http://maker.ifttt.com/trigger/" + IFTTT_EVENT_NAME + "/with/key/" + WEBHOOKS_KEY +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() +(GPIO, I2C_bus) = sensor.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.") - +I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, + const.CYCLE_TIME_PERIOD_REG, [cycle_period]) +I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) + +while True: + + # Wait for the next new data release, indicated by a falling edge on READY + while (not GPIO.event_detected(sensor.READY_pin)): + time.sleep(0.05) + + # Read the air data and air quality data + air_data = sensor.get_air_data(I2C_bus) + air_quality_data = sensor.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_text = 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_text = v['low_advice'] + + if send_alert: + # Send data using an HTTP POST request + try: + warning_txt = f"The {v['name']} is too {threshold_description}" + print("Sending new alert to IFTTT: " + warning_txt) + payload = {"value1": warning_txt, + "value2": ("The measurement was " + "{:.{dps}f} ".format( + v['data'], dps=v['decimal_places']) + + v['unit']), + "value3": advice_text} + 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/Python/Raspberry_Pi/IoT_cloud_logging.py b/Python/Raspberry_Pi/IoT_cloud_logging.py index 553b272..db59613 100644 --- a/Python/Raspberry_Pi/IoT_cloud_logging.py +++ b/Python/Raspberry_Pi/IoT_cloud_logging.py @@ -1,160 +1,176 @@ -# IoT_cloud_logging.py - -# Example IoT data logging code for the Metriful MS430. -# This example is designed to run with Python 3 on a Raspberry Pi. - -# Environmental data values are measured and logged to an internet -# cloud account every 100 seconds. The example gives the choice of -# using either the Tago.io or Thingspeak.com cloud - both of these -# offer a free account for low data rates. - -# Copyright 2020 Metriful Ltd. +"""Example IoT data logging code for the Metriful MS430. + +This example is designed to run with Python 3 on a Raspberry Pi. + +Environmental data values are measured and logged to an internet +cloud account every 100 seconds. The example gives the choice of +using either the Tago.io or Thingspeak.com cloud - both of these +offer a free account for low data rates. +""" + +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor import requests -from sensor_package.sensor_functions import * +import time +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const ######################################################### # USER-EDITABLE SETTINGS # How often to read and log data (every 3, 100, 300 seconds) -# Note: due to data rate limits on free cloud services, this should +# Note: due to data rate limits on free cloud services, this should # be set to 100 or 300 seconds, not 3 seconds. -cycle_period = CYCLE_PERIOD_100_S +cycle_period = const.CYCLE_PERIOD_100_S # IoT cloud settings. -# This example uses the free IoT cloud hosting services provided +# This example uses the free IoT cloud hosting services provided # by Tago.io or Thingspeak.com -# Other free cloud providers are available. -# An account must have been set up with the relevant cloud provider and -# an internet connection to the Pi must exist. See the accompanying +# An account must have been set up with the relevant cloud provider and +# an internet connection to the Pi must exist. See the accompanying # readme and User Guide for more information. # Choose which provider to use 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): - # settings for Tago.io cloud - TAGO_DEVICE_TOKEN_STRING = "PASTE YOUR TOKEN HERE WITHIN QUOTES" +# The chosen account's key/token must be inserted below. +if use_Tago_cloud: + # settings for Tago.io cloud + TAGO_DEVICE_TOKEN_STRING = "PASTE YOUR TOKEN HERE WITHIN QUOTES" else: - # settings for ThingSpeak.com cloud - THINGSPEAK_API_KEY_STRING = "PASTE YOUR API KEY HERE WITHIN QUOTES" + # settings for ThingSpeak.com cloud + THINGSPEAK_API_KEY_STRING = "PASTE YOUR API KEY HERE WITHIN QUOTES" # END OF USER-EDITABLE SETTINGS ######################################################### # Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() +(GPIO, I2C_bus) = sensor.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]) +I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, + const.PARTICLE_SENSOR_SELECT_REG, + [sensor.PARTICLE_SENSOR]) +I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, + const.CYCLE_TIME_PERIOD_REG, [cycle_period]) ######################################################### -# Full cloud settings for HTTP logging -if (use_Tago_cloud): - # settings for Tago.io cloud - tago_url = "http://api.tago.io/data" - tago_header = {"Content-type": "application/json","Device-Token":TAGO_DEVICE_TOKEN_STRING} +# Cloud settings for HTTP logging +if use_Tago_cloud: + # settings for Tago.io cloud + tago_url = "http://api.tago.io/data" + tago_header = {"Content-type": "application/json", + "Device-Token": TAGO_DEVICE_TOKEN_STRING} else: - # settings for ThingSpeak.com cloud - thingspeak_url = "http://api.thingspeak.com/update" - thingspeak_header = {"Content-type": "application/x-www-form-urlencoded"} + # settings for ThingSpeak.com cloud + thingspeak_url = "http://api.thingspeak.com/update" + thingspeak_header = {"Content-type": "application/x-www-form-urlencoded"} print("Logging data. Press ctrl-c to exit.") # Enter cycle mode -I2C_bus.write_byte(i2c_7bit_address, CYCLE_MODE_CMD) +I2C_bus.write_byte(sensor.i2c_7bit_address, const.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 - # 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. - 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 (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. - 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 (measurement unit is selected in sensor_functions.py) - # 2 Pressure/Pa - # 3 Humidity/% - # 4 Air quality index - # 5 bVOC/ppm - # 6 SPL/dBA - # 7 Illuminance/lux - # 8 Particle concentration - - # Additionally, for Tago, the following 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'])} - 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'])} - payload[4] = {"variable":"aqi_string","value":interpret_AQI_value(air_quality_data['AQI'])} - payload[5] = {"variable":"bvoc","value":"{:.2f}".format(air_quality_data['bVOC'])} - payload[6] = {"variable":"spl","value":"{:.1f}".format(sound_data['SPL_dBA'])} - payload[7] = {"variable":"peak_amp","value":"{:.2f}".format(sound_data['peak_amp_mPa'])} - payload[8] = {"variable":"illuminance","value":"{:.2f}".format(light_data['illum_lux'])} - payload[9] = {"variable":"particulates","value":"{:.2f}".format(particle_data['concentration'])} - requests.post(tago_url, json=payload, headers=tago_header, timeout=2) - else: - # Use ThingSpeak.com cloud - payload = "api_key=" + THINGSPEAK_API_KEY_STRING - 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']) - payload += "&field5=" + "{:.2f}".format(air_quality_data['bVOC']) - payload += "&field6=" + "{:.1f}".format(sound_data['SPL_dBA']) - payload += "&field7=" + "{:.2f}".format(light_data['illum_lux']) - payload += "&field8=" + "{:.2f}".format(particle_data['concentration']) - requests.post(thingspeak_url, data=payload, headers=thingspeak_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 retry with the next data release and will succeed - # if the internet reconnects. - print("HTTP POST failed with the following error:") - print(repr(e)) - print("The program will continue and retry on the next data output.") - + # Wait for the next new data release, indicated by a falling edge on READY + while (not GPIO.event_detected(sensor.READY_pin)): + time.sleep(0.05) + + # Now read all data from the MS430 + + # Air data + # Choose output temperature unit (C or F) in sensor_functions.py + air_data = sensor.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. + air_quality_data = sensor.get_air_quality_data(I2C_bus) + + # Light data + light_data = sensor.get_light_data(I2C_bus) + + # Sound data + sound_data = sensor.get_sound_data(I2C_bus) + + # Particle data + # 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. + particle_data = sensor.get_particle_data(I2C_bus, sensor.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 (measurement unit is selected in sensor_functions.py) + # 2 Pressure/Pa + # 3 Humidity/% + # 4 Air quality index + # 5 bVOC/ppm + # 6 SPL/dBA + # 7 Illuminance/lux + # 8 Particle concentration + + # Additionally, for Tago, the following are sent: + # 9 Air Quality Assessment summary (Good, Bad, etc.) + # 10 Peak sound amplitude / mPa + + try: + if use_Tago_cloud: + payload = [] + payload.append({"variable": "temperature", + "value": f"{air_data['T']:.1f}"}) + payload.append({"variable": "pressure", "value": air_data['P_Pa']}) + payload.append({"variable": "humidity", + "value": f"{air_data['H_pc']:.1f}"}) + payload.append({"variable": "aqi", + "value": f"{air_quality_data['AQI']:.1f}"}) + payload.append( + {"variable": "aqi_string", + "value": sensor.interpret_AQI_value(air_quality_data['AQI'])}) + payload.append({"variable": "bvoc", + "value": f"{air_quality_data['bVOC']:.2f}"}) + payload.append({"variable": "spl", + "value": f"{sound_data['SPL_dBA']:.1f}"}) + payload.append({"variable": "peak_amp", + "value": f"{sound_data['peak_amp_mPa']:.2f}"}) + payload.append({"variable": "illuminance", + "value": f"{light_data['illum_lux']:.2f}"}) + payload.append({"variable": "particulates", + "value": f"{particle_data['concentration']:.2f}"}) + requests.post(tago_url, json=payload, + headers=tago_header, timeout=2) + else: + # Use ThingSpeak.com cloud + payload = "api_key=" + THINGSPEAK_API_KEY_STRING + payload += "&field1=" + f"{air_data['T']:.1f}" + payload += "&field2=" + str(air_data['P_Pa']) + payload += "&field3=" + f"{air_data['H_pc']:.1f}" + payload += "&field4=" + f"{air_quality_data['AQI']:.1f}" + payload += "&field5=" + f"{air_quality_data['bVOC']:.2f}" + payload += "&field6=" + f"{sound_data['SPL_dBA']:.1f}" + payload += "&field7=" + f"{light_data['illum_lux']:.2f}" + payload += "&field8=" + f"{particle_data['concentration']:.2f}" + requests.post(thingspeak_url, data=payload, + headers=thingspeak_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 retry with the next data release and will succeed + # if the internet 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/cycle_readout.py b/Python/Raspberry_Pi/cycle_readout.py index 5cc9268..6a77e23 100644 --- a/Python/Raspberry_Pi/cycle_readout.py +++ b/Python/Raspberry_Pi/cycle_readout.py @@ -1,28 +1,28 @@ -# cycle_readout.py +"""Example of using the Metriful MS430 in cycle mode, from a Raspberry Pi. -# Example code for using the Metriful MS430 in cycle mode. -# This example is designed to run with Python 3 on a Raspberry Pi. - -# Continually measures and displays all environmental data in a -# repeating cycle. User can choose from a cycle time period -# of 3, 100, or 300 seconds. +Continually measures and displays all environmental data in a +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. +The measurements can be displayed as either labeled text, or as +simple columns of numbers. +""" -# Copyright 2020 Metriful Ltd. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -from sensor_package.sensor_functions import * +import time +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const ######################################################### # USER-EDITABLE SETTINGS # How often to read data (every 3, 100, or 300 seconds) -cycle_period = CYCLE_PERIOD_3_S +cycle_period = const.CYCLE_PERIOD_3_S # How to print the data: If print_data_as_columns = True, # data are columns of numbers, useful to copy/paste to a spreadsheet @@ -33,58 +33,62 @@ ######################################################### # Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() +(GPIO, I2C_bus) = sensor.SensorHardwareSetup() # Apply the chosen settings -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]) +I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, + const.PARTICLE_SENSOR_SELECT_REG, + [sensor.PARTICLE_SENSOR]) +I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, + const.CYCLE_TIME_PERIOD_REG, [cycle_period]) ######################################################### print("Entering cycle mode and waiting for data. Press ctrl-c to exit.") -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 and print all data - - # Air 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. - air_quality_data = get_air_quality_data(I2C_bus) - writeAirQualityData(None, air_quality_data, print_data_as_columns) - - # Light data - light_data = get_light_data(I2C_bus) - writeLightData(None, light_data, print_data_as_columns) - - # Sound 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 (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 (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: - print("") - else: - print("-------------------------------------------") +I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) + +while True: + + # Wait for the next new data release, indicated by a falling edge on READY + while (not GPIO.event_detected(sensor.READY_pin)): + time.sleep(0.05) + + # Now read and print all data + + # Air data + # Choose output temperature unit (C or F) in sensor_functions.py + air_data = sensor.get_air_data(I2C_bus) + sensor.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. + air_quality_data = sensor.get_air_quality_data(I2C_bus) + sensor.writeAirQualityData(None, air_quality_data, print_data_as_columns) + + # Light data + light_data = sensor.get_light_data(I2C_bus) + sensor.writeLightData(None, light_data, print_data_as_columns) + + # Sound data + sound_data = sensor.get_sound_data(I2C_bus) + sensor.writeSoundData(None, sound_data, print_data_as_columns) + + # Particle data + # 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 (sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF): + particle_data = sensor.get_particle_data(I2C_bus, + sensor.PARTICLE_SENSOR) + sensor.writeParticleData(None, particle_data, print_data_as_columns) + + if print_data_as_columns: + print("") + else: + print("-------------------------------------------") diff --git a/Python/Raspberry_Pi/graph_web_server.py b/Python/Raspberry_Pi/graph_web_server.py index 3357a55..2c28809 100644 --- a/Python/Raspberry_Pi/graph_web_server.py +++ b/Python/Raspberry_Pi/graph_web_server.py @@ -1,60 +1,61 @@ -# graph_web_server.py +"""Example of serving a web page with graphs, from a Raspberry Pi. -# 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. +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 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. +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. +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. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor +import time import socketserver -from sensor_package.servers import * -from sensor_package.sensor_functions import * +import sensor_package.servers as server +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const ######################################################### # 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 +cycle_period = const.CYCLE_PERIOD_3_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 +buffer_length = 200 # 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: +# To find your Raspberry Pi's IP address: # 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" +# 4. The IP address on each network is displayed after "inet" # -# Example - part of an output block showing the address 172.24.1.1 +# 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 @@ -63,74 +64,85 @@ ######################################################### # Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() +(GPIO, I2C_bus) = sensor.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]) +I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) +I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, const.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 +if (cycle_period == const.CYCLE_PERIOD_3_S): + server.GraphWebpageHandler.data_period_seconds = 3 +elif (cycle_period == const.CYCLE_PERIOD_100_S): + server.GraphWebpageHandler.data_period_seconds = 100 +else: # CYCLE_PERIOD_300_S + server.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') +server.GraphWebpageHandler.set_buffer_length(buffer_length) -# Choose the TCP port number for the web page. -port = 8080 +# 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 +# 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) +print("Starting the web server...") +ips = server.get_IP_addresses() +if not ips: + print("Warning: no networks detected.") +else: + print("Your web page will be available at:") + for ip in ips: + print(f" http://{ip}:{port}") + print("For more information on network IP addresses, " + "run the command ifconfig in a terminal.") +print("Press ctrl-c to exit at any time.") + +the_server = socketserver.TCPServer(("", port), server.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)) - +I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) + +while True: + + # Respond to the web page client requests while waiting for new data + while not GPIO.event_detected(sensor.READY_pin): + the_server.handle_request() + time.sleep(0.05) + + # Now read all data from the MS430 and pass to the web page + + # Air data + server.GraphWebpageHandler.update_air_data(sensor.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. + server.GraphWebpageHandler.update_air_quality_data( + sensor.get_air_quality_data(I2C_bus)) + + # Light data + server.GraphWebpageHandler.update_light_data( + sensor.get_light_data(I2C_bus)) + + # Sound data + server.GraphWebpageHandler.update_sound_data( + sensor.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 (sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF): + server.GraphWebpageHandler.update_particle_data( + sensor.get_particle_data(I2C_bus, sensor.PARTICLE_SENSOR)) diff --git a/Python/Raspberry_Pi/interrupts.py b/Python/Raspberry_Pi/interrupts.py index c671d8a..b2b5544 100644 --- a/Python/Raspberry_Pi/interrupts.py +++ b/Python/Raspberry_Pi/interrupts.py @@ -1,111 +1,123 @@ -# interrupts.py +"""Example of using the Metriful MS430 interrupt outputs, from a Raspberry Pi. -# Example code for using the Metriful MS430 interrupt outputs. -# This example is designed to run with Python 3 on a Raspberry Pi. - -# Light and sound interrupts are configured and the program then -# waits indefinitely. When an interrupt occurs, a message is -# displayed, the interrupt is cleared (if set to latch type), -# and the program returns to waiting. +Light and sound interrupts are configured and the program then +waits indefinitely. When an interrupt occurs, a message is +displayed, the interrupt is cleared (if set to latch type), +and the program returns to waiting. +""" -# Copyright 2020 Metriful Ltd. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -from sensor_package.sensor_functions import * +import time +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const ######################################################### # USER-EDITABLE SETTINGS -# Light level interrupt settings. - enable_light_interrupts = True -light_int_type = LIGHT_INT_TYPE_LATCH -# Choose the interrupt polarity: trigger when level rises above -# threshold (positive), or when level falls below threshold (negative). -light_int_polarity = LIGHT_INT_POL_POSITIVE +light_int_type = const.LIGHT_INT_TYPE_LATCH + +# Choose the light interrupt polarity. The interrupt triggers when +# the light level rises above the threshold (positive), or when +# the level falls below the threshold (negative). +light_int_polarity = const.LIGHT_INT_POL_POSITIVE light_thres_lux_i = 100 light_thres_lux_f2dp = 50 -# The interrupt threshold value in lux units can be fractional and is formed as: +# The interrupt threshold in lux units can be fractional and is formed as: # threshold = light_thres_lux_i + (light_thres_lux_f2dp/100) # E.g. for a light threshold of 56.12 lux, set: # light_thres_lux_i = 56 -# light_thres_lux_f2dp = 12 - -# Sound level interrupt settings. +# light_thres_lux_f2dp = 12 enable_sound_interrupts = True -sound_int_type = SOUND_INT_TYPE_LATCH +sound_int_type = const.SOUND_INT_TYPE_LATCH sound_thres_mPa = 100 # END OF USER-EDITABLE SETTINGS ######################################################### -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 of " + str(MAX_LUX_VALUE) + " lux") +if ((light_thres_lux_i + + (float(light_thres_lux_f2dp)/100.0)) > const.MAX_LUX_VALUE): + raise ValueError("The chosen light interrupt threshold exceeds the " + f"maximum allowed value of {const.MAX_LUX_VALUE} lux") # Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() +(GPIO, I2C_bus) = sensor.SensorHardwareSetup() ######################################################### -if (enable_sound_interrupts): - # Set the interrupt type (latch or comparator) - I2C_bus.write_i2c_block_data(i2c_7bit_address, SOUND_INTERRUPT_TYPE_REG, [sound_int_type]) - - # Set the threshold - setSoundInterruptThreshold(I2C_bus, sound_thres_mPa) - - # Tell the Pi to monitor the interrupt line for a falling edge event (high-to-low voltage change) - GPIO.add_event_detect(sound_int_pin, GPIO.FALLING) - - # Enable the interrupt on the MS430 - I2C_bus.write_i2c_block_data(i2c_7bit_address, SOUND_INTERRUPT_ENABLE_REG, [ENABLED]) - - -if (enable_light_interrupts): - # Set the interrupt type (latch or comparator) - I2C_bus.write_i2c_block_data(i2c_7bit_address, LIGHT_INTERRUPT_TYPE_REG, [light_int_type]) - - # Set the threshold - setLightInterruptThreshold(I2C_bus, light_thres_lux_i, light_thres_lux_f2dp) - - # Set the interrupt polarity - I2C_bus.write_i2c_block_data(i2c_7bit_address, LIGHT_INTERRUPT_POLARITY_REG, [light_int_polarity]) - - # Tell the Pi to monitor the interrupt line for a falling edge event (high-to-low voltage change) - GPIO.add_event_detect(light_int_pin, GPIO.FALLING) - - # Enable the interrupt on the MS430 - I2C_bus.write_i2c_block_data(i2c_7bit_address, LIGHT_INTERRUPT_ENABLE_REG, [ENABLED]) +if enable_sound_interrupts: + # Set the interrupt type (latch or comparator) + I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.SOUND_INTERRUPT_TYPE_REG, [sound_int_type]) + # Set the threshold + sensor.setSoundInterruptThreshold(I2C_bus, sound_thres_mPa) -if (not enable_light_interrupts) and (not enable_sound_interrupts): - print("No interrupts have been enabled. Press ctrl-c to exit.") -else: - print("Waiting for interrupts. Press ctrl-c to exit.") - print("") + # Tell the Pi to monitor the interrupt line for a falling + # edge event (high-to-low voltage change) + GPIO.add_event_detect(sensor.sound_int_pin, GPIO.FALLING) + + # Enable the interrupt on the MS430 + I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.SOUND_INTERRUPT_ENABLE_REG, [const.ENABLED]) -while (True): +if enable_light_interrupts: + # Set the interrupt type (latch or comparator) + I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.LIGHT_INTERRUPT_TYPE_REG, [light_int_type]) - # Check whether a light interrupt has occurred - if (GPIO.event_detected(light_int_pin) and enable_light_interrupts): - print("LIGHT INTERRUPT.") - if (light_int_type == LIGHT_INT_TYPE_LATCH): - # Latch type interrupts remain set until cleared by command - I2C_bus.write_byte(i2c_7bit_address, LIGHT_INTERRUPT_CLR_CMD) + # Set the threshold + sensor.setLightInterruptThreshold( + I2C_bus, light_thres_lux_i, light_thres_lux_f2dp) - # Check whether a sound interrupt has occurred - if (GPIO.event_detected(sound_int_pin) and enable_sound_interrupts): - print("SOUND INTERRUPT.") - if (sound_int_type == SOUND_INT_TYPE_LATCH): - # Latch type interrupts remain set until cleared by command - I2C_bus.write_byte(i2c_7bit_address, SOUND_INTERRUPT_CLR_CMD) - - sleep(0.5) + # Set the interrupt polarity + I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.LIGHT_INTERRUPT_POLARITY_REG, [light_int_polarity]) + # Tell the Pi to monitor the interrupt line for a falling + # edge event (high-to-low voltage change) + GPIO.add_event_detect(sensor.light_int_pin, GPIO.FALLING) + # Enable the interrupt on the MS430 + I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.LIGHT_INTERRUPT_ENABLE_REG, [const.ENABLED]) + + +if (not enable_light_interrupts) and (not enable_sound_interrupts): + print("No interrupts have been enabled. Press ctrl-c to exit.") +else: + print("Waiting for interrupts. Press ctrl-c to exit.") + print("") + + +while True: + + # Check whether a light interrupt has occurred + if GPIO.event_detected(sensor.light_int_pin) and enable_light_interrupts: + print("LIGHT INTERRUPT.") + if (light_int_type == const.LIGHT_INT_TYPE_LATCH): + # Latch type interrupts remain set until cleared by command + I2C_bus.write_byte(sensor.i2c_7bit_address, + const.LIGHT_INTERRUPT_CLR_CMD) + + # Check whether a sound interrupt has occurred + if GPIO.event_detected(sensor.sound_int_pin) and enable_sound_interrupts: + print("SOUND INTERRUPT.") + if (sound_int_type == const.SOUND_INT_TYPE_LATCH): + # Latch type interrupts remain set until cleared by command + I2C_bus.write_byte(sensor.i2c_7bit_address, + const.SOUND_INTERRUPT_CLR_CMD) + + time.sleep(0.5) diff --git a/Python/Raspberry_Pi/log_data_to_file.py b/Python/Raspberry_Pi/log_data_to_file.py index 65ab8bd..e6d9739 100644 --- a/Python/Raspberry_Pi/log_data_to_file.py +++ b/Python/Raspberry_Pi/log_data_to_file.py @@ -1,22 +1,22 @@ -# log_data_to_file.py - -# Example file data logging code for the Metriful MS430. -# This example is designed to run with Python 3 on a Raspberry Pi. - -# All environment data values are measured and saved as columns -# of numbers in a text file (one row of data every three seconds). -# This type of file can be imported into various graph and spreadsheet -# applications. To prevent very large file sizes, a new file is -# started every time it reaches a preset size limit. - -# Copyright 2020 Metriful Ltd. +"""Example of logging data from the Metriful MS430, using a Raspberry Pi. + +All environment data values are measured and saved as columns +of numbers in a text file (one row of data every three seconds). +This type of file can be imported into various graph and spreadsheet +applications. To prevent very large file sizes, a new file is +started every time it reaches a preset size limit. +""" + +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -import datetime -from sensor_package.sensor_functions import * +import time +from datetime import datetime +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const ######################################################### # USER-EDITABLE SETTINGS @@ -32,95 +32,95 @@ data_file_directory = "/home/pi/Desktop" # How often to measure and read data (every 3, 100, or 300 seconds): -cycle_period = CYCLE_PERIOD_3_S +cycle_period = const.CYCLE_PERIOD_3_S # END OF USER-EDITABLE SETTINGS ######################################################### # Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() +(GPIO, I2C_bus) = sensor.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]) +I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) +I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, const.CYCLE_TIME_PERIOD_REG, [cycle_period]) ######################################################### if log_to_file: - datafile = startNewDataFile(data_file_directory) - data_file_lines = 0 + datafile = sensor.startNewDataFile(data_file_directory) + data_file_lines = 0 print("Entering cycle mode and waiting for data. 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) - - # 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. - 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 (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. - 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) - writeAirQualityData(None, air_quality_data, False) - writeLightData(None, light_data, False) - writeSoundData(None, sound_data, False) - if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF): - writeParticleData(None, particle_data, False) - - if (log_to_file): - # Write the data as simple columns in a text file (without labels or - # measurement units). - # Start by writing date and time in columns 1-6 - datafile.write(datetime.datetime.now().strftime('%Y %m %d %H %M %S ')) - # Air data in columns 7-10 - writeAirData(datafile, air_data, True) - # Air quality data in columns 11-14 - writeAirQualityData(datafile, air_quality_data, True) - # Light data in columns 15 - 16 - writeLightData(datafile, light_data, True) - # Sound data in columns 17 - 25 - writeSoundData(datafile, sound_data, True) - if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF): - # Particle data in columns 26 - 28 - writeParticleData(datafile, particle_data, True) - datafile.write("\n") - datafile.flush() - data_file_lines+=1 - if (data_file_lines >= lines_per_file): - # Start a new log file to prevent very large files - datafile.close() - datafile = startNewDataFile(data_file_directory) - data_file_lines = 0 - - - +I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) + +while True: + + # Wait for the next new data release, indicated by a falling edge on READY + while (not GPIO.event_detected(sensor.READY_pin)): + time.sleep(0.05) + + # Air data + # Choose output temperature unit (C or F) in sensor_functions.py + air_data = sensor.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. + air_quality_data = sensor.get_air_quality_data(I2C_bus) + + # Light data + light_data = sensor.get_light_data(I2C_bus) + + # Sound data + sound_data = sensor.get_sound_data(I2C_bus) + + # Particle data + # 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. + particle_data = sensor.get_particle_data(I2C_bus, sensor.PARTICLE_SENSOR) + + if print_to_screen: + # Display all data on screen as named quantities with units + print("") + print("------------------") + sensor.writeAirData(None, air_data, False) + sensor.writeAirQualityData(None, air_quality_data, False) + sensor.writeLightData(None, light_data, False) + sensor.writeSoundData(None, sound_data, False) + if (sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF): + sensor.writeParticleData(None, particle_data, False) + + if log_to_file: + # Write the data as simple columns in a text file (without labels or + # measurement units). + # Start by writing date and time in columns 1-6 + datafile.write(datetime.now().strftime('%Y %m %d %H %M %S ')) + # Air data in columns 7-10 + sensor.writeAirData(datafile, air_data, True) + # Air quality data in columns 11-14 + sensor.writeAirQualityData(datafile, air_quality_data, True) + # Light data in columns 15 - 16 + sensor.writeLightData(datafile, light_data, True) + # Sound data in columns 17 - 25 + sensor.writeSoundData(datafile, sound_data, True) + if (sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF): + # Particle data in columns 26 - 28 + sensor.writeParticleData(datafile, particle_data, True) + datafile.write("\n") + datafile.flush() + data_file_lines += 1 + if data_file_lines >= lines_per_file: + # Start a new log file to prevent very large files + datafile.close() + datafile = sensor.startNewDataFile(data_file_directory) + data_file_lines = 0 diff --git a/Python/Raspberry_Pi/on_demand_readout.py b/Python/Raspberry_Pi/on_demand_readout.py index a4ac230..2e64c44 100644 --- a/Python/Raspberry_Pi/on_demand_readout.py +++ b/Python/Raspberry_Pi/on_demand_readout.py @@ -1,22 +1,22 @@ -# on_demand_readout.py +"""Example using the Metriful MS430 in "on-demand" mode, from a Raspberry Pi. -# Example code for using the Metriful MS430 in "on-demand" mode. -# This example is designed to run with Python 3 on a Raspberry Pi. - -# Repeatedly measures and displays all environment data, with a pause -# of any chosen duration between measurements. Air quality data are -# unavailable in this mode (instead use cycle_readout.py). +Repeatedly measures and displays all environment data, with a pause +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. +The measurements can be displayed as either labeled text, or as +simple columns of numbers. +""" -# Copyright 2020 Metriful Ltd. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -from sensor_package.sensor_functions import * +import time +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const ######################################################### # USER-EDITABLE SETTINGS @@ -24,7 +24,7 @@ # Pause (in seconds) between data measurements (note that the # measurement itself takes 0.5 seconds) pause_s = 3.5 -# Choosing a pause of less than 2 seconds will cause inaccurate +# Choosing a pause of less than 2 seconds will cause inaccurate # temperature, humidity and particle data. # How to print the data: If print_data_as_columns = True, @@ -36,53 +36,56 @@ ######################################################### # Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() +(GPIO, I2C_bus) = sensor.SensorHardwareSetup() -I2C_bus.write_i2c_block_data(i2c_7bit_address, PARTICLE_SENSOR_SELECT_REG, [PARTICLE_SENSOR]) +I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) ######################################################### while (True): - sleep(pause_s) - - # 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. - # This will take 0.5 seconds. - while (not GPIO.event_detected(READY_pin)): - sleep(0.05) - - # Now read and print all data - - # Air 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 - light_data = get_light_data(I2C_bus) - writeLightData(None, light_data, print_data_as_columns) - - # Sound 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 (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 (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: - print("") - else: - print("-------------------------------------------") + time.sleep(pause_s) + + # Trigger a new measurement + I2C_bus.write_byte(sensor.i2c_7bit_address, const.ON_DEMAND_MEASURE_CMD) + + # 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(sensor.READY_pin)): + time.sleep(0.05) + + # Now read and print all data + + # Air data + # Choose output temperature unit (C or F) in sensor_functions.py + air_data = sensor.get_air_data(I2C_bus) + sensor.writeAirData(None, air_data, print_data_as_columns) + + # Air quality data are not available with on demand measurements + + # Light data + light_data = sensor.get_light_data(I2C_bus) + sensor.writeLightData(None, light_data, print_data_as_columns) + + # Sound data + sound_data = sensor.get_sound_data(I2C_bus) + sensor.writeSoundData(None, sound_data, print_data_as_columns) + + # Particle data + # 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 (sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF): + particle_data = sensor.get_particle_data(I2C_bus, + sensor.PARTICLE_SENSOR) + sensor.writeParticleData(None, particle_data, print_data_as_columns) + + if print_data_as_columns: + print("") + else: + print("-------------------------------------------") diff --git a/Python/Raspberry_Pi/particle_sensor_toggle.py b/Python/Raspberry_Pi/particle_sensor_toggle.py index dbf4bb9..5d71efc 100644 --- a/Python/Raspberry_Pi/particle_sensor_toggle.py +++ b/Python/Raspberry_Pi/particle_sensor_toggle.py @@ -1,25 +1,28 @@ -# particle_sensor_toggle.py - -# Optional advanced demo. This program shows how to generate an output -# 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. 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. - -# Copyright 2020 Metriful Ltd. +"""An example of how to control power to the external particle sensor. + +Optional advanced demo. This program shows how to generate an output +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. 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. +""" + +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -from sensor_package.sensor_functions import * +import time +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const ######################################################### # USER-EDITABLE SETTINGS @@ -27,22 +30,22 @@ # How often to read data; choose only 100 or 300 seconds for this demo # because the sensor should be on for at least one minute before reading # its data. -cycle_period = CYCLE_PERIOD_100_S +cycle_period = const.CYCLE_PERIOD_100_S # 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 +print_data_as_columns = False # Particle sensor power control options -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 +off_cycles = 2 # leave the sensor off for this many cycles between reads +particle_sensor_control_pin = 10 # Pi pin which outputs the control signal # END OF USER-EDITABLE SETTINGS ######################################################### # Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() +(GPIO, I2C_bus) = sensor.SensorHardwareSetup() # Set up the particle sensor control, and turn it off initially GPIO.setup(particle_sensor_control_pin, GPIO.OUT) @@ -51,80 +54,87 @@ particle_sensor_count = 0 # Apply the chosen settings -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]) +I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) +I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, const.CYCLE_TIME_PERIOD_REG, [cycle_period]) ######################################################### -sound_data = extractSoundData([0]*SOUND_DATA_BYTES) -particle_data = extractParticleData([0]*PARTICLE_DATA_BYTES, PARTICLE_SENSOR) +sound_data = sensor.extractSoundData([0]*const.SOUND_DATA_BYTES) +particle_data = sensor.extractParticleData([0]*const.PARTICLE_DATA_BYTES, + sensor.PARTICLE_SENSOR) print("Entering cycle mode and waiting for data. Press ctrl-c to exit.") -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 and print all data. The previous loop's particle or - # sound data will be printed if no reading is done on this loop. - - # Air 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. - air_quality_data = get_air_quality_data(I2C_bus) - writeAirQualityData(None, air_quality_data, print_data_as_columns) - - # Light data - light_data = get_light_data(I2C_bus) - writeLightData(None, light_data, print_data_as_columns) - - # 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 (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 (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: - print("") - else: - print("-------------------------------------------") - - #Turn the particle sensor on/off if required - 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) - particle_sensor_is_on = False - else: - 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, [PARTICLE_SENSOR]) - - particle_sensor_count = 0 - particle_sensor_is_on = True - - +I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) + +while True: + + # Wait for the next new data release, indicated by a falling edge on READY + while (not GPIO.event_detected(sensor.READY_pin)): + time.sleep(0.05) + + # Now read and print all data. The previous loop's particle or + # sound data will be printed if no reading is done on this loop. + + # Air data + air_data = sensor.get_air_data(I2C_bus) + sensor.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. + air_quality_data = sensor.get_air_quality_data(I2C_bus) + sensor.writeAirQualityData(None, air_quality_data, print_data_as_columns) + + # Light data + light_data = sensor.get_light_data(I2C_bus) + sensor.writeLightData(None, light_data, print_data_as_columns) + + # Sound data - only read new data when particle sensor is off + if (not particle_sensor_is_on): + sound_data = sensor.get_sound_data(I2C_bus) + sensor.writeSoundData(None, sound_data, print_data_as_columns) + + # Particle data + # 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 (particle_sensor_is_on): + particle_data = sensor.get_particle_data(I2C_bus, + sensor.PARTICLE_SENSOR) + sensor.writeParticleData(None, particle_data, print_data_as_columns) + + if print_data_as_columns: + print("") + else: + print("-------------------------------------------") + + # Turn the particle sensor on/off if required + if (particle_sensor_is_on): + # Stop the particle detection on the MS430 + I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.PARTICLE_SENSOR_SELECT_REG, [const.PARTICLE_SENSOR_OFF]) + + # Turn off the hardware: + GPIO.output(particle_sensor_control_pin, 0) + particle_sensor_is_on = False + else: + 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( + sensor.i2c_7bit_address, + const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) + + particle_sensor_count = 0 + particle_sensor_is_on = True diff --git a/Python/Raspberry_Pi/sensor_package/graph_web_page.html b/Python/Raspberry_Pi/sensor_package/graph_web_page.html index 2259f91..2343896 100644 --- a/Python/Raspberry_Pi/sensor_package/graph_web_page.html +++ b/Python/Raspberry_Pi/sensor_package/graph_web_page.html @@ -1,333 +1,378 @@ - + - -Indoor Environment Data - - - + + Indoor Environment Data + + + + -

Indoor Environment Data

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

sensor.metriful.com

-
- - + // Make a "comma separated values" file containing all data and start the download. + // This file can be opened with most spreadsheet software and text editors. + function makeCSVfile() { + let csvData = '\uFEFF'; // UTF-8 byte order mark + csvData += '"Time and Date"'; + for (let i = 0; i < Ngraphs; i++) { + csvData += ',"' + titles[i] + '"'; + } + csvData += '\r\n'; + for (let n = 0; n < xValues.length; n++) { + csvData += '"' + xValues[n] + '"'; + for (let i = 0; i < Ngraphs; i++) { + csvData += ',"' + data[i][n].toFixed(decimalPlaces[i]) + '"'; + } + csvData += '\r\n'; + } + let f = document.getElementById('CSVlink'); + URL.revokeObjectURL(f.href); + f.href = URL.createObjectURL(new Blob([csvData], {type: 'text/csv;charset=utf-8'})); + f.download = 'data.csv'; + f.click(); + } + diff --git a/Python/Raspberry_Pi/sensor_package/sensor_constants.py b/Python/Raspberry_Pi/sensor_package/sensor_constants.py index 5b2493a..5bb4244 100644 --- a/Python/Raspberry_Pi/sensor_package/sensor_constants.py +++ b/Python/Raspberry_Pi/sensor_package/sensor_constants.py @@ -1,27 +1,28 @@ -# sensor_constants.py +"""Constant values for use in Python programs. -# This file defines constant values which are used in the control -# of the Metriful MS430 board and the interpretation of its output data. -# All values have been taken from the MS430 datasheet. +This file defines constant values which are used in the control +of the Metriful MS430 board and the interpretation of its output data. +All values have been taken from the MS430 datasheet. +""" -# Copyright 2020 Metriful Ltd. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor # Settings registers -PARTICLE_SENSOR_SELECT_REG = 0x07 -LIGHT_INTERRUPT_ENABLE_REG = 0x81 -LIGHT_INTERRUPT_THRESHOLD_REG = 0x82 -LIGHT_INTERRUPT_TYPE_REG = 0x83 -LIGHT_INTERRUPT_POLARITY_REG = 0x84 -SOUND_INTERRUPT_ENABLE_REG = 0x85 -SOUND_INTERRUPT_THRESHOLD_REG = 0x86 -SOUND_INTERRUPT_TYPE_REG = 0x87 -CYCLE_TIME_PERIOD_REG = 0x89 - -# Executable commands +PARTICLE_SENSOR_SELECT_REG = 0x07 +LIGHT_INTERRUPT_ENABLE_REG = 0x81 +LIGHT_INTERRUPT_THRESHOLD_REG = 0x82 +LIGHT_INTERRUPT_TYPE_REG = 0x83 +LIGHT_INTERRUPT_POLARITY_REG = 0x84 +SOUND_INTERRUPT_ENABLE_REG = 0x85 +SOUND_INTERRUPT_THRESHOLD_REG = 0x86 +SOUND_INTERRUPT_TYPE_REG = 0x87 +CYCLE_TIME_PERIOD_REG = 0x89 + +# Executable commands ON_DEMAND_MEASURE_CMD = 0xE1 RESET_CMD = 0xE2 CYCLE_MODE_CMD = 0xE4 @@ -30,9 +31,9 @@ SOUND_INTERRUPT_CLR_CMD = 0xE7 # Read the operational mode -OP_MODE_READ = 0x8A +OP_MODE_READ = 0x8A -# Read data for whole categories +# Read data for whole categories AIR_DATA_READ = 0x10 AIR_QUALITY_DATA_READ = 0x11 LIGHT_DATA_READ = 0x12 @@ -62,7 +63,7 @@ # I2C address of sensor board: can select using solder bridge I2C_ADDR_7BIT_SB_OPEN = 0x71 # if solder bridge is left open -I2C_ADDR_7BIT_SB_CLOSED = 0x70 # if solder bridge is soldered closed +I2C_ADDR_7BIT_SB_CLOSED = 0x70 # if solder bridge is soldered closed # Values for enabling/disabling of sensor functions ENABLED = 1 @@ -90,7 +91,7 @@ SOUND_INT_TYPE_COMP = 1 # Maximum for illuminance measurement and threshold setting -MAX_LUX_VALUE = 3774 +MAX_LUX_VALUE = 3774 # Light interrupt type LIGHT_INT_TYPE_LATCH = 0 @@ -101,8 +102,8 @@ LIGHT_INT_POL_NEGATIVE = 1 # Decoding the temperature integer.fraction value format -TEMPERATURE_VALUE_MASK = 0x7F -TEMPERATURE_SIGN_MASK = 0x80 +TEMPERATURE_VALUE_MASK = 0x7F +TEMPERATURE_SIGN_MASK = 0x80 # Particle sensor module selection: PARTICLE_SENSOR_OFF = 0 @@ -139,3 +140,12 @@ CONCENTRATION_BYTES = 3 PARTICLE_VALID_BYTES = 1 PARTICLE_DATA_BYTES = 6 + +############################################################################# + +# Unicode symbol strings +CELSIUS_SYMBOL = "°C" +FAHRENHEIT_SYMBOL = "°F" +SDS011_CONC_SYMBOL = "µg/m³" # micrograms per cubic meter +SUBSCRIPT_2 = "₂" +OHM_SYMBOL = "Ω" diff --git a/Python/Raspberry_Pi/sensor_package/sensor_functions.py b/Python/Raspberry_Pi/sensor_package/sensor_functions.py index ab4ede8..9d72430 100644 --- a/Python/Raspberry_Pi/sensor_package/sensor_functions.py +++ b/Python/Raspberry_Pi/sensor_package/sensor_functions.py @@ -1,46 +1,44 @@ -# sensor_functions.py +"""Functions and settings for use on Raspberry Pi. -# 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. +""" -# Choose the preferred temperature measurement unit (Celsius or -# Fahrenheit) in this file, and select the optional particle sensor. - -# Copyright 2020 Metriful Ltd. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor import sys from time import sleep -import datetime +from datetime import datetime import RPi.GPIO as GPIO import smbus import os -from .sensor_constants import * +from . import sensor_constants as const -########################################################################################## +############################################################################# -# Choose to display output temperatures in Fahrenheit: +# Choose to display output temperatures in Fahrenheit (instead of Celsius): USE_FAHRENHEIT = False # Specify which particle sensor is connected: -PARTICLE_SENSOR = PARTICLE_SENSOR_OFF +PARTICLE_SENSOR = const.PARTICLE_SENSOR_OFF # The possibilities are: # PARTICLE_SENSOR_PPD42 for the Shinyei PPD42 -# PARTICLE_SENSOR_SDS011 for the Nova SDS011 +# PARTICLE_SENSOR_SDS011 for the Nova SDS011 (recommended) # PARTICLE_SENSOR_OFF if no sensor is connected -########################################################################################## +############################################################################# # GPIO (input/output) header pin numbers: these must match the hardware wiring. # 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 +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 -# In addition to these GPIO pins, the following I2C and power -# connections must be made: +# In addition to these GPIO pins, the following I2C and power +# connections must be made: # Raspberry Pi pin 3 to SDA # Raspberry Pi pin 5 to SCL # Raspberry Pi pin 6 to GND (0 V) @@ -60,327 +58,368 @@ # For further details, see the readme and User Guide # The I2C address of the Metriful MS430 board -i2c_7bit_address = I2C_ADDR_7BIT_SB_OPEN +i2c_7bit_address = const.I2C_ADDR_7BIT_SB_OPEN -########################################################################################## +############################################################################# -# 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) + GPIO.setmode(GPIO.BOARD) + GPIO.setup(READY_pin, GPIO.IN) + GPIO.setup(light_int_pin, GPIO.IN) + GPIO.setup(sound_int_pin, GPIO.IN) -########################################################################################## + # Initialize the I2C communications bus object + I2C_bus = smbus.SMBus(1) # Port 1 is the default for I2C on Raspberry Pi + + # Wait for the MS430 to finish power-on initialization: + while (GPIO.input(READY_pin) == 1): + sleep(0.05) + + # Reset MS430 to clear any previous state: + I2C_bus.write_byte(i2c_7bit_address, const.RESET_CMD) + sleep(0.005) + + # Wait for reset completion and entry to standby mode + while (GPIO.input(READY_pin) == 1): + sleep(0.05) + + # Tell the Pi to monitor READY for a falling edge + # event (high-to-low voltage change) + GPIO.add_event_detect(READY_pin, GPIO.FALLING) + + return (GPIO, I2C_bus) + +############################################################################# + +# "extract*Data" are functions to convert the raw data bytes (received over +# I2C) into Python dictionaries containing the environmental data values. -def SensorHardwareSetup(): - # Set up the Raspberry Pi GPIO - GPIO.setwarnings(False) - GPIO.setmode(GPIO.BOARD) - GPIO.setup(READY_pin, GPIO.IN) - GPIO.setup(light_int_pin, GPIO.IN) - GPIO.setup(sound_int_pin, GPIO.IN) - - # Initialize the I2C communications bus object - I2C_bus = smbus.SMBus(1) # Port 1 is the default for I2C on Raspberry Pi - - # Wait for the MS430 to finish power-on initialization: - while (GPIO.input(READY_pin) == 1): - sleep(0.05) - - # Reset MS430 to clear any previous state: - I2C_bus.write_byte(i2c_7bit_address, RESET_CMD) - sleep(0.005) - - # Wait for reset completion and entry to standby mode - while (GPIO.input(READY_pin) == 1): - sleep(0.05) - - # Tell the Pi to monitor READY for a falling edge event (high-to-low voltage change) - GPIO.add_event_detect(READY_pin, GPIO.FALLING) - - return (GPIO, I2C_bus) - -########################################################################################## - -# Functions to convert the raw data bytes (received over I2C) -# into Python dictionaries containing the environmental data values. def extractAirData(rawData): - if (len(rawData) != AIR_DATA_BYTES): - raise Exception('Incorrect number of Air Data bytes') - 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 - 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 + if len(rawData) != const.AIR_DATA_BYTES: + raise ValueError('Incorrect number of Air Data bytes') + air_data = {'T_C': 0, 'P_Pa': 0, 'H_pc': 0, 'G_ohm': 0} + air_data['T_C'] = ((rawData[0] & const.TEMPERATURE_VALUE_MASK) + + (float(rawData[1])/10.0)) + if (rawData[0] & const.TEMPERATURE_SIGN_MASK) != 0: + # the most-significant bit is set, indicating that the + # temperature is negative + 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'] = const.FAHRENHEIT_SYMBOL + air_data['C_unit'] = const.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') - 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 + if len(rawData) != const.AIR_QUALITY_DATA_BYTES: + raise ValueError('Incorrect number of Air Quality Data bytes') + 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') - 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 + if len(rawData) != const.LIGHT_DATA_BYTES: + raise ValueError('Incorrect number of Light Data bytes') + 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') - 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): - sound_data['SPL_bands_dB'][i] = rawData[j] + (float(rawData[j+SOUND_FREQ_BANDS])/10.0) - j+=1 - j+=SOUND_FREQ_BANDS - 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 + if len(rawData) != const.SOUND_DATA_BYTES: + raise ValueError('Incorrect number of Sound Data bytes') + sound_data = {'SPL_dBA': 0, + 'SPL_bands_dB': [0]*const.SOUND_FREQ_BANDS, + 'peak_amp_mPa': 0, 'stable': 0} + sound_data['SPL_dBA'] = rawData[0] + (float(rawData[1])/10.0) + offset = 2 + for band in range(const.SOUND_FREQ_BANDS): + sound_data['SPL_bands_dB'][band] = (rawData[offset] + + (float(rawData[offset + const.SOUND_FREQ_BANDS])/10.0)) + offset += 1 + offset += const.SOUND_FREQ_BANDS + sound_data['peak_amp_mPa'] = (rawData[offset] + (rawData[offset + 1] << 8) + + (float(rawData[offset + 2])/100.0)) + sound_data['stable'] = rawData[offset + 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): + particle_data = {'duty_cycle_pc': 0, + 'concentration': 0, 'conc_unit': "", 'valid': False} + if particleSensor == const.PARTICLE_SENSOR_OFF: + return particle_data + if len(rawData) != const.PARTICLE_DATA_BYTES: + raise ValueError('Incorrect number of Particle Data bytes') + 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: + particle_data['valid'] = True + if particleSensor == const.PARTICLE_SENSOR_PPD42: + particle_data['conc_unit'] = "ppL" + elif particleSensor == const.PARTICLE_SENSOR_SDS011: + particle_data['conc_unit'] = const.SDS011_CONC_SYMBOL return particle_data - if (len(rawData) != PARTICLE_DATA_BYTES): - raise Exception('Incorrect number of Particle Data bytes supplied to function') - 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): - particle_data['valid'] = True - if (particleSensor == PARTICLE_SENSOR_PPD42): - particle_data['conc_unit'] = "ppL" - elif (particleSensor == PARTICLE_SENSOR_SDS011): - 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 + +############################################################################# + +# "get_*_data" are functions to read data over I2C and then return +# Python dictionaries containing the environmental data values. 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) - + raw_data = I2C_bus.read_i2c_block_data( + i2c_7bit_address, const.AIR_DATA_READ, const.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) - + raw_data = I2C_bus.read_i2c_block_data( + i2c_7bit_address, const.AIR_QUALITY_DATA_READ, + const.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) - + raw_data = I2C_bus.read_i2c_block_data( + i2c_7bit_address, const.LIGHT_DATA_READ, const.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) + raw_data = I2C_bus.read_i2c_block_data( + i2c_7bit_address, const.SOUND_DATA_READ, const.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, const.PARTICLE_DATA_READ, const.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) + """Function to convert Celsius temperature to Fahrenheit.""" + return ((temperature_C*1.8) + 32.0) -########################################################################################## +############################################################################# -# Provide a readable interpretation of the accuracy code for -# 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"; - elif (AQI_accuracy_code == 2): - return "Medium accuracy, self-calibration ongoing"; - elif (AQI_accuracy_code == 3): - return "High accuracy"; - else: - return "Not yet valid, self-calibration incomplete"; + """Provide a readable interpretation of the air quality accuracy code.""" + if (AQI_accuracy_code == 1): + return "Low accuracy, self-calibration ongoing" + elif (AQI_accuracy_code == 2): + return "Medium accuracy, self-calibration ongoing" + elif (AQI_accuracy_code == 3): + return "High accuracy" + else: + return "Not yet valid, self-calibration incomplete" -# Provide a readable interpretation of the AQI (air quality index) def interpret_AQI_value(AQI): - if (AQI < 50): - return "Good" - elif (AQI < 100): - return "Acceptable" - elif (AQI < 150): - return "Substandard" - elif (AQI < 200): - return "Poor" - elif (AQI < 300): - return "Bad" - else: - return "Very bad" - -########################################################################################## - -# Functions to write data to a text file, or to the screen (if the Python script is -# run in a terminal). + """Provide a readable interpretation of the AQI (air quality index).""" + if (AQI < 50): + return "Good" + elif (AQI < 100): + return "Acceptable" + elif (AQI < 150): + return "Substandard" + elif (AQI < 200): + return "Poor" + elif (AQI < 300): + return "Bad" + else: + return "Very bad" + +############################################################################# + +# "write*Data": functions to write data to a text file, or to the screen +# (if the Python script is run in a terminal). # # Each function takes 3 arguments: # -# textFileObject = An open text file object (to write to file), or None (to write to screen) +# textFileObject = An open text file object (to write to file), or +# None (to write to screen) -# XXXData = The data dictionary previously returned by the function named -# extractXXXData(), where XXX = Air, Sound, Light, AirQuality, Particle +# XXXData = The data dictionary previously returned by the +# function named extractXXXData(), where +# XXX = Air, Sound, Light, AirQuality, Particle -# writeAsColumns = Boolean; if False, values are written one per line, labeled with name and -# measurement unit. If True, values are written in columns (suitable for +# writeAsColumns = Boolean; if False, values are written one per line, +# labeled with name and measurement unit. +# If True, values are written in columns (suitable for # spreadsheets), without labels or units. -# Air data column order is: -# Temperature/C, Pressure/Pa, Humidity/%RH, Gas sensor resistance/ohm + def writeAirData(textFileObject, air_data, writeAsColumns): - if (textFileObject is None): - textFileObject = sys.stdout - if (writeAsColumns): - 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} ".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 + """Air data column order is: + Temperature/C, Pressure/Pa, Humidity/%RH, + Gas sensor resistance/ohm + """ + if textFileObject is None: + textFileObject = sys.stdout + if writeAsColumns: + textFileObject.write(f"{air_data['T']:.1f} " + f"{air_data['P_Pa']} " + f"{air_data['H_pc']:.1f} " + f"{air_data['G_ohm']} ") + else: + textFileObject.write(f"Temperature = {air_data['T']:.1f} " + f"{air_data['T_unit']}\n") + textFileObject.write(f"Pressure = {air_data['P_Pa']} Pa\n") + textFileObject.write(f"Humidity = {air_data['H_pc']:.1f} %\n") + textFileObject.write(f"Gas Sensor Resistance = {air_data['G_ohm']} " + + const.OHM_SYMBOL + "\n") + + def writeAirQualityData(textFileObject, air_quality_data, writeAsColumns): - if (textFileObject is None): - textFileObject = sys.stdout - if (writeAsColumns): - 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 (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(air_quality_data['AQI_accuracy']) + "\n") - - -# Light data column order is: -# Illuminance/lux, white light level + """Air quality data column order is: + Air Quality Index, Estimated CO2/ppm, + Equivalent breath VOC/ppm, Accuracy + """ + if textFileObject is None: + textFileObject = sys.stdout + if writeAsColumns: + textFileObject.write(f"{air_quality_data['AQI']:.1f} " + f"{air_quality_data['CO2e']:.1f} " + f"{air_quality_data['bVOC']:.2f} " + f"{air_quality_data['AQI_accuracy']} ") + else: + if air_quality_data['AQI_accuracy'] > 0: + textFileObject.write( + f"Air Quality Index = {air_quality_data['AQI']:.1f}" + f" ({interpret_AQI_value(air_quality_data['AQI'])})\n") + textFileObject.write(f"Estimated CO{const.SUBSCRIPT_2}" + f" = {air_quality_data['CO2e']:.1f} ppm\n") + textFileObject.write("Equivalent Breath VOC = " + f"{air_quality_data['bVOC']:.2f} ppm\n") + textFileObject.write("Air Quality Accuracy: " + interpret_AQI_accuracy( + air_quality_data['AQI_accuracy']) + "\n") + + def writeLightData(textFileObject, light_data, writeAsColumns): - if (textFileObject is None): - textFileObject = sys.stdout - if (writeAsColumns): - textFileObject.write("{:.2f} ".format(light_data['illum_lux'])) - textFileObject.write(str(light_data['white']) + " ") - else: - 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 + """Light data column order is: + Illuminance/lux, white light level + """ + if textFileObject is None: + textFileObject = sys.stdout + if writeAsColumns: + textFileObject.write(f"{light_data['illum_lux']:.2f} " + f"{light_data['white']} ") + else: + textFileObject.write( + f"Illuminance = {light_data['illum_lux']:.2f} lux\n") + textFileObject.write(f"White Light Level = {light_data['white']}\n") + + def writeSoundData(textFileObject, sound_data, writeAsColumns): - if (textFileObject is None): - textFileObject = sys.stdout - if (writeAsColumns): - textFileObject.write("{:.1f} ".format(sound_data['SPL_dBA'])) - for i in range(0,SOUND_FREQ_BANDS): - 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(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(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, particle_data, writeAsColumns): - if (textFileObject is None): - textFileObject = sys.stdout - if (writeAsColumns): - textFileObject.write("{:.2f} ".format(particle_data['duty_cycle_pc'])) - textFileObject.write("{:.2f} ".format(particle_data['concentration'])) - if (particle_data['valid']): - textFileObject.write("1 ") + """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 + """ + if textFileObject is None: + textFileObject = sys.stdout + if writeAsColumns: + textFileObject.write(f"{sound_data['SPL_dBA']:.1f} ") + for band in range(const.SOUND_FREQ_BANDS): + textFileObject.write(f"{sound_data['SPL_bands_dB'][band]:.1f} ") + textFileObject.write(f"{sound_data['peak_amp_mPa']:.2f} " + f"{sound_data['stable']} ") else: - textFileObject.write("0 ") - else: - 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") + textFileObject.write("A-weighted Sound Pressure Level = " + f"{sound_data['SPL_dBA']:.1f} dBA\n") + for band in range(const.SOUND_FREQ_BANDS): + textFileObject.write(f"Frequency Band {band + 1} " + f"({const.sound_band_mids_Hz[band]} Hz) " + f"SPL = {sound_data['SPL_bands_dB'][band]:.1f} dB\n") + textFileObject.write("Peak Sound Amplitude = " + f"{sound_data['peak_amp_mPa']:.2f} mPa\n") + + +def writeParticleData(textFileObject, particle_data, writeAsColumns): + """Particle data column order is: + Sensor duty cycle/%, particle concentration + """ + if textFileObject is None: + textFileObject = sys.stdout + if writeAsColumns: + textFileObject.write(f"{particle_data['duty_cycle_pc']:.2f} " + f"{particle_data['concentration']:.2f} ") + if particle_data['valid']: + textFileObject.write("1 ") + else: + textFileObject.write("0 ") else: - textFileObject.write("Particle data valid: Yes\n") + textFileObject.write("Particle Sensor Duty Cycle = " + f"{particle_data['duty_cycle_pc']:.2f} %\n") + textFileObject.write("Particle Concentration = " + f"{particle_data['concentration']:.2f} " + f"{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") + +############################################################################# -########################################################################################## -# Function to open a new output data file, in a specified -# directory, with a name containing the time and date def startNewDataFile(dataFileDirectory): - filename = os.path.join(dataFileDirectory,datetime.datetime.now().strftime('data_%Y-%m-%d_%H-%M-%S.txt')) - print("Logging data to file " + filename) - return open(filename, 'a') + """Open an output text data file. -########################################################################################## - -# Set the threshold for triggering a sound interrupt. -# -# sound_thres_mPa = peak sound amplitude threshold in milliPascals, any 16-bit integer is allowed. -def setSoundInterruptThreshold(I2C_bus, sound_thres_mPa): - # The 16-bit threshold value is split and sent as two 8-bit values: [LSB, MSB] - data_to_send = [(sound_thres_mPa & 0x00FF), (sound_thres_mPa >> 8)] - I2C_bus.write_i2c_block_data(i2c_7bit_address, SOUND_INTERRUPT_THRESHOLD_REG, data_to_send) + Append to an existing file, else create a new one. + """ + filename = os.path.join( + dataFileDirectory, + datetime.now().strftime('data_%Y-%m-%d_%H-%M-%S.txt')) + print("Logging data to file " + filename) + return open(filename, 'a') -# Set the threshold for triggering a light interrupt. -# -# The threshold value in lux units can be fractional and is formed as: -# threshold = light_thres_lux_i + (light_thres_lux_f2dp/100) -# -# Threshold values exceeding MAX_LUX_VALUE will be limited to MAX_LUX_VALUE. -def setLightInterruptThreshold(I2C_bus, light_thres_lux_i, light_thres_lux_f2dp): - # The 16-bit integer part of the threshold value is split and sent as two 8-bit values, while - # the fractional part is sent as an 8-bit value: - data_to_send = [(light_thres_lux_i & 0x00FF), (light_thres_lux_i >> 8), light_thres_lux_f2dp] - I2C_bus.write_i2c_block_data(i2c_7bit_address, LIGHT_INTERRUPT_THRESHOLD_REG, data_to_send) +def setSoundInterruptThreshold(I2C_bus, sound_thres_mPa): + """Set the threshold for triggering a sound interrupt. + + sound_thres_mPa = peak sound amplitude threshold in + milliPascals; any 16-bit integer is allowed. + """ + data_to_send = [(sound_thres_mPa & 0x00FF), (sound_thres_mPa >> 8)] + I2C_bus.write_i2c_block_data( + i2c_7bit_address, const.SOUND_INTERRUPT_THRESHOLD_REG, data_to_send) + + +def setLightInterruptThreshold(I2C_bus, light_thres_lux_i, + light_thres_lux_f2dp): + """Set the threshold for triggering a light interrupt. + + The threshold value in lux units can be fractional and is formed as: + threshold = light_thres_lux_i + (light_thres_lux_f2dp / 100) + Threshold values exceeding MAX_LUX_VALUE will be limited to MAX_LUX_VALUE. + """ + data_to_send = [(light_thres_lux_i & 0x00FF), + (light_thres_lux_i >> 8), light_thres_lux_f2dp] + I2C_bus.write_i2c_block_data( + i2c_7bit_address, const.LIGHT_INTERRUPT_THRESHOLD_REG, data_to_send) diff --git a/Python/Raspberry_Pi/sensor_package/servers.py b/Python/Raspberry_Pi/sensor_package/servers.py index cede269..035b094 100644 --- a/Python/Raspberry_Pi/sensor_package/servers.py +++ b/Python/Raspberry_Pi/sensor_package/servers.py @@ -1,212 +1,195 @@ -# servers.py +"""HTTP request handler classes. -# This file contains HTTP request handler classes which are used in the -# web_server.py and graph_web_server.py examples. +This file contains HTTP request handler classes which are used in the +web_server.py and graph_web_server.py examples. +They also use files text_web_page.html and graph_web_page.html +""" -# Copyright 2020 Metriful Ltd. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -import http.server +from http.server import BaseHTTPRequestHandler 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() +import pkgutil +import jinja2 +from pathlib import Path +from subprocess import check_output +from . import sensor_functions as sensor +from . import sensor_constants as const + + +class SimpleWebpageHandler(BaseHTTPRequestHandler): + """Make a simple text webpage to display environment data. + + The webpage is HTML and CSS only and does not use javascript. + Periodic refresh is achieved using the 'Refresh' HTTP header. + """ + + the_web_page = "Awaiting data..." + air_data = None + air_quality_data = None + sound_data = None + light_data = None + particle_data = None + interpreted_AQI_accuracy = None + interpreted_AQI_value = None + refresh_period_seconds = 3 + template = jinja2.Environment( + loader=jinja2.FileSystemLoader(Path(__file__).parent), + autoescape=True).get_template("text_web_page.html") + + @classmethod + def _get_http_headers(cls): + """Make headers for HTTP response with variable refresh time.""" + return ("HTTP/1.1 200 OK\r\n" + "Content-type: text/html\r\n" + "Connection: close\r\n" + f"Refresh: {cls.refresh_period_seconds}\r\n\r\n") + + def do_GET(self): + """Implement the HTTP GET method.""" + self.wfile.write( + bytes(self._get_http_headers() + self.the_web_page, "utf8")) + + def do_HEAD(self): + """Implement the HTTP HEAD method.""" + self.wfile.write(bytes(self._get_http_headers(), "utf8")) + + @classmethod + def assemble_web_page(cls, readout_time_and_date=None): + """Create the updated webpage, for serving to all clients.""" + cls._interpret_data() + cls.the_web_page = cls.template.render( + air_data=cls.air_data, air_quality_data=cls.air_quality_data, + sound_data=cls.sound_data, light_data=cls.light_data, + particle_data=cls.particle_data, + interpreted_AQI_accuracy=cls.interpreted_AQI_accuracy, + interpreted_AQI_value=cls.interpreted_AQI_value, + sound_band_mids_Hz=const.sound_band_mids_Hz, + readout_time_and_date=readout_time_and_date) + + @classmethod + def _interpret_data(cls): + if cls.air_quality_data is not None: + cls.interpreted_AQI_accuracy = sensor.interpret_AQI_accuracy( + cls.air_quality_data['AQI_accuracy']) + cls.interpreted_AQI_value = sensor.interpret_AQI_value( + cls.air_quality_data['AQI']) + + +class GraphWebpageHandler(BaseHTTPRequestHandler): + """Make a web page with graphs to display environment data.""" + + the_web_page = pkgutil.get_data(__name__, 'graph_web_page.html') + 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") + + def do_GET(self): + """Implement the HTTP GET method.""" + if self.path == '/': + # The web page is requested + self.wfile.write(bytes(self.page_header, "utf8")) + self.wfile.write(self.the_web_page) + 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 do_HEAD(self): + """Implement the HTTP HEAD method.""" + if self.path == '/': + self.wfile.write(bytes(self.page_header, "utf8")) + elif self.path == '/1' or self.path == '/2': + self.wfile.write(bytes(self.data_header, "utf8")) + else: + self.wfile.write(bytes(self.error_response_HTTP, "utf8")) + + def send_all_data(self): + """Respond to client request by sending all buffered data.""" + 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 particle sensor type + self.wfile.write(struct.pack('B', sensor.PARTICLE_SENSOR)) + # Send choice of temperature unit + self.wfile.write(struct.pack('B', int(sensor.USE_FAHRENHEIT))) + # 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 in the specific order: + 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): + """Respond to client request by sending only the most recent data.""" + self.wfile.write(bytes(self.data_header, "utf8")) + # Send the most recent values, if buffers are not empty + if self.temperature: + data = [self.AQI[-1], self.temperature[-1], self.pressure[-1], + self.humidity[-1], self.SPL[-1], self.illuminance[-1], + self.bVOC[-1]] + if self.particle: + data.append(self.particle[-1]) + self.wfile.write(struct.pack(str(len(data)) + 'f', *data)) + + @classmethod + def set_buffer_length(cls, buffer_length): + """Create a FIFO data buffer for each variable.""" + 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']) + + +def get_IP_addresses(): + """Get this computer's IP addresses.""" + ips = [x.strip() for x in check_output( + ['hostname', '-I']).decode().strip().split('\n')] + if len(ips) == 1 and ips[0] == '': + return [] 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']) - + return ips diff --git a/Python/Raspberry_Pi/sensor_package/text_web_page.html b/Python/Raspberry_Pi/sensor_package/text_web_page.html new file mode 100644 index 0000000..9647327 --- /dev/null +++ b/Python/Raspberry_Pi/sensor_package/text_web_page.html @@ -0,0 +1,146 @@ + + + + + + + Metriful Sensor Demo + + + +

Indoor Environment Data

+ + {% if air_data is not none %} +

+

Air Data

+ + + + + + + + + + + + + + + + + + +
Temperature{{ '%.1f' % air_data['T'] }}{{ air_data['T_unit'] }}
Pressure{{ air_data['P_Pa'] }}Pa
Humidity{{ '%.1f' % air_data['H_pc'] }}%
Gas Sensor Resistance{{ air_data['G_ohm'] }}Ω
+

+ {% endif %} + + {% if air_quality_data is not none %} +

+

Air Quality Data

+ {% if air_quality_data['AQI_accuracy'] == 0 %} + {{ interpreted_AQI_accuracy }} + {% else %} + + + + + + + + + + + + + + + + + +
Air Quality Index{{ '%.1f' % air_quality_data['AQI'] }}
Air Quality Summary{{ interpreted_AQI_value }}
Estimated CO₂{{ '%.1f' % air_quality_data['CO2e'] }}ppm
Equivalent Breath VOC{{ '%.2f' % air_quality_data['bVOC'] }}ppm
+ {% endif %} +

+ {% endif %} + + {% if sound_data is not none %} +

+

Sound Data

+ + + + + + {% for band_freq in sound_band_mids_Hz %} + + + + + + {% endfor %} + + + + +
A-weighted Sound Pressure Level{{ '%.1f' % sound_data['SPL_dBA'] }}dBA
Frequency Band {{ loop.index }} ({{ band_freq }} Hz) SPL{{ '%.1f' % sound_data['SPL_bands_dB'][loop.index0] }}dB
Peak Sound Amplitude{{ '%.2f' % sound_data['peak_amp_mPa'] }}mPa
+

+ {% endif %} + + {% if light_data is not none %} +

+

Light Data

+ + + + + + + + + +
Illuminance{{ '%.2f' % light_data['illum_lux'] }}lux
White Light Level{{ light_data['white'] }}
+

+ {% endif %} + + {% if particle_data is not none %} +

+

Air Particulate Data

+ + + + + + + + + + +
Sensor Duty Cycle{{ '%.2f' % particle_data['duty_cycle_pc'] }}%
Particle Concentration{{ '%.2f' % particle_data['concentration'] }}{{ particle_data['conc_unit'] }}
+

+ {% endif %} + + {% if readout_time_and_date is not none %} +

+

The last sensor reading was at {{ readout_time_and_date }}

+

+ {% endif %} + +

+ sensor.metriful.com +

+ + diff --git a/Python/Raspberry_Pi/simple_read_T_H.py b/Python/Raspberry_Pi/simple_read_T_H.py index c2688f9..54951b8 100644 --- a/Python/Raspberry_Pi/simple_read_T_H.py +++ b/Python/Raspberry_Pi/simple_read_T_H.py @@ -1,82 +1,79 @@ -# simple_read_T_H.py +"""Example of measurements using the Metriful MS430 from a Raspberry Pi. -# 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. +""" -# 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. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -from sensor_package.sensor_functions import * +import time +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const # Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() +(GPIO, I2C_bus) = sensor.SensorHardwareSetup() # Initiate an on-demand data measurement -I2C_bus.write_byte(i2c_7bit_address, ON_DEMAND_MEASURE_CMD) +I2C_bus.write_byte(sensor.i2c_7bit_address, const.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. +while (not GPIO.event_detected(sensor.READY_pin)): + time.sleep(0.05) + +# New data are now ready to read; this can be done in multiple ways: -######################################################### -# 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 +# 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) +air_data = sensor.get_air_data(I2C_bus) # Then print all the values onto the screen -writeAirData(None, air_data, False) +sensor.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'])) +print(f"The temperature is: {air_data['T_C']:.1f} {air_data['C_unit']}") +print(f"The humidity is: {air_data['H_pc']:.1f} %") # Temperature can also be output in Fahrenheit units -print("The temperature is: {:.1f} ".format(air_data['T_F']) + air_data['F_unit']) +print(f"The temperature is: {air_data['T_F']:.1f} {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(f"The temperature is: {air_data['T']:.1f} {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) +raw_data = I2C_bus.read_i2c_block_data(sensor.i2c_7bit_address, + const.H_READ, const.H_BYTES) -# Decode the humidity: the first received byte is the integer part, the +# 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(f"Humidity = {humidity:.1f} %") 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) +raw_data = I2C_bus.read_i2c_block_data(sensor.i2c_7bit_address, + const.T_READ, const.T_BYTES) -# Find the positive magnitude of the integer part of the temperature by +# 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 +temp_positive_integer = raw_data[0] & const.TEMPERATURE_VALUE_MASK # The second received byte is the fractional part to one decimal place temp_fraction = raw_data[1] @@ -84,16 +81,15 @@ # 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 +# 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 +if (raw_data[0] & const.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) +print(f"Temperature = {temperature:.1f} {const.CELSIUS_SYMBOL}") ######################################################### GPIO.cleanup() - diff --git a/Python/Raspberry_Pi/simple_read_sound.py b/Python/Raspberry_Pi/simple_read_sound.py index 69d604a..6f4ee35 100644 --- a/Python/Raspberry_Pi/simple_read_sound.py +++ b/Python/Raspberry_Pi/simple_read_sound.py @@ -1,69 +1,65 @@ -# simple_read_sound.py +"""Example of using the Metriful MS430 to measure sound, from a Raspberry Pi. -# Example code for using the Metriful MS430 to measure sound. -# This example is designed to run with Python 3 on a Raspberry Pi. - -# 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. +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. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor -from sensor_package.sensor_functions import * +import time +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const # Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() +(GPIO, I2C_bus) = sensor.SensorHardwareSetup() ######################################################### -# Wait for the microphone signal to stabilize (takes approximately 1.5 seconds). -# This only needs to be done once after the MS430 is powered-on or reset. -sleep(1.5); +# Wait for the microphone signal to stabilize (takes approximately +# 1.5 seconds). This only needs to be done once after the +# MS430 is powered-on or reset. +time.sleep(1.5) ######################################################### # Initiate an on-demand data measurement -I2C_bus.write_byte(i2c_7bit_address, ON_DEMAND_MEASURE_CMD) +I2C_bus.write_byte(sensor.i2c_7bit_address, const.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. +while (not GPIO.event_detected(sensor.READY_pin)): + time.sleep(0.05) + +# New data are now ready to read; this can be done in multiple ways: -######################################################### -# 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) +sound_data = sensor.get_sound_data(I2C_bus) # Then print all the values onto the screen -writeSoundData(None, sound_data, False) +sensor.writeSoundData(None, sound_data, False) # 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) +# 2. Read the raw data bytes from the MS430 using an I2C function +raw_data = I2C_bus.read_i2c_block_data( + sensor.i2c_7bit_address, const.SOUND_DATA_READ, const.SOUND_DATA_BYTES) # Decode the values and return then as a Python dictionary -sound_data = extractSoundData(raw_data) +sound_data = sensor.extractSoundData(raw_data) # Print the dictionary values in the same ways as before -writeSoundData(None, sound_data, False) +sensor.writeSoundData(None, sound_data, False) print("The sound pressure level is: " + str(sound_data['SPL_dBA']) + " dBA") - ######################################################### GPIO.cleanup() - diff --git a/Python/Raspberry_Pi/web_server.py b/Python/Raspberry_Pi/web_server.py index a640b07..a9bbdf2 100644 --- a/Python/Raspberry_Pi/web_server.py +++ b/Python/Raspberry_Pi/web_server.py @@ -1,47 +1,50 @@ -# 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. +"""Example of serving a text web page on a local network, from a Raspberry Pi. + +Example code for serving a simple 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-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor +import time import socketserver -from sensor_package.servers import * -from sensor_package.sensor_functions import * +from datetime import datetime +import sensor_package.servers as server +import sensor_package.sensor_functions as sensor +import sensor_package.sensor_constants as const ######################################################### # 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 +cycle_period = const.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: +# To find your Raspberry Pi's IP address: # 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" +# 4. The IP address on each network is displayed after "inet" # -# Example - part of an output block showing the address 172.24.1.1 +# 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 @@ -50,74 +53,88 @@ ######################################################### # Set up the GPIO and I2C communications bus -(GPIO, I2C_bus) = SensorHardwareSetup() +(GPIO, I2C_bus) = sensor.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]) +I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) +I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, const.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 +# 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 +if (cycle_period == const.CYCLE_PERIOD_3_S): + server.SimpleWebpageHandler.refresh_period_seconds = 3 +elif (cycle_period == const.CYCLE_PERIOD_100_S): + server.SimpleWebpageHandler.refresh_period_seconds = 30 +else: # CYCLE_PERIOD_300_S + server.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 +# 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) +print("Starting the web server...") +ips = server.get_IP_addresses() +if not ips: + print("Warning: no networks detected.") +else: + print("Your web page will be available at:") + for ip in ips: + print(f" http://{ip}:{port}") + print("For more information on network IP addresses, " + "run the command ifconfig in a terminal.") +print("Press ctrl-c to exit at any time.") + +the_server = socketserver.TCPServer(("", port), server.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() +I2C_bus.write_byte(sensor.i2c_7bit_address, const.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(sensor.READY_pin): + the_server.handle_request() + time.sleep(0.05) + + # Now read all data from the MS430 and pass to the web page + + # Air data + server.SimpleWebpageHandler.air_data = sensor.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. + server.SimpleWebpageHandler.air_quality_data = sensor.get_air_quality_data(I2C_bus) + + # Light data + server.SimpleWebpageHandler.light_data = sensor.get_light_data(I2C_bus) + + # Sound data + server.SimpleWebpageHandler.sound_data = sensor.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 sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF: + server.SimpleWebpageHandler.particle_data = sensor.get_particle_data( + I2C_bus, sensor.PARTICLE_SENSOR) + + # Create the updated web page ready for client requests, passing + # the current date and time for displaying with the data + server.SimpleWebpageHandler.assemble_web_page( + f'{datetime.now():%H:%M:%S %Y-%m-%d}') diff --git a/Python/graph_viewer_I2C.py b/Python/graph_viewer_I2C.py index 9ad9235..c26d757 100644 --- a/Python/graph_viewer_I2C.py +++ b/Python/graph_viewer_I2C.py @@ -1,159 +1,167 @@ -# graph_viewer_I2C.py +"""Real-time display of MS430 data, using a Raspberry Pi. -# 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. -# 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). -# 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. -######################################################### - -# 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. +Installation instructions for the necessary packages are in the +readme and User Guide. +""" -# Copyright 2020 Metriful Ltd. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# 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 * +import time +from datetime import datetime +from PyQt5.QtWidgets import QApplication +from GraphViewer import GraphViewer +import Raspberry_Pi.sensor_package.sensor_functions as sensor +import Raspberry_Pi.sensor_package.sensor_constants as const ######################################################### # 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 +# Choose the time 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 +# Set cycle_period_code=None to use on-demand mode, or choose any +# of: CYCLE_PERIOD_3_S, CYCLE_PERIOD_100_S, CYCLE_PERIOD_300_S +cycle_period_code = None # OR: on_demand_delay_ms = 0 # Choose any number of milliseconds -# This delay is in addition to the 0.5 second readout time +# This delay is in addition to the 0.5 second readout time. -# Temperature and particle data are less accurate if read more +# 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 +# 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()) +class GraphViewerI2C(GraphViewer): + """Real-time display of MS430 data, using a Raspberry Pi.""" + + def __init__(self, buffer_length, cycle_period, OD_delay_ms): + """Set up the I2C and the MS430 board.""" + super().__init__(buffer_length, sensor.PARTICLE_SENSOR, + sensor.USE_FAHRENHEIT) + self.setupSensorI2C(cycle_period, OD_delay_ms) + air_quality_data = self.cycle_mode + particle_data = sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF + flag_data = False + self.setDataRequired(air_quality_data, particle_data, flag_data) + + def setupSensorI2C(self, cycle_period, OD_delay_ms): + """Set up the MS430 sensor by selecting mode and particle sensor.""" + (self.GPIO, self.I2C_bus) = sensor.SensorHardwareSetup() + if sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF: + self.I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) + if (cycle_period is None) and (OD_delay_ms is None): + raise ValueError( + "Either cycle_period or OD_delay_ms must be specified") + if cycle_period is not None: # Cycle mode with 3/100/300 second delays + self.cycle_mode = True + self.I2C_bus.write_i2c_block_data( + sensor.i2c_7bit_address, + const.CYCLE_TIME_PERIOD_REG, [cycle_period]) + self.I2C_bus.write_byte( + sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) + else: # On-demand mode with chosen time delay between measurements + self.cycle_mode = False + self.I2C_bus.write_byte( + sensor.i2c_7bit_address, const.ON_DEMAND_MEASURE_CMD) + self.delaying = False + self.OD_delay_ms = OD_delay_ms + + def getDataFunction(self): + """Obtain new data from I2C and put in data_buffer. + + Returns True if new data were obtained, else returns False. + """ + if self.cycle_mode: + if self.GPIO.event_detected(sensor.READY_pin): + self.readData() + return True + else: # On-demand mode + if self.delaying: + time_now_ms = time.time()*1000 + if (time_now_ms - self.time_start_ms) >= self.OD_delay_ms: + # Trigger a new measurement + self.I2C_bus.write_byte( + sensor.i2c_7bit_address, const.ON_DEMAND_MEASURE_CMD) + self.delaying = False + else: + if self.GPIO.event_detected(sensor.READY_pin): + self.readData() + self.delaying = True + self.time_start_ms = time.time()*1000 + return True + return False + + def readData(self): + """Read the newly available data from the sensor board.""" + self.setWindowTitle('Indoor Environment Data') + air_data = sensor.get_air_data(self.I2C_bus) + air_quality_data = sensor.get_air_quality_data(self.I2C_bus) + light_data = sensor.get_light_data(self.I2C_bus) + sound_data = sensor.get_sound_data(self.I2C_bus) + particle_data = sensor.get_particle_data( + self.I2C_bus, sensor.PARTICLE_SENSOR) + self.putDataInBuffer(air_data, air_quality_data, + light_data, sound_data, particle_data) + + def appendData(self, start_index, data): + """Add new data to the data buffer.""" + for i, value in enumerate(data): + self.data_buffer[start_index + i].append(value) + return (start_index + len(data)) + + def putDataInBuffer(self, air_data, air_quality_data, light_data, + sound_data, particle_data): + """Store the data and also the time/date.""" + 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 sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF: + i = self.appendData( + i, [particle_data['duty_cycle_pc'], + particle_data['concentration']]) + self.time_data.append(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_() + theApp = 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 index 556c45f..1f585c8 100644 --- a/Python/graph_viewer_serial.py +++ b/Python/graph_viewer_serial.py @@ -1,52 +1,45 @@ -# graph_viewer_serial.py +"""Real-time display of MS430 data, from a host device over USB serial. -# 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). -# 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. -# 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. -######################################################### - -# 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) +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 +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. +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. +3) Put the serial port name (system dependent) in the serial_port_name +parameter below. -# 4) Run this program with python3 +4) Run this program with python3 +""" -# Copyright 2020 Metriful Ltd. +# Copyright 2020-2023 Metriful Ltd. # Licensed under the MIT License - for further details see LICENSE.txt -# For code examples, datasheet and user guide, visit +# For code examples, datasheet and user guide, visit # https://github.com/metriful/sensor +import serial +from datetime import datetime +from PyQt5.QtWidgets import QApplication +from GraphViewer import GraphViewer +import Raspberry_Pi.sensor_package.sensor_constants as const + ######################################################### # 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" @@ -56,83 +49,74 @@ # 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 (Celsius/Fahrenheit): +particle_sensor_type = const.PARTICLE_SENSOR_OFF +use_fahrenheit = False # else uses Celsius + # 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()) + """Real-time display of MS430 data, from a host device over USB serial.""" + + def __init__(self, buffer_length, serial_port): + """Set up the serial interface to the MS430 host.""" + super().__init__(buffer_length, particle_sensor_type, use_fahrenheit) + 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.initial_discard_lines = 2 + self.startup_line_count = 0 + self.startup = True + + def serialStartupCompleted(self, data_strings): + """Check that the number of values received is correct.""" + if not self.startup: return True - except: - pass - return False # no new data - + if self.startup_line_count < self.initial_discard_lines: + self.startup_line_count += 1 + return False + self.startup = False + if len(data_strings) == 15: + self.setDataRequired(False, False, True) + elif len(data_strings) == 18: + self.setDataRequired(False, True, True) + elif len(data_strings) == 19: + self.setDataRequired(True, False, True) + elif len(data_strings) == 22: + self.setDataRequired(True, True, True) + else: + raise RuntimeError('Unexpected number of data columns') + self.setWindowTitle('Indoor Environment Data') + return True + + def getDataFunction(self): + """Check for new serial data.""" + response = self.serial_port.readline() + if (not ((response is None) or (len(response) == 0))): + try: + data_strings = response.decode('utf-8').split() + if self.serialStartupCompleted(data_strings): + if (len(data_strings) == len(self.data_name_index)): + # Convert strings to numbers and store the data + float_data = [float(s) for s in data_strings] + for i in range(len(self.data_name_index)): + self.data_buffer[i].append(float_data[i]) + self.time_data.append(datetime.now().timestamp()) + return True + except Exception: + 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_() + theApp = QApplication([]) + gv = GraphViewerSerial(data_buffer_length, serial_port_name) + gv.start() + theApp.exec_() diff --git a/README.md b/README.md index a890edf..7cdb4e6 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,15 @@

-[**Buy the hardware now - REDUCED PRICES.**](https://www.metriful.com/shop) - 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. -This repository provides instructions and software examples for running the MS430 with **Raspberry Pi, Arduino, ESP8266** and **ESP32** host systems. +This repository provides instructions and software examples for running the MS430 with **Raspberry Pi (0/2/3/4/Pico), Arduino, ESP8266** and **ESP32** host systems. -Code examples include interfaces to **IFTTT, Home Assistant** and **IoT cloud platforms**, as well as **real-time graph software, web servers** and **interrupt detection**. +Code examples include interfaces to **IFTTT, Home Assistant, ESPHome** 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 [**User Guide**](User_guide.pdf) explains more about what the device measures. The [**Datasheet**](Datasheet.pdf) is a detailed specification of the electrical and communications interfaces of the MS430. @@ -23,8 +21,9 @@ The [**Datasheet**](Datasheet.pdf) is a detailed specification of the electrical #### Hardware setup - **[Handling precautions](#handling-precautions)**
-- **[Arduino](#arduino)**
-- **[Raspberry Pi](#raspberry-pi)**
+- **[Arduino Nano 33 IoT, MKR WiFi 1010, Uno, Nano](#arduino-nano-33-iot-mkr-wifi-1010-uno-nano)**
+- **[Raspberry Pi 0/2/3/4](#raspberry-pi-versions-0234)**
+- **[Raspberry Pi Pico](#raspberry-pi-pico)**
- **[ESP8266](#esp8266)**
- **[ESP32](#esp32)**
- **[Particle sensor](#connecting-and-enabling-a-particle-sensor)**
@@ -32,10 +31,12 @@ The [**Datasheet**](Datasheet.pdf) is a detailed specification of the electrical - **[IoT cloud setup](#iot-cloud-setup)**
- **[Graph web server](#graph-web-server)**
- **[IFTTT example](#ifttt-example)**
-- **[Home Assistant example](#home-assistant-example)**
+- **[ESPHome for Home Assistant](#esphome-for-home-assistant)**
+- **[Home Assistant POST example](#home-assistant-post-example)**
- **[Graph viewer software](#graph-viewer-software)**
- **[Fahrenheit temperatures](#fahrenheit-temperatures)**
-#### Other information +#### Other information +- **[Library and software versions](#library-and-software-versions)**
- **[Case and enclosure ideas](#case-enclosure-and-mounting-ideas)**
- **[Troubleshooting](#troubleshooting)**
- **[Changelog](#changelog)**
@@ -53,7 +54,7 @@ The MS430 can be damaged by static electricity discharges. Minimize this risk by - Keep away from metal objects which could cause shorted connections -## Arduino +## Arduino Nano 33 IoT, MKR WiFi 1010, Uno, Nano 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. @@ -63,7 +64,7 @@ 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. Delete any previous version you may have. +3. Clone, or 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. The rest of the downloaded repository can be placed anywhere e.g. in your documents folder. If using **Arduino Nano 33 IoT** or **Arduino MKR WiFi 1010**, also do the following: @@ -85,9 +86,8 @@ If using **Arduino Nano 33 IoT** or **Arduino MKR WiFi 1010**, also do the follo | SIT | D7 | D7 | A2 | D5 | | RDY | D2 | D2 | D11 | D0 | -* 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. +* MS430 pin VDD is not used with the 5V systems (Uno and Nano) and pin 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 interrupt outputs. * 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 @@ -95,38 +95,39 @@ If using **Arduino Nano 33 IoT** or **Arduino MKR WiFi 1010**, also do the follo 1. Wire the MS430 board to the Arduino as described in the previous section. 2. Plug the Arduino 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 Arduino attached. -5. Go to Tools > Board and select the Arduino model (Uno / Nano / Nano 33 IoT / MKR WiFi 1010). +4. In the Arduino IDE menu, go to Tools > Board and select the Arduino model (Uno / Nano / Nano 33 IoT / MKR WiFi 1010). +5. Go to Tools > Port and select the port with the Arduino attached. 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). -## Raspberry Pi +## Raspberry Pi (versions 0/2/3/4) -The example programs for Raspberry Pi use Python 3 and are provided in the **Raspberry_Pi** folder, within the **Python** folder. +The example programs for Raspberry Pi computers use Python 3 and are located in the **Python/Raspberry_Pi** folder. ### First time Raspberry Pi setup 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: +1. If you are using Raspberry Pi OS Lite (or get missing package errors), run the following commands to install required packages: ``` sudo apt-get update sudo apt install i2c-tools python3-smbus python3-rpi.gpio + pip3 install jinja2 psutil pyserial requests ``` 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. + Select **Interface Options** and then **I2C**. A prompt will appear asking "Would you like the ARM I2C interface to be enabled?": select **Yes** and then exit the utility. 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**. -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. +5. Clone, or download and unzip, this [Sensor repository](https://www.github.com/metriful/sensor), placing the folder anywhere on your system e.g. in your user home folder. The Raspberry Pi examples are in the **Python/Raspberry_Pi** folder. ### Wiring for Raspberry Pi @@ -142,40 +143,84 @@ This setup assumes that you are using Raspberry Pi OS. The standard OS version c | SIT | 8 | GPIO 14 | | RDY | 11 | GPIO 17 | -* Raspberry Pi pin numbering is [shown here](https://www.raspberrypi.org/documentation/usage/gpio/README.md). +* Raspberry Pi pin numbering is [shown here](https://www.raspberrypi.com/documentation/computers/os.html#gpio-and-the-40-pin-header). * MS430 pin VIN is not used. * LIT/SIT connections are optional and only required if you are using light/sound interrupts. ### To run an example Raspberry Pi program: 1. Wire the MS430 to the Pi as described in the previous section. -2. Open a terminal and change to the code examples folder **Raspberry_Pi**. -3. Run one of the example programs using Python 3: +2. Open a terminal and navigate to the code examples folder **Python/Raspberry_Pi**. +3. Run the example programs using Python 3, for example: ``` python3 simple_read_sound.py ``` +## Raspberry Pi Pico + +All code examples in the Arduino folder work with the official Raspberry Pi Pico W and all non-WiFi examples work with the non-wireless Pico version. + +### First time Raspberry Pi Pico 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. Clone, or 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. The rest of the downloaded repository can be placed anywhere e.g. in your documents folder. +4. In the Arduino IDE menu, go to File > Preferences. In the box labeled "Additional Boards Manager URLs", paste the following link on a new line: + ``` + https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json + ``` +5. In the Arduino IDE menu, go to Tools > Board > Boards Manager. Search for and install the package named **Raspberry Pi Pico/RP2040 by Earle F. Philhower**. + +### Wiring for Raspberry Pi Pico + +| MS430 pin | Pico pin number | +|:---------------:|:--------------------:| +| VIN | - | +| VDD | 36 | +| GND | 23 | +| VPU | 36 | +| SCL | 27 | +| SDA | 26 | +| LIT | 31 | +| SIT | 32 | +| RDY | 34 | + +* The pin numbering is shown on the offial pinout datasheet available at [https://datasheets.raspberrypi.com](https://datasheets.raspberrypi.com). +* 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 Raspberry Pi Pico + +1. Wire the MS430 board to the Pico as described in the previous section. +2. Plug the Pico 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 > Board and select "Raspberry Pi Pico W" or "Raspberry Pi Pico" as appropriate. +5. Go to Tools > Port and select the port with the Pico attached. +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). + + ## ESP8266 All code examples in the Arduino folder have been tested on NodeMCU and Wemos D1 Mini, and are programmed using the Arduino IDE. 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 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. 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: +3. Clone, or 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. The rest of the downloaded repository can be placed anywhere e.g. in your documents folder. +4. In the Arduino IDE menu, go to File > Preferences. In the box labeled "Additional Boards Manager URLs", paste the following link on a new line: ``` 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 ESP8266 @@ -202,8 +247,8 @@ Note that steps 1 and 2 are already complete if you have used Arduino IDE before 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. +4. In the Arduino IDE menu, go to Tools > Board and select your development board, or "Generic ESP8266 Module", or experiment until you find one that works. +5. Go to Tools > Port and select the port with the ESP8266 attached. 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). @@ -220,13 +265,12 @@ Note that steps 1 and 2 are already complete if you have used Arduino IDE before 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: +3. Clone, or 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. The rest of the downloaded repository can be placed anywhere e.g. in your documents folder. +4. In the Arduino IDE menu, go to File > Preferences. In the box labeled "Additional Boards Manager URLs", paste the following link on a new line: ``` - https://dl.espressif.com/dl/package_esp32_index.json + https://espressif.github.io/arduino-esp32/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**. +5. In the Arduino IDE menu, go to Tools > Board > Boards Manager. Search for and install the package named **esp32 by Espressif Systems** then restart the IDE. ### Wiring for ESP32 @@ -251,15 +295,15 @@ Note that steps 1 and 2 are already complete if you have used Arduino IDE before 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 ESP32 attached. -5. Go to Tools > Board and select your development board, or experiment until you find one that works. +4. In the Arduino IDE menu, go to Tools > Board and select your development board, or experiment until you find one that works. +5. Go to Tools > Port and select the port with the ESP32 attached. 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. +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. **The PPD42 is not recommended** and is supported for backwards compatibility only. Both sensor models require three wire connections: **5V, GND, PRT** and a small edit to the example code. @@ -277,7 +321,8 @@ Both sensor models require three wire connections: **5V, GND, PRT** and a small | Host device | 5V pin name/number | |:---------------------:|:------------------:| -| Raspberry Pi | Pin 2 | +| Raspberry Pi 0/2/3/4 | Pin 2 | +| Raspberry Pi Pico | Pin 40 | | Arduino Uno | 5V or IOREF (*) | | Arduino Nano | 5V | | Arduino Nano 33 IoT | VUSB (**) | @@ -289,7 +334,8 @@ Both sensor models require three wire connections: **5V, GND, PRT** and a small (**) 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. +* **Raspberry Pi 0/2/3/4:** pin 9 can be used as an extra GND connection. +* **Raspberry Pi Pico:** pins 3, 8, 13, 18, 28, 33, 38 can be used as extra GND connections. * Pin labels for ESP8266 and ESP32 may be different on some boards ### Enable the particle sensor in the code examples @@ -407,19 +453,65 @@ You can customize all parts of this message. ``` 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. +12. Click the triangle Webhooks icon, then on **Documentation**. 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). +14. Edit the **IFTTT** example code file, pasting in this key and the event name (from step 4). 15. Run the program. -## Home Assistant example +## ESPHome for Home Assistant + +Send sensor data to an installation of [Home Assistant](https://www.home-assistant.io) using the [ESPHome system](https://esphome.io). + +Your microcontroller device must be one of: ESP8266, ESP32, Raspberry Pi Pico W. Other devices (Arduino or Raspberry Pi 0/2/3/4) must use the [Home Assistant POST](#home-assistant-post-example) method instead. + +These instructions assume that you already have a working Home Assistant installation. All of the steps are done from your personal computer, not the Home Assistant server computer. + +1. [Install ESPHome](https://esphome.io/guides/installing_esphome) on your computer. + +2. In the Arduino/Metriful_Sensor folder, run terminal command: ```esphome dashboard .``` (note the dot). +This starts a temporary local web server on your computer. Leave this process running. + +3. Go to [http://0.0.0.0:6052](http://0.0.0.0:6052) or [http://127.0.0.1:6052](http://127.0.0.1:6052) in a browser to view the temporary ESPHome dashboard. Click "new device", ignore any note about HTTPS, and click "continue". + +4. In the dialog, give your device a unique name (this is the **device_name**) and input your WiFi network name (SSID) and password. + +5. Choose your board type from the list (ESP, Pico, etc.), then copy the encryption key for later use (it can also be found in a local yaml file). + +6. Plug the microcontroller board into your computer via USB, then click "install" and choose: + * **Manual download** for Raspberry Pi Pico W; install is complete when the drive disappears. + * **Plug into the computer running ESPHome Dashboard** for all other boards. The install is complete when coloured logs about WiFi signal appear in the window: then click "stop" to close the window. + +7. In the Arduino/Metriful_Sensor folder, a new file called .yaml should have appeared. As its first three lines, it has: +``` +esphome: + name: + friendly_name: +``` +Where `````` is the name you chose. **Replace** these three lines with the entire contents of Arduino/Metriful_sensor/ESPHome_patch.yaml copied and pasted. + +8. In the **substitutions** section of the edited yaml file, provide your device_name and (optionally) values for the other two variables. Save the file. + +9. A new tile should have appeared on the web page, titled with your device_name. Click the 3 dots on it, then on "install" and choose "wirelessly". The install is complete when coloured logs about WiFi signal appear in the window: then click "stop" to close the window. **Close the browser and stop the server process that began in step 2.** + +10. Go to your Home Assistant web dashboard. There may be a notification that a new device has been discovered. Click "configure" on it and enter the encryption key from step 5 (the key is also saved in .yaml). + +**If you are not prompted about the device discovery:** go to Settings > Devices & Services > Devices tab > "Add device". Search for ESPHome in the list, then for "Host" enter **.local** where is the name you chose. + +11. In Home Assistant go to Settings > Devices & Services > Devices tab, click on the newly added device and click "add to dashboard" under its sensors list. The list of data variables appears as a card on the dashboard/overview page. + +12. You can improve the appearance of the dashboard card, for example: edit the card to rename the two air quality values (remove the device_name from their display name), add a title to the card, remove variables, and reorder the list. You can also add data graphs, gauges, etc. + + +## Home Assistant POST 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. +Send sensor data to an installation of [Home Assistant](https://www.home-assistant.io) using HTTP requests. + +This code example is suitable for all supported MS430 hosts. If you are using ESP8266, ESP32 or Raspberry Pi Pico W, you can also use our [ESPHome example](#esphome-for-home-assistant). -Note: this was tested on Home Assistant OS v0.117.2 +These instructions assume that you already have a working Home Assistant installation. ### Setup @@ -431,20 +523,18 @@ Note: this was tested on Home Assistant OS v0.117.2 * 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 + * Go to Settings > Devices & Services > Entities tab * 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 + 1. Click the 3 dots at the top right corner and choose **Edit Dashboard** then **+ Add Card** + 2. On the "By entity" tab, search for your chosen SENSOR_NAME and select one or more data variables + 3. Pick a display card, e.g. **Entities**, **Gauge**, or **History graph** -* You can also view data graphs on the **History** page. +* You can also view data graphs on the Home Assistant **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. @@ -453,9 +543,9 @@ Note: this was tested on Home Assistant OS v0.117.2 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** +1. Go to Settings > Automations & Scenes +2. Click **+ create automation** to create a new automation +3. On the "New Automation" page, click the 3 dots at the top right corner and choose **Edit in YAML** 4. Delete everything in the text box and replace with: ``` trigger: @@ -470,38 +560,7 @@ This simple example shows how to generate a notification when the temperature go ``` 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. +6. Optional: click **run** to test it (the action is forced to run without the trigger condition). ## Graph viewer software @@ -512,15 +571,15 @@ The **graph viewer** uses a graphical interface to show environment data updatin 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. +There are two versions 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. + Runs only on Raspberry Pi 0/2/3/4 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). + Runs on multiple operating systems (windows, linux, mac) and uses serial over USB to get data from the MS430 sensor via a microcontroller board (e.g. Arduino, ESP8266, Raspberry Pi Pico etc). ### Package installation commands @@ -533,15 +592,16 @@ pip3 install pyqtgraph pyqt5 pyserial **Linux, including Raspberry Pi** ``` +sudo apt install python3-pyqt5 libatlas-base-dev libopenblas-dev pip3 install pyqtgraph pyserial -sudo apt install python3-pyqt5 ``` +Also on Raspberry Pi: you will need to complete the [first time Raspberry Pi setup](#first-time-raspberry-pi-setup) if not already done. **Extra steps for some Linux versions e.g. Ubuntu** -* Install pip3 by enabling the "Universe" software repository, then ```sudo apt install python3-pip``` +* If not already present, 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) +### Running graph_viewer_I2C.py (Raspberry Pi 0/2/3/4 only) 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``` @@ -549,7 +609,7 @@ sudo apt install python3-pyqt5 ### 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``` +2. Program the microcontroller board with either **cycle_readout.ino** or **on_demand_readout.ino**, with parameter ```printDataAsColumns = true``` and a cycle period of 3 seconds (or less). 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. @@ -569,19 +629,56 @@ The temperature is always measured by the MS430 as a Celsius value. The software * On Arduino - In file **Metriful_sensor.h** set the following: + In file **Metriful_sensor.h** un-comment the following line: ``` #define USE_FAHRENHEIT ``` +## Library and software versions + +The following versions were used for testing. + +### Arduino + +* Arduino IDE: 2.2.1 +* Raspberry Pi Pico/RP2040 by Earle F. Philhower board package: 3.6.0 +* ESP32 by Espressif Systems board package: 2.0.14 +* ESP8266 by ESP8266 Community board package: 3.1.2 +* Arduino SAMD board package: 1.8.13 +* WiFiNINA library: 1.8.14 + +### Python / Raspberry Pi + +* Raspberry Pi OS: 11 (bullseye) +* i2c-tools: 4.2-1+b1 +* python3-smbus: 4.2-1+b1 +* python3-rpi.gpio: 0.7.0-0.2+b1 +* Python version: 3.9.2 +* Jinja2: 2.11.3 +* psutil: 5.8.0 +* PyQt5: 5.15.2 +* pyqtgraph: 0.13.3 +* pyserial: 3.5b0 +* requests: 2.25.1 + +### Home Assistant / ESPHome + +* Home Assistant OS: 10.5 (RPi 3) +* ESPHome: 2023.9.3 + + ## 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: +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. + +A smoke alarm with the electronics removed makes an ideal, inexpensive case. Ensure that this cannot be mistaken for a functioning smoke alarm for safety reasons. + +The following tips may also help: * Fix the board as close as possible to the box wall but without having the sensors touching the wall. diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 0204a65..ca55296 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -6,7 +6,9 @@ This file lists solutions for some common problems. Please check it before openi ### Contents **[Standard checks](#standard-checks)**
+**[WiFi Access Point examples not starting](#wifi-access-point-examples-not-starting)**
**[ESP8266 problems](#esp8266-problems)**
+**[ESP32 problems](#esp32-problems)**
**[Arduino Nano 33 IoT problems](#arduino-nano-33-iot-problems)**
**[WiFi connection problems](#wifi-connection-problems)**
**[Particle sensor problems](#particle-sensor-problems)**
@@ -18,10 +20,17 @@ This file lists solutions for some common problems. Please check it before openi 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. +1. Check that you can run a simple program on your host system **without the MS430 board** e.g. blink and serial demos 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. +3. Check your software, library and board versions: for best reliability, use the versions listed in the [Readme](README.md#library-and-software-versions). +4. Remove all wire connections and re-wire carefully. +5. If you have edited the code, go back to the original version and ensure it still works. + + +## WiFi Access Point examples not starting + +* The Arduino web server examples, when the host is configured to be a WiFi Access Point, may fail to start immediately after programming the board. This seems to affect ESP8266 and Raspberry Pi Pico W, and is presumably due to the host board failing to reset after programming. +* Solution: simply power-cycle the host board (unplug and re-plug power). ## ESP8266 problems @@ -31,6 +40,13 @@ There are many different development boards which use the ESP8266 module. Some m The ESP8266 does not have a hardware I2C module, so any of the normal GPIO pins can be used for the I2C bus. +## ESP32 problems + +There are many different development boards which use the ESP32 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. + +After uploading a new program, the serial monitor may show a stream of nonsense characters instead of starting the program. Try pressing the board's EN/RESET button. + + ## 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. @@ -51,7 +67,7 @@ If WiFi connection never succeeds, check the following: ### 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. +* If using a separate 5V source, the source GND must be connected to both the host GND and the MS430 GND. ### Measured value fluctuates a lot * This is typical for particle sensors, especially the PPD42 @@ -71,7 +87,7 @@ 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. +* If the accuracy is low (0 or 1) after running for an hour, expose the sensor to polluted air for a few minutes. A solvent vapor such as from a marker pen or alcohol 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. diff --git a/User_guide.pdf b/User_guide.pdf index 0da2eb9..0d54ccb 100644 Binary files a/User_guide.pdf and b/User_guide.pdf differ diff --git a/pictures/tago.png b/pictures/tago.png index 64ead71..888fdc4 100644 Binary files a/pictures/tago.png and b/pictures/tago.png differ