こんにちは、仙台支社で学生アルバイトをしているhashimoto,hayasaka,mimaです。
前回、「電光掲示システムReloadedの紹介」をさせていただきました。
今回はそこで使われた技術を「電光掲示システムReloadedを支える技術」と題して紹介させていただこうと思います!
ハードウェア構成
図のようにシンプルなハード構成になっています。
スイッチはLEDパネルの電源のみ制御する設計になっています。本当はスイッチでRaspberry Piの電源も制御するようにしたかったのですが、外部からシャットダウン処理を走らせる為にはRaspberry PiのOSと連動する専用の回路を用意しなければならない上に、シャットダウン処理をせずに電源を切断するのはRaspberry Piに負担がかかるため、今回は見送りました。
また、LEDパネルは大電流を要求することや、安全性や安定性、将来的にRaspberry Piから制御することを考慮して直接スイッチを接続するのではなく、リレーを挿入しました。
Raspberry Pi 3
通称ラズパイ。この会社はラズパイが大好きです。きっとこれは古事記にも書かれています。
聞くところによると札幌本社では野生のラズパイやジャンク充電器が出現するそうなので、早く仙台でも野生のラズパイや充電器が出現するといいなと思います。
スイッチング電源(DCDCコンバータ 100V→5V)
TDK-Lambda製VS100E-5を採用しました。
そこそこ効率の良いスイッチング電源です。ちなみにこれの仕様上パネルを天に向けておくことができません。
ただ、電流を多く食うと「ミューッ」っていう音が少しうるさい(とはいえそこまでではない)ので、気になる人はこれのハイエンド版を使うとよいかもしれません。
RGB Matrix LED
32×32 RGB LED Matrix Panel – 6mm pitchを採用しました。
このパネルは制御にあたって、後述するrpi-fb-matrixという便利なライブラリを使えます。自作に挑戦するならば恐らく一番手の出しやすいLEDパネルなので、採用しました。
以下仕様です。
- 32×32=1024個のRGB LED
- デイジーチェーンで複数個接続可能
- 3.3V〜5V駆動
- パネル1枚について、16MHz ArduinoのCPU使用率40%程度で12bitカラー(4096色)が表示可能
- 0.15ピッチの4ピン電源ケーブル
- 16ピン(2×8)リボンケーブルで接続
ピン配置は以下のようになっています。
- Hub 75のインターフェース
- Hub 75のインターフェースのピン配置
- パネルのロットにより
G0
とG1
のように印刷されているデータラインのピン番号が違う場合がありますが、表示上の違いですので気にせずに使用してください
- パネルのロットにより
ピンアサインの表
Panel Pin name | Panel Connector Pin | Notes |
---|---|---|
R0 | 1 | Red data (columns 1-16) |
G0 | 2 | Green data (columns 1-16) |
B0 | 3 | Blue data (columns 1-16) |
GND | 4 | Ground |
R1 | 5 | Red data (columns 17-32) |
G1 | 6 | Green data (columns 17-32) |
B1 | 7 | Blue data (columns 17-32) |
GND | 8 | Ground |
A | 9 | Demux input A0 |
B | 10 | Demux input A1 |
C | 11 | Demux input A2 |
D | 12 | Demux input E1, E3 (32×32 panels only) |
CLK | 13 | LED drivers’ clock |
STB | 14 | LED drivers’ latch |
OE | 15 | LED drivers’ output enable |
GND | 16 | Ground |
ピンは2×8の16本となっており、この32×32のLEDパネルの場合は、1つのクロックで2個のLEDの色を同時に決めることができるようになっています。
文字等を表示させるには、表示させる内容に応じてそれぞれ光らせたい色をRGBのピンアサイン、そのLEDの場所をA,B,C,Dピンアサインの4ビットで指定してクロックを立ち上げることでデータを次々に送り、所望の内容を表示しています。
ABCDの4ビットでは16個分のLEDの位置しか指定することができませんが、ラッチを使って一度に2個のLEDの色を決めることができるので、無事32個分つまりこのLEDパネル1列分の色を決めることができます。この動作を表示させたい画像や文字のRGB値に応じて高速に繰り返すことでフルカラーの表示が可能になっています。
これらの情報は販売元のadafruit社のサイトにあるpdfから詳しく得ることが出来ます。
Adafruit HAT
自分が手作りして信じて送り出した(自作した)シールドは、肝心なときに突然死んでしまったので、ハットを買ったほうが幸せです。そもそもLEDの信号は高速回路を意識したものにする必要があるのでこれがよいと思います。
また、これのうっかりポイントとして、後述するrpi-fb-matrixにおいてmakeするときに、パラメータを切り替える必要があります。切り替えないとデフォルトのGPIOピンで起動するのでうまく使えません。
自作筐体
前回の記事で掲載したデモ筐体についてのお話です。
完全にDIYの連続でした。
アイデアを書き起こしたりしました。
どのような筐体にするのか、相談しながら設計をしました。
作ってる過程の写真です。ハンダをいじった時は手を焼き肉してしまって辛かったです:(
内部はメンテナンス性を考え、パネルをボルトで固定出来るよう、鬼目ナットを使った柱を使用しました。パネルの整列は板に小型のボルトとダブルナットで固定しているので、細かい高さの調整が可能です。
工具はオフィスにあるものでは足りないので持参しました。悲しい事にハンダゴテの先っぽを2回ぐらい交換してしまいましたし、木材加工は節にあたって細いドリルが折れたりしたので苦労しました。
なおオフィスで木材加工をするのは音や木くずなど色々な部分で気を使うので、極力ホームセンターで加工して、オフィスで組み立てるほうが幸せです。
ソフトウェア構成
図のようなソフトウェア構成になっています。
rpi-fb-matrix
ラズパイのVRAMの内容を取得し、GPIOを通じてLEDパネルに送信するソフトウェアです。
adafruite/rpi-fb-matrixをforkしたものを使っています。
特にmatrixdからパラメーターを引き取ってはいませんが、一応のちらつき防止として、matrixdと同じFPSで動作するようにしています。
VRAMに描画されていれば何でもLEDパネルに表示できるので、簡単に動画を流せて面白いです。
matrixd
matrixdは、設定ファイルに書かれてる設定やビューモデルをもとに描画するデーモンです。C++で書かれてます。
ここでは、ビューモデルという言葉を「描画内容を表したデータ」という意味で使っています。
Xlibを直接使って描画していますが、Xのクライアント・サーバモデルから来るつらい所が多くあり、gtkなどのライブラリを使うべきだったと思いました。
上述したバンクのスクロール、繰り返し、チェインなどの処理もこのデーモンが行ってます。
ところで、今回使ったC++のテストフレームワークであるGoogle Testでは、構造体やクラスをテストする際に、EXPECT_THATやASSERT_THATを使うことでパターンマッチ的にテストを書くことができます。
しかし、Field Matcherのエラーメッセージには、どのフィールドのMatcherかが含まれないためわかりづらく、その点がつらかったです。
さらに、いくらパターンマッチ的にテストが書けると言っても巨大な構造に対するテストを書くのは大変なので、スナップショットテストを使いたいなぁと常に思いながらテストを書いてました。つらい。
matrixd cli
matrixdのコマンドラインインターフェースです。同じくC++で書かれています。
設定ファイルからビューモデルを読み込み、コマンドライン引数からビューモデルを変更し、設定ファイルに書き込んだ後にmatrixdにSIGHUPを送ります。
初期の頃は仮想ディスプレイやバンクの変更の頻度がそんなに高くならないことを想定していたので、ファイルとSIGHUPを使ったプロセス間通信を採用しました。
しかし、デモを作る段階になってから、仮想ディスプレイやバンクの変更の頻度がそれなりに高くなる場合があることが判明しました。
今となってみれば、別の方法のほうが良かったかなぁと思います。UNIXドメインソケットとか。
matrixd cliはgitのようにサブコマンドを使うインターフェースになっています。
リフレクションを使えると、このサブコマンドを非常にうまく実装できますが、C++にはリフレクションがないので、なくなくswitchを使っています。
D言語さえ、D言語さえ使えれば・・・
api-server
なんのひねりもない名前ですが、WebAPIとmatrixd cliの橋渡しをするAPIサーバです。
Rubyで書かれてます。ライブラリとしてはSinatraを使っています。
ブラウザからPUTメソッドかDELETEメソッドを使ってクロスオリジンHTTPリクエストをサーバに送信する場合、
ブラウザはまずOPTIONSメソッドでリクエストを送り、クロスオリジンのPUTメソッドやDELETEメソッドが許可されているかどうかを確認します。
HTTP アクセス制御 (CORS)
これを知らずに結構な時間ハマってました。
ちゃんとCORSの設定をしましょう。
Reloaded WebUI
ReloadedのWebUIです。JavaScriptで書かれてます。
ライブラリとしてReact、Redux、Material-UIを使っています。
パッケージマネージャとしてyarn、バンドラとしてwebpack、型チェッカとしてflow、linterとしてESLint、フォーマッタとしてPrettier、テストランナーとしてAVAを使っています。
Reduxのactionを直和型として型付けすることで、action creatorを作らずに直接actionをdispatchできるようにしました。
export type Action = | {| type: "REQUEST_GET_WHOLE_DATA", async: true |} | {| type: "REQUEST_PUT_VIEW_MODEL", async: true |} | {| type: "SUCCEED_GET_WHOLE_DATA", sync: true, payload: { viewModel: ViewModel, images: Image[], fonts: Font[] } |} | {| type: "SUCCEED_PUT_VIEW_MODEL", sync: true, payload: { viewModel: ViewModel } |}
action creatorを使わなくなったので、actionをsyncとasyncの2種類に分け、asyncなactionをmiddlewareで処理するようにしました。
export function handleAsyncActionMiddleware( api: Api ): (MiddlewareAPI<State, Action>) => (Dispatch<Action>) => Action => Action | Promis<Action> { return middlewareApi => next => async action => { if (action.async) { switch (action.type) { case "REQUEST_GET_WHOLE_DATA": { const { apiUrl } = middlewareApi.getState() const [viewModel, images, fonts] = await Promise.all([ api.getViewModel(apiUrl), api.getImages(apiUrl), api.getFonts(apiUrl), ]) return middlewareApi.dispatch({ type: "SUCCEED_GET_WHOLE_DATA", sync: true, payload: { viewModel, images, fonts }, }) } case "REQUEST_PUT_VIEW_MODEL": { const { apiUrl, viewModel } = middlewareApi.getState() const updatedViewModel = await api.putViewModel(apiUrl, viewModel) return middlewareApi.dispatch({ type: "SUCCEED_PUT_VIEW_MODEL", sync: true, payload: { viewModel: updatedViewModel }, }) } default: { ;(action.type: empty) throw "switch statement should be exhaustive" } } } return next(action) } }
flowは直和型の値に対してswitch文を書いた時に、default節でその値に型注釈emptyを付けることで、caseの網羅性をチェックできてとても良いです。
テストについても少し触れます。
今回は、AVAでreducerとmiddlewareのテストを書きました。
上のコードを見てもらうとわかりますが、DIのために、APIを呼ぶときは直接fetchを叩かずにapiオブジェクトを介して呼ぶようにしています。
さらに、テストをしやすくするため、apiオブジェクトを作る際にfetch関数を渡すようにして、テストのときにはfetch-mockを使うようにしました。
reducerとmiddlewareのテストは、スナップショットテストを使ってサクッと書きました。
今回reduxで扱っているstateはちょっと大きいので、普通にassertを書くと大変です。
スナップショットテストを使うと、こういった大きなオブジェクトが簡単にテストできてとても良いです。
test("REQUEST_GET_WHOLE_DATA", async t => { const fakeFetch = fetchMock .sandbox() .get(`${STATE_FIXTURE.apiUrl}/view-model`, { status: 200, body: WHOLE_DATA_FIXTURE.viewModel }) .get(`${STATE_FIXTURE.apiUrl}/images`, { status: 200, body: WHOLE_DATA_FIXTURE.images }) .get(`${STATE_FIXTURE.apiUrl}/fonts`, { status: 200, body: WHOLE_DATA_FIXTURE.fonts }) const store = createMockedStore(fakeFetch) await store.dispatch({ type: "REQUEST_GET_WHOLE_DATA", async: true }) t.snapshot(fakeFetch.calls()) t.snapshot(store.getActions()) })
一部のapiではFormDataを使います。
testdouble.jsというライブラリを使ってFormDataのテストダブルを作ることでテストしています。
更に、テストダブルを少し変更し、FormDataのテストダブルのスナップショットにコンストラクタとメソッドの呼び出し一覧が含まれるようにすることで、必要なスナップショットの数を減らしています。
function createMockedStore(fakeFetch: *): * { const FakeFormData = td.constructor(["append"]) td.when(FakeFormData()).thenDo(function() { // $flow/issues/285 Object.defineProperty(this, "calls", { enumerable: true, get: () => ({ constructor: td.explain(FakeFormData).calls, append: td.explain(FakeFormData.prototype.append).calls, }), }) }) return configureStore([handleAsyncActionMiddleware(createApi(fakeFetch, FakeFormData))])(STATE_FIXTURE) }
終わりに
これが私たちの進捗です!
所感として、Reloadedを作ってみて、C++を使うべき状況は非常に限られているので、パフォーマンスが必要だからといって何も考えずにC++を選択するのはやめたほうがいいことに気づきました。
JavaScriptに関しては、最後に多少時間的余裕があったので、じっくりテストを書くことができました。とても満足しています。
ハードウェアに関して、工具が少ない中オフィスでハンダゴテ以上の工作をやるのは無謀だなぁと思いました。つらい。
皆さんもハードウェアは既製品を使ってPythonで作ってみましょう!
インフィニットループ仙台支社はアルバイトを募集しています!
現在の仙台アルバイト勢は、COMPを愛しCOMPで生きてて人工言語が好きな人だったり、自作コンパイラ最近作ってる人だったり、電気通信を活用することに生きがいを感じてる人だったりがみんな楽しく業務に励んでいますので、ぜひ応募してください!