ラズパイゼロ+64x32LEDマトリクスでスマート電光掲示板を作った

# 概要
Raspberry Pi Zero WHと64x32のLEDマトリクスで**声で操作できる電光掲示板**を作成しました。

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">声だけで気温&amp;ニュース掲示板の起動<br>ただ起動まで1分近くかかるw<br>WAITINGを出してるのはそれが理由 <a href="https://t.co/dI30bpcZtW">pic.twitter.com/dI30bpcZtW</a></p>&mdash; すいばり@&#39;22年5戦4勝⚾️ (@Suibari_cha) <a href="https://twitter.com/Suibari_cha/status/1609171215163494401?ref_src=twsrc%5Etfw">December 31, 2022</a></blockquote>

実現にあたって必要な情報は大方検索すれば出てくるので、本記事はキュレーション的なまとめ記事になると思っています。
**ただし[rpi-rgb-led-matrix](https://github.com/hzeller/rpi-rgb-led-matrix/blob/master/wiring.md/)（本記事で使用するLEDマトリクスパネル（以下、パネル）点灯用ライブラリ）の使い方関連の情報は自分の調べた限りでは無かったので、本記事の情報が参考になると思います。**

# 目次
1. [必要なもの](https://qiita.com/Suibari_cha/items/45f4decd07f9dd51159a#%E5%BF%85%E8%A6%81%E3%81%AA%E3%82%82%E3%81%AE)
1. [ラズパイのセッティング](https://qiita.com/Suibari_cha/items/45f4decd07f9dd51159a#ラズパイのセッティング)
1. [配線](https://qiita.com/Suibari_cha/items/45f4decd07f9dd51159a#配線)
1. [コーディング](https://qiita.com/Suibari_cha/items/45f4decd07f9dd51159a#コーディング)
1. [スマートホーム化](https://qiita.com/Suibari_cha/items/45f4decd07f9dd51159a#スマートホーム化)

# 内容
## 必要なもの
以下が必要です。

|商品|価格|URL|
|----|----|----|
|Raspberry Pi Zero WH|元々所持|[スイッチサイエンス](https://www.switch-science.com/products/7600)|
|Raspberry Pi Zero用ACアダプタ 5V1.2A|元々所持|[楽天](https://item.rakuten.co.jp/sanwadirect/700-ac022w/?s-id=ph_pc_itemname)|
|64x32 HUB75規格対応LEDマトリクスパネル|2,970|[Amazon](https://www.amazon.co.jp/RGB-LED%E3%83%9E%E3%83%88%E3%83%AA%E3%83%83%E3%82%AF%E3%82%B9%E3%83%91%E3%83%8D%E3%83%AB-64X32%E3%83%94%E3%82%AF%E3%82%BB%E3%83%AB-256mmx128mm-%E5%AE%A4%E5%86%85%E7%94%A8/dp/B07KW9152T/ref=d_pd_sbs_sccl_4_6/356-0895899-2317717?pd_rd_w=k33BE&content-id=amzn1.sym.820591ed-a555-4556-9bf6-5ebd5493c69e&pf_rd_p=820591ed-a555-4556-9bf6-5ebd5493c69e&pf_rd_r=2FHZY63EPKZ5D67X5TDB&pd_rd_wg=IXPyl&pd_rd_r=d5e7916c-f3bb-434c-8277-e9af4e8a201a&pd_rd_i=B07KW9152T&psc=1)|
|メス-メス ジャンパー線最低14本|790|[楽天](https://item.rakuten.co.jp/zekei/e01_raspi_dupontwire01/)|
|パネル用ACアダプタ 5V4A|1,400|[楽天](https://item.rakuten.co.jp/cosmone/scl-5v4a-adapter-pse-ty01/?s-id=ph_pc_itemname)|
|DCジャック|送料込270|[楽天](https://item.rakuten.co.jp/wowsystem/dcjack2155/?s-id=ph_pc_itemname)|
|計|5,430|

パネルについて、2023/1/1現在、Amazonで最も安いRGB/HUB75規格対応/64x32のパネルなのでおすすめです。

**電源について、[ライブラリのREADME](https://github.com/hzeller/rpi-rgb-led-matrix/blob/master/wiring.md/#a-word-about-power)によると64x32のパネル用電源は5V7Aが推奨のようです。**
ただ、今回使用した5V4A電源でも輝度が落ちたり表示が消えるといったことはありませんでした。
判断は自己責任でお願いしたいですが、この電力でも問題ないと思います。

## ラズパイのセッティング
たくさんの記事があるので以下などを参考にするといいと思います。
自分は記事同様ヘッドレスセットアップしました。
https://memo.appri.me/iot/rpi0-headless-setup

## 配線
画像のような配線となっています。

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">パネル配線写真<br>qiita投稿用 <a href="https://t.co/tZ6XnpPjaD">pic.twitter.com/tZ6XnpPjaD</a></p>&mdash; すいばり@&#39;22年5戦4勝⚾️ (@Suibari_cha) <a href="https://twitter.com/Suibari_cha/status/1609518265218531328?ref_src=twsrc%5Etfw">January 1, 2023</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

以下ライブラリの配線を参考にすれば問題なくできると思います。
😄のマークのピン（E除く）を配線します。今回はrow32のパネルなのでDピンを使用し、Eピンは使用しません。
https://github.com/hzeller/rpi-rgb-led-matrix/blob/master/wiring.md/#connection

## コーディング
コードは以下です。
```Python
#!/usr/bin/env python
# Display a runtext and a static text with double-buffering.

# sys.pathにsitepackagesがあるのにbs4を読み込んでくれない…強制的に読み込ませる
import sys
sys.path.append("/home/pi/.local/lib/python3.7/site-packages")

from rgbmatrix import RGBMatrix, RGBMatrixOptions
from graphics import graphics
import time
import requests
from bs4 import BeautifulSoup
import re
import threading
import datetime
from PIL import Image

atcl_arr = []
temp = ""

def getNews():
    global atcl_arr
    atcl_arr_new = []
    
    # 接続
    print(str(datetime.datetime.now())+": getting NEWS...")
    #URL = "https://www.japantimes.co.jp/" # JT
    URL = "https://www.nikkei.com/news/category/" # 日経
    rest = requests.get(URL)
    soup = BeautifulSoup(rest.text, "html.parser")
    
    # HTMLパース
    #atcl_list = soup.find_all(attrs={"class" : "article-title"}) #JT
    atcl_list = soup.select('#CONTENTS_MAIN')[0].find_all(class_=re.compile("_titleL")) # 日経
    
    # 格納
    for atcl in atcl_list:
        print(atcl.string)
        atcl_arr_new.append(atcl.string)
    atcl_arr = atcl_arr_new
    
    return atcl_arr
        
def getTemperature():
    global temp
    
    print(str(datetime.datetime.now())+": getting current temperature...")
    URL = "https://tenki.jp/amedas/3/17/46091.html" # 海老名のアメダス
    rest = requests.get(URL)
    soup = BeautifulSoup(rest.text, "html.parser")
    temp_tag = soup.select('.amedas-current-list')[0].select('li')[0]
    temp_tag.select('span')[0].decompose()
    temp = temp_tag.text
    #print(temp.text)
    
    return temp

class createLED():
    # パネル設定用関数
    def __init__(self):
        self.font = []             # フォント
        self.textColor = []        # テキストカラー
        self.flagPreparedToDisplay = False # メイン関数を表示する準備ができたかどうかのフラグ
        
        # LEDマトリクス設定
        options = RGBMatrixOptions()
        options.rows = 32
        options.cols = 64
        options.gpio_slowdown = 0
        self.matrix = RGBMatrix(options = options)
        
        # キャンバス作成
        self.offscreen_canvas = self.matrix.CreateFrameCanvas()
        
        # 最低限のフォントだけ読み込み、WAITINGを表示させておく
        self.f = threading.Thread(target=self.displayLEDTemp)
        self.f.setDaemon(True)
        self.f.start()
        
        # 文字設定
        print("setting LED matrix options...")
        self.font.append(graphics.Font())
        self.font[0].LoadFont("/home/pi/Downloads/font/sazanami-20040629/sazanami-gothic_14.bdf") # 上の行と分割しないとセグフォ
        self.font.append(graphics.Font())
        self.font[1].LoadFont("/home/pi/Downloads/font/misaki_bdf_2021-05-05/misaki_gothic_2nd.bdf")
        self.textColor.append(graphics.Color(255, 255, 0))
        
        # 画像設定
        self.image = Image.open("/home/pi/ledmatrix/twitter_dot.png")

    # パネル点灯する関数（一時的）
    def displayLEDTemp(self):
        print('display "WAITING..."')
        font_simple = graphics.Font()
        font_simple.LoadFont("/home/pi/rpi-rgb-led-matrix/fonts/5x8.bdf")
        textColor_simple = graphics.Color(255, 255, 0)
        #while (self.flagPreparedToDisplay == False):
        timeout_start = time.time()
        while time.time() < timeout_start + 10: # 10秒経過後にWAITING表示を消すはずだが機能しない。ただ上のwhileより処理が早いのでこちらを使ってる…
            graphics.DrawText(self.offscreen_canvas, font_simple, 0, 31, textColor_simple, "WAITING...") # 静止文字
            self.offscreen_canvas = self.matrix.SwapOnVSync(self.offscreen_canvas)

    # パネル点灯する関数（メイン）
    def displayLEDMain(self):
        global atcl_arr
        global temp
        
        # Start loop
        i = 0
        pos = self.offscreen_canvas.width
        self.flagPreparedToDisplay = True
        print("display LED, Press CTRL-C to stop")
        while True:
            self.offscreen_canvas.Clear()
            graphics.DrawText(self.offscreen_canvas, self.font[1], 0, 7, self.textColor[0], "外気温"+temp) # 静止文字
            length = graphics.DrawText(self.offscreen_canvas, self.font[0], pos, 29, self.textColor[0], atcl_arr[i]) # 動く文字
            self.matrix.SetImage(self.image.convert('RGB'), 0, 8, False) # 画像
            
            # 動く文字の位置をずらす
            pos = pos - 1
            if (pos + length < 0): # 文字が左まで行って消えたら、posをリセット
                pos = self.offscreen_canvas.width
                # iをインクリメント、iがMAXなら0にする
                if (i == len(atcl_arr)-1):
                    i = 0
                else:
                    i += 1
            
            time.sleep(0.05)
            self.offscreen_canvas = self.matrix.SwapOnVSync(self.offscreen_canvas)
    
def worker():
    global atcl_arr
    global temp
    atcl_arr = getNews()
    temp = getTemperature()

def mainloop(time_interval, f, another):
    f() # 最初に情報取得の完了まで待たないと描画時にoutofindex
    
    now = time.time()
    t0 = threading.Thread(target=another) # argsの,は必要。ないとエラー
    t0.setDaemon(True)
    t0.start() # 描画
    while True: # 5分後、以後5分ごとに実行
        wait_time = time_interval - ( (time.time() - now) % time_interval )
        time.sleep(wait_time)
        t = threading.Thread(target=f)
        t.setDaemon(True)
        t.start() # 情報取得を実行
    
if __name__ == "__main__":
    LED = createLED()
    mainloop(300, worker, LED.displayLEDMain)
```

処理の流れは以下です。
1. 2の表示に必要な最低限のフォントなどを読み込み
1. パネルに"WAITING..."を表示
1. 最新ニュースおよび気温の情報をスクレイピングして取得
1. 5の表示に必要なフォント、画像などを読み込み
1. パネルに最新ニュースと気温を表示
1. 以後、5分ごとに3を実行しパネルにも取得情報を反映

こだわったポイントとしては以下2点があります。
* ライブラリのデモにもサポートがない2行以上の表示に対応
* マルチスレッド（Threading Module）を採用。描画と表示で役割を分担し処理を高速化

一点注意として、**パネルでの日本語の表示は不可能で一旦文字を画像に変換して表示するのが必要とかQiitaにも情報が掲載されていますが、不要だ**と思います。余計にオーバーヘッドが大きくなると思います。比較したわけではありませんが。。
普通にBDF形式の日本語フォントをダウンロードしてくるか、TTF形式のフォントをOTF2BDFで変換して読み込ませれば表示できると思います。
方法は以下参照。
https://yoshisyou.com/electric_bulletin_board_raspberry-pi/#%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%8C%E6%B5%81%E3%82%8C%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B

### パネル表示プログラムの最小構成

ライブラリのサンプルプログラムだとパネル表示するために必要なプログラムの最小構成が分かりにくかったので以下に載せます。
**描画はwhileの無限ループで行い、毎ループでRGBMatrixクラスのSwapOnVSyncメソッドを実行することが必要です。**
これを応用すればどんなに複雑な表示でも行えると思います。

```Python
# ライブラリインポート
from rgbmatrix import RGBMatrix, RGBMatrixOptions
from graphics import graphics
import time

# パネル設定
options = RGBMatrixOptions()
options.rows = 32
options.cols = 64
options.gpio_slowdown = 0
matrix = RGBMatrix(options = options)

# キャンバス作成
offscreen_canvas = matrix.CreateFrameCanvas()

# フォント設定
font_simple = graphics.Font()
font_simple.LoadFont("/home/pi/rpi-rgb-led-matrix/fonts/5x8.bdf")
textColor_simple = graphics.Color(255, 255, 255)

# 描画
while True:
    graphics.DrawText(offscreen_canvas, font_simple, 0, 31, textColor_simple, "hello world")
    offscreen_canvas = matrix.SwapOnVSync(offscreen_canvas) # 必要
```

## スマートホーム化
Node-REDを導入し、アレクサのウェイクワードからラズパイのコマンドを起動できるようにします。
以下記事が参考になります。
https://qiita.com/g-iki/items/a5d4d4674a30de7ed124

Node-REDで入力するコマンドは以下の通りにしました。
OFF時のコマンドについて、これでPythonで動かしている全てのプロセスを停止するようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110901/3affeef8-fa52-a791-d48a-22df6bb90eb8.png)

# おわりに
ネットに情報が多く上がっていたため、デモまでは凄くすんなり行きましたが、2行以上の表示など応用をやろうとすると大変でした。
ジャンパー線を使った工作作業は趣味でやるのは初めてだったため楽しかったです。
電光掲示板に表示する情報のアイデア募集中です。