Over the years I’ve built a few dust sensor systems (‘PM2.5’) with SHARP and SHINYEI sensors, but calibration was always a headache as they have analog output. Earlier this year (2019) i looked into the current sensor technology again, and found out that PLANTOWER has established itself as a leader, supplying laser based optical sensors (instead of infrared) with serial output. These sensors come with a built-in fan, and serial communication, so there is less room for errors. I picked up a few from Taobao for less than RMB 100 each (around US$13).

I will describe the hardware and software below, including some data visualisation combining the readings with those of official monitoring stations in Hong Kong.

IMG_20190614_184059 - Edited

There are quite a few models available and i think the PMSA003 is the latest, after the PMS7003, i think the A is hex for 10.

The sensor gives a lot of data points (see datasheet), including PM1, PM2.5, PM10 (in 2 variants: ‘standard’ and ‘atmospheric’). I’m only interested in ‘atmospheric’ PM2.5; the standard variant is for factory environments according to the datasheet.

I made 2 versions of this project:

  1. a portable Arduino NANO based one with an OLED screen, buzzer, and pot to set an alarm level, to be powered by powerbank 5V USB)
  2. a stationary ESP8266 based one with a buzzer, permanently plugged into 5V, sending data over WiFi to a cloud server

IMG_20190614_110207

Screenshot 2019-08-19 at 10.57.06

Reading the sensor is quite straightforward; in the default state, the unit outputs 32 bytes over serial at 9600 baud, which need to be parsed to extract the readings. The data comes about every second, with some variation according to the datasheet. I used code provided by the Scapeler project. As i was happy with this rate, i did not have to connect the sensor’s RX pin: i’m only receiving data from the sensor, not sending any instructions.

Let’s look at the PORTABLE system first.

I use an Arduino NANO because it’s cheap and small, and easy to plug into 2 rows of 15-pin female headers.

The 8-pin connector on the PMSA003 is tiny, and it’s really hard to solder on it, so i advise to get a adapter board. My adapter board has a connector for a 8-pin ribbon cable, but also 4 male pins with the standard 2.5mm spacing. It has: 5V, GND, TX, RX so that is enough for this version.

I added a 10K potentiometer on A0 to set the alarm level for a buzzer: i can set the level from 0 to 100ug/m3. The buzzer beeps as long as the level is exceeded.

I use a 0.96in OLED screen with SPI interface, with the U8G library.

As i still wanted the Serial Monitor to work, i could not use the hardware serial on D0/D1, so we use a software serial on D6 and D7, but D7 (TX) does not need to be connected as we’re only receiving from the sensor. The sensor will be active all the time, with the fan spinning.

IMG_20190819_104409

The connections are further explained in the code, and the soldering is quite easy as you can see from the bottom of the perf board.

IMG_20190819_110240 (1) - Edited

The sensor needs 30 seconds to stabilise, so the code does a countdown before showing the first reading. It shows a Simple Moving Average of the 5 most recent readings.

IMG_20190819_100340 - Edited

/*
 BuffaloLabs Dust Sensor PMSA003
 Arduino NANO on perf board

PMSA003 Plantower dust sensor
 serial output every second in active mode
 can switch to passive mode to request data - not used here
 output is ug/m3 for PM1, PM2.5, PM10 in 2 different assumptions: industrial standard particles, and atmospheric particles
 we will use atmospheric as reference
 also output of 6 number concentrations: 0.3 0.5 1 2.5 5 10 - not used here

use Simple Moving Average of 5 most recent readings
 
 OLED on 13,11,8,9,10
 BUZZER on 3
 POT on A0
 SENSOR TX on 6 (7 dummy for RX, not connected)
 all powered by 5V

Tom Tobback BuffaloLabs June 2019

*/


/*
 Copyright 2017 Scapeler
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
*/

/////////////// SENSOR ///////////////////////////////////
#include <SoftwareSerial.h>
SoftwareSerial Serial1(6, 7); // serial ports RX, TX
// input byte variables
int inputHigh = 0;
int inputLow = 0;
// variable to caclulate checksum input variables
uint16_t inputChecksum = 0;
// sensor variables
uint16_t concPM1_0_CF1;
uint16_t concPM2_5_CF1;
uint16_t concPM10_0_CF1;
uint16_t concPM1_0_amb;
uint16_t concPM2_5_amb;
uint16_t concPM10_0_amb;
uint16_t rawGt0_3um;
uint16_t rawGt0_5um;
uint16_t rawGt1_0um;
uint16_t rawGt2_5um;
uint16_t rawGt5_0um;
uint16_t rawGt10_0um;
uint8_t version;
uint8_t errorCode;
uint16_t checksum;
int pm2_5, pm10; // readings
float pm2_5_avg; // average
int pm2_5_hist [5] = {0, 0, 0, 0, 0}; // history for averaging, using simple moving average
int alarm_level;
unsigned long last_alarm_timestamp;
const unsigned long alarm_interval = 5000; // min time between alarm beeps

////////////// OLED //////////////////////////////////////
#include "U8glib.h"
U8GLIB_SSD1306_128X64 u8g(10, 9); // OLED display: HW SPI CS = 10, A0/DC = 9 (Hardware Pins are SCK/D0 = 13 and MOSI/D1 = 11) + RST to Arduino D8
unsigned long success_timestamp;
const unsigned long no_data_interval = 5000; // warning if no fresh data

//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////

void setup() {
 pinMode(8, OUTPUT); // RST pin for OLED display
 digitalWrite(8, LOW);
 delay(100);
 digitalWrite(8, HIGH);
 drawIntro();

tone(3, 500);
 delay(100);
 tone(3, 1000);
 delay(100);
 noTone(3);

Serial.begin(115200);
 Serial.println("BuffaloLabs PMSA003 dust sensor");

Serial1.begin(9600); // software serial for sensor
 while (Serial1.read() != -1) {}; //clear buffer
 Serial.println("Sensor port ready");

Serial.println("Countdown 30s to stabilise");
 delay(3000);
 drawCountdown(); // sensor needs 30 seconds to stabilise
 Serial.println("5 latest readings and average PM2.5 (ug/m3)");

}

//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////

void loop () {

alarm_level = map(analogRead(A0), 0, 1023, 1, 100);

if (pms7003ReadData()) { // if successful, reading takes at least 700ms because it includes a delay(700)

// calculate average of 5 most recent readings
 for (int i = 0; i < 4; i++) {
 pm2_5_hist[i] = pm2_5_hist[i + 1]; // shift historic readings one down, drop oldest [0]
 if (pm2_5_hist[i] == 0) pm2_5_hist[i] = pm2_5; // at startup, if 0 (no data), use current reading
 }
 pm2_5_hist[4] = pm2_5; // last one is current reading
 pm2_5_avg = ( pm2_5_hist[0] + pm2_5_hist[1] + pm2_5_hist[2] + pm2_5_hist[3] + pm2_5_hist[4]) / 5.0; // simple moving average
 for (int i = 0; i < 5; i++) {
 Serial.print(pm2_5_hist[i]);
 Serial.print("\t");
 }
 Serial.print("average: ");
 Serial.println(pm2_5_avg);

drawData();
 success_timestamp = millis();
 } else {
 if (millis() - success_timestamp > no_data_interval) {
 drawWarning();
 }
 }

if (pm2_5_avg > alarm_level && millis() - last_alarm_timestamp > alarm_interval) {
 beeps();
 last_alarm_timestamp = millis();
 }
}

//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////

bool pms7003ReadData() {

// while (Serial1.read()!=-1) {}; //clear buffer

if (Serial1.available() < 32) {
 if (Serial1.available() == 0) {
 delay(150);
 return false;
 };
 if (Serial1.available() > 16) {
 delay(10);
 return false;
 };
 if (Serial1.available() > 0) {
 delay(30);
 return false;
 };
 delay(100);
 return false;
 }
 if (Serial1.read() != 0x42) return false;
 if (Serial1.read() != 0x4D) return false;

inputChecksum = 0x42 + 0x4D;

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 if (inputHigh != 0x00) return false;
 if (inputLow != 0x1c) return false;

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 concPM1_0_CF1 = inputLow + (inputHigh << 8);

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 concPM2_5_CF1 = inputLow + (inputHigh << 8);

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 concPM10_0_CF1 = inputLow + (inputHigh << 8);

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 concPM1_0_amb = inputLow + (inputHigh << 8);

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 concPM2_5_amb = inputLow + (inputHigh << 8);

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 concPM10_0_amb = inputLow + (inputHigh << 8);

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 rawGt0_3um = inputLow + (inputHigh << 8);

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 rawGt0_5um = inputLow + (inputHigh << 8);

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 rawGt1_0um = inputLow + (inputHigh << 8);

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 rawGt2_5um = inputLow + (inputHigh << 8);

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 rawGt5_0um = inputLow + (inputHigh << 8);

inputHigh = Serial1.read();
 inputLow = Serial1.read();
 inputChecksum += inputHigh + inputLow;
 rawGt10_0um = inputLow + (inputHigh << 8);

inputLow = Serial1.read();
 inputChecksum += inputLow;
 version = inputLow;

inputLow = Serial1.read();
 inputChecksum += inputLow;
 errorCode = inputLow;

/*
 Serial.print("PMSA003;\t");
 Serial.print(concPM1_0_CF1);
 Serial.print(';');
 Serial.print(concPM2_5_CF1);
 Serial.print(';');
 Serial.print(concPM10_0_CF1);
 Serial.print(";\t");
 Serial.print(concPM1_0_amb);
 Serial.print(';');
 Serial.print(concPM2_5_amb);
 Serial.print(';');
 Serial.print(concPM10_0_amb);
 Serial.print(";\t");
 Serial.print(rawGt0_3um);
 Serial.print(';');
 Serial.print(rawGt0_5um);
 Serial.print(';');
 Serial.print(rawGt1_0um);
 Serial.print(';');
 Serial.print(rawGt2_5um);
 Serial.print(';');
 Serial.print(rawGt5_0um);
 Serial.print(';');
 Serial.print(rawGt10_0um);
 Serial.print(';');
 Serial.print(version);
 Serial.print(';');
 Serial.print(errorCode);
 */
 inputHigh = Serial1.read();
 inputLow = Serial1.read();
 checksum = inputLow + (inputHigh << 8);
 if (checksum != inputChecksum) {
 Serial.print(';');
 Serial.print(checksum);
 Serial.print(';');
 Serial.print(inputChecksum);
 Serial.println(" CHECKSUM ERROR, ignore data");
 return false;
 }
 // Serial.print('\n');

delay(700); // higher will get you checksum errors

pm2_5 = concPM2_5_amb;
 pm10 = concPM10_0_amb;

return true;
}

////////////////////////////////////////////////////////////////////////

void drawIntro() {
 u8g.firstPage();
 do {
 u8g.setFont(u8g_font_unifont);
 u8g.drawStr(10, 15, "BuffaloLabs");
 // u8g.setFont(u8g_font_fub25r);
 u8g.drawStr(10, 40, "PM2.5 SENSOR");
 u8g.drawStr(10, 55, "with alarm");
 } while ( u8g.nextPage() );
}

////////////////////////////////////////////////////////////////////////

void drawWarning() {
 u8g.firstPage();
 do {
 u8g.setFont(u8g_font_unifont);
 u8g.drawStr(10, 15, "WARNING");
 // u8g.setFont(u8g_font_fub25r);
 u8g.drawStr(10, 40, "no fresh data");
 u8g.drawStr(10, 55, "sensor connected?");
 } while ( u8g.nextPage() );
}

////////////////////////////////////////////////////////////////////////

void drawData() {
 u8g.firstPage();
 do {
 u8g.setFont(u8g_font_unifont);
 u8g.drawStr(10, 15, "PM2.5 sensor");
 u8g.setFont(u8g_font_fub25r);
 u8g.setPrintPos(10, 45);
 u8g.print(pm2_5_avg, 0);
 u8g.setFont(u8g_font_unifont);
 u8g.print("ug/m3");
 u8g.setPrintPos(10, 60);
 u8g.print("alarm:");
 u8g.print(alarm_level);
 u8g.print("ug/m3");
 } while ( u8g.nextPage() );
}

////////////////////////////////////////////////////////////////////////

void drawCountdown() {
 while (30000 - millis() < 29000) { // funny way with unsigned long, more easy: millis() - 30000 < 0
 alarm_level = map(analogRead(A0), 0, 1023, 1, 100); // allow alarm setting during wait
 u8g.firstPage();
 do {
 u8g.setFont(u8g_font_unifont);
 u8g.drawStr(10, 15, "PM2.5 sensor");
 u8g.drawStr(10, 30, "stabilising..");
 u8g.setPrintPos(10, 45);
 u8g.print("wait ");
 u8g.print((30000 - millis()) / 1000);
 u8g.print(" sec");
 u8g.setPrintPos(10, 60);
 u8g.print("alarm:");
 u8g.print(alarm_level);
 u8g.print("ug/m3");
 } while ( u8g.nextPage() );
 delay(100);
 }
}

////////////////////////////////////////////////////////////////////////

void beeps() {
 for (int u = 0; u < 5; u++) {
 tone(3, 2000);
 delay(100);
 noTone(3);
 delay(100);
 }
 delay(500); // buzzer draws a lot of current, let arduino stabilise
}

Now let’s look at the WIFI version with the ESP8266. I used a NodeMCUv1 module which includes a USB converter. It is powered from a 5V USB wall plug. The only external parts are a piezo buzzer with a 220ohm resistor in series to limit the current to the buzzer.

The sensor is powered by 5V and GND, and again we’re only interested in its TX pin as we’re not going to send any instructions to the sensor. To save power, and probably more importantly, to save wear of the components, i decided to use the SLEEP function of the sensor (pin 10). This pin can switch off the sensor, stopping the fan.

Again we want to use the hardware serial RX TX for debugging on the Serial Monitor, so we use a software serial for the sensor, at 9600 baud. The loop() wakes the sensor up with the SLEEP pin, waits for 30 seconds, then reads the sensor and sends the data. If the PM2.5 level exceeds a certain pre-set level, the buzzer beeps. Then the sensor is put to sleep for 1 minute.

For cloud server i use emoncms.org which has been available for more than 5 years and is well suited for this application; easy to visualise data over time.

Initially my main concern was outdoor air quality. I live in Mui Wo village in Hong Kong, with no official air quality monitoring stations nearby, somewhere half way between the stations of Tung Chung and Central/Western. I wanted to compare my readings to the official numbers, so i wrote some code for the ESP8266 to harvest the current official readings from the the website, e.g. for Tung Chung. I found that the data comes from an XML file, and my code is extracting the current PM2.5 and PM10 from that file. Same for data from Central/Western. If you don’t live in HK, just comment out the ‘updateEPDdata()’ function, or modify it for your own location!

Screenshot 2019-08-19 at 11.52.36

Above graph, with my PMSA003 sensor data for PM2.5 in pink (filled), shows a good correlation with the official (HK EPD) data in red and yellow. The graph is too busy with also the PM10 showing, so let’s look at only PM2.5

Screenshot 2019-08-19 at 11.58.43

The PMSA003 sensor readings (2 sensors: red and pink) correlate well with the official data, despite the fact that:

  • both sensors are placed indoor; due to very imperfect insulation the indoor air generally seems to follow the outdoor air trend
  • the 2 official stations are respectively 5 and 15km away

Both sensors have a few spikes, and these are due to soldering(!) activity indoor. The smoke created by soldering immediately puts the PM2.5 well above 25ug/m3 (WHO 24-hour mean guideline), and spreads from the ground floor (red) to the first floor (pink fill), lingering for several hours.

Screenshot 2019-08-19 at 12.10.00

Running a small active carbon filter near the soldering activity does not seem to make much difference in PM2.5

I am not sure how harmful this soldering smoke is, but my advice to all: hot summer or freezing winter, open a window whenever you are soldering!

/*
 PM sensor PMSA003 with ESP8266 NodeMCU v1

serial output every second in active mode
 can switch to passive mode to request data - not used here
 output is ug/m3 for PM1, PM2.5, PM10 in 2 different assumptions: industrial standard particles, and atmospheric particles
 we will use atmospheric as reference
 also output of 6 number concentrations: 0.3 0.5 1 2.5 5 10 - not used here

sensor pin connections to NodeMCU:
 pin1: 5V VIN
 pin3: GND GND
 pin9: TX D2 (GPIO4)
 pin10: SLEEP D5 (GPIO14)
 RX pin is not connected but software serial defined on dummy GPIO5

buzzer on D6 (GPIO12) with 220ohm resistor to limit current (esp8266 can only do 12mA?)

connecting to EPD website to retrieve official PM10 and PM2.5 data for Tung Chung (78) and Central/West (80)
 see www.aqhi.gov.hk/en/aqhi/past-24-hours-pollutant-concentration45fd.html?stationid=80


 based on Scapeler example github
 Copyright 2017 Scapeler
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
*/

//------------------------------------------------------------------------------

/////////////////////////////// WIFI STUFF /////////////////////////////////////////////
#include <ESP8266WiFi.h>
const char* ssid = "xxx";
const char* password = "xxx";
#define DST_IP "80.243.190.58" // emoncms.org
#define APIKEY "xxx"
#define NODE 31

/////////////////////////////// EPD DATA ///////////////////////////////////////////////
float tc_pm10;
float tc_pm25;
float cw_pm10;
float cw_pm25;

/////////////////////////////// SENSOR /////////////////////////////////////////////////
#include <SoftwareSerial.h>
SoftwareSerial soft_serial(4, 5); // serial ports RX, TX (D2/D1 but only need RX=D2)
// input byte variables
int inputHigh = 0;
int inputLow = 0;
// variable to caclulate checksum input variables
uint16_t inputChecksum = 0;
// sensor variables
uint16_t concPM1_0_CF1;
uint16_t concPM2_5_CF1;
uint16_t concPM10_0_CF1;
uint16_t concPM1_0_amb;
uint16_t concPM2_5_amb;
uint16_t concPM10_0_amb;
uint16_t rawGt0_3um;
uint16_t rawGt0_5um;
uint16_t rawGt1_0um;
uint16_t rawGt2_5um;
uint16_t rawGt5_0um;
uint16_t rawGt10_0um;
uint8_t version;
uint8_t errorCode;
uint16_t checksum;
int pm2_5, pm10;
float avg_pm2_5, avg_pm10; // because average is not int
const int SLEEP_PIN = 14; // D5
const int LED_PIN = 2; // on-board, inverted
const int BUZZER_PIN = 12; // D6
const int samples = 5;
const int sleep_seconds = 60;
const int alarm_level = 40;

///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////

void setup() {

pinMode(SLEEP_PIN, OUTPUT); // sleep pin of sensor
 digitalWrite(SLEEP_PIN, HIGH); // sensor awake

pinMode(LED_PIN, OUTPUT); // on-board led
 digitalWrite(LED_PIN, HIGH); // led off

tone(BUZZER_PIN, 500);
 delay(100);
 tone(BUZZER_PIN, 1000);
 delay(100);
 noTone(BUZZER_PIN);

Serial.begin(115200);
 soft_serial.begin(9600);
 while (soft_serial.read() != -1) {}; //clear buffer
 Serial.println("=============================");
 Serial.println("BuffaloLabs PM2.5 sensor node");
 Serial.println("ESP8266 with PMSA003 sensor");
 Serial.println("Parameters:");
 Serial.println("Samples: " + String(samples));
 Serial.println("Sleep seconds: " + String(sleep_seconds));
 Serial.println("Alarm level: " + String(alarm_level));
 Serial.println("Sensor port ready");

WiFi.begin(ssid, password);
 Serial.print("connecting to wifi");
 while (WiFi.status() != WL_CONNECTED) {
 delay(500);
 Serial.print(".");
 }
 Serial.println("OK");
 /*
 Serial.println("");
 Serial.println("WiFi connected");
 Serial.println("IP address: ");
 Serial.println(WiFi.localIP());
 */
 /*
 Serial.println(tc_pm10);
 Serial.println(tc_pm25);
 Serial.println(cw_pm10);
 Serial.println(cw_pm25);
 */
 Serial.println("=============================");

}

///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////

void loop () {

Serial.println("wake up sensor and wait 30 seconds...");
 digitalWrite(SLEEP_PIN, HIGH); // wake up sensor

updateEPDdata(); // this will take a few seconds
 Serial.print(">>> EPD data= ");
 Serial.print(tc_pm25);
 Serial.print("\t");
 Serial.print(tc_pm10);
 Serial.print("\t");
 Serial.print(cw_pm25);
 Serial.print("\t");
 Serial.print(cw_pm10);
 Serial.println();

delay(30 * 1000); // wait for stability

digitalWrite(LED_PIN, LOW); // led on
 // take average of 5 readings
 float cum_pm2_5 = 0; // will be int but average should be float so this assures the division returns a float
 float cum_pm10 = 0;
 int counter = 0;

while (counter < samples) {
 while (!pmsA003ReadData()) {}; // wait for successful reading
 counter++;
 cum_pm2_5 += pm2_5;
 cum_pm10 += pm10;
 Serial.print(pm2_5);
 Serial.print("\t");
 Serial.print(pm10);
 Serial.println();
 }
 avg_pm2_5 = cum_pm2_5 / counter; 
 avg_pm10 = cum_pm10 / counter;

Serial.print(">>> PM2.5= ");
 Serial.print(avg_pm2_5);
 Serial.print("\t PM10= ");
 Serial.print(avg_pm10);
 Serial.println(" ug/m3");
 digitalWrite(LED_PIN, HIGH); // led off

Serial.println("sleep sensor...");
 digitalWrite(SLEEP_PIN, LOW); // sleep sensor

sendData();

if (avg_pm2_5 > alarm_level) {
 Serial.println(">>> ALARM beeps");
 beeps();
 }

Serial.println("wait 1 minute before next cycle...");
 Serial.println("--------------------------------------");
 delay(sleep_seconds * 1000);
}

///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////

bool pmsA003ReadData() {

// while (soft_serial.read()!=-1) {}; //clear buffer

if (soft_serial.available() < 32) {
 if (soft_serial.available() == 0) {
 delay(150);
 return 0;
 };
 if (soft_serial.available() > 16) {
 delay(10);
 return 0;
 };
 if (soft_serial.available() > 0) {
 delay(30);
 return 0;
 };
 delay(100);
 return 0;
 }
 if (soft_serial.read() != 0x42) return 0;
 if (soft_serial.read() != 0x4D) return 0;

inputChecksum = 0x42 + 0x4D;

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 if (inputHigh != 0x00) return 0;
 if (inputLow != 0x1c) return 0;

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 concPM1_0_CF1 = inputLow + (inputHigh << 8);

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 concPM2_5_CF1 = inputLow + (inputHigh << 8);

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 concPM10_0_CF1 = inputLow + (inputHigh << 8);

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 concPM1_0_amb = inputLow + (inputHigh << 8);

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 concPM2_5_amb = inputLow + (inputHigh << 8);

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 concPM10_0_amb = inputLow + (inputHigh << 8);

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 rawGt0_3um = inputLow + (inputHigh << 8);

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 rawGt0_5um = inputLow + (inputHigh << 8);

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 rawGt1_0um = inputLow + (inputHigh << 8);

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 rawGt2_5um = inputLow + (inputHigh << 8);

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 rawGt5_0um = inputLow + (inputHigh << 8);

inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 inputChecksum += inputHigh + inputLow;
 rawGt10_0um = inputLow + (inputHigh << 8);

inputLow = soft_serial.read();
 inputChecksum += inputLow;
 version = inputLow;

inputLow = soft_serial.read();
 inputChecksum += inputLow;
 errorCode = inputLow;
 /*
 Serial.print("PMSA003;\t");
 Serial.print(concPM1_0_CF1);
 Serial.print(';');
 Serial.print(concPM2_5_CF1);
 Serial.print(';');
 Serial.print(concPM10_0_CF1);
 Serial.print(";\t");
 Serial.print(concPM1_0_amb);
 Serial.print(';');
 Serial.print(concPM2_5_amb);
 Serial.print(';');
 Serial.print(concPM10_0_amb);
 Serial.print(";\t");
 Serial.print(rawGt0_3um);
 Serial.print(';');
 Serial.print(rawGt0_5um);
 Serial.print(';');
 Serial.print(rawGt1_0um);
 Serial.print(';');
 Serial.print(rawGt2_5um);
 Serial.print(';');
 Serial.print(rawGt5_0um);
 Serial.print(';');
 Serial.print(rawGt10_0um);
 Serial.print(';');
 Serial.print(version);
 Serial.print(';');
 Serial.print(errorCode);
 */
 inputHigh = soft_serial.read();
 inputLow = soft_serial.read();
 checksum = inputLow + (inputHigh << 8);
 if (checksum != inputChecksum) {
 /*
 Serial.print(';');
 Serial.print(checksum);
 Serial.print(';');
 Serial.print(inputChecksum);
 */
 return 0;
 }

delay(700); // higher will get you checksum errors

pm2_5 = concPM2_5_amb;
 pm10 = concPM10_0_amb;

return 1;
}

///////////////////////////////////////////////////////////////////////////////////

void sendData() {

Serial.print(">>> sending to emoncms.org.. ");
 WiFiClient client;
 if (!client.connect(DST_IP, 80)) {
 Serial.println("connection failed");
 return;
 }

String url = "/input/post.json?node=";
 url += NODE;
 url += "&apikey=";
 url += APIKEY;
 url += "&csv=";
 url += 0; // sharp sensor value
 url += ",";
 url += tc_pm10;
 url += ",";
 url += tc_pm25;
 url += ",";
 url += cw_pm10;
 url += ",";
 url += cw_pm25;
 url += ",";
 url += avg_pm2_5; // sensor value
 url += ",";
 url += avg_pm10; // sensor value

client.print(String("GET ") + url + " HTTP/1.1\r\n" +
 "Host: emoncms.org\r\n" +
 "Connection: close\r\n\r\n");
 delay(10);
 // Serial.println(url);

/*
 unsigned long send_timeout = millis();
 while (client.available() == 0) {
 if (millis() - send_timeout > 5000) {
 Serial.println(">>> Client Timeout !");
 client.stop();
 return;
 }
 }
 */
 // if server replies OK, send success to UNO
 if (client.find("200 OK")) {
 Serial.println(" sent OK");
 } else {
 Serial.println(" failed");
 }

// flush rest of server reply
 while (client.available()) { // read the remaining text from serial otherwise connection cannot be closed
 client.read();
 }

client.stop();

}

////////////////////////////////////////////////////////////////////////

void updateEPDdata() {

float pm10, pm25;
 // Serial.println("start update EPD");

WiFiClient client;
 if (!client.connect("www.aqhi.gov.hk", 80)) {
 // Serial.println("connection failed");
 return;
 }

String url = "/epd/ddata/html/h24_concentration_Eng_78.xml"; // tung chung

client.print(String("GET ") + url + " HTTP/1.1\r\n" +
 "Host: www.aqhi.gov.hk\r\n\r\n");
 // "Connection: close\r\n\r\n"); // do not close
 delay(10);
 // Serial.println(url);

unsigned long timeout = millis();
 while (client.available() == 0) {
 if (millis() - timeout > 5000) {
 // Serial.println(">>> Client Timeout !");
 client.stop();
 return;
 }
 }

// Read all the lines of the reply from server and print them to Serial

if (client.available()) { // parse the XML page; look for station name, then count columns
 if (client.find("Tung Chung")) {
 for (int i = 0; i < 11; i++) {
 client.find("H24C_ColItem");
 }
 pm10 = client.parseFloat();
 client.find("H24C_ColItem");
 pm25 = client.parseFloat();
 if (pm10 > 0) tc_pm10 = pm10;
 if (pm25 > 0) tc_pm25 = pm25;
 }
 }

// flush rest of server reply
 while (client.available()) { // read the remaining text from serial otherwise connection cannot be closed
 client.read();
 }

////////////////////////////////////////////////////////////////////////////////////
 // new request
 url = "/epd/ddata/html/h24_concentration_Eng_80.xml"; // central western

client.print(String("GET ") + url + " HTTP/1.1\r\n" +
 "Host: www.aqhi.gov.hk\r\n" +
 "Connection: close\r\n\r\n");
 delay(10);
 // Serial.println(url);

timeout = millis();
 while (client.available() == 0) {
 if (millis() - timeout > 5000) {
 // Serial.println(">>> Client Timeout !");
 client.stop();
 return;
 }
 }

// Read all the lines of the reply from server and print them to Serial

if (client.available()) { // parse the XML page; look for station name, then count columns
 if (client.find("Central")) {
 for (int i = 0; i < 11; i++) {
 client.find("H24C_ColItem");
 }
 pm10 = client.parseFloat();
 client.find("H24C_ColItem");
 pm25 = client.parseFloat();
 if (pm10 > 0) cw_pm10 = pm10;
 if (pm25 > 0) cw_pm25 = pm25;
 }
 }

// flush rest of server reply
 while (client.available()) { // read the remaining text from serial otherwise connection cannot be closed
 client.read();
 }

client.stop();
}

////////////////////////////////////////////////////////////////////////

void beeps() {
 for (int u = 0; u < 5; u++) {
 tone(BUZZER_PIN, 2000);
 delay(100);
 noTone(BUZZER_PIN);
 delay(100);
 }
}

Leave a Reply

Your email address will not be published. Required fields are marked *