Android 4.3(API レベル 18)で、セントラル役割の Bluetooth Low Energy(BLE)の組み込みプラットフォーム サポートが導入され、デバイスの検出、サービスの検索、および情報の送信に使用できるアプリ向けの API が提供されるようになりました。
主な使用例は次のとおりです。
- 近くにあるデバイス間で少量のデータを転送する。
- Google Beacon のような近接センサーとやり取りして、ユーザーが現在の場所に基づいてカスタマイズされた体験を得ることができるようにする。
Bluetooth Low Energy(BLE)は、クラシック Bluetooth と比較して、消費電力を大幅に抑えた設計となっています。そのおかげで Android アプリが、近接センサー、心拍数モニター、フィットネス端末など電力要件が厳しい BLE 端末と通信することが可能になっています。
注意:ユーザーが、BLE を使って自分の端末を別の端末とペア設定すると、2 台の端末間でやり取りされるデータは、ユーザーの端末上のすべてのアプリからアクセス可能になります。
そのため、ユーザーのアプリが機密性の高いデータを取り込む場合は、アプリ層でセキュリティを実装して、そのデータのプライバシーを保護する必要があります。
主な用語とコンセプト
主な BLE 用語とコンセプトを簡潔に説明します。
- ジェネリック アトリビュート プロファイル(GATT)—GATT プロファイルは、BLE リンク上で「アトリビュート」と呼ぶ短いデータを送受信するための一般的な仕様です。現在使用されている LE アプリ プロファイルはすべて GATT に基づいています。
- LE 端末用のプロファイルは、その多くを Bluetooth SIG が定義しています。プロファイルとは、特定のアプリケーションで端末がどのように動作するかを定める仕様のことです。1 台の端末に複数のプロファイルを実装することも可能です。例えば、ある端末に心拍モニターとバッテリー レベル検出装置を搭載することができます。
- アトリビュート プロトコル(ATT)—GATT はアトリビュート プロトコル(ATT)の上に構築されています。この関係は GATT/ATT とも表されます。ATT は BLE 端末上での実行に最適化されています。そのため、ATT では最小限のバイト数が使用されています。各アトリビュートは UUID(Universally Unique Identifier)によって一意に識別されます。UUIDは、情報を一意に識別するために使用される標準化された 128 ビット形式の文字列 ID です。ATT によって送信されるアトリビュートは、キャラクタリスティックとサービスとして形式化されています。
- キャラクタリスティック—1 つのキャラクタリスティックには、1 つの値とそのキャラクタリスティックの値を記述する 0~n 個の記述子が含まれます。キャラクタリスティックは型(タイプ)のようなもので、クラスに似ています。
- 記述子—記述子はキャラクタリスティック値を記述するために定義されるアトリビュートです。例えば、記述子を使って、人間が読める記述、キャラクタリスティックの値の許容範囲、またはキャラクタリスティックの値に固有の測定単位を指定することができます。
- サービス—複数のキャラクタリスティックが集まってサービスを構成します。例えば、「心拍モニター」というサービスがあり、このサービスに「心拍数測定」など複数のキャラクタリスティックが含まれているというような具合です。現在使用されている GATT ベースのプロファイルとサービスのリストは bluetooth.org で公開されています。
役割と責任
Android 端末が BLE 端末とやり取りするときには、次のような役割と責任が適用されます。
- 「セントラル」または「ペリフェラル」。これは BLE 接続自体に適用されます。セントラル役割の端末はスキャンしてアドバタイズメントを検索し、ペリフェラル役割の端末はアドバタイズメントを行います。
- 「GATT サーバー」または「GATT クライアント」。この関係により、2 台の端末が接続を確立した後、相互に対話する方法が決まります。
役割の違いを理解するために、Android スマートフォンと BLE 端末である活動トラッカーの関係を考えてみましょう。スマートフォンはセントラルの役割をサポートし、活動トラッカーはペリフェラルの役割をサポートします(BLE 接続を確立するにはセントラルとペリフェラルが 1 つずつ必要です、ペリフェラルだけをサポートする端末 2 台も、セントラルだけをサポートする端末 2 台もお互いに通信できません)。
Android スマートフォンと活動トラッカーの接続が確立されると、これら 2 台の端末は互いに GATT メタデータの転送を開始します。転送するデータの種類に応じて、どちらがサーバーとして機能するかが決まります。たとえば、活動トラッカーがセンサーデータをスマートフォンに報告する場合は、当然ながら活動トラッカーがサーバーとして機能します。他方、活動トラッカーがスマートフォンからアップデートを受信する場合は、スマートフォンがサーバーとして機能します。
上述の例では、Android 端末で実行される Android アプリが GATT クライアントです。Android アプリは GATT サーバーつまり心拍プロファイルをサポートする BLE 心拍モニターからデータを取得します。また、その逆に Android アプリが GATT サーバーの役割を果たすように設計することもできます。詳細については、BluetoothGattServer
をご覧ください。
BLE パーミッション
アプリで Bluetooth 機能を使用するには、Bluetooth パーミッション BLUETOOTH
を宣言する必要があります。このパーミッションは、接続のリクエスト、接続の受け入れ、データの転送など、Bluetooth 通信を実行するために必要です。
また、LE ビーコンは位置情報に関連付けられていることが多いため、ACCESS_FINE_LOCATION
パーミッションも宣言する必要があります。このパーミッションがないと、スキャン結果を取得できません。
注:アプリが Android 9(API レベル 28)以下をターゲットにしている場合、ACCESS_FINE_LOCATION
パーミッションの代わりに、ACCESS_COARSE_LOCATION
パーミッションを宣言できます。
アプリで端末の検出を開始したり Bluetooth の設定を操作する場合は、BLUETOOTH_ADMIN
パーミッションも宣言する必要があります。注:BLUETOOTH_ADMIN
パーミッションを使用する場合は、BLUETOOTH
パーミッションも必要です。
アプリのマニフェスト ファイルでこれらのパーミッションを宣言してください。次に例を示します。
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <!-- If your app targets Android 9 or lower, you can declare ACCESS_COARSE_LOCATION instead. --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
アプリケーションが BLE 対応端末でしか使用できないことを宣言する場合は、アプリのマニフェストに以下を含めてください。
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
しかし、BLE をサポートしていない端末でアプリを利用できるようにする場合は、アプリのマニフェストに上記の要素を含め、かつ required="false"
を設定する必要があります。実行時になったら、PackageManager.hasSystemFeature()
を使用して、BLE を使用可能かどうかを判断できます。
Kotlin
private fun PackageManager.missingSystemFeature(name: String): Boolean = !hasSystemFeature(name) ... packageManager.takeIf { it.missingSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) }?.also { Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show() finish() }
Java
// Use this check to determine whether BLE is supported on the device. Then // you can selectively disable BLE-related features. if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show(); finish(); }
BLE のセットアップ
アプリで BLE 通信を行う前に、その端末で BLE がサポートされているか、サポートされている場合は有効になっているかどうかを確認する必要があります。この確認が必要なのは、<uses-feature.../>
が false に設定されている場合のみであることに注意してください。
BLE がサポートされていない場合は、すべての BLE 機能を定められた手順で無効にする必要があります。BLE がサポートされているものの無効にされている場合、ユーザーがアプリから離れることなく Bluetooth を有効にするようにリクエストすることができます。この設定は、BluetoothAdapter
を使用して、2 つのステップで行えます。
BluetoothAdapter
を取得するすべての Bluetooth のアクティビティには
BluetoothAdapter
が必要です。BluetoothAdapter
は、端末独自の Bluetooth アダプタ(Bluetooth 無線通信)を表します。システム全体には 1 つの Bluetooth アダプタが存在し、アプリはこのオブジェクトを使用して Bluetooth アダプタとやり取りができます。次のスニペットは、アダプタを取得する方法を示しています。このアプローチでは、getSystemService()
を使用してBluetoothManager
のインスタンスを取得し、次にこのインスタンスを使用してアダプタを取得します。BluetoothManager
は Android 4.3 (API レベル 18)で導入されています。Kotlin
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) { val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager bluetoothManager.adapter }
Java
private BluetoothAdapter bluetoothAdapter; ... // Initializes Bluetooth adapter. final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); bluetoothAdapter = bluetoothManager.getAdapter();
- Bluetooth を有効化する
次に、Bluetooth が有効であることを確認する必要があります。
isEnabled()
を呼び出して、Bluetooth が現在有効かどうかを確認します。このメソッドが false を返したら、Bluetooth は無効です。下のスニペットは Bluetooth が有効かどうかを確認します。有効でない場合、スニペットはエラーを表示し、[設定] に移動して Bluetooth を有効にするようユーザーを促します。Kotlin
private val BluetoothAdapter.isDisabled: Boolean get() = !isEnabled ... // Ensures Bluetooth is available on the device and it is enabled. If not, // displays a dialog requesting user permission to enable Bluetooth. bluetoothAdapter?.takeIf { it.isDisabled }?.apply { val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) }
Java
// Ensures Bluetooth is available on the device and it is enabled. If not, // displays a dialog requesting user permission to enable Bluetooth. if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }
注:
startActivityForResult(android.content.Intent, int)
に渡されるREQUEST_ENABLE_BT
定数は、ローカルで定義された整数(1 以上)です。システムはこの値をrequestCode
パラメータとしてonActivityResult(int, int, android.content.Intent)
の実装に渡します。
BLE 端末の検出
BLE デバイスを検索するには、startLeScan()
メソッドを使用します。このメソッドは BluetoothAdapter.LeScanCallback
をパラメータとして取ります。スキャン結果が返される手段となるため、このコールバックの実装は必要です。スキャンはバッテリへの負荷が高いため、次のガイドラインに従う必要があります。
- 目的のデバイスが見つかったら、すぐにスキャンを停止します。
- スキャンを際限なく繰り返さず、スキャンに時間制限を設定します。接続していたデバイスが使用可能範囲外に移動しているかもしれず、スキャンを長時間継続するとバッテリーが消耗します。
次のスニペットは、スキャンを開始および停止する方法を示しています。
Kotlin
private const val SCAN_PERIOD: Long = 10000 /** * Activity for scanning and displaying available BLE devices. */ class DeviceScanActivity( private val bluetoothAdapter: BluetoothAdapter, private val handler: Handler ) : ListActivity() { private var mScanning: Boolean = false private fun scanLeDevice(enable: Boolean) { when (enable) { true -> { // Stops scanning after a pre-defined scan period. handler.postDelayed({ mScanning = false bluetoothAdapter.stopLeScan(leScanCallback) }, SCAN_PERIOD) mScanning = true bluetoothAdapter.startLeScan(leScanCallback) } else -> { mScanning = false bluetoothAdapter.stopLeScan(leScanCallback) } } } }
Java
/** * Activity for scanning and displaying available BLE devices. */ public class DeviceScanActivity extends ListActivity { private BluetoothAdapter bluetoothAdapter; private boolean mScanning; private Handler handler; // Stops scanning after 10 seconds. private static final long SCAN_PERIOD = 10000; ... private void scanLeDevice(final boolean enable) { if (enable) { // Stops scanning after a pre-defined scan period. handler.postDelayed(new Runnable() { @Override public void run() { mScanning = false; bluetoothAdapter.stopLeScan(leScanCallback); } }, SCAN_PERIOD); mScanning = true; bluetoothAdapter.startLeScan(leScanCallback); } else { mScanning = false; bluetoothAdapter.stopLeScan(leScanCallback); } ... } ... }
特定のタイプのペリフェラルのみをスキャンする場合は、代わりに startLeScan(UUID[], BluetoothAdapter.LeScanCallback)
を呼び出すことができます。この際、アプリがサポートする GATT サービスを指定する一連の UUID
オブジェクトを指定します。
ここで BluetoothAdapter.LeScanCallback
が実装されていますが、これは BLE スキャン結果を提供するのに使用されるインターフェースです。
Kotlin
val leDeviceListAdapter: LeDeviceListAdapter = ... private val leScanCallback = BluetoothAdapter.LeScanCallback { device, rssi, scanRecord -> runOnUiThread { leDeviceListAdapter.addDevice(device) leDeviceListAdapter.notifyDataSetChanged() } }
Java
private LeDeviceListAdapter leDeviceListAdapter; ... // Device scan callback. private BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { runOnUiThread(new Runnable() { @Override public void run() { leDeviceListAdapter.addDevice(device); leDeviceListAdapter.notifyDataSetChanged(); } }); } };
注:Bluetooth で説明されているように、Bluetooth LE 端末のスキャンまたはクラシック Bluetooth 端末のスキャンのいずれかしか実行できません。Bluetooth LE とクラシック Bluetooth の両方の端末を同時にスキャンすることはできません。
GATT サーバーへの接続
BLE 端末と対話するための最初のステップは、BLE 端末に接続すること、厳密に言うと、BLE 端末上の GATT サーバーに接続することです。BLE 端末上の GATT サーバーに接続するには、connectGatt()
メソッドを使用します。このメソッドは、Context
オブジェクト、autoConnect
(BLE 端末が使用可能になったらすぐに自動的に接続するかどうかを指定するブール値)、および BluetoothGattCallback
への参照の 3 つのパラメータを受け取ります。
Kotlin
var bluetoothGatt: BluetoothGatt? = null ... bluetoothGatt = device.connectGatt(this, false, gattCallback)
Java
bluetoothGatt = device.connectGatt(this, false, gattCallback);
このメソッドは BLE 端末がホストする GATT サーバーに接続して BluetoothGatt
インスタンスを返すので、このインスタンスを使用して GATT クライアントの操作を実行することができます。呼び出し元(Android アプリ)は GATT クライアントです。BluetoothGattCallback
は、接続状態や他の GATT クライアント操作などの結果をクライアントに伝えるために使用されます。
この例では、BLE アプリは、接続、データの表示、および端末がサポートする GATT サービスとキャラクタリスティックの表示を行うアクティビティ(DeviceControlActivity
)を提供しています。このアクティビティはユーザー入力に従って BluetoothLeService
という Service
と通信し、このサービスは Android BLE API を介して BLE デバイスと対話します。
Kotlin
private val TAG = BluetoothLeService::class.java.simpleName private const val STATE_DISCONNECTED = 0 private const val STATE_CONNECTING = 1 private const val STATE_CONNECTED = 2 const val ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED" const val ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED" const val ACTION_GATT_SERVICES_DISCOVERED = "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED" const val ACTION_DATA_AVAILABLE = "com.example.bluetooth.le.ACTION_DATA_AVAILABLE" const val EXTRA_DATA = "com.example.bluetooth.le.EXTRA_DATA" val UUID_HEART_RATE_MEASUREMENT = UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT) // A service that interacts with the BLE device via the Android BLE API. class BluetoothLeService(private var bluetoothGatt: BluetoothGatt?) : Service() { private var connectionState = STATE_DISCONNECTED // Various callback methods defined by the BLE API. private val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange( gatt: BluetoothGatt, status: Int, newState: Int ) { val intentAction: String when (newState) { BluetoothProfile.STATE_CONNECTED -> { intentAction = ACTION_GATT_CONNECTED connectionState = STATE_CONNECTED broadcastUpdate(intentAction) Log.i(TAG, "Connected to GATT server.") Log.i(TAG, "Attempting to start service discovery: " + bluetoothGatt?.discoverServices()) } BluetoothProfile.STATE_DISCONNECTED -> { intentAction = ACTION_GATT_DISCONNECTED connectionState = STATE_DISCONNECTED Log.i(TAG, "Disconnected from GATT server.") broadcastUpdate(intentAction) } } } // New services discovered override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { when (status) { BluetoothGatt.GATT_SUCCESS -> broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED) else -> Log.w(TAG, "onServicesDiscovered received: $status") } } // Result of a characteristic read operation override fun onCharacteristicRead( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int ) { when (status) { BluetoothGatt.GATT_SUCCESS -> { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic) } } } } }
Java
// A service that interacts with the BLE device via the Android BLE API. public class BluetoothLeService extends Service { private final static String TAG = BluetoothLeService.class.getSimpleName(); private BluetoothManager bluetoothManager; private BluetoothAdapter bluetoothAdapter; private String bluetoothDeviceAddress; private BluetoothGatt bluetoothGatt; private int connectionState = STATE_DISCONNECTED; private static final int STATE_DISCONNECTED = 0; private static final int STATE_CONNECTING = 1; private static final int STATE_CONNECTED = 2; public final static String ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED"; public final static String ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED"; public final static String ACTION_GATT_SERVICES_DISCOVERED = "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED"; public final static String ACTION_DATA_AVAILABLE = "com.example.bluetooth.le.ACTION_DATA_AVAILABLE"; public final static String EXTRA_DATA = "com.example.bluetooth.le.EXTRA_DATA"; public final static UUID UUID_HEART_RATE_MEASUREMENT = UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT); // Various callback methods defined by the BLE API. private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { String intentAction; if (newState == BluetoothProfile.STATE_CONNECTED) { intentAction = ACTION_GATT_CONNECTED; connectionState = STATE_CONNECTED; broadcastUpdate(intentAction); Log.i(TAG, "Connected to GATT server."); Log.i(TAG, "Attempting to start service discovery:" + bluetoothGatt.discoverServices()); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { intentAction = ACTION_GATT_DISCONNECTED; connectionState = STATE_DISCONNECTED; Log.i(TAG, "Disconnected from GATT server."); broadcastUpdate(intentAction); } } @Override // New services discovered public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED); } else { Log.w(TAG, "onServicesDiscovered received: " + status); } } @Override // Result of a characteristic read operation public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); } } ... }; ... }
特定のコールバックがトリガーされると、そのコールバックは適切な broadcastUpdate()
ヘルパー メソッドを呼び出して、そのメソッドにアクションを渡します。このセクションで説明されているデータ解析は、Bluetooth Heart Rate Measurement プロファイル仕様に従って実行されることに注意してください。
Kotlin
private fun broadcastUpdate(action: String) { val intent = Intent(action) sendBroadcast(intent) } private fun broadcastUpdate(action: String, characteristic: BluetoothGattCharacteristic) { val intent = Intent(action) // This is special handling for the Heart Rate Measurement profile. Data // parsing is carried out as per profile specifications. when (characteristic.uuid) { UUID_HEART_RATE_MEASUREMENT -> { val flag = characteristic.properties val format = when (flag and 0x01) { 0x01 -> { Log.d(TAG, "Heart rate format UINT16.") BluetoothGattCharacteristic.FORMAT_UINT16 } else -> { Log.d(TAG, "Heart rate format UINT8.") BluetoothGattCharacteristic.FORMAT_UINT8 } } val heartRate = characteristic.getIntValue(format, 1) Log.d(TAG, String.format("Received heart rate: %d", heartRate)) intent.putExtra(EXTRA_DATA, (heartRate).toString()) } else -> { // For all other profiles, writes the data formatted in HEX. val data: ByteArray? = characteristic.value if (data?.isNotEmpty() == true) { val hexString: String = data.joinToString(separator = " ") { String.format("%02X", it) } intent.putExtra(EXTRA_DATA, "$data\n$hexString") } } } sendBroadcast(intent) }
Java
private void broadcastUpdate(final String action) { final Intent intent = new Intent(action); sendBroadcast(intent); } private void broadcastUpdate(final String action, final BluetoothGattCharacteristic characteristic) { final Intent intent = new Intent(action); // This is special handling for the Heart Rate Measurement profile. Data // parsing is carried out as per profile specifications. if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { int flag = characteristic.getProperties(); int format = -1; if ((flag & 0x01) != 0) { format = BluetoothGattCharacteristic.FORMAT_UINT16; Log.d(TAG, "Heart rate format UINT16."); } else { format = BluetoothGattCharacteristic.FORMAT_UINT8; Log.d(TAG, "Heart rate format UINT8."); } final int heartRate = characteristic.getIntValue(format, 1); Log.d(TAG, String.format("Received heart rate: %d", heartRate)); intent.putExtra(EXTRA_DATA, String.valueOf(heartRate)); } else { // For all other profiles, writes the data formatted in HEX. final byte[] data = characteristic.getValue(); if (data != null && data.length > 0) { final StringBuilder stringBuilder = new StringBuilder(data.length); for(byte byteChar : data) stringBuilder.append(String.format("%02X ", byteChar)); intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString()); } } sendBroadcast(intent); }
DeviceControlActivity
に戻ると、これらのイベントは BroadcastReceiver
によって処理されます。
Kotlin
// Handles various events fired by the Service. // ACTION_GATT_CONNECTED: connected to a GATT server. // ACTION_GATT_DISCONNECTED: disconnected from a GATT server. // ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services. // ACTION_DATA_AVAILABLE: received data from the device. This can be a // result of read or notification operations. private val gattUpdateReceiver = object : BroadcastReceiver() { private lateinit var bluetoothLeService: BluetoothLeService override fun onReceive(context: Context, intent: Intent) { val action = intent.action when (action){ ACTION_GATT_CONNECTED -> { connected = true updateConnectionState(R.string.connected) (context as? Activity)?.invalidateOptionsMenu() } ACTION_GATT_DISCONNECTED -> { connected = false updateConnectionState(R.string.disconnected) (context as? Activity)?.invalidateOptionsMenu() clearUI() } ACTION_GATT_SERVICES_DISCOVERED -> { // Show all the supported services and characteristics on the // user interface. displayGattServices(bluetoothLeService.getSupportedGattServices()) } ACTION_DATA_AVAILABLE -> { displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA)) } } } }
Java
// Handles various events fired by the Service. // ACTION_GATT_CONNECTED: connected to a GATT server. // ACTION_GATT_DISCONNECTED: disconnected from a GATT server. // ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services. // ACTION_DATA_AVAILABLE: received data from the device. This can be a // result of read or notification operations. private final BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) { connected = true; updateConnectionState(R.string.connected); invalidateOptionsMenu(); } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) { connected = false; updateConnectionState(R.string.disconnected); invalidateOptionsMenu(); clearUI(); } else if (BluetoothLeService. ACTION_GATT_SERVICES_DISCOVERED.equals(action)) { // Show all the supported services and characteristics on the // user interface. displayGattServices(bluetoothLeService.getSupportedGattServices()); } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) { displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA)); } } };
BLE 属性の読み取り
Android アプリは、GATT サーバーに接続してサービスを検出すると、属性の読み取りと書き込みをすることができます(サポートされている場合)。たとえば、次のスニペットはサーバーのサービスとキャラクタリスティックを繰り返し読み取って、それらを UI に表示します。
Kotlin
class DeviceControlActivity : Activity() { // Demonstrates how to iterate through the supported GATT // Services/Characteristics. // In this sample, we populate the data structure that is bound to the // ExpandableListView on the UI. private fun displayGattServices(gattServices: List<BluetoothGattService>?) { if (gattServices == null) return var uuid: String? val unknownServiceString: String = resources.getString(R.string.unknown_service) val unknownCharaString: String = resources.getString(R.string.unknown_characteristic) val gattServiceData: MutableList<HashMap<String, String>> = mutableListOf() val gattCharacteristicData: MutableList<ArrayList<HashMap<String, String>>> = mutableListOf() mGattCharacteristics = mutableListOf() // Loops through available GATT Services. gattServices.forEach { gattService -> val currentServiceData = HashMap<String, String>() uuid = gattService.uuid.toString() currentServiceData[LIST_NAME] = SampleGattAttributes.lookup(uuid, unknownServiceString) currentServiceData[LIST_UUID] = uuid gattServiceData += currentServiceData val gattCharacteristicGroupData: ArrayList<HashMap<String, String>> = arrayListOf() val gattCharacteristics = gattService.characteristics val charas: MutableList<BluetoothGattCharacteristic> = mutableListOf() // Loops through available Characteristics. gattCharacteristics.forEach { gattCharacteristic -> charas += gattCharacteristic val currentCharaData: HashMap<String, String> = hashMapOf() uuid = gattCharacteristic.uuid.toString() currentCharaData[LIST_NAME] = SampleGattAttributes.lookup(uuid, unknownCharaString) currentCharaData[LIST_UUID] = uuid gattCharacteristicGroupData += currentCharaData } mGattCharacteristics += charas gattCharacteristicData += gattCharacteristicGroupData } } }
Java
public class DeviceControlActivity extends Activity { ... // Demonstrates how to iterate through the supported GATT // Services/Characteristics. // In this sample, we populate the data structure that is bound to the // ExpandableListView on the UI. private void displayGattServices(List<BluetoothGattService> gattServices) { if (gattServices == null) return; String uuid = null; String unknownServiceString = getResources(). getString(R.string.unknown_service); String unknownCharaString = getResources(). getString(R.string.unknown_characteristic); ArrayList<HashMap<String, String>> gattServiceData = new ArrayList<HashMap<String, String>>(); ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData = new ArrayList<ArrayList<HashMap<String, String>>>(); mGattCharacteristics = new ArrayList<ArrayList<BluetoothGattCharacteristic>>(); // Loops through available GATT Services. for (BluetoothGattService gattService : gattServices) { HashMap<String, String> currentServiceData = new HashMap<String, String>(); uuid = gattService.getUuid().toString(); currentServiceData.put( LIST_NAME, SampleGattAttributes. lookup(uuid, unknownServiceString)); currentServiceData.put(LIST_UUID, uuid); gattServiceData.add(currentServiceData); ArrayList<HashMap<String, String>> gattCharacteristicGroupData = new ArrayList<HashMap<String, String>>(); List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics(); ArrayList<BluetoothGattCharacteristic> charas = new ArrayList<BluetoothGattCharacteristic>(); // Loops through available Characteristics. for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { charas.add(gattCharacteristic); HashMap<String, String> currentCharaData = new HashMap<String, String>(); uuid = gattCharacteristic.getUuid().toString(); currentCharaData.put( LIST_NAME, SampleGattAttributes.lookup(uuid, unknownCharaString)); currentCharaData.put(LIST_UUID, uuid); gattCharacteristicGroupData.add(currentCharaData); } mGattCharacteristics.add(charas); gattCharacteristicData.add(gattCharacteristicGroupData); } ... } ... }
GATT 通知の受信
一般に、端末の特定のキャラクタリスティックが変化すると、BLE アプリに通知されます。次のスニペットは、setCharacteristicNotification()
メソッドを使用してキャラクタリスティックに関する通知を設定する方法を示しています。
Kotlin
lateinit var bluetoothGatt: BluetoothGatt lateinit var characteristic: BluetoothGattCharacteristic var enabled: Boolean = true ... bluetoothGatt.setCharacteristicNotification(characteristic, enabled) val uuid: UUID = UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG) val descriptor = characteristic.getDescriptor(uuid).apply { value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE } bluetoothGatt.writeDescriptor(descriptor)
Java
private BluetoothGatt bluetoothGatt; BluetoothGattCharacteristic characteristic; boolean enabled; ... bluetoothGatt.setCharacteristicNotification(characteristic, enabled); ... BluetoothGattDescriptor descriptor = characteristic.getDescriptor( UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); bluetoothGatt.writeDescriptor(descriptor);
あるキャラクタリスティックに対する通知が有効化されると、リモート端末でそのキャラクタリスティックに変化が生じた場合に、onCharacteristicChanged()
コールバックがトリガーされます。
Kotlin
// Characteristic notification override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic ) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic) }
Java
@Override // Characteristic notification public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); }
クライアント アプリの終了
アプリが BLE デバイスの使用を終了したなら、close()
を呼び出して、システムが適切にリソースを解放できるようにする必要があります。
Kotlin
fun close() { bluetoothGatt?.close() bluetoothGatt = null }
Java
public void close() { if (bluetoothGatt == null) { return; } bluetoothGatt.close(); bluetoothGatt = null; }