Bluetooth SPP（無線シリアル通信）でExcel連携しリアルタイムに姿勢情報をグラフ描画する

## はじめに
リコーの @KA-2 です。

弊社では[RICOH THETA](https://theta360.com/ja/)という全周囲360度撮れるカメラを出しています。
<B>RICOH THETA V</B>や<b>RICOH THETA Z1</b>は、OSに<B>Android</B>を採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます（詳細は本記事の末尾を参照）。

「<b>[Bluetooth AudioデバイスからTHETAを操る](https://qiita.com/mShiiina/items/4b9f74625deeb43763e9)</b>」や「<b>[THETAプラグインで複数のSwitchBot温湿度計と連携する](https://qiita.com/KA-2/items/66222ca713c7ab82bd32)</b>」の記事でも紹介している通り、2020年6月17日のファームウェアアップデートにてBluetooth Classic機器連携が解禁されています。

今回は「Bluetooth SPP(Serial Port Profile)」というプロファイルを利用して、無線LANと同時利用も可能な無線シリアル通信を行う事例を紹介します。

[2021年1月26日に出版された<b>「THETAプラグインで電子工作」</b>](https://www.c-r.com/book/detail/1389) の[3つ目の事例](https://github.com/theta-skunkworks/theta-plugin-spp-roverc)でも利用しているのですが、いろいろな要素が盛りだくさんですし、そのまま同じ事例ではツマライですよね！
というわけで、同じ通信でも、ちょっと違う使い方を紹介します。

具体的には、<b>「THETAは、無線LAN側でスマートフォンにライブプリビューを送信しつつ、PC側にBlutooth SPPで姿勢データ送り、PCはExcelでそのデータをリアルタイムにグラフ描画する」</b>という事例です。 
2つの機器と、種類が異なる無線通信を、同時に行います。動作させていている動画はこちら。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">◤Qiita記事公開◢<br>無線LANと同時にBluetoothSPP通信。スマホに映像、PCに姿勢情報を送ります！さらにExcel DataStreamerでリアルタイムグラフ描画も！<a href="https://t.co/sTjRaI3JGd">https://t.co/sTjRaI3JGd</a><a href="https://twitter.com/hashtag/THETA?src=hash&amp;ref_src=twsrc%5Etfw">#THETA</a> <a href="https://twitter.com/hashtag/thetaplugin?src=hash&amp;ref_src=twsrc%5Etfw">#thetaplugin</a> <a href="https://twitter.com/hashtag/Bluetooth?src=hash&amp;ref_src=twsrc%5Etfw">#Bluetooth</a> <a href="https://twitter.com/hashtag/SPP?src=hash&amp;ref_src=twsrc%5Etfw">#SPP</a> <a href="https://twitter.com/hashtag/Excel?src=hash&amp;ref_src=twsrc%5Etfw">#Excel</a> <a href="https://twitter.com/hashtag/DataStreamer?src=hash&amp;ref_src=twsrc%5Etfw">#DataStreamer</a> <a href="https://t.co/IamJVkQmhk">pic.twitter.com/IamJVkQmhk</a></p>&mdash; THETAプラグイン開発者コミュニティ (@thetaplugin) <a href="https://twitter.com/thetaplugin/status/1372734984763543554?ref_src=twsrc%5Etfw">March 19, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

それでは、詳しく説明していきましょう。

## Bluetooth SPP とは

Bluetooth SPP （Serial Port Profile）は、「Bluetoothでシリアル通信」を行うためのプロファイルです。
このプロファイルを利用すると、同じプロファイルを持つ他の機器とBluetoothでシリアル通信が行えます。他の無線通信と比べ容易に通信が行える手軽さから、一般的にはBluetooth機器のデバッグや電子工作で利用されることが多い通信です。Bluetooth Classicに対応したスマートフォンやパソコンでも利用できます。

この通信は、有線で行っていたシリアル通信と同レベルの通信を無線で行えます。そして、無線LANとの同時利用が可能です。
通信速度は電波環境や距離によりますが、115200bps程度が最大値となるようです。通信速度はその場の状況に応じて変化するため、指定できません。
画像データや音楽データの通信には向きませんが、BLEよりは大きなデータを取り扱える程度の通信と思ってください。

Bluetooth Classicの通信は、接続を確立する段階でHostとDevice（MasterとSlaveで表現されることもあります）の役割があります。Device側は、Hostからの接続を待ち受けるだけですが、Hostは能動的にDeviceを探して接続を確立する必要があります。
ペアリングと呼ばれる振る舞いです。今回の事例では、RICOH THETAをHost、PCをDeviceとして利用します。

Bluetooth Classicにおけるペアリングが成立した後、SPPのプロトコルスタックで通信を行うこととなります。SPPの通信自体にはHostとDeviceのような役割はなく、通常のシリアル通信と同じ非同期双方向通信が行えます。
が、<b>今回の事例では、THETAから接続した機器にデータを送り付けるだけの簡単事例とします。</b>

## THETAプラグインにBluetooth ClassicのHostの振る舞いをさせる手順

THETAプラグインにBluetooth ClassicのHostの振る舞いをさせるには、次の手順で行います。

1. RICOH THETA APIを使い、Bluetoothリモコン機能がONであった場合にOFFする
2. RICOH THETA APIを使い、Bluetooth Classicを有効にする
3. RICOH THETA APIを使い、Bluetoothモジュールの電源がOFFであった場合、ONにする。<br>（Bluetoothモジュールの電源がOnであった場合、「1.」のコマンドの中でBluetoothモジュールの電源OFF→ONが自動で行われるので、ケア不要）
4. 一般Androidアプリのようにスキャン、接続をして、必要なプロトコルスタックを利用する<br>（今回はBluetooth SPPというプロトコルスタックを利用します）

Bluetoothに関するRICOH THETA APIは下記があります。すべてsetOption/getOptionのOptionsとして定義されています。

|実施する事項|対応するRICOH THETA API|
|---|---|
|リモートコントロールのON/OFF |Optionsの「_bluetoothRole」。<br>設定値とリモートコントロールON/OFFの対応関係は下表参照。|
|Bluetooth Classicの有効/無効|Optionsの「_bluetoothClassicEnable」|
|Bluetoothモジュールの電源ON/OFF|Optionsの「_bluetoothPower」|


|設定値|リモートコントロールの状態|
|---|---|
|Central|ON|
|Central_Peripheral|ON|
|Peripheral|OFF|


ドキュメントはそれぞれ下記のURLになります。

- bluetoothRole : https://github.com/ricohapi/theta-api-specs/blob/main/theta-web-api-v2.1/options/_bluetooth_role.md
- bluetoothClassicEnable : https://github.com/ricohapi/theta-api-specs/blob/main/theta-web-api-v2.1/options/_bluetooth_classic_enable.md
- bluetoothPower : https://github.com/ricohapi/theta-api-specs/blob/main/theta-web-api-v2.1/options/_bluetooth_power.md

THETAプラグインでSPP通信を行うための具体的な実装方法については、「ソースコード」の章を参照してください。


## ソースコード

[こちらのプロジェクト一式](https://github.com/theta-skunkworks/theta-plugin-bluetooth-spp)を参照してください。
詳細な説明を以下に記載します。

ご自身が作られたプロジェクトでもBluetooth SPPを使えるようにする（必要コードを移植する）という時や、しくみが知りたい時以外は、参照しなくてもよいかもです。
たとえば、「このプロジェクトをベースに、Bluetooth SPPでデータ出力だけをするプラグインをつくる時」は、さほど気にしなくてもよいのかも。。。

### ファイル構成

無線LANのライブプリビューと同時にBluetooth SPPが利用できることを示すために、[「THETAプラグインでライブプリビューを扱いやすくする」記事のソースコード](https://qiita.com/KA-2/items/cef05a4960663bd2ba2f)をベースに作成しました。
新規作成、または、変更を加えたファイルは以下のとおりです。

```
theta-plugin-bluetooth-spp\app
└src\main
　├assets　　　　　　　 // ベースとしたプロジェクトのままです。
　└java\com\theta360
　　　├pluginapplication
　　　│　├model　　 　　// ベースとしたプロジェクトのままです。
　　　│　├network　 　　// ベースとしたプロジェクトのままです。
　　　│　├oled　　　　　// ベースとしたプロジェクトのままです。
　　　│　├task　　　　　// EnableBluetoothClassicTask.java以外はベースとしたプロジェクトのままです。
　　　│　└view　　　　　// ベースとしたプロジェクトのままです。
　　　└ bluetoothspp　 // MainActivity.java, Attitude.java 以外はベースとしたプロジェクトのままです。
　　　　　└bluetooth　 // Bluetooth Classic SPPを利用するためのファイルが2つあります。
```

#### ※注意※　サービスの置き場所について

[書籍の事例](https://github.com/theta-skunkworks/theta-plugin-spp-roverc/)と比較して１点だけ注意があります。
今回の事例では、「bluetooth」配下ファイルの置き場所を「pluginapplication」から「bluetoothspp」に変更しています。
これは「2つのアプリケーションが同一のサービスを独自に定義している」という競合が起こるのを避けるためです。
サービスの名称を変えるだけでも良かったのですが、今回作成したサービスはアプリケーション固有の構成に含まれるべきです。
というわけで、このような構成としました。こちらの構成のほうがよいと思います。


### Bluetooth関連のパーミッション

AndroidでBluetooth Classicを利用するため、「AndroidManifest.xml」に次の定義を追加しています。

```xml:AndroidManifest.xml
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
〜省略〜
<application
〜省略〜
<service android:name="com.theta360.pluginapplication.bluetooth.BluetoothClientService"
/>
</application>
```

末尾付近、「service」タグは、あとに説明する自身で作成したサービスを宣言しています。
上の3行はパーミッションに関する宣言です。「ACCESS_COARSE_LOCATION」の宣言を行ったTHETAプラグインをパソコンからRICOH THETAにインストールして動かす場合には、Vysorを使って「Location」（=位置情報）に関する権限を手動で与える必要があります。ストアからインストールした場合には不要な手順です。


### THETAプラグイン固有事項の処理

「THETAプラグインにBluetooth ClassicのHostの振る舞いをさせる手順」の章では、THETAプラグインでBluetooth ClassicのHostとして動作させてペアリング動作を行い、SPP通信を開始するまでの大枠の流れを説明しました。この大きな流れを行っているのが「EnableBluetoothClassicTask.java」です。
このタスクは、「MainActivity」の「onCreate」の先頭で起動され、doInBackgroundでRICOH THETA API実行を行い、「onPostExecute」でコールバック関数を呼び出して終了します。SPPを利用する処理のキックはコールバック関数の中で行っています。

```java:EnableBluetoothClassicTask.java
    @Override
    synchronized protected String doInBackground(Void... params) {

        HttpConnector camera = new HttpConnector("127.0.0.1:8080");
        String errorMessage ;
        boolean bluetoothRebootWait = false;

        //_bluetoothRoleの確認 -> Peripheral以外なら Peripheralにする。
        String bluetoothRole = camera.getOption("_bluetoothRole");
        if (!bluetoothRole.equals("Peripheral")) {
            Log.d(TAG,"_bluetoothRole:" + bluetoothRole + " -> Peripheral");
            errorMessage = camera.setOption("_bluetoothRole", "Peripheral");
            if (errorMessage != null) { // パラメータの設定に失敗した場合はエラーメッセージを表示
                return "NG";
            }
            bluetoothRebootWait=true;
        } else {
            Log.d(TAG,"_bluetoothRole:" + bluetoothRole);
        }

        Log.d(TAG,"set _bluetoothClassicEnable=true");
        errorMessage = camera.setOption("_bluetoothClassicEnable", Boolean.toString(Boolean.TRUE));
        if (errorMessage != null) { // パラメータの設定に失敗した場合はエラーメッセージを表示
            return "NG";
        }
        String bluetoothPower = camera.getOption("_bluetoothPower");
        if (bluetoothPower.equals("OFF")) {
            //The processing that was missing in the following sample program was added.
            //https://github.com/theta-skunkworks/theta-plugin-spp-roverc/
            bluetoothRebootWait = true;

            Log.d(TAG,"set _bluetoothPower=ON");
            errorMessage = camera.setOption("_bluetoothPower", "ON");
            if (errorMessage != null) { // パラメータの設定に失敗した場合はエラーメッセージを表示
                return "NG";
            }
        } else {
            Log.d(TAG,"_bluetoothPower:" + bluetoothPower);
        }

        if (bluetoothRebootWait) {
            // _bluetoothRoleをPeripheral以外からPeripheralに切り替えたとき
            // _bluetoothClassicEnable を trueにする処理の中で行われている
            // Bluetoothモジュールのリブート完了が遅延するのでWaitする
            try {
                Log.d(TAG,"bluetoothPower Off->On Wait 3sec.");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return "OK";
    }
```

「_bluetoothRole」と「_bluetoothPower」に関して、この事例では、起動直後の状態を保持し、終了時に戻す処理をしていません。必要な方は、ご自身で設定の保存と復帰のコードを追加してみてください。
「doInBackground」末尾では、コード中のコメントに書いてある特定条件で起こるRICOH THETAの独特な振る舞いをリカバリーするために、3秒の待ちを行っています。対症療法的な対策です。

なお、ソースコード中にコメントと共に記述してある以下行は、<b>書籍では抜けていたケースです。本事例では修正してあります。</b>

```java:EnableBluetoothClassicTask.java
            //The processing that was missing in the following sample program was added.
            //https://github.com/theta-skunkworks/theta-plugin-spp-roverc/
            bluetoothRebootWait = true;
```


コールバック関数の内容は「MainActivity」に記述しています。

```java:MainActivity.java
    private boolean mServiceEnable = false;
    private EnableBluetoothClassicTask.Callback mEnableBluetoothClassicTask = new EnableBluetoothClassicTask.Callback() {
        @Override
        public void onEnableBluetoothClassic(String result) {
            if( result.equals("OK") ) {
                mServiceEnable = true;

                getApplicationContext()
                        .startService(
                                new Intent(getApplicationContext(), BluetoothClientService.class));

                getApplicationContext()
                        .bindService(
                                new Intent(getApplicationContext(), BluetoothClientService.class),
                                MainActivity.this,
                                Context.BIND_AUTO_CREATE);
            }
        }
    };
```

「BluetoothClientService.java」に記述してあるサービスを生成し動作を開始しています。
サービスの終了は、「MainActivity」の「onDestroy」で行っています。


### スキャン、ペアリング、接続の確立（サービスの定義）

「BluetoothClientService.java」には、Androidが提供しているBluetooth関連のAPIを利用し、Bluetooth Classic SPP通信を行うための一連の動作が記述されています。Androidが提供しているBluetooth関連のAPIを利用すると、ペアリングの途中過程や、接続中に状態が変化したときなどにIntentが送られてきます。このIntentを拾い、対応する処理に振り分けをしているのが「BluetoothDeviceReceiver.java」です。Intentに対応するコールバックを定義しているだけですので説明を割愛します。

「BluetoothClientService.java」を詳しく見てみましょう。正常ケースの大きな流れは次のようになります。その他の細かな事項については説明を割愛します。

1. スキャンを開始する。
2. 発見したデバイスをペアリングする。
3. ペアリングしたデバイスを接続する。

#### スキャン開始

スキャンの開始は次のように行っています。

```BluetoothClientService.java
    private void startClassicScan() {
        if (mBluetoothAdapter.isDiscovering()) {
            mBluetoothAdapter.cancelDiscovery();
        }
        boolean isStartDiscovery = mBluetoothAdapter.startDiscovery();
        Log.d(TAG, "startClassicScan :" + isStartDiscovery);
    }
```

スキャン動作は、Androidの仕様で開始から12秒後に停止します。スキャンを開始したときに検索中であった場合は、一度停止してから開始します。今回はTHETAプラグインを起動したときのみスキャン動作をしていますが、1回のスキャン動作で対象のデバイスが見つからなかった場合に、スキャンを再開したり、接続断をトリガーにスキャン動作を再開させたりする処理を行いたい場合、ご自身で追加してください。

#### ペアリング

接続対象のデバイスを発見した場合、ペアリングを行います。接続対象のデバイスは、CoDで判断します。CoDは、Bluetooth SIGで定められたデバイスの用途を識別する値です。

Bluetooth SIGのTOPページは次のURLです。
URL：https://www.bluetooth.com/ja-jp/

CoDについては次のURLに記載されています。
URL：https://www.bluetooth.com/specifications/assigned-numbers/baseband/



探索により、デバイスが見つかったIntentを受け取ると「onFound」メソッドが呼ばれます。

```BluetoothClientService.java
        @Override
        public void onFound(BluetoothDevice bluetoothDevice,
                BluetoothClass bluetoothClass,
                int rssi) {
            String name = bluetoothDevice.getName();
            Log.d(TAG, "name" + name);
            if (name != null) {
                int type = bluetoothDevice.getType();
                if (type == bluetoothDevice.DEVICE_TYPE_CLASSIC) {
                    int classNo = bluetoothClass.getDeviceClass();
                    Log.d(TAG, "class" + classNo);
                    if (classNo == Device.COMPUTER_HANDHELD_PC_PDA ||
                        classNo == Device.COMPUTER_DESKTOP ||
                        classNo == Device.COMPUTER_LAPTOP ) {
                        stopClassicScan();
                        bluetoothDevice.createBond();
                    }
                }
            }
        }
```

「onFound」では、Bluetooth Classicの通信が行える以下機器をペアリング対象にしています。

|定義名称|接続対象機器|
|---|---|
|COMPUTER_HANDHELD_PC_PDA|ハンドヘルドPCやPDA (M5Stackなどもこのジャンルに含まれます)|
|COMPUTER_DESKTOP |デスクトップPC|
|COMPUTER_LAPTOP|ラップトップPC|

<b>書籍の事例と異なり、今回の事例ではラップトップPCやデスクトップPCも加えてあります！</b>
利用する状況に応じて、条件を減らしたり、変えたりしてください。

Androidにおける定義名称と数値の対応関係は以下のドキュメントに記されています。
https://developer.android.com/reference/android/bluetooth/BluetoothClass.Device

前述のコードの「Log.d(TAG, "class" + classNo);」で出力されるログから数値を調べ、定義名称を逆引きすることで、別の機器にも対応できるようになります。


今回は、最初に見つかったデバイスを無条件に接続対象にしています。探索中に複数のデバイスがペアリング待ちになっていないように注意して利用してください。接続する機器をMACアドレスや名称で絞ることも可能ですが、今回は実装していません。必要な方はご自身でコードを追加してください。

#### 接続

ペアリング完了後に接続します。「bondState」が「BOND_BONDED」になるとペアリング完了です。「connect」メソッドで接続処理を開始します。

```BluetoothClientService.java
        public void onBondStateChanged(BluetoothDevice bluetoothDevice, int bondState) {
            Log.d(TAG, "onBondStateChanged");
            if (bondState == BluetoothDevice.BOND_BONDED) {
                connect(bluetoothDevice);
            }
        }
```

connectでは、UUIDを指定してsocketを開いています。

```BluetoothClientService.java
    private void connect(BluetoothDevice device) {
        int type = device.getType();
        if (type == BluetoothDevice.DEVICE_TYPE_CLASSIC) {
            int classNo = device.getBluetoothClass().getDeviceClass();
            Log.d(TAG, "classNo:" + classNo);
            //if (classNo == Device.COMPUTER_HANDHELD_PC_PDA) {
            if (    (classNo == Device.COMPUTER_LAPTOP)     ||
                    (classNo == Device.COMPUTER_DESKTOP)    ) {
                try {
                    mBluetoothSocket = device.createRfcommSocketToServiceRecord(BT_UUID);
                    mBluetoothSocket.connect();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
```

UUIDはこのクラス先頭付近で以下のように定義してあり、SPPを指定しています。

```BluetoothClientService.java
    //SerialPort Service UUID
    private static final UUID BT_UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb");
```

接続が完了すると、「onAclConnected」メソッドが呼ばれるので、以下の処理を行い、接続完了を通知しています。

|接続完了通知方法|左記の方法で通知が確認できる機種|
|---|---|
|無線LANのLEDを白にする|RICOH THETA V|
|録画開始と同じ音を鳴らす|RICOH THETA V、RICOH THETA Z1|


```BluetoothClientService.java
        @Override
        public void onAclConnected(BluetoothDevice bluetoothDevice) {
            Log.d(TAG, "onAclConnected");
            //LED3点灯
            Intent intentLedShow = new Intent("com.theta360.plugin.ACTION_LED_SHOW");
            intentLedShow.putExtra("color", LedColor.WHITE.toString());
            intentLedShow.putExtra("target", LedTarget.LED3.toString());
            mContext.sendBroadcast(intentLedShow);

            Intent intentSound = new Intent("com.theta360.plugin.ACTION_AUDIO_MOVSTART");
            mContext.sendBroadcast(intentSound);
        }
```

その他、接続の過程や、接続失敗、接続後の状態が変わったときに呼ばれる各種メソッドをいくつか記述していますが、コードを見れば内容を理解できると思います。


### プロセス間通信　Messenger の利用

SPPのデータ送受信を担っているのは「BluetoothClientService」なのですが、送信したいデータが生まれるのは「MainActivity」です。このため、「MainActivity」と前述のサービスの間で、Androidの「Messenger」と呼ばれるプロセス間通信を利用してデータの受け渡しを行っています。
Messengerを使うと、双方向の非同期プロセス間通信が行えるのですが、<b>※今回は送信のみ</b>としてコードを簡略化しています。

#### MainActivity : Messenger送信側のコード

「MainActivity」側はサービスと「Messenger」で通信するために、次のimplementsを行っています。

```MainActivity.java
public class MainActivity extends PluginActivity implements ServiceConnection {
```

「Messenger」を利用するために記述するコードは次の通りです。

```MainActivity.java
    private Messenger _messenger;

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Log.d(TAG, "サービスに接続しました");
        _messenger = new Messenger(service);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        Log.d(TAG, "サービスから切断しました");
        _messenger = null;
    }
```

あとは、次のような記述を行うとデータをサービスに送ることができます。

```MainActivity.java
                        if (_messenger!=null) {
                            try {
                                Log.d(TAG, "[csvAttitude]:" + csvAttitude  );
                                _messenger.send(Message.obtain(null, 0, csvAttitude));
                            } catch (RemoteException e) {
                                e.printStackTrace();
                            }
                        }
```

#### BluetoothClientService: Messenger受信側のコード

「BluetoothClientService」側は、「MainActivity」からのデータを受け取るために次のようなコードを記述しています。

```BluetoothClientService.java
    private Messenger _messenger;
    static class SppHandler extends Handler {

        private Context _cont;

        public SppHandler(Context cont) {
            _cont = cont;
        }

        @Override
        public void handleMessage(Message msg) {

            switch(msg.what) {
                case 0:
                    String sendCmd = (String)msg.obj;
                    //Log.d(TAG, "Received message :" + sendCmd);
                    sendSppCommand(sendCmd);
                    break;
                default:
                    Log.d(TAG, "Undefined message :" + msg);
            }
        }
    }
```

サービスから「MainActivity」へデータを送りたい場合には、「MainActivity」側にも同じようなハンドラを準備しておけばよいです。

### サービスによるデータの送受信

サービスは、メッセージで受信したデータを次のメソッド内でPCに送出しています。
本ソースコードは、書籍での事例をベースとしており、データ受信部分はコメントアウトして残してあります。データ受信をするときに参考にしてみてください。

```BluetoothClientService.java
    public static  void sendSppCommand(String inSendCommand) {

        if (mBluetoothSocket != null) {
            boolean isConnected = mBluetoothSocket.isConnected();
            if (isConnected) {
                try {
                    OutputStream out = mBluetoothSocket.getOutputStream();
                    InputStream in = mBluetoothSocket.getInputStream();

                    //Write command
                    Log.d(TAG, "SPP send data :" + inSendCommand);
                    out.write(inSendCommand.getBytes());


                    //Read the response (In this example, reception processing is not required.)
                    /*
                    byte[] incomingBuff = new byte[64];
                    int incomingBytes = in.read(incomingBuff);
                    byte[] buff = new byte[incomingBytes];
                    System.arraycopy(incomingBuff, 0, buff, 0, incomingBytes);
                    String s = new String(buff, StandardCharsets.UTF_8);
                    Log.d(TAG, "SPP receive data :" + s);
                    */

                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                Log.d(TAG, "Not connected to a Bluetooth SPP device.");
            }
        } else {
            Log.d(TAG, "Not ready for Bluetooth SPP communication.");
        }
    }

```

送信は「OutputStream」、受信は「InputStream」の仕組みを利用しています。
送信する前に「mBluetoothSocketが有効であること」「接続中であること」を確認し、送信を「OutputStream」の「write」メソッド、受信を「InputStream」の「read」メソッドで行っているだけです。

非同期の受信に対応したい場合には、サービスの中で受信処理をスレッドに分離し、周期動作をさせるなどの方法があります。必要な場合には、ご自身でカスタマイズしてみてください。


### サービスの終了

今回、Bluetooth Classic SPP通信を行うためにサービスを利用しました。サービスは、正しく終了させないとアプリケーション終了後も処理が残ってしまう可能性があります。確実に終了させましょう。

[Googleのドキュメント](https://developer.android.com/guide/components/services?hl=ja#Lifecycle)には次の記述があります

```
サービスの全体の生存期間は、onCreate()が呼び出されてから、onDestroy()から戻るまでの間です。
アクティビティと同様に、サービスはonCreate()で初期セットアップを行い、onDestroy()で残りのすべてのリソースを解放します。
たとえば、音楽再生サービスでは、音楽を再生するスレッドをonCreate()で作成でき、onDestroy()でスレッドを停止できます。
```

本プラグインでも上記に従って「MainActivity」に「onDestroy」メソッドの「Override」を追記し、サービスの終了とメディアレシーバーの登録解除をしています。

```MainActivity.java
    @Override
    protected void onDestroy() {
        if (mServiceEnable) {
            getApplicationContext().unbindService(MainActivity.this);
            getApplicationContext().stopService(new Intent(getApplicationContext(), BluetoothClientService.class));
        }

        〜省略〜
    }
```

### THETAから送出しているデータの形式

シリアル通信やプロセス間通信（Messenger）は「データの通り道を作っている」だけです。データの通り道には、受信機器のプログラムに合わせて、バイナリでもASCIIでもお好みの形式のデータを通せます。

今回の事例では、「受信機器(PC)がExcelでデータを扱う」という事情から、姿勢情報の数値をASCII文字列のCSV形式にして送信しています。

```MainActivity.java
                        // Get Attitude
                        double corrAzimath = attitude.getDegAzimath();
                        double corrPitch = attitude.getDegPitch();
                        double corrRoll = attitude.getDegRoll();

                        //Send Attitude
                        String csvAttitude = String.valueOf(corrAzimath) + ","
                                            + String.valueOf(corrPitch) + ","
                                            + String.valueOf(corrRoll)  + "\r\n" ;
```

姿勢情報については以下記事の<b>「[姿勢を求めるクラス(Attitude.java)の追加と利用方法](https://qiita.com/KA-2/items/6f8395e4ea0dc378cc7a#%E5%A7%BF%E5%8B%A2%E3%82%92%E6%B1%82%E3%82%81%E3%82%8B%E3%82%AF%E3%83%A9%E3%82%B9attitudejava%E3%81%AE%E8%BF%BD%E5%8A%A0%E3%81%A8%E5%88%A9%E7%94%A8%E6%96%B9%E6%B3%95)」</b>の説明を参照してください。

姿勢情報は、Azimath(Yaw), Pitch, Rollの3つで1セットのデータなので、1行を3つのカンマ区切りデータとしています。
参考として、後述するExcelのアドイン「Data Streamer」では1行が100個のカンマ区切りデータまで扱えるようです。


## 今回作成したTHETAプラグインの使い方

### Windows 10 の設定

Windows 10 での方法を記します。機器によって不要であったり手順が異なったりするとは思いますが、

- RICOH THETAから機器を探せるようにするため、機器名称をステルスにしない
- Bluetooth SPPのCOMポートをあらかじめわりあてておく（接続後に決まる場合、調べ方を知っておく）

ということが満たせれば問題ありません。

1. 「Bluetoothの設定」を開<br>![01_BT設定を開く.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/b1e39ea5-51b0-3748-271e-7e531eb26c8b.png)
2. 「その他Bluetoothオプション」を開く<br>![02_その他BTオプションを開く.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/8ed180fe-c0cc-a182-74d0-01a1311d3926.png)
3. RICOH THETAから機器を探せるようにする<br>![03_検出許可.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/433e5194-a2f6-2c47-1529-a35777b838cf.png)
4. Bluetooth SPPのCOMポートをあらかじめわりあてておく（赤線部分、最初は空欄です）<br>![04_COMポート割り付け.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/4f6d861e-db49-cceb-4d37-02c3d2c2c868.png)


この設定をしておくと、COMポートを扱える各種ソフトウェアにてRICOH THETAからのデータを取り込めます。
有名どころではTeraTermなどです。


### Excelの Data Streamer アドインを有効にする

<b>[Microsoftのドキュメント](https://support.microsoft.com/ja-jp/office/data-streamer-%E3%82%A2%E3%83%89%E3%82%A4%E3%83%B3%E3%82%92%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B-70052b28-3b00-41e7-8ab6-8a9f142dffeb)</b> を参照してください。

### Excel へのデータの取り込みとグラフ描画

今回は、COMポートを事前に割り当ててあるので、THETAからPCへの接続を行わなくても作業できます。
<b>[Microsoftのドキュメント](https://support.microsoft.com/ja-jp/office/data-streamer-%E3%82%A2%E3%83%89%E3%82%A4%E3%83%B3%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E3%83%AA%E3%82%A2%E3%83%AB%E3%82%BF%E3%82%A4%E3%83%A0%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E3%82%B9%E3%83%88%E3%83%AA%E3%83%BC%E3%83%9F%E3%83%B3%E3%82%B0%E3%82%92%E9%96%8B%E5%A7%8B%E3%81%99%E3%82%8B-b6fac0bb-a495-423b-99eb-60c1f1e338d4)</b> を参考にしてください。

大雑把には、

- 新規に空白のブックを作成
- 「Data Streamer」のメニューから「デバイスの接続」→「Bluetoothリンク経由の標準シリアル（COM*）」をクリック<br>
![05_データの取り込み1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/aa72a4a1-c824-0d8a-2492-023de3556647.png)
- 雛形がでてくるので、必要に応じて「設定」のシートを入力する。<br>今回は何も設定しなくても取り込めますがソコソコそれっぽくするため、以下の2箇所だけ設定しています。
![06_データの取り込み2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/fd1b19c1-b176-8b84-ccd1-3e98e0e22093.png)
- THETAとPCを接続する（後述の「立ち上げ手順」参照）と「Data Streamer」のメニューの「データの開始」「データの停止」などで操れます。
- グラフ描画は、リアルタイムに更新されているセルをお好みで選択して、いつも通りにグラフを描くだけです。かんたんかんたん、なにも迷わない。
![07_グラフ描画.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/a625d737-1e7b-f5c4-6e41-1c65dc12d466.png)


### 立ち上げ手順

グラフ表示したいPCが起動している状態でTHETAプラグインを起動します。
Windows 10 側には、以下のようなダイアログがでますので順にこたえてください。

- デバイスの追加ダイアログをクリック<br>![08_BT接続1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/f055da95-4302-24c4-a280-18f8e44d5828.png)

- ペアリングを許可する（設定画面→ダイアログの順に2つ開きます）<br>![08_BT接続2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/efc9e926-b64a-35dd-6701-2a34f5742776.png)

- ペアリングできたら「閉じる」をクリック<br>![08_BT接続3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/c7a86def-0516-32c2-7c2d-aed277128c0b.png)

Excelのシートはあらかじめ表示しておいても、あとから表示しても、どちらでも問題ありません。他のTerminalソフトであっても同じです。
あらかじめCOMポートを固定しておけるので、RICOH THETAの状態にかかわらず準備できるのが良い点ですね。


## まとめ

AndroidでBluetooth ClassicのHost動作をさせると、自身でコードを記述しなければならない事項が多いものの、接続さえ確立してしまえば、ポートにread/writeするだけです。サービスにBluetooth周りの処理を集約したので「MainActivity」トリガーでデータ送信をするために「Messenger」という仕組みが登場して少々ややこしくなりましたが、根本的なところはわりとシンプルです。

<b>本事例をひな形とすればかなり手間を省けます！</b> 

他のSPP通信を利用したTHETAプラグイン作成にもトライしていただけると幸いです。


今回は姿勢データを垂れ流しにしましたが、別のデータでもいいわけです。
無線LANと同時に利用できますので、Bluetooth SPP側はデバッグ情報を垂れ流しにするなどの使い方が有用ですが、積極的に機器連携に利用してもよいと思います。

書籍では、RICOH THETAで応答を受け取るところも（送信に対する応答が１対１のケース限定ですが）実装してあります。
非同期双方向通信をしようと思うと、さらに実装に工夫が必要ですが、そのためのヒントは記載されています。各自で使い方を広げていただけると幸いです。

今回の事例では、RICOH THETAをHost、PCをDeviceとして利用しましたが、外部機器がHost、RICOH THETAをDeviceとして接続するケースなどにもトライして頂けたらと思います。


## RICOH THETAプラグインパートナープログラムについて

THETAプラグインをご存じない方は[こちら](https://github.com/ricohapi/theta-api-specs/blob/main/core/products/theta-plugin.md)をご覧ください。
パートナープログラムへの登録方法は[こちら](https://qiita.com/mShiiina/items/55d98f366e650ca42251)にもまとめてあります。
Qiitaの[RICOH THETAプラグイン開発者コミュニティ TOPページ「About」](https://qiita.com/organizations/theta-plugin)に便利な記事リンク集もあります。
興味を持たれた方は[Twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発者コミュニティ(Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU)への参加もよろしくおねがいします｡