⌨️

Vial-QMKのファームウェアでkey_overridesを書いてロータリーエンコーダを拡張する

に公開

はじめに

前回の記事で、Vial GUIのKey Overridesを使って、ロータリーエンコーダを修飾キーで拡張する記事を書きました。
https://zenn.dev/karbou12/articles/e59289cbcefb5d

キーボード毎にVial GUIでKey Overridesを設定したりVilファイルを直接弄るのは面倒なので、ファームウェアで書きたくなりますよね。というわけで、今回はVial-QMKのファームウェアでKey Overridesを書いて、ロータリーエンコーダを拡張する説明となります。

QMK Key Overridesの設定

QMKのドキュメントに書かれている通り、keymap.ckey_override_t *key_overrides[]のglobal変数を定義する必要があります。

keymap.c
const key_override_t delete_key_override = ko_make_basic(MOD_MASK_SHIFT, KC_BSPC, KC_DEL);

// This globally defines all key overrides to be used
const key_override_t *key_overrides[] = {
	&delete_key_override
};

私のロータリーエンコーダ拡張の場合、以下のようなコードになります。

keymap.c
// negative_maskを設定する際に、layerの引数を省略するためのマクロを定義
#define ko_make_with_negmods(trigger_mods, trigger_key, replacement_key, negative_mask) \
    ko_make_with_layers_and_negmods(trigger_mods, trigger_key, replacement_key, ~0, negative_mask)

// Vial GUIの設定をコードにしただけなので詳細説明は省略
const key_override_t wheel_left_override  = ko_make_with_negmods(MOD_BIT(KC_LSFT), MS_WHLU, MS_WHLL, MOD_BIT(KC_LALT) | MOD_BIT(KC_LCTL) | MOD_BIT(KC_LGUI));
const key_override_t wheel_right_override = ko_make_with_negmods(MOD_BIT(KC_LSFT), MS_WHLD, MS_WHLR, MOD_BIT(KC_LALT) | MOD_BIT(KC_LCTL) | MOD_BIT(KC_LGUI));

const key_override_t arrow_left_override  = ko_make_with_negmods(MOD_BIT(KC_LALT), MS_WHLD, KC_LEFT, MOD_BIT(KC_LCTL));
const key_override_t arrow_right_override = ko_make_with_negmods(MOD_BIT(KC_LALT), MS_WHLU, KC_RGHT, MOD_BIT(KC_LCTL));

const key_override_t arrow_up_override    = ko_make_basic(MOD_BIT(KC_LALT) | MOD_BIT(KC_LCTL), MS_WHLD, KC_UP);
const key_override_t arrow_down_override  = ko_make_basic(MOD_BIT(KC_LALT) | MOD_BIT(KC_LCTL), MS_WHLU, KC_DOWN);

const key_override_t app_prev_override    = ko_make_basic(MOD_BIT(KC_LGUI), MS_WHLD, LSG(KC_TAB));
const key_override_t app_next_override    = ko_make_basic(MOD_BIT(KC_LGUI), MS_WHLU, LGUI(KC_TAB));

const key_override_t tab_prev_override    = ko_make_with_negmods(MOD_BIT(KC_LCTL), MS_WHLD, LSFT(LCTL(KC_TAB)), MOD_BIT(KC_LALT));
const key_override_t tab_next_override    = ko_make_with_negmods(MOD_BIT(KC_LCTL), MS_WHLU, LCTL(KC_TAB),   MOD_BIT(KC_LALT));

const key_override_t *key_overrides[] = {
    &wheel_left_override,
    &wheel_right_override,
    &arrow_left_override,
    &arrow_right_override,
    &arrow_up_override,
    &arrow_down_override,
    &app_prev_override,
    &app_next_override,
    &tab_prev_override,
    &tab_next_override,
};

このコードをVial-QMKでビルドすると、ビルドは通りますが設定通りに動作しません...
Vial-QMKのコードを見て原因を調査します。

Vial-QMKのコード分析

変数の定義

Vial-QMKのコードでkey_overridesを検索すると、以下の実装コードが見つかります。
!defined(VIAL_KEY_OVERRIDE_ENABLE)となっているので、VIALの場合は無効になっていますね…

quantum/keymap_introspection.c
#if defined(KEY_OVERRIDE_ENABLE) && !defined(VIAL_KEY_OVERRIDE_ENABLE)

uint16_t key_override_count_raw(void) {
    return ARRAY_SIZE(key_overrides);
}
...
#endif // defined(KEY_OVERRIDE_ENABLE)

key_override_tで検索すると、以下のstatic変数 vial_key_overridesが見つかります。Vialではこの変数を使っているようです。

quantum/vial.c
#ifdef VIAL_KEY_OVERRIDE_ENABLE
static key_override_t vial_key_overrides[VIAL_KEY_OVERRIDE_ENTRIES] = { 0 };
...

Vial reload_key_override()

vial_key_overridesは以下のstatic関数reload_key_override()で使われています。

quantum/vial.c
static void reload_key_override(void) {
    for (size_t i = 0; i < VIAL_KEY_OVERRIDE_ENTRIES; ++i)
        vial_get_key_override(i, &vial_key_overrides[i]);
}

reload_key_override()が呼んでいるvial_get_key_override()は以下のstatic関数です。

quantum/vial.c
static int vial_get_key_override(uint8_t index, key_override_t *out) {
    vial_key_override_entry_t entry;
    int ret;
    if ((ret = dynamic_keymap_get_key_override(index, &entry)) != 0)
        return ret;

    memset(out, 0, sizeof(*out));
    out->trigger = entry.trigger;
    out->trigger_mods = entry.trigger_mods;
    out->layers = entry.layers;
    out->negative_mod_mask = entry.negative_mod_mask;
    out->suppressed_mods = entry.suppressed_mods;
    out->replacement = entry.replacement;
    out->options = 0;
    uint8_t opt = entry.options;
    if (opt & vial_ko_enabled)
        out->enabled = NULL;
    else
        out->enabled = &vial_key_override_disabled;
    if (opt & vial_ko_option_activation_trigger_down) out->options |= ko_option_activation_trigger_down;
    if (opt & vial_ko_option_activation_required_mod_down) out->options |= ko_option_activation_required_mod_down;
    if (opt & vial_ko_option_activation_negative_mod_up) out->options |= ko_option_activation_negative_mod_up;
    if (opt & vial_ko_option_one_mod) out->options |= ko_option_one_mod;
    if (opt & vial_ko_option_no_reregister_trigger) out->options |= ko_option_no_reregister_trigger;
    if (opt & vial_ko_option_no_unregister_on_other_key_down) out->options |= ko_option_no_unregister_on_other_key_down;

    return 0;
}

dynamic_keymap_get_key_override()でeepromからデータを読み込んで、vial_key_override_entry_t entryに代入して、key_override_t outに代入して返していますね。

reload_key_override()は、reload_combo()と同様にmain()keyboard_setup()vail_init()と初期化処理の中で呼ばれています。つまり、起動時に変数が初期化されて、eepromにデータがあれば変数にセットされるということです。

Vial vial_handle_cmd()

dynamic_keymap_get_key_override()は、comboと同様に他にはvial_handle_cmd()から呼ばれています。

quantum/vial.c
void vial_handle_cmd(uint8_t *msg, uint8_t length) {
...
            case dynamic_vial_key_override_get: {
                uint8_t idx = msg[3];
                vial_key_override_entry_t entry = { 0 };
                msg[0] = dynamic_keymap_get_key_override(idx, &entry);
                memcpy(&msg[1], &entry, sizeof(entry));
                break;
            }

引数のmsg[1]entryを代入していますね。vial_handle_cmd()はVial GUIから呼ばれていると想定されます。

Vial key_override_get()

vial_key_overridesは他に以下のkey_override_get()から使われています。

quantum/vial.c
const key_override_t* key_override_get(uint16_t key_override_idx) {
    if (key_override_idx >= VIAL_KEY_OVERRIDE_ENTRIES)
        return NULL;
    return &vial_key_overrides[key_override_idx];
}

key_override_get()は以下の関数から呼ばれています。

quantum/process_keycode/process_key_override.c
static bool try_activating_override(const uint16_t keycode, const uint8_t layer, const bool key_down, const bool is_mod, const uint8_t active_mods, bool *activated) {
...
    for (uint8_t i = 0; i < key_override_count(); i++) {
        const key_override_t *const override = key_override_get(i);
...

実際の処理の箇所のようなので、ここは深追いしない方が良さそうです。

以上から、Vial-QMKを弄る場合はdynamic_keymap_get_key_override()を呼んでいる2箇所を修正すれば良さそうです。

Vial-QMKを弄らない方法の検討

Vial-QMKのソースコードを弄らない方法も模索します。
eepromにセットするにはdynamic_keymap_set_key_override()keymap.cで呼べばよさそうです。
Vial GUI用の変数vial_key_overridesはstatic変数で、それに値をセットしているのはeepromから読み込むvial_get_key_override(), reload_key_override()ですが、どちらもstatic関数のためkeymap.cからは呼べません。reload_key_override()を呼んでいるvial_init()はstaticではないのでkeymap.cから呼べますが、他のreload関数も呼ぶことになるので、影響範囲が広がるのが少し嫌ですね。初期化なので害はないと思いますが。

対策方法

A.Vial-QMKを弄る方法
B.Vial-QMKを弄らず、keymap.cからvial_init()を呼ばない方法
C.Vial-QMKを弄らず、keymap.cからvial_init()を呼ぶ方法
の3つの対策方法を紹介します。

A.Vial-QMKのソースコードを弄る方法

dynamic_keymap_set_key_override()に、keymap.cで定義したkey_overridesを代入するようにします。

https://github.com/karbou12/vial-qmk/commit/528e29756868210cc0a0bbb9ddcf331ed055f4d5

Vial-QMKのコードを弄る場合は、使いたい時だけコードが有効になるように、かつ、変更箇所が明確になるように、config.hでdefineし、コード実装箇所もifdefで囲むようにします。ここではUSE_LOCAL_KEY_OVERRIDESとします。

config.h
#define USE_LOCAL_KEY_OVERRIDES

keymap.ckey_overridesを定義します。また、配列のサイズをkey_overrides_raw_sizeで定義します。

keymap.c
#ifdef USE_LOCAL_KEY_OVERRIDES
// 記載済みのため省略
...
const key_override_t *key_overrides[] = {
...
};

const uint16_t key_overrides_raw_size = ARRAY_SIZE(key_overrides);
#endif

vial.cでextern宣言します。

quantum/vial.c
#ifdef USE_LOCAL_KEY_OVERRIDES
extern key_override_t *key_overrides[];
extern uint16_t key_overrides_raw_size;
#endif

主処理ですが、dynamic_keymap_get_key_override()でeepromからentryに読み込んだ後に、entrykey_overridesで上書きするようにします。また、optionvial_ko_enabledのbitを立てるようにします。
その際、entryが空でない、または、Vial GUIのdefault設定でない場合は、Vial GUIで何か設定している可能性があるので、上書きしないようにします。

quantum/vial.c
// helper関数
// 空 か defaultの場合はtrueを返す
#ifdef USE_LOCAL_KEY_OVERRIDES
static bool is_init_or_default_key_override(const vial_key_override_entry_t *entry) {
    vial_key_override_entry_t init_entry = { 0 };
    if (memcmp(&init_entry, entry, sizeof(vial_key_override_entry_t)) == 0) {
        return true;
    }

    init_entry.layers = ~0;
    init_entry.options = ko_options_default;

    return (memcmp(&init_entry, entry, sizeof(vial_key_override_entry_t)) == 0) ? true : false;
}
#endif
quantum/vial.c
void vial_handle_cmd(uint8_t *msg, uint8_t length) {
...
#ifdef VIAL_KEY_OVERRIDE_ENABLE
            case dynamic_vial_key_override_get: {
                uint8_t idx = msg[3];
                vial_key_override_entry_t entry = { 0 };
                msg[0] = dynamic_keymap_get_key_override(idx, &entry);

#ifdef USE_LOCAL_KEY_OVERRIDES
                if (idx < key_overrides_raw_size) {
                    if (is_init_or_default_key_override(&entry)) {
                        entry.trigger = key_overrides[idx]->trigger;
                        entry.trigger_mods = key_overrides[idx]->trigger_mods;
                        entry.layers = key_overrides[idx]->layers;
                        entry.negative_mod_mask = key_overrides[idx]->negative_mod_mask;
                        entry.suppressed_mods = key_overrides[idx]->suppressed_mods;
                        entry.replacement = key_overrides[idx]->replacement;
                        entry.options = key_overrides[idx]->options;
                        entry.options |= vial_ko_enabled;
                    }
                }
#endif

                memcpy(&msg[1], &entry, sizeof(entry));
                break;
            }
quantum/vial.c
static int vial_get_key_override(uint8_t index, key_override_t *out) {
    vial_key_override_entry_t entry;
    int ret;
    if ((ret = dynamic_keymap_get_key_override(index, &entry)) != 0)
        return ret;

#ifdef USE_LOCAL_KEY_OVERRIDES
    if (index < key_overrides_raw_size) {
        if (is_init_or_default_key_override(&entry)) {
            entry.trigger = key_overrides[index]->trigger;
            entry.trigger_mods = key_overrides[index]->trigger_mods;
            entry.layers = key_overrides[index]->layers;
            entry.negative_mod_mask = key_overrides[index]->negative_mod_mask;
            entry.suppressed_mods = key_overrides[index]->suppressed_mods;
            entry.replacement = key_overrides[index]->replacement;
            entry.options = key_overrides[index]->options;
            entry.options |= vial_ko_enabled;
        }
    }
#endif

    memset(out, 0, sizeof(*out));
    out->trigger = entry.trigger;
    ...

B.Vial-QMKを弄らず、keymap.cからvial_init()を呼ばない方法

Vial-QMKを弄らずにkeymap.cだけ修正し、keymap.cからvial_init()を呼ばない (変数に代入しない)方法です。

https://github.com/karbou12/vial-qmk/commit/7830e5853d4de7c0d7555bcd61b00800c5f21fdf

主処理はkeyboard_post_init_user()に実装します。
key_overridesentryに代入した後、dynamic_keymap_set_key_override()を呼んでeepromに保存します。

keymap.c
// 対策方法Aと同じく、key_overridesを定義します。ここでは省略。

// 対策方法Aと同じhelper関数を定義します。中身は同じなので省略
static bool is_init_or_default_key_override(const vial_key_override_entry_t *entry) {
...
}

void keyboard_post_init_user(void) {
    // check already key_override is there
    // eepromから1個目のkey_overrideをgetして、0でなければVialで設定済みとして処理しない
    {
        vial_key_override_entry_t entry = {0};
        if (dynamic_keymap_get_key_override(0, &entry) < 0) {
            return;
        }

        if (!is_init_or_default_key_override(&entry)) {
            return;
        }
    }

    for (uint8_t i = 0; i < key_overrides_raw_size; i++) {
        vial_key_override_entry_t entry = {0};
        entry.trigger = key_overrides[i]->trigger;
        entry.replacement = key_overrides[i]->replacement;
        entry.layers = key_overrides[i]->layers;
        entry.trigger_mods = key_overrides[i]->trigger_mods;
        entry.negative_mod_mask = key_overrides[i]->negative_mod_mask;
        entry.suppressed_mods = key_overrides[i]->suppressed_mods;
        entry.options = key_overrides[i]->options;
        entry.options |= vial_ko_enabled;

        // set to eeprom
        dynamic_keymap_set_key_override(i, &entry);
    }
}

この方法の場合、ファームウェア書き込み直後は、eepromには書き込みますが、Vial GUI用の変数には代入されません。そのため、ファームウェア書き込み直後は動作せず、一度キーボードを抜き差ししたら動作します。
これは、抜き差し後の起動処理でvial_init()が呼ばれてeepromから読み込んでVial GUI用の変数に代入するためです。影響範囲が広がるのが嫌という人向けの対応となりますが、抜き差しする前は更新前の挙動になるので色々弄っていると脳がバグります。

C.Vial-QMKを弄らず、keymap.cからvial_init()を呼ぶ方法

Vial-QMKを弄らずにkeymap.cだけ修正し、keymap.cからvial_init()を呼ぶ (変数に代入する)方法です。
コードとしては、対策方法Bに加えてkeyboard_post_init_user()の最後にvial_init()を呼ぶだけです。

keymap.c
void keyboard_post_init_user(void) {
...
    for (uint8_t i = 0; i < key_overrides_raw_size; i++) {
...
        dynamic_keymap_set_key_override(i, &entry);
    }

    // call vial_init() to load eeprom to variables.
    vial_init();
}

この方法の場合、ファームウェア書き込み直後にeepromには書き込んだ後、vial_init()を呼ぶため、eepromから読み込んでVial GUI用の変数には代入するため、キーボードを抜き差ししなくても動作します。

まとめ

Vial-QMKのコードを分析し、ファームウェアでkey_overridesを書いてロータリーエンコーダを拡張する3つの方法を紹介しました。
どの方法を参考にするにしても、冒頭に注意書きした通り、コードを十分に理解した上で参考にしてください。
また、かなり細かく分析内容を記述したつもりなので、同じように分析すれば、Combo以外のMacroやTap-Danceなどにも応用できると思いますので、今回の記事が参考になれば幸いです。

Discussion