CircuitPythonでUSB HIDデバイスをカスタマイズする方法

AdafruitのLearning Guideの非公式日本語訳です。
英語独特の言い回しを可能な限り日本語的な表現に直していますが、不自然に感じる部分も残っていますことを何卒ご容赦ください。

概要

CircuitPythonでの標準のUSBデバイス

CircuitPythonボードをホストコンピュータに接続すると、複数のUSBデバイスとして表示されます。
通常は次のように表示されます。

  • CIRCUITPYドライブはUSBの “Mass Storage”(MSC)デバイスとして表示されます。
  • REPLへのシリアル接続は、WindowsではCOMポート、Linuxでは/dev/ttyデバイス、MacOSでは/dev/cuデバイスとして表示されます。
  • MIDI入出力ストリーム。これはオーディオデバイスの一種として表示されます。
  • マウスやキーボードなど、これらはすべて「ヒューマンインターフェースデバイス」(HID)の一種です。これらのHIDデバイスは、1つの「コンポジット」デバイスにまとめられます。

デバイスを非表示にできる?

CircuitPythonがこれらのUSB機能を提供するのは素晴らしいことですが、時にはそのすべてを見たくないこともあります。例えば、ボリュームコントロール、マクロキーパッド、独自のキーボードなどを作って、常に接続しておきたいとします。その場合、CIRCUITPYとシリアル接続は見えないようにしたいと思うでしょう。別のCircuitPythonボードを接続した場合、どれが先に接続していたボードで、どれが新しいボードか分からなくなってしまいます。

CircuitPython 7.0.0リリース前は、USBデバイスを無効にするにはCircuitPythonを自分でカスタムビルドするしかありませんでした。しかし、自分でビルドするのは簡単ではありません。また、プロジェクトのcode.pyを編集したり、REPLに接続したい場合は、通常のCircuitPythonのビルドをリロードしないとアクセスできません。

ランタイムでのデバイスの有効化と無効化

CircuitPython 7.0.0以降では、カスタムビルドを作ってデバイスを有効化/無効化をする必要はありません。代わりに、制御したいデバイスを無効にしたり有効にしたりするコードをboot.pyファイルに書くことができます。例えば、このコードは、起動時にCIRCUITPYとREPLをボタンを押した時以外は無効にします。(気をつけてほしいのは、無条件に両方を無効にしてはいけないということです。ロックアウトされてしまいます)

if not button.value:
    storage.disable_usb_drive()    # Turn off CIRCUITPY.
    usb_cdc.disable()              # Turn off REPL.

2個目のシリアルポートを追加する

標準デバイスのオン/オフだけでなく、2つ目のシリアルポートを有効にすることもできます。2つ目のCOMポートまたは/dev/ttyまたは/dev/cuデバイスが表示されます。これはREPLには接続されていないので、ホストコンピュータとの自由な通信に使用することができます。バイナリデータを送受信することができ、ctrl-cをエスケープしなければならないことや、読んだデータにprint文やエラーが表示されることを心配する必要はありません。

# Turn on both REPL and data serial connections.
usb_cdc.enable(console=True, data=True)

カスタムHIDデバイスを定義する

最後に、専用のゲームコントローラやデジタイザ、カスタムマウスなど、新しいHIDデバイスを作成することもできます。HIDレポート記述子の作成方法を理解する必要がありますが、既存のものをコピーすることもできます。

# Create my own joystick.
joystick = usb_hid.Device(...)    # Details omitted.
 # Present a keyboard and joystick.
usb_hid.enable((usb_hid.Device.KEYBOARD, joystick))

このガイドの残りの部分を読むと、CircuitPython USBをコントロールする方法がわかります。

CIRCUITPY、MIDIおよびシリアル

ここでは、CIRCUITPY、MIDI、シリアルUSBデバイスを有効にしたり無効にしたりする方法の詳細を説明します。HIDデバイスはより複雑なので、別のページで説明します。

USBのセットアップと設定はboot.pyで行う

このページにあるすべてのコード例は、boot.pyに記述する必要があります。code.pyでこれらを使おうとすると、code.pyが実行されるまでにUSBデバイスが設定されてしまうため、エラーが発生します。

boot.pyを変更した後はハードリセットが必要

boot.pyはハードリセット後にのみ実行されます。そのため、boot.pyを変更した場合、再実行させるためにはボードをリセットする必要があります。boot.pyを編集したり、REPLでctrl-Dを入力するだけでは、再実行されません。混乱やファイルシステムの破損を避けるため、リセットする前に変更内容が完全に書き出されていることを確認してください。

CIRCUITPY マスストレージデバイス

CIRCUITPYドライブは通常、ホストコンピュータ上に表示されます。USBデバイスとして表示されないようにするには、次のようなコードを使用します。

import storage
storage.disable_usb_drive()

なお、USBデバイスを無効にしても、ドライブが動作しなくなるわけではありません。ドライブはプログラムで使用することができ、デフォルトでは読み取り専用になっています。プログラムで CIRCUITPY に書き込む必要がある場合は、storage.remount(“/”, readonly=False) を使用して、読み取り/書き込みとして再マウントする必要があります。詳しくはこちらのガイドページ(英語)をご覧ください。

storage.enable_usb_drive() 関数もありますが、通常は使用する必要はありません。ただし、お使いのビルドで CIRCUITPY がデフォルトで無効になっている場合や、boot.py で無効にした後に再度有効にしたい場合を除きます。

import storage
storage.disable_usb_drive()
storage.enable_usb_drive()   # Changed my mind :)

MIDI

ほとんどのボードでは、USB MIDIデバイスがデフォルトで有効になっています。MIDIを無効にするには、次のようにします。

import usb_midi
usb_midi.disable()

STM32F4やESP32-S2などの一部のマイクロコントローラでは、MIDIを常時有効にできるだけの十分なUSBエンドポイントが用意されていません。それらのボードでは、MIDIはデフォルトでは無効になっていますが、使用可能です。MIDIを有効にしたい場合は、他のUSBデバイスを無効にして、MIDIに対応するエンドポイントのペアを空ける必要があります。例えば、次のようになります

import usb_hid, usb_midi
# On some boards, we need to give up HID to accomodate MIDI.
usb_hid.disable()
usb_midi.enable()

USBシリアルコンソール(REPL)とデータ

CircuitPythonは通常、Python REPLを使用するCircuitPythonコンソールに接続するためのUSBシリアルデバイスを提供する。Windowsでは、このデバイスはCOM5のような番号付きのCOMポートとして表示されます。Linuxでは、/dev/ttyデバイス、多くは/dev/ttyACM0として表示されます。MacOSでは、/dev/cu.usbmodem14301のように、/dev/cuで始まる名前で表示されます。

シリアルデバイスはCDCデバイスと呼ばれ、「Communications Device Class」の略です。シリアルデバイスを制御するCircuitPythonのモジュールはusb_cdcといいます。

CircuitPythonはオプションで、コンソールに接続されていない第2のシリアルデバイスを用意することもできます。これをデータシリアルデバイスと呼びます。このデバイスでは任意のバイナリデータを送受信することができるので、REPLの干渉やctrl-C文字でプログラムが停止することを気にせずにデータやコマンドをやり取りしたい場合に非常に便利なデバイスです。2つ目のシリアルチャンネルは、2つ目のCOMポート、/dev/ttyまたは/dev/cuデバイスとして、異なる名前で表示されます。

2つ目のシリアルデバイスの使用方法の詳細については、usb_cdcのドキュメントを参照してください。

コンソールデバイスとデータデバイスはそれぞれ独立して有効・無効にすることができます。デフォルトでは、コンソールデバイスは有効ですが、データデバイスは無効です。

import usb_cdc
usb_cdc.disable()   # Disable both serial devices.
usb_cdc.enable(console=True, data=False)   # Enable just console
                                           # (the default setting)
  
usb_cdc_enable(console=True, data=True)    # Enable console and data
usb_cdc_enable(console=False, data=False)  # Disable both
                                           # Same as usb_cdc.disable()

[重要] 自分自身をロックアウトしないように!

boot.pyでCIRCUITPYとREPLの両方をオフにして、それらをオンにする方法を提供しない場合、あなたはボードからロックアウトされます。もうファイルを編集する方法がありません。セーフモードでボードを強制的に起動し、boot.pyの実行をスキップすることで回復することができます。例えば、ボタンを押すことでデバイスの無効化をスキップするコードを書くことでこの問題を回避できます。

次のコードはあなたをロックアウトしてしまいます。

import storage, usb_cdc
# DON'T DO THIS!
storage.disable_usb_drive()
usb_cdc.disable()

このコードはより安全です。ボタンが押されたときに両方のデバイスの電源を切ることを省略しています。

import storage, usb_cdc
import board, digitalio
# In this example, the button is wired to connect D2 to +V when pushed.
button = digitalio.DigitalInOut(board.D2)
button.pull = digitalio.Pull.DOWN
# Disable devices only if button is not pressed.
if not button.value:
	storage.disable_usb_drive()
	usb_cdc.disable()

HIDデバイス

HIDとは「Human Interface Device」の略です。キーボード、マウス、液晶タブレット、ジョイスティック、ゲームコントローラーなどがこれにあたります。

標準HIDデバイス

CircuitPythonはデフォルトで3つのHIDデバイスを提供します。これらは、usb_hid.Devicesで定義されています。

KEYBOARD – 標準的なキーボードで、5つの(仮想)LEDインジケータを備えています。
MOUSE – 5つのボタンとマウスホイールを備えた標準的なマウスです。
CONSUMER_CONTROL – USBコンシューマーコントロールデバイス:マルチメディアコントロール、ブラウザのショートカットキーなど。

コンシューマーコントロール

コンシューマ・コントロール・デバイスという言葉を聞いたことはないかもしれませんが、あなたの机の上にもあるはずです。キーボードには、音量調節、再生、一時停止などのキーのほか、ブラウザの操作や電卓を開くためのキーなどが付いていると思います。これらのキーは、実は通常のキーボードデバイスのキーではありません。これらのキーを押すと、キーボードデバイスではなく、コンシューマーコントロールデバイスを介してホストコンピュータに送信されます。

例えば、下のキーボードでは、マークされた機能がコンシューマーコントロールキーを介して送信されています。これらの物理キーは、通常のキーボードの押下とコンシューマーコントロールの押下の両方を送信します。例えば、Fnキーを押すかどうかによって、通常のキーボードのキーコードF4を送ることも、コンシューマ・コントロールのコードMUTEを送ることもできます。

コンシューマ・コントロール・コードは、USB規格書(85ページ)で定義されています。

HIDデバイスを選択する

CircuitPythonが提供するHIDデバイスは、boot.pyの以下のようなコードで選択できます。

import usb_hid
# These are the default devices, so you don't need to write
# this explicitly if the default is what you want.
usb_hid.enable(
    (usb_hid.Device.KEYBOARD,
     usb_hid.Device.MOUSE,
     USB_hid.Device.CONSUMER_CONTROL)
)
usb_hid.enable((usb_hid.Device.KEYBOARD,))   # Enable just KEYBOARD.
usb_hid.disable()       # Disable all HID devices.
usb_hid.enable(())      # Another way to disable all the devices.

なお、usb_hid.enable()は、デバイスが1つしかない場合や、デバイスが0個の場合でも、常にデバイスのタプルを受け取ります。

上級編

コンポジットHIDデバイス

複数のHIDデバイスは、1つの複合HIDデバイスにまとめて、すべてのデバイスを一度に含むことができます。それらは単一のエンドポイントペアを共有し(詳細は後述)、各デバイスは複合デバイス内の他のデバイスと区別するために、個別のレポートIDを使用します。上のコードでは、タプル内のすべてのデバイスが1つの複合デバイスになっています。デバイスが1つしかリストされていない場合、そのデバイスは個別のレポートIDを必要とせず、使用されません。

現在、CircuitPythonでは1つのHID複合デバイスしか使用できませんが、将来的には変更される可能性があります。

カスタムHIDデバイス

上記のデバイス以外にも、カスタムHIDデバイスを定義することができます。HIDレポート記述子とは、デバイスの種類と、そのデバイスが送受信するレポートの詳細を定義したバイトのバイナリ文字列である。例えば、マウスのレポートには、現在押されているボタン、マウスのX、Y方向の移動量、スクロールホイールの回転量などのデータが含まれます。キーボードのレポートでは、どの通常のキーが押されたか、どの修飾キー(Shift、Ctrlなど)が押されたかが報告されます。

独自のHIDレポート記述子を一から作成するには、多くの詳細な知識が必要であり、このガイドの範囲を超えています。しかし、エミュレートしたい既存のHIDデバイスのレポート記述子を入手して、そのまま使用するか、目的に応じて少し変更することはよくあります。HIDのレポート記述子については、こちらのようなチュートリアル(英語)がたくさんあります。また、既存のレポートディスクリプターを解読できるオンラインツールもあります。

また、新しいデバイスを扱うためにCircuitPythonのドライバを書く必要があります。adafruit_hidライブラリに例があります。

可能な例として、ゲームパッドコントローラのHIDデバイスを定義し、HIDデバイスの標準セットに追加する未完成のコードを以下に示します。

import usb_hid
gamepad = usb_hid.Device(
    report_descriptor=b'...',  # Descriptor omitted for brevity.
    usage_page=0x01,           # Generic Desktop Control
    usage=0x05,                # Gamepad
    in_report_length=6,        # This gamepad sends 6 bytes in its report.
    out_report_length=0,       # It does not receive any reports.
    report_id_index=7,         # The report id is at byte 7 (counting from 0)
                               # in the report descriptor.
)
usb_hid.enable(
    (usb_hid.Device.KEYBOARD,
     usb_hid.Device.MOUSE,
     usb_hid.Device.CONSUMER_CONTROL,
     gamepad)
)

ブートキーボード、マウス

CircuitPythonが提供するキーボードとマウスは、「ブート」デバイスとして定義されていません。これはUSB HIDデバイスの特別な機能で、コンピュータが起動しているときやBIOSに話しかける必要があるときに使用します。ブートキーボードやマウスのレポートディスクリプタは標準的なものです。デバイスがブートモードで使用される場合、あなたが提供する記述子は無視されます。

CircuitPythonはまだブートデバイスをサポートしていませんが、将来的には変わるかもしれません。

USBセットアップのタイミング

USBデバイスの設定はcode.pyではなく、boot.pyで行うことを先に述べました。

boot.pyはCircuitPythonがUSBでホストコンピュータと接続する前に実行されます。code.pyが実行されると、USBデバイスを変更するには遅すぎます。以下、順を追って説明します。

(1) ボードはハードリスタートします。これは、ボードを接続したり、リセットボタンを押したり、プログラムでmicrocontroller.reset()を実行したりしたときに起こります。

(2) boot.pyがあれば、それを実行します。ここでは、USBデバイスの設定を行います。

(3) boot.pyが実行されると、CircuitPythonはホストに全てのUSBデバイスを伝えるために必要なデータを作成します。このバイナリデータはいくつかのUSBディスクリプタにまとめられています。

(4) CircuitPythonはホストにUSBの準備ができたことを伝えます。

(5) ホストは、ディスクリプタを要求したり受け取ったりして、すべてのUSBデバイスを列挙し、デバイスへの接続を設定します。同時にcode.pyの実行が開始されます。code.pyがない場合、CircuitPythonはREPLを起動するだけです。

エラー表示

記の手順では様々なことがうまくいかない可能性があります。例えば、boot.pyにタイプミスのようなプログラミングエラーがあると、CIRCUITPYのboot_out.txtファイルにエラーが表示されます。

しかし、あなたのコードは、あまりにも多くのUSBデバイスを作成しようとする可能性もあります。このエラーはステップ3まで検出されません。その代わり、CircuitPythonはセーフモードに入り、code.pyを実行しません。REPLに入ると、CircuitPythonがセーフモードになった理由を「USB devices need more endpoints than are available(USBデバイスは利用可能な数よりも多くのエンドポイントを必要としています)」などと報告しているのがわかります。

いくつのUSBデバイスを設定できますか?

一度にアクティブにできるUSBデバイスの数には制限があります。ここでは、その制限について説明します。

CircuitPythonの制限

指定できるHIDデバイスの数は合計8個までです。また、コンポジットHIDデバイスは1つしか定義できませんが、将来的にはもっと定義できるようになるかもしれません。

ハードウェアの制限

マイクロコントローラーが提供するUSBエンドポイントの数は限られています。これはハードウェア上の制限です。エンドポイントとは、1つのローレベルUSB通信チャネルのことです。通常、エンドポイントはペアになっています。各エンドポイントのペアには、INエンドポイントとOUTエンドポイントがあります。INはデバイスからホストへのデータ送信、OUTはホストからデバイスへのデータ送信を意味しています。

エンドポイントペアは0から始まる番号で、エンドポイントペア0は常にUSBの設定と制御のために予約されているので、通常のデバイスには使用できません。

別々のUSBデバイスは、それぞれ1つ以上のエンドポイントペアを使用する必要があります。以下は、Adafruitが提供するデバイスのエンドポイント要件です。

  • CIRCUITPY(MSC):1つのIN/OUTエンドポイントペア。
  • MIDI:1つのIN/OUTペア。
  • CDC:コントロール用に1つのINエンドポイント、データ用に1つのIN/OUTペア、合計2ペア、各CDCデバイスに対して。CircuitPythonでコンソール用とデータ用のusb_cdcデバイスの両方を有効にした場合、合計で2+2=4ペアが必要になります。
  • HID:各コンポジットデバイスに1つのIN/OUTペアが必要です。複合デバイスのすべてのデバイスは、1つのエンドポイントペアを共有します。

したがって、両方のusb_cdcデバイスを含めて、これらのデバイスをすべて有効にすると、1+1+2+2+1=7つのエンドポイントペアが必要になります。

SAMD21、SAMD51、nRF52840、RP2040の各マイコンは、それぞれ8組のエンドポイントペアを提供しています。ペア0は予約済みなので、7組のエンドポイントペアが利用でき、上記のデバイスをすべて有効にすることができます。

しかし、他のマイクロコントローラでは、8ペアよりも少ないペアしか提供されていません。

  • STM32F4チップは通常、ペア0を除いて3ペアしか提供しません。つまり、CIRCUITPYと1つのCDCデバイスしか適合しません。STM32F4でHIDやMIDIを使用したい場合、CIRCUITPYやCDCをオフにする必要があります。
  • ESP32-S2にはペア0を除いて6ペアが用意されていますが、一度に5つのINエンドポイントしかアクティブにできません。そのため、通常は5ペアしか利用できません。
  • Spresenseは6ペアを提供しますが、ビルド時にエンドポイントを割り当てますので、MIDIや追加のCDCデバイスをオンにすることはできません。

デバイスが多すぎる場合

boot.pyの設定があまりにも多くのデバイスをオンにしている場合、CircuitPythonはboot.pyを実行した後にセーフモードになり、code.pyを実行しません。REPLに行くと、CircuitPythonがセーフモードになった理由を “USB devices need more endpoints than are available “のように報告しているのがわかります。

Follow me on Twitter