Мало хто турбується про якість повітря в приміщені, що може негативно вплинути на наше з вами самопочуття, особливо, це важливо в холодну пору року, коли вікна закриті для збереження тепла. Тому в мене виникла ідея зібрати сенсор якості повітря, який буде сигналізувати блиманням червоного діода про порушення прийнятних для здоров'я значеннь. Побачивши відхилення будемо проводити провітрювання приміщення.

З цим завданням нам допоможе мікроконтролер ESP32 C3 super mini та датчик CO2 SCD40.

Список компонентів

Схема підключення

Програмний код

З коду прибрав скрін, де вказано, що це подарунок для моєї донечки. Все решта без змін.

main.cpp for PlatformIO
    
#include 
#include 
#include 
#include 

// --- CONFIGURATION ---
#define SCREEN_SWITCH_INTERVAL 4000  // Rotate screens every 4 seconds
#define BLINK_INTERVAL 500           // LED blink speed (ms)

#define OLED_SDA 8
#define OLED_SCL 9
#define LED_PIN  5

// --- CO2 THRESHOLDS ---
#define CO2_ALERT_START 1100  // Alarm starts here
#define CO2_ALERT_STOP  1050   // Alarm stops here (Hysteresis)

// --- GRAPHICS DATA ---
// A custom 11x10 pixel Heart icon
#define HEART_WIDTH  11
#define HEART_HEIGHT 11
static const unsigned char heart_bits[] U8X8_PROGMEM = {
  0x08,0x00,
  0xDE,0x07,
  0xFE,0x03,
  0xFF,0x07,
  0xFE,0x07,
  0xFF,0x07,
  0xFE,0x03,
  0xFC,0x01,
  0xF8,0x00,
  0x70,0x00,
  0x00,0x00
};

// Initialize display
U8G2_SH1106_128X64_NONAME_F_HW_I2C display(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

SensirionI2cScd4x co2;

// Variables
unsigned long lastSwitchTime = 0;
unsigned long lastBlinkTime = 0;
int currentScreen = 0; 
uint16_t valCo2 = 0;
float valTemp = 0.0;
float valHum = 0.0;
bool ledState = LOW; 
bool isAlarmActive = false; 

// --- STARTUP SCREEN: HEALTHY VALUES ---
void showReferenceScreen() {
  display.clearBuffer();

  // Title
  display.setFont(u8g2_font_helvB08_tr);
  display.drawStr(25, 12, "HEALTHY VALUES");
  display.drawHLine(0, 15, 128);

  // Values List
  display.setFont(u8g2_font_profont12_tf);
  
  display.drawStr(10, 30, "CO2:  < 1100 ppm");
  display.drawStr(10, 45, "Temp: 18 - 26 C");
  display.drawStr(10, 60, "Hum:  40 - 60 %");

  display.sendBuffer();
  delay(3000); // Show for 3 seconds
}

void setup() {
  Serial.begin(115200);
  
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  Wire.begin(OLED_SDA, OLED_SCL); 

  co2.begin(Wire, SCD40_I2C_ADDR_62);
  co2.startPeriodicMeasurement();

  display.begin();

  // --- SHOW STARTUP SCREENS ---
  showReferenceScreen();
  
  // Reset timer so the first data screen doesn't switch immediately
  lastSwitchTime = millis();
}

void blinkWarningLed() {
  if (millis() - lastBlinkTime >= BLINK_INTERVAL) {
    lastBlinkTime = millis();
    ledState = !ledState;
    digitalWrite(LED_PIN, ledState);
  }
}

void stopLed() {
  digitalWrite(LED_PIN, LOW);
  ledState = LOW;
}

void drawBigScreen(String label, String value, String unit, bool isWarning) {
  display.clearBuffer();

  // Label
  display.setFont(u8g2_font_profont12_tf);
  int wLabel = display.getStrWidth(label.c_str());
  display.setCursor((128 - wLabel) / 2, 12);
  display.print(label);
  
  // Value
  display.setFont(u8g2_font_logisoso32_tf); 
  int wValue = display.getStrWidth(value.c_str());
  display.setCursor((128 - wValue) / 2, 50);
  display.print(value);

  // Unit
  display.setFont(u8g2_font_profont12_tf);
  int wUnit = display.getStrWidth(unit.c_str());
  display.setCursor(128 - wUnit - 4, 62); 
  display.print(unit);

  // Warning Styles
  if (isWarning) {
     display.drawFrame(0, 0, 128, 64);
     display.drawFrame(1, 1, 126, 62); 
     display.setFont(u8g2_font_open_iconic_embedded_1x_t);
     display.drawGlyph(2, 10, 73); 
     display.drawGlyph(118, 10, 73);
  } else {
     unsigned long elapsed = millis() - lastSwitchTime;
     int progressWidth = map(elapsed, 0, SCREEN_SWITCH_INTERVAL, 0, 128);
     display.drawHLine(0, 63, progressWidth);
  }

  display.sendBuffer();
}

void loop() {
  // 1. Get Data
  bool dataReady = false;
  uint16_t error = co2.getDataReadyStatus(dataReady);
  if (error == 0 && dataReady) {
    co2.readMeasurement(valCo2, valTemp, valHum);
  }

  // 2. Alarm Logic
  if (valCo2 > CO2_ALERT_START) {
    isAlarmActive = true;
  } else if (valCo2 < CO2_ALERT_STOP) {
    isAlarmActive = false;
  }

  // 3. Display Logic
  if (isAlarmActive) {
    drawBigScreen("HIGH CO2 LEVEL", String(valCo2), "ppm", true);
    blinkWarningLed();
  } 
  else {
    stopLed();

    if (millis() - lastSwitchTime > SCREEN_SWITCH_INTERVAL) {
      lastSwitchTime = millis();
      currentScreen++;
      if (currentScreen > 2) currentScreen = 0;
    }

    switch (currentScreen) {
      case 0:
        drawBigScreen("AIR QUALITY", String(valCo2), "ppm", false);
        break;
      case 1:
        drawBigScreen("TEMPERATURE", String(valTemp, 1), "\260C", false);
        break;
      case 2:
        drawBigScreen("HUMIDITY", String(valHum, 0), "%", false);
        break;
    }
  }
  delay(50);
}
    
  

Корпус

Корпус розмірами 45х45 був спеціально розроблений під використані компоненти.

Посилання на корпус

Відео на youtube

Підтримка