├── 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 | %s |
65 | %s |
66 | %s |
67 |
68 | );
69 |
70 | // End of a data table. No variables.
71 | const char * tableEnd = QUOTE(
72 |
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 | Temperature |
33 | {{ '%.1f' % air_data['T'] }} |
34 | {{ air_data['T_unit'] }} |
35 |
36 |
37 | Pressure |
38 | {{ air_data['P_Pa'] }} | Pa |
39 |
40 |
41 | Humidity |
42 | {{ '%.1f' % air_data['H_pc'] }} | % |
43 |
44 |
45 | Gas Sensor Resistance |
46 | {{ air_data['G_ohm'] }} | Ω |
47 |
48 |
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 | Air Quality Index |
61 | {{ '%.1f' % air_quality_data['AQI'] }} | |
62 |
63 |
64 | Air Quality Summary |
65 | {{ interpreted_AQI_value }} | |
66 |
67 |
68 | Estimated CO₂ |
69 | {{ '%.1f' % air_quality_data['CO2e'] }} | ppm |
70 |
71 |
72 | Equivalent Breath VOC |
73 | {{ '%.2f' % air_quality_data['bVOC'] }} | ppm |
74 |
75 |
76 | {% endif %}
77 |
78 | {% endif %}
79 |
80 | {% if sound_data is not none %}
81 |
82 |
Sound Data
83 |
84 |
85 | A-weighted Sound Pressure Level |
86 | {{ '%.1f' % sound_data['SPL_dBA'] }} | dBA |
87 |
88 | {% for band_freq in sound_band_mids_Hz %}
89 |
90 | Frequency Band {{ loop.index }} ({{ band_freq }} Hz) SPL |
91 | {{ '%.1f' % sound_data['SPL_bands_dB'][loop.index0] }} |
92 | dB |
93 |
94 | {% endfor %}
95 |
96 | Peak Sound Amplitude |
97 | {{ '%.2f' % sound_data['peak_amp_mPa'] }} | mPa |
98 |
99 |
100 |
101 | {% endif %}
102 |
103 | {% if light_data is not none %}
104 |
105 |
Light Data
106 |
107 |
108 | Illuminance |
109 | {{ '%.2f' % light_data['illum_lux'] }} | lux |
110 |
111 |
112 | White Light Level |
113 | {{ light_data['white'] }} | |
114 |
115 |
116 |
117 | {% endif %}
118 |
119 | {% if particle_data is not none %}
120 |
121 |
Air Particulate Data
122 |
123 |
124 | Sensor Duty Cycle |
125 | {{ '%.2f' % particle_data['duty_cycle_pc'] }} | % |
126 |
127 |
128 | Particle Concentration |
129 | {{ '%.2f' % particle_data['concentration'] }} |
130 | {{ particle_data['conc_unit'] }} |
131 |
132 |
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
--------------------------------------------------------------------------------