Bluetooth PC-PC/Android-PC間通信（RFCOMM）

BluetoothのプロトコルRFCOMM（[RFCOMM ‐ 通信用語の基礎知識](https://www.wdic.org/w/WDIC/RFCOMM)）での通信を試した。

- クライアント-サーバ
- PC-PC
- Android-PC

## PC-PC間 通信
- サーバ
    - Ubuntu 18.04
    - Python 3.7.4
- クライアント
    - Windows 10
    - Python 3.6.8

### 導入（Ubuntu）
```shell
sudo apt install libbluetooth-dev
pip3 install pybluez
```

### 導入（Windows）
```
# ref: https://github.com/pybluez/pybluez/issues/279
pip3 install PyBluez-win10
```

### ペアリング
ペアリングはOSのUIを使って手動で行った。

Ubuntu：Settings>Bluetooth
Windows：設定>デバイス>Bluetoothとその他のデバイス

Bluetoothデバイスのアドレス（MACアドレス）は`bluetoothctl`（Ubuntu）かデバイスマネージャ（Windows）で分かる。

### 簡易コード
インタフェースは[socket](https://docs.python.org/ja/3/library/socket.html)とほぼ同じものらしい。

サーバサイド

```python
import bluetooth

PORT = 1
server_sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
server_sock.bind(('', PORT))
server_sock.listen(1) # backlog: 接続待ち受け数

client_sock, client_addrport = server_sock.accept() # blocking until connection

data = client_sock.recv(1024)
print(data) # bytes
# print(data.decode('ascii'))
```

クライアントサイド

```python
import bluetooth

server_addr = '**:**:**:**:**:**' # replace here
server_port = 1

sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
sock.connect((server_addr, server_port))

sock.send(b'Hello World') # bytes
```

- [Check! Raspberry Pi で Bluetooth 経由のセンサーデータを取得する 〜 準備編 - Qiita](https://qiita.com/dz_/items/4e858e4d50db19b57491)
- [RaspberryPi同士でbluetooth通信 - Qiita](https://qiita.com/shippokun/items/0953160607833077163f)


## PC-Android間 通信
[Bluetooth の概要  |  Android デベロッパー  |  Android Developers](https://developer.android.com/guide/topics/connectivity/bluetooth?hl=ja)

接続先を指定するときはポートではなくUUIDを使うらしい（直接ポートを使うインタフェースはなさそう）。

### サーバサイド（PC）
pybluezのサンプル（[サーバ](https://github.com/pybluez/pybluez/blob/master/examples/simple/rfcomm-server.py)、[クライアント](https://github.com/pybluez/pybluez/blob/master/examples/simple/rfcomm-client.py)）でもポートを自動にしてUUIDで接続先を決めるようになっているので、Bluetooth的にもともとこれが推奨なのかな（ポートふさがってたら困るし）。

```python
import uuid
print(uuid.uuid4()) # ef7ce24a-a1eb-45d4-9208-f896b0ae8336
```

```python
import bluetooth

server_sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
server_sock.bind(('', bluetooth.PORT_ANY))
server_sock.listen(1) # backlog: 接続待ち受け数
# server_port = server_sock.getsockname()[1]

uuid = 'ef7ce24a-a1eb-45d4-9208-f896b0ae8336'
bluetooth.advertise_service(
    server_sock, "MyServer", service_id=uuid,
    service_classes=[ uuid, bluetooth.SERIAL_PORT_CLASS ],
    profiles=[ bluetooth.SERIAL_PORT_PROFILE ],
)

client_sock, client_addrport = server_sock.accept() # blocking until connection

data = client_sock.recv(1024)
print(data) # bytes
# print(data.decode('ascii'))
```

適当なUUIDを生成して、サーバ側のコードを修正した。

```
***site-packages/bluetooth/bluez.py", line 275, in advertise_service
    raise BluetoothError (*e.args)
bluetooth.btcommon.BluetoothError: [Errno 2] No such file or directory
```

- [bluetooth.btcommon.BluetoothError: (2, 'No such file or directory') - Stack Overflow](https://stackoverflow.com/a/46810116)
- [Bluetooth Python script. - Raspberry Pi Forums](https://raspberrypi.stackexchange.com/a/42262)

pybluezのラップしてるコマンドラインインタフェースが非推奨になってるらしい。仕方ないので互換モードを有効にして、Serial Port Profileを追加する。

```
# vim /etc/systemd/system/dbus-org.bluez.service

# ExecStart=/usr/lib/bluetooth/bluetoothd
ExecStart=/usr/lib/bluetooth/bluetoothd -C

# sdptool add SP
# systemctl daemon-reload
# systemctl restart bluetooth
```

```
***site-packages/bluetooth/bluez.py", line 275, in advertise_service
    raise BluetoothError (*e.args)
bluetooth.btcommon.BluetoothError: [Errno 13] Permission denied
```

実行にroot権限が必要になってしまったみたいなので、rootのPython環境（3.6.9）に`pybluez`を入れて再実行したところ動いた。

### クライアントサイド（Android/Java）

try-catchを分割したので長くなったが、`createRfcommSocketToServiceRecord`で`BluetoothSocket`のインスタンスを取得すれば`java.net.Socket`とだいたい同じ。

Bluetoothの権限が必要なので、`AndroidManifest.xml`に以下を追加。

```xml
<uses-permission android:name="android.permission.BLUETOOTH" />
```

以下のようなコードを実機で動かして（ペアリング済みの）PCと通信できることが確認できた。実際には、`adapter.getBondedDevices()`でペアリング済みのデバイスリスト（`List<BluetoothDevice>`）が手に入るのでそこから宛先を選択させる実装をするか、未ペアリングのアドレスにconnectしようとすれば自動でペアリング用のOSのUIが開くらしいので、アドレスをサーバ側に（テキストとかQRコードとかで）表示して入力させるか、みたいな感じなのかな。

```java
final String SERVER_DEVICE_ADDRESS = "**:**:**:**:**:**";
final String SERVER_SERVICE_UUID = "ef7ce24a-a1eb-45d4-9208-f896b0ae8336";

final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
    Log.d(TAG, "No bluetooth support.");
    return;
}

if (! adapter.isEnabled()) {
    Log.d(TAG, "Bluetooth is disabled.");

    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_CODE_ENABLE_BT);

    return; // onActivityResult省略
}

new Thread(new Runnable() {
  @Override
  public void run() {
      BluetoothDevice server = adapter.getRemoteDevice(SERVER_DEVICE_ADDRESS);
      final BluetoothSocket socket;
      try {
          socket = server.createRfcommSocketToServiceRecord(UUID.fromString(SERVER_SERVICE_UUID));
      } catch (IOException e) {
          e.printStackTrace();
          return;
      }

      try {
          socket.connect();
      } catch (IOException e) {
          e.printStackTrace();
          Log.d(TAG, "Unable to connect.");

          try {
              socket.close();
          } catch (IOException ex) {
              ex.printStackTrace();
              Log.d(TAG, "Unable to close socket.");
              return;
          }

          return;
      }

      final OutputStream os;
      try {
          os = socket.getOutputStream();
      } catch (IOException e) {
          e.printStackTrace();
          Log.d(TAG, "Unable to create output stream.");

          try {
              socket.close();
          } catch (IOException ex) {
              ex.printStackTrace();
              Log.d(TAG, "Unable to close socket.");
              return;
          }
          return;
      }

      try {
          os.write("Hello World from Android\n".getBytes(Charset.forName("ascii")));
      } catch (IOException e) {
          e.printStackTrace();
          Log.d(TAG, "Unable to write to output stream.");
      }

      try {
          os.close();
      } catch (IOException e) {
          e.printStackTrace();
          Log.d(TAG, "Unable to close output stream.");
      }

      try {
          socket.close();
      } catch (IOException e) {
          e.printStackTrace();
          Log.d(TAG, "Unable to close socket.");
      }

  }
}).run();
```