【Android】BLE通信ざっくりまとめ

AndroidアプリにおけるBLEを利用した通信処理について。
<font color=red>サンプルコードはAndroid 5.0以降のみ対応</font>

* BLEとは
* Android OSのBLE対応状況
* AndroidアプリでBLE通信を利用する
* AndroidアプリでBLE機器として配信する(ペリフェラル)
* AndroidアプリでBLE機器を利用する(セントラル)
* Tips

# BLEとは
## 概要
* Bluetooth Low Energyの略称
* 省電力

## 用語
<font color=red>個人見解</font>をかなり混ぜて説明

**ペリフェラル**

* BLE機器そのもの
* 周辺にの一定距離に対し、定期的にアドバタイズを行う
* セントラルによるキャラクタリスティクスの操作を受け付ける

**セントラル**

* BLE機器を利用するもの
* 周辺のBLE機器をスキャンし、任意のGATTあるいはサービスに接続を行う
* 接続したサービスのキャラクタリスティクスを操作し、情報取得や機器の操作を行う

**GATT**

* サーバのようなもの
* 1つ以上のサービスを持ち、その機器がどのようなサービスを提供するのかを決定する

**サービス**

* いくつかのキャラクタリスティクスをパッケージ化してコンテンツをもたせたもの
* 必ず1つ以上のキャラクタリスティクスをもつ

**キャラクタリスティクス**

* 値、属性、ディスクリプタをもつ
* セントラルが実際に触ることが可能なのはキャラクタリスティクス単位

**アドバタイズ**

* ペリフェラルが自分のもつサービスを接続可能な状態にし、周辺に機器情報を発信すること
* アドバタイズされていないサービスは、セントラルからは発見できない


# Android OSのBLE対応状況
更新時点でのAndroidとBLEの状況について。

## 対応OS
* Android 4.3からBLE通信(Centralのみ)に対応 
* Android 5.0からPeripheralにも対応(一部端末で不具合あり)

## OSバージョンによる差異
#### Android 4.4以前
* 128bitのサービスUUIDをフィルタにしたスキャンが出来ない
* 機器スキャン時の引数でScanRecordが渡されるが、自分でbyte配列を解析しなければいけない


# AndroidアプリでBLE通信を利用する
* マニフェストファイルの設定

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

* アプリがBLE対応端末のみに対応していることを指定する場合

```ruby:AndroidManifest.xml
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
```

# AndroidアプリでBLE機器として配信する。(ペリフェラル)
BLE通信を用いたサービス配信を行う場合の実装方法について。

**バグによりNexus5での実装確認ができていない為、今回は記載なし。**


# AndroidアプリでBLE機器を利用する(セントラル)
BLE機器をスキャンし、そのサービスを利用する場合の実装方法について。

### 初期化

```
        BluetoothManager bluetoothManager = (BluetoothManager) context.
                getSystemService(Context.BLUETOOTH_SERVICE);

        // mBluetoothAdapterの取得
        mBluetoothAdapter = bluetoothManager.getAdapter();

        // mBluetoothLeScannerの初期化
        mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();  
```

### スキャン処理

```

    // BLEスキャンのタイムアウト時間
    private static final long SCAN_PERIOD = 10000;

    private ArrayList<BluetoothDevice> deviceList = new ArrayList<>();
    private BluetoothAdapter mBluetoothAdapter;
    private BluetoothLeScanner mBluetoothLeScanner;
    private ScanCallback mScanCallback;
    private Handler mHandler = new Handler();

    // ScanCallbackの初期化
    private ScanCallback initCallbacks() {

        return new ScanCallback() {
            @Override
            public void onScanResult(int callbackType, ScanResult result) {
                super.onScanResult(callbackType, result);

                if (result != null && result.getDevice() != null) {
                    if (isAdded(result.getDevice())) {
                        // No add
                    } else {
                        saveDevice(result.getDevice());
                    }
                }

            }

            @Override
            public void onBatchScanResults(List<ScanResult> results) {
                super.onBatchScanResults(results);
            }

            @Override
            public void onScanFailed(int errorCode) {
                super.onScanFailed(errorCode);
            }

        };

    }

    // スキャン実施
    public void scan(List<ScanFilter> filters, ScanSettings settings,
                             boolean enable) {

        mScanCallback = initCallbacks();

        if (enable) {
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    isScanning = false;
                    mBluetoothLeScanner.stopScan(mScanCallback);
                }
            }, SCAN_PERIOD);

            isScanning = true;
            mBluetoothLeScanner.startScan(mScanCallback);
            // スキャンフィルタを設定するならこちら
            // mBluetoothLeScanner.startScan(filters, settings, mScanCallback);
        } else {
            isScanning = false;
            mBluetoothLeScanner.stopScan(mScanCallback);
        }

    }

    // スキャン停止
    public void stopScan() {

        if (mBluetoothLeScanner != null) {
            mBluetoothLeScanner.stopScan(mScanCallback);
        }

    }

    // スキャンしたデバイスのリスト保存
    public void saveDevice(BluetoothDevice device) {

        if (deviceList == null) {
            deviceList = new ArrayList<>();
        }

        deviceList.add(device);

    }

    // スキャンしたデバイスがリストに追加済みかどうかの確認
    public boolean isAdded(BluetoothDevice device) {

        if (deviceList != null && deviceList.size() > 0) {
            return deviceList.contains(device);
        } else {
            return false;
        }

    }
```

### スキャンした端末への接続とサービス検索

```
    private BluetoothGatt bluetoothGatt;

    private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status,
                                            int newState) {
            super.onConnectionStateChange(gatt, status, newState);

            // 接続成功し、サービス取得
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                bluetoothGatt = gatt;
                discoverService();
            }

        }

        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);

            serviceList = gatt.getServices();

            for (BluetoothGattService s : serviceList) {
                // サービス一覧を取得したり探したりする処理
                // あとキャラクタリスティクスを取得したり探したりしてもよい
            }
        }
    }

    // Gattへの接続要求
    public void connect(Context context, BluetoothDevice device) {

        bluetoothGatt = device.connectGatt(context, false, mGattCallback);
        bluetoothGatt.connect();

    }

    // サービス取得要求
    public void discoverService() {

        if (bluetoothGatt != null) {
            bluetoothGatt.discoverServices();
        }

    }
```

# Tips
その他、よくあるシチュエーション等。

## 通常のBluetooth機器とBLE機器を同時にスキャンしたい
Androidの仕様で、機器スキャンはどちらか一方しか出来ない。

## Nexus5 (Lollipop)でアドバタイズしようとするとアプリがクラッシュする
詳細不明。仕様らしい。
他にもペリフェラル側の実装に対応していない端末が存在する模様。
 
## ペリフェラルによるキャラクタリスティクス更新通知を取得したい

キャラクタリスティクスに対し、Notificationのセットとディスクリプタの更新が必要

```lang:実装例

// キャラクタリスティック設定UUID
String CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";

// Notification を要求する
boolean registered = gatt.setCharacteristicNotification(characteristic, true);

// Characteristic の Notification 有効化
if (registered) {
    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
            UUID.fromString(CHARACTERISTIC_CONFIG));

    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    gatt.writeDescriptor(descriptor);
}
```

上記の処理をしておくと、ペリフェラル側でキャラクタリスティクスに更新を行った際に
セントラル(アプリ)側でonCharacteristicChangedが呼ばれる。