// 2021.2.14 co2checker.ino by S.Koyama // 2021.2.10 アラート機能を追加 // アラートをONにした場合 // CO2濃度が1000ppm以上でピピッ 1500ppm以上でピピピッ // モード2の時に WBGTが25度以上でピピッ 28度以上でピピピッ // 2021.1.15 温度REFとの差分表示 // 温度REFのSHT31(ADRジャンパでI2Cアドレス0x44)を接続すると // 冬モード時に温度インジケータの代わりにREF温度を表示し // 温度箇所に差分(補正温度-REF)を表示する→SCDOFFSET/SHTOFFSET/WIOOFFSETで補正する // CDMは温度の測定値を補正し SCDはsetTemperatureOffsetでデバイスに補正値を書き込む #define SCDOFFSET 2.2 // SCD30の温度オフセット(M5STACK) #define SHTOFFSET 1.1 // SHT31の温度オフセット(M5STACK) #define WIOOFFSET 2.2 // SCD30の温度オフセット(WIO) #ifndef WIO_TERMINAL // ----M5STACK---------- #include #endif // --------------------- #include static LGFX lcd; #include #include "Adafruit_SHT31.h" #define SHT 0x45 #define REF 0x44 Adafruit_SHT31 sht31=Adafruit_SHT31(); Adafruit_SHT31 ref31=Adafruit_SHT31(); // ----SCD30------------ #include "SparkFun_SCD30_Arduino_Library.h" SCD30 SCD; #ifndef WIO_TERMINAL // ----M5STACK---------- // ---CDM7160----------- #define CDM 0x69 #define CDM_CTL 0x01 #define CDM_ST1 0x02 #define CDM_DAL 0x03 #define CDM_DAH 0x04 #define CDM_BUSY 0x08 #define CDM_CAL 5 #endif // --------------------- #define FONT123 &fonts::Font8 #define FONTABC &fonts::Font4 #define setCursorFont(x,y,font,mag) {lcd.setCursor(x,y);lcd.setFont(font);lcd.setTextSize(mag);} #define P8 8./14. #define P10 10./14. #define P14 14./14. #define P16 16./55. #define P40 40./55. #define P48 48./55. #define isPressed(b) (S[b]==1||S[b]==2||S[b]==3) #define isReleased(b) (S[b]==0||S[b]==4||S[b]==5) #define wasPressed(b) (S[b]==1) #define wasReleased(b) (S[b]==4) #define pressedFor(b,t) (S[b]==3 && millis()>Btnmillis[b]+t) #define tempL (-20.) // 温度の有効最低値 #define AVESIZE 10 #define BUFSIZE 240 #define BRIGHTNESS 127 // 0-255 bool isSCD=false; // SCD30 or CDM7160 bool isSHT=false; // SHT31 bool isREF=false; // SHT31(REF) bool isAlert=false; // 5分毎のアラート int co2ave, humiave, pave=0, pBUF=0, ss=0; int mode=1; // 0:OFF 1:温度・湿度・CO2(冬モード) 2:WBGT・CO2(夏モード) 3:CO2グラフ 4: WBGTグラフ int co2buf[AVESIZE], humibuf[AVESIZE]; // 平均値算出用バッファ int CO2BUF[BUFSIZE], WBGTBUF[BUFSIZE]; // グラフ表示用バッファ float tempave, wbgt, tempREFave; float tempbuf[AVESIZE], wbgtbuf[AVESIZE], tempREFbuf[AVESIZE]; // 平均値算出用バッファ unsigned long lastmillis=0, Btnmillis[3]={0, 0, 0}; int S[3]={0, 0, 0}; // ボタンの状態 #ifdef WIO_TERMINAL // ----WIO-------------- #define XOF 2 // LCDのX方向オフセット(WIOは左端が見えない!) #define YOF 0 // LCDのY方向オフセット int Btnpin[3]={WIO_KEY_C, WIO_KEY_B, WIO_KEY_A}; #else // ----M5STACK---------- #define XOF 0 // LCDのX方向オフセット #define YOF 2 // LCDのY方向オフセット(M5STACKは下が見えない!) #define PowerONhour 9 // 電源ONの時間 int Btnpin[3]={BUTTON_A_PIN, BUTTON_B_PIN, BUTTON_C_PIN}; unsigned int ssON=0; // 電源ONの残り時間(秒) #endif // --------------------- void Btnread(int i){ // 0:BtnA, 1:BtnB, 2:BtnC int val=digitalRead(Btnpin[i]); if(S[i]==0 && val==LOW){ S[i]=1; Btnmillis[i]=millis(); } else if(S[i]==1) S[i]=2; else if(S[i]==2 && millis()>Btnmillis[i]+50) S[i]=3; else if(S[i]==3 && val==HIGH){S[i]=4; Btnmillis[i]=millis(); } else if(S[i]==4) S[i]=5; else if(S[i]==5 && millis()>Btnmillis[i]+50) S[i]=0; } void toneEx(int freq, int duration){ // Hz, ms #ifdef WIO_TERMINAL // ----WIO-------------- int t_us=1000000L/freq; for(long i=0; i>8); delay(duration); ledcWriteTone(TONE_PIN_CHANNEL, 0); digitalWrite(SPEAKER_PIN, 0); #endif // --------------------- } void measure(){ // 結果はco2ave, tempave, humiave, wbgt float temp=tempL-1., tempREF=tempL-1.; int co2=-1, humi=-1; if(isREF){ // REFは温度のreference tempREF=ref31.readTemperature(); // 温度 if(isnan(tempREF)) tempREF=tempL-1.; } if(isSHT){ // SHT31があれば temp=sht31.readTemperature()-SHTOFFSET; // 温度(補正値) if(isnan(temp)) temp=tempL-1.; humi=sht31.readHumidity(); // 湿度 if(isnan(humi)||humi>100||humi<0) humi=-1; } if(isSCD){ // ----SCD30------------ if(SCD.dataAvailable()){ co2=SCD.getCO2(); // CO2 if(co2>=10000||co2<200) co2=-1; // 200未満は無効 if(!isSHT){ // SHT31がなければSCDの温度・湿度 temp=SCD.getTemperature(); // 温度(デバイスで補正済) humi=SCD.getHumidity(); // 湿度 } } #ifndef WIO_TERMINAL // ----M5STACK---------- }else{ // ---CDM7160----------- Wire.beginTransmission(CDM); Wire.write(CDM_ST1); if(Wire.endTransmission()==0){ Wire.requestFrom(CDM,1); if((Wire.read()&CDM_BUSY)==0){ // CDM busyでなければデータリード Wire.beginTransmission(CDM); Wire.write(CDM_DAL); if(Wire.endTransmission()==0){ Wire.requestFrom(CDM,2); co2=Wire.read(); co2+=(Wire.read()<<8); if(co2>=10000||co2<200) co2=-1; // 200未満は無効 } } } #endif // --------------------- } // --------------------- co2buf[pave]=co2; humibuf[pave]=humi; tempbuf[pave]=temp; tempREFbuf[pave]=tempREF; // Serial.println(String(temp)+","+humi+","+co2); // 測定値の確認 int co2count=0, humicount=0, tempcount=0, tempREFcount=0; co2ave=0; humiave=0; tempave=0.; tempREFave=0.; for(int i=0; i=0){ co2ave+=co2buf[i]; co2count++; } if(humibuf[i]>=0){ humiave+=humibuf[i]; humicount++;} if(tempbuf[i]>=tempL){tempave+=tempbuf[i]; tempcount++;} if(tempREFbuf[i]>=tempL){tempREFave+=tempREFbuf[i]; tempREFcount++;} } co2ave=(co2count>0? (co2ave/co2count+5)/10*10: -1); // 直近10秒間(5~6個)の平均(1の位を四捨五入) humiave=(humicount>0? humiave/humicount: -1); // 直近10秒間(5~6個)の平均 tempave=(tempcount>0? tempave/tempcount:(tempL-1.));// 直近10秒間(5~6個)の平均 tempREFave=(tempREFcount>0? tempREFave/tempREFcount:(tempL-1.)); // ***** //----------------------------------------------------------------------------------- // co2ave=1270; tempave=26.3; humiave=63; // 撮影用 // co2ave=random(0,5000)/10*10;tempave=random(-20*10,40*10)/10.;humiave=random(10,100);humicount=tempcount=1; // 表示チェック用 //----------------------------------------------------------------------------------- if(humicount>0 && tempcount>0){ // WBGTの計算(日本生気象学会の表) wbgt=-1.7+.693*tempave+.0388*humiave+.00355*humiave*tempave; }else wbgt=-1.; wbgtbuf[pave]=wbgt; pave=(pave+1)%AVESIZE; // pave: 0~9 if(ss%15==0){ // 15秒毎 CO2BUF[pBUF]=co2ave; WBGTBUF[pBUF]=wbgt;// バッファに保存(グラフ表示用) pBUF=(pBUF+1)%BUFSIZE; // pBUF: 0~239(1H) } } int yco2(int co2){ // co2値のy座標 int y=239-co2*2/25-YOF; return (y<0?0:y); } int ywbgt(float val){ // wbgt値のy座標 int y=239-(int)((val-10.)*8.)-YOF; if(y>=240) y=239-YOF; return (y<0?0:y); } int co2Color(int co2){ // co2値の表示色 if(co2>=1500) return TFT_RED; // 1500~ else if(co2>=1000)return TFT_YELLOW; // 1000~1500 else if(co2>=0) return TFT_GREEN; // ~1000 else return TFT_BLACK; } int wbgtColor(float val){ // wbgt値の表示色 if(val>=31.) return TFT_RED; // 31~ else if(val>=28.) return TFT_ORANGE; // 28~31 else if(val>=25.) return TFT_YELLOW; // 25~28 else if(val>=0.) return TFT_GREEN; // 0~25 else return TFT_BLACK; } int humiColor(int val){ // humi値の表示色 if(val>=80) return TFT_CYAN; // 80~ else if(val>=30) return TFT_GREEN; // 30~80 else if(val>=0) return TFT_WHITE; // 0~30 else return TFT_BLACK; } int tempColor(float val){ // temp値の表示色 if(val>=28.) return TFT_ORANGE; // 28~ else if(val>=17.) return TFT_GREEN; // 17~28 else if(val>=tempL) return TFT_CYAN; // -20~17 else return TFT_BLACK; } void co2print(){ // CO2表示 --------- if(mode==1){ // 温度・湿度・CO2(冬モード) setCursorFont(132,174, FONT123,P40); if(co2ave>=100) lcd.printf(co2ave<1000?"%5d":"%4d",co2ave); else lcd.print(" - "); // 8 setCursorFont(260,144, FONTABC,P14); lcd.print("ppm"); lcd.fillRect(0,164,110,76,co2Color(co2ave)); // インジケータ }else if(mode==2){ // WBGT・CO2(夏モード) setCursorFont(120,150, FONT123,P48); if(co2ave>=100) lcd.printf(co2ave<1000?"%5d":"%4d",co2ave); else lcd.print(" - "); // 8 setCursorFont(260,120, FONTABC,P14); lcd.print("ppm"); lcd.fillRect(0,123,110,116,co2Color(co2ave)); // インジケータ }else if(mode==3){ // CO2グラフ setCursorFont(241+XOF,10, FONTABC,P14); lcd.print(" CO2"); setCursorFont(241+XOF,80, FONT123,P16); if(co2ave>=100) lcd.printf(co2ave<1000?"%5d":"%4d",co2ave); else lcd.print(" - "); // 8 setCursorFont(280,60, FONTABC,P10); lcd.print("ppm"); } } void wbgtprint(){ // WBGT表示 -------- if(mode==2){ // WBGT・CO2(夏モード) setCursorFont(138,24, FONT123,P48); if(wbgt>=0.) lcd.printf(wbgt<10.?"%5.1f":"%4.1f",wbgt); else lcd.print(" - "); // 7 setCursorFont(296,0, FONTABC,P14); lcd.print("C"); lcd.fillRect(0, 0,110,116,wbgtColor(wbgt)); // インジケータ }else if(mode==4){ // WBGTグラフ setCursorFont(241+XOF,10, FONTABC,P14); lcd.print("WBGT"); setCursorFont(241+XOF,80, FONT123,P16); if(wbgt>=0.) lcd.printf(wbgt<10.?"%5.1f":"%4.1f",wbgt); else lcd.print(" - "); // 7 setCursorFont(300,70, FONTABC,P10); lcd.print("C"); } } void tempprint(){ // 温度表示 -------- setCursorFont(128,10, FONT123,P40); if(tempave>=tempL){ if(isREF) lcd.printf(tempave-tempREFave>-10.&&tempave-tempREFave<10.?"%6.1f":"%5.1f",tempave-tempREFave); // REFがあればtempとの差分を表示 else lcd.printf(tempave>-10.&&tempave<10.?"%6.1f":"%5.1f",tempave); // なければ温度 }else lcd.print(" - "); // 8 setCursorFont(296,0, FONTABC,P14); lcd.print("C"); setCursorFont(0,10, FONT123,P40); if(isREF) lcd.printf(tempREFave<10.?"%5.1f":"%4.1f",tempREFave); // REFがあればtempREF else lcd.fillRect(0,0,110,76,tempColor(tempave)); // なければインジケータ } void humiprint(){ // 湿度表示 -------- setCursorFont(150,92, FONT123,P40); if(humiave>=0) lcd.printf("%2d",humiave); else lcd.print(" - "); // 4 setCursorFont(260,82, FONTABC,P14); lcd.print("%RH"); lcd.fillRect(0,82,110,76,humiColor(humiave)); // インジケータ } void co2graph(){ lcd.setFont(FONTABC); lcd.setTextSize(P8); for(int i=0; i<=BUFSIZE; i++){ if(i%(10*4)==0){ // 縦の補助線 lcd.drawFastVLine(i+XOF,0, 239-YOF, (i==0?TFT_WHITE:TFT_DARKGREY)); if(i==40) for(int co2=1000; co2<=3000; co2+=1000){ lcd.setCursor(1+XOF, yco2(co2)+1); lcd.print(co2); } }else{ int co2=CO2BUF[(pBUF+i)%BUFSIZE]; if(co2>0){ // plot co2!! lcd.drawFastVLine(i+XOF,1, yco2(co2), TFT_BLACK); lcd.drawFastVLine(i+XOF,yco2(co2), 238-yco2(co2), co2Color(co2)); }else lcd.drawFastVLine(i+XOF,1, 238-YOF, TFT_BLACK); // 無効データ for(int u=0; u<=3000; u+=500){ if(u==0) lcd.drawPixel(i+XOF,yco2(u), TFT_WHITE); // X軸 else if(u==1000&&co20.){ // plot wbgt!! lcd.drawFastVLine(i+XOF,1, ywbgt(val), TFT_BLACK); lcd.drawFastVLine(i+XOF,ywbgt(val), 238-ywbgt(val), wbgtColor(val)); }else lcd.drawFastVLine(i+XOF,1, 238-YOF, TFT_BLACK); // 無効データ for(int u=10; u<=40; u+=10){ if(u==10) lcd.drawPixel(i+XOF,ywbgt(u), TFT_WHITE); // X軸 else lcd.drawPixel(i+XOF,ywbgt(u), TFT_DARKGREY); // 20,30,40度 lcd.drawPixel(i+XOF,ywbgt(25), (val<25?TFT_YELLOW:TFT_DARKGREY)); // 25度 lcd.drawPixel(i+XOF,ywbgt(28), (val<28?TFT_ORANGE:TFT_DARKGREY)); // 28度 lcd.drawPixel(i+XOF,ywbgt(31), (val<31?TFT_RED:TFT_DARKGREY)); // 31度 } } } } void setup(){ for(int i=0;i<3;i++) pinMode(Btnpin[i], INPUT_PULLUP); #ifdef WIO_TERMINAL // ----WIO-------------- pinMode(WIO_BUZZER, OUTPUT); pinMode(WIO_LIGHT, INPUT); #else // ----M5STACK---------- pinMode(CDM_CAL, OUTPUT); // G2ピンをCDMのcalに接続 digitalWrite(CDM_CAL, HIGH); // 通常HIGH M5.begin(); M5.Power.begin(); if(M5.Power.canControl()) M5.Power.setPowerBoostSet(true); ledcSetup(TONE_PIN_CHANNEL, 0, 10); ledcAttachPin(SPEAKER_PIN, TONE_PIN_CHANNEL); #endif // --------------------- lcd.init(); lcd.setRotation(1); lcd.clear(); lcd.setBrightness(BRIGHTNESS); // 0-255 lcd.setFont(FONT123); lcd.setTextColor(TFT_WHITE, TFT_BLACK); Wire.begin(); isSCD=SCD.begin(); // SCD30の有無 isSHT=sht31.begin(SHT); // SHT31の有無 isREF=ref31.begin(REF); // SHT31(REF)の有無 #ifdef WIO_TERMINAL // ----WIO-------------- if(isSCD) SCD.setTemperatureOffset(WIOOFFSET); #else // ----M5STACK---------- if(isSCD) SCD.setTemperatureOffset(SCDOFFSET); else{ // ---CDM7160----------- Wire.beginTransmission(CDM); Wire.write(CDM_CTL); Wire.write(0x06); Wire.endTransmission(); } // --------------------- Btnread(0); // BtnA while(isPressed(0)){ // BtnA オフまで待つ Btnread(0); // BtnA if(pressedFor(0, 1000)){ // BtnAが1秒押されたら toneEx(1000, 50); // ピッ lcd.clear(); setCursorFont(0,50, FONTABC,2); lcd.print(" Power OFF\n after 9H"); delay(2000); lcd.clear(); while(isPressed(0)) Btnread(0); // BtnA オフまで待つ ssON=PowerONhour*60*60; // オン時間は9H } } #endif // --------------------- for(int i=0; ilastmillis+1000){ // 1秒ごとに lastmillis=millis(); measure(); // 結果はco2ave, tempave, humiave, wbgt if(mode==1){ // mode 1:温度・湿度・CO2(冬モード) tempprint(); // 温度は1秒毎に表示更新 humiprint(); // 湿度は1秒毎に表示更新 if(ss%5==0) co2print(); // co2値は5秒毎に表示更新 }else if(mode==2){ // mode 2:WBGT・CO2(夏モード) wbgtprint(); // wbgt値は1秒毎に表示更新 if(ss%5==0) co2print(); // co2値は5秒毎に表示更新 }else if(mode==3 || mode==4){ // mode 3:co2グラフ 4:wbgtグラフ wbgtprint(); // wbgt値を表示更新 if(ss%5==0) co2print(); // co2値は5秒毎に表示更新 if(mode==3 && ss%15==0) co2graph(); // co2グラフは15秒毎に表示更新 if(mode==4 && ss%15==0) wbgtgraph(); // wbgtグラフは15秒毎に表示更新 } if(++ss%300==0){ // 300秒(5分)毎に ss=0; if(isAlert) // AlertがONなら if(co2ave>=1500 || mode==2&&wbgt>=28){ // ピピピッ toneEx(1000, 50); delay(100); toneEx(1000, 50); delay(100); toneEx(1000, 50); delay(100); }else if(co2ave>=1000 || mode==2&&wbgt>=25){ // ピピッ toneEx(1000, 50); delay(100); toneEx(1000, 50); delay(100); } } #ifndef WIO_TERMINAL // ----M5STACK---------- if(ssON>0 && --ssON==0) M5.Power.deepSleep(); // 電源オフ(deepsleep) if(ssON>0){ // 電源オンの残り時間 8×16 setCursorFont(280,220, FONTABC,.75); lcd.printf("%d:%02d",ssON/60/60,ssON/60%60); } #endif // --------------------- } Btnread(0); // BtnA #ifndef WIO_TERMINAL // ----M5STACK---------- if(pressedFor(0, 1000)){ // BtnAが1秒押されたら lcd.clear(); lcd.setBrightness(BRIGHTNESS); // 0-255 setCursorFont(40,80, FONTABC,2); lcd.print("Power OFF"); // 28×52 toneEx(1000, 50); // ピッ while(isPressed(0)) Btnread(0); // BtnAオフまで待つ delay(1000); M5.Power.deepSleep(); // 電源オフ(deepsleep) }else #endif // --------------------- if(wasReleased(0)){ // BtnAが押されたら mode=(mode+1)%5; // 表示モード切替 0-4 for(int i=0; i