d***@qq.com
d***@qq.com
  • 发布:2026-07-18 13:27
  • 更新:2026-07-18 13:27
  • 阅读:9

Uniapp安卓(包含华为)BLE低功耗蓝牙写入时10007,writeBLECharacteristicValue:fail property not support

分类:uni-app

Uni-App BLE 蓝牙通讯跨平台适配经验分享

一、背景说明

这篇文章用于整理当前项目在 uni-app 中做 BLE 蓝牙通讯的实战经验,重点覆盖以下内容:

  • BLE 蓝牙通讯初始化示例代码;
  • BLE 发送命令并等待回包的示例代码;
  • 微信小程序、iOS 环境下的标准流程;
  • 安卓 APP 环境下的通用兼容处理;
  • 华为安卓 APP 环境下的特殊时序兼容处理;
  • 实际踩过的问题,以及最终的处理思路。

当前项目的协议属于典型的“一问一答”模式,核心约定如下:

  • APP 下发 type=cmd
  • 设备回包 type=ack
  • 设备主动事件上报为 type=evt
  • ack 需要回显相同的 cmdreq
  • 当前主控制链路主要使用:
    • E0F00001-0000-1000-8000-00805F9B34FB:控制命令通道,APP 写入;
    • E0F00002-0000-1000-8000-00805F9B34FB:控制响应通道,设备 Notify 回包。

二、推荐的初始化流程

对于 BLE 通讯,推荐把初始化流程固定成下面这个顺序:

  1. 初始化蓝牙模块;
  2. 扫描设备;
  3. 建立连接;
  4. 获取服务;
  5. 协商 MTU;
  6. 获取特征;
  7. 注册 Notify 监听;
  8. 开启响应特征的 Notify;
  9. 等待链路稳定后再发送首条命令。

注意:

  • 微信里和 iOS 基本可以按标准流程直接走;
  • 安卓 APP 需要增加“首条命令延迟发送”的兼容;
  • 华为安卓 APP 还要进一步把“开启 Notify”本身也延后。

三、BLE 初始化代码示例

下面的示例代码是分享思路用的简化版本。

const SERVICE_UUID = 'E0F00000-0000-1000-8000-00805F9B34FB';  
const CHAR_UUIDS = {  
  COMMAND: 'E0F00001-0000-1000-8000-00805F9B34FB',  
  RESPONSE: 'E0F00002-0000-1000-8000-00805F9B34FB',  
};  

let deviceId = '';  
let negotiatedMtu = 20;  
let notifyReadyAt = 0;  
let isFirstCommandAfterConnect = false;  
let pendingReqMap = {};  
let reqSeed = 1;  

/**  
 * 延迟函数  
 * 说明:  
 * 1. BLE 初始化和命令发送时,很多兼容逻辑都依赖显式延时。  
 * 2. 这里单独抽出,便于统一控制所有时序等待。  
 */  
function delay(ms) {  
  return new Promise((resolve) => setTimeout(resolve, ms));  
}  

/**  
 * 注册全局 Notify 监听  
 * 说明:  
 * 1. 设备通过 RESPONSE 特征回传 ack。  
 * 2. 这里拿到回包后,通过 req 去匹配对应的待处理 Promise。  
 * 3. 如果收到 evt,也可以在这里统一分发给页面。  
 */  
function registerNotifyListener() {  
  uni.onBLECharacteristicValueChange((res) => {  
    if ((res.characteristicId || '').toUpperCase() !== CHAR_UUIDS.RESPONSE) {  
      return;  
    }  

    const raw = String.fromCharCode.apply(null, new Uint8Array(res.value));  
    const packet = JSON.parse(raw);  

    if (packet.type === 'ack' && packet.req && pendingReqMap[packet.req]) {  
      const pending = pendingReqMap[packet.req];  
      clearTimeout(pending.timer);  
      delete pendingReqMap[packet.req];  
      pending.resolve(packet);  
    }  

    if (packet.type === 'evt') {  
      console.log('[BLE][evt] 收到设备主动事件:', packet);  
    }  
  });  
}  

/**  
 * 协商 MTU  
 * 说明:  
 * 1. 安卓 / 鸿蒙支持主动设置 MTU。  
 * 2. iOS 通常由系统自动协商,不需要手动设置。  
 * 3. 这里的示例只展示安卓风格写法。  
 */  
function setMtu(currentDeviceId, mtu = 480) {  
  return new Promise((resolve) => {  
    if (!uni.setBLEMTU) {  
      resolve();  
      return;  
    }  

    uni.setBLEMTU({  
      deviceId: currentDeviceId,  
      mtu,  
      success: (res) => {  
        negotiatedMtu = res.mtu || mtu;  
        console.log('[BLE] MTU 协商成功:', negotiatedMtu);  
        resolve();  
      },  
      fail: () => {  
        negotiatedMtu = 20;  
        resolve();  
      },  
    });  
  });  
}  

/**  
 * 标准 BLE 初始化  
 * 说明:  
 * 1. 这个函数展示的是“普通安卓 / 微信 / iOS”的标准初始化流程。  
 * 2. 华为安卓 APP 的特殊处理会放到后面单独说明。  
 * 3. 当前项目中,实际也是在服务发现后再协商 MTU,再找特征,再开 Notify。  
 */  
async function initBleConnection(targetDeviceId) {  
  deviceId = targetDeviceId;  

  await new Promise((resolve, reject) => {  
    uni.createBLEConnection({  
      deviceId,  
      success: resolve,  
      fail: reject,  
    });  
  });  

  // 连接建立后先等一小段时间,避免刚连上时底层状态还不稳定。  
  await delay(300);  

  await new Promise((resolve, reject) => {  
    uni.getBLEDeviceServices({  
      deviceId,  
      success: resolve,  
      fail: reject,  
    });  
  });  

  // 安卓平台建议在发现服务后再协商 MTU。  
  await setMtu(deviceId, 480);  

  await new Promise((resolve, reject) => {  
    uni.getBLEDeviceCharacteristics({  
      deviceId,  
      serviceId: SERVICE_UUID,  
      success: resolve,  
      fail: reject,  
    });  
  });  

  // 先注册监听,避免 Notify 打开后设备太快回包而前端还没开始监听。  
  registerNotifyListener();  

  await new Promise((resolve, reject) => {  
    uni.notifyBLECharacteristicValueChange({  
      deviceId,  
      serviceId: SERVICE_UUID,  
      characteristicId: CHAR_UUIDS.RESPONSE,  
      state: true,  
      success: resolve,  
      fail: reject,  
    });  
  });  

  notifyReadyAt = Date.now();  
  isFirstCommandAfterConnect = true;  
}

四、发送命令代码示例

发送命令时,建议统一封装成“发送 JSON 指令并等待对应 ack”的模式。

/**  
 * 字符串转 ArrayBuffer  
 * 说明:  
 * 1. uni.writeBLECharacteristicValue 需要传 ArrayBuffer。  
 * 2. 这里把 JSON 字符串编码成 Uint8Array 后再取 buffer。  
 */  
function strToArrayBuffer(str) {  
  const encoder = new TextEncoder();  
  return encoder.encode(str).buffer;  
}  

/**  
 * 生成业务报文  
 * 说明:  
 * 1. req 用于把一次请求和对应 ack 做精确匹配。  
 * 2. ts 主要用于诊断日志和设备侧排障。  
 */  
function buildCmdPacket(cmd, payload = {}) {  
  const req = reqSeed++;  
  return {  
    _req: req,  
    ver: '1.0',  
    type: 'cmd',  
    cmd,  
    req,  
    ts: Date.now(),  
    payload,  
  };  
}  

/**  
 * 按 MTU 分包写入  
 * 说明:  
 * 1. 大部分短命令在 MTU=480 时可以单包写完。  
 * 2. 这里仍保留分包逻辑,作为 MTU 协商失败时的兜底。  
 * 3. 安卓平台多包之间建议保留短暂间隔,减少底层写队列拥堵。  
 */  
async function writeInChunks(currentDeviceId, buffer, writeType = 'write') {  
  const chunkSize = Math.max(negotiatedMtu - 3, 20);  
  const bytes = new Uint8Array(buffer);  

  for (let offset = 0; offset < bytes.length; offset += chunkSize) {  
    const chunk = bytes.slice(offset, offset + chunkSize);  

    await new Promise((resolve, reject) => {  
      uni.writeBLECharacteristicValue({  
        deviceId: currentDeviceId,  
        serviceId: SERVICE_UUID,  
        characteristicId: CHAR_UUIDS.COMMAND,  
        value: chunk.buffer,  
        writeType,  
        success: resolve,  
        fail: reject,  
      });  
    });  

    // 安卓多包写入建议保留一个小间隔,降低连续写入过快导致失败的概率。  
    await delay(180);  
  }  
}  

/**  
 * 发送命令并等待 ack  
 * 说明:  
 * 1. 首条命令在安卓 APP 环境通常不要紧接着 Notify 成功后立即发送。  
 * 2. 微信和 iOS 大多数情况下可以按标准流程直接写入。  
 * 3. 这里为了演示,统一保留了“首条命令延迟发送”的处理。  
 */  
async function sendCommand(cmd, payload = {}, timeout = 10000) {  
  const packet = buildCmdPacket(cmd, payload);  
  const req = packet._req;  
  delete packet._req;  

  if (isFirstCommandAfterConnect) {  
    const waitMs = Math.max(notifyReadyAt + 1000 - Date.now(), 0);  
    if (waitMs > 0) {  
      await delay(waitMs);  
    }  
    isFirstCommandAfterConnect = false;  
  }  

  const json = JSON.stringify(packet);  
  const buffer = strToArrayBuffer(json);  

  return new Promise(async (resolve, reject) => {  
    const timer = setTimeout(() => {  
      delete pendingReqMap[req];  
      reject(new Error(`命令 ${cmd} 超时`));  
    }, timeout);  

    pendingReqMap[req] = { resolve, reject, timer };  

    try {  
      await writeInChunks(deviceId, buffer, 'write');  
    } catch (err) {  
      clearTimeout(timer);  
      delete pendingReqMap[req];  
      reject(err);  
    }  
  });  
}  

/**  
 * 示例:读取设备信息  
 * 说明:  
 * 1. 当前项目里最常用的测试命令之一就是 device_info_get。  
 * 2. 这个命令非常适合用来验证“连接成功后整条链路是否已经打通”。  
 */  
async function getDeviceInfo() {  
  const ack = await sendCommand('device_info_get', {});  
  return ack.payload.info;  
}

五、微信小程序与 iOS:按标准流程走即可

从当前项目的实测结果来看:

  • 微信小程序:标准流程稳定;
  • iOS APP:标准流程稳定;
  • 安卓非华为:大多数情况下标准流程也能跑通,但建议保留通用兼容处理。

微信小程序与 iOS 的推荐流程如下:

  1. 连接设备;
  2. 获取服务;
  3. 获取特征;
  4. 开启响应特征 Notify;
  5. 注册监听;
  6. 发送首条业务命令;
  7. 设备返回 ack;
  8. 前端按 req 匹配回包。

标准时序图如下:

APP                                                    设备  
 |                                                      |  
 |-- 连接设备 ----------------------------------------->|  
 |<------------------------- 连接成功 -------------------|  
 |-- 获取服务 ----------------------------------------->|  
 |<------------------------- 服务列表 -------------------|  
 |-- 获取特征 ----------------------------------------->|  
 |<------------------------- 特征列表 -------------------|  
 |-- 开启 E0F00002 Notify ----------------------------->|  
 |<------------------------- Notify 开启成功 ------------|  
 |-- 写 E0F00001: type=cmd ---------------------------->|  
 |                                                      |-- 执行业务逻辑  
 |<===== Notify E0F00002: type=ack =====================|  
 |-- 按 req 匹配 pending 请求并返回结果 ----------------|

六、安卓 APP:需要增加通用兼容处理

虽然安卓非华为很多时候也能按标准流程直接成功,但在真实项目里,建议仍然加上通用兼容逻辑,原因是安卓不同 ROM、不同蓝牙芯片和不同系统版本差异很大。

当前项目对安卓 APP 的通用处理主要包括:

1. 连接成功后先等 300ms

目的:

  • 避免刚建立连接后马上做 GATT 操作,底层状态还没稳定。

2. 发现服务后再协商 MTU

目的:

  • 避免过早协商 MTU 导致某些安卓机型初始化顺序异常。

3. Notify 开启后,首条命令额外延后 1000ms

目的:

  • 减少 CCCD 刚写成功后,立即写命令造成失败的概率。

4. 如果首写失败,再补等到 1600ms 再重试一次

目的:

  • 对偶发的 10007 做一次时序层面的兜底。

5. 多包写入时每包间隔约 180ms

目的:

  • 避免连续快速写入把安卓底层队列打满。

6. 写入失败时尝试切换 writeType

当前项目会在必要时尝试:

  • write
  • omit
  • writeNoResponse

这样做的原因是:

  • 不同安卓机型对 writeType 的兼容表现不完全一致;
  • 某些机型表面上特征支持 write,但实际写入时仍然会表现异常;
  • 多一种候选写法,能提高项目现场的兼容成功率。

七、华为安卓 APP:不是普通安卓问题,而是更强的时序敏感

华为安卓 APP 是当前项目里最典型、也最容易踩坑的平台。

1. 第一阶段遇到的问题

一开始按标准流程:

  1. 连接成功;
  2. 发现特征;
  3. 立即开启 Notify;
  4. 很快发送首条命令;

结果经常出现错误:

  • writeBLECharacteristicValue:fail property not support
  • 错误码 10007

这个现象的关键点是:

  • 设备的 E0F00001 明明支持写;
  • 微信小程序、iOS、小米安卓都能正常工作;
  • 只有华为安卓 APP 在“开 Notify 后很快发送首条命令”时容易失败。

所以问题不在协议本身,而在华为安卓 APP 下的 GATT 时序。

2. 第二阶段验证后的新结论

后续继续做时序测试后发现:

  • 华为并不是“绝对不能开 Notify”;
  • 真正敏感的是“何时开 Notify”和“开完 Notify 后多久发送第一条命令”;
  • 如果调整成更保守的时序,标准 Notify 链路是可以恢复成功的。

3. 华为当前推荐时序

当前项目里,华为安卓 APP 推荐按下面这个顺序走:

  1. 连接设备;
  2. 获取服务;
  3. 协商 MTU;
  4. 获取特征;
  5. 先注册监听;
  6. 发现特征后等待约 2000ms
  7. 开启 E0F00002 的 Notify;
  8. Notify 成功后再等待约 2000ms
  9. 再发送首条业务命令;
  10. 后续按标准 Notify 回包处理。

对应简化时序如下:

APP                                                    设备  
 |                                                      |  
 |-- 连接设备 ----------------------------------------->|  
 |<------------------------- 连接成功 -------------------|  
 |-- 获取服务 / MTU / 特征 ----------------------------->|  
 |<------------------------- 初始化完成 -----------------|  
 |-- 注册监听 ------------------------------------------|  
 |-- 等待约 2 秒 -------------------------------------->|  
 |-- 开启 E0F00002 Notify ----------------------------->|  
 |<------------------------- Notify 开启成功 ------------|  
 |-- 再等待约 2 秒 ------------------------------------>|  
 |-- 写 E0F00001: type=cmd ---------------------------->|  
 |                                                      |-- 执行业务逻辑  
 |<===== Notify E0F00002: type=ack =====================|  
 |-- 按 req 匹配 pending 请求并返回结果 ----------------|

4. 华为兼容代码示例

下面是一个简化后的华为时序示例:

/**  
 * 判断是否属于华为安卓 APP  
 * 说明:  
 * 1. 项目里通常会结合 brand、manufacturer、romName、osName 等信息综合判断。  
 * 2. 这里示例只表达“识别华为家族设备后走更严格的时序”这个思路。  
 */  
function isHuaweiAndroidApp(systemInfo) {  
  const brand = (systemInfo.brand || '').toLowerCase();  
  const platform = (systemInfo.platform || '').toLowerCase();  
  return platform === 'android' && (brand.includes('huawei') || brand.includes('honor'));  
}  

/**  
 * 华为安卓 APP 初始化  
 * 说明:  
 * 1. 和普通安卓相比,关键差异是“先等 2 秒再开 Notify”。  
 * 2. Notify 成功后,还要把首条命令的发送时间继续往后推。  
 */  
async function initBleConnectionForHuawei(targetDeviceId) {  
  const systemInfo = uni.getSystemInfoSync();  

  await initBleConnectionBase(targetDeviceId);  

  if (isHuaweiAndroidApp(systemInfo)) {  
    // 发现特征后,不要马上开 Notify,先等待底层 GATT 队列更稳定。  
    await delay(2000);  

    await new Promise((resolve, reject) => {  
      uni.notifyBLECharacteristicValueChange({  
        deviceId: targetDeviceId,  
        serviceId: SERVICE_UUID,  
        characteristicId: CHAR_UUIDS.RESPONSE,  
        state: true,  
        success: resolve,  
        fail: reject,  
      });  
    });  

    // 记录 Notify 就绪时刻,后面首条命令还要继续延迟。  
    notifyReadyAt = Date.now();  
    isFirstCommandAfterConnect = true;  
  }  
}  

/**  
 * 华为首条命令发送保护  
 * 说明:  
 * 1. 华为不是只延后开启 Notify,还要延后首条命令。  
 * 2. 当前项目里实测可用的是“Notify 成功后再等待约 2 秒”。  
 */  
async function prepareBeforeHuaweiFirstCommand() {  
  const waitMs = Math.max(notifyReadyAt + 2000 - Date.now(), 0);  
  if (waitMs > 0) {  
    await delay(waitMs);  
  }  
  isFirstCommandAfterConnect = false;  
}

八、一次次遇到的问题,以及如何解决

下面按真实踩坑顺序,总结一次次遇到的问题和处理思路。

问题 1:标准流程在华为安卓 APP 下出现 10007

现象:

  • 设备连接成功;
  • 服务、特征都发现成功;
  • 一旦开启 Notify 后很快发送首条命令;
  • 就出现 writeBLECharacteristicValue:fail property not support
  • 命令实际没有写到设备。

排查结果:

  • 设备并不是不支持写;
  • 协议命令也没有错;
  • 因为微信小程序、iOS、小米安卓都正常;
  • 所以问题集中在华为安卓 APP 下的 GATT 时序。

解决办法:

  • 不再把“Notify 开启”和“首条命令发送”挤得太近;
  • 改成更保守的延后时序。

问题 2:只是“开完 Notify 后等 2 秒”仍然不稳定

现象:

  • 有些测试里,仅仅把“开完 Notify 后发送命令”延后到 2 秒,仍然失败;
  • 说明不仅是首条命令时间有问题,Notify 本身的开启时机也有问题。

排查结果:

  • 华为底层可能对 CCCD 写入前后的 GATT 操作都比较敏感;
  • 不能只延后命令发送,还要延后 Notify 的开启时机。

解决办法:

  • 发现特征后先等待约 2000ms
  • 再开启 Notify;
  • Notify 成功后再等待约 2000ms
  • 然后再发首条命令。

问题 3:安卓平台个别机型偶发首写失败

现象:

  • 非华为安卓大多数可以成功;
  • 但个别安卓机型偶发首条命令失败。

排查结果:

  • 很多时候不是协议问题,而是初始化顺序、首写时机、包间隔过快导致。

解决办法:

  • 连接后先等 300ms
  • 服务发现后再协商 MTU;
  • Notify 开启后首条命令默认等 1000ms
  • 如果首写报 10007,再补等到 1600ms 再重试一次;
  • 多包写入时每包间隔约 180ms

问题 4:设备日志出现 GATTS_SendRsp / Sending response failed

现象:

  • 设备日志里可见:
    • BT_GATT: GATTS_SendRsp ... waiting for op_code = 00
    • BT_APPL: Sending response failed

排查结果:

  • 从当前成功样本看,这两条日志并没有阻断最终 ack 返回;
  • 所以它是固件端需要单独排查的问题,但不是当前 APP 成功与否的唯一决定因素。

解决办法:

  • 前端层面先把 BLE 时序兼容处理到位;
  • 固件侧继续检查是否存在“自动响应模式”和“手动响应”冲突问题。

问题 5:客户现场无法提供足够日志

现象:

  • 很多 BLE 问题只在客户手机上出现;
  • 客户通常无法提供完整控制台日志。

解决办法:

  • 在前端维护最近 BLE 关键日志的内存缓冲区;
  • 页面出错时,把系统信息、设备信息、BLE 快照、当前阶段、最近日志统一上传后端;
  • 这样即使用户不会看 console,也能还原失败链路。

九、平台建议总结

当前建议如下:

  • 微信小程序:按标准 BLE 流程开发;
  • iOS APP:按标准 BLE 流程开发;
  • 安卓 APP(非华为):按标准流程开发,但建议保留通用兼容时序;
  • 华为安卓 APP:必须走“延后开 Notify + 延后首发命令”的兼容逻辑;
  • 纯血鸿蒙:还需要继续单独验证。

十、结语

这次 BLE 跨平台兼容实践最重要的经验,不是“协议写对了就一定能通”,而是:

  • BLE 在不同平台、不同 ROM、不同蓝牙栈上的时序表现差异非常大;
  • 微信小程序和 iOS 相对标准;
  • 安卓需要更谨慎地处理首条命令发送时机;
  • 华为安卓 APP 对 Notify 时序尤其敏感;
  • 只要日志留得够完整,问题最终都能从“看起来像协议错了”还原到“其实是平台时序差异”。

如果后续要继续提升稳定性,建议优先做两件事:

  1. 继续验证更多业务指令在华为安卓 APP 下的稳定性;
  2. 继续验证设备主动上报 evt 在当前延后时序下是否同样稳定。

以上经验分享,你可以把以上信息扔给AI,让AI封装一个符合你项目的bleHelper.js工具类,用于处理BLE通讯。

0 关注 分享

要回复文章请先登录注册