Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 44 additions & 38 deletions OpenCO2_Sensor.ino
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@
- WiFiManager: https://github.com/tzapu/WiFiManager
- ArduinoMqttClient (if MQTT is defined)
*/
#define VERSION "v5.9"
#define VERSION "v6.0"

#define HEIGHT_ABOVE_SEA_LEVEL 50 // Berlin
#define TZ_DATA "CET-1CEST,M3.5.0,M10.5.0/3" // Europe/Berlin time zone from https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv
#define LIGHT_SLEEP_TIME 500
#define DEEP_SLEEP_TIME 29124 // 30 sec
#define LOW_ENERGY_DEEP_SLEEP_TIME 287924 // 5 min
#define DEEP_SLEEP_TIME_NO_DISPLAY_UPDATE DEEP_SLEEP_TIME + 965 // offset for no display update
static unsigned long lastMeasurementTimeMs = 0;

Expand Down Expand Up @@ -77,19 +76,13 @@ CRGB leds[1];
#include <Wire.h>
SensirionI2cScd4x scd4x;


#ifndef ARDUINO_USB_MODE
#error This ESP32 SoC has no Native USB interface
#elif ARDUINO_USB_MODE == 1
#error This sketch should be used when USB is in OTG mode and MSC On Boot enabled
#endif
#include "USB.h"
#include <USBMSC.h>
USBMSC usbmsc;
#include "FirmwareMSC.h"
FirmwareMSC MSC_Update;

RTC_DATA_ATTR bool USB_ACTIVE = false, initDone = false, BatteryMode = false, comingFromDeepSleep = false;
RTC_DATA_ATTR bool LEDonBattery, LEDonUSB, useSmoothLEDcolor, invertDisplay, useFahrenheit, useWiFi, lowEnergyMode, english, limitMaxBattery;
RTC_DATA_ATTR uint8_t ledbrightness, HWSubRev, font;
RTC_DATA_ATTR bool LEDonBattery, LEDonUSB, useSmoothLEDcolor, invertDisplay, useFahrenheit, useWiFi, english, limitMaxBattery;
RTC_DATA_ATTR uint8_t ledbrightness, HWSubRev, font, skipMeasurement = 10;
RTC_DATA_ATTR float maxBatteryVoltage;

/* TEST_MODE */
Expand Down Expand Up @@ -327,8 +320,7 @@ float getTempOffset() {
if (useWiFi) return 12.2;
return 4.4;
} else {
if (lowEnergyMode) return 0.0;
return 0.8;
return 0.0; // was with periodic measurment 0.8
}
}

Expand All @@ -348,7 +340,7 @@ void initOnce() {
if (HWSubRev < 3) {
Wire.begin(33, 34); // green, yellow
digitalWrite(LED_POWER, LOW); // LED on
FastLED.addLeds<APA102, 40, 39, RGB>(leds, 1);
FastLED.addLeds<APA102, 40, 39, BGR>(leds, 1);
} else {
Wire.begin(3, 2);
digitalWrite(LED_POWER, HIGH); // LED on
Expand All @@ -370,7 +362,6 @@ void initOnce() {
preferences.begin("co2-sensor", true);
maxBatteryVoltage = preferences.getFloat("MBV", 3.95);
useWiFi = preferences.getBool("WiFi", false);
lowEnergyMode = preferences.getBool("lowEnergy", false);
LEDonBattery = preferences.getBool("LEDonBattery", false);
LEDonUSB = preferences.getBool("LEDonUSB", true);
ledbrightness = preferences.getInt("ledbrightness", 5);
Expand All @@ -389,7 +380,7 @@ void initOnce() {
scd4x.setSensorAltitude(HEIGHT_ABOVE_SEA_LEVEL);
scd4x.setAutomaticSelfCalibrationEnabled(1); // Or use setAutomaticSelfCalibrationTarget if needed
scd4x.setTemperatureOffset(getTempOffset());
if (!(BatteryMode && lowEnergyMode)) scd4x.startPeriodicMeasurement();
if (!BatteryMode) scd4x.startPeriodicMeasurement();

displayInit();
delay(3000); // Wait for co2 measurement
Expand Down Expand Up @@ -594,6 +585,9 @@ void calibrate() {
/* Only run this, if calibration is needed!
let the Sensor run outside for 3+ minutes before.
*/
scd4x.stopPeriodicMeasurement();
scd4x.wakeUp();
scd4x.startLowPowerPeriodicMeasurement();
displayCalibrationWarning();
delay(500);
for (int i = 0; i < 180; i++) {
Expand Down Expand Up @@ -789,15 +783,13 @@ void setup() {
/* scd4x */
if (HWSubRev < 3) {
Wire.begin(33, 34); // green, yellow
FastLED.addLeds<APA102, 40, 39, RGB>(leds, 1);
FastLED.addLeds<APA102, 40, 39, BGR>(leds, 1);
} else {
Wire.begin(3, 2);
FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, 1);
}
scd4x.begin(Wire, 0x62); // 0x62 is the default I2C address for SCD4x

USB.onEvent(usbEventCallback);
usbmsc.isWritable(true);
if (!initDone) initOnce();

#if ARDUINO_USB_CDC_ON_BOOT && !ARDUINO_USB_MODE
Expand All @@ -818,19 +810,24 @@ void setup() {
delay(1);
setLED(co2);

scd4x.stopPeriodicMeasurement(); // stop low power measurement
scd4x.setTemperatureOffset(getTempOffset());
scd4x.startPeriodicMeasurement();
/* Wait for co2 measurement */
delay(5000);
}

if (useWiFi && !BatteryMode) startWiFi();

if (!BatteryMode && !comingFromDeepSleep) {
USB.onEvent(usbEventCallback);
MSC_Update.begin();
USB.begin();
}
}


void loop() {
if ((!useWiFi || (lowEnergyMode && BatteryMode)) && esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) handleButtonPress();
if ((!useWiFi || BatteryMode) && esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) handleButtonPress();
updateBatteryMode(); // check again in USB Power mode
updateCharging();
measureESP32temperature();
Expand All @@ -856,7 +853,7 @@ void loop() {
goto_light_sleep(5000 - (millis() - lastMeasurementTimeMs));
}

if (!(BatteryMode && lowEnergyMode) && !TEST_MODE) {
if (!BatteryMode && !TEST_MODE) {
bool isDataReady = false;
uint16_t ready_error = scd4x.getDataReadyStatus(isDataReady);
if (ready_error || !isDataReady) {
Expand All @@ -870,11 +867,27 @@ void loop() {
uint16_t new_co2 = 420;
float new_temperature = 0.0f;
uint16_t error;
if (BatteryMode && lowEnergyMode) {
scd4x.stopPeriodicMeasurement();
if (BatteryMode) {
if (!comingFromDeepSleep) scd4x.stopPeriodicMeasurement();
scd4x.wakeUp();
scd4x.setTemperatureOffset(getTempOffset());
//delay(10);

/* check if temp/humidity changed */
scd4x.measureSingleShotRhtOnly();
uint16_t dummyCo2; // CO2 output is returned as 0 ppm
float new_humidity = 0.0f;
error = scd4x.readMeasurement(dummyCo2, new_temperature, new_humidity);
if (!error
&& (skipMeasurement > 1)
&& (fabs(new_temperature - temperature) < 0.5)
&& (fabs(new_humidity - humidity) < 2.0)) {
scd4x.powerDown();
skipMeasurement--;
saveMeasurement(co2, new_temperature, new_humidity);
goto_deep_sleep(DEEP_SLEEP_TIME_NO_DISPLAY_UPDATE);
}
skipMeasurement = 10; // force update every 5 minutes

if (HWSubRev < 3) scd4x.measureSingleShot(); // Ignore first measurement after wake up.
error = scd4x.measureAndReadSingleShot(new_co2, new_temperature, humidity);
scd4x.powerDown();
Expand All @@ -895,19 +908,12 @@ void loop() {
extern uint16_t refreshes;
if (BatteryMode || (refreshes % 6 == 1)) {
saveMeasurement(new_co2, new_temperature, humidity);

if (BatteryMode && lowEnergyMode) { // fill measurements of past 5 min
for (int i=0; i<9; i++) {
saveMeasurement(new_co2, new_temperature, humidity);
}
}
}

/* don't update in Battery mode, unless CO2 has changed by 4% or temperature by 0.5°C */
if (!TEST_MODE && BatteryMode && comingFromDeepSleep) {
if ((abs(new_co2 - co2) < (0.04 * co2)) && (fabs(new_temperature - temperature) < 0.5)) {
if (lowEnergyMode) goto_deep_sleep(LOW_ENERGY_DEEP_SLEEP_TIME);
else goto_deep_sleep(DEEP_SLEEP_TIME_NO_DISPLAY_UPDATE);
goto_deep_sleep(DEEP_SLEEP_TIME_NO_DISPLAY_UPDATE);
}
}

Expand Down Expand Up @@ -957,11 +963,11 @@ void loop() {
if (BatteryMode) {
if (!comingFromDeepSleep) {
scd4x.stopPeriodicMeasurement();
scd4x.setTemperatureOffset(getTempOffset());
if (!lowEnergyMode) scd4x.startLowPowerPeriodicMeasurement();
MSC_Update.end();
USB_ACTIVE = false;
delay(100);
}
if (lowEnergyMode) goto_deep_sleep(LOW_ENERGY_DEEP_SLEEP_TIME);
else goto_deep_sleep(DEEP_SLEEP_TIME);
goto_deep_sleep(DEEP_SLEEP_TIME);
}

goto_light_sleep(LIGHT_SLEEP_TIME);
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ OpenCO2 Sensor is an Arduino IDE compatible Repository for an E-Ink Indoor air q

## Buy it [here on Tindie](https://www.tindie.com/products/davidkreidler/open-co2-sensor/)

Especially in winter, when windows are closed, a reminder to ventilate regularly is useful for health, comfort and well-being. Poor indoor air quality can lead to decreased productivity and learning disabilities. Therefore, I developed an Open-source ESP32 project that uses an E-Ink display and a LED to show the indoor CO2 content. Take the small Sensor anywhere you go to monitor the Air Quality, with Battery life of 11+ days.
Especially in winter, when windows are closed, a reminder to ventilate regularly is useful for health, comfort and well-being. Poor indoor air quality can lead to decreased productivity and learning disabilities. Therefore, I developed an Open-source ESP32 project that uses an E-Ink display and a LED to show the indoor CO2 content. Take the small Sensor anywhere you go to monitor the Air Quality, with Battery life of up to 11 weeks.

# CO2 Sensor

Expand Down Expand Up @@ -109,8 +109,9 @@ Use [internet-pi](https://github.com/geerlingguy/internet-pi) to store the CO2 /
# Update via USB

1. Download `FIRMWARE.BIN` from the latest [release](https://github.com/davidkreidler/OpenCO2_Sensor/releases)
2. Plug a data USB-C cable into your PC and the Sensor
3. copy `FIRMWARE.BIN` to the USB device
2. Plug a data USB-C Cable
3. Restart the OpenCO2 Sensor using the switch on the side
4. Copy `FIRMWARE.BIN` to the USB device

# OTA Update

Expand Down
3 changes: 0 additions & 3 deletions epd_abstraction.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ enum LEDMenuOptions {
NUM_LED_OPTIONS
};
enum DisplayMenuOptions {
UPDATE,
MAX_BATTERY,
INVERT,
TEMP_UNIT,
Expand Down Expand Up @@ -57,7 +56,6 @@ const char* EnglishLEDmenuItems[NUM_LED_OPTIONS] = {
"Exit"
};
const char* EnglishOptionsMenuItems[NUM_DISPLAY_OPTIONS] = {
"Update",
"Battery",
"Invert",
"Unit",
Expand Down Expand Up @@ -90,7 +88,6 @@ const char* GermanLEDmenuItems[NUM_LED_OPTIONS] = {
"Beenden"
};
const char* GermanOptionsMenuItems[NUM_DISPLAY_OPTIONS] = {
"Update",
"Battery",
"Invert",
"Einheit",
Expand Down
34 changes: 5 additions & 29 deletions epd_abstraction.ino
Original file line number Diff line number Diff line change
Expand Up @@ -210,18 +210,6 @@ void OptionsMenu() {
}
if (mspressed > 1000) { // long press
switch (selectedOption) {
case UPDATE:
lowEnergyMode = !lowEnergyMode;
preferences.begin("co2-sensor", false);
preferences.putBool("lowEnergy", lowEnergyMode);
preferences.end();

if (BatteryMode) {
scd4x.stopPeriodicMeasurement();
scd4x.setTemperatureOffset(getTempOffset());
if (!lowEnergyMode) scd4x.startPeriodicMeasurement();
}
break;
case MAX_BATTERY:
toggleMaxBattery();
break;
Expand Down Expand Up @@ -806,15 +794,13 @@ void displayOptionsMenu(uint8_t selectedOption) {
else OptionsMenuItem = GermanOptionsMenuItems[i];
Paint_DrawString_EN(5, 25*(i+1), OptionsMenuItem, &Font24, WHITE, BLACK);
}
if (lowEnergyMode) Paint_DrawString_EN(200-17*4, 25, "5min", &Font24, WHITE, BLACK);
else Paint_DrawString_EN(200-17*5, 25, "30sec", &Font24, WHITE, BLACK);

if (limitMaxBattery && HWSubRev > 2) Paint_DrawString_EN(200-17*4, 25*2, "~80%", &Font24, WHITE, BLACK);
else Paint_DrawString_EN(200-17*4, 25*2, "100%", &Font24, WHITE, BLACK);
if (limitMaxBattery && HWSubRev > 2) Paint_DrawString_EN(200-17*4, 25, "~80%", &Font24, WHITE, BLACK);
else Paint_DrawString_EN(200-17*4, 25, "100%", &Font24, WHITE, BLACK);

Paint_DrawString_EN(166, 25*4, (useFahrenheit? "*F":"*C"), &Font24, WHITE, BLACK);
Paint_DrawNum(149, 25*6, (int32_t)(font+1), &Font24, BLACK, WHITE);
Paint_DrawString_EN(166, 25*6, "/2", &Font24, WHITE, BLACK);
Paint_DrawString_EN(166, 25*3, (useFahrenheit? "*F":"*C"), &Font24, WHITE, BLACK);
Paint_DrawNum(149, 25*5, (int32_t)(font+1), &Font24, BLACK, WHITE);
Paint_DrawString_EN(166, 25*5, "/2", &Font24, WHITE, BLACK);

invertSelected(selectedOption);
updateDisplay();
Expand Down Expand Up @@ -1252,7 +1238,6 @@ void displayinfo() {
delay(10);
scd4x.getTemperatureOffset(tOffset);
if (!BatteryMode) scd4x.startPeriodicMeasurement();
else if (!lowEnergyMode) scd4x.startLowPowerPeriodicMeasurement();
Paint_DrawString_EN(1, 145, "T_offset:", &Font16, WHITE, BLACK);
char offset[20];
snprintf(offset, sizeof(offset), "%.2f", tOffset);
Expand Down Expand Up @@ -1336,15 +1321,6 @@ void displayBattery(uint8_t percentage) {
BlackImage[y+x*25] = ~BlackImage[y+x*25];
}
}

/* low Energy Mode */
if (lowEnergyMode) {
Paint_DrawRectangle(97, 13, 115, 47, BLACK, DOT_PIXEL_2X2, DRAW_FILL_EMPTY); //case
Paint_DrawLine(103, 10, 109, 10, BLACK, DOT_PIXEL_3X3, LINE_STYLE_SOLID);//nippel
Paint_DrawLine(106, 25, 106, 35, BLACK, DOT_PIXEL_2X2, LINE_STYLE_SOLID);//+
Paint_DrawLine(102, 30, 110, 30, BLACK, DOT_PIXEL_2X2, LINE_STYLE_SOLID);//+
//Xstart, Ystart, Xend, Yend
}
#endif /* EINK_1IN54V2 */

#ifdef EINK_4IN2
Expand Down