简介面向借助HBuilderX完成蓝牙通讯开发的工程师这份可直接运行的HTML5蓝牙示例覆盖了状态监听、设备连接、数据收发全链路并额外提供Android原生蓝牙实现代码便于进行跨端方案对比和实际联调。压缩包共258个文件容量约11.45MB内部主要包括Java代码、JavaScript脚本、XML配置、APK安装包以及PDF、文本格式的蓝牙模块文档Web工程与Android工程分层存放结构清晰易检索。项目已有5166人学习下载在物联网、智能硬件和混合应用方向具有一定参考热度适合初中级开发者深入学习。资源内附BLE调试Demo与JDY-08透传示例能让学习者直观看到蓝牙收发效果同时MLT-BT05模块资料包含技术规格和使用说明配合串口调试工具有助于快速完成软硬件联调降低H5蓝牙项目的入门门槛。1. HBuilderX 实现蓝牙通讯为什么 html5 方案比原生开发更值得先跑通用 HBuilderX 调蓝牙通讯最怕的不是不会写原生代码而是 HTML5 页面打包成 App 之后发现蓝牙 API 根本调不起来。html5-bluetooth-demo 这类项目把路走通了用网页脚本直接完成扫描、连接、读写和 notify 订阅HBuilderX 负责把页面包成 Android、iOS 都能装的壳。这个方案适合两类人一类是手里有 BLE 设备、需要一个快速调试工具的硬件开发另一类是前端工程师被临时拉去接蓝牙不想从 Java 和 Objective-C 学起。它解决的核心问题是把蓝牙这套复杂的状态交互收进几个 JS 函数里。不过要提醒的是浏览器里的 navigator.bluetooth 和 HBuilderX 的 plus.bluetooth 是两条不同路线选错会让“亲测可用”变成“真机翻车”下面从选型开始讲。2. 蓝牙通讯的前置判断navigator.bluetooth 与 plus.bluetooth 的选型边界2.1 Web Bluetooth API 的真实兼容情况Android 的 HTTPS 页面才可靠如果你的 demo 代码里出现的是navigator.bluetooth.requestDevice那它就是标准的 Web Bluetooth API。这套 API 设计得很漂亮用 Promise 把扫描、连接、读写串起来前端很熟悉。但它的运行条件比大多数人想的苛刻首先必须处于 secure context也就是说页面得是 HTTPS或者 localhost 这种被浏览器信任的来源其次是系统 WebView 的版本要足够新很多国产 ROM 的 Android System WebView 停在一个很老的版本上navigator.bluetooth压根不存在最后是 iOS 的 WKWebView 完全不支持这套 APISafari 里也找不到这个对象。所以拿到一个基于 Web Bluetooth 的 demo第一反应不应该是“直接跑”而是先确认运行环境。HBuilderX 的内置浏览器跑的是本地调试服务协议上通常不满足 secure context 的要求打开页面后navigator.bluetooth很可能就是 undefined。这种情况下页面在电脑 Chrome 里正常一上真机就黑问题往往不是代码写的错而是环境不满足。可以先写一行探测代码判断当前浏览器是否暴露了蓝牙对象function checkWebBluetooth() { if (navigator.bluetooth navigator.bluetooth.requestDevice) { console.log(Web Bluetooth API 可用当前是 secure context); return true; } console.warn(navigator.bluetooth 不存在请检查 HTTPS 和浏览器内核版本); return false; }这段代码的意义在于把环境问题从业务逻辑里剥出来。调用requestDevice之前先执行它能省掉后面一大半排查时间。Web Bluetooth 适合的场景是你用 Android 手机里的 Chrome 打开一个 HTTPS 的调试页临时验证设备广播和读写逻辑。它不适合作为 HBuilderX 打包 App 的最终方案因为打包后的页面加载自本地资源协议和内核都不可控。2.2 plus.bluetoothHBuilderX 给 HTML5 页面的原生蓝牙能力HBuilderX 里真正为打包场景准备的是plus.bluetooth它属于 HTML5 规范的一部分。这套 API 不是浏览器标准而是 DCloud 在 5 运行时里通过 JS 桥接原生系统蓝牙模块暴露出来的能力。底层调用的是 Android 的 BluetoothAdapter 和 iOS 的 CoreBluetooth所以 Android、iOS 都能用不需要 HTTPS不需要考虑 WebView 内核版本。调用方式也更贴近原生回调函数加事件监听而不是 Promise。判断当前环境有没有这个模块同样建议在页面启动时做一次探测function checkPlusBluetooth() { if (window.plus plus.bluetooth plus.bluetooth.openBluetoothAdapter) { console.log(plus.bluetooth 可用); return true; } console.warn(plus 环境不存在请在 HBuilderX 真机运行或打包后的 App 中打开); return false; }这里要注意window.plus本身也可能不存在因为 5 运行时只在 App 环境下注入这个对象。如果你是在普通浏览器里直接打开 HTML 文件plus是 undefined这很正常不代表代码有问题。plus.bluetooth 的 API 名称和微信小程序的蓝牙 API 很接近比如openBluetoothAdapter、createBLEConnection、writeBLECharacteristicValue如果你之前写过微信小程序蓝牙迁移成本会非常低。这也是我建议硬件工程师优先学这套 API 的原因它的资料多行为接近原生网上踩坑记录也丰富。2.3 拿到 html5-bluetooth-demo 后的第一步识别代码用的是哪条路线很多人拿到一个蓝牙 demo第一件事是找设备名、改 UUID然后兴奋地点连接结果连“为什么报错”都看不懂。其实拿到代码的第一步应该是按下 CtrlShiftF 全局搜索确定它走的是哪条 API 路线。判断标准很简单看两个关键字出现的频率和上下文。判断点navigator.bluetoothplus.bluetooth出现位置浏览器全局对象plus 命名空间下iOS 支持不支持支持HTTPS 要求必须不需要回调方式Promise回调函数加事件适合场景Android Chrome 调试网页HBuilderX 打包 App搜索时可以直接用这两行作为关键字// 如果满屏都是 navigator.bluetooth这是 Web Bluetooth API 路线 navigator.bluetooth.requestDevice // 如果满屏都是 plus.bluetooth 开头这是 HTML5 原生模块路线 plus.bluetooth.createBLEConnection这个判断决定了你后续所有的改动方向。如果是 Web Bluetooth 路线你要么把它改成 plus.bluetooth要么就放弃打包专心用 Android Chrome 打开 HTTPS 调试页如果是 plus.bluetooth 路线那基本上可以直接往 HBuilderX 打包方向走。还有一种情况是代码里两套 API 都出现了通常是作者做了兼容层两边都调了一遍。这种代码要格外小心因为两套 API 的事件回调、时序行为不同直接混用容易出现在网页里正常、打包后失灵的问题。我的建议是确定目标平台后删掉不用的那条分支保持代码单一。3. 用 HBuilderX 真机跑通蓝牙通讯从新建项目到扫描出第一个设备3.1 HBuilderX 新建 5 App 项目并完成模块配置manifest.json 与权限html5-bluetooth-demo 这类项目通常是一个纯 HTML 页面加几个 JS 文件对应的是 HBuilderX 里的 5 App 项目不是 uni-app 项目。uni-app 是 Vue 语法编译到多端页面结构完全不同而 5 App 项目直接使用 HTML、CSS、JavaScript把页面放进 www 目录就能跑这和普通前端项目的组织方式一致。新建项目的路径是HBuilderX 菜单栏选择“文件 新建 项目”项目类型选“5 App”然后把 demo 的页面文件和资源复制到 www 目录下。项目建好后关键的一步是检查 manifest.json。在 HBuilderX 中双击 manifest.json 会打开可视化配置界面App 模块配置里可以勾选需要的原生模块蓝牙模块要确保是勾选状态。如果是在源码视图下配置里通常会出现蓝牙相关的 permissions 声明具体字段名会随 HBuilderX 版本略有差异。除了蓝牙模块Android 上还有一个隐形的权限要求扫描蓝牙设备需要定位权限Android 6.0 以上系统在 App 申请蓝牙扫描时会连带检查定位服务是否开启。所以 manifest.json 里还要确认位置权限相关配置否则后面扫描回调会一直沉默。真机运行的方式是手机开启 USB 调试连接电脑然后在 HBuilderX 菜单栏选择“运行 运行到手机或模拟器”。HBuilderX 会把项目打包成一个调试 App 装到手机上这个过程中plus环境会被注入plus.bluetooth才能正常工作。第一次真机运行如果有弹窗提示安装 HBuilderX 调试基座直接确认即可。需要注意的是调试基座和正式打包的 App 在蓝牙权限配置上可能不一致如果真机运行正常但打包后异常优先检查打包时的 manifest.json 模块和权限配置。3.2 打开蓝牙适配器与扫描设备startBluetoothDevicesDiscovery 的 3 个关键参数扫描是蓝牙通讯里最容易出错的一步因为它的表现很“玄学”设备明明在广播代码就是不回报。用 plus.bluetooth 扫描标准流程是先openBluetoothAdapter再startBluetoothDevicesDiscovery再注册onBluetoothDeviceFound监听。有一个很容易忽略的点是openBluetoothAdapter的 success 回调里有一个isSupport字段这是判断当前设备是否支持蓝牙的直接依据必须先判断再往下走。function initBluetooth() { if (!window.plus || !plus.bluetooth) { console.error(当前环境不支持 plus.bluetooth); return; } plus.bluetooth.openBluetoothAdapter({ success: function(e) { if (e.isSupport) { console.log(蓝牙适配器已打开开始扫描); startDiscovery(); } else { console.error(当前设备不支持蓝牙); } }, fail: function(err) { console.error(打开蓝牙适配器失败, JSON.stringify(err)); } }); } function startDiscovery() { plus.bluetooth.startBluetoothDevicesDiscovery({ interval: 100, allowDuplicatesKey: true, success: function() { plus.bluetooth.onBluetoothDeviceFound(function(ev) { ev.devices.forEach(function(device) { console.log(发现设备:, device.name, device.deviceId); }); }); }, fail: function(err) { console.error(启动扫描失败请检查蓝牙开关和定位权限, JSON.stringify(err)); } }); }这里三个参数要解释清楚。interval是设备上报的时间间隔单位毫秒默认值是 0表示不限制频率实际使用中 100 毫秒是个稳妥值既能拿到较新的 RSSI又不会让回调过于频繁。allowDuplicatesKey表示是否允许上报重复设备设为 true 才能持续收到同一设备的新广播方便观察信号强度变化如果设为 false同一设备只会报一次。还有第三个参数services它是一个 UUID 数组用来过滤只包含特定服务的设备在 iOS 上这个参数几乎必须传后面避坑章节会细说。扫描到设备后不要依赖device.name做唯一判断很多 BLE 设备广播名是空的或者多个设备共用同一个名字。真正稳定的是deviceId后续连接、读取服务、读写特征值都靠它所以建议在发现设备时就把deviceId和name一起缓存到 Map 里避免后续回调里丢失上下文。3.3 连接设备并获取服务createBLEConnection 之后的时序扫描到设备后调用createBLEConnection发起连接。这一步的坑在于success 回调只代表蓝牙协议层的连接建立了不等于服务的发现已经完成。如果你在 success 回调里立刻去拿服务列表很可能拿到一个空数组。常见做法是连接成功后做一个短延时或者写一个带重试的获取函数。function connectDevice(deviceId) { plus.bluetooth.createBLEConnection(deviceId, { success: function() { console.log(连接成功开始获取服务); setTimeout(function() { getServiceAndCharacteristic(deviceId); }, 300); }, fail: function(err) { console.error(连接失败, JSON.stringify(err)); } }); } function getServiceAndCharacteristic(deviceId) { plus.bluetooth.getBLEDeviceServices(deviceId, { success: function(res) { const services res.services; console.log(发现服务数量:, services.length); // 这里先用第一个服务做演示实际要根据设备的协议确定 const service services[0]; if (!service) { console.error(没有拿到服务可能需要重试); return; } plus.bluetooth.getBLEDeviceCharacteristics(deviceId, service.uuid, { success: function(charRes) { const characteristics charRes.characteristics; characteristics.forEach(function(c) { console.log(特征值:, c.uuid, 属性:, JSON.stringify(c.properties)); }); }, fail: function(err) { console.error(获取特征值失败, JSON.stringify(err)); } }); }, fail: function(err) { console.error(获取服务失败, JSON.stringify(err)); } }); }为什么要在 success 回调里加 300 毫秒延时这其实是蓝牙协议栈的时序问题。BLE 连接建立后GATT 服务发现需要几个 ATT 协议层的请求-响应周期而且不同设备的实现差异很大有些设备固件处理慢300 毫秒都不够。更稳健的做法是封装一个带重试次数的服务获取函数失败后间隔 200 毫秒再试最多试 3 次这比盲目加大延时更可靠。还要注意service.uuid可能是完整的 128 位 UUID也可能是 16 位的短 UUID两者需要归一化处理判断服务时要同时兼容这两种格式。4. 把数据收发和 notify 订阅跑通网页里读一条、收一帧、发一条指令4.1 读特征值的一次性流程与回调接收拿到特征值 UUID 之后读数据就很简单了调用readBLECharacteristicValue结果通过 success 回调返回res.value是一个 ArrayBuffer。需要注意的是这个 API 只适合读一次性数据比如设备版本号、电量等持续的数据流要靠 notify 订阅不能靠轮询读。下面这段代码演示了一次性读取和结果转换const SERVICE_UUID 0000ffe0-0000-1000-8000-00805f9b34fb; const CHARACTERISTIC_UUID 0000ffe1-0000-1000-8000-00805f9b34fb; function readOnce(deviceId) { plus.bluetooth.readBLECharacteristicValue( deviceId, SERVICE_UUID, CHARACTERISTIC_UUID, { success: function(res) { const bytes new Uint8Array(res.value); console.log(读取结果:, bytesToHex(bytes)); }, fail: function(err) { console.error(读取失败, JSON.stringify(err)); } } ); } function bytesToHex(bytes) { return Array.from(bytes) .map(function(b) { return b.toString(16).padStart(2, 0); }) .join( ); }bytesToHex函数建议直接放进工具的公共文件里后面写日志、解析协议都会用到。这里有一个细节res.value的返回类型是 ArrayBuffer但不同版本的 HBuilderX 可能返回ArrayBuffer或Uint8Array稳妥的做法是new Uint8Array(res.value)之前先判断一下res.value instanceof ArrayBuffer不是的话直接用原值。很多“读取结果不对”的反馈其实都是类型转换时把 ArrayBuffer 当成了普通数组。4.2 写入指令writeBLECharacteristicValue 与 ArrayBuffer 的编码坑向设备发送指令是蓝牙调试里最常见的操作但writeBLECharacteristicValue的 value 参数要求是 ArrayBuffer直接传字符串一定会报参数错误。很多设备的指令是十六进制格式比如开关继电器、查询状态这类指令不能简单用字符串发送。我习惯在公共工具里放两个转换函数一个处理 ASCII 字符串一个处理十六进制字符串function stringToArrayBuffer(str) { const encoder new TextEncoder(); return encoder.encode(str).buffer; } function hexToArrayBuffer(hex) { const cleaned hex.replace(/\s/g, ); const tokens cleaned.match(/.{2}/g) || []; return Uint8Array.from(tokens.map(function(token) { return parseInt(token, 16); })).buffer; } function sendCommand(deviceId, command) { const value /^[0-9a-fA-F\s]$/.test(command) ? hexToArrayBuffer(command) : stringToArrayBuffer(command); plus.bluetooth.writeBLECharacteristicValue( deviceId, SERVICE_UUID, CHARACTERISTIC_UUID, value, { success: function() { console.log(指令已发送:, command); }, fail: function(err) { console.error(发送失败, JSON.stringify(err)); } } ); }这段代码做了一个小技巧通过正则判断用户输入的是十六进制串还是普通字符串然后自动选择转换方式。发送指令时还要注意特征值的写属性有些特征值支持带响应写入有些只支持不带响应写入API 层面writeBLECharacteristicValue通常会根据特征值属性自动选择底层调用方式但如果出现设备端无响应的情况就要去核对特征值的 properties 属性。另外一个容易忽略的是 MTU 大小普通 BLE 默认 MTU 是 23 字节去掉协议头后单包只有 20 字节发送长指令时要拆包这部分不在 demo 范围内但做真实设备通讯时一定会遇到。4.3 如果 demo 用的是 Web Bluetooth API等价代码长什么样上面全是 plus.bluetooth 的路子但如果你拿到的 html5-bluetooth-demo 走的是navigator.bluetooth代码会长得完全不一样。Web Bluetooth 用 Promise代码更简洁但它的局限在前面章节已经说过只适合 Android 的 HTTPS 页面。下面是它最核心的连接与订阅代码作为对照方便你识别async function connectWebBluetooth() { const device await navigator.bluetooth.requestDevice({ filters: [{ services: [SERVICE_UUID] }] }); const server await device.gatt.connect(); const service await server.getPrimaryService(SERVICE_UUID); const characteristic await service.getCharacteristic(CHARACTERISTIC_UUID); await characteristic.startNotifications(); characteristic.addEventListener(characteristicvaluechanged, function(event) { const value event.target.value; console.log(收到数据:, new Uint8Array(value.buffer)); }); }注意这里的requestDevice必须在用户手势回调里调用比如按钮的 click 事件否则浏览器会拦截弹窗。另外filters里的 services 如果设备和广播里的服务对不上浏览器会直接报“没有找到匹配设备”。如果你打算在 HBuilderX 打包 App 里用这套代码趁早放弃改成前面的 plus.bluetooth 版本否则 iOS 和部分 Android WebView 会把你卡死在环境问题上。5. HBuilderX 蓝牙通讯避坑指南真机调试最容易翻车的 6 个现场5.1 Android 上搜不到设备定位权限和定位开关都没到位现象startBluetoothDevicesDiscovery的 success 回调执行了但onBluetoothDeviceFound一直不触发或者只触发一次后再无动静。原因Android 6.0 以上蓝牙扫描的底层逻辑需要访问定位服务才能拿到广播结果这是系统层面的行为不是 HBuilderX 能绕过的。很多人只开了蓝牙没开定位或者入库时没在 manifest.json 里声明定位权限扫描自然就沉默了。Android 12 以上还把蓝牙扫描权限拆成了独立的运行时权限需要在系统设置里单独开启。解决真机上把定位服务打开HBuilderX 项目 manifest.json 里声明定位权限并重新打调试基座Android 12 以上还要检查系统设置里的“附近设备”权限是否允许。每次改完权限配置后必须重新打包或重新运行调试基座改 manifest.json 不重装是无效的。5.2 iOS 上扫描列表空白services 过滤参数必须传现象同一套代码在 Android 上能扫到设备换到 iPhone 上列表一片空白蓝牙开关是打开的权限也给了。原因iOS 的 CoreBluetooth 对扫描有严格限制如果startBluetoothDevicesDiscovery没有传services参数系统默认只会扫描广播包里带有 Service UUID 的设备而很多 BLE 设备为了省电广播包里只带设备名和少量标志位不带完整的服务信息。在 Android 上这些设备能出现在结果里在 iOS 上直接就被过滤掉了。解决扫描时把设备的 Service UUID 放到services数组里如果不知道 UUID先看设备手册或咨询固件厂商。实在拿不到完整 UUID 的可以考虑在初始化时用onBluetoothDeviceFound的原始广播数据解析但这也依赖设备广播内容。iOS 上还要注意同时打开系统设置里的蓝牙和定位服务缺一个都扫不到。5.3 连接成功却拿不到服务getBLEDeviceServices 时机错了现象createBLEConnection的 success 回调触发得很顺利紧接着调用getBLEDeviceServices返回的 services 数组是空的或者直接报错误码。原因BLE 协议层连接建立后GATT 服务发现是异步完成的。不同设备的固件实现速度差别很大速度快的一瞬间就完成速度慢的要等几百毫秒甚至更久。连接成功回调并不能保证服务列表已经准备好这是 demo 改成产品时最容易翻车的地方。解决不要直接加一个固定长延时而是封装一个带重试的服务获取函数失败后间隔 200 毫秒再试最多 3 到 5 次。另外连接回调里如果有deviceId之外的连接状态变化要监听onBLEConnectionStateChange防止设备在服务发现过程中断开导致后续调用全部失败。5.4 数据发不出去或收不到ArrayBuffer 与 notify 特征值搞混现象writeBLECharacteristicValue返回成功设备端却没反应或者订阅了 notify 却一直收不到数据。原因第一value 参数传了普通字符串没有转成 ArrayBufferAPI 在类型校验时会直接拒绝。第二写入时选错了特征值有些设备有多个特征值写入通道和通知通道是分开的一个负责收指令一个负责发数据。第三设备端的数据是主动上发的不是查询后返回的这类设备必须订阅 notify 才能收到。解决写操作前打印特征值的properties确认它支持 write订阅前确认支持 notify 或 indicate。发送任何指令前用console.log打印转换后的 ArrayBuffer 内容确认字节数和预期一致。比如发送“01 03 00 00 00 01”时经常有人把字符串转成 ASCII 再传设备收到的就是 18 个字节而不是 6 个字节。5.5 HBuilderX 内置浏览器里 navigator.bluetooth 是 undefined上下文不对现象在 HBuilderX 内置浏览器里打开页面代码报navigator.bluetooth is undefined但同一个页面在 Android Chrome 里能正常弹选择设备窗口。原因内置浏览器的调试服务走的是本地 HTTP 协议不满足 secure context 要求加上内置 WebView 内核版本不支持 Web Bluetooth API所以navigator.bluetooth根本不会被暴露出来。这是环境限制不是代码问题。解决如果只是调试页面用 Android Chrome 打开一个 HTTPS 地址的版本如果目标是打包 App直接改成 plus.bluetooth。不要在 HBuilderX 内置浏览器里纠结 Web Bluetooth那是浪费时间。同理window.plus在普通浏览器里也是 undefined这两套环境的边界要分清楚。5.6 设备掉线后界面无感知onBLEConnectionStateChange 没有全局监听现象设备走远、休眠或关机后App 界面还显示“已连接”直到下一次读写才报连接失败甚至一直不报。原因连接状态变化是系统底层事件如果不在初始化阶段注册onBLEConnectionStateChange监听App 根本拿不到断开通知。demo 里通常不会做这个监听只处理了用户主动点击断开的情况。解决在openBluetoothAdapter成功后立即注册监听在回调里更新全局连接状态并触发重连流程plus.bluetooth.onBLEConnectionStateChange(function(res) { const connected res.state connected; console.log(连接状态变化:, res.deviceId, connected ? 已连接 : 已断开); // 在这里更新页面状态必要时执行重连 });要特别提醒的是这个监听一旦在初始化时注册就会一直存在不需要每次连接都重复注册否则回调会叠加导致一次断开事件触发多次 UI 更新。很多“重复收到提示”的问题就是这么来的。6. 把 demo 加固成能交付的蓝牙调试页状态机、日志与验证清单6.1 用一个状态机管理连接生命周期蓝牙通讯最复杂的是状态交错扫描、连接、服务发现、读写、掉线重连每个阶段的回调都是异步的用散落的回调函数管理很容易出现野状态。我的习惯是维护一个简单的状态机把所有异步回调都收敛到状态更新上const BLE_STATUS { IDLE: idle, SCANNING: scanning, CONNECTING: connecting, CONNECTED: connected, DISCONNECTED: disconnected }; let bleStatus BLE_STATUS.IDLE; function updateStatus(status) { bleStatus status; console.log([BLE] 状态切换为:, status); // 这里统一刷新页面 UI避免在回调里各处改 DOM }每个 API 的 success 回调里第一行就是updateStatus(...)fail 回调里记录错误并且回到 IDLE。这样做最大的好处是日志里能直接看出状态流转顺序比如“connected 被意外切回 connecting”多半是重复点击了连接按钮。在 HBuilderX 里调试时可以按住 Ctrl 点击updateStatus的调用处快速反向查到底哪段代码在改状态。6.2 日志面板ArrayBuffer 转十六进制与 UTF-8 的显示技巧调试蓝牙设备时原始数据几乎都是二进制帧直接打印到控制台会变成乱码。我建议页面上固定留一个日志输出区域所有收发数据统一经过格式化函数再展示。十六进制展示用前面的bytesToHex如果设备返回的是可见字符串就同时展示 UTF-8 解码结果方便对照function logReceived(buffer) { const bytes new Uint8Array(buffer); const hex Array.from(bytes).map(function(b) { return b.toString(16).padStart(2, 0); }).join( ); let text ; try { text new TextDecoder().decode(bytes); } catch (e) { text (不可解码); } console.log(RX , hex, |, text); // 追加到页面日志区域 }这个函数能同时满足两种需求看协议时用十六进制看字符串指令时用 UTF-8。注意TextDecoder在部分老 WebView 里不可用可以降级到手动拼接字符但至少先试一次。6.3 改完代码必跑的 4 个验证场景扫描、连接、指令、掉线每次改动蓝牙逻辑后不要只测“能连上”就收工。我给自己定了一个最小验证清单四个用例跑完才敢合代码表格如下用例操作步骤期望结果扫描点扫描按钮等待 10 秒日志出现设备列表重复广播有更新连接点连接按钮观察状态日志状态由 connecting 变 connected服务列表非空指令发送已知指令比如查电量日志显示发送成功设备端有动作或返回数据掉线关闭设备电源等待 30 秒页面状态自动变为 disconnected不卡在已连接这四件事做完蓝牙链路的基本可靠性就确认了。我现在的习惯是改一次代码就完整跑一遍清单不做这个清单就在 HBuilderX 里随便连一下后续往往会栽在“上次明明好好的”这种假象上。蓝牙调试没有捷径状态日志和数据日志看得越细现场问题越少希望这个思路帮到你。本文还有配套的精品资源点击获取