资讯动态

HBuilderX蓝牙通讯实战:plus.bluetooth接口从扫描到数据收发全解析

发布时间:2026/10/15 21:16:02 来源:尧图企业网站定制
简介基于HBuilderX实现的HTML5蓝牙通信示例工程面向Web前端与移动端开发者演示如何通过Web Bluetooth API与低功耗蓝牙设备建立连接、读取特征值并完成双向数据收发同时给出常见异常和连接失败的处理思路。项目亲测可用包含蓝牙状态监听、连接管理、数据透传的完整逻辑还附带Android原生蓝牙实现源码和MLT-BT05、JDY-08等透传模块资料软硬件对接时可直接参照。压缩包共258个文件以Java、JavaScript、XML与APK为主另有Gradle构建配置、图片和资源文件整体约11.45MB目录中同时包含可运行APK示例和调试辅助内容便于直接安装验证。目前已有5166人浏览学习适合需要快速搭建BLE通信项目、希望同时掌握Web与原生蓝牙开发路径的开发者参考尤其适合物联网场景下健康监测与智能家居等应用开发。1. HBuilder蓝牙通讯这个Demo凭什么敢说是“亲测可用”做智能硬件控制方向最怕的不是写业务逻辑而是蓝牙链路本身的不确定性。很多开发者习惯性复用PC浏览器里的Web Bluetooth代码结果挪到移动端直接翻车——不是扫描不到设备就是连接上了却收不到数据。这个Demo的定位很明确基于HBuilderX的5 App标准封装直接调用plus.bluetooth原生接口把BLE从初始化到数据收发全链路跑通。它适合正在用HBuilderX开发门锁、灯控、透传模块等项目的开发者新人照着改改UUID就能对接硬件熟手可以直接抽走打包逻辑当基建用。2. 蓝牙选型逻辑为什么必须用HBuilderX的plus.bluetooth而不是Web Bluetooth2.1 浏览器直连BLE的“黑匣子”问题很多初接触物联网的开发者第一反应是去翻Web Bluetooth API的文档。这个API在Chrome桌面浏览器上确实能用但放到实际项目里处处受制。我之前在某实验室帮人整过一个模拟项目X用Chrome的navigator.bluetooth去连一台BLE体脂秤。代码写起来很简洁就那么几行navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [battery_service] }).then(device { return device.gatt.connect(); }).then(server { return server.getPrimaryService(battery_service); }).then(service { return service.getCharacteristic(battery_level); }).then(characteristic { return characteristic.readValue(); }).then(value { console.log(电量:, value.getUint8(0)); });这段代码在笔记本上的Chrome里跑得很欢但一旦挪到移动端App场景就彻底哑火。原因在于Web Bluetooth要求页面必须在HTTPS环境下运行且目前只有Android端的Chrome给了一部分支持iOS全部不支持。Android后台一锁屏蓝牙扫描进程随时可能被系统回收连接也就跟着断了。这就是典型的“黑匣子”——浏览器层封装得太干净开发者根本看不到底层的权限管理、蓝牙状态切换、MTU协商这些真正决定成败的参数。所以在这个Demo里没有去碰Web Bluetooth API而是直接落到了HTML5的plus.bluetooth模块。这才是HBuilderX打包成原生App后我们真正能稳定控制的蓝牙入口。2.2 HTML5 Bluetooth模块的能力边界与选型plus.bluetooth是HBuilderX的5运行时提供的原生能力封装它和Web Bluetooth API有个本质区别它走的是系统级蓝牙框架Android和iOS各走各的原生实现再由统一JS接口暴露给前端。也就是说你写一遍JS逻辑系统帮你适配了两端的差异这正是“H5Blu”这套方案的价值所在。对比项Web Bluetooth APIplus.bluetooth (5 App)运行环境Chrome浏览器、HTTPS强制HBuilderX打包的原生App壳iOS支持不支持支持Android后台稳定性低锁屏易掉高可配合原生APK保活权限配置依赖浏览器弹窗需在manifest.json中声明原生权限MTU协商字面量自动但不可控可感知错误码并做分包处理选型结论很直接只要你是要上架到Android/iOS的应用商店或者要交付给客户装到手机里的正式产品就绕开Web Bluetooth直接用plus.bluetooth。它虽然看起来“不那么时髦”但却是HBuilderX生态里唯一能扛住真机生产环境的方案。2.3 Demo的文件结构与API映射关系拿到这份源码后第一件事不是急着运行而是先看清工程目录。这个Demo延续了HBuilderX里典型的5 App结构主要分三层├── manifest.json // 应用配置权限声明、App图标、启动图 ├── index.html // 入口页面UI布局、事件绑定 ├── js/ │ ├── bluetooth.js // 核心蓝牙API封装层 │ └── protocol.js // 可选数据帧解析与组包新版已集成index.html直接引用了bluetooth.js里的全局对象BluetoothManager。这个管理器把零散的plus.bluetooth接口串成了有状态的流程——从openBluetoothAdapter到startBluetoothDevicesDiscovery再到createBLEConnection每一步都做了状态锁和错误回调。为什么要封装成管理器而不是直接在每个按钮事件里写API因为蓝牙流程是异步且状态机驱动的如果不在中间层统一管理后续维护时会出现大量重复代码而且很容易在“连接中”状态下二次点击操作导致不可预期的错误。这个Demo的价值就在于此它把状态流转从业务层剥离出去了。3. 核心API实战从扫描到数据回传的完整生命周期3.1 第一步初始化蓝牙适配器一切蓝牙操作的前提是适配器可用。很多刚接触的开发者容易栽在这一步直接调用startBluetoothDevicesDiscovery却忘了先开启蓝牙、初始化适配器结果得到一堆莫名其妙的错误码。在Demo里这一步被封装得很谨慎bluetoothManager.openAdapter function(successCallback, failCallback) { if (plus.bluetooth typeof plus.bluetooth.openBluetoothAdapter function) { plus.bluetooth.openBluetoothAdapter({ success: function(res) { console.log(适配器开启成功, res); successCallback successCallback(res); }, fail: function(err) { console.error(适配器开启失败, err); // 常见错误码10001 表示蓝牙未开启 if (err.code 10001) { failCallback failCallback(请先打开系统蓝牙); } else { failCallback failCallback(蓝牙初始化失败代码: err.code); } } }); } else { failCallback failCallback(当前环境不支持蓝牙接口); } };逻辑说明这里先做了能力检测判断plus.bluetooth是否存在而不是假定所有打包环境都一定有蓝牙模块。这可以避免在Android模拟器或某些定制ROM上运行时出现白屏。参数说明openBluetoothAdapter的success返回的res对象里包含isSupportBLE字段用来判断当前设备是否支持低功耗蓝牙。fail回调里的err.code有固定含义——10001是蓝牙未开启10002是系统不支持BLE。后续步骤的排错完全依赖这些错误码所以我在封装时特意把它们转成了人类可读的字符串。3.2 第二步扫描设备与过滤规则蓝牙扫描是异步过程不能像调用普通函数那样指望一次性返回所有结果。Demo里的扫描封装采用了“定时累加自动去重”的模式bluetoothManager.startDiscovery function(duration) { var scanInterval null; var deviceMap {}; // 监听设备发现事件 plus.bluetooth.onBluetoothDeviceFound(function(res) { var devices res.devices || []; devices.forEach(function(device) { var key device.deviceId; // 利用RSSI信号强度过滤掉过于微弱的设备 if (device.RSSI device.RSSI -70) { return; } if (!deviceMap[key]) { deviceMap[key] device; } else { // 更新已存在设备的信号强度避免列表闪烁 deviceMap[key].RSSI device.RSSI; } }); // 触发页面渲染回调 bluetoothManager._renderDeviceList(deviceMap); }); // 开始扫描 plus.bluetooth.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, // 默认去重避免重复上报 interval: 100, // 上报间隔单位毫秒越大越省电 success: function() { console.log(扫描启动成功); }, fail: function(err) { console.error(扫描启动失败, err); } }); // 自动停止扫描防止持续耗电 scanInterval setTimeout(function() { plus.bluetooth.stopBluetoothDevicesDiscovery({ success: function() { console.log(扫描已停止); } }); }, duration || 10000); return scanInterval; };逻辑说明deviceMap是用来做设备去重的核心容器。BLE设备在广播时会周期性重复发送广播包如果不做去重列表里会出现同一个设备刷屏几十条记录的情况。这里用RSSI -70过滤是为了避免把信号很弱、根本无法稳定通信的设备列出来干扰选择。参数说明allowDuplicatesKey设置为false时系统会过滤重复广播设为true则更耗电但能拿到更频繁的RSSI刷新。调试时建议设为true生产环境设false。interval控制设备上报间隔实测100毫秒比较均衡太密集页面频繁刷新会卡顿太稀疏体验不好。3.3 第三步连接设备并遍历服务与特征值扫描到设备只是万里长征第一步。连接成功后真正的工作是找到这个设备暴露了哪些服务、每个服务下有哪些特征值。这个过程可能让新手头晕因为一个BLE设备通常包含多个服务而每个服务又对应多个特征值读写和通知往往分布在不同的特征值上。Demo里的做法是递归遍历全部服务然后打平成一个可索引的映射bluetoothManager.connectAndDiscover function(deviceId, callback) { plus.bluetooth.createBLEConnection({ deviceId: deviceId, timeout: 10000, success: function() { console.log(连接成功:, deviceId); bluetoothManager._discoverServices(deviceId, callback); }, fail: function(err) { console.error(连接失败:, err); // 错误码 10003 表示连接超时或设备不可达 callback callback({ code: err.code || 10003, message: 连接失败 }); } }); }; // 内部方法遍历所有服务下的特征值 bluetoothManager._discoverServices function(deviceId, callback) { var allCharacteristics {}; var serviceIndex 0; var servicesList []; plus.bluetooth.getBLEDeviceServices({ deviceId: deviceId, success: function(res) { servicesList res.services || []; // 遍历服务获取每个服务的特征值 var fetchNextService function() { if (serviceIndex servicesList.length) { // 全部遍历完成 callback callback({ code: 0, services: allCharacteristics }); return; } var service servicesList[serviceIndex]; var serviceId service.uuid; plus.bluetooth.getBLEDeviceCharacteristics({ deviceId: deviceId, serviceId: serviceId, success: function(charRes) { var chars charRes.characteristics || []; allCharacteristics[serviceId] chars; serviceIndex; fetchNextService(); }, fail: function(err) { // 单个服务失败不中断记录后继续下一个 console.warn(获取服务特征值失败:, serviceId, err); serviceIndex; fetchNextService(); } }); }; fetchNextService(); }, fail: function(err) { callback callback({ code: err.code, message: 获取服务列表失败 }); } }); };逻辑说明fetchNextService是一个递归迭代器确保在Android设备上即使某个服务读取失败也不会阻断整个发现流程。很多廉价BLE模组固件写得不太规范读取某些保留服务会返回无权限错误这里选择跳过而不是直接让整个流程崩溃。参数说明createBLEConnection里的timeout参数要特别注意。iOS上默认连接很快10000毫秒足够但Android上如果设备处于深度休眠或距离较远连接可能持续卡顿。设太短容易误判失败设太长又会让用户等得焦躁10秒是折中值。3.4 第四步收发数据与开启NotifyBLE通信中写数据是主动发送读数据通常不用轮询而是通过开启Notify让设备主动上报。这两个场景在Demo里被分开处理因为它们的机制完全不同。// 发送数据将字符串转为ArrayBuffer并写入特征值 bluetoothManager.writeData function(deviceId, serviceId, characteristicId, strData) { // 将字符串按UTF-8编码转为ArrayBuffer var buffer new ArrayBuffer(strData.length); var dataView new DataView(buffer); for (var i 0; i strData.length; i) { dataView.setUint8(i, strData.charCodeAt(i)); } // 注意部分设备要求写入字节长度不能超过MTU-3 plus.bluetooth.writeBLECharacteristicValue({ deviceId: deviceId, serviceId: serviceId, characteristicId: characteristicId, value: buffer, success: function() { console.log(数据写入成功); }, fail: function(err) { console.error(数据写入失败, err); } }); }; // 接收数据开启Notify并监听变化 bluetoothManager.startNotify function(deviceId, serviceId, characteristicId) { plus.bluetooth.notifyBLECharacteristicValueChange({ deviceId: deviceId, serviceId: serviceId, characteristicId: characteristicId, state: true, success: function() { console.log(Notify已开启); } }); // 全局监听设备数据变化 plus.bluetooth.onBLECharacteristicValueChange(function(res) { if (res.deviceId ! deviceId) return; var byteArray new Uint8Array(res.value); var resultStr ; for (var j 0; j byteArray.length; j) { resultStr String.fromCharCode(byteArray[j]); } // 将收到的数据抛给业务层 bluetoothManager._onDataReceived bluetoothManager._onDataReceived(resultStr); }); };逻辑说明写入时使用ArrayBuffer而不是直接传字符串这是HBuilderX官方API的硬性要求。每次发送前先申请一个和字符串等长的字节缓冲区然后逐字节填入避免中文等Unicode字符在编码转换时出现乱码。开启Notify后数据回调是全局的所以回调里必须比对res.deviceId否则多个设备连接时会串数据。参数说明notifyBLECharacteristicValueChange的state是个布尔值true开启false关闭。部分设备在重新连接后需要重新开启Notify否则会收不到任何数据。这种情况在低功耗传感器里特别常见后面避坑章节会详细说。4. 避坑四个经典踩坑案例与问题排查路径4.1 现象Android 10以上扫描不到任何设备现象描述项目在Android 8上开发调试时一切正常升级到Android 10及以上后扫描列表一只空着onBluetoothDeviceFound一次都没触发。原因分析Android 6.0起蓝牙扫描开始要求定位权限Android 10起进一步收紧了规则——必须同时开启定位服务且应用必须动态申请ACCESS_FINE_LOCATION权限。HBuilderX云打包默认只在manifest.json中声明了蓝牙权限并没有把定位权限置为必选。更隐蔽的是小米、华为等厂商ROM还额外加了一道“基于位置的扫描限制”即使权限开了如果定位服务没打开扫描结果依然为空。解决路径在manifest.json的permissions节点中同时加入ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION并在App启动时用plus.android.requestPermissions主动申请定位权限。同时在真机调试时检查定位服务是否处于开启状态。这个坑花了我一整天排查最终发现是ROM自带的“精准定位”开关在作祟。4.2 现象连接成功但无法读写数据错误码10003反复出现现象描述设备能扫描到也能显示已连接但一执行writeBLECharacteristicValue就回调失败错误码10003偶尔伴随“连接中断”。原因分析10003在官方文档里表示连接失败或已断开。实际上在这个场景下往往是因为连接成功后操作系统还没来得及完成服务发现业务层就立刻去写数据。更常见的情况是设备端固件没有正确处理连接事件的时序——它认为连接刚建立但系统底层还处于GATT服务就绪的过渡期。这时候写数据相当于往一扇还没完全打开的门里递东西。解决路径在createBLEConnection的成功回调里不要立即发送数据而是等getBLEDeviceServices的所有特征值遍历完成后再执行写操作。这个Demo里的connectAndDiscover已经做了串行处理但如果自己改过代码千万别把连接和发现拆成并行。另一个原因是上次连接未调用closeBLEConnection释放资源残存的连接句柄导致新连接无法正常收发。调试时每次断开后都手动调用一次closeBluetoothAdapter把整个蓝牙栈重置。4.3 现象收到数据乱码或只有一半字节现象描述设备端发送一串十六进制AA 55 01 02 03App侧收到的resultStr变成了乱码甚至长度都对不上。原因分析BLE的MTU默认只有23字节除去ATT头部的3字节开销真正能装数据的只有20字节。这不是HBuilderX的限制而是BLE 4.0协议栈的默认真实情况。更麻烦的是iOS会自己把MTU协商到更大的值而Android不同机型协商结果五花八门。当你写数据超过MTU长度时大部分Android设备会静默丢弃超出的部分同时如果你把二进制数据转成字符串时用了默认的UTF-8解码某些字节落在非ASCII区就会变成乱码。解决路径发送端严格分包每包控制在20字节以内接收端不要用String.fromCharCode直接使用Uint8Array处理后按十六进制拼接展示。这个Demo里给出的写入函数比较基础实际生产代码我会改成这样function bufferToHexString(buffer) { var bytes new Uint8Array(buffer); var hex []; for (var i 0; i bytes.length; i) { var v bytes[i].toString(16); if (v.length 2) v 0 v; hex.push(v); } return hex.join( ); }这段代码把收到的ArrayBuffer转成十六进制字符串用来排查数据内容能立刻看清协议对不对。如果要在页面上显示中文再单独用TextDecoder解码不要混用字符串转换。4.4 现象iOS上断开后无法再次连接App只能重启现象描述所有操作在iOS 14以下都正常升级到iOS 15后第一次连接读写都成功断开后再次扫描、连接系统弹出了蓝牙配对框但点了之后App直接无响应只有杀掉App重开才能恢复。原因分析这是HBuilderX的5引擎对iOS蓝牙回调处理的一个历史包袱。iOS的CoreBluetooth在系统弹配对框时会打断当前GATT会话而旧版引擎没有正确处理系统级的中断事件导致JS层回调永远等不到结果。属于经典的“弹窗阻塞”问题。解决路径将蓝牙模块运行逻辑脱离标准内置API改用官方最新的uni.connectSocket之类替代方案或者升级到最新的HBuilderX云打包环境刷新引擎版本。另一个临时规避方案是在调用createBLEConnection之前先调用一次closeBluetoothAdapter把之前残留的系统蓝牙会话清干净让系统以全新状态进入连接流程。代码里加个防抖即可代价是每次连接前要多花两秒左右的重置时间。5. 协议组包把Demo改造成能对接自家硬件透传模块的形态5.1 定义一套简洁实用的私有帧协议拿到Demo后最大的痛点就是怎么把它从“示例”变成“能跑业务的代码”。蓝牙透传模块比如常用的JDY-08、CC2541等通常支持AT指令配置和透传模式。透传模式下App写入什么数据模组就原样发出但这不等于你可以直接把裸数据扔进去。没有帧头、长度、校验的话一旦数据隔断或错位接收端完全无法判断这一包数据的完整性。我这里设计了一个简单的帧协议适合控制类设备字段长度说明帧头1字节固定0xAA长度2字节高字节在前表示数据字段长度N命令字1字节如0x01开灯、0x02关灯数据位N字节具体业务数据校验位1字节异或校验对长度到数据位做异或5.2 JavaScript实现封包与解包逻辑以这个协议为基础封装两个核心函数// 封包根据命令字和负载数据生成完整帧 function packFrame(cmd, payload) { var len payload.length; var buffer new ArrayBuffer(len 5); var view new DataView(buffer); view.setUint8(0, 0xAA); // 帧头 view.setUint16(1, len, false); // 长度大端模式 view.setUint8(3, cmd); // 命令字 for (var i 0; i len; i) { view.setUint8(4 i, payload[i]); } // 异或校验从长度字段算到数据字段末尾 var check 0; for (var j 1; j len 4; j) { check ^ view.getUint8(j); } view.setUint8(len 4, check); return buffer; } // 解包从原始字节中提取完整帧并校验 function parseFrame(bytes) { var length bytes.length; if (length 5) return null; // 不足帧头长度校验 var view new Uint8Array(bytes); // 找帧头 var frameStart -1; for (var i 0; i length - 1; i) { if (view[i] 0xAA view[i 1] 0xAA) { // 模拟一个双帧头做数据同步实际使用可去掉 } if (view[i] 0xAA) { frameStart i; break; } } if (frameStart -1) return null; var payloadLen (view[frameStart 1] 8) | view[frameStart 2]; if (frameStart 3 payloadLen 1 length) return null; // 数据不够等待下一包 var dataStart frameStart 3; var check 0; for (var j frameStart 1; j dataStart payloadLen; j) { check ^ view[j]; } if (check ! view[dataStart payloadLen]) { console.warn(校验失败帧数据损坏); return null; } return { cmd: view[dataStart], payload: view.slice(dataStart 1, dataStart 1 payloadLen) }; }逻辑说明packFrame里的view.setUint16(1, len, false)指定了大端序这是绝大多数嵌入式设备的默认字节序。parseFrame里对半包数据的处理才是BLE开发的关键——蓝牙传输是流式的每几十毫秒报一次数一次回调里可能只有半截帧所以要先把帧头找到再判断数据长度是否够不够就返回null等下一次上报的数据拼接。参数说明这里的异或校验比CRC16要轻量得多适合灯光、开关这类实时性要求高的场景。如果设备端已有硬件CRC硬件单元也可以换成CRC16/Modbus原理相同。5.3 断线重连的工程化处理Demo里对断线重连的处理比较基础但实际硬件场景下用户拿起手机设备可能已经休眠了几分钟断线是常态。工程化的做法是引入状态机bluetoothManager.handleConnectionLoss function(deviceId) { // 重置设备状态缓存 delete bluetoothManager._deviceCache[deviceId]; // 尝试重连最多重试3次 var retryCount 0; var maxRetry 3; var attemptReconnect function() { if (retryCount maxRetry) { bluetoothManager._emit(disconnected, { deviceId: deviceId }); return; } retryCount; bluetoothManager.connectAndDiscover(deviceId, function(err) { if (err err.code 0) { // 重连成功恢复Notify状态 bluetoothManager.startNotify(deviceId, serviceId, charId); } else { // 延迟1秒继续尝试 setTimeout(attemptReconnect, 1000); } }); }; attemptReconnect(); };逻辑说明重连时的_deviceCache清理非常重要因为旧的服务信息可能已经失效不清理会导致读写调用拿到过期的UUID。重连成功后必须重新开启Notify因为低功耗蓝牙设备在断开后通常不会再主动上报数据。这里的3次重试和1秒间隔是经验值既不会让用户等太久也不会因为频繁请求把设备端固件搞出连接风暴。参数说明connectAndDiscover如果返回err.code 0代表成功这个约定在Demo里是统一定义的。实际硬件连接中有时候重连失败是因为设备端同时连了其他手机这种场景下重试也没用需要提醒用户手动关掉设备侧连接。6. 真机验证与交付前的最后一道手艺6.1 两个必测的极端场景交付前别急着打正式包先用同样的代码跑两次真机验证。第一测锁屏休眠页面停留在蓝牙首页锁屏等5分钟再解锁看连接是否还在。大多数坑都是因为这个暴露的——iOS会蹦出“蓝牙已断开”的弹窗Android则会静默恢复。第二测连续收发压力用脚本每秒发一包20字节数据连续发300包统计丢包率。6.2 云打包的证书与权限复核用HBuilderX的云打包功能出正式包前建议把manifest.json里的权限列表逐条过一遍。Android端不要勾选不必要的权限比如存储权限如果业务没用到就删掉避免应用市场上架审核被问询。iOS端要确认NSBluetoothAlwaysUsageDescription描述文案是否写清楚了用途。证书方面用测试证书调试正式上架再换发布证书不要混用否则Android应用签名对不上会导致升级安装失败。所有的工程我都习惯放在同一个目录下管理蓝牙模块单独抽离成bluetoothBridge.js这样以后换项目不用删改历史逻辑直接引用这个文件再按硬件需求改改UUID即可。这也是我为什么把这个Demo推荐给做HBuilderX蓝牙开发的人的一个核心理由它是少有的没堆花活、直接给你可复用模块的源码。从那以后我每次交付蓝牙模块都会强制走一遍“锁屏-重连-大数据流”的测试脚本。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑