ESP32とAndroid(Kotlin)でBluetoothSPPを使って通信する

# 目的
センサ評価用の基板を使ってアプリとか作るなら、やっぱり無線がいいですよね。
でも、だいたい評価用の基板にはbluetooothとかwifiなんてついていません。
最近は無線モジュール側はけっこうすぐに使えたりするのですが、（僕にとっては）Androidアプリが難しかったりします。
サンプルコードもjavaしか見つけられなくて、手こずりました。
ここらへんを簡単にできるようにメモしておきます✌

# 環境
テストには以下のものを用いました。

* Nexus 6P (Android 8.1.0)
* Android Studio 3.5.3
* Kotlin
* [ESP32-DevKitC ESP-WROOM-32開発ボード](https://akizukidenshi.com/catalog/g/gM-11819/)
* Arduino 1.8.13
* Tera Term

# ソリューションの提案
下図の上のようにセンサとスマホで通信したいけど、できないというのが今回解決したい課題です。

![今回の課題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211445/7d82bc2c-ac64-b333-449e-10f273239896.png)

図の下側のように、センサとスマホの間に [ESP32](https://akizukidenshi.com/catalog/g/gM-11819/) を挟むだけで、センサからはUARTでデータを送信するだけで、スマホ側に届くんです！
（もうこれ便利すぎなんですが、もっと便利なBluetooth+Wifiのモジュールがあれば教えてください。）
たいていUARTはついていると思いますが、そうじゃない場合はESP側のソフトを適宜変えましょう。

# ESP32編

無線モジュール側はとても簡単です。
まずはArduinoのボードマネージャでESP32用のライブラリたちをダウンロードしましょう。
ダウンロード方法については、[ESP32とBluetoothSerialで文字列を送受信する](https://qiita.com/daivr7774/items/06770e1d5288a7127f83) を参考にしてみてください。

ダウンロード出来たら、サンプルコードを開きます。
以下の画像にある通り、SerialToSerialBTを選択しましょう。

![使用するスケッチ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211445/fe4a239b-bc57-2b99-1e05-b54fe9d2ba17.png)

PCやスマホからは`ESP32test`という名前のデバイスとして見つけることができます。スマホとペアリングをしておきましょう。
このソースコードを使えば、ちゃんと動いているかどうかをUSB-UART経由でテストすることが可能です。
ですがここで使われているUART0はUSB-UARTに使われており通信できないため、センサ基板からデータを受け取るという目的が果たせません。
そこで、IO16,IO17にあるUART2を使います。
参考： [Arduino-ESP32 Serial通信](https://qiita.com/T-YOSH/items/0ce41136c535c723affb)

![ESP32で使用できるピン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211445/9a896a73-ab3b-6423-41eb-b2fd150e2331.png)

以下のソースコードはほぼサンプルコードそのままですが、UART2を使用できるように変更してあります。
`Serial.begin(115200)`を残しているのは、これをなくすと、デバッグメッセージが出せないから（？）かESP32が動かなくなるからです。

```SerialToSerialBT.cpp
//This example code is in the Public Domain (or CC0 licensed, at your option.)
//By Evandro Copercini - 2018
//
//This example creates a bridge between Serial and Classical Bluetooth (SPP)
//and also demonstrate that SerialBT have the same functionalities of a normal Serial

#include "BluetoothSerial.h"

#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
#endif

BluetoothSerial SerialBT;

void setup() {
  Serial.begin(115200);
  Serial2.begin(115200); // これがUART2
  SerialBT.begin("ESP32test"); //Bluetooth device name
  Serial.println("The device started, now you can pair it with bluetooth!");
}

void loop() {
  if (Serial2.available()) {
    SerialBT.write(Serial2.read());
  }
  if (SerialBT.available()) {
    Serial2.write(SerialBT.read());
  }
  delay(20);
}
```


# Android編
Androidは基本的にはサンプルソースを読むのが大変だったので、[Bluetooth の概要](https://developer.android.com/guide/topics/connectivity/bluetooth?hl=ja)を参考にしました。
ここのソースコードを全部コピーしただけでは動かなかったのですが、動かないポイントとしては[クライアント側の接続](https://developer.android.com/guide/topics/connectivity/bluetooth?hl=ja#ConnectAsAClient)にありました。
connectしたsocketを使って通信をするのですが、useという関数が最後まで行くとsocket.closeが呼ばれてしまい、通信が終わってしまいます。これがなんかうまくいかない原因だったようです。

```ConnectThread.kt
private inner class ConnectThread(device: BluetoothDevice) : Thread() {
・・・
        // ここから問題のコード
        mmSocket?.use { socket ->
            // Connect to the remote device through the socket. This call blocks
            // until it succeeds or throws an exception.
            socket.connect()

            // The connection attempt succeeded. Perform work associated with
            // the connection in a separate thread.
            manageMyConnectedSocket(socket)
            // この関数の後にsocket.close()が自動で呼ばれる
        }
        // ここまで問題のコード
    }
・・・
}
```

もう一つ、SPPを使う場合に注意するべきは、UUIDです。
bluetoothのSPPを使う場合にはもともと用意されているUUIDを`device.createRfcommSocketToServiceRecord(MY_UUID)`のMY_UUIDのところに入れてやる必要があります。以下はSPPのUUIDです。

```UUID
00001101-0000-1000-8000-00805F9B34FB
```

以下のソースコードはAndroidStudioのEmptyActivityをベースにして、[Bluetooth の概要](https://developer.android.com/guide/topics/connectivity/bluetooth?hl=ja)をコピーしてきたものです。
ここでは`onCreate`の中で、ペアリング済みのbluetoothデバイスを探して、`ESP32test`というデバイスを見つけたらそれに接続するというプログラムになっています。
途中の`device.createInsecureRfcommSocketToServiceRecord(device.uuids[0].uuid)`でペアリングした際に取得したUUID（これはSPPと一致する）を入れているところがポイントです。


```MainActivity.kt
const val TAG = "MainActivity"
const val TARGET_DEVICE_NAME = "ESP32test"
const val REQUEST_ENABLE_BLUETOOTH = 1
// Defines several constants used when transmitting messages between the
// service and the UI.
const val MESSAGE_READ: Int = 0
const val MESSAGE_WRITE: Int = 1
const val MESSAGE_TOAST: Int = 2

class MainActivity : AppCompatActivity() {
    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
    private var connectedThread: ConnectedThread? = null
    private var connectThread: ConnectThread? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (bluetoothAdapter == null) {
            Log.d(TAG, "bluetooth is not supported.")
            return
        }

        if (!bluetoothAdapter.isEnabled) {
            val enableBluetoothIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            startActivityForResult(enableBluetoothIntent, REQUEST_ENABLE_BLUETOOTH)
        }

        val pairedDevices: Set<BluetoothDevice>? = bluetoothAdapter.bondedDevices
        pairedDevices?.forEach { device ->
            val deviceName = device.name
            val deviceHardwareAddress = device.address // MAC address
            if (device.name == TARGET_DEVICE_NAME) {
                Log.d(TAG, "name = %s, MAC <%s>".format(deviceName, deviceHardwareAddress))
                device.uuids.forEach { uuid ->
                    Log.d(TAG, "uuid is %s".format(uuid.uuid))
                }
                connectThread = ConnectThread(device)
                connectThread?.start()
                return
            }
        }
    }

    fun manageMyConnectedSocket(socket: BluetoothSocket) {
        connectedThread = ConnectedThread(socket)
        connectedThread?.start()
    }
    
    private inner class ConnectThread(device: BluetoothDevice) : Thread() {
        private val mmSocket: BluetoothSocket? by lazy(LazyThreadSafetyMode.NONE) {
            device.createInsecureRfcommSocketToServiceRecord(device.uuids[0].uuid)
            // SPPのUUIDは決まっているけどほかのUUIDだったり、そもそもUUIDを打たなくていいから便利
        }

        public override fun run() {
            // Cancel discovery because it otherwise slows down the connection.
            bluetoothAdapter?.cancelDiscovery()
            if (mmSocket == null) {
                return
            }
            val socket = mmSocket
            socket ?: return
            socket.connect()
            // The connection attempt succeeded. Perform work associated with
            // the connection in a separate thread.
            manageMyConnectedSocket(socket)
// ここではないどこかでcloseの処理は入れないといけない
//            socket.close() 

//            mmSocket?.use { socket ->
//                // Connect to the remote device through the socket. This call blocks
//                // until it succeeds or throws an exception.
//                socket.connect()
//                // The connection attempt succeeded. Perform work associated with
//                // the connection in a separate thread.
//                manageMyConnectedSocket(socket)
//            }
        }

        // Closes the client socket and causes the thread to finish.
        fun cancel() {
            try {
                mmSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the client socket", e)
            }
        }
    }

    private inner class ConnectedThread(private val mmSocket: BluetoothSocket) : Thread() {

        private val mmInStream: InputStream = mmSocket.inputStream
        private val mmOutStream: OutputStream = mmSocket.outputStream
        private val mmBuffer: ByteArray = ByteArray(1024) // mmBuffer store for the stream

        override fun run() {
            var numBytes: Int // bytes returned from read()
            Log.d(TAG, "connect start!")
            // Keep listening to the InputStream until an exception occurs.
            while (true) {
                // Read from the InputStream.
                numBytes = try {
                    mmInStream.read(mmBuffer)
                } catch (e: IOException) {
                    Log.d(TAG, "Input stream was disconnected", e)
                    break
                }
                Log.d(TAG, mmBuffer[0].toString())
//                // Send the obtained bytes to the UI activity.
//                val readMsg = handler.obtainMessage(
//                    MESSAGE_READ, numBytes, -1,
//                    mmBuffer)
//                readMsg.sendToTarget()
            }
        }

//        // Call this from the main activity to send data to the remote device.
//        fun write(bytes: ByteArray) {
//            try {
//                mmOutStream.write(bytes)
//            } catch (e: IOException) {
//                Log.e(TAG, "Error occurred when sending data", e)
//
//                // Send a failure message back to the activity.
//                val writeErrorMsg = handler.obtainMessage(MESSAGE_TOAST)
//                val bundle = Bundle().apply {
//                    putString("toast", "Couldn't send data to the other device")
//                }
//                writeErrorMsg.data = bundle
//                handler.sendMessage(writeErrorMsg)
//                return
//            }
//
//            // Share the sent message with the UI activity.
//            val writtenMsg = handler.obtainMessage(
//                MESSAGE_WRITE, -1, -1, mmBuffer)
//            writtenMsg.sendToTarget()
//        }

        // Call this method from the main activity to shut down the connection.
        fun cancel() {
            try {
                mmSocket.close()
            } catch (e: IOException) {
                Log.e(TAG, "Could not close the connect socket", e)
            }
        }
    }
}
```

参考：[BluetoothのSPPで使用するUUID](http://masao6739.blog89.fc2.com/blog-entry-18.html)

# 結果
AndroidStudioのlogcatで確認できました！
abと入力したら97,98とlogcat上で表示されています。

![Android側の出力.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211445/d7b8aae0-2241-f5fd-37f4-0c8e4ab5acf9.png)