Skip to content

Commit de6dd5a

Browse files
committedOct 25, 2021
Initial commit
1 parent c525466 commit de6dd5a

File tree

4 files changed

+573
-1
lines changed

4 files changed

+573
-1
lines changed
 

‎README.md

+66-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,67 @@
11
# RateMyAir_FW
2-
Esp8266 firmware for the air quality monitoring system project
2+
3+
Firmware for the air quality monitoring system project. Runs on the Esp8266 reading the temperature, humidity, air pressure and particulate matter.
4+
The measurements are stored in an SQLite database by calling the [RateMyAir API](https://github.com/Gbertaz/RateMyAir_API) hosted on a Raspberry Pi 4 which act as a web server. Check out the [step by step guide](https://github.com/Gbertaz/RateMyAir_API#hosting-a-net-core-5-application-on-a-raspberry-pi) on how to set up the Raspberry to host the API.
5+
6+
This is the [flowchart](https://github.com/Gbertaz/RateMyAir_FW/blob/master/images/Flowchart.png) of the implementation. Please check it out to see the details of the machine state algorithm.
7+
8+
# Features
9+
10+
* The sensors measurements are not buffered. That means it always sends to server the last sensor reading
11+
* Sends the measurements to the server by calling a HTTP Post Rest API passing the values as a json string
12+
* Sends the first measurement as soon as it is availble without waiting *SEND_DATA_INTERVAL* to elapse
13+
* The Wifi connection status is checked and re-initialized if necessary before sending data to server (see [flowchart](https://github.com/Gbertaz/RateMyAir_FW/blob/master/images/Flowchart.png))
14+
* Supports Over The Air update
15+
* Doesn't make use of the DNS. You have to assing a static *IP Address*
16+
* All the timings, intervals, Wifi parameters are configurable in [Config.h](https://github.com/Gbertaz/RateMyAir_FW/blob/master/src/Config.h)
17+
* Schematics are coming soon
18+
19+
# Prerequisites
20+
21+
This project uses a couple of libraries that I wrote with the aim to wrap a machine state into an easy and reusable code:
22+
23+
* [NonBlockingDallas](https://github.com/Gbertaz/NonBlockingDallas)
24+
* [WarmTheSDS011](https://github.com/Gbertaz/WarmTheSDS011)
25+
26+
The following libraries are also required:
27+
28+
* ESP8266HTTPClient
29+
* ESP8266mDNS
30+
* WiFiUdp
31+
* ArduinoOTA
32+
* ESP8266WiFi
33+
* OneWire
34+
* Wire
35+
* DallasTemperature
36+
* ArduinoJson
37+
* Adafruit_Sensor
38+
* Adafruit_BME280
39+
40+
# Usage
41+
42+
Edit the following parameters in [Config.h](https://github.com/Gbertaz/RateMyAir_FW/blob/master/src/Config.h) to match your LAN configuration:
43+
44+
```
45+
WIFI_SSID
46+
WIFI_PASSWORD
47+
API_URL
48+
API_KEY
49+
```
50+
51+
The *API_KEY* can be found in the [RateMyAir API](https://github.com/Gbertaz/RateMyAir_API) [*appsettings.json*](https://github.com/Gbertaz/RateMyAir_API/blob/master/RateMyAir/RateMyAir.API/appsettings.Production.json) configuration file. Replace it with your own key.
52+
53+
Finally, in [RateMyAir_FW.ino](https://github.com/Gbertaz/RateMyAir_FW/blob/master/RateMyAir_FW.ino) *SetupWifi()* function, edit the IP, Gateway and Subnet mask according to your LAN configuration
54+
55+
```
56+
IPAddress ip(192,168,1,11);
57+
IPAddress gateway(192,168,1,1);
58+
IPAddress subnet(255,255,255,0);
59+
```
60+
61+
# Hardware
62+
63+
* Esp8266 Wemos D1 Mini
64+
* BME280 I2C Humidity, Temperature and Pressure sensor
65+
* Number 2 DS18B20 Digital Temperature sensors
66+
* [Nova Fitness SDS011](http://inovafitness.com/en/a/chanpinzhongxin/95.html) particulate matter sensor
67+
* Resistor 4.7 K-Ohm

‎RateMyAir_FW.ino

+454
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,454 @@
1+
// MIT License
2+
//
3+
// Copyright(c) 2021 Giovanni Bertazzoni <nottheworstdev@gmail.com>
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files(the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions :
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
#include "src/Config.h"
24+
#include <ESP8266HTTPClient.h>
25+
#include <ESP8266mDNS.h>
26+
#include <WiFiUdp.h>
27+
#include <ArduinoOTA.h>
28+
#include <ESP8266WiFi.h>
29+
#include <OneWire.h>
30+
#include <Wire.h>
31+
#include <DallasTemperature.h>
32+
#include <ArduinoJson.h>
33+
#include <Adafruit_Sensor.h>
34+
#include <Adafruit_BME280.h>
35+
#include <NonBlockingDallas.h>
36+
#include <WarmTheSDS011.h>
37+
38+
//====================================================================
39+
// MACHINE STATE
40+
//====================================================================
41+
typedef enum states
42+
{
43+
IDLING, //0
44+
INITIAL_READING, //1
45+
UPDATING_SENSORS, //2
46+
INITIALIZING_WIFI, //3
47+
CONNECTING_WIFI, //4
48+
SENDING_DATA //5
49+
} state;
50+
51+
state _currentState; //Current state
52+
//====================================================================
53+
54+
float _temperatureOutdoor;
55+
float _temperatureIndoor;
56+
float _humidity;
57+
float _pressure;
58+
float _pm25;
59+
float _pm10;
60+
61+
bool bme280Found = false;
62+
unsigned long _lastSendingMillis;
63+
unsigned long _lastBme280ReadingMillis;
64+
unsigned long _startWifiConnectionMillis;
65+
66+
//==================================================
67+
// DS18B20 Digital Temperature sensors
68+
//==================================================
69+
OneWire oneWire(DS18B20_BUS);
70+
DallasTemperature dallasTemp(&oneWire);
71+
NonBlockingDallas nonBlockingDallas(&dallasTemp);
72+
73+
//==================================================
74+
// SDS011 Particulate matter sensor
75+
//==================================================
76+
WarmTheSDS011 sds011(SDS_RX, SDS_TX);
77+
78+
//==================================================
79+
// BME280 Temperature, Pressure, Humidity sensor
80+
//==================================================
81+
Adafruit_BME280 bme;
82+
83+
84+
//====================================================================
85+
// SETUP
86+
//====================================================================
87+
void setup() {
88+
89+
#ifdef DEBUG_INFO
90+
Serial.begin(9600);
91+
while (!Serial)
92+
;
93+
#endif
94+
95+
_lastSendingMillis = 0;
96+
_lastBme280ReadingMillis = 0;
97+
_startWifiConnectionMillis = 0;
98+
_temperatureOutdoor = 9999;
99+
_temperatureIndoor = 9999;
100+
_humidity = 9999;
101+
_pressure = 9999;
102+
_pm25 = 9999;
103+
_pm10 = 9999;
104+
105+
//Setup sensors
106+
SetupBME280();
107+
delay(100);
108+
SetupDS18B20();
109+
delay(100);
110+
SetupSDS011();
111+
delay(100);
112+
113+
SetupOverTheAirUpdate();
114+
115+
//This is necessary to reset the Wifi status
116+
WiFi.disconnect();
117+
118+
//Query the sensors to get the initial values
119+
//without waiting for their time interval to expire
120+
sds011.requestPollution();
121+
nonBlockingDallas.requestTemperature();
122+
ReadBME208();
123+
124+
//Machine state initial value
125+
_currentState = INITIAL_READING;
126+
}
127+
128+
129+
//====================================================================
130+
// LOOP
131+
//====================================================================
132+
void loop() {
133+
switch(_currentState){
134+
case IDLING:
135+
LoopIdling();
136+
break;
137+
case INITIAL_READING:
138+
LoopInitialReading();
139+
break;
140+
case UPDATING_SENSORS:
141+
LoopUpdatingSensors();
142+
break;
143+
case INITIALIZING_WIFI:
144+
LoopInitializingWifi();
145+
break;
146+
case CONNECTING_WIFI:
147+
LoopConnectingWifi();
148+
break;
149+
case SENDING_DATA:
150+
LoopSendingData();
151+
break;
152+
}
153+
154+
ArduinoOTA.handle();
155+
}
156+
157+
158+
//====================================================================
159+
// MACHINE STATE LOOPS
160+
//====================================================================
161+
void LoopIdling(){ }
162+
163+
164+
void LoopInitialReading(){
165+
166+
UpdateSensors();
167+
168+
//Once all sensors reading are complete send data to server
169+
//so I don't have to wait SEND_DATA_INTERVAL to get the first values
170+
if(_temperatureOutdoor != 9999 &&
171+
_temperatureIndoor != 9999 &&
172+
_humidity != 9999 &&
173+
_pressure != 9999 &&
174+
_pm25 != 9999 &&
175+
_pm10 != 9999){
176+
_currentState = INITIALIZING_WIFI;
177+
}
178+
}
179+
180+
void LoopUpdatingSensors(){
181+
UpdateSensors();
182+
183+
//Time to read the BME280
184+
if(millis() - _lastBme280ReadingMillis >= BME_INTERVAL || _lastBme280ReadingMillis == 0){
185+
ReadBME208();
186+
_lastBme280ReadingMillis = millis();
187+
}
188+
189+
//Time to send new data?
190+
if((millis() - _lastSendingMillis >= SEND_DATA_INTERVAL) && sds011.isBusy() == false){
191+
_currentState = SENDING_DATA;
192+
}
193+
}
194+
195+
void LoopInitializingWifi(){
196+
SetupWifi();
197+
_startWifiConnectionMillis = millis();
198+
_currentState = CONNECTING_WIFI;
199+
}
200+
201+
void LoopConnectingWifi(){
202+
203+
if(WiFi.status() == WL_CONNECTED){
204+
_currentState = SENDING_DATA;
205+
206+
#ifdef DEBUG_INFO
207+
Serial.println("Wifi connected!");
208+
#endif
209+
}
210+
else {
211+
//Check if the Wifi connection time has expired.
212+
//This is necessary to avoid getting stuck forever in LoopConnectingWifi
213+
if(millis() - _startWifiConnectionMillis >= WIFI_CONNECTION_TIMEOUT){
214+
_currentState = UPDATING_SENSORS;
215+
216+
#ifdef DEBUG_INFO
217+
Serial.println("Wifi connection failed!");
218+
#endif
219+
}
220+
}
221+
}
222+
223+
void LoopSendingData(){
224+
if(WiFi.status() != WL_CONNECTED){
225+
_currentState = INITIALIZING_WIFI;
226+
return;
227+
}
228+
229+
SendSensorData();
230+
_lastSendingMillis = millis();
231+
_currentState = UPDATING_SENSORS;
232+
233+
#ifdef DEBUG_INFO
234+
Serial.println("Data sent!");
235+
#endif
236+
}
237+
238+
void UpdateSensors(){
239+
nonBlockingDallas.update();
240+
sds011.update();
241+
}
242+
243+
244+
//====================================================================
245+
// SETUP DS18B20 TEMPERATURE SENSORS
246+
//====================================================================
247+
void SetupDS18B20(){
248+
nonBlockingDallas.begin(NonBlockingDallas::resolution_12, NonBlockingDallas::unit_C, DS18B20_INTERVAL);
249+
nonBlockingDallas.onTemperatureChange(OnTemperatureChange);
250+
}
251+
252+
//====================================================================
253+
// SETUP PARTICULATE MATTER SDS011 SENSOR
254+
//====================================================================
255+
void SetupSDS011(){
256+
sds011.begin(SDS_INTERVAL, SDS_WARMUP_INTERVAL);
257+
sds011.onIntervalElapsed(OnSdsIntervalElapsed);
258+
}
259+
260+
//====================================================================
261+
// SETUP BME280 SENSOR
262+
//====================================================================
263+
void SetupBME280(){
264+
Wire.begin(BME280_SDA, BME280_SCL);
265+
Wire.setClock(100000);
266+
delay(1000);
267+
268+
bme280Found = bme.begin();
269+
if(bme280Found == false){
270+
271+
#ifdef DEBUG_INFO
272+
Serial.println("Could not find a valid BME280 sensor, check wiring!");
273+
#endif
274+
}
275+
}
276+
277+
//====================================================================
278+
// SETUP WIFI
279+
//====================================================================
280+
void SetupWifi(){
281+
282+
// Connecting to WiFi network
283+
#ifdef DEBUG_INFO
284+
Serial.print("Connecting to Wifi network: ");
285+
Serial.println(WIFI_SSID);
286+
#endif
287+
288+
//Available options are:
289+
//WIFI_AP (only Access Point)
290+
//WIFI_STA (only client)
291+
//WIFI_AP_STA (dual, Access Point and Client. This is the default)
292+
//WIFI_OFF (turn off wifi).
293+
WiFi.mode(WIFI_STA);
294+
295+
//Static IP Address
296+
IPAddress ip(192,168,1,11);
297+
IPAddress gateway(192,168,1,1);
298+
IPAddress subnet(255,255,255,0);
299+
WiFi.config(ip, gateway, subnet);
300+
301+
//Start Wifi connection
302+
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
303+
}
304+
305+
//====================================================================
306+
// SETUP OVER THE AIR PROGRAMMING
307+
//====================================================================
308+
void SetupOverTheAirUpdate(){
309+
310+
// Default port is 8266
311+
// ArduinoOTA.setPort(8266);
312+
313+
// Default Hostname is esp8266-[ChipID]
314+
ArduinoOTA.setHostname(HOSTNAME);
315+
316+
// Set a password
317+
// ArduinoOTA.setPassword("admin");
318+
// Set a MD5 encrypted password
319+
// ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
320+
321+
ArduinoOTA.onStart([]() {
322+
if (ArduinoOTA.getCommand() == U_FLASH) {
323+
324+
} else { // U_SPIFFS
325+
326+
}
327+
});
328+
329+
ArduinoOTA.onEnd([]() {
330+
//Serial.println("\nEnd");
331+
});
332+
333+
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
334+
//Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
335+
});
336+
337+
ArduinoOTA.onError([](ota_error_t error) {
338+
//Serial.printf("Error[%u]: ", error);
339+
if (error == OTA_AUTH_ERROR) {
340+
//Serial.println("Auth Failed");
341+
} else if (error == OTA_BEGIN_ERROR) {
342+
//Serial.println("Begin Failed");
343+
} else if (error == OTA_CONNECT_ERROR) {
344+
//Serial.println("Connect Failed");
345+
} else if (error == OTA_RECEIVE_ERROR) {
346+
//Serial.println("Receive Failed");
347+
} else if (error == OTA_END_ERROR) {
348+
//Serial.println("End Failed");
349+
}
350+
351+
ESP.restart();
352+
});
353+
354+
ArduinoOTA.begin();
355+
}
356+
357+
358+
//====================================================================
359+
// SENDING SENSORS DATA TO SERVER
360+
//====================================================================
361+
void SendSensorData(){
362+
HTTPClient http;
363+
http.begin(API_URL);
364+
http.addHeader("Content-Type", "application/json");
365+
http.addHeader("ApiKey", API_KEY);
366+
367+
char *json = EncodeJson();
368+
int httpCode = http.POST(json);
369+
free(json); // !!!!! DO NOT FORGET TO FREE THE MEMORY !!!!!
370+
371+
//FOR DEBUG
372+
//String payload = http.getString();
373+
//Serial.println(httpCode); //Print HTTP return code
374+
//Serial.println(payload); //Print request response payload
375+
http.end();
376+
}
377+
378+
379+
380+
char* EncodeJson(){
381+
DynamicJsonBuffer jsonBuffer;
382+
JsonObject& jsonEncoder = jsonBuffer.createObject();
383+
384+
jsonEncoder["TemperatureOutdoor"] = _temperatureOutdoor;
385+
jsonEncoder["TemperatureIndoor"] = _temperatureIndoor;
386+
jsonEncoder["Humidity"] = _humidity;
387+
jsonEncoder["Pressure"] = _pressure;
388+
jsonEncoder["Pm25"] = _pm25;
389+
jsonEncoder["Pm10"] = _pm10;
390+
391+
int jsonLenght = jsonEncoder.measureLength();
392+
char *buf = (char *)malloc(jsonLenght + 1);
393+
jsonEncoder.printTo(buf, jsonLenght + 1);
394+
return buf;
395+
}
396+
397+
void ReadBME208(){
398+
if(bme280Found == false) return;
399+
400+
float h = bme.readHumidity();
401+
float p = bme.readPressure();
402+
bool valid = !isnan(h) && !isnan(p);
403+
404+
if(valid){
405+
406+
_humidity = h;
407+
_pressure = p / 100.0F;
408+
409+
#ifdef DEBUG_INFO
410+
Serial.print("Humidity ");
411+
Serial.print(_humidity);
412+
Serial.print(" % Pressure ");
413+
Serial.print(_pressure);
414+
Serial.println(" hPa");
415+
#endif
416+
}
417+
else {
418+
419+
#ifdef DEBUG_INFO
420+
Serial.println("Failed to read BME280!");
421+
#endif
422+
423+
}
424+
}
425+
426+
//====================================================================
427+
// SENSORS CALLBACKS
428+
//====================================================================
429+
430+
//Invoked ONLY when the temperature changes between two sensor readings
431+
void OnTemperatureChange(float temperature, bool valid, int deviceIndex){
432+
433+
#ifdef DEBUG_INFO
434+
Serial.print("Temperature sensor ");
435+
Serial.print(deviceIndex);
436+
Serial.print(" ");
437+
Serial.print(temperature);
438+
Serial.println(" °C");
439+
#endif
440+
441+
switch(deviceIndex){
442+
case 0:
443+
_temperatureIndoor = temperature;
444+
break;
445+
case 1:
446+
_temperatureOutdoor = temperature;
447+
break;
448+
}
449+
}
450+
451+
void OnSdsIntervalElapsed(float pm25, float pm10, bool valid){
452+
_pm25 = pm25;
453+
_pm10 = pm10;
454+
}

‎images/Flowchart.png

81 KB
Loading

‎src/Config.h

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// MIT License
2+
//
3+
// Copyright(c) 2021 Giovanni Bertazzoni <nottheworstdev@gmail.com>
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files(the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions :
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
#ifndef CONFIG_H
24+
#define CONFIG_H
25+
26+
//#define DEBUG_INFO
27+
28+
#define SDS_RX D1 //Particulate matter sensor RX Pin
29+
#define SDS_TX D2 //Particulate matter sensor TX Pin
30+
#define BME280_SDA D3 //Bme280 I2C data Pin
31+
#define BME280_SCL D4 //Bme280 I2C clock Pin
32+
#define DS18B20_BUS 14 //One Wire bus Pin D5 (14)
33+
34+
//Timing
35+
const long BME_INTERVAL = 60 * 1000; //60 seconds
36+
const long DS18B20_INTERVAL = 60 * 1000; //60 seconds
37+
const long SDS_INTERVAL = 10 * 60 * 1000; //10 minutes
38+
const long SDS_WARMUP_INTERVAL = 30 * 1000; //30 seconds
39+
const long WIFI_CONNECTION_TIMEOUT = 30 * 1000; //30 seconds
40+
const long SEND_DATA_INTERVAL = 10 * 60 * 1000; //10 minutes
41+
42+
//Network configuration
43+
const char* WIFI_SSID = "YOUR-WIFI-SSID";
44+
const char* WIFI_PASSWORD = "YOUR-WIFI-PASSWORD";
45+
const char* HOSTNAME = "RateMyAir_Sensor_1";
46+
47+
//RateMyAir_API URL
48+
const char* API_URL = "http://192.168.1.10/ratemyair/api/airquality";
49+
50+
//Get authentication ApiKey from RateMyAir_API repository => RateMyAir.API => appsettings.json
51+
const char* API_KEY = "YOUR-API-KEY";
52+
53+
#endif

0 commit comments

Comments
 (0)
Please sign in to comment.