Visão geral do Bluetooth de baixa energia


O Android 4.3 (API de nível 18) introduz suporte à plataforma integrada para Bluetooth de baixa energia (BLE, na sigla em inglês) no papel central e fornece APIs que os aplicativos podem usar para descobrir dispositivos, fazer consultas de serviços e transmitir informações.

Alguns casos de uso comuns são:

  • Transferência de pequenas quantidades de dados entre dispositivos próximos.
  • Interação com sensores de proximidade como os Sensores do Google para oferecer aos usuários uma experiência personalizada com base na localização deles.

Em comparação com o Bluetooth clássico, o Bluetooth de baixa energia (BLE) foi projetado para fornecer um consumo de energia significativamente menor. Isso permite que os aplicativos Android se comuniquem com dispositivos BLE que tenham requisitos de energia mais rígidos, como sensores de proximidade, monitores de frequência cardíaca e dispositivos de fitness.

Atenção: quando um usuário pareia dispositivos por meio do BLE, os dados comunicados entre os dois aparelhos ficam acessíveis para todos os aplicativos no dispositivo do usuário.

Assim, se o aplicativo capturar dados confidenciais, implemente segurança na camada do aplicativo para proteger a privacidade dos dados.

Principais termos e conceitos

Veja um resumo dos principais termos e conceitos do BLE:

  • Perfil de atributo genérico (GATT, na sigla em inglês): o perfil GATT é uma especificação geral para envio e recebimento de pequenas quantidades de dados conhecidos como “atributos” por um vínculo BLE. Todos os perfis de aplicativos de baixa energia atuais têm como base o GATT.
    • O Bluetooth SIG define muitos perfis para dispositivos de baixa energia. O perfil é uma especificação para como um dispositivo funciona em um aplicativo em particular. O dispositivo pode implementar mais de um perfil. Por exemplo, um dispositivo pode conter um detector de nível da bateria e um monitor de frequência cardíaca.
  • Protocolo de atributo (ATT, na sigla em inglês): o GATT foi criado com base no Protocolo de atributo (ATT). Ele também é chamado de GATT/ATT. O ATT é otimizado para operar em dispositivos BLE. Para isso, ele usa o mínimo possível de bytes. Cada atributo é identificado exclusivamente por um Identificador universalmente único (UUID, na sigla em inglês), um formato padronizado de 128 bits para um código de string usado para identificar esse tipo de informação. Os atributos transportados pelo ATT são formatados como características e serviços.
  • Característica: a característica contém um valor único e descritores 0-n para informar o valor dela. Ela pode ser considerada como um tipo, semelhante a uma classe.
  • Descritor: os descritores são atributos definidos que descrevem o valor de uma característica. Por exemplo, um descritor pode especificar uma descrição legível, um intervalo aceitável para o valor de uma característica ou uma unidade de medida específica a esse valor.
  • Serviço: um serviço é um conjunto de características. Por exemplo, você pode ter um serviço chamado “Monitor de frequência cardíaca” que inclui características como “medida da frequência cardíaca”. É possível encontrar uma lista de perfis e serviços existentes com base em GATT acessando bluetooth.org.

Papéis e responsabilidades

Estes são os papéis e as responsabilidades que se aplicam quando um dispositivo Android interage com um dispositivo BLE:

  • Central x periférico. Aplica-se à conexão BLE em si. O dispositivo no papel central faz a verificação do anúncio e o dispositivo no papel periférico emite o anúncio.
  • Servidor GATT x cliente GATT. Isso determina como os dois dispositivos se comunicam entre si após o estabelecimento da comunicação.

Para entender a diferença, imagine que você tenha um smartphone Android e um rastreador de atividades que seja um dispositivo BLE. O smartphone é compatível com o papel central, enquanto o rastreador de atividades é compatível com o papel periférico. Para estabelecer uma conexão BLE, você precisará de um de cada. Dois itens que tenham compatibilidade somente periférica ou central não podem se comunicar entre si.

Após o smartphone e o rastreador estabelecerem uma conexão, eles começam a transferir metadados GATT um para o outro. Dependendo do tipo de dados transferidos, um deles poderá atuar como servidor. Por exemplo, pode fazer sentido para o rastreador de atividades atuar como servidor caso ele precise enviar dados do sensor para o smartphone. Se o rastreador precisar receber atualizações do smartphone, pode fazer sentido que este último atue como servidor.

No exemplo usado neste documento, o aplicativo Android (operando em um dispositivo Android) é o cliente GATT. O aplicativo recebe dados do servidor GATT, que é um monitor de frequência cardíaca BLE compatível com o Perfil de frequência cardíaca. No entanto, como alternativa, você pode projetar o aplicativo Android para atuar no papel de servidor GATT. Consulte BluetoothGattServer para ver mais informações.

Permissões BLE

Para usar recursos do Bluetooth no aplicativo, é necessário declarar a permissão Bluetooth BLUETOOTH. Você precisa dessa permissão para executar todas as comunicações do Bluetooth, como solicitar ou aceitar uma conexão e transferir dados.

Além disso, como os sensores LE geralmente estão associados com o local, é necessário declarar a permissão ACCESS_FINE_LOCATION. Sem ela, as verificações não retornarão nenhum resultado.

Observação: caso seu aplicativo seja direcionado ao Android 9 (API de nível 28) ou posteriores, você pode declarar a permissão ACCESS_COARSE_LOCATION em vez da permissão ACCESS_FINE_LOCATION.

Se você quiser que o aplicativo inicie a descoberta de dispositivos ou manipule configurações do Bluetooth, será necessário declarar também a permissão .BLUETOOTH_ADMIN Observação: se você usar a permissão BLUETOOTH_ADMIN, deverá também ter a permissão BLUETOOTH.

Declare as permissões no arquivo de manifesto do aplicativo. Por exemplo:

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

Se quiser declarar que o aplicativo está disponível somente para dispositivos compatíveis com BLE, inclua a seguinte informação no manifesto do aplicativo:

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

No entanto, caso queira tornar o aplicativo disponível para dispositivos incompatíveis com BLE, ainda será necessário incluir esse elemento no manifesto do aplicativo, mas defini-lo como required="false". Após isso, no tempo de execução, será possível determinar a disponibilidade do BLE usando PackageManager.hasSystemFeature():

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()
}

Configurar o BLE

Antes que o aplicativo possa se comunicar por meio do BLE, é necessário verificar se o dispositivo permite esse tipo de comunicação e, em caso positivo, verificar se ela está ativada. Essa verificação só será necessária se <uses-feature.../> estiver definido como falso.

Caso não haja suporte ao BLE, desative ordenadamente todos os recursos desse tipo. Se o dispositivo for compatível com BLE, mas esse estiver desativado, será possível solicitar que o usuário ative o Bluetooth sem sair do aplicativo. Essa configuração é efetuada em duas etapas, usando o BluetoothAdapter:

  1. Acesse o BluetoothAdapterBluetoothAdapter é necessário para todas as atividades do Bluetooth. O BluetoothAdapter representa o próprio adaptador Bluetooth do dispositivo (o rádio Bluetooth). Há um adaptador Bluetooth para todo o sistema, e o aplicativo pode usar esse objeto para interagir com ele. O snippet abaixo mostra como acessar o adaptador. Essa abordagem usa getSystemService() para retornar uma instância do BluetoothManager, que é usado em seguida para acessar o adaptador. O Android 4.3 (API de nível 18) introduz o BluetoothManager
    private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
        val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothManager.adapter
    }
    
  2. Ative o BluetoothEm seguida, é necessário garantir a ativação do Bluetooth. Chame isEnabled() para verificar se o Bluetooth está ativado. Se método retornar “false”, o Bluetooth está desativado. O snippet a seguir verifica se o Bluetooth está ativado. Caso ele não esteja, o snippet exibirá um erro solicitando ao usuário que acesse as Configurações e ative o Bluetooth:
    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)
    }
    

    Observação: a constante REQUEST_ENABLE_BT passada a startActivityForResult(android.content.Intent, int) é um inteiro definido localmente (que precisa ser maior que 0) passado pelo sistema para a implementação de onActivityResult(int, int, android.content.Intent) como o parâmetro requestCode.

Encontrar dispositivos BLE

Para encontrar dispositivos BLE, use o método startLeScan(). Esse método usa um BluetoothAdapter.LeScanCallback como parâmetro. Por essa ser a forma de retornar os resultados da verificação, será necessário implementar esse callback. Como a verificação usa intensivamente a bateria, observe as diretrizes a seguir:

  • Assim que encontrar o dispositivo desejado, interrompa a verificação.
  • Não verifique repetidamente e defina um limite de tempo para a busca. Um dispositivo anteriormente disponível pode ter sido movido para um local fora do alcance. Continuar a verificação causa o consumo da bateria.

O snippet a seguir mostra como iniciar e interromper uma verificação:

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)
            }
        }
    }
}

Se quiser verificar somente tipos específicos de periféricos, você pode chamar startLeScan(UUID[], BluetoothAdapter.LeScanCallback), fornecendo uma matriz de objetos UUID que especificam os serviços GATT compatíveis com o aplicativo.

Esta é a implementação da BluetoothAdapter.LeScanCallback, a interface usada para entregar os resultados da verificação de BLE:

val leDeviceListAdapter: LeDeviceListAdapter = ...

private val leScanCallback = BluetoothAdapter.LeScanCallback { device, rssi, scanRecord ->
    runOnUiThread {
        leDeviceListAdapter.addDevice(device)
        leDeviceListAdapter.notifyDataSetChanged()
    }
}

Observação: é possível verificar somente dispositivos de Bluetooth LE ou somente de Bluetooth clássico, conforme descrito em Bluetooth. Não é possível verificar dispositivos de Bluetooth LE e clássico ao mesmo tempo.

Conectar-se ao servidor GATT

A primeira etapa para interagir com um dispositivo BLE é se conectar a ele: mais especificamente ao servidor GATT do dispositivo. Para se conectar ao servidor GATT em um dispositivo BLE, use o método connectGatt(). Esse método usa três parâmetros: um objeto ContextautoConnect (um valor booleano que indica se a conexão com o dispositivo BLE será feita automaticamente quando ele ficar disponível) e uma referência para um BluetoothGattCallback:

var bluetoothGatt: BluetoothGatt? = null
...

bluetoothGatt = device.connectGatt(this, false, gattCallback)

Com isso, é feita a conexão com o servidor GATT hospedado pelo dispositivo BLE e é retornada uma instância BluetoothGatt, que pode ser usada para conduzir as operações do cliente GATT. O autor da chamada (o aplicativo Android) é o cliente GATT. O BluetoothGattCallback é usado para entregar resultados ao cliente, como o status da conexão e outras operações do cliente GATT.

Neste exemplo, o aplicativo BLE fornece uma atividade (DeviceControlActivity) para conectar, exibir dados e mostrar características e serviços GATT compatíveis com o dispositivo. Com base na entrada do usuário, essa atividade se comunica com um Service chamado BluetoothLeService, que interage com o dispositivo BLE por meio da API Android BLE:

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)
                    }
            }
        }
    }
}

Quando um callback em particular é acionado, ele chama o método auxiliar broadcastUpdate() adequado e passa uma ação a ele. Os dados em análise nesta seção são realizados de acordo com as especificações de perfil da Medida de frequência cardíaca Bluetooth:

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)
}

Na DeviceControlActivity, esses eventos são gerenciados por um BroadcastReceiver:

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

Ler atributos BLE

Após o aplicativo Android se conectar a um servidor GATT e descobrir serviços, ele poderá ler e gravar atributos, quando houver compatibilidade. Por exemplo, este snippet itera nos serviços e nas características do servidor e os exibe na IU:

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

Receber notificações GATT

É comum que aplicativos BLE peçam para ser notificados quando uma característica em particular é alterada no dispositivo. Este snippet mostra como definir a notificação para uma característica, usando o método setCharacteristicNotification():

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)

Após as notificações serem ativadas para uma característica, um callback onCharacteristicChanged() será acionado caso as características mudem no dispositivo remoto.

// Characteristic notification
override fun onCharacteristicChanged(
        gatt: BluetoothGatt,
        characteristic: BluetoothGattCharacteristic
) {
    broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic)
}

Fechar o aplicativo do cliente

Quando o aplicativo terminar de usar um dispositivo BLE, ele chamará close() para que o sistema possa liberar os recursos de forma adequada:

fun close() {
    bluetoothGatt?.close()
    bluetoothGatt = null
}
Esta página foi útil?

Content and code samples on this page are subject to the licenses described in the Content License. Java is a registered trademark of Oracle and/or its affiliates.

Last updated 2019-12-27 UTC.

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s