Hackster is hosting Impact Spotlights: Smart Home. Watch the stream live on Thursday!Hackster is hosting Impact Spotlights: Smart Home. Stream on Thursday!
Jason
Published © MIT

Easy DIY Zigbee Smart Air Monitor with XIAO ESP32C6!

Built an air quality monitor with XIAO C6, CO2/tVOC sensors, and Zigbee for HA. Designed a custom shell for it!

BeginnerWork in progress1 hour2,773
Easy DIY Zigbee Smart Air Monitor with XIAO ESP32C6!

Things used in this project

Hardware components

Seeed Studio XIAO ESP32C6
Seeed Studio XIAO ESP32C6
×1
Seeed Studio XIAO Expansion Board
Seeed Studio XIAO Expansion Board
×1
Grove - VOC and eCO2 Gas Sensor SGP30
Seeed Studio Grove - VOC and eCO2 Gas Sensor SGP30
×1
smart home Home Assistant Connect ZBT-1
×1

Software apps and online services

Arduino IDE
Arduino IDE
Fusion
Autodesk Fusion
Home Assistant
Home Assistant

Story

Read more

Custom parts and enclosures

Left foot

right foot

body

back

Code

Arduino Core Code

C/C++
#ifndef ZIGBEE_MODE_ED
#error "Zigbee end device mode is not selected in Tools->Zigbee mode"
#endif

#include "Zigbee.h"
#include "sensirion_common.h"
#include "sgp30.h"
#include <U8g2lib.h>
#include <Wire.h>

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

uint8_t HugoUI_Animation_EasyOut(float *a, float *a_trg, uint16_t n) {
  if (*a == *a_trg)
    return 0;

  float cz = fabs(*a - *a_trg);

  if (cz <= 1)
    *a = *a_trg;
  else {
    if (cz < 10)
      n = n * cz * 0.1f;
    if (n < 10)
      n = 10;
    *a += (*a_trg - *a) / (n * 0.1f);
  }
  return 1;
}

uint8_t HugoUI_Animation_EasyIn(float *a, float *a_trg, uint16_t n) {
  if (*a == *a_trg)
    return 0;

  float cz = fabs(*a - *a_trg);
  if (cz <= 1)
    *a = *a_trg;
  else if (cz > 20)
    n = n * 3;
  else if (cz > 15)
    n = n * 2;
  else if (cz > 5)
    n = n * 1;
  if (*a != *a_trg)
    *a += (*a_trg - *a) / (n * 0.1f);
  else
    return 0;
  return 1;
}

void Oled_DrawSlowBitmapResize(int x, int y, const uint8_t *bitmap, int w1, int h1, int w2, int h2) {
  uint8_t color = u8g2.getDrawColor();

  float mw = (float)w2 / w1;
  float mh = (float)h2 / h1;
  uint8_t cmw = ceil(mw);
  uint8_t cmh = ceil(mh);
  int xi, yi, byteWidth = (w1 + 7) / 8;
  for (yi = 0; yi < h1; yi++) {
    for (xi = 0; xi < w1; xi++) {
      if (*(uint8_t *)(bitmap + yi * byteWidth + xi / 8) & (1 << (xi & 7))) 
      {
        u8g2.drawBox(x + xi * mw, y + yi * mh, cmw, cmh);
      } else if (color != 2) {
        u8g2.setDrawColor(0);
        u8g2.drawBox(x + xi * mw, y + yi * mh, cmw, cmh);
        u8g2.setDrawColor(color);
      }
    }
  }
}

const unsigned char gImage_humidity[] = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xF0,
  0x07, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0x3E, 0x7C, 0x00,
  0x00, 0x1F, 0xF8, 0x00, 0x80, 0x07, 0xF0, 0x01, 0xC0, 0x07, 0xE0, 0x01, 0xC0, 0x03,
  0xC0, 0x03, 0xE0, 0x81, 0x81, 0x07, 0xE0, 0xC1, 0x83, 0x07, 0xE0, 0xD0, 0x03, 0x07,
  0xF0, 0xF8, 0x03, 0x0F, 0xF0, 0xF8, 0x0B, 0x0F, 0xF0, 0xF0, 0x1F, 0x0F, 0xF0, 0xE0,
  0x1F, 0x0F, 0xE0, 0xC0, 0x0F, 0x07, 0xE0, 0xC0, 0x07, 0x07, 0xE0, 0xC1, 0x83, 0x07,
  0xC0, 0xC3, 0xC3, 0x03, 0xC0, 0xC7, 0xE3, 0x03, 0x80, 0x8F, 0xF3, 0x01, 0x00, 0xFF,
  0xFF, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0xC0, 0x03, 0x00,
  0x00, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00,
  0x00, 0x00
};

const unsigned char gImage_homeassistant[] = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0xE0, 0x07, 0x00,
  0x00, 0xF0, 0x0F, 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0xFC, 0xBF, 0x07, 0x00, 0xFE, 0xFF, 0x07,
  0x00, 0xFF, 0xFF, 0x07, 0x80, 0xFF, 0xFF, 0x07, 0xC0, 0xFF, 0xFF, 0x07, 0xE0, 0xFF, 0xFF, 0x07,
  0xF0, 0xFF, 0xFF, 0x0F, 0xF8, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0x3F, 0xFE, 0xF8, 0x1F, 0x7F,
  0x7F, 0xF7, 0xEF, 0xFE, 0x7F, 0xF7, 0xEF, 0xFE, 0xF8, 0xFF, 0xFF, 0x1F, 0xF8, 0xFF, 0xFF, 0x1F,
  0xF8, 0xEF, 0xF7, 0x1F, 0xF8, 0xCF, 0xF3, 0x1F, 0xF8, 0x9F, 0xF9, 0x1F, 0xF8, 0x7F, 0xFE, 0x1F,
  0xF8, 0xFF, 0xFF, 0x1F, 0xF8, 0xFF, 0xFF, 0x1F, 0xF8, 0xFF, 0xFF, 0x1F, 0xF8, 0xFF, 0xFF, 0x1F,
  0xF8, 0xFF, 0xFF, 0x1F, 0xF8, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};  // 32x32


#define CARBON_DIOXIDE_SENSOR_ENDPOINT_NUMBER 10
uint8_t button = BOOT_PIN;

ZigbeeCarbonDioxideSensor zbCarbonDioxideSensor = ZigbeeCarbonDioxideSensor(CARBON_DIOXIDE_SENSOR_ENDPOINT_NUMBER);

int16_t err = 0;
uint16_t tvoc_ppb, co2_eq_ppm;
uint16_t carbon_dioxide_value;
static uint32_t timeCounter = 0;
static float img_a = 4, img_a_trg = 24;
static float img_b = -2, img_b_trg = 24;
static float img_c = -10, img_c_trg = 13;
static float img_d = 5, img_d_trg = 90;


static void carbon_sensor_update(void *arg) {
  for (;;) {
    if (!(timeCounter++ % 20)) {
      err = sgp_measure_iaq_blocking_read(&tvoc_ppb, &co2_eq_ppm);

      if (err == STATUS_OK) {
        Serial.printf("tVOC Concentration: %d ppb\n", tvoc_ppb);
        Serial.printf("CO2eq Concentration: %d ppm\n", co2_eq_ppm);


        carbon_dioxide_value = co2_eq_ppm;
        zbCarbonDioxideSensor.setCarbonDioxide(carbon_dioxide_value);


      } else {
        Serial.println("Error reading IAQ values\n");
      }

      zbCarbonDioxideSensor.report();
      delay(6000);
    }
  }
}

void setup() {
  int16_t err;
  uint16_t scaled_ethanol_signal, scaled_h2_signal;

  Serial.begin(115200);
  u8g2.begin();

  // Init RF
  pinMode(WIFI_ENABLE, OUTPUT);
  digitalWrite(WIFI_ENABLE, LOW);

  delay(100);

  pinMode(WIFI_ANT_CONFIG, OUTPUT);
  digitalWrite(WIFI_ANT_CONFIG, LOW);

  // Init button switch
  pinMode(button, INPUT_PULLUP);

  // Init SGP30
  while (sgp_probe() != STATUS_OK) {
    Serial.println("SGP failed");
    while (1)
      ;
  }

  err = sgp_measure_signals_blocking_read(&scaled_ethanol_signal, &scaled_h2_signal);
  if (err == STATUS_OK) {
    Serial.println("get ram signal!");
  } else {
    Serial.println("error reading signals");
  }
  err = sgp_iaq_init();

  zbCarbonDioxideSensor.setManufacturerAndModel("Espressif", "ZigbeeCarbonDioxideSensor");
  zbCarbonDioxideSensor.setMinMaxValue(0, 1500);
  Zigbee.addEndpoint(&zbCarbonDioxideSensor);

  Serial.println("Starting Zigbee...");
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_ncenB08_tr);
  u8g2.drawStr(0, 30, "Connecting to Zigbee...");
  u8g2.sendBuffer();

  if (!Zigbee.begin()) {
    Serial.println("Zigbee failed to start!");
    Serial.println("Rebooting...");
    ESP.restart();
  } else {
    Serial.println("Zigbee started successfully!");
  }

  Serial.println("Connecting to network");
  while (!Zigbee.connected()) {
    Serial.print(".");
    delay(100);
  }

  u8g2.clearBuffer();
  u8g2.drawStr(0, 30, "Successfully connect");
  u8g2.drawStr(0, 50, "Zigbee network!");
  u8g2.sendBuffer();
  Serial.println();

  delay(5000);

  // Start carbon sensor reading task
  xTaskCreate(carbon_sensor_update, "carbon_sensor_update", 2048, NULL, 10, NULL);

  zbCarbonDioxideSensor.setReporting(0, 30, 0);
}

void loop() {

  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_ncenB08_tr);

  u8g2.drawXBM(0, 0, 32, 32, gImage_homeassistant);

  u8g2.drawStr(43, img_b, " Air  Monitor");

  u8g2.setDrawColor(2);
  u8g2.drawRBox(36, img_c, img_d, 15, 1);
  u8g2.setDrawColor(1);

  u8g2.drawStr(0, 45, "CO2: ");
  u8g2.setCursor(30, 45);
  u8g2.print(carbon_dioxide_value);
  u8g2.drawStr(55, 45, "ppb");
  u8g2.drawStr(0, 60, "TVOC: ");
  u8g2.setCursor(38, 60);
  u8g2.print(tvoc_ppb);
  u8g2.drawStr(55, 60, "ppm");

  if (img_a == img_a_trg) {
    if (img_a == 4) {
      img_a_trg = 24;
    } else if (img_a == 24)
      img_a_trg = 4;
  }

  HugoUI_Animation_EasyOut(&img_b, &img_b_trg, 100);
  HugoUI_Animation_EasyIn(&img_a, &img_a_trg, 115);
  HugoUI_Animation_EasyOut(&img_c, &img_c_trg, 100);
  HugoUI_Animation_EasyOut(&img_d, &img_d_trg, 100);

  Oled_DrawSlowBitmapResize(118 - img_a / 2, 50 - img_a / 4, gImage_humidity, 32, 32, img_a, img_a);

  u8g2.sendBuffer();

  if (digitalRead(button) == LOW) {
    delay(100);
    int startTime = millis();
    while (digitalRead(button) == LOW) {
      delay(50);
      if ((millis() - startTime) > 3000) {
        Serial.println("Resetting Zigbee to factory and rebooting in 1s.");
        delay(1000);
        Zigbee.factoryReset();
      }
    }
  }
}

Credits

Jason
2 projects • 5 followers
I'm an Application Engineer specializing in smart home solutions with ESP32. I enjoy developing demos.
Contact

Comments

Please log in or sign up to comment.