見出し画像

Keychron Q0 MAXという10キーを手に入れてqmkファームウェアでフルカスタマイズしていく話

年が変わってそろそろ確定申告の準備をしなきゃなと思い立ち、いやでも面倒だなとすぐさま逃げ腰になり、ネットの海へ。
何か確定申告を楽にするアイテムはないものかなー(そんなの無い)と死んだ魚の目でガジェットを漁っているうちに、そういえば去年面倒だったの数字の打ち込みだっけと思い出しました。面倒すぎてとち狂ってしまい、楽器メーカーのTeenage Engineeringに「EP-133に10キーのモード追加して!」などというお願いメールさえ送ってしまっていたのでした(恥

自分は2月と3月以外はテンキーが必要な仕事はそうそうないのでテンキーがついていないキーボードを使っています。
で、数字の打ち込み必要になっていざキーボード上段のキーを使って入力していくのってなかなかにダルいんです。普段使っているのはMoonlanderという格子配列に近いカラムスタッガードのキーボードで、これに10キーをマッピングしてあるレイヤーを作ってはあるものの、なんかやっぱり不便。なぜ不便なのか逆に不思議なんですけど……

Oryxで設定してある10キーレイヤー、これでじゅうぶんなはずでした

そこで今年初の買い物はテンキーにしようかと。キーボードの類は良いものを買えば買い換えることはそうそうありません(買い増しはあります)。良いものがあればそれを買って長く使い続けたいです。といったところからテンキー探しをはじめました。

いちおう
http://www.datamath.org/
http://www.arithmomuseum.com/index.php?lang=en
The Old Calculator Web Museum
これらのサイトを一般的な計算機のリファレンス的な知識として頭に入れておきます。

それからいろいろ探して行って見つかったのは、まず計算機の流れを汲むものとして

こちらはキースイッチを赤軸、茶軸、青軸から選べます。シンプルですし、使い方に迷うことはなさそう。

レトロフューチャー感覚に溢れるのは

これバック・トゥ・ザ・フューチャーに出てくるあの車のディスプレイっぽいし日付もそれっぽい……ここまで来て10キーだけの機能だともったいなくないかということに気づいてしまいました。

自分の身の回りで一番地価(?)の高いのは机の上です。次に冷蔵庫の中。
要するに必要ないものは極力置きたくないということでデスク上に置くものには実用性がめちゃめちゃ求められます。どうせならレイヤー切り替えできてショートカットキーの割り当てやマクロバリバリ使えたりする10キー絶対あるでしょうとさらに探索に向かいます。

結果見つけたのがまずこちら。

色合いも可愛いですね。シンプル。それから次に

こちらもマッピング次第ですぐに10キーとして使えそうですね。で、最終的には途中でチラ見しながらも在庫ないし高いよねとスルーしていた、

こちらの10キーに視線と物欲が収束していきました。

Keychronはキーボードも持っているのですが、ずっしり感がまず良いのと落ち着いた色合いを選べることも嬉しいです。さらにQ0 MaxはKSAプロファイルのキーキャップが標準でついててこれも素敵ポイント。いつかは使ってみたかったんです。外形的な良さに止まらず、QMKファームウェアなのでカスタマイズも自由自在。
ちょっと価格が高めでも、10キーって買い替えないし買い増しもしないだろうということでこれに決断。色はこのホワイト系のを。キーボード関連は黒だと埃が目立つのでホワイト系が最近のお気に入り。キーキャップが汚れてきたら外して洗剤で洗うようにしています。ほか、キースイッチも3種類の中から選べます。どれもGateronのJupiterという、Keychron監修(?)のGateronスイッチで、自分は赤軸を選択しました。茶軸とバナナ軸もありますが無難に静かなやつに。今までスイッチいろいろ試してみましたが自分は青軸以外でそれなりに打鍵音が小さいやつがいいかなと。

で、待つこと2週間で着弾しました。以下レビューというか設定など。

このKeychron Q0 Maxって接続が

  • ケーブル接続

  • Bluetooth接続

  • 2.4GHz無線接続

の3種類で、さらにBluetoothのホストを3台まで登録できるのがめちゃめちゃ嬉しいですね。ボタンを押したり切り替えたりするだけで自分の持っているコンピュータ全てに接続できます。

試してみたところ、KSAプロファイルはポジションがわかりやすく打ちやすいですし、キーを押してみると控えめなコトコトとした音とそれにマッチした柔らかいフィーリングでとても良いです。さすが。

設定

では設定していきましょう。
基本はKeychronのWebアプリであるLaucnherを利用しての設定となります。カスタムファームウェア入れたりQMKで設定したりということもできそうですが、現状はそこまでする必要がないのでLauncherを。

最初に戸惑ったのはキーコンビネーションのアサインです。「Command」 + 「A」みたいに二つ以上のキーを組み合わせての入力。これをどうやって設定するのか調べるのにかなり時間がかかりました。MoonlanderのOryxの設定画面だとスルスルと設定できるのにこちらのLauncherではぱっと見わかりません。

最終的に「Custom」の「Any」を利用して16進数のキーコードを打ち込まなければならないということがわかりました。簡単とはいえキーコード同士の計算が必要です。

例えば「Commandキー+A」を特定のキーにアサインする場合を考えてみます。
QMKにおいて「Command」キーは「LGUI (Left GUI)」に相当し、その基底アドレスが 0x0800 となっており、「A」キーのキーコードは 0x04 (16進) です。

よって、LGUI(KC_A)

0x0800 + 0x0004 = 0x0804

となり、この0x0804Anyで設定する、という感じになります。

YouTube見てるとマクロ機能を使ってキーコンビネーションを登録している人が多いようですが、マクロは15個までしか登録できないので自分の使い方だとキーコンビネーションを登録していくのにさすがに足りません。

16進のキーコンビネーションコードはChatGPTで生成してもらうのが良さげ

よく利用するのでもうちょっとわかりやすくしてくれると良いですね。

ちなみにこのAnyを利用してロータリーエンコーダーにキーコンビネーションを割り当てると現状、バグなのか設定値が表示されなくなって再設定できなくなってしまうようです。

さて、Q0 Maxはデフォルトだとレイヤーが0, 1, 2, 3の4枚まで設定可能で、VIALやQMKのカスタムファームウェアといったものを利用すると32枚まで利用可能になるようです。いきなりカスタムファームウェアを利用して文鎮化させるのは怖いのでひとまずはLauncherアプリを利用することで可能な範囲のカスタマイズに留めておきます。

カスタムの方針については

  • M1キーとM2キーには何か便利な機能を

  • M3キー、M4キー、M5キーはレイヤーの切り替えに使用

  • レイヤー0は基本デフォルト

  • レイヤー1は基本にプラスアルファ

  • レイヤー2とレイヤー3はよく使うアプリのショートカット

としました。M1とM2にはRaycastやKeyboard Maestroのショートカットでも入れておこうかなと。M3にはレイヤー3への切り替え、M4はレイヤー2への切り替え、M5にはシングルプレスでレイヤー0に、押したまま他のキーを押すとそのキーをレイヤー1のキーと認識させるように設定します(できるのでしょうか?追記:レイヤータップで行けるかと思いきやそうでもないようで、ではタップダンスは?と思いきやLauncherから登録することはできないようでした。VIAやLauncherは視覚的にわかりやすくなっているぶん、制限がキツいみたいです)

Layer 0

基本レイヤー。ノブとマクロキー以外はデフォルトのまま利用しています。いわゆる10キーとしてのレイヤ

Layer1

基本のプラスアルファ。Bluetoothのや無線の切り替えである上部の⚪︎△◻︎×キーについてはそのままにして、他のものは入れ替えます。
デフォルトでLayer1に設定されているのはQ0 Maxのバックライトの設定とメディアキー、輝度調整などですが、照明設定などでキーを消費するのはもったいないので変更しています。バックライトはLauncher側で簡単に変更することが可能なのでそちら側で調整しています。
で、追加するキーはHOMEやENDなどの特殊キーやカーソルキー。HOMEやENDってよくアプリケーションのショートカットキーに要求されてしまうので。

Layer2 & Layer3

よく使うアプリのショートカット。まだ作り込んではいないのですが、使っていて一番ダルいiZotope RXのショートカットでも割り当てたいところです。
Keyboard Shortcuts

ホントによく使うアプリケーションならDaVinci Resolveのエディターキーボードのような専用のものがあると良いのですが……

DaVinci Resolveでしか使えないけどめちゃ使い心地良いやつ

と、まぁ言い出すときりがないですね。最初は「10キーが欲しい!」から始まっていたので。

最後に残る問題

で、最後まで、いや現在も悩み続けているのが、左手側に置くべきか、右手側にべきかという問題。数字を打ち込むのだったら絶対に右手側が有利な気がします。10キー下段の横長の「0」キーって絶対これ親指で押すように作られたキーな気がするんですよ。親指って左右には動くけど手前と奥への稼働長ってないじゃないないですか。
なので左手用に作るならキー配置を全て右手用の逆、つまり

| 9 | 8 | 7 |
| 6 | 5 | 4 |
| 3 | 2 | 1 |
| . |   0   |

というような配置にすべきだと思うんですよね。マウスも左手用のはシングルクリックボタンは右側だったりするように、10キーもそうなっていたらなーと。
キーマップはカスタマイズできますが、さすがに横長や縦長のキー配置を変更することはできないようでした。

結局今は右手奥側に置くようにしています。左手側は今まで同様トラックパッドで、右手手前側にマウス、みたいな感じです。この配置にすると今度はQ0 Maxのエンコーダーがかなり回しにくくて……となかなか難しいですね。

今はひとまずこんな配置で使っています。

おおまかな利用法はわかって、LauncherやVIA経由で設定を行うことによるトレードオフ、フルにその能力を引き出すためのQMKの必要性なども理解しました。ということで標準で利用できる機能ですぐに物足りなくなったのでやっぱりqmkでカスタマイズしていきます!

QMKカスタムファームウェア開発環境の構築

早速Keychron Q0 MaxをフルカスタマイズするべくQMKのファームウェア作成に挑戦してみます。カスタムファームウェアを入れてしまうとおそらくLauncherやVIAなどは使えなくなりそうです。さらにVIAの高機能版であるVIALのほうは残念ながら無線/BLEモデルのKeychronのProやMaxで動いた実績はなさそうでした。ということで以下の手順をやってしまうとGUIでお気楽に設定できなくなると思われるのでお気をつけください。ある程度落ち着いてしまえばそんなにキーマップって変えたりしないのでいいかなと。

全てがすんなりといくわけではなくいろいろとハマり続けました。以下自分の作業記録を残しておきます。めっちゃ長いし、現在進行形で書き足されています。お覚悟。

まず転ばぬ先の杖でユーザーガイドやファームウェアが提供されていることを確認しました。なんかあったら公式ファームウェアを書き込めば良いかなって。

Keychron Q0 Max User Guide

ビルド環境の準備とテストビルド

次にQMKのビルド環境を整えていきます。
Setting Up Your QMK Environment | QMK Firmware
この辺にリソースがあるのですが、

qmk_firmware/keyboards/keychron/q0_max at wireless_playground · Keychron/qmk_firmware

上のKeychronのqmkフォークリポジトリにQ0 Maxの定義があるのでそちらを利用します。とはいえ参照されているドキュメントは一緒で、まず

brewでqmkをインストール

brew install qmk/qmk/qmk

その後にKeychronのqmkリポジトリをクローン。submoduleも入っているので`--recursive`オプションを付けてクローンします

git clone --recursive git@github.com:Keychron/qmk_firmware.git

Q0 Maxなど無線が利用可能なモデル定義はmasterブランチにはないのでwireless_playgroundブランチに切り替えることをお忘れなく、ブランチを切り替えたらさらにsubmoduleもアップデートします。

で、qmkのセットアップを行なっていきます

qmk setup --help

とすると

SyntaxWarning: invalid escape sequence '\['

みたいなWarningが出ていたので

python3 -m pip install -U qmk

としてpython側のライブラリをアップデートしました。でも後でまた同じようなエラーが出てqmkのプログラム側で対処してたっぽいのでこの手順はいらないかも。

さて実際のセットアップです。qmk_firmwareの一つ上のディレクトリで

qmk setup -H .

これはプロジェクトディレクトリを設定するためのもののようでした。
自分はここでqmk_firmwareのディレクトリを設定してしまい、そうするとqmk_firmwareのディレクトリの中にさらにqmk_firmwareの公式リポジトリをクローンしそうになったのでやり直しました。

セットアップ中にavr-gcc がないというエラーが出たので

brew install avr-gcc

してもう一回やり直し。

⚠ The official repository does not seem to be configured as git remote "upstream".

というwarningが出て

Ψ QMK is ready to go, but minor problems were found

という表示になりました。minor problemsというのは公式リポジトリがupstreamに設定されていないというエラーだろうということで一旦無視します。

ようやく準備ができたのでビルドしていきます。

qmk compile -kb keychron/q0_max/encoder -km default

Q0 Maxで利用されているSTM32F402ってSTMicroのサイトで見当たらなくてコンパイルできるのかなと思ってちょっと心配していましたが、F401と互換性あるようですね。OEM生産しているものなのでしょうか?

Compiling: lib/chibios/os/hal/ports/common/ARMCMx/nvic.c                                            [OK]
Compiling: lib/chibios/os/hal/ports/STM32/STM32F4xx/stm32_isr.c                                     [OK]
Compiling: lib/chibios/os/hal/ports/STM32/STM32F4xx/hal_lld.c                                       [OK]
Compiling: lib/chibios/os/hal/ports/STM32/STM32F4xx/hal_efl_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/ADCv2/hal_adc_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/CANv1/hal_can_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/CRYPv1/hal_crypto_lld.c                               [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/DACv1/hal_dac_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/DMAv2/stm32_dma.c                                     [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/EXTIv1/stm32_exti.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/GPIOv2/hal_pal_lld.c                                  [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/I2Cv1/hal_i2c_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/MACv1/hal_mac_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/OTGv1/hal_usb_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/QUADSPIv1/hal_wspi_lld.c                              [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/RTCv2/hal_rtc_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/SPIv1/hal_i2s_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/SPIv1/hal_spi_v2_lld.c                                [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/SDIOv1/hal_sdc_lld.c                                  [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/SYSTICKv1/hal_st_lld.c                                [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/TIMv1/hal_gpt_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/TIMv1/hal_icu_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/TIMv1/hal_pwm_lld.c                                   [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/USARTv1/hal_serial_lld.c                              [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/USARTv1/hal_uart_lld.c                                [OK]
Compiling: lib/chibios/os/hal/ports/STM32/LLD/xWDGv1/hal_wdg_lld.c                                  [OK]
Compiling: lib/chibios/os/hal/boards/ST_STM32F401C_DISCOVERY/board.c                                [OK]
Compiling: lib/chibios/os/hal/lib/streams/chprintf.c                                                [OK]
Compiling: lib/chibios/os/hal/lib/streams/chscanf.c                                                 [OK]
Compiling: lib/chibios/os/hal/lib/streams/memstreams.c                                              [OK]
Compiling: lib/chibios/os/hal/lib/streams/nullstreams.c                                             [OK]
Compiling: lib/chibios/os/hal/lib/streams/bufstreams.c                                              [OK]
Compiling: lib/chibios/os/various/syscalls.c                                                        [OK]
Compiling: platforms/chibios/syscall-fallbacks.c                                                    [OK]
Compiling: platforms/chibios/wait.c                                                                 [OK]
Compiling: platforms/chibios/synchronization_util.c                                                 [OK]
Compiling: platforms/chibios/interrupt_handlers.c                                                   [OK]
Linking: .build/keychron_q0_max_encoder_default.elf                                                 [OK]
Creating binary load file for flashing: .build/keychron_q0_max_encoder_default.bin                  [OK]
Creating load file for flashing: .build/keychron_q0_max_encoder_default.hex                         [OK]

Size after:
   text    data     bss     dec     hex filename
      0   59162       0   59162    e71a keychron_q0_max_encoder_default.bin

Copying keychron_q0_max_encoder_default.bin to qmk_firmware folder                                  [OK]

ということでビルドまで無事確認できました。

ざっとkeymap.cを確認

ということでいよいよ自分用のkeymap作成への準備です。

先ほどのオプションで

-km default

というものを渡していました。

`q0_max`ディレクトリは以下のような構成になっています。

eza -T
.
├── board.h
├── config.h
├── encoder
│  ├── config.h
│  ├── encoder.c
│  ├── info.json
│  ├── keymaps
│  │  ├── default
│  │  │  └── keymap.c
│  │  └── via
│  │     ├── keymap.c
│  │     └── rules.mk
│  └── rules.mk
├── firmware
│  └── keychron_q0_max_encoder_via.bin
├── halconf.h
├── info.json
├── mcuconf.h
├── q0_max.c
├── readme.md
├── rules.mk
└── via_json
   └── q0_max_encoder.json

defaultとviaというディレクトリがkeymapsの下に見えます。おそらく-km defaultというのはこのディレクトリの指定ですかね。viaとdefaultのkeymap.cの差分を取ってみると全く同一のファイルでした。つまりviaとdefaultの違いはrules.mkファイルの有無のみです。

しかも`rules.mk`の中身は

VIA_ENABLE = yes

という1行だけ。シンプルですね。今回はVIA_ENABLEとしてVIA経由で設定できる以上のカスタマイズを施すのでVIA_ENABLEは使用しません。

デフォルトの`keymap.c`の内容は短いので載せてしまいます。

/* Copyright 2024 @ Keychron (https://www.keychron.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include QMK_KEYBOARD_H
#include "keychron_common.h"

enum layers {
    BASE,
    FN,
    L2,
    L3,
};
// clang-format off
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [BASE] = LAYOUT_tenkey_27(
        KC_MUTE, KC_ESC, KC_DEL, KC_TAB, KC_BSPC,
        MC_1,  KC_NUM, KC_PSLS,KC_PAST,KC_PMNS,
        MC_2,  KC_P7,  KC_P8,  KC_P9,  KC_PPLS,
        MC_3,  KC_P4,  KC_P5,  KC_P6,
        MC_4,  KC_P1,  KC_P2,  KC_P3,  KC_PENT,
        MO(FN),  KC_P0,          KC_PDOT         ),

    [FN] = LAYOUT_tenkey_27(
        RGB_TOG, BT_HST1, BT_HST2, BT_HST3, P2P4G,
        _______, RGB_MOD, RGB_VAI, RGB_HUI, _______,
        _______, RGB_RMOD,RGB_VAD, RGB_HUD, _______,
        _______, RGB_SAI, RGB_SPI, KC_MPRV,
        _______, RGB_SAD, RGB_SPD, KC_MPLY, _______,
        _______, RGB_TOG,          KC_MNXT          ),

    [L2] = LAYOUT_tenkey_27(
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______,          _______          ),

    [L3] = LAYOUT_tenkey_27(
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______,          _______          )
};

// clang-format on
#if defined(ENCODER_MAP_ENABLE)
const uint16_t PROGMEM encoder_map[][NUM_ENCODERS][2] = {
    [BASE] = {ENCODER_CCW_CW(KC_VOLD, KC_VOLU)},
    [FN]   = {ENCODER_CCW_CW(RGB_VAD, RGB_VAI)},
    [L2]   = {ENCODER_CCW_CW(KC_VOLD, KC_VOLU)},
    [L3]   = {ENCODER_CCW_CW(RGB_VAD, RGB_VAI)},
};
#endif // ENCODER_MAP_ENABLE

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    if (!process_record_keychron_common(keycode, record)) {
        return false;
    }
    return true;
}

こんな感じになっていて、見ればなんとなくわかるようになっているかと思います。

ちなみにキーコードの一覧はこちら
Keycodes Overview | QMK Firmware

内容的にレイヤーやキーマップの配置はすんなりと理解できます。 `encoder_map`についてもわかりやすいですね、
`ENCODER_CCW_CW`で左回し(Counter Clock Wise)、右回し(Clock Wise)の設定です。

残った`process_record_user`とその内部で呼び出している`process_record_keychron_common`というのがちょっとわからないので調べてみました。まず`process_record_keychron_common`については

bool process_record_keychron_common(uint16_t keycode, keyrecord_t *record) {
    switch (keycode) {
        case KC_MCTRL:
            if (record->event.pressed) {
                register_code(KC_MISSION_CONTROL);
            } else {
                unregister_code(KC_MISSION_CONTROL);
            }
            return false; // Skip all further processing of this key
        case KC_LNPAD:
            if (record->event.pressed) {
                register_code(KC_LAUNCHPAD);
            } else {
                unregister_code(KC_LAUNCHPAD);
            }
            return false; // Skip all further processing of this key
        case KC_LOPTN:
        case KC_ROPTN:
        case KC_LCMMD:
        case KC_RCMMD:
            if (record->event.pressed) {
                register_code(mac_keycode[keycode - KC_LOPTN]);
            } else {
                unregister_code(mac_keycode[keycode - KC_LOPTN]);
            }
            return false; // Skip all further processing of this key
        case KC_SIRI:
            if (record->event.pressed) {
                if (!is_siri_active) {
                    is_siri_active = true;
                    register_code(KC_LCMD);
                    register_code(KC_SPACE);
                }
                siri_timer = timer_read32();
            } else {
                // Do something else when release
            }
            return false; // Skip all further processing of this key
        case KC_TASK:
        case KC_FILE:
        case KC_SNAP:
        case KC_CTANA:
#ifdef WIN_LOCK_SCREEN_ENABLE
        case KC_WLCK:
#endif
#ifdef MAC_LOCK_SCREEN_ENABLE
        case KC_MLCK:
#endif
            if (record->event.pressed) {
                for (uint8_t i = 0; i < key_comb_list[keycode - KC_TASK].len; i++) {
                    register_code(key_comb_list[keycode - KC_TASK].keycode[i]);
                }
            } else {
                for (uint8_t i = 0; i < key_comb_list[keycode - KC_TASK].len; i++) {
                    unregister_code(key_comb_list[keycode - KC_TASK].keycode[i]);
                }
            }
            return false; // Skip all further processing of this key
        default:
            return true; // Process all other keycodes normally
    }
}

となっており、単純にキーコードの追加定義みたいな感じでしょうか?
めっちゃわかりづらいんですが、

KC_MCTRL

という定義のKCってKeyChronの独自キーコードっぽいですね。`keychron_common.h`で

enum {
    KC_LOPTN = QK_KB_0,
    KC_ROPTN,
    KC_LCMMD,
    KC_RCMMD,
    KC_MCTRL,
    KC_LNPAD,
    KC_TASK_VIEW,
    KC_FILE_EXPLORER,
    KC_SCREEN_SHOT,
    KC_CORTANA,

のように定義されていて、`keycodes.h`では

QK_KB_0 = 0x7E00,

となっているので`0x7E00`から少しKeychron用のキーコードとして拝借しているようです。なので`KC_MCTRL`は`0x7E04`となるでしょうか。
で、このキーが押されたら

KC_MISSION_CONTROL = 0x00C1,

を送っているということですね。
とまぁKeychron独自キーの割り当てなどをやっているのが`process_record_keychron_common`ということのようです。
これは生かしつつ`process_record_keychron_common`で他のキーの割り当てをしていく必要がありそうな感じ。

ビルドした際の`qmk_firmware/.build/obj_keychron_q0_max_encoder_default/cflags.txt`を見つつ、どんなものが有効化されているかチェックします。

フィーチャーの選択

自分がやりたいことは

  • レイヤーを増やしたい

  • 今のM5キーはシングルクリックでレイヤー0、ホールドでFnとして機能させたい

  • M5キーを押しながらM1からM4を押すことでレイヤーの切り替えをしたい

ということろです。
レイヤー0、レイヤー1に関してはほぼ現状どおりですが、レイヤー1のM1からM4にはレイヤー有効化(レイヤー2からレイヤー5まで)のショートカットを割り当てます。こうすることでM5キーを押しながら別のMキーを押すことでレイヤー移動できるようにしておきたいなと。
追加でパニックになったらレイヤー0に戻れるように各レイヤーでM5キーのダブルタップにはレイヤー0への移動を割り当てる機能も欲しいですね。

ということでM5キーは

  • シングルタップ

  • ホールド

  • ダブルタップ

の3つの割り当てを仕込みたいところ。これを実現するのにタップダンスという機能が必要です。リファレンスはこちら

combo機能も面白いですね。例えば「E」と「M」を同時押ししたら自分のメールアドレスを出力してくれる機能とか。でも10キーだと事故りそう……。

レイヤーに関しては32レイヤーまで可能と`quantum_keycodes.h`に書かれていました。

あとMagicbootを有効化しておきます、DFUモードに楽に入れるようになるらしい。

他configuration optionに関してはこちらを参照してください

独自キーマップの実装とビルド、書き込み

では実際に設定していきましょう。
まず新しいKeymapを用意します

qmk new-keymap -kb <keyboard_name>

自分のgithubユーザーネームを聞かれますがなんでもいいんじゃないかなと。ここで指定したものはcompile時に必要になります。これで生成されるのは指定した名前のディレクトリと`default`からの`keymap.c`のコピーです。なのでいったん

qmk compile -kb keychron/q0_max/encoder -km 指定した名前

として正常にビルドできるか確かめましょう。

次に新しく作成したkeymapのディレクトリに`rules.mk`ファイルを追加して必要なフィーチャーを追加します

TAP_DANCE_ENABLE = yes
COMBO_ENABLE = yes
BOOTMAGIC_ENABLE = yes

ちなみにフィーチャーのENABLE入れるだけで良いというわけではなく、rules.mkでENABLEしたものは何かしらの実装が必要になるものもあるようです。上で言うとBOOTMAGIC_ENABLEは必要ありませんでしたが、残り二つについては実装が必要でした。

ではkeymap.cの試験実装に移ります。

/* Copyright 2024 @ Keychron (https://www.keychron.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include QMK_KEYBOARD_H
#include "keychron_common.h"

enum combo_events {
    EM_EMAIL
};

enum {
    TD_NUM_ESC
};

enum layers {
    BASE,
    FN,
    L2,
    L3,
    L4,
    L5
};

const uint16_t PROGMEM email_combo[] = {KC_PMNS, KC_PPLS, COMBO_END};

combo_t key_combos[] = {
    [EM_EMAIL] = COMBO_ACTION(email_combo),
};
/* COMBO_ACTION(x) is same as COMBO(x, KC_NO) */

tap_dance_action_t tap_dance_actions[] = {
    // Tap once for Escape, twice for Caps Lock
    [TD_NUM_ESC] = ACTION_TAP_DANCE_DOUBLE(KC_NUM, KC_ESC),
};

// clang-format off
const uint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [BASE] = LAYOUT_tenkey_27(
        KC_MUTE, KC_ESC, KC_DEL, KC_TAB, KC_BSPC,
        MC_1,  TD(TD_NUM_ESC), KC_PSLS,KC_PAST,KC_PMNS,
        MC_2,  KC_P7,  KC_P8,  KC_P9,  KC_PPLS,
        MC_3,  KC_P4,  KC_P5,  KC_P6,
        MC_4,  KC_P1,  KC_P2,  KC_P3,  KC_PENT,
        MO(FN),  KC_P0,          KC_PDOT         ),

    [FN] = LAYOUT_tenkey_27(
        RGB_TOG, BT_HST1, BT_HST2, BT_HST3, P2P4G,
        _______, RGB_MOD, RGB_VAI, RGB_HUI, _______,
        _______, RGB_RMOD,RGB_VAD, RGB_HUD, _______,
        _______, RGB_SAI, RGB_SPI, KC_MPRV,
        _______, RGB_SAD, RGB_SPD, KC_MPLY, _______,
        _______, RGB_TOG,          KC_MNXT          ),

    [L2] = LAYOUT_tenkey_27(
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______,          _______          ),

    [L3] = LAYOUT_tenkey_27(
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______,          _______          ),

    [L4] = LAYOUT_tenkey_27(
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______,          _______          ),

    [L5] = LAYOUT_tenkey_27(
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______, _______, _______,
        _______, _______, _______, _______, _______,
        _______, _______,          _______          )
};

// clang-format on
#if defined(ENCODER_MAP_ENABLE)
const uint16_t PROGMEM encoder_map[][NUM_ENCODERS][2] = {
    [BASE] = {ENCODER_CCW_CW(KC_VOLD, KC_VOLU)},
    [FN]   = {ENCODER_CCW_CW(RGB_VAD, RGB_VAI)},
    [L2]   = {ENCODER_CCW_CW(KC_VOLD, KC_VOLU)},
    [L3]   = {ENCODER_CCW_CW(RGB_VAD, RGB_VAI)},
    [L4]   = {ENCODER_CCW_CW(RGB_VAD, RGB_VAI)},
    [L5]   = {ENCODER_CCW_CW(RGB_VAD, RGB_VAI)},
};
#endif // ENCODER_MAP_ENABLE

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    if (!process_record_keychron_common(keycode, record)) {
        return false;
    }
    return true;
}

void process_combo_event(uint16_t combo_index, bool pressed) {
    switch(combo_index) {
        case EM_EMAIL:
            if (pressed) {
                SEND_STRING("name@example.com");
            }
        break;
    }
}

config.hについては

#define TAPPING_TERM 175
#define TAPPING_TERM_PER_KEY

としておきました。

ビルドが終わったらファームウェアを実際にQ0 Maxに書き込みます。qmk flashコマンドでもいけるようですが今回はqmk_toolboxというものを利用しました。

brew install qmk-toolbox

qmk_toolboxを起動し、ファームウェアのファイルを選択します。ログの表示部分には接続されているUSB機器などが一覧で表示されていると思います。で、このままではファームウェアは書き込みできません。Q0 Maxをファームウェアを書き込み可能なDFUモードに入れる必要があります。それにはケーブル接続モードにしてからケーブルを抜き、「0」キーと「.」キーを外してそれらのキーの中間にある穴からアクセスできるリセットボタンをクリップの先などで押しながらUSBケーブルを接続します。

赤丸内の穴からリセットボタンにアクセスできます

するとqmk_toolboxのログ部分に

STM32 DFU device connected: STMicroelectronics STM32  BOOTLOADER

と表示されるのでqmk_toolboxの「Flash」ボタンをクリックできるようになります。

クリックするとQ0 Maxのファームウェアの消去と書き込みが始まり、

> Download done.
> File downloaded successfully
> Submitting leave request...
Flash complete
> Transitioning to dfuMANIFEST state
STM32 DFU device disconnected: STMicroelectronics STM32  BOOTLOADER

となれば完了です。

カスタムファームウェアを試してみる

では無事に書き込み完了できたので新しいファームウェアを試してみます。

まずはcombo機能のテスト、「-」キーと「+」キーを同時に押します。数字キーならうっかり同時に押すこともなさそうなのでこちらにアサインしました。で、「name@example.com」と入力されて、こちらはきちんと動いています。

次にTap Dance機能のテストです。今回は「num」キーをダブルタップすると「ESC」が入力される実装です。
こちらもオンラインのキーボードテストツールなどを利用して「num」キーをダブルタップして「ESC」が入力されるのを確認できました。

最後にBootMagic機能ですが、デフォルトではROW0のCOLUMN0のキーを押しながらケーブルを接続するとDFUモードに入るとのことでした。今回ROW0とCOLUMN0のキーはロータリーエンコーダーのボタンになると思っていたのですが、押しながらケーブル接続してもDFUモードに入りません。試しにその横の「⚪︎」キーを押しながらケーブル接続するとDFUモードになりました。釈然としないものの、DFUモードに簡単に入れるようになったのは嬉しいですね。ファームウェアをカスタマイズするたびにキーを引っこ抜いてピンでリセットボタンを押しながらケーブル接続するのってダルそうだったので。その後よく見直してみたらencoderディレクトリ直下のconfig.hで「⚪︎」キーがBootMagic用に指定されていました、なんと。
ちなみに上では

BOOTMAGIC_ENABLE = yes

で指定していましたが、今はyesではなく、fullかliteで指定するのが正しいみたいです。自分のはliteに変更しておこうかと。qmkのドキュメントサイトはリポジトリのdocsに追随できていないみたいなので今後はリポジトリのドキュメントを参照していくことにします。で、いざliteを指定したらコンパイルに失敗します。どうやらkeychronのフォークリポジトリはqmkのちょっと古いバージョンみたいなのでkeychronのリポジトリのドキュメントを参照することにします!

ファームウェアの書き込みに伴ってBluetoothの設定はクリアされるかなと思っていましたがホストの接続設定などはそのままでした。多分Bluetoothや無線の設定については別ファームウェアなのかも。2.4GHzの無線接続も問題なしです。

ということでひとまずファームウェアの試験的なカスタマイズと書き込みまで完了です。あとは自分の思うがままにキーカスタマイズしていきます。

カスタマイズを仕上げていく

Leaderキーとの出会い

いろいろとドキュメントを漁っているうちにLeaderキーの存在に気がつきました。
LeaderキーというのはVim系のエディタを使っている人であればすぐにわかると思いますが、ある特定のキーをタップした後にその他のキータップのコンビネーションでとやりたい機能を出力します。

例えば以前Combo機能を使って設定したメールアドレス入力をLeaderキーで置き換えてみます

  • スペースバーをタップ

  • 「+」をタップ

  • 「-」をタップ

  • (メールアドレスを入力)

とすることができます。ここの「+」や「-」キーなどのコンビネーションは好きに設定できるため、自由度が格段にアップします。

Q0 Maxで例えばM5キーのシングルタップをリーダーキーに設定して、

  • 「M5」をタップ

  • 「3」をタップ

  • -> レイヤー3をトグル

なんていうことができるんですね。これで貴重なキーをレイヤーのトグルに割り当てる必要がなくなるんです。レイヤーの切り替えを数字キーに割り当てておけば忘れることもなさそうですしいいことずくめ。

ということでM5キーへの割り当てはタップダンスとLeaderキーを組み合わせて

  • シングルタップ:リーダーキー

  • ホールド:MO(1)、押しながら別キーを押すとLayer1でアサインされたキーが入力される

  • ダブルタップ:レイヤー0に戻る

とすると使い勝手が良さそうです。

キーボードマクロの登録

なんかキーボードマクロで嬉しいことあったっけなと考えてもなかなか良いものが思いつきません。qmkですとダイナミックマクロと言ってキーボードでマクロ記録&再生することもできるのですがさすがに10キーですとあまり使いどころがなさそうです。
MacだとSpotlightを呼び出し(自分の環境だとCmd+Space)、"chrome"と入力、さらにEnterとすることでボタンひとつでChromeを立ち上げたりはできますが、これそんなに意味のあるマクロじゃないなって思っちゃうんです。
ということでマクロはKeyboard Maestroみたいにキーボード本体よりもシステム側にあるやつのほうが良さげな気がします。実は上でcombo機能によって実現したメールアドレスの入力なんかもIMEが日本語のかな入力に設定されているとめっちゃ残念な結果になるんですよね。なので結果としてはキーボードマクロはあまり使わないということに。OBSを立ち上げた後にStart Virtual Cameraしてくれるくらいのマクロならいいかも(カメラの入力にBlackmagicのUltraStudio Recorder 3Gを使っていて、そのままだとWebCamとして認識されずOBS経由にする必要があるため)。ただこれは1台のコンピュータに限った話で全台でできるものではなく……、歯切れ悪い感じ。

レイヤーの使い分け

ということで各レイヤーの設定です。いったん0から9までの10枚を想定します。10キーだとデフォルトの4枚はちょっと少ないですよね。qmkカスタマイズファームウェアを使う上でこのレイヤーの枚数を増やせるのとタップダンス機能がなんとも嬉しいです。

  • レイヤー0

    • 基本レイヤー、10キー

  • レイヤー1

    • 基本の拡張レイヤー、Bluetoothの接続先の切り替え、カーソルキーに加えてHOMEやENDなど

  • レイヤー2

    • Intellijショートカット、debug中の操作もろもろ

  • レイヤー3

    • Xcodeショートカット、これもdebug中の操作などをIntellijのショートカットと合わせたい

  • レイヤー4

    • iZotope RXショートカット、ズームイン&アウト、各種ノイズリダクションツールの実行

  • レイヤー5

    • Pro Toolsショートカット、全然足りないような気もしますが……

  • レイヤー6

    • DaVinci Resolveショートカット、Speed Editorでは足りない部分を補完します

  • レイヤー7

    • Blenderショートカット、Blenderは10キーを利用するショートカットが結構あるんです。ただメインのキーボードのModifierキーなどは検出できないのでModifierキーは10キー側にアサインして利用します。

  • レイヤー8

    • Houdiniショートカット

  • レイヤー9

    • 照明設定系、カスタムファームウェアを利用するようになったことで簡単にはこの辺を設定できなくなったため

とこんな感じにしようと。調べていくとMIDIとかシーケンサーなど面白そうな機能もあります。ただこれらはケーブル接続じゃないと動作しなさそうなので一旦保留。

keymap.cのソース部分は

enum layers {
    BASE,
    FN,
    L2,
    L3,
    L4,
    L5,
    L6,
    L7,
    L8,
    L9
};

として定義します。
レイヤーに関してはkeymapsとencoder_mapに定義をコピペで増やしておきました。

レイヤー0の設定

ほぼデフォルトのままのレイアウトを採用します。変更点はいくつかあってエンコーダーの部分とMキーの部分。
M5はTap DanceでシングルタップはLeaderキーに、ホールドでレイヤー1を指定します。

まずはLeaderキーの設定から

rules.mkに

LEADER_ENABLE = yes

を追加します。

これでキーコードのQK_LEADがアサイン可能となり、コールバックが使えるようになります。コールバックでよく使うのは下の2つ。

void leader_start_user(void) {
  // 押された時の処理
}

void leader_end_user(void) {
  // リーダーキーに続いて押されたキーを判別
  // leader_sequence_one_key(), leader_sequesnce_two_keys(), leader_sequence_three_keys()などが使える
}

config.hで指定する項目としては

#define LEADER_PER_KEY_TIMING
#define LEADER_TIMEOUT 360

と言ったものがあり、LEADER_PER_KEY_TIMINGを使うとシーケンス内の各キー入力ごとにタイミングがリセットされます。逆に使わないとLEADER_TIMEOUTで設定した時間内にシーケンスを完了する必要があります。今回はLeaderキーはレイヤーの移動にしか使わないのでLEADER_PER_KEY_TIMINGを使う必要はなさそうです。

今回レイヤーは0から9までの10枚のみなのでleader_sequence_one_keyを利用してレイヤー移動を実現できます。例えばleader_sequence_one_key(KC_P5)でLeaderキーの後にキーパッドの5がタップされたことを検出みたいな感じ。
もし10枚からMaxの32枚までを設定するのであればleader_sequence_two_keysを利用して、例えばレイヤー12を指定する場合はleader_sequence_two_keys(KC_P1, KC_P2)などとして検出できます。

さて、Leaderキーの設定の前にTap Danceの設定をしておきます。今回はM5キーをシングルタップでリーダーキー、ホールドでレイヤー1のモメンタリを設定します。

と、ここでMod Tapの説明を読んでいたんですが、Tap Danceを利用しなくてもMod Tapの機能だけでいけそうということに気がつきました。

The Mod-Tap key MT(mod, kc) acts like a modifier when held, and a regular keycode when tapped. In other words, you can have a key that sends Escape when you tap it, but functions as a Control or Shift key when you hold it down.

レイヤー0はMod Tapを利用してタップとホールドを、それ以外のレイヤーはTap Danceを利用してタップ(リーダー)とダブルタップ(レイヤー0のトグル)にしたほうが良さそう。

ということでレイヤー0のM5キーには

MT(MO(FN), QK_LEAD)

とするだけで良さそう。

で、Layerのドキュメント読んでいたらさらにLayer Tapの機能でも実現できるというか、こちらの方が良いのではということに気がつきました(気づいてばっかり

LT(FN, QK_LEAD)

で良さそう(令和最新版)
なんか、Mod TapってModifier Tapの略で、要するにModifierキーと組み合わせて利用するのかなと。

引き続いてQK_LEAD、Leaderキーの動作を実装していきます。

static uint8_t saved_layer = 0; // 現在のレイヤーを保持
static bool numpad_key_pressed = false; // リーダーキーに続いてnumPadキーが押されたかどうかを記録

void leader_start_user(void) {
    // 現在のレイヤーを保存し、レイヤー0に移動
    saved_layer = get_highest_layer(layer_state);
    layer_move(0);
    numpad_key_pressed = false; // 初期化
}

void leader_end_user(void) {
    for (uint8_t i = 1; i <= 9; i++) {
        if (leader_sequence_one_key(KC_P1 + (i - 1))) {
            layer_move(i); // numPadキー P1〜P9: レイヤー1〜9に移動
            numpad_key_pressed = true;
            return; // 処理を終了
        }
    }

    // numPadキーが押されなかった場合、元のレイヤーに戻る
    if (!numpad_key_pressed) {
        layer_move(saved_layer);
    }
}

KC_P1からKC_P9の定義は

KC_KP_1 = 0x0059,
KC_KP_2 = 0x005A,
KC_KP_3 = 0x005B,
KC_KP_4 = 0x005C,
KC_KP_5 = 0x005D,
KC_KP_6 = 0x005E,
KC_KP_7 = 0x005F,
KC_KP_8 = 0x0060,
KC_KP_9 = 0x0061,

となっているので上の関数のようにKC_P1 + (i -1)としてうまいこと省略できます。

次にレイヤー1から9で、M5キーをシングルタップでLeaderキー、ダブルタップした際にレイヤー0に戻るようにタップダンスを仕込みます。
今はnumキーダブルタップでESCになっている部分に定義を追加します

//enumにも追加
enum {
    TD_NUM_ESC,
    TD_LEAD_BASE
};

tap_dance_action_t tap_dance_actions[] = {
    [TD_NUM_ESC] = ACTION_TAP_DANCE_DOUBLE(KC_NUM, KC_ESC),
	[TD_LEAD_BASE] = ACTION_TAP_DANCE_DOUBLE(QK_LEAD, TG(BASE))
};

// FNのキーマップのM5ボタンのところにTD(TD_LEAD_BASE)として記述しておく

引き続いて小さな変更を入れます。エンコーダープッシュのKC_MUTEはオーディオインターフェイスを使っていてあまり意味ないのでCTRL + SPACEに変更します(自分の環境だとIMEの切り替え)。さらにエンコーダーの回転操作をカーソル左とカーソル右に振ってしまいます。

さて、これらを組み込んだLayer0の設定は

[BASE] = LAYOUT_tenkey_27(
    C(KC_SPC),       KC_ESC,         KC_DEL, KC_TAB, KC_BSPC,
    MC_1,            TD(TD_NUM_ESC), KC_PSLS,KC_PAST,KC_PMNS,
    MC_2,            KC_P7,          KC_P8,  KC_P9,  KC_PPLS,
    MC_3,            KC_P4,          KC_P5,  KC_P6,
    MC_4,            KC_P1,          KC_P2,  KC_P3,  KC_PENT,
    LT(FN, QK_LEAD), KC_P0,                        KC_PDOT   ),

となりました。
encoder_mapの定義は

[BASE] = {ENCODER_CCW_CW(KC_LEFT, KC_RGHT)},

と変更しています。

この辺まででコンパイルが通ることは確認していたのですが実機に書き込んで確認はしていなかったのでひとまず書き込んでみます。

結果

  • Layer Tap:動いてる

  • Leaderキー:動いてない?

  • IME切り替え:動いてる

  • エンコーダー:動いてる

ということになりました。
QK_LEAD単発だと行けるのかな……ということでM4キーをQK_LEADに書き直して再度トライです。

ひとまずレイヤー番号を適当にキー出力させてみたところ、M4キータップでのLEADERキーは正しく動作してレイヤーの移動もできています。ダブルタップでレイヤー0に戻る動作もOKです。ですが相変わらずM5キーをタップするとエンターキーのような挙動を示します。
んー。

ということでLT(FN, QK_LEAD)は動作していませんでした

調べてみたところ、Layerのドキュメントに

Currently, the layer argument of LT() is limited to layers 0-15, and the kc argument to the ~Basic Keycode set~, meaning you can't use keycodes like LCTL(), KC_TILD, or anything greater than 0xFF. This is because QMK uses 16-bit keycodes, of which 4 bits are used for the function identifier and 4 bits for the layer, leaving only 8 bits for the keycode.

https://github.com/Keychron/qmk_firmware/blob/master/docs/feature_layers.md

と書かれていて、つまりQK_LEADはLTの引数に渡せないということですね。

タップダンスも考えてみたものの、タップダンスはタイマーを持っていて、hold判定するのに一定の時間が必要です。つまりM5キーを押されて例えば175msたってはじめてタップかホールドかを判定されます。そこでタップだった場合に今度はLeaderキーのタイマーが始まるのでレイヤー移動のためのキーを押すタイミングは

M5をタップ ->175ms経過 -> Leaderキーのタイマー開始 ->(ココ) -> タイマー終了でLeaderキー処理

となるんですよね。使い勝手悪そうすぎる……

その後いくつか試してみましたが改善せず、一旦アレです、LayerTapとLeaderキーの両立からは撤退です!

さて、方針の変更です。タイマーの絡むものをさらにタイマーの絡むものと組み合わせるのはムズい、つまり今回のM5キーをLayer Tapでホールド時にMO(FN)、タップ時にQK_LEAD動作させるのは難しそうという結果が出たのでM5キーの動作を下のようにすることにしました。

  • Baseレイヤー

    • タップ:スペース

    • ホールド:MO(FN)動作

  • Baseレイヤー以外

    • タップ:スペース

    • ダブルタップ:Baseレイヤーに帰還

    • ホールド:MO(FN)動作

この変更に伴っていったんM4キーをLeaderキーに設定します。

さて、このM5キー動作を実装します。Tap Dance一択ですね。

Tap Danceで利用可能なマクロは

ACTION_TAP_DANCE_DOUBLE(kc1, kc2): Sends the kc1 keycode when tapped once, kc2 otherwise. When the key is held, the appropriate keycode is registered: kc1 when pressed and held, kc2 when tapped once, then pressed and held.
ACTION_TAP_DANCE_LAYER_MOVE(kc, layer): Sends the kc keycode when tapped once, or moves to layer. (this functions like the TO layer keycode).
ACTION_TAP_DANCE_LAYER_TOGGLE(kc, layer): Sends the kc keycode when tapped once, or toggles the state of layer. (this functions like the TG layer keycode).
ACTION_TAP_DANCE_FN(fn): Calls the specified function - defined in the user keymap - with the final tap count of the tap dance action.
ACTION_TAP_DANCE_FN_ADVANCED(on_each_tap_fn, on_dance_finished_fn, on_dance_reset_fn): Calls the first specified function - defined in the user keymap - on every tap, the second function when the dance action finishes (like the previous option), and the last function when the tap dance action resets.
ACTION_TAP_DANCE_FN_ADVANCED_WITH_RELEASE(on_each_tap_fn, on_each_release_fn, on_dance_finished_fn, on_dance_reset_fn): This macro is identical to ACTION_TAP_DANCE_FN_ADVANCED with the addition of on_each_release_fn which is invoked every time the key for the tap dance is released. It is worth noting that on_each_release_fn will still be called even when the key is released after the dance finishes (e.g. if the key is released after being pressed and held for longer than the TAPPING_TERM).

となっており、今回はACTION_TAP_DANCE_FN_ADVANCEDを利用します。以下keymap.cの該当部分

void td_spc_lmove_mo_finished(tap_dance_state_t *state, void *user_data) {
    if (state->count == 1) {
        // タップでスペース
        tap_code(KC_SPC);
    } else if (state->count == 2) {
        // 2回タップでレイヤー移動(レイヤー0に移動)
        layer_move(BASE);
    }
}

void td_spc_lmove_mo_reset(tap_dance_state_t *state, void *user_data) {
    if (state->pressed) {
        // ホールドでMO(FN)
        layer_on(FN);
    } else {
        // ホールド解除時にレイヤーFNをオフ
        layer_off(FN);
    }
}

tap_dance_action_t tap_dance_actions[] = {
    // Tap once for Escape, twice for Caps Lock
    [TD_NUM_ESC] = ACTION_TAP_DANCE_DOUBLE(KC_NUM, KC_ESC),
    [TD_SPC_TG_MO] = ACTION_TAP_DANCE_FN_ADVANCED(NULL, td_spc_lmove_mo_finished, td_spc_lmove_mo_reset),
};

ここまで決めたところでBASEレイヤー(レイヤー0)は

[BASE] = LAYOUT_tenkey_27(
    C(KC_SPC),       KC_ESC,         KC_DEL, KC_TAB, KC_BSPC,
    QK_LEAD,         TD(TD_NUM_ESC), KC_PSLS,KC_PAST,KC_PMNS,
    KC_LCTL,         KC_P7,          KC_P8,  KC_P9,  KC_PPLS,
    KC_LOPT,         KC_P4,          KC_P5,  KC_P6,
    KC_LCMD,         KC_P1,          KC_P2,  KC_P3,  KC_PENT,
    TD(TD_SPC_TG_MO),       KC_P0,           KC_PDOT        ),

結局無難な感じに。QK_LEADはM1キーに振ったのと、左の列はModifierキーに。

FNレイヤー(レイヤー1)もキーマップを決めてしまいます。が……

[FN] = LAYOUT_tenkey_27(
    RGB_TOG, BT_HST1, BT_HST2, BT_HST3, P2P4G,
    _______, _______, _______, _______, _______,
    _______, KC_HOME, KC_UP,   KC_END,  _______,
    _______, KC_LEFT, KC_DOWN, KC_RGHT, _______,
    _______, KC_PGDN, _______, KC_PGUP, _______,
    _______,     _______,      _______          ),

これめっちゃ悩みます。
M5を押さえながら打ち込むのに良さげなものがなかなか見つかりません。

で、ビルドしてテストしてみたところレイヤーの「+」キーが動作しておらず、なぜか「4」が出力されます……なぜ???

これは単に

bool process_record_user(uint16_t keycode, keyrecord_t *record) {
    if (!process_record_keychron_common(keycode, record)) {
        return false;
    }
    if (keycode == KC_PPLS) {
        if (record->event.pressed) {
            // 現在のレイヤーを取得
            uint8_t current_layer = get_highest_layer(layer_state);

            // 数値を文字列に変換
            char layer_string[3]; // 最大2桁 + NULL文字
            snprintf(layer_string, sizeof(layer_string), "%u", current_layer);

            // レイヤー番号を送信
            SEND_STRING(layer_string);
        }
        return false;
    }
    return true;
}

としていたからでした(プラスキーを押すとレイヤーの番号をデバッグ用に出力していました)
ソースが長くなってくると読みにくくなってきます。動作確認をしながら1ステップづつ進めていきましょう。

まずはレイヤー0の確認から

  • エンコーダーキーのタップでIME切り替え

  • NUMキーのタップでNUM、ダブルタップでESC

  • M5キーのタップでスペース、ダブルタップでベースレイヤー移動、ホールドでレイヤー1

エンコーダーキーのタップでIME切り替えは動作しています。ただIMEの切り替えが2回走って結局元に戻っていそうな挙動がよくあります。エンコーダー部分ってキー入力用のスイッチを使っているわけではないのでこの辺が弱いんでしょうね。いちおうこのような挙動については
https://github.com/qmk/qmk_firmware/blob/master/docs/feature_debounce_type.md
に対応策が載っているんですが、これキー単位で設定するの面倒そうです。結局複数入力しても問題ないキーをここに割り当てるのが最善策のようですね。なるほどなー

NUMキーダブルタップでESC動作も確認できました。で、調べてみるとNUMキーってMacにはないんですね。Macで10キーで数字を入力すると強制的に半角入力になるようです。普通にESCへのアサインでよかったですね……。

で、問題のM5キーの動作ですが、タップとダブルタップの挙動は確認できました。ですがホールドでレイヤー1が効いていません。

五里霧中感を払拭するため、デバッグの環境を整えます。
qmk_firmware/docs/faq_debug.md at master · Keychron/qmk_firmware
上のドキュメントを読むと
rules.mkに

CONSOLE_ENABLE = yes

を追加して

#include "print.h"

void keyboard_post_init_user(void) {
  // Customise these values to desired behaviour
  debug_enable=true;
  debug_matrix=true;
  //debug_keyboard=true;
  //debug_mouse=true;
}

のようなものをkeymap.cに追加すると(しなくても良いかも?)

  • print("string"): Print a simple string.

  • uprintf("%s string", var): Print a formatted string

  • dprint("string") Print a simple string, but only when debug mode is enabled

  • dprintf("%s string", var): Print a formatted string, but only when debug mode is enabled

といったデバッグ出力用の関数が使えるようになるとのことです。なのでrules.mkを修正して、CONSOLE_ENABLE = yesを追加するとビルドに失敗します……

Compiling: ./keyboards/keychron/common/wireless/indicator.c                                        In file included from tmk_core/protocol/chibios/usb_main.h:15,
                 from ./keyboards/keychron/common/wireless/indicator.c:26:
tmk_core/protocol/usb_descriptor.h:285:6: error: #error There are not enough available endpoints to support all functions. Please disable one or more of the following: Mouse Keys, Extra Keys, Console, NKRO, MIDI, Serial, Steno
  285 | #    error There are not enough available endpoints to support all functions. Please disable one or more of the following: Mouse Keys, Extra Keys, Console, NKRO, MIDI, Serial, Steno
      |      ^~~~~

なんか一杯一杯で無理っすっていうエラーのようなのでNKROをオフにします。10キーでNキーロールオーバー(初めて知った)は必要なさそう。NKROがどこで有効化されているのか探っていったところ

q0_maxディレクトリ直下のinfo.jsonに

"features": {
    "bootmagic": true,
    "extrakey" : true,
    "mousekey" : true,
    "dip_switch" : false,
    "encoder": true,
    "encoder_map": true,
    "nkro" : true,
    "rgb_matrix": true,
    "raw" : true,
    "sendstring" : true
},

という記述を発見しました。このinfo.jsonはrules.mk的な役割も果たしているようで、ということは最初からBootMagic有効化されていたんですね……。
ちょっとinfo.jsonでもfeatureの追加ができるのか気になって、こちら側に

"console": true

を追加して、NKROをfalseにして再度挑戦。それでもダメだったのでextrakeyやらmousekeyもfalseにしてようやくconsoleをtrueでコンパイルできました。

void td_spc_lmove_mo_finished(tap_dance_state_t *state, void *user_data) {
    if (state->count == 1) {
        tap_code(KC_SPC);
        dprint("single tap KC_SPC\n");
    } else if (state->count == 2) {
        layer_move(0);
        dprint("double taps move to layer 0\n");
    }
}

void td_spc_lmove_mo_reset(tap_dance_state_t *state, void *user_data) {
    if (state->pressed) {
        // ホールドでMO(FN)
        layer_on(1);
        dprint("Layer moved to 1\n");
    } else {
        // ホールド解除時にレイヤー1をオフ
        layer_off(1);
        dprint("Layer moved back from 1\n");
    }
}

さてこれで挑戦。ホールドが効かないということはlayer_on(1)の処理をできていない予感。

qmk toolboxのコンソールにもつながっていることを確認していざM5キーの動作確認。

> single tap KC_SPC <-- シングルタップ
> Layer moved back from 1
> single tap KC_SPC <-- ホールド
> Layer moved back from 1
> double taps move to layer 0 <-- ダブルタップ
> Layer moved back from 1

はい、ホールド効いていないですね。
あとLayer moved back from 1というのがタップの解除の際にも実行されてしまっています。やり直し。

調べ直しているうちにドキュメントにやりたいことそのまんまなサンプルがあったのでした……
qmk-firmware/docs/feature_tap_dance.md at master · samhocevar-forks/qmk-firmware
ところがこれは6年前で更新が止まっているドキュメントなのでさっくり動くわけではありません。ちょこちょことアップデートしてビルド

で、ようやく動きました………

//Functions that control what our tap dance key does
void ql_finished (tap_dance_state_t *state, void *user_data) {
    ql_tap_state.state = cur_dance(state);
    switch (ql_tap_state.state) {
        case SINGLE_TAP:
            tap_code(KC_SPC);
            dprint("single tap KC_SPC\n");
        break;
        case SINGLE_HOLD:
            layer_on(FN);
            dprint("hold and layer moved to 1\n");
        break;
        case DOUBLE_TAP:
            //check to see if the layer is already set
                if (IS_LAYER_ON(FN)) {
                    //if already set, then switch it off
                    layer_off(FN);
                    dprint("double tap layer hold off and layer off\n");
                } else {
                    //if not already set, then switch the layer on
                    layer_move(BASE);
                    dprint("move to base layer\n");
                }
        break;
    }
}

void ql_reset (tap_dance_state_t *state, void *user_data) {
    //if the key was held down and now is released then switch off the layer
    if (ql_tap_state.state==SINGLE_HOLD) {
        layer_off(FN);
        dprint("reset: hold off and layer FN off\n");
    }
    ql_tap_state.state = 0;
}

tap_dance_action_t tap_dance_actions[] = {
    [TD_SPC_LAYR] = ACTION_TAP_DANCE_FN_ADVANCED(NULL, ql_finished, ql_reset),
    // Tap once for Escape, twice for Caps Lock
    [TD_NUM_ESC] = ACTION_TAP_DANCE_DOUBLE(KC_NUM, KC_ESC),
    [TD_SPC_TG_MO] = ACTION_TAP_DANCE_FN_ADVANCED(NULL, td_spc_lmove_mo_finished, td_spc_lmove_mo_reset),
};

ふぇぇ、そんなに難しくもないはずなのに躓きまくり。

さて、ここまでで一番の山場であるTap Dance機能の動作は確認できたのでキーマッピングのやり直しです。

まずもったいないですがq0_max直下のinfo.jsonを編集してコンソール機能をオフにするなどします。じゃないとメディアキー関連のキーが使えません。

"features": {
    "bootmagic": true,
    "extrakey" : true,
    "mousekey" : true,
    "dip_switch" : false,
    "encoder": true,
    "encoder_map": true,
    "nkro" : true,
    "rgb_matrix": true,
    "raw" : true,
    "sendstring" : true,
    "console": false
},

で、ベースのキーマップを再び変更です。

  • エンコーダープッシュは結局Raycastの起動に割り当てました。Cmd + Shift +「/」(=G(S(KC_SLSH)))

  • numキーは「=」に割り当てます

[BASE] = LAYOUT_tenkey_27(
    G(S(KC_SLSH)),   KC_ESC, KC_DEL, KC_TAB, KC_BSPC,
    QK_LEAD,         KC_EQL, KC_PSLS,KC_PAST,KC_PMNS,
    KC_LCTL,         KC_P7,  KC_P8,  KC_P9,  KC_PPLS,
    KC_LOPT,         KC_P4,  KC_P5,  KC_P6,
    KC_LCMD,         KC_P1,  KC_P2,  KC_P3,  KC_PENT,
    TD(TD_SPC_LAYR),    KC_P0,       KC_PDOT        ),

FNレイヤー(レイヤー1)は

  • NOOP(何もしない、「XXXXXXX」)で間違えやすそうなやつを無効化

[FN] = LAYOUT_tenkey_27(
    RGB_TOG, BT_HST1, BT_HST2, BT_HST3, P2P4G,
    _______, _______, _______, _______, _______,
    _______, KC_HOME, KC_UP,   KC_END,  _______,
    _______, KC_LEFT, KC_DOWN, KC_RGHT,
    _______, KC_PGDN, XXXXXXX, KC_PGUP, _______,
    _______,     XXXXXXX,      XXXXXXX          ),

で設定します。

デフォルトのFNはL9に移動

[L9] = LAYOUT_tenkey_27(
    RGB_TOG, BT_HST1, BT_HST2, BT_HST3, P2P4G,
    _______, RGB_MOD, RGB_VAI, RGB_HUI, _______,
    _______, RGB_RMOD,RGB_VAD, RGB_HUD, _______,
    _______, RGB_SAI, RGB_SPI, KC_MPRV,
    _______, RGB_SAD, RGB_SPD, KC_MPLY, _______,
    _______, RGB_TOG,          KC_MNXT          ),

さて、一旦ビルドしてうまく動いているかを確認します。

無事動いている様子です。

あとはアプリケーションのショートカットキーを設定していくだけです。
アプリ側にショートカットキーを設定する場合には10キーの数字のキーコードはキーボード上段に一列に並ぶ数字のキーコードとは違うので気をつけながら設定していきます。
例えば自分はMacでAerospaceというタイリングウィンドウマネージャを利用していて、option+数字キーで目的のウィンドウにジャンプするのですが

    alt-1 = 'workspace 1'
    alt-2 = 'workspace 2'
    alt-3 = 'workspace 3'
    alt-4 = 'workspace 4'
    alt-5 = 'workspace 5'
    alt-6 = 'workspace 6'
    alt-7 = 'workspace 7'
    alt-8 = 'workspace 8'
    alt-9 = 'workspace 9'
    alt-keypad1 = 'workspace 1'
    alt-keypad2 = 'workspace 2'
    alt-keypad3 = 'workspace 3'
    alt-keypad4 = 'workspace 4'
    alt-keypad5 = 'workspace 5'
    alt-keypad6 = 'workspace 6'
    alt-keypad7 = 'workspace 7'
    alt-keypad8 = 'workspace 8'
    alt-keypad9 = 'workspace 9'

のように10キー用のショートカットを用意する必要があります。

ここまででようやく山場は越えた感じです。つづく、かも。