You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1303 lines
29 KiB
1303 lines
29 KiB
/* |
|
|
|
AirQuality.ino |
|
|
|
ATMEGA328 (Arduino UNO) only |
|
|
|
Universal 8bit Graphics Library (https://github.com/olikraus/u8g2/) |
|
|
|
Copyright (c) 2018, olikraus@gmail.com |
|
All rights reserved. |
|
|
|
Redistribution and use in source and binary forms, with or without modification, |
|
are permitted provided that the following conditions are met: |
|
|
|
* Redistributions of source code must retain the above copyright notice, this list |
|
of conditions and the following disclaimer. |
|
|
|
* Redistributions in binary form must reproduce the above copyright notice, this |
|
list of conditions and the following disclaimer in the documentation and/or other |
|
materials provided with the distribution. |
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
|
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
|
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
|
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
|
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
|
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
|
|
http://shelvin.de/arduino-in-den-sleep_mode_pwr_down-schlaf-modus-setzen/ |
|
|
|
|
|
battery symbol |
|
trinket 3.3V requires at least 3.5V (https://learn.adafruit.com/introducing-pro-trinket/pinouts) |
|
Assuming 3x AAA and a fresh cell with 1.5V: --> 4.5V |
|
There are 6 battery symbols |
|
symbol 0: < 3.5V |
|
symbol 1: 3.5 - 3.7 |
|
symbol 2: 3.7 - 3.9 |
|
symbol 3: 3.9 - 4.1 |
|
symbol 4: 4.1 - 4-5 |
|
symbol 5: >= 4.5V |
|
1V difference --> 5 steps --> 1V / 5 --> 0.2V step |
|
|
|
R1 = 470K |
|
R2 = 100K |
|
Rsum = 570K |
|
|
|
analogRead will use the internal 1.1V reference signal |
|
|
|
Um = 100K * 4.5V / 570K = 0.7894 V --> *1024/1.1 --> 735 (analogRead) |
|
Um = 100K * 3.5V / 570K = 0.6140 V --> 571 (analogRead) |
|
|
|
735 - 571 = 164 --> 33 |
|
|
|
analogRead*1.1/1024 = Um, Um = 100 * Ubat/570 |
|
analogRead*1.1/1024 = 10 * Ubat/57 |
|
analogRead*1.1*57/10240 = Ubat |
|
Ubat = analogRead*1.1*57/10240 = analogRead*0.006123 = analogRead/163 |
|
|
|
|
|
*/ |
|
|
|
|
|
#include <Arduino.h> |
|
#include <Wire.h> |
|
#include <U8g2lib.h> |
|
#include <Adafruit_SGP30.h> |
|
#include <SHTSensor.h> // adafruit sht31 lib v1.0.0 has a wrong 500 milliseconds delay, so use the SHTSensor.h lib |
|
#include <avr/sleep.h> |
|
|
|
|
|
//=================================================== |
|
// Constants: Timing values for the sensor & display state machine |
|
// all values are "seconds" |
|
|
|
// define startup calibration time: 2h |
|
//#define STARTUP_TIME (60*60*2) |
|
// five minutes |
|
#define STARTUP_TIME (60*5) |
|
|
|
// define duration after which the display is disabled: 30 seconds |
|
#define DISPLAY_TIME ((30)) |
|
|
|
// Datasheet, page 8: |
|
// For the first 15s after the “Init_air_quality” command the sensor |
|
// is in an initialization phase during which a “Measure_air_quality” |
|
// command returns fixed values |
|
// --> sensor warmup time |
|
#define SENSOR_WARMUP_TIME ((16)) |
|
|
|
// SENSOR_MEASURE_TIME defines the duration how long the measurement |
|
// shell be aktive (after SENSOR_WARMUP_TIME) |
|
#define SENSOR_MEASURE_TIME (14) |
|
|
|
// This is the time, after which the gas sensor should do another measurement. |
|
// The sum of SENSOR_WARMUP_TIME and SENSOR_MEASURE_TIME must |
|
// be lesser than SENSOR_SAMPLE_TIME |
|
// every 7 minutes, so there are at least two measures per history entry |
|
#define SENSOR_SAMPLE_TIME (7*60) |
|
|
|
// This is the time after which a new history entry is generated |
|
// There are 96 entries in the history table (HIST_CNT) |
|
// 15*60 = 15 minutes --> 96 * 15 minutes --> 24h |
|
#define NEW_HISTORY_DELAY (15*60) |
|
//#define NEW_HISTORY_DELAY 2 |
|
|
|
// changing the content of the display is done via "shakes" |
|
// shakes are detected by a tilt switsch. |
|
// This value defines the number of shake events to change the display page. |
|
#define NEW_DISPLAY_SHAKE_THRESHOLD 7 |
|
|
|
// number of seconds, for which a new display page is fixed |
|
// this means, for this duration, the user can not change the display page |
|
#define NEW_DISPLAY_COOL_DOWN 3 |
|
|
|
|
|
// Battery empty value |
|
// U = 100K * 3.5V / 570K = 0.6140 V --> 571 (analogRead) |
|
#define BATTERY_LOW 571 |
|
// step width for each battery symbol |
|
#define BATTERY_STEP 33 |
|
|
|
|
|
U8G2_SSD1306_128X64_NONAME_2_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); |
|
|
|
SHTSensor sht; // temperature & humidity sensor |
|
|
|
Adafruit_SGP30 sgp; // air quality sensor |
|
|
|
U8G2LOG u8g2log; |
|
|
|
// setup the terminal (U8G2LOG) and connect to u8g2 for automatic refresh of the display |
|
// The size (width * height) depends on the selected font and the display |
|
|
|
#define U8LOG_WIDTH 24 |
|
#define U8LOG_HEIGHT 5 |
|
uint8_t u8log_buffer[U8LOG_WIDTH*U8LOG_HEIGHT]; |
|
|
|
//=================================================== |
|
// Constants: Font definitions for U8g2 |
|
|
|
#define FONT_NARROW u8g2_font_mercutio_basic_nbp_tr |
|
#define FONT_SMALL u8g2_font_helvB08_tf |
|
#define FONT_MED_NUM u8g2_font_helvB14_tn |
|
//#define FONT_BIG u8g2_font_inr49_mn |
|
#define FONT_BIG u8g2_font_logisoso38_tn |
|
|
|
//=================================================== |
|
// Constants: State values for the sensor & display coordination |
|
|
|
#define STATE_RESET 0 |
|
#define STATE_STARTUP_DISP_ON 1 |
|
#define STATE_STARTUP_DISP_OFF 2 |
|
#define STATE_WARMUP_DISP_ON 11 |
|
#define STATE_WARMUP_DISP_OFF 12 |
|
#define STATE_MEASURE_DISP_ON 21 |
|
#define STATE_MEASURE_DISP_OFF 22 |
|
#define STATE_SENSOR_SLEEP_DISP_OFF 32 |
|
|
|
|
|
//=================================================== |
|
// Constants: Temperature boundaries |
|
|
|
#define TEMP_LOW -20 |
|
#define TEMP_HIGH (TEMP_LOW+120) |
|
|
|
//=================================================== |
|
// Constants: Number of different display pages |
|
#define DISPLAY_PAGE_CNT 3 |
|
|
|
//=================================================== |
|
// Constants: History |
|
|
|
#define HIST_CNT 96 |
|
|
|
// history sample time: number of seconds between each history entry |
|
// --> see NEW_HISTORY_DELAY |
|
// 15 min = 15*60 seconds: 96 entries for 24h |
|
|
|
//=================================================== |
|
// State variable for the sensor & display coordination |
|
|
|
uint8_t state = STATE_RESET; // assign STATE_xxx constants |
|
|
|
uint8_t is_display_enabled = 0; // modified by enable_display() and disable_display() |
|
|
|
|
|
//=================================================== |
|
// Variables: Air quality sensor related varables |
|
|
|
float temperature_raw; |
|
float humidity_raw; |
|
uint8_t temperature; /* with offset and multiplied by 2 */ |
|
uint8_t humidity; /* *2 */ |
|
uint16_t tvoc_raw; |
|
uint16_t tvoc_lp; |
|
uint16_t eco2_raw; |
|
uint16_t eco2_lp; |
|
int is_air_quality_available = 0; |
|
|
|
// Calibration values, values seem to be 0x8a27 and 0x08a98 for my sensor, so 0 will be used as not set |
|
uint16_t eco2_base = 0; // calibration value eCO2 |
|
uint16_t tvoc_base = 0; // calibration value TVOC |
|
|
|
//=================================================== |
|
uint16_t battery_raw; |
|
uint16_t battery_glyph; |
|
|
|
//=================================================== |
|
// Variables: Timer for the state machine |
|
|
|
volatile uint32_t wdt_count = 0; |
|
volatile uint8_t wdt_sec = 0; |
|
volatile uint8_t wdt_min = 0; |
|
volatile uint8_t wdt_hour = 0; |
|
volatile uint16_t wdt_day = 0; |
|
|
|
|
|
volatile uint16_t startup_timer = 0; |
|
volatile uint16_t sensor_sample_timer = SENSOR_SAMPLE_TIME; |
|
volatile uint8_t is_sensor_sample_timer_alarm = 0; // if zero: wake up gas sensor |
|
volatile uint8_t display_timer = 0; |
|
|
|
volatile uint8_t sensor_warmup_timer = 0; |
|
volatile uint8_t sensor_measure_timer = 0; |
|
|
|
volatile uint8_t new_display_cool_down_timer = 0; |
|
|
|
volatile uint8_t is_wdt_irq = 0; |
|
|
|
volatile uint8_t is_new_history_entry = 0; |
|
volatile uint16_t new_history_timer = NEW_HISTORY_DELAY; |
|
|
|
//volatile uint16_t battery_level = 0; // in millivolt |
|
|
|
|
|
uint32_t millis_sensor; // Debugging: Duration of the sensour measurement |
|
uint32_t millis_display; // Debugging: Duration of the display refresh |
|
|
|
|
|
//=================================================== |
|
// Variables: Shake detection |
|
|
|
volatile uint8_t is_shake = 0; |
|
volatile uint8_t shake_cnt = 0; |
|
volatile uint8_t shake_last_cnt = 0; |
|
|
|
//=================================================== |
|
// Variables: Current visible display page |
|
|
|
uint8_t current_display_page = 0; // 0 .. DISPLAY_PAGE_CNT - 1 |
|
|
|
//=================================================== |
|
// Variables: History management |
|
|
|
uint8_t hist_start = 0; |
|
uint8_t hist_end = 1; |
|
uint8_t hist_last = 0; |
|
|
|
uint16_t hist_eco2_max[HIST_CNT]; |
|
uint16_t hist_eco2_min[HIST_CNT]; |
|
|
|
//uint8_t hist_temp_max[HIST_CNT]; |
|
//uint8_t hist_temp_min[HIST_CNT]; |
|
|
|
//uint8_t hist_rh_max[HIST_CNT]; |
|
//uint8_t hist_rh_min[HIST_CNT]; |
|
|
|
//=================================================== |
|
|
|
void hist_append(void) |
|
{ |
|
hist_last = hist_end; |
|
|
|
if ( hist_end == hist_start ) |
|
{ // history is full |
|
hist_start++; |
|
if ( hist_start >= HIST_CNT ) |
|
hist_start = 0; |
|
} |
|
|
|
|
|
hist_end++; |
|
if ( hist_end >= HIST_CNT ) |
|
{ |
|
hist_end = 0; |
|
} |
|
} |
|
|
|
|
|
uint16_t maximum(uint16_t a, uint16_t b) |
|
{ |
|
if ( a < b ) |
|
return b; |
|
return a; |
|
} |
|
|
|
uint16_t minimum(uint16_t a, uint16_t b) |
|
{ |
|
if ( a > b ) |
|
return b; |
|
return a; |
|
} |
|
|
|
|
|
/* append current values to last history entry */ |
|
void add_hist_minmax(void) |
|
{ |
|
|
|
hist_eco2_max[hist_last] = maximum(hist_eco2_max[hist_last], eco2_raw); |
|
hist_eco2_min[hist_last] = minimum(hist_eco2_min[hist_last], eco2_raw); |
|
|
|
//hist_temp_max[hist_last] = maximum(hist_temp_max[hist_last], temperature); |
|
//hist_temp_min[hist_last] = minimum(hist_temp_min[hist_last], temperature); |
|
|
|
//hist_rh_max[hist_last] = maximum(hist_rh_max[hist_last], humidity); |
|
//hist_rh_min[hist_last] = minimum(hist_rh_min[hist_last], humidity); |
|
} |
|
|
|
void add_hist_new(void) |
|
{ |
|
hist_append(); |
|
|
|
hist_eco2_max[hist_last] = eco2_raw; |
|
hist_eco2_min[hist_last] = eco2_raw; |
|
|
|
//hist_temp_max[hist_last] = temperature; |
|
//hist_temp_min[hist_last] = temperature; |
|
|
|
//hist_rh_max[hist_last] = humidity; |
|
//hist_rh_min[hist_last] = humidity; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//=================================================== |
|
|
|
|
|
ISR(WDT_vect) |
|
{ |
|
is_wdt_irq = 1; |
|
wdt_count++; |
|
|
|
|
|
wdt_sec++; |
|
if ( wdt_sec >= 60 ) |
|
{ |
|
wdt_sec = 0; |
|
wdt_min++; |
|
if ( wdt_min >= 60 ) |
|
{ |
|
wdt_min = 0; |
|
wdt_hour++; |
|
if ( wdt_hour >= 24 ) |
|
{ |
|
wdt_hour = 0; |
|
wdt_day++; |
|
} |
|
} |
|
} |
|
|
|
if ( startup_timer > 0 ) |
|
startup_timer--; |
|
if ( display_timer > 0 ) |
|
display_timer--; |
|
if ( sensor_warmup_timer > 0 ) |
|
sensor_warmup_timer--; |
|
if ( sensor_measure_timer > 0 ) |
|
sensor_measure_timer--; |
|
if ( sensor_sample_timer > 0 ) |
|
{ |
|
sensor_sample_timer--; |
|
} |
|
else |
|
{ |
|
sensor_sample_timer = SENSOR_SAMPLE_TIME; |
|
if ( is_sensor_sample_timer_alarm == 0 ) |
|
is_sensor_sample_timer_alarm = 1; |
|
} |
|
|
|
if ( new_history_timer > 0 ) |
|
{ |
|
new_history_timer--; |
|
} |
|
else |
|
{ |
|
new_history_timer = NEW_HISTORY_DELAY; |
|
if ( is_new_history_entry == 0 ) |
|
is_new_history_entry = 1; |
|
} |
|
|
|
|
|
shake_last_cnt = shake_cnt; |
|
shake_cnt = 0; |
|
} |
|
|
|
void enableWDT() |
|
{ |
|
MCUSR = 0; // clear all reset flags including the WDT flag |
|
WDTCSR = B00011000; // enable bit 4 (WDCE) and bit 3 (WDE) to change the prescalar |
|
WDTCSR = B01000110; // Enable watchdog IRQ and set prescaler to 128k --> 1 sec |
|
} |
|
|
|
void reducePower(void) |
|
{ |
|
//ADCSRA = ADCSRA & B01111111; // ADC abschalten, ADEN bit7 zu 0 |
|
ACSR = B10000000; // Analogen Comparator abschalten, ACD bit7 zu 1 |
|
DIDR0 = DIDR0 | B00111111; // Digitale Eingangspuffer ausschalten, analoge Eingangs Pins 0-5 auf 1 |
|
} |
|
|
|
|
|
// argument for attachInterrupt |
|
// This is called if something happens on the tilt switch |
|
|
|
void detectShake(void) |
|
{ |
|
is_shake = 1; // used and cleared in is_display_on_event() |
|
if ( shake_cnt < 255 ) |
|
shake_cnt++; // used and cleared in ISR(WDT_vect) |
|
} |
|
|
|
//=================================================== |
|
|
|
|
|
void setup(void) { |
|
uint8_t i; |
|
reducePower(); |
|
analogReference(INTERNAL); // use the internal 1.1V reference of the ATmega |
|
|
|
Wire.begin(); |
|
|
|
u8g2.begin(); |
|
u8g2.enableUTF8Print(); |
|
|
|
u8g2.setFont(FONT_NARROW); // set the font for the terminal window |
|
u8g2log.begin(u8g2, U8LOG_WIDTH, U8LOG_HEIGHT, u8log_buffer); |
|
u8g2log.setLineHeightOffset(0); // set extra space between lines in pixel, this can be negative |
|
u8g2log.setRedrawMode(0); // 0: Update screen with newline, 1: Update screen for every char |
|
|
|
|
|
u8g2log.print(F("Air Quality\n")); |
|
|
|
for( i = 0; i < HIST_CNT; i++ ) |
|
{ |
|
hist_eco2_max[i] = 0; |
|
hist_eco2_min[i] = 0; |
|
} |
|
|
|
|
|
if (sht.init()) { |
|
u8g2log.print(F("SHT31 found at 0x44\n")); |
|
} else { |
|
u8g2log.print(F("SHT31 not found\n")); |
|
while (1); |
|
} |
|
sht.setAccuracy(SHTSensor::SHT_ACCURACY_HIGH); |
|
|
|
|
|
if (sgp.begin()) { |
|
u8g2log.print(F("SGP30 found\n")); |
|
} else { |
|
u8g2log.print(F("SGP30 not found\n")); |
|
while (1); |
|
} |
|
|
|
u8g2log.print(F("SGP #")); |
|
u8g2log.print(sgp.serialnumber[0], HEX); |
|
u8g2log.print(sgp.serialnumber[1], HEX); |
|
u8g2log.println(sgp.serialnumber[2], HEX); |
|
|
|
|
|
delay(1000); |
|
|
|
// tilt detection at pin 2 |
|
//pinMode(2, INPUT_PULLUP); |
|
//attachInterrupt(digitalPinToInterrupt(2), detectShake, CHANGE); |
|
|
|
// tilt detection at pin 3 |
|
pinMode(3, INPUT_PULLUP); |
|
attachInterrupt(digitalPinToInterrupt(3), detectShake, CHANGE); |
|
|
|
set_sleep_mode(SLEEP_MODE_PWR_DOWN); |
|
|
|
enableWDT(); |
|
} |
|
|
|
|
|
//=================================================== |
|
void readBatteryVoltageLevel(void) |
|
{ |
|
battery_raw = analogRead(0); |
|
battery_glyph = 0; |
|
if ( battery_raw >= BATTERY_LOW ) |
|
{ |
|
battery_glyph = (battery_raw - BATTERY_LOW)/BATTERY_STEP; |
|
if ( battery_glyph > 5 ) |
|
battery_glyph = 5; |
|
} |
|
battery_glyph += 48; |
|
} |
|
|
|
|
|
//=================================================== |
|
|
|
/* |
|
Calculate absolute humidity [mg/m^3] |
|
Args: Temperature [°C], humidity [%RH] |
|
*/ |
|
|
|
uint32_t getAbsoluteHumidity(float temperature, float humidity) |
|
{ |
|
// approximation formula from Sensirion SGP30 Driver Integration chapter 3.15 |
|
const float absoluteHumidity = 216.7f * ((humidity / 100.0f) * 6.112f * exp((17.62f * temperature) / (243.12f + temperature)) / (273.15f + temperature)); // [g/m^3] |
|
const uint32_t absoluteHumidityScaled = static_cast<uint32_t>(1000.0f * absoluteHumidity); // [mg/m^3] |
|
return absoluteHumidityScaled; |
|
} |
|
|
|
|
|
|
|
void startAirQuality(void) |
|
{ |
|
} |
|
|
|
void readAirQuality(void) |
|
{ |
|
uint8_t i; |
|
int32_t tvoc_coeff = 7; // 1..32 |
|
int32_t eco2_coeff = 7; // 1..32 |
|
|
|
|
|
sht.readSample(); |
|
temperature_raw = sht.getTemperature(); |
|
humidity_raw = sht.getHumidity(); |
|
|
|
if ( temperature_raw <= (float)TEMP_LOW ) |
|
{ |
|
temperature = 0; |
|
} |
|
else if ( temperature_raw >= (float)TEMP_HIGH ) |
|
{ |
|
temperature = TEMP_HIGH - TEMP_LOW; |
|
temperature *= 2; |
|
} |
|
else |
|
{ |
|
temperature = (uint8_t)((temperature_raw-(float)TEMP_LOW)*2.0); |
|
} |
|
|
|
humidity = (uint8_t)((humidity_raw)*2.0); |
|
|
|
sgp.setHumidity(getAbsoluteHumidity(temperature_raw, humidity_raw)); |
|
|
|
is_air_quality_available = sgp.IAQmeasure(); |
|
|
|
if ( is_air_quality_available ) |
|
{ |
|
tvoc_raw = sgp.TVOC; |
|
eco2_raw = sgp.eCO2; |
|
} |
|
else |
|
{ |
|
tvoc_raw = 0; |
|
eco2_raw = 400; |
|
} |
|
|
|
if ( is_new_history_entry != 0 ) |
|
{ |
|
add_hist_new(); |
|
is_new_history_entry = 0; |
|
} |
|
else |
|
{ |
|
add_hist_minmax(); |
|
} |
|
|
|
} |
|
|
|
|
|
|
|
uint16_t get_uint8(void *ptr, uint8_t pos) |
|
{ |
|
return ((uint8_t *)ptr)[pos]; |
|
} |
|
|
|
uint16_t get_uint16(void *ptr, uint8_t pos) |
|
{ |
|
return ((uint16_t *)ptr)[pos]; |
|
} |
|
|
|
void draw_temperature(uint16_t x) |
|
{ |
|
uint8_t frac; |
|
frac = x & 1; |
|
x >>= 1; |
|
x += TEMP_LOW; |
|
x = x & 0x0ff; |
|
u8g2.print(x); |
|
u8g2.print('.'); |
|
if ( frac ) |
|
u8g2.print('5'); |
|
else |
|
u8g2.print('0'); |
|
} |
|
|
|
void draw_humidity(uint16_t x) |
|
{ |
|
uint8_t frac; |
|
frac = x & 1; |
|
x >>= 1; |
|
u8g2.print(x); |
|
u8g2.print('.'); |
|
if ( frac ) |
|
u8g2.print('5'); |
|
else |
|
u8g2.print('0'); |
|
} |
|
|
|
void draw_16bit(uint16_t x) |
|
{ |
|
u8g2.print(x); |
|
} |
|
|
|
void draw_graph( uint16_t (*get_val)(void *ptr, uint8_t pos), void *min_array, void *max_array, void (*draw_value)(uint16_t x)) |
|
{ |
|
uint8_t i, ii, x; |
|
uint16_t max = 0; |
|
uint16_t min = 0x0ffff; |
|
uint16_t delta; |
|
uint8_t ymin, ymax; |
|
i = hist_start; |
|
for(;;) |
|
{ |
|
ii = i; |
|
i++; |
|
if ( i >= HIST_CNT ) |
|
i = 0; |
|
if ( i == hist_end ) |
|
break; |
|
if ( min > get_val(min_array, ii) ) |
|
min = get_val(min_array, ii); |
|
if ( max < get_val(max_array, ii) ) |
|
max = get_val(max_array, ii); |
|
} |
|
if ( min > max ) |
|
{ |
|
min = 0; |
|
max = 600; |
|
return; |
|
} |
|
if ( min + 30 >= max ) |
|
max = min + 30; |
|
|
|
if ( max - min < 100 ) |
|
{ |
|
max += 19; |
|
max /= 20; |
|
max *= 20; |
|
|
|
min /= 20; |
|
min *= 20; |
|
} |
|
else |
|
{ |
|
max += 199; |
|
max /= 200; |
|
max *= 200; |
|
|
|
min /= 200; |
|
min *= 200; |
|
} |
|
|
|
delta = max-min; |
|
|
|
i = hist_start; |
|
x = 127-HIST_CNT; |
|
|
|
u8g2.setFont(FONT_NARROW); |
|
u8g2.setDrawColor(1); |
|
|
|
for(;;) |
|
{ |
|
ii = i; |
|
i++; |
|
if ( i >= HIST_CNT ) |
|
i = 0; |
|
if ( i == hist_end ) |
|
break; |
|
ymin = ((unsigned long)(get_val(min_array, ii) - min)*30UL)/delta; |
|
ymax = ((unsigned long)(get_val(max_array, ii) - min)*30UL)/delta; |
|
u8g2.drawVLine(x, 63-ymax, ymax-ymin+1); |
|
x++; |
|
} |
|
u8g2.setCursor(0, 46); |
|
draw_value(max); |
|
u8g2.setCursor(0, 63); |
|
draw_value(min); |
|
} |
|
|
|
//=================================================== |
|
|
|
void draw_1_2_eco2_history(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
|
|
draw_graph( get_uint16, hist_eco2_min, hist_eco2_max, draw_16bit); |
|
} |
|
|
|
void draw_1_4_temperature(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
u8g2.setFont(FONT_MED_NUM); |
|
|
|
u8g2.setCursor(x, 28+y); |
|
draw_temperature(temperature); |
|
|
|
u8g2.setFont(FONT_SMALL); |
|
|
|
u8g2.setCursor(x, 10+y); |
|
u8g2.print(F("°C")); |
|
} |
|
|
|
void draw_1_4_humidity(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
u8g2.setFont(FONT_MED_NUM); |
|
u8g2.setCursor(x, 28+y); |
|
draw_humidity(humidity); |
|
|
|
u8g2.setFont(FONT_SMALL); |
|
u8g2.setCursor(x, 10+y); |
|
u8g2.print(F("%RH")); |
|
} |
|
|
|
void draw_gray_out(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
if ( state == STATE_WARMUP_DISP_ON ) |
|
{ |
|
uint8_t xx, yy; |
|
u8g2.setDrawColor(0); |
|
for( yy = y+14; yy < y+28; yy+=1 ) |
|
{ |
|
for( xx = 0; xx < 52; xx += 2 ) |
|
{ |
|
u8g2.drawPixel(xx+x+(yy&1), yy); |
|
} |
|
} |
|
u8g2.setDrawColor(1); |
|
} |
|
} |
|
|
|
void draw_1_4_eco2(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
u8g2.setFont(FONT_SMALL); |
|
|
|
u8g2.setCursor(x, y+10); |
|
u8g2.print(F("ppm CO")); |
|
u8g2.setCursor(x+40, y+15); |
|
u8g2.print(F("²")); |
|
|
|
u8g2.setFont(FONT_MED_NUM); |
|
|
|
u8g2.setCursor(x, y+28); |
|
u8g2.print(eco2_raw); |
|
|
|
draw_gray_out(x, y); |
|
} |
|
|
|
void draw_1_4_tvoc(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
u8g2.setFont(FONT_SMALL); |
|
u8g2.setCursor(x, y+10); |
|
u8g2.print(F("ppb TVOC")); |
|
|
|
u8g2.setFont(FONT_MED_NUM); |
|
|
|
u8g2.setCursor(x, y+28); |
|
u8g2.print(tvoc_raw); |
|
|
|
draw_gray_out(x, y); |
|
} |
|
|
|
void draw_1_4_base(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
u8g2.setFont(FONT_SMALL); |
|
u8g2.setCursor(x, y+11); |
|
u8g2.print(F("Sec ")); |
|
u8g2.print(wdt_count); |
|
|
|
u8g2.setCursor(x, y+21); |
|
u8g2.print(F("B-C ")); |
|
u8g2.print(eco2_base, HEX); |
|
|
|
u8g2.setCursor(x, y+31); |
|
u8g2.print(F("B-T ")); |
|
u8g2.print(tvoc_base, HEX); |
|
|
|
} |
|
|
|
void draw_1_4_delay(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
u8g2.setFont(FONT_SMALL); |
|
u8g2.setCursor(x, y+11); |
|
u8g2.print(F("Sens ")); |
|
u8g2.print(millis_sensor); |
|
|
|
u8g2.setCursor(x, y+21); |
|
u8g2.print(F("Disp ")); |
|
u8g2.print(millis_display); |
|
|
|
} |
|
|
|
void draw_1_4_uptime(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
u8g2.setFont(FONT_SMALL); |
|
u8g2.setCursor(x, y+11); |
|
u8g2.print(F("Day ")); |
|
u8g2.print(wdt_day); |
|
|
|
u8g2.setCursor(x, y+21); |
|
u8g2.print(F("Hour ")); |
|
u8g2.print(wdt_hour); |
|
|
|
u8g2.setCursor(x, y+31); |
|
u8g2.print(F("Min ")); |
|
u8g2.print(wdt_min); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
void draw_1_4_system(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
u8g2.setFont(FONT_SMALL); |
|
u8g2.setCursor(x, y+11); |
|
u8g2.print(F("Shake ")); |
|
u8g2.print(shake_last_cnt); |
|
|
|
u8g2.setCursor(x, y+21); |
|
u8g2.print(F("State ")); |
|
u8g2.print(state); |
|
|
|
u8g2.setCursor(x, y+31); |
|
u8g2.print(F("Bat ")); |
|
u8g2.print(battery_raw); |
|
|
|
} |
|
|
|
void draw_1_4_battery(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
u8g2.setFont(u8g2_font_battery19_tn); |
|
u8g2.drawGlyph(x, y+20, battery_glyph); |
|
} |
|
|
|
void draw_1_4_emoticon(u8g2_uint_t x, u8g2_uint_t y) |
|
{ |
|
uint8_t tvoc_idx, eco2_idx, emo_idx; |
|
if ( tvoc_raw <= 75 ) |
|
tvoc_idx = 1; |
|
else if ( tvoc_raw <= 150 ) |
|
tvoc_idx = 2; |
|
else if ( tvoc_raw <= 300 ) |
|
tvoc_idx = 3; |
|
else if ( tvoc_raw <= 500 ) |
|
tvoc_idx = 4; |
|
else if ( tvoc_raw <= 1000 ) |
|
tvoc_idx = 5; |
|
else if ( tvoc_raw <= 1500 ) |
|
tvoc_idx = 6; |
|
else if ( tvoc_raw <= 3000 ) |
|
tvoc_idx = 7; |
|
else if ( tvoc_raw <= 5000 ) |
|
tvoc_idx = 8; |
|
else |
|
tvoc_idx = 9; |
|
|
|
if ( eco2_raw <= 500 ) |
|
eco2_idx = 1; |
|
else if ( eco2_raw <= 600 ) |
|
eco2_idx = 2; |
|
else if ( eco2_raw <= 800 ) |
|
eco2_idx = 3; |
|
else if ( eco2_raw <= 1000 ) |
|
eco2_idx = 4; |
|
else if ( eco2_raw <= 1200 ) |
|
eco2_idx = 5; |
|
else if ( eco2_raw <= 1400 ) |
|
eco2_idx = 6; |
|
else if ( eco2_raw <= 5000 ) |
|
eco2_idx = 7; |
|
else if ( eco2_raw <= 10000 ) |
|
eco2_idx = 8; |
|
else |
|
eco2_idx = 9; |
|
|
|
emo_idx = tvoc_idx; |
|
if ( emo_idx < eco2_idx ) |
|
emo_idx = eco2_idx; |
|
u8g2.setFont(u8g2_font_emoticons21_tr); |
|
u8g2.drawGlyph(x+20, y+21, 32+emo_idx); |
|
|
|
|
|
} |
|
|
|
//=================================================== |
|
|
|
void draw_all_numbers(void) |
|
{ |
|
u8g2.setFontMode(1); |
|
u8g2.firstPage(); |
|
do { |
|
u8g2.drawHLine(0, 32, 128); |
|
u8g2.drawVLine(62, 0, 64); |
|
|
|
|
|
draw_1_4_temperature(1, 0); |
|
draw_1_4_humidity(66, 0); |
|
|
|
if ( is_air_quality_available ) |
|
{ |
|
draw_1_4_eco2(1, 32+3); |
|
draw_1_4_tvoc(66, 32+3); |
|
} |
|
|
|
} while ( u8g2.nextPage() ); |
|
} |
|
|
|
|
|
void draw_with_emo(void) |
|
{ |
|
u8g2.setFontMode(1); |
|
u8g2.firstPage(); |
|
do { |
|
//u8g2.drawHLine(0, 32, 128); |
|
//u8g2.drawVLine(62, 0, 64); |
|
|
|
u8g2.drawHLine(0, 32, 128); |
|
u8g2.drawVLine(62-14, 0, 32); |
|
u8g2.drawVLine(98, 0, 32); |
|
u8g2.drawVLine(62-8, 32, 32); |
|
u8g2.drawVLine(128-12, 32, 32); |
|
|
|
|
|
draw_1_4_temperature(1, 0); |
|
draw_1_4_humidity(66-14, 0); |
|
draw_1_4_emoticon(127-20-21, 4); |
|
//draw_1_4_wdt_count(66, 0); |
|
|
|
if ( is_air_quality_available ) |
|
{ |
|
draw_1_4_eco2(1, 32+3); |
|
//draw_1_4_emoticon(66, 32+6); |
|
draw_1_4_tvoc(66-8, 32+3); |
|
} |
|
|
|
draw_1_4_battery(128-8, 32+7); |
|
} while ( u8g2.nextPage() ); |
|
} |
|
|
|
void draw_system(void) |
|
{ |
|
u8g2.setFontMode(1); |
|
u8g2.firstPage(); |
|
do { |
|
u8g2.drawHLine(0, 32, 128); |
|
u8g2.drawVLine(62, 0, 64); |
|
|
|
|
|
draw_1_4_uptime(1, 0); |
|
//draw_1_4_delay(1,0); |
|
//draw_1_4_temperature(1, 0); |
|
draw_1_4_eco2(66, 0); |
|
//draw_1_4_humidity(66, 0); |
|
//draw_1_4_wdt_count(66, 0); |
|
|
|
draw_1_4_base(0, 32); |
|
draw_1_4_system(66, 32); |
|
|
|
} while ( u8g2.nextPage() ); |
|
} |
|
|
|
|
|
void draw_with_history(void) |
|
{ |
|
u8g2.setFontMode(1); |
|
u8g2.firstPage(); |
|
do { |
|
u8g2.drawHLine(0, 32, 128); |
|
u8g2.drawVLine(62-20, 0, 32); |
|
u8g2.drawVLine(98, 0, 32); |
|
|
|
|
|
draw_1_4_temperature(1, 0); |
|
if ( is_air_quality_available ) |
|
{ |
|
draw_1_4_eco2(66-20, 0); |
|
draw_1_4_emoticon(127-20-21, 4); |
|
} |
|
|
|
draw_1_2_eco2_history(0, 32); |
|
|
|
|
|
|
|
} while ( u8g2.nextPage() ); |
|
} |
|
|
|
//=================================================== |
|
|
|
|
|
uint8_t is_display_on_event(void) |
|
{ |
|
if ( is_shake > 0 ) |
|
{ |
|
is_shake = 0; |
|
return 1; |
|
} |
|
if ( shake_last_cnt >= 1 ) |
|
return 1; |
|
return 0; |
|
} |
|
|
|
void handle_new_display_page(void) |
|
{ |
|
if ( new_display_cool_down_timer > 0 ) |
|
{ |
|
new_display_cool_down_timer--; |
|
} |
|
else |
|
{ |
|
if ( shake_last_cnt > NEW_DISPLAY_SHAKE_THRESHOLD ) |
|
{ |
|
new_display_cool_down_timer = NEW_DISPLAY_COOL_DOWN; |
|
current_display_page++; |
|
if ( current_display_page >= DISPLAY_PAGE_CNT ) |
|
current_display_page = 0; |
|
} |
|
} |
|
} |
|
|
|
void disable_display(void) |
|
{ |
|
u8g2.clear(); |
|
u8g2.setPowerSave(1); |
|
is_display_enabled = 0; |
|
} |
|
|
|
void enable_display(void) |
|
{ |
|
is_display_enabled = 1; |
|
u8g2.setPowerSave(0); |
|
} |
|
|
|
void disable_sensors(void) |
|
{ |
|
/* both sensors will go to idle/sleep mode when the I2C soft reset is sent */ |
|
Wire.beginTransmission(0); |
|
Wire.write(6); |
|
Wire.endTransmission(true); // true: send full stop on I2C |
|
|
|
delay(5); |
|
|
|
} |
|
|
|
void enable_sensors(void) |
|
{ |
|
sht.init(); |
|
sgp.IAQinit(); |
|
delay(5); |
|
} |
|
|
|
//=================================================== |
|
|
|
|
|
void next_state(void) |
|
{ |
|
uint32_t start; |
|
switch(state) |
|
{ |
|
case STATE_RESET: |
|
enable_display(); |
|
startup_timer = STARTUP_TIME; |
|
state = STATE_STARTUP_DISP_ON; |
|
display_timer = DISPLAY_TIME; |
|
break; |
|
|
|
// - - - Start Up - - - |
|
|
|
case STATE_STARTUP_DISP_ON: |
|
start = millis(); |
|
sgp.getIAQBaseline(&eco2_base, &tvoc_base); // always store the calibration values during measure |
|
readAirQuality(); |
|
millis_sensor = millis() - start; |
|
|
|
if ( is_display_on_event() ) |
|
{ |
|
// display is already enabled, but the timer is reseted |
|
display_timer = DISPLAY_TIME; |
|
} |
|
|
|
if ( display_timer == 0 ) |
|
{ |
|
disable_display(); |
|
state = STATE_STARTUP_DISP_OFF; |
|
} |
|
else if ( startup_timer == 0 ) |
|
{ |
|
sensor_measure_timer = SENSOR_MEASURE_TIME; |
|
state = STATE_MEASURE_DISP_ON; |
|
} |
|
break; |
|
|
|
case STATE_STARTUP_DISP_OFF: |
|
start = millis(); |
|
sgp.getIAQBaseline(&eco2_base, &tvoc_base); // always store the calibration values during measure |
|
readAirQuality(); |
|
millis_sensor = millis() - start; |
|
|
|
if ( is_display_on_event() ) |
|
{ |
|
enable_display(); |
|
display_timer = DISPLAY_TIME; |
|
new_display_cool_down_timer = NEW_DISPLAY_COOL_DOWN; // ensure, that the display page is visible for a while |
|
state = STATE_STARTUP_DISP_ON; |
|
} |
|
else if ( startup_timer == 0 ) |
|
{ |
|
sensor_measure_timer = SENSOR_MEASURE_TIME; |
|
state = STATE_MEASURE_DISP_OFF; |
|
} |
|
break; |
|
|
|
// - - - Sensor Warm Up - - - |
|
|
|
case STATE_WARMUP_DISP_ON: // DONE |
|
if ( is_display_on_event() ) |
|
{ |
|
// display is already enabled, but the timer is reseted |
|
display_timer = DISPLAY_TIME; |
|
} |
|
|
|
if ( display_timer == 0 ) |
|
{ |
|
disable_display(); |
|
state = STATE_WARMUP_DISP_OFF; |
|
} |
|
else if ( sensor_warmup_timer == 0 ) |
|
{ |
|
// set the baseline at the end of the warmup |
|
// this is described in the "SGP30 Driver Integration" |
|
if ( eco2_base != 0 ) |
|
sgp.setIAQBaseline(eco2_base, tvoc_base); // Restore the baseline values |
|
|
|
sensor_measure_timer = SENSOR_MEASURE_TIME; |
|
state = STATE_MEASURE_DISP_ON; |
|
} |
|
break; |
|
|
|
case STATE_WARMUP_DISP_OFF: // DONE |
|
if ( is_display_on_event() ) |
|
{ |
|
enable_display(); |
|
display_timer = DISPLAY_TIME; |
|
new_display_cool_down_timer = NEW_DISPLAY_COOL_DOWN; // ensure, that the display page is visible for a while |
|
state = STATE_WARMUP_DISP_ON; |
|
} |
|
else if ( sensor_warmup_timer == 0 ) |
|
{ |
|
// set the baseline at the end of the warmup |
|
// this is described in the "SGP30 Driver Integration" |
|
if ( eco2_base != 0 ) |
|
sgp.setIAQBaseline(eco2_base, tvoc_base); // Restore the baseline values |
|
|
|
sensor_measure_timer = SENSOR_MEASURE_TIME; |
|
state = STATE_MEASURE_DISP_OFF; |
|
} |
|
break; |
|
|
|
// - - - Sensor Measure - - - |
|
|
|
case STATE_MEASURE_DISP_ON: // DONE |
|
start = millis(); |
|
sgp.getIAQBaseline(&eco2_base, &tvoc_base); // always store the calibration values during measure |
|
readAirQuality(); |
|
millis_sensor = millis() - start; |
|
|
|
if ( is_display_on_event() ) |
|
{ |
|
// display is already enabled, but the timer is reseted |
|
display_timer = DISPLAY_TIME; |
|
} |
|
|
|
if ( display_timer == 0 ) |
|
{ |
|
disable_display(); |
|
state = STATE_MEASURE_DISP_OFF; |
|
} |
|
else if ( sensor_measure_timer == 0 ) |
|
{ |
|
// ignored: Measurement is continued until the display goes off. |
|
} |
|
break; |
|
|
|
case STATE_MEASURE_DISP_OFF: // DONE |
|
start = millis(); |
|
sgp.getIAQBaseline(&eco2_base, &tvoc_base); // always store the calibration values during measure |
|
readAirQuality(); |
|
millis_sensor = millis() - start; |
|
|
|
if ( is_display_on_event() ) |
|
{ |
|
enable_display(); |
|
display_timer = DISPLAY_TIME; |
|
new_display_cool_down_timer = NEW_DISPLAY_COOL_DOWN; // ensure, that the display page is visible for a while |
|
state = STATE_MEASURE_DISP_ON; |
|
} |
|
else if ( sensor_measure_timer == 0 ) |
|
{ |
|
disable_sensors(); |
|
state = STATE_SENSOR_SLEEP_DISP_OFF; |
|
} |
|
break; |
|
|
|
// - - - Sensor Sleep - - - |
|
|
|
case STATE_SENSOR_SLEEP_DISP_OFF: // DONE |
|
if ( is_display_on_event() ) |
|
{ |
|
enable_display(); |
|
display_timer = DISPLAY_TIME; |
|
new_display_cool_down_timer = NEW_DISPLAY_COOL_DOWN; // ensure, that the display page is visible for a while |
|
|
|
enable_sensors(); |
|
|
|
sensor_warmup_timer = SENSOR_WARMUP_TIME; |
|
state = STATE_WARMUP_DISP_ON; |
|
} |
|
else if ( is_sensor_sample_timer_alarm != 0 ) |
|
{ |
|
is_sensor_sample_timer_alarm = 0; |
|
|
|
enable_sensors(); |
|
|
|
sensor_warmup_timer = SENSOR_WARMUP_TIME; |
|
state = STATE_WARMUP_DISP_OFF; |
|
} |
|
break; |
|
|
|
default: |
|
state = STATE_RESET; |
|
break; |
|
} |
|
} |
|
|
|
|
|
|
|
//=================================================== |
|
|
|
|
|
|
|
void loop(void) { |
|
uint8_t i; |
|
u8g2_uint_t g; |
|
uint32_t start; |
|
|
|
if ( is_wdt_irq ) |
|
{ |
|
is_wdt_irq = 0; |
|
|
|
next_state(); |
|
readBatteryVoltageLevel(); |
|
handle_new_display_page(); |
|
|
|
if ( is_display_enabled ) // "is_display_enabled" will be calculated in next_state() |
|
{ |
|
start = millis(); |
|
if ( current_display_page == 0 ) |
|
draw_with_emo(); |
|
else if ( current_display_page == 1 ) |
|
draw_with_history(); |
|
else if ( current_display_page == 2 ) |
|
draw_system(); |
|
millis_display = millis() - start; |
|
} |
|
else |
|
{ |
|
millis_display = 0; |
|
} |
|
} |
|
|
|
set_sleep_mode(SLEEP_MODE_PWR_DOWN); |
|
//noInterrupts(); |
|
sleep_enable(); |
|
//interrupts(); |
|
sleep_mode(); |
|
sleep_disable(); |
|
}
|
|
|