なんで作ったの?
毎朝、バスでいくか、それとも電車にするか、バスの待ち時間と混雑具合で決めたい、という娘のために作りました。そんなのケータイアプリがあるじゃん、と思われる方もおられるでしょうが、朝の忙しい時間に、常に表示されているのが便利なのです。また、バス情報がいらない時間には、ほかの情報も表示して遊びたいと思ったのです。
なにをするもの?
指定したバス停と、その前後二つのバス停(すぐ隣でなくても指定できる)のバス到着時間を表示します。色でバスの混み具合を表し、同じバスを表す数字は、線でつなぐようにしました。
色 | コード | 意味 | 意味 |
緑 | SEA | SEats Available | 座れるよ |
黄色 | SDA | StanDing Available | 立ってなら乗れる |
赤 | LSD | Limited StanDing | 立つ場所もないかも |


どういう仕組み?
バスの到着時刻のデータをインターネットから取ってきます。シンガポールの場合、Land Transport DataMallというところからデータを取得することができます。(簡単なユーザー登録が必要。無料。どんどんデータを活用して便利なアプリなどを作ってね、というノリで提供されているので、やる気もでます)データは、httpリクエスト(GET)のレスポンスとしてjson形式で得られます。あとはその中から必要な部分を抜き出して、少々加工して、LED matrixに表示させるという仕組みです。
Raspberry pi zero w(二号機)がデータを取得して加工し、シリアル通信に表示に関するデータを流します。Arduino(Adafruit METRO M0 Express)がシリアル通信の内容を受けてLED matrixに表示します。ってわけです。

使っている技(わざ)
興味のある方は、ここに出てくる文や単語をGoogle検索してさらに調べてみてください。
- Raspberry piで
- LEDをon-offする
- ボタンスイッチ(tactile switch button)が押されたことを検出する
- Raspberry piからArduinoにシリアル通信で情報を送る
- Python3でhttp request (GET)をしてjsonレスポンスを受け取る
- Python3, Arduinoでのjsonデータの取り扱い
作り方
材料
- Raspberry pi zero w
じゃなくてもいいですけど、これが安い。 - Adafruit METRO M0 Express (Arduino)
64×32 matrix(32×32より大きいもの)をコントロールするにはRAMが大きめじゃないとだめですよ、とあって、これにした。(Adafruitのページ) - Adafruit RGB Matrix Shield for Arduino
ArduinoとLED matrixをつなぐためのシールド。なくてもいいのですが、やたらケーブルが必要で、まとまりが悪い(最初のテストでそうしたのでわかります。下の写真参照) - Adafruit 64×32 RGB LED Matrix – 4mm pitch
表示板がないとはじまりません。 - 9V, 1A 電源
- あとは、LEDと330オームぐらいの抵抗、ボタンスイッチ(tactile switch button)、ジャンパーケーブルとブレッドボードです。
- できあがって面白がっている自分を想像する力と、途中でやめられないあきらめの悪さ(粘り強さともいいます)が必須です。

コンピューター環境
- Raspberry pi zero wはいわゆるheadless、モニタもマウスもキーボードもなし、という状態で使うので、ssh接続できるように準備しておく必要があります。
myToDo: Raspberry piのセットアップメモを記事にする - python codeを書いたりRaspberry piに接続してコードファイルを転送するためのPC
- python3
- Arduino IDE
接続
- Raspberry piの電源は5V USB電源から。
- METRO M0には9V電源をつなぎました。のせているシールドに5Vの出力があり、そこからLED matrixへの電源を取っています
- Raspberry piのUSB (mini)と、METRO M0のUSB (mini)とをUSBケーブルでつなぎます。ここをシリアル通信が流れます。
- LED、 ボタンスイッチ、 抵抗とRaspberry pi GPIOへの接続は以下の通りです。
- ボタン押しの検出は、GPIOをpulled-upにしておき、High to Low (falling)をとるようにします。

code
難しかったところや、あとで見たときに役に立ちそうなところだけについて書きます。
Raspberry pi側
まずはGPIOを使うための部分。self
のついた変数があるのは、CallBack
クラスの変数として使っているから。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import RPi.GPIO as GPIO ### 初期設定たち ### GPIO.setwarnings( False ) GPIO.setmode(GPIO.BCM) # GPIO pin numberで指定するため ## ボタン読み取りたち # pull upしておいて、ボタンでground touchさせる。 GPIO.setup( self .gray_btn_pin, GPIO.IN, pull_up_down = GPIO.PUD_UP) # fallingを検出する GPIO.add_event_detect( self .gray_btn_pin, GPIO.FALLING, bouncetime = 1000 ) # callbackを指定する GPIO.add_event_callback( self .gray_btn_pin, self .setProgramStatus) # setProgramStatusの中で、どのプログラムがactiveなのかを設定している ## LED点灯用 GPIO.setup( self .gray_LED_pin, GPIO.OUT, initial = GPIO.LOW) |
次にループ。ここが難しかった。手こずった。機械系のひとなら特になんでもない話かもしれないのだが、やりたいことをなんと表現したらいいのやらよくわからず、検索しても、 ちがうんだよ、そうじゃなくて、というページばかりにあたってしまい苦労した。最終的にlambda
で変数をcallableなものとして内側のループに持ち込むことで解決できた。
外側無限ループ:ボタンが押されるのを監視している。押されたボタンに応じてプログラムを呼び出す。呼び出されたプログラムも無限ループをもつ。(内側ループ)
内側無限ループ:今のところbus表示プログラム(ねずみ色ボタン)のみ。1分おきにbus statusをとりにいく。ほかの色のボタンが押されたとき、このループを止めなければならない。また、このループを回しつつ、外側ループにも制御が戻らなければならない。どうやる?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | # 外側ループ。ボタンが押されることを監視している。 # time.sleep(1)で回している。 # 基本骨格はいかのとおり while True : if self .gray_active = = 1 : # bus表示用のボタン(薄いねずみ色) # が押されたことをキャッチ。 # さて、bus表示用プログラムを起動したい。起動はできる。 # 呼べばいい。しかし、 # このプログラムも無限ループを持つ。 # - どうやって止めに入るか? # - このループが回っている間、いかにボタン監視をつづけるか。 # 普通に呼ぶと、向こうのループが回っている間、制御が帰ってこない。 # 一人管理人状態。 # これらをうまくやるにはどうしたらいいか、それが問題だった。 # 以下がいまのところの解決策。もっときれいな方法があるかもしれない。 # bus表示プログラム用のthreadを作る。 stop_th_gray = False # stop flagをFalseにする th_gray = threading.Thread(target = self .programGray, args = ( lambda : stop_th_gray,)) # ここで、programGray()が内側プログラムなのだが、 # argとしてstop_th_grayを渡す。 # が、だがしかし、そのまま渡してはいけない。 # lambdaの返り値として渡す。 # これで内側ループが回っている間にほかの色のボタンが押されたとき、 # stop_th_grayをTrueにすると、それがprogramGray()のループにわたる。 # なるほど、なのだが、なかなかこの答えにたどり着かなかった。 # thread開始 th_gray.start() # == truncated: blue, green btn captures === elif self .red_active = = 1 : # 赤ボタンプログラム。 # threadは立てない。立てる必要がない。 # programRed()が終了してここに制御がもどったら # breakでぬける。 self .programRed() # 終了メッセージをだして、赤LED点滅させる break # この無限ループから抜ける time.sleep( 1 ) sys.exit() |
Bus表示プログラム。あまり難しいことはしていない。bytesの取り扱いがpython3とpython(version2)とで違う。webで検索して試すと思いがけないエラーがでて混乱します。python3で動くコードなのか、python2じゃなきゃだめなのかをよく確認して採用しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | # 別ファイルとして書いている。 # 主なimportたちは import urllib.request import json import serial # http リクエストを送る部分:getBusInfo() # url構築は以下のように # Auth params headers = { 'AccountKey' : 'xxxxxxxxxxxxxxxxxxxxxx' , # 自分のものを使う 'accept' : 'application/json' } # API parameters url + = '/ltaodataservice/BusArrivalv2?' url + = 'BusStopCode=' + bus_stop url + = '&ServiceNo=' + bus_service # これで一発。 req = urllib.request.Request(url, headers = headers, method = 'GET' ) # jsonの内容をdictにとる。with構文で with urllib.request.urlopen(req) as res: body1 = res.read() # print(type(body1)) # bytesで返ってきますので decoded_body1 = body1.decode( 'utf8' ) # print(type(decoded_body1)) # strにして res_dict = json.loads(decoded_body1) # python dictに格納します # python dictに入ってしまえばこっちのものです。 # シリアルに送り出す方法:sendToSerial() # Raspberry piでのUSBがどういう名前になっているのかを確認しておく必要があります。 # ここではttyACM0だったので、それを使っています。 ser = serial.Serial( '/dev/ttyACM0' , 9600 , timeout = None ) # this is on raspi time.sleep( 2 ) json_string = json.dumps(info_dict) + '\n' ser.write(json_string.encode()) ser.close() |
Arduino側
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // Arduino IDEのFile > Examples > RGB matrix Panel > testshapes_32x64 // を参考にしました。 // 大事そうな行のみコピーしておく #include <ArduinoJson.h> #include <RGBmatrixPanel.h> #define CLK 8 // USE THIS ON ADAFRUIT METRO M0, etc. #define OE 9 #define LAT 10 #define A A0 #define B A1 #define C A2 #define D A3 RGBmatrixPanel matrix(A, B, C, D, CLK, LAT, OE, false , 64); void setup() { // Serial Serial.begin(9600); // LED matrix matrix.begin(); // draw a pixel in solid white matrix.drawPixel(0, 0, matrix.Color333(7, 7, 7)); delay(500); // fill the screen with 'black' 全部消す matrix.fillScreen(matrix.Color333(0, 0, 0)); // size, wrap matrix.setTextSize(1); // size 1 == 8 pixels high matrix.setTextWrap( false ); // Don't wrap at end of line - will do ourselves } // カーソル位置セット matrix.setCursor(3, 12); // テキスト色を指定 // Color333(r, g, b)で、0-7の値で指定する。 matrix.setTextColor(matrix.Color333(1, 1, 1)); // テキストを書く matrix.println( "something..." ); // 線を引く matrix.drawLine(19, 27, 23, 11, matrix.Color333(1, 1, 1)); // 1LEDだけ matrix.drawPixel(19, 27, matrix.Color333(1, 0, 0)); |
使ってみて
なんでもない表示といってしまえばそれまでなのですが、よくよく注意してみていると、次のバス停までの時間が伸びたり縮んだり、道路の込み具合が反映されるようですし、どの時間帯が最も混みやすいのかは、おおよそ把握できました。
電源が少し不安定で、同じコンセントから特にモーター系(コーヒーミル)などを使うと、突然プログラムが停止したりします。(コンセントの取り方を少し変えたらよくなりましたが、観察中)
このあとの予定
まだ、青と緑のボタンからの関数を作っていません。青ボタンは、はやぶさ2の地球までの距離(このページからweb scrapingできた)を表示させるために使おうと思ったのですが、Raspberry pi zero wではjava scriptの実行が遅いのかなんなのか、web scrapingがうまくいかず (Raspberry pi 3Bではできるんです) 、どうしようかと思案中です。別のRaspberry pi (3B)に取らせてpi zero wに送り込むか、あるいはpi zeroを3Bに置き換えるか。
家族からは
- ニュースの見出しを流す(新幹線みたいに)
- 覚えられない単語を繰り返し表示する(入力用webアプリかケータイアプリもつくってほしい)
- 雨、雷情報(そういうのがSMSでくるんです)をアラート音とともに表示
- パパの帰宅時、パパの家までの距離を表示(味噌汁温め開始タイミングをみはからう、ホント?)
などの意見が出ていますが(自分のも入ってます)、まだ決心がついていません。
うらばなし
実はこのバージョンの前に、Raspberry pi 3Bの上にLED matrix用のシールドをのせて試していました。(バス停表示はひとつのみ)LED matrixはpythonで動かしていたのですが、どうもノイズっぽい表示が消えず、またcrontabで起動直後に走るべきpythonスクリプトがうまくいったりエラーを出したり、再現性のあるエラー原因がつかめず、結局しばらく休業状態がつづいていました。やっぱり、こういう表示なんかはArduinoにまかせたほうが得意なんじゃない?データの取得と加工はRaspberry piでやろう、と思い立って作ったのが今回のバージョンです。ノイズも消え、ボタンもつけて、いろいろ表示できる(まだ作ってませんが)ようにもできたし、気に入ってます。