雑食エンジニアの気まぐれレシピ

日ごろ身に着けた技術や見知った知識などの備忘録的なまとめ.主にRaspberry Piやマイコンを使った電子工作について綴っていく予定.機械学習についても書けるといいな.

自作キーボード(Iris)のキーマップを練り上げる

最近ずっと3Dプリンタ関連の記事を書いていましたが,実は平行して自作キーボードというものを始めてみました.
これがある程度落ち着いたので一度まとめておきたいと思います.
作ったキーボード(Iris)はこんな子です.

f:id:shikky_lab:20200412073159p:plain
自作キーボード[Iris]
(角度調整機能も自作してみました.これはまだ暫定なので,いずれちゃんとしたやつ作ったら改めて紹介します.)

さて,組み込んだキーマップですが,LOWERキーを起点に次のような動作を仕込んでいます.

f:id:shikky_lab:20200412195339p:plain
キーマップ概略

  • LOWER(タップ) = 全角切り替え*
  • RAISE(タップ) = 半角切り替え*
  • LOWER + RAISE = Esc
  • LOWER + H/J/K/L = カーソルキー
  • LOWER + Space = Enter
  • LOWER(ダブルタップ) + H/J/K/L =マウス操作

*全角/半角切り替えは仮想キー(F14とか)を送り,OS側で処理しています

LOWERキーが非常に酷使されてますね...上述の機能ひとつひとつははそこまで大変ではないのですが,これらを組み合わせると色々問題が起こったので,それを回避するために工夫が必要でした.今回はその辺の話をしたいと思います.

はじめに

上記キーマップでピンときた方も多いかもしれませんが,私のキーマップはenthumbleというアプリケーションのアイデアをベースにしています.
キーボード入力、もっと速く、正確に - enthumble>
動作をさっとまとめると下記です.

  • 変換キータップでIMEオン,無変換キータップでIMEオフ(全角/半角キーの代用)
  • 変換/無変換キー+hjklでカーソル移動
  • 変換/無変換キー+スペースキーでEnter
  • 変換 + 無変換キー で Escape

普通のキーボードを使っている時からこのアプリを愛用していて(正確にはAutoHotKey使って模倣・拡張してましたが),これがないとタイプ速度が3割減になる気がします.とてもいいアプリだと思うので,興味のある方は試してみてください.そして,物足りないなと思ったらキーボードを自作しましょう(笑)

実装解説

では前置きはこの辺で,そろそろ実際のコード紹介に入りたいと思います.今回のキモは何といってもLOWERキーで,特に最後に実装したダブルタップのせいで非常に難航しました.そのあたりから,下記2点について説明したいと思います.
A. LOWERキーのダブルタップで別レイヤーに遷移する
B. LOWER+RAISEでEscを入力する

A. LOWERキーのダブルタップで別レイヤーに遷移する

冒頭の図におけるLOWERキーは,通常のホールドでLOWERレイヤに遷移して,ダブルタップからのホールドでMOUSEレイヤに遷移します.これはTapDance機能を応用すれば実現できます. Tap Dance - QMK のexample4ですね.
この例ではon_dance_finished_fn()内でTAP/HOLDの判定を行っており,ここでHOLDの場合にLayer_on()関数を呼べば実現できそうです.
が,この方法では次の問題が生じます.

Layerキー押下直後のキー入力が反映されない

これには次の二つの理由があります.TAPPING_TERM(デフォルト200ms) 内に別の入力があった場合,

  1. 上記の例ではSINGLE_TAPとして処理していること
  2. on_dance_finished_fn()は別の入力キー検出後に呼ばれること

一つ目はif文の話で,上記の例ではSINGLE_TAPとして処理する条件を次のように記述しています.

if (state->interrupted || !state->pressed)  return SINGLE_TAP;

interruptedは,TAPPING_TERM経過前に別のキーが押された場合にtrueになるフラグで,短時間に別のキーが押された場合にTAPPING_TERMを待たずにキー入力を行うことができます(たぶん).
しかしそれは逆説的に,ホールド判定されるためにはTAPPING_TERMの間別キーを押さずに待つ必要があるということになります.
TAPPING_TERMの設定次第だとは思いますが,if文を次のように変更して,interruptはHOLD判定にしておいたほうが馴染むと思います.

if (!state->interrupted && !state->pressed)  return SINGLE_TAP;
※もしくは,if (!state->pressed)  return SINGLE_TAP; //interruptかどうかはもはや見る必要ない.

二つ目はTapDance関連の関数の呼び出しタイミングの話です.上記例ではon_dance_finished_fn()はタップ動作完了後に呼び出されるのですが,これは正確には,

  • TAPPING_TERM経過した場合
  • TAPPING_TERM経過前に他のキー入力に割り込まれた場合

に発生するようです.入力が早い場合は後者のケースになる訳ですが,この瞬間における"他のキー入力"は,前レイヤでのキー入力になっています.そのため,思った通りの入力になりません.
この問題は,on_each_tap_fn()関数を使用することで回避できます. 実はこの関数はキーを離したタイミングではなく,押下したタイミングで毎回呼ばれます.このあたりのタイミングの話は下記で詳しく説明されています.
https://thomasbaart.nl/2018/12/13/qmk-basics-tap-dance/

つまり下記のようなon_each_tap_fn()を用意すると,押下した瞬間からレイヤーを切り替えることができます.

void dance_lower_each(qk_tap_dance_state_t *state, void *user_data){
   if(state->count == 1){
      lower_pressed=true;
      layer_on(_LOWER); 
      update_tri_layer(_LOWER, _RAISE, _ADJUST);
   }else if(state->count == 2){
      lower_pressed=false;
      layer_on(_MOUSE); 
   }
}

なお,ここで切り替えたレイヤは,on_dance_reset_fn()でちゃんと戻しておきましょう.

これらによって,スムーズなHOLD動作を実現することができます.が,ここで次の問題が起こります.

タップ後のキー入力が早い場合,HOLD判定になってしまう

これは先ほどの問題と背反のようですが,LOWERキーを押した瞬間にLOWERレイヤに切り替えてしまっているため,タップを意図していたとしてもTAPPING_TERMの間別レイヤに切り替わってしまいます.
これの回避策は良い方法が思いつかなかったのですが,"LOWERレイヤのキー入力が起こった瞬間に,本当にLOWERキーが押されているかどうか"で判断することにしました.
具体的には上述のdance_lower_each()関数にて,押下していることを表すlower_pressedというフラグ変数をtrueにしています.
そして,LOWERレイヤに配置しているキーそれぞれについて,lower_pressedの状態を見ながら動作を切り替えています.

bool process_record_user(uint16_t keycode, keyrecord_t *record) {//いずれかのキーが押された/離された際に呼ばれるイベント
  switch (keycode) {
   case J_HACK://LOWERレイヤでの"J"キーを入力した際のカスタムキーコード.
    if (record->event.pressed) {//押下時
         if(lower_pressed==false){//LOWERキーが押されていないかの判定
            register_code(KC_J);//LOWERが押されていない場合は,デフォルトレイヤの動作を行う
         }else{
            register_code(KC_DOWN);//LOWERレイヤの場合,カーソルキー動作
        }
    }else{//離したとき
         unregister_code(KC_J);
         unregister_code(KC_DOWN);//unregisterは弊害なさそうなので,常に実行
    }
    return false;
    break;
}

LOWERレイヤにあるキー全てに設定が必要なため面倒ですし美しくない気がしますが,これで目的の動きを実現できました.

B. LOWER+RAISEでEscを入力する

これは単純にLOWERレイヤのRAISEの位置にESCを割り当てておけばよい気がしますが,ここも色々と工夫しています.
というのも,実は冒頭では述べていませんでしたが,LOWER+RAISEでADJUSTという別のキーマップに遷移する設定にしています.
また咄嗟にESCを入力する際,RAISEキーを離す前にLOWERキーを離してしまうことが多々あり,それにも対応する必要がありました.
これらに対応するため,次のような条件でESCが動作するように記述しました.

  • RAISEキーを押下した後,RAISEキーを離すまでの間に,RAISEキーとLOWERキー以外が押下されていない場合

具体的にはこんな感じです.

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    static bool rl_interrupted = false;//LOWER/RAISEキー以外が押された際にtrueになるフラグ
   if(!(keycode == TD(TD_LOWER)) &&  !(keycode == RAISE) ){
      rl_interrupted = true;
   }
  switch (keycode) {
    case RAISE:
      if (record->event.pressed) {
        layer_on(_RAISE);
        update_tri_layer(_LOWER, _RAISE, _ADJUST);
        rl_interrupted = false;
      } else {
        layer_off(_RAISE);
        update_tri_layer(_LOWER, _RAISE, _ADJUST);
        if (is_tapped && layer_state_is(_QWERTY)) {//単独タップ判定
            tap_code(KC_F13);//OS側で強制半角に設定
        }else if(rl_interrupted == false){
            tap_code(KC_ESC);
         }
      }
      return false;
      break;
}

ポイントは,if(!(keycode == TD(TD_LOWER)) の部分です.
TapDance用のキーコードを割り当てていても,precess_record_user()関数のイベントは呼ばれるため,このようにちゃんと取得できます.TapDance側の関数とどっちが先に呼ばれるかははっきりしていないですが,こういったフラグ管理用途では問題なく使えます.

おわりに

思ったよりも長々と書いてしまいましたが,工夫した点(もしかしたら他の人の参考になるかもしれない点)については一通り記載できたかと思います.
一応ソースコードは上げておきますが,実際にはもっと色々な機能が組み込まれているため,非常に見づらいものになっています. もし参照される場合は,そのあたりを念頭に置いたうえでお願いします.
iris/keymap.c at master · shikky-lab/iris · GitHub

さて,これで一旦自作キーボードについては落ち着きましたが,機会があれば基板設計からやってみたいですね.特につくばminiMakerFaireにてトラックボール付きのキーボードを自作している方にお会いして心を動かされたので,挑戦してみたいです.
ただ,他にもやりたいことは沢山あるので悩みどころですね.なんにせよ,何か作ったらまた記事にしたいと思います.
それでは.

#この記事は,Irisを使って作成しました.