├── Arduino ├── Examples │ ├── Home_Assistant │ │ └── Home_Assistant.ino │ ├── IFTTT │ │ └── IFTTT.ino │ ├── IoT_cloud_logging │ │ └── IoT_cloud_logging.ino │ ├── cycle_readout │ │ └── cycle_readout.ino │ ├── graph_web_server │ │ └── graph_web_server.ino │ ├── interrupts │ │ └── interrupts.ino │ ├── on_demand_readout │ │ └── on_demand_readout.ino │ ├── particle_sensor_toggle │ │ └── particle_sensor_toggle.ino │ ├── simple_read_T_H │ │ └── simple_read_T_H.ino │ ├── simple_read_sound │ │ └── simple_read_sound.ino │ └── web_server │ │ └── web_server.ino └── Metriful_Sensor │ ├── ESPHome_patch.yaml │ ├── MS430_ESPHome.yaml │ ├── Metriful_sensor.cpp │ ├── Metriful_sensor.h │ ├── WiFi_functions.cpp │ ├── WiFi_functions.h │ ├── esphome_components │ └── ms430 │ │ ├── __init__.py │ │ ├── ms430_esphome.cpp │ │ ├── ms430_esphome.h │ │ └── sensor.py │ ├── graph_web_page.h │ ├── host_pin_definitions.h │ ├── sensor_constants.h │ └── text_web_page.h ├── CHANGELOG.md ├── DISCLAIMER.txt ├── Datasheet.pdf ├── LICENSE.txt ├── Python ├── GraphViewer.py ├── Raspberry_Pi │ ├── Home_Assistant.py │ ├── IFTTT.py │ ├── IoT_cloud_logging.py │ ├── cycle_readout.py │ ├── graph_web_server.py │ ├── interrupts.py │ ├── log_data_to_file.py │ ├── on_demand_readout.py │ ├── particle_sensor_toggle.py │ ├── sensor_package │ │ ├── __init__.py │ │ ├── graph_web_page.html │ │ ├── sensor_constants.py │ │ ├── sensor_functions.py │ │ ├── servers.py │ │ └── text_web_page.html │ ├── simple_read_T_H.py │ ├── simple_read_sound.py │ └── web_server.py ├── graph_viewer_I2C.py └── graph_viewer_serial.py ├── README.md ├── TROUBLESHOOTING.md ├── User_guide.pdf └── pictures ├── graph_viewer.png ├── graph_web_server.png ├── group.png ├── home_assistant.png └── tago.png /Arduino/Examples/Home_Assistant/Home_Assistant.ino: -------------------------------------------------------------------------------- 1 | /* 2 | Home_Assistant.ino 3 | 4 | An example of using HTTP POST requests to send environment data 5 | from the Metriful MS430 to an installation of Home Assistant on 6 | your local WiFi network. This does not use ESPHome. 7 | 8 | This example is designed for the following WiFi enabled hosts: 9 | * Arduino Nano 33 IoT 10 | * Arduino MKR WiFi 1010 11 | * ESP8266 boards (e.g. Wemos D1, NodeMCU) 12 | * ESP32 boards (e.g. DOIT DevKit v1) 13 | * Raspberry Pi Pico W 14 | The non-Arduino boards can also use the separate ESPHome method. 15 | 16 | Data are sent at regular intervals over your WiFi network to Home 17 | Assistant and can be viewed on the dashboard or used to control 18 | home automation tasks. More setup information is provided in the 19 | Readme. 20 | 21 | Copyright 2020-2023 Metriful Ltd. 22 | Licensed under the MIT License - for further details see LICENSE.txt 23 | 24 | For code examples, datasheet and user guide, visit 25 | https://github.com/metriful/sensor 26 | */ 27 | 28 | #include 29 | #include 30 | 31 | ////////////////////////////////////////////////////////// 32 | // USER-EDITABLE SETTINGS 33 | 34 | // How often to read and report the data (every 3, 100 or 300 seconds) 35 | uint8_t cycle_period = CYCLE_PERIOD_100_S; 36 | 37 | // The details of your WiFi network: 38 | const char * SSID = "PUT WIFI NETWORK NAME HERE"; // network SSID (name) 39 | const char * password = "PUT WIFI PASSWORD HERE"; 40 | 41 | // Home Assistant settings 42 | 43 | // You must have already installed Home Assistant on a computer on your 44 | // network. Go to www.home-assistant.io for help on this. 45 | 46 | // Choose a unique name for this MS430 sensor board so you can identify it. 47 | // Variables in HA will have names like: SENSOR_NAME.temperature, etc. 48 | #define SENSOR_NAME "kitchen3" 49 | 50 | // Change this to the IP address of the computer running Home Assistant. 51 | // You can find this from the admin interface of your router. 52 | #define HOME_ASSISTANT_IP "192.168.1.152" 53 | 54 | // Security access token: the Readme explains how to get this 55 | #define LONG_LIVED_ACCESS_TOKEN "PASTE YOUR TOKEN HERE WITHIN QUOTES" 56 | 57 | // END OF USER-EDITABLE SETTINGS 58 | ////////////////////////////////////////////////////////// 59 | 60 | #if !defined(HAS_WIFI) 61 | #error ("This example program has been created for specific WiFi enabled hosts only.") 62 | #endif 63 | 64 | WiFiClient client; 65 | 66 | // Buffers for assembling http POST requests 67 | char postBuffer[500] = {0}; 68 | char fieldBuffer[200] = {0}; 69 | 70 | // Structs for data 71 | AirData_t airData = {0}; 72 | AirQualityData_t airQualityData = {0}; 73 | LightData_t lightData = {0}; 74 | ParticleData_t particleData = {0}; 75 | SoundData_t soundData = {0}; 76 | 77 | // Define the display attributes of data to send to Home Assistant. 78 | // The chosen name, unit and icon will appear in on the overview 79 | // dashboard in Home Assistant. The icons can be chosen at 80 | // https://pictogrammers.com/library/mdi/ 81 | // The attribute fields are: {name, unit, icon, decimal places} 82 | HA_Attributes_t pressure = {"Air pressure", "Pa", "weather-partly-rainy", 0}; 83 | HA_Attributes_t humidity = {"Humidity", "%", "cloud-percent", 1}; 84 | HA_Attributes_t illuminance = {"Illuminance", "lux", "white-balance-sunny", 2}; 85 | HA_Attributes_t white_light = {"White light level", "", "circle-outline", 0}; 86 | HA_Attributes_t soundLevel = {"Sound pressure level", "dBA", "microphone", 1}; 87 | HA_Attributes_t peakAmplitude = {"Peak sound amplitude", "mPa", "waveform", 2}; 88 | HA_Attributes_t AQI = {"Air Quality Index", "", "flower-tulip-outline", 1}; 89 | HA_Attributes_t AQ_assessment = {"Air quality", "", "flower-tulip-outline", 0}; 90 | HA_Attributes_t AQ_accuracy = {"Air quality accuracy", "", "magnify", 0}; 91 | HA_Attributes_t gas_resistance = {"Gas sensor resistance", OHM_SYMBOL, "scent", 0}; 92 | #if (PARTICLE_SENSOR == PARTICLE_SENSOR_PPD42) 93 | HA_Attributes_t particulates = {"Particle concentration", "ppL", "chart-bubble", 0}; 94 | #else 95 | HA_Attributes_t particulates = {"Particle concentration", SDS011_UNIT_SYMBOL, 96 | "chart-bubble", 2}; 97 | #endif 98 | #ifdef USE_FAHRENHEIT 99 | HA_Attributes_t temperature = {"Temperature", FAHRENHEIT_SYMBOL, "thermometer", 1}; 100 | #else 101 | HA_Attributes_t temperature = {"Temperature", CELSIUS_SYMBOL, "thermometer", 1}; 102 | #endif 103 | HA_Attributes_t soundBands[SOUND_FREQ_BANDS] = {{"SPL at 125 Hz", "dB", "sine-wave", 1}, 104 | {"SPL at 250 Hz", "dB", "sine-wave", 1}, 105 | {"SPL at 500 Hz", "dB", "sine-wave", 1}, 106 | {"SPL at 1000 Hz", "dB", "sine-wave", 1}, 107 | {"SPL at 2000 Hz", "dB", "sine-wave", 1}, 108 | {"SPL at 4000 Hz", "dB", "sine-wave", 1}}; 109 | 110 | 111 | void setup() 112 | { 113 | // Initialize the host's pins, set up the serial port and reset: 114 | SensorHardwareSetup(I2C_ADDRESS); 115 | 116 | connectToWiFi(SSID, password); 117 | 118 | // Apply settings to the MS430 and enter cycle mode 119 | uint8_t particleSensorCode = PARTICLE_SENSOR; 120 | TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensorCode, 1); 121 | TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); 122 | ready_assertion_event = false; 123 | TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); 124 | } 125 | 126 | 127 | void loop() 128 | { 129 | // Wait for the next new data release, indicated by a falling edge on READY 130 | while (!ready_assertion_event) 131 | { 132 | yield(); 133 | } 134 | ready_assertion_event = false; 135 | 136 | // Read data from the MS430 into the data structs. 137 | ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); 138 | ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, 139 | AIR_QUALITY_DATA_BYTES); 140 | ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); 141 | ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); 142 | ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); 143 | 144 | // Check that WiFi is still connected 145 | uint8_t wifiStatus = WiFi.status(); 146 | if (wifiStatus != WL_CONNECTED) 147 | { 148 | // There is a problem with the WiFi connection: attempt to reconnect. 149 | Serial.print("Wifi status: "); 150 | Serial.println(interpret_WiFi_status(wifiStatus)); 151 | connectToWiFi(SSID, password); 152 | ready_assertion_event = false; 153 | } 154 | 155 | uint8_t T_intPart = 0; 156 | uint8_t T_fractionalPart = 0; 157 | bool isPositive = true; 158 | getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); 159 | 160 | // Send data to Home Assistant 161 | sendNumericData(&temperature, (uint32_t) T_intPart, T_fractionalPart, isPositive); 162 | sendNumericData(&pressure, (uint32_t) airData.P_Pa, 0, true); 163 | sendNumericData(&humidity, (uint32_t) airData.H_pc_int, airData.H_pc_fr_1dp, true); 164 | sendNumericData(&gas_resistance, airData.G_ohm, 0, true); 165 | sendNumericData(&illuminance, (uint32_t) lightData.illum_lux_int, 166 | lightData.illum_lux_fr_2dp, true); 167 | sendNumericData(&white_light, (uint32_t) lightData.white, 0, true); 168 | sendNumericData(&soundLevel, (uint32_t) soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp, true); 169 | sendNumericData(&peakAmplitude, (uint32_t) soundData.peak_amp_mPa_int, 170 | soundData.peak_amp_mPa_fr_2dp, true); 171 | sendNumericData(&AQI, (uint32_t) airQualityData.AQI_int, airQualityData.AQI_fr_1dp, true); 172 | if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) 173 | { 174 | sendNumericData(&particulates, (uint32_t) particleData.concentration_int, 175 | particleData.concentration_fr_2dp, true); 176 | } 177 | sendTextData(&AQ_assessment, interpret_AQI_value(airQualityData.AQI_int)); 178 | sendTextData(&AQ_accuracy, interpret_AQI_accuracy_brief(airQualityData.AQI_accuracy)); 179 | for (uint8_t i = 0; i < SOUND_FREQ_BANDS; i++) 180 | { 181 | sendNumericData(&soundBands[i], (uint32_t) soundData.SPL_bands_dB_int[i], 182 | soundData.SPL_bands_dB_fr_1dp[i], true); 183 | } 184 | } 185 | 186 | // Send numeric data with specified sign, integer and fractional parts 187 | void sendNumericData(const HA_Attributes_t * attributes, uint32_t valueInteger, 188 | uint8_t valueDecimal, bool isPositive) 189 | { 190 | char valueText[20] = {0}; 191 | const char * sign = isPositive ? "" : "-"; 192 | switch (attributes->decimalPlaces) 193 | { 194 | case 0: 195 | default: 196 | snprintf(valueText, sizeof valueText, "%s%" PRIu32, sign, valueInteger); 197 | break; 198 | case 1: 199 | snprintf(valueText, sizeof valueText, "%s%" PRIu32 ".%u", sign, 200 | valueInteger, valueDecimal); 201 | break; 202 | case 2: 203 | snprintf(valueText, sizeof valueText, "%s%" PRIu32 ".%02u", sign, 204 | valueInteger, valueDecimal); 205 | break; 206 | } 207 | http_POST_Home_Assistant(attributes, valueText); 208 | } 209 | 210 | // Send a text string: must have quotation marks added. 211 | void sendTextData(const HA_Attributes_t * attributes, const char * valueText) 212 | { 213 | char quotedText[20] = {0}; 214 | snprintf(quotedText, sizeof quotedText, "\"%s\"", valueText); 215 | http_POST_Home_Assistant(attributes, quotedText); 216 | } 217 | 218 | // Send the data to Home Assistant as an HTTP POST request. 219 | void http_POST_Home_Assistant(const HA_Attributes_t * attributes, const char * valueText) 220 | { 221 | client.stop(); 222 | if (client.connect(HOME_ASSISTANT_IP, 8123)) 223 | { 224 | // Form the entity_id from the variable name but replace spaces with underscores 225 | snprintf(fieldBuffer, sizeof fieldBuffer, SENSOR_NAME ".%s", attributes->name); 226 | for (uint8_t i = 0; i < strlen(fieldBuffer); i++) 227 | { 228 | if (fieldBuffer[i] == ' ') 229 | { 230 | fieldBuffer[i] = '_'; 231 | } 232 | } 233 | snprintf(postBuffer, sizeof postBuffer, "POST /api/states/%s HTTP/1.1", fieldBuffer); 234 | client.println(postBuffer); 235 | client.println("Host: " HOME_ASSISTANT_IP ":8123"); 236 | client.println("Content-Type: application/json"); 237 | client.println("Authorization: Bearer " LONG_LIVED_ACCESS_TOKEN); 238 | 239 | // Assemble the JSON content string: 240 | snprintf(postBuffer, sizeof postBuffer, "{\"state\":%s,\"attributes\":" 241 | "{\"unit_of_measurement\":\"%s\",\"friendly_name\":\"%s\",\"icon\":\"mdi:%s\"}}", 242 | valueText, attributes->unit, attributes->name, attributes->icon); 243 | 244 | snprintf(fieldBuffer, sizeof fieldBuffer, "Content-Length: %u", strlen(postBuffer)); 245 | client.println(fieldBuffer); 246 | client.println(); 247 | client.print(postBuffer); 248 | } 249 | else 250 | { 251 | Serial.println("Client connection failed."); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Arduino/Examples/IFTTT/IFTTT.ino: -------------------------------------------------------------------------------- 1 | /* 2 | IFTTT.ino 3 | 4 | Example code for sending data from the Metriful MS430 to IFTTT.com 5 | 6 | This example is designed for the following WiFi enabled hosts: 7 | * Arduino Nano 33 IoT 8 | * Arduino MKR WiFi 1010 9 | * ESP8266 boards (e.g. Wemos D1, NodeMCU) 10 | * ESP32 boards (e.g. DOIT DevKit v1) 11 | * Raspberry Pi Pico W 12 | 13 | Environmental data values are periodically measured and compared with 14 | a set of user-defined thresholds. If any values go outside the allowed 15 | ranges, an HTTP POST request is sent to IFTTT.com, triggering an alert 16 | email to your inbox, with customizable text. 17 | This example requires a WiFi network and internet connection. 18 | 19 | Copyright 2020-2023 Metriful Ltd. 20 | Licensed under the MIT License - for further details see LICENSE.txt 21 | 22 | For code examples, datasheet and user guide, visit 23 | https://github.com/metriful/sensor 24 | */ 25 | 26 | #include 27 | #include 28 | #include 29 | 30 | ////////////////////////////////////////////////////////// 31 | // USER-EDITABLE SETTINGS 32 | 33 | // The details of the WiFi network that we will connect to: 34 | const char * SSID = "PUT WIFI NETWORK NAME HERE"; // network SSID (name) 35 | const char * password = "PUT WIFI PASSWORD HERE"; 36 | 37 | // Define the details of variables for monitoring. 38 | // The seven fields are: 39 | // {Name, measurement unit, high threshold, low threshold, 40 | // initial inactive cycles (2), advice when high, advice when low} 41 | ThresholdSetting_t humiditySetting = {"humidity", "%", 60, 30, 2, 42 | "Reduce moisture sources.", "Start the humidifier."}; 43 | ThresholdSetting_t airQualitySetting = {"air quality index", "", 150, -1, 2, 44 | "Improve ventilation and reduce sources of VOCs.", ""}; 45 | // Change these values if Fahrenheit output temperature is selected in Metriful_sensor.h 46 | ThresholdSetting_t temperatureSetting = {"temperature", CELSIUS_SYMBOL, 28, 18, 2, 47 | "Turn on the fan.", "Turn on the heating."}; 48 | 49 | // An inactive period follows each alert, during which the same alert 50 | // will not be generated again - this prevents too many emails/alerts. 51 | // Choose the period as a number of readout cycles (each 5 minutes) 52 | // e.g. for a 1 hour period, choose inactiveWaitCycles = 12 53 | uint16_t inactiveWaitCycles = 12; 54 | 55 | // IFTTT.com settings 56 | 57 | // You must set up a free account on IFTTT.com and create a Webhooks 58 | // applet before using this example. This is explained further in the 59 | // readme. 60 | 61 | #define WEBHOOKS_KEY "PASTE YOUR KEY HERE WITHIN QUOTES" 62 | #define IFTTT_EVENT_NAME "PASTE YOUR EVENT NAME HERE WITHIN QUOTES" 63 | 64 | // END OF USER-EDITABLE SETTINGS 65 | ////////////////////////////////////////////////////////// 66 | 67 | #if !defined(HAS_WIFI) 68 | #error ("This example program has been created for specific WiFi enabled hosts only.") 69 | #endif 70 | 71 | // Measure the environment data every 300 seconds (5 minutes). This is 72 | // recommended for long-term monitoring. 73 | uint8_t cycle_period = CYCLE_PERIOD_300_S; 74 | 75 | WiFiClient client; 76 | 77 | // Buffers for assembling the http POST requests 78 | char postBuffer[400] = {0}; 79 | char fieldBuffer[120] = {0}; 80 | 81 | // Structs for data 82 | AirData_t airData = {0}; 83 | AirQualityData_t airQualityData = {0}; 84 | 85 | 86 | void setup() 87 | { 88 | // Initialize the host's pins, set up the serial port and reset: 89 | SensorHardwareSetup(I2C_ADDRESS); 90 | 91 | connectToWiFi(SSID, password); 92 | 93 | // Enter cycle mode 94 | TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); 95 | ready_assertion_event = false; 96 | TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); 97 | } 98 | 99 | 100 | void loop() 101 | { 102 | // Wait for the next new data release, indicated by a falling edge on READY 103 | while (!ready_assertion_event) 104 | { 105 | yield(); 106 | } 107 | ready_assertion_event = false; 108 | 109 | // Read the air data and air quality data 110 | ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); 111 | ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, 112 | AIR_QUALITY_DATA_BYTES); 113 | 114 | // Check that WiFi is still connected 115 | uint8_t wifiStatus = WiFi.status(); 116 | if (wifiStatus != WL_CONNECTED) 117 | { 118 | // There is a problem with the WiFi connection: attempt to reconnect. 119 | Serial.print("Wifi status: "); 120 | Serial.println(interpret_WiFi_status(wifiStatus)); 121 | connectToWiFi(SSID, password); 122 | ready_assertion_event = false; 123 | } 124 | 125 | // Process temperature value and convert if using Fahrenheit 126 | float temperature = convertEncodedTemperatureToFloat(airData.T_C_int_with_sign, 127 | airData.T_C_fr_1dp); 128 | #ifdef USE_FAHRENHEIT 129 | temperature = convertCtoF(temperature); 130 | #endif 131 | 132 | // Send an alert to IFTTT if a variable is outside the allowed range 133 | // Just use the integer parts of values (ignore fractional parts) 134 | checkData(&temperatureSetting, (int32_t) temperature); 135 | checkData(&humiditySetting, (int32_t) airData.H_pc_int); 136 | checkData(&airQualitySetting, (int32_t) airQualityData.AQI_int); 137 | } 138 | 139 | 140 | // Compare the measured value to the chosen thresholds and create an 141 | // alert if the value is outside the allowed range. After triggering 142 | // an alert, it cannot be re-triggered within the chosen number of cycles. 143 | void checkData(ThresholdSetting_t * setting, int32_t value) 144 | { 145 | // Count down to when the monitoring is active again: 146 | if (setting->inactiveCount > 0) 147 | { 148 | setting->inactiveCount--; 149 | } 150 | 151 | if ((value > setting->thresHigh) && (setting->inactiveCount == 0)) 152 | { 153 | // The variable is above the high threshold 154 | setting->inactiveCount = inactiveWaitCycles; 155 | sendAlert(setting, value, true); 156 | } 157 | else if ((value < setting->thresLow) && (setting->inactiveCount == 0)) 158 | { 159 | // The variable is below the low threshold 160 | setting->inactiveCount = inactiveWaitCycles; 161 | sendAlert(setting, value, false); 162 | } 163 | } 164 | 165 | // Create part of the HTTP response in fieldBuffer and append 166 | // it to postBuffer. 167 | void appendResponse(const char * format, ...) 168 | { 169 | va_list args; 170 | va_start(args, format); 171 | vsnprintf(fieldBuffer, sizeof fieldBuffer, format, args); 172 | va_end(args); 173 | strncat(postBuffer, fieldBuffer, (sizeof postBuffer) - strlen(postBuffer) - 1); 174 | } 175 | 176 | // Send an alert message to IFTTT.com as an HTTP POST request. 177 | // isOverHighThres = true means (value > thresHigh) 178 | // isOverHighThres = false means (value < thresLow) 179 | void sendAlert(ThresholdSetting_t * setting, int32_t value, bool isOverHighThres) 180 | { 181 | client.stop(); 182 | if (client.connect("maker.ifttt.com", 80)) 183 | { 184 | client.println("POST /trigger/" IFTTT_EVENT_NAME "/with/key/" WEBHOOKS_KEY " HTTP/1.1"); 185 | client.println("Host: maker.ifttt.com"); 186 | client.println("Content-Type: application/json"); 187 | 188 | snprintf(fieldBuffer, sizeof fieldBuffer, "The %s is too %s.", 189 | setting->variableName, isOverHighThres ? "high" : "low"); 190 | snprintf(postBuffer, sizeof postBuffer, "{\"value1\":\"%s\",", fieldBuffer); 191 | Serial.print("Sending new alert to IFTTT: "); 192 | Serial.println(fieldBuffer); 193 | 194 | appendResponse("\"value2\":\"The measurement was %" PRId32 " %s\"", 195 | value, setting->measurementUnit); 196 | appendResponse(",\"value3\":\"%s\"}", 197 | isOverHighThres ? setting->adviceHigh : setting->adviceLow); 198 | 199 | snprintf(fieldBuffer, sizeof fieldBuffer, "Content-Length: %u", strlen(postBuffer)); 200 | client.println(fieldBuffer); 201 | client.println(); 202 | client.print(postBuffer); 203 | } 204 | else 205 | { 206 | Serial.println("Client connection failed."); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Arduino/Examples/IoT_cloud_logging/IoT_cloud_logging.ino: -------------------------------------------------------------------------------- 1 | /* 2 | IoT_cloud_logging.ino 3 | 4 | Example IoT data logging code for the Metriful MS430. 5 | 6 | This example is designed for the following WiFi enabled hosts: 7 | * Arduino Nano 33 IoT 8 | * Arduino MKR WiFi 1010 9 | * ESP8266 boards (e.g. Wemos D1, NodeMCU) 10 | * ESP32 boards (e.g. DOIT DevKit v1) 11 | * Raspberry Pi Pico W 12 | 13 | Environmental data values are measured and logged to an internet 14 | cloud account every 100 seconds, using a WiFi network. The example 15 | gives the choice of using either the Tago.io or Thingspeak.com 16 | clouds – both of these offer a free account for low data rates. 17 | 18 | Copyright 2020-2023 Metriful Ltd. 19 | Licensed under the MIT License - for further details see LICENSE.txt 20 | 21 | For code examples, datasheet and user guide, visit 22 | https://github.com/metriful/sensor 23 | */ 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | ////////////////////////////////////////////////////////// 30 | // USER-EDITABLE SETTINGS 31 | 32 | // How often to read and log data (every 100 or 300 seconds) 33 | // Note: due to data rate limits on free cloud services, this should 34 | // be set to 100 or 300 seconds, not 3 seconds. 35 | uint8_t cycle_period = CYCLE_PERIOD_100_S; 36 | 37 | // The details of the WiFi network that we will connect to: 38 | const char * SSID = "PUT WIFI NETWORK NAME HERE"; // network SSID (name) 39 | const char * password = "PUT WIFI PASSWORD HERE"; 40 | 41 | // IoT cloud settings 42 | // This example uses the free IoT cloud hosting services provided 43 | // by Tago.io or Thingspeak.com 44 | // An account must have been set up with the relevant cloud provider 45 | // and a WiFi internet connection must exist. See the readme for 46 | // more information. 47 | 48 | // Choose which provider to use 49 | bool useTagoCloud = true; 50 | // To use the ThingSpeak cloud, set: useTagoCloud=false 51 | 52 | // The chosen account's key/token must be put into the relevant define below. 53 | #define TAGO_DEVICE_TOKEN_STRING "PASTE YOUR TOKEN HERE WITHIN QUOTES" 54 | // or 55 | #define THINGSPEAK_API_KEY_STRING "PASTE YOUR API KEY HERE WITHIN QUOTES" 56 | 57 | // END OF USER-EDITABLE SETTINGS 58 | ////////////////////////////////////////////////////////// 59 | 60 | #if !defined(HAS_WIFI) 61 | #error ("This example program has been created for specific WiFi enabled hosts only.") 62 | #endif 63 | 64 | WiFiClient client; 65 | 66 | // Buffers for assembling http POST requests 67 | char postBuffer[600] = {0}; 68 | char fieldBuffer[70] = {0}; 69 | char valueBuffer[20] = {0}; 70 | 71 | typedef enum {FIRST, LAST, OTHER} FirstLast; 72 | 73 | // Structs for data 74 | AirData_t airData = {0}; 75 | AirQualityData_t airQualityData = {0}; 76 | LightData_t lightData = {0}; 77 | ParticleData_t particleData = {0}; 78 | SoundData_t soundData = {0}; 79 | 80 | void setup() 81 | { 82 | // Initialize the host's pins, set up the serial port and reset: 83 | SensorHardwareSetup(I2C_ADDRESS); 84 | 85 | connectToWiFi(SSID, password); 86 | 87 | // Apply chosen settings to the MS430 88 | uint8_t particleSensor = PARTICLE_SENSOR; 89 | TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); 90 | TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); 91 | 92 | // Enter cycle mode 93 | ready_assertion_event = false; 94 | TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); 95 | } 96 | 97 | 98 | void loop() 99 | { 100 | // Wait for the next new data release, indicated by a falling edge on READY 101 | while (!ready_assertion_event) 102 | { 103 | yield(); 104 | } 105 | ready_assertion_event = false; 106 | 107 | // Read data from the MS430 into the data structs. 108 | 109 | // Air data 110 | // You can enable Fahrenheit temperature unit in Metriful_sensor.h 111 | ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); 112 | 113 | /* Air quality data 114 | The initial self-calibration of the air quality data may take several 115 | minutes to complete. During this time the accuracy parameter is zero 116 | and the data values are not valid. 117 | */ 118 | ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, 119 | AIR_QUALITY_DATA_BYTES); 120 | 121 | // Light data 122 | ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); 123 | 124 | // Sound data 125 | ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); 126 | 127 | /* Particle data 128 | This requires the connection of a particulate sensor (invalid 129 | values will be obtained if this sensor is not present). 130 | Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h 131 | Also note that, due to the low pass filtering used, the 132 | particle data become valid after an initial initialization 133 | period of approximately one minute. 134 | */ 135 | if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) 136 | { 137 | ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); 138 | } 139 | 140 | // Check that WiFi is still connected 141 | uint8_t wifiStatus = WiFi.status(); 142 | if (wifiStatus != WL_CONNECTED) 143 | { 144 | // There is a problem with the WiFi connection: attempt to reconnect. 145 | Serial.print("Wifi status: "); 146 | Serial.println(interpret_WiFi_status(wifiStatus)); 147 | connectToWiFi(SSID, password); 148 | ready_assertion_event = false; 149 | } 150 | 151 | // Send data to the cloud 152 | if (useTagoCloud) 153 | { 154 | http_POST_data_Tago_cloud(); 155 | } 156 | else 157 | { 158 | http_POST_data_Thingspeak_cloud(); 159 | } 160 | } 161 | 162 | 163 | /* For both example cloud providers, the following quantities will be sent: 164 | 1 Temperature (C or F) 165 | 2 Pressure/Pa 166 | 3 Humidity/% 167 | 4 Air quality index 168 | 5 bVOC/ppm 169 | 6 SPL/dBA 170 | 7 Illuminance/lux 171 | 8 Particle concentration 172 | 173 | Additionally, for Tago, the following are sent: 174 | 9 Air Quality Assessment summary (Good, Bad, etc.) 175 | 10 Peak sound amplitude / mPa 176 | */ 177 | 178 | // Add the field for a single data variable to the Tago HTTP response in 179 | // the postBuffer. 180 | void addTagoVariable(FirstLast firstLast, const char * variableName, 181 | const char * valueFormat, ...) 182 | { 183 | va_list args; 184 | va_start(args, valueFormat); 185 | vsnprintf(valueBuffer, sizeof valueBuffer, valueFormat, args); 186 | va_end(args); 187 | const char * fieldFormat; 188 | switch (firstLast) 189 | { 190 | case FIRST: 191 | postBuffer[0] = 0; 192 | fieldFormat = "[{\"variable\":\"%s\",\"value\":%s},"; 193 | break; 194 | case LAST: 195 | fieldFormat = "{\"variable\":\"%s\",\"value\":%s}]"; 196 | break; 197 | case OTHER: default: 198 | fieldFormat = "{\"variable\":\"%s\",\"value\":%s},"; 199 | } 200 | snprintf(fieldBuffer, sizeof fieldBuffer, fieldFormat, variableName, valueBuffer); 201 | strncat(postBuffer, fieldBuffer, (sizeof postBuffer) - strlen(postBuffer) - 1); 202 | } 203 | 204 | // Assemble the data into the required format, then send it to the 205 | // Tago.io cloud as an HTTP POST request. 206 | void http_POST_data_Tago_cloud(void) 207 | { 208 | client.stop(); 209 | if (client.connect("api.tago.io", 80)) 210 | { 211 | client.println("POST /data HTTP/1.1"); 212 | client.println("Host: api.tago.io"); 213 | client.println("Content-Type: application/json"); 214 | client.println("Device-Token: " TAGO_DEVICE_TOKEN_STRING); 215 | 216 | uint8_t T_intPart = 0; 217 | uint8_t T_fractionalPart = 0; 218 | bool isPositive = true; 219 | getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); 220 | 221 | addTagoVariable(FIRST, "temperature", "%s%u.%u", isPositive ? "" : "-", T_intPart, 222 | T_fractionalPart); 223 | addTagoVariable(OTHER, "pressure", "%" PRIu32, airData.P_Pa); 224 | addTagoVariable(OTHER, "humidity", "%u.%u", airData.H_pc_int, airData.H_pc_fr_1dp); 225 | addTagoVariable(OTHER, "aqi", "%u.%u", airQualityData.AQI_int, airQualityData.AQI_fr_1dp); 226 | addTagoVariable(OTHER, "aqi_string", "\"%s\"", interpret_AQI_value(airQualityData.AQI_int)); 227 | addTagoVariable(OTHER, "bvoc", "%u.%02u", airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); 228 | addTagoVariable(OTHER, "spl", "%u.%u", soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); 229 | addTagoVariable(OTHER, "peak_amp", "%u.%02u", soundData.peak_amp_mPa_int, 230 | soundData.peak_amp_mPa_fr_2dp); 231 | addTagoVariable(OTHER, "particulates", "%u.%02u", particleData.concentration_int, 232 | particleData.concentration_fr_2dp); 233 | addTagoVariable(LAST, "illuminance", "%u.%02u", lightData.illum_lux_int, 234 | lightData.illum_lux_fr_2dp); 235 | 236 | snprintf(fieldBuffer, sizeof fieldBuffer, "Content-Length: %u", strlen(postBuffer)); 237 | client.println(fieldBuffer); 238 | client.println(); 239 | client.print(postBuffer); 240 | } 241 | else 242 | { 243 | Serial.println("Client connection failed."); 244 | } 245 | } 246 | 247 | // Add the field for a single data variable to the Thingspeak HTTP 248 | // response in the postBuffer. 249 | void addThingspeakVariable(uint8_t fieldNumber, const char * valueFormat, ...) 250 | { 251 | va_list args; 252 | va_start(args, valueFormat); 253 | vsnprintf(valueBuffer, sizeof valueBuffer, valueFormat, args); 254 | va_end(args); 255 | snprintf(fieldBuffer, sizeof fieldBuffer, "&field%u=%s", fieldNumber, valueBuffer); 256 | strncat(postBuffer, fieldBuffer, (sizeof postBuffer) - strlen(postBuffer) - 1); 257 | } 258 | 259 | // Assemble the data into the required format, then send it to the 260 | // Thingspeak.com cloud as an HTTP POST request. 261 | void http_POST_data_Thingspeak_cloud(void) 262 | { 263 | client.stop(); 264 | if (client.connect("api.thingspeak.com", 80)) 265 | { 266 | client.println("POST /update HTTP/1.1"); 267 | client.println("Host: api.thingspeak.com"); 268 | client.println("Content-Type: application/x-www-form-urlencoded"); 269 | 270 | uint8_t T_intPart = 0; 271 | uint8_t T_fractionalPart = 0; 272 | bool isPositive = true; 273 | getTemperature(&airData, &T_intPart, &T_fractionalPart, &isPositive); 274 | 275 | snprintf(postBuffer, sizeof postBuffer, "%s", "api_key=" THINGSPEAK_API_KEY_STRING); 276 | addThingspeakVariable(1, "%s%u.%u", isPositive ? "" : "-", T_intPart, T_fractionalPart); 277 | addThingspeakVariable(2, "%" PRIu32, airData.P_Pa); 278 | addThingspeakVariable(3, "%u.%u", airData.H_pc_int, airData.H_pc_fr_1dp); 279 | addThingspeakVariable(4, "%u.%u", airQualityData.AQI_int, airQualityData.AQI_fr_1dp); 280 | addThingspeakVariable(5, "%u.%02u", airQualityData.bVOC_int, airQualityData.bVOC_fr_2dp); 281 | addThingspeakVariable(6, "%u.%u", soundData.SPL_dBA_int, soundData.SPL_dBA_fr_1dp); 282 | addThingspeakVariable(7, "%u.%02u", lightData.illum_lux_int, lightData.illum_lux_fr_2dp); 283 | addThingspeakVariable(8, "%u.%02u", particleData.concentration_int, 284 | particleData.concentration_fr_2dp); 285 | snprintf(fieldBuffer, sizeof fieldBuffer, "Content-Length: %u", strlen(postBuffer)); 286 | client.println(fieldBuffer); 287 | client.println(); 288 | client.print(postBuffer); 289 | } 290 | else 291 | { 292 | Serial.println("Client connection failed."); 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /Arduino/Examples/cycle_readout/cycle_readout.ino: -------------------------------------------------------------------------------- 1 | /* 2 | cycle_readout.ino 3 | 4 | Example code for using the Metriful MS430 in cycle mode. 5 | 6 | Continually measures and displays all environment data in 7 | a repeating cycle. User can choose from a cycle time period 8 | of 3, 100, or 300 seconds. View the output in the Serial Monitor. 9 | 10 | The measurements can be displayed as either labeled text, or as 11 | simple columns of numbers. 12 | 13 | Copyright 2020-2023 Metriful Ltd. 14 | Licensed under the MIT License - for further details see LICENSE.txt 15 | 16 | For code examples, datasheet and user guide, visit 17 | https://github.com/metriful/sensor 18 | */ 19 | 20 | #include 21 | 22 | ////////////////////////////////////////////////////////// 23 | // USER-EDITABLE SETTINGS 24 | 25 | // How often to read data (every 3, 100, or 300 seconds) 26 | uint8_t cycle_period = CYCLE_PERIOD_3_S; 27 | 28 | // How to print the data over the serial port. 29 | // If printDataAsColumns = true, data are columns of numbers, useful 30 | // to copy/paste to a spreadsheet application. Otherwise, data are 31 | // printed with explanatory labels and units. 32 | bool printDataAsColumns = false; 33 | 34 | // END OF USER-EDITABLE SETTINGS 35 | ////////////////////////////////////////////////////////// 36 | 37 | // Structs for data 38 | AirData_t airData = {0}; 39 | AirQualityData_t airQualityData = {0}; 40 | LightData_t lightData = {0}; 41 | SoundData_t soundData = {0}; 42 | ParticleData_t particleData = {0}; 43 | 44 | 45 | void setup() 46 | { 47 | // Initialize the host pins, set up the serial port and reset: 48 | SensorHardwareSetup(I2C_ADDRESS); 49 | 50 | // Apply chosen settings to the MS430 51 | uint8_t particleSensor = PARTICLE_SENSOR; 52 | TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); 53 | TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cycle_period, 1); 54 | 55 | // Wait for the serial port to be ready, for displaying the output 56 | while (!Serial) 57 | { 58 | yield(); 59 | } 60 | 61 | Serial.println("Entering cycle mode and waiting for data."); 62 | ready_assertion_event = false; 63 | TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); 64 | } 65 | 66 | 67 | void loop() 68 | { 69 | // Wait for the next new data release, indicated by a falling edge on READY 70 | while (!ready_assertion_event) 71 | { 72 | yield(); 73 | } 74 | ready_assertion_event = false; 75 | 76 | // Read data from the MS430 into the data structs. 77 | 78 | // Air data 79 | // Choose output temperature unit (C or F) in Metriful_sensor.h 80 | airData = getAirData(I2C_ADDRESS); 81 | 82 | /* Air quality data 83 | The initial self-calibration of the air quality data may take several 84 | minutes to complete. During this time the accuracy parameter is zero 85 | and the data values are not valid. 86 | */ 87 | airQualityData = getAirQualityData(I2C_ADDRESS); 88 | 89 | // Light data 90 | lightData = getLightData(I2C_ADDRESS); 91 | 92 | // Sound data 93 | soundData = getSoundData(I2C_ADDRESS); 94 | 95 | /* Particle data 96 | This requires the connection of a particulate sensor (invalid 97 | values will be obtained if this sensor is not present). 98 | Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h 99 | Also note that, due to the low pass filtering used, the 100 | particle data become valid after an initial initialization 101 | period of approximately one minute. 102 | */ 103 | if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) 104 | { 105 | particleData = getParticleData(I2C_ADDRESS); 106 | } 107 | 108 | // Print all data to the serial port 109 | printAirData(&airData, printDataAsColumns); 110 | printAirQualityData(&airQualityData, printDataAsColumns); 111 | printLightData(&lightData, printDataAsColumns); 112 | printSoundData(&soundData, printDataAsColumns); 113 | if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) 114 | { 115 | printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR); 116 | } 117 | Serial.println(); 118 | } 119 | -------------------------------------------------------------------------------- /Arduino/Examples/interrupts/interrupts.ino: -------------------------------------------------------------------------------- 1 | /* 2 | interrupts.ino 3 | 4 | Example code for using the Metriful MS430 interrupt outputs. 5 | 6 | Light and sound interrupts are configured and the program then 7 | waits forever. When an interrupt occurs, a message prints over 8 | the serial port, the interrupt is cleared (if set to latch type), 9 | and the program returns to waiting. 10 | View the output in the Serial Monitor. 11 | 12 | Copyright 2020-2023 Metriful Ltd. 13 | Licensed under the MIT License - for further details see LICENSE.txt 14 | 15 | For code examples, datasheet and user guide, visit 16 | https://github.com/metriful/sensor 17 | */ 18 | 19 | #include 20 | 21 | ////////////////////////////////////////////////////////// 22 | // USER-EDITABLE SETTINGS 23 | 24 | // Light level interrupt settings 25 | 26 | bool enableLightInterrupts = true; 27 | uint8_t light_int_type = LIGHT_INT_TYPE_LATCH; 28 | // Choose the interrupt polarity: trigger when level rises above 29 | // threshold (positive), or when level falls below threshold (negative). 30 | uint8_t light_int_polarity = LIGHT_INT_POL_POSITIVE; 31 | uint16_t light_int_thres_lux_i = 100; 32 | uint8_t light_int_thres_lux_f2dp = 50; 33 | // The interrupt threshold value in lux units can be fractional and is formed as: 34 | // threshold = light_int_thres_lux_i + (light_int_thres_lux_f2dp/100) 35 | // E.g. for a light threshold of 56.12 lux, set: 36 | // light_int_thres_lux_i = 56 37 | // light_int_thres_lux_f2dp = 12 38 | 39 | // Sound level interrupt settings 40 | 41 | bool enableSoundInterrupts = true; 42 | uint8_t sound_int_type = SOUND_INT_TYPE_LATCH; 43 | uint16_t sound_thres_mPa = 100; 44 | 45 | // END OF USER-EDITABLE SETTINGS 46 | ////////////////////////////////////////////////////////// 47 | 48 | uint8_t transmit_buffer[1] = {0}; 49 | 50 | void setup() 51 | { 52 | // Initialize the host pins, set up the serial port and reset 53 | SensorHardwareSetup(I2C_ADDRESS); 54 | 55 | // check that the chosen light threshold is a valid value 56 | if (light_int_thres_lux_i > MAX_LUX_VALUE) 57 | { 58 | Serial.println("The chosen light interrupt threshold exceeds the maximum allowed value."); 59 | while (true) 60 | { 61 | yield(); 62 | } 63 | } 64 | 65 | if ((!enableSoundInterrupts) && (!enableLightInterrupts)) 66 | { 67 | Serial.println("No interrupts have been selected."); 68 | while (true) 69 | { 70 | yield(); 71 | } 72 | } 73 | 74 | if (enableSoundInterrupts) 75 | { 76 | // Set the interrupt type (latch or comparator) 77 | transmit_buffer[0] = sound_int_type; 78 | TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_TYPE_REG, transmit_buffer, 1); 79 | 80 | // Set the threshold 81 | setSoundInterruptThreshold(I2C_ADDRESS, sound_thres_mPa); 82 | 83 | // Enable the interrupt 84 | transmit_buffer[0] = ENABLED; 85 | TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_ENABLE_REG, transmit_buffer, 1); 86 | } 87 | 88 | if (enableLightInterrupts) 89 | { 90 | // Set the interrupt type (latch or comparator) 91 | transmit_buffer[0] = light_int_type; 92 | TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_TYPE_REG, transmit_buffer, 1); 93 | 94 | // Set the threshold 95 | setLightInterruptThreshold(I2C_ADDRESS, light_int_thres_lux_i, light_int_thres_lux_f2dp); 96 | 97 | // Set the interrupt polarity 98 | transmit_buffer[0] = light_int_polarity; 99 | TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_POLARITY_REG, transmit_buffer, 1); 100 | 101 | // Enable the interrupt 102 | transmit_buffer[0] = ENABLED; 103 | TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_ENABLE_REG, transmit_buffer, 1); 104 | } 105 | 106 | // Wait for the serial port to be ready, for displaying the output 107 | while (!Serial) 108 | { 109 | yield(); 110 | } 111 | 112 | Serial.println("Waiting for interrupts."); 113 | Serial.println(); 114 | } 115 | 116 | 117 | void loop() 118 | { 119 | 120 | // Check whether a light interrupt has occurred 121 | if ((digitalRead(L_INT_PIN) == LOW) && enableLightInterrupts) 122 | { 123 | Serial.println("LIGHT INTERRUPT."); 124 | if (light_int_type == LIGHT_INT_TYPE_LATCH) 125 | { 126 | // Latch type interrupts remain set until cleared by command 127 | TransmitI2C(I2C_ADDRESS, LIGHT_INTERRUPT_CLR_CMD, 0, 0); 128 | } 129 | } 130 | 131 | // Check whether a sound interrupt has occurred 132 | if ((digitalRead(S_INT_PIN) == LOW) && enableSoundInterrupts) 133 | { 134 | Serial.println("SOUND INTERRUPT."); 135 | if (sound_int_type == SOUND_INT_TYPE_LATCH) 136 | { 137 | // Latch type interrupts remain set until cleared by command 138 | TransmitI2C(I2C_ADDRESS, SOUND_INTERRUPT_CLR_CMD, 0, 0); 139 | } 140 | } 141 | 142 | delay(500); 143 | } 144 | -------------------------------------------------------------------------------- /Arduino/Examples/on_demand_readout/on_demand_readout.ino: -------------------------------------------------------------------------------- 1 | /* 2 | on_demand_readout.ino 3 | 4 | Example code for using the Metriful MS430 in "on-demand" mode. 5 | 6 | Repeatedly measures and displays all environment data, with a pause 7 | between measurements. Air quality data are unavailable in this mode 8 | (instead see cycle_readout.ino). View output in the Serial Monitor. 9 | 10 | Copyright 2020-2023 Metriful Ltd. 11 | Licensed under the MIT License - for further details see LICENSE.txt 12 | 13 | For code examples, datasheet and user guide, visit 14 | https://github.com/metriful/sensor 15 | */ 16 | 17 | #include 18 | 19 | ////////////////////////////////////////////////////////// 20 | // USER-EDITABLE SETTINGS 21 | 22 | // Pause (in milliseconds) between data measurements (note that the 23 | // measurement itself takes 0.5 seconds) 24 | uint32_t pause_ms = 2500; 25 | // Choosing a pause of less than about 2000 ms will cause inaccurate 26 | // temperature, humidity and particle data. 27 | 28 | // How to print the data over the serial port. If printDataAsColumns = true, 29 | // data are columns of numbers, useful to copy/paste to a spreadsheet 30 | // application. Otherwise, data are printed with explanatory labels and units. 31 | bool printDataAsColumns = false; 32 | 33 | // END OF USER-EDITABLE SETTINGS 34 | ////////////////////////////////////////////////////////// 35 | 36 | // Structs for data 37 | AirData_t airData = {0}; 38 | LightData_t lightData = {0}; 39 | SoundData_t soundData = {0}; 40 | ParticleData_t particleData = {0}; 41 | 42 | 43 | void setup() 44 | { 45 | // Initialize the host pins, set up the serial port and reset: 46 | SensorHardwareSetup(I2C_ADDRESS); 47 | 48 | uint8_t particleSensor = PARTICLE_SENSOR; 49 | TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); 50 | 51 | // Wait for the serial port to be ready, for displaying the output 52 | while (!Serial) 53 | { 54 | yield(); 55 | } 56 | } 57 | 58 | 59 | void loop() 60 | { 61 | // Trigger a new measurement 62 | ready_assertion_event = false; 63 | TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); 64 | 65 | // Wait for the measurement to finish, indicated by a falling edge on READY 66 | while (!ready_assertion_event) 67 | { 68 | yield(); 69 | } 70 | 71 | // Read data from the MS430 into the data structs. 72 | 73 | // Air data 74 | // Choose output temperature unit (C or F) in Metriful_sensor.h 75 | airData = getAirData(I2C_ADDRESS); 76 | 77 | // Air quality data are not available with on demand measurements 78 | 79 | // Light data 80 | lightData = getLightData(I2C_ADDRESS); 81 | 82 | // Sound data 83 | soundData = getSoundData(I2C_ADDRESS); 84 | 85 | /* Particle data 86 | This requires the connection of a particulate sensor (invalid 87 | values will be obtained if this sensor is not present). 88 | Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h 89 | Also note that, due to the low pass filtering used, the 90 | particle data become valid after an initial initialization 91 | period of approximately one minute. 92 | */ 93 | if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) 94 | { 95 | particleData = getParticleData(I2C_ADDRESS); 96 | } 97 | 98 | // Print all data to the serial port 99 | printAirData(&airData, printDataAsColumns); 100 | printLightData(&lightData, printDataAsColumns); 101 | printSoundData(&soundData, printDataAsColumns); 102 | if (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF) 103 | { 104 | printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR); 105 | } 106 | Serial.println(); 107 | 108 | // Wait for the chosen time period before repeating everything 109 | delay(pause_ms); 110 | } 111 | -------------------------------------------------------------------------------- /Arduino/Examples/particle_sensor_toggle/particle_sensor_toggle.ino: -------------------------------------------------------------------------------- 1 | /* 2 | particle_sensor_toggle.ino 3 | 4 | Optional advanced demo. This program shows how to generate an output 5 | control signal from one of the host's pins, which can be used to turn 6 | the particle sensor on and off. An external transistor circuit is 7 | also needed - this will gate the sensor power supply according to 8 | the control signal. Further details are given in the User Guide. 9 | 10 | The program continually measures and displays all environment data 11 | in a repeating cycle. The user can view the output in the Serial 12 | Monitor. After reading the data, the particle sensor is powered off 13 | for a chosen number of cycles ("off_cycles"). It is then powered on 14 | and read before being powered off again. Sound data are ignored 15 | while the particle sensor is on, to avoid its fan noise. 16 | 17 | Copyright 2020-2023 Metriful Ltd. 18 | Licensed under the MIT License - for further details see LICENSE.txt 19 | 20 | For code examples, datasheet and user guide, visit 21 | https://github.com/metriful/sensor 22 | */ 23 | 24 | #include 25 | 26 | ////////////////////////////////////////////////////////// 27 | // USER-EDITABLE SETTINGS 28 | 29 | // How often to read data; choose only 100 or 300 seconds for this demo 30 | // because the sensor should be on for at least one minute before reading 31 | // its data. 32 | uint8_t cycle_period = CYCLE_PERIOD_100_S; 33 | 34 | // How to print the data over the serial port. If printDataAsColumns = true, 35 | // data are columns of numbers, useful for transferring to a spreadsheet 36 | // application. Otherwise, data are printed with explanatory labels and units. 37 | bool printDataAsColumns = false; 38 | 39 | // Particle sensor power control options 40 | uint8_t off_cycles = 2; // leave the sensor off for this many cycles between reads 41 | uint8_t particle_sensor_control_pin = 10; // host pin number which outputs the control signal 42 | bool particle_sensor_ON_state = true; 43 | // particle_sensor_ON_state is the required polarity of the control 44 | // signal; true means +V is output to turn the sensor on, while false 45 | // means 0 V is output. Use true for 3.3 V hosts and false for 5 V hosts. 46 | 47 | // END OF USER-EDITABLE SETTINGS 48 | ////////////////////////////////////////////////////////// 49 | 50 | uint8_t transmit_buffer[1] = {0}; 51 | 52 | // Structs for data 53 | AirData_t airData = {0}; 54 | AirQualityData_t airQualityData = {0}; 55 | LightData_t lightData = {0}; 56 | SoundData_t soundData = {0}; 57 | ParticleData_t particleData = {0}; 58 | 59 | bool particleSensorIsOn = false; 60 | uint8_t particleSensor_count = 0; 61 | 62 | 63 | void setup() 64 | { 65 | // Initialize the host pins, set up the serial port and reset: 66 | SensorHardwareSetup(I2C_ADDRESS); 67 | 68 | // Set up the particle sensor control, and turn it off initially 69 | pinMode(particle_sensor_control_pin, OUTPUT); 70 | digitalWrite(particle_sensor_control_pin, !particle_sensor_ON_state); 71 | particleSensorIsOn = false; 72 | 73 | // Apply chosen settings to the MS430 74 | transmit_buffer[0] = PARTICLE_SENSOR; 75 | TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); 76 | transmit_buffer[0] = cycle_period; 77 | TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, transmit_buffer, 1); 78 | 79 | // Wait for the serial port to be ready, for displaying the output 80 | while (!Serial) 81 | { 82 | yield(); 83 | } 84 | 85 | Serial.println("Entering cycle mode and waiting for data."); 86 | ready_assertion_event = false; 87 | TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); 88 | } 89 | 90 | 91 | void loop() 92 | { 93 | // Wait for the next new data release, indicated by a falling edge on READY 94 | while (!ready_assertion_event) 95 | { 96 | yield(); 97 | } 98 | ready_assertion_event = false; 99 | 100 | /* Read data from the MS430 into the data structs. 101 | For each category of data (air, sound, etc.) a pointer to the data struct is 102 | passed to the ReceiveI2C() function. The received byte sequence fills the data 103 | struct in the correct order so that each field within the struct receives 104 | the value of an environmental quantity (temperature, sound level, etc.) 105 | */ 106 | 107 | // Air data 108 | ReceiveI2C(I2C_ADDRESS, AIR_DATA_READ, (uint8_t *) &airData, AIR_DATA_BYTES); 109 | 110 | /* Air quality data 111 | The initial self-calibration of the air quality data may take several 112 | minutes to complete. During this time the accuracy parameter is zero 113 | and the data values are not valid. 114 | */ 115 | ReceiveI2C(I2C_ADDRESS, AIR_QUALITY_DATA_READ, (uint8_t *) &airQualityData, 116 | AIR_QUALITY_DATA_BYTES); 117 | 118 | // Light data 119 | ReceiveI2C(I2C_ADDRESS, LIGHT_DATA_READ, (uint8_t *) &lightData, LIGHT_DATA_BYTES); 120 | 121 | // Sound data - only read when particle sensor is off 122 | if (!particleSensorIsOn) 123 | { 124 | ReceiveI2C(I2C_ADDRESS, SOUND_DATA_READ, (uint8_t *) &soundData, SOUND_DATA_BYTES); 125 | } 126 | 127 | /* Particle data 128 | This requires the connection of a particulate sensor (invalid 129 | values will be obtained if this sensor is not present). 130 | Specify your sensor model (PPD42 or SDS011) in Metriful_sensor.h 131 | Also note that, due to the low pass filtering used, the 132 | particle data become valid after an initial initialization 133 | period of approximately one minute. 134 | */ 135 | if (particleSensorIsOn) 136 | { 137 | ReceiveI2C(I2C_ADDRESS, PARTICLE_DATA_READ, (uint8_t *) &particleData, PARTICLE_DATA_BYTES); 138 | } 139 | 140 | // Print all data to the serial port. The previous loop's particle or 141 | // sound data will be printed if no reading was done on this loop. 142 | printAirData(&airData, printDataAsColumns); 143 | printAirQualityData(&airQualityData, printDataAsColumns); 144 | printLightData(&lightData, printDataAsColumns); 145 | printSoundData(&soundData, printDataAsColumns); 146 | printParticleData(&particleData, printDataAsColumns, PARTICLE_SENSOR); 147 | Serial.println(); 148 | 149 | // Turn the particle sensor on/off if required 150 | if (particleSensorIsOn) 151 | { 152 | // Stop the particle detection on the MS430 153 | transmit_buffer[0] = OFF; 154 | TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); 155 | 156 | // Turn off the hardware: 157 | digitalWrite(particle_sensor_control_pin, !particle_sensor_ON_state); 158 | particleSensorIsOn = false; 159 | } 160 | else 161 | { 162 | particleSensor_count++; 163 | if (particleSensor_count >= off_cycles) 164 | { 165 | // Turn on the hardware: 166 | digitalWrite(particle_sensor_control_pin, particle_sensor_ON_state); 167 | 168 | // Start the particle detection on the MS430 169 | transmit_buffer[0] = PARTICLE_SENSOR; 170 | TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, transmit_buffer, 1); 171 | 172 | particleSensor_count = 0; 173 | particleSensorIsOn = true; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Arduino/Examples/simple_read_T_H/simple_read_T_H.ino: -------------------------------------------------------------------------------- 1 | /* 2 | simple_read_T_H.ino 3 | 4 | Example code for using the Metriful MS430 to measure humidity 5 | and temperature. 6 | 7 | Demonstrates multiple ways of reading and displaying the temperature 8 | and humidity data. View the output in the Serial Monitor. The other 9 | data can be measured and displayed in a similar way. 10 | 11 | Copyright 2020-2023 Metriful Ltd. 12 | Licensed under the MIT License - for further details see LICENSE.txt 13 | 14 | For code examples, datasheet and user guide, visit 15 | https://github.com/metriful/sensor 16 | */ 17 | 18 | #include 19 | 20 | 21 | void setup() 22 | { 23 | // Initialize the host pins, set up the serial port and reset: 24 | SensorHardwareSetup(I2C_ADDRESS); 25 | 26 | // Wait for the serial port to be ready, for displaying the output 27 | while (!Serial) 28 | { 29 | yield(); 30 | } 31 | 32 | // Clear the global variable in preparation for waiting for READY assertion 33 | ready_assertion_event = false; 34 | 35 | // Initiate an on-demand data measurement 36 | TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); 37 | 38 | // Now wait for the ready signal before continuing 39 | while (!ready_assertion_event) 40 | { 41 | yield(); 42 | } 43 | 44 | // We know that new data are ready to read. 45 | 46 | //////////////////////////////////////////////////////////////////// 47 | 48 | // There are different ways to read and display the data 49 | 50 | // 1. Simplest way: use the example float (_F) functions 51 | 52 | // Read the "air data" from the MS430. This includes temperature and 53 | // humidity as well as pressure and gas sensor data. 54 | AirData_F_t airDataF = getAirDataF(I2C_ADDRESS); 55 | 56 | // Print all of the air measurements to the serial monitor 57 | printAirDataF(&airDataF); 58 | // Fahrenheit temperature is printed if USE_FAHRENHEIT is defined 59 | // in "Metriful_sensor.h" 60 | 61 | Serial.println("-----------------------------"); 62 | 63 | 64 | // 2. After reading from the MS430, you can also access and print the 65 | // float data directly from the float struct: 66 | Serial.print("The temperature is: "); 67 | Serial.print(airDataF.T_C, 1); // print to 1 decimal place 68 | Serial.println(" " CELSIUS_SYMBOL); 69 | 70 | // Optional: convert to Fahrenheit 71 | float temperature_F = convertCtoF(airDataF.T_C); 72 | 73 | Serial.print("The temperature is: "); 74 | Serial.print(temperature_F, 1); // print to 1 decimal place 75 | Serial.println(" " FAHRENHEIT_SYMBOL); 76 | 77 | Serial.println("-----------------------------"); 78 | 79 | 80 | // 3. If host resources are limited, avoid using floating point and 81 | // instead use the integer versions (without "F" in the name) 82 | AirData_t airData = getAirData(I2C_ADDRESS); 83 | 84 | // Print to the serial monitor 85 | printAirData(&airData, false); 86 | // If the second argument is "true", data are printed as columns. 87 | // Fahrenheit temperature is printed if USE_FAHRENHEIT is defined 88 | // in "Metriful_sensor.h" 89 | 90 | Serial.println("-----------------------------"); 91 | 92 | 93 | // 4. Access and print integer data directly from the integer struct: 94 | Serial.print("The humidity is: "); 95 | Serial.print(airData.H_pc_int); // the integer part of the value 96 | Serial.print("."); // the decimal point 97 | Serial.print(airData.H_pc_fr_1dp); // the fractional part (1 decimal place) 98 | Serial.println(" %"); 99 | 100 | Serial.println("-----------------------------"); 101 | 102 | 103 | // 5. Advanced: read and decode only the humidity value from the MS430 104 | 105 | // Read the raw humidity data 106 | uint8_t receive_buffer[2] = {0}; 107 | ReceiveI2C(I2C_ADDRESS, H_READ, receive_buffer, H_BYTES); 108 | 109 | // Decode the humidity: the first received byte is the integer part, the 110 | // second received byte is the fractional part to one decimal place. 111 | uint8_t humidity_integer = receive_buffer[0]; 112 | uint8_t humidity_fraction = receive_buffer[1]; 113 | // Print it: the units are percentage relative humidity. 114 | Serial.print("Humidity = "); 115 | Serial.print(humidity_integer); 116 | Serial.print("."); 117 | Serial.print(humidity_fraction); 118 | Serial.println(" %"); 119 | 120 | Serial.println("-----------------------------"); 121 | 122 | 123 | // 6. Advanced: read and decode only the temperature value from the MS430 124 | 125 | // Read the raw temperature data 126 | ReceiveI2C(I2C_ADDRESS, T_READ, receive_buffer, T_BYTES); 127 | 128 | // The temperature is encoded differently to allow negative values 129 | 130 | // Find the positive magnitude of the integer part of the temperature 131 | // by doing a bitwise AND of the first received byte with TEMPERATURE_VALUE_MASK 132 | uint8_t temperature_positive_integer = receive_buffer[0] & TEMPERATURE_VALUE_MASK; 133 | 134 | // The second received byte is the fractional part to one decimal place 135 | uint8_t temperature_fraction = receive_buffer[1]; 136 | 137 | Serial.print("Temperature = "); 138 | // If the most-significant bit of the first byte is a 1, the temperature 139 | // is negative (below 0 C), otherwise it is positive 140 | if ((receive_buffer[0] & TEMPERATURE_SIGN_MASK) != 0) 141 | { 142 | // The bit is a 1: celsius temperature is negative 143 | Serial.print("-"); 144 | } 145 | Serial.print(temperature_positive_integer); 146 | Serial.print("."); 147 | Serial.print(temperature_fraction); 148 | Serial.println(" " CELSIUS_SYMBOL); 149 | 150 | } 151 | 152 | void loop() 153 | { 154 | // There is no loop for this program. 155 | } 156 | -------------------------------------------------------------------------------- /Arduino/Examples/simple_read_sound/simple_read_sound.ino: -------------------------------------------------------------------------------- 1 | /* 2 | simple_read_sound.ino 3 | 4 | Example code for using the Metriful MS430 to measure sound. 5 | 6 | Demonstrates multiple ways of reading and displaying the sound data. 7 | View the output in the Serial Monitor. The other data can be measured 8 | and displayed in a similar way. 9 | 10 | Copyright 2020-2023 Metriful Ltd. 11 | Licensed under the MIT License - for further details see LICENSE.txt 12 | 13 | For code examples, datasheet and user guide, visit 14 | https://github.com/metriful/sensor 15 | */ 16 | 17 | #include 18 | 19 | 20 | void setup() 21 | { 22 | // Initialize the host pins, set up the serial port and reset: 23 | SensorHardwareSetup(I2C_ADDRESS); 24 | 25 | // Wait for the serial port to be ready, for displaying the output 26 | while (!Serial) 27 | { 28 | yield(); 29 | } 30 | 31 | //////////////////////////////////////////////////////////////////// 32 | 33 | // Wait for the microphone signal to stabilize (takes approximately 1.5 seconds). 34 | // This only needs to be done once after the MS430 is powered-on or reset. 35 | delay(1500); 36 | 37 | //////////////////////////////////////////////////////////////////// 38 | 39 | // Clear the global variable in preparation for waiting for READY assertion 40 | ready_assertion_event = false; 41 | 42 | // Initiate an on-demand data measurement 43 | TransmitI2C(I2C_ADDRESS, ON_DEMAND_MEASURE_CMD, 0, 0); 44 | 45 | // Now wait for the ready signal (falling edge) before continuing 46 | while (!ready_assertion_event) 47 | { 48 | yield(); 49 | } 50 | 51 | // We now know that newly measured data are ready to read. 52 | 53 | //////////////////////////////////////////////////////////////////// 54 | 55 | // There are multiple ways to read and display the data 56 | 57 | 58 | // 1. Simplest way: use the example float (_F) functions 59 | 60 | // Read the sound data from the board 61 | SoundData_F_t soundDataF = getSoundDataF(I2C_ADDRESS); 62 | 63 | // Print all of the sound measurements to the serial monitor 64 | printSoundDataF(&soundDataF); 65 | 66 | Serial.println("-----------------------------"); 67 | 68 | 69 | // 2. After reading from the MS430, you can also access and print the 70 | // float data directly from the float struct: 71 | Serial.print("The sound pressure level is: "); 72 | Serial.print(soundDataF.SPL_dBA, 1); // print to 1 decimal place 73 | Serial.println(" dBA"); 74 | 75 | Serial.println("-----------------------------"); 76 | 77 | 78 | // 3. If host resources are limited, avoid using floating point and 79 | // instead use the integer versions (without "F" in the name) 80 | SoundData_t soundData = getSoundData(I2C_ADDRESS); 81 | 82 | // Print to the serial monitor 83 | printSoundData(&soundData, false); 84 | // If the second argument is "true", data are printed as columns. 85 | 86 | Serial.println("-----------------------------"); 87 | 88 | 89 | // 4. Access and print integer data directly from the integer struct: 90 | Serial.print("The sound pressure level is: "); 91 | Serial.print(soundData.SPL_dBA_int); // the integer part of the value 92 | Serial.print("."); // the decimal point 93 | Serial.print(soundData.SPL_dBA_fr_1dp); // the fractional part (1 decimal place) 94 | Serial.println(" dBA"); 95 | 96 | Serial.println("-----------------------------"); 97 | } 98 | 99 | void loop() 100 | { 101 | // There is no loop for this program. 102 | } 103 | -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/ESPHome_patch.yaml: -------------------------------------------------------------------------------- 1 | # Use the MS430 with ESPHome by adding this file to your yaml 2 | # device configuration file. 3 | 4 | # 1. Generate a yaml file using ESPHome. 5 | # 2. Replace the "esphome:" section (the first 3 lines) in your 6 | # yaml file with the entire contents of this file. 7 | # 3. Edit the values for the three substitutions: 8 | 9 | # "device_name" identifies the microcontroller board and is the default 10 | # display name in Home Assistant. 11 | 12 | # "particle_sensor_disabled" should be 'true' if a particle sensor is 13 | # not attached. If a particle sensor is attached, configure its type in 14 | # Metriful_sensor.h and set particle_sensor_disabled to be 'false'. 15 | 16 | # "temperature_offset" is an optional constant adjustment for the 17 | # temperature measurement. 18 | 19 | substitutions: 20 | device_name: put_your_device_name_here 21 | particle_sensor_disabled: 'true' 22 | temperature_offset: "0.0" 23 | 24 | # The file below this line should not require any edits. 25 | ############################################################################### 26 | 27 | esphome: 28 | name: ${device_name} 29 | friendly_name: ${device_name} 30 | includes: 31 | - sensor_constants.h 32 | - host_pin_definitions.h 33 | - Metriful_sensor.h 34 | - Metriful_sensor.cpp 35 | libraries: 36 | - Wire 37 | 38 | # MS430 configuration 39 | <<: !include MS430_ESPHome.yaml 40 | -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/MS430_ESPHome.yaml: -------------------------------------------------------------------------------- 1 | # ESPHome configuration for the MS430. 2 | 3 | # Copyright 2025 Metriful Ltd. 4 | # Licensed under the MIT License - for further details see LICENSE.txt 5 | 6 | # For code examples, datasheet and user guide, visit 7 | # https://github.com/metriful/sensor 8 | 9 | # This file must be included in another yaml configuration file, in 10 | # which "particle_sensor_disabled" and "temperature_offset" are defined 11 | # as substitutions. 12 | 13 | ######################################################################### 14 | 15 | external_components: 16 | - source: 17 | type: local 18 | path: esphome_components 19 | 20 | sensor: 21 | - platform: ms430 22 | temperature: 23 | name: "Temperature" 24 | icon: "mdi:thermometer" 25 | filters: 26 | - offset: ${temperature_offset} 27 | humidity: 28 | name: "Humidity" 29 | icon: "mdi:cloud-percent" 30 | pressure: 31 | name: "Air pressure" 32 | icon: "mdi:weather-partly-rainy" 33 | gas_resistance: 34 | name: "Gas sensor resistance" 35 | icon: "mdi:scent" 36 | white_light: 37 | name: "White light level" 38 | icon: "mdi:circle-outline" 39 | illuminance: 40 | name: "Illuminance" 41 | icon: "mdi:white-balance-sunny" 42 | particle_duty: 43 | name: "Particle sensor duty cycle" 44 | icon: "mdi:square-wave" 45 | internal: ${particle_sensor_disabled} 46 | particle_concentration: 47 | name: "Particle concentration" 48 | icon: "mdi:chart-bubble" 49 | internal: ${particle_sensor_disabled} 50 | aqi: 51 | name: "Air quality index" 52 | icon: "mdi:flower-tulip-outline" 53 | on_value_range: 54 | - above: -0.5 55 | below: 50 56 | then: 57 | - text_sensor.template.publish: 58 | id: aqi_text 59 | state: "Good" 60 | - above: 50 61 | below: 100 62 | then: 63 | - text_sensor.template.publish: 64 | id: aqi_text 65 | state: "Acceptable" 66 | - above: 100 67 | below: 150 68 | then: 69 | - text_sensor.template.publish: 70 | id: aqi_text 71 | state: "Substandard" 72 | - above: 150 73 | below: 200 74 | then: 75 | - text_sensor.template.publish: 76 | id: aqi_text 77 | state: "Poor" 78 | - above: 200 79 | below: 300 80 | then: 81 | - text_sensor.template.publish: 82 | id: aqi_text 83 | state: "Bad" 84 | - above: 300 85 | then: 86 | - text_sensor.template.publish: 87 | id: aqi_text 88 | state: "Very bad" 89 | e_co2: 90 | name: "Estimated CO₂" 91 | icon: "mdi:molecule-co2" 92 | b_voc: 93 | name: "Equivalent breath VOC" 94 | icon: "mdi:account-voice" 95 | iaq_accuracy: 96 | name: "Air quality accuracy" 97 | internal: true 98 | on_value_range: 99 | - above: -0.5 100 | below: 0.5 101 | then: 102 | - text_sensor.template.publish: 103 | id: aqi_acc_text 104 | state: "Not yet valid" 105 | - above: 0.5 106 | below: 1.5 107 | then: 108 | - text_sensor.template.publish: 109 | id: aqi_acc_text 110 | state: "Low" 111 | - above: 1.5 112 | below: 2.5 113 | then: 114 | - text_sensor.template.publish: 115 | id: aqi_acc_text 116 | state: "Medium" 117 | - above: 2.5 118 | then: 119 | - text_sensor.template.publish: 120 | id: aqi_acc_text 121 | state: "High" 122 | sound_spl: 123 | name: "Sound pressure level" 124 | icon: "mdi:microphone" 125 | sound_peak: 126 | name: "Peak sound amplitude" 127 | icon: "mdi:waveform" 128 | sound_band_0: 129 | name: "SPL at 125 Hz" 130 | icon: "mdi:sine-wave" 131 | sound_band_1: 132 | name: "SPL at 250 Hz" 133 | icon: "mdi:sine-wave" 134 | sound_band_2: 135 | name: "SPL at 500 Hz" 136 | icon: "mdi:sine-wave" 137 | sound_band_3: 138 | name: "SPL at 1000 Hz" 139 | icon: "mdi:sine-wave" 140 | sound_band_4: 141 | name: "SPL at 2000 Hz" 142 | icon: "mdi:sine-wave" 143 | sound_band_5: 144 | name: "SPL at 4000 Hz" 145 | icon: "mdi:sine-wave" 146 | 147 | 148 | text_sensor: 149 | - platform: template 150 | name: "Air quality accuracy" 151 | icon: "mdi:magnify" 152 | id: aqi_acc_text 153 | - platform: template 154 | name: "Air quality" 155 | icon: "mdi:flower-tulip-outline" 156 | id: aqi_text 157 | -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/Metriful_sensor.h: -------------------------------------------------------------------------------- 1 | /* 2 | Metriful_sensor.h 3 | 4 | This file declares functions and settings which are used in the code 5 | examples. The function definitions are in file Metriful_sensor.cpp 6 | 7 | Copyright 2020-2023 Metriful Ltd. 8 | Licensed under the MIT License - for further details see LICENSE.txt 9 | 10 | For code examples, datasheet and user guide, visit 11 | https://github.com/metriful/sensor 12 | */ 13 | 14 | #ifndef METRIFUL_SENSOR_H 15 | #define METRIFUL_SENSOR_H 16 | 17 | #include "Arduino.h" 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include "sensor_constants.h" 25 | #include "host_pin_definitions.h" 26 | 27 | // Un-comment the following line to display temperatures in Fahrenheit 28 | // else they will be in Celsius 29 | //#define USE_FAHRENHEIT 30 | 31 | // Specify which particle sensor is connected: 32 | #define PARTICLE_SENSOR PARTICLE_SENSOR_OFF 33 | // Define PARTICLE_SENSOR as: 34 | // PARTICLE_SENSOR_PPD42 for the Shinyei PPD42 35 | // PARTICLE_SENSOR_SDS011 for the Nova SDS011 36 | // PARTICLE_SENSOR_OFF if no sensor is connected 37 | 38 | // The I2C address of the MS430 board. 39 | #define I2C_ADDRESS I2C_ADDR_7BIT_SB_OPEN 40 | // The default is I2C_ADDR_7BIT_SB_OPEN and must be changed to 41 | // I2C_ADDR_7BIT_SB_CLOSED if the solder bridge SB1 on the board 42 | // is soldered closed 43 | 44 | ///////////////////////////////////////////////////////////////////// 45 | 46 | // The Arduino Wire library has a limited internal buffer size: 47 | #define ARDUINO_WIRE_BUFFER_LIMIT_BYTES 32 48 | 49 | #define I2C_CLK_FREQ_HZ 100000 50 | #define SERIAL_BAUD_RATE 9600 51 | 52 | // Unicode symbol strings 53 | #define CELSIUS_SYMBOL "°C" 54 | #define FAHRENHEIT_SYMBOL "°F" 55 | #define SDS011_UNIT_SYMBOL "µg/m³" 56 | #define SUBSCRIPT_2 "₂" 57 | #define OHM_SYMBOL "Ω" 58 | 59 | extern volatile bool ready_assertion_event; 60 | 61 | ///////////////////////////////////////////////////////////////////// 62 | 63 | // Data category structs containing floats. If floats are not wanted, 64 | // use the integer-only struct versions in sensor_constants.h 65 | 66 | typedef struct 67 | { 68 | float SPL_dBA; 69 | float SPL_bands_dB[SOUND_FREQ_BANDS]; 70 | float peakAmp_mPa; 71 | bool stable; 72 | } SoundData_F_t; 73 | 74 | typedef struct 75 | { 76 | float T_C; 77 | uint32_t P_Pa; 78 | float H_pc; 79 | uint32_t G_Ohm; 80 | } AirData_F_t; 81 | 82 | typedef struct 83 | { 84 | float AQI; 85 | float CO2e; 86 | float bVOC; 87 | uint8_t AQI_accuracy; 88 | } AirQualityData_F_t; 89 | 90 | typedef struct 91 | { 92 | float illum_lux; 93 | uint16_t white; 94 | } LightData_F_t; 95 | 96 | typedef struct 97 | { 98 | float duty_cycle_pc; 99 | float concentration; 100 | bool valid; 101 | } ParticleData_F_t; 102 | 103 | ///////////////////////////////////////////////////////////////////// 104 | 105 | // Custom type used to select the particle sensor being used (if any) 106 | typedef enum 107 | { 108 | OFF = PARTICLE_SENSOR_OFF, 109 | PPD42 = PARTICLE_SENSOR_PPD42, 110 | SDS011 = PARTICLE_SENSOR_SDS011 111 | } ParticleSensor_t; 112 | 113 | // Struct used in the IFTTT example 114 | typedef struct 115 | { 116 | const char * variableName; 117 | const char * measurementUnit; 118 | int32_t thresHigh; 119 | int32_t thresLow; 120 | uint16_t inactiveCount; 121 | const char * adviceHigh; 122 | const char * adviceLow; 123 | } ThresholdSetting_t; 124 | 125 | // Struct used in the Home Assistant example 126 | typedef struct 127 | { 128 | const char * name; 129 | const char * unit; 130 | const char * icon; 131 | uint8_t decimalPlaces; 132 | } HA_Attributes_t; 133 | 134 | ///////////////////////////////////////////////////////////////////// 135 | 136 | void SensorHardwareSetup(uint8_t i2c_7bit_address); 137 | void ready_ISR(void); 138 | 139 | bool TransmitI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, 140 | const uint8_t * data, uint8_t data_length); 141 | bool ReceiveI2C(uint8_t dev_addr_7bit, uint8_t commandRegister, 142 | uint8_t data[], uint8_t data_length); 143 | 144 | const char * interpret_AQI_accuracy(uint8_t AQI_accuracy_code); 145 | const char * interpret_AQI_accuracy_brief(uint8_t AQI_accuracy_code); 146 | const char * interpret_AQI_value(uint16_t AQI); 147 | 148 | void s_printf(const char * format, ...); 149 | 150 | void convertAirDataF(const AirData_t * airData_in, AirData_F_t * airDataF_out); 151 | void convertAirQualityDataF(const AirQualityData_t * airQualityData_in, 152 | AirQualityData_F_t * airQualityDataF_out); 153 | void convertLightDataF(const LightData_t * lightData_in, 154 | LightData_F_t * lightDataF_out); 155 | void convertSoundDataF(const SoundData_t * soundData_in, 156 | SoundData_F_t * soundDataF_out); 157 | void convertParticleDataF(const ParticleData_t * particleData_in, 158 | ParticleData_F_t * particleDataF_out); 159 | 160 | void printAirDataF(const AirData_F_t * airDataF); 161 | void printAirQualityDataF(const AirQualityData_F_t * airQualityDataF); 162 | void printLightDataF(const LightData_F_t * lightDataF); 163 | void printSoundDataF(const SoundData_F_t * soundDataF); 164 | void printParticleDataF(const ParticleData_F_t * particleDataF, 165 | uint8_t particleSensor); 166 | 167 | void printAirData(const AirData_t * airData, bool printColumns); 168 | void printAirQualityData(const AirQualityData_t * airQualityData, 169 | bool printColumns); 170 | void printLightData(const LightData_t * lightData, bool printColumns); 171 | void printSoundData(const SoundData_t * soundData, bool printColumns); 172 | void printParticleData(const ParticleData_t * particleData, 173 | bool printColumns, uint8_t particleSensor); 174 | 175 | bool setSoundInterruptThreshold(uint8_t dev_addr_7bit, uint16_t threshold_mPa); 176 | bool setLightInterruptThreshold(uint8_t dev_addr_7bit, uint16_t thres_lux_int, 177 | uint8_t thres_lux_fr_2dp); 178 | 179 | SoundData_t getSoundData(uint8_t i2c_7bit_address); 180 | AirData_t getAirData(uint8_t i2c_7bit_address); 181 | LightData_t getLightData(uint8_t i2c_7bit_address); 182 | AirQualityData_t getAirQualityData(uint8_t i2c_7bit_address); 183 | ParticleData_t getParticleData(uint8_t i2c_7bit_address); 184 | 185 | SoundData_F_t getSoundDataF(uint8_t i2c_7bit_address); 186 | AirData_F_t getAirDataF(uint8_t i2c_7bit_address); 187 | LightData_F_t getLightDataF(uint8_t i2c_7bit_address); 188 | AirQualityData_F_t getAirQualityDataF(uint8_t i2c_7bit_address); 189 | ParticleData_F_t getParticleDataF(uint8_t i2c_7bit_address); 190 | 191 | float convertCtoF(float C); 192 | void convertCtoF_int(float C, uint8_t * F_int, uint8_t * F_fr_1dp, 193 | bool * isPositive); 194 | float convertEncodedTemperatureToFloat(uint8_t T_C_int_with_sign, 195 | uint8_t T_C_fr_1dp); 196 | const char * getTemperature(const AirData_t * pAirData, uint8_t * T_intPart, 197 | uint8_t * T_fractionalPart, bool * isPositive); 198 | #endif 199 | -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/WiFi_functions.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | WiFi_functions.cpp 3 | 4 | This file defines functions used by examples connecting to, 5 | or creating, a WiFi network. 6 | 7 | Copyright 2020-2023 Metriful Ltd. 8 | Licensed under the MIT License - for further details see LICENSE.txt 9 | 10 | For code examples, datasheet and user guide, visit 11 | https://github.com/metriful/sensor 12 | */ 13 | 14 | #include "host_pin_definitions.h" 15 | #ifdef HAS_WIFI 16 | #include "Arduino.h" 17 | #include "WiFi_functions.h" 18 | 19 | // Repeatedly attempt to connect to the WiFi network using the input 20 | // network name (SSID) and password. 21 | void connectToWiFi(const char * SSID, const char * password) 22 | { 23 | WiFi.disconnect(); 24 | #if defined(ESP8266) || defined(ESP32) || defined(ARDUINO_ARCH_RP2040) 25 | WiFi.persistent(false); 26 | WiFi.mode(WIFI_STA); 27 | #endif 28 | uint8_t wStatus = WL_DISCONNECTED; 29 | while (wStatus != WL_CONNECTED) 30 | { 31 | Serial.print("Attempting to connect to "); 32 | Serial.println(SSID); 33 | uint8_t statusChecks = 0; 34 | WiFi.begin(SSID, password); 35 | while ((wStatus != WL_CONNECTED) && (statusChecks < 8)) 36 | { 37 | delay(1000); 38 | Serial.print("."); 39 | wStatus = WiFi.status(); 40 | statusChecks++; 41 | } 42 | if (wStatus != WL_CONNECTED) 43 | { 44 | Serial.println("Failed."); 45 | WiFi.disconnect(); 46 | delay(5000); 47 | } 48 | } 49 | Serial.println("Connected."); 50 | } 51 | 52 | // Configure the host as a WiFi access point, creating a WiFi network 53 | // with specified network SSID (name), password and host IP address. 54 | bool createWiFiAP(const char * SSID, const char * password, 55 | IPAddress hostIP) 56 | { 57 | Serial.print("Creating access point named: "); 58 | Serial.println(SSID); 59 | #if defined(ESP8266) || defined(ESP32) || defined(ARDUINO_ARCH_RP2040) 60 | WiFi.persistent(false); 61 | WiFi.mode(WIFI_AP); 62 | IPAddress subnet(255, 255, 255, 0); 63 | bool success = WiFi.softAPConfig(hostIP, hostIP, subnet); 64 | success = success && WiFi.softAP(SSID, password); 65 | #else 66 | WiFi.config(hostIP); 67 | bool success = (WiFi.beginAP(SSID, password) == WL_AP_LISTENING); 68 | #endif 69 | return success; 70 | } 71 | 72 | // Provide a readable interpretation of the WiFi status. 73 | // statusCode is the value returned by WiFi.status() 74 | const char * interpret_WiFi_status(uint8_t statusCode) 75 | { 76 | switch (statusCode) 77 | { 78 | case WL_CONNECTED: 79 | return "Connected"; 80 | case WL_NO_SHIELD: 81 | return "No shield/module"; 82 | case WL_IDLE_STATUS: 83 | return "Idle (temporary)"; 84 | case WL_NO_SSID_AVAIL: 85 | return "No SSID available"; 86 | case WL_SCAN_COMPLETED: 87 | return "Scan completed"; 88 | case WL_CONNECT_FAILED: 89 | return "Connection failed"; 90 | case WL_CONNECTION_LOST: 91 | return "Connection lost"; 92 | case WL_DISCONNECTED: 93 | return "Disconnected"; 94 | #if !defined(ESP8266) && !defined(ESP32) 95 | case WL_AP_CONNECTED: 96 | return "AP connected"; 97 | case WL_AP_LISTENING: 98 | return "AP listening"; 99 | #endif 100 | default: 101 | return "Unknown"; 102 | } 103 | } 104 | 105 | // Get the IP address of the host. 106 | // We need this function because the different board types 107 | // do not have a consistent WiFi API. 108 | IPAddress getIPaddress(bool isAccessPoint) 109 | { 110 | if (isAccessPoint) 111 | { 112 | #if defined(ESP8266) || defined(ESP32) || defined(ARDUINO_ARCH_RP2040) 113 | return WiFi.softAPIP(); 114 | #else 115 | return WiFi.localIP(); 116 | #endif 117 | } 118 | else 119 | { 120 | return WiFi.localIP(); 121 | } 122 | } 123 | 124 | // Either: connect to a wifi network, or create a new wifi network 125 | // and assign the specified host IP address. 126 | bool wifiCreateOrConnect(bool createWifiNetwork, bool waitForSerial, 127 | const char * SSID, const char * password, 128 | IPAddress hostIP) 129 | { 130 | if (createWifiNetwork) 131 | { 132 | // The host generates its own WiFi network ("Access Point") with 133 | // a chosen static IP address 134 | if (!createWiFiAP(SSID, password, hostIP)) 135 | { 136 | return false; 137 | } 138 | } 139 | else 140 | { 141 | // The host connects to an existing Wifi network 142 | 143 | // Wait for the serial port to start because the user must be able 144 | // to see the printed IP address in the serial monitor 145 | while (waitForSerial && (!Serial)) 146 | { 147 | yield(); 148 | } 149 | 150 | // Attempt to connect to the Wifi network and obtain the IP 151 | // address. Because the address is not known before this point, 152 | // a serial monitor must be used to display it to the user. 153 | connectToWiFi(SSID, password); 154 | } 155 | 156 | // Print the IP address: use this address in a browser to view the 157 | // generated web page 158 | Serial.print("View your page at http://"); 159 | Serial.println(getIPaddress(createWifiNetwork)); 160 | return true; 161 | } 162 | 163 | 164 | WiFiClient getClient(WiFiServer * server) 165 | { 166 | #ifdef ESP8266 167 | return server->accept(); 168 | #else 169 | return server->available(); 170 | #endif 171 | } 172 | 173 | #endif 174 | -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/WiFi_functions.h: -------------------------------------------------------------------------------- 1 | /* 2 | WiFi_functions.h 3 | 4 | This file declares functions used by examples connecting to, 5 | or creating, a WiFi network. 6 | 7 | Copyright 2020-2023 Metriful Ltd. 8 | Licensed under the MIT License - for further details see LICENSE.txt 9 | 10 | For code examples, datasheet and user guide, visit 11 | https://github.com/metriful/sensor 12 | */ 13 | 14 | #include "host_pin_definitions.h" 15 | #ifdef HAS_WIFI 16 | #ifndef WIFI_FUNCTIONS_H 17 | #define WIFI_FUNCTIONS_H 18 | 19 | #include 20 | 21 | void connectToWiFi(const char * SSID, const char * password); 22 | bool createWiFiAP(const char * SSID, const char * password, 23 | IPAddress hostIP); 24 | const char * interpret_WiFi_status(uint8_t statusCode); 25 | IPAddress getIPaddress(bool isAccessPoint); 26 | bool wifiCreateOrConnect(bool createWifiNetwork, bool waitForSerial, 27 | const char * SSID, const char * password, 28 | IPAddress hostIP); 29 | WiFiClient getClient(WiFiServer * server); 30 | 31 | #endif 32 | #endif 33 | -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/esphome_components/ms430/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metriful/sensor/751fc1a6fce5657c1600a4088bb6f1746aad5e1f/Arduino/Metriful_Sensor/esphome_components/ms430/__init__.py -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/esphome_components/ms430/ms430_esphome.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ms430_esphome.cpp 3 | 4 | This file creates an interface so that the MS430 Arduino code can 5 | be used as an external component in ESPHome. 6 | 7 | Suitable for ESP8266, ESP32 and Raspberry Pi Pico W. 8 | 9 | Copyright 2025 Metriful Ltd. 10 | Licensed under the MIT License - for further details see LICENSE.txt 11 | 12 | For code examples, datasheet and user guide, visit 13 | https://github.com/metriful/sensor 14 | */ 15 | 16 | #include "ms430_esphome.h" 17 | #include "esphome/core/helpers.h" 18 | #include "Metriful_sensor.h" 19 | #include "host_pin_definitions.h" 20 | #include "sensor_constants.h" 21 | 22 | namespace esphome { 23 | namespace ms430 { 24 | 25 | void MS430::setup() 26 | { 27 | enableSerial = false; 28 | SensorHardwareSetup(I2C_ADDRESS); 29 | uint8_t particleSensor = PARTICLE_SENSOR; 30 | TransmitI2C(I2C_ADDRESS, PARTICLE_SENSOR_SELECT_REG, &particleSensor, 1); 31 | uint8_t cyclePeriod = CYCLE_PERIOD; 32 | TransmitI2C(I2C_ADDRESS, CYCLE_TIME_PERIOD_REG, &cyclePeriod, 1); 33 | ready_assertion_event = false; 34 | TransmitI2C(I2C_ADDRESS, CYCLE_MODE_CMD, 0, 0); 35 | } 36 | 37 | void MS430::loop() 38 | { 39 | static uint8_t stage = 0; 40 | if (ready_assertion_event) 41 | { 42 | ready_assertion_event = false; 43 | if (stage == 0) 44 | { 45 | stage = 1; 46 | } 47 | } 48 | stage = this->output(stage); 49 | } 50 | 51 | float MS430::get_setup_priority() const 52 | { 53 | return setup_priority::BUS; 54 | } 55 | 56 | uint8_t MS430::output(uint8_t stage) 57 | { 58 | static AirData_F_t airDataF; 59 | static AirQualityData_F_t airQualityDataF; 60 | static ParticleData_F_t particleDataF; 61 | static LightData_F_t lightDataF; 62 | static SoundData_F_t soundDataF; 63 | 64 | if (stage == 0) 65 | { 66 | return 0; 67 | } 68 | if (stage == 1) 69 | { 70 | airDataF = getAirDataF(I2C_ADDRESS); 71 | } 72 | if (stage == 2) 73 | { 74 | airQualityDataF = getAirQualityDataF(I2C_ADDRESS); 75 | } 76 | if ((stage == 3) && (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF)) 77 | { 78 | particleDataF = getParticleDataF(I2C_ADDRESS); 79 | } 80 | if (stage == 4) 81 | { 82 | lightDataF = getLightDataF(I2C_ADDRESS); 83 | } 84 | if (stage == 5) 85 | { 86 | soundDataF = getSoundDataF(I2C_ADDRESS); 87 | } 88 | if (stage == 6) 89 | { 90 | if (this->temperature_sensor != nullptr) { 91 | this->temperature_sensor->publish_state(airDataF.T_C); 92 | } 93 | if (this->pressure_sensor != nullptr) { 94 | this->pressure_sensor->publish_state(airDataF.P_Pa); 95 | } 96 | if (this->humidity_sensor != nullptr) { 97 | this->humidity_sensor->publish_state(airDataF.H_pc); 98 | } 99 | if (this->gas_sensor != nullptr) { 100 | this->gas_sensor->publish_state(airDataF.G_Ohm); 101 | } 102 | } 103 | if (stage == 7) 104 | { 105 | // Only publish air quality values when the algorithm has 106 | // initialized. 107 | if (this->aqi_acc_sensor != nullptr) { 108 | this->aqi_acc_sensor->publish_state(airQualityDataF.AQI_accuracy); 109 | } 110 | if (airQualityDataF.AQI_accuracy > 0) 111 | { 112 | AQIinitialized = true; 113 | } 114 | 115 | if (AQIinitialized) 116 | { 117 | if (this->aqi_sensor != nullptr) { 118 | this->aqi_sensor->publish_state(airQualityDataF.AQI); 119 | } 120 | if (this->CO2e_sensor != nullptr) { 121 | this->CO2e_sensor->publish_state(airQualityDataF.CO2e); 122 | } 123 | if (this->bVOC_sensor != nullptr) { 124 | this->bVOC_sensor->publish_state(airQualityDataF.bVOC); 125 | } 126 | } 127 | } 128 | if ((stage == 8) && (PARTICLE_SENSOR != PARTICLE_SENSOR_OFF)) 129 | { 130 | if (this->particle_duty_sensor != nullptr) { 131 | this->particle_duty_sensor->publish_state(particleDataF.duty_cycle_pc); 132 | } 133 | if (this->particle_conc_sensor != nullptr) { 134 | this->particle_conc_sensor->publish_state(particleDataF.concentration); 135 | } 136 | } 137 | if (stage == 9) 138 | { 139 | if (this->w_light_sensor != nullptr) { 140 | this->w_light_sensor->publish_state(lightDataF.white); 141 | } 142 | if (this->illum_sensor != nullptr) { 143 | this->illum_sensor->publish_state(lightDataF.illum_lux); 144 | } 145 | } 146 | if (stage == 10) 147 | { 148 | if (this->sound_spl_sensor != nullptr) { 149 | this->sound_spl_sensor->publish_state(soundDataF.SPL_dBA); 150 | } 151 | if (this->sound_peak_sensor != nullptr) { 152 | this->sound_peak_sensor->publish_state(soundDataF.peakAmp_mPa); 153 | } 154 | if (this->sound_band0_sensor != nullptr) { 155 | this->sound_band0_sensor->publish_state(soundDataF.SPL_bands_dB[0]); 156 | } 157 | if (this->sound_band1_sensor != nullptr) { 158 | this->sound_band1_sensor->publish_state(soundDataF.SPL_bands_dB[1]); 159 | } 160 | } 161 | if (stage == 11) 162 | { 163 | if (this->sound_band2_sensor != nullptr) { 164 | this->sound_band2_sensor->publish_state(soundDataF.SPL_bands_dB[2]); 165 | } 166 | if (this->sound_band3_sensor != nullptr) { 167 | this->sound_band3_sensor->publish_state(soundDataF.SPL_bands_dB[3]); 168 | } 169 | if (this->sound_band4_sensor != nullptr) { 170 | this->sound_band4_sensor->publish_state(soundDataF.SPL_bands_dB[4]); 171 | } 172 | if (this->sound_band5_sensor != nullptr) { 173 | this->sound_band5_sensor->publish_state(soundDataF.SPL_bands_dB[5]); 174 | } 175 | } 176 | stage++; 177 | if (stage > 11) { 178 | stage = 0; 179 | } 180 | this->status_clear_warning(); 181 | return stage; 182 | } 183 | 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/esphome_components/ms430/ms430_esphome.h: -------------------------------------------------------------------------------- 1 | /* 2 | ms430_esphome.h 3 | 4 | This file creates an interface so that the MS430 Arduino code can 5 | be used as an external component in ESPHome. 6 | 7 | Suitable for ESP8266, ESP32 and Raspberry Pi Pico W. 8 | 9 | Copyright 2025 Metriful Ltd. 10 | Licensed under the MIT License - for further details see LICENSE.txt 11 | 12 | For code examples, datasheet and user guide, visit 13 | https://github.com/metriful/sensor 14 | */ 15 | 16 | #pragma once 17 | 18 | #include "esphome/core/component.h" 19 | #include "esphome/core/hal.h" 20 | #include "esphome/components/sensor/sensor.h" 21 | 22 | // Choose time interval for reading data (every 3, 100, or 300 seconds) 23 | // 100 or 300 seconds are recommended to avoid self-heating. 24 | #define CYCLE_PERIOD CYCLE_PERIOD_100_S 25 | 26 | ////////////////////////////////////////////////////////////// 27 | 28 | extern bool enableSerial; 29 | 30 | namespace esphome { 31 | namespace ms430 { 32 | 33 | class MS430 : public Component { 34 | public: 35 | void set_temperature_s(sensor::Sensor *s) { temperature_sensor = s; } 36 | void set_pressure_s(sensor::Sensor *s) { pressure_sensor = s; } 37 | void set_humidity_s(sensor::Sensor *s) { humidity_sensor = s; } 38 | void set_gas_s(sensor::Sensor *s) { gas_sensor = s; } 39 | void set_w_light_s(sensor::Sensor *s) { w_light_sensor = s; } 40 | void set_illum_s(sensor::Sensor *s) { illum_sensor = s; } 41 | void set_aqi_acc_s(sensor::Sensor *s) { aqi_acc_sensor = s; } 42 | void set_aqi_s(sensor::Sensor *s) { aqi_sensor = s; } 43 | void set_CO2e_s(sensor::Sensor *s) { CO2e_sensor = s; } 44 | void set_bVOC_s(sensor::Sensor *s) { bVOC_sensor = s; } 45 | void set_particle_duty_s(sensor::Sensor *s) { particle_duty_sensor = s; } 46 | void set_particle_conc_s(sensor::Sensor *s) { particle_conc_sensor = s; } 47 | void set_sound_spl_s(sensor::Sensor *s) { sound_spl_sensor = s; } 48 | void set_sound_peak_s(sensor::Sensor *s) { sound_peak_sensor = s; } 49 | void set_sound_band0_s(sensor::Sensor *s) { sound_band0_sensor = s; } 50 | void set_sound_band1_s(sensor::Sensor *s) { sound_band1_sensor = s; } 51 | void set_sound_band2_s(sensor::Sensor *s) { sound_band2_sensor = s; } 52 | void set_sound_band3_s(sensor::Sensor *s) { sound_band3_sensor = s; } 53 | void set_sound_band4_s(sensor::Sensor *s) { sound_band4_sensor = s; } 54 | void set_sound_band5_s(sensor::Sensor *s) { sound_band5_sensor = s; } 55 | 56 | void setup() override; 57 | void loop() override; 58 | float get_setup_priority() const override; 59 | 60 | protected: 61 | uint8_t output(uint8_t stage); 62 | sensor::Sensor *temperature_sensor{nullptr}; 63 | sensor::Sensor *pressure_sensor{nullptr}; 64 | sensor::Sensor *humidity_sensor{nullptr}; 65 | sensor::Sensor *gas_sensor{nullptr}; 66 | sensor::Sensor *w_light_sensor{nullptr}; 67 | sensor::Sensor *illum_sensor{nullptr}; 68 | sensor::Sensor *aqi_acc_sensor{nullptr}; 69 | sensor::Sensor *aqi_sensor{nullptr}; 70 | sensor::Sensor *CO2e_sensor{nullptr}; 71 | sensor::Sensor *bVOC_sensor{nullptr}; 72 | sensor::Sensor *particle_duty_sensor{nullptr}; 73 | sensor::Sensor *particle_conc_sensor{nullptr}; 74 | sensor::Sensor *sound_spl_sensor{nullptr}; 75 | sensor::Sensor *sound_peak_sensor{nullptr}; 76 | sensor::Sensor *sound_band0_sensor{nullptr}; 77 | sensor::Sensor *sound_band1_sensor{nullptr}; 78 | sensor::Sensor *sound_band2_sensor{nullptr}; 79 | sensor::Sensor *sound_band3_sensor{nullptr}; 80 | sensor::Sensor *sound_band4_sensor{nullptr}; 81 | sensor::Sensor *sound_band5_sensor{nullptr}; 82 | bool AQIinitialized = false; 83 | }; 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/esphome_components/ms430/sensor.py: -------------------------------------------------------------------------------- 1 | """ 2 | sensor.py 3 | 4 | This file creates an interface so that the MS430 Arduino code can 5 | be used as an external component in ESPHome. 6 | 7 | Suitable for ESP8266, ESP32 and Raspberry Pi Pico W. 8 | 9 | Copyright 2025 Metriful Ltd. 10 | Licensed under the MIT License - for further details see LICENSE.txt 11 | 12 | For code examples, datasheet and user guide, visit 13 | https://github.com/metriful/sensor 14 | """ 15 | 16 | from esphome import codegen 17 | import esphome.config_validation as cv 18 | from esphome.components import sensor 19 | from esphome import const as c 20 | 21 | CONF_WHITE_LIGHT = 'white_light' 22 | CONF_AQI = 'aqi' 23 | CONF_E_CO2 = 'e_co2' 24 | CONF_B_VOC = 'b_voc' 25 | CONF_PARTICLE_DUTY = 'particle_duty' 26 | CONF_PARTICLE_CONC = 'particle_concentration' 27 | CONF_SOUND_SPL = 'sound_spl' 28 | CONF_SOUND_PEAK = 'sound_peak' 29 | UNIT_DECIBEL_A = "dBA" 30 | UNIT_MILLIPASCAL = "mPa" 31 | CONF_SOUND_BAND_0 = 'sound_band_0' 32 | CONF_SOUND_BAND_1 = 'sound_band_1' 33 | CONF_SOUND_BAND_2 = 'sound_band_2' 34 | CONF_SOUND_BAND_3 = 'sound_band_3' 35 | CONF_SOUND_BAND_4 = 'sound_band_4' 36 | CONF_SOUND_BAND_5 = 'sound_band_5' 37 | 38 | ns = codegen.esphome_ns.namespace("ms430") 39 | MS430 = ns.class_("MS430", codegen.Component) 40 | 41 | CONFIG_SCHEMA = cv.Schema( 42 | { 43 | cv.GenerateID(): cv.declare_id(MS430), 44 | cv.Optional(c.CONF_TEMPERATURE): sensor.sensor_schema( 45 | unit_of_measurement=c.UNIT_CELSIUS, 46 | accuracy_decimals=1, 47 | device_class=c.DEVICE_CLASS_TEMPERATURE, 48 | state_class=c.STATE_CLASS_MEASUREMENT, 49 | ), 50 | cv.Optional(c.CONF_HUMIDITY): sensor.sensor_schema( 51 | unit_of_measurement=c.UNIT_PERCENT, 52 | accuracy_decimals=1, 53 | device_class=c.DEVICE_CLASS_HUMIDITY, 54 | state_class=c.STATE_CLASS_MEASUREMENT, 55 | ), 56 | cv.Optional(c.CONF_PRESSURE): sensor.sensor_schema( 57 | unit_of_measurement=c.UNIT_PASCAL, 58 | accuracy_decimals=0, 59 | device_class=c.DEVICE_CLASS_ATMOSPHERIC_PRESSURE, 60 | state_class=c.STATE_CLASS_MEASUREMENT, 61 | ), 62 | cv.Optional(c.CONF_GAS_RESISTANCE): sensor.sensor_schema( 63 | unit_of_measurement=c.UNIT_OHM, 64 | accuracy_decimals=0, 65 | device_class=c.DEVICE_CLASS_AQI, 66 | state_class=c.STATE_CLASS_MEASUREMENT, 67 | ), 68 | cv.Optional(CONF_WHITE_LIGHT): sensor.sensor_schema( 69 | unit_of_measurement=c.UNIT_EMPTY, 70 | accuracy_decimals=0, 71 | device_class=c.DEVICE_CLASS_ILLUMINANCE, 72 | state_class=c.STATE_CLASS_MEASUREMENT, 73 | ), 74 | cv.Optional(c.CONF_ILLUMINANCE): sensor.sensor_schema( 75 | unit_of_measurement=c.UNIT_LUX, 76 | accuracy_decimals=2, 77 | device_class=c.DEVICE_CLASS_ILLUMINANCE, 78 | state_class=c.STATE_CLASS_MEASUREMENT, 79 | ), 80 | cv.Optional(c.CONF_IAQ_ACCURACY): sensor.sensor_schema( 81 | unit_of_measurement=c.UNIT_EMPTY, 82 | accuracy_decimals=0, 83 | device_class=c.DEVICE_CLASS_EMPTY, 84 | state_class=c.STATE_CLASS_MEASUREMENT, 85 | ), 86 | cv.Optional(CONF_AQI): sensor.sensor_schema( 87 | unit_of_measurement=c.UNIT_EMPTY, 88 | accuracy_decimals=1, 89 | device_class=c.DEVICE_CLASS_AQI, 90 | state_class=c.STATE_CLASS_MEASUREMENT, 91 | ), 92 | cv.Optional(CONF_E_CO2): sensor.sensor_schema( 93 | unit_of_measurement=c.UNIT_PARTS_PER_MILLION, 94 | accuracy_decimals=1, 95 | device_class=c.DEVICE_CLASS_GAS, 96 | state_class=c.STATE_CLASS_MEASUREMENT, 97 | ), 98 | cv.Optional(CONF_B_VOC): sensor.sensor_schema( 99 | unit_of_measurement=c.UNIT_PARTS_PER_MILLION, 100 | accuracy_decimals=2, 101 | device_class=c.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, 102 | state_class=c.STATE_CLASS_MEASUREMENT, 103 | ), 104 | cv.Optional(CONF_PARTICLE_DUTY): sensor.sensor_schema( 105 | unit_of_measurement=c.UNIT_PERCENT, 106 | accuracy_decimals=2, 107 | device_class=c.DEVICE_CLASS_EMPTY, 108 | state_class=c.STATE_CLASS_MEASUREMENT, 109 | ), 110 | cv.Optional(CONF_PARTICLE_CONC): sensor.sensor_schema( 111 | unit_of_measurement=c.UNIT_MICROGRAMS_PER_CUBIC_METER, 112 | accuracy_decimals=2, 113 | device_class=c.DEVICE_CLASS_EMPTY, 114 | state_class=c.STATE_CLASS_MEASUREMENT, 115 | ), 116 | cv.Optional(CONF_SOUND_SPL): sensor.sensor_schema( 117 | unit_of_measurement=UNIT_DECIBEL_A, 118 | accuracy_decimals=1, 119 | device_class=c.DEVICE_CLASS_SOUND_PRESSURE, 120 | state_class=c.STATE_CLASS_MEASUREMENT, 121 | ), 122 | cv.Optional(CONF_SOUND_PEAK): sensor.sensor_schema( 123 | unit_of_measurement=UNIT_MILLIPASCAL, 124 | accuracy_decimals=2, 125 | device_class=c.DEVICE_CLASS_SOUND_PRESSURE, 126 | state_class=c.STATE_CLASS_MEASUREMENT, 127 | ), 128 | cv.Optional(CONF_SOUND_BAND_0): sensor.sensor_schema( 129 | unit_of_measurement=c.UNIT_DECIBEL, 130 | accuracy_decimals=1, 131 | device_class=c.DEVICE_CLASS_SOUND_PRESSURE, 132 | state_class=c.STATE_CLASS_MEASUREMENT, 133 | ), 134 | cv.Optional(CONF_SOUND_BAND_1): sensor.sensor_schema( 135 | unit_of_measurement=c.UNIT_DECIBEL, 136 | accuracy_decimals=1, 137 | device_class=c.DEVICE_CLASS_SOUND_PRESSURE, 138 | state_class=c.STATE_CLASS_MEASUREMENT, 139 | ), 140 | cv.Optional(CONF_SOUND_BAND_2): sensor.sensor_schema( 141 | unit_of_measurement=c.UNIT_DECIBEL, 142 | accuracy_decimals=1, 143 | device_class=c.DEVICE_CLASS_SOUND_PRESSURE, 144 | state_class=c.STATE_CLASS_MEASUREMENT, 145 | ), 146 | cv.Optional(CONF_SOUND_BAND_3): sensor.sensor_schema( 147 | unit_of_measurement=c.UNIT_DECIBEL, 148 | accuracy_decimals=1, 149 | device_class=c.DEVICE_CLASS_SOUND_PRESSURE, 150 | state_class=c.STATE_CLASS_MEASUREMENT, 151 | ), 152 | cv.Optional(CONF_SOUND_BAND_4): sensor.sensor_schema( 153 | unit_of_measurement=c.UNIT_DECIBEL, 154 | accuracy_decimals=1, 155 | device_class=c.DEVICE_CLASS_SOUND_PRESSURE, 156 | state_class=c.STATE_CLASS_MEASUREMENT, 157 | ), 158 | cv.Optional(CONF_SOUND_BAND_5): sensor.sensor_schema( 159 | unit_of_measurement=c.UNIT_DECIBEL, 160 | accuracy_decimals=1, 161 | device_class=c.DEVICE_CLASS_SOUND_PRESSURE, 162 | state_class=c.STATE_CLASS_MEASUREMENT, 163 | ) 164 | } 165 | ).extend(cv.COMPONENT_SCHEMA) 166 | 167 | 168 | async def to_code(config): 169 | var = codegen.new_Pvariable(config[c.CONF_ID]) 170 | await codegen.register_component(var, config) 171 | 172 | if c.CONF_TEMPERATURE in config: 173 | sens = await sensor.new_sensor(config[c.CONF_TEMPERATURE]) 174 | codegen.add(var.set_temperature_s(sens)) 175 | if c.CONF_HUMIDITY in config: 176 | sens = await sensor.new_sensor(config[c.CONF_HUMIDITY]) 177 | codegen.add(var.set_humidity_s(sens)) 178 | if c.CONF_PRESSURE in config: 179 | sens = await sensor.new_sensor(config[c.CONF_PRESSURE]) 180 | codegen.add(var.set_pressure_s(sens)) 181 | if c.CONF_GAS_RESISTANCE in config: 182 | sens = await sensor.new_sensor(config[c.CONF_GAS_RESISTANCE]) 183 | codegen.add(var.set_gas_s(sens)) 184 | if CONF_WHITE_LIGHT in config: 185 | sens = await sensor.new_sensor(config[CONF_WHITE_LIGHT]) 186 | codegen.add(var.set_w_light_s(sens)) 187 | if c.CONF_ILLUMINANCE in config: 188 | sens = await sensor.new_sensor(config[c.CONF_ILLUMINANCE]) 189 | codegen.add(var.set_illum_s(sens)) 190 | if c.CONF_IAQ_ACCURACY in config: 191 | sens = await sensor.new_sensor(config[c.CONF_IAQ_ACCURACY]) 192 | codegen.add(var.set_aqi_acc_s(sens)) 193 | if CONF_AQI in config: 194 | sens = await sensor.new_sensor(config[CONF_AQI]) 195 | codegen.add(var.set_aqi_s(sens)) 196 | if CONF_E_CO2 in config: 197 | sens = await sensor.new_sensor(config[CONF_E_CO2]) 198 | codegen.add(var.set_CO2e_s(sens)) 199 | if CONF_B_VOC in config: 200 | sens = await sensor.new_sensor(config[CONF_B_VOC]) 201 | codegen.add(var.set_bVOC_s(sens)) 202 | if CONF_PARTICLE_DUTY in config: 203 | sens = await sensor.new_sensor(config[CONF_PARTICLE_DUTY]) 204 | codegen.add(var.set_particle_duty_s(sens)) 205 | if CONF_PARTICLE_CONC in config: 206 | sens = await sensor.new_sensor(config[CONF_PARTICLE_CONC]) 207 | codegen.add(var.set_particle_conc_s(sens)) 208 | if CONF_SOUND_SPL in config: 209 | sens = await sensor.new_sensor(config[CONF_SOUND_SPL]) 210 | codegen.add(var.set_sound_spl_s(sens)) 211 | if CONF_SOUND_PEAK in config: 212 | sens = await sensor.new_sensor(config[CONF_SOUND_PEAK]) 213 | codegen.add(var.set_sound_peak_s(sens)) 214 | if CONF_SOUND_BAND_0 in config: 215 | sens = await sensor.new_sensor(config[CONF_SOUND_BAND_0]) 216 | codegen.add(var.set_sound_band0_s(sens)) 217 | if CONF_SOUND_BAND_1 in config: 218 | sens = await sensor.new_sensor(config[CONF_SOUND_BAND_1]) 219 | codegen.add(var.set_sound_band1_s(sens)) 220 | if CONF_SOUND_BAND_2 in config: 221 | sens = await sensor.new_sensor(config[CONF_SOUND_BAND_2]) 222 | codegen.add(var.set_sound_band2_s(sens)) 223 | if CONF_SOUND_BAND_3 in config: 224 | sens = await sensor.new_sensor(config[CONF_SOUND_BAND_3]) 225 | codegen.add(var.set_sound_band3_s(sens)) 226 | if CONF_SOUND_BAND_4 in config: 227 | sens = await sensor.new_sensor(config[CONF_SOUND_BAND_4]) 228 | codegen.add(var.set_sound_band4_s(sens)) 229 | if CONF_SOUND_BAND_5 in config: 230 | sens = await sensor.new_sensor(config[CONF_SOUND_BAND_5]) 231 | codegen.add(var.set_sound_band5_s(sens)) 232 | -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/host_pin_definitions.h: -------------------------------------------------------------------------------- 1 | /* 2 | host_pin_definitions.h 3 | 4 | This file defines which host pins are used to interface to the 5 | Metriful MS430 board. The relevant file section is selected 6 | automatically when the board is chosen in the Arduino IDE. 7 | 8 | More detail is provided in the readme. 9 | 10 | This file provides settings for the following host systems: 11 | * Arduino Uno 12 | * Arduino Nano 33 IoT 13 | * Arduino Nano 14 | * Arduino MKR WiFi 1010 15 | * ESP8266 (tested on NodeMCU and Wemos D1 Mini - other boards may require changes) 16 | * ESP32 (tested on DOIT ESP32 DEVKIT V1 - other boards may require changes) 17 | * Raspberry Pi Pico (including WiFi version) 18 | 19 | The Metriful MS430 is compatible with many more development boards 20 | than those listed. You can use this file as a guide to define the 21 | necessary settings for other host systems. 22 | 23 | Copyright 2020-2023 Metriful Ltd. 24 | Licensed under the MIT License - for further details see LICENSE.txt 25 | 26 | For code examples, datasheet and user guide, visit 27 | https://github.com/metriful/sensor 28 | */ 29 | 30 | #ifndef ARDUINO_PIN_DEFINITIONS_H 31 | #define ARDUINO_PIN_DEFINITIONS_H 32 | 33 | #ifdef ARDUINO_AVR_UNO 34 | 35 | // Arduino Uno 36 | 37 | #define ISR_ATTRIBUTE 38 | 39 | #define READY_PIN 2 // Arduino digital pin 2 connects to RDY 40 | #define L_INT_PIN 4 // Arduino digital pin 4 connects to LIT 41 | #define S_INT_PIN 7 // Arduino digital pin 7 connects to SIT 42 | /* Also make the following connections: 43 | Arduino pins GND, SCL, SDA to MS430 pins GND, SCL, SDA 44 | Arduino pin 5V to MS430 pins VPU and VIN 45 | MS430 pin VDD is unused 46 | 47 | If a PPD42 particle sensor is used, connect the following: 48 | Arduino pin 5V to PPD42 pin 3 49 | Arduino pin GND to PPD42 pin 1 50 | PPD42 pin 4 to MS430 pin PRT 51 | 52 | If an SDS011 particle sensor is used, connect the following: 53 | Arduino pin 5V to SDS011 pin "5V" 54 | Arduino pin GND to SDS011 pin "GND" 55 | SDS011 pin "25um" to MS430 pin PRT 56 | */ 57 | 58 | #elif defined ARDUINO_SAMD_NANO_33_IOT 59 | 60 | // Arduino Nano 33 IoT 61 | 62 | #include 63 | #include 64 | #define HAS_WIFI 65 | #define ISR_ATTRIBUTE 66 | 67 | #define READY_PIN 11 // Arduino pin D11 connects to RDY 68 | #define L_INT_PIN A1 // Arduino pin A1 connects to LIT 69 | #define S_INT_PIN A2 // Arduino pin A2 connects to SIT 70 | /* Also make the following connections: 71 | Arduino pin GND to MS430 pin GND 72 | Arduino pin 3.3V to MS430 pins VPU and VDD 73 | Arduino pin A5 to MS430 pin SCL 74 | Arduino pin A4 to MS430 pin SDA 75 | MS430 pin VIN is unused 76 | 77 | If a PPD42 particle sensor is used, connect the following: 78 | Arduino pin VUSB to PPD42 pin 3 79 | Arduino pin GND to PPD42 pin 1 80 | PPD42 pin 4 to MS430 pin PRT 81 | 82 | If an SDS011 particle sensor is used, connect the following: 83 | Arduino pin VUSB to SDS011 pin "5V" 84 | Arduino pin GND to SDS011 pin "GND" 85 | SDS011 pin "25um" to MS430 pin PRT 86 | 87 | The solder bridge labeled "VUSB" on the underside of the Arduino 88 | must be soldered closed to provide 5V to the PPD42/SDS011. 89 | */ 90 | 91 | #elif defined ARDUINO_AVR_NANO 92 | 93 | // Arduino Nano 94 | 95 | #define ISR_ATTRIBUTE 96 | 97 | #define READY_PIN 2 // Arduino pin D2 connects to RDY 98 | #define L_INT_PIN 4 // Arduino pin D4 connects to LIT 99 | #define S_INT_PIN 7 // Arduino pin D7 connects to SIT 100 | /* Also make the following connections: 101 | Arduino pin GND to MS430 pin GND 102 | Arduino pin A5 (SCL) to MS430 pin SCL 103 | Arduino pin A4 (SDA) to MS430 pin SDA 104 | Arduino pin 5V to MS430 pins VPU and VIN 105 | MS430 pin VDD is unused 106 | 107 | If a PPD42 particle sensor is used, connect the following: 108 | Arduino pin 5V to PPD42 pin 3 109 | Arduino pin GND to PPD42 pin 1 110 | PPD42 pin 4 to MS430 pin PRT 111 | 112 | If an SDS011 particle sensor is used, connect the following: 113 | Arduino pin 5V to SDS011 pin "5V" 114 | Arduino pin GND to SDS011 pin "GND" 115 | SDS011 pin "25um" to MS430 pin PRT 116 | */ 117 | 118 | #elif defined ARDUINO_SAMD_MKRWIFI1010 119 | 120 | // Arduino MKR WiFi 1010 121 | 122 | #include 123 | #include 124 | #define HAS_WIFI 125 | #define ISR_ATTRIBUTE 126 | 127 | #define READY_PIN 0 // Arduino digital pin 0 connects to RDY 128 | #define L_INT_PIN 4 // Arduino digital pin 4 connects to LIT 129 | #define S_INT_PIN 5 // Arduino digital pin 5 connects to SIT 130 | /* Also make the following connections: 131 | Arduino pin GND to MS430 pin GND 132 | Arduino pin D12 (SCL) to MS430 pin SCL 133 | Arduino pin D11 (SDA) to MS430 pin SDA 134 | Arduino pin VCC (3.3V) to MS430 pins VPU and VDD 135 | MS430 pin VIN is unused 136 | 137 | If a PPD42 particle sensor is used, connect the following: 138 | Arduino pin 5V to PPD42 pin 3 139 | Arduino pin GND to PPD42 pin 1 140 | PPD42 pin 4 to MS430 pin PRT 141 | 142 | If an SDS011 particle sensor is used, connect the following: 143 | Arduino pin 5V to SDS011 pin "5V" 144 | Arduino pin GND to SDS011 pin "GND" 145 | SDS011 pin "25um" to MS430 pin PRT 146 | */ 147 | 148 | #elif defined ESP8266 149 | 150 | // The examples have been tested on NodeMCU and Wemos D1 Mini. 151 | // Other ESP8266 boards may require changes. 152 | 153 | #include 154 | #define HAS_WIFI 155 | #define ISR_ATTRIBUTE IRAM_ATTR 156 | 157 | #define SDA_PIN 4 // GPIO4 (labeled D2) connects to SDA 158 | #define SCL_PIN 5 // GPIO5 (labeled D1) connects to SCL 159 | #define READY_PIN 12 // GPIO12 (labeled D6) connects to RDY 160 | #define L_INT_PIN 0 // GPIO0 (labeled D3) connects to LIT 161 | #define S_INT_PIN 14 // GPIO14 (labeled D5) connects to SIT 162 | /* Also make the following connections: 163 | ESP8266 pin GND to MS430 pin GND 164 | ESP8266 pin 3V3 to MS430 pins VPU and VDD 165 | MS430 pin VIN is unused 166 | 167 | If a PPD42 particle sensor is used, also connect the following: 168 | ESP8266 pin Vin (may be labeled Vin or 5V or VU) to PPD42 pin 3 169 | ESP8266 pin GND to PPD42 pin 1 170 | PPD42 pin 4 to MS430 pin PRT 171 | 172 | If an SDS011 particle sensor is used, connect the following: 173 | ESP8266 pin Vin (may be labeled Vin or 5V or VU) to SDS011 pin "5V" 174 | ESP8266 pin GND to SDS011 pin "GND" 175 | SDS011 pin "25um" to MS430 pin PRT 176 | */ 177 | 178 | #elif defined ESP32 179 | 180 | // The examples have been tested on DOIT ESP32 DEVKIT V1 development board. 181 | // Other ESP32 boards may require changes. 182 | 183 | #include 184 | #define HAS_WIFI 185 | #define ISR_ATTRIBUTE IRAM_ATTR 186 | 187 | #define READY_PIN 23 // Pin D23 connects to RDY 188 | #define L_INT_PIN 18 // Pin D18 connects to LIT 189 | #define S_INT_PIN 19 // Pin D19 connects to SIT 190 | /* Also make the following connections: 191 | ESP32 pin D21 to MS430 pin SDA 192 | ESP32 pin D22 to MS430 pin SCL 193 | ESP32 pin GND to MS430 pin GND 194 | ESP32 pin 3V3 to MS430 pins VPU and VDD 195 | MS430 pin VIN is unused 196 | 197 | If a PPD42 particle sensor is used, also connect the following: 198 | ESP32 pin Vin to PPD42 pin 3 199 | ESP32 pin GND to PPD42 pin 1 200 | PPD42 pin 4 to MS430 pin PRT 201 | 202 | If an SDS011 particle sensor is used, connect the following: 203 | ESP32 pin Vin to SDS011 pin "5V" 204 | ESP32 pin GND to SDS011 pin "GND" 205 | SDS011 pin "25um" to MS430 pin PRT 206 | */ 207 | 208 | #elif defined ARDUINO_ARCH_RP2040 209 | 210 | // The examples have been tested on the official Raspberry Pi Pico and 211 | // Pico W development boards. Other Pico/RP2040 boards may require changes. 212 | 213 | #ifdef ARDUINO_RASPBERRY_PI_PICO_W 214 | #include 215 | #define HAS_WIFI 216 | #endif 217 | 218 | #define ISR_ATTRIBUTE 219 | #define SDA_PIN 20 // GP20 (Pin 26) connects to SDA 220 | #define SCL_PIN 21 // GP21 (Pin 27) connects to SCL 221 | #define READY_PIN 28 // GP28 (Pin 34) connects to RDY 222 | #define L_INT_PIN 26 // GP26 (Pin 31) connects to LIT 223 | #define S_INT_PIN 27 // GP27 (Pin 32) connects to SIT 224 | /* Also make the following connections: 225 | MS430 pin GND to any of the Pico GND pins 226 | Pico pin 36 to MS430 pins VPU and VDD 227 | MS430 pin VIN is unused 228 | 229 | If a PPD42 particle sensor is used, also connect the following: 230 | Pico pin 40 to PPD42 pin 3 231 | PPD42 pin 1 to any of the Pico GND pins 232 | PPD42 pin 4 to MS430 pin PRT 233 | 234 | If an SDS011 particle sensor is used, connect the following: 235 | Pico pin 40 to SDS011 pin "5V" 236 | SDS011 pin "GND" to any of the Pico GND pins 237 | SDS011 pin "25um" to MS430 pin PRT 238 | */ 239 | 240 | #else 241 | #error ("Your development board is not yet supported.") 242 | // Please make a new section in this file to define 243 | // the correct input/output pins. 244 | #endif 245 | 246 | #endif 247 | -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/sensor_constants.h: -------------------------------------------------------------------------------- 1 | /* 2 | sensor_constants.h 3 | 4 | This file defines constant values and data structures which are used 5 | in the control of the Metriful MS430 board and the interpretation of 6 | its output data. All values have been taken from the MS430 datasheet. 7 | 8 | Copyright 2020-2023 Metriful Ltd. 9 | Licensed under the MIT License - for further details see LICENSE.txt 10 | 11 | For code examples, datasheet and user guide, visit 12 | https://github.com/metriful/sensor 13 | */ 14 | 15 | #ifndef SENSOR_CONSTANTS_H 16 | #define SENSOR_CONSTANTS_H 17 | 18 | #include 19 | 20 | /////////////////////////////////////////////////////////// 21 | // Register and command addresses: 22 | 23 | // Settings registers 24 | #define PARTICLE_SENSOR_SELECT_REG 0x07 25 | 26 | #define LIGHT_INTERRUPT_ENABLE_REG 0x81 27 | #define LIGHT_INTERRUPT_THRESHOLD_REG 0x82 28 | #define LIGHT_INTERRUPT_TYPE_REG 0x83 29 | #define LIGHT_INTERRUPT_POLARITY_REG 0x84 30 | 31 | #define SOUND_INTERRUPT_ENABLE_REG 0x85 32 | #define SOUND_INTERRUPT_THRESHOLD_REG 0x86 33 | #define SOUND_INTERRUPT_TYPE_REG 0x87 34 | 35 | #define CYCLE_TIME_PERIOD_REG 0x89 36 | 37 | // Executable commands 38 | #define ON_DEMAND_MEASURE_CMD 0xE1 39 | #define RESET_CMD 0xE2 40 | #define CYCLE_MODE_CMD 0xE4 41 | #define STANDBY_MODE_CMD 0xE5 42 | #define LIGHT_INTERRUPT_CLR_CMD 0xE6 43 | #define SOUND_INTERRUPT_CLR_CMD 0xE7 44 | 45 | // Read the operational mode 46 | #define OP_MODE_READ 0x8A 47 | 48 | // Read data for whole categories 49 | #define AIR_DATA_READ 0x10 50 | #define AIR_QUALITY_DATA_READ 0x11 51 | #define LIGHT_DATA_READ 0x12 52 | #define SOUND_DATA_READ 0x13 53 | #define PARTICLE_DATA_READ 0x14 54 | 55 | // Read individual data quantities 56 | #define T_READ 0x21 57 | #define P_READ 0x22 58 | #define H_READ 0x23 59 | #define G_READ 0x24 60 | 61 | #define AQI_READ 0x25 62 | #define CO2E_READ 0x26 63 | #define BVOC_READ 0x27 64 | #define AQI_ACCURACY_READ 0x28 65 | 66 | #define ILLUMINANCE_READ 0x31 67 | #define WHITE_LIGHT_READ 0x32 68 | 69 | #define SPL_READ 0x41 70 | #define SPL_BANDS_READ 0x42 71 | #define SOUND_PEAK_READ 0x43 72 | #define SOUND_STABLE_READ 0x44 73 | 74 | #define DUTY_CYCLE_READ 0x51 75 | #define CONCENTRATION_READ 0x52 76 | #define PARTICLE_VALID_READ 0x53 77 | 78 | /////////////////////////////////////////////////////////// 79 | 80 | // I2C address of sensor board: can select using solder bridge 81 | #define I2C_ADDR_7BIT_SB_OPEN 0x71 // if solder bridge is left open 82 | #define I2C_ADDR_7BIT_SB_CLOSED 0x70 // if solder bridge is soldered closed 83 | 84 | // Values for enabling/disabling of sensor functions 85 | #define ENABLED 1 86 | #define DISABLED 0 87 | 88 | // Device modes 89 | #define STANDBY_MODE 0 90 | #define CYCLE_MODE 1 91 | 92 | // Sizes of data expected when setting interrupt thresholds 93 | #define LIGHT_INTERRUPT_THRESHOLD_BYTES 3 94 | #define SOUND_INTERRUPT_THRESHOLD_BYTES 2 95 | 96 | // Frequency bands for sound level measurement 97 | #define SOUND_FREQ_BANDS 6 98 | static const uint16_t sound_band_mids_Hz[SOUND_FREQ_BANDS] = 99 | {125, 250, 500, 1000, 2000, 4000}; 100 | static const uint16_t sound_band_edges_Hz[SOUND_FREQ_BANDS+1] = 101 | {88, 177, 354, 707, 1414, 2828, 5657}; 102 | 103 | // Cycle mode time period 104 | #define CYCLE_PERIOD_3_S 0 105 | #define CYCLE_PERIOD_100_S 1 106 | #define CYCLE_PERIOD_300_S 2 107 | 108 | // Sound interrupt type: 109 | #define SOUND_INT_TYPE_LATCH 0 110 | #define SOUND_INT_TYPE_COMP 1 111 | 112 | // Maximum for illuminance measurement and threshold setting 113 | #define MAX_LUX_VALUE 3774 114 | 115 | // Light interrupt type: 116 | #define LIGHT_INT_TYPE_LATCH 0 117 | #define LIGHT_INT_TYPE_COMP 1 118 | 119 | // Light interrupt polarity: 120 | #define LIGHT_INT_POL_POSITIVE 0 121 | #define LIGHT_INT_POL_NEGATIVE 1 122 | 123 | // Decoding the temperature integer.fraction value format 124 | #define TEMPERATURE_VALUE_MASK 0x7F 125 | #define TEMPERATURE_SIGN_MASK 0x80 126 | 127 | // Particle sensor module selection: 128 | #define PARTICLE_SENSOR_OFF 0 129 | #define PARTICLE_SENSOR_PPD42 1 130 | #define PARTICLE_SENSOR_SDS011 2 131 | 132 | /////////////////////////////////////////////////////////// 133 | 134 | // Structs for accessing individual data quantities after 135 | // reading a category of data. 136 | 137 | typedef struct __attribute__((packed)) 138 | { 139 | uint8_t T_C_int_with_sign; 140 | uint8_t T_C_fr_1dp; 141 | uint32_t P_Pa; 142 | uint8_t H_pc_int; 143 | uint8_t H_pc_fr_1dp; 144 | uint32_t G_ohm; 145 | } AirData_t; 146 | 147 | typedef struct __attribute__((packed)) 148 | { 149 | uint16_t AQI_int; 150 | uint8_t AQI_fr_1dp; 151 | uint16_t CO2e_int; 152 | uint8_t CO2e_fr_1dp; 153 | uint16_t bVOC_int; 154 | uint8_t bVOC_fr_2dp; 155 | uint8_t AQI_accuracy; 156 | } AirQualityData_t; 157 | 158 | typedef struct __attribute__((packed)) 159 | { 160 | uint16_t illum_lux_int; 161 | uint8_t illum_lux_fr_2dp; 162 | uint16_t white; 163 | } LightData_t; 164 | 165 | typedef struct __attribute__((packed)) 166 | { 167 | uint8_t SPL_dBA_int; 168 | uint8_t SPL_dBA_fr_1dp; 169 | uint8_t SPL_bands_dB_int[SOUND_FREQ_BANDS]; 170 | uint8_t SPL_bands_dB_fr_1dp[SOUND_FREQ_BANDS]; 171 | uint16_t peak_amp_mPa_int; 172 | uint8_t peak_amp_mPa_fr_2dp; 173 | uint8_t stable; 174 | } SoundData_t; 175 | 176 | typedef struct __attribute__((packed)) 177 | { 178 | uint8_t duty_cycle_pc_int; 179 | uint8_t duty_cycle_pc_fr_2dp; 180 | uint16_t concentration_int; 181 | uint8_t concentration_fr_2dp; 182 | uint8_t valid; 183 | } ParticleData_t; 184 | 185 | /////////////////////////////////////////////////////////// 186 | 187 | // Byte lengths for each readable data quantity and data category 188 | 189 | #define T_BYTES 2 190 | #define P_BYTES 4 191 | #define H_BYTES 2 192 | #define G_BYTES 4 193 | #define AIR_DATA_BYTES sizeof(AirData_t) 194 | 195 | #define AQI_BYTES 3 196 | #define CO2E_BYTES 3 197 | #define BVOC_BYTES 3 198 | #define AQI_ACCURACY_BYTES 1 199 | #define AIR_QUALITY_DATA_BYTES sizeof(AirQualityData_t) 200 | 201 | #define ILLUMINANCE_BYTES 3 202 | #define WHITE_BYTES 2 203 | #define LIGHT_DATA_BYTES sizeof(LightData_t) 204 | 205 | #define SPL_BYTES 2 206 | #define SPL_BANDS_BYTES (2*SOUND_FREQ_BANDS) 207 | #define SOUND_PEAK_BYTES 3 208 | #define SOUND_STABLE_BYTES 1 209 | #define SOUND_DATA_BYTES sizeof(SoundData_t) 210 | 211 | #define DUTY_CYCLE_BYTES 2 212 | #define CONCENTRATION_BYTES 3 213 | #define PARTICLE_VALID_BYTES 1 214 | #define PARTICLE_DATA_BYTES sizeof(ParticleData_t) 215 | 216 | #endif 217 | -------------------------------------------------------------------------------- /Arduino/Metriful_Sensor/text_web_page.h: -------------------------------------------------------------------------------- 1 | /* 2 | text_web_page.h 3 | 4 | This file contains parts of the web page code which is used in the 5 | web_server example. 6 | 7 | Copyright 2020-2023 Metriful Ltd. 8 | Licensed under the MIT License - for further details see LICENSE.txt 9 | 10 | For code examples, datasheet and user guide, visit 11 | https://github.com/metriful/sensor 12 | */ 13 | 14 | #ifndef TEXT_WEB_PAGE_H 15 | #define TEXT_WEB_PAGE_H 16 | 17 | #define QUOTE(...) #__VA_ARGS__ 18 | 19 | // This is the HTTP response header. Variable = refresh time in seconds. 20 | const char * responseHeader = "HTTP/1.1 200 OK\r\n" 21 | "Content-type: text/html\r\n" 22 | "Connection: close\r\n" 23 | "Refresh: %u\r\n\r\n"; 24 | 25 | // This is the web page up to the start of the table data. No variables. 26 | const char * pageStart = QUOTE( 27 | 28 | 29 | 30 | 31 | 32 | Metriful Sensor Demo 33 | 48 | 49 | 50 |

Indoor Environment Data

51 | ); 52 | 53 | // Start of a data table. Variable = title. 54 | const char * tableStart = QUOTE( 55 |

56 |

%s

57 | 58 | ); 59 | 60 | // A table row. 61 | // Variables = data name, class number, value, unit 62 | const char * tableRow = QUOTE( 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | 70 | // End of a data table. No variables. 71 | const char * tableEnd = QUOTE( 72 |
%s%s%s
73 |

74 | ); 75 | 76 | // End of the web page. No variables. 77 | const char * pageEnd = QUOTE( 78 |

79 | sensor.metriful.com 80 |

81 | 82 | 83 | ); 84 | 85 | #endif 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the project software and documentation will be documented in this file. 3 | 4 | ## [3.3.0] - 2025-03-19 5 | ### Changed 6 | - Update ESPHome support using new External Components feature 7 | 8 | ## [3.2.1] - 2024-01-04 9 | ### Fixed 10 | - Wiring table for ESP8266 in README.md had incorrect SCL and SDA pin identification (D and GPIO numbers). 11 | 12 | ## [3.2.0] - 2023-11-19 13 | ### Added 14 | - Support for Raspberry Pi Pico (W) 15 | - ESPHome support for Home Assistant 16 | 17 | ### Changed 18 | - Swap SDA and SCL pin definitions for ESP8266 in host_pin_definitions.h to match the standard defaults. 19 | - Improve web page formatting and introduce jinja2 templating in Python. 20 | - Improve string formatting in Arduino code. 21 | 22 | ## [3.1.0] - 2020-11-16 23 | ### Added 24 | - Fahrenheit temperature output. 25 | - Improved support for ESP8266. 26 | - Support for ESP32. 27 | - IFTTT example. 28 | - Home Assistant example. 29 | - Graph viewer software for cross-platform, real-time data monitoring. 30 | - Graph webpage server example using Plotly.js 31 | - Text webpage server added to Python examples (to match the one for Arduino). 32 | - Updated the User Guide (v2.1) for the new examples. 33 | 34 | ### Changed 35 | - **All software changes are backwards-compatible so old programs should still work.** 36 | - The Raspberry Pi Python code now uses a package folder, so the import command has changed slightly. 37 | - Arduino Nano 33 IoT now uses hardware I2C port, so the previously-used software I2C library is no longer needed. This requires a re-wire of two pins for this host only. 38 | - The particle sensor being used (or not) is now set **once** rather than in each example code file - see readme for details. 39 | - The I2C address in the Arduino examples is now set **once** rather than in each example code file. 40 | - Small changes in most code files. 41 | 42 | 43 | ## [3.0.1] - 2020-10-14 44 | ### Fixed 45 | - Arduino IoT_cloud_logging HTTP request problem. 46 | 47 | 48 | ## [3.0.0] - 2020-09-06 49 | ### Changed 50 | - First release after hardware delivery. 51 | - Datasheet (v2.0), user guide (v2.0), readme and code comments were edited. 52 | 53 | -------------------------------------------------------------------------------- /DISCLAIMER.txt: -------------------------------------------------------------------------------- 1 | Legal Disclaimer Notice 2 | 3 | ALL PRODUCTS, PRODUCT SPECIFICATIONS AND DATA ARE SUBJECT TO CHANGE WITHOUT NOTICE TO IMPROVE RELIABILITY, FUNCTION OR DESIGN, OR OTHERWISE. 4 | 5 | Metriful Ltd., its affiliates, agents, and employees, and all persons acting on its or their behalf (collectively, “Metriful”), disclaim any and all liability for any errors, inaccuracies or incompleteness contained in any datasheet or in any other disclosure. 6 | 7 | Metriful makes no warranty, representation or guarantee regarding the suitability of the products for any particular purpose or the continuing production of any product. To the maximum extent permitted by applicable law, Metriful disclaims (i) any and all liability arising out of the application or use of any product or data, (ii) any and all liability, including without limitation special, consequential or incidental damages, and (iii) any and all implied warranties, including warranties of fitness for particular purpose, non-infringement and merchantability. 8 | 9 | Statements regarding the suitability of products for certain types of applications are based on Metriful’s knowledge of typical requirements that are often placed on Metriful products in generic applications. Such statements are not binding statements about the suitability of products for a particular application. It is the customer’s responsibility to validate that a particular product is suitable for use in a particular application. 10 | 11 | Parameters provided in datasheets and / or specifications may vary in different applications and performance may vary over time. All operating parameters, including typical parameters, must be validated for each customer application by the customer. Product specifications do not expand or otherwise modify Metriful’s terms and conditions of purchase, including but not limited to the warranty expressed therein. Product photographs and diagrams are for illustration purposes only and may differ from the real product appearance. 12 | 13 | Metriful products are not designed for use in medical, life-saving, safety, or life-sustaining applications or for any other application in which the failure of the Metriful product could result in personal injury or death. Customers using or selling Metriful products for use in such applications do so at their own risk. No information contained in any Metriful document or disclosure constitutes health or safety advice. 14 | 15 | 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. 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Datasheet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metriful/sensor/751fc1a6fce5657c1600a4088bb6f1746aad5e1f/Datasheet.pdf -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020-2023 Metriful Ltd. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/Home_Assistant.py: -------------------------------------------------------------------------------- 1 | """Example of sending data from the Metriful MS430 to Home Assistant. 2 | 3 | This example is designed to run with Python 3 on a Raspberry Pi and 4 | requires an installation of Home Assistant (www.home-assistant.io) 5 | on your home network. 6 | 7 | Data are sent at regular intervals over your local network, can be 8 | viewed on the Home Assistant dashboard, and can be used to control 9 | home automation tasks. More setup information is provided in the 10 | Readme and User Guide. 11 | """ 12 | 13 | # Copyright 2020-2023 Metriful Ltd. 14 | # Licensed under the MIT License - for further details see LICENSE.txt 15 | 16 | # For code examples, datasheet and user guide, visit 17 | # https://github.com/metriful/sensor 18 | 19 | import requests 20 | import time 21 | import sensor_package.sensor_functions as sensor 22 | import sensor_package.sensor_constants as const 23 | 24 | ######################################################### 25 | # USER-EDITABLE SETTINGS 26 | 27 | # How often to read and report the data (every 3, 100 or 300 seconds) 28 | cycle_period = const.CYCLE_PERIOD_100_S 29 | 30 | # Home Assistant settings 31 | 32 | # You must have already installed Home Assistant on a computer on your 33 | # network. Go to www.home-assistant.io for help on this. 34 | 35 | # Choose a unique name for this MS430 sensor board so you can identify it. 36 | # Variables in HA will have names like: SENSOR_NAME.temperature, etc. 37 | SENSOR_NAME = "kitchen3" 38 | 39 | # Specify the IP address of the computer running Home Assistant. 40 | # You can find this from the admin interface of your router. 41 | HOME_ASSISTANT_IP = "192.168.43.144" 42 | 43 | # Security access token: the Readme and User Guide explain how to get this 44 | LONG_LIVED_ACCESS_TOKEN = "PASTE YOUR TOKEN HERE WITHIN QUOTES" 45 | 46 | # END OF USER-EDITABLE SETTINGS 47 | ######################################################### 48 | 49 | # Set up the GPIO and I2C communications bus 50 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 51 | 52 | # Apply the settings to the MS430 53 | I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, 54 | const.PARTICLE_SENSOR_SELECT_REG, 55 | [sensor.PARTICLE_SENSOR]) 56 | I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, 57 | const.CYCLE_TIME_PERIOD_REG, [cycle_period]) 58 | 59 | ######################################################### 60 | 61 | print("Reporting data to Home Assistant. Press ctrl-c to exit.") 62 | 63 | # Enter cycle mode 64 | I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) 65 | 66 | while (True): 67 | 68 | # Wait for the next new data release, indicated by a falling edge on READY 69 | while (not GPIO.event_detected(sensor.READY_pin)): 70 | time.sleep(0.05) 71 | 72 | # Now read all data from the MS430 73 | air_data = sensor.get_air_data(I2C_bus) 74 | air_quality_data = sensor.get_air_quality_data(I2C_bus) 75 | light_data = sensor.get_light_data(I2C_bus) 76 | sound_data = sensor.get_sound_data(I2C_bus) 77 | particle_data = sensor.get_particle_data(I2C_bus, sensor.PARTICLE_SENSOR) 78 | 79 | # Specify information needed by Home Assistant. 80 | # Icons are chosen from https://cdn.materialdesignicons.com/5.3.45/ 81 | # (remove the "mdi-" part from the icon name). 82 | pressure = dict(name='Pressure', data=air_data['P_Pa'], unit='Pa', 83 | icon='weather-cloudy', decimals=0) 84 | humidity = dict(name='Humidity', data=air_data['H_pc'], unit='%', 85 | icon='water-percent', decimals=1) 86 | temperature = dict(name='Temperature', data=air_data['T'], 87 | unit=air_data['T_unit'], icon='thermometer', decimals=1) 88 | illuminance = dict(name='Illuminance', data=light_data['illum_lux'], 89 | unit='lx', icon='white-balance-sunny', decimals=2) 90 | sound_level = dict(name='Sound level', data=sound_data['SPL_dBA'], 91 | unit='dBA', icon='microphone', decimals=1) 92 | sound_peak = dict(name='Sound peak', data=sound_data['peak_amp_mPa'], 93 | unit='mPa', icon='waveform', decimals=2) 94 | AQI = dict(name='Air Quality Index', data=air_quality_data['AQI'], 95 | unit=' ', icon='thought-bubble-outline', decimals=1) 96 | AQI_interpret = dict(name='Air quality assessment', 97 | data=sensor.interpret_AQI_value( 98 | air_quality_data['AQI']), 99 | unit='', icon='flower-tulip', decimals=0) 100 | particle = dict(name='Particle concentration', 101 | data=particle_data['concentration'], 102 | unit=particle_data['conc_unit'], icon='chart-bubble', 103 | decimals=2) 104 | 105 | # Send data to Home Assistant using HTTP POST requests 106 | variables = [pressure, humidity, temperature, illuminance, 107 | sound_level, sound_peak, AQI, AQI_interpret] 108 | if sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF: 109 | variables.append(particle) 110 | try: 111 | for v in variables: 112 | url = ("http://" + HOME_ASSISTANT_IP + ":8123/api/states/" + 113 | SENSOR_NAME + "." + v['name'].replace(' ', '_')) 114 | head = {"Content-type": "application/json", 115 | "Authorization": "Bearer " + LONG_LIVED_ACCESS_TOKEN} 116 | try: 117 | valueStr = "{:.{dps}f}".format(v['data'], dps=v['decimals']) 118 | except Exception: 119 | valueStr = v['data'] 120 | payload = {"state": valueStr, "attributes": { 121 | "unit_of_measurement": v['unit'], "friendly_name": v['name'], 122 | "icon": "mdi:" + v['icon']}} 123 | requests.post(url, json=payload, headers=head, timeout=2) 124 | except Exception as e: 125 | # An error has occurred, likely due to a lost network connection, 126 | # and the post has failed. 127 | # The program will retry with the next data release and will succeed 128 | # if the network reconnects. 129 | print("HTTP POST failed with the following error:") 130 | print(repr(e)) 131 | print("The program will continue and retry on the next data output.") 132 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/IFTTT.py: -------------------------------------------------------------------------------- 1 | """Example code for sending data from the Metriful MS430 to IFTTT.com. 2 | 3 | This example is designed to run with Python 3 on a Raspberry Pi. 4 | 5 | Environmental data values are periodically measured and compared with 6 | a set of user-defined thresholds. If any values go outside the allowed 7 | ranges, an HTTP POST request is sent to IFTTT.com, triggering an alert 8 | email to your inbox, with customizable text. 9 | 10 | More setup information is provided in the readme and User Guide. 11 | """ 12 | 13 | # Copyright 2020-2023 Metriful Ltd. 14 | # Licensed under the MIT License - for further details see LICENSE.txt 15 | 16 | # For code examples, datasheet and user guide, visit 17 | # https://github.com/metriful/sensor 18 | 19 | import requests 20 | import time 21 | import sensor_package.sensor_functions as sensor 22 | import sensor_package.sensor_constants as const 23 | 24 | ######################################################### 25 | # USER-EDITABLE SETTINGS 26 | 27 | # IFTTT.com settings: WEBHOOKS_KEY and IFTTT_EVENT_NAME 28 | 29 | # You must set up a free account on IFTTT.com and create a Webhooks 30 | # applet before using this example. This is explained further in the 31 | # instructions in the GitHub Readme and in the User Guide. 32 | 33 | WEBHOOKS_KEY = "PASTE YOUR KEY HERE WITHIN QUOTES" 34 | IFTTT_EVENT_NAME = "PASTE YOUR EVENT NAME HERE WITHIN QUOTES" 35 | 36 | # An inactive period follows each alert, during which the same alert 37 | # will not be generated again - this prevents too many emails/alerts. 38 | # Choose the period as a number of readout cycles (each 5 minutes) 39 | # e.g. for a 1 hour period, choose inactive_wait_cycles = 12 40 | inactive_wait_cycles = 12 41 | 42 | # Define the details of the variables for monitoring: 43 | humidity = {'name': 'humidity', 44 | 'unit': '%', 45 | 'decimal_places': 1, 46 | 'high_threshold': 60, 47 | 'low_threshold': 30, 48 | 'inactive_count': 2, 49 | 'high_advice': 'Reduce moisture sources.', 50 | 'low_advice': 'Start the humidifier.'} 51 | 52 | air_quality_index = {'name': 'air quality index', 53 | 'unit': '', 54 | 'decimal_places': 1, 55 | 'high_threshold': 250, 56 | 'low_threshold': -1, 57 | 'inactive_count': 2, 58 | 'high_advice': 'Improve ventilation.', 59 | 'low_advice': ''} 60 | 61 | # Change the following values if Fahrenheit output 62 | # temperature is selected in sensor_functions.py 63 | temperature = {'name': 'temperature', 64 | 'unit': const.CELSIUS_SYMBOL, 65 | 'decimal_places': 1, 66 | 'high_threshold': 23, 67 | 'low_threshold': 18, 68 | 'inactive_count': 2, 69 | 'high_advice': 'Turn on the fan.', 70 | 'low_advice': 'Turn on the heating.'} 71 | 72 | # END OF USER-EDITABLE SETTINGS 73 | ######################################################### 74 | 75 | # Measure the environment data every 300 seconds (5 minutes). 76 | # This is suitable for long-term monitoring. 77 | cycle_period = const.CYCLE_PERIOD_300_S 78 | 79 | # IFTTT settings: 80 | IFTTT_url = ("http://maker.ifttt.com/trigger/" + IFTTT_EVENT_NAME 81 | + "/with/key/" + WEBHOOKS_KEY) 82 | IFTTT_header = {"Content-type": "application/json"} 83 | 84 | # Set up the GPIO and I2C communications bus 85 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 86 | 87 | ######################################################### 88 | 89 | print("Monitoring data. Press ctrl-c to exit.") 90 | 91 | # Enter cycle mode 92 | I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, 93 | const.CYCLE_TIME_PERIOD_REG, [cycle_period]) 94 | I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) 95 | 96 | while True: 97 | 98 | # Wait for the next new data release, indicated by a falling edge on READY 99 | while (not GPIO.event_detected(sensor.READY_pin)): 100 | time.sleep(0.05) 101 | 102 | # Read the air data and air quality data 103 | air_data = sensor.get_air_data(I2C_bus) 104 | air_quality_data = sensor.get_air_quality_data(I2C_bus) 105 | temperature['data'] = air_data['T'] 106 | humidity['data'] = air_data['H_pc'] 107 | air_quality_index['data'] = air_quality_data['AQI'] 108 | 109 | # Check the new values and send an alert to IFTTT if a variable is 110 | # outside its allowed range. 111 | for v in [temperature, humidity, air_quality_index]: 112 | 113 | if v['inactive_count'] > 0: 114 | # Count down to when the monitoring is active again 115 | v['inactive_count'] -= 1 116 | 117 | send_alert = False 118 | if (v['data'] > v['high_threshold']) and (v['inactive_count'] == 0): 119 | # The variable is above the high threshold: send an alert then 120 | # ignore this variable for the next inactive_wait_cycles 121 | v['inactive_count'] = inactive_wait_cycles 122 | send_alert = True 123 | threshold_description = 'high.' 124 | advice_text = v['high_advice'] 125 | elif (v['data'] < v['low_threshold']) and (v['inactive_count'] == 0): 126 | # The variable is below the low threshold: send an alert then 127 | # ignore this variable for the next inactive_wait_cycles 128 | v['inactive_count'] = inactive_wait_cycles 129 | send_alert = True 130 | threshold_description = 'low.' 131 | advice_text = v['low_advice'] 132 | 133 | if send_alert: 134 | # Send data using an HTTP POST request 135 | try: 136 | warning_txt = f"The {v['name']} is too {threshold_description}" 137 | print("Sending new alert to IFTTT: " + warning_txt) 138 | payload = {"value1": warning_txt, 139 | "value2": ("The measurement was " 140 | "{:.{dps}f} ".format( 141 | v['data'], dps=v['decimal_places']) 142 | + v['unit']), 143 | "value3": advice_text} 144 | requests.post(IFTTT_url, json=payload, 145 | headers=IFTTT_header, timeout=2) 146 | except Exception as e: 147 | # An error has occurred, likely due to a lost internet 148 | # connection, and the post has failed. The program will 149 | # continue and new alerts will succeed if the internet 150 | # reconnects. 151 | print("HTTP POST failed with the following error:") 152 | print(repr(e)) 153 | print("The program will attempt to continue.") 154 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/IoT_cloud_logging.py: -------------------------------------------------------------------------------- 1 | """Example IoT data logging code for the Metriful MS430. 2 | 3 | This example is designed to run with Python 3 on a Raspberry Pi. 4 | 5 | Environmental data values are measured and logged to an internet 6 | cloud account every 100 seconds. The example gives the choice of 7 | using either the Tago.io or Thingspeak.com cloud - both of these 8 | offer a free account for low data rates. 9 | """ 10 | 11 | # Copyright 2020-2023 Metriful Ltd. 12 | # Licensed under the MIT License - for further details see LICENSE.txt 13 | 14 | # For code examples, datasheet and user guide, visit 15 | # https://github.com/metriful/sensor 16 | 17 | import requests 18 | import time 19 | import sensor_package.sensor_functions as sensor 20 | import sensor_package.sensor_constants as const 21 | 22 | ######################################################### 23 | # USER-EDITABLE SETTINGS 24 | 25 | # How often to read and log data (every 3, 100, 300 seconds) 26 | # Note: due to data rate limits on free cloud services, this should 27 | # be set to 100 or 300 seconds, not 3 seconds. 28 | cycle_period = const.CYCLE_PERIOD_100_S 29 | 30 | # IoT cloud settings. 31 | 32 | # This example uses the free IoT cloud hosting services provided 33 | # by Tago.io or Thingspeak.com 34 | # An account must have been set up with the relevant cloud provider and 35 | # an internet connection to the Pi must exist. See the accompanying 36 | # readme and User Guide for more information. 37 | 38 | # Choose which provider to use 39 | use_Tago_cloud = True # set this False to use the Thingspeak cloud 40 | 41 | # The chosen account's key/token must be inserted below. 42 | if use_Tago_cloud: 43 | # settings for Tago.io cloud 44 | TAGO_DEVICE_TOKEN_STRING = "PASTE YOUR TOKEN HERE WITHIN QUOTES" 45 | else: 46 | # settings for ThingSpeak.com cloud 47 | THINGSPEAK_API_KEY_STRING = "PASTE YOUR API KEY HERE WITHIN QUOTES" 48 | 49 | # END OF USER-EDITABLE SETTINGS 50 | ######################################################### 51 | 52 | # Set up the GPIO and I2C communications bus 53 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 54 | 55 | # Apply the chosen settings to the MS430 56 | I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, 57 | const.PARTICLE_SENSOR_SELECT_REG, 58 | [sensor.PARTICLE_SENSOR]) 59 | I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, 60 | const.CYCLE_TIME_PERIOD_REG, [cycle_period]) 61 | 62 | ######################################################### 63 | 64 | # Cloud settings for HTTP logging 65 | if use_Tago_cloud: 66 | # settings for Tago.io cloud 67 | tago_url = "http://api.tago.io/data" 68 | tago_header = {"Content-type": "application/json", 69 | "Device-Token": TAGO_DEVICE_TOKEN_STRING} 70 | else: 71 | # settings for ThingSpeak.com cloud 72 | thingspeak_url = "http://api.thingspeak.com/update" 73 | thingspeak_header = {"Content-type": "application/x-www-form-urlencoded"} 74 | 75 | print("Logging data. Press ctrl-c to exit.") 76 | 77 | # Enter cycle mode 78 | I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) 79 | 80 | while (True): 81 | 82 | # Wait for the next new data release, indicated by a falling edge on READY 83 | while (not GPIO.event_detected(sensor.READY_pin)): 84 | time.sleep(0.05) 85 | 86 | # Now read all data from the MS430 87 | 88 | # Air data 89 | # Choose output temperature unit (C or F) in sensor_functions.py 90 | air_data = sensor.get_air_data(I2C_bus) 91 | 92 | # Air quality data 93 | # The initial self-calibration of the air quality data may take several 94 | # minutes to complete. During this time the accuracy parameter is zero 95 | # and the data values are not valid. 96 | air_quality_data = sensor.get_air_quality_data(I2C_bus) 97 | 98 | # Light data 99 | light_data = sensor.get_light_data(I2C_bus) 100 | 101 | # Sound data 102 | sound_data = sensor.get_sound_data(I2C_bus) 103 | 104 | # Particle data 105 | # This requires the connection of a particulate sensor (zero/invalid 106 | # values will be obtained if this sensor is not present). 107 | # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py 108 | # Also note that, due to the low pass filtering used, the 109 | # particle data become valid after an initial initialization 110 | # period of approximately one minute. 111 | particle_data = sensor.get_particle_data(I2C_bus, sensor.PARTICLE_SENSOR) 112 | 113 | # Assemble the data into the required format, then send it to the cloud 114 | # as an HTTP POST request. 115 | 116 | # For both example cloud providers, the following quantities will be sent: 117 | # 1 Temperature (measurement unit is selected in sensor_functions.py) 118 | # 2 Pressure/Pa 119 | # 3 Humidity/% 120 | # 4 Air quality index 121 | # 5 bVOC/ppm 122 | # 6 SPL/dBA 123 | # 7 Illuminance/lux 124 | # 8 Particle concentration 125 | 126 | # Additionally, for Tago, the following are sent: 127 | # 9 Air Quality Assessment summary (Good, Bad, etc.) 128 | # 10 Peak sound amplitude / mPa 129 | 130 | try: 131 | if use_Tago_cloud: 132 | payload = [] 133 | payload.append({"variable": "temperature", 134 | "value": f"{air_data['T']:.1f}"}) 135 | payload.append({"variable": "pressure", "value": air_data['P_Pa']}) 136 | payload.append({"variable": "humidity", 137 | "value": f"{air_data['H_pc']:.1f}"}) 138 | payload.append({"variable": "aqi", 139 | "value": f"{air_quality_data['AQI']:.1f}"}) 140 | payload.append( 141 | {"variable": "aqi_string", 142 | "value": sensor.interpret_AQI_value(air_quality_data['AQI'])}) 143 | payload.append({"variable": "bvoc", 144 | "value": f"{air_quality_data['bVOC']:.2f}"}) 145 | payload.append({"variable": "spl", 146 | "value": f"{sound_data['SPL_dBA']:.1f}"}) 147 | payload.append({"variable": "peak_amp", 148 | "value": f"{sound_data['peak_amp_mPa']:.2f}"}) 149 | payload.append({"variable": "illuminance", 150 | "value": f"{light_data['illum_lux']:.2f}"}) 151 | payload.append({"variable": "particulates", 152 | "value": f"{particle_data['concentration']:.2f}"}) 153 | requests.post(tago_url, json=payload, 154 | headers=tago_header, timeout=2) 155 | else: 156 | # Use ThingSpeak.com cloud 157 | payload = "api_key=" + THINGSPEAK_API_KEY_STRING 158 | payload += "&field1=" + f"{air_data['T']:.1f}" 159 | payload += "&field2=" + str(air_data['P_Pa']) 160 | payload += "&field3=" + f"{air_data['H_pc']:.1f}" 161 | payload += "&field4=" + f"{air_quality_data['AQI']:.1f}" 162 | payload += "&field5=" + f"{air_quality_data['bVOC']:.2f}" 163 | payload += "&field6=" + f"{sound_data['SPL_dBA']:.1f}" 164 | payload += "&field7=" + f"{light_data['illum_lux']:.2f}" 165 | payload += "&field8=" + f"{particle_data['concentration']:.2f}" 166 | requests.post(thingspeak_url, data=payload, 167 | headers=thingspeak_header, timeout=2) 168 | 169 | except Exception as e: 170 | # An error has occurred, likely due to a lost internet connection, 171 | # and the post has failed. 172 | # The program will retry with the next data release and will succeed 173 | # if the internet reconnects. 174 | print("HTTP POST failed with the following error:") 175 | print(repr(e)) 176 | print("The program will continue and retry on the next data output.") 177 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/cycle_readout.py: -------------------------------------------------------------------------------- 1 | """Example of using the Metriful MS430 in cycle mode, from a Raspberry Pi. 2 | 3 | Continually measures and displays all environmental data in a 4 | repeating cycle. User can choose from a cycle time period 5 | of 3, 100, or 300 seconds. 6 | 7 | The measurements can be displayed as either labeled text, or as 8 | simple columns of numbers. 9 | """ 10 | 11 | # Copyright 2020-2023 Metriful Ltd. 12 | # Licensed under the MIT License - for further details see LICENSE.txt 13 | 14 | # For code examples, datasheet and user guide, visit 15 | # https://github.com/metriful/sensor 16 | 17 | import time 18 | import sensor_package.sensor_functions as sensor 19 | import sensor_package.sensor_constants as const 20 | 21 | ######################################################### 22 | # USER-EDITABLE SETTINGS 23 | 24 | # How often to read data (every 3, 100, or 300 seconds) 25 | cycle_period = const.CYCLE_PERIOD_3_S 26 | 27 | # How to print the data: If print_data_as_columns = True, 28 | # data are columns of numbers, useful to copy/paste to a spreadsheet 29 | # application. Otherwise, data are printed with explanatory labels and units. 30 | print_data_as_columns = False 31 | 32 | # END OF USER-EDITABLE SETTINGS 33 | ######################################################### 34 | 35 | # Set up the GPIO and I2C communications bus 36 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 37 | 38 | # Apply the chosen settings 39 | I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, 40 | const.PARTICLE_SENSOR_SELECT_REG, 41 | [sensor.PARTICLE_SENSOR]) 42 | I2C_bus.write_i2c_block_data(sensor.i2c_7bit_address, 43 | const.CYCLE_TIME_PERIOD_REG, [cycle_period]) 44 | 45 | ######################################################### 46 | 47 | print("Entering cycle mode and waiting for data. Press ctrl-c to exit.") 48 | 49 | I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) 50 | 51 | while True: 52 | 53 | # Wait for the next new data release, indicated by a falling edge on READY 54 | while (not GPIO.event_detected(sensor.READY_pin)): 55 | time.sleep(0.05) 56 | 57 | # Now read and print all data 58 | 59 | # Air data 60 | # Choose output temperature unit (C or F) in sensor_functions.py 61 | air_data = sensor.get_air_data(I2C_bus) 62 | sensor.writeAirData(None, air_data, print_data_as_columns) 63 | 64 | # Air quality data 65 | # The initial self-calibration of the air quality data may take several 66 | # minutes to complete. During this time the accuracy parameter is zero 67 | # and the data values are not valid. 68 | air_quality_data = sensor.get_air_quality_data(I2C_bus) 69 | sensor.writeAirQualityData(None, air_quality_data, print_data_as_columns) 70 | 71 | # Light data 72 | light_data = sensor.get_light_data(I2C_bus) 73 | sensor.writeLightData(None, light_data, print_data_as_columns) 74 | 75 | # Sound data 76 | sound_data = sensor.get_sound_data(I2C_bus) 77 | sensor.writeSoundData(None, sound_data, print_data_as_columns) 78 | 79 | # Particle data 80 | # This requires the connection of a particulate sensor (zero/invalid 81 | # values will be obtained if this sensor is not present). 82 | # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py 83 | # Also note that, due to the low pass filtering used, the 84 | # particle data become valid after an initial initialization 85 | # period of approximately one minute. 86 | if (sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF): 87 | particle_data = sensor.get_particle_data(I2C_bus, 88 | sensor.PARTICLE_SENSOR) 89 | sensor.writeParticleData(None, particle_data, print_data_as_columns) 90 | 91 | if print_data_as_columns: 92 | print("") 93 | else: 94 | print("-------------------------------------------") 95 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/graph_web_server.py: -------------------------------------------------------------------------------- 1 | """Example of serving a web page with graphs, from a Raspberry Pi. 2 | 3 | Example code for serving a web page over a local network to display 4 | graphs showing environment data read from the Metriful MS430. A CSV 5 | data file is also downloadable from the page. 6 | This example is designed to run with Python 3 on a Raspberry Pi. 7 | 8 | The web page can be viewed from other devices connected to the same 9 | network(s) as the host Raspberry Pi, including wired and wireless 10 | networks. 11 | 12 | The browser which views the web page uses the Plotly javascript 13 | library to generate the graphs. This is automatically downloaded 14 | over the internet, or can be cached for offline use. If it is not 15 | available, graphs will not appear but text data and CSV downloads 16 | should still work. 17 | 18 | NOTE: if you run, exit, then re-run this program, you may get an 19 | "Address already in use" error. This ends after a short period: wait 20 | one minute then retry. 21 | """ 22 | 23 | # Copyright 2020-2023 Metriful Ltd. 24 | # Licensed under the MIT License - for further details see LICENSE.txt 25 | 26 | # For code examples, datasheet and user guide, visit 27 | # https://github.com/metriful/sensor 28 | 29 | import time 30 | import socketserver 31 | import sensor_package.servers as server 32 | import sensor_package.sensor_functions as sensor 33 | import sensor_package.sensor_constants as const 34 | 35 | ######################################################### 36 | # USER-EDITABLE SETTINGS 37 | 38 | # Choose how often to read and update data (every 3, 100, or 300 seconds) 39 | # The web page can be refreshed more often but the data will not change 40 | cycle_period = const.CYCLE_PERIOD_3_S 41 | 42 | # The BUFFER_LENGTH parameter is the number of data points of each 43 | # variable to store on the host. It is limited by the available host RAM. 44 | buffer_length = 200 45 | # Examples: 46 | # For 16 hour graphs, choose 100 second cycle period and 576 buffer length 47 | # For 24 hour graphs, choose 300 second cycle period and 288 buffer length 48 | 49 | # The web page address will be: 50 | # http://:8080 e.g. http://172.24.1.1:8080 51 | 52 | # To find your Raspberry Pi's IP address: 53 | # 1. Enter the command ifconfig in a terminal 54 | # 2. Each available network connection displays a block of output 55 | # 3. Ignore the "lo" output block 56 | # 4. The IP address on each network is displayed after "inet" 57 | # 58 | # Example - part of an output block showing the address 172.24.1.1 59 | # 60 | # wlan0: flags=4163 mtu 1500 61 | # inet 172.24.1.1 netmask 255.255.255.0 broadcast 172.24.1.255 62 | 63 | # END OF USER-EDITABLE SETTINGS 64 | ######################################################### 65 | 66 | # Set up the GPIO and I2C communications bus 67 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 68 | 69 | # Apply the chosen settings to the MS430 70 | I2C_bus.write_i2c_block_data( 71 | sensor.i2c_7bit_address, 72 | const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) 73 | I2C_bus.write_i2c_block_data( 74 | sensor.i2c_7bit_address, const.CYCLE_TIME_PERIOD_REG, [cycle_period]) 75 | 76 | # Get time period value to send to web page 77 | if (cycle_period == const.CYCLE_PERIOD_3_S): 78 | server.GraphWebpageHandler.data_period_seconds = 3 79 | elif (cycle_period == const.CYCLE_PERIOD_100_S): 80 | server.GraphWebpageHandler.data_period_seconds = 100 81 | else: # CYCLE_PERIOD_300_S 82 | server.GraphWebpageHandler.data_period_seconds = 300 83 | 84 | # Set the number of each variable to be retained 85 | server.GraphWebpageHandler.set_buffer_length(buffer_length) 86 | 87 | # Choose the TCP port number for the web page. 88 | port = 8080 89 | # The port can be any unused number from 1-65535 but values below 1024 90 | # require this program to be run as super-user as follows: 91 | # sudo python3 web_server.py 92 | # Port 80 is the default for HTTP, and with this value the port number 93 | # can be omitted from the web address. e.g. http://172.24.1.1 94 | 95 | print("Starting the web server...") 96 | ips = server.get_IP_addresses() 97 | if not ips: 98 | print("Warning: no networks detected.") 99 | else: 100 | print("Your web page will be available at:") 101 | for ip in ips: 102 | print(f" http://{ip}:{port}") 103 | print("For more information on network IP addresses, " 104 | "run the command ifconfig in a terminal.") 105 | print("Press ctrl-c to exit at any time.") 106 | 107 | the_server = socketserver.TCPServer(("", port), server.GraphWebpageHandler) 108 | the_server.timeout = 0.1 109 | 110 | # Enter cycle mode to start periodic data output 111 | I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) 112 | 113 | while True: 114 | 115 | # Respond to the web page client requests while waiting for new data 116 | while not GPIO.event_detected(sensor.READY_pin): 117 | the_server.handle_request() 118 | time.sleep(0.05) 119 | 120 | # Now read all data from the MS430 and pass to the web page 121 | 122 | # Air data 123 | server.GraphWebpageHandler.update_air_data(sensor.get_air_data(I2C_bus)) 124 | 125 | # Air quality data 126 | # The initial self-calibration of the air quality data may take several 127 | # minutes to complete. During this time the accuracy parameter is zero 128 | # and the data values are not valid. 129 | server.GraphWebpageHandler.update_air_quality_data( 130 | sensor.get_air_quality_data(I2C_bus)) 131 | 132 | # Light data 133 | server.GraphWebpageHandler.update_light_data( 134 | sensor.get_light_data(I2C_bus)) 135 | 136 | # Sound data 137 | server.GraphWebpageHandler.update_sound_data( 138 | sensor.get_sound_data(I2C_bus)) 139 | 140 | # Particle data 141 | # This requires the connection of a particulate sensor (invalid 142 | # values will be obtained if this sensor is not present). 143 | # Also note that, due to the low pass filtering used, the 144 | # particle data become valid after an initial initialization 145 | # period of approximately one minute. 146 | if (sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF): 147 | server.GraphWebpageHandler.update_particle_data( 148 | sensor.get_particle_data(I2C_bus, sensor.PARTICLE_SENSOR)) 149 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/interrupts.py: -------------------------------------------------------------------------------- 1 | """Example of using the Metriful MS430 interrupt outputs, from a Raspberry Pi. 2 | 3 | Light and sound interrupts are configured and the program then 4 | waits indefinitely. When an interrupt occurs, a message is 5 | displayed, the interrupt is cleared (if set to latch type), 6 | and the program returns to waiting. 7 | """ 8 | 9 | # Copyright 2020-2023 Metriful Ltd. 10 | # Licensed under the MIT License - for further details see LICENSE.txt 11 | 12 | # For code examples, datasheet and user guide, visit 13 | # https://github.com/metriful/sensor 14 | 15 | import time 16 | import sensor_package.sensor_functions as sensor 17 | import sensor_package.sensor_constants as const 18 | 19 | ######################################################### 20 | # USER-EDITABLE SETTINGS 21 | 22 | enable_light_interrupts = True 23 | light_int_type = const.LIGHT_INT_TYPE_LATCH 24 | 25 | # Choose the light interrupt polarity. The interrupt triggers when 26 | # the light level rises above the threshold (positive), or when 27 | # the level falls below the threshold (negative). 28 | light_int_polarity = const.LIGHT_INT_POL_POSITIVE 29 | light_thres_lux_i = 100 30 | light_thres_lux_f2dp = 50 31 | # The interrupt threshold in lux units can be fractional and is formed as: 32 | # threshold = light_thres_lux_i + (light_thres_lux_f2dp/100) 33 | # E.g. for a light threshold of 56.12 lux, set: 34 | # light_thres_lux_i = 56 35 | # light_thres_lux_f2dp = 12 36 | 37 | enable_sound_interrupts = True 38 | sound_int_type = const.SOUND_INT_TYPE_LATCH 39 | sound_thres_mPa = 100 40 | 41 | # END OF USER-EDITABLE SETTINGS 42 | ######################################################### 43 | 44 | if ((light_thres_lux_i 45 | + (float(light_thres_lux_f2dp)/100.0)) > const.MAX_LUX_VALUE): 46 | raise ValueError("The chosen light interrupt threshold exceeds the " 47 | f"maximum allowed value of {const.MAX_LUX_VALUE} lux") 48 | 49 | # Set up the GPIO and I2C communications bus 50 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 51 | 52 | ######################################################### 53 | 54 | if enable_sound_interrupts: 55 | # Set the interrupt type (latch or comparator) 56 | I2C_bus.write_i2c_block_data( 57 | sensor.i2c_7bit_address, 58 | const.SOUND_INTERRUPT_TYPE_REG, [sound_int_type]) 59 | 60 | # Set the threshold 61 | sensor.setSoundInterruptThreshold(I2C_bus, sound_thres_mPa) 62 | 63 | # Tell the Pi to monitor the interrupt line for a falling 64 | # edge event (high-to-low voltage change) 65 | GPIO.add_event_detect(sensor.sound_int_pin, GPIO.FALLING) 66 | 67 | # Enable the interrupt on the MS430 68 | I2C_bus.write_i2c_block_data( 69 | sensor.i2c_7bit_address, 70 | const.SOUND_INTERRUPT_ENABLE_REG, [const.ENABLED]) 71 | 72 | 73 | if enable_light_interrupts: 74 | # Set the interrupt type (latch or comparator) 75 | I2C_bus.write_i2c_block_data( 76 | sensor.i2c_7bit_address, 77 | const.LIGHT_INTERRUPT_TYPE_REG, [light_int_type]) 78 | 79 | # Set the threshold 80 | sensor.setLightInterruptThreshold( 81 | I2C_bus, light_thres_lux_i, light_thres_lux_f2dp) 82 | 83 | # Set the interrupt polarity 84 | I2C_bus.write_i2c_block_data( 85 | sensor.i2c_7bit_address, 86 | const.LIGHT_INTERRUPT_POLARITY_REG, [light_int_polarity]) 87 | 88 | # Tell the Pi to monitor the interrupt line for a falling 89 | # edge event (high-to-low voltage change) 90 | GPIO.add_event_detect(sensor.light_int_pin, GPIO.FALLING) 91 | 92 | # Enable the interrupt on the MS430 93 | I2C_bus.write_i2c_block_data( 94 | sensor.i2c_7bit_address, 95 | const.LIGHT_INTERRUPT_ENABLE_REG, [const.ENABLED]) 96 | 97 | 98 | if (not enable_light_interrupts) and (not enable_sound_interrupts): 99 | print("No interrupts have been enabled. Press ctrl-c to exit.") 100 | else: 101 | print("Waiting for interrupts. Press ctrl-c to exit.") 102 | print("") 103 | 104 | 105 | while True: 106 | 107 | # Check whether a light interrupt has occurred 108 | if GPIO.event_detected(sensor.light_int_pin) and enable_light_interrupts: 109 | print("LIGHT INTERRUPT.") 110 | if (light_int_type == const.LIGHT_INT_TYPE_LATCH): 111 | # Latch type interrupts remain set until cleared by command 112 | I2C_bus.write_byte(sensor.i2c_7bit_address, 113 | const.LIGHT_INTERRUPT_CLR_CMD) 114 | 115 | # Check whether a sound interrupt has occurred 116 | if GPIO.event_detected(sensor.sound_int_pin) and enable_sound_interrupts: 117 | print("SOUND INTERRUPT.") 118 | if (sound_int_type == const.SOUND_INT_TYPE_LATCH): 119 | # Latch type interrupts remain set until cleared by command 120 | I2C_bus.write_byte(sensor.i2c_7bit_address, 121 | const.SOUND_INTERRUPT_CLR_CMD) 122 | 123 | time.sleep(0.5) 124 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/log_data_to_file.py: -------------------------------------------------------------------------------- 1 | """Example of logging data from the Metriful MS430, using a Raspberry Pi. 2 | 3 | All environment data values are measured and saved as columns 4 | of numbers in a text file (one row of data every three seconds). 5 | This type of file can be imported into various graph and spreadsheet 6 | applications. To prevent very large file sizes, a new file is 7 | started every time it reaches a preset size limit. 8 | """ 9 | 10 | # Copyright 2020-2023 Metriful Ltd. 11 | # Licensed under the MIT License - for further details see LICENSE.txt 12 | 13 | # For code examples, datasheet and user guide, visit 14 | # https://github.com/metriful/sensor 15 | 16 | import time 17 | from datetime import datetime 18 | import sensor_package.sensor_functions as sensor 19 | import sensor_package.sensor_constants as const 20 | 21 | ######################################################### 22 | # USER-EDITABLE SETTINGS 23 | 24 | # Choose any combination of where to save data: 25 | log_to_file = True 26 | print_to_screen = True 27 | # The log files are text files containing columns of data separated by spaces. 28 | 29 | # Number of lines of data to log in each file before starting a new file 30 | # (required if log_to_file == True), and which directory to save them in. 31 | lines_per_file = 300 32 | data_file_directory = "/home/pi/Desktop" 33 | 34 | # How often to measure and read data (every 3, 100, or 300 seconds): 35 | cycle_period = const.CYCLE_PERIOD_3_S 36 | 37 | # END OF USER-EDITABLE SETTINGS 38 | ######################################################### 39 | 40 | # Set up the GPIO and I2C communications bus 41 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 42 | 43 | # Apply the chosen settings to the MS430 44 | I2C_bus.write_i2c_block_data( 45 | sensor.i2c_7bit_address, 46 | const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) 47 | I2C_bus.write_i2c_block_data( 48 | sensor.i2c_7bit_address, const.CYCLE_TIME_PERIOD_REG, [cycle_period]) 49 | 50 | ######################################################### 51 | 52 | if log_to_file: 53 | datafile = sensor.startNewDataFile(data_file_directory) 54 | data_file_lines = 0 55 | 56 | print("Entering cycle mode and waiting for data. Press ctrl-c to exit.") 57 | 58 | # Enter cycle mode 59 | I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) 60 | 61 | while True: 62 | 63 | # Wait for the next new data release, indicated by a falling edge on READY 64 | while (not GPIO.event_detected(sensor.READY_pin)): 65 | time.sleep(0.05) 66 | 67 | # Air data 68 | # Choose output temperature unit (C or F) in sensor_functions.py 69 | air_data = sensor.get_air_data(I2C_bus) 70 | 71 | # Air quality data 72 | # The initial self-calibration of the air quality data may take several 73 | # minutes to complete. During this time the accuracy parameter is zero 74 | # and the data values are not valid. 75 | air_quality_data = sensor.get_air_quality_data(I2C_bus) 76 | 77 | # Light data 78 | light_data = sensor.get_light_data(I2C_bus) 79 | 80 | # Sound data 81 | sound_data = sensor.get_sound_data(I2C_bus) 82 | 83 | # Particle data 84 | # This requires the connection of a particulate sensor (zero/invalid 85 | # values will be obtained if this sensor is not present). 86 | # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py 87 | # Also note that, due to the low pass filtering used, the 88 | # particle data become valid after an initial initialization 89 | # period of approximately one minute. 90 | particle_data = sensor.get_particle_data(I2C_bus, sensor.PARTICLE_SENSOR) 91 | 92 | if print_to_screen: 93 | # Display all data on screen as named quantities with units 94 | print("") 95 | print("------------------") 96 | sensor.writeAirData(None, air_data, False) 97 | sensor.writeAirQualityData(None, air_quality_data, False) 98 | sensor.writeLightData(None, light_data, False) 99 | sensor.writeSoundData(None, sound_data, False) 100 | if (sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF): 101 | sensor.writeParticleData(None, particle_data, False) 102 | 103 | if log_to_file: 104 | # Write the data as simple columns in a text file (without labels or 105 | # measurement units). 106 | # Start by writing date and time in columns 1-6 107 | datafile.write(datetime.now().strftime('%Y %m %d %H %M %S ')) 108 | # Air data in columns 7-10 109 | sensor.writeAirData(datafile, air_data, True) 110 | # Air quality data in columns 11-14 111 | sensor.writeAirQualityData(datafile, air_quality_data, True) 112 | # Light data in columns 15 - 16 113 | sensor.writeLightData(datafile, light_data, True) 114 | # Sound data in columns 17 - 25 115 | sensor.writeSoundData(datafile, sound_data, True) 116 | if (sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF): 117 | # Particle data in columns 26 - 28 118 | sensor.writeParticleData(datafile, particle_data, True) 119 | datafile.write("\n") 120 | datafile.flush() 121 | data_file_lines += 1 122 | if data_file_lines >= lines_per_file: 123 | # Start a new log file to prevent very large files 124 | datafile.close() 125 | datafile = sensor.startNewDataFile(data_file_directory) 126 | data_file_lines = 0 127 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/on_demand_readout.py: -------------------------------------------------------------------------------- 1 | """Example using the Metriful MS430 in "on-demand" mode, from a Raspberry Pi. 2 | 3 | Repeatedly measures and displays all environment data, with a pause 4 | of any chosen duration between measurements. Air quality data are 5 | unavailable in this mode (instead use cycle_readout.py). 6 | 7 | The measurements can be displayed as either labeled text, or as 8 | simple columns of numbers. 9 | """ 10 | 11 | # Copyright 2020-2023 Metriful Ltd. 12 | # Licensed under the MIT License - for further details see LICENSE.txt 13 | 14 | # For code examples, datasheet and user guide, visit 15 | # https://github.com/metriful/sensor 16 | 17 | import time 18 | import sensor_package.sensor_functions as sensor 19 | import sensor_package.sensor_constants as const 20 | 21 | ######################################################### 22 | # USER-EDITABLE SETTINGS 23 | 24 | # Pause (in seconds) between data measurements (note that the 25 | # measurement itself takes 0.5 seconds) 26 | pause_s = 3.5 27 | # Choosing a pause of less than 2 seconds will cause inaccurate 28 | # temperature, humidity and particle data. 29 | 30 | # How to print the data: If print_data_as_columns = True, 31 | # data are columns of numbers, useful to copy/paste to a spreadsheet 32 | # application. Otherwise, data are printed with explanatory labels and units. 33 | print_data_as_columns = False 34 | 35 | # END OF USER-EDITABLE SETTINGS 36 | ######################################################### 37 | 38 | # Set up the GPIO and I2C communications bus 39 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 40 | 41 | I2C_bus.write_i2c_block_data( 42 | sensor.i2c_7bit_address, 43 | const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) 44 | 45 | ######################################################### 46 | 47 | while (True): 48 | 49 | time.sleep(pause_s) 50 | 51 | # Trigger a new measurement 52 | I2C_bus.write_byte(sensor.i2c_7bit_address, const.ON_DEMAND_MEASURE_CMD) 53 | 54 | # Wait for the next new data release, indicated by a falling edge on READY. 55 | # This will take 0.5 seconds. 56 | while (not GPIO.event_detected(sensor.READY_pin)): 57 | time.sleep(0.05) 58 | 59 | # Now read and print all data 60 | 61 | # Air data 62 | # Choose output temperature unit (C or F) in sensor_functions.py 63 | air_data = sensor.get_air_data(I2C_bus) 64 | sensor.writeAirData(None, air_data, print_data_as_columns) 65 | 66 | # Air quality data are not available with on demand measurements 67 | 68 | # Light data 69 | light_data = sensor.get_light_data(I2C_bus) 70 | sensor.writeLightData(None, light_data, print_data_as_columns) 71 | 72 | # Sound data 73 | sound_data = sensor.get_sound_data(I2C_bus) 74 | sensor.writeSoundData(None, sound_data, print_data_as_columns) 75 | 76 | # Particle data 77 | # This requires the connection of a particulate sensor (zero/invalid 78 | # values will be obtained if this sensor is not present). 79 | # Specify your sensor model (PPD42 or SDS011) in sensor_functions.py 80 | # Also note that, due to the low pass filtering used, the 81 | # particle data become valid after an initial initialization 82 | # period of approximately one minute. 83 | if (sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF): 84 | particle_data = sensor.get_particle_data(I2C_bus, 85 | sensor.PARTICLE_SENSOR) 86 | sensor.writeParticleData(None, particle_data, print_data_as_columns) 87 | 88 | if print_data_as_columns: 89 | print("") 90 | else: 91 | print("-------------------------------------------") 92 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/particle_sensor_toggle.py: -------------------------------------------------------------------------------- 1 | """An example of how to control power to the external particle sensor. 2 | 3 | Optional advanced demo. This program shows how to generate an output 4 | control signal from one of the Pi pins, which can be used to turn 5 | the particle sensor on and off. An external transistor circuit is 6 | also needed - this will gate the sensor power supply according to 7 | the control signal. Further details are given in the User Guide. 8 | 9 | The program continually measures and displays all environment data 10 | in a repeating cycle. The user can view the output in the Serial 11 | Monitor. After reading the data, the particle sensor is powered off 12 | for a chosen number of cycles ("off_cycles"). It is then powered on 13 | and read before being powered off again. Sound data are ignored 14 | while the particle sensor is on, to avoid fan noise. 15 | """ 16 | 17 | # Copyright 2020-2023 Metriful Ltd. 18 | # Licensed under the MIT License - for further details see LICENSE.txt 19 | 20 | # For code examples, datasheet and user guide, visit 21 | # https://github.com/metriful/sensor 22 | 23 | import time 24 | import sensor_package.sensor_functions as sensor 25 | import sensor_package.sensor_constants as const 26 | 27 | ######################################################### 28 | # USER-EDITABLE SETTINGS 29 | 30 | # How often to read data; choose only 100 or 300 seconds for this demo 31 | # because the sensor should be on for at least one minute before reading 32 | # its data. 33 | cycle_period = const.CYCLE_PERIOD_100_S 34 | 35 | # How to print the data: If print_data_as_columns = True, 36 | # data are columns of numbers, useful to copy/paste to a spreadsheet 37 | # application. Otherwise, data are printed with explanatory labels and units. 38 | print_data_as_columns = False 39 | 40 | # Particle sensor power control options 41 | off_cycles = 2 # leave the sensor off for this many cycles between reads 42 | particle_sensor_control_pin = 10 # Pi pin which outputs the control signal 43 | 44 | # END OF USER-EDITABLE SETTINGS 45 | ######################################################### 46 | 47 | # Set up the GPIO and I2C communications bus 48 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 49 | 50 | # Set up the particle sensor control, and turn it off initially 51 | GPIO.setup(particle_sensor_control_pin, GPIO.OUT) 52 | GPIO.output(particle_sensor_control_pin, 0) 53 | particle_sensor_is_on = False 54 | particle_sensor_count = 0 55 | 56 | # Apply the chosen settings 57 | I2C_bus.write_i2c_block_data( 58 | sensor.i2c_7bit_address, 59 | const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) 60 | I2C_bus.write_i2c_block_data( 61 | sensor.i2c_7bit_address, const.CYCLE_TIME_PERIOD_REG, [cycle_period]) 62 | 63 | ######################################################### 64 | 65 | sound_data = sensor.extractSoundData([0]*const.SOUND_DATA_BYTES) 66 | particle_data = sensor.extractParticleData([0]*const.PARTICLE_DATA_BYTES, 67 | sensor.PARTICLE_SENSOR) 68 | 69 | print("Entering cycle mode and waiting for data. Press ctrl-c to exit.") 70 | 71 | I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) 72 | 73 | while True: 74 | 75 | # Wait for the next new data release, indicated by a falling edge on READY 76 | while (not GPIO.event_detected(sensor.READY_pin)): 77 | time.sleep(0.05) 78 | 79 | # Now read and print all data. The previous loop's particle or 80 | # sound data will be printed if no reading is done on this loop. 81 | 82 | # Air data 83 | air_data = sensor.get_air_data(I2C_bus) 84 | sensor.writeAirData(None, air_data, print_data_as_columns) 85 | 86 | # Air quality data 87 | # The initial self-calibration of the air quality data may take several 88 | # minutes to complete. During this time the accuracy parameter is zero 89 | # and the data values are not valid. 90 | air_quality_data = sensor.get_air_quality_data(I2C_bus) 91 | sensor.writeAirQualityData(None, air_quality_data, print_data_as_columns) 92 | 93 | # Light data 94 | light_data = sensor.get_light_data(I2C_bus) 95 | sensor.writeLightData(None, light_data, print_data_as_columns) 96 | 97 | # Sound data - only read new data when particle sensor is off 98 | if (not particle_sensor_is_on): 99 | sound_data = sensor.get_sound_data(I2C_bus) 100 | sensor.writeSoundData(None, sound_data, print_data_as_columns) 101 | 102 | # Particle data 103 | # This requires the connection of a particulate sensor (zero/invalid 104 | # values will be obtained if this sensor is not present). 105 | # Also note that, due to the low pass filtering used, the 106 | # particle data become valid after an initial initialization 107 | # period of approximately one minute. 108 | if (particle_sensor_is_on): 109 | particle_data = sensor.get_particle_data(I2C_bus, 110 | sensor.PARTICLE_SENSOR) 111 | sensor.writeParticleData(None, particle_data, print_data_as_columns) 112 | 113 | if print_data_as_columns: 114 | print("") 115 | else: 116 | print("-------------------------------------------") 117 | 118 | # Turn the particle sensor on/off if required 119 | if (particle_sensor_is_on): 120 | # Stop the particle detection on the MS430 121 | I2C_bus.write_i2c_block_data( 122 | sensor.i2c_7bit_address, 123 | const.PARTICLE_SENSOR_SELECT_REG, [const.PARTICLE_SENSOR_OFF]) 124 | 125 | # Turn off the hardware: 126 | GPIO.output(particle_sensor_control_pin, 0) 127 | particle_sensor_is_on = False 128 | else: 129 | particle_sensor_count += 1 130 | if (particle_sensor_count >= off_cycles): 131 | # Turn on the hardware: 132 | GPIO.output(particle_sensor_control_pin, 1) 133 | 134 | # Start the particle detection on the MS430 135 | I2C_bus.write_i2c_block_data( 136 | sensor.i2c_7bit_address, 137 | const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) 138 | 139 | particle_sensor_count = 0 140 | particle_sensor_is_on = True 141 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/sensor_package/__init__.py: -------------------------------------------------------------------------------- 1 | # This file remains empty but is needed by Python to create a package 2 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/sensor_package/sensor_constants.py: -------------------------------------------------------------------------------- 1 | """Constant values for use in Python programs. 2 | 3 | This file defines constant values which are used in the control 4 | of the Metriful MS430 board and the interpretation of its output data. 5 | All values have been taken from the MS430 datasheet. 6 | """ 7 | 8 | # Copyright 2020-2023 Metriful Ltd. 9 | # Licensed under the MIT License - for further details see LICENSE.txt 10 | 11 | # For code examples, datasheet and user guide, visit 12 | # https://github.com/metriful/sensor 13 | 14 | # Settings registers 15 | PARTICLE_SENSOR_SELECT_REG = 0x07 16 | LIGHT_INTERRUPT_ENABLE_REG = 0x81 17 | LIGHT_INTERRUPT_THRESHOLD_REG = 0x82 18 | LIGHT_INTERRUPT_TYPE_REG = 0x83 19 | LIGHT_INTERRUPT_POLARITY_REG = 0x84 20 | SOUND_INTERRUPT_ENABLE_REG = 0x85 21 | SOUND_INTERRUPT_THRESHOLD_REG = 0x86 22 | SOUND_INTERRUPT_TYPE_REG = 0x87 23 | CYCLE_TIME_PERIOD_REG = 0x89 24 | 25 | # Executable commands 26 | ON_DEMAND_MEASURE_CMD = 0xE1 27 | RESET_CMD = 0xE2 28 | CYCLE_MODE_CMD = 0xE4 29 | STANDBY_MODE_CMD = 0xE5 30 | LIGHT_INTERRUPT_CLR_CMD = 0xE6 31 | SOUND_INTERRUPT_CLR_CMD = 0xE7 32 | 33 | # Read the operational mode 34 | OP_MODE_READ = 0x8A 35 | 36 | # Read data for whole categories 37 | AIR_DATA_READ = 0x10 38 | AIR_QUALITY_DATA_READ = 0x11 39 | LIGHT_DATA_READ = 0x12 40 | SOUND_DATA_READ = 0x13 41 | PARTICLE_DATA_READ = 0x14 42 | 43 | # Read individual data quantities 44 | T_READ = 0x21 45 | P_READ = 0x22 46 | H_READ = 0x23 47 | G_READ = 0x24 48 | AQI_READ = 0x25 49 | CO2E_READ = 0x26 50 | BVOC_READ = 0x27 51 | AQI_ACCURACY_READ = 0x28 52 | ILLUMINANCE_READ = 0x31 53 | WHITE_LIGHT_READ = 0x32 54 | SPL_READ = 0x41 55 | SPL_BANDS_READ = 0x42 56 | SOUND_PEAK_READ = 0x43 57 | SOUND_STABLE_READ = 0x44 58 | DUTY_CYCLE_READ = 0x51 59 | CONCENTRATION_READ = 0x52 60 | PARTICLE_VALID_READ = 0x53 61 | 62 | ############################################################### 63 | 64 | # I2C address of sensor board: can select using solder bridge 65 | I2C_ADDR_7BIT_SB_OPEN = 0x71 # if solder bridge is left open 66 | I2C_ADDR_7BIT_SB_CLOSED = 0x70 # if solder bridge is soldered closed 67 | 68 | # Values for enabling/disabling of sensor functions 69 | ENABLED = 1 70 | DISABLED = 0 71 | 72 | # Device modes 73 | STANDBY_MODE = 0 74 | CYCLE_MODE = 1 75 | 76 | LIGHT_INTERRUPT_THRESHOLD_BYTES = 3 77 | SOUND_INTERRUPT_THRESHOLD_BYTES = 2 78 | 79 | # Frequency bands for sound level measurement 80 | SOUND_FREQ_BANDS = 6 81 | sound_band_mids_Hz = [125, 250, 500, 1000, 2000, 4000] 82 | sound_band_edges_Hz = [88, 177, 354, 707, 1414, 2828, 5657] 83 | 84 | # Cycle mode time period 85 | CYCLE_PERIOD_3_S = 0 86 | CYCLE_PERIOD_100_S = 1 87 | CYCLE_PERIOD_300_S = 2 88 | 89 | # Sound interrupt type 90 | SOUND_INT_TYPE_LATCH = 0 91 | SOUND_INT_TYPE_COMP = 1 92 | 93 | # Maximum for illuminance measurement and threshold setting 94 | MAX_LUX_VALUE = 3774 95 | 96 | # Light interrupt type 97 | LIGHT_INT_TYPE_LATCH = 0 98 | LIGHT_INT_TYPE_COMP = 1 99 | 100 | # Light interrupt polarity 101 | LIGHT_INT_POL_POSITIVE = 0 102 | LIGHT_INT_POL_NEGATIVE = 1 103 | 104 | # Decoding the temperature integer.fraction value format 105 | TEMPERATURE_VALUE_MASK = 0x7F 106 | TEMPERATURE_SIGN_MASK = 0x80 107 | 108 | # Particle sensor module selection: 109 | PARTICLE_SENSOR_OFF = 0 110 | PARTICLE_SENSOR_PPD42 = 1 111 | PARTICLE_SENSOR_SDS011 = 2 112 | 113 | ############################################################### 114 | 115 | # Byte lengths for each readable data quantity and data category 116 | 117 | T_BYTES = 2 118 | P_BYTES = 4 119 | H_BYTES = 2 120 | G_BYTES = 4 121 | AIR_DATA_BYTES = 12 122 | 123 | AQI_BYTES = 3 124 | CO2E_BYTES = 3 125 | BVOC_BYTES = 3 126 | AQI_ACCURACY_BYTES = 1 127 | AIR_QUALITY_DATA_BYTES = 10 128 | 129 | ILLUMINANCE_BYTES = 3 130 | WHITE_BYTES = 2 131 | LIGHT_DATA_BYTES = 5 132 | 133 | SPL_BYTES = 2 134 | SPL_BANDS_BYTES = (2*SOUND_FREQ_BANDS) 135 | SOUND_PEAK_BYTES = 3 136 | SOUND_STABLE_BYTES = 1 137 | SOUND_DATA_BYTES = 18 138 | 139 | DUTY_CYCLE_BYTES = 2 140 | CONCENTRATION_BYTES = 3 141 | PARTICLE_VALID_BYTES = 1 142 | PARTICLE_DATA_BYTES = 6 143 | 144 | ############################################################################# 145 | 146 | # Unicode symbol strings 147 | CELSIUS_SYMBOL = "°C" 148 | FAHRENHEIT_SYMBOL = "°F" 149 | SDS011_CONC_SYMBOL = "µg/m³" # micrograms per cubic meter 150 | SUBSCRIPT_2 = "₂" 151 | OHM_SYMBOL = "Ω" 152 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/sensor_package/servers.py: -------------------------------------------------------------------------------- 1 | """HTTP request handler classes. 2 | 3 | This file contains HTTP request handler classes which are used in the 4 | web_server.py and graph_web_server.py examples. 5 | They also use files text_web_page.html and graph_web_page.html 6 | """ 7 | 8 | # Copyright 2020-2023 Metriful Ltd. 9 | # Licensed under the MIT License - for further details see LICENSE.txt 10 | 11 | # For code examples, datasheet and user guide, visit 12 | # https://github.com/metriful/sensor 13 | 14 | from http.server import BaseHTTPRequestHandler 15 | from collections import deque 16 | import struct 17 | import pkgutil 18 | import jinja2 19 | from pathlib import Path 20 | from subprocess import check_output 21 | from . import sensor_functions as sensor 22 | from . import sensor_constants as const 23 | 24 | 25 | class SimpleWebpageHandler(BaseHTTPRequestHandler): 26 | """Make a simple text webpage to display environment data. 27 | 28 | The webpage is HTML and CSS only and does not use javascript. 29 | Periodic refresh is achieved using the 'Refresh' HTTP header. 30 | """ 31 | 32 | the_web_page = "Awaiting data..." 33 | air_data = None 34 | air_quality_data = None 35 | sound_data = None 36 | light_data = None 37 | particle_data = None 38 | interpreted_AQI_accuracy = None 39 | interpreted_AQI_value = None 40 | refresh_period_seconds = 3 41 | template = jinja2.Environment( 42 | loader=jinja2.FileSystemLoader(Path(__file__).parent), 43 | autoescape=True).get_template("text_web_page.html") 44 | 45 | @classmethod 46 | def _get_http_headers(cls): 47 | """Make headers for HTTP response with variable refresh time.""" 48 | return ("HTTP/1.1 200 OK\r\n" 49 | "Content-type: text/html\r\n" 50 | "Connection: close\r\n" 51 | f"Refresh: {cls.refresh_period_seconds}\r\n\r\n") 52 | 53 | def do_GET(self): 54 | """Implement the HTTP GET method.""" 55 | self.wfile.write( 56 | bytes(self._get_http_headers() + self.the_web_page, "utf8")) 57 | 58 | def do_HEAD(self): 59 | """Implement the HTTP HEAD method.""" 60 | self.wfile.write(bytes(self._get_http_headers(), "utf8")) 61 | 62 | @classmethod 63 | def assemble_web_page(cls, readout_time_and_date=None): 64 | """Create the updated webpage, for serving to all clients.""" 65 | cls._interpret_data() 66 | cls.the_web_page = cls.template.render( 67 | air_data=cls.air_data, air_quality_data=cls.air_quality_data, 68 | sound_data=cls.sound_data, light_data=cls.light_data, 69 | particle_data=cls.particle_data, 70 | interpreted_AQI_accuracy=cls.interpreted_AQI_accuracy, 71 | interpreted_AQI_value=cls.interpreted_AQI_value, 72 | sound_band_mids_Hz=const.sound_band_mids_Hz, 73 | readout_time_and_date=readout_time_and_date) 74 | 75 | @classmethod 76 | def _interpret_data(cls): 77 | if cls.air_quality_data is not None: 78 | cls.interpreted_AQI_accuracy = sensor.interpret_AQI_accuracy( 79 | cls.air_quality_data['AQI_accuracy']) 80 | cls.interpreted_AQI_value = sensor.interpret_AQI_value( 81 | cls.air_quality_data['AQI']) 82 | 83 | 84 | class GraphWebpageHandler(BaseHTTPRequestHandler): 85 | """Make a web page with graphs to display environment data.""" 86 | 87 | the_web_page = pkgutil.get_data(__name__, 'graph_web_page.html') 88 | data_period_seconds = 3 89 | error_response_HTTP = "HTTP/1.1 400 Bad Request\r\n\r\n" 90 | data_header = ("HTTP/1.1 200 OK\r\n" 91 | "Content-type: application/octet-stream\r\n" 92 | "Connection: close\r\n\r\n") 93 | page_header = ("HTTP/1.1 200 OK\r\n" 94 | "Content-type: text/html\r\n" 95 | "Connection: close\r\n\r\n") 96 | 97 | def do_GET(self): 98 | """Implement the HTTP GET method.""" 99 | if self.path == '/': 100 | # The web page is requested 101 | self.wfile.write(bytes(self.page_header, "utf8")) 102 | self.wfile.write(self.the_web_page) 103 | elif self.path == '/1': 104 | # A URI path of '1' indicates a request of all buffered data 105 | self.send_all_data() 106 | elif self.path == '/2': 107 | # A URI path of '2' indicates a request of the latest data only 108 | self.send_latest_data() 109 | else: 110 | # Path not recognized: send a standard error response 111 | self.wfile.write(bytes(self.error_response_HTTP, "utf8")) 112 | 113 | def do_HEAD(self): 114 | """Implement the HTTP HEAD method.""" 115 | if self.path == '/': 116 | self.wfile.write(bytes(self.page_header, "utf8")) 117 | elif self.path == '/1' or self.path == '/2': 118 | self.wfile.write(bytes(self.data_header, "utf8")) 119 | else: 120 | self.wfile.write(bytes(self.error_response_HTTP, "utf8")) 121 | 122 | def send_all_data(self): 123 | """Respond to client request by sending all buffered data.""" 124 | self.wfile.write(bytes(self.data_header, "utf8")) 125 | # First send the time period, so the web page knows 126 | # when to do the next request 127 | self.wfile.write(struct.pack('H', self.data_period_seconds)) 128 | # Send particle sensor type 129 | self.wfile.write(struct.pack('B', sensor.PARTICLE_SENSOR)) 130 | # Send choice of temperature unit 131 | self.wfile.write(struct.pack('B', int(sensor.USE_FAHRENHEIT))) 132 | # Send the length of the data buffers (the number of values 133 | # of each variable) 134 | self.wfile.write(struct.pack('H', len(self.temperature))) 135 | # Send the data in the specific order: 136 | for p in [self.AQI, self.temperature, self.pressure, self.humidity, 137 | self.SPL, self.illuminance, self.bVOC, self.particle]: 138 | self.wfile.write(struct.pack(str(len(p)) + 'f', *p)) 139 | 140 | def send_latest_data(self): 141 | """Respond to client request by sending only the most recent data.""" 142 | self.wfile.write(bytes(self.data_header, "utf8")) 143 | # Send the most recent values, if buffers are not empty 144 | if self.temperature: 145 | data = [self.AQI[-1], self.temperature[-1], self.pressure[-1], 146 | self.humidity[-1], self.SPL[-1], self.illuminance[-1], 147 | self.bVOC[-1]] 148 | if self.particle: 149 | data.append(self.particle[-1]) 150 | self.wfile.write(struct.pack(str(len(data)) + 'f', *data)) 151 | 152 | @classmethod 153 | def set_buffer_length(cls, buffer_length): 154 | """Create a FIFO data buffer for each variable.""" 155 | cls.temperature = deque(maxlen=buffer_length) 156 | cls.pressure = deque(maxlen=buffer_length) 157 | cls.humidity = deque(maxlen=buffer_length) 158 | cls.AQI = deque(maxlen=buffer_length) 159 | cls.bVOC = deque(maxlen=buffer_length) 160 | cls.SPL = deque(maxlen=buffer_length) 161 | cls.illuminance = deque(maxlen=buffer_length) 162 | cls.particle = deque(maxlen=buffer_length) 163 | 164 | @classmethod 165 | def update_air_data(cls, air_data): 166 | cls.temperature.append(air_data['T']) 167 | cls.pressure.append(air_data['P_Pa']) 168 | cls.humidity.append(air_data['H_pc']) 169 | 170 | @classmethod 171 | def update_air_quality_data(cls, air_quality_data): 172 | cls.AQI.append(air_quality_data['AQI']) 173 | cls.bVOC.append(air_quality_data['bVOC']) 174 | 175 | @classmethod 176 | def update_light_data(cls, light_data): 177 | cls.illuminance.append(light_data['illum_lux']) 178 | 179 | @classmethod 180 | def update_sound_data(cls, sound_data): 181 | cls.SPL.append(sound_data['SPL_dBA']) 182 | 183 | @classmethod 184 | def update_particle_data(cls, particle_data): 185 | cls.particle.append(particle_data['concentration']) 186 | 187 | 188 | def get_IP_addresses(): 189 | """Get this computer's IP addresses.""" 190 | ips = [x.strip() for x in check_output( 191 | ['hostname', '-I']).decode().strip().split('\n')] 192 | if len(ips) == 1 and ips[0] == '': 193 | return [] 194 | else: 195 | return ips 196 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/sensor_package/text_web_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Metriful Sensor Demo 8 | 23 | 24 | 25 |

Indoor Environment Data

26 | 27 | {% if air_data is not none %} 28 |

29 |

Air Data

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
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'] }}Ω
49 |

50 | {% endif %} 51 | 52 | {% if air_quality_data is not none %} 53 |

54 |

Air Quality Data

55 | {% if air_quality_data['AQI_accuracy'] == 0 %} 56 | {{ interpreted_AQI_accuracy }} 57 | {% else %} 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
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
76 | {% endif %} 77 |

78 | {% endif %} 79 | 80 | {% if sound_data is not none %} 81 |

82 |

Sound Data

83 | 84 | 85 | 86 | 87 | 88 | {% for band_freq in sound_band_mids_Hz %} 89 | 90 | 91 | 92 | 93 | 94 | {% endfor %} 95 | 96 | 97 | 98 | 99 |
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
100 |

101 | {% endif %} 102 | 103 | {% if light_data is not none %} 104 |

105 |

Light Data

106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
Illuminance{{ '%.2f' % light_data['illum_lux'] }}lux
White Light Level{{ light_data['white'] }}
116 |

117 | {% endif %} 118 | 119 | {% if particle_data is not none %} 120 |

121 |

Air Particulate Data

122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 |
Sensor Duty Cycle{{ '%.2f' % particle_data['duty_cycle_pc'] }}%
Particle Concentration{{ '%.2f' % particle_data['concentration'] }}{{ particle_data['conc_unit'] }}
133 |

134 | {% endif %} 135 | 136 | {% if readout_time_and_date is not none %} 137 |

138 |

The last sensor reading was at {{ readout_time_and_date }}

139 |

140 | {% endif %} 141 | 142 |

143 | sensor.metriful.com 144 |

145 | 146 | 147 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/simple_read_T_H.py: -------------------------------------------------------------------------------- 1 | """Example of measurements using the Metriful MS430 from a Raspberry Pi. 2 | 3 | Demonstrates multiple ways of reading and displaying the temperature 4 | and humidity data. View the output in the terminal. The other data 5 | can be measured and displayed in a similar way. 6 | """ 7 | 8 | # Copyright 2020-2023 Metriful Ltd. 9 | # Licensed under the MIT License - for further details see LICENSE.txt 10 | 11 | # For code examples, datasheet and user guide, visit 12 | # https://github.com/metriful/sensor 13 | 14 | import time 15 | import sensor_package.sensor_functions as sensor 16 | import sensor_package.sensor_constants as const 17 | 18 | # Set up the GPIO and I2C communications bus 19 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 20 | 21 | # Initiate an on-demand data measurement 22 | I2C_bus.write_byte(sensor.i2c_7bit_address, const.ON_DEMAND_MEASURE_CMD) 23 | 24 | # Now wait for the ready signal (falling edge) before continuing 25 | while (not GPIO.event_detected(sensor.READY_pin)): 26 | time.sleep(0.05) 27 | 28 | # New data are now ready to read; this can be done in multiple ways: 29 | 30 | 31 | # 1. Simplest way: use the example functions 32 | 33 | # Read all the "air data" from the MS430. This includes temperature and 34 | # humidity as well as pressure and gas sensor data. Return the data as 35 | # a data dictionary. 36 | air_data = sensor.get_air_data(I2C_bus) 37 | 38 | # Then print all the values onto the screen 39 | sensor.writeAirData(None, air_data, False) 40 | 41 | # Or you can use the values directly 42 | print(f"The temperature is: {air_data['T_C']:.1f} {air_data['C_unit']}") 43 | print(f"The humidity is: {air_data['H_pc']:.1f} %") 44 | 45 | # Temperature can also be output in Fahrenheit units 46 | print(f"The temperature is: {air_data['T_F']:.1f} {air_data['F_unit']}") 47 | 48 | # The default temperature unit can be set in sensor_functions.py and used like: 49 | print(f"The temperature is: {air_data['T']:.1f} {air_data['T_unit']}") 50 | 51 | print("-----------------------------") 52 | 53 | # 2. Advanced: read and decode only the humidity value 54 | 55 | # Get the data from the MS430 56 | raw_data = I2C_bus.read_i2c_block_data(sensor.i2c_7bit_address, 57 | const.H_READ, const.H_BYTES) 58 | 59 | # Decode the humidity: the first received byte is the integer part, the 60 | # second byte is the fractional part (to one decimal place). 61 | humidity = raw_data[0] + float(raw_data[1])/10.0 62 | 63 | # Print it: the units are percentage relative humidity. 64 | print(f"Humidity = {humidity:.1f} %") 65 | 66 | print("-----------------------------") 67 | 68 | # 3. Advanced: read and decode only the temperature value (Celsius) 69 | 70 | # Get the data from the MS430 71 | raw_data = I2C_bus.read_i2c_block_data(sensor.i2c_7bit_address, 72 | const.T_READ, const.T_BYTES) 73 | 74 | # Find the positive magnitude of the integer part of the temperature by 75 | # doing a bitwise AND of the first received byte with TEMPERATURE_VALUE_MASK 76 | temp_positive_integer = raw_data[0] & const.TEMPERATURE_VALUE_MASK 77 | 78 | # The second received byte is the fractional part to one decimal place 79 | temp_fraction = raw_data[1] 80 | 81 | # Combine to form a positive floating point number: 82 | temperature = temp_positive_integer + float(temp_fraction)/10.0 83 | 84 | # Now find the sign of the temperature: if the most-significant bit of 85 | # the first byte is a 1, the temperature is negative (below 0 C) 86 | if (raw_data[0] & const.TEMPERATURE_SIGN_MASK) != 0: 87 | # The bit is a 1: temperature is negative 88 | temperature = -temperature 89 | 90 | # Print the temperature: the units are degrees Celsius. 91 | print(f"Temperature = {temperature:.1f} {const.CELSIUS_SYMBOL}") 92 | 93 | ######################################################### 94 | 95 | GPIO.cleanup() 96 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/simple_read_sound.py: -------------------------------------------------------------------------------- 1 | """Example of using the Metriful MS430 to measure sound, from a Raspberry Pi. 2 | 3 | Demonstrates multiple ways of reading and displaying the sound data. 4 | View the output in the terminal. The other data can be measured 5 | and displayed in a similar way. 6 | """ 7 | 8 | # Copyright 2020-2023 Metriful Ltd. 9 | # Licensed under the MIT License - for further details see LICENSE.txt 10 | 11 | # For code examples, datasheet and user guide, visit 12 | # https://github.com/metriful/sensor 13 | 14 | import time 15 | import sensor_package.sensor_functions as sensor 16 | import sensor_package.sensor_constants as const 17 | 18 | # Set up the GPIO and I2C communications bus 19 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 20 | 21 | ######################################################### 22 | 23 | # Wait for the microphone signal to stabilize (takes approximately 24 | # 1.5 seconds). This only needs to be done once after the 25 | # MS430 is powered-on or reset. 26 | time.sleep(1.5) 27 | 28 | ######################################################### 29 | 30 | # Initiate an on-demand data measurement 31 | I2C_bus.write_byte(sensor.i2c_7bit_address, const.ON_DEMAND_MEASURE_CMD) 32 | 33 | # Now wait for the ready signal (falling edge) before continuing 34 | while (not GPIO.event_detected(sensor.READY_pin)): 35 | time.sleep(0.05) 36 | 37 | # New data are now ready to read; this can be done in multiple ways: 38 | 39 | 40 | # 1. Simplest way: use the example functions 41 | 42 | # Read all sound data from the MS430 and convert to a Python dictionary 43 | sound_data = sensor.get_sound_data(I2C_bus) 44 | 45 | # Then print all the values onto the screen 46 | sensor.writeSoundData(None, sound_data, False) 47 | 48 | # Or you can use the dictionary values directly, for example: 49 | print("The sound pressure level is: " + str(sound_data['SPL_dBA']) + " dBA") 50 | 51 | 52 | # 2. Read the raw data bytes from the MS430 using an I2C function 53 | raw_data = I2C_bus.read_i2c_block_data( 54 | sensor.i2c_7bit_address, const.SOUND_DATA_READ, const.SOUND_DATA_BYTES) 55 | 56 | # Decode the values and return then as a Python dictionary 57 | sound_data = sensor.extractSoundData(raw_data) 58 | 59 | # Print the dictionary values in the same ways as before 60 | sensor.writeSoundData(None, sound_data, False) 61 | print("The sound pressure level is: " + str(sound_data['SPL_dBA']) + " dBA") 62 | 63 | ######################################################### 64 | 65 | GPIO.cleanup() 66 | -------------------------------------------------------------------------------- /Python/Raspberry_Pi/web_server.py: -------------------------------------------------------------------------------- 1 | """Example of serving a text web page on a local network, from a Raspberry Pi. 2 | 3 | Example code for serving a simple web page over a local network to 4 | display environment data read from the Metriful MS430. 5 | This example is designed to run with Python 3 on a Raspberry Pi. 6 | 7 | All environment data values are measured and displayed on a text 8 | web page generated by this program acting as a simple web server. 9 | The web page can be viewed from other devices connected to the same 10 | network(s) as the host Raspberry Pi, including wired and wireless 11 | networks. 12 | 13 | NOTE: if you run, exit, then re-run this program, you may get an 14 | "Address already in use" error. This ends after a short period: wait 15 | one minute then retry. 16 | """ 17 | 18 | # Copyright 2020-2023 Metriful Ltd. 19 | # Licensed under the MIT License - for further details see LICENSE.txt 20 | 21 | # For code examples, datasheet and user guide, visit 22 | # https://github.com/metriful/sensor 23 | 24 | import time 25 | import socketserver 26 | from datetime import datetime 27 | import sensor_package.servers as server 28 | import sensor_package.sensor_functions as sensor 29 | import sensor_package.sensor_constants as const 30 | 31 | ######################################################### 32 | # USER-EDITABLE SETTINGS 33 | 34 | # Choose how often to read and update data (every 3, 100, or 300 seconds) 35 | # The web page can be refreshed more often but the data will not change 36 | cycle_period = const.CYCLE_PERIOD_3_S 37 | 38 | # The web page address will be: 39 | # http://:8080 e.g. http://172.24.1.1:8080 40 | 41 | # To find your Raspberry Pi's IP address: 42 | # 1. Enter the command ifconfig in a terminal 43 | # 2. Each available network connection displays a block of output 44 | # 3. Ignore the "lo" output block 45 | # 4. The IP address on each network is displayed after "inet" 46 | # 47 | # Example - part of an output block showing the address 172.24.1.1 48 | # 49 | # wlan0: flags=4163 mtu 1500 50 | # inet 172.24.1.1 netmask 255.255.255.0 broadcast 172.24.1.255 51 | 52 | # END OF USER-EDITABLE SETTINGS 53 | ######################################################### 54 | 55 | # Set up the GPIO and I2C communications bus 56 | (GPIO, I2C_bus) = sensor.SensorHardwareSetup() 57 | 58 | # Apply the chosen settings to the MS430 59 | I2C_bus.write_i2c_block_data( 60 | sensor.i2c_7bit_address, 61 | const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) 62 | I2C_bus.write_i2c_block_data( 63 | sensor.i2c_7bit_address, const.CYCLE_TIME_PERIOD_REG, [cycle_period]) 64 | 65 | # Set the automatic refresh period of the web page. It should refresh 66 | # at least as often as new data are obtained. A more frequent refresh is 67 | # best for long cycle periods because the page access will be 68 | # out-of-step with the cycle. Users can also manually refresh the page. 69 | if (cycle_period == const.CYCLE_PERIOD_3_S): 70 | server.SimpleWebpageHandler.refresh_period_seconds = 3 71 | elif (cycle_period == const.CYCLE_PERIOD_100_S): 72 | server.SimpleWebpageHandler.refresh_period_seconds = 30 73 | else: # CYCLE_PERIOD_300_S 74 | server.SimpleWebpageHandler.refresh_period_seconds = 50 75 | 76 | # Choose the TCP port number for the web page. 77 | port = 8080 78 | # The port can be any unused number from 1-65535 but values below 1024 79 | # require this program to be run as super-user as follows: 80 | # sudo python3 web_server.py 81 | # Port 80 is the default for HTTP, and with this value the port number 82 | # can be omitted from the web address. e.g. http://172.24.1.1 83 | 84 | print("Starting the web server...") 85 | ips = server.get_IP_addresses() 86 | if not ips: 87 | print("Warning: no networks detected.") 88 | else: 89 | print("Your web page will be available at:") 90 | for ip in ips: 91 | print(f" http://{ip}:{port}") 92 | print("For more information on network IP addresses, " 93 | "run the command ifconfig in a terminal.") 94 | print("Press ctrl-c to exit at any time.") 95 | 96 | the_server = socketserver.TCPServer(("", port), server.SimpleWebpageHandler) 97 | the_server.timeout = 0.1 98 | 99 | # Enter cycle mode to start periodic data output 100 | I2C_bus.write_byte(sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) 101 | 102 | while True: 103 | 104 | # While waiting for the next data release, respond to client requests 105 | # by serving the web page with the last available data. 106 | while not GPIO.event_detected(sensor.READY_pin): 107 | the_server.handle_request() 108 | time.sleep(0.05) 109 | 110 | # Now read all data from the MS430 and pass to the web page 111 | 112 | # Air data 113 | server.SimpleWebpageHandler.air_data = sensor.get_air_data(I2C_bus) 114 | 115 | # Air quality data 116 | # The initial self-calibration of the air quality data may take several 117 | # minutes to complete. During this time the accuracy parameter is zero 118 | # and the data values are not valid. 119 | server.SimpleWebpageHandler.air_quality_data = sensor.get_air_quality_data(I2C_bus) 120 | 121 | # Light data 122 | server.SimpleWebpageHandler.light_data = sensor.get_light_data(I2C_bus) 123 | 124 | # Sound data 125 | server.SimpleWebpageHandler.sound_data = sensor.get_sound_data(I2C_bus) 126 | 127 | # Particle data 128 | # This requires the connection of a particulate sensor (invalid 129 | # values will be obtained if this sensor is not present). 130 | # Also note that, due to the low pass filtering used, the 131 | # particle data become valid after an initial initialization 132 | # period of approximately one minute. 133 | if sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF: 134 | server.SimpleWebpageHandler.particle_data = sensor.get_particle_data( 135 | I2C_bus, sensor.PARTICLE_SENSOR) 136 | 137 | # Create the updated web page ready for client requests, passing 138 | # the current date and time for displaying with the data 139 | server.SimpleWebpageHandler.assemble_web_page( 140 | f'{datetime.now():%H:%M:%S %Y-%m-%d}') 141 | -------------------------------------------------------------------------------- /Python/graph_viewer_I2C.py: -------------------------------------------------------------------------------- 1 | """Real-time display of MS430 data, using a Raspberry Pi. 2 | 3 | This example runs on Raspberry Pi only, and the MS430 sensor board 4 | must be connected to the Raspberry Pi I2C/GPIO pins. 5 | 6 | An alternate version, "graph_viewer_serial.py" runs on multiple operating 7 | systems (including Windows and Linux) and uses serial over USB to get 8 | data from the MS430 sensor via a microcontroller board (e.g. Arduino, 9 | ESP8266, etc). 10 | 11 | This example displays a graphical user interface with real-time 12 | updating graphs showing data from the MS430 sensor board. 13 | 14 | Installation instructions for the necessary packages are in the 15 | readme and User Guide. 16 | """ 17 | 18 | # Copyright 2020-2023 Metriful Ltd. 19 | # Licensed under the MIT License - for further details see LICENSE.txt 20 | 21 | # For code examples, datasheet and user guide, visit 22 | # https://github.com/metriful/sensor 23 | 24 | import time 25 | from datetime import datetime 26 | from PyQt5.QtWidgets import QApplication 27 | from GraphViewer import GraphViewer 28 | import Raspberry_Pi.sensor_package.sensor_functions as sensor 29 | import Raspberry_Pi.sensor_package.sensor_constants as const 30 | 31 | ######################################################### 32 | # USER-EDITABLE SETTINGS 33 | 34 | # Choose the time delay between data measurements. 35 | # This can be 3/100/300 seconds in cycle mode, or any 36 | # delay time in on-demand mode. 37 | 38 | # Set cycle_period_code=None to use on-demand mode, or choose any 39 | # of: CYCLE_PERIOD_3_S, CYCLE_PERIOD_100_S, CYCLE_PERIOD_300_S 40 | cycle_period_code = None 41 | # OR: 42 | on_demand_delay_ms = 0 # Choose any number of milliseconds 43 | # This delay is in addition to the 0.5 second readout time. 44 | 45 | # Temperature and particle data are less accurate if read more 46 | # frequently than every 2 seconds 47 | 48 | # Maximum number of values of each variable to store and display: 49 | data_buffer_length = 500 50 | 51 | # Specify the particle sensor model (PPD42/SDS011/none) and temperature 52 | # units (Celsuis/Fahrenheit) in Raspberry_Pi/sensor_functions.py 53 | 54 | # END OF USER-EDITABLE SETTINGS 55 | ######################################################### 56 | 57 | 58 | class GraphViewerI2C(GraphViewer): 59 | """Real-time display of MS430 data, using a Raspberry Pi.""" 60 | 61 | def __init__(self, buffer_length, cycle_period, OD_delay_ms): 62 | """Set up the I2C and the MS430 board.""" 63 | super().__init__(buffer_length, sensor.PARTICLE_SENSOR, 64 | sensor.USE_FAHRENHEIT) 65 | self.setupSensorI2C(cycle_period, OD_delay_ms) 66 | air_quality_data = self.cycle_mode 67 | particle_data = sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF 68 | flag_data = False 69 | self.setDataRequired(air_quality_data, particle_data, flag_data) 70 | 71 | def setupSensorI2C(self, cycle_period, OD_delay_ms): 72 | """Set up the MS430 sensor by selecting mode and particle sensor.""" 73 | (self.GPIO, self.I2C_bus) = sensor.SensorHardwareSetup() 74 | if sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF: 75 | self.I2C_bus.write_i2c_block_data( 76 | sensor.i2c_7bit_address, 77 | const.PARTICLE_SENSOR_SELECT_REG, [sensor.PARTICLE_SENSOR]) 78 | if (cycle_period is None) and (OD_delay_ms is None): 79 | raise ValueError( 80 | "Either cycle_period or OD_delay_ms must be specified") 81 | if cycle_period is not None: # Cycle mode with 3/100/300 second delays 82 | self.cycle_mode = True 83 | self.I2C_bus.write_i2c_block_data( 84 | sensor.i2c_7bit_address, 85 | const.CYCLE_TIME_PERIOD_REG, [cycle_period]) 86 | self.I2C_bus.write_byte( 87 | sensor.i2c_7bit_address, const.CYCLE_MODE_CMD) 88 | else: # On-demand mode with chosen time delay between measurements 89 | self.cycle_mode = False 90 | self.I2C_bus.write_byte( 91 | sensor.i2c_7bit_address, const.ON_DEMAND_MEASURE_CMD) 92 | self.delaying = False 93 | self.OD_delay_ms = OD_delay_ms 94 | 95 | def getDataFunction(self): 96 | """Obtain new data from I2C and put in data_buffer. 97 | 98 | Returns True if new data were obtained, else returns False. 99 | """ 100 | if self.cycle_mode: 101 | if self.GPIO.event_detected(sensor.READY_pin): 102 | self.readData() 103 | return True 104 | else: # On-demand mode 105 | if self.delaying: 106 | time_now_ms = time.time()*1000 107 | if (time_now_ms - self.time_start_ms) >= self.OD_delay_ms: 108 | # Trigger a new measurement 109 | self.I2C_bus.write_byte( 110 | sensor.i2c_7bit_address, const.ON_DEMAND_MEASURE_CMD) 111 | self.delaying = False 112 | else: 113 | if self.GPIO.event_detected(sensor.READY_pin): 114 | self.readData() 115 | self.delaying = True 116 | self.time_start_ms = time.time()*1000 117 | return True 118 | return False 119 | 120 | def readData(self): 121 | """Read the newly available data from the sensor board.""" 122 | self.setWindowTitle('Indoor Environment Data') 123 | air_data = sensor.get_air_data(self.I2C_bus) 124 | air_quality_data = sensor.get_air_quality_data(self.I2C_bus) 125 | light_data = sensor.get_light_data(self.I2C_bus) 126 | sound_data = sensor.get_sound_data(self.I2C_bus) 127 | particle_data = sensor.get_particle_data( 128 | self.I2C_bus, sensor.PARTICLE_SENSOR) 129 | self.putDataInBuffer(air_data, air_quality_data, 130 | light_data, sound_data, particle_data) 131 | 132 | def appendData(self, start_index, data): 133 | """Add new data to the data buffer.""" 134 | for i, value in enumerate(data): 135 | self.data_buffer[start_index + i].append(value) 136 | return (start_index + len(data)) 137 | 138 | def putDataInBuffer(self, air_data, air_quality_data, light_data, 139 | sound_data, particle_data): 140 | """Store the data and also the time/date.""" 141 | i = 0 142 | i = self.appendData( 143 | i, [air_data['T'], air_data['P_Pa'], 144 | air_data['H_pc'], air_data['G_ohm']]) 145 | if (self.cycle_mode): 146 | i = self.appendData(i, [air_quality_data['AQI'], 147 | air_quality_data['CO2e'], 148 | air_quality_data['bVOC'], 149 | air_quality_data['AQI_accuracy']]) 150 | i = self.appendData(i, [light_data['illum_lux'], light_data['white']]) 151 | i = self.appendData(i, [sound_data['SPL_dBA']] 152 | + [sound_data['SPL_bands_dB'][i] for i in 153 | range(0, self.sound_band_number)] 154 | + [sound_data['peak_amp_mPa']]) 155 | if sensor.PARTICLE_SENSOR != const.PARTICLE_SENSOR_OFF: 156 | i = self.appendData( 157 | i, [particle_data['duty_cycle_pc'], 158 | particle_data['concentration']]) 159 | self.time_data.append(datetime.now().timestamp()) 160 | 161 | 162 | if __name__ == '__main__': 163 | theApp = QApplication([]) 164 | gv = GraphViewerI2C(data_buffer_length, 165 | cycle_period_code, on_demand_delay_ms) 166 | gv.start() 167 | theApp.exec_() 168 | -------------------------------------------------------------------------------- /Python/graph_viewer_serial.py: -------------------------------------------------------------------------------- 1 | """Real-time display of MS430 data, from a host device over USB serial. 2 | 3 | This example runs on multiple operating systems (including Windows 4 | and Linux) and uses serial over USB to get data from the MS430 sensor 5 | via a microcontroller board (e.g. Arduino, ESP8266, etc). 6 | 7 | An alternate version, "graph_viewer_I2C.py" is provided for the 8 | Raspberry Pi, where the MS430 board is directly connected to the Pi 9 | using the GPIO/I2C pins. 10 | 11 | This example displays a graphical user interface with real-time 12 | updating graphs showing data from the MS430 sensor board. 13 | 14 | Instructions (installation instructions are in the readme / User Guide) 15 | 16 | 1) Program the microcontroller board with either "cycle_readout.ino" 17 | or "on_demand_readout.ino", with printDataAsColumns = true 18 | 19 | 2) Connect the microcontroller USB cable to your PC and close any 20 | serial monitor software. 21 | 22 | 3) Put the serial port name (system dependent) in the serial_port_name 23 | parameter below. 24 | 25 | 4) Run this program with python3 26 | """ 27 | 28 | # Copyright 2020-2023 Metriful Ltd. 29 | # Licensed under the MIT License - for further details see LICENSE.txt 30 | 31 | # For code examples, datasheet and user guide, visit 32 | # https://github.com/metriful/sensor 33 | 34 | import serial 35 | from datetime import datetime 36 | from PyQt5.QtWidgets import QApplication 37 | from GraphViewer import GraphViewer 38 | import Raspberry_Pi.sensor_package.sensor_constants as const 39 | 40 | ######################################################### 41 | # USER-EDITABLE SETTINGS 42 | 43 | # Specify the serial port name on which the microcontroller is connected 44 | # e.g. on Windows this is usually a name like "COM1", on Linux it is 45 | # usually a path like "/dev/ttyACM0" 46 | # NOTE: close all other serial applications (e.g. Arduino Serial Monitor) 47 | serial_port_name = "/dev/ttyACM0" 48 | 49 | # Maximum number of values of each variable to store and display: 50 | data_buffer_length = 500 51 | 52 | # Specify the particle sensor model (PPD42/SDS011/none) and temperature 53 | # units (Celsius/Fahrenheit): 54 | particle_sensor_type = const.PARTICLE_SENSOR_OFF 55 | use_fahrenheit = False # else uses Celsius 56 | 57 | # END OF USER-EDITABLE SETTINGS 58 | ######################################################### 59 | 60 | 61 | class GraphViewerSerial(GraphViewer): 62 | """Real-time display of MS430 data, from a host device over USB serial.""" 63 | 64 | def __init__(self, buffer_length, serial_port): 65 | """Set up the serial interface to the MS430 host.""" 66 | super().__init__(buffer_length, particle_sensor_type, use_fahrenheit) 67 | self.serial_port = serial.Serial( 68 | port=serial_port, 69 | baudrate=9600, 70 | parity=serial.PARITY_NONE, 71 | stopbits=serial.STOPBITS_ONE, 72 | bytesize=serial.EIGHTBITS, 73 | timeout=0.02) 74 | self.initial_discard_lines = 2 75 | self.startup_line_count = 0 76 | self.startup = True 77 | 78 | def serialStartupCompleted(self, data_strings): 79 | """Check that the number of values received is correct.""" 80 | if not self.startup: 81 | return True 82 | if self.startup_line_count < self.initial_discard_lines: 83 | self.startup_line_count += 1 84 | return False 85 | self.startup = False 86 | if len(data_strings) == 15: 87 | self.setDataRequired(False, False, True) 88 | elif len(data_strings) == 18: 89 | self.setDataRequired(False, True, True) 90 | elif len(data_strings) == 19: 91 | self.setDataRequired(True, False, True) 92 | elif len(data_strings) == 22: 93 | self.setDataRequired(True, True, True) 94 | else: 95 | raise RuntimeError('Unexpected number of data columns') 96 | self.setWindowTitle('Indoor Environment Data') 97 | return True 98 | 99 | def getDataFunction(self): 100 | """Check for new serial data.""" 101 | response = self.serial_port.readline() 102 | if (not ((response is None) or (len(response) == 0))): 103 | try: 104 | data_strings = response.decode('utf-8').split() 105 | if self.serialStartupCompleted(data_strings): 106 | if (len(data_strings) == len(self.data_name_index)): 107 | # Convert strings to numbers and store the data 108 | float_data = [float(s) for s in data_strings] 109 | for i in range(len(self.data_name_index)): 110 | self.data_buffer[i].append(float_data[i]) 111 | self.time_data.append(datetime.now().timestamp()) 112 | return True 113 | except Exception: 114 | pass 115 | return False # no new data 116 | 117 | 118 | if __name__ == '__main__': 119 | theApp = QApplication([]) 120 | gv = GraphViewerSerial(data_buffer_length, serial_port_name) 121 | gv.start() 122 | theApp.exec_() 123 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | 4 | 5 | This file lists solutions for some common problems. Please check it before opening a GitHub issue. 6 | 7 | ### Contents 8 | **[Standard checks](#standard-checks)**
9 | **[RuntimeError: Failed to add edge detection](#edge-detection-error)**
10 | **[WiFi Access Point examples not starting](#wifi-access-point-examples-not-starting)**
11 | **[ESP8266 problems](#esp8266-problems)**
12 | **[ESP32 problems](#esp32-problems)**
13 | **[Arduino Nano 33 IoT problems](#arduino-nano-33-iot-problems)**
14 | **[WiFi connection problems](#wifi-connection-problems)**
15 | **[Particle sensor problems](#particle-sensor-problems)**
16 | **[Slow air quality accuracy change](#slow-air-quality-accuracy-change)**
17 | **[Temperature measurement is too high](#temperature-measurement-is-too-high)**
18 | 19 | 20 | ## Standard checks 21 | 22 | Most problems can be resolved by following these steps: 23 | 24 | 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. 25 | 2. Ensure you have the most recent sensor code and instructions from our [GitHub repository](https://www.github.com/metriful/sensor) 26 | 3. Check your software, library and board versions: for best reliability, use the versions listed in the [Readme](README.md#library-and-software-versions). 27 | 4. Remove all wire connections and re-wire carefully. 28 | 5. If you have edited the code, go back to the original version and ensure it still works. 29 | 30 | 31 | ## Edge detection error 32 | 33 | The Python examples running on Raspberry Pi may cause a ```Failed to add edge detection``` runtime error. This happens in the "Bookworm" version of Raspberry Pi OS, due to a problem with the old GPIO library. It may also occur in future OS versions. The simple fix is to install the new improved GPIO library by running the following: 34 | ``` 35 | sudo apt remove python3-rpi.gpio 36 | sudo apt update 37 | sudo apt install python3-rpi-lgpio 38 | ``` 39 | 40 | 41 | ## WiFi Access Point examples not starting 42 | 43 | * 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. 44 | * Solution: simply power-cycle the host board (unplug and re-plug power). 45 | 46 | 47 | ## ESP8266 problems 48 | 49 | There are many different development boards which use the ESP8266 module. Some may have different pin labels, or have different pin positions, so you may need to research your board or (rarely) edit the host_pin_definitions.h file. 50 | 51 | The ESP8266 does not have a hardware I2C module, so any of the normal GPIO pins can be used for the I2C bus. 52 | 53 | 54 | ## ESP32 problems 55 | 56 | 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. 57 | 58 | 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. 59 | 60 | 61 | ## Arduino Nano 33 IoT problems 62 | 63 | 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. 64 | 65 | 66 | ## WiFi connection problems 67 | 68 | If WiFi connection never succeeds, check the following: 69 | * SSID and password are correct 70 | * The WiFi access point is functioning and within range 71 | * WiFi library, Arduino board package, and board firmware versions are up-to-date and compatible 72 | * Power supply is sufficient 73 | * Arduino WiFi antenna is not damaged (very easy to damage on the Arduino Nano 33 IoT) 74 | 75 | 76 | ## Particle sensor problems 77 | 78 | ### Measured value does not change 79 | * Check wiring 80 | * Check the input voltage - the "5V" must be 4.7-5.3 V 81 | * If using a separate 5V source, the source GND must be connected to both the host GND and the MS430 GND. 82 | 83 | ### Measured value fluctuates a lot 84 | * This is typical for particle sensors, especially the PPD42 85 | * Check the input voltage - the "5V" must be 4.7-5.3 V 86 | * Using a separate, regulated 5V supply can reduce the fluctuation. 87 | 88 | 89 | ## Slow air quality accuracy change 90 | 91 | The air quality measurement accuracy can be slow to increase, especially with new sensors, for two reasons: 92 | 93 | 1. The air quality sensor uses a small internal heater - this will gradually evaporate off impurities when used after a period of storage. 94 | 95 | 2. The analysis algorithm self-calibrates in response to a range of air qualities, which must be provided to the sensor after it starts monitoring. 96 | 97 | To speed up this process: 98 | 99 | * 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) 100 | * Keep it running as long as possible, ideally at least 48 hours 101 | * 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. 102 | 103 | In normal use the accuracy does not remain on 3 (highest) all the time but instead will periodically decrease/increase as calibration is ongoing. 104 | 105 | 106 | ## Temperature measurement is too high 107 | 108 | * The temperature sensor measurement may have a small offset compared to the true temperature. 109 | 110 | * The offset is different for each sensor but should remain mostly constant, so you can subtract it in your software after comparing with an accurate thermometer. 111 | 112 | * A larger offset will result if you put the sensor board in a case, especially if together with the host system. Hosts (and particle sensors) create heat, so should be separated or insulated from the sensor board. 113 | 114 | 115 | 116 | ## Support 117 | 118 | If the information here does not help, please open a GitHub issue. 119 | 120 | -------------------------------------------------------------------------------- /User_guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metriful/sensor/751fc1a6fce5657c1600a4088bb6f1746aad5e1f/User_guide.pdf -------------------------------------------------------------------------------- /pictures/graph_viewer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metriful/sensor/751fc1a6fce5657c1600a4088bb6f1746aad5e1f/pictures/graph_viewer.png -------------------------------------------------------------------------------- /pictures/graph_web_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metriful/sensor/751fc1a6fce5657c1600a4088bb6f1746aad5e1f/pictures/graph_web_server.png -------------------------------------------------------------------------------- /pictures/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metriful/sensor/751fc1a6fce5657c1600a4088bb6f1746aad5e1f/pictures/group.png -------------------------------------------------------------------------------- /pictures/home_assistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metriful/sensor/751fc1a6fce5657c1600a4088bb6f1746aad5e1f/pictures/home_assistant.png -------------------------------------------------------------------------------- /pictures/tago.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metriful/sensor/751fc1a6fce5657c1600a4088bb6f1746aad5e1f/pictures/tago.png --------------------------------------------------------------------------------