vaguely

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

Android5.0〜でBLEを使う(Central編)

長らく放置していましたが、Android ver.4.4でBLEを試した記事のver.5.0編です。

はじめに

Android5.0からはBluetooth連携のSlave側となるPeripheralとしての役割も担うことができるようになりました。
が、今回は以前作成したver.4.4用のコードを元に、MasterとなるCentral側を作成してみます。

ちなみに当時実機での確認はしていなかったものの5.0用のコードも作成しており、
「これ何もしなくても動くんじゃね?」と淡い期待を抱きながらNexus7で動かしたところ、見事にクラッシュして動きませんでしたorz

また、連携には相手(Peripheral)が必要ですが、Macこれを動かして確認しています。

準備

バイスがBLEに対応しているかを確認する

ここ最近のデバイスなら問題無いと思いますが、BLEが使用できないと動作させられないので確認しておきます。

MainActivity.java

〜省略〜
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // デバイスがBLEに対応しているかを確認する.
    if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
      // BLEに対応していない旨のToastやダイアログを表示する.
      finish();
    }
〜省略〜

AndroidManifest

BLEの使用、BLEを使ったデバイスのコントロールのための権限を付与します。

AndroidManifest.xml

〜省略〜
< uses-permission android:name="android.permission.BLUETOOTH" / >
< uses-permission android:name="android.permission.BLUETOOTH_ADMIN" / >
< uses-feature
    android:name="android.hardware.bluetooth_le"
    android:required="true" / >
〜省略〜

ソースコード

package jp.androidblecontroller;

import android.annotation.TargetApi;
import android.app.Activity;
import android.bluetooth.le.*;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAdapter.LeScanCallback;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;

public class CentralActivity extends Activity {

  private final static int SDKVER_LOLLIPOP = 21;
  private final static int MESSAGE_NEW_RECEIVEDNUM = 0;
  private final static int MESSAGE_NEW_SENDNUM = 1;
  private final static int REQUEST_ENABLE_BT = 123456;
  private BluetoothManager mBleManager;
  private BluetoothAdapter mBleAdapter;
  private boolean mIsBluetoothEnable = false;
  private BluetoothLeScanner mBleScanner;
  private BluetoothGatt mBleGatt;
  private BluetoothGattCharacteristic mBleCharacteristic;
  private TextView mTxtReceivedNum;
  private TextView mTxtSendNum;
  private String mStrReceivedNum = "";
  private String mStrSendNum = "";

  // 対象のサービスUUID.
  private static final String SERVICE_UUID = "2B1DA6DE-9C29-4D6C-A930-B990EA2F12BB";
  // キャラクタリスティックUUID.
  private static final String CHARACTERISTIC_UUID = "7F855F82-9378-4508-A3D2-CD989104AF22";
  // キャラクタリスティック設定UUID(固定値).
  private static final String CHARACTERISTIC_CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb";

  // 乱数送信用.
  private Random mRandom = new Random();
  private Timer mTimer;
  private SendDataTimer mSendDataTimer;

  private final LeScanCallback mScanCallback = new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
      runOnUiThread(new Runnable() {
        @Override
        public void run() {
          // スキャン中に見つかったデバイスに接続を試みる.第三引数には接続後に呼ばれるBluetoothGattCallbackを指定する.
          mBleGatt = device.connectGatt(getApplicationContext(), false, mGattCallback);
        }
      });
    }
  };
  private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
    {
      // 接続状況が変化したら実行.
      if (newState == BluetoothProfile.STATE_CONNECTED) {
        // 接続に成功したらサービスを検索する.
        gatt.discoverServices();
      } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
        // 接続が切れたらGATTを空にする.
        if (mBleGatt != null)
        {
            mBleGatt.close();
            mBleGatt = null;
        }
        mIsBluetoothEnable = false;
      }
    }
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status)
    {
      // Serviceが見つかったら実行.
      if (status == BluetoothGatt.GATT_SUCCESS) {
        // UUIDが同じかどうかを確認する.
        BluetoothGattService service = gatt.getService(UUID.fromString(SERVICE_UUID));
        if (service != null)
        {
          // 指定したUUIDを持つCharacteristicを確認する.
          mBleCharacteristic = service.getCharacteristic(UUID.fromString(CHARACTERISTIC_UUID));

          if (mBleCharacteristic != null) {
            // Service, CharacteristicのUUIDが同じならBluetoothGattを更新する.
            mBleGatt = gatt;

            // キャラクタリスティックが見つかったら、Notificationをリクエスト.
            boolean registered = mBleGatt.setCharacteristicNotification(mBleCharacteristic, true);

            // Characteristic の Notificationを有効化する.
            BluetoothGattDescriptor descriptor = mBleCharacteristic.getDescriptor(
                    UUID.fromString(CHARACTERISTIC_CONFIG_UUID));

            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            mBleGatt.writeDescriptor(descriptor);
            // 接続が完了したらデータ送信を開始する.
            mIsBluetoothEnable = true;
          }
        }
      }
    }
    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)
    {
      // キャラクタリスティックのUUIDをチェック(getUuidの結果が全て小文字で帰ってくるのでUpperCaseに変換)
      if (CHARACTERISTIC_UUID.equals(characteristic.getUuid().toString().toUpperCase()))
      {
        // Peripheralで値が更新されたらNotificationを受ける.
        mStrReceivedNum = characteristic.getStringValue(0);
        // メインスレッドでTextViewに値をセットする.
        mBleHandler.sendEmptyMessage(MESSAGE_NEW_RECEIVEDNUM);
      }
    }
  };
  private Handler mBleHandler = new Handler()
  {
    public void handleMessage(Message msg)
    {
      // UIスレッドで実行する処理.
      switch (msg.what)
      {
        case MESSAGE_NEW_RECEIVEDNUM:
          mTxtReceivedNum.setText(mStrReceivedNum);
          break;
        case MESSAGE_NEW_SENDNUM:
          mTxtSendNum.setText(mStrSendNum);
          break;
      }
    }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_central);

    mIsBluetoothEnable = false;

    // Writeリクエストで送信する値、Notificationで受け取った値をセットするTextView.
    mTxtReceivedNum = (TextView) findViewById(R.id.received_num);
    mTxtSendNum = (TextView) findViewById(R.id.send_num);

    // Bluetoothの使用準備.
    mBleManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    mBleAdapter = mBleManager.getAdapter();

    // Writeリクエスト用のタイマーをセット.
    mTimer = new Timer();
    mSendDataTimer = new SendDataTimer();
    // 第二引数:最初の処理までのミリ秒 第三引数:以降の処理実行の間隔(ミリ秒).
    mTimer.schedule(mSendDataTimer, 500, 1000);

    // BluetoothがOffならインテントを表示する.
    if ((mBleAdapter == null)
            || (! mBleAdapter.isEnabled())) {
      Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
      // Intentでボタンを押すとonActivityResultが実行されるので、第二引数の番号を元に処理を行う.
      startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }
    else
    {
      // BLEが使用可能ならスキャン開始.
      this.scanNewDevice();
    }
  }
  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // Intentでユーザーがボタンを押したら実行.
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {
      case REQUEST_ENABLE_BT:
        if ((mBleAdapter != null)
                || (mBleAdapter.isEnabled())) {
          // BLEが使用可能ならスキャン開始.
          this.scanNewDevice();
        }
        break;
    }
  }
  private void scanNewDevice()
  {
    // OS ver.5.0以上ならBluetoothLeScannerを使用する.
    if (Build.VERSION.SDK_INT >= SDKVER_LOLLIPOP)
    {
      this.startScanByBleScanner();
    }
    else
    {
      // デバイスの検出.
      mBleAdapter.startLeScan(mScanCallback);
    }
  }
  @TargetApi(SDKVER_LOLLIPOP)
  private void startScanByBleScanner()
  {
    mBleScanner = mBleAdapter.getBluetoothLeScanner();
    // デバイスの検出.
    mBleScanner.startScan(new ScanCallback() {
      @Override
      public void onScanResult(int callbackType, ScanResult result) {
        super.onScanResult(callbackType, result);
        // スキャン中に見つかったデバイスに接続を試みる.第三引数には接続後に呼ばれるBluetoothGattCallbackを指定する.
        result.getDevice().connectGatt(getApplicationContext(), false, mGattCallback);
      }
      @Override
      public void onScanFailed(int intErrorCode)
      {
        super.onScanFailed(intErrorCode);
      }
    });
  }
  public class SendDataTimer extends TimerTask{
    @Override
    public void run() {
      if(mIsBluetoothEnable)
      {
        // 設定時間ごとに0~999までの乱数を作成.
        mStrSendNum = String.valueOf(mRandom.nextInt(1000));
        // UIスレッドで生成した数をTextViewにセット.
        mBleHandler.sendEmptyMessage(MESSAGE_NEW_SENDNUM);
        // キャラクタリスティックに値をセットして、Writeリクエストを送信.
        mBleCharacteristic.setValue(mStrSendNum);
        mBleGatt.writeCharacteristic(mBleCharacteristic);
      }
    }
  }
  @Override
  protected void onDestroy()
  {
    // 画面遷移時は通信を切断する.
    mIsBluetoothEnable = false;
    if(mBleGatt != null) {
      mBleGatt.close();
      mBleGatt = null;
    }
    super.onDestroy();
  }
}

修正したところ

  • Peripheralとの接続時にNotifyリクエストを送りますが、その際指定するキャラクタリスティックの設定用UUID(CHARACTERISTIC_CONFIG_UUID)が違っていました。
    当時なぜ接続できていたのかは不明ですが、「00002902-0000-1000-8000-00805f9b34fb」を指定しないとリクエスト送信で使用する「BluetoothGattDescriptor」がNullになり、エラーが発生します。

  • onCreateで、デバイスBluetoothがOnになっていない場合、Intentで変更するよう求めるポップアップを表示するのですが、設定変更後に呼ばれる「onActivityResult」でスキャン開始の条件が逆になっていました。
    これは完全に見逃していたので、言い訳の余地はないですね汗

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  // Intentでユーザーがボタンを押したら実行.
  super.onActivityResult(requestCode, resultCode, data);
  switch (requestCode) {
    case REQUEST_ENABLE_BT:
      if ((mBleAdapter != null)
              || (mBleAdapter.isEnabled())) {
        // BLEが使用可能ならスキャン開始.
        this.scanNewDevice();
      }
      break;
  }
}

ver5.0〜のために追加したところ

  • 接続先のデバイスを探すためのスキャン(startScan)の引数として「LeScanCallback」型の「mScanCallback」を「ScanCallback」にキャストして使用しようとしていたのですが、
    少なくとも「(ScanCallback)mScanCallback」とするだけではダメなようで、エラーが発生しました。

※2017.11.25更新
コメントにてご指摘いただいた内容を反映しました。

■Before
result.getDevice().connectGatt(getApplicationContext(), false, mGattCallback);

■After
mBleGatt = result.getDevice().connectGatt(getApplicationContext(), false, mGattCallback);

jさん、ありがとうございます(..)_

@TargetApi(SDKVER_LOLLIPOP)
private void startScanByBleScanner()
{
  mBleScanner = mBleAdapter.getBluetoothLeScanner();
  // デバイスの検出.

  mBleScanner.startScan(new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
      super.onScanResult(callbackType, result);
      // スキャン中に見つかったデバイスに接続を試みる.第三引数には接続後に呼ばれるBluetoothGattCallbackを指定する.
      mBleGatt = result.getDevice().connectGatt(getApplicationContext(), false, mGattCallback);
    }
    @Override
    public void onScanFailed(int intErrorCode)
    {
      super.onScanFailed(intErrorCode);
    }
  });
}

バイスが見つかったあとはGATTの接続処理で、ここからはver.4.4の時と同じように実行できます。

課題

これでver.5.0以上でもPeripheralデバイスへの番号の送信、Peripheralデバイスで値を更新した時の通知を受けられるようになります。

が、一度Backボタンなどで前の画面に戻り、もう一度開くとスキャン開始後コールバックが受けられませんでした。
エラーが発生しているわけではなさそうで、画面の遷移前のデータが残っているのでは?と考えていますが、「BluetoothGatt」をClose()してNullを入れるだけではダメなようです。

これについてはもう少し調べてみたいと思います。

おわりに

4.4までの一部コードがDeprecatedになり、新しくコードが追加された!という前印象の割にはCentral側はそんなに変更なく5.0にも対応出来ました
(シンプルになるならもっと変わってくれても良かったとは思いますが)。

次はPeripheral側にいきたいのですが、少しググってみたところNexus7で動かすのは厳しいという話が…。

Androidもバージョン5.1.1になったので、もしかしたら…という淡い期待もありますが、
次回以降しれっと別の話になったら、あっ(察し) という感じで見逃してくださいw

やっぱりBLEを使う部分に関してはiOSに遅れをとっている印象がぬぐい去れないですね。。

まぁ面倒な手続きをしなくてもBluetooth3以下が使えるとか、AndroidバイスにBLEを使える他の機器を接続して利用できるとか、という別の解決策があるので需要がiOS程はない、ということなのかもしれませんね。

あとこちらは動作確認はしていないのですが、BluetoothデベロッパーサイトでBluetooth Smartを利用するコードが提供されており、
Androidのコードは「android.bluetooth.le」は使っていない(ように見える)のにPeripheralも実装されているようでした。

こちらを試してみるのも良いかもしれませんね。

参考

Bluetooth Low Energy

Backボタン