CO2 Meter


Created 5 project docs

Firmware Starter — ESP32-S3-MINI-1-N8

View Document

MCU: U1, ESP32-S3-MINI-1-N8 Framework: Arduino via PlatformIO

Schematic Verification Report — USB CO2 Air Monitor

View Document

Schematic-level electrical checks passed for the selected ERC/passive/component review set after wiring and no-connect cleanup. USB-C input ...

Board Bring-Up Plan — USB CO2 Air Monitor

View Document

Equipment: USB current meter or current-limited 5 V supply, DMM, oscilloscope if available, USB-C cable, computer with PlatformIO/esptool. F...

Technical Construction File Notes — USB CO2 Air Monitor

View Document

Draft notes for a USB desktop product containing an ESP32-S3 Wi-Fi/BLE module. Wireless firmware is disabled by default in the starter firmw...
Show 1 More
Completed the schematic for the USB-powered desktop CO₂ monitor and verified the schematic-focused ERC/passive/component checks are clean. Key docs are here: Schematic Verification Report, Schematic Wiring Summary, Firmware Starter, Board Bring-Up Plan, and Power Budget Analysis.
Open items:
  • PCB layout/routing is not started, so board DRC airwires are expected.
  • Generic passives still need final package/MPN selection before manufacturing.
  • ESP32 antenna keepout must be enforced during layout.
Suggested next steps:
  1. Set compact board size/stackup and place components.
  2. Route USB, I2C, power, and LED nets.
  3. Assign production MPNs/footprints to generic passives.
  4. Run full PCB DRC and prepare manufacturing outputs.
Layout and production prep
Start PCB layout
Assign passive MPNs
Route the board
Run layout DRC

Demo Complete

The demo is finished. It didn't use any of your included AI usage. This is a real Flux project now, you can continue working on it.


Firmware Starter — ESP32-S3-MINI-1-N8
Platform & Toolchain
  • MCU: U1, ESP32-S3-MINI-1-N8
  • Framework: Arduino via PlatformIO
  • Interfaces: I2C CO2 sensor/display, RGB LED, native USB CDC serial
Pin Mapping

Table


FunctionGPIOSchematic NetConnected ToDirectionNotes
I2C SDA4I2C_SDAU2 SDA, U3 SDA, R12 pull-upBidirectionalShared 3.3 V I2C bus
I2C SCL5I2C_SCLU2 SCL, U3 SCL, R13 pull-upOutput/open-drain400 kHz max for SCD41
OLED reset9OLED_RESETU3 RESETOutputActive-low reset
RGB red cathode drive6LED_RR16 -> D1 red cathodeOutputCommon-anode LED, active LOW
RGB green cathode drive7LED_GR17 -> D1 green cathodeOutputCommon-anode LED, active LOW
RGB blue cathode drive8LED_BR18 -> D1 blue cathodeOutputCommon-anode LED, active LOW
BOOT0ESP_BOOT_IO0R15 pull-up, SW3 to GND, J2Input/strapHold low during reset for download mode
USB D-19USB_DNJ1/D8USBNative USB Serial/JTAG
USB D+20USB_DPJ1/D8USBNative USB Serial/JTAG
UART0 TXTXD0UART0_TXDJ2OutputOptional debug header
UART0 RXRXD0UART0_RXDJ2InputOptional debug header
Enable/resetENESP_ENR14/C13/SW4/J2InputActive-low reset via SW4
platformio.ini

Ini


[env:esp32-s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
upload_speed = 921600
build_flags =
    -DARDUINO_USB_MODE=1
    -DARDUINO_USB_CDC_ON_BOOT=1
lib_deps =
    sensirion/Sensirion I2C SCD4x@^1.0.0
    adafruit/Adafruit SSD1306@^2.5.13
    adafruit/Adafruit GFX Library@^1.11.11
Complete Firmware Source (src/main.cpp)

Cpp


#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <SensirionI2CScd4x.h>

#define I2C_SDA_PIN        4
#define I2C_SCL_PIN        5
#define OLED_RESET_PIN     9
#define LED_RED_PIN        6
#define LED_GREEN_PIN      7
#define LED_BLUE_PIN       8
#define BOOT_PIN           0

#define SCREEN_WIDTH       128
#define SCREEN_HEIGHT      64
#define OLED_ADDR          0x3C
#define SCD41_ADDR         0x62
#define SENSOR_INTERVAL_MS 5000

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET_PIN);
SensirionI2CScd4x scd4x;

static uint32_t lastReadMs = 0;
static uint16_t lastCo2ppm = 0;
static float lastTemperatureC = NAN;
static float lastHumidityRh = NAN;

void setRgb(bool redOn, bool greenOn, bool blueOn) {
  // D1 is common-anode: LOW turns a channel on, HIGH turns it off.
  digitalWrite(LED_RED_PIN,   redOn   ? LOW : HIGH);
  digitalWrite(LED_GREEN_PIN, greenOn ? LOW : HIGH);
  digitalWrite(LED_BLUE_PIN,  blueOn  ? LOW : HIGH);
}

void setAirQualityLed(uint16_t co2ppm) {
  if (co2ppm < 800) {
    setRgb(false, true, false);   // good: green
  } else if (co2ppm <= 1200) {
    setRgb(true, true, false);    // moderate: yellow/red+green
  } else {
    setRgb(true, false, false);   // poor: red
  }
}

const char* airQualityLabel(uint16_t co2ppm) {
  if (co2ppm < 800) return "GOOD";
  if (co2ppm <= 1200) return "MODERATE";
  return "POOR";
}

void drawDisplay(uint16_t co2ppm, float tempC, float rh) {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("USB CO2 Air Monitor");

  display.setCursor(0, 18);
  display.setTextSize(2);
  display.print(co2ppm);
  display.print(" ppm");

  display.setTextSize(1);
  display.setCursor(0, 43);
  display.print("Air: ");
  display.print(airQualityLabel(co2ppm));

  display.setCursor(0, 55);
  display.print(tempC, 1);
  display.print("C  ");
  display.print(rh, 0);
  display.print("%RH");

  display.display();
}

void showError(const char* line1, const char* line2) {
  setRgb(true, false, true); // magenta = fault
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println(line1);
  display.println(line2);
  display.display();
  Serial.printf("ERROR: %s %s\n", line1, line2);
}

void initRgb() {
  pinMode(LED_RED_PIN, OUTPUT);
  pinMode(LED_GREEN_PIN, OUTPUT);
  pinMode(LED_BLUE_PIN, OUTPUT);
  setRgb(false, false, false);
}

bool initDisplay() {
  pinMode(OLED_RESET_PIN, OUTPUT);
  digitalWrite(OLED_RESET_PIN, HIGH);
  delay(10);
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
    return false;
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println("Starting CO2 monitor");
  display.display();
  return true;
}

bool initScd41() {
  scd4x.begin(Wire);

  uint16_t error;
  char errorMessage[256];

  error = scd4x.stopPeriodicMeasurement();
  delay(500);

  uint16_t serial0, serial1, serial2;
  error = scd4x.getSerialNumber(serial0, serial1, serial2);
  if (error) {
    errorToString(error, errorMessage, sizeof(errorMessage));
    Serial.printf("SCD41 getSerialNumber error: %s\n", errorMessage);
    return false;
  }
  Serial.printf("SCD41 serial: %04X-%04X-%04X\n", serial0, serial1, serial2);

  error = scd4x.startPeriodicMeasurement();
  if (error) {
    errorToString(error, errorMessage, sizeof(errorMessage));
    Serial.printf("SCD41 startPeriodicMeasurement error: %s\n", errorMessage);
    return false;
  }
  return true;
}

bool readScd41(uint16_t &co2ppm, float &tempC, float &rh) {
  uint16_t error;
  char errorMessage[256];
  bool dataReady = false;

  error = scd4x.getDataReadyFlag(dataReady);
  if (error) {
    errorToString(error, errorMessage, sizeof(errorMessage));
    Serial.printf("SCD41 dataReady error: %s\n", errorMessage);
    return false;
  }
  if (!dataReady) return false;

  error = scd4x.readMeasurement(co2ppm, tempC, rh);
  if (error) {
    errorToString(error, errorMessage, sizeof(errorMessage));
    Serial.printf("SCD41 readMeasurement error: %s\n", errorMessage);
    return false;
  }
  if (co2ppm == 0) return false;
  return true;
}

void setup() {
  Serial.begin(115200);
  delay(300);
  Serial.println("USB CO2 Air Monitor starting");

  WiFi.mode(WIFI_OFF);
  btStop();

  pinMode(BOOT_PIN, INPUT_PULLUP);
  initRgb();
  setRgb(false, false, true); // blue during startup

  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  Wire.setClock(100000);

  if (!initDisplay()) {
    Serial.println("OLED init failed");
  }

  if (!initScd41()) {
    showError("SCD41 init failed", "Check I2C/power");
  } else {
    setRgb(false, true, false);
  }
}

void loop() {
  const uint32_t now = millis();
  if (now - lastReadMs >= SENSOR_INTERVAL_MS) {
    lastReadMs = now;
    uint16_t co2;
    float tempC, rh;
    if (readScd41(co2, tempC, rh)) {
      lastCo2ppm = co2;
      lastTemperatureC = tempC;
      lastHumidityRh = rh;
      setAirQualityLed(co2);
      drawDisplay(co2, tempC, rh);
      Serial.printf("CO2=%u ppm, T=%.2f C, RH=%.2f %%\n", co2, tempC, rh);
    } else if (lastCo2ppm == 0) {
      showError("Waiting for SCD41", "warming up...");
    }
  }
}
Build & Flash
  1. Install PlatformIO.
  2. Connect USB-C to J1.
  3. Build: pio run
  4. Flash: pio run -t upload
  5. Monitor: pio device monitor
  6. If flashing fails, hold SW3 (BOOT), tap SW4 (RESET), release SW3, then retry upload.
  • Platform & Toolchain

  • Pin Mapping

  • `platformio.ini`

  • Complete Firmware Source (`src/main.cpp`)

  • Build & Flash