Last active
February 3, 2017 20:10
-
-
Save proffalken/f35130a6e8b347f5da6130a9291de77c to your computer and use it in GitHub Desktop.
TTN Dust Sensor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Decoder(bytes, port) { | |
// Decode an uplink message from a buffer | |
// (array) of bytes to an object of fields. | |
var decoded = {}; | |
decoded.port = port; | |
decoded.pm10error = 0; | |
decoded.pm25error = 0; | |
if (port === 10) { | |
decoded.pm25count_cu_m = parseFloat((bytes[1] << 8) | bytes[2]).toFixed(2); | |
decoded.pm10count_cu_m = parseFloat((bytes[3] << 8) | bytes[4]).toFixed(2); | |
if (decoded.pm10count_cu_m > 50){ | |
decoded.pm10error = 1; | |
} | |
if (decoded.pm25count_cu_m > 25){ | |
decoded.pm25error = 1; | |
} | |
} | |
return decoded; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/******************************************************************************* | |
* Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman | |
* | |
* Permission is hereby granted, free of charge, to anyone | |
* obtaining a copy of this document and accompanying files, | |
* to do whatever they want with them without any restriction, | |
* including, but not limited to, copying, modification and redistribution. | |
* NO WARRANTY OF ANY KIND IS PROVIDED. | |
* | |
* This example sends a valid LoRaWAN packet with payload "Hello, | |
* world!", using frequency and encryption settings matching those of | |
* the (early prototype version of) The Things Network. | |
* | |
* Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in g1, | |
* 0.1% in g2). | |
* | |
* Change DEVADDR to a unique address! | |
* See http://thethingsnetwork.org/wiki/AddressSpace | |
* | |
* Do not forget to define the radio type correctly in config.h. | |
* | |
*******************************************************************************/ | |
#include <lmic.h> | |
#include <hal/hal.h> | |
#include <SPI.h> | |
#include "LowPower.h" // TODO: remove and test. Low power is not useful for this sensor | |
int TX_COMPLETE = 0; | |
static const PROGMEM u1_t NWKSKEY[16] = <NETWORK KEY>; | |
static const u1_t PROGMEM APPSKEY[16] = <APP KEY>; | |
static const u4_t DEVADDR = <DEVICE ADDR>; | |
// These callbacks are only used in over-the-air activation, so they are | |
// left empty here (we cannot leave them out completely unless | |
// DISABLE_JOIN is set in config.h, otherwise the linker will complain). | |
void os_getArtEui (u1_t* buf) { } | |
void os_getDevEui (u1_t* buf) { } | |
void os_getDevKey (u1_t* buf) { } | |
static uint8_t mydata[] = " "; | |
static osjob_t sendjob; | |
int i = 10; | |
// Schedule TX every this many seconds (might become longer due to duty | |
// cycle limitations). | |
const unsigned TX_INTERVAL = 2000; // mq: not used anymore because scheduling mechanism is not used | |
// Pin mapping | |
const lmic_pinmap lmic_pins = { | |
.nss = 6, | |
.rxtx = LMIC_UNUSED_PIN, | |
.rst = 5, | |
.dio = {2, 3, 4}, | |
}; | |
// 161204 MQ dustduino start | |
unsigned long starttime; | |
unsigned long triggerOnP1; | |
unsigned long triggerOffP1; | |
unsigned long pulseLengthP1; | |
unsigned long durationP1; | |
boolean valP1 = HIGH; | |
boolean triggerP1 = false; | |
unsigned long triggerOnP2; | |
unsigned long triggerOffP2; | |
unsigned long pulseLengthP2; | |
unsigned long durationP2; | |
boolean valP2 = HIGH; | |
boolean triggerP2 = false; | |
float ratioP1 = 0; | |
float ratioP2 = 0; | |
unsigned long sampletime_ms = 30000; // 30 Seconds | |
float countP1; | |
float countP2; | |
// 161204 MQ dustduino end | |
// Taken from https://raw.githubusercontent.com/chihchun/upm/ad31559281bb5522511b26309a1ee73cd1fe208a/src/ppd42ns/ppd42ns.cxx | |
// Assues density, shape, and size of dust to estimate mass concentration from particle count. | |
// | |
// This method was described in a 2009 paper | |
// Preliminary Screening System for Ambient Air Quality in Southeast Philadelphia by Uva, M., Falcone, R., McClellan, A., and Ostapowicz, E. | |
// http://www.cleanair.org/sites/default/files/Drexel%20Air%20Monitoring_-_Final_Report_-_Team_19_0.pdf | |
// | |
// This method does not use the correction factors, based on the presence of humidity and rain in the paper. | |
// | |
// convert from particles/0.01 ft3 to μg/m3 | |
double pcs2ugm3 (double concentration_pcs) | |
{ | |
double pi = 3.14159; | |
// All particles are spherical, with a density of 1.65E12 µg/m3 | |
double density = 1.65 * pow (10, 12); | |
// The radius of a particle in the PM2.5 channel is .44 µm | |
double r25 = 0.44 * pow (10, -6); | |
double vol25 = (4/3) * pi * pow (r25, 3); | |
double mass25 = density * vol25; // ug | |
double K = 3531.5; // per m^3 | |
return concentration_pcs * K * mass25; | |
} | |
// https://www3.epa.gov/airquality/particlepollution/2012/decfsstandards.pdf | |
static struct aqi { | |
float clow; | |
float chigh; | |
int llow; | |
int lhigh; | |
} aqi[] = { | |
{0.0, 12.4, 0, 50}, | |
{12.1, 35.4, 51, 100}, | |
{35.5, 55.4, 101, 150}, | |
{55.5, 150.4, 151, 200}, | |
{150.5, 250.4, 201, 300}, | |
{250.5, 350.4, 301, 350}, | |
{350.5, 500.4, 401, 500}, | |
}; | |
// Guidelines for the Reporting of Daily Air Quality – the Air Quality Index (AQI) | |
// https://www3.epa.gov/ttn/oarpg/t1/memoranda/rg701.pdf | |
// | |
// Revised air quality standards for particle pollution and updates to the air quality index (aqi) | |
// https://www3.epa.gov/airquality/particlepollution/2012/decfsstandards.pdf | |
// | |
// calculate AQI (Air Quality Index) based on μg/m3 concentration | |
int ugm32aqi (double ugm3) | |
{ | |
int i; | |
for (i = 0; i < 7; i++) { | |
if (ugm3 >= aqi[i].clow && | |
ugm3 <= aqi[i].chigh) { | |
// Ip = [(Ihi-Ilow)/(BPhi-BPlow)] (Cp-BPlow)+Ilow, | |
return ((aqi[i].lhigh - aqi[i].llow) / (aqi[i].chigh - aqi[i].clow)) * | |
(ugm3 - aqi[i].clow) + aqi[i].llow; | |
} | |
} | |
return 0; | |
} | |
void onEvent (ev_t ev) { | |
Serial.print(os_getTime()); | |
Serial.print(": "); | |
switch(ev) { | |
case EV_SCAN_TIMEOUT: | |
Serial.println(F("EV_SCAN_TIMEOUT")); | |
break; | |
case EV_BEACON_FOUND: | |
Serial.println(F("EV_BEACON_FOUND")); | |
break; | |
case EV_BEACON_MISSED: | |
Serial.println(F("EV_BEACON_MISSED")); | |
break; | |
case EV_BEACON_TRACKED: | |
Serial.println(F("EV_BEACON_TRACKED")); | |
break; | |
case EV_JOINING: | |
Serial.println(F("EV_JOINING")); | |
break; | |
case EV_JOINED: | |
Serial.println(F("EV_JOINED")); | |
break; | |
case EV_RFU1: | |
Serial.println(F("EV_RFU1")); | |
break; | |
case EV_JOIN_FAILED: | |
Serial.println(F("EV_JOIN_FAILED")); | |
break; | |
case EV_REJOIN_FAILED: | |
Serial.println(F("EV_REJOIN_FAILED")); | |
break; | |
break; | |
case EV_TXCOMPLETE: | |
Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)")); | |
TX_COMPLETE = 1; | |
if(LMIC.dataLen) { | |
// data received in rx slot after tx | |
Serial.print(F("Data Received: ")); | |
Serial.write(LMIC.frame+LMIC.dataBeg, LMIC.dataLen); | |
Serial.println(); | |
} | |
// Schedule next transmission | |
// | |
os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send); | |
break; | |
case EV_LOST_TSYNC: | |
Serial.println(F("EV_LOST_TSYNC")); | |
break; | |
case EV_RESET: | |
Serial.println(F("EV_RESET")); | |
break; | |
case EV_RXCOMPLETE: | |
// data received in ping slot | |
Serial.println(F("EV_RXCOMPLETE")); | |
break; | |
case EV_LINK_DEAD: | |
Serial.println(F("EV_LINK_DEAD")); | |
break; | |
case EV_LINK_ALIVE: | |
Serial.println(F("EV_LINK_ALIVE")); | |
break; | |
default: | |
Serial.println(F("Unknown event")); | |
break; | |
} | |
} | |
void do_send(osjob_t* j){ | |
// Check if there is not a current TX/RX job running | |
if (LMIC.opmode & OP_TXRXPEND) { | |
Serial.println(F("OP_TXRXPEND, not sending")); | |
} else { | |
// Prepare upstream data transmission at the next possible time. | |
LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0); | |
Serial.println(F("Packet queued")); | |
} | |
// Next TX is scheduled after TX_COMPLETE event. | |
} | |
void setup() { | |
Serial.begin(9600); | |
Serial.println(F("Starting")); | |
#ifdef VCC_ENABLE | |
// For Pinoccio Scout boards | |
pinMode(VCC_ENABLE, OUTPUT); | |
digitalWrite(VCC_ENABLE, HIGH); | |
delay(1000); | |
#endif | |
// LMIC init | |
os_init(); | |
// Reset the MAC state. Session and pending data transfers will be discarded. | |
LMIC_reset(); | |
// Set static session parameters. Instead of dynamically establishing a session | |
// by joining the network, precomputed session parameters are be provided. | |
#ifdef PROGMEM | |
// On AVR, these values are stored in flash and only copied to RAM | |
// once. Copy them to a temporary buffer here, LMIC_setSession will | |
// copy them into a buffer of its own again. | |
uint8_t appskey[sizeof(APPSKEY)]; | |
uint8_t nwkskey[sizeof(NWKSKEY)]; | |
memcpy_P(appskey, APPSKEY, sizeof(APPSKEY)); | |
memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY)); | |
LMIC_setSession (0x1, DEVADDR, nwkskey, appskey); | |
#else | |
// If not running an AVR with PROGMEM, just use the arrays directly | |
LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY); | |
#endif | |
// Set up the channels used by the Things Network, which corresponds | |
// to the defaults of most gateways. Without this, only three base | |
// channels from the LoRaWAN specification are used, which certainly | |
// works, so it is good for debugging, but can overload those | |
// frequencies, so be sure to configure the full frequency range of | |
// your network here (unless your network autoconfigures them). | |
// Setting up channels should happen after LMIC_setSession, as that | |
// configures the minimal channel set. | |
LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band | |
/*LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI); // g-band | |
LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band | |
LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band | |
LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band | |
LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band | |
LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band | |
LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band | |
LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK, DR_FSK), BAND_MILLI); // g2-band */ | |
// TTN defines an additional channel at 869.525Mhz using SF9 for class B | |
// devices' ping slots. LMIC does not have an easy way to define set this | |
// frequency and support for class B is spotty and untested, so this | |
// frequency is not configured here. | |
// Disable link check validation | |
LMIC_setLinkCheckMode(0); | |
// Set data rate and transmit power (note: txpow seems to be ignored by the library) | |
LMIC_setDrTxpow(DR_SF7,14); | |
// Start job | |
// do_send(&sendjob); | |
// 161204 MQ dustduino start | |
pinMode(8, INPUT); | |
pinMode(9, INPUT); // 161204 MQ added, not present in dustduino.ino. Not sure why | |
starttime = millis(); | |
// 161204 MQ dustduino end | |
} | |
void loop() { | |
valP1 = digitalRead(8); | |
valP2 = digitalRead(9); | |
if(valP1 == LOW && triggerP1 == false){ | |
triggerP1 = true; | |
triggerOnP1 = micros(); | |
} | |
if (valP1 == HIGH && triggerP1 == true){ | |
triggerOffP1 = micros(); | |
pulseLengthP1 = triggerOffP1 - triggerOnP1; | |
durationP1 = durationP1 + pulseLengthP1; | |
triggerP1 = false; | |
} | |
if(valP2 == LOW && triggerP2 == false){ | |
triggerP2 = true; | |
triggerOnP2 = micros(); | |
} | |
if (valP2 == HIGH && triggerP2 == true){ | |
triggerOffP2 = micros(); | |
pulseLengthP2 = triggerOffP2 - triggerOnP2; | |
durationP2 = durationP2 + pulseLengthP2; | |
triggerP2 = false; | |
} | |
//wdt_reset(); // MQ wifi, probably not needed | |
// Function creates particle count and mass concentration | |
// from PPD-42 low pulse occupancy (LPO). | |
if ((millis() - starttime) > sampletime_ms) { | |
// Generates PM10 and PM2.5 count from LPO. | |
// Derived from code created by Chris Nafis | |
// http://www.howmuchsnow.com/arduino/airquality/grovedust/ | |
ratioP1 = durationP1/(sampletime_ms*10.0); | |
ratioP2 = durationP2/(sampletime_ms*10.0); | |
countP1 = 1.1*pow(ratioP1,3)-3.8*pow(ratioP1,2)+520*ratioP1+0.62; | |
countP2 = 1.1*pow(ratioP2,3)-3.8*pow(ratioP2,2)+520*ratioP2+0.62; | |
float PM10count = countP2; | |
float PM25count = countP1 - countP2; | |
// Assues density, shape, and size of dust | |
// to estimate mass concentration from particle | |
// count. This method was described in a 2009 | |
// paper by Uva, M., Falcone, R., McClellan, A., | |
// and Ostapowicz, E. | |
// http://wireless.ece.drexel.edu/research/sd_air_quality.pdf | |
// begins PM10 mass concentration algorithm | |
double r10 = 2.6*pow(10,-6); | |
double pi = 3.14159; | |
double vol10 = (4.0/3.0)*pi*pow(r10,3); | |
double density = 1.65*pow(10,12); | |
double mass10 = density*vol10; | |
double K = 3531.5; | |
float concLarge = (PM10count)*K*mass10; | |
// next, PM2.5 mass concentration algorithm | |
double r25 = 0.44*pow(10,-6); | |
double vol25 = (4.0/3.0)*pi*pow(r25,3); | |
double mass25 = density*vol25; | |
float concSmall = (PM25count)*K*mass25; | |
//sendData(concLarge, concSmall, PM10count, PM25count); | |
mydata[0] = 0x04; // message type: dust measurement | |
mydata[1] = (int)concSmall >> 8; | |
mydata[2] = (int)concSmall & 0xFF; | |
mydata[3] = (int)concLarge >> 8; | |
mydata[4] = (int)concLarge & 0xFF; | |
Serial.print("Small Concentration: "); | |
Serial.println(concSmall); | |
Serial.print("Large Concentration: "); | |
Serial.println(concLarge); | |
LMIC_setTxData2(i, mydata, sizeof(mydata)-1, 0); | |
durationP1 = 0; | |
durationP2 = 0; | |
starttime = millis(); | |
// wdt_reset(); // MQ wifi, probably not needed | |
} | |
// 161204 MQ dustduino end | |
os_runloop_once(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment