国立研究開発法人防災科学技術研究所(NIED)が提供しするサービス『強震モニタ』( http://www.kmoni.bosai.go.jp/ )のデータをM5Stackを使って監視し、地震の発生を示すデータを見つけたときに予想震度、震源、リアルタイム震度を表示するプログラムを公開します。
実際にしばらく使ってみて、地震発生時に自分がいる場所にいつ地震波が到来するのかわかり、テレビやスマホの緊急地震速報よりもおどろおどろしい音ではないため、気持ちに余裕を持って揺れに備えることができました。
2021/05/01 10:27ごろに発生した地震
このプログラムは国立研究開発法人防災科学技術研究所が提供しているサービス『強震モニタ』のデータを定期的に確認し、予想震度情報の取得に成功したとき「地震が起きた」と見なし、M5Stackでビープ音を鳴らし、LCDに予想震度情報、震源・P波・S波、リアルタイム震度情報を一定時間表示するプログラムです。
一度M5Stackに転送してしまうと、電源を入れれば自動的にWi-Fi接続を行い、接続確立後はずっと定期的に確認処理を行います。(Wi-Fi接続完了後、およそ30秒経過した時点でLCDを消灯します。)
ボタンAを押すとLCDを点灯しリアルタイム震度を表示し、ボタンCを押すとLCDを消灯します。
NTP同期をするために使用しているArduino Time LibraryがSDKに含まれるTime.hを上書きしているために発生しているようです。非推奨として上書きをしているようですが完全互換ではないためにエラーとなるようです。
ダウンロード (2021/05/01更新)
ソース: http://www.ria-lab.com/omiyage/KYOSHIN_1.0.0.zip
#include <Arduino.h> #include <M5Stack.h> #include <WiFi.h> #include <WiFiMulti.h> #include <HTTPClient.h> #include <AnimatedGIF.h> // ******************************************************************** // M5Stackを使った強震モニタのウォッチャ // // このプログラムは国立研究開発法人防災科学技術研究所の『強震モニタ』のデータを // 定期的に確認し、予想震度情報の取得に成功した場合、M5Stackでビープ音を鳴らし、 // LCDに予想震度情報、震源・P波・S波リアルタイム震度情報を一定時間表示するプロ // グラムです。 // // This software is distributed under the license of NYSL. // http://www.kmonos.net/nysl/ // // 《謝辞》 // このプログラムで使用している画像データは国立研究開発法人防災科学技術研究所が // 提供しているサービス『強震モニタ』のデータを使用させていただいております。 // 貴重なデータを提供していただき国立研究開発法人防災科学技術研究所に感謝を申し上 // げます。 // // ■国立研究開発法人防災科学技術研究所 // https://www.bosai.go.jp/ // ■強震モニタ // http://www.kmoni.bosai.go.jp/ // // このプログラムのHTTPダウンロード部分はporurubaさんの『ESP32でバイナリ // ファイルのダウンロード・アップロード』を参照させていただきました。 // 貴重なノウハウを提供していただき感謝を申し上げます。 // https://qiita.com/poruruba/items/82a683866aef872665a4 // // このプログラムのGIF展開部分は、Larry BankさんのAnimatedGIFライブラリに // 含まれるサンプルプログラムを参照させていただきました。 // 素晴らしいライブラリを提供していただき感謝を申し上げます。 // https://platformio.org/lib/show/10952/AnimatedGIF/ // // ******************************************************************** // libdeps\m5XXXXXXXXX\Time\Time.hにあるpaulstoffregen/TimeのTime.hを // つぶさないとコンパイルに失敗する // Time.hをリネームしてSDKのTime.hが読み込まれるように対処した #include <Time.h> #include <TimeLib.h> #define LCD_WIDTH (320) // LCDの横ドット数(M5Stack) #define LCD_HEIGHT (240) // LCDの縦ドット数(M5Stack) #define DISPLAY_COUNT (30) // LCD点灯時間(だいたい秒) #define CHECK_INTERVAL (5) // チェック間隔(だいたい秒) unsigned long file_buffer_size; // ダウンロードしたサイズ unsigned char file_buffer[20000]; // ダウンロード用のバッファ const char *ssid = "《Wi-Fi接続のアクセスポイント名》"; // Wi-Fi接続のアクセスポイント名 const char *password = "《Wi-Fi接続のパスワード》"; // Wi-Fi接続のパスワード const char *ntpServer = "ntp.jst.mfeed.ad.jp"; // NTPサーバ名 const long gmtOffset_sec = 9 * 60 * 60; // GMTとJSTの時差 const int daylightOffset_sec = 0; // 夏時間 char gif_dir_path[30]; // GIFが格納されているディレクトリ名 char gif_file_name[100]; // GIFファイル名 char gif_url[200]; // GIFが格納されているURL AnimatedGIF gif; // GIF展開用 // 日本地図スプライト TFT_eSprite mapimg = TFT_eSprite(&M5.Lcd); bool displayOn; // 点灯中フラグ long displayOffCount = 0; // 消灯までのカウント long procCount = 0; // 処理カウント long doHttpGet(String url, uint8_t *p_buffer, unsigned long *p_len) { HTTPClient http; http.begin(url); int httpCode = http.GET(); unsigned long index = 0; if (httpCode <= 0) { http.end(); return -1; } if (httpCode != HTTP_CODE_OK) { http.end(); return -1; } WiFiClient *stream = http.getStreamPtr(); int len = http.getSize(); if (len != -1 && len > *p_len) { http.end(); return -1; } while (http.connected() && (len > 0 || len == -1)) { size_t size = stream->available(); if (size > 0) { if ((index + size) > *p_len) { http.end(); return -1; } int c = stream->readBytes(&p_buffer[index], size); index += c; if (len > 0) { len -= c; } } delay(1); } http.end(); *p_len = index; return 0; } // 地図スプライトに描画 void GIFDrawSprite(GIFDRAW *pDraw) { uint8_t *s; uint16_t *d, *usPalette, usTemp[1000]; int x, y; usPalette = pDraw->pPalette; y = pDraw->iY + pDraw->y; // current line s = pDraw->pPixels; if (pDraw->ucDisposalMethod == 2) // restore to background color { for (x = 0; x < LCD_WIDTH; x++) { if (s[x] == pDraw->ucTransparent) s[x] = pDraw->ucBackground; } pDraw->ucHasTransparency = 0; } // Apply the new pixels to the main image if (pDraw->ucHasTransparency) // if transparency used { uint8_t *pEnd, c, ucTransparent = pDraw->ucTransparent; int x, iCount; pEnd = s + pDraw->iWidth; x = 0; iCount = 0; // count non-transparent pixels while (x < pDraw->iWidth) { c = ucTransparent - 1; d = usTemp; while (c != ucTransparent && s < pEnd) { c = *s++; if (c == ucTransparent) // done, stop { s--; // back up to treat it like transparent } else // opaque { *d++ = usPalette; iCount++; } } // while looking for opaque pixels if (iCount) // any opaque pixels? { for (int xOffset = 0; xOffset < iCount; xOffset++) { int32_t dx = (int32_t)(((double)(x + xOffset) / pDraw->iWidth) * LCD_WIDTH); int32_t dy = (int32_t)(((double)y / pDraw->iHeight) * LCD_HEIGHT); mapimg.drawPixel(dx, dy, usTemp[xOffset]); } x += iCount; iCount = 0; } // no, look for a run of transparent pixels c = ucTransparent; while (c == ucTransparent && s < pEnd) { c = *s++; if (c == ucTransparent) iCount++; else s--; } if (iCount) { x += iCount; // skip these iCount = 0; } } } else { s = pDraw->pPixels; // Translate the 8-bit pixels through the RGB565 palette (already byte reversed) for (x = 0; x < pDraw->iWidth; x++) { int32_t dx = (int32_t)(((double)x / pDraw->iWidth) * LCD_WIDTH); int32_t dy = (int32_t)(((double)y / pDraw->iHeight) * LCD_HEIGHT); mapimg.drawPixel(dx, dy, usPalette[*s++]); } } } // LCDに直接描画 void GIFDrawLcd(GIFDRAW *pDraw) { uint8_t *s; uint16_t *d, *usPalette, usTemp[1000]; int x, y; usPalette = pDraw->pPalette; y = pDraw->iY + pDraw->y; // current line s = pDraw->pPixels; if (pDraw->ucDisposalMethod == 2) // restore to background color { for (x = 0; x < LCD_WIDTH; x++) { if (s[x] == pDraw->ucTransparent) s[x] = pDraw->ucBackground; } pDraw->ucHasTransparency = 0; } // Apply the new pixels to the main image if (pDraw->ucHasTransparency) // if transparency used { uint8_t *pEnd, c, ucTransparent = pDraw->ucTransparent; int x, iCount; pEnd = s + pDraw->iWidth; x = 0; iCount = 0; // count non-transparent pixels while (x < pDraw->iWidth) { c = ucTransparent - 1; d = usTemp; while (c != ucTransparent && s < pEnd) { c = *s++; if (c == ucTransparent) // done, stop { s--; // back up to treat it like transparent } else // opaque { *d++ = usPalette; iCount++; } } // while looking for opaque pixels if (iCount) // any opaque pixels? { for (int xOffset = 0; xOffset < iCount; xOffset++) { int32_t dx = (int32_t)(((double)(x + xOffset) / pDraw->iWidth) * LCD_WIDTH); int32_t dy = (int32_t)(((double)y / pDraw->iHeight) * LCD_HEIGHT); M5.Lcd.drawPixel(dx, dy, usTemp[xOffset]); } x += iCount; iCount = 0; } // no, look for a run of transparent pixels c = ucTransparent; while (c == ucTransparent && s < pEnd) { c = *s++; if (c == ucTransparent) iCount++; else s--; } if (iCount) { x += iCount; // skip these iCount = 0; } } } else { s = pDraw->pPixels; // Translate the 8-bit pixels through the RGB565 palette (already byte reversed) for (x = 0; x < pDraw->iWidth; x++) { int32_t dx = (int32_t)(((double)x / pDraw->iWidth) * LCD_WIDTH); int32_t dy = (int32_t)(((double)y / pDraw->iHeight) * LCD_HEIGHT); M5.Lcd.drawPixel(dx, dy, usPalette[*s++]); } } } // 強震モニタのデータをチェック void checkKmoni() { // 現在日時を取得し対象日時を決定 // サーバからデータを取得できないことがあるのでリアルタイム日時の1秒前を対象日時とする time_t timer; time(&timer); timer -= 1; struct tm timeinfo; localtime_r(&timer, &timeinfo); // 対象日時から強震モニタ上で取得したいデータのディレクトリ名とファイル名を決定 sprintf(gif_dir_path, "%04d%02d%02d", (1900 + timeinfo.tm_year), (1 + timeinfo.tm_mon), timeinfo.tm_mday); sprintf(gif_file_name, "%s%02d%02d%02d", gif_dir_path, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); // LCD描画開始 M5.Lcd.startWrite(); // 日本地図を描画 M5.Lcd.fillScreen(TFT_WHITE); mapimg.pushSprite(((TFT_HEIGHT - LCD_WIDTH) / 2), ((TFT_WIDTH - LCD_HEIGHT) / 2), TFT_TRANSPARENT); // 予想震度を取得 sprintf(gif_url, "http://www.kmoni.bosai.go.jp/data/map_img/EstShindoImg/eew/%s/%s.eew.gif", gif_dir_path, gif_file_name); file_buffer_size = sizeof(file_buffer); if (doHttpGet(gif_url, file_buffer, &file_buffer_size) == 0) { // EEWをダウンロードできたときから指定秒数分処理が回るまでLCDを点灯 displayOffCount = DISPLAY_COUNT; // LCDが消灯中の場合は点灯、BEEPを鳴らしていない場合はBEEPを鳴らす if (!displayOn) { M5.Lcd.wakeup(); M5.Lcd.setBrightness(255); displayOn = true; M5.Speaker.setVolume(5); M5.Speaker.tone(1200, 1000); } // 予想震度を描画 gif.open((uint8_t *)file_buffer, file_buffer_size, GIFDrawLcd); while (gif.playFrame(true, NULL)) {} gif.close(); } // LCDが点灯している状態の場合、震源とリアルタイム震度情報を取得を試みて // 取得できた場合は描画する if (displayOn) { // 震源・P波・S波 sprintf(gif_url, "http://www.kmoni.bosai.go.jp/data/map_img/PSWaveImg/eew/%s/%s.eew.gif", gif_dir_path, gif_file_name); file_buffer_size = sizeof(file_buffer); if (doHttpGet(gif_url, file_buffer, &file_buffer_size) == 0) { // 震源・P波・S波を描画 gif.open((uint8_t *)file_buffer, file_buffer_size, GIFDrawLcd); while (gif.playFrame(true, NULL)) {} gif.close(); } // リアルタイム震度 sprintf(gif_url, "http://www.kmoni.bosai.go.jp/data/map_img/RealTimeImg/jma_s/%s/%s.jma_s.gif", gif_dir_path, gif_file_name); file_buffer_size = sizeof(file_buffer); if (doHttpGet(gif_url, file_buffer, &file_buffer_size) == 0) { // リアルタイム震度を描画 gif.open((uint8_t *)file_buffer, file_buffer_size, GIFDrawLcd); while (gif.playFrame(true, NULL)) {} gif.close(); } } } void setup() { // M5Stackを初期化 M5.begin(); // LCDを初期化し、Wi-Fi接続、NTP同期状況を接続する M5.Lcd.begin(); M5.Lcd.setSwapBytes(true); M5.Lcd.setRotation(1); M5.Lcd.setCursor(0, 0); M5.Lcd.fillScreen(TFT_BLUE); M5.Lcd.setTextColor(TFT_WHITE); M5.Lcd.setTextSize(2); M5.Lcd.println("[KYOSHIN]"); M5.Lcd.println(""); M5.Lcd.setTextSize(2); // Wi-Fi接続 M5.Lcd.printf("Wi-Fi: Connecting to %s\n", ssid); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); M5.Lcd.print("."); } M5.Lcd.print("\n"); // NTP同期 M5.Lcd.printf("NTP: Sync NTP Server(%s)\n", ntpServer); configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); struct tm timeinfo; if (!getLocalTime(&timeinfo)) { M5.Lcd.println("NTP synchronization failed"); return; } gif.begin(LITTLE_ENDIAN_PIXELS); // サーバから日本地図を取得しスプライトとして描画 file_buffer_size = sizeof(file_buffer); if (doHttpGet("http://www.kmoni.bosai.go.jp/data/map_img/CommonImg/base_map_w.gif", file_buffer, &file_buffer_size) != 0) { M5.Lcd.println("Map download failed"); return; } mapimg.setColorDepth(4); mapimg.createSprite(LCD_WIDTH, LCD_HEIGHT); mapimg.setSwapBytes(false); mapimg.fillScreen(TFT_TRANSPARENT); gif.open((uint8_t *)file_buffer, file_buffer_size, GIFDrawSprite); while (gif.playFrame(true, NULL)) { } gif.close(); // スピーカーをミュート M5.Speaker.mute(); // 画面を塗りつぶし M5.Lcd.fillScreen(TFT_WHITE); // 画面を点灯 M5.Lcd.wakeup(); M5.Lcd.setBrightness(100); displayOn = true; displayOffCount = DISPLAY_COUNT; M5.Speaker.setVolume(1); M5.Speaker.tone(440, 50); delay(50); M5.Speaker.mute(); } void loop() { M5.update(); // ボタンの状態をチェック if (M5.BtnA.wasPressed()) { // ボタンA -> LCDを点灯 M5.Lcd.wakeup(); M5.Lcd.setBrightness(255); displayOn = true; displayOffCount = DISPLAY_COUNT; M5.Speaker.tone(1200, 50); delay(20); M5.Speaker.mute(); } else if (M5.BtnB.wasPressed()) { // ボタンB -> ミュート M5.Speaker.mute(); } else if (M5.BtnC.wasPressed()) { // ボタンC -> LCDを消灯 M5.Lcd.sleep(); M5.Lcd.setBrightness(0); displayOn = false; displayOffCount = 0; M5.Speaker.tone(1200, 50); delay(20); M5.Speaker.mute(); } // 強震モニタをチェックする if (procCount % CHECK_INTERVAL == 0) { checkKmoni(); } // LCD消灯までカウントダウン if (--displayOffCount <= 0) { if (displayOn) { // LCDを消灯 M5.Lcd.sleep(); M5.Lcd.setBrightness(0); displayOn = false; // BEEPをミュート M5.Speaker.mute(); } } // 処理カウンタを進める procCount++; if (procCount >= CHECK_INTERVAL) { procCount = 0; } // 次の処理まで待機 delay(1000); }
時間の経過とともに揺れが広がっていくことが確認できます。
2021/05/01 10:27ごろに発生した強い地震では、画面上で地震波が到来すると実際の揺れが到来しました。
最初に出される予想震度とリアルタイム震度がほぼ同じ(面で表示される予想震度と、点で表示されている観測点ごとのリアルタイム震度の色が同じになる)なのを見ると、日本の地震防災技術の高さを実感できます。
このプログラムは2021年2月13日に発生した福島県沖地震のあと余震が続き、この地方の特有の家が揺れるほどの強風の時期と重なり、地震で揺れているのか風で揺れているのかはたまた気のせいなのか判断がつかずに非常に気持ち悪かったため作成しました。
当初は9軸加速度センサーを使い「センサーがこの場所で感知した揺れ」も用いていたのですが、今回の公開用に『強震モニタ』のデータだけを使うように変更しています。
『強震モニタ』では、JSON形式で地震発生時に震源域やマグニチュード、最大震度などの情報も提供されています。
もう少し頑張って作り込むと自分がいる場所から震源域が近いときのみBEEPを鳴らしたり画面表示したりするといったこともできると思うのですが、M5StackでJSONデータをパースし日本語情報を表示するために一手間必要だったため割愛しています。
最初は自分用に作ったものなのでエラー処理などがほとんどありません。
たとえば強震モニタを定期的に確認している状態で何らかの原因によりWi-Fiが切れてしまうと通知や復旧の仕組みがないため以後はただ通電しているだけの箱になります。
このプログラムは、国立研究開発法人防災科学技術研究所(防災科研、NIED)が提供しているサービス『強震モニタ』の画像データを使用させていただいております。
このような貴重で有意義なデータを提供していただき国立研究開発法人防災科学技術研究所に感謝を申し上げます。
公式サイトは以下の通りです。
このプログラムのHTTPダウンロード部分はporurubaさんの『ESP32でバイナリファイルのダウンロード・アップロード』を参照させていただきました。
貴重なノウハウを提供していただき感謝を申し上げます。
https://qiita.com/poruruba/items/82a683866aef872665a4
このプログラムのGIF展開部分は、Larry BankさんのAnimatedGIFライブラリに含まれるサンプルプログラムを参照させていただきました。
素晴らしいライブラリを提供していただき感謝を申し上げます。
https://platformio.org/lib/show/10952/AnimatedGIF/
Please give us your valuable comment