71歳の... ESP32はDOSマシンになるか？コマンド入力で操作したい。ついでに問題点の解決も？

　いわゆる **DOS** の自作なんて私には到底無理ですが、まねっこしてみました。大げさですが、これまでの様々な知識の集大成です。併せて幾つかの問題も解決できたかな？
　概要。

:::note info
・**TFT-LCD** を日本語表示、自動スクロール可能に（横約60桁、縦19+1行）
・ファイルシステムとして **microSDカード** を接続
・**ESP32** に対し、シリアル通信でコマンドとパラメーターを送信し、何か操作を行う。
:::

環境・構成・内容は以下です。

![console.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3189926/99ded76a-7e37-4f3b-ae7c-2bde7ad74202.jpeg)

    １．Windows 11 Pro
    ２．マイコンは、ESP32-S3-DevKit-C（16MBのFlash）
        Arduino IDE 2.3.6 では「ESP32S3 Dev Module」となる。
        ツール>Partition Scheme:"Huge APP...
    ３．ファイルシステムとして、 microSD Card(32GB)
        コンパイルエラーが起こるので「esp32 by Espressif Systems 3.0.0」にする
    ４．画面表示器は、3.95" TFT LCD Shield Display Module
        8bitパラレル接続、480*320、ILI9488、SPI接続microSDスロット付き
    ５．TFT用ライブラリは、「LovyanGFX」（これである必要はないと...）
    ６．Serial.print(ln)出力とほぼ同じ内容を TFTLCD にも表示させる。
        TFTLCD は横長にて使用。横60桁程度、縦19行＋プロンプト行まで表示可能。
        縦19行を越えたら自動スクロール。
    ７．WAVファイル再生にはPCM5102Aを使用。電源は別途用意。   
    ８．現在までに実装したコマンド
        ・ファイル操作関連のコマンド
        ・画像ファイルの表示
        ・グラフィックの基本コマンド
        ・PWM出力コマンド（音の出力、LEDの点灯）
        ・以下のレベルのWAVファイルの再生
            Total size :38067156
            Format section size :16
            Wave format :1
            Channels :2
            Sample Rate :44100
            Byte Rate :176400
            Block Align :4
            Bits Per Sample :16
![TFTLCDBoard.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3189926/f58e0f04-2183-4bc5-bf28-cc3b0c7911ca.jpeg)

コマンド文字列をシリアル通信で ESP32 側に送るのですが、送信後に文字列の編集ができません。それで、Arduino IDE 付属の「シリアルモニター」を使用します。「Tera Term」では一文字ごとに送信してしまい、使えませんでした。コマンド入力を例示します。


|機能|コマンド文字列の例|備考|
|----|---------------------|-----|
|画面の消去|cls||
|WAVファイル再生|wav *.wav|ルートより下の階層も可能 |
|画像の表示|pic *.bmp|*.png *.jpg |
|音を出す|tone 440 6| 周波数 時間|
|SDカードの情報|sd||
|Dir/Fileを全て表示|ls||
|指定のDir/fileを調査|ls dirname/filename||
|ファイル名の変更|rn /file0 /file1|安全のため、ルートファイルの頭には/を付ける仕様|
|ファイルの中身の表示|re readme.txt||
|ファイルの生成|cr test.txt||
|ファイルへの書込み|wr test.c||
|ファイルの削除|RM test1.txt|安全のため大文字|
|ドローモードへ入る|draw|無限ループに入る|
|直線の描画|li 20 10 200 160|xBegin yBegin xEnd yEnd|
|楕円・円の描画|el 240 160 150 100|xCenter yCenter xRadius yRadius|
|四角形の描画|ra 30 20 200 150|xBegin yBegin width height|
|ドローモードを抜ける|q|無限ループから抜ける|

-----------------------------------------------------------------

　工夫した事、問題点、悩んだ事を挙げます。
 １．SDカードルート上のファイル名には、頭に`/`を付ける必要があります。
 
 ２．行が多くなった場合の自動スクロールのために`<vector>`コンテナを使い、コードが簡潔になりました。
 
 ３．TFTLCDへの表示に`drawchar()`、`drawstring()`ではなく、`print()`を使ったので、日本語表示が可能になりました。昔、レコードから録音生成したWAVファイルに付けた長い日本語名も問題なく表示できています。
 
 ４．日本語フォントの問題なのかアンダーライン「＿」が TFTLCD には表示できません。
 
 ５．ファイル名の変更操作はFAT部分を書換えるだけ(?)なので簡単に実装できました。ファイルのコピーは実体を伴い面倒そうなので見送り。
 
 ６．ファイルの書込みは一行ずつ行います。日本語の入力については、シリアルモニターの一行窓内で日本語に変換すると動作がおかしくなります。でも、別のところからコピー＆ペーストで持ってくれば日本語も入力可能のようです。
 
 ７．次のエラーメッセージが出て、jpgなどの画像ファイル表示ができなくなっていました。解決できました。少し方法が変わったようです。

     error: cannot declare variable 'file' to be of abstract type 'lgfx::v1::DataWrapperT<const char [7]>'
       DataWrapperT<T> file ( &fs ); \
                       ^~~~
 
 ８．このエラーに悩みました。
     
     Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled.
 　さっきまで動いてたのに突然発生するなどの実行時エラーです。ネット上では様々な意見があります。私の場合は、画面表示メモリ（以後**VRAM**）外への書込みが発生したと思われます。テキストラップを無視していたので、極端に長い文字列や**VRAM**終端近くでの文字列書込みによるもののようです。`t.setTextWrap(false, false)`と設定するとともに、画面最終の20行目に、`>`以外は書込まないように気を付け解決できています。

-------------------------------------------------------------------------
  専用のLovyanGFX.hpp（スケッチと同じフォルダーに置く）とスケッチです。長くてすいません。できることが多く、詰込んじゃいました。
  利用される場合は、SDカードの破損など、自己責任でお願いします。

```html:myLovyanGFX.hpp
#pragma once

#define LGFX_USE_V1

#include <LovyanGFX.hpp>

class LGFX : public lgfx::LGFX_Device 
{
  lgfx::Panel_ILI9488   _panel_instance;
  lgfx::Bus_Parallel8   _bus_instance;  // 8ビットパラレルバスのインスタンス (ESP32のみ)
  
public:
  LGFX(void) {   
    {                                   // バス制御の設定を行います。
      auto cfg = _bus_instance.config();// バス設定用の構造体を取得します。
      // 8ビットパラレルバスの設定
      cfg.freq_write = 20000000;  // 送信クロック (最大20MHz, 80MHzを整数で割った値に丸められます)
      cfg.pin_wr = 6;//11;            // WR を接続しているピン番号
      cfg.pin_rd = 7;//12; // must    // RD を接続しているピン番号
      cfg.pin_rs = 5;//10;            // RS(D/C)を接続しているピン番号
      cfg.pin_d0 = 18;            // D0を接続しているピン番号
      cfg.pin_d1 = 39;//37;            // D1を接続しているピン番号
      cfg.pin_d2 = 17;            // D2を接続しているピン番号
      cfg.pin_d3 = 40;//38;            // D3を接続しているピン番号
      cfg.pin_d4 = 16;            // D4を接続しているピン番号
      cfg.pin_d5 = 41;//39;            // D5を接続しているピン番号
      cfg.pin_d6 = 15;            // D6を接続しているピン番号
      cfg.pin_d7 = 42;//40;            // D7を接続しているピン番号
      _bus_instance.config(cfg);               // 設定値をバスに反映します。
      _panel_instance.setBus(&_bus_instance);  // バスをパネルにセットします。
    }

    {                                       // 表示パネル制御の設定を行います。
      auto cfg = _panel_instance.config();  // 表示パネル設定用の構造体を取得します。
      cfg.pin_cs  =  4;//9;    // 27 CSが接続されているピン番号 (-1 = disable)
      cfg.pin_rst =  -1;//8;   // 33 RSTが接続されているピン番号  (-1 = disable)
      cfg.pin_busy = -1;  // BUSYが接続されているピン番号 (-1 = disable)
      cfg.memory_width = 320;                // ドライバICがサポートしている最大の幅
      cfg.memory_height = 480;               // ドライバICがサポートしている最大の高さ
      cfg.panel_width = 320;     // 実際に表示可能な幅
      cfg.panel_height = 480;    // 実際に表示可能な高さ
      cfg.offset_x = 0;          // パネルのX方向オフセット量
      cfg.offset_y = 0;          // パネルのY方向オフセット量
      cfg.offset_rotation = 0;   // 回転方向の値のオフセット 0~7 (4~7は上下反転)
      cfg.dummy_read_pixel = 8;  // ピクセル読出し前のダミーリードのビット数
      cfg.dummy_read_bits = 1;   // ピクセル以外のデータ読出し前のダミーリードのビット数
      cfg.readable = false;       // データ読出しが可能な場合 trueに設定
      cfg.invert = false;        // パネルの明暗が反転してしまう場合 trueに設定
      cfg.rgb_order = false;     // パネルの赤と青が入れ替わってしまう場合 trueに設定
      cfg.dlen_16bit = false;    // 16bitパラレルやSPIでデータ長を16bit単位で送信するパネルの場合 trueに設定
      cfg.bus_shared = false;    // SDカードとバスを共有している場合 trueに設定(drawJpgFile等でバス制御を行います)
      _panel_instance.config(cfg);
    }

    setPanel(&_panel_instance);  // 使用するパネルをセットします。
  }

};
```

```c:Prompt_Command_SD.ino
#include "myLovyanGFX.hpp"
#include "SD.h"
#include "driver/i2s.h"  // Library of I2S routines, comes with ESP32 standard install
#include <string>
#include <vector>
using namespace std;

// SD Card
#define SD_CS 10  // SD Card chip select
#define SPI_MOSI 11
#define SPI_MISO 13
#define SPI_SCK 12
// Display
#define WIDTH 480
#define HEIGHT 320
#define cellX 8                         // character width
#define cellY 16                        // line feed step
#define COLS (int)(WIDTH / cellX)       //48
#define ROWS (int)(HEIGHT / cellY) - 1  //16
#define NAVY 0x0002

// I2S
#define I2S_DOUT 35  // i2S Data out oin
#define I2S_BCLK 36  // Bit clock
#define I2S_LRC 37   // Left/Right clock, also known as Frame clock or word select
#define I2S_NUM 0    // i2s port number
#define byte uint8_t
// Wav File reading
#define NUM_BYTES_TO_READ_FROM_FILE 1024 * 4  // How many bytes to read from wav file at a time
//-------------------------------------------------------------------------------------------
// structures and also variables
//  I2S configuration
static const i2s_config_t i2s_config = {
  .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
  .sample_rate = 44100,  // Note, this will be changed later
  .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
  .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
  .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
  .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,  // high interrupt priority
  .dma_buf_count = 8,                        // 8 buffers
  .dma_buf_len = 64,                         // 64 bytes per buffer, so 8K of buffer space
  .use_apll = 0,
  .tx_desc_auto_clear = true,
  .fixed_mclk = -1
};

static const i2s_pin_config_t pin_config = {
  .bck_io_num = I2S_BCLK,           // The bit clock connectiom, goes to pin 27 of ESP32
  .ws_io_num = I2S_LRC,             // Word select, also known as word select or left right clock
  .data_out_num = I2S_DOUT,         // Data out from the ESP32, connect to DIN on 38357A
  .data_in_num = I2S_PIN_NO_CHANGE  // (-1)we are not interested in I2S data into the ESP32
};

struct WavHeader_Struct {
  //   RIFF Section
  char RIFFSectionID[4];  // Letters "RIFF"
  uint32_t Size;          // Size of entire file less 8
  char RiffFormat[4];     // Letters "WAVE"

  //   Format Section
  char FormatSectionID[4];  // letters "fmt"
  uint32_t FormatSize;      // Size of format section less 8
  uint16_t FormatID;        // 1=uncompressed PCM
  uint16_t NumChannels;     // 1=mono,2=stereo
  uint32_t SampleRate;      // 44100, 16000, 8000 etc.
  uint32_t ByteRate;        // =SampleRate * Channels * (BitsPerSample/8)
  uint16_t BlockAlign;      // =Channels * (BitsPerSample/8)
  uint16_t BitsPerSample;   // 8,16,24 or 32

  // Data Section
  char DataSectionID[4];  // The letters "data"
  uint32_t DataSize;      // Size of the data that follows
} WavHeader;

//  Global Variables/objects
static const i2s_port_t i2s_num = I2S_NUM_0;  // i2s port number
// The file we're currently playing
File WavFile;                              // Object for root of SD card directory
static uint16_t _BytesRead = 0;
// I2S END

LGFX t;                          // LGFXのインスタンスを作成。
static LGFX_Sprite sprite1(&t);  // 画像表示用

File fp;
std::vector<String> l_buffer;
const String _rt = "/";
const int PWM_PIN = 14;

int CurY = 0, by = 0;
void tString(String s);
void pri_both(String s);
void printDirectory(File rt, int numTabs);
int split(String data, char delimiter, String* dst, int num);
void ascr();
int pn;

int pastY = 0; // 前スクロールのため

void ls_0(File fp) {
  String s;
  while (File file = fp.openNextFile()) {
    // Loop through all the files in the root directory
    Serial.print("  ");
    if (file.isDirectory()) {
      s = (String)file.name() + "/";
      Serial.print(s);
    } else {
      s = (String)file.name();
      Serial.print(s);
    }
    Serial.print("\t");
    Serial.println(file.size());  // Print the filesize
    tString("  " + s + " ;  size=" + (String)file.size() + "bytes");
    file.close();  // Close the file
  }
  Serial.println(" --- Found Dirs/Files.");
  tString(" --- Found Dirs/Files.");
}

void SD_Info() {
  uint8_t cardType = SD.cardType();  // カード情報
  String s = " - Card Type: ";
  if (cardType == CARD_MMC) s += "MMC";
  else if (cardType == CARD_SD) s += "SDSC";
  else if (cardType == CARD_SDHC) s += "SDHC";
  else s += "UNKNOWN";
  pri_both(s);

  uint64_t cardSize = SD.cardSize() / (1024 * 1024 * 1024);
  s = " - Card Size: " + (String)(cardSize) + "GB";
  pri_both(s);
}

void setup() {
  Serial.begin(115200);
  Serial.println("\n\rSerial Console start...");

  t.init();
  t.setRotation(1);
  t.setBaseColor(NAVY);
  t.clear();
  t.setFreeFont(&fonts::lgfxJapanMinchoP_12);  //&lgfx::fonts::Font2);
  t.setTextSize(1);
  t.setTextWrap(false, false);
  t.setTextColor(TFT_WHITE, NAVY);

  SD.begin(SD_CS, SPI, 24000000, "/sd");

  sprite1.createSprite(250, 250);  // 画像表示用
  ledcAttach(PWM_PIN, 24000, 8);   // for tone

  //I2S
  i2s_driver_install(i2s_num, &i2s_config, 0, NULL);
  i2s_set_pin(i2s_num, &pin_config);
}

void loop() {
  Serial.println(">");
  t.setCursor(0, CurY * cellY);
  t.print(">");
  t.setCursor(52 * cellX, 0);
  t.print("line=" + (String)by);

  while (Serial.available() == 0) { ; }

  String line = Serial.readString();
  line.trim();

  if (line.substring(0, 1) == "^") { // 前スクロール １行ごとにリターンキー入力が必要
    if ((2 + by - ROWS + pastY) > 0) pastY--;
    ascr();
  } else {

    pastY = 0;
    pri_both(">" + line);

    if (line.substring(0, 3) == "cls") {
      t.clear();
      CurY = 0;
    }

    else if (line.substring(0, 3) == "wav") {

      WavFile = SD.open("/music/wav/Kohaku.wav");  // Open the wav file
      if (WavFile == false){
        Serial.println("Could not open 'wavfile(*.wav)'");
      }else {
        WavFile.read((byte*)&WavHeader, 44);                    // Read in the WAV header, which is first 44 bytes of the file.
                                                                // We have to typecast to bytes for the "read" function
        DumpWAVHeader(&WavHeader);                              // Dump the header data to serial, optional!
        if (ValidWavData(&WavHeader))                           // optional if your sure the WAV file will be valid.
          i2s_set_sample_rates(i2s_num, WavHeader.SampleRate);  //set sample rate
      }                                                         // Your normal code to do your task can go here
      
      while ( PlayWav() > 0){;} // 曲の終了までここでloop

    }

    else if (line.substring(0, 3) == "pic") {
      String fn = _rt + line.substring(4);  // "pic *.png" or "pic *.jpg"
      File picfile = SD.open(fn, FILE_READ, false);
      if (picfile) {
        if (line.substring(line.length() - 3) == "bmp") {
          t.drawBmp(&picfile, 300, 0);  // tft_lcd座標(300,0)に描画
        } else if (line.substring(line.length() - 3) == "png") {
          t.drawPng(&picfile, 50, 0);  // tft_lcd座標(50,0)に描画
        } else if (line.substring(line.length() - 3) == "jpg") {
          // Setted as [static LGFX_Sprite sprite1(&t);]
          sprite1.drawJpg(&picfile, 0, 0);  // スプライト座標(0,0)に描画
          sprite1.pushSprite(50, 50);       // tft_lcd座標(50,50)にスプライトを描画
          sprite1.pushRotateZoom(350, 200, 0, 0.5, 0.5, TFT_WHITE);
        } else {
          ;
        }
      } else {
        String s = " " + fn + " not exists.";
        pri_both(s);
      }
    }

    else if (line.length() > 5 && line.substring(0, 4) == "tone") {
      int n = 2;  // "tone freq duration"
      String za[n];
      int co[n];
      bool flag = true;
      if (split(line.substring(5), ' ', za, n) != n) flag = false;
      for (int i = 0; i < n; i++) {
        if (!(co[i] = za[i].toInt())) {
          flag = false;
          break;
        }
      }
      if (flag) {
        ledcWriteTone(PWM_PIN, co[0]);  // freq
        delay(co[1] * 500);             // duration
        ledcWriteTone(PWM_PIN, 0);
      }
    } else if (line.substring(0, 2) == "sd") {  // SD information
      SD_Info();

    } else if (line.substring(0, 2) == "ls") {
      if (line.length() == 2) {  // list up all dirs/files
        File root = SD.open(_rt);
        pn = 0;
        printDirectory(root, 0);
      } else {
        String s, fn = _rt + line.substring(3);  // "ls dir/file"
        if (SD.exists(fn)) {
          s = " " + fn + " exists.";
          pri_both(s);
          File fp = SD.open(fn);
          if (fp.isDirectory()) ls_0(fp);  //そのdir内のみを表示
        } else {
          s = " " + fn + " not exists.";
          pri_both(s);
        }
      }

    } else if (line.substring(0, 2) == "rn") {  // rename filename
      int n = 2;                                // "rn /before.txt /after.txt"
      String za[n];
      bool flag = true;
      if (split(line.substring(3), ' ', za, n) != n) flag = false;
      if (flag) {
        if (!SD.exists(za[0])) {
          String s = " " + za[0] + " not exists. unabel!";
          pri_both(s);
        } else if (SD.exists(za[1])) {
          String s = " " + za[1] + " already exists. Prohibited!";
          pri_both(s);
        } else {
          String s = " rename " + za[0] + " -> " + za[1] + ".";
          pri_both(s);
          if (SD.rename(za[0], za[1])) pri_both(" successed.");
          else pri_both(" failed.");
        }
      }

    } else if (line.substring(0, 2) == "re") {  // read file
      String s, fn = _rt + line.substring(3);   // "re filename"
      if (SD.exists(fn)) {
        File fp = SD.open(fn, FILE_READ);
        if (fp) {
          s = " --- " + fn + " begin ...";
          pri_both(s);
          while (fp.available()) {  // read from the file until there's nothing else in it
            String l = fp.readStringUntil('\n');
            l.trim();
            pri_both(l);
          }
          fp.close();  // close the file:
          s = " --- " + fn + " ends.";
          pri_both(s);
        } else {  // if the file didn't open, print an error
          s = " --- " + fn + " open error.";
          pri_both(s);
        }
      } else {
        s = " --- " + fn + " not exists.";
        pri_both(s);
      }

    } else if (line.length() > 3 && line.substring(0, 2) == "cr") {
      String s, fn = _rt + line.substring(3);  // "cr filename"
      if (SD.exists(fn)) {
        s = " " + fn + " already exists. Prohibited!";
        pri_both(s);
      } else {
        File fp = SD.open(fn, FILE_WRITE);
        if (fp) {
          fp.close();
          s = " " + fn + " created.";
          pri_both(s);
        } else {
          s = " " + fn + " create error.";
          pri_both(s);
        }
      }

    } else if (line.length() > 3 && line.substring(0, 2) == "wr") {
      String fn = _rt + line.substring(3);  // "wr filename"
      String s = " Write to " + fn;
      pri_both(s);
      File fp = SD.open(fn, FILE_APPEND);
      if (fp) {
        s = " file opened. Type a line or EOF.";
        pri_both(s);
        while (1) {
          if (Serial.available()) {
            String data = Serial.readString();
            data.trim();
            if (data.equals("EOF")) {
              fp.close();
              break;
            }
            fp.println(data);  // 1行ごと書き込む
            s = " Inputed a line >" + data;
            pri_both(s);
          }
        }
        s = " --- EOF Ended. file closed.";
        pri_both(s);
      } else {
        s = " file open refused.";
        pri_both(s);
      }

    } else if (line.length() > 3 && line.substring(0, 2) == "RM") {
      String s, fn = _rt + line.substring(3);
      if (SD.exists(fn)) {
        SD.remove(fn);
        s = " " + fn + " removed";
        pri_both(s);
      } else {
        s = " " + fn + " not exists.";
        pri_both(s);
      }

    } else if (line.equals("draw")) {  // Draw Graphics mode
      t.clear();
      String dr = "";  // line , ellipse or rectangle
      while (1) {
        t.drawLine(0, HEIGHT / 2, WIDTH, HEIGHT / 2, 0x39E7);
        t.drawLine(WIDTH / 2, 0, WIDTH / 2, HEIGHT, 0x39E7);
        Serial.print(">");
        t.setCursor(0, 0);
        t.print(">");

        while (Serial.available() == 0) { ; }  // ever loop
        dr = Serial.readString();
        dr.trim();

        if (dr.substring(0, 1) == "q") break;

        if (dr.length() > 3) {  // Input->"li/el/rt x0 y0 x1 y1"
          t.fillRect(0, 0, WIDTH / 2, cellY, NAVY);
          String k = dr.substring(0, 2);
          int n = 4;
          String za[n];
          int co[n];
          bool flag = true;
          if (split(dr.substring(3), ' ', za, n) != n) flag = false;
          for (int i = 0; i < n; i++) {
            if (!(co[i] = za[i].toInt())) {
              flag = false;
              break;
            }
          }
          if (flag) {
            if (k.equals("li")) t.drawLine(co[0], co[1], co[2], co[3], TFT_YELLOW);
            else if (k.equals("el")) t.drawEllipse(co[0], co[1], co[2], co[3], TFT_MAGENTA);
            else if (k.equals("ra")) t.drawRect(co[0], co[1], co[2], co[3], TFT_RED);
            else { ; }
          }
          t.setCursor(0, 0);
          t.print(">" + dr);
        }
      }
      t.clear();
    }

    else {
      pri_both("?");
    }
  }
}

void pri_both(String s) {
  Serial.println(s);
  tString(s);
}

void tString(String s) {
  l_buffer.push_back(s);
  t.setCursor(0, CurY * cellY);
  t.print(s);
  ++by;
  ++CurY;
  ascr();
}

void ascr() {
  if (by >= ROWS) {
    t.clear();

    for (int y = 0; y < ROWS; y++) {
      t.setCursor(0, y * cellY);
      t.print(l_buffer[2 + by - ROWS + y + pastY]);  // add +pastY
    }
    CurY = ROWS - pastY;  // add - pastY
  }
}

int split(String data, char delimiter, String* dst, int num) {
  int index = 0;
  int len = data.length();
  for (int i = 0; i < len; i++) {
    char tmp = data.charAt(i);
    if (tmp == delimiter) {
      index++;
      if (index > num - 1) return -1;
    } else {
      dst[index] += tmp;
    }
  }
  return index + 1;
}

void printDirectory(File rt, int numTabs) {
  while (File file = rt.openNextFile()) {
    Serial.printf(" #%d=", ++pn);
    for (uint8_t i = 0; i < numTabs; i++) Serial.print('\t');

    if (file.isDirectory()) {
      Serial.println((String)file.name() + "/");
      tString(" #" + (String)pn + "=" + (String)file.name() + '/');
      printDirectory(file, numTabs + 1);
    } else {  // files have sizes, directories do not
      Serial.print(file.name());
      Serial.print("\t\t");
      Serial.println(file.size(), DEC);
      tString(" #" + (String)pn + "=" + (String)file.name() + " _ " + (String)file.size());
    }
    file.close();
  }
}

// I2S functions for playing wav file ---------------------------------------------------------------------------------------------------
uint16_t PlayWav() {
  static bool ReadingFile = true;                   // True if reading file from SD. false if filling I2S buffer
  static byte Samples[NUM_BYTES_TO_READ_FROM_FILE]; // Memory allocated to store the data read in from the wav file
                                                    // Num bytes actually read from the wav file which will either be
                                                    // NUM_BYTES_TO_READ_FROM_FILE or less than this if we are very
                                                    // near the end of the file. i.e. we can't read beyond the file.
  if (ReadingFile)  // Read next chunk of data in from file if needed
  {
    _BytesRead = ReadFile(Samples);  // Read data into our memory buffer, return num bytes read in
    if (_BytesRead == 0) return 0;
    ReadingFile = false;  // Switch to sending the buffer to the I2S
  } else
    ReadingFile = FillI2SBuffer(Samples, _BytesRead);  // We keep calling this routine until it returns true, at which point
                                                       // this will swap us back to Reading the next block of data from the file.
                                                       // Reading true means it has managed to push all the data to the I2S
                                                       // Handler, false means there still more to do and you should call this
                                                       // routine again and again until it returns true.
  return _BytesRead;
}

uint16_t ReadFile(byte* Samples) {
  static uint32_t BytesReadSoFar = 0;  // Number of bytes read from file so far
  uint16_t BytesToRead;                // Number of bytes to read from the file

  if (BytesReadSoFar + NUM_BYTES_TO_READ_FROM_FILE > WavHeader.DataSize)  // If next read will go past the end then adjust the
    BytesToRead = WavHeader.DataSize - BytesReadSoFar;                    // amount to read to whatever is remaining to read
  else
    BytesToRead = NUM_BYTES_TO_READ_FROM_FILE;  // Default to max to read

  WavFile.read(Samples, BytesToRead);  // Read in the bytes from the file
  BytesReadSoFar += BytesToRead;       // Update the total bytes red in so far

  if (BytesReadSoFar >= WavHeader.DataSize)  // Have we read in all the data?
  {
    // WavFile.seek(44); // Reset to start of wav data.(loop back)
    BytesReadSoFar = 0;  // Clear to no bytes read in so far
    BytesToRead = 0;
  }
  return BytesToRead;  // return the number of bytes read into buffer
}

bool FillI2SBuffer(byte* Samples, uint16_t BytesInBuffer) {
  // Writes bytes to buffer, returns true if all bytes sent else false, keeps track itself of how many left
  // to write, so just keep calling this routine until returns true to know they've all been written, then
  // you can re-fill the buffer

  size_t BytesWritten;            // Returned by the I2S write routine,
  static uint16_t BufferIdx = 0;  // Current pos of buffer to output next
  uint8_t* DataPtr;               // Point to next data to send to I2S
  uint16_t BytesToSend;           // Number of bytes to send to I2S

  // To make the code eaier to understand I'm using to variables to some calculations, normally I'd write this calcs
  // directly into the line of code where they belong, but this make it easier to understand what's happening

  DataPtr = Samples + BufferIdx;                               // Set address to next byte in buffer to send out
  BytesToSend = BytesInBuffer - BufferIdx;                     // This is amount to send (total less what we've already sent)
  i2s_write(i2s_num, DataPtr, BytesToSend, &BytesWritten, 1);  // Send the bytes, wait 1 RTOS tick to complete
  BufferIdx += BytesWritten;                                   // increasue by number of bytes actually written

  if (BufferIdx >= BytesInBuffer) {
    // sent out all bytes in buffer, reset and return true to indicate this
    BufferIdx = 0;
    return true;
  } else
    return false;  // Still more data to send to I2S so return false to indicate this
}

bool ValidWavData(WavHeader_Struct* Wav) {
  if (memcmp(Wav->RIFFSectionID, "RIFF", 4) != 0) {
    Serial.print("Invalid data - Not RIFF format");
    return false;
  }
  if (memcmp(Wav->RiffFormat, "WAVE", 4) != 0) {
    Serial.print("Invalid data - Not Wave file");
    return false;
  }
  if (memcmp(Wav->FormatSectionID, "fmt", 3) != 0) {
    Serial.print("Invalid data - No format section found");
    return false;
  }
  if (memcmp(Wav->DataSectionID, "data", 4) != 0) {
    Serial.print("Invalid data - data section not found");
    return false;
  }
  if (Wav->FormatID != 1) {
    Serial.print("Invalid data - format Id must be 1");
    return false;
  }
  if (Wav->FormatSize != 16) {
    Serial.print("Invalid data - format section size must be 16.");
    return false;
  }
  if ((Wav->NumChannels != 1) & (Wav->NumChannels != 2)) {
    Serial.print("Invalid data - only mono or stereo permitted.");
    return false;
  }
  if (Wav->SampleRate > 48000) {
    Serial.print("Invalid data - Sample rate cannot be greater than 48000");
    return false;
  }
  if ((Wav->BitsPerSample != 8) & (Wav->BitsPerSample != 16)) {
    Serial.print("Invalid data - Only 8 or 16 bits per sample permitted.");
    return false;
  }
  return true;
}

void DumpWAVHeader(WavHeader_Struct* Wav) {
  if (memcmp(Wav->RIFFSectionID, "RIFF", 4) != 0) {
    Serial.print("Not a RIFF format file - ");
    PrintData(Wav->RIFFSectionID, 4);
    return;
  }
  if (memcmp(Wav->RiffFormat, "WAVE", 4) != 0) {
    Serial.print("Not a WAVE file - ");
    PrintData(Wav->RiffFormat, 4);
    return;
  }
  if (memcmp(Wav->FormatSectionID, "fmt", 3) != 0) {
    Serial.print("fmt ID not present - ");
    PrintData(Wav->FormatSectionID, 3);
    return;
  }
  if (memcmp(Wav->DataSectionID, "data", 4) != 0) {
    Serial.print("data ID not present - ");
    PrintData(Wav->DataSectionID, 4);
    return;
  }
  // All looks good, dump the data
  Serial.print("Total size :");
  Serial.println(Wav->Size);
  Serial.print("Format section size :");
  Serial.println(Wav->FormatSize);
  Serial.print("Wave format :");
  Serial.println(Wav->FormatID);
  Serial.print("Channels :");
  Serial.println(Wav->NumChannels);
  Serial.print("Sample Rate :");
  Serial.println(Wav->SampleRate);
  Serial.print("Byte Rate :");
  Serial.println(Wav->ByteRate);
  Serial.print("Block Align :");
  Serial.println(Wav->BlockAlign);
  Serial.print("Bits Per Sample :");
  Serial.println(Wav->BitsPerSample);
  Serial.print("Data Size :");
  Serial.println(Wav->DataSize);
}

void PrintData(const char* Data, uint8_t NumBytes) {
  for (uint8_t i = 0; i < NumBytes; i++)
    Serial.print(Data[i]);
  Serial.println();
}
```
　長年の夢（ささやかな）が少しだけ叶いました。最後まで見ていただきありがとうございました。