天気予報LED
今後の天気をLEDでお知らせしてくれる「天気予報LED」を作っています。前回は、PCBWayから届いた基板を組み立てて、ハードウェアが完成しました。
今回は、ESP32のArduinoのプログラムで、OpenWeathreMapから天気予報を取得してLEDに表示させたいと思います。
OpenWeatherMap
OpenWeatherMapは世界のあらゆる都市の天気予報を取得できる、無料のサービスです。有料版もありますが、アクセス数が少なければ無料で利用できます。
OpenWeatherMapのを利用するには、ユーザー登録をしてAPI KEYという固有のKEYが必要になります。登録の仕方や利用方法は、こちらの記事を👇👇を参考にしてください。
天気予報を取得する場所を指定する方法
知りたい場所の指定方法として、
・都市IDで指定する方法
・郵便番号で指定する方法
・緯度経度で指定する方法があります。
都市ID(city ID)はOpenWeathreMapが都市ごとに割り振った独自のIDです。都市IDを調べる方法がよくわからないので、ここでは割愛します。
郵便番号で指定する方法は、先ほどの「登録の仕方や利用方法の記事」にあります。2019年時点では、日本の全ての郵便番号に対応していませんでした。そのため実在する郵便番号を指定しても、正しい結果が得られないことがありました。今ではだいぶ改善しているみたいです。
緯度経度で指定する方法は、指定した緯度経度に最も近い都市の天気予報が得られます。郵便番号のように実在するのに結果が得られないということがなく、確実に天気予報が得られます。
そこで、今回は緯度経度で指定する方法で、天気予報を取得したいと思います。
緯度経度で天気予報を取得するには
緯度経度で指定した地点の天気予報を取得するには、以下のようなURLでアクセスします。
「lat=」の後に緯度を、「lon=」の後に経度を指定します。小数点以下2桁以上指定しても、2桁に丸められてしまうようです。上記のlat=35.68,lon=139.76は東京駅の緯度経度になっています。
「units=metric」は単位をメートルや摂氏に指定しています。
「lang=jp」は天気の様子などが日本語で返ってきます。
「cnt=」は何個分の天気予報を得るかを指定します。無料版では3時間毎の天気予報が5日分で40セットの天気予報が取得できます。今回は、3時間先の天気予報だけが知りたいので、1セット分だけとして「cnt=1」を指定します。
「appid=」は予め取得したAPI KEYを指定します。
このURLを試しにブラウザのURLにコピーしてみましょう。
このようなJSON形式のテキストが返ってきます。見やすく整理すると、
結構いろいろな情報が詰まっています。
dtやdt_txtが予報の時刻を表しています。時刻は国際標準時なので、日本の場合には時差として+9時間加算します。得られたJSONの中から天気予報の項目をピックアップすると、
「2020年8月17日 12:00:00」の「東京都丸の内」の天気予報は
- id:802
- description:曇
です。idはこちらに一覧が載っています。
天気予報はこのidを利用します。この表を見ると
- 200-500番台:雨
- 600番台:雪
- 700番台:曇り
- 800-802:晴れ
- 803-804:曇り
といった感じに分類分けができます。
これで天気予報を取得する方法がわかりました。次にArduinoのプログラムにしていきます。
ESP32 ArduinoでOpenWeatherMapから天気予報を取得する
HTTPSアクセス
目的のサーバーに接続して応答を得るプログラムを作ります。hostとURLを指定して応答をresponseで取得できるHttpGetという関数を作りました。
//WiFi #include <WiFiClientSecure.h> WiFiClientSecure client; 〜中略〜 void setup() { 〜中略〜 } //httpsのgetをする bool HttpsGet(String host, String url , String * response ) { if (WiFi.status() != WL_CONNECTED) return false; int code = client.connect( host.c_str(), (uint16_t)443); if ( !code ) { Serial.println( "サーバーに接続できませんでした" ); return false; } Serial.println( "サーバーに接続できました" ); String req = ""; req += "GET " + url + " HTTP/1.1\r\n"; req += "Host: " + host + "\r\n"; req += "Accept: text/plain\r\n"; req += "Connection: close\r\n"; req += "\r\n"; req += "\0"; client.print(req); client.flush(); unsigned long timeout = micros(); while (client.available() == 0) { if ( micros() - timeout > 5000000) { Serial.println("応答が無くタイムアウトしました"); client.stop(); return false; } } String res; while (client.available()) { res = client.readStringUntil('\r'); Serial.print(res); } *response = res; client.stop(); return true; }
今回の場合
- host:api.openweathermap.org
- URL:/data/2.5/forecast?lat=35.68&lon=139.76&units=metric&lang=ja&cnt=1&appid=APIKEY
となります。サーバーからの応答がresponseに格納されます。
OpenWheatherMapの応答をArduinoJSONでパース
OpenWheatherMapにアクセスしやすいよう、専用の関数を用意しました。OpenWheatherMapの応答はJSONなので、ArduinoJSONでパースしてデータを抽出しやすくしておきます。
ArduinoJSONはライブラリマネージャから「ArduinoJSON」と検索してインストールできます。
//天気予報 const String host = "api.openweathermap.org"; const String APIKEY = "********"; char geoLat[20] = "35.1234"; char geoLon[20] = "137.1234"; //Json 天気予報の応答を格納する #include <ArduinoJson.h> const size_t capacity = 3000; //capacity size can caluclated in https://arduinojson.org/v6/assistant/ DynamicJsonDocument forecaseDoc(capacity); 〜中略〜 void setup() { 〜中略〜 GetOpenWeatherMap() } //OpenWeatherMapから応答を得て結果をパースしてforecaseDocに入れる bool GetOpenWeatherMap() { //urlの生成 String url = "/data/2.5/forecast?"; url += "lat=" + String(geoLat) + "&lon=" + String(geoLon); url += "&units=metric&lang=ja"; if (forecastNum > 0) url += "&cnt=" + String(forecastNum); url += "&appid=" + APIKEY; Serial.println(url); //OpenWeatherMapから情報を得る String response; if ( false == HttpsGet( host, url , &response ) ) return false; Serial.printf("\nresponse:%s\n", response.c_str()); //jsonドキュメントの作成 make JSON document DeserializationError err = deserializeJson(forecaseDoc, response); if ( err != DeserializationError::Ok ) { Serial.printf("Deserialization Error:%s\n", err.c_str()); return false; } return true; }
JSONドキュメントは、予めデータを格納する領域のサイズを設定しておく必要があります。
公式の上記のサイトにアクセスして、OpenWeathreMapからの応答のJSONのデータをコピペします。
すると、右側にESP32で必要なメモリサイズが表示されます。今回のJSONデータでは983となりました。余裕を持って上記のプログラムの9行目では3000を指定しました。
あとはOpenWeatherMapより得られた応答(response)を、38行目のように
deserializeJsonに入れてあげると、JSONが構文解析されて、forecaseDocというJSONドキュメントが生成されます。これを利用することで、簡単に要素にアクセスできるようになります。
例えば天気idや説明、降水量、降雪量が知りたかったら
//天気予報の抽出 int wheatherId = forecaseDoc["list"][0]["weather"][0]["id"]; String discription = forecaseDoc["list"][0]["weather"][0]["description"]; float rain = forecaseDoc["list"][0]["rain"]["3h"]; float snow = forecaseDoc["list"][0]["snow"]["3h"]; Serial.printf("wheatherId:%d discription:%s rain:%.2f snow:%.2f\n", wheatherId, discription.c_str(), rain, snow);
というように、記述することで、以下のようにそれぞれの値を得ることができます。
- wheatherID:802
- discription:曇
- rain(3時間降水量):0.00
- snow(3時間降雪量):0.00
ArduinoJSONライブラリを使うことで、JSONの構文を自前で解析しなくても、とても簡単に各要素にアクセスできます。
weatherIDから天気予報のLEDに変換
天気予報が取得できたので、weatherIDからどのLEDを光らせるか決定します。プログラムはこちらです。
int nowLedWeather = -1; //最新の天気予報 WLEDenumで設定 -1はOFF enum WLEDenum { Sunny , Cloudy , Rainy , Snowy }; const String condition[4] = { "Sunny" , "Cloudy" , "Rainy" , "Snowy" }; const float rainMin = 0.7; 〜中略〜 void setup() { 〜中略〜 //wheatherIdから天気に変換 nowLedWeather = Cloudy; if ( wheatherId <= 802 ) nowLedWeather = Sunny; if ( wheatherId < 800 ) nowLedWeather = Cloudy; if ( wheatherId < 700 ) nowLedWeather = Snowy; if ( wheatherId < 600 ) nowLedWeather = Rainy; //降水量が少なかったら雨を曇りにする if ( nowLedWeather == Rainy && rain < rainMin ) if( wheatherId >= 500 && wheatherId <= 504 ) nowLedWeather = Sunny; else nowLedWeather = Cloudy; Serial.printf("nowLedWeather:%d condition:%s\n", nowLedWeather, condition[nowLedWeather]); }
公式のweatherIDのリストを見ながら、10〜18行目で、晴、曇り、雨、雪に分類しています。weatherIDが雨を示していても、降水量がとても少ない場合は、実際の天気は雨ではなく曇りや晴れの場合が多い印象です。これまでの経験から3時間降水量が0.7を下回っていると、雨が降らないことが多かったので、その場合には、晴れや曇りにすることにしました。20〜25行目。
あとは、nowLedWeatherの値に従って、LEDを点滅させればLED表示部分は完成です。
環境光センサを使ったLEDの調光
「天気予報LED」は一日中LEDが点滅しています。昼間はLEDが明るく点滅していてもいいですが、夕方や夜間は眩しく感じるので、部屋の明るさに応じてLEDの明るさを調光できるようにしたいと思います。
//ポート設定 const int WLED[4] = { 32, 33, 25 , 26 }; const int LightSensorPin = 36; //LED設定 const int LEDBrightnessMin = 10; //LEDの最低の明るさ int ledBrightnessMax = 0; //LEDの最大明るさ int ledBrightness = 0; //LEDの明るさ int LEDPeriod = 500; //点滅周期[ms] enum WLEDenum { Sunny , Cloudy , Rainy , Snowy }; int nowLedWeather = -1; //最新の天気予報 WLEDenumで設定 -1はOFF //タスクハンドル TaskHandle_t taskHandle[10]; enum task { ambientLightcheckTask , ledBlinkingTask , ledScanningTask }; 〜中略〜 void setup() { //ポートの初期化 for ( int i = 0 ; i < 4 ; i++ ) { pinMode( WLED[i], OUTPUT ); digitalWrite( WLED[i], LOW ); } pinMode( LightSensorPin, ANALOG ); analogSetAttenuation(ADC_11db); //PWM設定 ledcSetup(Sunny, 12800, 10); ledcSetup(Cloudy, 12800, 10); ledcSetup(Rainy, 12800, 10); ledcSetup(Snowy, 12800, 10); ledcAttachPin(WLED[Sunny], Sunny); ledcAttachPin(WLED[Cloudy], Cloudy); ledcAttachPin(WLED[Rainy], Rainy); ledcAttachPin(WLED[Snowy], Snowy); ledcWrite(Sunny, 0); ledcWrite(Cloudy, 0); ledcWrite(Rainy, 0); ledcWrite(Snowy, 0); //環境光測定タスク xTaskCreatePinnedToCore(ambientLightcheck, "ambientLightcheck", 4096, NULL, 1, &taskHandle[ambientLightcheckTask], 0 ); //LED点滅タスク xTaskCreatePinnedToCore(ledBlinking, "ledBlinking", 4096, NULL, 2, &taskHandle[ledBlinkingTask], 0 ); 〜中略〜 } //環境光を測定してLEDの明るさの最大値を設定する void ambientLightcheck(void* arg) { float ambientLight = 0; int ledBrightnessMax = 0; while (1) { if ( digitalRead(WLED[Sunny]) == LOW ) { ambientLight = analogRead(LightSensorPin) / 4095.0; ledBrightnessMax = ambientLight * 1024; if ( ledBrightnessMax < LEDBrightnessMin ) ledBrightnessMax = LEDBrightnessMin; ledBrightness = ( 9 * ledBrightness + ledBrightnessMax ) / 10; //Serial.printf("AD:%.2f %d %d\n",ambientLight , ledBrightnessMax, ledBrightness); } delay(50); } } //LED点滅タスク void ledBlinking(void* arg) { ledcWrite(Sunny, 0); ledcWrite(Cloudy, 0); ledcWrite(Rainy, 0); ledcWrite(Snowy, 0); while (1) { //指定した天気のLEDを点灯する ledcWrite(Sunny, 0); ledcWrite(Cloudy, 0); ledcWrite(Rainy, 0); ledcWrite(Snowy, 0); if ( nowLedWeather != -1 ) { ledcWrite(nowLedWeather, ledBrightness); } delay(LEDPeriod); //LEDを消灯する ledcWrite(Sunny, 0); ledcWrite(Cloudy, 0); ledcWrite(Rainy, 0); ledcWrite(Snowy, 0); delay(LEDPeriod); } }
PWMポートを利用する
ESP32には16chのPWM機能が内蔵されています。それらから4つ利用して、LEDの明るさをPWMで変えられるようにします。
30から37行目で、PWM機能の設定と、GPIOピンへの割付を行なっています。あとは、38行目から41行目のように
ledcWrite(Sunny, 128);
とすることで、その天気のLEDを指定した明るさで光らせることができるようになります。
環境光センサの測定はタスクで
タスクを利用することで、メインのプログラムとは別にマルチスレッドのように擬似的に独立してプログラムを動作せることができます。
53から70行目が環境光を測定して、LEDの明るさ(ledBrightness)を算出するタスクです。このタスクは他のプログラムとは独立して50msごとに実行されます。
このタスクを40行目のコードで、タスクが起動するよう設定しています。
LEDの点滅もタスクで
LEDの点滅も、他のプログラムに影響されず、500msごとにLEDをONしたりOFFしたりするようにタスクにしました。73から99行目がタスクのプログラムです。
ledcWrite(nowLedWeather, ledBrightness);
とすることで、予報の天気のLEDが、ledBrightnessの明るさで点灯します。
500msごとに、つけたり消したりしています。
このタスクは47行目のコードで、タスクが起動するよう設定しています。
無線LANの設定、緯度経度の設定はWiFiManager
アクセスポイントへの接続のためのSSIDやパスワードは、プログラムにベタ書きではなく、スマホから設定できるようにWiFiManagerを使いました。
WiFImanagerの使い方に関してはこちら👇👇の記事を参考にしてください。
今回は、アクセスポイントの情報だけでなく、天気予報を取得する位置情報も一緒に設定できるようにします。
緯度経度の項目を追加する
プログラムはこちらです。
//WifiManager関係 #include <DNSServer.h> #include <WebServer.h> #include "WiFiManager.h" WiFiManager wm; #define DeviceName "LEDWF" //アクセスポイント名 #define WMPassword "12345678" //パスワード const char defaultLat[20] = "35.1234"; const char defaultLon[20] = "137.1234"; char geoLat[20] = "35.1234"; char geoLon[20] = "137.1234"; 〜中略〜 void setup() { 〜中略〜 //WiFiManegerの設定 WiFiManagerParameter custom_lat("lat", "緯度", geoLat, 20); WiFiManagerParameter custom_lon("lon", "経度", geoLon, 20); wm.addParameter(&custom_lat); wm.addParameter(&custom_lon); //WiFiに接続する wm.setConfigPortalTimeout(5*60); //AutoConnectで接続してみる bool ret = wm.autoConnect(DeviceName, WMPassword); if ( ret == true ) { //緯度経度を取得 GetGeo( custom_lat.getValue() , custom_lon.getValue()); } 〜中略〜 } //緯度経度情報を保存する void GetGeo(const char* lat , const char* lon) { //緯度経度が正しくなければデフォルト値にする double latTemp = atof(lat); double lonTemp = atof(lon); if ( latTemp == 0 || lonTemp == 0 ) { Serial.printf("value is incorrect."); strcpy(geoLat, defaultLat); strcpy(geoLon, defaultLon); } else { sprintf(geoLat, "%.4f", latTemp); sprintf(geoLon, "%.4f", lonTemp); } Serial.printf("lat:%s %f lon:%s %f\n", geoLat, latTemp, geoLon, lonTemp); }
WiFIManagerでは、ユーザー独自の項目を追加できます。19〜22行目がそれです。
19、20行目で、custom_lat,custom_lonという追加のパラメータを生成し、設定値の格納先にgeoLat,geoLonを指定しておきます。
続いて、21,22行目でWiFimanagerにこれら項目を追加します。
これによって、WiFiManagerの画面に、緯度と経度の項目が追加されます。
30行目で、得られた値を整形するGetGeo関数に入れています。得られる緯度経度情報はテキストなので、無効な文字列の場合には、デフォルトの緯度経度が設定されるようにしています。40〜53行目がそれです。
60分+乱数でリセット
OpenWeatherMapの天気予報は、およそ1時間ごとに見直されているようです。そこで、1時間ごとに再起動して、天気予報の再取得を行うようにします。
void setup() { 〜中略〜 //約1時間後にリセット WiFi.disconnect(); //省電力のためWiFiをOFF int sleepTime = 60 + random(-5, 5); //サーバへのアクセスを分散するために±5分の乱数を加える Serial.printf("wait for %d[min]\n", sleepTime); delay( sleepTime * 60 * 1000 ); Reset(); } //ウィッチドッグタイマーを利用して即リセット void Reset() { Serial.println( "ウォッチドッグタイマーでリセット" ); esp_sleep_enable_timer_wakeup(1); //1mmsでウェイクアップ esp_deep_sleep_start(); }
6行目でWiFiをOFFにして省電力化します。
乱数を使って60±5分を生成し、delay関数でその時間待ちます。DeepSleepを使いたかったのですが、GPIOを使用していたりLEDが点滅するタスクが動いているので利用できませんでした。
リセットは、RTCメモリの情報をリセットしても保持したかったのでソフトウェアリセットではなく、ウォッチドッグタイマによるリセットにしました。
このほか、無線LANへの接続できなかったときの対処や、サーバーからの応答がおかしい場合の対処など、エラーの対処を追加してプログラム完成です。
子供たちに絵を描いてもらう
天気予報のマークを子供たちに描いてもらいます。
それぞれ個性的な天気の絵が描けました。
天気を描いたシールを天気予報LEDに貼り付けます。
世界に一つだけの天気予報LEDが完成です!!
子供たちの描いた絵だと、個性があって天気予報を見るのが楽しみになりますね。
コメント
現在ArduinoでOpenWeatherMapから天気予報を取得する勉強をしております。
コードを全て教えていただけないでしょうか?