Nerves電子ペーパーでHello world


https://qiita.com/advent-calendar/2021/nervesjp

本記事は[「NervesJP Advent Calendar 2021」](https://qiita.com/advent-calendar/2021/nervesjp)の1日目です。

## はじめに

世の中には様々な「[Hello World!](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program)」プログラムがあり、まずはそうして成功体験をすることが多いと思います
[Nerves]の世界ではよく[Lチカ](https://qiita.com/search?q=tag%3ANerves+body%3AL%E3%83%81%E3%82%AB&sort=)で練習したりしますが、今日は[Nerves]での[Hello World!]をひとつご提案させていただこうと思います

![nerves-hello-inky-20211120_221626](https://user-images.githubusercontent.com/7563926/142748201-8b472dc3-1f48-4686-bb84-8b6ad841c753.jpg)

## やること

- [nerves_livebook]にちょっと手を加えて、Raspberry Pi Zeroに最適なサイズの電子ペーパー[Inky pHAT]で遊べるようにする

参考資料

- [Underjord]のYouTubeビデオ[Raspberry Pi, eInk and Nerves with Livebook](https://www.youtube.com/watch?v=4cKwihjqiLc)
- [メーカーのPythonコードサンプル](https://learn.pimoroni.com/article/getting-started-with-inky-phat)

## 必要なもの

- [Inky pHAT]
  - Raspberry Pi Zeroに最適なサイズの電子ペーパー
- [Raspberry Pi Zero WH]
  - ヘッダーピン付き
  - 名前は似てますが、Raspberry Pi Zero Wはヘッダーピンがついていないので注意
  - 古いRaspberry Pi ZeroはWiFiがついていないので注意
  - おそらく他のラズパイでもOK、しらんけど
- [microSDカード] (スピードクラス10)
- microSDカード リーダーライター
- USBケーブル（microB端子）
  - ラズパイの電源用
  - また、USBガジェットモードでラズパイ<->ホストマシン間で直接通信が可能

## 準備

- 動作確認はMacOSで行ってます

```
elixir          1.13.0-rc.0-otp-24
erlang          24.1.4
```

### [nerves_livebook]プロジェクトをクローンする

```ssh
$ git clone https://github.com/livebook-dev/nerves_livebook.git
$ cd nerves_livebook
```

### 依存関係

[nerves_livebook]のUI上で必要な依存関係をインストールできれば理想的なのですが現時点(2021年11月)ではできないようですので、ファームウエアをビルドする前に予めお手元のPCで依存関係をインストールする方針です

- [pappersverk/inky]
  - Inkyディスプレイドライバー
  - あいにく[Inky pHAT]現行機種（pHAT SSD1608）に対応していないため、代わりに僕のフォーク[mnishiguchi/inky]を使います
- [luisgabrielroldan/chisel]
  - ビットマップフォント処理

[Elixir Circuits]の各種ライブラリーは[pappersverk/inky]内部で使用されているものと衝突するので、`override: true`オプションを付与します

`mix.exs`ファイルに依存関係を追加

```diff_elixir:mix.exs
defp deps do
  [
    # ...

-   {:circuits_uart, "~> 1.3", targets: @all_targets},
-   {:circuits_gpio, "~> 1.0 or ~> 0.4", targets: @all_targets},
-   {:circuits_i2c, "~> 1.0 or ~> 0.3", targets: @all_targets},
-   {:circuits_spi, "~> 1.0 or ~> 0.1", targets: @all_targets},
+   {:circuits_uart, "~> 1.3", targets: @all_targets, override: true},
+   {:circuits_gpio, "~> 1.0 or ~> 0.4", targets: @all_targets, override: true},
+   {:circuits_i2c, "~> 1.0 or ~> 0.3", targets: @all_targets, override: true},
+   {:circuits_spi, "~> 1.0 or ~> 0.1", targets: @all_targets, override: true},
    # ...
+   {:chisel, "~> 0.2.0", targets: @all_targets},
+   {:inky, git: "git@github.com:mnishiguchi/inky.git", branch: "mnishiguchi/ssd1608", targets: @all_targets},
    # ...
  ]
end
```

### [nerves_livebook]ファームウェアを[microSDカード]に焼く

[microSDカード]をリーダーライターにセットしてお手元のPCに接続します

```bash
# ターゲットマシンの機種を指定（Raspberry Pi Zeroは`rpi0`）
$ export MIX_TARGET=rpi0

# 依存関係のインストール
$ mix deps.get

# ファームウエアをビルド
$ mix firmware

# ファームウエアをmicroSDカードに焼く
$ mix burn
```

![nerves burn Screen Recording 2021-11-20 at 4 59 49 PM](https://user-images.githubusercontent.com/7563926/142742104-e68ce0a5-fe14-4db0-9794-f3ac16ce3726.gif)

### 電源ONして接続してみる

Pingコマンドを実行して接続確認

![ping nerves Screen Recording 2021-11-20 at 12 32 55 PM](https://user-images.githubusercontent.com/7563926/142735853-467ea209-3951-4549-9542-406e50ac6837.gif)

SSHしてみる

- パスワードは`nerves`です

![nerves-livebook ssh Screen Recording 2021-11-20 at 12 26 29 PM](https://user-images.githubusercontent.com/7563926/142735685-89fa925c-c3da-49e5-a29c-12fd05b6d4bf.gif)

ウェブブラウザーで[nerves_livebook]のUIを開いてみる

- http://nerves.local
- パスワードは`nerves`です

![nerves-livebook ui Screen Recording 2021-11-20 at 8 50 33 PM](https://user-images.githubusercontent.com/7563926/142746348-734a7735-3969-4871-9398-a327e275d22a.gif)

## [nerves_livebook]のUIから[Inky pHAT]を操作する

- [nerves_livebook]のUI（）、もしくはIExから[Inky pHAT]を操作できます
- 「[Livebook Lチカをイゴかす (Elixir)](https://qiita.com/torifukukaiou/items/2f7c9f460fde510356e8#%E6%BA%96%E5%82%99)」 by @torifukukaiou を併せて読むと良いかも

### Inkyサーバーを起動

```elixir
display_type = :phat_ssd1608
accent = :black

{:ok, inky_pid} = Inky.start_link(display_type, accent)
```
### フォントを読み込む

- フォントデータは[olikraus/u8g2](https://github.com/olikraus/u8g2/tree/master/tools/font/bdf)からダウンロードします
- 今回は便宜上フォントはインターネットから都度ダウンロードする方式にしましたが、使うフォントが決まっているのであればファームウエアに予め焼いておいたほうが良いかもしれません
  - 例、`rootfs_overlay/fonts/7x14.bdf`

```elixir
font_name = "7x14"
fonts_dir = "/data/fonts" |> tap(&File.mkdir_p/1)
font_path = Path.join([fonts_dir, "#{font_name}.bdf"])

font_url =
  "https://raw.githubusercontent.com/olikraus/u8g2/master/tools/font/bdf/#{font_name}.bdf"

if File.exists?(font_path) do
  IO.puts("Already exists #{font_path}")
else
  {:ok, {{_, 200, _}, _headers, body}} = :httpc.request(font_url)
  IO.puts("Downloaded font #{font_name}")
  File.write(font_path, List.to_string(body))
end

IO.puts("Loading font to #{font_path}")
{:ok, chisel_font} = Chisel.Font.load(font_path)
```

### Inkyにピクセルをプリントする

- [luisgabrielroldan/chisel]を用いてフォントをXYの座標に変換してもらいます
- [pappersverk/inky]に各座標を色を教えることにより、それがInkyにプリントされます

```elixir
person_name = "World!"

# 背景をプリントする関数
print_badge_fun = fn ->
  Inky.set_pixels(
    inky_pid,
    # 各座標を色を指定する
    fn _x, y, _w, h, _pixels ->
      name_field_top = div(h, 2)
      name_field_bottom = trunc(h * 0.9)

      cond do
        y < name_field_top -> :black
        y < name_field_bottom -> :white
        true -> :black
      end
    end,
    push: :skip
  )
end

# 文字をプリントする関数
print_text_fun = fn text, {x, y}, color, opts ->
  put_pixel_fun = fn x, y -> Inky.set_pixels(inky_pid, %{{x, y} => color}, push: :skip) end
  Chisel.Renderer.draw_text(text, x, y, chisel_font, put_pixel_fun, opts)
end

# ピクセルをInkyサーバーにセットする
print_badge_fun.()
print_text_fun.("Hello", {24, 1}, :white, size_x: 4, size_y: 3)
print_text_fun.("My name is", {24, 36}, :white, size_x: 2, size_y: 2)
print_text_fun.(person_name, {24, 72}, :black, size_x: 3, size_y: 3)

# ピクセルをまとめてプッシュする
Inky.set_pixels(inky_pid, %{}, push: :await)
```

[nerves_livebook]の使い方の一例

![Screen Recording 2021-11-20 at 9 46 07 PM](https://user-images.githubusercontent.com/7563926/142747666-8c59663f-613b-47a8-a6e0-e48cc1287c05.gif)

![nerves-hello-inky-20211120_221626](https://user-images.githubusercontent.com/7563926/142748201-8b472dc3-1f48-4686-bb84-8b6ad841c753.jpg)

## 問題点

- 画像の表示がElixirではむつかしかった
  - [メーカーのPythonライブラリー](https://github.com/pimoroni/inky)で[PIL](https://pillow.readthedocs.io/en/stable/)を用いて比較的簡単にできるようだが
  - PNG-8の画像をピクセルに変換するのに使えるElixirライブラリーが見つからなかった
  - 自力でPNGを[Elixir]で解読しようと思ったが最後のフィルターの部分が面倒そうだったので諦めた
- [Inky pHAT]のバージョン
  - 見た目が同じでもバージョンにより内部が全く異なる
  - Elixirライブラリー[pappersverk/inky]が現行機種に対応していない
- 何故かPythonライブラリーとElixirライブラリーとで印刷される向きが異なる
  - 僕のフォーク[mnishiguchi/inky]でPythonライブラリーに合わせて向きを調整した
  - プルリクエストは出してはいるがマージされるかは未定

## さいごに

文字で「[Hello World!](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program)」ができましたね
おかげでいろいろ電子ペーパーの特性が観察できました

- 電源OFFでも最後にプリントされた状態を保持
- ピクセルごとに（RGBではなく）デバイスがサポートする具体的な色を指定する
- プリントの実行が遅い（頻繁の画面更新には向いてない）

Pythonでできることがいくつか（僕の能力では）Elixirで完コピすることができませんでしたが、人それぞれ、言語それぞれ得意不得意があると割り切って前に進むことにしました

今日は[「Elixir Advent Calendar 2021」](https://qiita.com/advent-calendar/2021/elixir)にも参加したので、もしよろしければそちらも御覧ください

明日は@takasehidekiさんです。

<!-- Links -->

[nerves_livebook]: https://github-com.translate.goog/livebook-dev/nerves_livebook?_x_tr_sl=en&_x_tr_tl=ja&_x_tr_hl=en-US&_x_tr_pto=nui
[Nerves]: https://hexdocs.pm/nerves/getting-started.html
[Elixir]: https://elixir-lang.org/learning.html
[Hello World!]: https://en.wikipedia.org/wiki/%22Hello,_World!%22_program
[Underjord]: https://underjord.io/
[microSDカード]: https://ja.wikipedia.org/wiki/SD%E3%83%A1%E3%83%A2%E3%83%AA%E3%83%BC%E3%82%AB%E3%83%BC%E3%83%89#microSD%E3%82%AB%E3%83%BC%E3%83%89
[Inky pHAT]: https://www.switch-science.com/catalog/5983/
[Raspberry Pi Zero WH]: https://www.switch-science.com/catalog/3646/
[Elixir Circuits]: https://elixir-circuits.github.io/
[pappersverk/inky]: https://github.com/pappersverk/inky
[luisgabrielroldan/chisel]: https://github.com/luisgabrielroldan/chisel
[mnishiguchi/inky]: https://github.com/mnishiguchi/inky/tree/mnishiguchi/ssd1608
[olikraus/u8g2]: https://github.com/olikraus/u8g2/tree/master/tools/font/bdf