気象衛星ひまわりの衛星画像をM5Stack Core2に表示する方法

投稿者:

みなさんこんちは佐々木です。

さて今回は気象衛星ひまわりの衛星画像をM5Stack Core2に表示する方法をご紹介します。デスクトップの片隅に置いておけば台風の接近にいち早く気がつけるでしょう。

スワイプで衛星画像の移動・下部タッチボタンで画像の種類を変更
気象衛星ひまわりについて

気象観測を行う静止気象衛星です。赤道上空約35800kmで地球の自転と同じ周期で地球の周りを回っているそうです。いつも僕たちの頭上を飛んでいるようですね。もしかしたら地上から見えるかも。

気象庁 | 気象衛星観測について

気象衛星ひまわりの画像は下記ページで見れます。当初こちらのページから画像を借りてこようと思っていたのですが画像が256x256に分割されていて扱いづらく、

気象衛星ひまわりの画像

下記英語ページから借りてくることにしました。トゥルーカラー再現画像=True Color Reproduction Image、赤外画像=B13(Infrared)、雲頂強調画像=Sandwichの3種類を切り替えれるようにしています。

Meteorological Satellite Center (MSC) | Himawari Real-Time Image

必要なもの
セットアップ

公式サイトの手順を参考にデバイスドライバ・ArduinoIDE・ライブラリのインストールを行ってください。

スケッチ

新しいスケッチを作成し下記スケッチを張り付けてssidpasswordを環境にあわせて変更してM5Stack Core2に書き込んでください。マイクロSDカードをM5Stack Core2に挿入し電源を入れると自動的に衛星画像をダウンロードし画面に表示します。ダウンロードした衛星画像は日付毎にフォルダに分けてSDカードに保存されます。スワイプで画像を移動、下部のタッチボタンで衛星画像の種類を変更できます。

#include <WiFi.h>
#include <HTTPClient.h>
#include <M5Core2.h>

const char *ssid = ""; //Enter SSID
const char *password = "";  //Enter Password

const char *ca =
    // DigiCert Secure Site ECC CA-1
    "-----BEGIN CERTIFICATE-----\n"
    "MIIDyTCCArGgAwIBAgIQC1v2W2un+9CLKQ2QRTfe4DANBgkqhkiG9w0BAQsFADBh\n"
    "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
    "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD\n"
    "QTAeFw0xOTAyMTUxMjQ1MjRaFw0yOTAyMTUxMjQ1MjRaMGcxCzAJBgNVBAYTAlVT\n"
    "MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
    "b20xJjAkBgNVBAMTHURpZ2lDZXJ0IFNlY3VyZSBTaXRlIEVDQyBDQS0xMFkwEwYH\n"
    "KoZIzj0CAQYIKoZIzj0DAQcDQgAE5rt14WuvLvHn+zZK+KRSigbJRYTGRHg8A8Qk\n"
    "iFTl8S6JM3rPXQ45S7mZwHGqWnvKEM4w4PEPg9x0e8kZGb3fGqOCAUAwggE8MB0G\n"
    "A1UdDgQWBBTbNURdK+tTr54L9XE9o5lzrvtcUzAfBgNVHSMEGDAWgBQD3lA1VtFM\n"
    "u2bwo+IbG8OXsj3RVTAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUH\n"
    "AwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8CAQAwNAYIKwYBBQUHAQEEKDAm\n"
    "MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQgYDVR0fBDsw\n"
    "OTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFs\n"
    "Um9vdENBLmNybDA9BgNVHSAENjA0MDIGBFUdIAAwKjAoBggrBgEFBQcCARYcaHR0\n"
    "cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzANBgkqhkiG9w0BAQsFAAOCAQEAq/3I\n"
    "H6J/UvB/6Q9OECLSeul5xkW0PvhDzYJhcRJOZeWYWU575/+8ZnDJ/JhYx4wbEZ+P\n"
    "VCDLiVhiKU3//F8W7ZdsVLqhdVVrOoZJv+JZar3RZ1rgwhavgHB6Sq142nTSzG5J\n"
    "3O7+i2NZj4MJVM5uKPDUx659T2m2CsjzzXhFRnacQrN1QFh7+EUKXmxB1oFMcC8k\n"
    "4BSi4ZYvsAAvb8Xh0g4fHEq8fbAwffNSfEvY3JEbAjeRVA31J1ifBMwliRzPHGLf\n"
    "eyiYwvPQIUJ9ODieH5vDzLq9XvtdmFzBPXlFnHKI9LphN6sUVXdf4B+dao9drFZE\n"
    "ifuXbKlQ/2TRZPFeBg==\n"
    "-----END CERTIFICATE-----\n";

#define DBG_PRINT(s)     \
    do                   \
    {                    \
        Serial.print(s); \
        M5.Lcd.print(s); \
    } while (0);

#define DBG_PRINTLN(s)     \
    do                     \
    {                      \
        Serial.println(s); \
        M5.Lcd.println(s); \
    } while (0);

#define DBG_PRINTF(s, ...)             \
    do                                 \
    {                                  \
        Serial.printf(s, __VA_ARGS__); \
        M5.Lcd.printf(s, __VA_ARGS__); \
    } while (0);

HTTPClient http;
uint8_t http_buff[4096];
TFT_eSprite sprite(&M5.Lcd);

void setup()
{
    M5.begin();

    DBG_PRINTLN("Himawari Real-Time Image Viewer for M5Stack Core2");

    // Connect to wifi
    WiFi.begin(ssid, password);

    DBG_PRINT("WiFi connecting");

    // Wait some time to connect to wifi
    for (int i = 0; i < 30 && WiFi.status() != WL_CONNECTED; i++)
    {
        DBG_PRINT(".");
        delay(1000);
    }

    configTzTime("JST-9", "ntp.nict.jp");
    struct tm timeinfo;
    getLocalTime(&timeinfo, 60000);
    M5.Lcd.clear();
    sprite.createSprite(1024, 1024);

    http.setReuse(true);
}

const char *satelliteImageURL(const char *area, const char *band, const char *time)
{
    static char url[256];
    snprintf(url, sizeof(url), "https://www.data.jma.go.jp/mscweb/data/himawari/img/%s/%s_%s_%s.jpg", area, area, band, time);
    return url;
}

const char *satelliteImageFilename(const char *area, const char *band, const char *time)
{
    static char filename[128];
    snprintf(filename, sizeof(filename), "/%s_%s_%s.jpg", area, band, time);
    return filename;
}

typedef struct
{
    const char *area;
    const char *band;
    const char *describe;
    uint16_t w, h;
} AreaBand;

const AreaBand area_and_bands[] = {
    {"jpn", "trm", "True Color Reproduction (JMA, NOAA/NESDIS, CSU/CIRA)", 801, 641},
    {"jpn", "b13", "Band 13 (Infrared)", 801, 641},
    {"jpn", "snd", "Sandwich", 801, 641},
};

void loop()
{
    static unsigned long updateAt = 0;
    static const AreaBand *areaband = &area_and_bands[0];
    static int ofs_x = areaband->w / 2 - M5.Lcd.width() / 2;
    static int ofs_y = areaband->h / 2 - M5.Lcd.height() / 2;
    static char image_path[128];
    static char image_datetime[32];
    bool redraw = false;
    bool moving = false;

    M5.update();
    M5.Lcd.setTextSize(1);
    M5.Lcd.setCursor(0, 0);

    Event &e = M5.Buttons.event;
    if (e & E_MOVE)
    {
        ofs_x = min(max(ofs_x + (e.from.x - e.to.x), 0), areaband->w - M5.Lcd.width());
        ofs_y = min(max(ofs_y + (e.from.y - e.to.y), 0), areaband->h - M5.Lcd.height());
        redraw = true;
        moving = true;
    }
    if (e & E_RELEASE)
    {
        redraw = true;
        moving = false;
    }

    if (M5.BtnA.wasReleased())
    {
        areaband = &area_and_bands[0];
        updateAt = 0;
    }
    if (M5.BtnB.wasReleased())
    {
        areaband = &area_and_bands[1];
        updateAt = 0;
    }
    if (M5.BtnC.wasReleased())
    {
        areaband = &area_and_bands[2];
        updateAt = 0;
    }
    M5.update();

    unsigned long t = millis();
    if (t > updateAt)
    {
        updateAt = t + 60000;

        M5.Lcd.setCursor(0, 0);
        M5.Lcd.println("Loading..          ");

        time_t now;
        time(&now);

        time_t adjnow = int((now - 1200) / 600) * 600;
        struct tm timeinfo;
        gmtime_r(&adjnow, &timeinfo);

        char time_short[24];
        char time_full[24];
        strftime(time_short, sizeof(time_short), "%H%M", &timeinfo);
        strftime(time_full, sizeof(time_full), "%Y%m%d%H%M00", &timeinfo);

        const char *filename = satelliteImageFilename(areaband->area, areaband->band, time_full);
        const char *url = satelliteImageURL(areaband->area, areaband->band, time_short);
        bool sate_exists = downloadIfNeeded(url, ca, filename);
        if (sate_exists)
        {
            if (strcmp(image_path, filename) != 0)
            {
                strlcpy(image_path, filename, sizeof(image_path));
                sprite.drawJpgFile(SD, image_path, 0, 0, sprite.width(), sprite.height());
            }
            localtime_r(&adjnow, &timeinfo);
            strftime(image_datetime, sizeof(image_datetime), "%Y/%m/%d %H:%M%Z", &timeinfo);
            redraw = true;
        }
    }

    if (redraw)
    {
        bblt(&M5.Lcd, &sprite, ofs_x, ofs_y);
        if (!moving)
        {
            M5.Lcd.setCursor(0, 0);
            M5.Lcd.println(image_datetime);
            M5.Lcd.setCursor(M5.Lcd.width() - 6 * strlen(areaband->describe), M5.Lcd.height() - 8);
            M5.Lcd.println(areaband->describe);
        }
    }

    int wait = -millis() % 33;
    delay(wait ? wait : 33);
}

void bblt(TFT_eSPI *dst, TFT_eSprite *src, int16_t ofs_x, int16_t ofs_y)
{
    uint16_t iw = src->width();
    uint16_t *fb = (uint16_t *)src->frameBuffer(16) + iw * ofs_y + ofs_x;
    bool oldSwapBytes = dst->getSwapBytes();
    dst->setSwapBytes(false);
    for (int i = 0; i < dst->height(); i++)
    {
        dst->pushImage(0, i, dst->width(), 1, fb);
        fb += iw;
    }
    dst->setSwapBytes(oldSwapBytes);
}

bool downloadIfNeeded(const char *url, const char *ca, const char *save_to)
{
    bool exists = false;

    File f = SD.open(save_to, FILE_READ);
    if (f)
    {
        size_t sz = f.size();
        f.close();
        if (sz < 1024)
        {
            SD.remove(save_to);
        }
        else
        {
            exists = true;
        }
    }

    if (!exists)
    {
        if (!httpget(url, ca, save_to))
        {
            SD.remove(save_to);
        }
        else
        {
            exists = true;
        }
    }

    return exists;
}

bool httpget(const char *url, const char *ca, const char *save_to)
{
    bool result = true;

    DBG_PRINT("[HTTP] begin...\n");
    http.begin(url, ca);

    DBG_PRINTF("[HTTP] GET %s\n", url);
    int httpCode = http.GET();

    if (httpCode == HTTP_CODE_OK)
    {
        DBG_PRINTF("[HTTP] GET... code: %d\n", httpCode);

        WiFiClient *stream = http.getStreamPtr();
        int data_size = http.getSize();
        int read_size = 0;

        File f = SD.open(save_to, FILE_WRITE);
        if (f)
        {
            int x = M5.Lcd.getCursorX();
            int y = M5.Lcd.getCursorY();
            while (http.connected() && read_size < data_size)
            {
                int sz = stream->available();
                if (sz > 0)
                {
                    int rd = stream->readBytes(http_buff, min(sz, sizeof(http_buff)));
                    read_size += rd;
                    M5.Lcd.setCursor(x, y);
                    DBG_PRINTF("[HTTP] read: %d/%d\n", read_size, data_size);
                    f.write(http_buff, rd);
                }
                else
                {
                    delay(50);
                }
            }
            f.close();
        }
        else
        {
            DBG_PRINTLN("ERROR: OPEN FILE.");
            result = false;
        }
    }
    else
    {
        DBG_PRINTF("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
        result = false;
    }
    http.end();
    return result;
}
トゥルーカラー再現画像の利用規約

以下、トゥルーカラー再現画像の利用規約に従って表示します。

(1) トゥルーカラー再現画像の説明
トゥルーカラー再現画像は、ひまわり8号・9号の可視3バンド(バンド1、2、3)、近赤外1バンド(バンド4)及び赤外1バンド(バンド13)を利用し、人間の目で見たような色を再現した衛星画像です。本画像は、衛星によって観測された画像を人間の目で見たように再現する手法(参考文献[1])によって作成されています。この色の再現過程において緑色を調節するために、Millerらによる手法(参考文献[2])の応用として、バンド2、3、4が使用されています。また、画像をより鮮明にするために、大気分子により太陽光が散乱される影響を除去するための手法(レイリー散乱補正)(参考文献[2])が利用されています。

(2) 謝辞
トゥルーカラー再現画像は、気象庁気象衛星センターと米国海洋大気庁衛星部門GOES-Rアルゴリズムワーキンググループ画像チーム(NOAA/NESDIS/STAR GOES-R Algorithm Working Group imagery team)との協力により開発されました。また、レイリー散乱補正のためのソフトウェアは、NOAA/NESDISとコロラド州立大学との共同研究施設(Cooperative Institute for Research in the Atmosphere: CIRA)から気象庁気象衛星センターに提供されました。関係機関に感謝いたします。

(3) 参考文献
[1] Murata, H., K. Saitoh, Y. Sumida, 2018: True color imagery rendering for Himawari-8 with a color reproduction approach based on the CIE XYZ color system. J. Meteor. Soc. Japan., doi: 10.2151/jmsj.2018-049.

[2] Miller, S., T. Schmit, C. Seaman, D. Lindsey, M. Gunshor, R. Kohrs, Y. Sumida, and D. Hillger, 2016: A Sight for Sore Eyes - The Return of True Color to Geostationary Satellites. Bull. Amer. Meteor. Soc. doi: 10.1175/BAMS-D-15-00154.1

以上です、ぜひご活用ください!