Skip to content

Instantly share code, notes, and snippets.

@proffalken
Last active February 3, 2017 20:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save proffalken/f35130a6e8b347f5da6130a9291de77c to your computer and use it in GitHub Desktop.
Save proffalken/f35130a6e8b347f5da6130a9291de77c to your computer and use it in GitHub Desktop.
TTN Dust Sensor
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;
}
/*******************************************************************************
* 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