本文へスキップ
からだにいいもの

Rのトピックスを中心に『まだ、まだ、知らない、役に立つ情報?』を発信します。

「12キー、4ノブ」のプログラマブルキーボードを純正ソフトなしで設定!通信の仕組みと Python で書き込む方法

はじめに

以前の記事で紹介した、12個のキーと4個のノブの「プログラマブルキーボード」を、純正ソフトを使わずに設定する方法を調べました。Vendor IDは20812、Product IDは34896です。

今回は USB の通信を調べて、キーとノブへのキー入力・メディアキー・マウス操作の割り当て、LED の設定、本体からの設定の読み出しまでできるようになりました。記事の後半では、コピーしてすぐ使える Python スクリプトを紹介します。

記事で検証したデバイスは下記です。

AmazonBCDYH プログラム可能なマクロキーボード 12 キー 4 ノブ ゲーミングキーパッド USB 有線 + BT デュア¥3,300www.amazon.co.jpAmazonで見る
項目内容
形3列×4段のキー12個、左上に小さいノブ2個、中くらいのノブ1個、右上に大きいノブ1個
USB の IDVID 514C / PID 8850(10進数で 20812 / 34896)
チップWCH CH57x 系と推定

この記事では、既存のオープンソースをそのまま使うとどこが合わなかったのか、USB 通信を記録してどう確かめたのかを、実際にハマった順に紹介します。同じ系列の機種を使っている人の参考になればうれしいです。

スポンサーリンク

機器を特定する

設定の書き込み口は、USB のインターフェース MI_00 にある「HID 準拠ベンダー定義デバイス」(Usage Page FF00)です。デバイスマネージャーでは「HID キーボード デバイス」が増えるだけなので、PowerShell で VID/PID ごとに一覧にします。

Get-PnpDevice -PresentOnly | Where-Object { $_.InstanceId -match 'VID_514C' } |
  ForEach-Object {
    $hw = (Get-PnpDeviceProperty -InstanceId $_.InstanceId -KeyName 'DEVPKEY_Device_HardwareIds').Data
    [pscustomobject]@{ Class = $_.Class; Name = $_.FriendlyName; HW = ($hw -join ' | ') }
  } | Format-List

この機種は1台で次のように分かれていました。

インターフェースUsage Page役割
MI_00FF00(ベンダー定義)設定の読み書き用。出力 65 バイト(レポート ID 3 + 64 バイト)
MI_01 Col01 / Col030001 / 0006キーボード入力
MI_01 Col020001 / 0002マウス入力
MI_01 Col04000C音量などのメディアキー

レポート ID は機種によって 0 / 2 / 3 のどれかです。この機種では 0 と 2 の書き込みが失敗し、3 でした。

既存のオープンソースを調べる

この系列には、設定を書き込むオープンソースのツールが3つあります。この機種(514C:8850)にいちばん合っていたのは ch57x-keyboard-tool の k884x 形式で、読み出しは macropad_tool を参考にしました。

ツール言語この機種での結果
pohui/MacroPadC#(WinForms)キー入力は書けた。メディアキー・マウス・LED はデータの位置が合わず動かなかった
kriomant/ch57x-keyboard-toolRust514C:8850 の記載あり。k884x 形式でメディアキー・マウス・LED と「確定」の命令が合った
kamaaina/macropad_toolRust設定の読み出し命令と、LED 消灯の送り方の手がかりになった

前回の記事で ch57x-keyboard-tool が動かなかった理由も分かりました。このツールは VID 514C / PID 8850 を「4×4」向けの形式(0xFD で始まる書き込み)で扱いますが、この機種は同じツールの「k884x」向けの形式(0xFE で始まる書き込み)で動きます。同じ VID/PID でも中身が違う可能性があるので、最初はキー1個だけ書いて確かめるのが安全です。

通信の仕組み

やり取りはすべて 64 バイトのパケットで、先頭にレポート ID 03 を付けて MI_00 に書き込みます。足りない部分は 0 で埋めます。下の表のバイト位置は、レポート ID を除いた 64 バイトの中での位置です。

割り当ての書き込み

共通の頭は FE キー番号 レイヤー 種類 00 00 00 00 00 の 9 バイトで、続く [9] 以降が種類ごとのデータです。レイヤーは 1〜3 です。

種類[3][9] 以降のデータ例
キー入力01件数、(修飾キー, キー)× 件数。キーは HID の Usage ID、最大 18 回Ctrl+C → 01 01 06
メディアキー0200、Usage 下位、Usage 上位音量を上げる → 00 E9 00
マウス(クリック)0301、修飾、ボタン左クリック → 01 00 01
マウス(ホイール)0303、修飾、00 00 00、量ホイール下 → 03 00 00 00 00 FF
待ち時間05頭の [4][5] にミリ秒(リトルエンディアン、最大 6000)キー入力とは別のパケットで送る
LED0801 00 のあとに「色 × 16 + モード」。キー番号の位置は B0常時点灯・水色 → 01 00 51

マウスと一緒に押す修飾キーは Ctrl=01、Shift=02、Alt=04 で、Win は使えません。

書き込んだあとの「確定」

割り当て1件ごとに、次の3つを続けて送ります。LED のときは 2 だけです。

  1. AA AA
  2. FD FE FF(確定)
  3. AA AA

設定の読み出し

問い合わせ送るデータ返事
機種情報FB FB FBFB 0C 04(キー 12 個・ノブ 4 個)
レイヤーの設定FA 0F 03 レイヤー1 か所ごとに FA キー番号 レイヤー 種類 … 件数 データ が 24 件

返事は書き込みとほぼ同じ形ですが、細かい違いがあります(次の章)。LED の設定は読み出せませんでした。

ハマったところ

参考にしたツールのどれとも、この機種とはどこかが違っていました。実機で確かめて分かった違いを、見つけた順に並べます。

1. キー番号は「横置き」基準で、左下が 1 番

画面の左上のキーに書いたつもりが、左下のキーが変わりました。本体は横置きの向きで番号を振っていて、左下から上へ 1→2→3→4、次の列へ、と進みます。

縦置きの見た目      本体の番号
 1  2  3          4  8 12
 4  5  6          3  7 11
 7  8  9          2  6 10
10 11 12          1  5  9

2. ノブの番号は「左に回す・押す・右に回す」の順

ノブは 3 つずつ番号があり、先頭から左に回す+0、押す+1、右に回す+2 です。先頭の番号は、大きいノブが 13、左下の小さいノブが 16、左上の小さいノブが 19、真ん中のノブが 22 でした。

確かめるときは、全部の場所に a〜x を書いてメモ帳で押してみる方法が速いです。ただしノブは 1 回回すと同じ文字が何度も出るため、私は一度「押す」と「右に回す」を逆に読み違えました。ノブは 1 つずつ、動作ごとに確かめるのが確実です。

3. 純正ソフトもノブの割り当てがずれている

純正ソフトで大きいノブの「右に回す」に音量を割り当てると、14 番(本当は「押す」)に書き込まれていました。「純正ソフトで設定したのに動かない」という場合は、このずれが原因かもしれません。

4. メディアキー・マウス・LED は、参考元によって位置が違う

最初に参考にした MacroPad の形式では、メディアキーの番号が 1 バイト後ろにずれていて、マウスは種類のバイトがなく、LED はキー番号の位置が 0 でした。ch57x-keyboard-tool の k884x 形式(前の章の表)に合わせると、すべて動きました。

5. LED の消灯は 00 では効かない

消灯はモード 0 ですが、色も 0 にした 00 を送っても消えませんでした。純正ソフトも 00 を送っていました(USB の記録で確認)。macropad_tool と同じく色の値を付けて 10(赤+消灯)などを送ると、消えました。白の常時点灯だけは特別で、モード 5(05)です。

6. 読み出しは「キー 15 個・ノブ 3 個」として問い合わせる

本体は自分のことを「キー 12 個・ノブ 4 個」と答えます。ところがその数で問い合わせると、13〜15 番(大きいノブ)の設定が返ってこず、中身が空の返事が 3 件来ます。「キー 15 個・ノブ 3 個」として問い合わせると、1〜24 番がすべて返ってきます。

7. 読み出しの返事は、書き込みと並びが少し違う

  • メディアキー:書き込みは 00 E9 00 だが、返事は 01 E9 00(件数のあとに番号が詰まる)
  • マウス:書き込みで種類 01(クリック)だったところが、返事では 04 になる。ボタンとホイールの値で見分ける

USB 通信を記録して確かめる

参考元どうしで書き方が食い違うときは、純正ソフトが実際に送っているデータを記録するのが確実です。Windows では Wireshark と USBPcap を使います。

  1. Wireshark と USBPcap を入れて、PC を再起動する(USBPcap はドライバーなので、再起動まで使えない)
  2. Wireshark のコマンドライン版 tshark で、時間を決めて記録する
  3. 記録中に純正ソフトで設定を 1 つずつ、数秒空けて書き込む
  4. プログラマブルキーボードへの書き込み(エンドポイント 0x04)だけを取り出す
# 10分間記録して自動で止める
& "C:\Program Files\Wireshark\tshark.exe" -i \\.\USBPcap1 -a duration:600 -w capture.pcapng

# プログラマブルキーボード(例:デバイスアドレス 4)への書き込みを表示
& "C:\Program Files\Wireshark\tshark.exe" -r capture.pcapng `
  -Y "usb.device_address == 4 && usb.endpoint_address == 0x04 && usb.data_len > 0" `
  -T fields -e frame.time_relative -e usbhid.data

私が失敗した点が 3 つあります。

  • USBPcapCMD を強制終了すると、記録が消える:USBPcap はデータを溜めてから書き出すので、最後の分(設定の書き込みのように少ない通信)が丸ごと捨てられました。tshark の -a duration で自動で止めれば、最後まで保存されます。
  • 前の記録プロセスが残っていると、次の記録が 0 件になる:USBPcapCMD は管理者権限で動くので、止めるのにも管理者権限が要ります。
  • 一覧の番号と記録上の番号を取り違える:機器を絞らずに記録し、usb.device_address ごとに集計すると、どれがプログラマブルキーボードか分かりやすいです。

記録してみると、純正ソフトの書き込みは k884x 形式とバイト単位で同じでした。前の章の「純正ソフトのノブのずれ」も、この記録で分かりました。

Python で設定する

下のスクリプトを macropad.py として保存すれば、純正ソフトなしで設定できます。必要なのは Python 3.9 以降と hidapi だけです。Windows 11 で、キー入力・修飾キーだけの割り当て・メディアキーの書き込み、割り当ての削除、読み出しを確かめました。

pip install hidapi

使い方

python macropad.py read                    # L1 の割り当てを表示
python macropad.py read --layer 2          # L2 の割り当てを表示
python macropad.py key 1 ctrl+c            # キー1(左上)に Ctrl+C
python macropad.py key 2 h e l l o         # キー2 に h→e→l→l→o
python macropad.py key 7 shift             # キー7 を Shift だけにする
python macropad.py media knob4-right volup # 大きいノブを右に回すと音量を上げる
python macropad.py clear 1 --layer 2       # L2 のキー1を割り当てなしにする
python macropad.py led on cyan             # LED を水色で常時点灯
指定するもの書き方
場所(キー)1〜12。縦置きで左上から右へ、上の段から下の段へ
場所(ノブ)knob1〜knob4 に -left / -push / -right。knob1=左上の小、knob2=左下の小、knob3=真ん中、knob4=大きいノブ
キーa〜z、0〜9、f1〜f12、enter esc tab space backspace delete up down left right など
修飾キーctrl shift alt win を + でつなぐ(例:ctrl+shift+s)
メディアキーplay next prev stop mute volup voldown
LEDoff / on / press(押したキーが光る)と、色 white red orange yellow green cyan blue purple
レイヤー--layer 1〜3(省略すると 1)

LED の命令は、C# で確かめたのと同じ値を送っています。

スクリプト

"""12キー・4ノブのプログラマブルキーボード (VID 514C / PID 8850) を純正ソフトなしで設定する。

    pip install hidapi
    python macropad.py read                    # L1 の割り当てを表示
    python macropad.py key 1 ctrl+c            # キー1(左上)に Ctrl+C
    python macropad.py key 2 h e l l o         # キー2 に h→e→l→l→o(最大18回)
    python macropad.py media knob4-right volup # 大きいノブを右に回すと音量を上げる
    python macropad.py clear 1 --layer 2       # L2 のキー1を割り当てなしにする
    python macropad.py led on cyan             # LED を水色で常時点灯(off / on / press)

場所はキー 1〜12(縦置きで左上から右へ)と knob1〜4-left/push/right
(knob1=左上の小、knob2=左下の小、knob3=真ん中、knob4=大きいノブ)。
"""
import sys
import hid

VID, PID, REPORT_ID = 0x514C, 0x8850, 3

# 画面上の位置 → 本体の番号(本体は横置き基準で左下が 1)
KEY_IDS = [4, 8, 12, 3, 7, 11, 2, 6, 10, 1, 5, 9]
KNOB_BASE = {1: 19, 2: 16, 3: 22, 4: 13}          # 左に回す +0、押す +1、右に回す +2
KNOB_ACTION = {"left": 0, "push": 1, "right": 2}

MODS = {"ctrl": 0x01, "shift": 0x02, "alt": 0x04, "win": 0x08}
KEYS = {chr(c): 0x04 + i for i, c in enumerate(range(ord("a"), ord("z") + 1))}
KEYS |= {str(n % 10): 0x1E + n - 1 for n in range(1, 11)}            # 1〜9, 0
KEYS |= {f"f{n}": 0x3A + n - 1 for n in range(1, 13)}
KEYS |= {"enter": 0x28, "esc": 0x29, "backspace": 0x2A, "tab": 0x2B, "space": 0x2C,
         "minus": 0x2D, "equal": 0x2E, "comma": 0x36, "period": 0x37, "slash": 0x38,
         "printscreen": 0x46, "insert": 0x49, "home": 0x4A, "pageup": 0x4B, "delete": 0x4C,
         "end": 0x4D, "pagedown": 0x4E, "right": 0x4F, "left": 0x50, "down": 0x51, "up": 0x52}
MEDIA = {"play": 0xCD, "next": 0xB5, "prev": 0xB6, "stop": 0xB7, "mute": 0xE2,
         "volup": 0xE9, "voldown": 0xEA}
COLORS = ["white", "red", "orange", "yellow", "green", "cyan", "blue", "purple"]


def control_id(place):
    if place.startswith("knob"):
        knob, action = place[4:].split("-")
        return KNOB_BASE[int(knob)] + KNOB_ACTION[action]
    return KEY_IDS[int(place) - 1]


def open_device():
    for d in hid.enumerate(VID, PID):
        if d["usage_page"] == 0xFF00:                 # 設定用のベンダー定義インターフェース (MI_00)
            dev = hid.device()
            dev.open_path(d["path"])
            return dev
    sys.exit("プログラマブルキーボードが見つかりません")


def send(dev, *data):
    dev.write(bytes([REPORT_ID, *data]).ljust(65, b"\0"))


def bind(dev, layer, cid, kind, payload):
    """割り当てを書き込み、確定の命令を送る。kind: 1=キー 2=メディア 3=マウス"""
    send(dev, 0xFE, cid, layer, kind, 0, 0, 0, 0, 0, *payload)
    send(dev, 0xAA, 0xAA)
    send(dev, 0xFD, 0xFE, 0xFF)
    send(dev, 0xAA, 0xAA)


def parse_combo(combo):
    parts = combo.lower().split("+")
    if parts[-1] in MODS:                             # shift だけ、ctrl+alt だけ、など
        return sum(MODS[m] for m in parts), 0
    *mods, key = parts
    return sum(MODS[m] for m in mods), KEYS[key]


def read_layer(dev, layer):
    send(dev, 0xFA, 15, 3, layer)                    # キー15・ノブ3として聞くと 1〜24 番がすべて返る
    names = {v: k for k, v in KEYS.items()}
    media = {v: k for k, v in MEDIA.items()}
    places = [str(i + 1) for i in range(12)] + [f"knob{n}-{a}" for n in KNOB_BASE for a in KNOB_ACTION]
    found = {}
    while r := dev.read(65, 300):
        if r[1] != 0xFA or r[3] != layer:
            continue
        kind, count = r[4], r[10]
        if kind == 1:
            # 修飾キーだけの割り当ては件数 0 で入っている
            pairs = [(r[11 + i * 2], r[12 + i * 2]) for i in range(count)] or ([(r[11], 0)] if r[11] else [])
            text = " ".join("+".join([m for m, b in MODS.items() if mod & b] + ([names.get(k, hex(k))] if k else []))
                            for mod, k in pairs)
        elif kind == 2:
            text = "media " + media.get(r[11] | r[12] << 8, hex(r[11] | r[12] << 8))
        elif kind == 3:
            text = "mouse"
        else:
            text = ""
        found[r[2]] = text
    for place in places:
        print(f"{place:>12}: {found.get(control_id(place)) or '-'}")


def main(argv):
    layer = 1
    if "--layer" in argv:
        i = argv.index("--layer")
        layer = int(argv[i + 1])
        del argv[i:i + 2]
    if not argv:
        sys.exit(__doc__)
    dev = open_device()
    cmd, *args = argv
    if cmd == "read":
        read_layer(dev, layer)
    elif cmd == "key":
        pairs = [parse_combo(c) for c in args[1:]]
        count = 0 if len(pairs) == 1 and pairs[0][1] == 0 else len(pairs)   # 修飾キーだけなら件数 0
        bind(dev, layer, control_id(args[0]), 1, [count, *[b for p in pairs for b in p]])
    elif cmd == "media":
        usage = MEDIA[args[1]]
        bind(dev, layer, control_id(args[0]), 2, [0, usage & 0xFF, usage >> 8])
    elif cmd == "clear":
        bind(dev, layer, control_id(args[0]), 1, [0])
    elif cmd == "led":
        mode, color = args[0], COLORS.index(args[1] if len(args) > 1 else "red")
        if mode == "on":
            code = 5 if color == 0 else color << 4 | 1   # 白の常時点灯だけはモード 5
        elif mode == "press":
            code = max(color, 1) << 4 | 4
        else:
            code = max(color, 1) << 4                   # 消灯も色を付けないと効かない
        send(dev, 0xFE, 0xB0, layer, 0x08, 0, 0, 0, 0, 0, 1, 0, code)
        send(dev, 0xFD, 0xFE, 0xFF)
    else:
        sys.exit(__doc__)
    dev.close()


if __name__ == "__main__":
    main(sys.argv[1:])

まとめ

格安プログラマブルキーボードも、書き込み口とパケットの形さえ分かれば、純正ソフトなしで自由に設定できます。ただし同じ系列でも機種ごとに細かい違いがあるので、次の順で進めるのがおすすめです。

  1. VID/PID と、Usage Page FF00 のインターフェースを確かめる
  2. 既存のオープンソースから、自分の VID/PID に対応した形式を探す
  3. キー 1 個だけ書いて、動くか確かめる
  4. 全部の場所に a〜x を書いて、番号の対応を確かめる(ノブは動作ごとに 1 つずつ)
  5. 合わないところは USB 通信を記録して、純正ソフトと比べる

誰かのなにかの役に立ちますように!!

参考にしたもの

※ 記事の内容は、筆者の手元の 1 台(VID 514C / PID 8850)で確かめた結果です。本体に書き込む操作は自己責任で行ってください。


※ Amazonのアソシエイトとして、当サイトは適格販売により収入を得ています。

スポンサーリンク
価格および配送状況は変更される場合があります。購入時は商品ページをご確認ください。
当サイトに表示されている商品情報はAmazonから提供されたものであり、更新または削除される場合があります。
karada-goodはAmazonアソシエイトとして、適格販売により収入を得ています。