BLE 在 Android 里最常见的开发链路其实就三步:先扫到设备,再连上设备,最后通过 GATT 读写特征值收发数据。
单看 API 不算多,但这套流程有个特点,所有步骤几乎都是异步回调,而且顺序不能乱。你如果把扫描、连接、发现服务、发消息、收消息全揉在一起,代码很快就会变得很难维护。
这篇就只讲一条最常用的链路:
- 扫描 BLE 设备
- 连接目标设备
- 发现服务和特征值
- 写入消息
- 接收通知消息
示例代码用 Kotlin 写,目标是先把整个流程跑通。
先加权限
如果你 target 的是 Android 12 及以上,BLE 相关权限一般至少要这些:
xml
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
如果你还要兼容 Android 11 及以下,通常还会加:
xml
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
代码里也得在运行时申请:
kotlin
private val blePermissions = arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
这一步没搞定,后面很多 BLE 问题都没法排查。
先准备几个 UUID
BLE 通信最终靠的是 service 和 characteristic。
假设我们的设备有这样一组 UUID:
kotlin
val SERVICE_UUID: UUID =
UUID.fromString("0000FFF0-0000-1000-8000-00805F9B34FB")
val WRITE_UUID: UUID =
UUID.fromString("0000FFF1-0000-1000-8000-00805F9B34FB")
val NOTIFY_UUID: UUID =
UUID.fromString("0000FFF2-0000-1000-8000-00805F9B34FB")
val CCCD_UUID: UUID =
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
实际开发里,这几个 UUID 要跟硬件协议保持一致。
扫描设备
Android 扫描 BLE 设备最常用的是 BluetoothLeScanner。
先拿到蓝牙相关对象:
kotlin
val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter
val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
再定义扫描回调:
kotlin
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
Log.d("BLE", "scan result: ${device.address} name=${device.name}")
}
override fun onScanFailed(errorCode: Int) {
Log.e("BLE", "scan failed: $errorCode")
}
}
开始扫描:
kotlin
fun startScan() {
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
bluetoothLeScanner.startScan(null, settings, scanCallback)
}
停止扫描:
kotlin
fun stopScan() {
bluetoothLeScanner.stopScan(scanCallback)
}
如果你已经知道目标设备地址,可以在 onScanResult() 里匹配后停止扫描,然后发起连接:
kotlin
private var isConnecting = false
private val targetAddress = "AA:BB:CC:DD:EE:FF"
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
if (device.address == targetAddress && !isConnecting) {
isConnecting = true
stopScan()
connect(device)
}
}
连接设备
BLE 连接入口是 connectGatt():
kotlin
private var bluetoothGatt: BluetoothGatt? = null
fun connect(device: BluetoothDevice) {
bluetoothGatt = device.connectGatt(context, false, gattCallback)
}
这里的 gattCallback 是核心。扫描、连接、发现服务、写入、通知,几乎都靠它串起来。
先看一个最基本的版本:
kotlin
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
Log.d("BLE", "connected")
gatt.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.d("BLE", "disconnected")
bluetoothGatt?.close()
bluetoothGatt = null
isConnecting = false
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d("BLE", "services discovered")
enableNotification(gatt)
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
Log.d("BLE", "write status=$status")
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
val text = value.toString(Charsets.UTF_8)
Log.d("BLE", "received: $text")
}
}
这段代码把主链路串起来了:
- 连上后发现服务
- 服务发现后开启通知
- 写入后收写回调
- 设备主动推送时收通知回调
拿到 service 和 characteristic
服务发现以后,先把我们需要的特征值找出来:
kotlin
private fun getWriteCharacteristic(gatt: BluetoothGatt): BluetoothGattCharacteristic? {
val service = gatt.getService(SERVICE_UUID) ?: return null
return service.getCharacteristic(WRITE_UUID)
}
private fun getNotifyCharacteristic(gatt: BluetoothGatt): BluetoothGattCharacteristic? {
val service = gatt.getService(SERVICE_UUID) ?: return null
return service.getCharacteristic(NOTIFY_UUID)
}
如果这一步拿不到,通常说明 UUID 配错了,或者设备根本没暴露这个服务。
开启通知收消息
BLE 收消息一般不是一直去轮询读,而是让设备主动通知。
先打开本地通知:
kotlin
fun enableNotification(gatt: BluetoothGatt) {
val characteristic = getNotifyCharacteristic(gatt) ?: return
gatt.setCharacteristicNotification(characteristic, true)
val descriptor = characteristic.getDescriptor(CCCD_UUID) ?: return
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
这一步很多人容易漏掉 descriptor。
只调用 setCharacteristicNotification() 往往还不够,很多设备必须再写 CCCD 才会开始真正推送数据。
如果设备推消息,最终会走到:
kotlin
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
val hex = value.joinToString(" ") { "%02X".format(it) }
Log.d("BLE", "notify: $hex")
}
如果你设备发的是字符串,也可以直接转:
kotlin
val text = value.toString(Charsets.UTF_8)
写入消息
如果要给 BLE 设备发消息,本质上就是往写特征值里写字节数组。
Android 13,也就是 API 33+,推荐用新的写法:
kotlin
fun sendMessage(message: String) {
val gatt = bluetoothGatt ?: return
val characteristic = getWriteCharacteristic(gatt) ?: return
val data = message.toByteArray(Charsets.UTF_8)
val result = gatt.writeCharacteristic(
characteristic,
data,
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
)
Log.d("BLE", "write result=$result")
}
如果你发的是十六进制协议包,也可以直接写字节:
kotlin
val packet = byteArrayOf(0xA5.toByte(), 0x01, 0x02, 0x03)
gatt.writeCharacteristic(
characteristic,
packet,
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
)
设备收到后,如果协议设计里会回应一包数据,那么这包回应通常会从 onCharacteristicChanged() 里回来。
一个简单的 BLE 管理类
把上面的逻辑收一下,最起码应该整理成一个 manager,而不是散在 Activity 里。
kotlin
class BleManager(
private val context: Context
) {
private val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val bluetoothAdapter = bluetoothManager.adapter
private val scanner = bluetoothAdapter.bluetoothLeScanner
private var bluetoothGatt: BluetoothGatt? = null
private var isConnecting = false
var onMessageReceived: ((ByteArray) -> Unit)? = null
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
if (!isConnecting) {
isConnecting = true
stopScan()
connect(result.device)
}
}
}
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
gatt.close()
bluetoothGatt = null
isConnecting = false
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
enableNotification(gatt)
}
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
onMessageReceived?.invoke(value)
}
}
fun startScan() {
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
scanner.startScan(null, settings, scanCallback)
}
fun stopScan() {
scanner.stopScan(scanCallback)
}
private fun connect(device: BluetoothDevice) {
bluetoothGatt = device.connectGatt(context, false, gattCallback)
}
private fun enableNotification(gatt: BluetoothGatt) {
val service = gatt.getService(SERVICE_UUID) ?: return
val characteristic = service.getCharacteristic(NOTIFY_UUID) ?: return
gatt.setCharacteristicNotification(characteristic, true)
val descriptor = characteristic.getDescriptor(CCCD_UUID) ?: return
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
fun sendMessage(message: String) {
val gatt = bluetoothGatt ?: return
val service = gatt.getService(SERVICE_UUID) ?: return
val characteristic = service.getCharacteristic(WRITE_UUID) ?: return
gatt.writeCharacteristic(
characteristic,
message.toByteArray(Charsets.UTF_8),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
)
}
fun close() {
bluetoothGatt?.disconnect()
bluetoothGatt?.close()
bluetoothGatt = null
isConnecting = false
}
}
这个版本不算完整工业级,但已经够你把 BLE 的扫描、连接、收发消息跑通。
Activity 里怎么用
页面层尽量只做调用,不要把 BLE 细节都塞进去。
kotlin
class MainActivity : AppCompatActivity() {
private lateinit var bleManager: BleManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bleManager = BleManager(this)
bleManager.onMessageReceived = { bytes ->
val text = bytes.toString(Charsets.UTF_8)
runOnUiThread {
Log.d("BLE", "ui received: $text")
}
}
findViewById<Button>(R.id.btnScan).setOnClickListener {
bleManager.startScan()
}
findViewById<Button>(R.id.btnSend).setOnClickListener {
bleManager.sendMessage("hello ble")
}
}
override fun onDestroy() {
super.onDestroy()
bleManager.close()
}
}
这时候主流程就通了:
- 点扫描
- 扫到设备
- 自动连接
- 发现服务
- 开启通知
- 点发送按钮发消息
- 设备回消息后在通知回调里收到
最后几个实战提醒
BLE 真写起来,最常见的问题一般集中在这几件事:
- 权限没申请全
- UUID 写错
- 扫描时没停扫就连
- 没写
CCCD - 写入太快,没做串行控制
- 设备协议不是字符串,结果你按字符串解析
如果你后面要继续往下做,下一步通常就是补这些能力:
- 扫描结果过滤和设备列表展示
- 连接状态管理
- 读写操作队列
- 自动重连
- 十六进制协议解析
Flow/StateFlow包装 BLE 状态
先把扫描、连接、收发消息这条链跑顺,再去做这些高级能力会轻松很多。