Реалізація сканування та підключення BLE-пристроїв
Завдання виглядає просто: знайти BLE-пристрій, підключитися. На практиці — це обробка восьми різних станів адаптера, різних режимів сканування та нюансів роботи конкретних чипсетів. Розглянемо тільки цю частину BLE-стека, без читання характеристик.
iOS: сканування через CBCentralManager
Сканування можливе тільки коли CBCentralManager.state == .poweredOn. Інші стани потрібно обробляти:
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
startScanning()
case .poweredOff:
showAlert("Увімкніть Bluetooth у налаштуваннях")
case .unauthorized:
// iOS 13+ — CBManager.authorization
if CBCentralManager.authorization == .denied {
showSettingsLink()
}
case .unsupported:
showAlert("Пристрій не підтримує Bluetooth LE")
case .resetting:
// стек перезавантажується, чекаємо .poweredOn
break
default:
break
}
}
Фільтрація при скануванні
// Сканюємо тільки пристрої з потрібним сервісом
let serviceUUIDs = [CBUUID(string: "YOUR-SERVICE-UUID")]
centralManager.scanForPeripherals(withServices: serviceUUIDs, options: [
CBCentralManagerScanOptionAllowDuplicatesKey: false
])
withServices: nil — сканює всі близькі BLE-пристрої. Зручно при розробці. У production не ставити: споживає більше батареї та засміює список чужими пристроями.
Повторне підключення до відомого пристрою
Якщо UUID периферії збережено (наприклад, в UserDefaults), можна відновити об'єкт без повторного сканування:
let knownUUID = UUID(uuidString: savedUUIDString)!
let peripherals = centralManager.retrievePeripherals(withIdentifiers: [knownUUID])
if let peripheral = peripherals.first {
centralManager.connect(peripheral, options: nil)
} else {
// UUID застарів або пристрій замінено — запускаємо повне сканування
startScanning()
}
Це важливо для приложень, які часто переподключаються до одного пристрою (браслет, датчик). Без retrievePeripherals кожен раз запускається сканування з затримкою.
Android: BluetoothLeScanner
val bluetoothAdapter = (getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
val scanner = bluetoothAdapter.bluetoothLeScanner
val filters = listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(UUID.fromString("YOUR-SERVICE-UUID")))
.build()
)
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // при активному використанні
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.build()
val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
val rssi = result.rssi
val advertisingData = result.scanRecord
// додаємо в список знайдених пристроїв
}
override fun onScanFailed(errorCode: Int) {
// SCAN_FAILED_ALREADY_STARTED = 1 — сканування вже запущено
// SCAN_FAILED_APPLICATION_REGISTRATION_FAILED = 2 — занадто багато сканерів
// SCAN_FAILED_FEATURE_UNSUPPORTED = 4 — фільтри не підтримуються (старі пристрої)
}
}
scanner.startScan(filters, settings, scanCallback)
Режими сканування
| Режим | Частота | Споживання | Коли використовувати |
|---|---|---|---|
SCAN_MODE_LOW_POWER |
~512 мс | мінімальне | фоновий пошук |
SCAN_MODE_BALANCED |
~512 мс / ~1.5s | середнє | за замовчуванням |
SCAN_MODE_LOW_LATENCY |
безперервно | високе | активний пошук в UI |
SCAN_MODE_OPPORTUNISTIC |
тільки якщо інший сканер активний | нульове | пасивний моніторинг |
Дозволи Android 12+
// Перевіряємо перед скануванням
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
when {
ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED -> {
requestPermissions(arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
), REQUEST_CODE)
}
}
}
BLUETOOTH_SCAN без флага neverForLocation потребує ACCESS_FINE_LOCATION. Якщо сканування не для визначення місцезнаходження, у manifest:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
Тоді геолокація не потрібна і користувачам не доведеться пояснювати, чому Bluetooth-приложення просить доступ до GPS.
Термін реалізації: 2–3 дні — сканування + підключення з обробкою всіх станів та дозволів на обох платформах. Вартість розраховується індивідуально.







