M5atomでモールス式キーボードを作った話【ソフト編】

<a href = "https://www.youtube.com/watch?v=u5AYb778pwA">G◯ogle</a>の二番煎じとか言わないの

この記事は<a href = "https://advent.camph.net/"> CAMPHOR- Advent Calendar 2022 </a> 17日目の記事です
## 経緯
- @artic_kuee , 自作キーボード沼に片足を突っ込む
- キーボードを一つも組み立てていないのにスイッチを蒐め始める
- スイッチの押し心地は気になる
<img width=10%><img width=80% src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491138/2d791a97-476c-c962-cc96-c0a9519caecd.jpeg">

というわけで、キーチェッカーを自作する、という目標でやってきました
ただ光るだけじゃつまらないから、どうせならモールスでもやるか、と。そういうわけです。

なお本人はモールスはからきしの模様

諸々の事情があってハード編はしばらくおあずけになるんですが、、
まあ、そこはまたのお楽しみに

## 使用ツール
#### ハード
　　開発はM5atom-lite, 本番ではM5stampを使用(小型化のため)
　　積んでるチップは両者同じなので、設定を特にいじる必要はそんなになかった(ハズ)

#### ソフト
- <a href = "https://github.com/bblanchon/ArduinoJson">arduinojson
- <a href = "https://github.com/bblanchon/ArduinoJson">ESP32 BLE Keyboard library
- <a href = "https://github.com/FastLED/FastLED">FastLED
- esp32のタイマー割り込み
- esp32のdeep sleep

　モールス信号を処理する方式に関していくつか迷いましたが、適当にググったところモールスのjsonが見つかったので拝借することに
　トンツーから文字を検索したいので、<a href="https://www.haya-programming.com/entry/2018/01/29/183048">適当にググって見つけた方法</a>を使ってColabで適当に変換したものをそのまま文字列としてソースコードに貼りました。

## arduinojson

　arduinojsonの使い方はだいたい<a href="https://rikoubou.hatenablog.com/entry/2021/04/16/162258">ここ</a>を参照
```Cpp

//  String json = "調達したjsonの文字列をペースト"
  StaticJsonDocument<1536> doc;
  DeserializationError error = deserializeJson(doc, json);

  if (error) {
    Serial.print("deserializeJson() failed: ");
    Serial.println(error.c_str());
    return;
  }
//参照のしかた

  const char* read = doc["---"]; //"o"

```
簡単に叩けてありがたいですね
ちなみにjsonの部分は抜いています。　
著作権云々が怖かったのと、むやみに行が長くなって見づらくなるのでね。
なお、docの定義にある謎の数字1536は<a href="https://arduinojson.org/v6/assistant/#/step1">ここ</a>に読み込みたいjsonを食わせると得られます。バッファのサイズですね。
このページにおける設定は順に ESP32, Deserialize, Stringとしました。

#### Tips
- 文字列としてソースコードに組み込むので、クオーテーションマークのエスケープ(\\")をお忘れなく
- スコープを抜けると参照できなくなる(うまいやり方は知らん)ので、お気をつけて
　私がやった方法としては以下がある
　　- loop関数で毎回”読み込みが必要か”を確かめて必要なら読み込む
　　- setup関数内に無限ループを打ち込む(最後のソースコードはこの方法)

## ESP32 BLE Keyboard library
　こちらも非常に便利。
　まあ適当に調べたら日本語でも資料があるし、example読めば大体わかります。
　bleKeyboard.print()　で文章は打ち込めるし、BSやEnter,ShiftはbleKeyboard.write(KEY_***)という形式で送れます。

## タイマー割り込み
　長押しの判定のためですね。
　最初はM5atom.hを使っていました。
　ボタンに対してisPressed,isReleasedのほかにpressedFor,releasedForみたいな便利な関数が多かったんですね

　ただ、外部のボタンに対して基本的に適応できるものじゃないんですよ。
　適当にマクロ宣言したらいけるかな、とライブラリのソースとか見たんですが、だめでした。
　LavyanGFXとかと依存関係があるみたいで、追うのは
面倒
困難でした。
　らびやん大先生強過ぎません？

　というわけで、タイマーを叩くことにしました。
　Tickerとか、頼れるライブラリはいろいろあるんでしょうが、せっかく低ﾚｲﾔｰの民を僭称しているのでね
　  <a href="http://marchan.e5.valueserver.jp/cabin/comp/jbox/arc202/doc21105.html">ここ</a>なんかを参照しました。
　  <a href="https://lang-ship.com/blog/work/esp32-timer/">ここ</a>も非常にいいですね

```Cpp
//For timer interruption
volatile int PressStatus;
hw_timer_t *timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// detecting dash
void IRAM_ATTR onTimer(){
  portENTER_CRITICAL_ISR(&timerMux);
  PressStatus++;
  portEXIT_CRITICAL_ISR(&timerMux);
}
//中略
int setup(){
//中略
//タイマーの登録など
  timer = timerBegin(0,80,true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 1000, true);
  timerAlarmEnable(timer);

//タイマーのリセット
  portENTER_CRITICAL_ISR(&timerMux);
  PressStatus = 0;
  portEXIT_CRITICAL_ISR(&timerMux);
  timerWrite(timer, 0); 
  
//変数の読み込み
  portENTER_CRITICAL_ISR(&timerMux);
  status = PressStatus;
  portEXIT_CRITICAL_ISR(&timerMux);
  Serial.println(status);
//使用例
  if(status > sec_turnoff) {
```

変数名はすごく適当です　すまん

さて、割り込みハンドラを登録して、メインタスクと共有する変数のためにミューテックスを使っています。
1msごとにPressStatusがインクリメントされるだけです。
この程度の内容ならタイマーの発火時間を長くするだけで実装できるでしょう。

やりたかっただけです。ええ。

ちなみに、トンツーの時間を調整したい人はTやtdashなどをいじってください。

なお、エンター、スペース、BSは流石に機能としてほしいのでオレオレ実装してます。


## deep sleep
  ケースを作るときに、電源スイッチを設けるのを忘れていたので

というかスイッチテスターなのに別途スイッチが要るってどうなのよ?

ってわけでやってみました。とても簡単にできて良かった。
<a href = "https://hatakekara.com/esp32-deep-sleep-wake-up-sources/">ここ</a>なんかを見ました。
<a href = "https://lang-ship.com/blog/work/esp32-deep-sleep/">ここ</a>も大変良いですね
```Cpp
  //復帰条件の設定
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_39,0);
  //中略
  //deep sleep
  leds[0] = CRGB(0,0,0);
  FastLED.show();
  esp_deep_sleep_start();
```
復帰条件はタイマーとかGPIOとかいろいろ設定できるみたいですね。便利
注意点としては、LEDとかは勝手に消えません。手動で消しましょう。
ちなみに、今回のソースコードでは10秒で寝る設定になっているので、適宜sec_turnoffをいじってください

## 成果
実際に動いているのはこんな感じ
https://twitter.com/artic_kuee/status/1603813522734952448?s=20&t=eRNxpdJHO-51uOzpGtIFWQ
いいですね(小並感)
なお、キーテスタとしてのハードは結構頑張って設計していて
![Screenshot from 2022-12-17 01-38-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491138/a8f26f1e-1c4d-006c-9f44-38a7569e8252.png)
3Dプリンタの印刷までこぎつけたのですが…
![17306953444998.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2491138/4704481d-9725-2d05-a6c6-e9d89cab01c2.jpeg)
いざ組み上げる段になって致命的なミスが発覚！
LEDの窓の位置が合わないというね…

さらに @artic_kuee 君がCAMPHOR-Makeの3Dプリンタを詰まらせたり…


と、いうわけで、FreeCADで治安の悪いモデルを作った話や3Dプリンタを使ってみた所感など諸々をまとめて、次回ハード編をお送りしたいと思います！
乞うご期待っ！



## ソースコード
大変汚くて恐縮です。質問歓迎。
ただ、qiitaはあまり巡回しないのでtwitterで聞いた方がはやい
(学生の皆さんは、ぜひ<a href = "https://camph.net">CAMPHOR-HOUSE</a>にも足を運んでくださいね！)
先述の通りjsonの部分は抜いています。　使いたい人は適宜埋めてね

```Cpp
#include "Arduino.h"
#include <ArduinoJson.h>
#include <BleKeyboard.h>
#include <FastLED.h>
#include "esp_system.h"

BleKeyboard bleKeyboard;

//For timer interruption
volatile int PressStatus;
hw_timer_t *timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// detecting dash
void IRAM_ATTR onTimer(){
  portENTER_CRITICAL_ISR(&timerMux);
  PressStatus++;
  portEXIT_CRITICAL_ISR(&timerMux);
}


bool sign;

// time unit(ms)
int T = 200;

int tdash = 2 * T;
int tfunc = 4 * T;
int tblank = 4 * T;

int sec_turnoff = 10 * 1000;

CRGB dispColor(uint8_t r, uint8_t g, uint8_t b) {
  return (CRGB)((b << 16) | (r << 8) | g);
}

#define DATA_PIN 27
#define NUM_LEDS 1
CRGB leds[NUM_LEDS];

void setup() {

  bleKeyboard.begin();
  FastLED.addLeds<SK6812, DATA_PIN, RGB>(leds, NUM_LEDS);
  leds[0] = CRGB(128,128,128);
  FastLED.show();
  pinMode(GPIO_NUM_39, INPUT);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_39,0);
  

  Serial.begin(115200);
  delay(500);
  Serial.print("Pico Start\n");
  timer = timerBegin(0,80,true);
  timerAttachInterrupt(timer, &onTimer, true);
  timerAlarmWrite(timer, 1000, true);
  timerAlarmEnable(timer);
  
  int count = 0;
  char m[7];
  bool fin = false;
  bool func = false;
  bool shift = false;

  int status;

//  String json = "調達したjsonの文字列をペースト"
  StaticJsonDocument<1536> doc;
  DeserializationError error = deserializeJson(doc, json);

  if (error) {
    Serial.print("deserializeJson() failed: ");
    Serial.println(error.c_str());
    return;
  }

  //setupのスコープを抜けるとjsonの中身が読めなくなるためsetup関数内でループ
  while(1){
      portENTER_CRITICAL_ISR(&timerMux);
      PressStatus = 0;
      portEXIT_CRITICAL_ISR(&timerMux);

    //接続していない場合の挙動
    while (!bleKeyboard.isConnected()) {
      Serial.println("not connected");
      delay(100);
      if(!digitalRead(GPIO_NUM_39)){
        leds[0] = CRGB(128,128,128);
        portENTER_CRITICAL_ISR(&timerMux);
        PressStatus = 0;
        portEXIT_CRITICAL_ISR(&timerMux);
        timerWrite(timer,0);
      } else {
        leds[0] = CRGB(128,0,0);
      }
      FastLED.show();
    
      
      //sec_sleep秒後にdeep sleep
      portENTER_CRITICAL_ISR(&timerMux);
      status = PressStatus;
      portEXIT_CRITICAL_ISR(&timerMux);
      Serial.println(status);
      if(status > sec_turnoff) {
        Serial.println("deep sleep");
        leds[0] = CRGB(0,0,0);
        FastLED.show();
        esp_deep_sleep_start();
      }
    }
    count = 0;
    fin = false;
    func = false;
    shift = false;

    portENTER_CRITICAL_ISR(&timerMux);
    PressStatus = 0;
    portEXIT_CRITICAL_ISR(&timerMux);
    timerWrite(timer,0);
    leds[0] = CRGB(0,128,128);
    FastLED.show();    
    //ボタン押下待ち
    while (digitalRead(GPIO_NUM_39)){
      delay(10);
      //sec_sleep秒後にdeep sleep
      portENTER_CRITICAL_ISR(&timerMux);
      status = PressStatus;
      portEXIT_CRITICAL_ISR(&timerMux);
      if(status > sec_turnoff){
        Serial.println("deep sleep");
        leds[0] = CRGB(0,0,0);
        FastLED.show();
        esp_deep_sleep_start();
      }
    };
    //ボタンが押された状態からスタート
    while (!fin) {
      portENTER_CRITICAL_ISR(&timerMux);
      PressStatus = 0;
      portEXIT_CRITICAL_ISR(&timerMux);
      timerWrite(timer, 0); //ここでタイマーをリセット
      sign = false;
      leds[0] = CRGB(0,255,0);
      FastLED.show();
      do {
        delay(10);
        portENTER_CRITICAL_ISR(&timerMux);
        status = PressStatus;
        portEXIT_CRITICAL_ISR(&timerMux);
        //T以上経過すれば長音扱い
        if (status > T) {
          sign = true;
          leds[0] = CRGB(0,0,255);
          FastLED.show();
        }
        //2*T以上経過すれば特殊キーモードへ
        if (status > tfunc) {
          func = true;
          leds[0] = CRGB(128,0,128);
          FastLED.show();
        }
      } while (!digitalRead(GPIO_NUM_39));
      portENTER_CRITICAL_ISR(&timerMux);
      PressStatus = 0;
      portEXIT_CRITICAL_ISR(&timerMux);
      timerWrite(timer, 0); //ここでタイマーをリセット
      leds[0] = CRGB(128,128,128);
      FastLED.show();
      if (func) break;
      if (sign) {
        m[count] = '-';
      } else {
        m[count] = '.';
      }
      count++;

      //押されない状態で3*T以上経過すれば文字の切れ目
      do {
        delay(10);
        portENTER_CRITICAL_ISR(&timerMux);
        status = PressStatus;
        portEXIT_CRITICAL_ISR(&timerMux);
        if (status > tblank || count >= 6) {
          fin = true;
          break;
        }
      } while (digitalRead(GPIO_NUM_39));
    }
    //特殊キー用のモード
    if (func) {
      func = false;
      Serial.println("func mode");
      do {
        delay(10);
        portENTER_CRITICAL_ISR(&timerMux);
        status = PressStatus;
        portEXIT_CRITICAL_ISR(&timerMux);
        if (status > tblank) {
        leds[0] = CRGB(128,128,128);
        FastLED.show();
          fin = true;
          break;
        };
      } while (digitalRead(GPIO_NUM_39));
      portENTER_CRITICAL_ISR(&timerMux);
      PressStatus = 0;
      portEXIT_CRITICAL_ISR(&timerMux);
      timerWrite(timer,0);
      if (fin) {
        fin = false;
      } else {
        leds[0] = CRGB(0,255,0);
        FastLED.show();
        sign = false;
        do {
          delay(10);
          portENTER_CRITICAL_ISR(&timerMux);
          status = PressStatus;
          portEXIT_CRITICAL_ISR(&timerMux);
          if (status > T) {
            sign = true;
        leds[0] = CRGB(0,0,255);
        FastLED.show();
          }
        } while (!digitalRead(GPIO_NUM_39));
        portENTER_CRITICAL_ISR(&timerMux);
        PressStatus = 0;
        portEXIT_CRITICAL_ISR(&timerMux);
        timerWrite(timer,0);
        //スペースキー
        if (sign) {
          sign = false;
          bleKeyboard.print(" ");
          Serial.println("space");
          while (true) {
            do {
              delay(10);
              portENTER_CRITICAL_ISR(&timerMux);
              status = PressStatus;
              portEXIT_CRITICAL_ISR(&timerMux);
              if (status > 2*T) {
                fin = true;
              }
            } while (digitalRead(GPIO_NUM_39));
            if (fin) {
              break;
            }
            while (!digitalRead(GPIO_NUM_39)){};
            bleKeyboard.print(" ");
          }
          leds[0] = CRGB(128,128,128);
          FastLED.show();
          fin = false;
        } else {
          //短音1回はエンター
          do {
            delay(10);
            portENTER_CRITICAL_ISR(&timerMux);
            status = PressStatus;
            portEXIT_CRITICAL_ISR(&timerMux);
            if (status > tblank) {
              bleKeyboard.write(KEY_RETURN);
              Serial.println("enter");
              delay(50);
              bleKeyboard.releaseAll();
              shift = false;
              leds[0] = CRGB(128,128,128);
              FastLED.show();
              fin = true;
              break;
            };
          } while (digitalRead(GPIO_NUM_39));
          if (fin) {
            fin = false;
          } else {
            sign = false;
          leds[0] = CRGB(0,255,0);
          FastLED.show();
            do {
              delay(10);
              portENTER_CRITICAL_ISR(&timerMux);
              status = PressStatus;
              portEXIT_CRITICAL_ISR(&timerMux);
              if (status > T) {
                sign = true;
                leds[0] = CRGB(0,0,255);
                FastLED.show();
              }
            } while (!digitalRead(GPIO_NUM_39));
            portENTER_CRITICAL_ISR(&timerMux);
            PressStatus = 0;
            portEXIT_CRITICAL_ISR(&timerMux);
            timerWrite(timer,0);
            if (sign) {
              shift = !shift;
              if (shift) {
                bleKeyboard.press(KEY_LEFT_SHIFT);
              } else {
                bleKeyboard.releaseAll();
              }
              Serial.println("shift");
            } else {
              bleKeyboard.press(KEY_BACKSPACE);
              Serial.println("back space");
              delay(50);
              bleKeyboard.releaseAll();
              shift = false;
              while (true) {
                do {
                  delay(10);
                  portENTER_CRITICAL_ISR(&timerMux);
                  status = PressStatus;
                  portEXIT_CRITICAL_ISR(&timerMux);
                  if (status > tblank) {
                    fin = true;
                    break;
                  }
                } while (digitalRead(GPIO_NUM_39));
                if (fin) {
                  break;
                }while (!digitalRead(GPIO_NUM_39)){};
                bleKeyboard.press(KEY_BACKSPACE);
                delay(50);
                bleKeyboard.releaseAll();
                shift = false;
              }
              fin = false;
            }
          leds[0] = CRGB(128,128,128);
          FastLED.show();
          }
        }
      }
    } else {

      m[count] = '\0';
      Serial.println(m);
      const char* send = doc[m];
      if (send) {
        Serial.println(send);
        bleKeyboard.print(send);
      } else Serial.println("invalid morse code");
    }
  }
}

void loop() {}

```