5.1 KiB
5.1 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| android-bluetooth-le-scanning | Android BLE — Scan / Connect / GATT | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Android Bluetooth LE
Scan → Connect → Discover Services → Read/Write/Notify 흐름. 권한 / lifecycle / state machine 까다로움. Nordic / Polidea 같은 라이브러리 권장.
📖 핵심 개념
- BLE: low energy. peripherals (sensor, watch).
- GATT: 서비스 / characteristic / descriptor.
- Scan filter: UUID / name / MAC.
- Permission (Android 12+): BLUETOOTH_SCAN, BLUETOOTH_CONNECT.
💻 코드 패턴
Permission
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Android 11- -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
Scan
@SuppressLint("MissingPermission") // Permission check 별도
class BleScanner(private val ctx: Context) {
private val adapter = (ctx.getSystemService(BluetoothManager::class.java)).adapter
private val scanner: BluetoothLeScanner? get() = adapter?.bluetoothLeScanner
fun scan(serviceUuid: UUID): Flow<ScanResult> = callbackFlow {
val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(serviceUuid)).build()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
val cb = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
trySend(result)
}
override fun onScanFailed(errorCode: Int) {
close(IllegalStateException("scan failed: $errorCode"))
}
}
scanner?.startScan(listOf(filter), settings, cb)
awaitClose { scanner?.stopScan(cb) }
}
}
Connect + GATT
class BleClient(private val ctx: Context, private val device: BluetoothDevice) {
private var gatt: BluetoothGatt? = null
@SuppressLint("MissingPermission")
suspend fun connect(): BluetoothGatt = suspendCancellableCoroutine { cont ->
gatt = device.connectGatt(ctx, false, object : BluetoothGattCallback() {
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
g.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
g.close()
cont.resumeWithException(Exception("disconnected"))
}
}
override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) cont.resume(g)
else cont.resumeWithException(Exception("services discovery failed"))
}
})
}
suspend fun read(serviceUuid: UUID, charUuid: UUID): ByteArray { ... }
suspend fun write(serviceUuid: UUID, charUuid: UUID, data: ByteArray) { ... }
fun notifications(serviceUuid: UUID, charUuid: UUID): Flow<ByteArray> { ... }
fun disconnect() {
gatt?.disconnect()
gatt?.close()
gatt = null
}
}
MTU / connection priority
gatt.requestMtu(247) // 큰 패킷
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH) // 빠른 throughput
🤔 의사결정 기준
| 상황 | 도구 |
|---|---|
| Direct BLE | Android API + wrapper |
| 복잡 GATT 시나리오 | Nordic/Polidea/Kable 라이브러리 |
| Companion Device Pairing (Android 8+) | CompanionDeviceManager (자동 pair UI) |
| BLE Beacon scanning | iBeacon / Eddystone 라이브러리 |
| BLE peripheral (앱이 server) | BluetoothGattServer |
❌ 안티패턴
- Scan 무한: 배터리 폭발 + Android 가 throttle (5회/30s). 짧게 + filter.
- discoverServices 없이 read/write: characteristic null.
- gatt.close() 누락: 메모리 + 다음 connect 실패.
- 메인 스레드 callback 안에서 무거운 작업: 다른 callback 못 받음.
- 권한 체크 안 함 Android 12+: SecurityException.
- 여러 callback 같은 gatt: 마지막 것만 호출.
- MTU 협상 안 함: 작은 패킷 만. 247 바이트 설정.
- disconnect 후 즉시 connect: race. delay 또는 state machine.
🤖 LLM 활용 힌트
- 권한 + Scan filter + state machine + close 4종 강조.
- 라이브러리 권장 — Kable (Kotlin Multiplatform) 또는 Nordic.