Meta Quest 2でUSBシリアル通信

Meta Quest 2にRaspberryPi Picoを接続し、USBシリアル通信をしました。
Picoには照度センサを接続し、そのセンサ値を使ってUnity空間内の明るさを変えてみました。

同じ方法でArduinoや他のマイコンとも通信ができると思いますので、WifiやBluetoothが搭載されていないマイコンとの通信ができるようになります。
また、Picoへの電力供給もUSBで行いましたので、同じようにUSBで電力供給できるマイコンであればバッテリを他に用意する必要もありません。

これによってMeta Quest 2にセンサを追加するようなことができます。

# ハードウェア構成
![DSC_1546.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293692/bc40e906-3973-b76e-7258-84670dfa0d70.jpeg)

- Meta Quest 2
- RaspberryPi Pico
- USB OTGケーブル (USB Type-C - USB Micro, Type-C側がhost) [商品リンク](https://www.amazon.co.jp/gp/product/B09SB2T212/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1)
- Groveスターターキット（Raspberry Pi Pico用）
    - Grove Shield for Pi Pico
    - Grove - Light Sensor
    - Grove - Temperature & Humidity Sensor

自分は手元にあったGroveスターターキットとそれに付属しているセンサを使いましたが、Picoで使用できるセンサであれば何でもかまいません。  
Groveシールドを使わなければ、もっとコンパクトになると思います。

USBケーブルはOTGケーブルを使用して下さい。通常のUSBケーブルでは正常に動作しません。

# デモ動画
- 照度センサの値を使って、明るさを変更
- 照度センサ、温度湿度センサの値をテキスト表示

![OculusQuest2-Serial-Demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293692/ee66645d-4f78-1170-eb72-fa9e993c42a4.gif)


# 環境、ソフトバージョン
- Unity Version : 2021.3.8f
- Android Stadio
- https://github.com/mik3y/usb-serial-for-android (SHA-1 : 5bb8db02d5a6f2db32522ce602a6dd7d791de083) 

# 開発
## 概要
Unityにはシリアル通信ができるSystem.IO.Ports.SerialPortが含まれていますが、Meta Quest 2などのAndroidデバイスでは動作しません。それはこのクラスがデバイスのroot権限を必要とするからです。Meta Quest 2ではデバイスのroot権限を取得することはできないので、このクラスは使用できません。

そこでroot権限を必要としない[**usb-serial-for-android**](https://github.com/mik3y/usb-serial-for-android)を使用します。  
これはAndroid用のライブラリなので、これをビルドして.aarを生成し、この.aarをunityから呼び出して使用します。

このライブラリへは、USBデバイスへ接続するためのパーミッションを設定したり、unityから呼び出しやすいようにWrapperを追加しました。  
詳しくは以降で説明します。

なお、今回開発したUnityのアプリと、変更を加えたAndroidライブラリは以下のGithubリポジトリにpushしています。
- Unity Porject : TBD
- Androidライブラリ : https://github.com/hiro-han/usb-serial-for-android

## 1. Android用USBシリアル通信ライブラリのビルド
以下で説明している内容は[こちら](https://github.com/hiro-han/usb-serial-for-android/commit/e5630151a4c17697356da02052daf752a2f711b2)にコミットしてあります。

### 1-1. ソースコードの取得
[ここ](https://github.com/mik3y/usb-serial-for-android)からソースコードを取得し、Android Stadioで開きます。
``` bash
$ git clone https://github.com/mik3y/usb-serial-for-android.git
```

### 1-2. Wrapperの実装
このライブラリを使用してUSBデバイスと通信するためには多くの関数を呼び出す必要があります。それらの関数をUnityから呼び出すこともできるのですが、全部Unityから呼び出すと実装が面倒になります。そのためexampleとREAD.MEを参考にWrapperを実装しました。  
全コードを載せると長くなるので抜粋します。

``` UsbSerialWrapper.java

public class UsbSerialWrapper {

    // アプリ起動後、USBデバイスと接続するために呼び出す
    public static boolean OpenDevice(int baudrate)
    {
        baudrate_ = baudrate;
        usb_manager_ = (UsbManager) context_.getSystemService(Context.USB_SERVICE);
        List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(usb_manager_);
        usb_driver_ = availableDrivers.get(0);
        return Connect();
    }

    public static boolean Connect() {
        UsbDeviceConnection connection = usb_manager_.openDevice(usb_driver_.getDevice());

        port_ = usb_driver_.getPorts().get(0);
        port_.open(connection);
        port_.setParameters(baudrate_, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
        port_.setDTR(true);
        connected_ = true;

        return true;
    }

    // 受信したデータを取得する
    public static String Read()
    {
        length_ = 0;
        data_ = "";
        byte[] buffer = new byte[8192];
        length_ = port_.read(buffer, 2000);

        data_ = new String(buffer, StandardCharsets.UTF_8);
        return data_;
    }
```

### 1-3. USBデバイス（RaspberryPi Pico）への接続権限の設定
USBデバイスへ接続するためにはパーミッションが必要です。これは`device_filter.xml`を用意するのが簡単です。
(アプリの実行時にパーミッションを取得することもできますが、実装が複雑なのと自分の実装では意図通りに動作しませんでした。これは自分の実装ミスと思いますが、それを解決するよりもdevice_filter.xmlを書くほうが簡単です。）

`usbSerialForAndroid/src/main`以下に`res/xml`とディレクトリを作成し、xmlディレクトリ内にdevice_filter.xmlを作成します。
(ファイルパス：usbSerialForAndroid/src/main/res/xml/device_filter.xml)

```　device_filter.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-device vendor-id="11914" product-id="10" />
</resources>
```
ここで、vendor-idとproduct-idは、RaspberryPi Picoのものです。他のデバイスを接続する場合はそれぞれ変更する必要があります。  
これらの値はネットで検索してもいいですし、このライブラリ内で以下の関数を呼び出すことで確認できます。

```
usb_driver_.getDevice().getVendorId() // Pico : 11914
usb_driver_.getDevice().getProductId() // Pico : 10
```

### 1-4. Unityのclass.jarを追加
UnityからAndroidのライブラリを呼び出すためには、Unityの`class.jar`をAndroidプロジェクトにコピーする必要があります。

ファイルの場所：
```
/Applications/Unity/Hub/Editor/2021.3.8f/PlaybackEngines/AndroidPlayer/Variations/il2cpp/Release/Classes/classes.jar
```

`usbSerialForAndroid`下に`libs`ディレクトリを作成し、libsディレクトリ内にこの`class.jar`をコピーします。
(ファイルパス：usbSerialForAndroid/libs/classes.jar)

この実装は、[[Unity] Android NativePluginの実装の仕方](https://qiita.com/atsutama/items/28744b1a29e79184c3fc#unity%E6%A9%9F%E8%83%BD%E3%82%92android%E3%81%8B%E3%82%89%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E6%BA%96%E5%82%99)を参考にしました。


### 1-4. ビルド設定
Androidプロジェクトに含まれるbuild.gradleを修正します。

``` usbSerialForAndroid/build.gradle

...(略)...

android {
    compileSdkVersion 30 // MetaQuest2ではSDKのバージョンは30となっているので、それにあわせて32を30に変更します

    defaultConfig {
        minSdkVersion 17
        targetSdkVersion 30  // 同じく32 -> 30に変更します

...(略)...

dependencies {
    ...(略)...

    // 先に追加したclass.jarを出力される.aarファイルに含めないよう、以下の行を有効にします
    compileOnly fileTree(dir: 'libs', include: ['*.jar'])
}
```

### 1-5. ビルド設定
ライブラリをビルドします。成功すると.aarファイルが以下ディレクトリに出力されます。

```
usbSerialForAndroid/build/outputs/aar/usbSerialForAndroid-release.aar
```

## 2. Unityアプリの実装
ここではMetaQuest2アプリの開発方法の詳細は説明しません。多くの解説記事があるのでそちらを参照して下さい。

### 2-1. Androidライブラリの組み込み
上でビルドした`usbSerialForAndroid-release.aar`を`Assets/Plugins/Android`ディレクトリ下にコピーします。
（ファイルパス：Assets/Plugins/Android/usbSerialForAndroid-release.aar）

### 2-2. マニフェストファイルの作成
Androidアプリビルド用のマニフェストファイルを作成し、`Assets/Plugins/Android`ディレクトリ下に置きます。  
ここで重要なのはUSBデバイスを使用するための以下3つの設定です。これらがないとUSBデバイスに接続できません。

```
<uses-feature android:name="android.hardware.usb.host" />
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" />
```
3つめの設定で、Androidライブラリのビルド時に作成した`device_filter.xml`を指定しています。

マニフェストファイル全体:
``` Assets/Plugins/Android/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hoho.android.usbserial"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-sdk android:targetSdkVersion="29" />
    <uses-feature android:name="android.hardware.usb.host" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <application>
        <activity android:name="com.unity3d.player.UnityPlayerActivity"
                  android:theme="@style/UnityThemeSelector">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
            <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" />
        </activity>
    </application>
</manifest>

```

### 2-3. Androidライブラリの呼び出し機能の実装
上で実装したWrapperの関数を呼び出し、Ricoから送信されたデータを取得する機能を実装します。  
こちらもファイル全体をのせると長くなるので抜粋します。
全体は[こちら](https://github.com/hiro-han/OculusQuest2-Serial-Communication/blob/main/Assets/Scripts/ReceiveSerial/ReceiveSerialData.cs)を参照して下さい。

``` ReceiveSerialData.cs
public class ReceiveSerialData : MonoBehaviour
{
    void Start()
    {
        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");

        // Androidライブラリで必要となる、activity, context, intentを取得
        AndroidJavaObject activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
        AndroidJavaObject context = activity.Call<AndroidJavaObject>("getApplicationContext");
        AndroidJavaObject intent = activity.Call<AndroidJavaObject>("getIntent");
        androidJavaClass_ = new AndroidJavaClass("com.hoho.android.usbserial.wrapper.UsbSerialWrapper");

        androidJavaClass_.CallStatic("Initialize", context, activity, intent);
        bool ret = androidJavaClass_.CallStatic<bool>("OpenDevice", 115200);


        // データ受信用スレッドの生成
        // スレッドの生成は必須ではなく、Update()内で受信処理を呼び出してもいい
        _receiveThread = new Thread(_ReceiveData);
        _receiveThread.Start();
    }

　　// 固定周期で更新したかったため、FixedUpdate()を使用
   // Update()でもかまわない
    void FixedUpdate()
    {
        lock(lockObject_) {  // 複数スレッドで同じリソースにアクセスするので排他制御
            if (receivedData_ != null) {
                text.text = JsonUtility.ToJson(receivedData_, true);
                // 受信したデータを使ってカメラの明るさを変更
                // 2で割っているのは明るさの調整のため。2でなくてもよい
                light.intensity = receivedData_.brightness / 2.0f;
            }
        }
    }

    private void _ReceiveData()
    {
        AndroidJNI.AttachCurrentThread();  // 別スレッドを使う場合に必要
        string data = "";
        string msg = "";

        while (true) 
        {
            data = androidJavaClass_.CallStatic<string>("Read");　// 受信データ取得

            if (data.Length != 0) {
                ReceivedData receivedData ;
                receivedData = new ReceivedData();

                // 今回通信データはjsonフォーマットの文字列としたので変換
                receivedData = JsonUtility.FromJson<ReceivedData>(data);
            }
            Thread.Sleep(100);
        }
    }
```
### 2-4. 通信データ型について
今回はRaspberryPi Picoから送信するデータ型は、jsonフォーマットの文字列としました。Unity側ではデータをString型で受信するので、簡単に変換できるからです。
もしPicoからintやfloatの値を直接送信する場合は、stringをバイト配列に変換し、さらにintやfloatに変換する必要があります。
[こちら](https://github.com/hiro-han/OculusQuest2-Serial-Communication/blob/develop/Assets/Scripts/ReceiveSerial/PacketParser.cs)にサンプルを実装しているので参考にして下さい。

## 3. RaspberryPi Pico側の実装
Pico側は特別な実装はしていないのと、センサによって実装が変わるので詳しく説明しません。注意点としてはUSBシリアルでデータを送信するために、`CMakefiles.txt`で以下のように設定します。

``` CMakefiles.txt
pico_enable_stdio_usb(${APP} 1)
pico_enable_stdio_uart(${APP} 0)
```

こうすることで、アプリでは`printf()`を使うだけでデータを出力できます。

詳しい実装は、以下を参照下さい
- https://github.com/hiro-han/raspi-pico-grove-starter-kit/tree/main/tool/usb_serial_app
- https://github.com/hiro-han/raspi-pico-grove-starter-kit/blob/main/src/light_sensor.cpp

# 実行
以下の手順でアプリを実行します
1. Unityアプリのビルドをすると自動的にアプリが立ち上がるので、一度アプリを終了します。
1. PCとつないでいるUSBケーブルを外し、RaspberryPi PicoとUSBケーブルをMetaQuest2に接続します。
1. MetaQuest2の画面にRaspberryPi Pico (USBデバイス)へ接続許可を求める画面が表示されるはずなので、許可します。
1. アプリを実行します
    - センサを手で覆うなどして明るさを変え、それに応じてUnity空間の明るさが変われば成功です


# 参考
- 開発したアプリ
    - [Picoのソース](https://github.com/hiro-han/raspi-pico-grove-starter-kit)
    - [Androidライブラリ](https://github.com/hiro-han/usb-serial-for-android)
    - [MetaQuestのアプリ](https://github.com/hiro-han/OculusQuest2-Serial-Communication)
- [[Unity] Android NativePluginの実装の仕方](https://qiita.com/atsutama/items/28744b1a29e79184c3fc#unity%E6%A9%9F%E8%83%BD%E3%82%92android%E3%81%8B%E3%82%89%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E6%BA%96%E5%82%99)