안녕하세요. 이번에 BLE(Bluetooth Low Energy) 기기를 연결할 기회가 생겼습니다.
처음으로 BLE를 연결해서 안드로이드 공식 문서를 따라 만들었지만 데이터가 오지 않아서 많은 시간을 사용했습니다. 이 글을 보시는 분들이 고생하지 않길 바라며, 공식 문서를 따라 하면서 생긴 이슈를 공유 하겠습니다.
참고
저는 Activity를 기기 연결용으로만 사용했으며 서비스에서 알람 및 advertising data를 받는 것까지만 했습니다. 공식 문서와 조금 차이가 있을 수 있습니다.
AndroidManifest.xml 설정은 아래 링크 참고
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 |