Android BLE 扫描连接与收发消息实战

张开发
2026/6/7 13:28:04 15 分钟阅读
Android BLE 扫描连接与收发消息实战
BLE 在 Android 里最常见的开发链路其实就三步先扫到设备再连上设备最后通过 GATT 读写特征值收发数据。单看 API 不算多但这套流程有个特点所有步骤几乎都是异步回调而且顺序不能乱。你如果把扫描、连接、发现服务、发消息、收消息全揉在一起代码很快就会变得很难维护。这篇就只讲一条最常用的链路扫描 BLE 设备连接目标设备发现服务和特征值写入消息接收通知消息示例代码用 Kotlin 写目标是先把整个流程跑通。先加权限如果你 target 的是 Android 12 及以上BLE 相关权限一般至少要这些uses-permissionandroid:nameandroid.permission.BLUETOOTH_SCAN/uses-permissionandroid:nameandroid.permission.BLUETOOTH_CONNECT/如果你还要兼容 Android 11 及以下通常还会加uses-permissionandroid:nameandroid.permission.ACCESS_FINE_LOCATIONandroid:maxSdkVersion30/代码里也得在运行时申请privatevalblePermissionsarrayOf(Manifest.permission.BLUETOOTH_SCAN,Manifest.permission.BLUETOOTH_CONNECT)这一步没搞定后面很多 BLE 问题都没法排查。先准备几个 UUIDBLE 通信最终靠的是 service 和 characteristic。假设我们的设备有这样一组 UUIDvalSERVICE_UUID:UUIDUUID.fromString(0000FFF0-0000-1000-8000-00805F9B34FB)valWRITE_UUID:UUIDUUID.fromString(0000FFF1-0000-1000-8000-00805F9B34FB)valNOTIFY_UUID:UUIDUUID.fromString(0000FFF2-0000-1000-8000-00805F9B34FB)valCCCD_UUID:UUIDUUID.fromString(00002902-0000-1000-8000-00805F9B34FB)实际开发里这几个 UUID 要跟硬件协议保持一致。扫描设备Android 扫描 BLE 设备最常用的是BluetoothLeScanner。先拿到蓝牙相关对象valbluetoothManagercontext.getSystemService(Context.BLUETOOTH_SERVICE)asBluetoothManagervalbluetoothAdapterbluetoothManager.adaptervalbluetoothLeScannerbluetoothAdapter.bluetoothLeScanner再定义扫描回调privatevalscanCallbackobject:ScanCallback(){overridefunonScanResult(callbackType:Int,result:ScanResult){valdeviceresult.device Log.d(BLE,scan result:${device.address}name${device.name})}overridefunonScanFailed(errorCode:Int){Log.e(BLE,scan failed:$errorCode)}}开始扫描funstartScan(){valsettingsScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()bluetoothLeScanner.startScan(null,settings,scanCallback)}停止扫描funstopScan(){bluetoothLeScanner.stopScan(scanCallback)}如果你已经知道目标设备地址可以在onScanResult()里匹配后停止扫描然后发起连接privatevarisConnectingfalseprivatevaltargetAddressAA:BB:CC:DD:EE:FFoverridefunonScanResult(callbackType:Int,result:ScanResult){valdeviceresult.deviceif(device.addresstargetAddress!isConnecting){isConnectingtruestopScan()connect(device)}}连接设备BLE 连接入口是connectGatt()privatevarbluetoothGatt:BluetoothGatt?nullfunconnect(device:BluetoothDevice){bluetoothGattdevice.connectGatt(context,false,gattCallback)}这里的gattCallback是核心。扫描、连接、发现服务、写入、通知几乎都靠它串起来。先看一个最基本的版本privatevalgattCallbackobject:BluetoothGattCallback(){overridefunonConnectionStateChange(gatt:BluetoothGatt,status:Int,newState:Int){if(newStateBluetoothProfile.STATE_CONNECTED){Log.d(BLE,connected)gatt.discoverServices()}elseif(newStateBluetoothProfile.STATE_DISCONNECTED){Log.d(BLE,disconnected)bluetoothGatt?.close()bluetoothGattnullisConnectingfalse}}overridefunonServicesDiscovered(gatt:BluetoothGatt,status:Int){if(statusBluetoothGatt.GATT_SUCCESS){Log.d(BLE,services discovered)enableNotification(gatt)}}overridefunonCharacteristicWrite(gatt:BluetoothGatt,characteristic:BluetoothGattCharacteristic,status:Int){Log.d(BLE,write status$status)}overridefunonCharacteristicChanged(gatt:BluetoothGatt,characteristic:BluetoothGattCharacteristic,value:ByteArray){valtextvalue.toString(Charsets.UTF_8)Log.d(BLE,received:$text)}}这段代码把主链路串起来了连上后发现服务服务发现后开启通知写入后收写回调设备主动推送时收通知回调拿到 service 和 characteristic服务发现以后先把我们需要的特征值找出来privatefungetWriteCharacteristic(gatt:BluetoothGatt):BluetoothGattCharacteristic?{valservicegatt.getService(SERVICE_UUID)?:returnnullreturnservice.getCharacteristic(WRITE_UUID)}privatefungetNotifyCharacteristic(gatt:BluetoothGatt):BluetoothGattCharacteristic?{valservicegatt.getService(SERVICE_UUID)?:returnnullreturnservice.getCharacteristic(NOTIFY_UUID)}如果这一步拿不到通常说明 UUID 配错了或者设备根本没暴露这个服务。开启通知收消息BLE 收消息一般不是一直去轮询读而是让设备主动通知。先打开本地通知funenableNotification(gatt:BluetoothGatt){valcharacteristicgetNotifyCharacteristic(gatt)?:returngatt.setCharacteristicNotification(characteristic,true)valdescriptorcharacteristic.getDescriptor(CCCD_UUID)?:returndescriptor.valueBluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(descriptor)}这一步很多人容易漏掉 descriptor。只调用setCharacteristicNotification()往往还不够很多设备必须再写CCCD才会开始真正推送数据。如果设备推消息最终会走到overridefunonCharacteristicChanged(gatt:BluetoothGatt,characteristic:BluetoothGattCharacteristic,value:ByteArray){valhexvalue.joinToString( ){%02X.format(it)}Log.d(BLE,notify:$hex)}如果你设备发的是字符串也可以直接转valtextvalue.toString(Charsets.UTF_8)写入消息如果要给 BLE 设备发消息本质上就是往写特征值里写字节数组。Android 13也就是API 33推荐用新的写法funsendMessage(message:String){valgattbluetoothGatt?:returnvalcharacteristicgetWriteCharacteristic(gatt)?:returnvaldatamessage.toByteArray(Charsets.UTF_8)valresultgatt.writeCharacteristic(characteristic,data,BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)Log.d(BLE,write result$result)}如果你发的是十六进制协议包也可以直接写字节valpacketbyteArrayOf(0xA5.toByte(),0x01,0x02,0x03)gatt.writeCharacteristic(characteristic,packet,BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)设备收到后如果协议设计里会回应一包数据那么这包回应通常会从onCharacteristicChanged()里回来。一个简单的 BLE 管理类把上面的逻辑收一下最起码应该整理成一个 manager而不是散在 Activity 里。classBleManager(privatevalcontext:Context){privatevalbluetoothManagercontext.getSystemService(Context.BLUETOOTH_SERVICE)asBluetoothManagerprivatevalbluetoothAdapterbluetoothManager.adapterprivatevalscannerbluetoothAdapter.bluetoothLeScannerprivatevarbluetoothGatt:BluetoothGatt?nullprivatevarisConnectingfalsevaronMessageReceived:((ByteArray)-Unit)?nullprivatevalscanCallbackobject:ScanCallback(){overridefunonScanResult(callbackType:Int,result:ScanResult){if(!isConnecting){isConnectingtruestopScan()connect(result.device)}}}privatevalgattCallbackobject:BluetoothGattCallback(){overridefunonConnectionStateChange(gatt:BluetoothGatt,status:Int,newState:Int){if(newStateBluetoothProfile.STATE_CONNECTED){gatt.discoverServices()}elseif(newStateBluetoothProfile.STATE_DISCONNECTED){gatt.close()bluetoothGattnullisConnectingfalse}}overridefunonServicesDiscovered(gatt:BluetoothGatt,status:Int){if(statusBluetoothGatt.GATT_SUCCESS){enableNotification(gatt)}}overridefunonCharacteristicChanged(gatt:BluetoothGatt,characteristic:BluetoothGattCharacteristic,value:ByteArray){onMessageReceived?.invoke(value)}}funstartScan(){valsettingsScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()scanner.startScan(null,settings,scanCallback)}funstopScan(){scanner.stopScan(scanCallback)}privatefunconnect(device:BluetoothDevice){bluetoothGattdevice.connectGatt(context,false,gattCallback)}privatefunenableNotification(gatt:BluetoothGatt){valservicegatt.getService(SERVICE_UUID)?:returnvalcharacteristicservice.getCharacteristic(NOTIFY_UUID)?:returngatt.setCharacteristicNotification(characteristic,true)valdescriptorcharacteristic.getDescriptor(CCCD_UUID)?:returndescriptor.valueBluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE gatt.writeDescriptor(descriptor)}funsendMessage(message:String){valgattbluetoothGatt?:returnvalservicegatt.getService(SERVICE_UUID)?:returnvalcharacteristicservice.getCharacteristic(WRITE_UUID)?:returngatt.writeCharacteristic(characteristic,message.toByteArray(Charsets.UTF_8),BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)}funclose(){bluetoothGatt?.disconnect()bluetoothGatt?.close()bluetoothGattnullisConnectingfalse}}这个版本不算完整工业级但已经够你把 BLE 的扫描、连接、收发消息跑通。Activity 里怎么用页面层尽量只做调用不要把 BLE 细节都塞进去。classMainActivity:AppCompatActivity(){privatelateinitvarbleManager:BleManageroverridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)bleManagerBleManager(this)bleManager.onMessageReceived{bytes-valtextbytes.toString(Charsets.UTF_8)runOnUiThread{Log.d(BLE,ui received:$text)}}findViewByIdButton(R.id.btnScan).setOnClickListener{bleManager.startScan()}findViewByIdButton(R.id.btnSend).setOnClickListener{bleManager.sendMessage(hello ble)}}overridefunonDestroy(){super.onDestroy()bleManager.close()}}这时候主流程就通了点扫描扫到设备自动连接发现服务开启通知点发送按钮发消息设备回消息后在通知回调里收到最后几个实战提醒BLE 真写起来最常见的问题一般集中在这几件事权限没申请全UUID 写错扫描时没停扫就连没写CCCD写入太快没做串行控制设备协议不是字符串结果你按字符串解析如果你后面要继续往下做下一步通常就是补这些能力扫描结果过滤和设备列表展示连接状态管理读写操作队列自动重连十六进制协议解析Flow/StateFlow包装 BLE 状态先把扫描、连接、收发消息这条链跑顺再去做这些高级能力会轻松很多。

更多文章