Connettersi a un server GATT

Il primo passo per interagire con un dispositivo BLE è connettersi. Più in particolare, la connessione al server GATT sul dispositivo. Per connetterti a un server GATT su un dispositivo BLE, utilizza il metodo connectGatt(). Questo metodo accetta tre parametri: un oggetto Context, autoConnect (un valore booleano che indica se connettersi automaticamente al dispositivo BLE non appena diventa disponibile) e un riferimento a un BluetoothGattCallback:

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

Si connette al server GATT ospitato dal dispositivo BLE e restituisce un'istanza di BluetoothGatt, che puoi utilizzare per eseguire operazioni del client GATT. Il chiamante (l'app per Android) è il client GATT. BluetoothGattCallback viene utilizzato per fornire risultati al client, ad esempio lo stato della connessione, nonché eventuali ulteriori operazioni del client GATT.

Configurare un servizio associato

Nell'esempio seguente, l'app BLE fornisce un'attività (DeviceControlActivity) per connettersi ai dispositivi Bluetooth, visualizzare i dati del dispositivo e visualizzare i servizi e le caratteristiche GATT supportati dal dispositivo. In base all'input dell'utente, questa attività comunica con un Service chiamato BluetoothLeService, che interagisce con il dispositivo BLE tramite l'API BLE. La comunicazione viene eseguita utilizzando un servizio associato che consente all'attività di connettersi a BluetoothLeService e chiamare funzioni per connettersi ai dispositivi. BluetoothLeService richiede un'implementazione di Binder che fornisca l'accesso al servizio per l'attività.

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

L'attività può avviare il servizio utilizzando bindService(), passando un Intent per avviare il servizio, un'implementazione di ServiceConnection per ascoltare gli eventi di connessione e disconnessione e un flag per specificare opzioni di connessione aggiuntive.

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

Configurare BluetoothAdapter

Una volta associato, il servizio deve accedere a BluetoothAdapter. Dovrebbe verificare che l'adattatore sia disponibile sul dispositivo. Per saperne di più su BluetoothAdapter, consulta Configurare il Bluetooth. L'esempio seguente racchiude questo codice di configurazione in una funzione initialize() che restituisce un valore Boolean che indica l'esito positivo.

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
    }

    // ...
}

L'attività chiama questa funzione all'interno della sua implementazione ServiceConnection. La gestione di un valore restituito false dalla funzione initialize() dipende dalla tua applicazione. Potresti mostrare all'utente un messaggio di errore che indica che il dispositivo attuale non supporta l'operazione Bluetooth o disattivare qualsiasi funzionalità che richiede il funzionamento del Bluetooth. Nell'esempio seguente, finish() viene chiamato nell'attività per riportare l'utente alla schermata precedente.

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

    // ...
}

Collega a un dispositivo

Una volta inizializzata l'istanza BluetoothLeService, può connettersi al dispositivo BLE. L'attività deve inviare l'indirizzo del dispositivo al servizio in modo che possa avviare la connessione. Il servizio chiamerà prima getRemoteDevice() sul BluetoothAdapter per accedere al dispositivo. Se l'adattatore non riesce a trovare un dispositivo con quell'indirizzo, getRemoteDevice() genera un 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 chiama questa funzione connect() una volta inizializzato il servizio. L'attività deve passare l'indirizzo del dispositivo BLE. Nell'esempio seguente, l'indirizzo del dispositivo viene passato all'attività come extra dell'intent.

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

Dichiarare il callback GATT

Una volta che l'attività comunica al servizio a quale dispositivo connettersi e il servizio si connette al dispositivo, il servizio deve connettersi al server GATT sul dispositivo BLE. Questa connessione richiede un BluetoothGattCallback per ricevere notifiche sullo stato della connessione, sul rilevamento del servizio, sulle letture delle caratteristiche e sulle notifiche delle caratteristiche.

Questo argomento si concentra sulle notifiche relative allo stato della connessione. Consulta Trasferire dati BLE per scoprire come eseguire l'individuazione del servizio, le letture delle caratteristiche e le richieste di notifiche delle caratteristiche.

La funzione onConnectionStateChange() viene attivata quando cambia la connessione al server GATT del dispositivo. Nell'esempio seguente, il callback è definito nella classe Service, quindi può essere utilizzato con BluetoothDevice una volta che il servizio si connette.

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

Connettiti al servizio GATT

Una volta dichiarato BluetoothGattCallback, il servizio può utilizzare l'oggetto BluetoothDevice della funzione connect() per connettersi al servizio GATT sul dispositivo.

Viene utilizzata la funzione connectGatt(). A questo scopo sono necessari un oggetto Context, un flag booleano autoConnect e BluetoothGattCallback. In questo esempio, l'app si connette direttamente al dispositivo BLE, quindi false viene passato per autoConnect.

Viene aggiunta anche una proprietà BluetoothGatt. In questo modo, il servizio può chiudere la connessione quando non è più necessaria.

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

Aggiornamenti sulle trasmissioni

Quando il server si connette o si disconnette dal server GATT, deve notificare l'attività del nuovo stato. Esistono diversi modi per raggiungere questo obiettivo. L'esempio seguente utilizza le trasmissioni per inviare le informazioni dal servizio all'attività.

Il servizio dichiara una funzione per trasmettere il nuovo stato. Questa funzione accetta una stringa di azione che viene passata a un oggetto Intent prima di essere trasmessa al sistema.

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

Una volta implementata, la funzione di trasmissione viene utilizzata all'interno di BluetoothGattCallback per inviare informazioni sullo stato della connessione con il server GATT. Le costanti e lo stato attuale della connessione del servizio vengono dichiarati nel servizio che rappresenta le azioni 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
    }
}

Ascoltare gli aggiornamenti dell'attività

Una volta che il servizio trasmette gli aggiornamenti della connessione, l'attività deve implementare un BroadcastReceiver. Registra questo ricevitore durante la configurazione dell'attività e annullane la registrazione quando l'attività esce dallo schermo. Ascoltando gli eventi del servizio, l'attività è in grado di aggiornare l'interfaccia utente in base allo stato attuale della connessione con il dispositivo 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)
        }
    }
}

In Trasferisci dati BLE, il BroadcastReceiver viene utilizzato anche per comunicare l'individuazione del servizio e i dati delle caratteristiche del dispositivo.

Chiudi connessione GATT

Un passaggio importante quando si gestiscono le connessioni Bluetooth è chiuderle quando non ti servono più. Per farlo, chiama la funzione close() sull'oggetto BluetoothGatt. Nell'esempio seguente, il servizio contiene il riferimento a BluetoothGatt. Quando l'attività viene scollegata dal servizio, la connessione viene chiusa per evitare di scaricare la batteria del dispositivo.

class BluetoothLeService : Service() {

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

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