Android’s USB Host Mode

KiKUCHi
10 min readMay 15, 2019

Android端末をホストとしてUSBデバイスを使用する機会があり、知見を得ることができたので、備忘録もかね方法をまとめておきます。

概要

Androidでは、アクセサリーモードとホストモードで接続を行うことができます。

https://developer.android.com/guide/topics/connectivity/usb

普段使用している場合は、Android端末をホストとなるPCに接続しているため、アクセサリーモードになっています。

ですが、Android端末をホストとして使用することもでき、キーボードなどを接続することができます。

キーボードであれば、Androidが自動で物理キーボードを認識し、入力を行えるようになるのですが、QRコードリーダーなどの機器を接続する場合は、接続されている機器専用の処理を実装し、Android端末とUSBデバイスで通信を行う必要があります。

今回は、その通信方法について説明していきます。

注意点としては、Android3.1以上が必要であることと、すべてのAndroid端末がホストモードに対応しているわけではないということです。

あらかじめ、マニフェストファイルに、ホストモードが必要であることを明記しておくと良いでしょう。

使用するクラス

USB Hostモードとして使用するには、以下のクラスを使用する必要があります。

UsbManager

UsbManagerは、Android端末に接続されているUSBデバイスを管理しているクラスです。

このクラスからUSBデバイス一覧を取得し、通信を行いたいUSBデバイスのUsbDeviceを取得し、操作します。

UsbDevice

UsbDeviceは、Android端末に接続されている特定のUSBデバイスの管理を行うクラスです。

このクラスは、USBデバイスに関する情報を多数持っており、インターフェース、エンドポイントなど通信を行うために必要な情報もUsbDeviceから取得します。

UsbInterface

UsbInterfaceは、USBデバイスのインターフェースを扱うためのクラスです。

UsbDeviceは、一つ以上のUsbInterfaceを持っています。

そのため、適切なUsbInterfaceを使用し、通信を行う必要があります。

UsbEndpoint

UsbEndpointは、USBデバイスと通信を行うための位置をしめすクラスです。

UsbInterfaceは、一つ以上のUsbEndpointを持っています。

そのため、適切なUsbEndpointを使用し、通信を行う必要があります。

UsbDeviceConnection

UsbDeviceConnectionは、実際にUSBデバイスと通信を行うためのクラスです。

UsbInterfaceとUsbEndpontを使用し、通信を行います。

UsbRequest

UsbRequestは、非同期に通信を行うためのクラスです。

UsbConstants

UsbConstantsは、Linuxカーネル内に定義されている定数を参照するためクラスです。

このクラスの定数を使用し、接続したいUSBデバイスや、インターフェース、エンドポイントを判定します。

使用するクラスの数は多いのですが、実際に通信を行う場合の処理は、そこまで複雑ではありません。

実際に通信を行う方法を見ていきます。

USBデバイスとの通信

では、実際にUSB端末との通信を行う方法を見ていきます。

まずは、UsbManagerを取得します。

UsbManagerの取得

UsbManagerは、USB_SERVICEを指定することで、システムから取得することができます。

val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager

USBデバイス一覧の取得

次にUsbManagerから、接続されているUSBデバイスの一覧を取得します。

UsbManagerから取得するUSBデバイスリストは、HashMapになっており、Keyがデバイス名、ValueがUsbDeviceとなっています。

val usbDeviceList = usbManager.deviceList

HashMapのUSBデバイスリストからValueだけを取り出しListとすることも可能です。

val usbDeviceList = usbManager.deviceList.values.toList()

通信を行いたいUSBデバイスの特定

次にUSBデバイス一覧から通信を行いたいUSBデバイスを特定しUsbDeviceを取得します。

前述の通り、USBデバイス名がわかっている場合は、Keyとして指定することで、UsbDeviceを取得することが可能です。

usbManager.deviceList["<DeviceName>"]

Listの場合は、UsbDeviceを取り出し、インスタンスが持っている値から、通信を行いたいUSBデバイスかを判定します。

以下の例は、vendorIdとproductIdを使用して通信を行いたいUSBデバイスかを判定しています。

usbDeviceList = usbDeviceList.filter {
it.vendorId == 1000 && it.productId == 3 }

上記の例であれば、同じ機種のUSBデバイスを接続していない限り、1つのUsbDeviceに絞られるはずです。

その他にも、UsbDeviceは、deviceClassやdeviceIdなどの値を持っているので、特定するために適切な条件を指定し、USBデバイスを判定することが可能です。

インターフェースとエンドポイントの取得

次にUsbDeviceからインターフェースとエンドポイントを取得します。

まずは、インターフェースから取得します。

インターフェースを取得する際は、interfaceClassやinterfaceProtocolなどの値を見て適切なインターフェースを判定します。

UsbConstants内の定数を使用し比較を行うことで、適切なインターフェースかを判定することができます。

以下の例では、0番目のUsbInterfaceが、CDCプロトコル用のインターフェースかを判定しています。

val intf = usbDevice.getInterface(0)
if (intf.interfaceClass == UsbConstants.USB_CLASS_CDC_DATA)

適切なインターフェースを取得したら、次はエンドポイントを取得します。

エンドポイントもインターフェース同様に、UsbConstants内の定数を使用し比較を行うことで、適切なエンドポイントかを判定することができます。

以下の例では、0番目のUsbEndpointが、バルク通信用で、通信の向きがUSBデバイスからAndroid端末になっているエンドポイントかを判定しています。

val endpoint = intf.getEndpoint(0)
if (endpoint.type == UsbConstants.USB_ENDPOINT_XFER_BULK
&& endpoint.direction == UsbConstants.USB_DIR_IN)

これで、適切なインターフェースとエンドポイントを取得することができます。

USBデバイスとの接続と通信

ここまで取得してきた情報を使用することで、実際にUSBデバイスと接続を行うことができます。

ですが、接続を行いUSBデバイスと通信を行うためには、読み取りを行うための権限が必要になります。

この権限は、マニフェストファイルに定義する権限とは異なります。

マニフェストファイルに定義した権限は、一度許可をすれば、その後権限の許可の確認をする必要はなくなりますが、USBデバイスとの接続を行うための権限は、USBデバイスを接続するたびに取得し直す必要があります。

権限の確認は、UsbDeviceManager#requestPermissionを使用することで確認することができます。

権限が許可されている状態で、UsbDeviceManager#openDeviceを使用することで、通信を行いたいUSBデバイスとの接続を行うためのUsbDeviceConnectionを取得できます。

UsbDeviceConnectionが取得できたら、UsbDeviceConnection#claimInterfaceを使用し、インターフェースへの排他的なアクセスをUSBデバイスに要求します。

usbDeviceConnection.claimInterface(intf, true)

あとはエンドポイントに対して通信を行うだけです。

通信では、バッファを使用してデータの送受信を行います。

データの送信を行う場合は、バッファにデータを格納しUSBデバイスに渡します。

データの受信を行う場合は、空のバッファを渡し、通信が完了すると渡したバッファにデータが格納されます。

受信バッファは、あらかじめ受け取るサイズを指定する必要があるのですが、基本的には通信の最大パケットサイズを確保しておけば良いと思います。

エンドポイントの最大パケットサイズは、maxPocketSizeで取得することができます。

val byte = ByteArray(endpoint.maxPacketSize)

バルク通信を行う場合は、以下のような実装になります。

実際には、UIスレッド上で通信は行えないため、Coroutineなどを利用してUIスレッド以外から処理を実行してください。

bulkTransfer(endPoint, byte, bytes.size, 0)

このメソッドの返り値は、受け取ったデータのサイズが整数値で帰ってきます。

-1が帰ってきた場合は、通信でエラーが発生した場合です。

まとめ

Android端末でもUSBデバイスを接続して使用できるということがわかりました。

あまり利用する機会はなさそうですが、ホストモードで利用できるんだ〜程度で知っておいても良いのではないかと思います。

今回はバルク通信の場合のみを説明しましたが、UsbRequestを使用することで、非同期にいろいろな通信を行えると思うので、機会があれば試してみてください。

参考ページ

今回は、こちらのページを参考にさせていただきました。

--

--