M5Stackで『強震モニタ』のデータをウォッチする

土曜日 , 1, 5月 2021 Leave a comment

国立研究開発法人防災科学技術研究所(NIED)が提供しするサービス『強震モニタ』( http://www.kmoni.bosai.go.jp/ )のデータをM5Stackを使って監視し、地震の発生を示すデータを見つけたときに予想震度、震源、リアルタイム震度を表示するプログラムを公開します。

実際にしばらく使ってみて、地震発生時に自分がいる場所にいつ地震波が到来するのかわかり、テレビやスマホの緊急地震速報よりもおどろおどろしい音ではないため、気持ちに余裕を持って揺れに備えることができました。

IMG_6392

2021/05/01 10:27ごろに発生した地震

このプログラムでできること

このプログラムは国立研究開発法人防災科学技術研究所が提供しているサービス『強震モニタ』のデータを定期的に確認し、予想震度情報の取得に成功したとき「地震が起きた」と見なし、M5Stackでビープ音を鳴らし、LCDに予想震度情報、震源・P波・S波、リアルタイム震度情報を一定時間表示するプログラムです。

一度M5Stackに転送してしまうと、電源を入れれば自動的にWi-Fi接続を行い、接続確立後はずっと定期的に確認処理を行います。(Wi-Fi接続完了後、およそ30秒経過した時点でLCDを消灯します。)
ボタンAを押すとLCDを点灯しリアルタイム震度を表示し、ボタンCを押すとLCDを消灯します。

このプログラムを動かすために必要なもの

このプログラムの動かし方

  • ソースをダウンロードし展開します。
  • main.cppというソースファイルのWi-Fi接続情報を使用する環境に合わせて修正します。
    スクリーンショット 2021-05-01 180533
  • プロジェクトの\.pio\lib_deps\Timeフォルダ内にあるTime.hを適当なファイル名にリネームします。スクリーンショット 2021-05-01 173707

NTP同期をするために使用しているArduino Time LibraryがSDKに含まれるTime.hを上書きしているために発生しているようです。非推奨として上書きをしているようですが完全互換ではないためにエラーとなるようです。

  • プロジェクトをビルドし、M5Stackに転送します。

ソースコード

ダウンロード (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);
}

実行中の画面

IMG_6402IMG_6392

時間の経過とともに揺れが広がっていくことが確認できます。
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

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください