BLE 기기와 상호작용하는 첫 번째 단계는 기기에 연결하는 것입니다. 더 구체적으로는 기기의 GATT 서버에 연결하는 것입니다. BLE 기기의 GATT
서버에 연결하려면
connectGatt()
메서드를 사용하세요. 이 메서드는
Context 객체, autoConnect (BLE 기기가 사용 가능해지자마자 자동으로 연결할지 여부를 나타내는 불리언)
를 나타내는 불리언), 및
BluetoothGattCallback 참조의 세 가지 매개변수를 사용합니다.
var bluetoothGatt: BluetoothGatt? = null // ... bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback)
이렇게 하면 BLE 기기에서 호스팅하는 GATT 서버에 연결되고
BluetoothGatt 인스턴스가 반환됩니다. 이 인스턴스를 사용하여 GATT 클라이언트 작업을 실행할 수 있습니다. 호출자 (Android 앱)는 GATT 클라이언트입니다. BluetoothGattCallback은 연결 상태와 같은 결과를 클라이언트에 전달하고 추가 GATT 클라이언트 작업을 실행하는 데 사용됩니다.
바인드된 서비스 설정
다음 예에서 BLE 앱은 블루투스 기기에 연결하고, 기기 데이터를 표시하고, 기기에서 지원하는 GATT 서비스 및 특성을 표시하는 활동(DeviceControlActivity)을 제공합니다. 사용자 입력에 따라 이 활동은
Service라는 BluetoothLeService와 통신합니다. 이 서비스는 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에 관한 자세한 내용은 블루투스 설정을 참고하세요. 다음 예에서는 이 설정 코드를 성공을 나타내는 Boolean 값을 반환하는 initialize() 함수로 래핑합니다.
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 반환 값을 처리하는 것은 애플리케이션에 따라 다릅니다. 현재 기기가 블루투스 작업을 지원하지 않는다는 오류 메시지를 사용자에게 표시하거나 블루투스가 작동하는 데 필요한 기능을 사용 중지할 수 있습니다. 다음 예에서는
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 기기의 주소를 전달해야 합니다. 다음 예에서는 기기 주소가 인텐트 추가 항목으로 활동에 전달됩니다.
// 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 데이터 전송을 참고하세요.
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이 선언되면 서비스는 connect() 함수의 BluetoothDevice 객체를 사용하여 기기의 GATT 서비스에 연결할 수 있습니다.
The
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 연결 닫기
블루투스 연결을 처리할 때 중요한 단계 중 하나는 연결이 완료되면 연결을 닫는 것입니다. 이렇게 하려면 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 } } }