M5CoreInk 改良版『会議中』のステータスを自動表示する仕組み

以下の記事のM5CoreInkの改良版です。

https://qiita.com/ishidad2/items/8089f41f03d8b11464f8

# 開発環境

今回から開発環境をPlatformIOに変更しました。
PlatformIOはVSCodeの拡張機能を使っています。

 事前に設定などが必要になるので、以下の記事などを参考に環境は構築してださい。

 https://zenn.dev/kotaproj/articles/esp32_vscode_pio

 https://qiita.com/nextfp/items/f54b216212f08280d4e0

 # 構成

[前回](https://qiita.com/ishidad2/items/8089f41f03d8b11464f8#m5coreink)はArduino IDEを使用してプロジェクトを作成しました。
今回は前回作成したプログラムをVSCodeのPlatformIO拡張機能からプロジェクトをインポートして開発していきます。

構成は以下になります。

```text
 .
├── include
│   └── README
├── lib
│   ├── README
│   └── lib_ink_print
│       ├── lib_ink_print.cpp
│       └── lib_ink_print.h
├── platformio.ini
├── src
│   └── InkWatch.ino
└─ test
```

Arduinoのプロジェクトをインポートすると`src`フォルダの中に`InkWatch.ino`が作成されます。
`lib`フォルダには自作のライブラリを入れます（コードは後述）

# 実装

## PlatformIOの設定

`platformio.ini`の設定です。
こちらは各環境に合わせて修正してください。

```ini
; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:m5stack-coreink]
platform = espressif32
board = m5stack-coreink
framework = arduino
lib_extra_dirs = ~/Documents/Arduino/libraries
monitor_speed = 115200
```

## InkWatch.ino

メインプログラムです。

https://bokunimo.net/blog/esp/3743/#%E9%9D%A2%E5%80%92%E3%81%AA%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%82%B9%E3%83%AA%E3%83%BC%E3%83%97

こちらの記事を参考に全面的に書き直しました。

```c
#include "M5CoreInk.h"      // M5CoreInk用ライブラリ
#include "esp_adc_cal.h"    // ADCキャリブレーション用ライブラリ
#include <WiFi.h>           // WiFi通信用ライブラリ
#include <HTTPClient.h>     // HTTP通信用ライブラリ
#include <ArduinoJson.h>    // JSONデータのパース用ライブラリ
#include "esp_sleep.h"      // ESP32用Deep Sleep ライブラリ
#include "driver/rtc_io.h"  // ESP32側のRTC IO用ライブラリ
#include <lib_ink_print.h>

#define SSID "xxxxxxx"
#define PASS "xxxxxxxxxx"
#define API_ENDPOINT "https://script.google.com/macros/s/xxxxxxxxxx/exec"
#define NORMAL_SLEEP_P 180          // 通常のスリープ時間（秒）
#define TIMEZONE_OFFSET 9*3600      // 日本時間のオフセット（秒）
#define NIGHT_START_HOUR 21         // 夜間モード開始時刻
#define NIGHT_END_HOUR 7            // 夜間モード終了時刻


RTC_DATA_ATTR uint8_t PageBuf[200*200/8];
RTC_DATA_ATTR uint32_t ink_refresh_time;        // e-paper全消去後の経過時間
Ink_Sprite InkPageSprite(&M5.M5Ink);            // e-paper描画用インスタンス
int wake = (int)esp_sleep_get_wakeup_cause();   // 起動理由を変数wakeに保存

int batt_mv(){                                  // 電池電圧確認
    int PIN_AIN = 35;                           // 電池電圧取得用のADCポート
    float adc;                                  // ADC値の代入用
    analogSetAttenuation(ADC_2_5db);            // ADC 0.1V～1.25V入力用
    pinMode(PIN_AIN, ANALOG);                   // GPIO35をアナログ入力に
    adc = analogReadMilliVolts(PIN_AIN);        // AD変換器から値を取得
    adc /= 5.1 / (20 + 5.1);                    // 抵抗分圧の逆数
    return (int)(adc + 0.5);                    // 電圧値(mV)を整数で応答
}

// 夜間モードかどうかをチェックする関数
bool isNightMode(struct tm timeInfo) {
    if (timeInfo.tm_hour >= NIGHT_START_HOUR || timeInfo.tm_hour < NIGHT_END_HOUR) {
        return true;
    }
    return false;
}

// スリープ時間を計算する関数（戻り値はマイクロ秒）
uint32_t calculateSleepDuration(struct tm timeInfo) {
    if (isNightMode(timeInfo)) {
        if (timeInfo.tm_hour >= NIGHT_START_HOUR) {
            // 21時以降の場合、朝7時までの残り時間を計算
            int hoursUntil7am = (24 - timeInfo.tm_hour + NIGHT_END_HOUR);
            return (uint32_t)hoursUntil7am * 3600 * 1000000ul;
        } else {
            // 0-7時の場合、7時までの残り時間を計算
            int hoursUntil7am = (NIGHT_END_HOUR - timeInfo.tm_hour);
            return (uint32_t)hoursUntil7am * 3600 * 1000000ul;
        }
    }
    return NORMAL_SLEEP_P * 1000000ul;
}

void saveSpriteBuffer() {
    uint8_t* spritePtr = InkPageSprite.getSpritePtr();
    if (spritePtr != nullptr) {
        size_t size = 200*200/8;
        // コピー前にバッファをクリア
        memset(PageBuf, 0, size);
        // メモリコピー
        memcpy(PageBuf, spritePtr, size);
        // 検証（オプション）
        if (memcmp(PageBuf, spritePtr, size) != 0) {
            Serial.println("Buffer copy verification failed");
        }
    }
}

void ink_test_cls(){
    int delta = 25;
    for(int y=0;y<200;y+=delta){
        InkPageSprite.FillRect(0,y,200,delta,1);                    // White
        InkPageSprite.pushSprite();                                 // e-paperに描画
        InkPageSprite.FillRect(0,y,200,delta,0);                    // Black
        InkPageSprite.pushSprite();                                 // e-paperに描画
        InkPageSprite.FillRect(0,y,200,delta,1);                    // White
        InkPageSprite.pushSprite();                                 // e-paperに描画
    }
}

String fetchDataFromAPI() {
  HTTPClient http;
  String payload = "{}";

  Serial.println("Connecting to API...");
  Serial.println(API_ENDPOINT);

  http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
  http.begin(API_ENDPOINT);
  int httpResponseCode = http.GET();

  Serial.print("HTTP Response code: ");
  Serial.println(httpResponseCode);

  if (httpResponseCode > 0) {
    payload = http.getString();
    Serial.println("API Response:");
    Serial.println(payload);
  } else {
    Serial.println("Error on HTTP request");
  }

  http.end();
  return payload;
}

void sleep() {
    M5.update();                                                    // M5Stack用IO状態の更新
    InkPageSprite.pushSprite();                                     // e-paperに描画
    WiFi.disconnect();                                              // Wi-Fiの切断

    // バッファコピー
    saveSpriteBuffer();

    // 現在時刻の取得
    struct tm timeInfo;
    getLocalTime(&timeInfo);

    // スリープ時間の計算
    uint32_t sleepDuration = calculateSleepDuration(timeInfo);
    ink_refresh_time += millis() + sleepDuration/1000;              // debug

    ink_printPos(0, 176);                                           // 文字表示位置を移動
    int batt = batt_mv();                                           // 電池電圧を取得してbattに代入

    if(batt > 3300 && !M5.BtnPWR.wasPressed()){ // 電圧が3300mV以上のとき
        digitalWrite(LED_EXT_PIN, HIGH);
        /* スリープ中に GPIO12 をHighレベルに維持する(ESP32への電源供給) */
        rtc_gpio_init(GPIO_NUM_12);
        rtc_gpio_set_direction(GPIO_NUM_12,RTC_GPIO_MODE_OUTPUT_ONLY);
        rtc_gpio_set_level(GPIO_NUM_12,1);
        esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
        unsigned long us = millis() * 1000ul + 363000ul;
        if(sleepDuration > us) us = sleepDuration - us;
        else us = 1000000ul;
        ink_println("Sleeping for "+String((double)(us/100000)/10.,1)+" secs");
        if (isNightMode(timeInfo)) {
            ink_println("Night mode - See you at 7:00");
        }
        esp_deep_sleep(us);                                         // ESP32をDeep Sleepモードへ移行
    }
    ink_println("Power OFF ("+String(batt)+" mV)"); 
    /* スリープ中に GPIO12 をLowレベルに維持する(ESP32への電源供給を阻止) */
    rtc_gpio_init(GPIO_NUM_12);
    rtc_gpio_set_direction(GPIO_NUM_12,RTC_GPIO_MODE_OUTPUT_ONLY);
    rtc_gpio_set_level(GPIO_NUM_12,0);
    esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
    digitalWrite(LED_EXT_PIN, LOW);                                 // LED消灯
    M5.shutdown();                                                  // 電源OFF
}

/**
 * set up the M5Stack
 */
void setup() {

    M5.begin();                                                     // M5Stack用ライブラリの起動
    digitalWrite(LED_EXT_PIN, LOW);                                 // LED点灯

    // まずInkの初期化
    while(!M5.M5Ink.isInit()) delay(3000);                          // Inkの初期化状態確認

    switch(wake){
      case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
      case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
      case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
      case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動 PAD=%d\n", esp_sleep_get_touchpad_wakeup_status()); break;
      case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
      case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
      case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
      default                         : Serial.printf("スリープ以外からの起動\n"); break;
    }

    // 起動モードに応じた処理
    if(wake != ESP_SLEEP_WAKEUP_TIMER){                             // タイマー以外で起動時の処理
        Serial.println("wake = Power ON");                          // debug
        M5.M5Ink.clear();                                           // Inkを消去
        ink_refresh_time = 0;                                       // 消去した時刻を0に
        InkPageSprite.creatSprite(0,0,200,200,0);                   // 描画用バッファの作成
        ink_test_cls();                                             // 暫定対策(画面消去)
        ink_print_init(&InkPageSprite);                             // テキスト表示用 ink_print
        ink_printPos(160);                                          // 文字表示位置を移動
        ink_print("mode:("+String(wake)+")",false);                 // タイトルの描画
    }else if(ink_refresh_time >= 60*60*1000){                       // 1時間に1回の処理
        M5.M5Ink.clear();                                           // Inkを消去
        ink_refresh_time = 0;                                       // 消去した時刻を0に
        InkPageSprite.clear();
        InkPageSprite.creatSprite(0,0,200,200,0);                   // 描画用バッファの作成
        ink_test_cls();                                             // 暫定対策(画面消去)
        ink_print_init(&InkPageSprite);                             // テキスト表示用 ink_print
        ink_printPos(160);                                          // 文字表示位置を移動
        ink_print("mode:("+String(wake)+")",false);                 // タイトルの描画
    }else{                                                          // タイマー起動時の処理
        Serial.println("wake = ESP_SLEEP_WAKEUP_TIMER");            // debug
        InkPageSprite.creatSprite(0,0,200,200,0);                   // 描画用バッファの作成
        InkPageSprite.drawFullBuff(PageBuf);                        // RTCメモリから画像読み込み
        ink_print_setup(&InkPageSprite);                            // テキスト表示用 ink_print
    }
    InkPageSprite.pushSprite();

    ink_printPos(144,160);                                          // 文字表示位置を移動
    ink_print(String(batt_mv())+" mV",false);                       // 電圧値をバッファに描画

    // WiFiの設定
    Serial.printf("Connecting to %s\n", SSID);
    WiFi.begin(SSID, PASS);
    while(WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.printf("\nWiFi connected\n");

    InkPageSprite.pushSprite();
}

void loop(){

    while(WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.printf("\nWiFi connected\n");

    configTime(9 * 3600, 0, "ntp.nict.jp");
    struct tm timeInfo;
    getLocalTime(&timeInfo);
    char now[20];
    sprintf(now, "%04d/%02d/%02d %02d:%02d:%02d",
        timeInfo.tm_year + 1900,
        timeInfo.tm_mon + 1,
        timeInfo.tm_mday,
        timeInfo.tm_hour,
        timeInfo.tm_min,
        timeInfo.tm_sec
    );

    String apiData = fetchDataFromAPI();
    JsonDocument doc;
    DeserializationError error = deserializeJson(doc, apiData);

    if (!error && doc["status"] == "success") {
        const char* temp = doc["data"];

        if (strcmp(temp, "on") == 0) {
            Serial.println("会議中");
            ink_printPos(0, 16);
            ink_print("+++++++++++++++++++++++++",false);
            ink_printPos(0, 32);
            ink_print("The meeting is currently online.",false);
            ink_printPos(0, 64);
            ink_print("Please refrain from entering the room for a while.",false);
            ink_printPos(0, 96);
            ink_print("Thank you for your cooperation.", false);
            ink_printPos(0, 128);
            ink_print("+++++++++++++++++++++++++", false);
        } else {
            Serial.println("カメラが起動していない");
            ink_clear(16, 144);                                     // クリア
            ink_print_init(&InkPageSprite);                         // バッファを初期化
        }
        InkPageSprite.pushSprite();
    }

    ink_printPos(0, 0);                                             // 文字表示位置を移動
    ink_print(String(now), false);

    InkPageSprite.pushSprite();

    Serial.println("Starting sleep process...");

    // Sleep処理
    sleep();
}
```

# lib_ink_print

描画関係の処理は参考にした記事からほぼそのまま拝借しています。

- lib_ink_print.h

```c
#pragma once
#include <M5CoreInk.h>

extern RTC_DATA_ATTR char TextBuf[13][25];
extern Ink_Sprite *InkTextSprite;
extern int ink_x;
extern int ink_y;

void ink_push();
void ink_print_setup(Ink_Sprite *sprite, int y);
void ink_print_setup(Ink_Sprite *sprite);
void ink_print_init(Ink_Sprite *sprite, int y);
void ink_print_init(Ink_Sprite *sprite);
void ink_println();
void ink_printPos(int y);
void ink_printPos(int x, int y);
void ink_print(String text, bool push);
void ink_print(String text);
void ink_print(const char *text);
void ink_print(const char *text, bool push);
void ink_println(uint32_t ip);
void ink_println(String text);
void ink_println(const char *text);
void ink_clear(int start_y, int height);
```

- lib_ink_print.cpp

```c
#include "lib_ink_print.h"

RTC_DATA_ATTR char TextBuf[13][25];
Ink_Sprite *InkTextSprite;
int ink_x = 0;
int ink_y = 0;

void ink_push() {
    InkTextSprite->pushSprite();
}

void ink_print_setup(Ink_Sprite *sprite, int y) {
    Serial.println("Entered ink_print_setup");
    InkTextSprite = sprite;
    ink_x = 0;
    ink_y = y;
    for(int y=0; y<13; y++) for(int x=0; x<25; x++){
        if(TextBuf[y][x]) InkTextSprite->drawChar(x*8,y*16,TextBuf[y][x]);
    }
    Serial.println("Done");
}

void ink_print_setup(Ink_Sprite *sprite) {
    ink_print_setup(sprite, 0);
}

void ink_print_init(Ink_Sprite *sprite, int y) {
    memset(TextBuf,0,13*25);
    ink_print_setup(sprite, y);
}

void ink_print_init(Ink_Sprite *sprite) {
    memset(TextBuf,0,13*25);
    ink_print_setup(sprite, 0);
}

void ink_println() {
    for(; ink_x < 200; ink_x += 8){
        InkTextSprite->drawChar(ink_x,ink_y,' ');
        TextBuf[(ink_y/16)%13][(ink_x/8)%25]=' ';
    }
    ink_x = 0;
    if(ink_y <= 192){
        ink_y += 16;
    }
    ink_push();
}

void ink_printPos(int y) {
    if(y%16) ink_y = y + 16 - (y%16);
    else ink_y = y % 200;
}

void ink_printPos(int x, int y) {
    if(x%8) ink_x = x + 8 - (x%8);
    else ink_x = x % 200;
    ink_printPos(y);
}

void ink_print(String text, bool push) {
    char c[2];
    Serial.println("TEXT(" + String(text.length()) + ")" + text);
    for(int i=0; i < text.length(); i++){
        text.substring(i).toCharArray(c, 2);
        if(c[0] < 0x20 || c[0] >= 0x7f) continue;
        InkTextSprite->drawChar(ink_x,ink_y,c[0]);
        TextBuf[(ink_y/16)%13][(ink_x/8)%25]=c[0];
        ink_x += 8;
        if(ink_x >= 200){
            if(push) ink_println();
            else{
                ink_x = 0;
                if(ink_y <= 192)ink_y += 16;
            }
        }
    }
    if(push) ink_push();
}

void ink_print(String text) {
    ink_print(text, true);
}

void ink_print(const char *text) {
    ink_print(String(text));
}

void ink_print(const char *text, bool push) {
    ink_print(String(text), push);
}

void ink_println(uint32_t ip) {
    char s[16];
    sprintf(s,"%d.%d.%d.%d",ip&255,(ip>>8)&255,(ip>>16)&255,(ip>>24)&255);
    ink_print(String(s), false);
    ink_println();
}

void ink_println(String text) {
    ink_print(text, false);
    ink_println();
}

void ink_println(const char *text) {
    ink_println(String(text));
}

void ink_clear(int start_y, int height) {
    for(int y = start_y; y < start_y + height; y += 48) {
        ink_y = y;
        ink_x = 0;
        for(ink_x = 0; ink_x <= 200; ink_x += 24) {
            InkTextSprite->drawChar(ink_x, ink_y, ' ', &AsciiFont24x48);
            TextBuf[(ink_y/48)%13][(ink_x/24)%25]=' ';
        }
    }
    ink_push();
    ink_x = 0;
    ink_y = start_y;
}
```

# 実装内容

- 描画について
[前回](https://qiita.com/ishidad2/items/8089f41f03d8b11464f8#m5coreink)の実装では「起動するたびにe-Paper を消去する」実装になっていました。
しかしこの実装では一度、全画素を黒で表示してから白に戻すのでe-Paperの書き換え回数が増えてしまいe-Paperの寿命に影響を及ぼしてしまう心配があるそうです。
なので、今回の実装では必要な画素のみを書き換える「部分描画方式」を用いました。（[参考記事](https://bokunimo.net/blog/esp/3743/#%E9%9D%A2%E5%80%92%E3%81%AA%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%82%B9%E3%83%AA%E3%83%BC%E3%83%97)）
日本語表示はメモリの制限もあり、今回は入れていません。（メモリが足りずに書き込みできない）
文字の大きさについては、なぜか文字を大きく表示して、消去しようとすると消去時に部分的に文字が残ってしまう現象に見舞われ修正ができなかったので通常の文字サイズにしました。

- スリープ制御
スリープ制御も参考記事をもとに全面的に変更しました。

# 動作イメージ

カメラ起動には以下のようなメッセージが表示されるようにしました。

![1000004552.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/be1d89b7-c67f-5c07-08cc-486229d8bc7b.jpeg)

なにもしていない場合は以下のようになります。

![1000004554.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/7d5f85b8-c285-54f7-9f75-00224a8e7840.jpeg)


# 課題

現在の実装で安定して稼働はしているのですが、ペーパーの書き換え回数を重ねるとシミのようなものが発生してくることがわかりました。（これ自体は1時間に一回全面書き換え時に消えるので大きな問題ではないですが）
今の所原因がわかっておらず対応策としては1時間に一回全面書き換えをすることで解消はされています。
もし、原因がわかる方おられましたらコメント頂けると助かります。

![1000004546.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/5d8dd729-8003-3c3a-c916-d93809f599d4.jpeg)

![1000004550.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/2cf76c73-bf91-41ea-e3fc-1c795c5c78f6.jpeg)