めざせブラインドタッチ！M5Stack Cardputerでタイピング練習ソフトを作る

# M5Stack Cardputer
数量限定の激戦に勝利し、M5Stack Cardputerを購入できました。

https://www.switch-science.com/products/9277

https://x.com/siroitori0413/status/1727607801256857756?s=20

この小さいコンピュータはM5Stamp S3で動作しています。
キーがちっこくてとても可愛いのですが、その反面押しにくいです。
このキーボードに慣れるためにタイピング練習ソフトを作ろうと思いました。めざせブラインドタッチ！

# M5Stack Cardputerのサンプルコードを見てみる

M5Stack Cardputerの公式サイト↓を見てみますと、いろいろなサンプルプログラムが公開されています。

https://docs.m5stack.com/en/core/Cardputer

サイト抜粋
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308578/79254976-a21b-eb19-78a7-e65e7f3e18f4.png)


このうちkeyboardのプログラムのリンクが下記。

https://github.com/m5stack/M5Cardputer/tree/master/examples/Basic/keyboard

この中の inputText のプログラムを改造してタイピング練習ソフトにリメイクしてみました。

# 最初に出来上がったタイピング練習ソフト
ほとんど inputText のプログラムで、ちょっとだけ手を加えた形で実装しました。
```c++
#include "M5Cardputer.h"
#include "M5GFX.h"

#define mC 261.626 // ド
#define mD 293.665 // レ
#define mE 329.628 // ミ
#define mF 349.228 // ファ
#define mG 391.995 // ソ
#define mA 440.000 // ラ 
#define mB 493.883 // シ

M5Canvas canvas(&M5Cardputer.Display);
String data = "> ";

String target;
int targetIndex;

// 問題部分
const char *words[] = {
    "test",
    "m5stack",
    "hello",
    "good",
    "world",
};

void setup() {
    Serial.begin(115200);

    auto cfg = M5.config();
    M5Cardputer.begin(cfg, true);
    M5Cardputer.Display.setRotation(1);
    M5Cardputer.Display.setTextSize(0.5);
    // 枠の大きさを調整
    // M5Cardputer.Display.drawRect(0, 0, M5Cardputer.Display.width(),
    //                              M5Cardputer.Display.height() - 28, GREEN);
    M5Cardputer.Display.drawRect(0, 28, M5Cardputer.Display.width(),
                                 M5Cardputer.Display.height() - 28 - 28, GREEN);
    M5Cardputer.Display.setTextFont(&fonts::FreeSerifBoldItalic18pt7b);

    M5Cardputer.Display.fillRect(0, M5Cardputer.Display.height() - 4,
                                 M5Cardputer.Display.width(), 4, GREEN);

    // 問題表示部分を追加
    targetIndex = 0;
    target = words[targetIndex];
    M5Cardputer.Display.drawString("                   ", 4, 4);
    M5Cardputer.Display.drawString(target, 4, 4);

    // 入力内容の確定後表示部canvas
    canvas.setTextFont(&fonts::FreeSerifBoldItalic18pt7b);
    canvas.setTextSize(0.5);
    // canvas.createSprite(M5Cardputer.Display.width() - 8,
    //                     M5Cardputer.Display.height() - 36);
    canvas.createSprite(M5Cardputer.Display.width() - 8,
                        M5Cardputer.Display.height() - 36 - 36 );
    canvas.setTextScroll(true);
    // canvas.println("Press Key and Enter to Input Text");
    // target = words[0];
    // canvas.println(target); //ここに表示はしない

    // canvasの描画位置設定
    //canvas.pushSprite(4, 4);
    canvas.pushSprite(4, 4 + 28);


    M5Cardputer.Display.drawString(data, 4, M5Cardputer.Display.height() - 24);
}

void setNextTarget(){
  targetIndex++;
  int wordsLength = sizeof(words) / sizeof(words[0]);
  if (targetIndex >= wordsLength){
    targetIndex = 0;
  }
  target = words[targetIndex];
  // 問題表示部分の初期化
  M5Cardputer.Display.drawString("                   ", 4, 4);
  M5Cardputer.Display.drawString(target, 4, 4);
}

void loop() {
    M5Cardputer.update();
    if (M5Cardputer.Keyboard.isChange()) {
        if (M5Cardputer.Keyboard.isPressed()) {
            // 入力文字を取得
            Keyboard_Class::KeysState status = M5Cardputer.Keyboard.keysState();

            for (auto i : status.word) {
                data += i;
            }

            if (status.del) {
                // 削除だったとき
                data.remove(data.length() - 1);
            }

            if (status.enter) {
                // Enterだったとき

                // 最初の"> "を削除して
                // startIndex, count
                data.remove(0, 2);

                Serial.println("target : " + target);
                Serial.println("data1 : " + data);
                Serial.println("data2 : " + data.substring(0, data.length()-1));
                if (target == data){
                  // 入力OK
                  canvas.setTextColor(WHITE);
                  // M5Cardputer.Speaker.tone(10000, 1000);
                  M5Cardputer.Speaker.tone(mE, 100);
                  delay(100);
                  M5Cardputer.Speaker.tone(mG, 100);
                  delay(100);
                  M5Cardputer.Speaker.tone(mB, 100);

                  setNextTarget();
                }else{
                  // 入力NG
                  canvas.setTextColor(RED);
                  // M5Cardputer.Speaker.tone(4000, 20);
                  M5Cardputer.Speaker.tone(mF, 500);
                  M5Cardputer.Speaker.tone(mC, 500);
                }

                // 以降の文字列をcanvasに出力
                canvas.println(data);
                // canvasを画面に出力
                // canvas.pushSprite(4, 4);
                canvas.pushSprite(4, 4 + 28);
                // dataのリセット
                data = "> ";
            }

            // X, Y, Width, Height
            M5Cardputer.Display.fillRect(0, M5Cardputer.Display.height() - 28,
                                         M5Cardputer.Display.width(), 25,
                                         BLACK);

            // data, X, Y
            M5Cardputer.Display.drawString(data, 4,
                                           M5Cardputer.Display.height() - 24);
        }
    }
}
```

https://x.com/siroitori0413/status/1731207786649059515

動作は上記の動画をみていただきたいのですが、一番上の行に書かれた文字のとおりにタイプします。
そうすると一番下の行に入力した文字が出てきて、確定するときはEnterを押します。
確定された文字は中央の枠の中に表示され、入力が間違っていた場合は場合赤文字表示・エラー音（ファド）になります。入力があっていた場合は白文字でOKの音（ミソシ）が鳴ります。
またOKだった場合は一番上の入力すべき文字が切り替わります。

問題になる文字は、下記が順々に出てくるようになっています。
```c++
// 問題部分
const char *words[] = {
    "test",
    "m5stack",
    "hello",
    "good",
    "world",
};
```

これはSDカードとかどこかから読み込むようにするといいかもですね。

ここまでは私が作った作品なのですが、この続きを思い付きでChatGPTに書かせてみましたらなかなか良かったのでご紹介します。

# ここから拡張編 ※この章は文章もプログラムもすべてChatGPTによる生成です
### タイピング練習ソフトの改造と機能拡張
#### レベルの追加
最初に表示される単語は比較的簡単なものですが、次第に難易度を上げていくと面白いでしょう。例えば、単語の長さや使用される文字の種類を変更して、徐々に難しくしていくことが考えられます。

```c++
const char *easyWords[] = {
    "test",
    "m5stack",
    "hello",
    "good",
    "world",
};

const char *mediumWords[] = {
    "programming",
    "challenge",
    "keyboard",
    "learning",
    "development",
};

const char *hardWords[] = {
    "concatenation",
    "algorithm",
    "efficiency",
    "optimization",
    "recursion",
};
```
そして、setNextTarget 関数を以下のように変更します。

```c++
void setNextTarget() {
    targetIndex++;
    int wordsLength;
    const char **selectedWords;

    if (targetIndex % 3 == 0) {
        wordsLength = sizeof(easyWords) / sizeof(easyWords[0]);
        selectedWords = easyWords;
    } else if (targetIndex % 3 == 1) {
        wordsLength = sizeof(mediumWords) / sizeof(mediumWords[0]);
        selectedWords = mediumWords;
    } else {
        wordsLength = sizeof(hardWords) / sizeof(hardWords[0]);
        selectedWords = hardWords;
    }

    if (targetIndex >= wordsLength) {
        targetIndex = 0;
    }

    target = selectedWords[targetIndex];

    // 問題表示部分の初期化
    M5Cardputer.Display.drawString("                   ", 4, 4);
    M5Cardputer.Display.drawString(target, 4, 4);
}
```
#### スコアの追加
ユーザーが正確にタイピングできた単語数や時間を計測して、スコアを表示する機能を追加しましょう。

```c++
int correctWords = 0;
int totalWordsTyped = 0;
unsigned long startTime;

void setup() {
    // ... その他の初期化コード ...

    startTime = millis();
}

void setNextTarget() {
    // ... (前述の変更) ...

    // 正確にタイピングできた単語数を更新
    correctWords++;

    // 問題表示部分の初期化
    M5Cardputer.Display.drawString("                   ", 4, 4);
    M5Cardputer.Display.drawString(target, 4, 4);

    // タイマーをリセット
    startTime = millis();
}

void loop() {
    // ... (前述の変更) ...

    if (status.enter) {
        // ... (前述の変更) ...

        // スコア表示
        unsigned long endTime = millis();
        unsigned long elapsedTime = endTime - startTime;
        float typingSpeed = (float)correctWords / ((float)elapsedTime / 60000.0);  // 単語数を1分あたりに変換

        M5Cardputer.Display.drawString("Score: " + String(correctWords) + " / " + String(totalWordsTyped) +
                                           " Speed: " + String(typingSpeed, 2) + " wpm",
                                       4, M5Cardputer.Display.height() - 24);

        // 以降の文字列をcanvasに出力
        canvas.println(data);

        // スコア関連の変数をリセット
        correctWords = 0;
        totalWordsTyped = 0;
        startTime = millis();
    }

    // ... (前述の変更) ...
}
```
これらの改造と機能拡張により、より面白くて挑戦的なタイピング練習ソフトを作成することができます。ユーザーがどれだけ正確かつ迅速にタイピングできるかを競う要素が加わり、モチベーションも向上するでしょう。
（ChatGPT原文ママ）

# ChatGPTによる拡張の振り返り
ChatGPTの拡張の1つ目はコードを見たところ初級→中級→上級と１問ずつレベルが上がっています。上級のあとは初級に戻ってきますね。

2つ目のコードはスコア表示。ぱっと見よさそうです。ほうほう。

ということで実際に動作するか言われるがまま組み込んでみました。
すると**おおむねエラーになることなくコンパイル成功できました**。すごいです。
動作もするのですがスコア表示は何も表示されず、**内容の間違いや不足がありました**。

* setup 関数内でも、target = words[targetIndex] があるので easyWords への変更が必要。（そもそもこの部分は setNextTarget 関数の中の処理と重複するのでこれは私の最初に書いたコードがあまりよくなく共通化すべきでした）
* totalWordsTyped がカウントアップされていない
* スコア表示しているM5Cardputer.Display.drawString では表示できない（→canvas.printlnにすることで表示出力は可能）
* 上記で表示できたところで計算式がおかしい？？

このあたりをうまく調整して組み込むことで割と簡単に実現できそうです。
ここがうまくいかないと具体的に指示してChatGPTに再度考えてもらってもいいですね。

# まとめ
はじめてArduinoのコードをChatGPTに考えてもらいましたが、なかなか使える感じでした。

今回M5Stack Cardputerでタイピング練習ソフトを作りました。
これでブラインドタッチが実現・・できませんでした。いえ、最初からそう思っていましたけど。
キーが小さいので速打はなかなか難しいですね。

あとディスプレイが小さく文字が小さくいため英語の小文字がつぶれてしまってすごく読みにくいです。
もう少しフォントサイズを大きくするなり工夫が必要です。

M5Stack CardputerはほかにもSDカードが使えたりマイクもあったり一通りの機能が備わっているので何かしら使っていきたいなと思っています。