03.M5Tab5でホルダ内のjpgをタイル表示


03.M5Tab5でホルダ内のjpgをタイル表示

SDの指定ホルダにあるjpgを読込みソートし、昇順に表示しますが、表示はタイル(一部分)を表示し、タッチでタイルの表示場所を移動します。01.と02.を合わせたテスト版で、以下の条件があります。
・ホルダ名はSDのルートに "/test" (スケッチ内に記述)
・ファイル名は "01.jpg", "02.jpg", ...  (拡張子を除くファイル名は数字。比較演算子でソートしています)
・色の深さは 16bit(6万色)
・100頁まで (変更可)
・jpgの横は2432px (スケッチ内に記述) *1
・jpgの縦は3653px (スケッチ内に記述)
・jpgのサイズは全頁同じ

*1 元データは縦長A4以下の横書きpdfを考え、pdfをjpgにする時横2432pxに変換します。
Tab5の長辺1280pxで、2回で表示(左タイルと右タイル)するので、重なりは128pxとなります。
1280 x 2 - 2432 = 128

縦の重なり
jpgの縦は最低100pxとし、画面いっぱいに表示するため、タイル数は増やさず、重なりを均等に増やしています。ただし、頁最下のタイルは画面に合わせ広げています。

動作

1.起動すると、進行状況とタッチ位置を表示します。
 境界線をタッチするとクリック音がします。
2.ファイルを昇順に表示するため、SDのファイル名を配列に読込み、ソートします。
3.1頁分のjpgをキャンバスに入れます
4.縦のタイル数と重なり(px)を計算します。
5.1頁目の左上のタイルを表示します。

タッチの動作
左側:右タイルなら左へ移動。左タイルの時は何もしません
中央上:上の左タイルへ移動
   頁の一番上なら前頁の一番下の左へ移動
中央中:メニューへ戻る。(現在動作しません)
中央下:下の左タイルへ移動
   頁の一番下なら次頁の一番上の左へ移動
右側:左タイルなら右へ、右タイルなら左へ移動
境界線:クリック音のみ

スケッチ


// SDの指定ホルダにあるjpgを昇順に読込み、タッチで表示タイル変更
// ホルダ名は /test、ファイル名は 01.jpg, 02.jpg, ...、サイズは全頁同じとします
// 色の深さは 8bit(256色)
// Arduino-IDE 2.3.6
// M5Stack by M5Stack official 3.2.2
// M5Unified by M5Stack 0.2.7
// M5GFX by M5Stack 0.2.9
#include <SD.h>             // SDを使用
#include <M5Unified.h>      // M5統合ライブラリ SDが先に
#define SD_SPI_CS_PIN 42    // SD CS pin
#define SD_SPI_SCK_PIN 43   // SD SCK pin
#define SD_SPI_MOSI_PIN 44  // SD MOSI pin
#define SD_SPI_MISO_PIN 39  // SD MISO pin
int32_t imgW = 2432;        // jpg横(=38x64) tileW*2(=2560)以下
// 横重なりは、lcdW x 2 - imgW = 1280x2-2432 = 128px
int32_t imgH = 3653;                                 // jpg縦 1.41倍程度 スキャン切抜きにより異なる
int32_t lcdW = 1280;                                 // 表示横 画面横いっぱい (Tab5:1280x720)
int32_t lcdH = 720;                                  // 表示縦 全画面表示で無いと数倍遅い
int32_t overlapHmin = 100, overlapH;                 // 縦重なりmin,実際の縦重なり
int32_t xx1 = 425, xx2 = 805, xx3 = 1160;            // タッチ区切りのx位置
int32_t dx1 = 50, dx2 = 50, dx3 = 120;               // タッチ区切りのx重なり
int32_t yy1 = 170, dy1 = 100, yy2 = 450, dy2 = 100;  // 中央のタッチ区切りのy位置
int8_t tileW = 1, tileH = 1, tileEnd = 1;            // W=1:左,W=2:右,H=1:一番上のタイル,縦タイル数
int32_t offX, offY;                                  // 表示のオフセット
M5Canvas canvas(&M5.Display);                        // canvasをM5.Displayに描画
String fileN[100], fullN;                            // ファイル名格納は一応100まで,ホルダ付ファイル名
const char* folderN = "/test";                       // 読込ホルダ
int8_t fileCount = 1, fileCountEnd;                  // 指定ホルダ内のファイル番号,ファイル数

void Comment() {                                  // コメント表示関数
  M5.Display.setTextColor(TFT_GREEN, TFT_BLACK);  // txt色(文字,地)
  M5.Display.setTextSize(1.5);                    // txtサイズ
  M5.Display.setCursor(100, 720 / 2 - 40);        // カーソル位置
  M5.Display.printf(" %s 読込中 ", fullN);        // 読込ファイル名表示
}

void setFont() {                                   // FFont等画面初期設定
  M5.Display.setRotation(3);                       // 画面回転 (0-3,4-7)
  M5.Display.setFont(&fonts::lgfxJapanGothic_40);  // 固定幅ゴシック 8,12,16,20,24,28,32,36,40
  M5.Display.setTextColor(TFT_GREEN);              // 文字緑
  M5.Display.fillScreen(TFT_DARKGREEN);            // 全画面を深緑 起動確認のため
  M5.Display.setBrightness(100);                   // 明るさ (0暗-255明)
  M5.Display.clear();                              // 画面クリア
}

void SDcheck() {  // SD確認関数
  if (!SD.begin(SD_SPI_CS_PIN, SPI, 25000000)) {
    // SDの初期化に失敗時 or SDが存在しない時(CSpin,SPI通信,SPI25MHz)
    M5.Display.println(" SDが見つかりません。再起動してください。");  // 画面表示
    for (;;) delay(1000);                                             // 永久ループ
  }
  M5.Display.println(" SDに接続 完\n");  // 画面表示
}

void fileSort() {  // file名ソート関数
  // -------------------- 指定ホルダ確認
  File folder = SD.open(folderN);                       // 指定ホルダを開きます
  if (!folder || !folder.isDirectory()) {               // 開けなかったorホルダでなければ
    M5.Display.println(" 指定ホルダが見つかりません");  // 画面表示
    for (;;) delay(1000);                               // 永久ループ
  }
  M5.Display.printf(" 指定ホルダ %s\n\n", folderN);  // 画面表示

  // -------------------- ソートのため、ファイル名を配列に代入
  M5.Display.println(" ファイル名 読込中\n");  // 画面表示
  while (true) {                               // 永久ループ breakで抜ける
    File entry = folder.openNextFile();        // ホルダ内の次のファイルorホルダを取得
    if (!entry) break;                         // 無ければwhileの外へ
    if (!entry.isDirectory()) fileN[fileCount++] = String(entry.name());
    // ホルダでなければ、ファイル名を配列へ代入し1増加
    entry.close();  // fileを閉じ、データをSDに保存
  }
  fileCountEnd = fileCount;                               // 指定ホルダ内のファイル数
  M5.Display.printf(" ファイル数 %d\n\n", fileCountEnd);  // ファイル数表示

  // -------------------- ファイル名の配列をソート
  M5.Display.print(" fileソート中");  // 画面表示
  // ファイル名を昇順順にソート 小さい方から順に決まっていく
  // 1番目と後ろを比較し、小さい方を一番目と交換、配列の最終まで行くと1番目が最小値
  // 2番目以降も同様。
  for (int8_t i = 0; i < fileCountEnd - 1; i++) {    // i=最初から最後-1まで繰返す
    for (int8_t j = i + 1; j < fileCountEnd; j++) {  // J=iの次から最後まで繰返す
      if (fileN[i] > fileN[j]) {                     // ファイル名が i > j なら
        String temp = fileN[i];                      // 一時退避
        fileN[i] = fileN[j];
        fileN[j] = temp;  // i < jとなる
      }
    }
  }
  M5.Display.println(" ... 完\n");  // 画面表示
}

void jpgView() {         // jpgタイル表示関数
  if (tileW == 1) {      // 左タイルなら
    offX = 0;            // 左側
  } else {               // 右タイルなら
    offX = lcdW - imgW;  // 右側
  }
  offY = -1 * (tileH - 1) * (lcdH - overlapH);  // 画像のオフセットy位置
  if (lcdH > imgH + offY) {                     // 一番下のブロック(表示が余る)なら
    offY = -(imgH - lcdH);                      // 全画面表示するよう重なりを増やす
  }
  canvas.startWrite();            // バッファを効率的に使用開始
  canvas.pushSprite(offX, offY);  // キャンバスをプッシュ(左上基準点)
  canvas.endWrite();              // バッファを効率的に使用終了
}

void jpgMenu() {  // メニューへ戻る
  // まだ
}

void setup() {
  M5.begin();            // M5の初期化
  Serial.begin(115200);  // シリアルモニタ通信速度設定
  while (!Serial)        // 準備ができるまで待つ
    ;
  Serial.print("シリアル通信が起動しました");                                  // シリアルモニタに表示
  SPI.begin(SD_SPI_SCK_PIN, SD_SPI_MISO_PIN, SD_SPI_MOSI_PIN, SD_SPI_CS_PIN);  // SPIの初期化
  setFont();                                                                   // Font等画面初期設定

  // -------------------- タッチ位置表示
  M5.Display.startWrite();                                           // バッファを効率的に使用開始
  M5.Display.fillRect(xx1, 0, dx1, 720, M5.Lcd.color565(0, 20, 0));  // 縦1 未使用範囲は塗りつぶし
  M5.Display.fillRect(xx2, 0, dx2, 720, M5.Lcd.color565(0, 20, 0));  // 縦2
  M5.Display.fillRect(xx3, 0, dx3, 720, M5.Lcd.color565(0, 20, 0));  // 右端
  M5.Display.fillRect(xx1 + dx1, yy1, xx2 - xx1 - dx1, dy1,
                      M5.Lcd.color565(0, 20, 0));  // 中央上 ほぼ黒色
  M5.Display.fillRect(xx1 + dx1, yy2, xx2 - xx1 - dx1, dy2,
                      M5.Lcd.color565(0, 20, 0));  // 中央下
  M5.Display.fillRect(xx2 + dx1, yy2 + dy2, xx3 - xx2 - dx2, 720 - yy2 - dy2,
                      M5.Lcd.color565(0, 20, 0));  // 右下
  M5.Display.endWrite();                           // バッファを効率的に使用終了

  // -------------------- キャンバス初期設定
  canvas.setPsram(true);     // キャンバスはPSRAMを使用
  canvas.setColorDepth(16);  // 色の深さ(1,2,4,8,16,24,32),8はスキャンのにじみが出る
  // 2:4色,4:16色,8bit:2^8=256色,16:6万色,24:1677万色,32:42億色

  // -------------------- 1頁分丸ごとキャンバスを確保
  if (!canvas.createSprite(imgW, imgH)) {  // 1頁丸ごとキャンバスが作れなければ
    // 2432px × 3653px × RGB565(16bit)/8bit=約18MBを確保。PSRAM断片化で失敗したら再起動や初期確保
    M5.Display.println(" キャンバス枠作成 失敗");  // 画面表示
    for (;;) delay(1000);                          // 永久ループ
  }

  // -------------------- overlapHを計算
  tileEnd = ((imgH * 1.0 - lcdH) / (lcdH - overlapHmin) + 1 + 0.999);  // 縦タイル数
  overlapH = ((imgH * 1.0 - lcdH) / (tileEnd - 1) + 0.5);
  overlapH = lcdH - overlapH;                          // 縦のoverlap(px)
  M5.Display.printf(" 縦重なり %3dpx\n\n", overlapH);  // overlapHを表示

  // --------------------
  SDcheck();   // SD確認関数へ
  fileSort();  // file名ソート関数へ

  // -------------------- jpgをキャンバスに描画
  M5.Display.println(" jpgをキャンバスに描画中\n");     // 画面表示
  fileCount = 1;                                        // 1頁目
  fullN = String(folderN) + "/" + fileN[fileCount];     // ホルダ付ファイル名
  if (!canvas.drawJpgFile(SD, fullN.c_str(), 0, 0)) {   // キャンバスにjpgを描画できなければ
    M5.Display.println(" jpgをキャンバスに描画 失敗");  // 画面表示
    for (;;) delay(1000);                               // 永久ループ
  }

  // --------------------
  M5.Speaker.setVolume(150);  //音量(0-255) ngの時鳴らす
  delay(1000);                // 表示を1秒待つ
  jpgView();                  // 最初のjpgタイル表示関数へ
}

void loop() {
  M5.update();                                     // touch状態更新
  if (M5.Touch.isEnabled()) {                      // タッチが初期化されていれば
    auto touch = M5.Touch.getDetail();             // タッチの詳細情報取得
    if (touch.wasReleased()) {                     // 指を離したら
      int32_t touchX = static_cast<int>(touch.x);  // 安全な型変換
      int32_t touchY = static_cast<int>(touch.y);  // 安全な型変換
      // -------------------- 左側タッチ
      if (touchX < xx1) {  // 左をタッチしたら 425
        if (tileW == 2) {  // 今右タイルなら
          tileW = 1;       // 左タイルへ
          jpgView();       // jpgタイル表示関数へ
        }
      } else if (touchX < xx1 + dx1) {  // 475 何もしない
        M5.Speaker.tone(440, 100);      // 440Hzを100mS鳴らす
        // -------------------- 中央部タッチ
      } else if (touchX < xx2) {                                    // 805
        if (touchY < yy1) {                                         // 上をタッチしたら 170
          tileH -= 1;                                               // 上タイルへ
          tileW = 1;                                                // 左タイルへ
          if (tileH < 1) {                                          // タイルが一番上だったなら
            if (fileCount > 1) {                                    // 2頁名以降なら
              fileCount--;                                          // 前頁へ
              fullN = String(folderN) + "/" + fileN[fileCount];     // ホルダ付ファイル名
              Comment();                                            // コメント表示関数へ
              if (!canvas.drawJpgFile(SD, fullN.c_str(), 0, 0)) {   // キャンバスにjpgを描画できなければ
                M5.Display.println(" jpgをキャンバスに描画 失敗");  // 画面表示
                for (;;) delay(1000);                               // 永久ループ
              }
              tileH = tileEnd;  // 一番下のタイルへ
            }
          }
          jpgView();                                                // jpgタイル表示関数へ
        } else if (touchY < yy1 + dy1) {                            // 270 何もしない
          M5.Speaker.tone(440, 100);                                // 440Hzを100mS鳴らす
        } else if (touchY < yy2) {                                  // メニューをタッチしたら 450
          jpgMenu();                                                // メニュー関数へ
        } else if (touchY < yy2 + dy2) {                            // 550 何もしない
          M5.Speaker.tone(440, 100);                                // 440Hzを100mS鳴らす
        } else {                                                    // その下の"下"をタッチしたら
          tileH += 1;                                               // 下タイルへ
          tileW = 1;                                                // 左のタイルへ
          if (tileH > tileEnd) {                                    // タイルが一番下なら
            if (fileCount < fileCountEnd) {                         // 最終頁でなければ
              fileCount++;                                          // 次頁へ
              fullN = String(folderN) + "/" + fileN[fileCount];     // ホルダ付ファイル名
              Comment();                                            // コメント表示関数へ
              if (!canvas.drawJpgFile(SD, fullN.c_str(), 0, 0)) {   // キャンバスにjpgを描画できなければ
                M5.Display.println(" jpgをキャンバスに描画 失敗");  // 画面表示
                for (;;) delay(1000);                               // 永久ループ
              }
              tileH = 1;  // 1番上へ
            }
          }
          jpgView();  // jpgタイル表示
        }
      } else if (touchX < xx2 + dx2) {  // 855 何もしない
        M5.Speaker.tone(440, 100);      // 440Hzを100mS鳴らす
        // -------------------- 右側タッチ
      } else if (touchX < xx3) {   // 1160
        if (touchY < yy2 + dy2) {  // 右をタッチしたら 550
          if (tileW == 1) {        // 今左タイルなら
            tileW = 2;             // 右タイルへ
          } else {                 // 今右タイルなら
            tileW = 1;             // 左タイルへ
          }
          jpgView();  // jpgタイル表示関数へ
        } else {
          M5.Speaker.tone(440, 100);  // 440Hzを100mS鳴らす
        }
      } else {
        M5.Speaker.tone(440, 100);  // 440Hzを100mS鳴らす
      }
    }
  }
  vTaskDelay(1);  // 他のタスクを実行、マルチタスク用遅延(タスク数)
}
* flash memory(6.5Mbyte)のうち、スケッチが16%使用。
RAM(327kbyte)のうち、global変数が8%使用、
local変数で299kbyte使用可能。(1000byte=1kbyteで計算)