BluetoothLeScanner là một lớp Android để quét các thiết bị Bluetooth Low Energy, có sẵn từ API 21 (Android 5.0). BluetoothLeScanner đã thay thế phương thức startLeScan cũ trên BluetoothAdapter, cung cấp một API linh hoạt với cấu hình quét (ScanSettings), lọc (ScanFilter) và hỗ trợ chế độ nền (PendingIntent). Phiên bản được lấy qua BluetoothAdapter.getBluetoothLeScanner(). Theo Android Developers, 2026, BluetoothLeScanner hỗ trợ ba chế độ năng lượng và cho phép quét các gói quảng cáo BLE với bộ lọc theo UUID dịch vụ, tên thiết bị hoặc địa chỉ MAC.
Những Điểm Chính
BluetoothLeScanner là một lớp hệ thống để quản lý quét BLE trên Android. Không giống như BluetoothAdapter.startLeScan() chấp nhận LeScanCallback đơn giản, BluetoothLeScanner cung cấp một API hướng đối tượng với cài đặt, bộ lọc và xử lý lỗi nâng cao. Lớp này được giới thiệu trong API 21 (Android 5.0) cùng với hỗ trợ BLE 4.2 và vẫn là phương pháp quét BLE chính trên tất cả các phiên bản Android hiện đại.
Việc lấy phiên bản BluetoothLeScanner được thực hiện qua BluetoothAdapter.getBluetoothLeScanner(). Phương thức trả về null nếu bộ điều hợp Bluetooth không khả dụng (Bluetooth bị tắt hoặc thiết bị không hỗ trợ BLE). Trước khi lấy, hãy kiểm tra BluetoothAdapter.isEnabled() và sự hiện diện của FEATURE_BLUETOOTH_LE qua PackageManager. Sau khi có máy quét, có thể bắt đầu quét trên bất kỳ luồng nào — Android lên lịch các hoạt động BLE trên một luồng nội bộ của ngăn xếp Bluetooth.
// Lấy 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 {
// Kiểm tra khả năng khả dụng BLE
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
return false
}
// Kiểm tra Bluetooth đã bật
if (adapter?.isEnabled != true) {
return false
}
// Lấy máy quét
scanner = adapter?.bluetoothLeScanner
return scanner != null
}
// Kiểm tra khả năng khả dụng của máy quét
val isAvailable: Boolean
get() = scanner != null
// Bắt đầu quét cơ bản không có bộ lọc
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}")
}
}
Lớp BLEScannerManager minh họa cách lấy và khởi tạo BluetoothLeScanner an toàn. initScanner kiểm tra khả năng khả dụng BLE qua hasSystemFeature, Bluetooth được bật và lấy máy quét thành công. startBasicScan bắt đầu quét mà không có cài đặt hoặc bộ lọc — phát hiện tất cả các thiết bị BLE trong phạm vi. handleResult phân tích ScanResult: BluetoothDevice (tên, địa chỉ), RSSI (cường độ tín hiệu), scanRecord (dữ liệu quảng cáo).
ScanSettings là một lớp để cấu hình quét BLE. Tham số chính là chế độ quét (scanMode), xác định sự cân bằng giữa mức tiêu thụ điện năng và độ trễ phát hiện. ScanSettings.Builder cho phép bạn cấu hình: scanMode, callbackType (CALLBACK_TYPE_ALL_MATCHES, CALLBACK_TYPE_FIRST_MATCH, CALLBACK_TYPE_MATCH_LOST), matchMode (MATCH_MODE_AGGRESSIVE, MATCH_MODE_STICKY), reportDelay (độ trễ gửi hàng loạt) và phy (PHY_LE_1M, PHY_LE_2M, PHY_LE_CODED).
Ba chế độ quét: SCAN_MODE_LOW_POWER (0) — quét nền với mức tiêu thụ điện năng thấp, độ trễ phát hiện vài giây. SCAN_MODE_BALANCED (1) — chế độ cân bằng cho hầu hết các tình huống. SCAN_MODE_LOW_LATENCY (2) — độ trễ phát hiện tối thiểu (khoảng 100 ms), mức tiêu thụ điện năng tối đa. Để tìm kiếm thiết bị chủ động, hãy sử dụng LOW_LATENCY, để giám sát nền, hãy sử dụng LOW_POWER.
reportDelay — độ trễ tính bằng mili giây trước khi gửi hàng loạt kết quả. Nếu reportDelay = 0, kết quả được gửi ngay lập tức khi phát hiện. Nếu > 0, Android tích lũy kết quả và gửi một lô qua onBatchScanResults. Gửi hàng loạt giảm số lần gọi callback và mức tiêu thụ điện năng, phù hợp cho quét nền ưu tiên thấp.
// Cấu hình ScanSettings cho các tình huống khác nhau
class ScanSettingsProvider {
// 1. Quét nhanh (tìm kiếm chủ động)
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. Quét tiết kiệm năng lượng (giám sát nền)
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) // lô mỗi 2 giây
.build()
}
// 3. Quét BLE tầm xa (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. Chỉ quét trên 2M PHY (BLE 5.0+)
fun highSpeedScan(): ScanSettings {
return ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setPhy(ScanSettings.PHY_LE_2M)
.build()
}
}
Lớp ScanSettingsProvider chứa các cấu hình điển hình. lowLatencyScan — để quét UI (tìm kiếm “ngay tại đây”). lowPowerScan — để giám sát nền với lô mỗi 2 giây và callbackType FIRST_MATCH (chỉ kích hoạt ở lần phát hiện đầu tiên). longRangeScan sử dụng PHY_LE_CODED (BLE Long Range, lên đến 1 km). highSpeedScan — PHY_LE_2M (2 Mbit/s, chỉ thiết bị BLE 5.0+).
ScanFilter là một lớp để lọc kết quả quét BLE. Nếu không có bộ lọc, BluetoothLeScanner trả về tất cả các thiết bị BLE trong phạm vi — trong môi trường BLE dày đặc, có thể có hàng trăm gói mỗi phút. ScanFilter thu hẹp kết quả đến các thiết bị mong muốn, giảm mức tiêu thụ điện năng và tải ứng dụng. Các bộ lọc được áp dụng ở cấp độ ngăn xếp Bluetooth — các gói không phù hợp bị loại bỏ trước khi đến ứng dụng.
Các loại bộ lọc: setServiceUuid — UUID dịch vụ (định dạng 128-bit đầy đủ bắt buộc). setDeviceName — chuỗi con của tên thiết bị (phân biệt chữ hoa chữ thường, khớp chính xác chuỗi con). setDeviceAddress — địa chỉ MAC chính xác. setManufacturerData — dữ liệu nhà sản xuất (ID công ty + mặt nạ). Có thể đặt nhiều bộ lọc cho một lần quét — thiết bị phải khớp với tất cả (logic AND). Để logic OR, hãy bắt đầu nhiều lần quét.
// Tạo ScanFilter cho các tình huống khác nhau
class ScanFilterFactory {
// 1. Lọc theo UUID dịch vụ (Máy đo nhịp tim)
fun byHeartRateService(): ScanFilter {
return ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")
)
.build()
}
// 2. Lọc theo tên thiết bị ( "iBeacon*")
fun byDeviceName(): ScanFilter {
return ScanFilter.Builder()
.setDeviceName("Sensor")
.build()
}
// 3. MAC- ( thiết bị)
fun byMacAddress(mac: String): ScanFilter {
return ScanFilter.Builder()
.setDeviceAddress(mac)
.build()
}
// 4. Bộ lọc kết hợp (UUID + )
fun combinedFilter(): List<ScanFilter> {
return listOf(
ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000A001-0000-1000-8000-00805F9B34FB")
)
.setDeviceName("MyDevice")
.build()
)
}
// 5. dữ liệu nhà sản xuất
fun byManufacturer(companyId: Int, data: ByteArray, mask: ByteArray): ScanFilter {
return ScanFilter.Builder()
.setManufacturerData(companyId, data, mask)
.build()
}
}
Lớp ScanFilterFactory hiển thị tất cả các loại bộ lọc. byHeartRateService lọc các thiết bị có dịch vụ nhịp tim 0x180D. byDeviceName tìm các thiết bị chứa “Sensor” trong tên (Apple khuyến nghị tên duy nhất để lọc). byMacAddress — tìm kiếm chính xác một thiết bị cụ thể. combinedFilter — bộ lọc AND theo UUID và tên. byManufacturer — bộ lọc theo dữ liệu nhà sản xuất (ví dụ: đối với iBeacon, ID công ty Apple 0x004C được sử dụng).
ScanCallback là một lớp trừu tượng để nhận kết quả quét BLE. Nó chứa ba phương thức: onScanResult — kết quả đơn lẻ (loại callback, ScanResult), onBatchScanResults — kết quả hàng loạt cho reportDelay > 0, onScanFailed — mã lỗi. Tất cả các phương thức được gọi trên luồng chính của Android. Để xử lý lâu trong onScanResult, hãy sử dụng coroutines hoặc HandlerThread.
ScanResult chứa: BluetoothDevice device, int rssi (mức tín hiệu tính bằng dBm), ScanRecord scanRecord (dữ liệu quảng cáo), long timestampNanos (thời gian phát hiện kể từ khi khởi động hệ thống). ScanRecord cung cấp: getServiceData() — UUID + dữ liệu tùy chỉnh, getManufacturerSpecificData() — dữ liệu nhà sản xuất, getAdvertiseFlags() — cờ BLE. Loại callback cho biết: CALLBACK_TYPE_ALL_MATCHES — khớp với bộ lọc, CALLBACK_TYPE_FIRST_MATCH — phát hiện đầu tiên, CALLBACK_TYPE_MATCH_LOST — mất thiết bị.
Mã lỗi onScanFailed: SCAN_FAILED_ALREADY_STARTED (1) — quét đã bắt đầu, SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2) — đăng ký ứng dụng trong ngăn xếp Bluetooth thất bại, SCAN_FAILED_INTERNAL_ERROR (3) — lỗi nội bộ ngăn xếp, SCAN_FAILED_FEATURE_UNSUPPORTED (4) — quét BLE không được hỗ trợ trên thiết bị.
// Kết quả quét đầy đủ và xử lý lỗi
class ScanResultHandler {
private val results = mutableListOf<ScanResult>()
val scanCallback = object : ScanCallback() {
// 1. Kết quả đơn lẻ
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
}
// Thêm vào danh sách (khử trùng lặp theo địa chỉ)
val existingIndex = results.indexOfFirst {
it.device.address == result.device.address
}
if (existingIndex >= 0) {
results[existingIndex] = result // cập nhật RSSI
} else {
results.add(result)
}
// Trích xuất dữ liệu từ gói quảng cáo
val record = result.scanRecord
val serviceData = record?.serviceData
val manufacturerData = record?.manufacturerSpecificData
print("Found: ${result.device.name ?: "N/A"}, RSSI: ${result.rssi}")
}
// 2. Kết quả hàng loạt (reportDelay > 0)
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
results?.let { batch ->
print("Batch: ${batch.size} devices")
}
}
// 3. Lỗi quét
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}")
}
}
Lớp ScanResultHandler xử lý tất cả các loại callback của BluetoothLeScanner. onScanResult cập nhật danh sách thiết bị với khử trùng lặp theo địa chỉ MAC — RSSI được cập nhật cho các thiết bị đã tìm thấy. CALLBACK_TYPE_MATCH_LOST báo hiệu mất thiết bị (xóa khỏi danh sách). onBatchScanResults xử lý kết quả hàng loạt cho reportDelay > 0. onScanFailed ánh xạ mã lỗi thành thông báo dễ đọc — rất quan trọng để gỡ lỗi quét BLE.
Quét PendingIntent là một cơ chế của BluetoothLeScanner để quét BLE hoạt động ngay cả khi ứng dụng ở nền (với các hạn chế Android 8+). Thay vì ScanCallback, một PendingIntent được sử dụng, gửi Broadcast đến BroadcastReceiver hệ thống khi phát hiện thiết bị BLE. Điều này cho phép ứng dụng nhận thông báo thiết bị BLE mà không cần ở trong bộ nhớ (hệ thống tạo tiến trình khi nhận được broadcast).
Giới hạn quét nền: Trên Android 8+ (API 26), các dịch vụ nền bị hạn chế — quét PendingIntent vượt qua giới hạn này qua BroadcastReceiver, mà hệ thống có thể khởi chạy khi nhận được sự kiện BLE. Trên Android 10+ (API 29), quét BLE nền bị hạn chế thêm bởi các chính sách tiết kiệm năng lượng của nhà sản xuất (Xiaomi, Huawei, Samsung chặn các hoạt động BLE nền). Đối với các tình huống BLE quan trọng, cần có thông báo dịch vụ tiền cảnh.
// Quét BLE nền qua PendingIntent
class BackgroundBLEScanner(private val context: Context) {
private val scanner: BluetoothLeScanner? by lazy {
val adapter = BluetoothAdapter.getDefaultAdapter()
adapter?.bluetoothLeScanner
}
fun startBackgroundScan() {
// Tạo PendingIntent cho BroadcastReceiver
val intent = Intent(context, BLEBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Cài đặt quét nền
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.build()
// Bắt đầu quét nền
scanner?.startScan(
null, // bộ lọc
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) {
// Lấy kết quả quét
val results = BluetoothLeScanner.getPendingIntentScanResults(intent)
results?.let { scanResults ->
for (result in scanResults) {
// Gửi thông báo cho người dùng
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)
}
}
Lớp BackgroundBLEScanner bắt đầu quét BLE nền qua PendingIntent. startBackgroundScan tạo một PendingIntent mà khi phát hiện thiết bị BLE, sẽ gửi Broadcast đến BLEBroadcastReceiver. BroadcastReceiver trích xuất ScanResult qua getPendingIntentScanResults() và có thể hiển thị thông báo hoặc gửi dữ liệu đến máy chủ. Cách tiếp cận này hoạt động ngay cả khi ứng dụng đã bị hệ thống kết thúc — Android khởi động lại BroadcastReceiver khi nhận được broadcast.
Ví dụ hoàn chỉnh về máy quét BLE trong Kotlin sử dụng BluetoothLeScanner với ScanSettings, ScanFilter và ScanCallback để tìm thiết bị Heart Rate Monitor. Máy quét hiển thị danh sách các thiết bị được tìm thấy với RSSI và UUID dịch vụ, có khả năng kết nối qua BluetoothGatt.
// Máy quét BLE đầy đủ với coroutines trong 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 {
// Kiểm tra trạng thái Bluetooth
if (adapter?.isEnabled != true) {
close(IllegalStateException("Bluetooth disabled"))
return@callbackFlow
}
// Cấu hình quét
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"))
}
}
// Bắt đầu quét
scanner?.startScan(filters, settings, callback)
// Tự động dừng sau thời gian
delay(duration)
scanner?.stopScan(callback)
close()
}.flowOn(Dispatchers.IO)
fun stop() {
scanner?.stopScan(object : ScanCallback() {})
}
}
class BLEException(message: String) : Exception(message)
Lớp DeviceScanner sử dụng Kotlin Flow (callbackFlow) để quét BLE phản ứng. Quét bắt đầu với cài đặt LOW_LATENCY và bộ lọc UUID Heart Rate Service. Kết quả được phát qua onScanResult vào Flow. Tự động dừng sau một khoảng thời gian nhất định (mặc định 10 giây). FlowOn(Dispatchers.IO) chuyển các hoạt động BLE sang luồng nền. Cách tiếp cận này cho phép quét BLE trong kiến trúc MVVM qua viewModelScope.launch và collect.
Câu hỏi Thường gặp
BluetoothLeScanner là một lớp Android (API 21+) để quét BLE. Nó được lấy qua BluetoothAdapter.getBluetoothLeScanner(). Hỗ trợ ba chế độ quét (LOW_POWER, BALANCED, LOW_LATENCY), lọc theo UUID, tên và địa chỉ MAC, kết quả hàng loạt và PendingIntent để quét nền. Thay thế phương thức BluetoothAdapter.startLeScan() cũ.
SCAN_MODE_LOW_POWER — chế độ nền với độ trễ phát hiện 5–10 giây, mức tiêu thụ tối thiểu. SCAN_MODE_LOW_LATENCY — chế độ hoạt động với độ trễ khoảng 100 ms, mức tiêu thụ tối đa. SCAN_MODE_BALANCED — thỏa hiệp (~2 giây độ trễ). Sử dụng LOW_LATENCY để quét UI, LOW_POWER với PendingIntent để giám sát nền.
Nguyên nhân: Bluetooth bị tắt (kiểm tra adapter.isEnabled), thiếu quyền (BLUETOOTH_SCAN trên API 31+, ACCESS_FINE_LOCATION trên API 23–30), scanner = null (bộ điều hợp không khả dụng), thiết bị ngoài phạm vi hoặc bộ lọc sai. Cũng kiểm tra onScanFailed — mã lỗi cho biết nguyên nhân: SCAN_FAILED_ALREADY_STARTED (1) hoặc SCAN_FAILED_APPLICATION_REGISTRATION_FAILED (2).
Sử dụng phiên bản PendingIntent của startScan() — truyền PendingIntent thay vì ScanCallback. Khi một thiết bị BLE được phát hiện, Android gửi Broadcast đến BroadcastReceiver, có thể được hệ thống khởi chạy ngay cả khi ứng dụng ở nền. Đối với Android 8+, hãy thêm BroadcastReceiver vào tệp kê khai. Trên Android 10+, hãy xem xét các hạn chế tiết kiệm năng lượng của nhà sản xuất.
BluetoothLeScanner không có giới hạn về số lượng thiết bị có thể phát hiện — giới hạn phụ thuộc vào độ bão hòa BLE của môi trường. Một văn phòng có thể có 20–50 thiết bị BLE đang hoạt động, một trung tâm mua sắm có hàng trăm thiết bị. Sử dụng ScanFilter (theo UUID, tên) để lọc. Nếu không lọc, hãy xử lý kết quả không đồng bộ — onScanResult có thể được gọi hàng chục lần mỗi giây.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm