LoginSignup
58
54

More than 3 years have passed since last update.

M5StickCで天気通知システムを作る

Last updated at Posted at 2019-07-20

きっかけ

筆者は普段外出前に天気予報を確認する癖がなく、玄関先で空が晴れていたらそのまま家を出る、雨が降っている or 雨が振りそうなら傘を持って出る、という生活をしています。
このため、出かけるときは晴れていたけど昼方・夕方に突然雨が降り出すという状況には為す術なく濡れて帰宅することになります(最近雨多いし…)。
これを防ぐために玄関先でスマホを取り出し、画面を数ステップ操作して天気を確認するのも一つの手ですが、その日の天気予報がプッシュ通知的に表示されていれば一目で確認できより便利だと考えたので、小型IoTガジェットとしてコスパの高いM5StickCで通知システムを作りました。

完成品

まずは、完成品ですがこんな感じです。M5StickCはWiFi経由でインターネット接続ができるため、数時間置きに天気情報を提供するWebAPIを叩いて天気を取得してきて、一日の天気をアイコンによって「晴のち曇」のように表示させています(ちなみに、画面中央の▲印は点滅します)。出かける前に雨のアイコンが表示されていれば、傘を持って出ればいいという訳です。
P_20190720_192639_vHDR_Auto.jpg
P_20190720_193103_vHDR_Auto.jpg

制作過程

# 天気表示に使用するアイコンの用意
アイコンは素材サイトICOOON MONOさんから頂き、適宜加筆修正したpngファイルを用意しました。M5StickCのディスプレイ解像度が160 × 80であることを考えて、アイコンサイズは64 × 64としています。
また、背景については、透明色は表示できなさそうなのでとりあえず黒塗りにしています。
スクリーンショット 2019-07-19 20.31.22.png
pngファイルのままではM5StickC上で表示できないので、下記記事を参考にRGB565形式に変換します。
 - 【M5Stack】LCDに表示する文字や図形の色をRGBで指定したい時の変換作業
 - PNGからM5Stackで表示できるBitmapのテキストデータに変換するPythonツール

2つ目の記事に記載されているPythonスクリプトを使うと、画像を1次元バイト配列の形式で出力してくれるため、3つの画像分を1つにまとめて2次元配列定数 w_icon[3][4096] を定義し、imgs.cファイルとしました。配列宣言はArduinoスケッチ内でも可ですが、かなり行数を喰うので別ファイル化しました。アイコンが64 x 64pxのため画像1つ分の配列長は4096としています。
スクリーンショット 2019-07-20 11.39.11.png

# M5StickC用スケッチのコーディング
筆者はArduinoIDEによるM5StickCの開発環境を使っているので、次のようなArduino言語のソースコードファイル(スケッチ)を作成しました。

WeatherAlert.ino
#include <HTTPClient.h>
#include <M5StickC.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include "imgs.c"

const char* ssid     = "xxxx";   // your network SSID (name of wifi network)
const char* password = "xxxx";    // your network password
const String citycode = "270000";//天気を取得する地域コード(270000は大阪全域)

const byte SUNNY = 0;
const byte CLOUDY = 1;
const byte RAINY = 2;

byte AM_W;//午前の天気
byte PM_W;//午後の天気
String DATE = "";//画面に表示する日付文字列

unsigned int counter;//ループ回数のカウンタ


void updateWeather() { //Webから天気を取ってAM_W,PM_Wを更新
  HTTPClient http;
  String url = "http://weather.livedoor.com/forecast/webservice/json/v1?city=" + citycode;
  http.begin(url);
  int httpCode = http.GET();//GETメソッドで接続
  if (httpCode == HTTP_CODE_OK) {//正常にGETができたら
    String payload = http.getString();
    payload.replace("\\", "¥"); //JSONレスポンス内の"\"をエスケープしないとデシリアライズが失敗する

    DynamicJsonDocument doc(7000);//レスポンスデータのJSONオブジェクトを格納する領域を確保
    deserializeJson(doc, payload); //HTTPのレスポンス文字列をJSONオブジェクトに変換
    JsonVariant today = doc["forecasts"][0];//forecastsプロパティの0番目の要素が今日の天気に関するプロパティ
    DATE = today["date"].as<String>();//JSONから日付文字列を取得
    String telop = today["telop"];//予報文字列を取得("晴"、"曇り"、"曇のち雨"等)、ただしUnicode形式になっている

    int isFollow = telop.indexOf("¥u306e¥u3061");//予報文字列内の"のち"の位置を取得(なければ-1)
    if (isFollow < 0) { //"のち"がないなら午前も午後も同じ天気にする
      AM_W = decodeStr2Weather(telop);
      PM_W = AM_W;
    }
    else { //"のち"があるなら前後の文字列をそれぞれ午前・午後の天気にする
      telop.replace("¥u306e¥u3061", ""); //"のち"を除去
      AM_W = decodeStr2Weather(telop.substring(0, isFollow));
      PM_W = decodeStr2Weather(telop.substring(isFollow, telop.length()));
    }
  } 
  else { //GETが失敗したらフェールセーフとして雨を設定する
    AM_W = RAINY;
    PM_W = RAINY;
    DATE = "nodata";
  }
  http.end();
}

byte decodeStr2Weather(String str) { //文字列を天気定数に変換
  if (str.indexOf("¥u96e8") >= 0) //"雨"の字が含まれるなら
    return RAINY;
  else if (str.indexOf("¥u66c7") >= 0) //"曇"の字が含まれるなら
    return CLOUDY;
  else if (str.indexOf("¥u6674") >= 0) //"晴"の字が含まれるなら
    return SUNNY;
  else//上記の字が含まれないならフェイルセーフとして雨を返す
    return RAINY;
}

void drawWeather() { //AM_W,PM_Wの値に基づき画面に天気を描画
  M5.Lcd.setTextDatum(0);//文字描画の際の原点を左上に設定
  M5.Lcd.setTextColor(WHITE, 0x3186);//文字色を白、背景色を黒に設定
  M5.Lcd.drawString(DATE, 4, 0);//日付文字列を描画
  M5.Lcd.drawBitmap(8, 12, 64, 64, w_icon[AM_W]);//AMの天気アイコンを描画
  M5.Lcd.drawBitmap(88, 12, 64, 64, w_icon[PM_W]);//PMの天気アイコンを描画
}

void setup() {
  M5.begin();//M5StickCオブジェクトを初期化
  M5.Axp.ScreenBreath(8);//画面輝度を設定(7~15)
  M5.Lcd.setRotation(3);//画面を横向きに

  M5.Lcd.print("Connecting to WiFi");
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  WiFi.begin(ssid, password);//WiFiへの接続開始
  while (WiFi.status() != WL_CONNECTED) {
    M5.Lcd.print(".");
    delay(200);//接続待ち遅延
  }
  M5.Lcd.println("Connected!");
  delay(1500);

  M5.Lcd.fillScreen(0x3186);//画面を黒で塗りつぶし
  counter = 0;//電源投入時を基準に1[s]ごとにカウントアップされていく
}

void loop() {
  //画面中央の▲を2[s]周期で点滅表示
  if (counter % 2 == 0)
    M5.Lcd.fillTriangle(74, 40 - 16, 74, 40 + 16, 86, 40, GREEN); //緑色
  else
    M5.Lcd.fillTriangle(74, 40 - 16, 74, 40 + 16, 86, 40, 0x3186); //黒色

  //初回ループまたは1.5[h]ごとに天気情報を更新
  if (counter % 5400 == 0) {
    updateWeather();//Webから天気を取得
    drawWeather();//天気を描画
  }

  counter++;//符号なしなのでオーバーフローしてもok
  delay(1000);//1[s]
}

スケッチ内のssid , password , citycodeは各自の環境に合わせて変更してください。citycodeは、後述する天気取得APIで定義されている値で、各都市のコードをここ(※要PCからアクセス)で調べることができます。
画像データの表示は、 drawWeather() にあるように M5.Lcd.drawBitmap() 関数に座標や縦横サイズと先ほど作成した w_icon[][] を渡すだけと簡単です。
一方、WebAPIを叩いて天気をJSONで取得・デコードする updateWeather() については、下記記事を参考にArduinoJSONを使用して実装しました(公式ページを参考にArduinoIDEにArduinoJSONのライブラリを追加する必要があります)。
 - M5Stick-CでJsonをPOSTする

対象とする天気取得APIは、livedoorの提供するWeather Hacks お天気Webサービス(※要PCからアクセス)です。詳しくはリンク先をご確認ください。
また、本APIのレスポンスのJSONには、天気情報等にUnicode形式の日本語が含まれるのですが、コード内ではそのまま文字列比較等に使っています。

# スケッチの書き込み
画像のように、imgs.cとWeatherAlert.inoを同一フォルダに格納してArduinoIDEを使ってM5StickCに書き込めば完成です。
※ArduinoIDEによるM5StickCの開発環境構築についてはここでは触れません。
スクリーンショット 2019-07-20 13.13.02.png

所感

M5StickCは、2000円程度と安価ながら様々な機能が詰まっているので、ちょっとしたアイデアをプロトタイピングするのにうってつけだと感じました。特に、「WiFiやBluetoothによる接続」や「ディスプレイへの情報表示」が外付けの部品なく可能である点がArduinoやラズパイ等と比べたときの大きな利点だと思います。
一方、APIレスポンスを処理する部分のコーディング中に、文字列周りのライブラリ関数の少なさ、日本語の扱いにくさに苦労したため、その点は豊富なライブラリを持つPython等の高級言語が使えるラズパイに分がありそうです。
この製品の他にも、M5StackやM5StickVといった様々なファミリー機種も出ているので、そちらも今後試していきたいです。

追記

続編の記事を書きました(11/30)。修正版ソースコードも公開してます。
【続】M5StickCで天気通知システムを作る(機能追加&バグ修正&リファクタリング) - Qiita

58
54
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
58
54