AndroidからUSB HIDでマイコンを制御してみる

AndroidからUSB HIDでマイコンを制御するまとまった例が見当たらなかったので、試作してみました。

# 概要

Android端末のUSB端子にマイコンをつなぎ、マイコン上に搭載されたLEDの点灯をAndroidアプリから制御します。
マイコンには[ARM mbed](https://os.mbed.com/)を用い、Androidとマイコン間はUSB HIDで通信します。

![概要](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/37edfc67-ef5a-180a-a52b-91cb09c59037.png)

# ソースコード

https://github.com/yhirano/android_usb_hid_sample/

# mbedとは

mbedはARM社のプロトタイピング用マイコンボードとそれの開発環境です。
mbedに似たプロトタイピング環境としてはArduinoがありますが、Arduinoに比較すると

* ブラウザ上で動くオンライン開発環境の上で開発ができるためにコンパイラ等のツールのインストールが必要なかったり[^1]
* Arduinoにくらべてライブラリの抽象度が高めでプログラミングしやすいと

いった特徴があります。

今回はmbedがUSB HIDデバイスとして振る舞うことができるためこれを採用しています。
ほかにUSB HIDデバイスとして振る舞える開発ボードには、[Arduino Leonard](https://store.arduino.cc/usa/leonardo)があります。

[^1]: オフラインでも開発が可能です。VSCode + PlatformIOならば開発環境のインストールも手軽です。

# USB HIDとは

USB HID(Human Interface Device)はUSBでのデータの流し型の規格のひとつで、キーボードやマウスといった人間が操作するデバイスを使うためのものです。
WindowsやMac・Androidなどには標準でUSB HIDのドライバが標準で入っているためドライバのインストールが必要ないことや、mbedやArduino LeonardといったボードはUSB HIDデバイスと振る舞うための機能があらかじめ入っていることから、マイコン制御に使いやすいです。

マイコンの制御の方法としてUSB-シリアルを用いることが多いですが、USB HIDを使うメリットは下記の通りです。

* ホスト側にドライバのインストールが必要ない
* データが壊れているかどうかをUSBのレイヤーで判定してくれるので、受信側プログラム側でデータが壊れているかどうかを判定しなくてもよい
  * データが壊れている場合は、受信側プログラムにはデータが届かない


# ハードウェア準備

## 準備するもの

* mbedボード
* ブレッドボード
* USBブレークアウト基板
* ブレッドボード・ジャンパーワイヤ(オス－オス) 4本
* Android端末
* USB OTGケーブル

### mbedボード

mbedのボードとして[LPC11U24](https://os.mbed.com/users/okini3939/notebook/mbed_nxp_lpc11u24_jp/)を使います。
確認はしていませんが、[LPC1768](https://os.mbed.com/users/okini3939/notebook/mbed_NXP_LPC1768_jp/)でも同じように動くはずです。

<img width="25%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/0040955a-24b1-fa72-13f5-1ccd1a6eb2cd.jpeg">

画像は[https://www.switch-science.com/catalog/850/](https://www.switch-science.com/catalog/850/)より引用

### ブレッドボード

ブレッドボードは部品やジャンパワイヤを刺すだけで回路が組み立てられる基板です。
刺すだけで回路が組み立てられるので、試作に便利です。（半田付けが必要ないので楽）

適当な大きさのものを使用してください。
[幅が広めのブレッドボード](https://www.switch-science.com/catalog/3499/)がおすすめです。

<img width="25%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/5c4992e1-fdbc-7bc1-a1a0-cf3aaff3b164.jpeg">

画像は[https://www.switch-science.com/catalog/3499/](https://www.switch-science.com/catalog/3499/)より引用

### USBブレークアウト基板

USBブレークアウト基板はUSBポートをブレッドボードに接続するための基板です。

<img width="25%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/248fdd66-1767-07d2-5d1a-b2924b71184d.jpeg">

画像は[https://www.switch-science.com/catalog/1599/](https://www.switch-science.com/catalog/1599/)より引用

USBブレークアウト基板をブレッドボードにつなぐには、USBブレークアウト基板に[ピンヘッダ](https://www.switch-science.com/catalog/92/)を半田付けする必要があります。

### ブレッドボード・ジャンパーワイヤ(オス－オス) 4本

ブレッドボード上でmbedとUSBブレークアウト基板をつなぐための配線です。

<img width="25%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/50198900-f533-9c2b-bd42-c3b215f4a4cf.jpeg">

画像は[https://www.switch-science.com/catalog/620/](https://www.switch-science.com/catalog/620/)より引用

### USB OTGケーブル

私は普通のUSB-micro USBケーブルにダイソーのmicroUSB変換アダプタをつなげています。


## mbedとUSBブレークアウト基板をつなぐ

mbedからAndroidに接続するためできるように、mbedとUSBブレークアウト基板をつなぎます。
配線は下記の通りです。

* mbedのVINとUSBブレークアウト基板のVCC
* mbedのGNDとUSBブレークアウト基板のGND
* mbedのD+とUSBブレークアウト基板のD+
* mbedのD-とUSBブレークアウト基板のD-

<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/e1b0a33d-676d-0f18-b9ec-f1138063ff21.jpeg">
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/dce5d228-3627-ac8d-6bfd-71f2cab66205.png">

# Androidとマイコンのデータのプロトコル

今回のAndroidとマイコンのデータのプロトコルはこのように単純なものとします。

#### Androidからマイコンに送るデータ

4バイト長のデータで、
1バイト目が0だったらマイコンのLED1を消灯・0以外だったらマイコンのLED1を点灯
2バイト目が0だったらマイコンのLED2を消灯・0以外だったらマイコンのLED2を点灯
3バイト目が0だったらマイコンのLED3を消灯・0以外だったらマイコンのLED3を点灯
4バイト目が0だったらマイコンのLED4を消灯・0以外だったらマイコンのLED4を点灯

![Androidからマイコンに送るデータの例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67657/75a89a57-8d45-35da-db45-5e35b4825d37.png)

#### マイコンからAndroidへのレスポンス

データが4バイト以上だったら1バイトで0
データが4バイト未満だったら1バイトで1

# マイコン側(mbed)

前述のプロトコルをそのまま実装しています。

```c++:main.cpp
#include "mbed.h"
#include "USBHID.h"

// USB HIDデバイス
USBHID hid;

// HID読み込み
HID_REPORT hidReceive;
// HID書き込み
HID_REPORT hidSend;

// LED
DigitalOut led1(LED1);
DigitalOut led2(LED2);
DigitalOut led3(LED3);
DigitalOut led4(LED4);

int main(void) {
  // 起動時にはLEDは消灯する
  led1 = 0;
  led2 = 0;
  led3 = 0;
  led4 = 0;

  while (true) {
    // USBからデータを読み込む
    bool readResult = hid.read(&hidReceive);

    if (readResult) {
      // USBからのデータが4バイト以上の場合...
      if (hidReceive.length >= 4) {
        // USBからのデータを解析する
        //
        // 4バイトのデータで、LEDを点灯するかどうかのデータが各1バイトずつに格納されている
        // | LED1 | LED2 | LED3 | LED4 |
        led1 = hidReceive.data[0] == 0 ? 0 : 1;
        led2 = hidReceive.data[1] == 0 ? 0 : 1;
        led3 = hidReceive.data[2] == 0 ? 0 : 1;
        led4 = hidReceive.data[3] == 0 ? 0 : 1;

        // USBで0(成功)を返す
        hidSend.length = 1;
        hidSend.data[0] = 0;
        hid.sendNB(&hidSend);
      }
      // USBからのデータが4バイト以外の場合...
      else if (hidReceive.length > 0) {
        // USBで1(エラー)を返す
        hidSend.length = 1;
        hidSend.data[0] = 1;
        hid.sendNB(&hidSend);
      }
    }
  }
}
```

# Android側

## AndroidManifest.xml

AndroidManifest.xmlに `<uses-feature android:name="android.hardware.usb.host" />` を追加してください。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.github.yhirano.usb_hid_sample.android">

    <!-- USBホストをサポートしている端末である必要がある -->
    <uses-feature android:name="android.hardware.usb.host" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
```

## USB HID

USB HIDで通信するためのUsbHidクラスを作りました。
コピペで使えるはずです。

### UsbHid.kt

UsbHidクラスを通してUSB HIDデバイスを制御できます。
デバイスと接続するには `UsbHid#openDevice()` 、デバイスにデータを送信するには `UsbHid#write(data: ByteArray)` 、デバイスと切断するには `UsbHid#closeDevice()` を実行します。
USB HIDデバイスからのデータの読み込むのには `UsbHid#readListener` を設定してください。

```kotlin:UsbHid.kt
package com.github.yhirano.usbhid

import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbManager
import android.util.Log
import java.util.concurrent.Executors

class UsbHid constructor(
    context: Context,
    private val vendorId: Int,
    private val productId: Int
) {
    enum class State {
        Uninitialized,
        PermissionRequesting,
        FailedInitialize,
        Working
    }

    interface Listener {
        /**
         * Receive new data.
         */
        fun onNewData(data: ByteArray)

        /**
         * Occurred error.
         *
         * @param e Occurred exception.
         */
        fun onRunError(e: Exception)

        /**
         * Changed state.
         *
         * @param state state
         */
        fun onStateChanged(state: State)
    }

    var listener: Listener? = null

    /**
     * Default number of retries on write error.
     * If it is less than or equal to 0, no retries.
     */
    var defaultRetry: Int = 0
        set(value) {
            field = value
            writeManager?.defaultRetry = value
        }

    var state = State.Uninitialized
        private set(value) {
            val changed = field != value

            field = value

            if (changed) {
                listener?.onStateChanged(field)
            }
        }

    private val context = context.applicationContext

    private val usbManager: UsbManager by lazy {
        context.getSystemService(Context.USB_SERVICE) as UsbManager
    }

    private var device: UsbDevice? = null

    private var port: Port? = null

    private var readManager: ReadManager? = null
    private var writeManager: WriteManager? = null

    private var isRegisterUsbPermissionReceiver = false

    fun openDevice(): State {
        val usbAttachIntentFilter = IntentFilter().apply {
            addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
            addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
        }
        context.registerReceiver(usbAttachReceiver, usbAttachIntentFilter)

        state = connect()
        return state
    }

    fun closeDevice() {
        state = disconnect()
        try {
            context.unregisterReceiver(usbAttachReceiver)
        } catch (e: IllegalArgumentException) {
            Log.i(TAG, "Ignore IllegalArgumentException because usbAttachReceiver isn't attached.", e)
        }
        if (isRegisterUsbPermissionReceiver) {
            context.unregisterReceiver(usbPermissionReceiver)
            isRegisterUsbPermissionReceiver = false
        }
    }

    /**
     * @param retry Number of retries at data write error. If null, the number of times specified in [defaultRetry].
     */
    fun write(data: ByteArray, retry: Int? = null) {
        writeManager?.writeAsync(data, retry)
            ?: Log.w(TAG, "Failed to write data to USB HID because UsbHid class isn't open.")
    }

    private fun connect(): State {
        val deviceList = usbManager.deviceList
        val foundDevices = deviceList.values.filter {
            it.vendorId == vendorId && it.productId == productId
        }
        if (foundDevices.isEmpty()) {
            Log.d(TAG, "Not found devices.")
            return State.FailedInitialize
        }

        Log.d(TAG, "Found ${foundDevices.size} device(s).")
        foundDevices.forEachIndexed { i, device ->
            Log.d(TAG, "  ${i + 1}: DeviceName=\"${device.deviceName}\", ManufacturerName=\"${device.manufacturerName}\", ProductName=\"${device.productName}\", VendorId=${String.format("0x%04X", device.vendorId)}, ProductId=${String.format("0x%04X", device.productId)}")
        }

        val device = foundDevices[0]
        this.device = device
        Log.d(TAG, "Connect to \"${device.deviceName}\".")

        val connection = usbManager.openDevice(device)
        return if (connection == null) {
            val usbSerialPermissionIntent =
                PendingIntent.getBroadcast(context, 0, Intent(ACTION_USB_PERMISSION), 0)
            val usbPermissionIntentFilter = IntentFilter().apply {
                addAction(ACTION_USB_PERMISSION)
            }
            context.registerReceiver(usbPermissionReceiver, usbPermissionIntentFilter)
            isRegisterUsbPermissionReceiver = true
            usbManager.requestPermission(device, usbSerialPermissionIntent)
            State.PermissionRequesting
        } else {
            initPort(device, connection)
        }
    }

    private fun disconnect(): State {
        device = null
        port?.close()
        port = null
        stopIoManager()
        return State.Uninitialized
    }

    private fun initPort(device: UsbDevice, connection: UsbDeviceConnection): State {
        port = Port.create(device, connection)
        return if (port != null) {
            startIoManager()
            State.Working
        } else {
            Log.d(TAG, "Couldn't initialize port. Device has no read and write endpoint. port=\"${port}\"")
            State.FailedInitialize
        }
    }

    private fun startIoManager() {
        val port = port
        if (port == null) {
            Log.w(TAG, "Has no USB device. Maybe not initialize yet.")
            return
        }
        val readManager = ReadManager(
            port,
            object : ReadManager.Listener {
                override fun onNewData(data: ByteArray) {
                    listener?.onNewData(data)
                }

                override fun onRunError(e: Exception) {
                    listener?.onRunError(e)
                }
            })
        val writeManager = WriteManager(port, object : WriteManager.Listener {
            override fun onRunError(e: Exception) {
                listener?.onRunError(e)
            }
        }).apply {
            defaultRetry = this@UsbHid.defaultRetry
        }
        this.readManager = readManager
        this.writeManager = writeManager
        Executors.newSingleThreadExecutor().submit(readManager)
        Executors.newSingleThreadExecutor().submit(writeManager)
    }

    private fun stopIoManager() {
        readManager?.stop()
            ?: Log.w(TAG, "Has no data reading thread. Maybe not initialize yet.")
        readManager = null
        writeManager?.stop()
            ?: Log.w(TAG, "Has no data writing thread. Maybe not initialize yet.")
        writeManager = null
    }

    private val usbPermissionReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            if (intent?.action == ACTION_USB_PERMISSION) {
                synchronized(this) {
                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                        val device = device
                        if (device == null) {
                            Log.w(TAG, "Failed to connect device.")
                            state = State.FailedInitialize
                            return
                        }

                        val connection = usbManager.openDevice(device)
                        if (connection == null) {
                            Log.w(TAG, "Failed to connect device.")
                            state = State.FailedInitialize
                            return
                        }

                        state = initPort(device, connection)
                        return
                    } else {
                        Log.i(
                            TAG,
                            "Couldn't obtain connecting permission to \"${device?.deviceName}\"."
                        )
                        state = State.FailedInitialize
                    }
                }
            }
        }
    }

    private val usbAttachReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            when (intent?.action) {
                UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
                    Log.d(TAG, "USB Device attached.")
                    when (state) {
                        State.Uninitialized, State.FailedInitialize -> {
                            connect()
                        }
                        State.PermissionRequesting, State.Working -> {
                            // Ignore
                        }
                    }
                }
                UsbManager.ACTION_USB_DEVICE_DETACHED -> {
                    Log.d(TAG, "USB Device detached.")
                    when (state) {
                        State.Working -> {
                            state = disconnect()
                        }
                        State.Uninitialized, State.FailedInitialize, State.PermissionRequesting -> {
                            // Ignore
                        }
                    }
                }
            }
        }
    }

    companion object {
        private const val TAG = "UsbHid"

        private const val ACTION_USB_PERMISSION = "com.github.yhirano.usbhid.USB_PERMISSION"
    }
}
```

### Port.kt

USBのデバイスを管理するクラスです。
1デバイスにつき1つのPortインスタンスが作られます。

```kotlin:Port.kt
package com.github.yhirano.usbhid

import android.hardware.usb.*
import android.util.Log
import java.io.IOException

class Port private constructor(
    private val device: UsbDevice,
    private val connection: UsbDeviceConnection,
    private val usbInterface: UsbInterface,
    private val readEndpoint: UsbEndpoint,
    private val writeEndpoint: UsbEndpoint
) {
    override fun toString(): String {
        return "Port(device=$device)"
    }

    fun close() {
        connection.releaseInterface(usbInterface)
        connection.close()
    }


    fun read(dest: ByteArray, timeout: Int): Int {
        return connection.bulkTransfer(readEndpoint, dest, dest.size, timeout)
    }

    /**
     * @exception IOException Failed to write data to USB.
     */
    fun write(data: ByteArray, timeout: Int) {
        val length = connection.bulkTransfer(writeEndpoint, data, data.size, timeout)
        if (length <= 0) {
            throw IOException("Failed to write data to USB. status=$length")
        }
    }

    companion object {
        private const val TAG = "UsbHid/Port"

        fun create(device: UsbDevice, connection: UsbDeviceConnection): Port? {
            val interfaceCount = device.interfaceCount
            for (ii in 0 until interfaceCount) {
                val usbInterface = device.getInterface(ii)
                if (usbInterface.interfaceClass != UsbConstants.USB_CLASS_HID) {
                    continue
                }

                var readEndpoint: UsbEndpoint? = null
                var writeEndpoint: UsbEndpoint? = null

                val endpointCount = usbInterface.endpointCount
                for (ei in 0 until endpointCount) {
                    val endPoint = usbInterface.getEndpoint(ei)

                    if (endPoint.direction == UsbConstants.USB_DIR_IN &&
                        (endPoint.type == UsbConstants.USB_ENDPOINT_XFER_BULK || endPoint.type == UsbConstants.USB_ENDPOINT_XFER_INT)
                    ) {
                        readEndpoint = endPoint
                    } else if (endPoint.direction == UsbConstants.USB_DIR_OUT &&
                        (endPoint.type == UsbConstants.USB_ENDPOINT_XFER_BULK || endPoint.type == UsbConstants.USB_ENDPOINT_XFER_INT)
                    ) {
                        writeEndpoint = endPoint
                    }

                    if (readEndpoint != null && writeEndpoint != null) {
                        val connected = connection.claimInterface(usbInterface, true)
                        if (!connected) {
                            Log.w(TAG, "Failed to connect to USB device. Interface could not be claimed.")
                            connection.close()
                            return null
                        }

                        return Port(
                            device,
                            connection,
                            usbInterface,
                            readEndpoint,
                            writeEndpoint
                        )
                    }
                }
            }
            return null
        }
    }
}
```

データの読み込みには`UsbConnection#bulkTransfer`を使っています。
[UsbConnection#bulkTransferはバギーだ](https://stackoverflow.com/questions/9108548/android-usb-host-bulktransfer-is-losing-data)というレポートも目にしますが(実際にバギーです)、USB HIDの最大データ長である64バイトのデータを扱うのならば特に問題無さそうでした。

### ReadManager.kt

非同期でのUSB HIDデバイスからの読み込みを司るクラスです。
UsbHidクラス内で使っているクラスで、外部からは触れません。

```kotlin:ReadManager.kt
package com.github.yhirano.usbhid

import android.util.Log
import java.io.IOException
import java.nio.ByteBuffer

internal class ReadManager(
    private val port: Port,
    @Suppress("unused")
    var listener: Listener? = null
) : Runnable {
    interface Listener {
        fun onNewData(data: ByteArray)

        fun onRunError(e: Exception)
    }

    enum class State {
        STOPPED, RUNNING, STOPPING
    }

    @Suppress("unused")
    var timeout: Int = 30

    private var state = State.STOPPED

    private val readBuffer = ByteBuffer.allocate(4096)

    override fun run() {
        synchronized(state) {
            check(state == State.STOPPED) { "Already running" }
            state = State.RUNNING
        }

        try {
            while (true) {
                if (state != State.RUNNING) {
                    break
                }

                work()

                if (state != State.RUNNING) {
                    break
                }
                Thread.sleep(10)
            }
        } catch (e: Exception) {
            Log.w(TAG, "Occurred exception. exception=\"${e.message}\"", e)
            listener?.onRunError(e)
        } finally {
            synchronized(state) {
                state = State.STOPPED
            }
        }
    }

    fun stop() {
        synchronized(state) {
            if (state == State.RUNNING) {
                state = State.STOPPING
            }
        }
    }

    private fun work() {
        try {
            val length = port.read(readBuffer.array(), timeout)
            if (length > 0) {
                val data = ByteArray(length)
                readBuffer.get(data, 0, length)
                listener?.onNewData(data)
            }
        } catch (e: IOException) {
            Log.w(TAG, "Occurred exception when USB reading. exception=\"${e.message}\"", e)
            listener?.onRunError(e)
        } finally {
            readBuffer.clear()
        }
    }

    companion object {
        private const val TAG = "UsbHid/ReadManager"
    }
}
```

`Timeout`が30ミリ秒なのは、これくらいなら大丈夫そうな値を適当に設定しているだけです。
不具合がある場合は調整してみて下さい。

### WriteManager.kt

非同期でのUSB HIDデバイスへの書き込みを司るクラスです。
UsbHidクラス内で使っているクラスで、外部からは触れません。

```kotlin:WriteManager.kt
package com.github.yhirano.usbhid

import android.util.Log
import java.io.IOException
import java.util.*

internal class WriteManager(private val port: Port, var listener: Listener? = null) : Runnable {
    interface Listener {
        fun onRunError(e: Exception)
    }

    enum class State {
        STOPPED, RUNNING, STOPPING
    }

    private enum class WorkResult {
        QUEUE_IS_EMPTY,
        WROTE_DATA,
        CAUSE_WRITE_ERROR,
    }

    private class WriteData(val data: ByteArray, val retry: Int?)

    /**
     * USB data write timeout. in milliseconds, 0 is infinite.
    */
    @Suppress("unused")
    var timeout: Int = 30

    /**
     * If there is data to write next, write the data without the thread to sleep.
     * Not recommended for all devices.
     */
    @Suppress("unused")
    var writeDataImmediatelyIfExists = false

    /**
     * Default number of retries on write error.
     * If it is less than or equal to 0, no retries.
     */
    var defaultRetry: Int = 0

    /**
     * Sleeping time before retry bacause write data error.
     */
    @Suppress("unused")
    var sleepMillisSecBeforeRetry: Long = 10

    private var state = State.STOPPED

    private val writeDataQueue = LinkedList<WriteData>()

    override fun run() {
        synchronized(state) {
            check(state == State.STOPPED) { "Already running" }
            state = State.RUNNING
        }

        try {
            while (true) {
                if (state != State.RUNNING) {
                    break
                }

                val workResult = work()

                if (state != State.RUNNING) {
                    break
                }
                if (workResult == WorkResult.QUEUE_IS_EMPTY || !writeDataImmediatelyIfExists) {
                    Thread.sleep(10)
                }
            }
        } catch (e: Exception) {
            Log.w(TAG, "Occurred exception. exception=\"${e.message}\"", e)
            listener?.onRunError(e)
        } finally {
            synchronized(state) {
                state = State.STOPPED
            }
        }
    }

    fun stop() {
        synchronized(state) {
            if (state == State.RUNNING) {
                state = State.STOPPING
            }
        }
    }

    /**
     * @param retry Number of retries at data write error. If null, the number of times specified in [defaultRetry].
     */
    fun writeAsync(data: ByteArray, retry: Int? = null) {
        synchronized(writeDataQueue) {
            writeDataQueue.add(WriteData(data, retry))
        }
    }

    private fun work(): WorkResult {
        return try {
            val writeData = synchronized(writeDataQueue) {
                writeDataQueue.poll()
            }

            if (writeData != null) {
                val data = writeData.data
                val retry = writeData.retry ?: defaultRetry
                write(data, timeout, retry, sleepMillisSecBeforeRetry)
                WorkResult.WROTE_DATA
            } else {
                WorkResult.QUEUE_IS_EMPTY
            }
        } catch (e: IOException) {
            Log.w(TAG, "Occurred exception when USB writing. exception=\"${e.message}\"", e)
            listener?.onRunError(e)
            WorkResult.CAUSE_WRITE_ERROR
        }
    }

    /**
     * Write data to USB device.
     *
     * @param data Writing data
     * @param timeout USB data write timeout. in milliseconds, 0 is infinite.
     * @param retry Number of retries; if this number is less than or equal to 0, no retries.
     * @param sleepMillisSecBeforeRetry Sleeping time before retry.
     * @exception IOException When data is not written to USB after the specified number of retries.
     */
    private fun write(data: ByteArray, timeout: Int, retry: Int, sleepMillisSecBeforeRetry: Long) {
        try {
            port.write(data, timeout)
        } catch (e: IOException) {
            if (retry > 0) {
                Log.d(TAG, "Retry send data because occurred exception when USB writing. data=${data.contentToHexString()}, retry=$retry, exception=\"${e.message}\"")
                if (sleepMillisSecBeforeRetry > 0) {
                    Thread.sleep(sleepMillisSecBeforeRetry)
                }
                write(data, timeout, retry - 1, sleepMillisSecBeforeRetry)
            } else {
                Log.w(TAG, "Failed to retry send data because occurred exception when USB writing. data=${data.contentToHexString()} exception=\"${e.message}\"")
                throw e
            }
        }
    }

    companion object {
        private const val TAG = "UsbHid/WriteManager"

        private fun ByteArray?.contentToHexString(): String {
            if (this == null) return "null"
            val iMax = this.size - 1
            if (iMax == -1) return "[]"

            val b = StringBuilder()
            b.append('[')
            var i = 0
            while (true) {
                b.append(String.format("0x%02X", this[i]))
                if (i == iMax) return b.append(']').toString()
                b.append(", ")
                ++i
            }
        }
    }
}
```

`Timeout`が30ミリ秒なのは、これくらいなら大丈夫そうな値を適当に設定しているだけです。
不具合がある場合は調整してみて下さい。

## UI

```kotlin:MainActivity.kt
package com.github.yhirano.usb_hid_sample.android

import android.os.Bundle
import android.util.Log
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatToggleButton
import com.github.yhirano.usbhid.UsbHid

class MainActivity : AppCompatActivity() {
    private val led1Button by lazy {
        findViewById<AppCompatToggleButton>(R.id.led1_button)
    }

    private val led2Button by lazy {
        findViewById<AppCompatToggleButton>(R.id.led2_button)
    }

    private val led3Button by lazy {
        findViewById<AppCompatToggleButton>(R.id.led3_button)
    }

    private val led4Button by lazy {
        findViewById<AppCompatToggleButton>(R.id.led4_button)
    }

    private lateinit var usbHid : UsbHid

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        usbHid = UsbHid(applicationContext, 0x1234, 0x0006, object : UsbHid.ReadListener {
            override fun onRunError(e: Exception) {
                Log.w(TAG, "Occurred USB HID error.", e)
            }

            override fun onNewData(data: ByteArray) {
                Log.i(TAG, "Receive data from USB HID. data=${data.contentToString()}")
                runOnUiThread {
                    Toast
                        .makeText(this@MainActivity, data.contentToString(), Toast.LENGTH_SHORT)
                        .show()
                }
            }
        }).apply {
            openDevice()
        }

        led1Button.setOnCheckedChangeListener { _, _ -> sendLedsCommandToDevice() }
        led2Button.setOnCheckedChangeListener { _, _ -> sendLedsCommandToDevice() }
        led3Button.setOnCheckedChangeListener { _, _ -> sendLedsCommandToDevice() }
        led4Button.setOnCheckedChangeListener { _, _ -> sendLedsCommandToDevice() }
    }

    override fun onDestroy() {
        super.onDestroy()
        usbHid.closeDevice()
    }

    private fun sendLedsCommandToDevice() {
        val command = ByteArray(4)
        command[0] = if (led1Button.isChecked) 1 else 0
        command[1] = if (led2Button.isChecked) 1 else 0
        command[2] = if (led3Button.isChecked) 1 else 0
        command[3] = if (led4Button.isChecked) 1 else 0
        usbHid.write(command)
    }

    companion object {
        private const val TAG = "MainActivity"
    }
}
```

UsbHidクラスのコンストラクタに指定している`0x1234`はLPC11U24のVendorID, `0x0006`はProductIDです。

# できたもの

[![できたもの](http://img.youtube.com/vi/Gtr2Yuzl3PM/0.jpg)](http://www.youtube.com/watch?v=Gtr2Yuzl3PM)
クリックするとYouTubeに遷移します

# 参考文献

Android + USB HID

* [USB ホストの概要  |  Android デベロッパー  |  Android Developers](https://developer.android.com/guide/topics/connectivity/usb/host?hl=ja)
* [Example of code that uses HID for Android to connect to the device through USB.](https://gist.github.com/archeg/8333021)

mbed + USB HID

* [USBDevice - Handbook | Mbed](https://os.mbed.com/handbook/USBDevice)
* [USBHID - Handbook | Mbed](https://os.mbed.com/handbook/USBHID)

Android + mbed + USB HID

* [kshoji: \[Android Advent Calendar\] 自作のUSBデバイスを、Androidで動かす](http://blog.kshoji.jp/2012/12/android-usb-host.html)