Как подключиться к серверу GATT

Первый шаг при взаимодействии с устройством BLE – подключение к нему. а именно к серверу GATT на устройстве. Чтобы подключиться к серверу GATT на устройстве с поддержкой BLE, используйте метод connectGatt(). Этот метод принимает три параметра: объект Context, autoConnect (логическое значение, указывающее, нужно ли автоматически подключаться к устройству BLE, как только оно станет доступно) и ссылку на BluetoothGattCallback:

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

Это позволяет подключиться к серверу GATT, размещенному на устройстве BLE, и вернуть экземпляр BluetoothGatt, который затем можно использовать для выполнения операций клиента GATT. Вызывающее приложение (Android) является клиентом GATT. Символ BluetoothGattCallback используется для передачи клиенту результатов, например статуса подключения, а также для выполнения других операций клиента GATT.

Как настроить связанный сервис

В приведенном ниже примере приложение BLE предоставляет действие (DeviceControlActivity), чтобы подключаться к устройствам Bluetooth, показывать данные устройства, а также сервисы и характеристики GATT, поддерживаемые устройством. На основе ввода данных пользователем это действие взаимодействует с Service под названием BluetoothLeService, которое взаимодействует с устройством BLE через BLE API. Связь осуществляется с помощью связанного сервиса, который позволяет действию подключаться к 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(), зависит от вашего приложения. Вы можете показать пользователю сообщение об ошибке, указывающее, что текущее устройство не поддерживает операцию 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. Действие должно отправить адрес устройства сервису, чтобы тот мог установить подключение. Сначала сервис вызовет метод getRemoteDevice() для объекта BluetoothAdapter, чтобы получить доступ к устройству. Если адаптеру не удастся найти устройство с таким адресом, 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. В приведенном ниже примере адрес устройства передается в объект activity как дополнительный параметр намерения.

// 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

Когда сервис узнает, к какому устройству нужно подключиться, и устанавливает связь, ему необходимо подключиться к GATT-серверу на устройстве с поддержкой Bluetooth Low Energy. Для этого подключения требуется BluetoothGattCallback, чтобы получать уведомления о состоянии подключения, обнаружении сервисов, чтении характеристик и уведомлениях о характеристиках.

В этой статье рассказывается об уведомлениях о состоянии подключения. Информацию о том, как выполнять обнаружение сервисов, чтение характеристик и запрашивать уведомления о характеристиках, можно найти в разделе Передача данных BLE.

Функция onConnectionStateChange() активируется при изменении подключения к серверу GATT устройства. В следующем примере обратный вызов определен в классе 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 будет объявлен, сервис сможет использовать объект BluetoothDevice из функции connect() для подключения к сервису GATT на устройстве.

Используется функция connectGatt(). Для этого требуется объект Context, логический флаг autoConnect и параметр BluetoothGattCallback. В этом примере приложение напрямую подключается к устройству BLE, поэтому для autoConnect передается значение false.

Также добавляется свойство 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-подключение, когда оно вам больше не нужно. Для этого вызовите функцию close() для объекта BluetoothGatt. В примере ниже сервис содержит ссылку на 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
        }
    }
}