1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

M5Paper V1.1 で始める動的名札ソリューション

Last updated at Posted at 2025-02-19

電子ペーパーで表示画像を切り替えるアプリを作りました。電子ペーパーで SD カード内の画像を切り替えて表示します。勉強会やカンファレンスなどで、用途やシーンに応じて名札を切り替える目的や動機で作りました。

電子ペーパーを求めて

この節は導入パートです。これまで私が電子ペーパーに対して右往左往していた話を書きたいだけなので、スキップしても問題ありません。

電子書籍端末 Kindle Paperwhite などでお馴染みの電子ペーパー、低電力かつ電源無しでも表示し続けることができます。その特性を利用して、勉強会やカンファレンスで、アイコンや名札を表示したいと思い、いくつもの電子ペーパーを試してきました。

昨年入手した電子ペーパーはサイズ感もよく満足しました、書き込みがよく分からない専用アプリだというのを除いて。この電子ペーパーは NFC で画像を書き込みます。書き込みアプリは製造メーカー製アプリですが、使い辛いです。また iOS 版はストアアプリですが、Android 版は野良アプリと安全性から常用するのは厳しい。NFC の書き込みプロトコルが分かれば自作アプリを作って、書込画像の製作や書き込みの制御を一括で管理したい。

ちなみの他にも電子ペーパーをいくつか試しています。気になる方は上記ポストのリプライを見てください。

M5Paper V1.1 との出会い

昨年末(2024 年末)、M5PaperS3 が販売されました。マイコン ESP32 と電子ペーパーが一体になった開発キットです。

その販売を知ったとき、興味はあったもののマイコン制御が未経験で分からないため、購入は見送りしました。後日、国内販売店さんのブログで名刺利用の記事を読みました。記載内容から未経験の私でも開発できそう、M5PaperS3 を購入しようと思ったところ、すでに完売していました。

年末の判断は誤ったと悔やんでいたところ、その前モデルである M5Paper V1.1 があったので購入しました。前振りはここまで、次節から M5Paper V1.1 を使って開発します。

開発準備

開発環境は MacBook Pro 14インチ 2021 / Apple M1 Pro / メモリ 32 GB / macOS Sonoma 14.6.1 です。マイコン開発はまったくの未経験ですが、Arduino IDE を利用するといろいろ便利だと知ったので、これを利用しました。コードの開発、ビルド、そして本体への書き込みもできます。

M5Paper の販売元である M5Stack でも Arduino IDE を利用したチュートリアルを公開しています。

この Arduino IDE はライブラリマネージャーも搭載しています。今回利用するライブラリ M5Unified をインストールしました。M5Unified は初めて触るライブラリですが、公式ドキュメントが整備されているので、開発の流れは理解できます。また、他の方もブログなどで情報発信されており、それらも参考にしました。

Arduino IDE で作成するファイルは ino ファイルです。詳しい仕組みは分かっていませんが、ino ファイルはビルドの途中で C++ になるので、C++ の知識で書くことができます。私はかつて C++ に挫折した経験もあるので、主に C 言語の知識で開発しました。

プログラムの概要

作成したコードは次のとおりです。SD カード内のルートディレクトリから JPEG や PNG の画像ファイルを走査し、そのパスを配列に格納します。そして、M5Paper 本体のボタン(ここでは BottonB、GPIO番号 G38)を押すと、登録された画像を順番に表示します。

card_case_for_m5paper_v1_1.ino
#include <SD.h>
#include <M5Unified.h>

#define MAX_IMAGES 20          // 画像ファイルの最大数

String imageList[MAX_IMAGES];  // 画像パスを格納する配列
int imageCount = 0;            // 見つかった画像の数
int currentIndex = 0;          // 現在表示中の画像インデックス

void setup() {
  Serial.begin(115200);
  auto cfg = M5.config();
  M5.begin(cfg);
  M5.Lcd.setTextSize(6);
  M5.Lcd.println("CardCase For M5Paper V1.1\n");
  M5.Lcd.setTextSize(4);

  // SD カードのマウント
  // 10 回失敗したら、アプリを終了させる
  int beginCount = 0;
  while (false == SD.begin(GPIO_NUM_4, SPI, 25000000)) {
    Serial.println("wait to mount SD card");
    delay(500);
    beginCount++;
    if (beginCount > 10) {
      M5.Lcd.println("It failed to mount SD card.");
      Serial.println("It failed to mount SD card.");
      while (1)
        ;
    }
  }
  M5.Lcd.println("It opens SD card.");

  // SD のルートディレクトリを走査して画像ファイルをリストアップ
  File root = SD.open("/");
  if (!root) {
    M5.Lcd.println("It failed to open ROOT of SD card");
    Serial.println("It failed to open ROOT of SD card");
    while (1)
      ;
  }

  File file = root.openNextFile();
  while (file && imageCount < MAX_IMAGES) {
    if (!file.isDirectory()) {
      String filename = file.name();
      if (!filename.startsWith(".") && (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png"))) {
        imageList[imageCount] = String("/") + filename;
        imageCount++;

        String listedImage = "(" + String(imageCount) + ") " + String(filename);
        M5.Lcd.println(listedImage);
        Serial.println(listedImage);
      }
    }
    file.close();
    file = root.openNextFile();
  }
  root.close();

  if (imageCount > 0) {
    M5.Lcd.println("Push G38 (BottonB).");
  } else {
    M5.Lcd.println("It does not found images.");
    while (1)
      ;
  }
}

void loop() {
  M5.update();

  // ボタン (M5.BtnB) が押されたら画像を表示する
  if (M5.BtnB.wasPressed()) {
    displayImage(imageList[currentIndex]);
    currentIndex = (currentIndex + 1) % imageCount;
  }
}

// SDカード内の画像ファイルを描画する関数
void displayImage(String filename) {
  M5.Lcd.fillScreen(0xFFFFFF);

  if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) {
    M5.Lcd.drawJpgFile(SD, filename, 0, 0);
  } else if (filename.endsWith(".png")) {
    M5.Lcd.drawPngFile(SD, filename, 0, 0);
  }

  delay(200);
}

SDカードから画像パスを取得する

公式ドキュメントを参考にして、SD カードをマウントしました。while でマウント処理を繰り返すため、マウント失敗の状態取得が難しいです。回数を設定してエラー判定を行いましたが、この処理が適切かは分かっていません。

失敗したら while(1) で無限ループで処理を止めました。これもよく分かっていないですが、exit(1) ではなく無限ループで止めるようです。

SD カードのマウント
int beginCount = 0;
while (false == SD.begin(GPIO_NUM_4, SPI, 25000000)) {
  delay(500);
  beginCount++;
  if (beginCount > 10) {
    while (1)
      ;
  }
}

次に、SD カードのルートディレクトリを開いて、画像ファイルのパスを取得します。拡張子が .jpg, .jpeg, .png のファイルを対象にしています。各画像ファイルのパスは、配列 imageList に格納され、画像数は imageCount に記録されます。

私は macOS で開発する都合、不要ファイルをゴミ箱で入れたまま削除するのを忘れて、隠しファイル(名前がピリオドで始まるもの)になったファイルがあったので、隠しファイルも除外しました。通常では不要な処理です。

画像ファイルの走査
#define MAX_IMAGES 20          // 画像ファイルの最大数
String imageList[MAX_IMAGES];  // 画像パスを格納する配列
int imageCount = 0;            // 見つかった画像の数
int currentIndex = 0;          // 現在表示中の画像インデックス

// ルートディレクトリを開く
File root = SD.open("/");
if (!root) {
  while (1)
    ;
}

// ファイルを走査して、画像ファイルのパスを取得する
File file = root.openNextFile();
while (file && imageCount < MAX_IMAGES) {
  if (!file.isDirectory()) {
    String filename = file.name();
    if (!filename.startsWith(".") && (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".png"))) {
      imageList[imageCount] = String("/") + filename;
      imageCount++;
    }
  }
  file.close();
  file = root.openNextFile();
}
root.close();

ここで、対象画像は M5Paper の表示サイズである 540 x 960 ピクセルの白黒画像を想定しています。コードで画像のバリデーション機能は仕込んでいないので、事前に適切な画像を作成して、SD カードに保存しておきます。

ボタンのタップイベントで画像を表示する

loop() 関数内の M5.update() でボタン操作の状況を取得します。M5.BtnB.wasPressed() を利用して、ボタンB(G38)が押されたかを検出します。ボタンが押されると、現在の画像パスを displayImage() 関数に渡し、画像を描画します。その後、currentIndex を更新し、次回のボタンタップで次の画像が表示されるように準備しています。

ボタンイベントの取得
void loop() {
  M5.update();
  
  if (M5.BtnB.wasPressed()) {
    displayImage(imageList[currentIndex]);
    currentIndex = (currentIndex + 1) % imageCount;
  }
}

前述で取得した画像パスを引数として、画像を表示します。ここで、画像の種類によって、表示関数が異なるので、jpeg か png を判定して、それぞれの表示関数を呼び出します。

画像を表示する
void displayImage(String filename) {
  // 以前の表示が残らないように背景を白にする
  M5.Lcd.fillScreen(0xFFFFFF);

  if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) {
    M5.Lcd.drawJpgFile(SD, filename, 0, 0);
  } else if (filename.endsWith(".png")) {
    M5.Lcd.drawPngFile(SD, filename, 0, 0);
  }

  // 画像は即座に表示されないので、遅延処理を差し込む
  delay(200);
}

プログラムのまとめ

今回のプログラムでは、M5Paper V1.1 を利用した画像表示アプリとして、次の2点を中心に実装しました。

  1. SDカードから画像パスを取得する
    • SDカードの初期化とルートディレクトリ内の走査する
    • JPEG / PNG の画像ファイルのパスを取得して、配列に格納する
  2. ボタンタップイベントで画像を表示する
    • ボタンB(G38)の押下を検出し、画像描画の関数を読み出す
    • 画像描画は drawJpgFile() / drawPngFile() を利用して画像パスから表示する

今回作成したコードを GitHub に公開しました。ご興味ある方はご確認・ご利用ください。

謝辞

このアプリのアイディアは、すでに多くの方が取り組まれています。実装する際は、その方たちの記事やコードを参考にしました。改めて、ありがとうございました。

まとめ

ついに念願の「名札を動的に切り替えて表示できる電子ペーパー」が作れました。欲しい!と思ってから、形になるまで長かった。今度参加するオフラインイベントでは、この M5Paper を首から下げて名札のように使おうと思います。ただし、この M5Paper は立方体でストラップを付ける箇所がありません。汎用のフレキシブルなストラップが別途必要になります(シリコン素材で四隅を引っ掛けるやつ)。

おまけ

LILYGO 製の電子ペーパーも購入してました。こちらはモジュールとしての電子ペーパーと電子基板のセットです。製品というより電子工作感がより強いです。日常で使うと、剥き出しの基板やケーブルに負荷かかかるので、ケースが欲しいところです。私が調べた範囲だと、ケースは販売されておらず、ケースを 3D プリンターで印刷されている方を見かけました。私は 3D プリンターを持っていないので、今もパッケージに入ったままです。どうにか有効活用しないと…

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?