あんパン

こしあん派

USB Gadget API for Linux入門: BluetoothキーボードのイベントをUSB HIDのReportに変換する

はじめに

普段は各種開発用途でLinux、Windows、macOSを利用しており、これらはUSB切替器で同一のキーボード、トラックボールに接続されている。そのため機器切替が簡単にできないBluetooth接続のキーボードやマウスを導入することができなかった。そんなある日、以下の記事を読んで moguno/event2usbhid を利用すればBluetooth接続の入力機器をUSB切替器に接続できることを知った。この記事を読むまではUSB Gadget APIの存在も知らなかった。

moguno.hatenablog.jp

ということで最初の数日はこれを便利に使っていたのだけれど、ある日macOSで使った際に修飾キー(control, shift, alt, meta)が一切効かないことに気付いた。また、これは個人的な環境の話題だが、普段利用しているトラックボールの4つ目のボタンが反応しないことにも不便していた。これらを解消すべくUSB HIDの仕様やUSB Gadget APIについて調べて回ったところ、興が乗って同等のツールをフルスクラッチで書いてしまった。この記事ではツールを自分で実装するにあたって調べた内容についてメモがてら簡単に書き残す。

ただ単にツールを使いたい人は以下のrepoからREADMEに沿ってツールをインストールすることで利用することができる。現状で動作を確認しているのはRaspberry Pi Zero WHのみだが、おそらくRaspberry Pi 4上でも動く。

github.com

元となった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デバイスとして振る舞わせることができる。

Raspberry Piでこれらを利用するためには、事前にOTGモードを有効にした上で libcomposite モジュールをロードしておく必要がある。

  • /boot/config.txt の末尾に dtoverlay=dwc2 を追記する
  • modprobedwc2libcomposite を読み込んでおく
    • /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.

https://www.usb.org/hid

とのことで、Microsoftのページを辿っていくとLANGIDが記載されたPDFをダウンロードすることができ、この中に 0x0409en-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)76543210
0bytePaddingButton 3Button 2Button 1
1byteX
2byteY

つまり全長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 PageLogical 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)76543210
keyRIGHT GUIRIGHT ALTRIGHT SHIFTRIGHT CTRLLEFT GUILEFT ALTLEFT SHIFTLEFT 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を見ることでどのような実装になっているかを確認することもできる。

主に参考にしたページ等

*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