안녕하세요. 이번에 BLE(Bluetooth Low Energy) 기기를 연결할 기회가 생겼습니다.
처음으로 BLE를 연결해서 안드로이드 공식 문서를 따라 만들었지만 데이터가 오지 않아서 많은 시간을 사용했습니다. 이 글을 보시는 분들이 고생하지 않길 바라며, 공식 문서를 따라 하면서 생긴 이슈를 공유 하겠습니다.
참고
저는 Activity를 기기 연결용으로만 사용했으며 서비스에서 알람 및 advertising data를 받는 것까지만 했습니다. 공식 문서와 조금 차이가 있을 수 있습니다.
AndroidManifest.xml 설정은 아래 링크 참고
Bluetooth permissions | Android Developers
Bluetooth permissions Stay organized with collections Save and categorize content based on your preferences. To use Bluetooth features in your app, you must declare several permissions. You should also specify whether your app requires support for Bluetoot
developer.android.com
BLE 찾기
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
private val bluetoothLeScanner by lazy {
bluetoothAdapter?.bluetoothLeScanner
}
private fun scanLeDevice() {
if (!scanning) {
scanning = true
bluetoothLeScanner.startScan(leScanCallback)
} else {
scanning = false
bluetoothLeScanner.stopScan(leScanCallback)
}
}
// 디바이스 스캔 Call back
private val leScanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
super.onScanResult(callbackType, result)
// 로그 찍어보기!!!
}
}
BLE 기기를 찾는 것은 일반적인 Bluetooth 검색과는 다릅니다. bluetoothAdapter에서 bluetoothLeScanner를 불러와서 스캔을 해야합니다. scanLeDevice()를 부르면 스캔이 시작됩니다. 스캔을 시작하면 callback함수로 결과값이 오는데 저는 로그로 찍어봤습니다.
BLE Service
// Server - Clinet 구조를 위해 LocaBinder 사용
class BluetoothLeService : Service() {
private val binder = LocalBinder()
inner class LocalBinder : Binder() {
fun getService(): BluetoothLeService {
return this@BluetoothLeService
}
}
}
Server - Client 구조를 위해서 Local Binder를 사용합니다. Service는 Connect 및 BLE 기기에서 보낸 데이터들을 처리할 것 입니다.
Connect
class BluetoothLeService : Service() {
...
fun connect(address: String): Boolean {
bluetoothAdapter?.let { adapter ->
try {
val device = adapter.getRemoteDevice(address)
bluetoothGatt = device.connectGatt(
this, // Context
true, // autoConnect 여부
bluetoothGattCallback, // Callback 함수 (데이터 처리)
BluetoothDevice.TRANSPORT_LE // BLE 기기 연결을 위해 추가
)
Log.d("BLE", "DEVICE MAC: ${bluetoothGatt?.device?.address}")
return true
} catch (exception: IllegalArgumentException) {
Log.e("BLE", "Device Not Found")
return false
}
// connect to the GATT server on the device
} ?: run {
Log.e("BLE", "BluetoothAdapter not initialized")
return false
}
return true
}
...
}
device 정보를 들고온 뒤 connectGatt를 하면 페어링이 시작됩니다. 성공하면 true 아니면 false값을 리턴해서 연결 여부를 확인할 수 있습니다.
연결을 성공적으로 했다면 이제 Notification을 받으면 됩니다.
Callback 함수
private val bluetoothGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
LogMgr.d("BLE", "onConnectionState Change: $status $newState")
if (newState == BluetoothProfile.STATE_CONNECTED) {
connectionState = STATE_CONNECTED
// Handler를 사용해서 discoverService를 열어야함
Handler(Looper.getMainLooper()).post {
bluetoothGatt?.discoverServices()
}
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
connectionState = STATE_DISCONNECTED
}
}
...
}
연결 상태에 대한 콜백 함수입니다. Connect, Disconnect 시 이 함수로 들어와서 작동합니다.
Connect 상태가 되었다면, discoverServices()함수로 BLE기기가 보내는 서비스를 발견해야합니다.
private val bluetoothGattCallback = object : BluetoothGattCallback() {
...
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
Log.i("BLE", "onServicesDiscovered services.count=${gatt.services.size} status=$status")
if (status == 129) {
Log.e("BLE", "status: 129 GATT INTERNAL ERROR")
}
val service: BluetoothGattService =
gatt.getService(UUID.fromString("MYUUID"))
?: run {
Log.e("BLE", "Service Not Found")
return
}
// Thread.sleep(50L)을 통해서 순서대로 Notification을 해야한다. 하지 않으면 작동 x
Handler(Looper.getMainLooper()).post {
Thread.sleep(50L)
val char = service.getCharacteristic(UUID.fromString("MYUUID"))
setCharacteristicNotification(char, true)
}
}
...
}
Connect상태에서 discoverServices()를 한 경우 onServicesDiscovered가 호출된다.
이제부터 BLE기기에서 지정한 UUID에 따라 코드를 수정해야한다.
만약 내가 UUID를 모른다면 BluetoothGatt에서 모든 서비스를 forEach를 돌려서 일일이 Notification을 한 뒤 데이터가 오는 UUID를 찾아야한다.
private val bluetoothGattCallback = object : BluetoothGattCallback() {
...
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
Log.i("BLE", "onCharacteristicRead: $status")
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(
"BLE",
"onCharacteristicRead: ${characteristic.getStringValue(0)} ${characteristic.uuid} "
)
val data = characteristic.value
val stringBuilder = StringBuilder(data.size)
for (byteChar in data) stringBuilder.append("${byteChar.toChar()} ")
Log.i("BLE", "broadcastUpdate characteristic value: $stringBuilder")
Log.i("BLE", "broadcastUpdate characteristic value: ${data.contentToString()}")
}
}
...
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
val data = characteristic.value
val valueByteToString = StringBuilder(data.size)
if (data != null && data.isNotEmpty()) {
for (byteChar in data) valueByteToString.append(byteChar.toInt().toChar())
}
gatt.readCharacteristic(characteristic)
}
...
}
위와 같이 Notification을 등록한 경우, BLE에서 알람을 보낼 때 onCharacteristicChange 혹은 onCharacteristicRead에서 알람과 데이터를 받을 수 있다. 필자의 BLE 기기 경우 onCharacteristicChange에서만 알람이 왔다. 기기가 value 값을 포함하지 않아서 알람의 이름을 보고 데이터를 처리했으며, 데이터 처리 부분은 BLE 기기에 따라 다르니 기기에 따라 처리를 해주면 될 것 같다.
위의 부분은 GATT 서버에 연결하는 방식이고 Advertising Data를 받고싶은 경우는 아래와 같은 코드를 통해서 받으면 된다.
// 위의 Connect 부분과 동일
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
private val bluetoothLeScanner by lazy {
bluetoothAdapter?.bluetoothLeScanner
}
private fun scanLeDevice() {
if (!scanning) {
scanning = true
bluetoothLeScanner.startScan(leScanCallback)
} else {
scanning = false
bluetoothLeScanner.stopScan(leScanCallback)
}
}
// 디바이스 스캔 Call back
private val leScanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
super.onScanResult(callbackType, result)
result.scanRecord.bytes // 이 부분이 advertising 데이터를 받음
}
}
scan callback에서 advertising을 받을 수 있어서, scan부분을 확인하면 된다.
'안드로이드' 카테고리의 다른 글
[안드로이드] java.lang.nullpointerexception: inflate(...) must not be null 해결한 방법 (0) | 2024.08.19 |
---|---|
android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork 에러 해결법 (0) | 2023.07.04 |
[안드로이드] 안드로이드 14 베타 1 알아보기 - 2 (0) | 2023.06.10 |
DuraSpeed 때문에 고생함 (1) | 2023.02.20 |