カラー電子ペーパーを使った個人ダッシューボード作成 2024


:::note info
行き当たりばったりで作ったこともあり、全体的に構成を見直して再作成しました。最新版はこちらを御覧ください
:::

https://qiita.com/aoirohn/private/db8d52af4546fabbe032


## 概要
カラー電子ペーパーを使ったダッシュボードを作成しました。
同様の記事は昔から多くありますが、色数が2~3色だったり、描画をすべてPillowで行っている都合でUIの作成が大変だったりします。
今回は勉強も兼ねて以下の技術等を利用し、2024年にふさわしい綺麗で管理しやすいダッシュボードを作成します。

私の作ったソースコードは以下から確認できます。

https://github.com/aoirohn/epdash

## 利用したツール等
### ハードウェア
- Waveshare 7.3inch e-Paper HAT (F)
  - 7色描画可能な電子ペーパー
- Raspberry pi
  - なんでもいいですが、Raspberry pi Zero 2 WH もしくはRaspberry pi 4/5 を想定しています
  - 画像の取得、電子ペーパーの描画に利用します
  - 本記事ではRaspberry pi Zero 2 WHを利用します
- 適当なサーバー（Raspberry pi Zero 2を利用する場合）
  - ダッシュボードをWebページとしてローカルで公開するのに使います
  - Raspberry pi Zero 2を利用する場合、Webサーバーを立ち上げつつ画像を取得するのはメモリの制限上厳しいため別途用意します
  - Raspberry pi 4/5を利用する場合はWebサーバーとスクリーンショットの取得を同一マシン上で行っても動くので不要です（確認済み）

### ライブラリ・ソフトウェア
#### javscript / typescript
- bun
    - ダッシュボードプロジェクトの開発・パッケージ管理に使います
- remix
    - ダッシュボードのUIを作るフレームワークです
- pm2 
    - サーバーの永続化に使います

#### python
- uv
  - pythonのパッケージ管理に使います
- playwright
  - サーバーでホストしたダッシュボードのスクリーンショットを取得するのに使います
- pillow
  - 取得した画像の減色/ディザリング処理に利用します

#### その他
- mise
  - python, nodeのバージョン管理に利用しています


## サーバーのセットアップ
今回、ダッシュボードの画面はRemixを利用したWebページとして作成します。
そのためのWebページをホストするために raspberry piとは別にサーバーを利用します。

:::note info
Raspberry pi 4/5を利用する場合は別途サーバーを用意せずRaspberry pi上でホストすることも可能です。
Raspberry pi zero 2ではメモリが厳しく安定した動作が難しいため別途サーバーを用意します。
:::


### システムライブラリ・パッケージのインストール
ダッシュボードのサーバー用に以下のパッケージをインストールします
- mise
- bun
- node
- pm2
- playwright

```sh
# miseのインストール
# https://mise.jdx.dev/getting-started.htmlを参照してください
curl https://mise.run | sh

# bunのインストール
mise use -g bun@latest
# node
mise use -g node@latest

# pm2のインストール
bun add -g pm2

# playwrightと依存パッケージのインストール
cd <project dir>
bun add playwright
bunx playwright install-deps
```

### ダッシュボード画面の作成
今回はremixを利用してダッシュボード画面を作成します。
電子ペーパーのサイズに合わせて800 x 480 pxでダッシュボード画面を作成します

![ブラウザSS](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256100/97709db9-c98c-2e20-bd9f-e7303f21d42b.png)

今回は以下のサービスのAPIを利用しています
- [OpenWeatherMap](https://openweathermap.org/)
    - 天気予報の取得
- [GoogleCalenderAPI](https://developers.google.com/calendar?hl=ja)
    - カレンダーイベントの取得
    - こちら[^1]の記事を参考にAPIキーを使った認証を利用しています
- [今日は何の日API](https://note.com/sooz/n/naffb68c7f53b)
  - 右上の記念日の表示に利用

[^1]:[GoogleカレンダーのAPIを使ってWebサイトに表示させる方法](https://www.re-d.jp/news/article/9567/)


### pm2でサーバーの起動&永続化
画面が作成できたらダッシュボードのサーバーを永続化します。

必要に応じてpm2のconfigを作成します。今回はサーバーを3000番ポートで公開します。
```js:pm2.config.cjs
module.exports = {
  apps: [
    {
      name: "dashboard",
      script: "remix-serve",
      args: "build/server/index.js",
      autorestart: true,
      restart: "on-failure",
      error_file: "/dev/null",
      out_file: "/dev/null",

      env: { PORT: 3000, NODE_ENV: "production" },
      node_args: "--env-file .env",
    },
  ],
};

```

設定ファイルを作ったらpm2でサーバーを起動します

```sh:shell
bun run build # ビルド

pm2 start pm2.config.cjs
pm2 startup

# 次のようなスタートアップの設定用スクリプトが表示されるのでそれをコピー&ペーストして実行
# [PM2] To setup the Startup Script, copy/paste the following command:
# sudo env PATH=$PATH:/home/<username>/.nvm/versions/node/v22.12.0/bin /home/<username>/.bun/install/global/node_modules/pm2/bin/pm2 startup systemd -u <username> --hp /home/<username>

pm2 save # 設定保存
```


設定が終わったら一度再起動し、サーバーが永続化されていることを確認します。
```sh:shell
pm2 ls
# ┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
# │ id │ name               │ mode     │ ↺    │ status    │ cpu      │ memory   │
# ├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
# │ 0  │ dashboard          │ fork     │ 0    │ online    │ 0%       │ 165.2mb  │
# └────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
```

最後に、PCからもアクセスし接続できることを確認します。
今回作成したサンプルプロジェクトの場合は`http://<server ip address>:3000/dashboard`で確認できます。



## Raspberry pi のセットアップ

Raspberry pi Imagerを利用してマウス、キーボード、モニタなしでセットアップします。
sshでアクセスできるようにしておきます。

セットアップ手順はこちら[^2]の記事を参考にしました。
初回のセットアップが完了したら、電子ペーパーを使うための設定、ライブラリをインストールします

[^2]:[Raspberry Pi Zero(W, WH, 2 W)のセットアップ](https://qiita.com/hishi/items/8bdfd9d72fa8fe2e7573)


### vimのインストール（任意）
vim-tinyに慣れていないのでvimをインストールします。vim以外を使う場合は適宜読み替えてください
```sh:shell
sudo apt-get --purge remove vim-common vim-tiny
sudo apt-get install vim
```

### spi通信の有効化

`/boot/firmware/config.txt`を編集しspi通信を有効化します

```bash
sudo vim /boot/firmware/config.txt
```

```diff:/boot/firmware/config.txt
- #dtparam=spi=on
+ dtparam=spi=on
```

保存したらraspberrypiを再起動します


### システムライブラリ・パッケージのインストール
ダッシュボードへの描画用に以下のパッケージをインストールします
- swig
- liblgpio-dev
- libjpeg-dev
- mise
- uv
- (python)
    - uvで作った仮想環境を使うなら不要

```sh:shell
# swig, liblgpio-dev, libjpeg-devのインストール
sudo apt-get install swig liblgpio-dev libjpeg-dev

# miseのインストール（まだの場合）
# https://mise.jdx.dev/getting-started.htmlを参照してください
curl https://mise.run | sh

# uvのインストール
mise use -g uv@latest

# pythonのインストール
mise use -g python@latest
```


playwrightのセットアップを済ませておきます
```sh:shell
cd <path to project dir>
uv add playwright
uv sync
uv run python -m playwright install
```

### ダッシュボード画面のスクリーンショットを取得する

[playwright](https://playwright.dev/)を利用してダッシュボード画面のスクリーンショットを取得します。

:::note info
ポイント
- スクリーンショットを取るに当たりRaspberry pi Zero 2ではメモリに大きな制約があるため、以下の通り`CHROMIUM_FLAGS`を設定して軽量化します
- `device_scale_factor=2`を設定し、縦横2倍の解像度でスクリーンショットを取得します。大きめの画像を利用することで後に縮小&ディザリング処理をしたとき、より綺麗な画像が得られます
:::

```py
from pathlib import Path
from os import PathLike
from playwright.sync_api import sync_playwright

CHROMIUM_FLAGS = [
    "--disable-gpu",
    "--disable-dev-shm-usage",
    "--disable-setuid-sandbox",
    "--no-first-run",
    "--no-sandbox",
    "--no-zygote",
    "--single-process",
    "--disable-audio-output",
    "--disable-background-timer-throttling"
    "--disable-backgrounding-occluded-windows"
    "--disable-breakpad",
    "--disable-extensions",
    "--disable-sync",
    "--disable-translate",
]


def get_url_image(url: str, dir: PathLike, name: str):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True, args=CHROMIUM_FLAGS)
        context = browser.new_context(
            viewport={"width": 800, "height": 480},
            device_scale_factor=2,
        )

        page = context.new_page()
        page.goto(url)
        page.screenshot(path=str(Path(dir) / name), full_page=True) 
        browser.close()

get_url_image("http://<server ip address>:3000/dashboard","out/dashboard.png")

```

スクリーンショットが`output/dashboard.png`に出力されます

![dashboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256100/f7ccb664-0e79-fca3-a216-77b948cce438.png)


### 画像の縮小&ディザリング処理

Pillowを利用して画像を縮小&ディザリング処理を行います

:::note info
ポイント
- 大きめの画像を縮小することで文字や細い線を綺麗に描画できます
- 今回利用する電子ペーパーの使用可能色に合わせてディザリング処理を行います

:::

今回利用するWaveshare 7.3inch e-Paper HAT (F)で利用可能な以下の7色でパレットを作成し、Pillowの`quantize`関数を使って減色を行います
  - [255, 0, 0]  # Red
  - [0, 255, 0]  # Green
  - [0, 0, 255]  # Blue
  - [255, 255, 0]  # Yellow
  - [255, 128, 0]  # Orange
  - [0, 0, 0]  # Black
  - [255, 255, 255]  # White

```py
from PIL import Image

import numpy as np


def reduce_color_for_epd_7in3f(image: Image.Image):
    image = image.convert("RGB")
    image = image.resize((800, 480))

    EPAPER_PALETTE = np.array(
        [
            [255, 0, 0],  # Red
            [0, 255, 0],  # Green
            [0, 0, 255],  # Blue
            [255, 255, 0],  # Yellow
            [255, 128, 0],  # Orange
            [0, 0, 0],  # Black
            [255, 255, 255],  # White
        ]
    )

    palimage = Image.new("P", (16, 16))
    palimage.putpalette(list(EPAPER_PALETTE.flatten()))

    return reduce_color(image, palimage)


def reduce_color(image: Image.Image, palette: Image.Image):
    return image.quantize(palette=palette, dither=Image.Dither.FLOYDSTEINBERG)

image = Image.open("out/dashboard.png")
dithered_image = reduce_color_for_epd_7in3f(image)
dithered_image.save("output/dithered_image.png")
```

サンプルプロジェクトの場合は`packages/imagen`を使って画像を取得しディザリング処理した結果を保存できます。
このプロジェクトはgpio等を使っていないため、Raspberry piで実行する前にディスプレイ上で結果を確認できます。PC上でのデザイン作成時に便利です。

```sh:shell
cd epdash/packages/imagen
uv sync
echo "SS_URL=\"http://<server ip address>:<port number>/dashboard\"" > .env
uv run python -m imagen  # /outout に画像が出力される
```

縮小・ディザリング処理を行うと次のような画像が得られます。

![dithered_image_masked.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256100/a019231e-06a1-c73e-f526-8509dd7dce9b.png)

画像の表示サイズによってはモアレがかかって見えますが、元画像ファイルを見ればうまくディザリングできていることが確認できるかと思います。

### 電子ペーパーへの書き込み

電子ペーパーへの書き込みには[waveshare-epaper](https://pypi.org/project/waveshare-epaper/)パッケージが利用できます。

```py
from PIL import Image
import epaper
from os import PathLike
import time


def draw(model: str, img_path: PathLike):
    try:
        epd = epaper.epaper(model).EPD()
        epd.init()
        draw_img = Image.open(img_path)
        epd.display(epd.getbuffer(draw_img))
        epd.sleep()

    except IOError as e:
        print(e)

    except KeyboardInterrupt:
        epaper.epaper(model).epdconfig.module_exit(cleanup=True)
        exit()


def clear(model: str):
    try:
        epd = epaper.epaper(model).EPD()
        epd.init()
        epd.Clear()
        epd.sleep()

    except IOError as e:
        print(e)

    except KeyboardInterrupt:
        epaper.epaper(model).epdconfig.module_exit(cleanup=True)
        exit()



draw("epd7in3f", "output/dithered_image.png")
time.sleep(5)
clear("epd7in3f")
```


サンプルプロジェクトの場合は`packages/drawer`を使って画像を取得し電子ペーパーへの書き込みまで実行できます。こちらはgpioピンを使ったspi通信等を行うためRaspberry piでのみライブラリのインストール&実行が可能です。

実行できない場合は前述した以下のライブラリがインストールされていることを確認してください
- swig
- lgpio-dev


```sh:shell
cd epdash/packages/drawer
uv sync
echo "SS_URL=\"http://<server ip address>:<port number>/dashboard\"" > .env

uv run draw-calendar.py # 電子ペーパーへの書き込み
uv run clear.py # 電子ペーパーの画像クリア
```

書き込んだ画像がこちら。反射で少し見にくいですが思ったよりきれいに表示できています
![PXL_20241209_151822030.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256100/748bb47e-b22f-045e-3064-b01560d317be.png)


### cronを設定

動作確認ができたらcronで毎日0時に電子ペーパーを更新します

```sh:shell
crontab -e
```

```sh:cron設定
PATH=<'echo $PATH' したものと同じPATHを設定>

0 0 * * * cd <path to project dir> && uv run draw-calendar.py
```

cronの実行ログは以下のコマンドで確認できます

```sh
sudo journalctl -f -u cron
```

### OverlayFSの有効化（ファイルシステムのReadOnly化）

:::note warn
この手順はRaspberry pi 4/5を利用する場合のみ行います。Raspberry pi Zeroではシステムリソースが厳しく、OverlayFSを有効化するとplaywrightがうまく動作しません
:::

長期間運用することを想定し、電源断時のOS保護のためOverlayFSを有効にし、ファイルシステムをReadOnlyにします

```sh:shell
sudo raspi-config
```
以下の通り選択します
- 4 Performance Options Configure performance settings
- P3 Overlay File System Enable/disable read-only file system
- Would you like the overlay file system to be enabled? 
    - Yes
- Would you like the boot partition to be write-protected? 
    - Yes

設定が終わったら再起動します

##  まとめ
Raspberry Pi Zero 2とカラー電子ペーパーを使用して個人ダッシュボードを作成しました。
Remixを使った画面構築とPillowによるディザリング処理により、個人的に満足の行く画面を7色電子ペーパーでありながら出力できました。


### うまくいかなかったこと・今後の改善点
- 今回はRaspberry pi Zero W2 のメモリ制限が思った以上にきつく、ダッシュボードのサーバーの用意が必要でした。サーバーを用意する代わりにCloudflare PagesにてBasic認証つきで公開することも考えましたが、時間が取れなかったため一旦断念しました。時間があればやってみたいです
- Raspberry piの電源断対策としてファイルシステムをReadOnlyにしたかったのですが、Raspberry pi Zero 2では使用メモリが厳しく断念しました。長期運用したいダッシュボードとしては是非設定したいところなので改善したいところです。コンパクトさを捨ててRaspberry Pi 4や5を使ってもいいですが、せっかく低消費電力である電子ペーパーを使うのであればRaspberry pi Zeroにこだわりたいところなので悩ましいです
- 電子ペーパー自体が少し赤みがかったグレーの色合いをしているため、ディザリングした画像を実際に描画すると少し赤色の強い画像になってしまいます。減色時のパレットの白色を適切に変更することでもう少し色を近づけられるかもしれません
- ダッシュボードの画面としてWebページを作成しているため、別のURLパスとして画面を作ればOAuth認証をさせたり、設定画面を作ったりすることも可能です。例として、今回は天気の都市は環境変数として設定しましたが、設定画面で変更できるようにしても良さそうです

### 最後に

今回は `mise` ,`bun`, `remix`, `pm2`, `uv`, `playwright`, `pillow`, `電子ペーパー`, `Raspberry pi`等のいろいろなツールを（浅くではありますが）組み合わせることができとても楽しかったです。
みなさんも自分だけの電子ペーパーダッシュボードを作ってみてはどうでしょうか。