BLE 蓝牙开发使用速查
BLE 蓝牙开发使用速查
项目中 BLE 主要用于:
搜索附近的相机
连接相机蓝牙
发现 GATT 服务
读取相机 Wi-Fi 名称和密码
通过蓝牙打开相机 Wi-Fi
接收相机状态通知
最后切换到 Wi-Fi 连接相机
相关代码:
BleScanManager.ets -> BLE 扫描
GattClientManager.ets -> GATT 连接、读写、通知
BluetoothService.ets -> BLE 服务封装
BluetoothUUID.ets -> 服务和特征值 UUID
ConnectDeviceViewModel -> 连接流程和界面状态
1. BLE 连接的完整流程
检查蓝牙是否打开
-> 申请蓝牙权限
-> 开始扫描
-> 过滤相机设备
-> 连接 deviceId
-> 监听连接状态
-> 设置 MTU
-> 发现 GATT 服务
-> 读取或写入特征值
-> 订阅通知
-> 断开并释放 GATT
相机项目中的特殊流程:
BLE 连接相机
-> 判断相机是否已被其他设备连接
-> 判断相机 Wi-Fi 是否打开
-> 如果没打开,通过 BLE 写入开 Wi-Fi 指令
-> 读取 Wi-Fi SSID 和密码
-> 断开 BLE
-> 连接相机 Wi-Fi
2. 检查蓝牙是否打开
import { access } from '@kit.ConnectivityKit';
const isOpen =
access.getState() ===
access.BluetoothState.STATE_ON;
if (!isOpen) {
ToastUtil.showToast('请打开蓝牙');
return;
}
项目封装:
const bleManager =
BleScanManager.instance();
if (!bleManager.isBluetoothOpen()) {
ToastUtil.showToast('请打开蓝牙');
return;
}
蓝牙开关打开不代表已经连接设备:
STATE_ON -> 蓝牙开关已打开
BLE CONNECTED -> 已连接某个 BLE 设备
GATT 服务发现完成 -> 可以读写特征值
3. 蓝牙权限
项目当前使用:
const permissions: string[] = [
'ohos.permission.ACCESS_BLUETOOTH'
];
申请权限:
const atManager =
abilityAccessCtrl.createAtManager();
const result =
await atManager.requestPermissionsFromUser(
context,
['ohos.permission.ACCESS_BLUETOOTH']
);
if (result.authResults[0] !== 0) {
ToastUtil.showToast('没有蓝牙权限');
return;
}
也可以使用项目已有封装:
const granted =
await PermissionUtil.requestPermissionsEasy(
StyleConstants.BLUETOOTH_PERMISSION
);
if (!granted) {
ToastUtil.showToast('请开启蓝牙权限');
return;
}
权限失败时不要直接开始扫描:
// 不建议
ble.startBLEScan(null, options);
正确顺序:
检查蓝牙开关
-> 申请权限
-> 权限成功
-> 开始扫描
不同鸿蒙版本的权限要求可能不同,最终以项目当前 module.json5 和系统版本为准。
4. 开始扫描 BLE 设备
项目扫描入口:
const bleManager =
BleScanManager.instance();
bleManager.startScan();
底层扫描配置:
const scanOptions: ble.ScanOptions = {
interval: 0,
dutyMode:
ble.ScanDuty.SCAN_MODE_LOW_LATENCY,
matchMode:
ble.MatchMode.MATCH_MODE_AGGRESSIVE
};
ble.startBLEScan(
null,
scanOptions
);
扫描参数说明:
SCAN_MODE_LOW_LATENCY -> 更快发现设备,但更耗电
MATCH_MODE_AGGRESSIVE -> 更积极匹配设备
interval=0 -> 尽快接收扫描结果
适合连接相机时使用低延时扫描。
5. 过滤相机设备
不要把所有蓝牙设备都显示出来,应该按设备名称或广播数据过滤。
按设备名称过滤:
const supportedNames: string[] = [
'SJ30',
'C100P',
'C110',
'C300',
'C400',
'SJ6P',
'S10'
];
function isCameraDevice(
deviceName: string
): boolean {
const name =
deviceName.toLowerCase();
return supportedNames.some((item) => {
return name.startsWith(
item.toLowerCase()
);
});
}
扫描回调:
const onDeviceFind = (
reports: Array<ble.ScanResult>
): void => {
reports.forEach((item) => {
if (!item.deviceName) {
return;
}
if (!isCameraDevice(item.deviceName)) {
return;
}
console.info(
`发现相机: ${item.deviceName}`
);
});
};
更可靠的方式是同时判断:
设备名称
厂商数据
Service UUID
可连接状态
6. 扫描结果去重
扫描回调会多次返回同一个设备,不能每次都直接添加。
错误写法:
this.deviceList.push(device);
推荐使用 deviceId 去重:
const exists =
this.deviceList.some((item) => {
return item.deviceId === device.deviceId;
});
if (!exists) {
this.deviceList.push(device);
}
大量设备时使用 Set:
private deviceIdSet =
new Set<string>();
if (!this.deviceIdSet.has(device.deviceId)) {
this.deviceIdSet.add(device.deviceId);
this.deviceList.push(device);
}
扫描列表清空时同时清理:
this.deviceIdSet.clear();
this.deviceList = [];
7. 停止扫描
bleManager.stopScan();
底层实现:
ble.off(
'BLEDeviceFind',
this.onReceiveEvent
);
ble.stopBLEScan();
页面退出时停止扫描:
aboutToDisappear(): void {
this.bleManager.stopScan();
}
不停止扫描可能导致:
持续耗电
设备列表重复刷新
退出页面后仍然更新旧页面
再次进入时重复注册回调
扫描超时:
private scanTimer: number = -1;
startScan(): void {
this.stopScan();
ble.startBLEScan(
null,
scanOptions
);
this.scanTimer = setTimeout(() => {
this.stopScan();
if (this.deviceList.length === 0) {
this.scanTimeout = true;
}
}, 20000) as number;
}
8. 通过 deviceId 连接 GATT
扫描到设备后,使用 deviceId 连接:
const gattClient =
ble.createGattClientDevice(
deviceId
);
gattClient.connect();
项目封装:
const bluetoothService =
BluetoothService.instance();
bluetoothService.connect(
deviceId
);
连接前判断状态:
if (
bluetoothService.connectionState !==
BluetoothState.DISCONNECTED
) {
return;
}
不要重复连接同一个 GATT:
// 不建议
gattClient.connect();
gattClient.connect();
9. 监听 GATT 连接状态
gattClient.on(
'BLEConnectionStateChange',
(stateInfo) => {
switch (stateInfo.state) {
case 0:
console.info('BLE 已断开');
break;
case 1:
console.info('BLE 连接中');
break;
case 2:
console.info('BLE 已连接');
break;
case 3:
console.info('BLE 断开中');
break;
}
}
);
项目状态通常是:
0 -> DISCONNECTED
1 -> CONNECTING
2 -> CONNECTED
3 -> DISCONNECTING
注意:
BLE 已连接 != GATT 服务已经准备好
必须等服务发现完成后才能读写特征值。
10. 设置 MTU
连接成功后,项目会设置 MTU:
const MTU_SIZE = 512;
gattClient.setBLEMtuSize(
MTU_SIZE
);
监听 MTU 协商结果:
gattClient.on(
'BLEMtuChange',
(mtu: number) => {
console.info(
'MTU 已协商: ' + mtu
);
this.discoverServices();
}
);
MTU 的作用:
一次可以传输更多数据
减少拆包次数
适合传输较长的指令或响应
如果设备不支持 512,不要把 MTU 协商失败当成连接失败:
try {
gattClient.setBLEMtuSize(512);
} catch (error) {
LogUtils.warn(
TAG,
`MTU 设置失败: ${JSON.stringify(error)}`
);
}
11. 发现 GATT 服务
连接成功后调用:
const services =
await gattClient.getServices();
完整写法:
async discoverServices(): Promise<void> {
if (!this.gattClient) {
return;
}
try {
const services =
await this.gattClient.getServices();
services.forEach((service) => {
console.info(
`service=${service.serviceUuid}`
);
service.characteristics?.forEach(
(characteristic) => {
console.info(
`characteristic=${
characteristic.characteristicUuid
}`
);
}
);
});
} catch (error) {
LogUtils.error(
TAG,
`服务发现失败: ${JSON.stringify(error)}`
);
}
}
服务发现失败时不要直接读特征值:
// 服务还没发现完成
await readCharacteristic(...);
正确顺序:
connect
-> BLEConnectionStateChange=2
-> setBLEMtuSize
-> getServices
-> 找到目标 service
-> 读写 characteristic
12. 项目中的关键 UUID
项目把 UUID 集中放在:
BluetoothUUID.ets
常见服务:
const BLE_UUID = {
SERVICE_GET_WIFI_INFO:
'SERVICE_WIFI_INFO_UUID',
SERVICE_DEVICE_WIFI_STATUS:
'SERVICE_WIFI_STATUS_UUID',
SERVICE_DEVICE_CONNECT_STATUS:
'SERVICE_CONNECT_STATUS_UUID',
SERVICE_NOTIFY_PAIR:
'SERVICE_PAIR_UUID'
};
常见特征值:
const BLE_CHAR_UUID = {
CHAR_WRITE_OPEN_WIFI:
'CHAR_OPEN_WIFI_UUID',
CHAR_READ_SSID:
'CHAR_READ_SSID_UUID',
CHAR_READ_PASSWORD:
'CHAR_READ_PASSWORD_UUID',
CHAR_CAMERA_CONNECT_STATUS:
'CHAR_CONNECT_STATUS_UUID',
CHAR_PAIR:
'CHAR_PAIR_UUID'
};
实际开发时不要在业务代码里到处写 UUID:
// 不建议
const uuid = 'REAL_UUID_VALUE';
统一引用:
import { BLE_UUID } from './BluetoothUUID';
const serviceUuid =
BLE_UUID.SERVICE_GET_WIFI_INFO;
13. 读取特征值
const characteristic: ble.BLECharacteristic = {
serviceUuid,
characteristicUuid,
characteristicValue: new ArrayBuffer(0),
descriptors: []
};
const result =
await gattClient.readCharacteristicValue(
characteristic
);
const value = new TextDecoder()
.decode(
new Uint8Array(
result.characteristicValue
)
);
项目封装:
const value =
await BluetoothService.instance()
.readCharacteristic(
serviceUuid,
characteristicUuid
);
读取之前检查:
if (
!gattClient ||
this.connectionState !==
BluetoothState.CONNECTED
) {
return '';
}
读不到数据时检查:
设备是否已连接
服务是否发现完成
UUID 是否正确
特征值是否支持 Read
设备是否已经准备好返回数据
14. 写入特征值
写入字符串:
const encoder =
util.TextEncoder.create('UTF-8');
const characteristic:
ble.BLECharacteristic = {
serviceUuid,
characteristicUuid,
characteristicValue:
encoder.encodeInto('1').buffer,
descriptors: []
};
await gattClient.writeCharacteristicValue(
characteristic,
ble.GattWriteType.WRITE
);
项目封装:
const success =
await BluetoothService.instance()
.writeCharacteristic(
serviceUuid,
characteristicUuid,
'1'
);
相机打开 Wi-Fi 的指令通常类似:
await writeCharacteristic(
BLE_UUID.SERVICE_DEVICE_WIFI_STATUS,
BLE_UUID.CHAR_WRITE_OPEN_WIFI,
'1'
);
写入前必须确认:
GATT 已连接
服务已发现
特征值支持 Write
写入内容编码正确
15. 订阅通知或 Indicate
相机需要主动返回状态时,要订阅特征值变化:
const success =
await BluetoothService.instance()
.enableIndication(
serviceUuid,
characteristicUuid
);
底层逻辑:
gattClient.on(
'BLECharacteristicChange',
onCharacteristicChange
);
gattClient.setCharacteristicChangeIndication(
characteristic,
true,
(error) => {
if (error) {
console.error(
'开启 Indicate 失败'
);
return;
}
console.info(
'Indicate 开启成功'
);
}
);
收到数据:
const onCharacteristicChange = (
characteristic: ble.BLECharacteristic
): void => {
const value =
new TextDecoder().decode(
new Uint8Array(
characteristic.characteristicValue
)
);
console.info(
'BLE 通知: ' + value
);
};
注销时必须传入同一个回调:
gattClient.off(
'BLECharacteristicChange',
onCharacteristicChange
);
不要重复注册:
// 不建议每次读写都 on 一次
gattClient.on(
'BLECharacteristicChange',
callback
);
16. 相机 Wi-Fi 状态处理
相机 BLE 通知内容可能分多次返回:
#ST
1
#ED
不能只判断单次回调:
if (value === '1') {
// 可能不完整
}
可以先拼接:
private wifiStateValue: string = '';
onWifiStatusChange(
value: string
): void {
const current =
value.toUpperCase();
this.wifiStateValue += current;
if (
this.wifiStateValue.includes(
'1#ED'
)
) {
this.wifiStateValue = '';
this.getWifiInfoAndConnect();
}
}
特殊相机可能一次返回完整内容:
#ST1#ED
所以需要同时支持:
if (
value === '#ST1#ED' ||
this.wifiStateValue.includes('1#ED')
) {
this.getWifiInfoAndConnect();
}
17. 通过 BLE 打开相机 Wi-Fi
async openCameraWifi(): Promise<void> {
const success =
await this.writeCharacteristic(
BLE_UUID.SERVICE_DEVICE_WIFI_STATUS,
BLE_UUID.CHAR_WRITE_OPEN_WIFI,
'1'
);
if (!success) {
ToastUtil.showToast(
'打开相机 Wi-Fi 失败'
);
return;
}
console.info(
'打开 Wi-Fi 指令发送成功'
);
}
更完整的流程:
订阅 Wi-Fi 状态特征值
-> 写入 "1"
-> 等待相机返回 Wi-Fi 已开启
-> 读取 SSID
-> 读取密码
-> startScan()
-> 连接相机 Wi-Fi
18. 通过 BLE 读取 Wi-Fi 信息
async getCameraWifiInfo(): Promise<void> {
const ssid =
await BluetoothService.instance()
.readCharacteristic(
BLE_UUID.SERVICE_GET_WIFI_INFO,
BLE_UUID.CHAR_READ_SSID
);
const password =
await BluetoothService.instance()
.readCharacteristic(
BLE_UUID.SERVICE_GET_WIFI_INFO,
BLE_UUID.CHAR_READ_PASSWORD
);
if (!ssid || !password) {
ToastUtil.showToast(
'没有读取到相机 Wi-Fi 信息'
);
return;
}
console.info(
`相机 Wi-Fi 名称: ${ssid}`
);
// 不要打印 password
}
读取到信息后,连接 Wi-Fi 前要先扫描:
WifiManager.startScan();
setTimeout(() => {
WifiManager.connect(
ssid,
password,
3,
callback
);
}, 1000);
19. BLE 配对流程
部分相机首次连接时需要配对。
可能收到:
REFUSE
ACCEPT
配对数字
拒绝:
if (value === 'REFUSE') {
ToastUtil.showToast(
'相机拒绝蓝牙配对'
);
return;
}
接受:
if (value === 'ACCEPT') {
this.getWifiInfoAndConnect();
return;
}
收到配对码:
const pairCode =
extractPairCode(value);
if (pairCode) {
showDialogInfo({
title: '验证码',
message: pairCode,
confirmText: '取消连接',
clickConfirm: () => {
this.cancelPair();
}
});
}
取消配对:
await this.writeCharacteristic(
BLE_UUID.SERVICE_NOTIFY_PAIR,
BLE_UUID.CHAR_PAIR,
'PAIR_CANCEL_COMMAND'
);
配对码、设备信息不要写入日志。
20. BLE 断开和资源释放
async disconnect(): Promise<void> {
if (!this.gattClient) {
return;
}
this.gattClient.off(
'BLEConnectionStateChange',
this.onConnectionStateChange
);
this.gattClient.off(
'BLECharacteristicChange',
this.onCharacteristicChange
);
this.gattClient.disconnect();
this.gattClient.close();
this.gattClient = undefined;
this.connectionState =
BluetoothState.DISCONNECTED;
}
停止连接时要清理:
连接超时定时器
连接状态监听
特征值通知监听
GATT client
服务列表
临时 Wi-Fi 状态
配对弹窗
项目中 disconnect() 和 stopScan() 是两件事:
disconnect() -> 断开当前 GATT
stopScan() -> 停止搜索附近设备
根据页面需求决定是否同时调用。
21. 连接超时
private connectTimer: number = -1;
startConnect(deviceId: string): void {
this.connect(deviceId);
this.connectTimer = setTimeout(() => {
if (
this.connectionState !==
BluetoothState.CONNECTED
) {
this.disconnect();
this.onConnectTimeout();
}
}, 80000) as number;
}
连接成功后清理:
clearTimeout(this.connectTimer);
this.connectTimer = -1;
不要每次重试都创建一个新的定时器而不清理。
22. BLE 扫描和连接常见问题
扫描不到设备
检查:
1. 蓝牙开关是否打开
2. ACCESS_BLUETOOTH 权限是否授予
3. 是否调用 ble.startBLEScan
4. 过滤条件是否太严格
5. 相机是否真的在广播
6. 是否已经被其他设备连接
7. 扫描回调是否重复注册或提前移除
能扫描到但连不上
检查:
1. 使用的是 deviceId,不是 deviceName
2. 是否已经有旧 GATT 连接
3. 是否先 stop 了上一次连接
4. 连接状态是否仍是 CONNECTING
5. 是否同时调用了多个 connect
6. 是否超过连接超时时间
GATT 已连接但读不到数据
检查:
1. 是否已经 getServices
2. service UUID 是否正确
3. characteristic UUID 是否正确
4. 当前特征值是否支持 Read
5. 是否等待 MTU 和服务发现完成
收不到通知
检查:
1. 是否调用 setCharacteristicChangeIndication
2. 是否开启成功
3. 是否监听了正确 characteristic
4. 是否重复注册或提前 off
5. 回调中的 UUID 是否是目标 UUID
BLE 已连接但 Wi-Fi 连接失败
检查:
1. 是否成功读取 SSID
2. 是否成功读取密码
3. 连接 Wi-Fi 前是否调用 startScan
4. 是否等待相机 Wi-Fi 真正开启
5. Wi-Fi 连接后是否等待 DHCP
6. 是否绑定 App 到相机 Wi-Fi
23. 脱敏后的完整 BLE 连接案例
async function connectCameraByBle(
context: common.UIAbilityContext
): Promise<void> {
const bluetoothService =
BluetoothService.instance();
if (!bluetoothService.isBluetoothOpen()) {
ToastUtil.showToast('请打开蓝牙');
return;
}
await bluetoothService.startScan(context);
// 实际项目中应从 deviceList 中选择设备
const deviceId =
'BLE_DEVICE_ID';
if (!deviceId) {
ToastUtil.showToast('没有找到相机');
return;
}
bluetoothService.connect(deviceId);
try {
await bluetoothService.waitForConnected(
80000
);
await bluetoothService.enableIndication(
BLE_UUID.SERVICE_DEVICE_WIFI_STATUS,
BLE_UUID.CHAR_WRITE_OPEN_WIFI
);
await bluetoothService.writeCharacteristic(
BLE_UUID.SERVICE_DEVICE_WIFI_STATUS,
BLE_UUID.CHAR_WRITE_OPEN_WIFI,
'1'
);
const ssid =
await bluetoothService.readCharacteristic(
BLE_UUID.SERVICE_GET_WIFI_INFO,
BLE_UUID.CHAR_READ_SSID
);
const password =
await bluetoothService.readCharacteristic(
BLE_UUID.SERVICE_GET_WIFI_INFO,
BLE_UUID.CHAR_READ_PASSWORD
);
if (!ssid || !password) {
ToastUtil.showToast(
'读取相机 Wi-Fi 信息失败'
);
return;
}
bluetoothService.disconnect();
bluetoothService.stopScan();
// BLE 连接 Wi-Fi 前先扫描 Wi-Fi
WifiManager.startScan();
setTimeout(() => {
WifiManager.connect(
ssid,
password,
3,
async (result) => {
if (
result ===
WifiConnectErrorCode
.WIFI_CONNECT_SUCCESS
) {
await WifiManager.bindToCameraWifi();
ToastUtil.showToast(
'相机连接成功'
);
}
}
);
}, 1000);
} catch (error) {
LogUtils.error(
TAG,
`BLE 连接失败: ${JSON.stringify(error)}`
);
bluetoothService.disconnect();
bluetoothService.stopScan();
}
}
隐私字段统一使用:
BLE_DEVICE_ID
CAMERA_BLE_NAME
CAMERA_WIFI_SSID
CAMERA_WIFI_PASSWORD
CAMERA_GATEWAY
SERVICE_UUID
CHARACTERISTIC_UUID
DEVICE_SERIAL
浙公网安备 33010602011771号