HUB75をRaspberryPiで制御して新幹線車内の電光掲示板っぽいのを作る

# やりたいこと
（かなり曖昧な理解）
HUB75という規格のLEDマトリクスパネルがある。LEDマトリクスパネルというのはLEDが多数並んだディスプレイみたいなやつ。
これを利用して、新幹線車内の電光掲示板っぽいのを作ってみたい。え？ あれもうなくなったの？ コロナで出張してないから知らなかった……。

# 用意したもの
- Raspberry Pi3一式（手元に転がってた）
- LEDマトリクスパネル  https://www.amazon.co.jp/gp/product/B07SPFK8P1/
- 5V2.4Aの電源（LEDマトリクスパネル用）
- ブレッドボードとジャンパワイヤ

# 結線
今回利用するLEDマトリクスパネル制御用ライブラリ rpi-rgb-led-matrixの説明書を見ながらRaspberry PiとLEDマトリクスパネルを結線する。
https://github.com/hzeller/rpi-rgb-led-matrix/blob/master/wiring.md/
rpi-rgb-led-matrixで検索すれば日本語で説明してくれているサイトも見つかるはず。（ならこの記事の意味とは一体）

最初はRaspberry PiとLEDマトリクスパネルとを直接メスメスのジャンパワイヤで接続しようとしたのだけれど、LEDマトリクスパネル側のピンからすぐ抜けてしまった。十数本も接続するのに気がついたら抜けてるのは非常にストレスフル。

仕方ないので、LEDマトリクスパネルにデータケーブルを差し込み、Raspberry Piはブレッドボードに接続、それらを押す押すのジャンパワイヤで接続した。（直接データケーブルとRaspberry Piをオス・メスのジャンパワイヤで接続しても良かったのだけれど手元に数が無かった。）

## 電源
LEDマトリクスパネルはLEDのくせに意外と電気食いで、最大消費電力は20Wとか書いてある。実際には全てのLEDを最大輝度で白色に光らせることは無いのでそこまでではないにせよ、少なくとも5Vで2A〜3Aぐらいは供給できる電源が望ましい。
DAISOの5V3A USB-C電源（500円）が手元に転がっていたので、今回はこれを利用している。適当なUSBケーブルを切断して、赤(+)と黒(-)のラインを接続した。


# Raspberry Piのセットアップ
今回はヘッドレスモードのままで作業。
Raspberry Pi Imagerがバージョンアップして、無線LANの設定やホスト名、初期のID/PASSなどをGUIから設定できるようになってて便利。

## 初期設定
割愛
（sshだとかapt updateとかupgradeとかvimの設定とか）

## rpi-rgb-led-matrix
まずはgitからrpi-rgb-led-matrixのソースを取得してコンパイルする。
普段、ソースからコンパイルする機会が少ない生き方をしているので一周回って新鮮。
```
$ git clone https://github.com/hzeller/rpi-rgb-led-matrix.git
$ cd rpi-rgb-led-matrix/
$ make -C examples-api-use
```

後ほど、Pythonを利用して制御するつもりなので、そのための準備もしておく。
```
sudo apt update && sudo apt install python3-dev python3-pillow -y
PYTHON=$(which python3)
make build-python PYTHON=$PYTHON 
sudo make install-python PYTHON=$PYTHON
```

## オンボードオーディオのOFF
LED制御のためのクロックを作るときに、オンボードオーディオが邪魔するらしいのでOFFにしておく。
「/boot/config.txt」を編集し、

```
dtparam=audio=on
```
を探してoffに書き換えて再起動しておく。

## デモテスト
結線が正しかったのか、ここで明らかになる。

sudo ./examples-api-use/demo -D0 --led-rows=32 --led-cols=64 --led-no-hardware-pulse --led-slowdown-gpio=2

うまく行けば四角形がぐるぐるするデモがLEDマトリクスパネルに表示されるはず。
なお、このデモに限らず、今後作るPythonスクリプト含めてsudoが必要になってくる。動作するプログラムのpriorityが高くないとタイミング制御が維持できないためらしい。
> FYI: not running as root which means we can't properly control timing unless this is a real-time kernel. Expect color degradation. Consider running as root with sudo.

# Python

## フォント
電光掲示板では当然に日本語を流したいがrpi-rgb-led-matrixの機能では日本語が取り扱えない。
pillowを利用して64*32の画像を作ってそれをLEDマトリクスパネルに送ることで表示する。

ここで重要になったのはフォントの選択である。アンチエイリアスの利いた滑らかなフォントは、高精細ディスプレイでは素晴らしいがLEDパネル向きではない。縦16dotに縮めたら線や点が飛んで判別困難なこともしばしば。
ここは昔ながらのビットマップフォントの流れを汲んだ白黒２値のフォントが良さそう。

こちらのサイトで二次配布されているフォントがどれも素晴らしいものだった。
http://jikasei.me/font/jf-dotfont/
今回はこの中でJF-Dot-jiskan16.ttfを利用してみた。

Python用のディレクトリ下にフォント用のディレクトリを作成して放り込んでおく。

```
mkdir ~/python
mkdir ~/python/fonts
cd ~/python/fonts
curl -L https://osdn.jp/downloads/users/8/8542/jfdotfont-20150527.zip -o fonts.zip
unzip fonts.zip
```

LEDマトリクスパネル表示のサンプルプログラムを以下に示す。
正直、表示のための処理パターンさえ分かれば諸兄らにとっては後は表示したいものを準備するだけの作業となるだろう。

```
import time
from datetime import datetime
import sys

from rgbmatrix import RGBMatrix, RGBMatrixOptions
from PIL import Image, ImageDraw, ImageFont, ImageOps


font = ImageFont.truetype('fonts/JF-Dot-jiskan16.ttf', 16)

def main():
    try:
        matrix = get_matrix()

        print("Press CTRL-C to stop.")
        while True:
            image, draw = init_image()
            text = '国境の長いトンネルを抜けると雪国であった。夜の底が白くなった。'
            draw.text((0,16), text, (255,128,25), font=font)
            matrix.SetImage(image.convert('RGB'))
            time.sleep(2)

            # かなりいい加減なスクロール処理
            l = len(text) * 16
            for i in range(l):
                image, draw = init_image()
                draw.text((-i,16), text, (255,128,25), font=font)
                matrix.SetImage(image.convert('RGB'))
                time.sleep(0.05)

    except KeyboardInterrupt:
        sys.exit(0)


def init_image)):
    # Initialize image
    image = Image.new('RGB',(64,32),(0,0,0))
    draw = ImageDraw.Draw(image)
    clock = datetime.now().strftime('%H:%M')
    draw.text((0,0), clock, (255,128,25), font=font)
    return image, draw


def get_matrix():
    # Configuration for the matrix
    options = RGBMatrixOptions()
    options.rows = 32
    options.cols = 64
    options.chain_length = 1
    options.parallel = 1
    options.hardware_mapping = 'regular'

    options.gpio_slowdown = 5
    options.limit_refresh_rate_hz = 120
    # options.show_refresh_rate = 1
    options.brightness = 50
    options.disable_hardware_pulsing = True

    return RGBMatrix(options = options)


if __name__ == '__main__':
    main()
```

## ニュースの取得
正直、新幹線の中で流れるニュースについて、そこまで興味をもって眺めてたこともないので、それっぽいニュースのソースがあれば何でも良い。
今回はYahoo!ニュース トピックスのRSSを利用する。
https://news.yahoo.co.jp/rss/topics/top-picks.xml

RSSはXMLパーサがあれば構造的に取り出せる。標準ライブラリなので特にセットアップなくimportできるはずだ。
ざっくりとしたニュース取得のサンプルプログラムを以下に示す。

```
import requests
import xml.etree.ElementTree as ET

def get_news():
    url = 'https://news.yahoo.co.jp/rss/topics/top-picks.xml'
    response = requests.get(url)
    data = response.text
    root = ET.fromstring(data)
    return root

rss = get_news()
for item in rss.iter('item'):
    if item.find('title') is not None:
        news = item.find('title').text
        if item.find('description') is not None:
            news += '　　' + item.find('description').text
        else:
            continue
    print(news)
```
for文の中で変数newsにRSSのtitleとdescriptionが格納される。
これをさっきのLEDマトリクスパネル表示のスクリプトと組み合わせてやれば良い。


## 完成

```
import time
from datetime import datetime
import sys

import requests
import xml.etree.ElementTree as ET

from rgbmatrix import RGBMatrix, RGBMatrixOptions
from PIL import Image, ImageDraw, ImageFont, ImageOps


font = ImageFont.truetype('fonts/JF-Dot-jiskan16.ttf', 16)

def main():
    try:
        matrix = get_matrix()

        print("Press CTRL-C to stop.")
        while True:

            rss = get_news()
            for item in rss.iter('item'):
                if item.find('title') is not None:
                    news = item.find('title').text
                    if item.find('description') is not None:
                        news += '　　' + item.find('description').text
                    else:
                        continue

                image, draw = init_image()
                draw.text((0,16), news, (255,128,25), font=font)
                matrix.SetImage(image.convert('RGB'))
                time.sleep(2)

                l = len(news) * 16
                for i in range(l):
                    image, draw = init_image()
                    draw.text((-i,16), news, (255,128,25), font=font)
                    matrix.SetImage(image.convert('RGB'))
                    time.sleep(0.05)
                    
    except KeyboardInterrupt:
        sys.exit(0)


def init_image():
    # Initialize
    image = Image.new('RGB',(64,32),(0,0,0))
    draw = ImageDraw.Draw(image)
    clock = datetime.now().strftime('%H:%M')
    draw.text((0,0), clock, (255,128,25), font=font)
    return image, draw


def get_matrix():
    # Configuration for the matrix
    options = RGBMatrixOptions()
    options.rows = 32
    options.cols = 64
    options.chain_length = 1
    options.parallel = 1
    options.hardware_mapping = 'regular'

    options.gpio_slowdown = 5
    options.limit_refresh_rate_hz = 120
    # options.show_refresh_rate = 1
    options.brightness = 50 
    options.disable_hardware_pulsing = True

    return RGBMatrix(options = options)


def get_news():
    url = 'https://news.yahoo.co.jp/rss/topics/top-picks.xml'
    response = requests.get(url)
    data = response.text
    root = ET.fromstring(data)
    return root


if __name__ == '__main__':
    main()
```

# その他
Raspberry Pi Zero WHで同様の処理をすると、文字スクロールが著しく遅い。

```
sudo python -m cProfile -s tottime main.py > cProfile.txt
```

で何の処理に時間がかかっているかを確認したところ、Fontのrenderingがかなりの負荷になっているようだ。
ニュースごとに、文字列を矩形に書き出しておいて（=フォントレンダリングの回数を減らす）それを位置を調整しながら貼り付けていけばかなり速度的にはマシになった。