// include the software serial interface library
#include <SoftwareSerial.h>
// include the SDcard library
#include <SPI.h>
#include <SD.h>
// include the Digital Humidity and Temperature (DHT) library
#include <DHT.h>
// set up variables using the SDcard utility library functions
Sd2Card card;
SdVolume volume;
// set up variables using the softserial utility library functions
SoftwareSerial SDS_Serial(8, 9); // RX, TX (TX unused and assigned as RX-pin for DHT-sensor)
SoftwareSerial GPS_Serial(5, 6); // RX, TX (TX unused and assigned as output pin for warning signal)
// unique identification code of the finedustmeter (no leading zeroes allowed!)
const unsigned int id_lo = 802; // serial number (unique in each country)
const unsigned int id_hi = 32; // country code
const byte Select0 = 2; // Display select pin for bit0
const byte Select1 = 3; // Display select pin for bit1
const byte Select2 = 4; // Display select pin for bit2
const byte Warning = 6; // Output pin to initiate warning signal
const byte Write2LogLED = 7; // Notification that data is written to logfile
const byte DHT_pin = 9; // Input pin to receive serial DHT-data
const byte chipSelect = 10; // Select pin to enable access to the SDcard
const byte Data0 = 14; // Display data pin for bit0
const byte Data1 = 15; // Display data pin for bit1
const byte Data2 = 16; // Display data pin for bit2
const byte Data3 = 17; // Display data pin for bit3
const byte Latch_data = 18; // Clock line to latch data into selected register
const byte Power_hold = 19; // Output pin to switch off meter outside SDcard write cycle
const int NO2_pin = A6; // Analog PIN 6 to read the NO2-sensor
const int Switch_status = A7;// Analog PIN 7 to sense status of on-off switch and supply voltage
const byte Vmin = 43; // Minimum supply voltage for writing data to SDcard
static char line[40]; // Linebuffer for serial monitor
char datafile[] = "log_000.txt"; // Initial name of datalogfile
unsigned int filecount = 0;
unsigned int lowpowercount = 0;
unsigned int Pm25 = 0;
unsigned int Pm10 = 0;
uint8_t byteGPS;
uint8_t display_mode = 0; // 0->display PM-values, 1->display NO2 and voltage, 2->display temperature and humidity, 3->display time
uint32_t currentTime = 0;
uint32_t hash = 0;
char cmd[7] = "$GPRMC"; // Recommended minimum specific GPS/Transit data sentence
int counter1 = 0; // counts how many bytes were received (max 200) for each sentence
int counter2 = 0; // counts how many commas were seen in each sentence
int offsets[13]; // holds offset to 12 different data items and checksum in GPRMC sentence
char buf[200] = " "; // data buffer in which each received sentence is stored
float hum; // stores humidity value
float temp; // stores temperature value
boolean proceed;
boolean switch_off;
boolean logging_flag;
boolean hash_written;
boolean data_present;
DHT dht(DHT_pin, DHT22); // Initialize DHT-sensor
char digit100(unsigned int v) {return '0' + v / 100 - (v/1000) * 10;}
char digit10(unsigned int v) {return '0' + v / 10 - (v/100) * 10;}
char digit1(unsigned int v) {return '0' + v / 1 - (v/10) * 10;}
void ProcessSerialSDSData() {
uint8_t mData = 0;
uint8_t i = 0;
uint8_t mPkt[10] = {0};
uint8_t mCheck = 0;
while (SDS_Serial.available() > 0) {
// from www.inovafitness.com
// packet format: AA C0 PM25_Low PM25_High PM10_Low PM10_High 0 0 CRC AB
mData = SDS_Serial.read();
delay(2); //wait until packet is received
if(mData == 0xAA) { //first headerbyte ok
mPkt[0] = mData;
mData = SDS_Serial.read();
if(mData == 0xC0) { //second headerbyte ok
mPkt[1] = mData;
mCheck = 0;
for(i=0;i<6;i++) { //data reception and crc calculation
mPkt[i+2] = SDS_Serial.read();
delay(2);
mCheck += mPkt[i+2];
}
mPkt[8] = SDS_Serial.read(); //get crc
delay(1);
mPkt[9] = SDS_Serial.read();
if(mCheck == mPkt[8]) { //crc ok?
Pm25 = (uint16_t)mPkt[2] | (uint16_t)(mPkt[3]<<8);
Pm10 = (uint16_t)mPkt[4] | (uint16_t)(mPkt[5]<<8);
//one good packet received
return;
}
}
}
Check_power_switch_status();
}
}
void reset() {
counter1 = 0;
counter2 = 0;
}
void write_nibble(byte regnbr, byte regval) {
byte i;
for (i=0; i<4; i++)
{ if ((regval & (1 << i)) == 0) { digitalWrite(Data0 + i, LOW); } else { digitalWrite(Data0 + i, HIGH); } }
for (i=0; i<3; i++)
{ if ((regnbr & (1 << i)) == 0) { digitalWrite(Select0 + i, LOW); } else { digitalWrite(Select0 + i, HIGH); } }
digitalWrite(Latch_data, HIGH);
delay(1);
digitalWrite(Latch_data, LOW);
}
void write_data(byte regnbr, int regval) {
if (regval > 999) {
write_nibble(regnbr, 9);
write_nibble(regnbr + 1, 9);
write_nibble(regnbr + 2, 9);
}
else {
byte val_units = regval % 10;
byte val_tens = (regval / 10) % 10;
byte val_hundreds = regval / 100;
write_nibble(regnbr, val_units);
if (val_tens == 0 && val_hundreds == 0) {
write_nibble(regnbr + 1, 15);
} else {
write_nibble(regnbr + 1, val_tens);
}
if (val_hundreds == 0) {
write_nibble(regnbr + 2, 15);
} else {
write_nibble(regnbr + 2, val_hundreds);
}
}
}
void error_idling(byte error_nbr) {
while(1) {
write_data(1, 888);
write_data(4, 888);
delay(1000);
write_data(1, error_nbr);
write_data(4, 0);
delay(1000);
Check_power_switch_status();
}
}
int get_size(int offset) {
return offsets[offset+1] - offsets[offset] - 1;
}
boolean handle_byte(int byteGPS) {
uint8_t checksum = 0;
char str_check[2];
uint8_t chksum[2];
buf[counter1] = byteGPS;
counter1++;
if (counter1 == 200) {
return false;
}
if (byteGPS == ',') {
counter2++;
offsets[counter2] = counter1;
if (counter2 == 13) {
return false;
}
}
if (byteGPS == '*') {
offsets[12] = counter1;
}
// Check if we got a <LF>, which indicates the end of line
if (byteGPS == 10) {
// Check that we got 12 pieces, and that the first piece is 6 characters
if (counter2 != 12 || (get_size(0) != 6)) {
return false;
}
// Check that we received $GPRMC
for (int j=0; j<6; j++) {
if (buf[j] != cmd[j]) {
return false;
}
}
// Compute and validate checksum
for (int j=1; j<offsets[12]-1; j++) {
checksum ^= buf[j];
}
chksum[0] = buf[offsets[12]];
if (chksum[0] > 47 && chksum[0] < 58) {
chksum[0] -= 48;
} else {
chksum[0] -= 55;
}
chksum[1] = buf[offsets[12]+1];
if (chksum[1] > 47 && chksum[1] < 58) {
chksum[1] -= 48;
} else {
chksum[1] -= 55;
}
if (checksum != chksum[0]*16 + chksum[1]) {
return false;
}
proceed = true; // one good sentence received
return false;
}
return true;
}
void calc_hash(char c) {
hash += (byte)c;
hash += (hash << 10);
hash ^= (hash >> 6);
}
void write_file_hash() {
typedef union {
uint32_t v;
unsigned char b[4];
} short4bytes_t;
if (SD.exists(datafile)) {
if (data_present) {
hash += (hash << 3);
hash ^= (hash >> 11);
hash += (hash << 15);
short4bytes_t s4b;
s4b.v = hash;
File dataFile = SD.open(datafile, FILE_WRITE);
if (dataFile) {
digitalWrite(Write2LogLED, HIGH);
sprintf(buf, "Signature: 0x%02X 0x%02X 0x%02X 0x%02X", s4b.b[3], s4b.b[2], s4b.b[1], s4b.b[0]);
dataFile.println(buf);
dataFile.close();
digitalWrite(Write2LogLED, LOW);
}
}
else {
SD.remove(datafile);
}
}
}
void Check_power_switch_status() {
// when in off state more than 1 second, disable logging, write hash to SDcard and shutdown
// when toggled to on state within 1 second, switch to next display mode
uint16_t v = analogRead(Switch_status);
if (v < 100 && !switch_off) {
currentTime = millis();
switch_off = true;
delay(200);
}
if (v < 100 && switch_off && abs(millis() - currentTime) > 2000 && !hash_written) {
logging_flag = false;
write_file_hash();
hash_written = true;
delay(1000);
digitalWrite(Power_hold, LOW);
}
if (v > 100 && switch_off) {
switch_off = false;
++display_mode %= 4;
delay(200);
write_data(1, display_mode);
for (byte i=4; i<7; i++) write_nibble(i, 15);
delay(1000);
}
}
byte hour_inc(int date_offset) {
byte year;
byte month;
byte day;
byte i;
char datebuf[3];
uint8_t days[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30 }; // nbr of days to be added from prev month at beginning of each month
uint8_t hours[] = { 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1 }; // nbr of hours to be added to GMT for daylight saving time in GMT+1 timezone
datebuf[2] = '\0';
for (i=0; i<2; i++) datebuf[i] = buf[date_offset+i+2];
month = atoi(datebuf) - 1; // decrement month value from GPRMC sentence, since days and hours arrays range from 0 to 11
if (month == 2 || month == 9) {
for (i=0; i<2; i++) datebuf[i] = buf[date_offset+i+4];
year = atoi(datebuf);
for (i=0; i<2; i++) datebuf[i] = buf[date_offset+i];
day = atoi(datebuf);
// calculate number of days at 1/3 or 1/10 since monday, 1 january 2018
uint16_t nbr_days = (year - 18)*365;
for (i=0; i<=month; i++) nbr_days += days[i];
// correction for leap years
if (year > 20) nbr_days += (year - 21)/4 + 1;
if (year % 4 == 0) nbr_days++;
// find date of last sunday in march or october
uint8_t last_sunday = 31;
while ((nbr_days + last_sunday) % 7 > 0) last_sunday--;
// determine changeover from wintertime to summertime and vice versa
if (month == 2) { if (day < last_sunday) return 1; else return 2; }
if (month == 9) { if (day < last_sunday) return 2; else return 1; }
} else {
return hours[month];
}
}
void setup() {
pinMode(Data0, OUTPUT);
pinMode(Data1, OUTPUT);
pinMode(Data2, OUTPUT);
pinMode(Data3, OUTPUT);
pinMode(Select0, OUTPUT);
pinMode(Select1, OUTPUT);
pinMode(Select2, OUTPUT);
pinMode(Latch_data, OUTPUT);
pinMode(Power_hold, OUTPUT);
pinMode(Write2LogLED, OUTPUT);
digitalWrite(Warning, LOW);
digitalWrite(Power_hold, LOW);
digitalWrite(Latch_data, LOW);
analogReference(EXTERNAL); // Aref must be connected to 3.3V pin
// start serial connection to SDS-module
SDS_Serial.begin(9600);
// start serial connection to GPS-module
GPS_Serial.begin(9600);
Serial.begin(9600);
dht.begin();
delay(500); // wait for display to boot up
// put unique identification code on display
write_data(1, id_lo);
write_data(4, id_hi);
strcpy_P(line,PSTR("Initializing SD card..."));
Serial.println(line);
if (!card.init(SPI_HALF_SPEED, chipSelect)) {
strcpy_P(line,PSTR("Initialization failed."));
Serial.println(line);
error_idling(1);
} else {
strcpy_P(line,PSTR("SD card is OK. "));
Serial.println(line);
}
// print the type of the SDcard
strcpy_P(line,PSTR("Card type: "));
Serial.print(line);
switch (card.type()) {
case SD_CARD_TYPE_SD1:
Serial.println("SD1");
break;
case SD_CARD_TYPE_SD2:
Serial.println("SD2");
break;
case SD_CARD_TYPE_SDHC:
Serial.println("SDHC");
break;
default:
Serial.println("?");
}
delay(2000); // allow some time (2 seconds) to read init result
// try to open the 'volume'/'partition' - it should be FAT16 or FAT32
if (!volume.init(card)) {
strcpy_P(line,PSTR("No FAT partition found. Card formatted?"));
Serial.println(line);
error_idling(2);
}
// print the type and size of the first FAT-type volume
uint32_t volumesize;
strcpy_P(line,PSTR("Volume type: FAT"));
Serial.print(line);
Serial.println(volume.fatType(), DEC);
delay(2000); // allow some time (2 seconds) to read opening result
volumesize = volume.blocksPerCluster(); // clusters are collections of blocks
volumesize *= volume.clusterCount(); // we'll have a lot of clusters
volumesize *= 512; // SDcard blocks are always 512 bytes
strcpy_P(line,PSTR("Volume size (kbytes): "));
Serial.print(line);
volumesize /= 1024;
Serial.println(volumesize);
delay(2000); // allow some time (2 seconds) to read volume size result
offsets[0] = 0;
proceed = false;
switch_off = false;
logging_flag = false;
hash_written = false;
data_present = false;
// initialise SD-card for file access
if (!SD.begin(chipSelect)) {
strcpy_P(line,PSTR("Initialization failed!"));
Serial.println(line);
error_idling(3);
}
// find the next log_(n).txt file which doesn't exist yet
do {
filecount++;
datafile[4] = digit100(filecount);
datafile[5] = digit10(filecount);
datafile[6] = digit1(filecount);
} while (SD.exists(datafile));
Serial.print("Logging to: ");
Serial.println(datafile);
// disable power off switching to prevent damage to SDcard during write cycles
digitalWrite(Power_hold, HIGH);
File dataFile = SD.open(datafile, FILE_WRITE);
// if the file is available, write the identification code as first line
if (dataFile) {
digitalWrite(Write2LogLED, HIGH);
sprintf(buf, "ID: %03d%03d", id_hi, id_lo);
dataFile.println(buf); // save identification code
dataFile.close();
for (byte i=0; i<strlen(buf); i++) calc_hash(buf[i]);
calc_hash('\r'); calc_hash('\n');
delay(100);
digitalWrite(Write2LogLED, LOW);
}
// if the file isn't open, pop up an error
else {
strcpy_P(line,PSTR("Cannot open the logfile!"));
error_idling(6);
}
// display initial PM-values
write_data(1, Pm25);
write_data(4, Pm10);
}
void loop() {
SDS_Serial.listen();
while (SDS_Serial.available() == 0) {
Check_power_switch_status(); // wait for PM-data to arrive
}
// retrieve PM-data from the SDS011-module
ProcessSerialSDSData();
if (display_mode == 0) {
// write the PM-data to the optional 7-segment display
write_data(1, int(Pm25/10+0.5));
write_data(4, int(Pm10/10+0.5));
}
// initiate warning signal when PM10-value is too high
if (Pm10 > 2000) { digitalWrite(Warning, HIGH); } else { digitalWrite(Warning, LOW); }
// make a string for assembling the data to the logfile
String dataString1 = "PM2.5: " + String(Pm25/10) + ", PM10: " + String(Pm10/10);
Serial.println(dataString1);
// retrieve temperature and humidity from DHT-sensor
temp = dht.readTemperature();
hum = dht.readHumidity();
if (display_mode == 2) {
// write temperature and humidity values to the optional 7-segment display
write_data(1, int(temp+0.5));
write_data(4, int(hum+0.5));
}
// make a string for assembling the data to the logfile
String dataString2 = "Temp: " + String(temp) + ", Humid: " + String(hum);
Serial.println(dataString2);
GPS_Serial.listen();
while (GPS_Serial.available() == 0) {
Check_power_switch_status(); // wait for GPS-data to arrive
}
// retrieve GPS-data
while (!proceed) {
if (GPS_Serial.available() > 0) {
byteGPS=GPS_Serial.read(); // Read a byte of the serial port of GPS-module
if (!handle_byte(byteGPS)) {
reset();
}
}
delay(5);
Check_power_switch_status();
}
for (int i=0; i<offsets[12]+2; i++) {
Serial.print(buf[i]);
}
Serial.println(buf[offsets[12]+2]);
if (display_mode == 3) {
if (buf[offsets[2]] == 'V') {
write_data(1, 88);
write_data(4, 88);
delay(500);
write_data(1, 11);
write_data(4, 11);
} else {
// write the time (hour:min) to the optional 7-segment display
char timebuf[3];
timebuf[2] = '\0';
for (int i=0; i<2; i++) timebuf[i] = buf[offsets[1]+i];
write_data(4, atoi(timebuf) + hour_inc(offsets[9]));
for (int i=0; i<2; i++) timebuf[i] = buf[offsets[1]+i+2];
write_data(1, atoi(timebuf));
}
}
uint16_t v = analogRead(Switch_status);
uint8_t Vin = int(v/15.5+0.5); // result is Vin in 0.1V steps (voltage divider on board: 5V->2.5V and 10bit ADC for Vref=3.3V -> 1023, hence 2.5V -> 775 and 775/15.5 = 50)
Serial.println("Vin: " + String(Vin));
v = analogRead(NO2_pin); // take reading of ADC value from NOX-pin of NO2-sensor
float Vout = v/207.5; // convert ADC value to voltage (voltage divider on board: 5V->3.2V and 10bit ADC for Vref=3.3V -> 1023, hence 3.2V -> 992 and 992/198.4 = 5,
Serial.println("Vout: " + String(Vout)); // but because of resistor tolerance of 10% the value 207,5 is taken experimentally by measuring real output voltage on NOX-pin)
float RlRs = Vout/(Vin/10-Vout); // find load resistance over sensor resistance proportion from Vout, using Vin as supply voltage
float ppmNO2 = pow(10, 0.9682*log(RlRs)/log(10)-0.8108); // convert RsR0 to ppm concentration NO2 (refer to http://myscope.net/auswertung-der-airpi-gas-sensoren/)
Serial.println("ppm NO2: " + String(ppmNO2));
float mgNO2 = (560.5/(273.15+temp))*ppmNO2; // convert ppm concentration to mg NO2/m taking air temperature into account and assuming 1013 mbar atmospheric pressure
if (display_mode == 1) {
write_data(1, int(1000*mgNO2+0.5));
write_data(4, Vin);
}
dataString1 += ", NO2: " + String(1000*mgNO2);
// stop writing to SD card when Vin is below Vmin Volt or GPS-data not available
if (logging_flag && !switch_off && (Vin < Vmin || buf[offsets[2]] == 'V')) {
// display error number 4 or 5
write_data(1, 888);
write_data(4, 888);
delay(1000);
write_data(4, 0);
if (Vin < Vmin) {
write_data(1, 4);
lowpowercount++;
if (lowpowercount > 5) {
logging_flag = false;
write_file_hash();
delay(1000);
digitalWrite(Power_hold, LOW);
}
}
if (buf[offsets[2]] == 'V') {
logging_flag = false;
write_data(1, 5);
}
delay(1000);
}
// start writing to SD card when Vin is above Vmin Volt and GPS-data is available
if (!logging_flag && Vin > Vmin && buf[offsets[2]] == 'A') {
logging_flag = true;
delay(1000); // allow some time (1 second)
}
Check_power_switch_status();
if (logging_flag && !hash_written) {
File dataFile = SD.open(datafile, FILE_WRITE);
// if the file is available, write to it
if (dataFile) {
digitalWrite(Write2LogLED, HIGH);
dataFile.println(dataString1); // save PM-data & NO2-data
for (int i=0; i<dataString1.length(); i++) calc_hash(dataString1.charAt(i));
calc_hash('\r'); calc_hash('\n');
dataFile.println(dataString2); // save DHT-data
for (int i=0; i<dataString2.length(); i++) calc_hash(dataString2.charAt(i));
calc_hash('\r'); calc_hash('\n');
for (int i=0; i<offsets[12]-1; i++) {
dataFile.print(buf[i]); // save GPS-data
calc_hash(buf[i]);
}
dataFile.println(buf[offsets[12]-1]);
calc_hash(buf[offsets[12]-1]);
calc_hash('\r'); calc_hash('\n');
dataFile.close();
data_present = true;
delay(100);
digitalWrite(Write2LogLED, LOW);
}
// if the file isn't open, pop up an error
else {
strcpy_P(line,PSTR("Cannot open the logfile!"));
Serial.println(line);
// display error number 6
write_data(1, 888);
write_data(4, 888);
delay(1000);
write_data(1, 6);
write_data(4, 0);
delay(1000);
}
}
// reset the GPS-data buffer
for (int i=0; i<200; i++) {
buf[i] = ' ';
}
proceed = false;
}
Comments
Please log in or sign up to comment.