蓝牙都快连上冰箱了,你的鸿蒙 BLE 还写不顺?
大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
一、蓝牙权限:不先搞清这个,你后面全白写
先说个残酷的事实:绝大部分 “怎么蓝牙没反应” 的问题,都死在权限这一步。
鸿蒙对蓝牙做了比较严格的权限控制,不仅有普通蓝牙权限,还有位置 / 扫描相关的权限,这一点跟 Android 有点类似:扫 BLE 必须有定位权限,否则你根本扫不到设备。
1.1 常见蓝牙相关权限
通常 BLE 场景至少会碰到这些权限(名字可能按版本略有调整,但核心意思类似):
ohos.permission.BLUETOOTH—— 基础蓝牙使用ohos.permission.BLUETOOTH_ADMIN/MANAGE_BLUETOOTH—— 扫描、配对、管理ohos.permission.LOCATION—— 扫描 BLE 时获取附近设备- 某些版本上还有数据相关权限,如记录设备信息等
经验结论:
你只声明蓝牙权限而不声明定位权限,很可能扫不到任何 BLE 设备,看日志还以为设备不在线。
1.2 在 module.json5 中声明静态权限
先在 module.json5 里面把该写的“话”跟系统说清楚:
{
"module": {
// ...
"requestPermissions": [
{
"name": "ohos.permission.BLUETOOTH"
},
{
"name": "ohos.permission.MANAGE_BLUETOOTH"
},
{
"name": "ohos.permission.LOCATION"
}
]
}
}
这一步只是静态声明,告诉系统“我这个应用未来可能要用这些权限”,还不等于用户已经授权。
1.3 动态申请权限(否则很多 API 直接给你拒绝)
鸿蒙里典型的动态权限申请流程还是走 abilityAccessCtrl:
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
async function ensurePermission(context, permission: string) {
const atManager = abilityAccessCtrl.createAtManager();
const status = await atManager.checkAccessToken(
context.tokenId,
permission
);
if (status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
return true;
}
const result = await atManager.requestPermissionsFromUser(context, [permission]);
return result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}
export async function ensureBlePermissions(context) {
const perms = [
'ohos.permission.BLUETOOTH',
'ohos.permission.MANAGE_BLUETOOTH',
'ohos.permission.LOCATION'
];
for (const p of perms) {
const ok = await ensurePermission(context, p);
if (!ok) {
console.error(`权限 [${p}] 被拒绝`);
return false;
}
}
return true;
}
建议做法:
- 不要应用一启动就弹一堆权限
- 在用户点击“连接设备”、“搜索设备”等入口时再触发权限申请
- 用户拒绝时,配一个简单的 UI 提示:为什么需要蓝牙 / 定位权限
这样用户体验会好很多,审核也不会揪你。
二、蓝牙扫描 / 连接:从“看见设备”到“握手成功”
权限搞定之后,下一步才轮到 扫描 & 连接。
这部分大多数人是这么经历的:
“扫描没问题,列表里能看到设备,点击连接就开始无限失败。”
通常要么是 过滤条件写错,要么是 连接时机不对,要么是 蓝牙状态没处理。
下面按正常流程来过一遍。
2.1 初始化 BLE 客户端
鸿蒙 BLE 一般会通过蓝牙子系统提供的 client API 来做 GATT 连接(不同版本模块名字略有差异,这里写成通用伪代码风格,重点放在流程和模式上):
import ble from '@ohos.bluetooth'; // 示例名称,具体以实际版本为准
let bleClient: any = null;
function initBleClient() {
if (!bleClient) {
bleClient = ble.createGattClient();
}
}
初始化一般放在:
- App 启动时
- 或点击“进入蓝牙设备页面”时
总之不要每次扫描都 new 一遍,避免资源浪费。
2.2 启动扫描(Scan)
典型扫描流程:
function startScan(onDeviceFound: (device) => void) {
// 监听扫描回调
bleClient.on('scanResult', (result) => {
console.info(`发现设备:${result.deviceName}, ${result.deviceId}`);
onDeviceFound(result);
});
// 开始扫描,可配置过滤条件
bleClient.startScan({
// 可选:serviceUuids: ['your-service-uuid'],
// 可选:deviceName: 'MyDevice'
interval: 1000 // 扫描间隔等配置
});
}
function stopScan() {
bleClient.stopScan();
}
实战建议:
- 加过滤:按设备名 / Service UUID 过滤,否则你会看到一堆乱七八糟的设备
- 扫描时间控制在一定范围(比如 10–20 秒),不要一直扫描到手机发热
2.3 用户点击某个设备 → 发起连接(Connect)
通常你会有一个设备列表,用户点击某一项时去连接:
function connect(deviceId: string) {
return new Promise((resolve, reject) => {
bleClient.connect(deviceId, (err, data) => {
if (err) {
console.error('连接失败:', JSON.stringify(err));
reject(err);
return;
}
console.info('连接成功:', JSON.stringify(data));
resolve(data);
});
});
}
连接成功后,通常要做几件重要的事情:
- 保存 deviceId / 连接句柄,后面读写要用
- 发现服务(discoverServices)
- 找到你关心的 Service / Characteristic UUID
2.4 发现服务(Discover Services)
这一块是后面能不能读写的关键。
function discoverServices(deviceId: string) {
return new Promise((resolve, reject) => {
bleClient.discoverServices(deviceId, (err, services) => {
if (err) {
console.error('发现服务失败:', JSON.stringify(err));
reject(err);
return;
}
console.info('发现服务:', JSON.stringify(services));
resolve(services);
});
});
}
你拿到 services 后,需要:
- 根据 Service UUID 找到对应 service
- 再从该 service 的 characteristics 中找到你要的 Characteristic UUID
大部分 IoT 设备都会在文档里给你这样一段东西:
- Service UUID:
0xFFF0 - Write Characteristic:
0xFFF1 - Notify Characteristic:
0xFFF2
一定要跟产品 / 硬件那一侧确认清楚!
不要自己拍脑袋猜。
三、读取 & 写入特征值:BLE 真正干活的地方
蓝牙 BLE 的真正业务数据交互,是通过 Characteristic(特征值) 完成的。
一旦你拿到:
deviceIdserviceUuidcharacteristicUuid
你才算“挽起袖子真的开始工作”。
3.1 读取特征值(Read Characteristic)
示例:
function readCharacteristic(deviceId: string, serviceUuid: string, charUuid: string) {
return new Promise((resolve, reject) => {
bleClient.readCharacteristicValue(
deviceId,
serviceUuid,
charUuid,
(err, data) => {
if (err) {
console.error('读取特征值失败:', JSON.stringify(err));
reject(err);
return;
}
console.info('读取特征值成功:', JSON.stringify(data));
resolve(data); // 一般 data.value 是 ArrayBuffer
}
);
});
}
记得解析数据:
如果设备返回的是字节流,你要把它和协议对上。比如:
function parseTemperature(buffer: ArrayBuffer): number {
const view = new DataView(buffer);
// 假设第一个字节是温度
return view.getInt8(0);
}
3.2 写入特征值(Write Characteristic)
写入是 IoT 里最常用的动作,比如:
- 开灯 / 关灯
- 设置亮度
- 修改设备模式
function writeCharacteristic(deviceId: string, serviceUuid: string, charUuid: string, payload: Uint8Array) {
return new Promise((resolve, reject) => {
bleClient.writeCharacteristicValue(
deviceId,
serviceUuid,
charUuid,
payload.buffer,
(err, data) => {
if (err) {
console.error('写入特征值失败:', JSON.stringify(err));
reject(err);
return;
}
console.info('写入特征值成功:', JSON.stringify(data));
resolve(data);
}
);
});
}
构造 payload:
// 比如要给设备发送一个简单协议:0xA0 0x01 0x01
const payload = new Uint8Array([0xA0, 0x01, 0x01]);
await writeCharacteristic(deviceId, serviceUuid, writeCharUuid, payload);
请务必跟硬件侧确认通讯协议!
1 个字节写错,设备就会当你在说外星话。
四、BLE Notify 通信:从“我问你答”到“你主动给我推消息”
轮询太土了。BLE 的精华在于 Notify / Indicate ——设备主动推数据。
比如:
- 设备状态改变时及时上报
- IoT 传感器定时发数据(温度 / 湿度 / 电量等)
在鸿蒙 BLE 中,你需要先:
- 开启某个特征值的 Notify
- 监听对应回调
4.1 开启 Notify
function enableNotify(deviceId: string, serviceUuid: string, notifyCharUuid: string) {
return new Promise((resolve, reject) => {
bleClient.setCharacteristicNotification(
deviceId,
serviceUuid,
notifyCharUuid,
true,
(err) => {
if (err) {
console.error('开启 Notify 失败:', JSON.stringify(err));
reject(err);
return;
}
console.info('开启 Notify 成功');
resolve(true);
}
);
});
}
有些协议还要求你向 Descriptor(描述符) 里写特定值(比如 0x01 0x00)才算真正开启通知,这种就得按设备协议来。
4.2 监听 Notify 数据
function subscribeNotify(onData: (data) => void) {
bleClient.on('characteristicChanged', (res) => {
console.info('收到 Notify :', JSON.stringify(res));
// res 里面通常包含 deviceId、serviceUuid、characteristicUuid、value
onData(res);
});
}
解析方式与前面读特征值类似,关键是要根据协议对每个字节做解析。
4.3 前端 UI 上的常见 Notify 玩法
比如做一个 IoT 温湿度设备的实时数据页面:
- 上方:当前温度 / 湿度
- 中间:折线实时刷新
- 下方:设备状态(在线 / 离线)
你可以:
- 在进入页面时自动
enableNotify - 在
onPageHide / aboutToDisappear时取消通知(或者断开连接)
避免一直占用蓝牙资源和电量。
五、常见错误处理:别动不动就“重启手机”
说点现实的:BLE 的失败情况,非常多。
如果你没做错误处理,你只能看到“失败”两个字,然后一脸懵。
下面列几个最常见、也最坑的错误场景。
5.1 扫描不到设备
可能原因:
- 权限不全(尤其是定位权限)
- 蓝牙没打开(有的机型需要手动打开系统蓝牙)
- 过滤条件过于严格(比如写死了错误的 Service UUID)
- 设备根本没广播(很多 IoT 设备要按某个物理按钮才进入广播模式)
实战建议:
- 日志里打印:权限状态 / 蓝牙开关状态
- 提示用户检查设备是否进入配对模式
- 测试时可以用手机 / BLE 调试工具确认设备是否在广播
5.2 连接失败 / 频繁断开
可能原因:
- 设备一次只允许一个连接,你前一个连接没断干净
- 连接过程超时,设备自动断开
- 信号太差(物理环境问题)
- 设备端有连接数 / 会话数限制
建议做法:
- 在页面退出时,记得
disconnect(deviceId) - 出错时提示用户:靠近设备、确认设备电量
- 日志中把错误码打印完整,方便和硬件同学对
5.3 写入失败 / 没反应
可能原因:
- 写错 Service / Characteristic UUID
- 写入数据格式与协议不符
- 写入特征值不支持写(属性 flag 不包含 write)
- 写间隔太短导致设备处理不过来
排查建议:
- 用第 3 方调试工具(nRF Connect 等)先验证协议
- 尽量不要在毫秒级疯狂写入(除非协议明确支持)
- 根据硬件文档确认特征值属性
5.4 Notify 没回调
经典问题,通常出现在:
- 只调用了
setCharacteristicNotification,却没写客户端配置描述符(CCCD) - 设备侧根本没有向该特征值发送 Notify
- Notify 的 UUID 搞错(你以为是这个,实际上是另一个)
最佳实践:
先在 BLE 调试工具里把整套流程走通,再在代码里复刻。
不要只盯着代码怀疑人生,有时候是设备这边没发。
六、IoT 设备接入示例:以“智能灯”场景串一遍全流程
前面说了这么多,最后我们用一个简单但很典型的例子,把整个 BLE 流程串起来:
接入一款 BLE 智能灯设备。
假设硬件给你的协议是这样的(很常见的风格):
-
Service UUID:
0xFFF0 -
写入控制特征:
0xFFF1 -
状态 Notify 特征:
0xFFF2 -
写入命令格式:
- 开灯:
0xA0 0x01 0x01 - 关灯:
0xA0 0x01 0x00 - 调亮度:
0xA1 0x01 <0-100>
- 开灯:
6.1 总体流程图(文字版)
- 检查 / 申请蓝牙 + 定位权限
- 初始化 BLE 客户端
- 扫描设备,过滤名称为 “SmartLamp”
- 用户点击某台灯设备 → 连接
- 发现服务,找到
FFF0 - 记录写入特征
FFF1,Notify 特征FFF2 - 开启 Notify,监听灯状态变化
- 用户在 UI 上操作:开关、调光
- 将对应命令写入到
FFF1 - 灯反馈状态 →
FFF2通知 → 页面 UI 更新
6.2 简化代码示意(流程版,便于理解)
说明:这里不是逐 API 精确代码,而是把流程、结构、调用关系写清楚,便于你在项目里套用。
const SERVICE_UUID = 'FFF0';
const WRITE_CHAR_UUID = 'FFF1';
const NOTIFY_CHAR_UUID = 'FFF2';
class SmartLampBleManager {
private deviceId: string | null = null;
async init(context) {
const ok = await ensureBlePermissions(context);
if (!ok) throw new Error('BLE 权限申请失败');
initBleClient();
}
startScan(onDeviceFound) {
startScan((result) => {
if (result.deviceName === 'SmartLamp') {
onDeviceFound(result);
}
});
}
async connectToDevice(deviceId: string) {
await connect(deviceId);
this.deviceId = deviceId;
const services: any = await discoverServices(deviceId);
// 此处可遍历 services 找到 uuid = FFF0 的 service
await enableNotify(deviceId, SERVICE_UUID, NOTIFY_CHAR_UUID);
subscribeNotify((res) => {
if (res.characteristicUuid === NOTIFY_CHAR_UUID) {
this.handleLampStatus(res.value);
}
});
}
private handleLampStatus(buffer: ArrayBuffer) {
// 根据协议解析,比如第 1 字节代表开关,第 2 字节代表亮度
const v = new DataView(buffer);
const on = v.getUint8(0) === 1;
const brightness = v.getUint8(1); // 0-100
console.info(`灯状态:${on ? '开' : '关'},亮度=${brightness}`);
// 你可以通过状态管理 / 回调通知 UI 更新
}
async turnOn() {
if (!this.deviceId) return;
const payload = new Uint8Array([0xA0, 0x01, 0x01]);
await writeCharacteristic(this.deviceId, SERVICE_UUID, WRITE_CHAR_UUID, payload);
}
async turnOff() {
if (!this.deviceId) return;
const payload = new Uint8Array([0xA0, 0x01, 0x00]);
await writeCharacteristic(this.deviceId, SERVICE_UUID, WRITE_CHAR_UUID, payload);
}
async setBrightness(value: number) {
if (!this.deviceId) return;
const level = Math.max(0, Math.min(100, value));
const payload = new Uint8Array([0xA1, 0x01, level]);
await writeCharacteristic(this.deviceId, SERVICE_UUID, WRITE_CHAR_UUID, payload);
}
disconnect() {
if (this.deviceId) {
bleClient.disconnect(this.deviceId);
this.deviceId = null;
}
}
}
再配合一个简单的 ArkUI 页面:
@Entry
@Component
struct LampPage {
private manager: SmartLampBleManager = new SmartLampBleManager();
@State isOn: boolean = false;
@State brightness: number = 50;
async aboutToAppear() {
await this.manager.init(getContext(this));
this.manager.startScan((device) => {
// 这里你可以展示设备列表,这里直接连接第一个
this.manager.connectToDevice(device.deviceId);
});
}
build() {
Column() {
Button(this.isOn ? '关灯' : '开灯')
.onClick(async () => {
if (this.isOn) {
await this.manager.turnOff();
} else {
await this.manager.turnOn();
}
this.isOn = !this.isOn;
})
Slider({
value: this.brightness,
min: 0,
max: 100
}).onChange(async (value) => {
this.brightness = value;
await this.manager.setBrightness(value);
})
}
}
}
这样,一个完整的 鸿蒙 BLE + IoT 智能灯控制 Demo 流程 就跑通了:
从 权限 → 扫描 → 连接 → 服务发现 → 写入控制 → Notify 状态回传 → UI 反馈 一条龙。
最后来一句真心话 🌈
BLE 在任何平台上都不算“好脾气”的模块:协议要对、UUID 要对、权限要对、时机要对,设备还不能抽风。鸿蒙上 BLE 的整体 API 其实并不算复杂,但是对流程的要求非常严格:权限先到位、状态先判断、再做扫描 / 连接 / 读写 / Notify。
只要你心里有那条清晰的“时间线”:
权限 → 初始化 → 扫描 → 连接 → 发现服务 → 建立读写 / Notify 通道 → 稳定维护连接
再加上一点点和硬件同学“对协议”的耐心,你会发现其实 BLE 开发也没有那么可怕,甚至——还挺有成就感的。
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐


所有评论(0)