/* 暑さ指数を表示する (熱中症指数表示器) ----------------------------------- ESP32 及び TENSTOR ROBO 2.8' TFT (7789V)を使用 2025 Jul. S.S ----------------------------------- 28.Jul.2025 Ver2 夜間は表示を暗くする 23.Jul.2025 グラフ表示を追加 22.JUL.2025 センサをDHT22に変更 */ #include #include #include #include #include //#include //#include #include // time #include // time() ctime() #include // struct timeval //#include //#include "NTP_Time.h" // Attached to this sketch, see that tab for library needs //#include // settimeofday_cb() //#include "WeatherStationImages.h" #include "DHTesp.h" #include #include #include #define DHT0 27 // 2.8'TFT // TFT接続(VSPI MOSI(SDA)=23,[MISO=19],CLK(SCL)=18,CS=5,RSt=4),DC=15,BL=22 // バックライトは常時ON:BL→Vcc D23 #define TFT_CS 5 #define TFT_RST 4 #define TFT_DC 15 #define TFT_BL 22 Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST); /*************************** * Begin Settings **************************/ // WIFI const char* WIFI_SSID = "SSID"; const char* WIFI_PWD = "Password"; #define TZ 9 // (utc+) TZ in hours #define DST_MN 0 // use 60mn for summer time in some countries // OpenWeatherMap Settings // Sign up here to get an API key: // https://docs.thingpulse.com/how-tos/openweathermap-key/ String OPEN_WEATHER_MAP_APP_ID = "xxxxxxxxxxxxxxxxxxxxxxxxx"; /* Go to https://openweathermap.org/find?q= and search for a location. Go through the result set and select the entry closest to the actual location you want to display data for. It'll be a URL like https://openweathermap.org/city/2657896. The number at the end is what you assign to the constant below. */ String OPEN_WEATHER_MAP_LOCATION_ID = "1856057"; //Nagoya,JA // Pick a language code from this list: String OPEN_WEATHER_MAP_LANGUAGE = "en"; const uint8_t MAX_FORECASTS = 4; const boolean IS_METRIC = true; // Adjust according to your language const String WDAY_NAMES[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; const String MONTH_NAMES[] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; // 画面位置 /* ** HIGHT = 320 ** <-width = 240 --> +---------------+ ! WBGT now ! +---------------+ ---> TTL ! XX C XX % ! +---------------+ ---> REAL_ENV ! at SUNLIGHT ! ! ! ---> OUTDOOR ! XX.X ! ! Strict Warning! +---------------+ ---> INDOOR ! at INDOOR ! ! XX.X ! ! Warning ! +---------------+ ---> BTM_INFO ! xx, xx/xx/2025! +---------------+ +----+----+----+----+----+ ! 40 ! 40 ! 40 ! 40 ! 40 ! ---> Status Graphic(Hight=10) +----+----+----+----+----+ */ // 画面位置の定義(Y軸) #define TTL 20 #define REAL_ENV 36 #define OUTDOOR 68 #define INDOOR 180 #define BTM_INFO 292 /*************************** * End Settings **************************/ //OpenWeatherMapCurrentData currentWeather; //OpenWeatherMapCurrent currentWeatherClient; //OpenWeatherMapForecastData forecasts[MAX_FORECASTS]; //OpenWeatherMapForecast forecastClient; // WBGT表示関連の変数 float RH = 0.; float Ta = 0.; float wbgt = 0.; float SR = 0.2; int WS = 4; uint16_t Wcolor = ST77XX_WHITE; String Wmsg; #define SUNLIGHT INDOOR-9 #define ROOM BTM_INFO-9 #define TZ_MN ((TZ)*60) #define TZ_SEC ((TZ)*3600) #define DST_SEC ((DST_MN)*60) time_t now; bool night_flg = false; // 夜間フラグ( true=夜間、false=昼間) // DHT22 Settings DHTesp dht; /*************************************************************************************** ** Draw the clock digits ***************************************************************************************/ void drawDateTime() { now = time(nullptr); struct tm* timeInfo; timeInfo = localtime(&now); char buff[20]; tft.setFont(); //tft.setFont(&FreeSerifBold18pt7b); tft.setTextSize(1); tft.setTextColor(ST77XX_CYAN); tft.setCursor(80, BTM_INFO+4); // NTPから「曜日、月・日・年」を得る String date = WDAY_NAMES[timeInfo->tm_wday]; sprintf_P(buff, PSTR("%s, %02d/%02d/%04d "), WDAY_NAMES[timeInfo->tm_wday].c_str(), timeInfo->tm_mon+1, timeInfo->tm_mday, timeInfo->tm_year + 1900); //sprintf_P(buff, PSTR("%s, %02d/%02d/%04d "), WDAY_NAMES[timeInfo->tm_wday].c_str(), timeInfo->tm_mday, timeInfo->tm_mon+1, timeInfo->tm_year + 1900); tft.print(String(buff)); //Serial.print(String(buff)); // 「時︰分」 sprintf_P(buff, PSTR("%02d:%02d"), timeInfo->tm_hour, timeInfo->tm_min); if ((timeInfo->tm_hour > 20) || (timeInfo->tm_hour < 6)) night_flg = true; // 20時から6時までは夜とする(V2) else night_flg = true; // 背景=黒の方が見易そうなので、常にナイトモードに… //else night_flg = false; //sprintf_P(buff, PSTR("%02d:%02d:%02d"), timeInfo->tm_hour, timeInfo->tm_min, timeInfo->tm_sec); //Serial.println(String(buff)); //tft.setCursor(0, 310); tft.print(String(buff)); //display->drawString(64 + x, 15 + y, String(buff)); //display->setTextAlignment(TEXT_ALIGN_LEFT); } //--------------------------------------------------------------- // 温湿度センサからデータを読み込む //--------------------------------------------------------------- void updateDHT() { //void updateDHT(OLEDDisplay *display) { RH = dht.getHumidity(); Ta = dht.getTemperature(); Serial.print("Temperature: "); Serial.print(Ta, 1); Serial.print(" Humidity: "); Serial.println(RH, 1); delay(50); } // // タイトルと背景を表示 // ST77XXは16bitカラー r=5/g=6/b=5 bitで表現 // void title(){ //drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color);0xfdf9 if ( night_flg ) tft.fillRect(0, 0, 240, 320, ST77XX_BLACK); else tft.fillRect(0, 0, 240, 30, ST77XX_WHITE); // TITLE tft.setCursor(30, TTL); tft.setFont(&FreeSerifBold18pt7b); tft.setTextSize(1); tft.setTextColor(ST77XX_BLUE); tft.print("WBGT now"); // 現在の気温・湿度の背景 tft.fillRect(0, REAL_ENV, 240, OUTDOOR, ST77XX_BLACK); //tft.fillRect(0, OUTDOOR, 240, INDOOR, 0xe79c); // 室外熱中症指数の背景 if ( !night_flg ) tft.fillRect(0, OUTDOOR, 240, INDOOR, ST77XX_WHITE); // 屋内熱中症指数の背景 if ( !night_flg ) tft.fillRect(0, INDOOR, 240, BTM_INFO, 0xc718); //tft.fillRect(0, INDOOR, 240, BTM_INFO, 0xfdf9); // 時刻の背景 tft.fillRect(0, BTM_INFO, 240, 319, ST77XX_BLACK); tft.drawFastHLine(0, OUTDOOR, 240, ST77XX_BLUE); tft.drawFastHLine(0, INDOOR, 240, ST77XX_BLUE); tft.drawFastHLine(0, BTM_INFO, 240, ST77XX_BLUE); // 区切り線 tft.drawFastHLine(20, INDOOR-10, 200, ST77XX_BLUE); tft.drawFastHLine(20, BTM_INFO-10, 200, ST77XX_BLUE); for ( int i=0; i<6; i++ ){ tft.drawFastVLine(20+i*40, INDOOR-10, 10, ST77XX_BLUE); } for ( int i=0; i<6; i++ ){ tft.drawFastVLine(20+i*40, BTM_INFO-10, 10, ST77XX_BLUE); } } //--------------------------------------------------------------- // 温湿度を表示 //--------------------------------------------------------------- void drawroom() { tft.setTextSize(1); tft.setFont(&FreeSerifBold18pt7b); if ( !night_flg ) tft.setTextColor(ST77XX_WHITE); else tft.setTextColor(ST77XX_RED); tft.setCursor(0, REAL_ENV+18+6); tft.printf(" %2.1f%s ", Ta, "C"); tft.printf(" %2.1f%s", RH, "%"); } //--------------------------------------------------------------- // WBGTを表示 //--------------------------------------------------------------- void draw_wgbt( String subttl, int y_pos ){ //tft.fillRect(0, y_pos, 240, y_pos+112, ST77XX_WHITE); tft.setCursor(0, y_pos+22); // サブタイトル tft.setFont(&FreeSerifBold12pt7b); tft.setTextSize(1); tft.setTextColor(ST77XX_RED); tft.print(subttl); tft.setCursor(30, y_pos+22+24+20); // 暑さ指数 tft.setFont(&FreeSerifBold24pt7b); tft.setTextSize(1); tft.setTextColor(Wcolor); tft.printf("%6.1f", wbgt); tft.setCursor(0, y_pos+22+24+24+20); // 指標 tft.setFont(&FreeSerifBold12pt7b); tft.setTextSize(1); tft.printf("%15s", Wmsg); Serial.print("WBGT="); Serial.println(wbgt); } /* 暑さ指数の表示 ------------------------ wcolor:表示色 wmsg: 指標 */ void set_WBGT_color( float wbgt, int kind ){ Wcolor = ST77XX_BLUE; // 安全 Wmsg = "Safety"; tft.fillRect(21+160, kind, 39, 9, Wcolor); if ( wbgt > 21 ) { Wcolor = ST77XX_GREEN; // 注意 Wmsg = "Attention"; tft.fillRect(21+120, kind, 39, 9, Wcolor); } if ( wbgt > 25 ) { Wcolor = ST77XX_ORANGE; // 警戒 Wmsg = "Warning"; tft.fillRect(21+80, kind, 39, 9, Wcolor); } if ( wbgt > 28 ) { Wcolor = ST77XX_RED; // 厳重警戒 Wmsg = "StrictWarning"; tft.fillRect(21+40, kind, 39, 9, Wcolor); } if ( wbgt > 31 ) { Wcolor = ST77XX_MAGENTA; // 危険 Wmsg = "Danger"; tft.fillRect(21, kind, 39, 9, Wcolor); } } //--------------------------------------------------------------- // 屋外のWBGTを計算して表示 //--------------------------------------------------------------- void draw_sun_wbgt() { wbgt = (Ta * 0.735) + (RH * 0.0374) + (Ta * 0.00292 * RH) + (SR * 7.619 - 4.557 * SR * SR) - ( WS * 0.0572) - 4.064; //wbgt = (RH * 0.7) + (Ta + 2) * 0.2 + Ta * 0.1; set_WBGT_color( wbgt, SUNLIGHT ); draw_wgbt( " at SUNLIGHT", OUTDOOR ); } //--------------------------------------------------------------- // 屋内のWBGTを計算して表示 //--------------------------------------------------------------- void drawwbgt() { wbgt = (Ta * 0.735) + (RH * 0.0374) + (Ta * 0.00292 * RH) - 4.064; //wbgt = (RH * 0.7) + Ta * 0.3; set_WBGT_color( wbgt, ROOM ); draw_wgbt( " at INDOORS", INDOOR ); } void setup(void) { dht.setup(DHT0, DHTesp::DHT22); // 22.JUL.2025i //dht.setup(DHT0, DHTesp::DHT11); Serial.begin(115200); pinMode(BUILTIN_LED, OUTPUT); pinMode(TFT_BL, OUTPUT); // バックライト digitalWrite(TFT_BL, HIGH); // 点灯 digitalWrite(BUILTIN_LED, LOW); // 背景を設定 tft.init(240, 320); tft.setRotation(2); tft.fillScreen(ST77XX_BLACK); tft.invertDisplay(false); WiFi.begin(WIFI_SSID, WIFI_PWD); tft.println("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { digitalWrite( BUILTIN_LED, HIGH ); delay(500); tft.print("."); Serial.print("."); digitalWrite( BUILTIN_LED, LOW ); delay(500); } tft.println(""); digitalWrite( BUILTIN_LED, HIGH ); // Get time from network time service configTime(TZ_SEC, DST_SEC, "pool.ntp.org"); } void loop() { title(); updateDHT(); drawroom(); draw_sun_wbgt(); drawwbgt(); drawDateTime(); delay( 10000 ); }