📷

AWS × e-Paperで作るデジタルアートフレーム

に公開

はじめに

「飾りたい写真がたくさんあるけどプリントしてたらきりがない」、「液晶ディスプレイにスライドショーで流したいけど、ディスプレイの耐久性や電気代が気になる」と思っていたところ、電子ペーパーを利用したデジタルアートフレームに行き着きました。
SwitchBot や Samsung が商品化しており、これから一般家庭にも浸透していきそうです。
自分はGoogleフォト連携など追加でやりたいことがあったので自作することにしました。(特にこだわりがない人は普通にSwitchBotのアートフレームを買った方が幸せになれると思います)

SWITCHBOTがカラー電子ペーパーを採用したアートフレームを発売
電子ペーパーが急拡大、更新可能な広告やポスター まるで印刷物

つくったもの

ユーザがアップロードした画像を e-Paper 用に自動変換し、宅内の Raspberry Pi が定期的に取得して e-Paper に表示します。

  1. e-Paperに写したい画像をWebアプリ経由でクラウドストレージ(AWS S3)にアップロード
  2. アップロード検知でAWS Lambdaが動作し、e-Paper用の画像(800×480のBMP)に変換
  3. 宅内の Raspberry Pi が API Gateway (mTLS) にリクエストし、次に表示すべき画像を取得
  4. Raspberry Pi が e-Paperに画像を描写
  5. Raspberry Pi でsystemd timerを設定し、30 分おきに 上記の3~4 を繰り返しスライドショーを実現

本記事でカバーしているもの

本記事ではアップロード画像の整形, デジタルアートフレームによるアップロード画像の取得/電子ペーパーへの描写の実装を紹介します。
Webアプリ経由での画像のアップロードやGoogleフォト連携については別の記事 GoogleフォトとS3を繋ぐ Photos Picker Appで紹介します。
本記事では一先ず画像を手動で直接S3へアップロードすることを想定しています。


システム全体像

AWS 側

S3 (uploads/, processed/)

  • uploads/: ユーザからアップロードされたオリジナル画像を保管する
  • processed/: uploads/内の画像に対して、Lambda がe-Paper用に変換したBMP画像を保管する

Lambda (FormatImage)

  • uploads/ に追加された画像に対して、e-Paper に合わせたリサイズ・色調補正・BMP 変換を行う
  • 変換後の画像は processed/ に保存する

Lambda (GetNextImage)

  • Raspberry Pi からのリクエストを受け、e-Paperに写す画像をprocessed/ 内から選択する
  • そして選択した画像のダウンロードURL(S3の署名付きURL)を生成して返す
  • 表示履歴はstate/.display_state.jsonで管理し、同じ画像ばかり表示しないロジックを実装している

API Gateway

  • Lambda GetNextImageを HTTPS で呼び出すためのエンドポイントを提供する
  • mTLSによるクライアント認証を必須とし、第三者からのリクエストを拒否する

Raspberry Pi 側

fetch_next_image.py

  • mTLS 接続で API Gateway にアクセスし、S3署名付きURL経由で BMP画像をダウンロードする
  • 取得した画像をe-paper に描画する。30 分間隔で定期実行し、スライドショーを実現する

用意するもの

Waveshare 7.3inch ACeP 7-Color E-Paperについて

  • 7.3インチ、800x480ピクセル、7色表示可能なe-Paperです
  • 送料込みで$83(12,000円くらい)でした
  • 梱包が2重になっており、ちゃんとしていて感動しました


記事で扱うコード

https://github.com/MasayoshiIwamoto/picker2paper/tree/main

picker2paper/
├─ cdk_display_pipeline/       # AWS 側(本記事の対象)
│  ├─ app.py                   # CDK エントリーポイント
│  ├─ display_pipeline/app_stack.py
│  └─ lambda/
│     ├─ format_image/handler.py
│     └─ get_next_image/handler.py
├─ raspberryPi_code/           # Raspberry Pi 側(本記事の対象)
│  ├─ fetch_next_image.py      # mTLS で画像取得・描画・キャッシュするスクリプト
│  └─ clear_display.py         # 夜間などに画面を初期化するユーティリティ
└─ cdk_photo_picker/           # Web アップロード側(本記事では扱わず別記事で解説予定)

補足: Photo Picker Web アプリ (picker2paper/cdk_photo_picker/) は別の記事 GoogleフォトとS3を繋ぐ Photos Picker Appで詳しく触れます。本記事では AWS 側の変換パイプラインと Raspberry Pi 用スクリプトに絞って解説します。


Raspberry Pi, e-Paper のセットアップ

Raspberry Pi のOSインストール

OSはRaspberry Pi OS (64bit) を選択します。SSHやWifiもここで設定しておくと便利です。
参考: Raspberry Pi Imager のインストールと使い方 - Qiita

Raspberry Pi における e-Paper 利用のための各種設定

Waveshareが公開している公式マニュアル 7.3inch e-Paper HAT (F) Manual - Waveshare Wiki 内の Working With Raspberry Pi > Python のケースに従って作業します。
SPI interfaceの有効化、依存パッケージのインストール、デモコードのダウンロードを行います。
マニュアル最後のデモコードの実行は次のe-Paper取り付け後に行います。

e-Paperの取り付け

このおじさんの解説動画 Review of Waveshare e-paper displays for raspberry pi / Making an e-paper etch-a-sketch clone が参考になります。
動画の4:40からe-Paperとe-Paper HATの接続と,e-Paper HATとRaspberry Piの接続のデモをしています。

e-Paperの動作確認

Raspberry Piで下記コマンドを実行し、e-Paperへの描写が動作するかを確認します。
実行後にいくつかのイラストが連続で描写され、最後に画面が初期化されれば成功です。

# Make sure it's in e-Paper/RaspberryPi_JetsonNano/
cd python/examples/
python3 epd_7in3f_test.py

Raspberry Pi用クライアント証明書の発行

Raspberry Pi と API Gateway 間でmTLS認証を行うため、PrivateCAを利用してクライアント証明書を発行します。
ルートCA証明書 myCA.pem は以降の作業でAWS S3に設置し、mTLSの信頼ストアとして利用します。
AWSが myCA.pem をルートCA証明書として登録し、Raspberry Piがリクエスト時にクライアント証明書等を提示することで、mTLS認証が利用できます。

クライアント証明書発行用のコマンド
mkdir -p ~/.ssh/myCA
cd ~/.ssh/myCA

# ルートCA秘密鍵(2048bit RSA)
openssl genrsa -out myCA.key 2048

# ルートCA証明書(有効期限10年)
openssl req -x509 -new -nodes -key myCA.key -sha256 -days 3650 -out myCA.pem \
  -subj "/CN=MyCA"
  
# デバイス秘密鍵
openssl genrsa -out epaper-device.key 2048

# CSR(証明書署名要求)
openssl req -new -key epaper-device.key -out epaper-device.csr -subj "/CN=epaper-device"

# CAで署名してクライアント証明書発行
openssl x509 -req -in epaper-device.csr -CA myCA.pem -CAkey myCA.key -CAcreateserial \
  -out epaper-device.crt -days 365 -sha256

AWS 環境の構築

事前準備

(任意) Amazon Route 53のDNSゾーン作成

以降の証明書発効時のドメイン認証や、API Gatewayのドメイン公開でDNSゾーンが必要です。
Route 53利用の場合は$0.5/月かかります。その他のDNSサービスでも代替可能です。

AWS ACMによるサーバ証明書の作成

API Gatewayの公開ドメイン (例: display.example.com) 用の証明書が必要です。
ACM (ap-northeast-1) で証明書を発行しておきます。本記事ではワイルドカード証明書 (*.example.com) を想定します。

S3バケットを作成し、上記でRaspberry Pi用に作成したルートCA証明書をアップロード

S3バケット、ルートCA証明書の名前、Pathをそれぞれ指定

SUFFIX="example"
BUCKET="trust-store-${SUFFIX}"
OBJECT_KEY="myCA.pem"
KEY_PATH="/path/to/myCA.pem"

S3バケットを作成

aws s3 mb "s3://${BUCKET}/"

API GatewayがルートCA証明書を参照できるようにポリシーを設定

aws s3api put-bucket-policy \
  --bucket "${BUCKET}" \
  --policy "{
    \"Version\": \"2012-10-17\",
    \"Statement\": [
      {
        \"Sid\": \"AllowApiGatewayToReadTruststore\",
        \"Effect\": \"Allow\",
        \"Principal\": {\"Service\": \"apigateway.amazonaws.com\"},
        \"Action\": [\"s3:GetObject\", \"s3:GetObjectVersion\"],
        \"Resource\": \"arn:aws:s3:::${BUCKET}/${OBJECT_KEY}\"
      }
    ]
  }" \

ルートCA証明書をアップロード

aws s3 cp "${KEY_PATH}" "s3://${BUCKET}/${OBJECT_KEY}"

cdkファイルの準備

picker2paper/cdk_display_pipeline/
├─ app.py
├─ cdk.json
├─ requirements.txt
├─ display_pipeline/app_stack.py
└─ lambda/
   ├─ format_image/
   └─ get_next_image/

デプロイ

cd picker2paper/cdk_display_pipeline
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cdk bootstrap #cdk初回実行時のみ

# デプロイ
cdk deploy DisplayPipelineStack \
  --context uploadsBucketName=photo-picker-uploads-ap-northeast-1-example \
  --context uploadsPrefix=uploads/ \
  --context processedPrefix=processed/ \
  --context displayStateKey=state/.display_state.json \
  --context presignedTtlSeconds=120 \
  --context nextImageDomainName=display.example.com \
  --context nextImageCertificateArn=arn:aws:acm:ap-northeast-1:123456789012:certificate/xxxxxxxx \
  --context nextImageTruststoreUri=s3://trust-store-example/myCA.pem \
  --context nextImageStageName=prod \
  --context hostedZoneName=example.com \
  --context manageDns=true

デプロイ後に確認する Outputs

  • ManualUploadBucketName : 手動アップロードに使う S3 バケット (uploadsPrefix がプレフィックス)
  • ProcessedBucketName : 変換済み画像を保存する S3 バケット (processedPrefix がプレフィックス)
  • NextImageMtlsEndpoint : mTLS を有効にしたカスタムドメインのエンドポイント
  • NextImageManualDnsRecord : DNSレコードを手動登録する際の案内

AWSの課金について

  • S3 / Lambda / API Gateway の合計で$0.1/月程度(東京リージョンで画像100枚想定)
  • Route 53 のホストゾーンを利用する場合は追加で$0.5/月

S3 への画像アップロード

e-Paperに写したい画像を S3 photo-picker-uploads-.../uploads/  にアップロードします。
本実装ではオリジナル画像を横800×縦480に切り抜くため、横向きの画像がおすすめです。
またスライドショーにするために、複数枚の画像をアップロードしてください。
AWS CLI 例

SUFFIX="example"
UPLOAD_BUCKET="photo-picker-uploads-ap-northeast-1-${SUFFIX}"
UPLOAD_PREFIX="uploads/"
aws s3 cp myphoto.jpg "s3://${UPLOAD_BUCKET}/${UPLOAD_PREFIX}"

コマンド実行後、少し待ってからアップロードならびに画像変換が完了していることを確認します。

SUFFIX="example"
UPLOAD_BUCKET="photo-picker-uploads-ap-northeast-1-${SUFFIX}"
UPLOAD_PREFIX="uploads/"
PROCESSED_PREFIX="processed/"
FILE_NAME="myphoto"
aws s3 ls "s3://${UPLOAD_BUCKET}/${UPLOAD_PREFIX}${FILE_NAME}.jpg"
aws s3 ls "s3://${UPLOAD_BUCKET}/${PROCESSED_PREFIX}${FILE_NAME}.bmp"

Raspberry Pi 側の準備

AWSからの画像取得と描写

まずテストとして、curlコマンドでAPI Gatewayのエンドポイント (例: https://display.example.com/next-image)AWSへリクエストを送信してみます。
クライアント認証の成功と、画像DL用のURLの受取が動作することを確認します。
レスポンス内のbmp_urlがS3署名付きURLであり、これにアクセスすることで画像を取得できます。

$ curl \
  --cert ~/.ssh/myCA/epaper-device.crt \
  --key  ~/.ssh/myCA/epaper-device.key \
  --cacert ~/.ssh/myCA/myCA.pem \
  https://display.example.com/next-image

# レスポンス例
{"bmp_url": "https://photo-picker-uploads-ap-northeast-1-example.s3.amazonaws.com/processed/2025-xx-xxT00-50-20-991Z_2025xxxx_104434.bmp?AWSAccessKeyId=xxxxxxxxxx", "object_key": "processed/2025-xx-xxT00-50-20-991Z_2025xxxx_104434.bmp", "displayed_at": xxxxxxxx, "expires_in": 120}

次に、AWSから画像を取得してe-Paperに描写するまでを自動化したスクリプト /fetch_next_image.py  を実行します。
スクリプトはe-Paperの動作確認時にダウンロードしたデモコードに含まれるライブラリ e-Paper/RaspberryPi_JetsonNano/python/lib を参照します。そのため以下のようにデモコードのフォルダ内に配置します。

cd ~/e-Paper/RaspberryPi_JetsonNano/python/
mkdir prod
cd prod
wget https://raw.githubusercontent.com/miwamot/picker2paper/refs/heads/main/raspberryPi_code/fetch_next_image.py

ダウンロードしたスクリプトを実行します。

python e-Paper/RaspberryPi_JetsonNano/python/prod/fetch_next_image.py \
  --api-url https://display.example.com/next-image \
  --cert ~/.ssh/myCA/epaper-device.crt \
  --key  ~/.ssh/myCA/epaper-device.key \
  --save-dir ~/e-Paper/RaspberryPi_JetsonNano/python/prod/ \
  --display

これでアップロードした写真が e-Paperに描写されれば動作成功です!

systemd によるスクリプトの定期実行

今回の目的はe-Paperでスライドショーを実現することでした。
上記のスクリプトを定期実行させ、画像を切り替えていきます。

/etc/systemd/system/fetch_next_image.service
[Unit]
Description=Fetch next display image and render on e-paper
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/pi/picker2paper/raspberryPi_code/fetch_next_image.py --api-url https://display.example.com/next-image --cert /home/pi/.ssh/myCA/epaper-device.crt --key /home/pi/.ssh/myCA/epaper-device.key --save-dir /home/pi/display/pic --display
WorkingDirectory=/home/pi/picker2paper/raspberryPi_code
StandardOutput=journal
StandardError=journal
Restart=on-failure
User=pi
/etc/systemd/system/fetch_next_image.timer
[Unit]
Description=Run fetch_next_image.service every 30 min between 6:00 and 23:00

[Timer]
OnCalendar=*-*-* 06..22:00:00
OnCalendar=*-*-* 06..22:30:00
Persistent=true

[Install]
WantedBy=timers.target

systemdの有効化

sudo systemctl daemon-reload
sudo systemctl enable --now fetch_next_image.timer

(任意) 夜間の停止処理

夜間は見ないのでe-Paperの描写を初期化させます。
同じ写真を長時間描写している場合は e-Paperの焼き付き防止の効果もあります。

スクリプトの取得

cd ~/e-Paper/RaspberryPi_JetsonNano/python/prod
wget https://raw.githubusercontent.com/miwamot/picker2paper/refs/heads/main/raspberryPi_code/clear_display.py
/etc/systemd/system/clear_display.service
[Unit]
Description=My Python Script Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 clear_display.py
WorkingDirectory=/home/example/e-Paper/RaspberryPi_JetsonNano/python/prod
StandardOutput=journal
StandardError=journal
Restart=on-failure
User=example
/etc/systemd/system/clear_display.timer
[Unit]
Description=Run myscript.service daily at 23:00

[Timer]
OnCalendar=*-*-* 23:00:00
Persistent=true

[Install]
WantedBy=timers.target

systemdの有効化

sudo systemctl daemon-reload
sudo systemctl enable --now clear_display.timer

まとめ

CDK で CloudFront・S3・Lambda・API Gateway (mTLS) をまとめて構築し、画像の BMP 変換と署名付き URL 発行までをサーバレスで完結させました。Raspberry Pi 側では fetch_next_image.py が API 経由で次の画像を取得し、キャッシュを活用しながら e-paper へ描画します。アップロードは手動で S3 に置くだけでも動作するため、Photo Picker Web アプリが完成していなくてもスライドショーを運用できます。


Discussion