AirQuality.ino 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361
  1. /*
  2. AirQuality.ino
  3. ATMEGA328P (Arduino UNO/Pro Trinket) only
  4. Universal 8bit Graphics Library (https://github.com/olikraus/u8g2/)
  5. Copyright (c) 2018, olikraus@gmail.com
  6. All rights reserved.
  7. Redistribution and use in source and binary forms, with or without modification,
  8. are permitted provided that the following conditions are met:
  9. * Redistributions of source code must retain the above copyright notice, this list
  10. of conditions and the following disclaimer.
  11. * Redistributions in binary form must reproduce the above copyright notice, this
  12. list of conditions and the following disclaimer in the documentation and/or other
  13. materials provided with the distribution.
  14. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  15. CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  16. INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  17. MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  18. DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
  19. CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  20. SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  21. NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  23. CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  24. STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  25. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  26. ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27. http://shelvin.de/arduino-in-den-sleep_mode_pwr_down-schlaf-modus-setzen/
  28. battery symbol
  29. trinket 3.3V requires at least 3.5V (https://learn.adafruit.com/introducing-pro-trinket/pinouts)
  30. Assuming 3x AAA and a fresh cell with 1.5V: --> 4.5V
  31. There are 6 battery symbols
  32. symbol 0: < 3.5V
  33. symbol 1: 3.5 - 3.7
  34. symbol 2: 3.7 - 3.9
  35. symbol 3: 3.9 - 4.1
  36. symbol 4: 4.1 - 4-5
  37. symbol 5: >= 4.5V
  38. 1V difference --> 5 steps --> 1V / 5 --> 0.2V step
  39. R1 = 470K
  40. R2 = 100K
  41. Rsum = 570K
  42. analogRead will use the internal 1.1V reference signal
  43. Um = 100K * 4.5V / 570K = 0.7894 V --> *1024/1.1 --> 735 (analogRead)
  44. Um = 100K * 3.5V / 570K = 0.6140 V --> 571 (analogRead)
  45. 735 - 571 = 164 --> 33
  46. analogRead*1.1/1024 = Um, Um = 100 * Ubat/570
  47. analogRead*1.1/1024 = 10 * Ubat/57
  48. analogRead*1.1*57/10240 = Ubat
  49. Ubat = analogRead*1.1*57/10240 = analogRead*0.006123 = analogRead/163
  50. */
  51. #include <Arduino.h>
  52. #include <Wire.h>
  53. #include <U8g2lib.h>
  54. #include <Adafruit_SGP30.h>
  55. #include <SHTSensor.h> // adafruit sht31 lib v1.0.0 has a wrong 500 milliseconds delay, so use the SHTSensor.h lib
  56. #include <avr/sleep.h>
  57. //===================================================
  58. // Constants: Timing values for the sensor & display state machine
  59. // all values are "seconds"
  60. // define startup calibration time: 2h
  61. //#define STARTUP_TIME (60*60*2)
  62. // five minutes
  63. #define STARTUP_TIME (60*5)
  64. // define duration after which the display is disabled: 30 seconds
  65. #define DISPLAY_TIME ((30))
  66. // Datasheet, page 8:
  67. // For the first 15s after the “Init_air_quality” command the sensor
  68. // is in an initialization phase during which a “Measure_air_quality”
  69. // command returns fixed values
  70. // --> sensor warmup time
  71. #define SENSOR_WARMUP_TIME ((16))
  72. // SENSOR_MEASURE_TIME defines the duration how long the measurement
  73. // shell be aktive (after SENSOR_WARMUP_TIME)
  74. #define SENSOR_MEASURE_TIME (14)
  75. // This is the time, after which the gas sensor should do another measurement.
  76. // The sum of SENSOR_WARMUP_TIME and SENSOR_MEASURE_TIME must
  77. // be lesser than SENSOR_SAMPLE_TIME
  78. // every 7 minutes, so there are at least two measures per history entry
  79. #define SENSOR_SAMPLE_TIME (7*60)
  80. // This is the time after which a new history entry is generated
  81. // There are 96 entries in the history table (HIST_CNT)
  82. // 15*60 = 15 minutes --> 96 * 15 minutes --> 24h
  83. #define NEW_HISTORY_DELAY (15*60)
  84. //#define NEW_HISTORY_DELAY 2
  85. // changing the content of the display is done via "shakes"
  86. // shakes are detected by a tilt switsch.
  87. // This value defines the number of shake events to change the display page.
  88. #define NEW_DISPLAY_SHAKE_THRESHOLD 7
  89. // number of seconds, for which a new display page is fixed
  90. // this means, for this duration, the user can not change the display page
  91. #define NEW_DISPLAY_COOL_DOWN 3
  92. // Battery empty value
  93. // U = 100K * 3.5V / 570K = 0.6140 V --> 571 (analogRead)
  94. #define BATTERY_LOW 571
  95. // step width for each battery symbol
  96. #define BATTERY_STEP 33
  97. U8G2_SSD1306_128X64_NONAME_2_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
  98. SHTSensor sht; // temperature & humidity sensor
  99. Adafruit_SGP30 sgp; // air quality sensor
  100. U8G2LOG u8g2log;
  101. // setup the terminal (U8G2LOG) and connect to u8g2 for automatic refresh of the display
  102. // The size (width * height) depends on the selected font and the display
  103. #define U8LOG_WIDTH 24
  104. #define U8LOG_HEIGHT 5
  105. uint8_t u8log_buffer[U8LOG_WIDTH*U8LOG_HEIGHT];
  106. //===================================================
  107. // Constants: Font definitions for U8g2
  108. #define FONT_NARROW u8g2_font_mercutio_basic_nbp_tr
  109. #define FONT_SMALL u8g2_font_helvB08_tf
  110. #define FONT_MED_NUM u8g2_font_helvB14_tn
  111. //#define FONT_BIG u8g2_font_inr49_mn
  112. #define FONT_BIG u8g2_font_logisoso38_tn
  113. //===================================================
  114. // Constants: State values for the sensor & display coordination
  115. #define STATE_RESET 0
  116. #define STATE_STARTUP_DISP_ON 1
  117. #define STATE_STARTUP_DISP_OFF 2
  118. #define STATE_WARMUP_DISP_ON 11
  119. #define STATE_WARMUP_DISP_OFF 12
  120. #define STATE_MEASURE_DISP_ON 21
  121. #define STATE_MEASURE_DISP_OFF 22
  122. #define STATE_SENSOR_SLEEP_DISP_OFF 32
  123. //===================================================
  124. // Constants: Temperature boundaries
  125. #define TEMP_LOW -20
  126. #define TEMP_HIGH (TEMP_LOW+120)
  127. //===================================================
  128. // Constants: Number of different display pages
  129. #define DISPLAY_PAGE_CNT 3
  130. //===================================================
  131. // Constants: History
  132. #define HIST_CNT 96
  133. // history sample time: number of seconds between each history entry
  134. // --> see NEW_HISTORY_DELAY
  135. // 15 min = 15*60 seconds: 96 entries for 24h
  136. //===================================================
  137. // State variable for the sensor & display coordination
  138. uint8_t state = STATE_RESET; // assign STATE_xxx constants
  139. uint8_t is_display_enabled = 0; // modified by enable_display() and disable_display()
  140. //===================================================
  141. // Variables: Air quality sensor related varables
  142. uint8_t is_ethanol_read = 0;
  143. float temperature_raw;
  144. float humidity_raw;
  145. uint8_t temperature; /* with offset and multiplied by 2 */
  146. uint8_t humidity; /* *2 */
  147. uint16_t tvoc_raw;
  148. uint16_t tvoc_lp;
  149. uint16_t eco2_raw;
  150. uint16_t eco2_lp;
  151. uint16_t ethanol_raw = 0;
  152. int is_air_quality_available = 0;
  153. // Calibration values, values seem to be 0x8a27 and 0x08a98 for my sensor, so 0 will be used as not set
  154. uint16_t eco2_base = 0; // calibration value eCO2
  155. uint16_t tvoc_base = 0; // calibration value TVOC
  156. //===================================================
  157. uint16_t battery_raw;
  158. uint16_t battery_glyph;
  159. //===================================================
  160. // Variables: Timer for the state machine
  161. volatile uint32_t wdt_count = 0;
  162. volatile uint8_t wdt_sec = 0;
  163. volatile uint8_t wdt_min = 0;
  164. volatile uint8_t wdt_hour = 0;
  165. volatile uint16_t wdt_day = 0;
  166. volatile uint16_t startup_timer = 0;
  167. volatile uint16_t sensor_sample_timer = SENSOR_SAMPLE_TIME;
  168. volatile uint8_t is_sensor_sample_timer_alarm = 0; // if zero: wake up gas sensor
  169. volatile uint8_t display_timer = 0;
  170. volatile uint8_t sensor_warmup_timer = 0;
  171. volatile uint8_t sensor_measure_timer = 0;
  172. volatile uint8_t new_display_cool_down_timer = 0;
  173. volatile uint8_t is_wdt_irq = 0;
  174. volatile uint8_t is_new_history_entry = 0;
  175. volatile uint16_t new_history_timer = NEW_HISTORY_DELAY;
  176. //volatile uint16_t battery_level = 0; // in millivolt
  177. uint32_t millis_sensor; // Debugging: Duration of the sensour measurement
  178. uint32_t millis_display; // Debugging: Duration of the display refresh
  179. //===================================================
  180. // Variables: Shake detection
  181. volatile uint8_t is_shake = 0;
  182. volatile uint8_t shake_cnt = 0;
  183. volatile uint8_t shake_last_cnt = 0;
  184. //===================================================
  185. // Variables: Current visible display page
  186. uint8_t current_display_page = 0; // 0 .. DISPLAY_PAGE_CNT - 1
  187. //===================================================
  188. // Variables: History management
  189. uint8_t hist_start = 0;
  190. uint8_t hist_end = 1;
  191. uint8_t hist_last = 0;
  192. uint16_t hist_eco2_max[HIST_CNT];
  193. uint16_t hist_eco2_min[HIST_CNT];
  194. //uint8_t hist_temp_max[HIST_CNT];
  195. //uint8_t hist_temp_min[HIST_CNT];
  196. //uint8_t hist_rh_max[HIST_CNT];
  197. //uint8_t hist_rh_min[HIST_CNT];
  198. //===================================================
  199. void hist_append(void)
  200. {
  201. hist_last = hist_end;
  202. if ( hist_end == hist_start )
  203. { // history is full
  204. hist_start++;
  205. if ( hist_start >= HIST_CNT )
  206. hist_start = 0;
  207. }
  208. hist_end++;
  209. if ( hist_end >= HIST_CNT )
  210. {
  211. hist_end = 0;
  212. }
  213. }
  214. uint16_t maximum(uint16_t a, uint16_t b)
  215. {
  216. if ( a < b )
  217. return b;
  218. return a;
  219. }
  220. uint16_t minimum(uint16_t a, uint16_t b)
  221. {
  222. if ( a > b )
  223. return b;
  224. return a;
  225. }
  226. /* append current values to last history entry */
  227. void add_hist_minmax(void)
  228. {
  229. hist_eco2_max[hist_last] = maximum(hist_eco2_max[hist_last], eco2_raw);
  230. hist_eco2_min[hist_last] = minimum(hist_eco2_min[hist_last], eco2_raw);
  231. //hist_temp_max[hist_last] = maximum(hist_temp_max[hist_last], temperature);
  232. //hist_temp_min[hist_last] = minimum(hist_temp_min[hist_last], temperature);
  233. //hist_rh_max[hist_last] = maximum(hist_rh_max[hist_last], humidity);
  234. //hist_rh_min[hist_last] = minimum(hist_rh_min[hist_last], humidity);
  235. }
  236. void add_hist_new(void)
  237. {
  238. hist_append();
  239. hist_eco2_max[hist_last] = eco2_raw;
  240. hist_eco2_min[hist_last] = eco2_raw;
  241. //hist_temp_max[hist_last] = temperature;
  242. //hist_temp_min[hist_last] = temperature;
  243. //hist_rh_max[hist_last] = humidity;
  244. //hist_rh_min[hist_last] = humidity;
  245. }
  246. //===================================================
  247. ISR(WDT_vect)
  248. {
  249. is_wdt_irq = 1;
  250. wdt_count++;
  251. wdt_sec++;
  252. if ( wdt_sec >= 60 )
  253. {
  254. wdt_sec = 0;
  255. wdt_min++;
  256. if ( wdt_min >= 60 )
  257. {
  258. wdt_min = 0;
  259. wdt_hour++;
  260. if ( wdt_hour >= 24 )
  261. {
  262. wdt_hour = 0;
  263. wdt_day++;
  264. }
  265. }
  266. }
  267. if ( startup_timer > 0 )
  268. startup_timer--;
  269. if ( display_timer > 0 )
  270. display_timer--;
  271. if ( sensor_warmup_timer > 0 )
  272. sensor_warmup_timer--;
  273. if ( sensor_measure_timer > 0 )
  274. sensor_measure_timer--;
  275. if ( sensor_sample_timer > 0 )
  276. {
  277. sensor_sample_timer--;
  278. }
  279. else
  280. {
  281. sensor_sample_timer = SENSOR_SAMPLE_TIME;
  282. if ( is_sensor_sample_timer_alarm == 0 )
  283. is_sensor_sample_timer_alarm = 1;
  284. }
  285. if ( new_history_timer > 0 )
  286. {
  287. new_history_timer--;
  288. }
  289. else
  290. {
  291. new_history_timer = NEW_HISTORY_DELAY;
  292. if ( is_new_history_entry == 0 )
  293. is_new_history_entry = 1;
  294. }
  295. shake_last_cnt = shake_cnt;
  296. shake_cnt = 0;
  297. }
  298. void enableWDT()
  299. {
  300. MCUSR = 0; // clear all reset flags including the WDT flag
  301. WDTCSR = B00011000; // enable bit 4 (WDCE) and bit 3 (WDE) to change the prescalar
  302. WDTCSR = B01000110; // Enable watchdog IRQ and set prescaler to 128k --> 1 sec
  303. }
  304. void reducePower(void)
  305. {
  306. //ADCSRA = ADCSRA & B01111111; // ADC abschalten, ADEN bit7 zu 0
  307. ACSR = B10000000; // Analogen Comparator abschalten, ACD bit7 zu 1
  308. DIDR0 = DIDR0 | B00111111; // Digitale Eingangspuffer ausschalten, analoge Eingangs Pins 0-5 auf 1
  309. }
  310. // argument for attachInterrupt
  311. // This is called if something happens on the tilt switch
  312. void detectShake(void)
  313. {
  314. is_shake = 1; // used and cleared in is_display_on_event()
  315. if ( shake_cnt < 255 )
  316. shake_cnt++; // used and cleared in ISR(WDT_vect)
  317. }
  318. //===================================================
  319. void setup(void) {
  320. uint8_t i;
  321. reducePower();
  322. analogReference(INTERNAL); // use the internal 1.1V reference of the ATmega
  323. Wire.begin();
  324. u8g2.begin();
  325. u8g2.enableUTF8Print();
  326. u8g2.setFont(FONT_NARROW); // set the font for the terminal window
  327. u8g2log.begin(u8g2, U8LOG_WIDTH, U8LOG_HEIGHT, u8log_buffer);
  328. u8g2log.setLineHeightOffset(0); // set extra space between lines in pixel, this can be negative
  329. u8g2log.setRedrawMode(0); // 0: Update screen with newline, 1: Update screen for every char
  330. u8g2log.print(F("Air Quality\n"));
  331. for( i = 0; i < HIST_CNT; i++ )
  332. {
  333. hist_eco2_max[i] = 0;
  334. hist_eco2_min[i] = 0;
  335. }
  336. if (sht.init()) {
  337. u8g2log.print(F("SHT31 found at 0x44\n"));
  338. } else {
  339. u8g2log.print(F("SHT31 not found\n"));
  340. while (1);
  341. }
  342. sht.setAccuracy(SHTSensor::SHT_ACCURACY_HIGH);
  343. if (sgp.begin()) {
  344. u8g2log.print(F("SGP30 found\n"));
  345. } else {
  346. u8g2log.print(F("SGP30 not found\n"));
  347. while (1);
  348. }
  349. u8g2log.print(F("SGP #"));
  350. u8g2log.print(sgp.serialnumber[0], HEX);
  351. u8g2log.print(sgp.serialnumber[1], HEX);
  352. u8g2log.println(sgp.serialnumber[2], HEX);
  353. delay(1000);
  354. // tilt detection at pin 2
  355. //pinMode(2, INPUT_PULLUP);
  356. //attachInterrupt(digitalPinToInterrupt(2), detectShake, CHANGE);
  357. // tilt detection at pin 3
  358. pinMode(3, INPUT_PULLUP);
  359. attachInterrupt(digitalPinToInterrupt(3), detectShake, CHANGE);
  360. set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  361. enableWDT();
  362. }
  363. //===================================================
  364. // read ethanol value from SGP30
  365. // bypass Adafruit lib, because this command is not included
  366. // not used at the moment
  367. uint16_t readEthanol(void)
  368. {
  369. uint8_t buf[6];
  370. uint8_t i;
  371. Wire.beginTransmission(0x58);
  372. Wire.write(0x20); // 0x2050: measure signals command
  373. Wire.write(0x50);
  374. Wire.endTransmission();
  375. delay(200); // max 200ms according to the datasheet
  376. if (Wire.requestFrom(0x58, 6) != 6)
  377. return 0;
  378. for (i=0; i<6; i++)
  379. buf[i] = Wire.read();
  380. // each data value has: msb, lsb and crc
  381. // ethanol value is the second data returned
  382. // crc is ignored
  383. return (((uint16_t)buf[3])<<8) + (uint16_t)buf[4];
  384. }
  385. //===================================================
  386. void readBatteryVoltageLevel(void)
  387. {
  388. battery_raw = analogRead(0);
  389. battery_glyph = 0;
  390. if ( battery_raw >= BATTERY_LOW )
  391. {
  392. battery_glyph = (battery_raw - BATTERY_LOW)/BATTERY_STEP;
  393. if ( battery_glyph > 5 )
  394. battery_glyph = 5;
  395. }
  396. battery_glyph += 48;
  397. }
  398. //===================================================
  399. /*
  400. Calculate absolute humidity [mg/m^3]
  401. Args: Temperature [°C], humidity [%RH]
  402. */
  403. uint32_t getAbsoluteHumidity(float temperature, float humidity)
  404. {
  405. // approximation formula from Sensirion SGP30 Driver Integration chapter 3.15
  406. const float absoluteHumidity = 216.7f * ((humidity / 100.0f) * 6.112f * exp((17.62f * temperature) / (243.12f + temperature)) / (273.15f + temperature)); // [g/m^3]
  407. const uint32_t absoluteHumidityScaled = static_cast<uint32_t>(1000.0f * absoluteHumidity); // [mg/m^3]
  408. return absoluteHumidityScaled;
  409. }
  410. void startAirQuality(void)
  411. {
  412. }
  413. void readAirQuality(void)
  414. {
  415. if ( is_ethanol_read == 0 )
  416. {
  417. // normal air quality read
  418. sht.readSample();
  419. temperature_raw = sht.getTemperature();
  420. humidity_raw = sht.getHumidity();
  421. if ( temperature_raw <= (float)TEMP_LOW )
  422. {
  423. temperature = 0;
  424. }
  425. else if ( temperature_raw >= (float)TEMP_HIGH )
  426. {
  427. temperature = TEMP_HIGH - TEMP_LOW;
  428. temperature *= 2;
  429. }
  430. else
  431. {
  432. temperature = (uint8_t)((temperature_raw-(float)TEMP_LOW)*2.0);
  433. }
  434. humidity = (uint8_t)((humidity_raw)*2.0);
  435. sgp.setHumidity(getAbsoluteHumidity(temperature_raw, humidity_raw));
  436. is_air_quality_available = sgp.IAQmeasure();
  437. if ( is_air_quality_available )
  438. {
  439. tvoc_raw = sgp.TVOC;
  440. eco2_raw = sgp.eCO2;
  441. }
  442. else
  443. {
  444. tvoc_raw = 0;
  445. eco2_raw = 400;
  446. }
  447. if ( is_new_history_entry != 0 )
  448. {
  449. add_hist_new();
  450. is_new_history_entry = 0;
  451. }
  452. else
  453. {
  454. add_hist_minmax();
  455. }
  456. // ethanol should be read only, if the display is active:
  457. /* not used
  458. if ( is_display_enabled )
  459. {
  460. is_ethanol_read = 1;
  461. }
  462. */
  463. }
  464. else
  465. {
  466. // special ethanol read
  467. //ethanol_raw = readEthanol();
  468. // according to the datasheet, the baseline values are corrupted... so restore them if available
  469. //if ( eco2_base != 0 )
  470. // sgp.setIAQBaseline(eco2_base, tvoc_base); // Restore the baseline values
  471. is_ethanol_read = 0; // next: read normal air quality values
  472. }
  473. }
  474. uint16_t get_uint8(void *ptr, uint8_t pos)
  475. {
  476. return ((uint8_t *)ptr)[pos];
  477. }
  478. uint16_t get_uint16(void *ptr, uint8_t pos)
  479. {
  480. return ((uint16_t *)ptr)[pos];
  481. }
  482. void draw_temperature(uint16_t x)
  483. {
  484. uint8_t frac;
  485. frac = x & 1;
  486. x >>= 1;
  487. x += TEMP_LOW;
  488. x = x & 0x0ff;
  489. u8g2.print(x);
  490. u8g2.print('.');
  491. if ( frac )
  492. u8g2.print('5');
  493. else
  494. u8g2.print('0');
  495. }
  496. void draw_humidity(uint16_t x)
  497. {
  498. uint8_t frac;
  499. frac = x & 1;
  500. x >>= 1;
  501. u8g2.print(x);
  502. u8g2.print('.');
  503. if ( frac )
  504. u8g2.print('5');
  505. else
  506. u8g2.print('0');
  507. }
  508. void draw_16bit(uint16_t x)
  509. {
  510. u8g2.print(x);
  511. }
  512. void draw_graph( uint16_t (*get_val)(void *ptr, uint8_t pos), void *min_array, void *max_array, void (*draw_value)(uint16_t x))
  513. {
  514. uint8_t i, ii, x;
  515. uint16_t max = 0;
  516. uint16_t min = 0x0ffff;
  517. uint16_t delta;
  518. uint8_t ymin, ymax;
  519. i = hist_start;
  520. for(;;)
  521. {
  522. ii = i;
  523. i++;
  524. if ( i >= HIST_CNT )
  525. i = 0;
  526. if ( i == hist_end )
  527. break;
  528. if ( min > get_val(min_array, ii) )
  529. min = get_val(min_array, ii);
  530. if ( max < get_val(max_array, ii) )
  531. max = get_val(max_array, ii);
  532. }
  533. if ( min > max )
  534. {
  535. min = 0;
  536. max = 600;
  537. return;
  538. }
  539. if ( min + 30 >= max )
  540. max = min + 30;
  541. if ( max - min < 100 )
  542. {
  543. max += 19;
  544. max /= 20;
  545. max *= 20;
  546. min /= 20;
  547. min *= 20;
  548. }
  549. else
  550. {
  551. max += 199;
  552. max /= 200;
  553. max *= 200;
  554. min /= 200;
  555. min *= 200;
  556. }
  557. delta = max-min;
  558. i = hist_start;
  559. x = 127-HIST_CNT;
  560. u8g2.setFont(FONT_NARROW);
  561. u8g2.setDrawColor(1);
  562. for(;;)
  563. {
  564. ii = i;
  565. i++;
  566. if ( i >= HIST_CNT )
  567. i = 0;
  568. if ( i == hist_end )
  569. break;
  570. ymin = ((unsigned long)(get_val(min_array, ii) - min)*30UL)/delta;
  571. ymax = ((unsigned long)(get_val(max_array, ii) - min)*30UL)/delta;
  572. u8g2.drawVLine(x, 63-ymax, ymax-ymin+1);
  573. x++;
  574. }
  575. u8g2.setCursor(0, 46);
  576. draw_value(max);
  577. u8g2.setCursor(0, 63);
  578. draw_value(min);
  579. }
  580. //===================================================
  581. void draw_1_2_eco2_history(u8g2_uint_t x, u8g2_uint_t y)
  582. {
  583. draw_graph( get_uint16, hist_eco2_min, hist_eco2_max, draw_16bit);
  584. }
  585. void draw_1_4_temperature(u8g2_uint_t x, u8g2_uint_t y)
  586. {
  587. u8g2.setFont(FONT_MED_NUM);
  588. u8g2.setCursor(x, 28+y);
  589. draw_temperature(temperature);
  590. u8g2.setFont(FONT_SMALL);
  591. u8g2.setCursor(x, 10+y);
  592. u8g2.print(F("°C"));
  593. }
  594. void draw_1_4_humidity(u8g2_uint_t x, u8g2_uint_t y)
  595. {
  596. u8g2.setFont(FONT_MED_NUM);
  597. u8g2.setCursor(x, 28+y);
  598. draw_humidity(humidity);
  599. u8g2.setFont(FONT_SMALL);
  600. u8g2.setCursor(x, 10+y);
  601. u8g2.print(F("%RH"));
  602. }
  603. void draw_gray_out(u8g2_uint_t x, u8g2_uint_t y)
  604. {
  605. if ( state == STATE_WARMUP_DISP_ON )
  606. {
  607. uint8_t xx, yy;
  608. u8g2.setDrawColor(0);
  609. for( yy = y+14; yy < y+28; yy+=1 )
  610. {
  611. for( xx = 0; xx < 52; xx += 2 )
  612. {
  613. u8g2.drawPixel(xx+x+(yy&1), yy);
  614. }
  615. }
  616. u8g2.setDrawColor(1);
  617. }
  618. }
  619. void draw_1_4_eco2(u8g2_uint_t x, u8g2_uint_t y)
  620. {
  621. u8g2.setFont(FONT_SMALL);
  622. u8g2.setCursor(x, y+10);
  623. u8g2.print(F("ppm CO"));
  624. u8g2.setCursor(x+40, y+15);
  625. u8g2.print(F("²"));
  626. u8g2.setFont(FONT_MED_NUM);
  627. u8g2.setCursor(x, y+28);
  628. u8g2.print(eco2_raw);
  629. draw_gray_out(x, y);
  630. }
  631. void draw_1_4_tvoc(u8g2_uint_t x, u8g2_uint_t y)
  632. {
  633. u8g2.setFont(FONT_SMALL);
  634. u8g2.setCursor(x, y+10);
  635. u8g2.print(F("ppb TVOC"));
  636. u8g2.setFont(FONT_MED_NUM);
  637. u8g2.setCursor(x, y+28);
  638. u8g2.print(tvoc_raw);
  639. draw_gray_out(x, y);
  640. }
  641. void draw_1_4_base(u8g2_uint_t x, u8g2_uint_t y)
  642. {
  643. u8g2.setFont(FONT_SMALL);
  644. u8g2.setCursor(x, y+11);
  645. u8g2.print(F("Sec "));
  646. u8g2.print(wdt_count);
  647. u8g2.setCursor(x, y+21);
  648. u8g2.print(F("B-C "));
  649. u8g2.print(eco2_base, HEX);
  650. u8g2.setCursor(x, y+31);
  651. u8g2.print(F("B-T "));
  652. u8g2.print(tvoc_base, HEX);
  653. }
  654. void draw_1_4_delay(u8g2_uint_t x, u8g2_uint_t y)
  655. {
  656. u8g2.setFont(FONT_SMALL);
  657. u8g2.setCursor(x, y+11);
  658. u8g2.print(F("Sens "));
  659. u8g2.print(millis_sensor);
  660. u8g2.setCursor(x, y+21);
  661. u8g2.print(F("Disp "));
  662. u8g2.print(millis_display);
  663. }
  664. void draw_1_4_uptime(u8g2_uint_t x, u8g2_uint_t y)
  665. {
  666. u8g2.setFont(FONT_SMALL);
  667. u8g2.setCursor(x, y+11);
  668. u8g2.print(F("Day "));
  669. u8g2.print(wdt_day);
  670. u8g2.setCursor(x, y+21);
  671. u8g2.print(F("Hour "));
  672. u8g2.print(wdt_hour);
  673. u8g2.setCursor(x, y+31);
  674. u8g2.print(F("Min "));
  675. u8g2.print(wdt_min);
  676. }
  677. void draw_1_4_system(u8g2_uint_t x, u8g2_uint_t y)
  678. {
  679. u8g2.setFont(FONT_SMALL);
  680. u8g2.setCursor(x, y+11);
  681. u8g2.print(F("ETH "));
  682. u8g2.print(ethanol_raw, HEX);
  683. u8g2.setCursor(x, y+21);
  684. u8g2.print(F("St "));
  685. u8g2.print(state);
  686. u8g2.print(F(" Sh "));
  687. u8g2.print(shake_last_cnt);
  688. u8g2.setCursor(x, y+31);
  689. u8g2.print(F("Bat "));
  690. u8g2.print(battery_raw);
  691. }
  692. void draw_1_4_battery(u8g2_uint_t x, u8g2_uint_t y)
  693. {
  694. u8g2.setFont(u8g2_font_battery19_tn);
  695. u8g2.drawGlyph(x, y+20, battery_glyph);
  696. }
  697. void draw_1_4_emoticon(u8g2_uint_t x, u8g2_uint_t y)
  698. {
  699. uint8_t tvoc_idx, eco2_idx, emo_idx;
  700. if ( tvoc_raw <= 75 )
  701. tvoc_idx = 1;
  702. else if ( tvoc_raw <= 150 )
  703. tvoc_idx = 2;
  704. else if ( tvoc_raw <= 300 )
  705. tvoc_idx = 3;
  706. else if ( tvoc_raw <= 500 )
  707. tvoc_idx = 4;
  708. else if ( tvoc_raw <= 1000 )
  709. tvoc_idx = 5;
  710. else if ( tvoc_raw <= 1500 )
  711. tvoc_idx = 6;
  712. else if ( tvoc_raw <= 3000 )
  713. tvoc_idx = 7;
  714. else if ( tvoc_raw <= 5000 )
  715. tvoc_idx = 8;
  716. else
  717. tvoc_idx = 9;
  718. if ( eco2_raw <= 500 )
  719. eco2_idx = 1;
  720. else if ( eco2_raw <= 600 )
  721. eco2_idx = 2;
  722. else if ( eco2_raw <= 800 )
  723. eco2_idx = 3;
  724. else if ( eco2_raw <= 1000 )
  725. eco2_idx = 4;
  726. else if ( eco2_raw <= 1200 )
  727. eco2_idx = 5;
  728. else if ( eco2_raw <= 1400 )
  729. eco2_idx = 6;
  730. else if ( eco2_raw <= 5000 )
  731. eco2_idx = 7;
  732. else if ( eco2_raw <= 10000 )
  733. eco2_idx = 8;
  734. else
  735. eco2_idx = 9;
  736. emo_idx = tvoc_idx;
  737. if ( emo_idx < eco2_idx )
  738. emo_idx = eco2_idx;
  739. u8g2.setFont(u8g2_font_emoticons21_tr);
  740. u8g2.drawGlyph(x+20, y+21, 32+emo_idx);
  741. }
  742. //===================================================
  743. void draw_all_numbers(void)
  744. {
  745. u8g2.setFontMode(1);
  746. u8g2.firstPage();
  747. do {
  748. u8g2.drawHLine(0, 32, 128);
  749. u8g2.drawVLine(62, 0, 64);
  750. draw_1_4_temperature(1, 0);
  751. draw_1_4_humidity(66, 0);
  752. if ( is_air_quality_available )
  753. {
  754. draw_1_4_eco2(1, 32+3);
  755. draw_1_4_tvoc(66, 32+3);
  756. }
  757. } while ( u8g2.nextPage() );
  758. }
  759. void draw_with_emo(void)
  760. {
  761. u8g2.setFontMode(1);
  762. u8g2.firstPage();
  763. do {
  764. //u8g2.drawHLine(0, 32, 128);
  765. //u8g2.drawVLine(62, 0, 64);
  766. u8g2.drawHLine(0, 32, 128);
  767. u8g2.drawVLine(62-14, 0, 32);
  768. u8g2.drawVLine(98, 0, 32);
  769. u8g2.drawVLine(62-8, 32, 32);
  770. u8g2.drawVLine(128-12, 32, 32);
  771. draw_1_4_temperature(1, 0);
  772. draw_1_4_humidity(66-14, 0);
  773. draw_1_4_emoticon(127-20-21, 4);
  774. //draw_1_4_wdt_count(66, 0);
  775. if ( is_air_quality_available )
  776. {
  777. draw_1_4_eco2(1, 32+3);
  778. //draw_1_4_emoticon(66, 32+6);
  779. draw_1_4_tvoc(66-8, 32+3);
  780. }
  781. draw_1_4_battery(128-8, 32+7);
  782. } while ( u8g2.nextPage() );
  783. }
  784. void draw_system(void)
  785. {
  786. u8g2.setFontMode(1);
  787. u8g2.firstPage();
  788. do {
  789. u8g2.drawHLine(0, 32, 128);
  790. u8g2.drawVLine(62, 0, 64);
  791. draw_1_4_uptime(1, 0);
  792. //draw_1_4_delay(1,0);
  793. //draw_1_4_temperature(1, 0);
  794. draw_1_4_eco2(66, 0);
  795. //draw_1_4_humidity(66, 0);
  796. //draw_1_4_wdt_count(66, 0);
  797. draw_1_4_base(0, 32);
  798. draw_1_4_system(66, 32);
  799. } while ( u8g2.nextPage() );
  800. }
  801. void draw_with_history(void)
  802. {
  803. u8g2.setFontMode(1);
  804. u8g2.firstPage();
  805. do {
  806. u8g2.drawHLine(0, 32, 128);
  807. u8g2.drawVLine(62-20, 0, 32);
  808. u8g2.drawVLine(98, 0, 32);
  809. draw_1_4_temperature(1, 0);
  810. if ( is_air_quality_available )
  811. {
  812. draw_1_4_eco2(66-20, 0);
  813. draw_1_4_emoticon(127-20-21, 4);
  814. }
  815. draw_1_2_eco2_history(0, 32);
  816. } while ( u8g2.nextPage() );
  817. }
  818. //===================================================
  819. uint8_t is_display_on_event(void)
  820. {
  821. if ( is_shake > 0 )
  822. {
  823. is_shake = 0;
  824. return 1;
  825. }
  826. if ( shake_last_cnt >= 1 )
  827. return 1;
  828. return 0;
  829. }
  830. void handle_new_display_page(void)
  831. {
  832. if ( new_display_cool_down_timer > 0 )
  833. {
  834. new_display_cool_down_timer--;
  835. }
  836. else
  837. {
  838. if ( shake_last_cnt > NEW_DISPLAY_SHAKE_THRESHOLD )
  839. {
  840. new_display_cool_down_timer = NEW_DISPLAY_COOL_DOWN;
  841. current_display_page++;
  842. if ( current_display_page >= DISPLAY_PAGE_CNT )
  843. current_display_page = 0;
  844. }
  845. }
  846. }
  847. void disable_display(void)
  848. {
  849. u8g2.clear();
  850. u8g2.setPowerSave(1);
  851. is_display_enabled = 0;
  852. }
  853. void enable_display(void)
  854. {
  855. is_display_enabled = 1;
  856. u8g2.setPowerSave(0);
  857. }
  858. void disable_sensors(void)
  859. {
  860. /* both sensors will go to idle/sleep mode when the I2C soft reset is sent */
  861. Wire.beginTransmission(0);
  862. Wire.write(6);
  863. Wire.endTransmission(true); // true: send full stop on I2C
  864. delay(5);
  865. }
  866. void enable_sensors(void)
  867. {
  868. sht.init();
  869. sgp.IAQinit();
  870. delay(5);
  871. }
  872. //===================================================
  873. void next_state(void)
  874. {
  875. uint32_t start;
  876. switch(state)
  877. {
  878. case STATE_RESET:
  879. enable_display();
  880. startup_timer = STARTUP_TIME;
  881. state = STATE_STARTUP_DISP_ON;
  882. display_timer = DISPLAY_TIME;
  883. break;
  884. // - - - Start Up - - -
  885. case STATE_STARTUP_DISP_ON:
  886. start = millis();
  887. sgp.getIAQBaseline(&eco2_base, &tvoc_base); // always store the calibration values during measure
  888. readAirQuality();
  889. millis_sensor = millis() - start;
  890. if ( is_display_on_event() )
  891. {
  892. // display is already enabled, but the timer is reseted
  893. display_timer = DISPLAY_TIME;
  894. }
  895. if ( display_timer == 0 )
  896. {
  897. disable_display();
  898. state = STATE_STARTUP_DISP_OFF;
  899. }
  900. else if ( startup_timer == 0 )
  901. {
  902. sensor_measure_timer = SENSOR_MEASURE_TIME;
  903. state = STATE_MEASURE_DISP_ON;
  904. }
  905. break;
  906. case STATE_STARTUP_DISP_OFF:
  907. start = millis();
  908. sgp.getIAQBaseline(&eco2_base, &tvoc_base); // always store the calibration values during measure
  909. readAirQuality();
  910. millis_sensor = millis() - start;
  911. if ( is_display_on_event() )
  912. {
  913. enable_display();
  914. display_timer = DISPLAY_TIME;
  915. new_display_cool_down_timer = NEW_DISPLAY_COOL_DOWN; // ensure, that the display page is visible for a while
  916. state = STATE_STARTUP_DISP_ON;
  917. }
  918. else if ( startup_timer == 0 )
  919. {
  920. sensor_measure_timer = SENSOR_MEASURE_TIME;
  921. state = STATE_MEASURE_DISP_OFF;
  922. }
  923. break;
  924. // - - - Sensor Warm Up - - -
  925. case STATE_WARMUP_DISP_ON: // DONE
  926. if ( is_display_on_event() )
  927. {
  928. // display is already enabled, but the timer is reseted
  929. display_timer = DISPLAY_TIME;
  930. }
  931. if ( display_timer == 0 )
  932. {
  933. disable_display();
  934. state = STATE_WARMUP_DISP_OFF;
  935. }
  936. else if ( sensor_warmup_timer == 0 )
  937. {
  938. // set the baseline at the end of the warmup
  939. // this is described in the "SGP30 Driver Integration"
  940. if ( eco2_base != 0 )
  941. sgp.setIAQBaseline(eco2_base, tvoc_base); // Restore the baseline values
  942. sensor_measure_timer = SENSOR_MEASURE_TIME;
  943. state = STATE_MEASURE_DISP_ON;
  944. }
  945. break;
  946. case STATE_WARMUP_DISP_OFF: // DONE
  947. if ( is_display_on_event() )
  948. {
  949. enable_display();
  950. display_timer = DISPLAY_TIME;
  951. new_display_cool_down_timer = NEW_DISPLAY_COOL_DOWN; // ensure, that the display page is visible for a while
  952. state = STATE_WARMUP_DISP_ON;
  953. }
  954. else if ( sensor_warmup_timer == 0 )
  955. {
  956. // set the baseline at the end of the warmup
  957. // this is described in the "SGP30 Driver Integration"
  958. if ( eco2_base != 0 )
  959. sgp.setIAQBaseline(eco2_base, tvoc_base); // Restore the baseline values
  960. sensor_measure_timer = SENSOR_MEASURE_TIME;
  961. state = STATE_MEASURE_DISP_OFF;
  962. }
  963. break;
  964. // - - - Sensor Measure - - -
  965. case STATE_MEASURE_DISP_ON: // DONE
  966. start = millis();
  967. sgp.getIAQBaseline(&eco2_base, &tvoc_base); // always store the calibration values during measure
  968. readAirQuality();
  969. millis_sensor = millis() - start;
  970. if ( is_display_on_event() )
  971. {
  972. // display is already enabled, but the timer is reseted
  973. display_timer = DISPLAY_TIME;
  974. }
  975. if ( display_timer == 0 )
  976. {
  977. disable_display();
  978. state = STATE_MEASURE_DISP_OFF;
  979. }
  980. else if ( sensor_measure_timer == 0 )
  981. {
  982. // ignored: Measurement is continued until the display goes off.
  983. }
  984. break;
  985. case STATE_MEASURE_DISP_OFF: // DONE
  986. start = millis();
  987. sgp.getIAQBaseline(&eco2_base, &tvoc_base); // always store the calibration values during measure
  988. readAirQuality();
  989. millis_sensor = millis() - start;
  990. if ( is_display_on_event() )
  991. {
  992. enable_display();
  993. display_timer = DISPLAY_TIME;
  994. new_display_cool_down_timer = NEW_DISPLAY_COOL_DOWN; // ensure, that the display page is visible for a while
  995. state = STATE_MEASURE_DISP_ON;
  996. }
  997. else if ( sensor_measure_timer == 0 )
  998. {
  999. disable_sensors();
  1000. state = STATE_SENSOR_SLEEP_DISP_OFF;
  1001. }
  1002. break;
  1003. // - - - Sensor Sleep - - -
  1004. case STATE_SENSOR_SLEEP_DISP_OFF: // DONE
  1005. if ( is_display_on_event() )
  1006. {
  1007. enable_display();
  1008. display_timer = DISPLAY_TIME;
  1009. new_display_cool_down_timer = NEW_DISPLAY_COOL_DOWN; // ensure, that the display page is visible for a while
  1010. enable_sensors();
  1011. sensor_warmup_timer = SENSOR_WARMUP_TIME;
  1012. state = STATE_WARMUP_DISP_ON;
  1013. }
  1014. else if ( is_sensor_sample_timer_alarm != 0 )
  1015. {
  1016. is_sensor_sample_timer_alarm = 0;
  1017. enable_sensors();
  1018. sensor_warmup_timer = SENSOR_WARMUP_TIME;
  1019. state = STATE_WARMUP_DISP_OFF;
  1020. }
  1021. break;
  1022. default:
  1023. state = STATE_RESET;
  1024. break;
  1025. }
  1026. }
  1027. //===================================================
  1028. void loop(void) {
  1029. uint8_t i;
  1030. u8g2_uint_t g;
  1031. uint32_t start;
  1032. if ( is_wdt_irq )
  1033. {
  1034. is_wdt_irq = 0;
  1035. next_state();
  1036. readBatteryVoltageLevel();
  1037. handle_new_display_page();
  1038. if ( is_display_enabled ) // "is_display_enabled" will be calculated in next_state()
  1039. {
  1040. start = millis();
  1041. if ( current_display_page == 0 )
  1042. draw_with_emo();
  1043. else if ( current_display_page == 1 )
  1044. draw_with_history();
  1045. else if ( current_display_page == 2 )
  1046. draw_system();
  1047. millis_display = millis() - start;
  1048. }
  1049. else
  1050. {
  1051. millis_display = 0;
  1052. }
  1053. }
  1054. set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  1055. //noInterrupts();
  1056. sleep_enable();
  1057. //interrupts();
  1058. sleep_mode();
  1059. sleep_disable();
  1060. }