あんパン

こしあん派

GitHubにcurlで公開鍵を登録する2021

masawada.hatenablog.jp

ここでcurlで公開鍵を登録する方法を紹介していたのだけど、GitHub全域でこれまでのユーザ名・パスワード・OTPによるBASIC認証が使えなくなったようだった。以下のようなエラーが返る。

{
  "message": "Requires authentication",
  "documentation_url": "https://docs.github.com/rest/reference/users#create-a-public-ssh-key-for-the-authenticated-user"
}

ではどうすれば良いかというと、Personal Access Tokenを利用する。

docs.github.com

まず Personal Access Tokens から Generate new token を選択してPersonal Access Tokenを発行する。この際にpublic keysに関する権限をつけておく。

f:id:masawada:20210310111118p:plain

あとは以下で公開鍵を登録できるようになる。

curl -XPOST \
  -H 'Content-Type: application/json' \
  --basic -u '<user>:<personal_access_token>' \
  -d '{"title": "user@hostname", "key": "<public_key>"}' \
  'https://api.github.com/user/keys'

または以下のようにAuthorizationヘッダをつける形でも登録できるようだった。こちらの方がシンプルだと思う。

curl -XPOST \
  -H "Content-Type: application/json" \
  -H "Authorization: token <personal_access_token>" \
  -d '{"title": "user@hostname", "key": "<public_key>"}' \
  "https://api.github.com/user/keys"

登録した公開鍵はPersonal Access Tokenをrevokeすると削除される。あくまで一時的なものとして扱われるらしい。

一見 X-GitHub-OTP ヘッダをつけた方がセキュアなのではと思ったが、Personal Access Tokenを利用することで権限を絞って認可できるし一般的な認証方法に乗っかることができるから良いという話なんだろうか。個人的にはOSインストールの自動化のために利用しているのでPersonal Access Tokenをrevokeすると鍵が削除される仕様を若干不便に感じるけどその方がセキュアそうなので諦めるしかない。

gitでコミットする前のファイルを選択して編集する

repoで作業していて変更を加えたファイルを開き直してさらに追記したいケースは少なくない。historyから絞り込んで選択するよりは、差分が発生したファイルのリストから絞り込む方が見通しが良いのではないか、ということで普段は差分が発生したファイルから絞り込んでいる。以下のようなファイルを git-edit-changed という名前で $PATH のいずれかのディレクトリの中に置いて実行権限をつけておくと git edit-changed コマンドとして呼び出せる。

#!/bin/bash

filepath=$({\
  git ls-files $(git rev-parse --show-toplevel) --exclude-standard --others --full-name; \
  git diff --name-only HEAD; \
} | sort -u | peco)
if [ ${#filepath} -ne 0 ]; then
  cd $(git rev-parse --show-toplevel) && ${EDITOR} ${filepath}
fi

git ls-files $(git rev-parse --show-top-level) --exclude-standard --others --full-name では、untrackedなファイルをrepo rootからのパスとして出力している。 --others はgit管理されていないファイル群を出力するためのオプション。これを指定するだけでは .gitignore 等でignoreしているファイル群も含まれてしまうため、これらのファイルを無視するのが --exclude-standard--full-name を指定することで自分がどのディレクトリにいてもrepo rootからの相対パスとして出力できる。

git diff --name-only HEAD では、HEADからの差分を出力している。git diff --name-only 単体では、indexと現在の差分が出力されてしまうので、 HEAD をつけることで最新コミットからの差分を出力する。untrackedなファイルはindexに追加すると上記のコマンドでは出力されなくなり、こちらのコマンドで出力されるようになる。

これらの出力を {} でひとつにまとめてパイプで sort -u | peco に渡して絞り込んでから $filepath に代入している。といった具合でHEADとの差分一覧(コミットする前のファイル一覧)を取得できる。

どうぞご利用ください。

git管理下のファイルのみをtree表示する

2023-06-28 追記: tree が --gitignore オプションに対応して tree --gitignore で同様のことができるようになりました。

mama.indstate.edu


treeはみなさんご存知便利なコマンドで、cwd以下のファイルをツリー表示するもの。だいたいの環境にはデフォルトで入っていないので、 brew などで入れる必要がある。

$ tree
.
├── hello.txt
├── test
│   └── hoge.txt
└── world.txt

1 directory, 3 files

大抵のソフトウェアのプロジェクトでは node_modules のような依存ライブラリ群をプロジェクトのディレクトリに内包しているのが常で、gitで管理している場合は .gitignore で無視することがほとんどだと思う。このような環境下でtreeを使うとこれらのファイルが一気に出力されてノイズとなることがしばしば。初めて参加するプロジェクトのコードを読む際はどこから読むかあたりをつけるのにtreeコマンドが便利そうだが、正直このままでは使いづらい。

treeには --fromfile オプションがあり、パスのリストをファイルから読み取ってツリー表示することができる。更に --fromfile=. とすると、標準入力からパスのリストを受け取ることができる。git ls-files | tree --fromfile=. とすることで、git管理しているファイルのみ、つまり依存などが無視されて純粋にプロジェクトそのもののファイルリストのみがツリー表示されるようになる。例えば以下は以前書いたbt2usbhidのプロジェクトルートでの出力。.gitignore で指定されている bin/bt2usbhid はファイルとしては存在するが、ツリー表示には出力されていないことが分かる。

$ ls bin
bt2usbhid  init_bt2usbhid
$ git ls-files | tree --fromfile=.
.
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── bin
│   └── init_bt2usbhid
├── config
│   ├── 99-bluetooth.rules.example
│   ├── bt2usbhid_keyboard.service
│   ├── bt2usbhid_mouse.service
│   └── init_bt2usbhid.service
└── src
    ├── keyboard.c
    ├── keyboard.h
    ├── main.c
    ├── mouse.c
    └── mouse.h

3 directories, 14 files

github.com

以下のシェルスクリプトを $PATH のいずれかのディレクトリの中に git-tree という名前で実行権限をつけて入れておくと、 git tree サブコマンドでこれを呼び出すことができる。

#!/bin/bash

set -e

if ! type tree > /dev/null 2>&1; then
  echo 'command not found: tree'
  exit 1;
fi

git ls-files | tree --fromfile=. "$@"

インターネットで探したところ、これと同等の機能をGoやPythonで実装したツールが出てきたが、tree自体に実現する機能があるのでtreeを使う方が柔軟性があって良い。treeにはまだ面白いオプションがいくつかある。例えば -J オプションでツリーをJSONに変換することができる。上記のスクリプトはそのままオプションを受け取れるようにしているので、以下のようにJSONを出力することができる。

$ git tree -J
[{"type":"directory","name": ".","contents":[
    {"type":"file","name":".gitignore"},
    {"type":"file","name":"LICENSE"},
    {"type":"file","name":"Makefile"},
    {"type":"file","name":"README.md"},
    {"type":"directory","name":"bin","contents":[
      {"type":"file","name":"init_bt2usbhid"}
    ]},
    {"type":"directory","name":"config","contents":[
      {"type":"file","name":"99-bluetooth.rules.example"},
      {"type":"file","name":"bt2usbhid_keyboard.service"},
      {"type":"file","name":"bt2usbhid_mouse.service"},
      {"type":"file","name":"init_bt2usbhid.service"}
    ]},
    {"type":"directory","name":"src","contents":[
      {"type":"file","name":"keyboard.c"},
      {"type":"file","name":"keyboard.h"},
      {"type":"file","name":"main.c"},
      {"type":"file","name":"mouse.c"},
      {"type":"file","name":"mouse.h"}
    ]}
  ]},
  {"type":"report","directories":3,"files":14}
]

これでファイルリストをプログラムやjqなどのコマンドに食わせやすくなる。同様に -X オプションでXMLを出力することもできるし、 -H . とすればHTMLを出力することもできる。もっともこれらが役立つ場面があるかというと滅多になさそうではあるが、覚えておくと役に立つことがあるかもしれない。


追記(2021-02-24): 指摘いただいて、 git-tree のスクリプトの $@"$@" に変更しました。引数にスペースが含まれている場合に分割されてしまうためです

自作ラップトップ: ThinkPad X270を組み立てる

本業を含めて作業環境をLinuxに移行してからmacOSで開発するのが億劫になってしまい、ここ半年はDeskMiniで組んだ自作機に入れたArch Linuxで個人の開発をしていた。しかしそろそろ寒いし布団の中で開発したいなーと思い、サブPCとしてラップトップを見繕うことにした。条件は以下の通り。

  • 新品
    • 天板とかキーボードがテカってるPCを使いたくないので
  • メモリは16GB以上
    • 最近の人権は64GBと聞くけどサブなのでこれくらい
  • CPUはi5くらい
    • i3は避けたいかな〜くらいのつもり。あんまり強いこだわりでない
  • 13インチ以下のサイズでFull HD以上の解像度
    • できれば12インチ台が良い
    • デカいラップトップがあんまり好きじゃない。4Kとかまでは不要
  • USキーボード
    • 別にJISキーでも開発できるだろうけど自宅のPCは全部USキーで統一してるので
  • できればRJ45ポートがある
    • ネットワーク回り触る人、RJ45ポートがないPCつらくないですか? わざわざアダプタ持ち歩くの?
    • 最近のPC、薄型を目指しすぎていてだいたい省略されていて困ることが多い
  • できればUSB-Cで充電できる
    • 変な充電器を増やしたくない
    • 家中にUSB-C 30W or 60Wの充電ケーブルが生えているからこれを使いたい
  • できればバッテリを換装できる
    • バッテリが弱まってたら換装できると便利なので

これだけ条件をつけるとだいたい絞られてきて、手頃なのはXPSかThinkPadあたりかなーというところ。どちらかというとXPSに傾いていた。が。メモリを16GBにしてSSDを512GBにすると13.5万程度とまあまあ高い。いや高いわけではなくて普通の値段なのは分かっているんだけど、サブにしてはやっぱり高い気がする。しかし他に選択肢もなぁと思っていたところ、突然中古のThinkPadを買ってきて外装部品変えたらいいじゃんという気持ちになった。本業で使っているのもT480なのでスムーズに生活に溶けこむであろう。世の中にはThinkPadを自分で組み立てる人間がいると聞いたことがあるし、自分もそちら側の人間になってみたい。

早速調べて実行に移すことにした。以下のような手順で進めることにした。

  • 組み立てるThinkPadのモデルを選ぶ
  • ヤフオクでThinkPadの中古を買う
  • ThinkPadの構成部品を調べつつ本体を解体して必要なパーツをリストする
  • パーツを注文する
  • 全てのパーツが届いたら組み立てる

ところで自作PCってなんで自作なんだろうか。解せないところはありつつも一般にはPCパーツを買ってきて組み上げる行いを自作PCと呼んでいるはずなのでこの記事でもそれに従った。

組み立てるThinkPadのモデルを選ぶ

13インチ以下が良いので、おのずとXシリーズに絞られる。Xシリーズの中でも比較的最近の機種を見て回ることにした。

  • X250
    • 中古価格がこなれていて安い
    • メモリがDDR3なのでもはや売られておらず、16GBのものはめっちゃ高い
    • 一応変換アダプタを噛ませればUSB-Cで充電できる
  • X260
    • X250よりちょっと高い
    • DDR4になっているのでメモリの入手性が向上した
    • 一応変換アダプタを噛ませればUSB-Cで充電できる
  • X270
    • X260よりちょっと高いけどだいたい同じくらい
    • USB-Cポートを搭載していて一応ここから充電できる
  • X280
    • 中古でも高い
    • メモリを換装できない
  • X390
    • 高い、新品で買ったほうがマシ
    • 13インチ
    • バッテリを換装できない
    • RJ45ポートがない
    • WLANアダプタとかがオンボードになってて突然Wi-Fi6にしたくなってもできない、LTEモジュールも搭載できない
      • もともと条件として考えてなかったのでどうでもいいけど
    • 正直これが発表されたときにXシリーズで良いと思っていたところが全てなくなって愕然とした

という感じでリストしたところ、どうみてもX270だよねーということで落ち着いた。一瞬X250と迷ったものの、やはり16GBのメモリの入手性が悪いのがひっかかった。USB-Cポートを備えているのも良くて、X260を組むならX270の方が明らかに良い。

ヤフオクでThinkPadの中古を買う

そうと決まれば本体を買うぞということでいろいろ見て回った。最初はBe-Stockかなーと思っていたが、どうせ外装全部換装するのだからボコボコになった中古でもかまわんということでヤフオクで安そうなX270を競り落とすことにした。i5モデルの相場はだいたい3万弱くらいで、運が良いと送料込みで2.5万くらいで買える。入札したところ他のユーザと微妙に競ることになったけど、無事2.5万くらいで入手することができた。

ThinkPadの構成部品を調べつつ本体を解体して必要なパーツをリストする

ではどの部品を買ったらよいのか。ThinkPadは保守マニュアルが充実しているのでこれを見ればどのような部品で構成されているのかが分かる。

また、以前のものがどうかは分からないが、サポートサイトでThinkPadのシリアルナンバーを入力すると当該製品に使われている部品を表示してくれる。型番も表示してくれるので便利。

届いたThinkPadのシリアルナンバーをこれに入力しつつ、解体して必要なパーツをリストする。

f:id:masawada:20210202043721j:plain
ベゼルを外したところ

最初は換装する必要がないと思っていたWi-Fiアンテナやカメラは天板に接着されているので新たに購入する必要があることが分かった。その他にも、指紋リーダーとトラックパッドと本体を繋ぐフレキ(ひとつのFPCで両方に繋がっている)とCMOSバッテリがキーボードベゼルに接着されていることも分かった。

最終的に、以下のパーツを購入することにした。

  • キーボードベゼル: 01HW957
  • 指紋センサー: SC50F54334
  • トラックパッド用FPC: SM10M38706
  • ベースカバーアセンブリー: 01HY501
  • コイン型CMOSバッテリ: AliExpressになかったのでAmazonで適当に検索して探した
  • ファン: 01HY452
  • トラックパッド: SM10K87872
  • キーボード: 01EP062
  • LCDベゼル: 01HW947
  • カメラモジュール: 00HN376
  • カメラケーブル: 01AW448
  • ワイヤレスLAN/WANアンテナ: 01HW955
  • LCD背面カバー: 01HW944
  • LCDパネル: LP125WF2-SPB2
  • LCDヒンジ: 00HN990
  • X270ステッカー: 適当に検索すると出てくる(LCDベゼルの右下にあるモデル名の部分)
  • カメラカバー: 適当に検索すると出てくる(LCDベゼル側に貼るやつ)

…ほぼ全部になってしまった。

なお購入した本体についていたLCDはFWXGAだったが、交換部品はFull HDのものにした。FWXGAのものはeDPコネクタが中央からズレていて横に寄っており、同じく横にズレたパネルを買わないとケーブルの長さが足りなくなってしまう。純正品のFull HDパネルは中央にあるようなので、元々Full HDだった場合は上記のパネルを買ってはいけない。

パーツを注文する

リストしたパーツを順に購入していく。基本はAliExpressで購入する。後述するがAliExpress Standard Shippingで注文することを強くオススメしたい。あと、必ずNew and Originalと書いてあるものを選ぶこと。これが書いていない場合は中古だったり互換品だったりする。もっともOriginalと書いてあるものが本当に純正品なのかはかなり疑わしいところではある。

AliにないものはAmazonなどで探すと出てくる場合がある。CMOSバッテリはAliで見当たらなかったのでAmazonで注文した。

その他、上記にないものとして512GBのSSDと16GBのDDR4 S.O.DIMMメモリを購入した。ファンを交換するのでCPUグリスも必要だが、手持ちのものがあったので買わなかった。あとはもともとCPUとファンの間に塗布されていて固着しているであろうグリスを拭くために無水アルコールがあると良い。こちらも手持ちのものがわずかに残っていたので買わなかった。

全てのパーツが届いたら組み立てる

パーツが届いたら組み立てる。注文から到着までだいたい2-3週間くらいで、全て公称の14-21days以内で届いた。今回の発送はすべてyanwenに引き継がれて台湾経由で日本に入り、そこからは日本郵政に引き継がれる。YPで終わるトラッキングコードが払出されている場合はyanwenのサイトでトラッキングできて、ここに日本に入ってきてからのコード(LXで始まってTWで終わる?)も表示される。毎日のようにパーツが届く時期があって、ディアゴスティーニのようなワクワク感があった。

分解する際はBIOSからバッテリを無効化して、一番最初にバッテリのコネクタを抜いておくこと。一度解体しているのでハマりどころも分かっていてここからはサクサク進む。といっても組み上げるまで2時間くらいかかったが。

f:id:masawada:20210202043724j:plain
パーツたち(一部)

ネジなどの細かいパーツは紛失しないようにジッパーがついた小さいビニール袋に入れてどこのパーツかを油性ペンで描いておくと良い。同一のパーツを固定するネジであっても場所によってネジの種類が違うことがあるので注意して分類しておく必要がある。

f:id:masawada:20210202043730j:plain
Wi-Fiアンテナを配線しようとしているところ

組み上げたらあとは電源を投入してBIOSの時刻を合わせて、OSをインストールすれば良い。Secure Bootのキーをリセットしておかないとインストーラを立ち上げられないことがあるので注意。OSインストール後にハマったことについては以前記事にしていた。

masawada.hatenablog.jp

発生したトラブル

とまあここまで順調に組み上げたように書いてきたけど、様々なトラブルにぶちあたったのでいくつか紹介したい。細かい話題だと、注文したWi-Fiアンテナの在庫が尽きていて「別のモデルのアンテナを改造して送るが良いか?」と連絡が来たりした(その後発注してくれて正しい製品が届いた)。

ディスプレイがうまく閉まらない

組み上げたは良いがディスプレイがうまく閉まらない。どうも天板が微妙に歪曲しているからなのかキーボード付近が盛り上がっているからなのか干渉してうまく閉まらない。これに関してはどうしようもないのでひとまず無視することにした。Aliで売られているパーツ、実は正規品ではありつつも検品かなんかで弾かれたやつが流れてるんじゃないかという気がする。どれも微妙に噛み合わせが悪い。

あんまりよくないのは分かっていつつも最近はディスプレイを閉じた状態で上に重たいものを載せて矯正している。少しマシになったかもしれない??

パーツが届かない

これまで運が良かったからなのかAliExpressで買ったものはだいたい届いていたのだけど、今回初めて届かないパーツがあった。正確には一部のパーツが含まれていなかった。すぐさま紛争を開始(Open Dispute)してお金を取り戻した。含まれていなかったパーツは別のショップで購入した。

AliExpress Standard Shippingを利用している場合、パーツがないなどで紛争を開始するとAliExpressが紛争の相手となってくれる。今回は届いたパーツの写真を撮って上げることで確認として含まれていない分を返金してくれた。結構ザルなので本当にこれでいいのか…と思うものの、まあいろいろやりとりするコストよりかは安くつくのであろう。自分はある程度利用履歴があるからさっと認めてくれたのかもしれない。よくわからん。

AliExpress Standard Shipping以外の発送方法を利用した場合は紛争の相手がショップになるようで、AliExpressは仲介をしてくれるだけの存在になる。この場合返事を待ったり相手がゴネたりと対応が面倒になるケースがあるという情報をインターネットでみかけたので特に理由がなければAliExpress Standard Shippingが良いと思う。

ディスプレイのバックライトが死ぬ

本当につらい出来事だったのだけど、ディスプレイのバックライトがつかなくなった。最初はケーブルの接触不良と思っていたが、iPhoneのフラッシュを常時点灯にして画面に近づけたところ表示が見えたのでバックライトが死んだと判断した。

いろいろ調べたところ、どうも分解する際にBIOSでバッテリを無効化するのを忘れてディスプレイを取り外ししたせいでロジックボード側のヒューズ飛んでバックライトがつかなくなったようだった。そう、ロジックボードなのでつまり本体の買い直しになる。直せるひとはヒューズを交換したら直せるのかもしれないが、そこまで労力をかける気持ちがなかったので本体を買い直した。2.5万の追加出費は痛い。

おわりに

かかった金額を合計したところ以下のようになった。

  • 本体2台分: 54,480円
  • SSD: 6,182円
  • メモリ: 5,770円
  • その他のパーツ: 40,445円
  • 合計: 106,877円

うーん微妙な結果! やはり本体を2台分購入することになったのが痛手だった。これがなければ8万円以下で収まっていたのだが。とはいえ故障してもパーツを買ってきたら自分の手で直せることが分かったし、新品のPCを買うよりは安かったしなにより最初に考えていた自分の要望は全て満たすことができた。16GBのメモリにしたので32GBへの拡張性も残されている。

実は去年の12月くらいから動き始めて年末には組み上げていたのだけれど、年始からこれまで1ヶ月ちょっと、ほぼ毎日のように組み上げたThinkPadで何かしらの作業をしていて満足度がかなり高い。ニッチな趣味であることは自覚しつつも、時間とお金に余裕がある人・単にラップトップPCを買うことに満足できない人には是非オススメしたいと思う。

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