vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

Android6.0でBLEを使う(Peripheral編) その1

はじめに

前回の続きで、今度はPeripheral(Slave)についてです。

Nexus5X(ver. 6.0)をPeripheral端末として、Nexus7(ver.5.1.1)と連携します。

AndroidManifest.xmlやアクセス権については前回参照のこと。

準備

Peripheral側ではざっと以下の処理を行います。

  1. 各種オブジェクトの準備
  2. Central側で、Write・Read・Notificationを許可する
  3. Advertising(Central側から発見・接続できるようにする)の開始
  4. タイマーで一定時間ごとに値を更新(Central側にNotificationが届く)
  5. WriteRequestがあれば、書き込まれたデータを受け取ってTextViewにその値をセット

各種オブジェクトの準備、Write・Read・Notificationの許可

まず、AndroidではPeripheral端末としてふるまうためには、Android5.0以上である必要があります。
そのため、アプリ自体の最低Apiバージョンの指定によっては確認が必要となります。

PeripheralActivity.java

~省略~
@Override
protected void onCreate(Bundle savedInstanceState) {
~省略~

    // Bluetoothの使用準備.
    // Bluetoothへのアクセス、機能を制御できるようにする.
    mBleManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    mBleAdapter = mBleManager.getAdapter();

    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
        // BluetoothがOffならインテントを表示する.
        if ((mBleAdapter == null)
                || (! mBleAdapter.isEnabled())) {
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            // Intentでボタンを押すとonActivityResultが実行されるので、第二引数の番号を元に処理を行う.
            startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
        }
        else{
            this.prepareBle();
        }
    }
    else{
        Toast.makeText(this, "お使いのOSバージョンでは使用できません。", Toast.LENGTH_SHORT).show();
    }
}
~省略~
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // インテントBluetoothをOnにしたら、使用準備開始.
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {
        case REQUEST_ENABLE_BT:
            if ((mBleAdapter != null)
                    || (mBleAdapter.isEnabled())) {
                // if BLE is enabled, start advertising.
                this.prepareBle();
            }
            break;
    }
}
~省略~

さらにNexus7(2013)などは、OSバージョン自体は問題ないものの、Peripheralとしては実行できない問題があります。
具体的には「getBluetoothLeAdvertiser()」の値が取得できず、nullが返ります。

PeripheralActivity.java

~省略~
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void prepareBle()
{
~省略~
    mBtAdvertiser = mBleAdapter.getBluetoothLeAdvertiser();
    //mBtAdvertiserの確認
    if(mBtAdvertiser != null){

        // BLEでのデータのやり取りで使用する、ServiceとCharacteristicの準備.
        // 各UUIDはCentralと同じものを使用すること.
        BluetoothGattService btGattService = new BluetoothGattService(UUID.fromString(SERVICE_UUID)), BluetoothGattService.SERVICE_TYPE_PRIMARY);

        // CentralからWrite・Read・Notificationができるようにする.
        mBtCharacteristic = new BluetoothGattCharacteristic(UUID.fromString(CHARACTERISTIC_UUID)
                        ,BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE
                        ,BluetoothGattDescriptor.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ);
        btGattService.addCharacteristic(mBtCharacteristic);

        // Notificationのやり取りで使用するDescriptorの準備.
        BluetoothGattDescriptor dataDescriptor = new BluetoothGattDescriptor(
                    UUID.fromString(CHARACTERISTIC_CONFIG_UUID)
                    ,BluetoothGattDescriptor.PERMISSION_WRITE | BluetoothGattDescriptor.PERMISSION_READ);
        mBtCharacteristic.addDescriptor(dataDescriptor);

        // やりとりするデータを管理するサーバを開き、サービスを追加する.
        // 接続・切断、WriteRequestなどの受け取りはmGattServerCallbackで.
        mBtGattServer = mBleManager.openGattServer(this, mGattServerCallback);
        mBtGattServer.addService(btGattService);
        
        // Advertisingの設定.
        AdvertiseData.Builder dataBuilder=new AdvertiseData.Builder();
        AdvertiseSettings.Builder settingsBuilder=new AdvertiseSettings.Builder();
        dataBuilder.setIncludeTxPowerLevel(false);
        dataBuilder.addServiceUuid(ParcelUuid.fromString(SERVICE_UUID));
        settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED);
        settingsBuilder.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH);
        
        ~省略~
    }else {
        Toast.makeText(this, "お使いのデバイスではPeripheralモードが使用できません。", Toast.LENGTH_SHORT).show();
    }
}
~省略~

Advertisingの開始

Central側から発見・接続できるようにするため、Advertisingを開始します。

PeripheralActivity.java

~省略~
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void prepareBle()
{
~省略~
    // Central側から発見・接続できるようにするためのAdvertisingの開始.
    BluetoothLeAdvertiser bluetoothLeAdvertiser = mBleAdapter.getBluetoothLeAdvertiser();
    bluetoothLeAdvertiser.startAdvertising(settingsBuilder.build(),dataBuilder.build()
                    , new AdvertiseCallback(){
                        @Override
                        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                            // Advertisingが開始できれば実行.
                        }
                        @Override
                        public void onStartFailure(int errorCode) {
                            // Advertisingが開始に失敗すれば実行.
                        }
                    });
~省略~

これで、Central側でScanを開始すれば接続できるようになるはずです。

Notification

接続が終わったら、値を更新して、CentralにNotificationが届くようにします。
1. 各種オブジェクトの準備のタイミングでタイマーを用意しておいて、一定時間ごとに乱数を生成し、値を更新します。

PeripheralActivity.java

~省略~
private boolean mIsConnected = false;
private Random mRandom = new Random();
private Timer mTimer;
private SendDataTimer mSendDataTimer;

public class SendDataTimer extends TimerTask {
    @Override
    public void run() {
        if(mIsConnected)
        {
            // 1000ミリ秒ごとに0~999までの乱数を生成する.
            mStrUpdateNum = String.valueOf(mRandom.nextInt(1000));
            // 値を更新して、TextViewにその値をセットする.
            mHndBleHandler.sendEmptyMessage(MESSAGE_NEW_UPDATEDNUM);
            // 更新した値をCentralに伝える.
            sendNotification();         
        }
    }
}
~省略~
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void prepareBle()
    {
        // タイマーの準備.
        mTimer = new Timer();
        mSendDataTimer = new SendDataTimer();
        // 第二引数:最初の処理までのミリ秒 第三引数:以降の処理実行の間隔(ミリ秒).
        mTimer.schedule(mSendDataTimer, 500, 1000);
~省略~

値の更新は、Characteristicに値をセットして、notifyCharacteristicChangedを実行することで可能です。
ただし、notifyCharacteristicChangedの第一引数で、対象の端末を指定する必要があります。

  1. 各種オブジェクトの準備で、openGattServerを実行したときに引数として渡していた、 BluetoothGattServerCallbackのonConnectionStateChangeにて、接続されたCentral端末を引数として受け取ることができます。
private BluetoothGattServerCallback mGattServerCallback = new BluetoothGattServerCallback() {
        ~省略~
        @Override
        public void onConnectionStateChange(android.bluetooth.BluetoothDevice device, int status,
                                            int newState) {
            // 接続・切断されたら実行されるので、接続されたら端末をセットしてタイマーからNotificationを送信できるようにする.
            if(newState == BluetoothProfile.STATE_CONNECTED){
                // set connected device.
                mConnectedDevice = device;
                mIsConnected = true;
            }
            else{
                mIsConnected = false;
            }
        }
~省略~
    private void sendNotification() {
        // 値を更新してCentralに伝える.
        mBtCharacteristic.setValue(mStrUpdateNum);
        mBtGattServer.notifyCharacteristicChanged(mConnectedDevice, mBtCharacteristic, false);
    }
}

なお、今回はCentral、Peripheralともに1台ずつのためBluetoothDeviceは1つのみ保持するようにしていますが、 複数の場合はArrayListに突っ込んで、sendNotificationでループ処理すればOKのようです。

と、ここまでは割と順調だったのですが、WriteRequestでハマってしまいまいました。 長くなってきたので次回に続きます。

参考