0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

G◯ogleの二番煎じとか言わないの

この記事は CAMPHOR- Advent Calendar 2022 17日目の記事です

経緯

  • @artic_kuee , 自作キーボード沼に片足を突っ込む
  • キーボードを一つも組み立てていないのにスイッチを蒐め始める
  • スイッチの押し心地は気になる

というわけで、キーチェッカーを自作する、という目標でやってきました
ただ光るだけじゃつまらないから、どうせならモールスでもやるか、と。そういうわけです。
なお本人はモールスはからきしの模様
諸々の事情があってハード編はしばらくおあずけになるんですが、、
まあ、そこはまたのお楽しみに

使用ツール

ハード

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

ソフト

 モールス信号を処理する方式に関していくつか迷いましたが、適当にググったところモールスのjsonが見つかったので拝借することに
 トンツーから文字を検索したいので、適当にググって見つけた方法を使ってColabで適当に変換したものをそのまま文字列としてソースコードに貼りました。

arduinojson

 arduinojsonの使い方はだいたいここを参照


//  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はここに読み込みたい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とか、頼れるライブラリはいろいろあるんでしょうが、せっかく低レイヤーの民を僭称しているのでね
  ここなんかを参照しました。
  ここも非常にいいですね

//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

ケースを作るときに、電源スイッチを設けるのを忘れていたので
というかスイッチテスターなのに別途スイッチが要るってどうなのよ?
ってわけでやってみました。とても簡単にできて良かった。
ここなんかを見ました。
ここも大変良いですね

  //復帰条件の設定
  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
3Dプリンタの印刷までこぎつけたのですが…
17306953444998.jpg
いざ組み上げる段になって致命的なミスが発覚!
LEDの窓の位置が合わないというね…
さらに @artic_kuee 君がCAMPHOR-Makeの3Dプリンタを詰まらせたり…

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

ソースコード

大変汚くて恐縮です。質問歓迎。
ただ、qiitaはあまり巡回しないのでtwitterで聞いた方がはやい
(学生の皆さんは、ぜひCAMPHOR-HOUSEにも足を運んでくださいね!)
先述の通りjsonの部分は抜いています。 使いたい人は適宜埋めてね

#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() {}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?