[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
---
|
||||
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
|
||||
<!-- 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
|
||||
```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<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
|
||||
```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<ByteArray> { ... }
|
||||
|
||||
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]]
|
||||
Reference in New Issue
Block a user