1100 lines
30 KiB
C++
1100 lines
30 KiB
C++
//#define DISABLE_WIFI
|
|
|
|
#include <WiFiManager.h> // https://github.com/admarschoonen/WiFiManager
|
|
#include <dESPatch.h>
|
|
|
|
#include <WiFi.h>
|
|
#include <WiFiMulti.h>
|
|
#include <HTTPClient.h>
|
|
|
|
#include <Adafruit_NeoPixel.h>
|
|
|
|
|
|
#include <math.h>
|
|
#include "PMS.h" // PMS Library from Mariusz Kacki https://github.com/fu-hsi/pms
|
|
|
|
#include "ClosedCube_HDC1080.h" // ClosedCube_HDC1080 from closedcube https://github.com/closedcube/ClosedCube_HDC1080_Arduino
|
|
#include <Adafruit_BMP280.h> // BMP280 from Adafruit https://github.com/adafruit/Adafruit_BMP280_Library
|
|
#include "Adafruit_CCS811.h" // CCS811 from Adafruit https://github.com/adafruit/Adafruit_CCS811
|
|
|
|
#define PMSA003_RESET 18
|
|
#define PMSA003_RX 17
|
|
#define PMSA003_TX 16
|
|
#define PMSA003_SET 19
|
|
|
|
ClosedCube_HDC1080 hdc1080;
|
|
|
|
Adafruit_BMP280 bmp;
|
|
|
|
Adafruit_CCS811 ccs;
|
|
|
|
PMS pms(Serial2);
|
|
PMS::DATA PMSA003_data;
|
|
|
|
typedef enum {
|
|
PMS_state_sleeping,
|
|
PMS_state_stabilizing,
|
|
PMS_state_ready
|
|
} PMS_state_t;
|
|
|
|
PMS_state_t PMS_state;
|
|
|
|
unsigned long CCS811_new_state_time = 0;
|
|
|
|
|
|
|
|
#define NUM_LEDS 93
|
|
//#define NUM_LEDS 10
|
|
#define DATA_PIN 5
|
|
|
|
#define PMSA003_RESET 18
|
|
#define PMSA003_RX 17
|
|
#define PMSA003_TX 16
|
|
#define PMSA003_SET 19
|
|
|
|
#define BUIENRADAR_START_LED 32
|
|
#define BUIENRADAR_SKIP_LED 1
|
|
#define BUIENRADAR_NUM_LEDS 12
|
|
|
|
#define PAQI_START_LED 72
|
|
#define PAQI_SKIP_LED 0
|
|
#define PAQI_NUM_LEDS 12
|
|
|
|
// #define UVI_LED 92 // LED in center
|
|
#define UVI_LED 84 // LED above center
|
|
#define AQI_LED 86 // LED to the right
|
|
#define POLLEN_LED 90 // LED to the left
|
|
#define IAQI_LED 88 // LED below center
|
|
|
|
#define SHOW_AQI_LED
|
|
#define SHOW_POLLEN_LED
|
|
#define SHOW_UVI_LED
|
|
|
|
#define MIN(x, y) (((x) <= (y)) ? (x) : (y))
|
|
|
|
Adafruit_NeoPixel leds(NUM_LEDS, DATA_PIN, NEO_GRB + NEO_KHZ800);
|
|
|
|
static HTTPClient http;
|
|
//static const String baseUrlAQI = "http://luon.net:2355/forecast?formula=[\"PAQI\",\"AQI\",\"pollen\",\"UVI\"]";
|
|
static const String baseUrlAQI = "http://target.luon.net:2356//forecast?&metrics=PAQI&metrics=AQI&metrics=pollen&metrics=UVI";
|
|
//static const String baseUrlPrecipitation = "http://luon.net:2355/forecast?formula=[\"precipitation\"]";
|
|
static const String baseUrlPrecipitation = "http://target.luon.net:2356//forecast?metrics=precipitation";
|
|
|
|
|
|
WiFiManager wifiManager;
|
|
DESPatch dESPatch;
|
|
const char* root_ca = \
|
|
"-----BEGIN CERTIFICATE-----\n" \
|
|
"MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\n" \
|
|
"MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\n" \
|
|
"DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\n" \
|
|
"PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\n" \
|
|
"Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n" \
|
|
"AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\n" \
|
|
"rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\n" \
|
|
"OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\n" \
|
|
"xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n" \
|
|
"7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\n" \
|
|
"aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\n" \
|
|
"HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\n" \
|
|
"SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\n" \
|
|
"ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\n" \
|
|
"AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\n" \
|
|
"R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\n" \
|
|
"JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\n" \
|
|
"Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n" \
|
|
"-----END CERTIFICATE-----\n";
|
|
|
|
const float location[] = {51.445466493287434, 5.515445691496135}; // Telefoonstraat, Eindhoven
|
|
const String address = "Telefoonstraat 18, Eindhoven";
|
|
//const float location[] = {51.51831326813842, 4.451744264773111}; // Bandeliersberg, Roosendaal
|
|
|
|
//const float location[] = {51.44083, 5.47778}; // Eindhoven
|
|
//const float location[] = {52.09083, 5.12222}; // Utrecht
|
|
//const float location[] = {51.8425, 5.85278}; // Nijmegen
|
|
//const float location[] = {52.37403, 4.88969}; // Amsterdam
|
|
//const float location[] = {52.15833, 4.49306}; // Leiden
|
|
|
|
typedef struct RainPickerDataArray24 {
|
|
const int len = 24;
|
|
long time[24];
|
|
float value[24];
|
|
} RainPickerDataArray24;
|
|
|
|
typedef struct RainPickerDataArray12 {
|
|
const int len = 12;
|
|
long time[12];
|
|
float value[12];
|
|
} RainPickerDataArray12;
|
|
|
|
typedef struct RainPickerDataArray5 {
|
|
const int len = 5;
|
|
long time[5];
|
|
float value[5];
|
|
} RainPickerDataArray5;
|
|
|
|
|
|
typedef struct RainpickerData {
|
|
float lat;
|
|
float lon;
|
|
float time;
|
|
|
|
RainPickerDataArray24 PAQI;
|
|
RainPickerDataArray24 AQI;
|
|
RainPickerDataArray24 pollen;
|
|
RainPickerDataArray5 UVI;
|
|
RainPickerDataArray24 precipitation;
|
|
} RainpickerData;
|
|
|
|
RainpickerData rainpickerData;
|
|
|
|
StaticJsonDocument<6144> doc;
|
|
|
|
void parseJson(String * payload)
|
|
{
|
|
doc.clear();
|
|
|
|
DeserializationError error = deserializeJson(doc, * payload);
|
|
|
|
if (error) {
|
|
Serial.print(F("deserializeJson() failed: "));
|
|
Serial.println(error.f_str());
|
|
return;
|
|
}
|
|
|
|
rainpickerData.lat = doc["lat"];
|
|
rainpickerData.lon = doc["lon"];
|
|
rainpickerData.time = doc["time"];
|
|
|
|
int n = 0;
|
|
for (JsonObject elem : doc["PAQI"].as<JsonArray>()) {
|
|
if (n >= rainpickerData.PAQI.len) {
|
|
break;
|
|
}
|
|
rainpickerData.PAQI.time[n] = elem["time"];
|
|
rainpickerData.PAQI.value[n] = elem["value"];
|
|
n = n + 1;
|
|
}
|
|
|
|
n = 0;
|
|
for (JsonObject elem : doc["AQI"].as<JsonArray>()) {
|
|
if (n >= rainpickerData.AQI.len) {
|
|
break;
|
|
}
|
|
rainpickerData.AQI.time[n] = elem["time"];
|
|
rainpickerData.AQI.value[n] = elem["value"];
|
|
n = n + 1;
|
|
}
|
|
|
|
n = 0;
|
|
for (JsonObject elem : doc["pollen"].as<JsonArray>()) {
|
|
if (n >= rainpickerData.pollen.len) {
|
|
break;
|
|
}
|
|
rainpickerData.pollen.time[n] = elem["time"];
|
|
rainpickerData.pollen.value[n] = elem["value"];
|
|
n = n + 1;
|
|
}
|
|
|
|
n = 0;
|
|
for (JsonObject elem : doc["UVI"].as<JsonArray>()) {
|
|
if (n >= rainpickerData.UVI.len) {
|
|
break;
|
|
}
|
|
rainpickerData.UVI.time[n] = elem["time"];
|
|
rainpickerData.UVI.value[n] = elem["value"];
|
|
n = n + 1;
|
|
}
|
|
|
|
n = 0;
|
|
for (JsonObject elem : doc["precipitation"].as<JsonArray>()) {
|
|
if (n >= rainpickerData.precipitation.len) {
|
|
break;
|
|
}
|
|
rainpickerData.precipitation.time[n] = elem["time"];
|
|
rainpickerData.precipitation.value[n] = elem["value"];
|
|
n = n + 1;
|
|
}
|
|
}
|
|
|
|
void setup() {
|
|
const char url[] = "https://apikey:cqprlgiafadnidsgeqozcpldkaeqimqw@despatch.luon.net/files/4/despatch.json";
|
|
unsigned long interval = 60; // By default check for updates every 60 seconds
|
|
int x;
|
|
|
|
// sanity check delay - allows reprogramming if accidently blowing power w/leds
|
|
delay(2000);
|
|
|
|
leds.begin(); // INITIALIZE NeoPixel strip object
|
|
|
|
Serial.begin(115200);
|
|
Serial.println("buienradarklok starting");
|
|
|
|
#ifndef DISABLE_WIFI
|
|
//wifiManager.resetSettings();
|
|
wifiManager.configure("clairvoyance-", true, LED_BUILTIN, true, BUTTON_BUILTIN, false);
|
|
|
|
//fetches ssid and pass and tries to connect
|
|
//if it does not connect it starts an access point
|
|
//and goes into a blocking loop awaiting configuration
|
|
if (!wifiManager.autoConnect()) {
|
|
Serial.println("failed to connect and hit timeout");
|
|
//reset and try again, or maybe put it to deep sleep
|
|
ESP.restart();
|
|
delay(1000);
|
|
}
|
|
|
|
//if you get here you have connected to the WiFi
|
|
Serial.print("connected with address: ");
|
|
Serial.println(WiFi.localIP());
|
|
|
|
//keep LED on
|
|
digitalWrite(LED_BUILTIN, LED_ON_VALUE_DEFAULT);
|
|
|
|
x = dESPatch.configure(url, true, false, interval, false, root_ca);
|
|
Serial.print("dESPatch.configure() returned with code ");
|
|
Serial.println(x);
|
|
#endif
|
|
|
|
|
|
hdc1080.begin(0x40);
|
|
Serial.print("HDC1080 Manufacturer ID=0x");
|
|
Serial.println(hdc1080.readManufacturerId(), HEX); // 0x5449 ID of Texas Instruments
|
|
Serial.print("HDC1080 Device ID=0x");
|
|
Serial.println(hdc1080.readDeviceId(), HEX); // 0x1050 ID of the device
|
|
|
|
if (!bmp.begin(BMP280_ADDRESS_ALT, BMP280_CHIPID)) {
|
|
Serial.println(F("Could not find a valid BMP280 sensor. Check wiring or try different address."));
|
|
while (1) {
|
|
delay(10);
|
|
}
|
|
}
|
|
|
|
/* Default settings from datasheet. */
|
|
bmp.setSampling(Adafruit_BMP280::MODE_NORMAL, /* Operating Mode. */
|
|
Adafruit_BMP280::SAMPLING_X2, /* Temp. oversampling */
|
|
Adafruit_BMP280::SAMPLING_X16, /* Pressure oversampling */
|
|
Adafruit_BMP280::FILTER_X16, /* Filtering. */
|
|
Adafruit_BMP280::STANDBY_MS_500); /* Standby time. */
|
|
|
|
if (!ccs.begin()) {
|
|
Serial.println("Could not find CSS811 sensor.");
|
|
while (1) {
|
|
delay(10);
|
|
}
|
|
|
|
ccs.setDriveMode(CCS811_DRIVE_MODE_IDLE); // wait for at least 10 minutes before entering a new state!
|
|
CCS811_new_state_time = millis() + 600000;
|
|
|
|
// Wait for the sensor to be ready
|
|
while(!ccs.available());
|
|
}
|
|
|
|
Serial2.begin(9600, SERIAL_8N1, PMSA003_TX, PMSA003_RX); // arg 3: ESP32 RX pin, arg 4: ESP32 TX pin
|
|
pms.passiveMode();
|
|
Serial.println("Waking up and waiting 30 seconds for stable readings...");
|
|
pms.wakeUp();
|
|
PMS_state = PMS_state_stabilizing;
|
|
|
|
}
|
|
|
|
void interpolateValues(Adafruit_NeoPixel * leds, int numLeds, int startLed, int skipLed, \
|
|
int * colors, int numColors) {
|
|
for (int led = 0; led < numLeds; led = led + skipLed + 1) {
|
|
int idxColorLow, idxColorHigh;
|
|
int idxLedLow, idxLedHigh;
|
|
int r, g, b;
|
|
|
|
idxColorLow = (numColors * led) / numLeds;
|
|
idxColorHigh = idxColorLow + 1;
|
|
idxLedLow = (idxColorLow * numLeds) / numColors;
|
|
idxLedHigh = (idxColorHigh * numLeds) / numColors;
|
|
|
|
if (idxColorHigh >= numColors) {
|
|
idxColorHigh = numColors - 1;
|
|
}
|
|
|
|
if (idxLedHigh >= numLeds) {
|
|
idxLedHigh = numLeds - 1;
|
|
}
|
|
|
|
int rgbLow = colors[idxColorLow];
|
|
int rgbHigh = colors[idxColorHigh];
|
|
|
|
int rLow = (rgbLow & 0xFF0000) >> 16;
|
|
int gLow = (rgbLow & 0x00FF00) >> 8;
|
|
int bLow = (rgbLow & 0x0000FF);
|
|
|
|
int rHigh = (rgbHigh & 0xFF0000) >> 16;
|
|
int gHigh = (rgbHigh & 0x00FF00) >> 8;
|
|
int bHigh = (rgbHigh & 0x0000FF);
|
|
|
|
if (idxLedLow == idxLedHigh) {
|
|
r = rLow;
|
|
g = gLow;
|
|
b = bLow;
|
|
} else {
|
|
r = ((rHigh - rLow) * (led - idxLedLow) + ((idxLedHigh - idxLedLow) >> 1)) / (idxLedHigh - idxLedLow) + rLow;
|
|
g = ((gHigh - gLow) * (led - idxLedLow) + ((idxLedHigh - idxLedLow) >> 1)) / (idxLedHigh - idxLedLow) + gLow;
|
|
b = ((bHigh - bLow) * (led - idxLedLow) + ((idxLedHigh - idxLedLow) >> 1)) / (idxLedHigh - idxLedLow) + bLow;
|
|
}
|
|
leds->setPixelColor(led + startLed, leds->Color(r, g, b));
|
|
}
|
|
}
|
|
|
|
/*
|
|
void getJson(const float * location) {
|
|
int httpCode;
|
|
//String url = baseUrl + "&lat=" + String(location[0]) + "&lon=" + String(location[1]);
|
|
String url, latS, lonS;
|
|
int tmp1, tmp2;
|
|
String payload = "";
|
|
|
|
tmp1 = int(location[0]);
|
|
tmp2 = int((location[0] - tmp1) * 1000000);
|
|
latS = String(tmp1) + "." + String(tmp2);
|
|
tmp1 = int(location[1]);
|
|
tmp2 = int((location[1] - tmp1) * 1000000);
|
|
lonS = String(tmp1) + "." + String(tmp2);
|
|
|
|
url = baseUrl + "&lat=" + latS + "&lon=" + lonS;
|
|
|
|
Serial.println(url);
|
|
|
|
http.begin(url);
|
|
httpCode = http.GET();
|
|
|
|
if (httpCode > 0) {
|
|
payload = http.getString();
|
|
parseJson(&payload);
|
|
} else {
|
|
Serial.print(__func__);
|
|
Serial.print("(): got http code ");
|
|
Serial.println(httpCode);
|
|
}
|
|
http.end();
|
|
}*/
|
|
|
|
// From https://circuits4you.com/2019/03/21/esp8266-url-encode-decode-example/
|
|
String urlencode(String str)
|
|
{
|
|
String encodedString="";
|
|
char c;
|
|
char code0;
|
|
char code1;
|
|
char code2;
|
|
for (int i =0; i < str.length(); i++){
|
|
c=str.charAt(i);
|
|
if (c == ' '){
|
|
encodedString+= '+';
|
|
} else if (isalnum(c)){
|
|
encodedString+=c;
|
|
} else{
|
|
code1=(c & 0xf)+'0';
|
|
if ((c & 0xf) >9){
|
|
code1=(c & 0xf) - 10 + 'A';
|
|
}
|
|
c=(c>>4)&0xf;
|
|
code0=c+'0';
|
|
if (c > 9){
|
|
code0=c - 10 + 'A';
|
|
}
|
|
code2='\0';
|
|
encodedString+='%';
|
|
encodedString+=code0;
|
|
encodedString+=code1;
|
|
//encodedString+=code2;
|
|
}
|
|
yield();
|
|
}
|
|
|
|
return encodedString;
|
|
}
|
|
|
|
void getPrecipitation(const float * location) {
|
|
int httpCode;
|
|
//String url = baseUrl + "&lat=" + String(location[0]) + "&lon=" + String(location[1]);
|
|
String url, latS, lonS;
|
|
int tmp1, tmp2;
|
|
String payload = "";
|
|
|
|
tmp1 = int(location[0]);
|
|
tmp2 = int((location[0] - tmp1) * 1000000);
|
|
latS = String(tmp1) + "." + String(tmp2);
|
|
tmp1 = int(location[1]);
|
|
tmp2 = int((location[1] - tmp1) * 1000000);
|
|
lonS = String(tmp1) + "." + String(tmp2);
|
|
|
|
url = baseUrlPrecipitation + "&lat=" + latS + "&lon=" + lonS;
|
|
|
|
Serial.println(url);
|
|
|
|
http.begin(url);
|
|
httpCode = http.GET();
|
|
|
|
if (httpCode > 0) {
|
|
payload = http.getString();
|
|
parseJson(&payload);
|
|
} else {
|
|
Serial.print(__func__);
|
|
Serial.print("(): got http code ");
|
|
Serial.println(httpCode);
|
|
}
|
|
http.end();
|
|
}
|
|
|
|
void getPrecipitation(const String address) {
|
|
int httpCode;
|
|
//String url = baseUrl + "&lat=" + String(location[0]) + "&lon=" + String(location[1]);
|
|
String url, a;
|
|
String payload = "";
|
|
|
|
a = urlencode(address);
|
|
|
|
url = baseUrlPrecipitation + "&address=" + a;
|
|
|
|
Serial.println(url);
|
|
|
|
http.begin(url);
|
|
httpCode = http.GET();
|
|
|
|
if (httpCode > 0) {
|
|
payload = http.getString();
|
|
parseJson(&payload);
|
|
} else {
|
|
Serial.print(__func__);
|
|
Serial.print("(): got http code ");
|
|
Serial.println(httpCode);
|
|
}
|
|
http.end();
|
|
}
|
|
|
|
void getAQI(const float * location) {
|
|
int httpCode;
|
|
//String url = baseUrl + "&lat=" + String(location[0]) + "&lon=" + String(location[1]);
|
|
String url, latS, lonS;
|
|
int tmp1, tmp2;
|
|
String payload = "";
|
|
|
|
tmp1 = int(location[0]);
|
|
tmp2 = int((location[0] - tmp1) * 1000000);
|
|
latS = String(tmp1) + "." + String(tmp2);
|
|
tmp1 = int(location[1]);
|
|
tmp2 = int((location[1] - tmp1) * 1000000);
|
|
lonS = String(tmp1) + "." + String(tmp2);
|
|
|
|
url = baseUrlAQI + "&lat=" + latS + "&lon=" + lonS;
|
|
|
|
Serial.println(url);
|
|
|
|
http.begin(url);
|
|
httpCode = http.GET();
|
|
|
|
if (httpCode > 0) {
|
|
payload = http.getString();
|
|
parseJson(&payload);
|
|
} else {
|
|
Serial.print(__func__);
|
|
Serial.print("(): got http code ");
|
|
Serial.println(httpCode);
|
|
}
|
|
http.end();
|
|
}
|
|
|
|
void getAQI(const String address) {
|
|
int httpCode;
|
|
//String url = baseUrl + "&lat=" + String(location[0]) + "&lon=" + String(location[1]);
|
|
String url, a;
|
|
String payload = "";
|
|
|
|
a = urlencode(address);
|
|
|
|
url = baseUrlAQI + "&address=" + a;
|
|
|
|
Serial.println(url);
|
|
|
|
http.begin(url);
|
|
httpCode = http.GET();
|
|
|
|
if (httpCode > 0) {
|
|
payload = http.getString();
|
|
parseJson(&payload);
|
|
} else {
|
|
Serial.print(__func__);
|
|
Serial.print("(): got http code ");
|
|
Serial.println(httpCode);
|
|
}
|
|
http.end();
|
|
}
|
|
|
|
static int colormap(float x) {
|
|
// Input: intensity
|
|
// Output: RGB encoded pixel color similar to luchtmeetnet legend
|
|
int y = 0;
|
|
|
|
if (x < 1.0) {
|
|
y = (0 << 16) | (0 << 8) | 0; // black (good)
|
|
} else if (x < 2.0) {
|
|
// 1.0 <= x < 2.0
|
|
y = (0 << 16) | (0 << 8) | 160; // blue (good)
|
|
} else if (x < 3.0) {
|
|
// 2.0 <= x < 3.0
|
|
y = (0 << 16) | (255 << 8) | 255; // cyan (good)
|
|
} else if (x < 4.0) {
|
|
// 3.0 <= x < 4.0
|
|
y = (255 << 16) | (255 << 8) | 255; // white (mediocre)
|
|
} else if (x < 5.0) {
|
|
// 4.0 <= x < 5.0
|
|
y = (200 << 16) | (200 << 8) | 32; // light yellow (mediocre)
|
|
} else if (x < 6.0) {
|
|
// 5.0 <= x < 6.0
|
|
y = (120 << 16) | (120 << 8) | 0; // yellow (mediocre)
|
|
} else if (x < 7.0) {
|
|
// 6.0 <= x < 7.0
|
|
y = (200 << 16) | (80 << 8) | 0; // orange (inadequate)
|
|
} else if (x < 8.0) {
|
|
// 7.0 <= x < 8.0
|
|
y = (255 << 16) | (50 << 8) | 0; // red orange (inadequate)
|
|
} else if (x < 9.0) {
|
|
// 8.0 <= x < 9.0
|
|
y = (180 << 16) | (0 << 8) | 0; // red (bad)
|
|
} else if (x < 10.0) {
|
|
// 9.0 <= x < 10.0
|
|
y = (200 << 16) | (0 << 8) | 160; // magenta (bad)
|
|
} else {
|
|
// 10.0 <= x
|
|
y = (60 << 16) | (0 << 8) | 210; // purple (terrible)
|
|
}
|
|
|
|
return y;
|
|
}
|
|
|
|
static float buienradarMap(float x) {
|
|
// Input: rain intensity (mm/h)
|
|
// Output: floating point number which can be mapped on color scale
|
|
float y = 0;
|
|
|
|
// Use E3 series of preferred numbers to map intensity to color
|
|
if (x < 0.1) {
|
|
y = 0.5;
|
|
} else if (x < 0.22) {
|
|
y = 1.5;
|
|
} else if (x < 0.47) {
|
|
y = 2.5;
|
|
} else if (x < 1.0) {
|
|
y = 3.5;
|
|
} else if (x < 2.2) {
|
|
y = 4.5;
|
|
} else if (x < 4.7) {
|
|
y = 5.5;
|
|
} else if (x < 10) {
|
|
y = 6.5;
|
|
} else if (x < 22) {
|
|
y = 7.5;
|
|
} else if (x < 47) {
|
|
y = 8.5;
|
|
} else if (x < 100) {
|
|
y = 9.5;
|
|
} else {
|
|
y = 10.5;
|
|
}
|
|
|
|
return y;
|
|
}
|
|
|
|
static int calculateIAQI(void) {
|
|
int IAQI[3] = {0};
|
|
int result = 0;
|
|
bool values_equal = true;
|
|
|
|
uint16_t eCO2 = ccs.geteCO2();
|
|
|
|
if (eCO2 < 400) {
|
|
IAQI[0] = 0;
|
|
} else if (eCO2 < 600) {
|
|
IAQI[0] = 1;
|
|
} else if (eCO2 < 800) {
|
|
IAQI[0] = 2;
|
|
} else if (eCO2 < 1500) {
|
|
IAQI[0] = 4;
|
|
} else if (eCO2 < 1800) {
|
|
IAQI[0] = 7;
|
|
} else if (eCO2 < 2500) {
|
|
IAQI[0] = 9;
|
|
} else {
|
|
IAQI[0] = 10;
|
|
}
|
|
|
|
if (PMSA003_data.PM_AE_UG_2_5 < 10) {
|
|
IAQI[1] = 0;
|
|
} else if (PMSA003_data.PM_AE_UG_2_5 < 15) {
|
|
IAQI[1] = 1;
|
|
} else if (PMSA003_data.PM_AE_UG_2_5 < 20) {
|
|
IAQI[1] = 2;
|
|
} else if (PMSA003_data.PM_AE_UG_2_5 < 30) {
|
|
IAQI[1] = 3;
|
|
} else if (PMSA003_data.PM_AE_UG_2_5 < 40) {
|
|
IAQI[1] = 4;
|
|
} else if (PMSA003_data.PM_AE_UG_2_5 < 50) {
|
|
IAQI[1] = 5;
|
|
} else if (PMSA003_data.PM_AE_UG_2_5 < 70) {
|
|
IAQI[1] = 6;
|
|
} else if (PMSA003_data.PM_AE_UG_2_5 < 90) {
|
|
IAQI[1] = 7;
|
|
} else if (PMSA003_data.PM_AE_UG_2_5 < 100) {
|
|
IAQI[1] = 8;
|
|
} else if (PMSA003_data.PM_AE_UG_2_5 < 140) {
|
|
IAQI[1] = 9;
|
|
} else {
|
|
IAQI[1] = 10;
|
|
}
|
|
|
|
|
|
if (PMSA003_data.PM_AE_UG_10_0 < 10) {
|
|
IAQI[1] = 0;
|
|
} else if (PMSA003_data.PM_AE_UG_10_0 < 20) {
|
|
IAQI[1] = 1;
|
|
} else if (PMSA003_data.PM_AE_UG_10_0 < 30) {
|
|
IAQI[1] = 2;
|
|
} else if (PMSA003_data.PM_AE_UG_10_0 < 45) {
|
|
IAQI[1] = 3;
|
|
} else if (PMSA003_data.PM_AE_UG_10_0 < 60) {
|
|
IAQI[1] = 4;
|
|
} else if (PMSA003_data.PM_AE_UG_10_0 < 75) {
|
|
IAQI[1] = 5;
|
|
} else if (PMSA003_data.PM_AE_UG_10_0 < 100) {
|
|
IAQI[1] = 6;
|
|
} else if (PMSA003_data.PM_AE_UG_10_0 < 125) {
|
|
IAQI[1] = 7;
|
|
} else if (PMSA003_data.PM_AE_UG_10_0 < 150) {
|
|
IAQI[1] = 8;
|
|
} else if (PMSA003_data.PM_AE_UG_10_0 < 200) {
|
|
IAQI[1] = 9;
|
|
} else {
|
|
IAQI[1] = 10;
|
|
}
|
|
|
|
result = 0;
|
|
for (int n = 0; n < sizeof(IAQI) / sizeof(IAQI[0]); n++) {
|
|
if (result < IAQI[n]) {
|
|
result = IAQI[n];
|
|
}
|
|
}
|
|
|
|
for (int n = 1; n < sizeof(IAQI) / sizeof(IAQI[0]); n++) {
|
|
if (IAQI[n] != IAQI[0]) {
|
|
values_equal = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ((values_equal) && (IAQI[0] != 0)) {
|
|
result = result + 1;
|
|
}
|
|
|
|
/*
|
|
Serial.print("IAQI data:");
|
|
for (int n = 0; n < sizeof(IAQI) / sizeof(IAQI[0]); n++) {
|
|
Serial.print(" ");
|
|
Serial.print(IAQI[n]);
|
|
}
|
|
Serial.println("");
|
|
Serial.print("IAQI score: ");
|
|
Serial.println(result);*/
|
|
|
|
return result;
|
|
}
|
|
|
|
static void updateLedsNoWifi(void) {
|
|
uint32_t ledIdx;
|
|
uint32_t ledCount;
|
|
uint32_t color;
|
|
int r, g, b, n;
|
|
|
|
// Start by setting all leds to value of the first datapoint
|
|
ledIdx = 11;
|
|
ledCount = 0;
|
|
for (ledCount = 0; ledCount < 11; ledCount++) {
|
|
color = colormap(ledCount);
|
|
|
|
r = (color & 0xFF0000) >> 16;
|
|
g = (color & 0x00FF00) >> 8;
|
|
b = (color & 0x0000FF);
|
|
|
|
leds.setPixelColor(ledIdx, leds.Color(r, g, b));
|
|
ledIdx = ledIdx + PAQI_SKIP_LED + 1;
|
|
}
|
|
}
|
|
|
|
static void updateLeds(void) {
|
|
uint32_t ledIdx;
|
|
uint32_t ledCount;
|
|
uint32_t color;
|
|
int r, g, b, n;
|
|
|
|
// Start by setting all leds to value of the first datapoint
|
|
ledIdx = PAQI_START_LED;
|
|
ledCount = 0;
|
|
for (ledCount = 0; ledCount < PAQI_NUM_LEDS; ledCount++) {
|
|
color = colormap(rainpickerData.PAQI.value[0]);
|
|
|
|
r = (color & 0xFF0000) >> 16;
|
|
g = (color & 0x00FF00) >> 8;
|
|
b = (color & 0x0000FF);
|
|
|
|
leds.setPixelColor(ledIdx, leds.Color(r, g, b));
|
|
ledIdx = ledIdx + PAQI_SKIP_LED + 1;
|
|
}
|
|
|
|
ledIdx = PAQI_START_LED;
|
|
ledCount = 0;
|
|
for (n = 0; n < rainpickerData.PAQI.len; n++) {
|
|
if (rainpickerData.time < rainpickerData.PAQI.time[n]) {
|
|
ledIdx = ledIdx + PAQI_SKIP_LED + 1;
|
|
ledCount = ledCount + 1;
|
|
}
|
|
if (ledCount >= PAQI_NUM_LEDS) {
|
|
break;
|
|
}
|
|
|
|
color = colormap(rainpickerData.PAQI.value[n]);
|
|
|
|
r = (color & 0xFF0000) >> 16;
|
|
g = (color & 0x00FF00) >> 8;
|
|
b = (color & 0x0000FF);
|
|
|
|
leds.setPixelColor(ledIdx, leds.Color(r, g, b));
|
|
}
|
|
|
|
#ifdef SHOW_AQI_LED
|
|
// calculate AQI max value for next 12 hours
|
|
uint32_t AQI_max_value = 0;
|
|
//Serial.println("AQI:");
|
|
for (n = 0; n < rainpickerData.AQI.len; n++) {
|
|
/*Serial.print(" ");
|
|
Serial.print(rainpickerData.AQI.time[n]);
|
|
Serial.print(": ");
|
|
Serial.println(rainpickerData.AQI.value[n]);*/
|
|
if (rainpickerData.time - rainpickerData.AQI.time[n] >= 60 * 60) {
|
|
continue;
|
|
}
|
|
if (rainpickerData.AQI.time[n] >= rainpickerData.time + PAQI_NUM_LEDS * 60 * 60) {
|
|
break;
|
|
}
|
|
if (rainpickerData.AQI.value[n] > AQI_max_value) {
|
|
AQI_max_value = rainpickerData.AQI.value[n];
|
|
}
|
|
}
|
|
//Serial.print("AQI max value: ");
|
|
//Serial.println(AQI_max_value);
|
|
color = colormap(AQI_max_value);
|
|
|
|
r = (color & 0xFF0000) >> 16;
|
|
g = (color & 0x00FF00) >> 8;
|
|
b = (color & 0x0000FF);
|
|
|
|
leds.setPixelColor(AQI_LED, leds.Color(r, g, b));
|
|
#endif
|
|
|
|
#ifdef SHOW_POLLEN_LED
|
|
// calculate pollen max value for next 12 hours
|
|
uint32_t pollen_max_value = rainpickerData.pollen.value[0];
|
|
for (n = 0; n < rainpickerData.pollen.len; n++) {
|
|
if (rainpickerData.time - rainpickerData.pollen.time[n] >= 60 * 60) {
|
|
continue;
|
|
}
|
|
if (rainpickerData.pollen.time[n] >= rainpickerData.time + PAQI_NUM_LEDS * 60 * 60) {
|
|
break;
|
|
}
|
|
if (rainpickerData.pollen.value[n] > pollen_max_value) {
|
|
pollen_max_value = rainpickerData.pollen.value[n];
|
|
}
|
|
}
|
|
|
|
color = colormap(pollen_max_value);
|
|
|
|
r = (color & 0xFF0000) >> 16;
|
|
g = (color & 0x00FF00) >> 8;
|
|
b = (color & 0x0000FF);
|
|
|
|
leds.setPixelColor(POLLEN_LED, leds.Color(r, g, b));
|
|
#endif
|
|
|
|
#ifdef SHOW_UVI_LED
|
|
// calculate UVI led value
|
|
// UVI data is daily based --> search for first timestamp in the future and select value of the timestamp just before that
|
|
uint32_t UVI_value = rainpickerData.UVI.value[0];
|
|
|
|
for (n = 0; n < rainpickerData.UVI.len; n++) {
|
|
if (rainpickerData.UVI.time[n] >= rainpickerData.time) {
|
|
break;
|
|
}
|
|
UVI_value = rainpickerData.UVI.value[n];
|
|
}
|
|
color = colormap(UVI_value);
|
|
|
|
r = (color & 0xFF0000) >> 16;
|
|
g = (color & 0x00FF00) >> 8;
|
|
b = (color & 0x0000FF);
|
|
|
|
leds.setPixelColor(UVI_LED, leds.Color(r, g, b));
|
|
#endif
|
|
|
|
// Start by setting all leds to value of the first datapoint
|
|
ledIdx = BUIENRADAR_START_LED;
|
|
for (ledCount = 0; ledCount < BUIENRADAR_NUM_LEDS; ledCount++) {
|
|
color = colormap(rainpickerData.precipitation.value[0]);
|
|
|
|
r = (color & 0xFF0000) >> 16;
|
|
g = (color & 0x00FF00) >> 8;
|
|
b = (color & 0x0000FF);
|
|
|
|
ledIdx = ledIdx + BUIENRADAR_SKIP_LED + 1;
|
|
}
|
|
|
|
ledIdx = BUIENRADAR_START_LED;
|
|
ledCount = 0;
|
|
for (n = 0; n < rainpickerData.precipitation.len; n++) {
|
|
if (rainpickerData.time < rainpickerData.precipitation.time[n]) {
|
|
ledIdx = ledIdx + BUIENRADAR_SKIP_LED + 1;
|
|
ledCount = ledCount + 1;
|
|
}
|
|
if (ledCount >= BUIENRADAR_NUM_LEDS) {
|
|
break;
|
|
}
|
|
|
|
color = colormap(buienradarMap(rainpickerData.precipitation.value[n]));
|
|
|
|
r = (color & 0xFF0000) >> 16;
|
|
g = (color & 0x00FF00) >> 8;
|
|
b = (color & 0x0000FF);
|
|
/*Serial.print("setting led ");
|
|
Serial.print(ledIdx);
|
|
Serial.print(" to value 0x");
|
|
Serial.println(color, HEX);*/
|
|
leds.setPixelColor(ledIdx, leds.Color(r, g, b));
|
|
}
|
|
|
|
|
|
color = colormap(calculateIAQI());
|
|
|
|
r = (color & 0xFF0000) >> 16;
|
|
g = (color & 0x00FF00) >> 8;
|
|
b = (color & 0x0000FF);
|
|
|
|
/* Serial.print("setting led ");
|
|
Serial.print(IAQI_LED);
|
|
Serial.print(" to value 0x");
|
|
Serial.println(color, HEX);*/
|
|
leds.setPixelColor(IAQI_LED, leds.Color(r, g, b));
|
|
}
|
|
|
|
void readSensors() {
|
|
static unsigned long t_start = 0;
|
|
static unsigned long bmp_time = 0;
|
|
static unsigned long hdc1080_time = 0;
|
|
static unsigned long ccs811_time = 0;
|
|
unsigned long t_now;
|
|
uint32_t pms_counter = 0;
|
|
double hdc1080_T_raw = -400;
|
|
double hdc1080_RH_raw = -400;
|
|
double ccs811_T_raw = -400;
|
|
|
|
t_now = millis();
|
|
|
|
switch (PMS_state) {
|
|
case PMS_state_sleeping:
|
|
if (t_now - t_start >= 300000) {
|
|
Serial.println("Waking up and waiting 30 seconds for stable readings...");
|
|
pms.wakeUp();
|
|
PMS_state = PMS_state_stabilizing;
|
|
t_start = t_now;
|
|
}
|
|
break;
|
|
case PMS_state_stabilizing:
|
|
if (t_now - t_start >= 30000) {
|
|
Serial.println("Sensor should be stable now...");
|
|
PMS_state = PMS_state_ready;
|
|
}
|
|
break;
|
|
case PMS_state_ready:
|
|
Serial.println("Reading sensor...");
|
|
pms.requestRead();
|
|
do {
|
|
if (pms.readUntil(PMSA003_data)) {
|
|
Serial.print("PM 1.0 (ug/m3): ");
|
|
Serial.println(PMSA003_data.PM_AE_UG_1_0);
|
|
|
|
Serial.print("PM 2.5 (ug/m3): ");
|
|
Serial.println(PMSA003_data.PM_AE_UG_2_5);
|
|
|
|
Serial.print("PM 10.0 (ug/m3): ");
|
|
Serial.println(PMSA003_data.PM_AE_UG_10_0);
|
|
|
|
Serial.println();
|
|
break;
|
|
} else {
|
|
Serial.println("No data :(");
|
|
pms_counter++;
|
|
if (pms_counter > 10) {
|
|
break;
|
|
}
|
|
}
|
|
} while(1);
|
|
|
|
Serial.println("Sleeping for ~ 270 seconds...");
|
|
pms.sleep();
|
|
PMS_state = PMS_state_sleeping;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (t_now - bmp_time > 5000) {
|
|
double bmp280_T_raw = bmp.readTemperature();
|
|
double bmp280_T_corrected = bmp280_T_raw - (25.5 - 18.4);
|
|
|
|
Serial.print(F("BMP280: temperature = "));
|
|
Serial.print(bmp280_T_raw);
|
|
Serial.print(", ");
|
|
Serial.print(bmp280_T_corrected);
|
|
|
|
Serial.println(" *C");
|
|
|
|
Serial.print(F("BMP280: pressure = "));
|
|
Serial.print(bmp.readPressure());
|
|
Serial.println(" Pa");
|
|
|
|
Serial.print(F("BMP280: approx altitude = "));
|
|
Serial.print(bmp.readAltitude(1013.25)); /* Adjusted to local forecast! */
|
|
Serial.println(" m");
|
|
|
|
bmp_time = t_now;
|
|
}
|
|
|
|
if (t_now - hdc1080_time > 5000) {
|
|
hdc1080_T_raw = hdc1080.readTemperature();
|
|
hdc1080_RH_raw = hdc1080.readHumidity();
|
|
|
|
// Both HDC1080 and BMP280 temperature readings are off by ~ 7 degrees
|
|
double hdc1080_T_corrected = hdc1080_T_raw - (25.5 - 18.4);
|
|
|
|
/*
|
|
* Re-calculate humidity based on corrected temperature; formulas from
|
|
* https://www.thecalculator.co/others/Relative-Humidity-Calculator-682.html
|
|
*/
|
|
// double T_dew = (pow(hdc1080_RH_raw/100, 1/8)*(112+0.9*hdc1080_T_raw) )+(0.1*hdc1080_T_raw)-112;
|
|
// double hdc1080_RH_corrected = 100*((112-0.1*hdc1080_T_corrected+T_dew)/(112+0.9*hdc1080_T_corrected));
|
|
|
|
// http://hyperphysics.phy-astr.gsu.edu/hbase/Kinetic/relhum.html
|
|
double VD_sat_raw = 5.018 + 0.32321 * hdc1080_T_raw +
|
|
0.0081847 * hdc1080_T_raw * hdc1080_T_raw +
|
|
0.00031243 * hdc1080_T_raw * hdc1080_T_raw * hdc1080_T_raw;
|
|
|
|
double VD_act = hdc1080_RH_raw * VD_sat_raw / 100;
|
|
|
|
double VD_sat_corrected = 5.018 + 0.32321 * hdc1080_T_corrected +
|
|
0.0081847 * hdc1080_T_corrected * hdc1080_T_corrected +
|
|
0.00031243 * hdc1080_T_corrected * hdc1080_T_corrected * hdc1080_T_corrected;
|
|
|
|
// double hdc1080_RH_corrected = VD_act / VD_sat_corrected * 100;
|
|
double hdc1080_RH_corrected = (hdc1080_RH_raw + VD_act / VD_sat_corrected * 100) / 2;
|
|
|
|
|
|
|
|
Serial.print("HDC1080: temperature = ");
|
|
Serial.print(hdc1080_T_raw);
|
|
Serial.print(", ");
|
|
Serial.print(hdc1080_T_corrected);
|
|
Serial.println(" *C");
|
|
Serial.print("HDC1080: RH = ");
|
|
Serial.print(hdc1080_RH_raw);
|
|
Serial.print(", ");
|
|
Serial.print(hdc1080_RH_corrected);
|
|
Serial.println(" %");
|
|
|
|
//ccs.setEnvironmentalData(hdc1080_RH_corrected, hdc1080_T_corrected);
|
|
|
|
hdc1080_time = t_now;
|
|
}
|
|
|
|
if ((CCS811_new_state_time != 0) && (CCS811_new_state_time < millis())) {
|
|
Serial.println("");
|
|
Serial.println("SETTING CCS811 TO DRIVE MODE 10 SEC");
|
|
Serial.println("");
|
|
ccs.setDriveMode(CCS811_DRIVE_MODE_10SEC); // wait for at least 10 minutes before entering a new state!
|
|
CCS811_new_state_time = 0;
|
|
}
|
|
|
|
if ((ccs.available()) && (t_now - ccs811_time > 5000)) {
|
|
if (!ccs.readData()) {
|
|
Serial.print("CCS811: eCO2 = ");
|
|
Serial.print(ccs.geteCO2());
|
|
Serial.println("ppm");
|
|
Serial.print("CCS811: eTVOC = ");
|
|
Serial.println(ccs.getTVOC());
|
|
} else {
|
|
Serial.println("Error reading CCS811 data");
|
|
}
|
|
Serial.println();
|
|
ccs811_time = t_now;
|
|
}
|
|
}
|
|
|
|
void loop(void) {
|
|
static unsigned long t_prev = 0;
|
|
static bool ledOn = false;
|
|
unsigned long t_now = millis();
|
|
const int buttonPin = 0;
|
|
static int rainColor[12] = {0};
|
|
static int AQIColor[12] = {0};
|
|
static bool firstTime = true;
|
|
static int counter = 0;
|
|
|
|
if (firstTime || (t_now - t_prev >= 60000)) {
|
|
t_prev = t_now;
|
|
firstTime = false;
|
|
|
|
//buienradar((float *) location, 12, rainColor);
|
|
//luchtmeetnet((float *) location, 12, AQIColor, gmtOffset_sec, daylightOffset_sec);
|
|
|
|
#ifndef DISABLE_WIFI
|
|
getPrecipitation(address);
|
|
|
|
if (counter == 0) {
|
|
getAQI(address);
|
|
}
|
|
|
|
counter = counter + 1;
|
|
|
|
if (counter == 5) {
|
|
// AQI should only be retreived every 5 minutes
|
|
counter = 0;
|
|
}
|
|
#endif
|
|
|
|
/*Serial.println("precipitation:");
|
|
for (int n = 0; n < rainpickerData.precipitation.len; n++) {
|
|
Serial.print(" ");
|
|
Serial.print(rainpickerData.precipitation.time[n]);
|
|
Serial.print(": ");
|
|
Serial.println(rainpickerData.precipitation.value[n]);
|
|
}*/
|
|
}
|
|
|
|
//readSensors();
|
|
|
|
#ifdef DISABLE_WIFI
|
|
updateLedsNoWifi();
|
|
#else
|
|
updateLeds();
|
|
#endif
|
|
|
|
leds.show();
|
|
|
|
delay(100);
|
|
#ifndef DISABLE_WIFI
|
|
dESPatch.checkForUpdate(true);
|
|
#endif
|
|
}
|