うちIoT

Hayabusa2情報表示板

なんで作ったの?

バス到着情報表示板というのを作ったとき、ボタンで表示するものを切り替えられるようにしておきました。その記事の中で、

青ボタンは、はやぶさ2の地球までの距離(このページからweb scrapingできた)を表示させるために使おうと思ったのですが、Raspberry pi zero wではjava scriptの実行が遅いのかなんなのか、web scrapingがうまくいかず (Raspberry pi 3Bではできるんです) 、どうしようかと思案中です。別のRaspberry pi (3B)に取らせてpi zero wに送り込むか、あるいはpi zeroを3Bに置き換えるか。

と書いたんですが、あのあとずっと考えていて、どうしてもはやぶさ2の地球までの距離を表示したいと思い、本日、ついになんとかなりましたので、記事にします。(粘り勝ち)

なにをするもの?

はやぶさ2の、打ち上げからの時間(days)と、地球までの距離をHayabusa2 Projectのページから取得(web scraping)してLED matrixに表示するというものです。Web pageでは1秒ごとに更新されていて、いかにものすごい速さで”はやぶさ2”が飛んでいるのかを感じることができます。javascriptが動いて1秒ごとの更新をしているようなのですが、その仕組みがどうなっているのか、よくわかりません。家の表示板でも1秒ごとの更新ができたらカッコいいんですが、1秒ごとにwebまで取りに行くのも申し訳ありませんし(実際処理に1秒以上時間がかかって何をしているのかよくわからなくなる)、javascriptの部分だけもらってやってみる力もないし(できたらいいんですが)、あとどのぐらいあるのかな、がわかればいいので、30分に一度、web scrapingするようにしました。

haya2_info
2020 Feb23, 14:10 (+8:00)現在、2億3700万キロ以上ある。がんばれはやぶさ2。

どういう仕組み?

haya2_flow
表示までの情報の流れ

Raspberry pi zero w(二号機)が必要な時にRaspberry pi 3B(二号機)に情報とってきてリクエストをします(1)。頼まれたRaspberry pi 3Bは、Hayabusa2 projectのページを取得して(2,3)、javascriptが走るのを待ち、日数と距離のデータをhtmlファイルから抜き出してRaspi zero wに返信します(4)。あとはバス情報表示板のときと同じく表示内容をシリアル通信でArduinoに伝え(5)、LED matrixに表示がでる(6)というわけです。

なんで二台もRaspberry piがいるの?と思われるかたもいるかもしれません。理由は簡単で、pi zero wではweb scrapingに時間がかかりすぎるからです。何度か試したのですが、30秒程度で取れてくることもあれば、数分かかってしまうこともある。ばらつきがあまりに大きく、無理です、とpi zeroが言っているように思えたので、それなら、兄貴にやってもらうか、ということでweb scrapingの部分は他の仕事で常時稼働している兄貴分の3Bにやってもらうことにしました。いっそのことpi zeroのところを3Bに替えたら?とも思いましたが、バス情報の取得ではpi zeroがうまく動いていますし(小さいのががんばってるのは尊重したい)、二台のRaspberry piにやり取りさせて一つのことをするのもいいかな、それにどうすればできるのか、を考えるのも面白そうだったので、今回のような仕組みにしました。

使っている技(わざ)

興味のある方は、ここに出てくる文や単語をGoogle検索してさらに調べてみてください。
  • flaskで簡単なhttpレスポンスサーバーを作る
  • pythonでweb scrapingする
  • httpで2台のRaspberry piにやりとりさせる

作り方

材料

code

Raspi 3B側:flaskの仕事部分

from flask import Flask
from flask import jsonify

# === flask server ===
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False

@app.route('/haya2')
def haya2():
    # get time_since_launch, earth_to_hayabusa from web
    time_since_launch, earth_to_hayabusa = getHaya2Data()
    return_dict = {'time_since_launch': time_since_launch,
                   'earth_to_hayabusa': earth_to_hayabusa}
    return jsonify(return_dict)

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8080, debug=True)

getHaya2Data()関数からの返り値をdictにいれてjsonify()でレスポンスとして返します。6行目はそのままではflaskはasciiで文字を返してしまうとのことで、False設定しています。これでutf-8がそのまま通るようです。flask本家のここに説明があります。

Raspi 3B側:web scraping部分

from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time

# === web scraping function ===
def getHaya2Data():
    # optionsを入れる入れ物。
    options = Options()
    # Headless設定はこうする
    options.headless = True
    # Headless browser起動
    driver = webdriver.Chrome('/usr/lib/chromium-browser/chromedriver', options=options)
    print('implicitly_wait for 10 seconds from now.')
    driver.implicitly_wait(10)
    # browser access
    driver.get("http://www.hayabusa2.jaxa.jp/en/")
    print('sleep 5 seconds from now.')
    time.sleep(5)
    # encode page source
    html = driver.page_source.encode('utf-8')
    # BeautifulSoup
    soup = BeautifulSoup(html, "html.parser")

    # table parsing
    time_since_launch = "+0d"
    earth_to_hayabusa = "0.00"
    # get a table
    table = soup.findAll("table")[0]
    tbody = table.find("tbody")
    trs = tbody.find_all("tr")
    for tr in trs:
        th = tr.find("th")
        td = tr.find("td")
        # get only
        # Time since launch
        # and
        # Earth to Hayabusa2
        if th.text == "Time since launch":
            time_since_launch = td.text.split(' ')[0]
            print(time_since_launch)
        elif th.text == "Earth to Hayabusa2":
            earth_to_hayabusa = td.text.split('×')[0]
            print(earth_to_hayabusa)  # wow! × is not x!!!
    # quit browser
    driver.quit()

    return time_since_launch, earth_to_hayabusa

15, 19行目の待ちをなくしたり短くしすぎたりするとうまくうごかない。マシンパワーによると思われる。

Raspi zero w側:Raspi 3Bに頼む部分

import json
import urllib.request

# ref: https://requests.readthedocs.io/projects/requests-html/en/latest/

def getDataFromWeb():
    # web scraping is done on 3B2
    url = 'http://192.168.XXX.YYY:8080/haya2'  # Raspi 3Bでflaskが動いている
    req = urllib.request.Request(url, method='GET')

    with urllib.request.urlopen(req) as res:
        body = res.read()  # bodyはbytes
        decoded_body = body.decode('utf8')  # これでdecoded_bodyはstrになる
        resp = json.loads(decoded_body)
        print('this is resp:', resp)
        time_since_launch = resp['time_since_launch']
        earth_to_hayabusa = resp['earth_to_hayabusa']

    return time_since_launch, earth_to_hayabusa

8行目が兄貴分のRaspi 3Bで動いているflaskでのサーバー上のあて先です。どちらのRaspiも同じLAN内においてあります。

with構文でresponseを扱うのはどこかで見つけて参考にしましたが、どこだったか忘れてしまいました。いろいろ調べてまず手こずるのが、bytes文字のdecodingです。webから飛んでくる情報はbytesなので、それをpythonで使える形にする必要があります。こういったことをwebで検索すると、python2とpython3との情報が錯そうしています。丁寧に両者の違いを解説してくれているところがあるはずなので、探して読んでみてください。私はpython3を主に使います。このコードもpython3で試しています。

使ってみて

30分に一度の更新なので、あんまりスピード感がありませんが、小数点のすぐ上が、1000キロであることを考えて、更新でどれだけ進んだのかを計算してみると、びっくりします。現時点では太陽までより遠いな、というところですが、これからどんどん近づいてくるのが楽しみです。x1000の部分がそのうちいらなくなるのでしょうか?カプセルを切り離すときのスピードもものすごいはず(じゃないとはやぶさ2自体が地球に落ちてきてしまう)なので、x1000はずっと残るのかも。いまは距離が減る方向に変化していますが、カプセルを地球に届けたあとは増加に転じるわけですね。いつまで情報提供がつづくかわかりませんが、じっと見守りたいとおもいます。

このあとの予定

できればHayabusa2 projectのページで動いているjavascriptを自分のところで動かすことができれば毎回scrapingしなくてもいいし、毎秒更新ではやさが実感できる。本家ページも毎秒情報をどこかからとっているとも思えないし、何かの計算式でだしていると勝手に想像しているのだが、どこかで実際の位置との補正もしているはずだし、どうなってるんだ?と気になっています。こうなったらJAXAに問い合わせてみるのがいいかもしれないですね。以前、相模原キャンパスに見学に行ったことがあるのですが、あの時にだれかにきいてみればよかった。

ということで、ひとまずやりたいことはできるようになったので、これ以上改変はしないかもしれません。が、急に何か思ついて面白いものができたら記事にします。

うらばなし

2020年の1月に、web scrapingの小手試しをしていたところ、突然Hayabusa2のweb pageにつながらなくなってしまった。げ。もしかしてblockされた?とはいえ、そんなに不自然な回数web接続をしたわけでもなく(1秒に100回とか)、普通に手動で、走らせては、エラー、コードを直しては走らせる、を数回繰り返しただけ。数日して普通につながるようになったのだが、単なる偶然?メンテ中だった?よくわかりませんが、web scrapingはサーバー側に迷惑をかけないように、また取得した情報の扱いには注意が必要です。

ABOUT ME
misson
ものづくりが趣味。頭の中でモヤモヤと渦巻くガスを形として沈殿させるのが趣味。座右の銘は:こたえは常に自分の中にある


follow us in feedly

COMMENT

メールアドレスが公開されることはありません。