「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 の ID | VID 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_00 | FF00(ベンダー定義) | 設定の読み書き用。出力 65 バイト(レポート ID 3 + 64 バイト) |
MI_01 Col01 / Col03 | 0001 / 0006 | キーボード入力 |
MI_01 Col02 | 0001 / 0002 | マウス入力 |
MI_01 Col04 | 000C | 音量などのメディアキー |
レポート ID は機種によって 0 / 2 / 3 のどれかです。この機種では 0 と 2 の書き込みが失敗し、3 でした。
既存のオープンソースを調べる
この系列には、設定を書き込むオープンソースのツールが3つあります。この機種(514C:8850)にいちばん合っていたのは ch57x-keyboard-tool の k884x 形式で、読み出しは macropad_tool を参考にしました。
| ツール | 言語 | この機種での結果 |
|---|---|---|
| pohui/MacroPad | C#(WinForms) | キー入力は書けた。メディアキー・マウス・LED はデータの位置が合わず動かなかった |
| kriomant/ch57x-keyboard-tool | Rust | 514C:8850 の記載あり。k884x 形式でメディアキー・マウス・LED と「確定」の命令が合った |
| kamaaina/macropad_tool | Rust | 設定の読み出し命令と、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 |
| メディアキー | 02 | 00、Usage 下位、Usage 上位 | 音量を上げる → 00 E9 00 |
| マウス(クリック) | 03 | 01、修飾、ボタン | 左クリック → 01 00 01 |
| マウス(ホイール) | 03 | 03、修飾、00 00 00、量 | ホイール下 → 03 00 00 00 00 FF |
| 待ち時間 | 05 | 頭の [4][5] にミリ秒(リトルエンディアン、最大 6000) | キー入力とは別のパケットで送る |
| LED | 08 | 01 00 のあとに「色 × 16 + モード」。キー番号の位置は B0 | 常時点灯・水色 → 01 00 51 |
マウスと一緒に押す修飾キーは Ctrl=01、Shift=02、Alt=04 で、Win は使えません。
書き込んだあとの「確定」
割り当て1件ごとに、次の3つを続けて送ります。LED のときは 2 だけです。
AA AAFD FE FF(確定)AA AA
設定の読み出し
| 問い合わせ | 送るデータ | 返事 |
|---|---|---|
| 機種情報 | FB FB FB | FB 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 92. ノブの番号は「左に回す・押す・右に回す」の順
ノブは 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 を使います。
- Wireshark と USBPcap を入れて、PC を再起動する(USBPcap はドライバーなので、再起動まで使えない)
- Wireshark のコマンドライン版 tshark で、時間を決めて記録する
- 記録中に純正ソフトで設定を 1 つずつ、数秒空けて書き込む
- プログラマブルキーボードへの書き込み(エンドポイント
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 |
| LED | off / 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:])まとめ
格安プログラマブルキーボードも、書き込み口とパケットの形さえ分かれば、純正ソフトなしで自由に設定できます。ただし同じ系列でも機種ごとに細かい違いがあるので、次の順で進めるのがおすすめです。
- VID/PID と、Usage Page
FF00のインターフェースを確かめる - 既存のオープンソースから、自分の VID/PID に対応した形式を探す
- キー 1 個だけ書いて、動くか確かめる
- 全部の場所に a〜x を書いて、番号の対応を確かめる(ノブは動作ごとに 1 つずつ)
- 合わないところは USB 通信を記録して、純正ソフトと比べる
誰かのなにかの役に立ちますように!!
参考にしたもの
※ 記事の内容は、筆者の手元の 1 台(VID 514C / PID 8850)で確かめた結果です。本体に書き込む操作は自己責任で行ってください。
※ Amazonのアソシエイトとして、当サイトは適格販売により収入を得ています。