#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1351.h>
#include <Adafruit_SCD30.h>
#include <Adafruit_PM25AQI.h>
#include <string.h>
#define SKIP_I2C_SCAN 0 // 1 = skip scan screen
#define I2C_SCAN_MS 1200 // time-limited scan (ms)
#define FIRST_SAMPLE_MS 30000UL // first graph point ~30s after boot
#define OLED_CS 10
#define OLED_DC 9
#define OLED_RST 8
#define SW 128
#define SH 128
Adafruit_SSD1351 display(SW, SH, &SPI, OLED_CS, OLED_DC, OLED_RST);
Adafruit_SCD30 scd30;
Adafruit_PM25AQI pm25;
#define C_BG display.color565(0,0,0)
#define C_FG display.color565(255,255,255)
#define C_GRID display.color565(70,70,70)
#define C_VGRID display.color565(40,40,40)
#define C_CO2 display.color565(0,200,255) // cyan
#define C_PM25 display.color565(170,0,255) // purple
#define C_TEMP display.color565(255,140,0) // orange
#define C_RH display.color565(0,180,0) // green
#define C_OK display.color565(0,180,0)
#define C_ERR display.color565(200,0,0)
const int HEADER_H = 18;
const int TOP_H = 56;
const int TOP_Y = HEADER_H;
const int Y_CO2 = TOP_Y + 6;
const int Y_PM = TOP_Y + 20;
const int Y_T = TOP_Y + 34;
const int Y_H = TOP_Y + 48;
const int LABEL_X = 6;
const int VALUE_X = 70;
const int DOT_X = 46;
const int RIGHT_MARGIN = 0;
const int TOP_PAD = 2;
const int BOT_PAD = 2;
const int SEPARATOR_GAP = 8;
const int GRAPH_Y = HEADER_H + TOP_H + SEPARATOR_GAP;
const int GRAPH_H = SH - GRAPH_Y - 2;
const int GRAPH_W = SW - RIGHT_MARGIN;
template<typename T> T ema(T p, T s, float a){ return p + a*(s - p); }
float co2_f=400, pm25_f=0, t_f=25, rh_f=40;
float co2_raw=400, t_raw=25, rh_raw=40;
float pm25_last = 0, pm10_last = 0;
bool scd_ok=false, scd_have=false;
bool pm_ready=false, pm_have=false;
uint8_t pm_fail_streak=0;
static uint8_t yCO2[GRAPH_W], yPM[GRAPH_W], yT[GRAPH_W], yRH[GRAPH_W];
static int nPoints=0;
unsigned long lastUI = 0;
const unsigned long UI_PERIOD_MS = 800;
const unsigned long GRAPH_PERIOD_MS = 180000UL;
unsigned long lastGraph = 0;
static inline float trunc1(float x){ return (int)(x*10.0f)/10.0f; }
int aqiFromPM25(float ug){
float c=trunc1(ug);
struct BP{ float Cl,Ch; int Il,Ih; };
const BP T[]={
{0.0,12.0,0,50},{12.1,35.4,51,100},{35.5,55.4,101,150},
{55.5,150.4,151,200},{150.5,250.4,201,300},
{250.5,350.4,301,400},{350.5,500.4,401,500}
};
for(auto &b:T) if(c>=b.Cl && c<=b.Ch)
return (int) (((b.Ih-b.Il)*(c-b.Cl))/(b.Ch-b.Cl) + b.Il + 0.5f);
return 500;
}
int aqiFromPM10(float ug){
float c = ug;
struct BP{ float Cl,Ch; int Il,Ih; };
const BP T[]={
{0, 54, 0, 50},
{55, 154, 51, 100},
{155,254, 101, 150},
{255,354, 151, 200},
{355,424, 201, 300},
{425,504, 301, 400},
{505,604, 401, 500}
};
for(auto &b:T) if(c>=b.Cl && c<=b.Ch)
return (int) (((b.Ih-b.Il)*(c-b.Cl))/(b.Ch-b.Cl) + b.Il + 0.5f);
return 500;
}
const char* aqiLabel(int aqi){
if (aqi<=50) return "Good";
if (aqi<=100) return "Moderate";
if (aqi<=150) return "Unhlth-SG";
if (aqi<=200) return "Unhealthy";
if (aqi<=300) return "Very Unh";
return "Hazardous";
}
void centerText(int y, const char* txt, uint16_t col, uint8_t sz=2){
display.setTextSize(sz);
int16_t x1,y1; uint16_t w,h;
display.getTextBounds((char*)txt, 0,0, &x1,&y1,&w,&h);
int x=(SW-(int)w)/2;
display.setTextColor(col);
display.setCursor(x,y);
display.print(txt);
}
void splash(){
display.fillScreen(C_BG);
display.setTextWrap(false);
centerText(30, "See02", C_FG, 3);
centerText(68, "see what you breath", display.color565(180,180,180), 1);
delay(2200);
}
void showI2CScan(){
if (SKIP_I2C_SCAN) return;
display.fillScreen(C_BG);
display.setTextColor(C_FG); display.setTextSize(1);
display.setCursor(4, 6); display.print(F("I2C scan (safe)"));
bool found12=false, found61=false;
uint8_t row=0, col=0;
unsigned long t0 = millis();
auto probe = [&](uint8_t addr){
Wire.beginTransmission(addr);
uint8_t ok = (Wire.endTransmission()==0);
if (ok){
char buf[8]; snprintf(buf, sizeof(buf), " %02X", addr);
display.setCursor(6 + col*24, 22 + row*12);
display.print(buf);
col++; if (col>=5){ col=0; row++; }
if (addr==0x12) found12=true;
if (addr==0x61) found61=true;
}
};
probe(0x12); probe(0x61);
for (uint8_t addr=0x03; addr<=0x77; addr++){
if (addr==0x12 || addr==0x61) continue;
if ((millis()-t0) > I2C_SCAN_MS) break;
probe(addr);
}
int y = 22 + (row+1)*12 + 6;
display.setCursor(6, y); display.print(F("PM @12: ")); display.fillCircle(56, y-2, 3, found12? C_OK : C_ERR);
display.setCursor(70, y); display.print(F("SCD@61: ")); display.fillCircle(120, y-2, 3, found61? C_OK : C_ERR);
delay(900);
}
void drawHeaderAQI(){
display.fillRect(0, 0, SW, HEADER_H, C_BG);
display.setTextColor(C_FG);
display.setTextSize(1);
display.setCursor(4, 5);
if (pm_have){
int aqi25 = aqiFromPM25(pm25_last);
int aqi10 = aqiFromPM10(pm10_last);
int aqi = (aqi25>aqi10)? aqi25 : aqi10;
display.print(F("AQI: ")); display.print(aqi); display.print(" "); display.print(aqiLabel(aqi));
} else {
display.print(F("AQI: --"));
}
}
void drawTopStaticLabels(){
display.setTextWrap(false);
display.setTextColor(C_CO2); display.setCursor(LABEL_X, Y_CO2); display.print(F("CO2"));
display.setTextColor(C_PM25); display.setCursor(LABEL_X, Y_PM); display.print(F("PM2.5"));
display.setTextColor(C_TEMP); display.setCursor(LABEL_X, Y_T); display.print(F("TEMP"));
display.setTextColor(C_RH); display.setCursor(LABEL_X, Y_H); display.print(F("HUM"));
display.setTextColor(C_FG);
uint16_t colSCD = scd_have ? C_OK : C_ERR;
uint16_t colPM = pm_have ? C_OK : C_ERR;
display.fillCircle(DOT_X, Y_CO2-3, 2, colSCD);
display.fillCircle(DOT_X, Y_T -3, 2, colSCD);
display.fillCircle(DOT_X, Y_H -3, 2, colSCD);
display.fillCircle(DOT_X, Y_PM -3, 2, colPM );
}
void drawTopValuesText(const char* co2Txt, const char* pmTxt, const char* tTxt, const char* hTxt){
display.fillRect(0, TOP_Y, SW, TOP_H, C_BG);
drawHeaderAQI();
drawTopStaticLabels();
display.setTextColor(C_FG);
display.setTextSize(1);
display.setCursor(VALUE_X, Y_CO2); display.print(co2Txt);
display.setCursor(VALUE_X, Y_PM ); display.print(pmTxt);
display.setCursor(VALUE_X, Y_T ); display.print(tTxt);
display.setCursor(VALUE_X, Y_H ); display.print(hTxt);
}
inline int baseY(){ return GRAPH_Y + GRAPH_H - 1; }
inline uint8_t clampY(int y){ int t=GRAPH_Y+TOP_PAD, b=baseY()-BOT_PAD; if(y<t) y=t; if(y>b) y=b; return (uint8_t)y; }
inline uint8_t mapCO2_Y(float v){ if(v<400)v=400; if(v>2000)v=2000; float usable=(GRAPH_H-1-TOP_PAD-BOT_PAD); int y=(baseY()-BOT_PAD)-(int)((v-400.0f)*usable/1600.0f+0.5f); return clampY(y);}
inline uint8_t mapPM_Y (float v){ if(v<0)v=0; if(v>150) v=150; float usable=(GRAPH_H-1-TOP_PAD-BOT_PAD); int y=(baseY()-BOT_PAD)-(int)((v)*usable/150.0f+0.5f); return clampY(y);}
inline uint8_t mapT_Y (float v){ if(v<0)v=0; if(v>40) v=40; float usable=(GRAPH_H-1-TOP_PAD-BOT_PAD); int y=(baseY()-BOT_PAD)-(int)((v)*usable/40.0f+0.5f); return clampY(y);}
inline uint8_t mapRH_Y (float v){ if(v<0)v=0; if(v>100) v=100; float usable=(GRAPH_H-1-TOP_PAD-BOT_PAD); int y=(baseY()-BOT_PAD)-(int)((v)*usable/100.0f+0.5f); return clampY(y);}
void drawGrid(){
display.fillRect(0, GRAPH_Y, GRAPH_W, GRAPH_H, C_BG);
int usable = GRAPH_H - TOP_PAD - BOT_PAD;
for (int i=1; i<=3; ++i){ int y=(baseY()-BOT_PAD)-(usable*i)/4; for (int x=0; x<GRAPH_W; x+=3) display.drawPixel(x,y,C_GRID); }
for (int x=0; x<GRAPH_W; x+=16) display.drawFastVLine(x, GRAPH_Y+TOP_PAD, usable, C_VGRID);
display.fillRect(0, GRAPH_Y - 1, SW, 1, C_BG);
}
void renderGraph(){
drawGrid();
if (nPoints == 0) return;
if (nPoints == 1){
display.drawPixel(0, yCO2[0], C_CO2);
display.drawPixel(0, yPM [0], C_PM25);
display.drawPixel(0, yT [0], C_TEMP);
display.drawPixel(0, yRH [0], C_RH);
} else {
for (int i=1; i<nPoints; ++i){
display.drawLine(i-1, yCO2[i-1], i, yCO2[i], C_CO2);
display.drawLine(i-1, yPM [i-1], i, yPM [i], C_PM25);
display.drawLine(i-1, yT [i-1], i, yT [i], C_TEMP);
display.drawLine(i-1, yRH [i-1], i, yRH [i], C_RH);
}
}
int lx = (nPoints-1);
display.drawPixel(lx, yCO2[lx], C_CO2);
display.drawPixel(lx, yPM [lx], C_PM25);
display.drawPixel(lx, yT [lx], C_TEMP);
display.drawPixel(lx, yRH [lx], C_RH);
}
void pushSample(float co2, float pm, float tC, float rh){
uint8_t yc=mapCO2_Y(co2), yp=mapPM_Y(pm), yt=mapT_Y(tC), yh=mapRH_Y(rh);
if (nPoints < GRAPH_W){
yCO2[nPoints]=yc; yPM[nPoints]=yp; yT[nPoints]=yt; yRH[nPoints]=yh; nPoints++;
} else {
memmove(&yCO2[0], &yCO2[1], GRAPH_W-1);
memmove(&yPM [0], &yPM [1], GRAPH_W-1);
memmove(&yT [0], &yT [1], GRAPH_W-1);
memmove(&yRH [0], &yRH [1], GRAPH_W-1);
yCO2[GRAPH_W-1]=yc; yPM[GRAPH_W-1]=yp; yT[GRAPH_W-1]=yt; yRH[GRAPH_W-1]=yh;
}
renderGraph();
}
bool initPMSA003I_I2C(){ delay(800); for (uint8_t i=0;i<6;i++){ if (pm25.begin_I2C()) return true; delay(220);} return false; }
void refreshUI(){
char co2Txt[20]="--", pmTxt[20]="--", tTxt[20]="--", hTxt[20]="--";
if (scd_have){
snprintf(co2Txt,sizeof(co2Txt), "%u ppm", (unsigned)(co2_f+0.5f));
char tb[10]; dtostrf(t_f,0,1,tb); snprintf(tTxt,sizeof(tTxt), "%s C", tb);
snprintf(hTxt,sizeof(hTxt), "%u %%", (unsigned)(rh_f+0.5f));
}
if (pm_have){
snprintf(pmTxt,sizeof(pmTxt), "%u ug/m3", (unsigned)(pm25_f+0.5f));
}
drawTopValuesText(co2Txt, pmTxt, tTxt, hTxt);
}
unsigned long tPM=0;
void setup(){
Wire.begin();
Wire.setClock(100000);
#if defined(WIRE_HAS_TIMEOUT)
Wire.setWireTimeout(250, true);
#endif
display.begin(); display.setTextWrap(false);
splash();
showI2CScan();
scd_ok = scd30.begin(); if (scd_ok) scd30.setMeasurementInterval(2);
pm_ready = initPMSA003I_I2C();
memset(yCO2,0,sizeof(yCO2)); memset(yPM,0,sizeof(yPM)); memset(yT,0,sizeof(yT)); memset(yRH,0,sizeof(yRH)); nPoints=0;
display.fillScreen(C_BG);
refreshUI();
renderGraph();
lastUI = millis();
lastGraph = millis() - (GRAPH_PERIOD_MS - FIRST_SAMPLE_MS);
}
void loop(){
if (pm_ready && millis() - tPM >= 1000){
tPM = millis(); PM25_AQI_Data d;
if (pm25.read(&d)){
pm25_last = d.pm25_env;
pm10_last = d.pm10_env;
pm25_f = ema(pm25_f, pm25_last, 0.18f);
pm_have = true; pm_fail_streak=0;
} else {
if (pm_fail_streak<10) pm_fail_streak++;
if (pm_fail_streak>=10){ pm_ready=false; pm_have=false; }
}
}
if (scd_ok && scd30.dataReady() && scd30.read()){
scd_have=true;
co2_raw = scd30.CO2; t_raw = scd30.temperature; rh_raw = scd30.relative_humidity;
co2_f=ema(co2_f, co2_raw, 0.15f);
t_f =ema(t_f , t_raw , 0.15f);
rh_f =ema(rh_f , rh_raw, 0.15f);
}
if (millis() - lastUI >= UI_PERIOD_MS){
refreshUI();
lastUI = millis();
}
if (millis() - lastGraph >= GRAPH_PERIOD_MS){
float co2_snap = scd_have ? co2_raw : 400.0f;
float pm_snap = pm_have ? pm25_last : 0.0f;
float t_snap = scd_have ? t_raw : 22.0f;
float rh_snap = scd_have ? rh_raw : 45.0f;
pushSample(co2_snap, pm_snap, t_snap, rh_snap);
lastGraph = millis();
}
}