--- id: android-bluetooth-le-scanning title: Android BLE — Scan / Connect / GATT category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [android, bluetooth, ble, gatt, vibe-coding] tech_stack: { language: "Kotlin / Bluetooth LE", applicable_to: ["Android"] } applied_in: [] aliases: [BLE, GATT, characteristic, scan filter, ScanCallback] --- # 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 ```xml ``` ### Scan ```kotlin @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 = 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 ```kotlin 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 { ... } fun disconnect() { gatt?.disconnect() gatt?.close() gatt = null } } ``` ### MTU / connection priority ```kotlin 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. ## 🔗 관련 문서 - [[Android_Lifecycle_Aware_Components]] - [[Native_Battery_Network_Profiling]]