You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android BLE扫描无结果:无法发现ESP32模块

Android BLE扫描无法发现ESP32模块的排查方案

问题背景

开发Android应用时,需与BLE ESP32模块建立连接并实现UTF-8消息收发,但低功耗扫描模式下无法发现目标模块。该模块可在nRF Connect中正常识别,应用无报错信息。以下为用户提供的蓝牙相关类代码:

lateinit var context: Context
var bluetoothManager: BluetoothManager
var bluetoothAdapter: BluetoothAdapter
var bluetoothLeScanner: BluetoothLeScanner
private var SERVICE_UUID: UUID = UUID.fromString("4fafc201-1fb5-459e-8fcc-c5c9c331914b")
private var serviceUUIDs = arrayOf(SERVICE_UUID)
var myDevices = HashMap<Int, BluetoothDevice>()
var deviceCounter = 0
var scanning = false
var handler = Handler()
val TAGe: String = "JOST_TAG_ERROR::  "
val TAGs: String = "JOST_TAG_SUCCESS::  "
lateinit var scanFilters: List<ScanFilter>
val scanSettings: ScanSettings = ScanSettings.Builder()
    .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
    .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
    .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
    .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
    .setReportDelay(0)
    .build()

@RequiresApi(Build.VERSION_CODES.S)
constructor(transmitted_context: Context){
    this.context = transmitted_context
    bluetoothManager = transmitted_context.getSystemService(BluetoothManager::class.java)
    bluetoothAdapter = bluetoothManager.adapter
    bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner

    if(serviceUUIDs != null){
        scanFilters = ArrayList<ScanFilter>()
        for(service: UUID in serviceUUIDs) {
            val filter = ScanFilter.Builder()
                .setServiceUuid(ParcelUuid(SERVICE_UUID))
                .build()
            (scanFilters as ArrayList<ScanFilter>).add(filter)
        }
    }

    this.lookingfordevices()
    this.scanLeDevice()
}

var scanCallback: ScanCallback = object : ScanCallback() {
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        Toast.makeText(context, "Starting onScanResult", Toast.LENGTH_LONG).show()
        var bluetoothGatt: BluetoothGatt? = null
        var device: BluetoothDevice = result.device as BluetoothDevice
        Toast.makeText(context, ""+result.device.uuids.toString(), Toast.LENGTH_LONG).show()
        myDevices[deviceCounter] = device
        deviceCounter++
        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.BLUETOOTH_CONNECT
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(context as Activity, arrayOf(Manifest.permission.BLUETOOTH_CONNECT), 1)
        }
        if ((result != null) && result.device.uuids.contentEquals(serviceUUIDs)) {
            Toast.makeText(context, "Scanresult isn't null and device UUID fits", Toast.LENGTH_LONG).show()
            bluetoothGatt = result.device.connectGatt(context, false, bluetoothGattCallback,
                BluetoothDevice.TRANSPORT_LE
            )
            Toast.makeText(context, "Connecting ot Gatt: "+bluetoothGatt.device.address,
                Toast.LENGTH_LONG).show()

        } else {
            Log.e(TAGe, "THERE ARE NO DEVICES FOUND FROM YOUR SCAN RESULT")
            Toast.makeText(context, "There are no devices found!", Toast.LENGTH_LONG).show()
        }
    }

    override fun onScanFailed(errorCode: Int) {
        super.onScanFailed(errorCode)
        Toast.makeText(context, "onScan Failed!", Toast.LENGTH_LONG).show()
    }

}

private val bluetoothGattCallback = object: BluetoothGattCallback() {
    @RequiresApi(Build.VERSION_CODES.S)
    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
        if(ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(context as Activity, arrayOf(Manifest.permission.BLUETOOTH_CONNECT),1)
        }
        if(status == BluetoothGatt.GATT_SUCCESS){
            Toast.makeText(context, "GATT_SUCCESS", Toast.LENGTH_LONG).show()
            if(newState == BluetoothProfile.STATE_CONNECTED){
                Toast.makeText(context, "GATT FINALLY CONNECTED", Toast.LENGTH_LONG).show()
                var bondstate = gatt?.device?.bondState
                if(bondstate == BluetoothDevice.BOND_NONE || bondstate == BluetoothDevice.BOND_BONDED){
                    gatt?.discoverServices()
                } else if (bondstate == BluetoothDevice.BOND_BONDING){
                    Toast.makeText(context, "Waiting Bonding to complete", Toast.LENGTH_LONG).show()
                }
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED){
                gatt?.close()
            } else {
                //TODO
            }
        } else {
            Toast.makeText(context, "NO SUCCES IN GATT", Toast.LENGTH_LONG).show()
            gatt?.close()
        }
    }
}



private val SCAN_PERIOD: Long = 10000
//stops scanning after 10 seconds


fun scanLeDevice(){
    Toast.makeText(context, "Scanning Devices", Toast.LENGTH_LONG).show()
    if(bluetoothAdapter.isEnabled){
        Toast.makeText(context, "b_adapter_enabled", Toast.LENGTH_LONG).show()
        if(!scanning){
            Toast.makeText(context, "Scanning was false", Toast.LENGTH_LONG).show()
            handler.postDelayed({
                scanning = false
                if (ActivityCompat.checkSelfPermission(
                        context,
                        Manifest.permission.BLUETOOTH_SCAN
                    ) != PackageManager.PERMISSION_GRANTED
                ) {
                    ActivityCompat.requestPermissions(
                        context as Activity,
                        arrayOf(Manifest.permission.BLUETOOTH_SCAN), 1
                    )
                }
                bluetoothLeScanner.stopScan(scanCallback)
            }, SCAN_PERIOD)
            scanning = true
            Toast.makeText(context, "Starting Scan therefore", Toast.LENGTH_LONG).show()
            bluetoothLeScanner.startScan(scanFilters,scanSettings,scanCallback)
        } else {
            Toast.makeText(context, "Scanned", Toast.LENGTH_LONG).show()
            scanning = false
            bluetoothLeScanner.stopScan(scanCallback)
        }
    }
}

fun lookingfordevices(){
    CoroutineScope(Dispatchers.IO).launch {
        handler.postDelayed({
            Log.d(TAGs, myDevices.size.toString())
            scanLeDevice()
            lookingfordevices()
        },15000)
    }
}

@RequiresApi(Build.VERSION_CODES.S)
fun checkinitialpermissionstate(){
    if(!PermissionCheck().requestPermissionState(context)){
        PermissionCheck().requestPermissions(context)
    }
}

}

核心排查与修复点

1. 扫描过滤器的匹配问题

  • 代码中创建扫描过滤器时,循环遍历serviceUUIDs但始终使用固定的SERVICE_UUID,属于冗余操作,但更关键的是:需确认ESP32广播的服务UUID与代码中指定的4fafc201-1fb5-459e-8fcc-c5c9c331914b完全一致。可通过nRF Connect查看ESP32的广播UUID,若不匹配,过滤器会直接过滤掉设备。
  • 临时测试可移除扫描过滤器,调用bluetoothLeScanner.startScan(null, scanSettings, scanCallback),验证是否能发现设备,以此确认过滤器是否为问题根源。

2. 低功耗扫描模式的漏扫问题

ScanSettings.SCAN_MODE_LOW_POWER扫描频率低,容易漏扫设备,而nRF Connect默认使用高功耗扫描,因此能正常发现。

  • 测试阶段可先切换为ScanSettings.SCAN_MODE_LOW_LATENCY,确认能发现设备后,再优化低功耗模式的参数(如调整MATCH_MODE和NUM_OF_MATCHES)。

3. 权限检查时机错误

scanLeDevice中延迟任务才检查BLUETOOTH_SCAN权限,但扫描已提前启动。权限检查必须在扫描操作前完成:

fun scanLeDevice(){
    Toast.makeText(context, "Scanning Devices", Toast.LENGTH_LONG).show()
    if(bluetoothAdapter.isEnabled){
        Toast.makeText(context, "b_adapter_enabled", Toast.LENGTH_LONG).show()
        if(!scanning){
            // 扫描前先检查权限
            if (ActivityCompat.checkSelfPermission(
                    context,
                    Manifest.permission.BLUETOOTH_SCAN
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                ActivityCompat.requestPermissions(
                    context as Activity,
                    arrayOf(Manifest.permission.BLUETOOTH_SCAN), 1
                )
                return // 未获取权限,终止扫描流程
            }
            Toast.makeText(context, "Scanning was false", Toast.LENGTH_LONG).show()
            handler.postDelayed({
                scanning = false
                bluetoothLeScanner.stopScan(scanCallback)
            }, SCAN_PERIOD)
            scanning = true
            Toast.makeText(context, "Starting Scan therefore", Toast.LENGTH_LONG).show()
            bluetoothLeScanner.startScan(scanFilters,scanSettings,scanCallback)
        } else {
            Toast.makeText(context, "Scanned", Toast.LENGTH_LONG).show()
            scanning = false
            bluetoothLeScanner.stopScan(scanCallback)
        }
    }
}

4. 重复扫描逻辑的冲突

lookingfordevices中使用协程配合Handler会导致重复调度,可能出现多次扫描叠加,干扰正常扫描。优化逻辑如下:

fun lookingfordevices(){
    handler.postDelayed({
        Log.d(TAGs, myDevices.size.toString())
        // 仅当前未扫描时启动新扫描
        if (!scanning) {
            scanLeDevice()
        }
        lookingfordevices()
    },15000)
}

无需使用协程,Handler本身在主线程调度,协程+Handler会造成不必要的线程切换。

5. onScanResult中的UUID判断错误

扫描阶段result.device.uuids为null(该字段仅在设备连接后才会填充),原判断逻辑永远不成立,导致错误提示。需从扫描记录中获取广播的UUID:

override fun onScanResult(callbackType: Int, result: ScanResult) {
    Toast.makeText(context, "Starting onScanResult", Toast.LENGTH_LONG).show()
    var bluetoothGatt: BluetoothGatt? = null
    val device: BluetoothDevice = result.device
    Toast.makeText(context, device.address, Toast.LENGTH_LONG).show()
    myDevices[deviceCounter] = device
    deviceCounter++

    // 权限检查提前
    if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(context as Activity, arrayOf(Manifest.permission.BLUETOOTH_CONNECT), 1)
        return
    }

    // 从扫描记录中获取广播的服务UUID
    val scanRecord = result.scanRecord
    val advertisedUuids = scanRecord?.serviceUuids?.map { it.uuid } ?: emptyList()
    if (advertisedUuids.contains(SERVICE_UUID)) {
        Toast.makeText(context, "Scanresult matches service UUID", Toast.LENGTH_LONG).show()
        bluetoothGatt = device.connectGatt(context, false, bluetoothGattCallback, BluetoothDevice.TRANSPORT_LE)
        Toast.makeText(context, "Connecting to Gatt: "+bluetoothGatt.device.address, Toast.LENGTH_LONG).show()
    } else {
        Log.e(TAGe, "Device does not advertise target service UUID")
        Toast.makeText(context, "Device found but UUID does not match", Toast.LENGTH_LONG).show()
    }
}

6. Android版本权限适配

确保AndroidManifest.xml中添加了对应版本的权限:

<!-- Android 11及以下 -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />

<!-- Android 12+ -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

内容的提问来源于stack exchange,提问作者SeikoFPS

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 06:07:08