GATT サーバーへの接続

BLE デバイスを操作する最初の手順は、デバイスに接続することです。具体的には、デバイス上の GATT サーバーに接続します。BLE デバイスの GATT サーバーに接続するには、connectGatt() メソッドを使用します。このメソッドは、Context オブジェクト、autoConnect(BLE デバイスが利用可能になったらすぐに自動的に接続するかどうかを示すブール値)、BluetoothGattCallback への参照の 3 つのパラメータを受け取ります。

var bluetoothGatt: BluetoothGatt? = null
// ...
bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback)

これにより、BLE デバイスでホストされている GATT サーバーに接続し、BluetoothGatt インスタンスを返します。このインスタンスを使用して、GATT クライアント オペレーションを実行できます。呼び出し元(Android アプリ)は GATT クライアントです。BluetoothGattCallback は、接続ステータスなどの結果や、それ以降の GATT クライアント オペレーションをクライアントに配信するために使用されます。

バインドされたサービスを設定する

次の例では、BLE アプリは、Bluetooth デバイスに接続し、デバイスデータを表示し、デバイスでサポートされている GATT サービスと特性を表示するアクティビティ(DeviceControlActivity)を提供します。ユーザー入力に基づいて、このアクティビティは BluetoothLeService という Service と通信し、BLE API を介して BLE デバイスとやり取りします。通信はバインドされたサービスを使用して行われます。これにより、アクティビティは BluetoothLeService に接続し、関数を呼び出してデバイスに接続できます。BluetoothLeService には、アクティビティのサービスへのアクセスを提供する Binder の実装が必要です。

class BluetoothLeService : Service() {

    private val binder = LocalBinder()

    override fun onBind(intent: Intent): IBinder? {
        return binder
    }

    inner class LocalBinder : Binder() {
        fun getService(): BluetoothLeService {
            return this@BluetoothLeService
        }
    }
}

アクティビティは、bindService() を使用してサービスを開始できます。このとき、Intent を渡してサービスを開始し、ServiceConnection 実装を渡して接続イベントと切断イベントをリッスンし、フラグを渡して追加の接続オプションを指定します。

class DeviceControlActivity : AppCompatActivity() {

    private var bluetoothService: BluetoothLeService? = null
    // Code to manage Service lifecycle.
    private val serviceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(
            componentName: ComponentName,
            service: IBinder
        ) {
            bluetoothService = (service as LocalBinder).getService()
            bluetoothService?.let { bluetooth ->
                // call functions on service to check connection and connect to devices
            }
        }

        override fun onServiceDisconnected(componentName: ComponentName) {
            bluetoothService = null
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.gatt_services_characteristics)

        val gattServiceIntent = Intent(this, BluetoothLeService::class.java)
        bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE)
    }
}

BluetoothAdapter を設定する

サービスがバインドされたら、BluetoothAdapter にアクセスする必要があります。アダプターがデバイスで利用可能であることを確認する必要があります。BluetoothAdapter について詳しくは、Bluetooth を設定するをご覧ください。次の例では、この設定コードを initialize() 関数でラップし、成功を示す Boolean 値を返しています。

private const val TAG = "BluetoothLeService"

class BluetoothLeService : Service() {

    private var bluetoothAdapter: BluetoothAdapter? = null

    fun initialize(): Boolean {
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
        if (bluetoothAdapter == null) {
            Log.e(TAG, "Unable to obtain a BluetoothAdapter.")
            return false
        }
        return true
    }

    // ...
}

アクティビティは、ServiceConnection 実装内でこの関数を呼び出します。initialize() 関数から返された false 値の処理は、アプリによって異なります。現在のデバイスが Bluetooth オペレーションをサポートしていないことを示すエラー メッセージをユーザーに表示するか、Bluetooth の動作を必要とする機能を無効にすることができます。次の例では、アクティビティで finish() が呼び出され、ユーザーが前の画面に戻ります。

class DeviceControlActivity : AppCompatActivity() {

    // Code to manage Service lifecycle.
    private val serviceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(
            componentName: ComponentName,
            service: IBinder
        ) {
            bluetoothService = (service as LocalBinder).getService()
            bluetoothService?.let { bluetooth ->
                if (!bluetooth.initialize()) {
                    Log.e(TAG, "Unable to initialize Bluetooth")
                    finish()
                }
                // perform device connection
            }
        }

        override fun onServiceDisconnected(componentName: ComponentName) {
            bluetoothService = null
        }
    }

    // ...
}

デバイスに接続

BluetoothLeService インスタンスが初期化されると、BLE デバイスに接続できます。アクティビティは、接続を開始できるように、デバイスのアドレスをサービスに送信する必要があります。サービスはまず、BluetoothAdaptergetRemoteDevice() を呼び出してデバイスにアクセスします。アダプターがそのアドレスのデバイスを見つけられない場合、getRemoteDevice()IllegalArgumentException をスローします。

fun connect(address: String): Boolean {
    bluetoothAdapter?.let { adapter ->
        try {
            val device = adapter.getRemoteDevice(address)
        } catch (exception: IllegalArgumentException) {
            Log.w(TAG, "Device not found with provided address.")
            return false
        }
        // connect to the GATT server on the device
        return true
    } ?: run {
        Log.w(TAG, "BluetoothAdapter not initialized")
        return false
    }
}

DeviceControlActivity は、サービスが初期化されると、この connect() 関数を呼び出します。アクティビティは BLE デバイスのアドレスを渡す必要があります。次の例では、デバイスのアドレスはインテント エクストラとしてアクティビティに渡されます。

// Code to manage Service lifecycle.
private val serviceConnection: ServiceConnection = object : ServiceConnection {
    override fun onServiceConnected(
        componentName: ComponentName,
        service: IBinder
    ) {
        bluetoothService = (service as LocalBinder).getService()
        bluetoothService?.let { bluetooth ->
            if (!bluetooth.initialize()) {
                Log.e(TAG, "Unable to initialize Bluetooth")
                finish()
            }
            // perform device connection
            deviceAddress?.let { bluetooth.connect(it) }
        }
    }

    override fun onServiceDisconnected(componentName: ComponentName) {
        bluetoothService = null
    }
}

GATT コールバックを宣言する

アクティビティが接続するデバイスをサービスに伝えて、サービスがデバイスに接続したら、サービスは BLE デバイスの GATT サーバーに接続する必要があります。この接続では、接続状態、サービス検出、特性の読み取り、特性の通知に関する通知を受け取るために BluetoothGattCallback が必要です。

このトピックでは、接続状態の通知に焦点を当てて説明します。サービス検出、特性の読み取り、特性通知のリクエストを行う方法については、BLE データを転送するをご覧ください。

デバイスの GATT サーバーへの接続が変更されると、onConnectionStateChange() 関数がトリガーされます。次の例では、コールバックは Service クラスで定義されているため、サービスが接続されると BluetoothDevice で使用できます。

private val bluetoothGattCallback = object : BluetoothGattCallback() {
    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            // successfully connected to the GATT Server
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            // disconnected from the GATT Server
        }
    }
}

GATT サービスに接続する

BluetoothGattCallback が宣言されると、サービスは connect() 関数の BluetoothDevice オブジェクトを使用して、デバイスの GATT サービスに接続できます。

connectGatt() 関数が使用されます。これには、Context オブジェクト、autoConnect ブール値フラグ、BluetoothGattCallback が必要です。この例では、アプリは BLE デバイスに直接接続しているため、autoConnectfalse が渡されます。

BluetoothGatt プロパティも追加されます。これにより、サービスは不要になったときに接続を閉じることができます。

class BluetoothLeService : Service() {

    // ...
    private var bluetoothGatt: BluetoothGatt? = null

    // ...
    fun connect(address: String): Boolean {
        bluetoothAdapter?.let { adapter ->
            try {
                val device = adapter.getRemoteDevice(address)
                // connect to the GATT server on the device
                bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback)
                return true
            } catch (exception: IllegalArgumentException) {
                Log.w(TAG, "Device not found with provided address.  Unable to connect.")
                return false
            }
        } ?: run {
            Log.w(TAG, "BluetoothAdapter not initialized")
            return false
        }
    }
}

ブロードキャストの更新

サーバーが GATT サーバーに接続または切断した場合は、新しい状態のアクティビティに通知する必要があります。これを行うにはいくつかの方法があります。次の例では、ブロードキャストを使用して、サービスからアクティビティに情報を送信しています。

サービスは、新しい状態をブロードキャストする関数を宣言します。この関数は、システムにブロードキャストされる前に Intent オブジェクトに渡されるアクション文字列を受け取ります。

private fun broadcastUpdate(action: String) {
    val intent = Intent(action)
    sendBroadcast(intent)
}

ブロードキャスト関数が配置されると、BluetoothGattCallback 内で使用され、GATT サーバーとの接続状態に関する情報を送信します。定数とサービスの現在の接続状態は、Intent アクションを表すサービスで宣言されます。

class BluetoothLeService : Service() {

    private var connectionState = STATE_DISCONNECTED

    private val bluetoothGattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                // successfully connected to the GATT Server
                connectionState = STATE_CONNECTED
                broadcastUpdate(ACTION_GATT_CONNECTED)
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                // disconnected from the GATT Server
                connectionState = STATE_DISCONNECTED
                broadcastUpdate(ACTION_GATT_DISCONNECTED)
            }
        }
    }

    // ...
    companion object {
        const val ACTION_GATT_CONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTED"
        const val ACTION_GATT_DISCONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED"

        private const val STATE_DISCONNECTED = 0
        private const val STATE_CONNECTED = 2
    }
}

アクティビティの更新をリッスンする

サービスが接続の更新をブロードキャストしたら、アクティビティは BroadcastReceiver を実装する必要があります。このレシーバは、アクティビティの設定時に登録し、アクティビティが画面から離れるときに登録を解除します。サービスからのイベントをリッスンすることで、アクティビティは BLE デバイスとの現在の接続状態に基づいてユーザー インターフェースを更新できます。

class DeviceControlActivity : AppCompatActivity() {

    private val gattUpdateReceiver: BroadcastReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            when (intent.action) {
                BluetoothLeService.ACTION_GATT_CONNECTED -> {
                    connected = true
                    updateConnectionState(R.string.connected)
                }
                BluetoothLeService.ACTION_GATT_DISCONNECTED -> {
                    connected = false
                    updateConnectionState(R.string.disconnected)
                }
            }
        }
    }

    override fun onResume() {
        super.onResume()
        registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter())
        bluetoothService?.let { service ->
            deviceAddress?.let { address ->
                val result = service.connect(address)
                Log.d(TAG, "Connect request result=$result")
            }
        }
    }

    override fun onPause() {
        super.onPause()
        unregisterReceiver(gattUpdateReceiver)
    }

    private fun makeGattUpdateIntentFilter(): IntentFilter {
        return IntentFilter().apply {
            addAction(BluetoothLeService.ACTION_GATT_CONNECTED)
            addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED)
        }
    }
}

BLE データの転送では、BroadcastReceiver はデバイスからのサービス検出と特性データの通信にも使用されます。

GATT 接続を閉じる

Bluetooth 接続を扱う際の重要な手順の 1 つは、接続が完了したら接続を閉じることです。これを行うには、BluetoothGatt オブジェクトで close() 関数を呼び出します。次の例では、サービスが BluetoothGatt への参照を保持しています。アクティビティがサービスからバインド解除されると、デバイスのバッテリーの消耗を防ぐために接続が閉じられます。

class BluetoothLeService : Service() {

    // ...
    override fun onUnbind(intent: Intent?): Boolean {
        close()
        return super.onUnbind(intent)
    }

    private fun close() {
        bluetoothGatt?.let { gatt ->
            gatt.close()
            bluetoothGatt = null
        }
    }
}