宇宙好きには堪らないことに、NASAが面白いAPIを公開している。その中には、太陽や小惑星の接近に関するものなど面白いものがある。そこで、今回は、太陽フレアの情報を常時表示するガジェットを作ってみよう。今回は、最初にブラウザ向けのものを作り、その後、電子ペーパーを搭載した端末M5PaperS3向けに配信するものを作ってみよう。

  • 太陽フレアを常時表示するガジェットを作ろう

    太陽フレアを常時表示するガジェットを作ろう

NASAのAPIを使うには

NASAが宇宙や天文学のデータのAPIを公開しており、とても興味深い。そもそも、APIとはプログラム同士が決められた手順で情報や機能をやり取りするための窓口のことだ。APIを使うと、自作のプログラムから手軽にそうしたデータを取得できる。

NASAのAPIを使うには、簡単な登録をして、APIキーの取得が必要だ。こちらにアクセスして、APIキーを取得しよう。名前とメールアドレスを入力するだけでよいので手続きも簡単だ。APIキーはメールで送られてくる。

  • NASAからAPIキーがメールで送られてきた

    NASAからAPIキーがメールで送られてきた

例えば火星の情報を取得するには?

例えば、火星の情報を取得するには、次のようなURLにアクセスする。DEMO_KEYの部分を上記で取得したAPIキーに差し替えてアクセスしよう。

火星情報を取得するAPIのURL
https://api.nasa.gov/insight_weather/?api_key=DEMO_KEY&feedtype=json&ver=1.0

なお、ブラウザでアクセスすると、次のように表示される。

  • ブラウザに火星の情報が表示されたところ

    ブラウザに火星の情報が表示されたところ

ただ、残念なことに、火星の情報は既にプロジェクト終了に伴い更新されなくなってしまっている。常に同じ値が表示される。それでも、宇宙のロマンを感じながらAPIの練習をするのには良い題材だ。

太陽フレアの発生状況を確認しよう

それでは、今回作成するガジェットに表示する情報を取得する方法を確認しよう。今回は、DONKI(The CCMC Space Weather [D]atabase [O]f [N]otifications, [K]nowledge, [I]nformationの略)と呼ばれる宇宙気象情報の中から太陽フレアに関する情報を取得してガジェットに表示しよう。

太陽フレアは、地球に対しても大きな影響があり、衛星や無線などにも影響するため、単にロマンではなく実用的にも面白い値だ。以下のようなURLにアクセスすることで情報を取得できる。

太陽フレアの情報を得るAPIのURL
https://api.nasa.gov/DONKI/FLR?startDate=2026-01-08&endDate=2026-01-09&api_key=DEMO_KEY

上記のURLをブラウザに貼り付けて見てみよう。すると1月8日から9日にかけての太陽フレアの発生状況を取得できる。ブラウザでアクセスしてみると次のように表示される。

  • 1月8日から9日の太陽フレアの発生状況データが得られたところ

    1月8日から9日の太陽フレアの発生状況データが得られたところ

いろいろなデータが記録されているが、ポイントとなるのが「beginTime」で、これは、フレアが発生した時間を表している。「peakTime」は、最もフレアが大きかった時間だ。そして、「classType」は、フレアの規模で「C」なら小規模、「M」なら中規模、「X」なら大規模であることを表す。

太陽のリアルタイム画像を取得しよう

とは言え、太陽フレアの発生情報を見るだけでは、それほど面白くない。そこで、現在時刻の太陽の画像も同時に表示してみよう。太陽の画像を取得するには、以下のようなURLにアクセスするだけだ。

太陽の画像を表示するURL
https://soho.nascom.nasa.gov/data/realtime/eit_284/512/latest.jpg

上記の画像を提供しているのは、SOHOのサイトだ。SOHOは、NASAとESA(欧州宇宙機関)が共同で運用している太陽観測衛星のことで、このサイトでは、太陽観測衛星が撮影した紫外線や極端紫外線を映した太陽画像が公開されている。

なお、太陽の色が黄色や緑になっているのは、波長により色を割り当てて可視化しているためで、今回は、黄色の高温コロナを可視化した画像を表示してみよう。

  • ブラウザに太陽の画像URLを入力したところ

    ブラウザに太陽の画像URLを入力したところ

Pythonで太陽フレア情報の画像を配信するサーバーを作ろう

そして、今回作成するプログラムだが、常時表示ガジェットを作成するために、まず、ローカルPCで太陽フレアに関する情報を画像を配信するWebサーバーを作成しよう。

太陽フレアの情報を配信するサーバーが以下のプログラムになる。以下のプログラムを「app.py」という名前で保存しよう。こちらにもアップロードしている。

""" 太陽フレア情報を画像で配信するWebサーバー """
from datetime import datetime, timedelta
from io import BytesIO
from pathlib import Path
import requests
from PIL import Image, ImageDraw, ImageFont
from flask import Flask, send_file, redirect

# NASA API キー (以下を書き換えてください) --- (*1)
NASA_API_KEY = "DEMO_KEY"
# IPAフォントのURLと保存パス
FONT_PATH = Path(__file__).parent / "ipag.ttf"
# Flaskアプリケーションの初期化 --- (*2)
app = Flask(__name__)

def get_solar_flare_data():
    """NASA DONKI APIから太陽フレア情報を取得"""  # --- (*3)
    # 昨日から今日までの日付範囲を設定
    today = datetime.now()
    yesterday = today - timedelta(days=1)
    start_date = yesterday.strftime("%Y-%m-%d")
    end_date = today.strftime("%Y-%m-%d")
    url = f"https://api.nasa.gov/DONKI/FLR?startDate={start_date}&endDate={end_date}&api_key={NASA_API_KEY}"
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        data = response.json()
        return data if isinstance(data, list) else []
    except Exception as e:
        print(f"API Error: {e}")
        return []

def get_solar_image():
    """SOHOから現在の太陽画像を取得"""  # --- (*4)
    url = "https://soho.nascom.nasa.gov/data/realtime/eit_284/512/latest.jpg"
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        img = Image.open(BytesIO(response.content))
        img = img.resize((400, 400), Image.Resampling.LANCZOS)  # リサイズ
        return img
    except Exception as e:
        print(f"Image Error: {e}")
        return Image.new('RGB', (400, 400), color=(0, 0, 0))  # 取得失敗時は黒い画像を返す

def analyze_flare_data(flare_data):
    """フレアデータを分析"""
    class_count = {'C': 0, 'M': 0, 'X': 0, 'Other': 0}
    for flare in flare_data:
        cls = flare.get('classType', 'Other')
        key = cls[0] if cls and cls[0] in 'CMX' else 'Other'
        class_count[key] += 1
    return class_count

def create_flare_info_image(flare_data, width=540, height=540):
    """太陽フレアの情報を表示する画像を作成"""  # --- (*5)
    # フレア情報を取得
    total_flares = len(flare_data)
    class_count = analyze_flare_data(flare_data)
    # 空の画像を作成
    img = Image.new('RGB', (width, height), color=(0, 0, 0))
    draw = ImageDraw.Draw(img)  # 描画用オブジェクトを取得
    font = ImageFont.truetype(str(FONT_PATH), 18)  # フォントサイズ
    text_color = (255, 255, 255)  # テキストの色
    # タイトル部分を描画
    draw.rectangle([(0, 0), (width, 50)], fill=(50, 50, 50))
    y_pos = 15
    draw.text((140, y_pos), "■ 太陽フレアの情報 ■", fill=text_color, font=font)
    y_pos += 70
    # テキスト情報を表示
    draw.text((20, y_pos), f"● 発生件数: {total_flares}", fill=text_color, font=font)
    y_pos += 40
    draw.text((20, y_pos), "● クラス毎の件数:", fill=text_color, font=font)
    y_pos += 40
    # 棒グラフを描画
    bar_colors = [(0, 255, 0), (255, 165, 0), (255, 0, 0)]  # C:緑, M:オレンジ, X:赤
    class_info = ["[C] 小規模", "[M] 中規模", "[X] 大規模"]  # クラスの説明
    bar_height = 20
    bar_width_unit = 500/ 10  # 最大10件まで表示可能と仮定
    # C/M/Xの順番でグラフを描画 --- (*6)
    for i, class_name in enumerate(['C', 'M', 'X']):
        graph_y = y_pos + 30
        # グラフのラベルを描画
        draw.text((20, y_pos), f"  {class_info[i]}:", fill=text_color, font=font)
        # 棒グラフの幅を計算
        v = class_count[class_name]
        bar_width = v * bar_width_unit
        draw.rectangle([(40, graph_y), (50 + bar_width, graph_y + bar_height)], fill=bar_colors[i])
        draw.text((50 + bar_width + 10, graph_y + 2), f"{v}", fill=text_color, font=font)
        y_pos += 70
    draw.text((20, y_pos), "● 詳細:", fill=text_color, font=font)
    y_pos += 40
    # 発生時刻リストを最大3件表示
    for flare in flare_data[:3]:
        begin_time = flare.get('beginTime', 'N/A').replace('T', ' ').replace('Z', '')
        classification = flare.get('classType', 'N/A')
        info_text = f"  - {begin_time} → {classification}"
        draw.text((20, y_pos), info_text, fill=text_color, font=font)
        y_pos += 40
    return img

def create_combined_image(solar_image, flare_info_image):
    """太陽画像とフレア情報を結合した960x540の画像を作成""" # --- (*7)
    # 最終画像を作成(黒い背景)
    final_img = Image.new('RGB', (960, 540), color=(0, 0, 0))
    # 左側に太陽画像を配置(上下中央揃え)
    y_offset = (540 - 400) // 2
    final_img.paste(solar_image, (20, y_offset))    
    # 右側にフレア情報を配置
    final_img.paste(flare_info_image, (460, 0))
    return final_img

@app.route('/image.jpg')
def get_image():
    """太陽フレア情報と画像を結合した画像を返す"""  # --- (*8)
    try:
        # データを取得して画像を生成
        flare_data, solar_image = [get_solar_flare_data(), get_solar_image()]
        flare_info_img = create_flare_info_image(flare_data, width=500, height=540)
        combined_image = create_combined_image(solar_image, flare_info_img)
        # メモリにJPEGとして保存して出力
        img_io = BytesIO()
        combined_image.save(img_io, 'JPEG', quality=85)
        img_io.seek(0)
        return send_file(img_io, mimetype='image/jpeg')
    except Exception as e:
        print(f"Error: {e}")
        return "Error generating image", 500

@app.route('/')
def root():
    return redirect('/image.jpg')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5555, debug=True)  # --- (*9)

プログラムを確認しよう。

(*1)では、NASAのAPIキーを設定している。先ほど取得したものに置き換えよう。(*2)では、Flaskアプリケーションを初期化している。(*3)は、太陽フレア情報を取得する関数だ。昨日から今日までの期間を指定してNASAのDONKI APIにアクセスし、太陽フレアの一覧データをJSON形式で取得する。通信エラーやAPIエラーが起きた場合でも、プログラムが止まらないように 例外処理(try ... except)で空のリストを返す工夫をしている

(*4)では、SOHO衛星が撮影した最新の太陽画像を取得している。取得したデータを画像処理ライブラリのPillowで開いてから 400×400にリサイズしている。もし画像が取得できなかった場合でも、黒いダミー画像を返すことで後続処理が壊れないようになっている。

(*5)は、このプログラムの中核となる画像生成処理だ。ここではフレア情報を分析し、発生件数・クラス別件数(C・M・X)・詳細リストを描画している。テキストだけでなく、棒グラフも描いているため、視覚的に状況が分かる画像になっている。特に、(*6)では、C・M・Xクラスの順番で棒グラフを描画する。

そして、(*7)では、左に太陽画像、右にフレア情報画像を配置し、960×540の1枚画像にまとめている。(*8)は、実際に画像を返すWebエンドポイントだ。サーバーのアドレス「/image.jpg」にアクセスすると、画像を生成して出力するようになっている。最後の(*9)でFlaskアプリを起動している。

プログラムを実行する方法

今回のプログラムでは、画像にテキストを描画するために、IPAフォントを利用している。IPAフォントをダウンロードして、プログラムと同じフォルダに保存しよう。こちらからダウンロードして解凍したら、「ipag.ttf」という名前で保存しよう。

あるいは、ターミナル(WindowsならPowerShell、macOSならターミナル.app)で「python」コマンドを実行して、REPL上で下記のプログラムを実行すると、ダウンロードできる。

import requests
FONT_URL = "https://github.com/hyoshiok/ttf-ipafont/raw/refs/heads/master/ipag.ttf"
response = requests.get(FONT_URL, timeout=10)
open("ipag.ttf", "wb").write(response.content)

プログラムを実行するには、ターミナルを起動して、以下のコマンドを実行しよう。

# Flaskをインストール
python -m pip install flask Pillow
# サーバーを実行
python app.py

そして、ブラウザで「http://localhost:5555/」にアクセスすると、次のように画像が生成される。

  • プログラムを実行したところ - ブラウザ上で太陽フレアの情報を表示する

    プログラムを実行したところ - ブラウザ上で太陽フレアの情報を表示する

ターミナル上でプログラムを実行すると、192.168…からはじまるIPアドレスが表示されるので、その値を控えておこう。このIPアドレスの値をこの後利用する。

表示端末のM5PaperS3について

さて、今回、太陽フレアの情報を表示する端末が「M5PaperS3」だ。これは、M5Stackが発売している電子ペーパー(E-Ink)を搭載したマイコン開発デバイスだ。4.7インチのディスプレイには、960×540ピクセル/16階調グレースケールを表示できる。電子ペーパーは表示を維持するのにほとんど電力を必要としないので、長時間の運用や電池での稼働が可能だ。

以下は、Amazonの電子ペーパー端末Kindleとサイズの比較をしている写真だ。iPhoneよりも小さいが、限られた情報を常時表示するのにはぴったりのサイズ感だ。なお、Amazonやスイッチサイエンスなどの通販サイトで購入できる。

  • M5PaperS3とKindleのサイズの比較

    M5PaperS3とKindleのサイズの比較

M5PaperS3でJPEG画像を定期的に表示しよう

M5PaperS3でプログラムを作成するには、まず、M5Burnerというアプリを使って、「UIFlow2.0 PaperS3」というファームウェアを端末に書き込む必要がある。

そのために、M5Burnerをインストールしたら、右上のメニューからM5Stackのユーザーを作成して、ログインをしよう。その際、メールアドレスなどを指定して、M5Stackのアカウントを作成する必要がある。

無事にM5Burnerでログインできたら、M5PaperS3をUSBケーブルでPCと接続したら、画面左側から「PAPER」を選び、画面右側の「UIFlow2.0 PaperS3」を選んで「Download」ボタンをクリックし、次いで「Burn」ボタンをクリックしよう。

  • M5BurnerでM5PaperS3にファームウェアを書き込んでいるところ

    M5BurnerでM5PaperS3にファームウェアを書き込んでいるところ

ファームウェアのイメージが無事に端末に転送されたら、こちらのUIFlow2のWebサイトにブラウザでアクセスしよう。UIFlow2では、Pythonもしくは、次の画面のようにブロックを組み合わせることでアプリを完成させることができる。

最初にプロジェクト作成画面が出るので、表示されたダイアログの右上にある[+]ボタンを押して、新規プロジェクトを作成しよう。そして、名前を「Flare Gadget」などと適当に決めて、デバイスに「M5PaperS3」を選択して「Confirm」ボタンを押す。すると、新規プロジェクトが作成される。

そして、プロジェクトの編集画面が出たら、次の画面のように、左上の(1)で画面設計をして、(2)でプログラムの動作を作成し、(3)の実行ボタンを押して、黒いコンソールが出たら(4)の実行ボタンを押すと、MicroPythonのプログラムが転送されて、M5PaperS3の端末上でプログラムが実行される。

  • UIFlow2の画面と使い方

    UIFlow2の画面と使い方

今回は、MicroPythonだけでプログラムを作るので、次の画面のように、画面上部にあるタブで「Python」を選択して、タブのすぐ下にある「Custom Edit」ボタンをクリックしよう。

  • MicroPythonのプログラムを使う手順

    MicroPythonのプログラムを使う手順

そして、下記のプログラムを書き込もう。先ほどと同じように、こちらにもアップロードしている。

import os, sys, io
import M5
from M5 import *
from image_plus import ImagePlus

image_plus0 = None

def setup():  # 初期化処理
  global image_plus0
  Widgets.setRotation(1)
  M5.begin()
  Widgets.fillScreen(0xeeeeee)
  # 定期的にWebサーバーから画像を取得する --- (*1) 書き換える★★★
  image_plus0 = ImagePlus("http://192.168.1.14:5555/image.jpg", 0, 0, True, 60000, default_img="/flash/res/img/default.png")
  Power.setLed(255)

def loop():  # メインループ
  global image_plus0
  M5.update()

if __name__ == '__main__':
  try:
    setup()  # 初期化
    while True:  # メインループ
      loop()
  except (Exception, KeyboardInterrupt) as e:
    try:
      image_plus0.deinit()
      from utility import print_error_msg
      print_error_msg(e)
    except ImportError:
      print("please update to latest firmware")

なお、上記の(*1)の部分「192.168.1.14」は筆者のPCのIPアドレスなので、先ほどPythonのプログラム「app.py」を実行した時に表示された、「192.168.」から始まる自分のPCのIPアドレスに書き換えよう。

LAN環境のセキュリティが厳しく、M5PaperS3実機からLAN内のPythonサーバーにアクセスできない環境、こちらにあるプログラムを試してみよう。これは筆者がテスト用の画像を用意したものだ。

上記のプログラムは、MicroPythonのコードだが、UIFlow2には「ImagePlus」というウィジェットが用意されており、これを使うと、自動的に指定秒数で画像をリロードするプログラムを作ることができる。ここでは、60秒に1回、画像生成サーバーにアクセスして、情報を取得して画面に表示するという設定にした。

プログラムを実行すると次の画面のように、M5StackS3に太陽フレアの情報が表示される。

  • M5PaperS3で実行したところ

    M5PaperS3で実行したところ

まとめ

以上、今回は、M5PaperS3を利用して、太陽フレアの画像を常時表示するガジェットを作成した。情報だけでなく、画像も表示するようにしたので、ちょっとしたSF感が出た。仕事机の上に設置して「今日は太陽フレアが少なくて良い」などと悦に浸ることができる。もちろん、M5PaperS3を使わなくても、一般的なブラウザでも同じ情報が表示できる。

なお、今回初めてM5PaperS3を使ったプログラムを作ったのだが、UIFlow2を使うことで、驚くほど簡単にプログラムを完成させることができた。面白かったので、次回もM5PaperS3を使ったプログラムを作ろうと思う。お楽しみに。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。これまで50冊以上の技術書を執筆した。直近では、「大規模言語モデルを使いこなすためのプロンプトエンジニアリングの教科書(マイナビ出版)」「Pythonでつくるデスクトップアプリ(ソシム)」「実践力を身につける Pythonの教科書 第2版」「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」など。