1-Key モールス符号キーボードをM5StickCでつくってみた

**この記事は[M5Stack Advent Calendar 2022](https://qiita.com/advent-calendar/2022/m5stack)の17日目の記事です。**

# 作ろうと思ったきっかけ

もともとはMONOISTの「[モールス信号キーボード](https://monoist.itmedia.co.jp/mn/articles/2211/24/news020.html)」の記事を読んで、「M5StickCだとボタンも大きいしハード追加もしで作れるし、モールス符号の勉強もできそう」と思ったのがきっかけです。(ちなみに筆者は電信級は持っていません)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">これ、<a href="https://twitter.com/hashtag/M5StickC?src=hash&amp;ref_src=twsrc%5Etfw">#M5StickC</a> で無線キーボード化してみようかと。<a href="https://t.co/EEeEwYXQFY">https://t.co/EEeEwYXQFY</a></p>&mdash; tomorrow56@モモモフ済 (@tomorrow56) <a href="https://twitter.com/tomorrow56/status/1600127888128237569?ref_src=twsrc%5Etfw">December 6, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 作ったもの

BLEキーボードとして機能する１キーのモールス符号デコーダー(CW Decode Keyboard)です。

https://twitter.com/tomorrow56/status/1601419239419961344?s=20&t=23EnaGe1ihbPRIKfPWJQFQ

Bluetoothの接続状態は画面右上のマークで表示しています。ホストと接続する前は、マークが赤くなります。

![no-connect.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/bab3e84d-c04a-7a1c-94a6-0fd68e8a8239.jpeg)

ホストと接続すると、マークが緑になります。

![connected.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/a4139a21-8281-e244-25e5-4a32b5fba41d.jpeg)

ホスト(PC)からは"CW-Keyboard"という名前のBluetoothキーボードとして認識されます。

![Pairing.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120903/afb21ea3-dc71-81ae-8a01-97ab8fd136dd.png)

# サンプルコード

作成したコード(Arduino IDE用スケッチ)はgithubにアップしています。
https://github.com/tomorrow56/CW_BLEKeyboard

スケッチはM5StickCを使って動作させていますが、M5Unifiedライブラリを使用していますので、入力ピンの設定を変更すれば他のM5Stackシリーズでも動作するはずです。

## コードの説明

モールス信号をデコードする部分の基本のアルゴリズムは **MONOISTの記事に掲載されていたコード** を使わせていただきました。
プログラムの見通しを良くするためパラメータの変数化と一部コードの修正を行っています。

### BLEキーボードのインスタンス

M5Stack(ESP32)用のBluetoothライブラリは"NimBLE"を使用します。
インスタンスするときの引数で"deviceName","deviceManufacturer","batteryLevel"を設定します。

``` M5StickC_CW_BLEKeyboard.ino
// for BLE Keyboard
#include <BleKeyboard.h>
BleKeyboard bleKeyboard("CW-Keyboard", "M5Stack", 100);
```
### モールス信号デコード用の各種パラメータの設定

M5StickCのボタンが押された長さから短点(・)と長点(－)の判定をするための各種パラメータです。
inputPinはM5StickCの液晶と同じ面にあるボタンA(G37)を指定しています。

``` M5StickC_CW_BLEKeyboard.ino
// for Morse Decode
unsigned long signal_len,t1,t2,t0;  //time for which button is pressed
String code = "";                   //string in which one alphabet is stored
const int spacePeriod = 2000;       //Waiting period until a space is entered
const int debouncingPeriod = 20;    //switch debouncing
const int longpressLimit = 500;     //Limit of long Press time
boolean connectDotDash = true;     //Code connect flag
int inputPin = G37;                 //M5StickC BtnA for input
```

### setup関数

setup関数の最初に各種設定を行います。ボードタイプの判別は関数化(boardType())しています。

``` M5StickC_CW_BLEKeyboard.ino
void setup(void)
{
  auto cfg = M5.config();

#if defined ( ARDUINO )
  cfg.serial_baudrate = 115200;   // default=115200. if "Serial" is not needed, set it to 0.
#endif
  cfg.clear_display = true;  // default=true. clear the screen when begin.
  cfg.output_power  = false;  // default=true. use external port 5V output.
  cfg.internal_imu  = false;  // default=true. use internal IMU.
  cfg.internal_rtc  = false;  // default=true. use internal RTC.
  cfg.internal_spk  = false;  // default=true. use internal speaker.
  cfg.internal_mic  = false;  // default=true. use internal microphone.

  M5.begin(cfg);

  M5.Display.setBrightness(64);

  if (M5.Display.width() < M5.Display.height())
  { /// Landscape mode.
    M5.Display.setRotation(M5.Display.getRotation() ^ 1);
  }

  int textsize = M5.Display.height() / 160;
  if (textsize == 0)
  {
    textsize = 1;
  }
  M5.Display.setTextSize(textsize);

  // hardware model check
  char* boardName = boardType();
  M5.Display.startWrite();
  M5.Display.print("Core:");
  M5.Display.println(boardName);
  ESP_LOGI("setup", "core:%s", boardName);
  M5.Display.println("Morse Code Keyboard");
```

inputPinをプルアップ入力に設定し、BLEキーボードを開始します。

``` M5StickC_CW_BLEKeyboard.ino
  // Key setting internal pullup
  pinMode(inputPin, INPUT_PULLUP);

  // start BLE Keyboard
  Serial.println("Starting BLE work!");
  bleKeyboard.begin();
}
```

### loop関数

入力したモールスコードをデコードして画面に表示する位置とサイズを指定します。

``` M5StickC_CW_BLEKeyboard.ino
void loop(void)
{
  vTaskDelay(1);
  int h = M5.Display.height() / 8;
  int w = M5.Display.width() / 12;

//  M5.update();

  M5.Display.setCursor(w*5, h*3);
  M5.Display.setTextSize(3);
```

ホストに接続されたら入力判別処理に入り、マークを緑にしてボタンが押されるのを待ちます。ボタンが押されたことを検出したら、t1に現在の時間を代入します。押されていなかった期間が"spacePeriod"より長い場合はスペースを入力します。

``` M5StickC_CW_BLEKeyboard.ino
  if(bleKeyboard.isConnected())
  {
    M5.Display.fillRect(w*11, h/2, w, h, TFT_GREEN);
    while (connectDotDash == true)
    {
      t0 = millis();
      // waiting for button press
      while (digitalRead(inputPin) == HIGH)
      {
      }
      // Enter a space if no key is pressed for a certain period of time
      if (millis()-t0 > spacePeriod)
      {
        bleKeyboard.print(' ');
        Serial.print(' ');
      }
      t1 = millis();                            //time at button press
```

次にボタンが離されるのを待ちます。ボタンが離されたら押されていた期間を"signal_len"に代入し、チャタリング防止期間(debouncingPeriod)より長い場合は、readio()関数を呼び出して、押された期間によって短点・長点を判定して"code"変数に追加します。

"longpressLimit"の期間でループを回し、この間にキーが押されたら、入力判別処理のループを繰り返します。
キーが押されなかったら、文字区切りと判断しループを抜けます。

``` M5StickC_CW_BLEKeyboard.ino
      // waiting for button released
      while (digitalRead(inputPin) == LOW)
      {
      }
      t2 = millis();                            //time at button release
      signal_len = t2 - t1;                     //time for which button is pressed
      if (signal_len > debouncingPeriod)        //to account for switch debouncing
      {
        code += readio();                       //function to read dot or dash
      }
      //limit of long press
      //if time between button press greater than 0.5sec, skip loop and go to next alphabet
      while ((millis() - t2) < longpressLimit)
      {     
        if (digitalRead(inputPin) == LOW)
        {
          connectDotDash = true;
          break;
        }else
        {
          connectDotDash = false;
        }
      }
    }
```

入力判別処理のループを抜けたら"convertor()"関数で"code"変数をデコードし、結果を"keyin"変数に代入し、LCD表示とホストへの送信を行います。

``` M5StickC_CW_BLEKeyboard.ino
    char keyin = convertor();       //function to decipher code into alphabet
    Serial.print(keyin);
    M5.Display.print(keyin);
    bleKeyboard.print(keyin);
    connectDotDash = true;
  }
  else
  {
    M5.Display.fillRect(w*11, h/2, w, h, TFT_RED);
  }
}
```

### boardType()関数

M5Unifiedのボードタイプの判別部分を切り出した関数です。戻り値はボードタイプです。

``` M5StickC_CW_BLEKeyboard.ino
/**********
* hardware model check
**********/
char* boardType()
{
  char* name;
  switch (M5.getBoard())
  {
#if defined (CONFIG_IDF_TARGET_ESP32C3)
  case m5::board_t::board_M5StampC3:
    name = "StampC3";
    break;
  case m5::board_t::board_M5StampC3U:
    name = "StampC3U";
    break;
#else
  case m5::board_t::board_M5Stack:
    name = "Stack";
    break;
  case m5::board_t::board_M5StackCore2:
    name = "StackCore2";
    break;
  case m5::board_t::board_M5StickC:
    name = "StickC";
    break;
  case m5::board_t::board_M5StickCPlus:
    name = "StickCPlus";
    break;
  case m5::board_t::board_M5StackCoreInk:
    name = "CoreInk";
    break;
  case m5::board_t::board_M5Paper:
    name = "Paper";
    break;
  case m5::board_t::board_M5Tough:
    name = "Tough";
    break;
  case m5::board_t::board_M5Station:
    name = "Station";
    break;
  case m5::board_t::board_M5Atom:
    name = "ATOM";
    break;
  case m5::board_t::board_M5AtomPsram:
    name = "ATOM PSRAM";
    break;
  case m5::board_t::board_M5AtomU:
    name = "ATOM U";
    break;
  case m5::board_t::board_M5TimerCam:
    name = "TimerCamera";
    break;
  case m5::board_t::board_M5StampPico:
    name = "StampPico";
    break;
#endif
  default:
    name = "Who am I ?";
    break;
  }

  return name;
}
```

### char readio()関数

短点・長点を判定する関数です。MONOISTに掲載のコードをそのまま使わせてもらっています。

``` M5StickC_CW_BLEKeyboard.ino
char readio()
{
  if (signal_len < 200 && signal_len > 20)
  {
    return '.';                        //if button press less than 0.2sec, it is a dot
  }
  else //if (signal_len >= 300)
  {
    return '-';                        //if button press more than 0.2sec, it is a dash
  }
}
```

### convertor()関数

モールス符号を英数字・記号に変換する関数です。MONOISTに掲載のコード（基本の英数字)をベースに英文モールス符号で規定されている記号を一通り追加しました。ただし、“ダイアクリティカルマーク”付きアルファベットは対応していません。
ここで定義されていないモールス符号が入力された場合は、ホストに改行コードを送信します。

``` M5StickC_CW_BLEKeyboard.ino
char convertor()
{
  static String letters[] = {
    ".-",   // A (1)
    "-...", // B
    "-.-.", // C
    "-..",  // D
    ".",    // E
    "..-.", // F
    "--.",  // G
    "....", // H
    "..",   // I
    ".---", // J
    "-.-",  // K
    ".-..", // L
    "--",   // M
    "-.",   // N
    "---",  // O
    ".--.", // P
    "--.-", // Q
    ".-.",  // R
    "...",  // S
    "-",    // T
    "..-",  // U
    "...-", // V
    ".--",  // W
    "-..-", // X
    "-.--", // Y
    "--..", // Z (26)
    "-----",  // 0
    ".----",  // 1
    "..---",  // 2
    "...--",  // 3
    "....-",  // 4
    ".....",  // 5
    "-....",  // 6
    "--...",  // 7
    "---..",  // 8
    "----.",  // 9 (36)
    "E"       // end code
  };

  int i = 0; 
  char c;
  if (code == ".-.-.-")
  {
    c = '.';        //period
  }
  else if (code == "--..--")
  {
    c = ',';        //comma
  }
  else if (code == "..--..")
  {
    c = '?';        //question
  }
  else if (code == ".----.")
  {
    c = 0x27;        //Apostrophe (')
  }
  else if (code == "-.-.--")
  {
    c = '!';        //Exclamation
  }
  else if (code == "-..-.")
  {
    c = '/';        //slash
  }
  else if (code == "-.--.")
  {
    c = '(';        //Open parenthesis
  }
  else if (code == "-.--.-")
  {
    c = ')';        //Close parenthesis
  }
    else if (code == ".-...")
  {
    c = '&';        //ampersand
  }
  else if (code == "---...")
  {
    c = ':';        //colon
  }
  else if (code == "-.-.-.")
  {
    c = ';';        //semicolon ';'
  }
  else if (code == "-...-")
  {
    c = '=';        //double dash
  }
  else if (code == ".-.-.")
  {
    c = '+';        //plus
  }
  else if (code == "-....-")
  {
    c = '-';        //hyphen
  }
  else if (code == "..--.-")
  {
    c = '_';        //underscore
  }
  else if (code == ".-..-.")
  {
    c = '?';        //Question
  }
  else if (code == "...-..-")
  {
    c = '$';        //Dollar
  }
  else if (code == ".--.-.")
  {
    c = '@';        //at mark
  }
  else if (code == "...-.-")
  {
    c = 0x0a;  //end of work, send "LF"
  }
  else
  {
    while (letters[i] != "E")  //loop for comparing input code with letters array
    {
      if (letters[i] == code)
      {
        if (i>25)
          c = '0'+(i-26);
        else
          c = 'A'+i;
        break;
      }
      i++;
    }
    if (letters[i] == "E")
    {
      c = 0x0a;  //if input code doesn't match any letter, send "LF"
//      Serial.println("");  //if input code doesn't match any letter, error
    }
  }
  code = "";                            //reset code to blank string
  return c;
}
```

# 参考にしたサイト

以下のサイトの情報を参考にさせていただきました。どうもありがとうございます。

## M5Unified

https://github.com/m5stack/M5Unified

## CW Decorder

https://monoist.itmedia.co.jp/mn/articles/2211/24/news020_2.html

https://github.com/imaoca/OneKey

## Morse Code

https://en.wikipedia.org/wiki/Morse_code

## NimBLE Keyboard

https://github.com/wakwak-koba/ESP32-NimBLE-Keyboard