BluetoothLeScanner — що це, методи та BLE-сканування в Android

Автор: IT Sectr Опубліковано: 2026-07-16 Час читання: 10 хв

BluetoothLeScanner — клас Android для сканування Bluetooth Low Energy пристроїв, доступний з API 21 (Android 5.0). BluetoothLeScanner замінив застарілий метод startLeScan на BluetoothAdapter, надаючи гнучке API з налаштуванням сканування (ScanSettings), фільтрацією (ScanFilter) та підтримкою фонового режиму (PendingIntent). Екземпляр отримується через BluetoothAdapter.getBluetoothLeScanner(). За даними Android Developers, 2026, BluetoothLeScanner підтримує три режими енергоспоживання та дозволяє сканувати рекламні пакети BLE з фільтрацією за UUID сервісу, імені пристрою або MAC-адресою.

Головне

  • BluetoothLeScanner — сучасне API Android (API 21+) для BLE-сканування, заміна застарілого startLeScan
  • ScanSettings — налаштування режиму сканування: LOW_POWER, BALANCED, LOW_LATENCY та тип callback
  • ScanFilter — фільтрація результатів за UUID сервісу, імені пристрою, MAC-адресою, manufacturer data
  • ScanCallback — callback результатів onScanResult, onBatchScanResults та onScanFailed з кодами помилок
  • PendingIntent — фонове сканування через BroadcastReceiver, навіть коли додаток у фоні

Що таке BluetoothLeScanner: суть та отримання екземпляра

BluetoothLeScanner — системний клас для керування BLE-скануванням на Android. На відміну від BluetoothAdapter.startLeScan(), який приймає простий LeScanCallback, BluetoothLeScanner надає об'єктне API з налаштуваннями, фільтрами та розширеною обробкою помилок. Клас з'явився в API 21 (Android 5.0) разом з підтримкою BLE 4.2 і залишається основним способом BLE-сканування на всіх сучасних версіях Android.

Отримання екземпляра BluetoothLeScanner виконується через BluetoothAdapter.getBluetoothLeScanner(). Метод повертає null, якщо Bluetooth-адаптер недоступний (Bluetooth вимкнено або пристрій не підтримує BLE). Перед отриманням перевірте BluetoothAdapter.isEnabled() та наявність FEATURE_BLUETOOTH_LE через PackageManager. Після отримання сканера можна запускати сканування в будь-якому потоці — Android сам планує BLE-операції на внутрішньому потоці Bluetooth-стека.

kotlin
// Отримання BluetoothLeScanner
class BLEScannerManager(context: Context) {

    private val bluetoothManager: BluetoothManager =
        context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    private val adapter: BluetoothAdapter? = bluetoothManager.adapter
    private var scanner: BluetoothLeScanner? = null

    fun initScanner(): Boolean {
        // Перевірка доступності BLE
        if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            return false
        }

        // Перевірка увімкнення Bluetooth
        if (adapter?.isEnabled != true) {
            return false
        }

        // Отримати сканер
        scanner = adapter?.bluetoothLeScanner
        return scanner != null
    }

    // Перевірка доступності сканера
    val isAvailable: Boolean
        get() = scanner != null

    // Запустити базове сканування без фільтрів
    fun startBasicScan() {
        scanner?.startScan(object : ScanCallback() {
            override fun onScanResult(callbackType: Int, result: ScanResult) {
                handleResult(result)
            }
        })
    }

    private fun handleResult(result: ScanResult) {
        val device = result.device
        print("Device: ${device.name ?: "Unnamed"}, RSSI: ${result.rssi}, address: ${device.address}")
    }
}

Клас BLEScannerManager демонструє безпечне отримання та ініціалізацію BluetoothLeScanner. initScanner перевіряє наявність BLE через hasSystemFeature, увімкнений Bluetooth та успішне отримання сканера. startBasicScan запускає сканування без налаштувань та фільтрів — виявляє всі BLE-пристрої в зоні дії. handleResult розбирає ScanResult: BluetoothDevice (ім'я, адреса), RSSI (рівень сигналу), scanRecord (рекламні дані).

ScanSettings: режими сканування та тип callback

ScanSettings — клас для конфігурації BLE-сканування. Головний параметр — режим сканування (scanMode), що визначає компроміс між енергоспоживанням та затримкою виявлення. ScanSettings.Builder дозволяє налаштувати: scanMode, callbackType (CALLBACK_TYPE_ALL_MATCHES, CALLBACK_TYPE_FIRST_MATCH, CALLBACK_TYPE_MATCH_LOST), matchMode (MATCH_MODE_AGGRESSIVE, MATCH_MODE_STICKY), reportDelay (затримка пакетної відправки) та phy (PHY_LE_1M, PHY_LE_2M, PHY_LE_CODED).

Три режими сканування: SCAN_MODE_LOW_POWER (0) — фонове сканування з низьким енергоспоживанням, затримка виявлення кілька секунд. SCAN_MODE_BALANCED (1) — збалансований режим для більшості сценаріїв. SCAN_MODE_LOW_LATENCY (2) — мінімальна затримка виявлення (близько 100 мс), максимальне енергоспоживання. Для активного пошуку пристроїв використовуйте LOW_LATENCY, для фонового моніторингу — LOW_POWER.

reportDelay — затримка в мілісекундах перед груповою відправкою результатів. Якщо reportDelay = 0, результати надсилаються негайно при виявленні. Якщо > 0, Android накопичує результати та надсилає батч через onBatchScanResults. Пакетна відправка зменшує кількість викликів callbackів та знижує енергоспоживання, підходить для сканування у фоні з низьким пріоритетом.

kotlin
// Конфігурація ScanSettings для різних сценаріїв
class ScanSettingsProvider {

    // 1. Швидке сканування (активний пошук)
    fun lowLatencyScan(): ScanSettings {
        return ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
            .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
            .setReportDelay(0)
            .setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED)
            .build()
    }

    // 2. Енергоефективне сканування (фоновий моніторинг)
    fun lowPowerScan(): ScanSettings {
        return ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
            .setMatchMode(ScanSettings.MATCH_MODE_STICKY)
            .setReportDelay(2000)  // батч кожні 2 секунди
            .build()
    }

    // 3. BLE Long Range сканування (Coded PHY)
    fun longRangeScan(): ScanSettings {
        return ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .setPhy(ScanSettings.PHY_LE_CODED)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
            .build()
    }

    // 4. Сканування лише на 2M PHY (BLE 5.0+)
    fun highSpeedScan(): ScanSettings {
        return ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .setPhy(ScanSettings.PHY_LE_2M)
            .build()
    }
}

Клас ScanSettingsProvider містить типові конфігурації. lowLatencyScan — для UI-сканування (пошук «тут і зараз»). lowPowerScan — для фонового моніторингу з батчем кожні 2 секунди та callbackType FIRST_MATCH (спрацьовує лише при першому виявленні). longRangeScan використовує PHY_LE_CODED (BLE Long Range, до 1 км). highSpeedScan — PHY_LE_2M (2 Мбіт/с, лише пристрої BLE 5.0+).

ScanFilter: фільтрація BLE-пристроїв за UUID та іменем

ScanFilter — клас для фільтрації результатів BLE-сканування. Без фільтра BluetoothLeScanner повертає всі BLE-пристрої в зоні дії — для щільного BLE-середовища це сотні пакетів на хвилину. ScanFilter звужує результати до потрібних пристроїв, зменшуючи енергоспоживання та навантаження на додаток. Фільтри застосовуються на рівні Bluetooth-стека — непідходящі пакети відкидаються до доставки в додаток.

Типи фільтрів: setServiceUuid — UUID сервісу (обов'язково повний 128-бітний формат). setDeviceName — підрядок імені пристрою (реєстрозалежно, точний збіг підрядка). setDeviceAddress — точна MAC-адреса. setManufacturerData — дані виробника (ID компанії + маска). Для одного сканування можна встановити кілька фільтрів — пристрій повинен відповідати всім (AND-логіка). Для OR-логіки запустіть кілька сканувань.

kotlin
// Створення ScanFilter для різних сценаріїв
class ScanFilterFactory {

    // 1. Фільтр за UUID сервісу (Heart Rate Monitor)
    fun byHeartRateService(): ScanFilter {
        return ScanFilter.Builder()
            .setServiceUuid(
                ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")
            )
            .build()
    }

    // 2. Фільтр за іменем пристрою ( "iBeacon*")
    fun byDeviceName(): ScanFilter {
        return ScanFilter.Builder()
            .setDeviceName("Sensor")
            .build()
    }

    // 3.   MAC- ( пристроїв)
    fun byMacAddress(mac: String): ScanFilter {
        return ScanFilter.Builder()
            .setDeviceAddress(mac)
            .build()
    }

    // 4. Комбінований фільтр (UUID + )
    fun combinedFilter(): List<ScanFilter> {
        return listOf(
            ScanFilter.Builder()
                .setServiceUuid(
                    ParcelUuid.fromString("0000A001-0000-1000-8000-00805F9B34FB")
                )
                .setDeviceName("MyDevice")
                .build()
        )
    }

    // 5.   дані виробника
    fun byManufacturer(companyId: Int, data: ByteArray, mask: ByteArray): ScanFilter {
        return ScanFilter.Builder()
            .setManufacturerData(companyId, data, mask)
            .build()
    }
}

Клас ScanFilterFactory показує всі типи фільтрів. byHeartRateService відфільтровує пристрої з сервісом пульсу 0x180D. byDeviceName знаходить пристрої, що містять «Sensor» в імені (Apple рекомендує унікальні імена для фільтрації). byMacAddress — точний пошук конкретного девайса. combinedFilter — AND-фільтр за UUID та іменем. byManufacturer — фільтр за даними виробника (наприклад, для iBeacon використовується company ID Apple 0x004C).

ScanCallback: обробка результатів та помилок сканування

ScanCallback — абстрактний клас для отримання результатів BLE-сканування. Містить три методи: onScanResult — одиничний результат (тип callbackа, ScanResult), onBatchScanResults — пакет результатів для reportDelay > 0, onScanFailed — код помилки. Всі методи викликаються на головному потоці Android (main thread). Для тривалої обробки в onScanResult використовуйте корутини або HandlerThread.

ScanResult містить: BluetoothDevice device (пристрій), int rssi (рівень сигналу в dBm), ScanRecord scanRecord (рекламні дані), long timestampNanos (час виявлення з моменту завантаження системи). ScanRecord надає: getServiceData() — UUID + кастомні дані, getManufacturerSpecificData() — дані виробника, getAdvertiseFlags() — BLE-прапорці. Тип callbackа (callbackType) вказує: CALLBACK_TYPE_ALL_MATCHES — збіг з фільтром, CALLBACK_TYPE_FIRST_MATCH — перше виявлення, CALLBACK_TYPE_MATCH_LOST — втрата пристрою.

Коди помилок onScanFailed: SCAN_FAILED_ALREADY_STARTED (1) — сканування вже запущено, SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2) — реєстрація додатку в Bluetooth-стеку не вдалася, SCAN_FAILED_INTERNAL_ERROR (3) — внутрішня помилка стека, SCAN_FAILED_FEATURE_UNSUPPORTED (4) — BLE-сканування не підтримується на пристрої.

kotlin
// Повні результати сканування та обробка помилок
class ScanResultHandler {

    private val results = mutableListOf<ScanResult>()

    val scanCallback = object : ScanCallback() {

        // 1. Одиничний результат
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            // callbackType: 1 = ALL_MATCHES, 2 = FIRST_MATCH, 4 = MATCH_LOST
            if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
                onDeviceLost(result)
                return
            }

            // Додати до списку (дедуплікація за адресою)
            val existingIndex = results.indexOfFirst {
                it.device.address == result.device.address
            }
            if (existingIndex >= 0) {
                results[existingIndex] = result  // оновити RSSI
            } else {
                results.add(result)
            }

            // Витягти дані з рекламного пакета
            val record = result.scanRecord
            val serviceData = record?.serviceData
            val manufacturerData = record?.manufacturerSpecificData

            print("Found: ${result.device.name ?: "N/A"}, RSSI: ${result.rssi}")
        }

        // 2. Пакетні результати (reportDelay > 0)
        override fun onBatchScanResults(results: MutableList<ScanResult>?) {
            results?.let { batch ->
                print("Batch: ${batch.size} devices")
            }
        }

        // 3. Помилка сканування
        override fun onScanFailed(errorCode: Int) {
            val error = when (errorCode) {
                ScanCallback.SCAN_FAILED_ALREADY_STARTED -> "Already scanning"
                ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "Registration failed"
                ScanCallback.SCAN_FAILED_INTERNAL_ERROR -> "Internal error"
                ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED -> "BLE not supported"
                else -> "Unknown error: $errorCode"
            }
            print("Error: $error")
        }
    }

    private fun onDeviceLost(result: ScanResult) {
        results.removeAll { it.device.address == result.device.address }
        print("Device lost: ${result.device.address}")
    }
}

Клас ScanResultHandler обробляє всі типи callbackів BluetoothLeScanner. onScanResult оновлює список пристроїв з дедуплікацією за MAC-адресою — RSSI оновлюється для вже знайдених пристроїв. CALLBACK_TYPE_MATCH_LOST сигналізує про втрату пристрою (видалення зі списку). onBatchScanResults обробляє пакетні результати для reportDelay > 0. onScanFailed маппить коди помилок у людиночитані повідомлення — критично для налагодження BLE-сканування.

PendingIntent: фонове BLE-сканування через BroadcastReceiver

PendingIntent-сканування — механізм BluetoothLeScanner для BLE-сканування, що працює навіть коли додаток у фоні (з обмеженнями Android 8+). Замість ScanCallback використовується PendingIntent, який надсилає Broadcast у системний BroadcastReceiver при виявленні BLE-пристрою. Це дозволяє додатку отримувати сповіщення про BLE-пристрої, не перебуваючи в пам'яті (система створює процес при отриманні broadcast).

Обмеження фонового сканування: На Android 8+ (API 26) фонові сервіси обмежені — PendingIntent-сканування оминає це обмеження через BroadcastReceiver, який система може запустити при отриманні BLE-події. На Android 10+ (API 29) фонове BLE-сканування додатково обмежене політиками енергозбереження виробників (Xiaomi, Huawei, Samsung блокують фонові BLE-операції). Для критичних BLE-сценаріїв потрібне сповіщення з foreground service.

kotlin
// Фонове BLE-сканування через PendingIntent
class BackgroundBLEScanner(private val context: Context) {

    private val scanner: BluetoothLeScanner? by lazy {
        val adapter = BluetoothAdapter.getDefaultAdapter()
        adapter?.bluetoothLeScanner
    }

    fun startBackgroundScan() {
        // Створити PendingIntent для BroadcastReceiver
        val intent = Intent(context, BLEBroadcastReceiver::class.java)
        val pendingIntent = PendingIntent.getBroadcast(
            context,
            0,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        // Налаштування фонового сканування
        val settings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
            .setMatchMode(ScanSettings.MATCH_MODE_STICKY)
            .build()

        // Запустити фонове сканування
        scanner?.startScan(
            null,  // фільтри
            settings,
            pendingIntent
        )
    }

    fun stopBackgroundScan() {
        val intent = Intent(context, BLEBroadcastReceiver::class.java)
        val pendingIntent = PendingIntent.getBroadcast(
            context, 0, intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        scanner?.stopScan(pendingIntent)
    }
}

// BroadcastReceiver   BLE-
class BLEBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        // Отримати результати сканування
        val results = BluetoothLeScanner.getPendingIntentScanResults(intent)
        results?.let { scanResults ->
            for (result in scanResults) {
                // Відправити сповіщення користувачеві
                showNotification(context, result.device.name ?: " ")
            }
        }
    }

    private fun showNotification(context: Context, name: String) {
        val notification = Notification.Builder(context, "ble_channel")
            .setSmallIcon(android.R.drawable.ic_dialog_info)
            .setContentTitle("BLE devices")
            .setContentText("Found: $name")
            .setAutoCancel(true)
            .build()
        val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
                as NotificationManager
        manager.notify(System.currentTimeMillis().toInt(), notification)
    }
}

Клас BackgroundBLEScanner запускає фонове BLE-сканування через PendingIntent. startBackgroundScan створює PendingIntent, який при виявленні BLE-пристрою надсилає Broadcast в BLEBroadcastReceiver. BroadcastReceiver витягує ScanResult через getPendingIntentScanResults() та може показувати сповіщення або надсилати дані на сервер. Такий підхід працює навіть якщо додаток було завершено системою — Android перезапускає BroadcastReceiver при отриманні broadcast.

Приклад BLE-сканера на Kotlin з BluetoothLeScanner

Повний приклад BLE-сканера на Kotlin, що використовує BluetoothLeScanner з ScanSettings, ScanFilter та ScanCallback для пошуку пристроїв Heart Rate Monitor. Сканер показує список знайдених пристроїв з RSSI та UUID сервісів, з можливістю підключення через BluetoothGatt.

kotlin
// Повний BLE-сканер з корутинами в Kotlin
class DeviceScanner(private val context: Context) {

    private val adapter: BluetoothAdapter? by lazy {
        val manager = context.getSystemService(Context.BLUETOOTH_SERVICE)
                as BluetoothManager
        manager.adapter
    }

    private val scanner: BluetoothLeScanner? by lazy {
        adapter?.bluetoothLeScanner
    }

    fun startScan(duration: Long = 10000): Flow<ScanResult> = callbackFlow {
        // Перевірити стан Bluetooth
        if (adapter?.isEnabled != true) {
            close(IllegalStateException("Bluetooth disabled"))
            return@callbackFlow
        }

        // Конфігурація сканування
        val settings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .build()

        val filters = listOf(
            ScanFilter.Builder()
                .setServiceUuid(ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB"))
                .build()
        )

        val callback = object : ScanCallback() {
            override fun onScanResult(callbackType: Int, result: ScanResult) {
                trySend(result)
            }

            override fun onScanFailed(errorCode: Int) {
                close(BLEException("Scan failed: $errorCode"))
            }
        }

        // Запустити сканування
        scanner?.startScan(filters, settings, callback)

        // Автоостановка після тривалості
        delay(duration)
        scanner?.stopScan(callback)
        close()
    }.flowOn(Dispatchers.IO)

    fun stop() {
        scanner?.stopScan(object : ScanCallback() {})
    }
}

class BLEException(message: String) : Exception(message)

Клас DeviceScanner використовує Kotlin Flow (callbackFlow) для реактивного BLE-сканування. Сканування запускається з налаштуваннями LOW_LATENCY та фільтром за UUID Heart Rate Service. Результати емітяться через onScanResult в Flow. Автоостановка через заданий duration (10 секунд за замовчуванням). FlowOn(Dispatchers.IO) виносить BLE-операції у фоновий потік. Такий підхід дозволяє використовувати BLE-сканування в MVVM-архітектурі через viewModelScope.launch та collect.

Часто задавані питання

Що таке BluetoothLeScanner в Android?

BluetoothLeScanner — клас Android (API 21+) для BLE-сканування. Отримується через BluetoothAdapter.getBluetoothLeScanner(). Підтримує три режими сканування (LOW_POWER, BALANCED, LOW_LATENCY), фільтрацію за UUID, іменем та MAC-адресою, пакетні результати та PendingIntent для фонового сканування. Замінює застарілий метод BluetoothAdapter.startLeScan().

Чим відрізняється LOW_POWER від LOW_LATENCY?

SCAN_MODE_LOW_POWER — фоновий режим із затримкою виявлення 5–10 секунд, мінімальне енергоспоживання. SCAN_MODE_LOW_LATENCY — активний режим із затримкою близько 100 мс, максимальне енергоспоживання. SCAN_MODE_BALANCED — компроміс (~2 секунди затримки). Для UI-сканування використовуйте LOW_LATENCY, для фонового моніторингу — LOW_POWER з PendingIntent.

Чому BluetoothLeScanner не знаходить пристрої?

Причини: Bluetooth вимкнено (перевірте adapter.isEnabled), не отримано дозволи (BLUETOOTH_SCAN на API 31+, ACCESS_FINE_LOCATION на API 23–30), scanner = null (адаптер недоступний), пристрій поза зоною дії, або використовується неправильний фільтр. Також перевірте onScanFailed — код помилки вкаже на причину: SCAN_FAILED_ALREADY_STARTED (1) або SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2).

Як сканувати BLE у фоні на Android?

Використовуйте PendingIntent версію startScan() — передайте PendingIntent замість ScanCallback. При виявленні BLE-пристрою Android надсилає Broadcast в BroadcastReceiver, який може бути запущений системою навіть якщо додаток у фоні. Для Android 8+ додайте BroadcastReceiver в маніфест. На Android 10+ враховуйте обмеження енергозбереження виробників.

Скільки BLE-пристроїв можна виявити за одне сканування?

BluetoothLeScanner не має ліміту на кількість виявлених пристроїв — обмеження залежить від BLE-насиченості середовища. В офісі може бути 20–50 активних BLE-пристроїв, у торговому центрі — сотні. Для фільтрації використовуйте ScanFilter (за UUID, іменем). Без фільтрації обробляйте результати асинхронно — onScanResult може викликатися десятки разів на секунду.

Підсумки

  • BluetoothLeScanner — сучасний клас Android (API 21+) для BLE-сканування з налаштуваннями та фільтрацією
  • ScanSettings — три режими: LOW_POWER (фоновий), BALANCED, LOW_LATENCY (активний)
  • ScanFilter — фільтрація за UUID сервісу, імені пристрою, MAC-адресою, manufacturer data з AND-логікою
  • ScanCallback — onScanResult (одиночні), onBatchScanResults (пакетні), onScanFailed (коди помилок)
  • PendingIntent — фонове сканування через BroadcastReceiver, що працює при завершеному додатку
  • ScanRecord — рекламні дані BLE: serviceData, manufacturerSpecificData, advertiseFlags, TX power level
  • Kotlin Flow — callbackFlow дозволяє використовувати BluetoothLeScanner в реактивній архітектурі з автоостановкою

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також