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で計算)