CO2 Meter
Firmware Starter — ESP32-S3-MINI-1-N8
Schematic Verification Report — USB CO2 Air Monitor
Board Bring-Up Plan — USB CO2 Air Monitor
Technical Construction File Notes — USB CO2 Air Monitor
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.
Table
| Function | GPIO | Schematic Net | Connected To | Direction | Notes |
|---|---|---|---|---|---|
| I2C SDA | 4 | I2C_SDA | U2 SDA, U3 SDA, R12 pull-up | Bidirectional | Shared 3.3 V I2C bus |
| I2C SCL | 5 | I2C_SCL | U2 SCL, U3 SCL, R13 pull-up | Output/open-drain | 400 kHz max for SCD41 |
| OLED reset | 9 | OLED_RESET | U3 RESET | Output | Active-low reset |
| RGB red cathode drive | 6 | LED_R | R16 -> D1 red cathode | Output | Common-anode LED, active LOW |
| RGB green cathode drive | 7 | LED_G | R17 -> D1 green cathode | Output | Common-anode LED, active LOW |
| RGB blue cathode drive | 8 | LED_B | R18 -> D1 blue cathode | Output | Common-anode LED, active LOW |
| BOOT | 0 | ESP_BOOT_IO0 | R15 pull-up, SW3 to GND, J2 | Input/strap | Hold low during reset for download mode |
| USB D- | 19 | USB_DN | J1/D8 | USB | Native USB Serial/JTAG |
| USB D+ | 20 | USB_DP | J1/D8 | USB | Native USB Serial/JTAG |
| UART0 TX | TXD0 | UART0_TXD | J2 | Output | Optional debug header |
| UART0 RX | RXD0 | UART0_RXD | J2 | Input | Optional debug header |
| Enable/reset | EN | ESP_EN | R14/C13/SW4/J2 | Input | Active-low reset via SW4 |
platformio.iniIni
[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
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..."); } } }
pio runpio run -t uploadpio device monitorPlatform & Toolchain
Pin Mapping
`platformio.ini`
Complete Firmware Source (`src/main.cpp`)
Build & Flash