M5Stack CoreInk+M5Unifiedでデジタル名札をつくる

M5Stack Japan Tour 2024 Spring Osakaに展示者として参加することにしたので、現在使用していないM5Stack CoreInkで名札を作ってもっていくことにしました。

元ネタは数年前にお見かけしたこちらのツイートです。

https://x.com/ksasao/status/1330399256537075713

ありがたいことに、ソースコードも公開してくださっています。

今回はこちらをベースにさせて頂きつつ、PlatformIO + M5Unifiedライブラリを使用して作成しています。PlatformIO + M5Unifiedライブラリの開発環境は各自で構築済みの前提になります。

 

CoreInkのマルチファンクションボタンの上下で、4つの情報を切り替えて表示できるようにしています。

  1. 名前+自画像
  2. ブログ QRコード
  3. X(旧Twitter) QRコード
  4. YouTube QRコード

また、マルチファンクションボタンを押し込むと、3秒間、バッテリーのおよその残量が表示されます。この部分は、先のソースコードをほぼそのまま使用させて頂いております。最後のバー表示の描画だけM5Unified風に書き換えています。

 

1.〜4.の画像は予め 200×200 pxで作成して、プロジェクト直下にdataフォルダを作成し、ここに格納しておきます。

格納後は”Upload Filesystem Image”の実行を忘れずに。

なんかプログラムを書き込むときに自動で書き込まれる、みたいな記述も見た気がするのですが、自分の場合は明示的にこれをやらないとうまく表示できませんでした。

 

以下、ソースコードの全文です。

#include <Arduino.h>
#include <SPIFFS.h> // M5Unified.hより先に宣言が必要
#include <M5Unified.h>
#include <esp_adc_cal.h>

// 画像ファイルはdataフォルダに置いた後、"Upload Filesystem Image"の実行が必要
const char *path_self_name = "/self_name.jpg";
const char *path_blog = "/go_to_blog.jpg";
const char *path_x = "/go_to_x.jpg";
const char *path_youtube = "/go_to_youtube.jpg"; 

#define STATE_SELF    0
#define STATE_BLOG    1
#define STATE_X       2
#define STATE_YOUTUBE 3
uint8_t state = STATE_SELF;

float getBatVoltage(){
    analogSetPinAttenuation(35,ADC_11db);
    esp_adc_cal_characteristics_t *adc_chars = (esp_adc_cal_characteristics_t *)calloc(1, sizeof(esp_adc_cal_characteristics_t));
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 3600, adc_chars);
    uint16_t ADCValue = analogRead(35);

    uint32_t BatVolmV  = esp_adc_cal_raw_to_voltage(ADCValue,adc_chars);
    float BatVol = float(BatVolmV) * 25.1 / 5.1 / 1000;
    return BatVol;
}

int getBatCapacity(){
    // Simple implementation
    // see https://www.maximintegrated.com/jp/design/technical-documents/app-notes/3/3958.html
    // 4.02 = 100%, 3.65 = 0%
    const float maxVoltage = 4.02;
    const float minVoltage = 3.65;
    int cap = (int)(100.0 * (getBatVoltage() - minVoltage) / (maxVoltage - minVoltage));
    if(cap > 100){
      cap = 100;
    }
    if(cap < 0){
      cap = 0;
    }
    return cap;
}

void showBattery(){
    char text[64];
    M5.Display.clear();

    int battery = getBatCapacity();

    int cx = 100;
    int cy = 100;
    int w = 190;
    int h = 20;
    int b = w * (100-battery) / 100;

    M5.Display.startWrite();
    M5.Display.setTextSize(2);
    M5.Display.setCursor(5,70);
    M5.Display.printf("Battery: %d %%",battery);
    M5.Display.drawRect(cx - w/2 - 1,  cy - h /2 - 1,  w + 2, h + 2  , 1);
    M5.Display.fillRect(cx + w/2 - b ,cy - h / 2, b, h, 1);
    M5.Display.endWrite();

    delay(3000);
}

void setup() {
  Serial.begin(115200);

  auto cfg = M5.config();
  M5.begin(cfg);

  // 画像データの表示用
  SPIFFS.begin();

  // 省電力化のため、CPU周波数を落としておく。240, 160, 80, 40, 20, 10
  setCpuFrequencyMhz(40);

  M5.Display.drawJpgFile(SPIFFS, path_self_name, 0, 19);
}

void loop() {
  M5.update();

  unsigned long now_ms = millis();

  if(M5.BtnC.wasPressed()){ // BtnDOWNと同義
    switch(state){
    case STATE_SELF:
      state = STATE_BLOG;
      M5.Display.drawJpgFile(SPIFFS, path_blog, 0, 0);
      break;
    case STATE_BLOG:
      state = STATE_X;
      M5.Display.drawJpgFile(SPIFFS, path_x, 0, 0);
      break;
    case STATE_X:
      state = STATE_YOUTUBE;
      M5.Display.drawJpgFile(SPIFFS, path_youtube, 0, 0);
      break;
    case STATE_YOUTUBE:
      state = STATE_SELF;
      M5.Display.clear();
      M5.Display.drawJpgFile(SPIFFS, path_self_name, 0, 19);
      break;
    default:
      ;
    }
  }

  if(M5.BtnB.wasPressed()){ // BtnMIDと同義
    showBattery();
    switch(state){
    case STATE_SELF:
      M5.Display.clear();
      M5.Display.drawJpgFile(SPIFFS, path_self_name, 0, 19);
      break;
    case STATE_BLOG:
      M5.Display.drawJpgFile(SPIFFS, path_blog, 0, 0);
      break;
    case STATE_X:
      M5.Display.drawJpgFile(SPIFFS, path_x, 0, 0);
      break;
    case STATE_YOUTUBE:
      M5.Display.drawJpgFile(SPIFFS, path_youtube, 0, 0);
      break;
    default:
      ;
    }
  }

  if(M5.BtnA.wasPressed()){ // BtnUPと同義
    switch(state){
    case STATE_SELF:
      state = STATE_YOUTUBE;
      M5.Display.drawJpgFile(SPIFFS, path_youtube, 0, 0);
      break;
    case STATE_BLOG:
      state = STATE_SELF;
      M5.Display.clear();
      M5.Display.drawJpgFile(SPIFFS, path_self_name, 0, 19);
      break;
    case STATE_X:
      state = STATE_BLOG;
      M5.Display.drawJpgFile(SPIFFS, path_blog, 0, 0);
      break;
    case STATE_YOUTUBE:
      state = STATE_X;
      M5.Display.drawJpgFile(SPIFFS, path_x, 0, 0);
      break;
    default:
      ;
    }
  }

  delay(10);
}

短さよりわかりやすさ重視で書いています。

M5Unifiedのボタン定義はBtnA, BtnB, BtnC, BtnPWR, BtnEXTの5つですが、CoreInkの場合、BtnAがマルチファンクションボタンのUP、BtnBがMID、BtnCがDOWNに対応しています。助かる。

省電力化のための施策としては動作周波数を40MHzまで落とすことぐらい(←これより下げると描画速度が明らかに落ちる)しかしていませんが、これでも電源ON状態で18時間ぐらいもったので、展示会とかで一日首からぶら下げていても余裕だと思います。

あと、今回、名前+自画像のファイルだけ諸事情で 200×162 pxにしてしまったので、このファイルを表示するときだけy座標が0じゃなくて19になっています。

 

出来上がりとしてはこんな感じです。

 

QRコードもバッチリ認識できます。

ちなみに、電源をOFFにすると電子ペーパーの表示が薄くなって残るのですが、この状態でもQRコードは読み取れました。

 

CoreInkは角に穴が空いているのでここに首からぶら下げる用の紐を通すこともできるのですが、何か向きがイマイチな感じになりそうだったので、せっかくなので首からぶら下げる用のホルダーを3Dプリンタで作成しました。

ネジでしっかり固定できます。

以上、M5Stack CoreInkを使ったお手軽デジタル名札のご紹介でした。簡単に作れる割に実用性が高いのでオススメです。