QoLが上がる自作左手デバイスのコーディング

この記事は [ここのえ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/99no_exit) Day 18の記事です。

# 自作左手デバイスでQoLを上げる

突然ですが、皆さんはいわゆる **左手デバイス** はお持ちでしょうか？

一般的にお絵描きやクリエイティブ用途としては、[**Loupedeck**](https://loupedeck.com/jp/products/loupedeck-ct/) とか、[**TourBox**](https://www.tourboxtech.com/jp/) とかが有名で、ゲーミング用途で持っている場合は [**Razer Tartarus Pro**](https://www.razer.com/jp-jp/gaming-keypads/razer-tartarus-pro) がメジャーです。

ただこれらのデバイスはPC側に専用のファームウェアが必要だったり、それなりにイイお値段がするので、なかなか敬遠してしまいがちです。

<br/>

そんな時、知人が自作キーボードを作り始めたことから興味を持ち、[遊舎工房](https://yushakobo.jp/) を眺めていたらこんなものを見つけました。

https://shop.yushakobo.jp/products/2795?_pos=1&_sid=9a29ed6f2&_ss=r

**自作左手デバイス…！？**

要は自作キーボードの一種ですが、コンパクトにまとまっていたり、特殊なインプットとしてホイール等がついている自作キットが複数存在しています。

今回組み立てたのは先述のキットで、[はやしたろう](https://twitter.com/w_vwbw) さんの [**Handyman**](https://shop.yushakobo.jp/products/2795?_pos=1&_sid=9a29ed6f2&_ss=r) を選びました。
[ビルドガイド](https://github.com/Taro-Hayashi/Handyman) がとても丁寧に書かれておりオススメです。自分も初めての自作でしたが、何とかなりました。この場を借りて感謝申し上げます……

:::note info
自分はソフトウェア畑で育ってきたので、電子工作はラズパイとArduinoをブレッドボードとかでいじって触ったことはあるけれど、半田付けには1μmも自信がない、といった具合でした。頑張れば人間なんとかなります。
:::

# カスタマイズする

実際に組み立てたら、デバイスのカスタマイズを行っていきます。
ここが自作デバイスの一番面白いところだと思います。

一般的なユースケースでは、**Remap** を使用することを強く推奨します。ブラウザベースでキーマッピングを設定して、すぐに書き込むことができます。

https://remap-keys.app/

ですが、それでは収まらないのがプログラマーの性です。
レイヤーの切り替えタイミングでコールバック関数を走らせて独自の処理を走らせたりとか、徹底的にカスタマイズしたくなります。

こうなると、~~ておくれです~~。
自分でファームウェアをビルドするしかありません……

<br/>

そこで **QMK Firmware** が出てきます。

QMKは自作キーボード界隈で(たぶん)一番使われているファームウェアで、例えばWindowsなら[**QMK MSYS**](https://msys.qmk.fm/)というQMKのCLIや、ファームウェア書き込み&デバッグコンソールの機能を兼ねた[**QMK Toolbox**](https://github.com/qmk/qmk_toolbox)といった、強力なツールチェーンが揃っています。

先述の **Remap** も、QMK Firmwareベースの端末に対応しており、[`Raw HID`](https://docs.qmk.fm/#/feature_rawhid) の機能を使って実装されているようです[^1]。

https://qmk.fm/ja/

# 実装サンプルなど

自前のカスタム後のコードを基に少し解説していきます。
今回は実際のセッティング例を挙げるので、環境構築などは省きます。

環境構築からビルドまでの一連の流れは、以下の記事がお勧めです。

https://qiita.com/mizuhof/items/c2f9eb173b21593d969e

**Handyman** は作者のはやしたろうさんが、[ファームウェアのサンプル](https://github.com/Taro-Hayashi/qmk_firmware/tree/tarohayashi/keyboards/tarohayashi/handyman)を公開して下さっていますので、これをベースに使っています。

## レイアウト

例えば、`Adobe Illustrator` 用のレイヤーはこんな構成になっています。

```c
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [LAYER_AI] = LAYOUT( /* Adobe Illustrator */
        LCTL(KC_2), LALT(KC_DOWN), LALT(KC_UP), LCTL(KC_1), LCTL(KC_0),
        KC_V, LCTL(KC_S), LCTL(LSFT(KC_G)), KC_DEL, LCTL(KC_RBRC),
        KC_A, XXXXXXX, LCTL(KC_G), XXXXXXX, LCTL(KC_LBRC),
        KC_LSFT, LCTL(KC_X), LCTL(KC_C), LCTL(KC_V), XXXXXXX,
        KC_LCTL, KC_LALT, LCTL(KC_7), LCTL(LSFT(KC_O)), KC_SPACE,
        DF(LAYER_XIV), XXXXXXX, DF(LAYER_CUBASE)
    ),
```

簡単に説明すると、以下のような感じです。

- `KC_A` `KC_LBRC` など
    - KeycodeのKC。キーボードのキーが対応している
- `LCTL()`, `LSFT()`
    - 左Ctrl、左Shift + αの操作を定義できる。
    - `LCTRL(LALT(Keycode))` のように重ねることも可能。
- `DF(LAYER_ID)`
    - デフォルトレイヤーの変更。
    - キーマップのセットを「レイヤー」として保持しておき、後から切り替えることができる。
    - 基本的にレイヤーのIDはint。今回はサンプルコード外で `#define LAYER_AI 0` などしている。

実装が何となくわかっても、イラレを知らないとなんのこっちゃいですね。さすがに怒られそうなので画像を作っておきました。

![qmk_ai.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3617021/996d1077-558e-e593-92f6-c897cbe0293a.png)


`選択`～`テキスト`までの列については、イラレのツールバーのうちよく使うものを抽出、違和感がないように画面上の表示と同じ順番で並べています。

`カット`, `コピー`, `貼り付け`については一般的なQWERTYキーボードと同様の並び順にすることで、使用時に違和感が起きないようにしています。
`全選択` だけ逆になってやや微妙なのですが、あまり頻繁に使わないのでこの位置にしています。

`やり直し`, `取り消し`がかなり特殊な位置に設定しています。
ダイヤル式のエンコーダーに当てることで、進む・戻るを直感的にしつつ、通常のCtrl+Zでは指が疲れてしまう連打も、ぐるぐる回して一気に戻せるので操作感が良く、かなり気持ちいいです。
加えてキーボードとは違うデバイスであることを**無意識的にわからせる**という意味でも役立っています。

何気に気に入ってるのが`画面サイズに拡大`や`前面/背面`あたりで、この辺りはキーボードから手を離さないとならず、加えて`0`なんか特に押し損ねがちです。この辺は何を選ぶか人の好みがありそうです。

エンコーダーの押し込みについては、誤ってスクロールするとストレスフルになりそうなので、敢えてイラレのレイアウトでは使っていません。他のレイヤーでは使うこともあります。

## エンコーダーのキーマッピング

先程のレイヤーのコードを見て勘のいい方は気づいたかもしれませんが、実はレイヤー定義ではホイールやダイヤルが動いたときの処理は定義していません。

エンコーダーが動くと `encoder_update_user` が呼び出されるので、ここに処理を書いてみます。

```c
bool encoder_update_user(uint8_t index, bool clockwise) {
    switch(get_highest_layer(default_layer_state)) {
        case LAYER_AI:
            if (index == 0) { /* First encoder */
                if (clockwise) {
                    tap_code(KC_WH_U);
                } else {
                    tap_code(KC_WH_D);
                }
            } else if (index == 1) { /* Second encoder */
                if (clockwise) {
                    tap_code(KC_WH_L);
                } else {
                    tap_code(KC_WH_R);
                }
            } else if (index == 2) { /* third encoder */
                if (clockwise) {
                    tap_code16(LCTL(LSFT(KC_Z)));
                } else {
                    tap_code16(LCTL(KC_Z));
                }
            }
            break;
```

エンコーダの処理については、[サンプルコード](https://github.com/Taro-Hayashi/qmk_firmware/tree/tarohayashi/keyboards/tarohayashi/handyman)を基にレイヤー処理を追加しています。

`index`はエンコーダーの番号で、`clockwise` は回転の方向です。

肝心の処理レイヤーチェックですが、`default_layer_state` を参照しています。stateの値はレイヤーのON/OFF状況をビットで保存しているので、レイヤー番号で比較するために `get_highest_layer()` を使っています。

レイヤーで`switch`したら、後は各エンコーダーごとに処理を定義していきます。

## `tap_code`は気を付ける必要がある

上記コードの3番ダイヤルの処理ですが、`tap_code16`になっています。この16は何でしょうか？

例えばこのコードは、以下のコードで置き換えることが可能です。

```c
if (clockwise) {
    register_code(KC_LCTL);
    register_code(KC_LSFT);
    tap_code(KC_Z);
    unregister_code(KC_LCTL);
    unregister_code(KC_LSFT);
```

これは `Ctrl + Shift + Z` を実現しているのですが、ちょっと厄介な手順を踏まなければいけません。

前提としてこれらの関数は、以下の役割を果たします。

- `register_code(KeyCode)`
    - 指定されたキーコードのキーを押す。
- `unregister_code(KeyCode)`
    - 指定されたキーコードのキーを離す。
- `tap_code(KeyCode)`
    - `register_code`と`unregister_code`の合わせ技。

ここで`tap_code(LCTL(KC_Z))`などとすると、コンパイル時に怒られます。

```c
quantum/quantum_keycodes.h:45:18: error: unsigned conversion from 'int' to 'uint8_t' {aka 'unsigned char'} changes value from '285' to '29' [-Werror=overflow]
#define LCTL(kc) (QK_LCTL | (kc))
^~~~~~~~~~~~~~~~
./keyboards/test/handyman/keymaps/default/keymap.c:117:30: note: in expansion of macro 'LCTL'
tap_code(LCTL(KC_Z));
```

ドキュメントには関数定義までは書いていませんが、ソースコードを読んでみると`quantum/action.c`で`tap_code`は`uint8_t`を引数に取っています。`uint8_t`は`0`～`255`なので、複合キーの値を取れなそうです。

そこで上記の例では`KC_LCTL`と`KC_LSFT`を個々でキーをDown, Upすることで対応しています。
ただ、これではあまりスマートではないです。

そこで`tap_code16(uint16_t code)`を使います。こちらは文字通り`uint16_t`を引数に取るので、`tap_code16(LCTL(LSFT(KC_Z)));` のような書き方ができます。

## レイヤーごとにRGB LEDの色を変える

苦労してLEDを全部半田付けしたので、せっかくなら設定中のレイヤーに合わせてLED色が変わるようにしたいです。

この実装はそんなに難しくありません。

```c
layer_state_t default_layer_state_set_user(layer_state_t state) {
    switch(get_highest_layer(state)) {
        case LAYER_AI:
            rgblight_setrgb(247, 149, 0);
            break;
        case LAYER_CUBASE:
            rgblight_setrgb(21, 55, 82);
            break;
    ...

    return state;
```

レイヤーの変更時に、`default_layer_state_set_user`関数が呼ばれるので、これを利用して色を変えます。

レイヤーの取り方については先述のエンコーダーの実装と同じ原理で、`get_highest_layer`を使ってレイヤー番号に変換し、`switch`で回しています。

RGBは`info.json`で定義してあったため、特に設定せず使っています。`rgblight_setrgb(r, g, b)`関数を呼び出せば、全体の色を変更することができます。

RGB Matrixを使う人は [**ここ**](https://docs.qmk.fm/#/feature_rgb_matrix?id=rgb-matrix-lighting) を見てください。自分はレイアウトを定義するのがめんどくさすぎて諦めました。

# まとめ

大分長文になってしまいましたが、レイアウトの具体例やコーディング時のTipsなどを纏めてみました。

左手デバイスは確かに癖はあるのですが、自作だとカスタマイズ性が段違いに高い上、キーキャップを好きなものに交換出来たり、「Pro MicroをMicro-BじゃなくType-Cにしたい…」といったハードウェア面での変更もでき、故障時もキーの交換などできます。何より愛着が湧くのが最大のオススメポイントです。

今回の記事を書いた段階ではまだ調整中なのですが、`LAYER_MARKDOWN`を作ってマークダウンのドキュメント・記事作成を左手デバイスで効率化できないか、色々試している所です。また纏まったら記事にしようかなと思ってます。

自分はC言語も電子工作も専門外ではありますが、意外となんとかなりました。フロントエンドがメインで、特にデザインとかのソフトを使うのであれば、結構便利な場面があると思います。良かったらチャレンジしてみてください。

# おまけの雑談

QMKの全キーコードのリストを見ると、**感熱式プリンタ** というセクションがあります。何に使うんだ？という疑問の前に、そもそもどうやって使うんでしょうか……

……自作界隈はまだまだ奥が深いですね。

https://docs.qmk.fm/#/ja/keycodes?id=thermal-printer

[^1]:https://github.com/remap-keys/remap