Files
aqi-monitor/aqi.ino

263 lines
7.3 KiB
C++

#include <Arduino.h>
#include <LiquidCrystal.h>
#include <math.h>
#include <SensirionI2CSen44.h>
#include <Wire.h>
// Setup Sen44 sensor.
SensirionI2CSen44 sen44;
// Sen44 measurements.
struct {
uint16_t pm1p0 = 0;
uint16_t pm2p5 = 0;
uint16_t pm4p0 = 0;
uint16_t pm10p0 = 0;
float voc = 0;
float hum = 0;
float temp = 0;
float f_temp = 0;
} sen44_mes;
// Setup LCD.
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
// LCD resolution.
enum { LCD_WIDTH = 16, LCD_HEIGHT = 2 };
// Supported values to be displayed on the LCD.
enum supported_vals { pm_1p0, pm2p5, pm4p0, pm10p0, voc, hum, temp, f_temp, empty };
// Values per screen. values[0...] is considered a screen and values[][0...]
// are its values.
enum supported_vals values[][4] = {
{temp, f_temp, hum, voc},
{pm_1p0, pm2p5, pm4p0, pm10p0},
};
// Screens and values counts.
#define SCREENS_COUNT (sizeof(values) / sizeof(values[0]))
#define VALUES_COUNT (sizeof(values[0]) / sizeof(values[0][0]))
// Current screen.
unsigned int current_screen = 0;
// Current columns and rows on the LCD.
int col = 0;
int row = 0;
// Setup buttons.
const int mode_btn = 6;
const int lcd_on_off_btn = 9;
// Setup HIGH/LOW output.
const int lcd_on_off_output = 10;
// Execution times for things that will be "parallelized".
struct {
unsigned long modes_btn = 0;
unsigned long on_off_btn = 0;
unsigned long update_lcd = 0;
unsigned long sen44 = 0;
} parallel;
// Force write data to screen.
void update_screen(void);
// Cycle between screens.
void cycle_screens(void);
// Convert Celcius to Fahrenheit.
float celsius_to_fahrenheit(float degrees);
// Calculate heat index (aka "feels like") termperature.
float heat_index(float t, float rh);
void setup(void)
{
// Setup Serial.
Serial.begin(115200);
while (!Serial)
delay(100);
// Set up the LCD's number of columns and rows.
lcd.begin(LCD_WIDTH, LCD_HEIGHT);
// Set LCD cursor to position 0, 0.
lcd.setCursor(0, 0);
lcd.print(" calibrating...");
// Setup buttons.
pinMode(mode_btn, INPUT_PULLUP);
pinMode(lcd_on_off_btn, INPUT_PULLUP);
// Setup HIGH/LOW output.
pinMode(lcd_on_off_output, OUTPUT);
digitalWrite(lcd_on_off_output, HIGH);
// Setup Sen44 sensor. From measurement to cleaning.
// Calibration takes 2 minutes.
Wire.begin();
sen44.begin(Wire);
uint16_t error;
char errorMessage[256];
error = sen44.deviceReset();
if (error) {
Serial.print("Error trying to execute getSerialNumber(): ");
errorToString(error, errorMessage, 256);
Serial.println(errorMessage);
}
error = sen44.startMeasurement();
if (error) {
Serial.print("Error trying to execute startMeasurement(): ");
errorToString(error, errorMessage, 256);
Serial.println(errorMessage);
}
sen44.startFanCleaning();
delay(120000);
}
void loop(void)
{
// "Parallel" execution for button to switch modes.
if (millis() - parallel.modes_btn >= 150) {
parallel.modes_btn = millis();
if (digitalRead(mode_btn) == LOW) {
cycle_screens();
update_screen();
}
}
// "Parallel" execution for button to turn on/off display.
if (millis() - parallel.on_off_btn >= 150) {
parallel.on_off_btn = millis();
if (digitalRead(lcd_on_off_btn) == LOW) {
if (digitalRead(lcd_on_off_output) == LOW)
digitalWrite(lcd_on_off_output, HIGH);
else
digitalWrite(lcd_on_off_output, LOW);
}
}
// "Parallel" execution for updating display.
if (millis() - parallel.update_lcd >= 2000) {
parallel.update_lcd = millis();
update_screen();
}
// "Parallel" execution for reading measurements from Sen44 sensor.
if (millis() - parallel.sen44 >= 1000) {
parallel.sen44 = millis();
uint16_t error;
char errorMessage[256];
error = sen44.readMeasuredMassConcentrationAndAmbientValues(
sen44_mes.pm1p0, sen44_mes.pm2p5, sen44_mes.pm4p0, sen44_mes.pm10p0,
sen44_mes.voc, sen44_mes.hum, sen44_mes.temp
);
if (error) {
Serial.print("Error trying to execute "
"readMeasuredMassConcentrationAndAmbientValues(): ");
errorToString(error, errorMessage, 256);
Serial.println(errorMessage);
return;
}
sen44_mes.temp = celsius_to_fahrenheit(sen44_mes.temp);
sen44_mes.f_temp = heat_index(sen44_mes.temp, sen44_mes.hum);
}
}
// Write data to screen.
void update_screen(void)
{
col = row = 0;
lcd.clear();
for (int f = 0; f < VALUES_COUNT; f++) {
lcd.setCursor(col, row);
switch (values[current_screen][f]) {
case pm_1p0:
lcd.print("01: ");
lcd.print(sen44_mes.pm1p0);
break;
case pm2p5:
lcd.print("02: ");
lcd.print(sen44_mes.pm2p5);
break;
case pm4p0:
lcd.print("04: ");
lcd.print(sen44_mes.pm4p0);
break;
case pm10p0:
lcd.print("10: ");
lcd.print(sen44_mes.pm10p0);
break;
case voc:
lcd.print("v: ");
lcd.print(round(sen44_mes.voc));
break;
case hum:
lcd.print("h: ");
lcd.print(round(sen44_mes.hum));
break;
case temp:
lcd.print("t: ");
lcd.print(round(sen44_mes.temp));
break;
case f_temp:
lcd.print("f: ");
lcd.print(round(sen44_mes.f_temp));
break;
case empty:
break;
}
// Evenly space all items on the LCD screen.
int spacing = LCD_WIDTH / (int)ceil(VALUES_COUNT / 2.0f);
col += spacing;
if (col > LCD_WIDTH - spacing) {
col = 0;
row += 1;
if (row > LCD_HEIGHT)
row = LCD_HEIGHT;
}
}
}
// Cycle between screens.
void cycle_screens(void)
{
current_screen += 1;
if (current_screen >= SCREENS_COUNT)
current_screen = 0;
}
// Convert Celcius to Fahrenheit.
float celsius_to_fahrenheit(float degrees)
{
return degrees * 1.8 + 32;
}
// Calculate heat index (aka "feels like") termperature.
float heat_index(float t, float rh)
{
float hi = 0.0;
if (t <= 40.0)
hi = t;
else {
float hitemp = 61.0 + ((t - 68.0) * 1.2) + (rh * 0.094);
float hifinal = 0.5 * (t + hitemp);
hi = -42.379 + 2.04901523 * t + 10.14333127 * rh - 0.22475541 * t * rh -
6.83783 * (pow(10, -3)) * (pow(t, 2)) -
5.481717 * (pow(10, -2)) * (pow(rh, 2)) +
1.22874 * (pow(10, -3)) * (pow(t, 2)) * rh +
8.5282 * (pow(10, -4)) * t * (pow(rh, 2)) -
1.99 * (pow(10, -6)) * (pow(t, 2)) * (pow(rh, 2));
if (hifinal > 79.0) {
if ((rh <= 13) && (t >= 80.0) && (t <= 112.0))
hi = hi - ((13.0 - rh) / 4.0) * sqrt((17.0 - fabs(t - 95.0)) / 17.0);
else if ((rh > 85.0) && (t >= 80.0) && (t <= 87.0))
hi = hi + ((rh - 85.0) / 10.0) * ((87.0 - t) / 5.0);
} else
hi = hifinal;
}
return hi;
}