はじめに
普段は各種開発用途でLinux、Windows、macOSを利用しており、これらはUSB切替器で同一のキーボード、トラックボールに接続されている。そのため機器切替が簡単にできないBluetooth接続のキーボードやマウスを導入することができなかった。そんなある日、以下の記事を読んで moguno/event2usbhid を利用すればBluetooth接続の入力機器をUSB切替器に接続できることを知った。この記事を読むまではUSB Gadget APIの存在も知らなかった。
ということで最初の数日はこれを便利に使っていたのだけれど、ある日macOSで使った際に修飾キー(control, shift, alt, meta)が一切効かないことに気付いた。また、これは個人的な環境の話題だが、普段利用しているトラックボールの4つ目のボタンが反応しないことにも不便していた。これらを解消すべくUSB HIDの仕様やUSB Gadget APIについて調べて回ったところ、興が乗って同等のツールをフルスクラッチで書いてしまった。この記事ではツールを自分で実装するにあたって調べた内容についてメモがてら簡単に書き残す。
ただ単にツールを使いたい人は以下のrepoからREADMEに沿ってツールをインストールすることで利用することができる。現状で動作を確認しているのはRaspberry Pi Zero WHのみだが、おそらくRaspberry Pi 4上でも動く。
元となったevent2usbhidと比べて、以下の機能が足されている。
- macOSのサポート
- 修飾キーを利用できる
- 4ボタンマウスへの対応
- 主にKensingtonのトラックボールを意識している
- bt2usbhidを起動中はBluetooth接続機器の入力をLinux本体に反映させない
- こちらはevent2usbhidにPull Requestを送っている状態
- その他細かな機能
- USB HIDの仕様上定義されているキーのいくつかに余分に対応している(はず)
なおこのツールを公開するにあたって id:moguno さんには状況を事前に事情をお伝えしており、公開を快諾していただきました。ありがとうございます!
USB Gadget APIとlibcomposite
bt2usbhidではUSB Gadget APIを利用している。このAPIはLinuxカーネルに組み込まれたもので通常のPCでは利用することができず、対応したハードウェアのみで利用することができる。Raspberry Pi ZeroとRaspberry Pi 4はこのUSB Gadget APIに対応している。 libcomposite
モジュールを有効にすると、適切なディレクトリに設定ファイルを書き出すだけで端末をUSBデバイスとして振る舞わせることができる。
- USB Gadget API for Linux — The Linux Kernel documentation
- Linux USB gadget configured through configfs — The Linux Kernel documentation
- linux/composite.c at master · torvalds/linux · GitHub
Raspberry Piでこれらを利用するためには、事前にOTGモードを有効にした上で libcomposite
モジュールをロードしておく必要がある。
/boot/config.txt
の末尾にdtoverlay=dwc2
を追記するmodprobe
でdwc2
とlibcomposite
を読み込んでおく/etc/modules
に足すでも可- bt2usbhidではUSB Gadgetの初期化スクリプトの中で
modprobe
を実行している
Gadgetを初期化する
概ね以下のインストラクションに沿えば良い。
設定はconfigfsに書き出す。Raspberry Piでは自動で /sys/kernel/config
以下にマウントされる。この領域は揮発するので起動する度に設定ファイルを書き出す必要がある。
gadgetを作成する
/sys/kernel/config/usb_gadget/
以下にgadgetを定義するディレクトリを任意の名前で作成して、その中にidVendor, idProductファイルを書き出す。
$ export GADGET_DIR=/sys/kernel/config/usb_gadget/g1 $ mkdir -p ${GADGET_DIR} $ echo <idVendor> > ${GADGET_DIR}/idVendor $ echo <idProduct> > ${GADGET_DIR}/idProduct
idVendorはUSB-IFによって払い出されるので、自由に指定することはできない。bt2usbhidではidVendorに 0x1d6b
(Linux Foundation)、idProductに 0x0104
(Multifunction Composite Gadget)を指定している。
gadgetを定義するディレクトリにserialnumber, manufacturer, productを書き出す。この値は任意で良い。
$ mkdir -p ${GADGET_DIR}/strings/0x409/ $ echo <serialnumber> > ${GADGET_DIR}/strings/0x409/serialnumber $ echo <manufacturer> > ${GADGET_DIR}/strings/0x409/manufacturer $ echo <product> > ${GADGET_DIR}/strings/0x409/product
0x409
はLanguage Identifierであり、en-US
を表している。
To get the latest LANGID definitions go to https://docs.microsoft.com/en-us/windows/desktop/intl/language-identifier-constants-and-strings. This page will change as new LANGIDs are added.
とのことで、Microsoftのページを辿っていくとLANGIDが記載されたPDFをダウンロードすることができ、この中に 0x0409
が en-US
として記載されている。
configを作成する
configのディレクトリを作成して、中にconfigurationファイルを書き出す。configのディクトリ名は <任意の文字列>.<数値>
である必要がある。configurationファイルの中身は任意の値で良い。
$ mkdir -p ${GADGET_DIR}/configs/c.1/strings/0x409 $ echo <configuration> > ${GADGET_DIR}/configs/c.1/strings/0x409/configuration
functionを作成する
functionのディレクトリを作成して、デバイスがどのように振る舞うのかを定義する。キーボードやマウスなどひとつの機能ごとに作成する。ここがUSB Gadget APIのキモといえる。
$ mkdir -p ${GADGET_DIR}/functions/hid.usb0 $ echo <protocol> > ${GADGET_DIR}/functions/hid.usb0/protocol $ echo <subclass> > ${GADGET_DIR}/functions/hid.usb0/subclass $ echo <report_length> > ${GADGET_DIR}/functions/hid.usb0/report_length $ echo -ne <report_description> > ${GADGET_DIR}/functions/hid.usb0/report_desc
protocolはこのデバイスが何を表現しているのかを通知する。 1
はキーボードで 2
はマウスを表す。subclassは、USB HIDデバイスがブートプロトコルに対応している(=BIOSで利用できる)かどうかを通知する。している場合は 1
、していない場合は 0
。
USBデバイスはReportという単位で情報をやりとりする。このReportの構造を定義するのがReport Descriptorで、report_desc
として書き出す。バイナリファイルとして書き出したいので、このファイルだけ echo -ne
オプションをつけている。Report Descriptorについては後述する。report_desc
はReport Descriptorのbyte長を表現している。
functionとconfigを紐付ける
これは単純にconfig内にfunctionへのsymlinkを置けば良い。
$ ln -s ${GADGET_DIR}/functions/hid.usb0 ${GADGET_DIR}/configs/c.1/hid.usb0
gadgetを有効化する
gadgetを有効化するには、USB Device Controllerの名前をUDCファイルに書き出せば良い。
$ ls /sys/class/udc > ${GADGET_DIR}/UDC
これで、USBデバイスを接続した端末から lsusb
するとデバイスが表示される。
$ lsusb ... Bus 001 Device 005: ID 1d6b:0104 Linux Foundation Multifunction Composite Gadget ...
また、USBデバイスとなった端末上では、キーボードとマウスの両方を定義していれば /dev/hidg0
/dev/hidg1
ファイルが生成されていることを確認できる。
$ ls -l /dev/hidg* crw------- 1 root root 239, 0 Feb 6 14:15 /dev/hidg0 crw------- 1 root root 239, 1 Feb 6 14:15 /dev/hidg1
これらのファイルに適切にReportを書き込むことで、キーボードやマウスとして接続先の端末に通信を行うことができる。
Report Descriptorを定義する
前述の通り、USBの通信において実際にやりとりするデータをReport、Reportの構造の定義をReport Descriptorと呼ぶ。ここではふんわりとReport Descriptorの記法について書き記す。多くのことを省いているので、より詳細な点については直接仕様を当たると良い。
例: マウスのReport Descriptor
Report Descriptorでは、Reportのbit列のどの範囲がどの用途に使われているのかを定義する。例えば、3ボタンマウスのReport Descriptorは以下のようになる*1。
Usage Page (Generic Desktop), ;Use the Generic Desktop Usage Page Usage (Mouse), Collection (Application), ;Start Mouse collection Usage (Pointer), Collection (Physical), ;Start Pointer collection Usage Page (Buttons) Usage Minimum (1), Usage Maximum (3), Logical Minimum (0), Logical Maximum (1), ;Fields return values from 0 to 1 Report Count (3), Report Size (1), ;Create three 1 bit fields (button 1, 2, & 3) Input (Data, Variable, Absolute), ;Add fields to the input report. Report Count (1), Report Size (5), ;Create 5 bit constant field Input (Constant), ;Add field to the input report Usage Page (Generic Desktop), Usage (X), Usage (Y), Logical Minimum (-127), Logical Maximum (127), ;Fields return values from -127 to 127 Report Size (8), Report Count (2), ;Create two 8 bit fields (X & Y position) Input (Data, Variable, Relative), ;Add fields to the input report End Collection, ;Close Pointer collection End Collection ;Close Mouse collection
この各行をhexで表現すると以下のようになる。
\0x05 \0x01 // Usage Page (Generic Desktop), ;Use the Generic Desktop Usage Page \0x09 \0x02 // Usage (Mouse), \0xA1 \0x01 // Collection (Application), ;Start Mouse collection \0x09 \0x01 // Usage (Pointer), \0xA1 \0x00 // Collection (Physical), ;Start Pointer collection \0x05 \0x09 // Usage Page (Buttons) \0x19 \0x01 // Usage Minimum (1), \0x29 \0x03 // Usage Maximum (3), \0x15 \0x00 // Logical Minimum (0), \0x25 \0x01 // Logical Maximum (1), ;Fields return values from 0 to 1 \0x95 \0x03 // Report Count (3), \0x75 \0x01 // Report Size (1), ;Create three 1 bit fields (button 1, 2, & 3) \0x81 \0x02 // Input (Data, Variable, Absolute), ;Add fields to the input report. \0x95 \0x01 // Report Count (1), \0x75 \0x05 // Report Size (5), ;Create 5 bit constant field \0x81 \0x01 // Input (Constant), ;Add field to the input report \0x05 \0x01 // Usage Page (Generic Desktop), \0x09 \0x30 // Usage (X), \0x09 \0x31 // Usage (Y), \0x15 \0x81 // Logical Minimum (-127), \0x25 \0x7F // Logical Maximum (127), ;Fields return values from -127 to 127 \0x75 \0x08 // Report Size (8), \0x95 \0x02 // Report Count (2), ;Create two 8 bit fields (X & Y position) \0x81 \0x06 // Input (Data, Variable, Relative), ;Add fields to the input report \0xC0 // End Collection, ;Close Pointer collection \0xC0 // End Collection ;Close Mouse collection
このReport Descriptorに対応するReportは以下のような構造になる。
(bit) | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
0byte | Padding | Button 3 | Button 2 | Button 1 | ||||
1byte | X | |||||||
2byte | Y |
つまり全長3byteで、0byte目ではボタンの状態、1byte目でX軸の移動、2byte目でY軸の移動を表現する。
さらに分解してボタン部分について見てみる。
\0x05 \0x09 // Usage Page (Buttons) \0x19 \0x01 // Usage Minimum (1), \0x29 \0x03 // Usage Maximum (3), \0x15 \0x00 // Logical Minimum (0), \0x25 \0x01 // Logical Maximum (1), \0x95 \0x03 // Report Count (3), \0x75 \0x01 // Report Size (1), \0x81 \0x02 // Input (Data, Variable, Absolute), \0x95 \0x01 // Report Count (1), \0x75 \0x05 // Report Size (5), \0x81 \0x01 // Input (Constant),
各行をitemと呼び、それぞれの0byte目はitemの型、データ部のbyte長を含んだprefixになっている。例えば1行目は \0x05
がUsage Pageを表現する。Usage Pageのうち、何を指定するかはその後のdata部に定義する。ここではButton Pageを指定したいので \0x09
にしている*2。prefixがデータ部のbyte長を含んでいるので、データ部は可変長、具体的には0,1,2,4byteのいずれかになっている(さらに長いデータ部を取る方法もある)。
itemにはmain item, global item, local itemが存在している。Report Descriptorのパース時の挙動はこれらのitemの種類に寄る*3。main itemが出てきた場合は、main itemに対してそれまで登場したglobal item, local itemの設定値を適用する。上記ボタンのDescriptorの例でいうと、最初に登場する Input(Data, Variable, Absolute)
には、それよりも上に登場した Usage Page
や Logical Maximum
などの値が適用される。main itemを解釈した時点でlocal itemの設定値は揮発して、global itemの設定値は以降のmain itemの値に引き継がれる。
ひとつめのInputは
- Usage PageがButton
- Button Pageで定義されているボタンのうち、Button 1からButton 3までを定義する
- 値は0 or 1
- 各ボタンの通信で使うbit数は1(0 or 1なので), 数は3(ボタンが3つなので)
ということになる。後続のInputはReportの穴埋め(padding)として指定している。詰めずにInputごとに1byteずつ出力する方がReport作成処理が簡素になるので穴埋めしているものと思われる(仕様にはpaddingできるとしか記載されていないのであくまで想像)
例えば4ボタンマウスを作りたければ、Button 4を定義することにしてReport Countを3から4に増やせば良いので、Descriptorを以下のように指定すれば良い。
\0x05 \0x09 // Usage Page (Buttons) \0x19 \0x01 // Usage Minimum (1), \0x29 \0x04 // Usage Maximum (4), \0x15 \0x00 // Logical Minimum (0), \0x25 \0x01 // Logical Maximum (1), \0x95 \0x04 // Report Count (4), \0x75 \0x01 // Report Size (1), \0x81 \0x02 // Input (Data, Variable, Absolute), \0x95 \0x01 // Report Count (1), \0x75 \0x04 // Report Size (4), \0x81 \0x01 // Input (Constant),
macOSにおけるキーボードのReport Descriptorの解釈
冒頭にも書いたとおり、macOSにおいてはmodifierキーが動作しない現象が発生していた。通常のUSBキーボードのReport Descriptorでは0byte目をmodifierキーの領域として定義している。具体的には以下のような調子になっている。
\x05 \x07 // Usage Page (Key Codes) \x19 \xE0 // Usage Minimum (224) \x29 \xE7 // Usage Maximum (231) \x15 \x00 // Logical Minimum (0) \x25 \x01 // Logical Maximum (1) \x75 \x01 // Report Size (1) \x95 \x08 // Report Count (8) \x81 \x02 // Input (Data, Variable, Absolute)
224-231はmodifierキーを指していて*4、この8つのキーについてはReportの0byte目の各bitを立てることでON/OFFを通知できる。
(bit) | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
key | RIGHT GUI | RIGHT ALT | RIGHT SHIFT | RIGHT CTRL | LEFT GUI | LEFT ALT | LEFT SHIFT | LEFT CTRL |
Linuxではmodifierキーも通常のキーと同様にキーコードを送れば認識されているが、macOSの場合はmodifierキーの場合はこの0byte目のbit列をON/OFFする必要があるようだった。
Reportを出力するプログラムを書く
ここまで設定すれば、あとはReport Descriptorの定義に沿って /dev/hidg0
や /dev/hidg1
に出力すれば良い。ここはどんな言語でも良い。bt2usbhidではReportの構造体を定義しておいて、そのままwrite
で出力している。
Report DescriptorとReportをデバッグする
実装がうまく動かない場合、Report Descriptorと実際に出力しているReportを確認しながらデバッグすることになる。usbhid-dump
というツールと、 hidrd-convert
というツールが役に立つ。usbhid-dump
はArch Linuxであれば sudo pacman -Sy usbutils
でインストールできる。 hidrd-convert
はAURからインストールすることができる。
利用するにはまず lsusb
して、addressを確認する。以下の例ではBusが001, Deviceが005なので、addressは 001.005
となる。
$ lsusb ... Bus 001 Device 005: ID 1d6b:0104 Linux Foundation Multifunction Composite Gadget ...
このデバイスのReport Descriptorを知りたい場合は、以下のコマンドで出力できる。
$ sudo usbhid-dump -e descriptor -a 001:005 | grep -v : | xxd -r -p | hidrd-convert -o spec Usage Page (Desktop), ; Generic desktop controls (01h) Usage (Mouse), ; Mouse (02h, application collection) Collection (Application), Usage (Pointer), ; Pointer (01h, physical collection) ...
Reportを全て確認したい場合は、以下のコマンドで待ち受けることができる。この例では、bt2usbhidのレポートを待ち受けており、キーボード上で a
を押下して離した後、マウスの主ボタンを押下して離している。
$ sudo usbhid-dump -e all -a 001:005 001:005:001:DESCRIPTOR 1612887753.901255 05 01 09 02 A1 01 09 01 A1 00 05 09 19 01 29 04 15 00 25 01 75 01 95 04 81 02 75 04 95 01 81 01 05 01 09 30 09 31 09 38 15 81 25 7F 75 08 95 03 81 06 C0 C0 001:005:000:DESCRIPTOR 1612887753.901707 05 01 09 06 A1 01 05 07 19 E0 29 E7 15 00 25 01 75 01 95 08 81 02 75 08 95 01 81 01 05 08 19 01 29 05 75 01 95 05 91 02 75 03 95 01 91 01 05 07 19 00 2A FF 00 15 00 26 FF 00 75 08 95 06 81 00 C0 Starting dumping interrupt transfer stream with 1 minute timeout. 001:005:000:STREAM 1612887757.455080 00 00 04 00 00 00 00 00 001:005:000:STREAM 1612887757.534081 00 00 00 00 00 00 00 00 001:005:001:STREAM 1612887759.112078 01 00 00 00 001:005:001:STREAM 1612887759.190067 00 00 00 00
既製品のUSBデバイスのaddressを指定すれば、Report DescriptorとReportを見ることでどのような実装になっているかを確認することもできる。
主に参考にしたページ等
- USB Gadget API for Linux — The Linux Kernel documentation
- USB Gadget APIの一般的な話題が載ってる
- Linux USB gadget configured through configfs — The Linux Kernel documentation
- USB Gadgetを初期化する手順が載ってる
- Device Class Definition for HID 1.11 | USB-IF
- USB HIDの一般的な仕様が載ってる
- HID Usage Tables 1.3 | USB-IF
- Keyboardのkey codeとかが載ってる
*1:Device Class Definition for Human Interface Devices (HID) Firmware Specification 5/27/01 Version 1.11 p.25より引用 https://www.usb.org/sites/default/files/hid1_11.pdf
*2:HID Usage Tables FOR Universal Serial Bus p.102 https://usb.org/sites/default/files/hut1_21_0.pdf
*3:Device Class Definition for Human Interface Devices (HID) Firmware Specification 5/27/01 Version 1.11 p.15 https://www.usb.org/sites/default/files/hid1_11.pdf
*4:HID Usage Tables FOR Universal Serial Bus p.87 https://usb.org/sites/default/files/hut1_21_0.pdf