Реализация сканирования и подключения 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-устройства рядом. Удобно при разработке. В продакшн не ставить: расходует больше батареи и засоряет список чужими устройствами.
Повторное подключение к известному устройству
Если 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.5с | среднее | по умолчанию |
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. Если сканирование не для определения местоположения, в манифесте:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
Тогда геолокация не нужна и пользователю не придётся объяснять, почему Bluetooth-приложение просит доступ к GPS.
Срок реализации: 2-3 дня — сканирование + подключение с обработкой всех состояний и разрешений на обеих платформах. Стоимость рассчитывается индивидуально.







