08_M5PaperS3でTxtReader


08_M5PaperS3でTxtReader

説明

M5PaperS3でテキストリーダーを作成しました。SD内のtxtファイルの中身を表示します。UFT-8のマルチバイト文字に対応していますが、表示フォントは、
lgfxJapanGothic_32 横幅32px か、
lgfxJapanGothic_36 横幅36px です。
これは、常用漢字+αで4639文字。半角カナ、→の矢印、①等はありません。日本語学習サイトなどの、レベル別漢字一覧リストだそうです。
https://lang-ship.com/blog/work/lovyangfx-3-ja-font/

文字リスト
https://lang-ship.com/blog/files/LovyanGFX/japan.html

インストールVerの制限

ボードマネージャ
・esp32 by Espressif Systems は v3.0.7以下の事

ライブラリ
・M5Unified by M5Stack は v0.2.2以上の事
・M5GFX by M5Stack は v0.2.2以上の事

ボードの設定

Arduino-IDE > ツール にて
・ボード : "ESP32S3 Dev Module"
・ポート : "COM□"
・USB CDC On Boot: “Enabled”
・Flash Size: “16MB(128Mb)” ← 32MBでない
・Partition Scheme: "16M Flash (3MB APP / 9.9MB FATFS)" ← Defaultでは容量不足
・PSRAM: “OPI PSRAM” ← M5GFXで必要

使い方

SDのファイルは、
・ルートにあるファイル
・8.3形式のファイル
・9ファイル以下
の制約があります。

起動するとメニュー画面が開きます。
・品質・速度 : 高品質 / 高速
 (タッチで変更)
・画面向き : 縦向き / 横向き
 (タッチで変更)
・フォントサイズ : 32px / 36px
 (タッチで変更)
・行間 : 5px / 10px / 15px
 (タッチで変更)
・ファイル名
 (タッチでメニュー画面を終了し、ファイルの中身を表示)
・戻る
 (タッチでメニュー画面を終了し、ファイル1を表示)
・次の頁 : 画面右側タッチ
・前の頁 : 画面左側タッチ

スケッチ


// 08_M5PaperS3_BookReader.ino
#include <SD.h>                      // SDを使用
File myFile;                         // Fileのインスタンスを作成
#include <epdiy.h>                   // 電子ペーパードライバー
#include <M5GFX.h>                   // M5用グラフィックライブラリ
M5GFX display;                       // M5GFXのインスタンスを作成
#define SD_SPI_CS_PIN GPIO_NUM_47    // SDのpin
#define SD_SPI_SCK_PIN GPIO_NUM_39   // SDのpin
#define SD_SPI_MOSI_PIN GPIO_NUM_38  // SDのpin
#define SD_SPI_MISO_PIN GPIO_NUM_40  // SDのpin
String buf;                          // 表示文章のbuf
int16_t fontS = 32;                  // fontサイズ (32or36)
int16_t gyoukan = 10;                // 行間px
bool edpQ = false;                   // (true)高品質or(false)高速
bool gamenTate = true;               // 画面(true)縦or(false)横
int32_t pageN, pageB[1000];          // 頁番号,頁最初のByte値
lgfx::touch_point_t tp[1];           // タッチポイント
int16_t menuDY = 70;                 // メニュー画面刻みpx
String fName, fList[9][2];           // ファイル名,ファイルリスト[n][fName,fByte]

void SetFont() {                                // Font等初期化関数
  display.init();                               // パネルの初期化
  display.startWrite();                         // バッファを効率的に使用開始
  display.setEpdMode(epd_mode_t::epd_fastest);  // 描画速 (低品質)
  //display.setEpdMode(epd_mode_t::epd_quality);// 高品質 (描画遅)
  display.setRotation(0);  // 縦長
  //display.setRotation(1);// 横長
  display.setFont(&fonts::lgfxJapanGothic_32);  // 固定幅ゴシック体 8,12,16,20,24,28,32,36,40
  //display.setFont(&fonts::lgfxJapanGothic_36);// 固定幅ゴシック体 8,12,16,20,24,28,32,36,40
  display.fillScreen(TFT_WHITE);               // 全画面白
  display.setTextColor(TFT_BLACK, TFT_WHITE);  // (字,地)
  display.setTextSize(1);                      // 文字サイズの倍数 1-7
  display.setTextWrap(false, false);           // 自動折返し(x,y)
  display.setCursor(0, 0);                     // カーソル位置
  display.setAutoDisplay(false);               // 即表示しない
}

String setBuf(int32_t byteP) {               // bytePの位置から読込みそのbufを返す
  myFile = SD.open("/" + fName, FILE_READ);  // 読込ファイルを開く
  myFile.seek(byteP);                        // 読取開始のByte位置
  int32_t i = 0;                             // 読取Byte数リセット
  buf = "";                                  // bufリセット
  while (myFile.available()) {               // 読取れるByteがあれば
    buf += char(myFile.read());              // 読取文字
    i++;                                     // 読取Byte数1加算
    if (i > ((540 / fontS) * (960 / fontS) * 3)) break;
    // 行間0,全3Byte文字として1頁以上読込んだら抜ける
  }
  myFile.close();  // ファイルを閉じる
  return buf;      // 1頁以上の文字を返す
}

int setView(String buf) {        // 1頁表示関数
  int16_t viewX = 0, viewY = 0;  // 表示x,y位置 リセット
  int16_t i = 0;                 // Byte位置 リセット
  int16_t bufL = buf.length();   // bufの全Byte数 \0は含まず
  display.clear();               // パネルをクリア
  while (i < bufL) {             // buf終わりまでループ
    bool CRon = false;           // 改行 リセット
    // -------------------------------- 1行セット終わり
    if (viewX > (display.width() / fontS - 1) * fontS) {  // 横が1文字分少なく を超えていたら改行
      CRon = true;                                        // 改行
      viewX = 0;                                          // 画面の左端
      viewY += (fontS + gyoukan);                         // 行間スペースを含めて移動へ
    }
    // -------------------------------- 改行
    if (buf.charAt(i) == 0x0D) {   // CR+LF だったら
      CRon = true;                 // 改行
      i += 2;                      // CR+LF Windows等
      viewX = 0;                   // 画面の左端
      viewY += (fontS + gyoukan);  // 行間スペースを含めて移動へ
    }
    // -------------------------------- LFは無視
    if (buf.charAt(i) == 0x0A) {   // LFだったら 無いはず
      i += 1;                      // 1Byte進める
      viewX = 0;                   // 画面の左端
      viewY += (fontS + gyoukan);  // 行間スペースを含めて移動へ
    }
    // -------------------------------- 1頁セット終わり
    if ((viewY + gyoukan + fontS > display.height())
        && (CRon == true)) break;  // 行が下まで来て改行なら抜ける
    // -------------------------------- prit
    display.setCursor(viewX, viewY);           // カーソル位置
    if (buf.charAt(i) < 0x80) {                // 1Byte文字なら
      display.print(buf.substring(i, i + 1));  // 1文字セット
      i += 1;                                  // 1Byte進める
      viewX += fontS / 2;                      // 半角
    } else if (buf.charAt(i) < 0xC0) {         // マルチByteの2Byte目以降
      // ここへは来ないはず
    } else if (buf.charAt(i) < 0xE0) {         // 2Byte文字なら
      display.print(buf.substring(i, i + 2));  // 1文字セット
      i += 2;                                  // 2Byte進める
      viewX += fontS;                          // 全角
    } else if (buf.charAt(i) < 0xF0) {         // 3Byte文字なら
      display.print(buf.substring(i, i + 3));  // 1文字セット
      i += 3;                                  // 3Byte進める
      viewX += fontS;                          // 全角
    } else if (buf.charAt(i) < 0xF8) {         // 4Byte文字なら
      display.print(buf.substring(i, i + 4));  // 1文字セット
      i += 4;                                  // 4Byte進める
      viewX += fontS;                          // 全角
    } else if (buf.charAt(i) < 0xFC) {         // 5Byte文字なら
      display.print(buf.substring(i, i + 5));  // 1文字セット
      i += 5;                                  // 5Byte進める
      viewX += fontS;                          // 全角
    } else if (buf.charAt(i) < 0xFE) {         // 6Byte文字なら
      display.print(buf.substring(i, i + 6));  // 1文字セット
      i += 6;                                  // 6Byte進める
      viewX += fontS;                          // 全角
    } else {
      // ここへは来ないはず
    }
  }
  display.display();  // セットしたのを表示
  return (i);         // 処理Byte数を返す
}

void printMenu() {
  display.setRotation(0);                       // 縦長
  display.setFont(&fonts::lgfxJapanGothic_32);  // 固定幅ゴシック体 8,12,16,20,24,28,32,36,40
  display.setEpdMode(epd_mode_t::epd_fastest);  // 描画速 (低品質)
  display.fillScreen(TFT_WHITE);                // 全画面白
  display.setTextColor(TFT_BLACK, TFT_WHITE);   // (字,地)
  while (true) {
    // -------------------------------- メニュー1行目セット
    int16_t menuX = 0, menuY = 20;                 // カーソル変数
    display.setCursor(menuX, menuY);               // カーソル位置
    if (edpQ == true) {                            // 高品質画面なら
      display.print(" 品質・速度 : ");              // 表示をセット
      display.setTextColor(TFT_WHITE, TFT_BLACK);  // (字,地)
      display.print("高品質");                     // 表示をセット
      display.setTextColor(TFT_BLACK, TFT_WHITE);  // (字,地)
      display.print("  高速");                   // 表示をセット
    } else {                                       // 高速画面なら
      display.print(" 品質・速度 : 高品質  ");    // 表示をセット
      display.setTextColor(TFT_WHITE, TFT_BLACK);  // (字,地)
      display.print("高速");                       // 表示をセット
      display.setTextColor(TFT_BLACK, TFT_WHITE);  // (字,地)
    }
    // -------------------------------- メニュー2行目セット
    menuY += menuDY;                               // 次の行へ
    display.setCursor(menuX, menuY);               // カーソル位置
    if (gamenTate == true) {                       // 縦画面なら
      display.print(" 画面向き  : ");              // 表示をセット
      display.setTextColor(TFT_WHITE, TFT_BLACK);  // (字,地)
      display.print("縦向き");                     // 表示をセット
      display.setTextColor(TFT_BLACK, TFT_WHITE);  // (字,地)
      display.print("  横向き");                 // 表示をセット
    } else {                                       // 横画面なら
      display.print(" 画面向き  : 縦向き  ");    // 表示をセット
      display.setTextColor(TFT_WHITE, TFT_BLACK);  // (字,地)
      display.print("横向き");                     // 表示をセット
      display.setTextColor(TFT_BLACK, TFT_WHITE);  // (字,地)
    }
    // -------------------------------- メニュー3行目セット
    menuY += menuDY;                               // 次の行へ
    display.setCursor(menuX, menuY);               // カーソル位置
    if (fontS == 32) {                             // fontサイズが32pxなら
      display.print(" Font Size : ");              // 表示をセット
      display.setTextColor(TFT_WHITE, TFT_BLACK);  // (字,地)
      display.print("32px");                     // 表示をセット
      display.setTextColor(TFT_BLACK, TFT_WHITE);  // (字,地)
      display.print("  36px");                 // 表示をセット
    } else {                                       // fontサイズが36pxなら
      display.print(" Font Size : 32px  ");    // 表示をセット
      display.setTextColor(TFT_WHITE, TFT_BLACK);  // (字,地)
      display.print("36px");                     // 表示をセット
      display.setTextColor(TFT_BLACK, TFT_WHITE);  // (字,地)
    }
    // -------------------------------- メニュー4行目セット
    menuY += menuDY;                                  // 次の行へ
    display.setCursor(menuX, menuY);                  // カーソル位置
    if (gyoukan == 5) {                               // 行間が5pxなら
      display.print(" 行間(px) : ");                  // 行間px
      display.setTextColor(TFT_WHITE, TFT_BLACK);     // (字,地)
      display.print("05");                          // 表示をセット
      display.setTextColor(TFT_BLACK, TFT_WHITE);     // (字,地)
      display.print("  10  15");              // 表示をセット
    } else if (gyoukan == 10) {                       // 行間が10pxなら
      display.print(" 行間(px) : 05  ");          // 表示をセット
      display.setTextColor(TFT_WHITE, TFT_BLACK);     // (字,地)
      display.print("10");                          // 表示をセット
      display.setTextColor(TFT_BLACK, TFT_WHITE);     // (字,地)
      display.print("  15");                      // 表示をセット
    } else {                                          // 行間が15pxなら
      display.print(" 行間(px) : 05  10  ");  // 表示をセット
      display.setTextColor(TFT_WHITE, TFT_BLACK);     // (字,地)
      display.print("15");                          // 表示をセット
      display.setTextColor(TFT_BLACK, TFT_WHITE);     // (字,地)
    }
    // -------------------------------- ファイル名セット
    menuY += menuDY;                    // 次の行へ
    for (int8_t i = 0; i < 9; i++) {    // 9ファイルまで表示
      display.setCursor(menuX, menuY);  // カーソル位置
      display.printf("%3d ", i + 1);    // ファイル番号セット
      fName = fList[i][0]               // ファイル名
              + "         ";            // 9文字空白追加
      fName = fName.substring(0, 12);   // 頭から12文字
      display.print(fName);             // ファイル名セット
      display.print(fList[i][1]);       // ファイルサイズセット
      menuY += menuDY;                  // 次の行へ
    }
    // -------------------------------- 罫線セット 960x540
    for (int8_t i = 1; i < 5; i++) {  // 水平線上4本
      display.drawFastHLine(0, menuDY * i, 540, TFT_BLACK);
    }
    for (int8_t i = 5; i < 13; i++) {  // 水平線下8本
      display.drawFastHLine(0, menuDY * i, 400, TFT_BLACK);
    }
    display.drawFastVLine(335, 0, 210, TFT_BLACK);    // 垂直線上3行
    display.drawFastVLine(285, 210, 70, TFT_BLACK);   // 4行目左
    display.drawFastVLine(420, 210, 70, TFT_BLACK);   // 4行目右
    display.drawFastVLine(400, 280, 680, TFT_BLACK);  // 垂直線下ファイル行
    // -------------------------------- 文字セット
    display.setCursor(450, 500);  // カーソル位置
    display.print("戻");          // 表示をセット
    display.setCursor(450, 600);  // カーソル位置
    display.print("る");          // 表示をセット
    display.display();            // セットを表示
    // -------------------------------- タッチ判定
    bool drawed = false;               // 描画変数=描画していない
    if (display.getTouchRaw(tp, 1)) {  // タッチ情報を取得
      drawed = true;                   // 描画した
      if (tp[0].y < 70) {              // 1行目なら
        if (tp[0].x < 335) {           // 左側なら
          edpQ = 1;                    // 高品質
        } else {                       // 右側なら
          edpQ = 0;                    // 高速
        }
      } else if (tp[0].y < 140) {  // 2行目なら
        if (tp[0].x < 335) {       // 左側なら
          gamenTate = 1;           // 縦向き
        } else {                   // 右側なら
          gamenTate = 0;           // 横向き
        }
      } else if (tp[0].y < 210) {  // 3行目なら
        if (tp[0].x < 335) {       // 左側なら
          fontS = 32;              // 32px
        } else {                   // 右側なら
          fontS = 36;              // 36px
        }
      } else if (tp[0].y < 280) {    // 4行目なら
        if (tp[0].x < 285) {         // 左側なら
          gyoukan = 5;               // 行間5px
        } else if (tp[0].x < 420) {  // 中央なら
          gyoukan = 10;              // 行間10px
        } else {                     // 右側なら
          gyoukan = 15;              // 行間15px
        }
      } else if (tp[0].x > 400) {  // 5行目以下の右なら
        fName = fList[0][0];       // file1
        break;                     // 戻る
      } else if (tp[0].y < 350) {  // 5行目のfile1なら
        fName = fList[0][0];       // file1
        break;                     // 戻る
      } else if (tp[0].y < 420) {  // 6行目のfile2なら
        fName = fList[1][0];       // file2
        break;                     // 戻る
      } else if (tp[0].y < 490) {  // 7行目のfile3なら
        fName = fList[2][0];       // file3
        Serial.println(fName);
        break;                     // 戻る
      } else if (tp[0].y < 560) {  // 8行目のfile4なら
        fName = fList[3][0];       // file4
        break;                     // 戻る
      } else if (tp[0].y < 630) {  // 9行目のfile5なら
        fName = fList[4][0];       // file5
        break;                     // 戻る
      } else if (tp[0].y < 700) {  // 10行目のfile6なら
        fName = fList[5][0];       // file6
        break;                     // 戻る
      } else if (tp[0].y < 770) {  // 11行目のfile7なら
        fName = fList[6][0];       // file7
        break;                     // 戻る
      } else if (tp[0].y < 840) {  // 12行目のfile8なら
        fName = fList[7][0];       // file8
        break;                     // 戻る
      } else {                     // 13行目のfile9なら
        fName = fList[8][0];       // file9
        break;                     // 戻る
      }
    } else if (drawed) {  // もし描画していれば
      drawed = false;     // 描画変数を戻す
    }
    display.display();// セットを表示
    vTaskDelay(1);  // 1tickの間タスクをブロック
  }
}

void setup() {
  Serial.begin(115200);  // シリアル通信開始
  SPI.begin(SD_SPI_SCK_PIN, SD_SPI_MISO_PIN,
            SD_SPI_MOSI_PIN, SD_SPI_CS_PIN);
  delay(100);                                    // 0.1秒待つ
  SetFont();                                     // Font等初期化関数へ
  if (!SD.begin(47)) {                           // SDライブラリとカードを初期化できなければ
    display.println("失敗 SDライブラリ初期化");  // 表示
    while (true)                                 // 無限ループ
      ;
  }
  // -------------------------------- ファイル名取得
  myFile = SD.open("/");                                 // ルートのファイルを開く
  char buf2[40];                                         // 作業用
  int8_t i = 0;                                          // ファイルかう
  while (true) {                                         // breakになるまで無限ループ
    File entry = myFile.openNextFile();                  // ディレクトリ内の次のファイルまたはホルダを取得
    if (!entry) break;                                   // 次のファイルまたはフォルダがないなら抜ける
    if (!entry.isDirectory()) {                          // ホルダでなければ
      fList[i][0] = String(entry.name());                // ファイル名を取得
      if (entry.size() < 1000) {                         // ファイルサイズが1kB未満なら
        sprintf(buf2, " %5dB\n", entry.size());          // bufにファイルサイズ(bytes)を
      } else {                                           // 1kB以上なら
        sprintf(buf2, " %4dkB\n", entry.size() / 1000);  // Bufにファイルサイズ(kB)を
      }
      fList[i][1] = String(buf2);  // ファイルサイズ(B or kB単位)
      i++;                         // 次のファイル番号
    }
    entry.close();  // ファイルを閉じる
  }
  // -------------------------------- メニュー画面
  fName = fList[0][0];  // 何も入力しなければfile1
  printMenu();          // メニュー表示関数
  // -------------------------------- メニュー内容で設定
  if (edpQ) {                                     // 画面の品質設定
    display.setEpdMode(epd_mode_t::epd_quality);  // 高品質 (描画遅)
  } else {
    display.setEpdMode(epd_mode_t::epd_fastest);  // 描画速 (低品質)
  }
  if (gamenTate) {           // 画面向き設定
    display.setRotation(0);  // 縦長
  } else {
    display.setRotation(1);  // 横長
  }
  if (fontS == 36) {                              // フォントサイズ設定
    display.setFont(&fonts::lgfxJapanGothic_36);  // 固定幅ゴシック体 8,12,16,20,24,28,32,36,40
  } else {
    display.setFont(&fonts::lgfxJapanGothic_32);  // 固定幅ゴシック体 8,12,16,20,24,28,32,36,40
  }
  // -------------------------------- 文章表示画面
  pageN = 0;                        // 頁番号0
  pageB[pageN] = 0;                 // 頁番号の最初のバイト値は0
  buf = setBuf(pageB[pageN]);       // その頁のbufを得る関数へ
  pageB[pageN + 1] = setView(buf);  // 戻り値(Byte値)は次の頁の開始Byte値
}

void loop() {
  bool drawed = false;                    // 描画変数=描画していない
  if (display.getTouch(tp, 1)) {          // タッチ情報を取得
    drawed = true;                        // 描画した
    if (tp[0].x > display.width() / 2) {  // 次頁タッチなら
      pageN++;                            // 頁番号1加算
      buf = setBuf(pageB[pageN]);         // その頁のbufを得る関数へ
      pageB[pageN + 1] = pageB[pageN] + setView(buf);
      // 1頁表示し、(開始+処理)のByte数を次頁の開始Byte数にする
    } else if (pageN > 0) {        // 前頁タッチで頁番号が1以上なら
      pageN--;                     // 頁番号1減算
      buf = setBuf(pageB[pageN]);  // その頁のbufを得る関数へ
      setView(buf);                // 1頁表示関数へ
    }
  } else if (drawed) {  // もし描画していれば
    drawed = false;     // 描画変数を戻す
  }
  vTaskDelay(1);  // 1tickの間タスクをブロック
}
* flash memory(3.1Mbyte)のうち、スケッチが44%使用。RAM(327kbyte)のうち、global変数が7%使用、local変数で302kbyte使用可能。(1000byte=1kbyteで計算)