見出し画像

Keyboard Quantizer Mini を使用してマウスを高機能オンボードメモリ化し、LogiOptions+ と同等のジェスチャを実装する

!:この記事の執筆者はQMKファームウェアに特別詳しいわけではないので誤った解説があるかもしれません。手順通りに進めればLogiジェスチャを実装できることは確認済みです。

全文無料で読めます。


はじめに

せきごんさん (https://x.com/_gonnoc) が開発・販売を行っているKeyboard Quantizer Mini(以下KQM)は、あらゆるUSBマウス・キーボードをQMK/Vial対応デバイスに変換できる便利なガジェットです。

PCにKQMのUSB-Aオス端子を差し、KQMのUSB-Aメス端子に使用したいマウス・キーボードのUSBケーブル / 2.4GHzUSBレシーバーを差すだけで使用できます。

KQMを介してPCに接続したデバイスは、QMKファームウェアデバイスのリマップWebアプリとして有名なVial・Remapにてボタン等のリマップが可能となります。
もちろんただリマップするだけでなく、レイヤー機能(MO, LTなど)やマクロ機能等も使えます。

しかしデフォルトのVialファームウェアでは、機能にやや物足りない部分があります。

特にデフォルトで用意されている「簡易ジェスチャ」は
・レイヤーキーを長押し→マウス or トラックボールを動かす→レイヤーキーを離す でようやくジェスチャが発動する
・ジェスチャ中カーソルが動く
という仕様で、これがまた使いにくい(とアライは思っています)。

この記事ではデフォルトファームウェアからLogiOptions+同等の挙動をするマウスジェスチャを追加実装します。

実装されるジェスチャの仕様

  • 発動条件: MO(モメンタリー)またはLT(レイヤータップ)キーを押しながらマウスを動かす

  • 方向判定: 上下左右の4方向(軸優先、移動量が多い方の軸で判定)

  • 発動タイミング: 閾値を超えた瞬間に即座に発動

  • カーソル凍結: ジェスチャ中はカーソルが動かない

  • 連続ジェスチャ: 発動後に累積がリセットされ、押しっぱなしのまま連続して別方向に発動できる

  • クールダウン: 誤発動防止のため、連続発動には150msの間隔が必要

アライはエレコムのHUGE PLUSという最高のハードウェアを持つのにソフトウェア(マウスアシスタント)があかんトラックボールをKQMの力でハードもソフトも最高のトラックボールに昇華させ、使用しています。(下記動画後半参照)

想定読者

  • LogiOptions+等の専用アプリを入れることができない端末 (社用PC、タブレットなど) でフルカスタムマウスを使用したい人

  • LogiOptions+のジェスチャを他メーカー製マウスでも使用可能にしたい人

かつ

  • QMKファームウェアに慣れている、あるいはそういった領域に苦手意識がない人

ファームウェアを直接書き換えたりもするため、最悪KQMが文鎮化する可能性もあります。
参考になればと思い記事を作成していますが、ファームウェアを書き換える場合は自己責任でよろしくお願いいたします。



用意するもの

  • Windows PC

  • Keyboard Quantizer Mini本体

  • KQMを介して使用する予定のマウス

  • QMK MSYS(後述)

  • TeraTerm(シリアル通信ソフト)


Step 1:必要なソフトウェアをインストールする

QMK MSYSのインストール

https://msys.qmk.fm/ から qmk_msys.exe をダウンロードしてインストールします。

インストール後、スタートメニューから QMK MSYS を起動します。以降のビルド作業はすべてこのターミナル上で行います。

TeraTermのインストール

TeraTermはKQMとシリアル通信するためのソフトです。ブートローダーモードへの切り替えとデバッグ出力の確認に使います。

https://github.com/TeraTermProject/teraterm/releases から最新版の .exe インストーラーをダウンロードしてインストールします。

TeraTermの接続手順:

  1. KQMをPCにUSB接続する

  2. Windowsのスタートボタンを右クリック →「デバイスマネージャー」を開く

画像
今回は3のほう

 3.「ポート(COMとLPT)」の左の矢印をクリックして展開する

 4. USB Serial Device (COM〇) のような項目が表示される。
    この 〇 の番号がKQMのCOMポート番号

 5. TeraTermを起動すると接続先を選ぶダイアログが出る

画像
シリアル→確認したCOM番号を選択

 6. シリアル」を選択し、ポートに手順4で確認したCOM番号を選ぶ

 7. OKを押して接続

接続できるとターミナル画面が表示されます。
ここに文字を入力してEnterを押すとKQMにコマンドを送れます。



Step 2:vial-qmkリポジトリをクローンする

KQMのVial対応ファームウェアは 通常のQMKリポジトリではなく、sekigon氏がメンテする専用のvial-qmkフォークをベースにしています。

git clone https://github.com/sekigon-gonnoc/vial-qmk.git \
  -b bmp-vial-1.0.6 \
  ~/vial_qmk_kq
cd ~/vial_qmk_kq
make git-submodule

make git-submodule はサブモジュールの初期化で数分かかります。

画像
こんな感じで進みます

Cloning into と表記されているところに "vial_qmk_kq"というフォルダが作成されていれば完了です。


Step 3:keymap.hを復元する

リポジトリに keymap.h が含まれていないため、過去のコミットから復元する必要があります。これがないとビルド時にエラーになります。

cd ~/vial_qmk_kq
git show f786d43769:keyboards/sekigon/keyboard_quantizer/mini/keymaps/vial/keymap.h \
  > keyboards/sekigon/keyboard_quantizer/mini/keymaps/vial/keymap.h

keymap.hの中身:

#pragma once

#define MATRIX_COLS_DEFAULT 8
#define MATRIX_MSGES_ROW    (MATRIX_ROWS - 1)

MATRIX_MSGES_ROW がジェスチャキーを格納する行番号の定義で、ジェスチャ発動処理から参照されます。



Step 4:quantizer_mouse.cを置き換える

既存の quantizer_mouse.c を丸ごと以下の内容に置き換えます。

ファイルのパス:

~/vial_qmk_kq/keyboards/sekigon/keyboard_quantizer/mini/keymaps/vial/quantizer_mouse.c

このファイルをメモ帳やVSCodeで開き、以下のファイルの内容に書き換えてください。(コピペすればokです)

ファイルの内容:

// Copyright 2023 sekigon-gonnoc
// SPDX-License-Identifier: GPL-2.0-or-later

#include QMK_KEYBOARD_H

#include "dynamic_keymap.h"
#include "vial.h"

#include "report_parser.h"

typedef enum {
    GESTURE_NONE = 0,
    GESTURE_RIGHT,
    GESTURE_LEFT,
    GESTURE_UP,
    GESTURE_DOWN,
} gesture_id_t;

typedef enum {
    DYNAMIC_CONFIG_MOUSE_SCALE_X,
    DYNAMIC_CONFIG_MOUSE_SCALE_Y,
    DYNAMIC_CONFIG_MOUSE_SCALE_V,
    DYNAMIC_CONFIG_MOUSE_SCALE_H,
} DYNAMIC_CONFIG_MOUSE_SCALE;

extern bool          matrix_has_changed;
extern matrix_row_t* matrix_dest;
extern bool          is_encoder_action;
extern bool          mouse_send_flag;

uint8_t  encoder_modifier            = 0;
uint16_t encoder_modifier_pressed_ms = 0;
bool     is_encoder_action           = false;
int      reset_flag                  = 0;

#ifndef ENCODER_MODIFIER_TIMEOUT_MS
#    define ENCODER_MODIFIER_TIMEOUT_MS 500
#endif

#ifndef GESTURE_THRESHOLD
#    define GESTURE_THRESHOLD 30
#endif

#ifndef GESTURE_COOLDOWN_MS
#    define GESTURE_COOLDOWN_MS 150
#endif

#ifndef SCROLL_SCALE
#    define SCROLL_SCALE 4  // スクロール量の分母。大きいほど遅くなる
#endif

static int16_t  gesture_move_x          = 0;
static int16_t  gesture_move_y          = 0;
static bool     gesture_wait            = false;
static int16_t  wheel_move_v            = 0;
static int16_t  wheel_move_h            = 0;
static uint16_t mouse_gesture_threshold = GESTURE_THRESHOLD;
static uint16_t gesture_last_fired_ms   = 0;

static void gesture_start(void) {
    dprint("Gesture start\n");
    gesture_wait   = true;
    gesture_move_x = 0;
    gesture_move_y = 0;
}

void set_mouse_gesture_threshold(uint16_t val) {
    if (val > 0) {
        mouse_gesture_threshold = val;
    }
}

gesture_id_t recognize_gesture(int16_t x, int16_t y) {
    if (abs(x) + abs(y) < mouse_gesture_threshold) {
        return GESTURE_NONE;
    }

    if (abs(x) >= abs(y)) {
        return x >= 0 ? GESTURE_RIGHT : GESTURE_LEFT;
    } else {
        return y < 0 ? GESTURE_UP : GESTURE_DOWN;
    }
}

static uint16_t dynamic_config_keymap_keycode_to_keycode(uint8_t layer, uint16_t keycode) {
    uint8_t row = keycode / MATRIX_COLS_DEFAULT + 1;
    uint8_t col = keycode & 0x07;
    return dynamic_keymap_get_keycode(layer, row, col);
}

static uint16_t get_remapped_keycode_from_keycode(uint16_t keycode) {
    for (uint16_t layer = 15; layer > 0; layer--) {
        if (layer_state & (1 << layer)) {
            uint16_t kc = dynamic_config_keymap_keycode_to_keycode(layer, keycode);
            if (kc != KC_TRNS) {
                return kc;
            }
        }
    }
    return dynamic_config_keymap_keycode_to_keycode(0, keycode);
}

static int8_t get_mouse_scale(DYNAMIC_CONFIG_MOUSE_SCALE scale_type) {
    return (16);
}

void process_gesture(uint8_t layer, gesture_id_t gesture_id) {
    switch (gesture_id) {
        case GESTURE_RIGHT ... GESTURE_DOWN: {
            uint16_t keycode = dynamic_config_keymap_keycode_to_keycode(
                layer, (MATRIX_MSGES_ROW - 1) * 8 + gesture_id - GESTURE_RIGHT);
            if (keycode == MATRIX_MSGES_ROW * 8 + gesture_id - GESTURE_RIGHT) {
                return;
            }
            vial_keycode_tap(keycode);
        } break;
        default:
            break;
    }
}

typedef struct {
    int8_t x;
    int8_t y;
    int8_t v;
    int8_t h;
    int8_t xh;
    int8_t yv;
} scaled_report_t;

static void calc_mouse_scaled_move(mouse_parse_result_t const* report, scaled_report_t* scaled) {
    static int16_t x_frac;
    static int16_t y_frac;
    static int16_t v_frac;
    static int16_t h_frac;
    static int16_t xh_frac;
    static int16_t yv_frac;

    int8_t scale_x = get_mouse_scale(DYNAMIC_CONFIG_MOUSE_SCALE_X);
    int8_t scale_y = get_mouse_scale(DYNAMIC_CONFIG_MOUSE_SCALE_Y);
    int8_t scale_v = get_mouse_scale(DYNAMIC_CONFIG_MOUSE_SCALE_V);
    int8_t scale_h = get_mouse_scale(DYNAMIC_CONFIG_MOUSE_SCALE_H);

    int16_t x_real  = (report->x * scale_x) + x_frac;
    int16_t y_real  = (report->y * scale_y) + y_frac;
    int16_t v_real  = (report->v * scale_v) + v_frac;
    int16_t h_real  = (report->h * scale_h) + h_frac;
    int16_t xh_real = (report->x * scale_x) + xh_frac;
    int16_t yv_real = (report->y * scale_y) + yv_frac;
    scaled->x       = x_real >> 4;
    scaled->y       = y_real >> 4;
    scaled->v       = v_real >> 4;
    scaled->h       = h_real >> 4;
    scaled->xh      = xh_real >> 8;
    scaled->yv      = yv_real >> 8;
    x_frac          = x_real - (scaled->x << 4);
    y_frac          = y_real - (scaled->y << 4);
    v_frac          = v_real - (scaled->v << 4);
    h_frac          = h_real - (scaled->h << 4);
    xh_frac         = xh_real - (scaled->xh << 8);
    yv_frac         = yv_real - (scaled->yv << 8);
}

void mouse_report_hook(mouse_parse_result_t const* report) {
    // 下位8ビットのみ有効(上位バイトは符号拡張による汚染のため無視)
    uint8_t button_current = (uint8_t)(report->button & 0x00FF);
    for (int bit = 0; bit < 8; bit++) {
        if (button_current & (1 << bit)) {
            matrix_dest[(KC_MS_BTN1 + bit) / 8 + 1] |= (1 << ((KC_MS_BTN1 + bit) & 0x07));
        } else {
            matrix_dest[(KC_MS_BTN1 + bit) / 8 + 1] &= ~(1 << ((KC_MS_BTN1 + bit) & 0x07));
        }
    }

    mouse_parse_result_t raw_report = *report;
    scaled_report_t      scaled;
    calc_mouse_scaled_move(&raw_report, &scaled);

    uint16_t ms_left_map = get_remapped_keycode_from_keycode(KC_MS_LEFT);
    uint16_t ms_up_map   = get_remapped_keycode_from_keycode(KC_MS_UP);
    if (ms_left_map == KC_MS_WH_RIGHT) {
        scaled.h += scaled.xh;
        scaled.x = 0;
    }
    if (ms_up_map == KC_MS_WH_DOWN) {
        scaled.v -= scaled.yv;
        scaled.y = 0;
    }

    if (scaled.v != 0) {
        keypos_t key;
        wheel_move_v      = scaled.v;
        uint16_t kc       = scaled.v > 0 ? KC_MS_WH_UP : KC_MS_WH_DOWN;
        key.row           = (kc / 8) + 1;
        key.col           = kc & 0x07;
        is_encoder_action = true;
        action_exec((keyevent_t){.key = key, .type = KEY_EVENT, .pressed = true, .time = (timer_read() | 1)});
        action_exec((keyevent_t){.key = key, .type = KEY_EVENT, .pressed = false, .time = (timer_read() | 1)});
        is_encoder_action = false;
    }

    if (scaled.h != 0) {
        keypos_t key;
        wheel_move_h      = scaled.h;
        uint16_t kc       = scaled.h > 0 ? KC_MS_WH_LEFT : KC_MS_WH_RIGHT;
        key.row           = (kc / 8) + 1;
        key.col           = kc & 0x07;
        is_encoder_action = true;
        action_exec((keyevent_t){.key = key, .type = KEY_EVENT, .pressed = true, .time = (timer_read() | 1)});
        action_exec((keyevent_t){.key = key, .type = KEY_EVENT, .pressed = false, .time = (timer_read() | 1)});
        is_encoder_action = false;
    }

    report_mouse_t mouse = pointing_device_get_report();

    if (gesture_wait) {
        if (ms_left_map == KC_MS_WH_LEFT || ms_up_map == KC_MS_WH_UP) {
            // スクロールレイヤー中はカーソル凍結せずスクロールとして送る
            if (scaled.x != 0 && ms_left_map == KC_MS_WH_LEFT) {
                mouse_send_flag = true;
                mouse.h += scaled.x / SCROLL_SCALE;
            }
            if (scaled.y != 0 && ms_up_map == KC_MS_WH_UP) {
                mouse_send_flag = true;
                mouse.v -= scaled.y / SCROLL_SCALE;
            }
        } else {
            // 通常のジェスチャ待機:カーソル凍結して累積
            gesture_move_x += scaled.x;
            gesture_move_y += scaled.y;

            gesture_id_t gesture_id = recognize_gesture(gesture_move_x, gesture_move_y);
            if (gesture_id != GESTURE_NONE) {
                if (timer_elapsed(gesture_last_fired_ms) > GESTURE_COOLDOWN_MS) {
                    uint8_t layer = 0;
                    for (uint8_t l = 15; l > 0; l--) {
                        if (layer_state & (1 << l)) {
                            layer = l;
                            break;
                        }
                    }
                    process_gesture(layer, gesture_id);
                    gesture_last_fired_ms = timer_read();
                    dprintf("Gesture fired: id:%d x:%d,y:%d\n", gesture_id, gesture_move_x, gesture_move_y);
                }
                gesture_move_x = 0;
                gesture_move_y = 0;
            }
        }
    } else {
        if (scaled.x != 0 && ms_left_map == KC_MS_LEFT) {
            mouse_send_flag = true;
            mouse.x += scaled.x;
        } else if (scaled.x != 0 && ms_left_map == KC_MS_WH_LEFT) {
            mouse_send_flag = true;
            mouse.h += scaled.x;
        } else if (scaled.xh != 0 && ms_left_map == KC_MS_WH_LEFT) {
            mouse_send_flag = true;
            mouse.h += scaled.xh;
        }

        if (scaled.y != 0 && ms_up_map == KC_MS_UP) {
            mouse_send_flag = true;
            mouse.y += scaled.y;
        } else if (scaled.y != 0 && ms_up_map == KC_MS_WH_UP) {
            mouse_send_flag = true;
            mouse.v -= scaled.y;
        } else if (scaled.yv != 0 && ms_up_map == KC_MS_WH_UP) {
            mouse_send_flag = true;
            mouse.v -= scaled.yv;
        }
    }

    pointing_device_set_report(mouse);
}

bool process_record_mouse(uint16_t keycode, keyrecord_t* record) {
    if (encoder_modifier != 0 && !is_encoder_action) {
        unregister_mods(encoder_modifier);
        encoder_modifier = 0;
    }

    switch (keycode) {
        case QK_MODS ... QK_MODS_MAX:
            if (is_encoder_action) {
                if (record->event.pressed) {
                    uint8_t current_mods        = keycode >> 8;
                    encoder_modifier_pressed_ms = timer_read();
                    if (current_mods != encoder_modifier) {
                        del_mods(encoder_modifier);
                        encoder_modifier = current_mods;
                        add_mods(encoder_modifier);
                    }
                    register_code(keycode & 0xff);
                } else {
                    unregister_code(keycode & 0xff);
                }
                return false;
            } else {
                return true;
            }
            break;
    }

    switch (keycode) {
        case KC_BTN1 ... KC_BTN5: {
            mouse_send_flag = true;
            return true;
        } break;

        case KC_MS_WH_UP ... KC_MS_WH_DOWN: {
            if (wheel_move_v != 0) {
                report_mouse_t report = pointing_device_get_report();
                report.v              = keycode == KC_MS_WH_UP ? abs(wheel_move_v) : -abs(wheel_move_v);
                pointing_device_set_report(report);
                mouse_send_flag = true;
                return false;
            } else {
                return true;
            }
        } break;

        case KC_MS_WH_LEFT ... KC_MS_WH_RIGHT: {
            if (wheel_move_h != 0) {
                report_mouse_t report = pointing_device_get_report();
                report.h              = keycode == KC_MS_WH_LEFT ? abs(wheel_move_h) : -abs(wheel_move_h);
                pointing_device_set_report(report);
                mouse_send_flag = true;
                return false;
            } else {
                return true;
            }
        } break;
    }

    return true;
}

void post_process_record_mouse(uint16_t keycode, keyrecord_t* record) {
    if (keycode >= QK_MOMENTARY && keycode <= QK_MOMENTARY_MAX) {
        if (record->event.pressed && gesture_wait == false) {
            gesture_start();
        }
    }

    if ((keycode >= QK_LAYER_TAP && keycode <= QK_LAYER_TAP_MAX) ||
        (keycode >= QK_MOMENTARY && keycode <= QK_MOMENTARY_MAX)) {
        if ((!record->event.pressed) && gesture_wait == true) {
            gesture_wait   = false;
            gesture_move_x = 0;
            gesture_move_y = 0;
        }
    }
}

bool pre_process_record_mouse(uint16_t keycode, keyrecord_t *record) {
    if (keycode >= QK_LAYER_TAP && keycode <= QK_LAYER_TAP_MAX) {
        if (record->event.pressed && gesture_wait == false) {
            gesture_start();
        }
    }
    return true;
}



Step 5:ビルドする

cd ~/vial_qmk_kq
make sekigon/keyboard_quantizer/mini:vial 2>&1 | tail -5

以下のような出力が出れば成功です。

Linking: .build/sekigon_keyboard_quantizer_mini_vial.elf                [OK]
Creating UF2 file for deployment: .build/sekigon_keyboard_quantizer_mini_vial.uf2  [OK]
Copying sekigon_keyboard_quantizer_mini_vial.uf2 to qmk_firmware folder [OK]

ビルドが成功すると、vial_qmk_kq の中にuf2ファイルが出来ています。



Step 6:KQMに書き込む

6-1. KQMをブートローダーモードに

KQMをPCに接続したのち、Step 1で案内したTeraTermの手順でKQMに接続します。
左上のCOM番号がKQMのと同じになっていることを確認してください。

画像
左上がCOM3

この画面にdfu と入力してEnterを押すと

画像
左上が未接続

KQMがブートローダーモードに入り、RPI-RP2 というドライブがWindowsエクスプローラーに出てきます。

画像
これが出てくればOK

6-2. uf2を書き込む

ビルドした sekigon_keyboard_quantizer_mini_vial.uf2 を RPI-RP2 ドライブにドラッグ&ドロップします。
正常に書き込むことができたら自動的に再起動し、RPI-RP2ドライブが閉じられます。



Step 7:Vialで設定する

7-1. Vialでのリマップ準備

Vialにはデスクトップアプリ版とWebアプリ版の2種類があります。

  • Webアプリ版: https://vial.rocks/ をブラウザで開くだけで使えます。インストール不要でいつでもどこでもカスタムできるので基本的にはこちらがおすすめです。ChromeまたはEdgeを使用してください。

  • デスクトップアプリ版: https://get.vial.today/ からダウンロードできます。(堕落猫さんがVial互換アプリを作ってたりします)

どちらの場合でもStartVialボタンをクリックし、KQMを選択するとリマップ画面へと移行します。

画像
Start Vialをクリック
画像
Keyboard Quantizerを選択する
画像
リマップの画面になる

Keyboard Quantizer は名前の通り、キーボードをQMK化する、というのが主目的の製品なので、デフォルトの仮想レイアウトがキーボードになっています。
これをマウス用に変更します。

画像
LayoutタブからMOUSEを選択する
画像
マウス用のレイアウトになる

一番上の横一列が
・左から順に各番号のマウスボタン(左クリックが一番左, 戻るが左端から4番目など)
・左下がマウスならマウス自体の動き、トラックボールならボールの動き
・真ん中下がスクロールホイールの前後、左右チルト
・右がジェスチャ
を設定できる画面になっています。

画像


7-2. ジェスチャキーの割り当て

VialのKeymapタブで、ジェスチャを発動させたいレイヤーを選択します。

画像
今回はレイヤー1


四角で囲った場所にジェスチャで発動させたいキーコードを設定します。

画像

左右のジェスチャで「タブの切り替え」を設定する場合

画像

該当箇所をダブルクリックして上の画像のウィンドウを出したのち

画像

C(S(KC_TAB))と入力します。C_S(KC_TAB)でも可。

これはCtrl + SHIFT + TAB をQMKのキーコードで表したものです。
詳細は以下。

画像

右ジェスチャの Ctrl + TAB も設定すると上の画像のようになります。

7-3. ジェスチャを発動するMO/LTキーの設定

今回はレイヤー1にジェスチャキーを設定しているので、MO1キーまたはLT1(kc)を押しながら、マウスを動かすとジェスチャが発動します。

画像
レイヤー0のMouse2があったところにLT1を割り当てた

レイヤー0の Mouse2 (= 右クリック) の箇所にLayersタブからLT1を割り当てます。

LT = Layer Tap とは、キーをHoldしている間はレイヤーを切り替え、Tapした時にはまた別のキーを入力できる機能です。
LT1と書いてある下の空白をクリックし、 App, Media and Mouse タブからMouse2を選択します。

画像
LT1(KC_BTN2)を本来右クリックであった箇所に設定できた


これで、右クリックを単発で押した際にはそのまま右クリックが使用できつつ、長押しするとレイヤー1に切り替わり、左右ジェスチャでタブの切り替えが出来るようになりました。

実際の映像↓ 

これでLogicool式ジェスチャをどんなマウスでも再現できるようになりました!やったね!

7-4. スクロールレイヤーの設定

特定のレイヤーでトラックボールをスクロールに切り替えるには、そのレイヤーの Mouse Left スロットに Mouse Wheel Left、Mouse Up スロットに Mouse Wheel Up を設定します。

注意: ホイールのスロット(Mouse Wheel Up/Down/Left/Rightと書かれている場所)ではなく、カーソル移動のスロット(Mouse Left / Mouse Up)に設定することが必要です。ここを間違えるとスクロールが動作しません。

MO/LTキーでそのレイヤーに切り替えながらトラックボールを動かすとスクロールになります。



パラメータの調整

quantizer_mouse.c の先頭付近にある以下の定数を変更することで挙動を調整できます。値を変更したら再ビルド→書き込みが必要です。

#define GESTURE_THRESHOLD 30
// マウス移動量の合計がこの値を超えたらジェスチャ発動。
// 大きくすると発動しにくくなる。小さくすると敏感になる。

#define GESTURE_COOLDOWN_MS 150
// ジェスチャ連続発動の最小間隔(ミリ秒)。
// 小さくするとより素早く連続発動できる。

#define SCROLL_SCALE 4
// スクロール量の分母。大きいほどゆっくりスクロールする。
// 1にすると最速、8にすると1/8の速さになる。

実装内容の解説

ジェスチャの仕組み

MO/LTキーを押した瞬間に gesture_wait = true になり、マウスの移動量の累積が始まります。この間カーソルは動きません。

累積した移動量が閾値(GESTURE_THRESHOLD)を超えると、移動量が多い軸の方向でジェスチャを判定します。X軸の絶対値がY軸以上なら左右、そうでなければ上下と判定します。これにより斜め入力でも最も意図した方向に発動します。

発動後は累積がリセットされ、そのままキーを押し続けて別方向にジェスチャを連続して発動できます。

スクロールレイヤーとジェスチャの共存

MO/LTキーを押しているとき(gesture_wait = true)に、現在のレイヤーで Mouse Left が Mouse Wheel Left にリマップされているかどうかを確認します。
スクロールレイヤーであればカーソル凍結を解除してスクロールとして送ります。そうでなければ通常のジェスチャ待機を行います。
これによりスクロールレイヤーに切り替えた時のみ、ボール操作でジェスチャではなくスクロールが可能となります。



トラブルシューティング

  • TeraTermで dfu を入力してもRPI-RP2が出てこない
    → COMポートが正しいか確認してください。デバイスマネージャーで「ポート(COMとLPT)」を見るとKQMのCOMポート番号が確認できます。

  • Vialでデバイスが認識されない
    → uf2の書き込みが正常に完了しているか確認してください。RPI-RP2 ドライブへのドロップ後、自動的にドライブが消えて再接続されれば成功です。

  • ジェスチャが発動しない・すぐ発動してしまう
    →GESTURE_THRESHOLD の値を調整してください。トラックボールの感度によって最適値が変わります。

  • スクロールが動かない
    → スクロールレイヤーの Mouse Left スロットに Mouse Wheel Left、Mouse Up スロットに Mouse Wheel Up が設定されているか確認してください。Mouse Wheel Right や Mouse Wheel Down では動きません。

  • 9ボタン以上のマウスで9番目以降のボタンをリマップしたい→デフォルトのコードはHIDレポートの下位8ビット(BTN1〜8)しか処理していません。BTN9以降を使うには quantizer_mouse.c のボタン処理ループを下記のように上位バイトも処理するよう拡張し、再ビルド・書き込みが必要です。変更後はVialのALLレイアウトから該当するマトリクス位置にキーコードを割り当てられます。

quantizer_mouse.cの変更箇所
現在のボタン処理ループ(下位8ビットのみ)を以下に置き換えます

// BTN1-8(下位バイト)
uint8_t button_low = (uint8_t)(report->button & 0x00FF);
for (int bit = 0; bit < 8; bit++) {
    if (button_low & (1 << bit)) {
        matrix_dest[(KC_MS_BTN1 + bit) / 8 + 1] |= (1 << ((KC_MS_BTN1 + bit) & 0x07));
    } else {
        matrix_dest[(KC_MS_BTN1 + bit) / 8 + 1] &= ~(1 << ((KC_MS_BTN1 + bit) & 0x07));
    }
}
// BTN9以降(上位バイト)
uint8_t button_high = (uint8_t)((report->button >> 8) & 0x00FF);
for (int bit = 0; bit < 8; bit++) {
    if (button_high & (1 << bit)) {
        matrix_dest[(KC_MS_BTN1 + 8 + bit) / 8 + 1] |= (1 << ((KC_MS_BTN1 + 8 + bit) & 0x07));
    } else {
        matrix_dest[(KC_MS_BTN1 + 8 + bit) / 8 + 1] &= ~(1 << ((KC_MS_BTN1 + 8 + bit) & 0x07));
    }
}

番外編:タップダンスを有効にする

タップダンスとは「1回タップで○○、2回タップで△△」のような複合動作を設定できる機能です。

VialのTap Danceタブから設定し、各エントリに以下を設定できます。

  • Tap: 1回タップしたときのキー

  • Hold: 長押ししたときのキー

  • Double Tap: 2回タップしたときのキー

  • Tap + Hold: タップしてから長押ししたときのキー

設定したTap DanceはLayersタブの TD(番号) からキーマップに割り当てられます。

便利な機能なんですが、どうやらKQMのデフォファームウェアではTapDanceが使えないので実装します。

Step 1:rules.mkを編集する

~/vial_qmk_kq/keyboards/sekigon/keyboard_quantizer/mini/keymaps/vial/rules.mk を開き、先頭付近に TAP_DANCE_ENABLE = yes を追加します。

編集後のrules.mkの先頭部分:

makefile

VIA_ENABLE = yes
VIAL_ENABLE = yes
VIAL_INSECURE = yes
TAP_DANCE_ENABLE = yes

これを追加することでVialのTap Danceタブが使えるようになります。デフォルトのKQMファームウェアではTap Danceは無効になっているため、この手順が必要です。

Step 2:vial.jsonを編集する

~/vial_qmk_kq/keyboards/sekigon/keyboard_quantizer/mini/keymaps/vial/vial.json をメモ帳やVSCodeで開き、"matrix" ブロックの直後に以下を追加します。

変更前:

json

{
  "matrix": {
    "rows": 32,
    "cols": 8
  },

変更後:

json

{
  "matrix": {
    "rows": 32,
    "cols": 8
  },
  "tap_dance_entries": 4,

tap_dance_entries の数値がVialで設定できるTap Danceの最大登録数です。4で足りなければ増やしても構いません。

Step 3:ビルド→書き込みをする

これでTapDanceが有効になります。



HUGE PLUSでのアライのVial設定

は、次回の記事でまとめます。

この記事が参考になった方は投げ銭していただけると嬉しいです。
今後もこういう解説記事を書く気になるかもしれません。

ここから先は

0字

¥ 300

PayPayなら抽選で全額還元 3/30まで

この記事が気に入ったらチップで応援してみませんか?

購入者のコメント

コメントするには、 ログイン または 会員登録 をお願いします。
Keyboard Quantizer Mini を使用してマウスを高機能オンボードメモリ化し、LogiOptions+ と同等のジェスチャを実装する|アライ
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1