简介:面向HBuilderX开发者与物联网初学者的蓝牙通信实战源码包,解决Web页面直接连接BLE设备时常见的扫描、配对、读写与状态监听难题。项目基于HTML5 Web Bluetooth API完成连接管理与数据收发,同时附带Android原生蓝牙实现源码(BluetoothAdapter、BluetoothGatt等),形成Web与原生双路径对照,便于开发者根据稳定性或传输量需求选择方案。包内还提供MLT-BT05、JDY-08透传模块资料与串口调试指引,覆盖从硬件配置到软件联调的关键环节。压缩包共258个文件,约11.45MB,以java、class、xml、js、apk、png、pdf为主:java与class对应原生逻辑及编译产物,xml用于界面布局与工程配置,js承载HBuilderX端交互逻辑,apk为已构建的测试应用可快速安装验证,png为界面截图,pdf为模块手册;目录同时包含Android工程与H5示例,结构清晰便于分层查阅。已有5166人学习下载,适合需要快速落地蓝牙功能、愿意对比学习双端实现的中级前端或Android开发者。
1. 在 HBuilderX 里用 HTML5 页面直连蓝牙:一次真机跑通的通讯方案
上个月接了一个模拟项目X,需求是手机连一块 SPP 串口蓝牙模块,实时接收传感器数据并在页面上展示。我第一反应是写原生 Android 工程,折腾了两天连环境都没理清;后来换到 HBuilderX 里用 HTML5 页面加原生调用桥,一个晚上就把收发链路完整跑通了。这个标题里的 html5-bluetooth-demo,核心思路就是把蓝牙能力通过 5+ Runtime 暴露给网页,让前端直接操作蓝牙适配器、扫描设备、建立 Socket 连接并收发数据。它最适合的场景是做嵌入式调试工具、串口透传、蓝牙外设测试页这类以 Android 为主、不想单独维护原生工程的工具型应用。有一点先说明白:这套方案在 Android 上亲测可用,在 iOS 上基本走不通,选型之前必须先看清平台边界。
2. HBuilderX 蓝牙通讯的三条技术路线:选型决定你能不能跑通
很多开发者在 HBuilderX 里搜蓝牙,第一个动作是去找插件市场,其实 HTML5 页面本身就有三条路可以走。区别在于你连的是哪一类设备:经典蓝牙串口(SPP)还是低功耗蓝牙(BLE)。选错了路线,后面所有代码都是白写。
2.1 三条路的能力边界:SPP、BLE 与平台支持差异
我按实际踩过的情况把三条路线整理成一张表,先看清楚再动手:
| 技术路线 | 平台支持 | SPP 串口 | BLE 低功耗 | 接入复杂度 |
|---|---|---|---|---|
| Web Bluetooth API | Android Chrome / 部分 WebView | 不支持 | 支持 | 低,纯前端 |
| Native.js(plus.android) | 仅 Android | 支持 | 支持 | 中,需懂 Android 蓝牙 API |
| uni-app 蓝牙 API | Android / iOS | 基本不支持 | 支持 | 低,官方封装 |
Web Bluetooth API 就是浏览器里的 navigator.bluetooth 对象,写起来最像普通网页,但 iOS Safari 一直不支持,HBuilderX 自带的 WebView 也要看系统 WebView 内核是否打开相关开关。uni-app 的蓝牙 API 跨平台能力强,却只覆盖 BLE 特征值读写,你拿它连那种老式的 SPP 串口模块,基本没有现成通道。Native.js 是 HBuilderX 5+ Runtime 给 Android 开的桥,能把 Java 层的蓝牙适配器、广播接收器、Socket 流直接暴露给 JS 调用,SPP 和 BLE 都能做。所以我的结论很直接:如果你要连的是单片机串口模块、血压计、老式蓝牙转串口设备,走 Native.js;如果目标是手环、Beacon 这类低功耗传感器,优先 uni-app 蓝牙 API;iOS 想用 HTML5 一锅端,趁早换原生规划。
2.2 为什么 HTML5 页面能拿到原生蓝牙能力:5+ Runtime 的桥接机制
要理解 html5-bluetooth-demo 能跑通,得先明白一个关键点:不是浏览器实现了蓝牙,而是 HBuilderX 的 5+ Runtime 在页面和 Java 层之间搭了一座桥。页面里的 JS 通过 plus.android.importClass 把 Java 类引用拿到手,然后像调用普通对象一样调用静态方法、实例方法,甚至可以直接注册 Android 的广播接收器来响应蓝牙扫描结果。这意味着你写的 HTML5 页面,本质上跑在一个开放了原生接口的 WebView 里,而不是沙箱里的普通网页。
这座桥最大的价值是省掉了原生工程。你不用建 Gradle 工程、不用写 Java 类、不用处理 Activity 生命周期,只要页面里 importClass 正确,Java 对象的返回值会被包装成 JObject,继续在 JS 里链式调用。代价是调试起来比原生麻烦:Java 层抛的异常大多只显示在真机日志里,页面上的 console.log 看不到,需要额外看 Logcat。所以后面第 4 章的代码我会尽量把每一步的验证点写清楚,避免你把时间耗在“代码看起来没问题但就是不跑”的排查上。
2.3 demo 的项目结构与权限准备
一个完整的 html5-bluetooth-demo,项目结构不需要复杂,三样东西就够:index.html 放页面和按钮,js/bluetooth.js 放通讯逻辑,manifest.json 做权限声明。很多人卡在第一步是权限没配全:Android 6.0 以后扫描蓝牙设备必须要有定位权限,Android 12 以后还多了 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT 这两个附近设备权限。只声明一个蓝牙权限是扫不到设备的。
打开 manifest.json 的源码视图,在 app-plus 节点下把权限声明补齐:
{ "app-plus": { "distribute": { "android": { "permissions": [ "android.permission.BLUETOOTH", "android.permission.BLUETOOTH_ADMIN", "android.permission.ACCESS_FINE_LOCATION", "android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT" ] } } } }这段配置的作用是让 HBuilderX 在打包或生成自定义基座时,把这些权限合并进 Android 的 AndroidManifest.xml。不同 HBuilderX 版本的 manifest 结构可能有轻微差异,但核心逻辑是一致的:找到 app-plus 下 Android 分发配置,把权限声明的数组补全。注意这里只是“清单声明”,Android 6.0 以后运行时还要动态请求,尤其是定位权限,这部分我在下一章展开。
3. 在 HBuilderX 里搭出最小可用工程:5+ App 项目、权限申请与真机运行
工程搭得好不好,直接影响后面排错效率。我见过不少人在一个默认模板里直接塞蓝牙代码,结果权限、页面、逻辑全混在一起,出了错都不知道看哪。下面这套结构是我验证过、适合做蓝牙调试工具的最小工程。
3.1 创建 5+ App 项目与页面骨架
打开 HBuilderX,新建项目时选择 5+ App 模板,这一步会生成带 manifest.json 的标准工程目录。我习惯把页面留在 index.html,蓝牙相关方法单独放 js/bluetooth.js,方便以后把同一个调试页迁移到别的项目里。
先写一个最小的页面骨架,至少要有三个东西:状态输出区、操作按钮、日志显示。日志区一定要做,蓝牙调试看不到日志等于盲写:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>蓝牙调试页</title> </head> <body> <div style="padding:12px;"> <button onclick="openBluetooth()">打开蓝牙</button> <button onclick="startScan()">扫描设备</button> <button onclick="disconnect()">断开连接</button> <pre id="log" style="white-space:pre-wrap;margin-top:12px;">等待操作...</pre> </div> <script> var logBox = document.getElementById("log"); function log(msg) { logBox.textContent += new Date().toLocaleTimeString() + " " + msg + "\n"; } </script> <script src="js/bluetooth.js"></script> </body> </html>页面这里的重点是给后续代码预留三个按钮:打开蓝牙、扫描设备、断开连接。log 函数把每次操作的结果追加到页面底部,时间戳能帮你判断是操作没触发还是回调没返回。js/bluetooth.js 在页面底部引入,保证 DOM 先就绪,蓝牙逻辑文件里再定义 openBluetooth、startScan 这些函数。
3.2 动态申请蓝牙与定位权限:Android 6 与 Android 12 的差异
权限声明写完不等于就绪。Android 6.0 以后定位权限是运行时权限,不弹窗申请直接调用蓝牙扫描,系统会静默拒绝;Android 12 以后,BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT 也需要动态申请。在 5+ Runtime 里,用 plus.android.requestPermissions 统一处理:
function requestBtPermissions() { var permissions = [ "android.permission.ACCESS_FINE_LOCATION", "android.permission.BLUETOOTH_SCAN", "android.permission.BLUETOOTH_CONNECT" ]; plus.android.requestPermissions(permissions, function(result) { var granted = result.granted || []; if (granted.length < permissions.length) { log("部分权限未授权,扫描可能失败"); } else { log("蓝牙相关权限已就绪"); } }, function(error) { log("权限请求失败:" + JSON.stringify(error)); } ); } // 页面加载完成后先申请权限 document.addEventListener("plusready", requestBtPermissions);这里有几个参数要解释一下。result.granted 是授权的权限名数组,用它和请求时的 permissions 数组做长度对比,不要只判断接口是否回调成功,因为用户可能取消某个权限。plusready 事件是 5+ Runtime 环境就绪的标志,所有 plus.android 调用必须等它触发后再执行,否则会报 plus is not defined。如果你的应用是直接加载到手机基座里跑的,这个事件通常已经触发过了,但保留这段逻辑能避免在浏览器预览时崩溃。
3.3 真机运行与第一次验收
权限就绪后,连接 Android 真机,在 HBuilderX 里选择运行到手机。这里有个容易被忽略的细节:首次运行用的是标准基座,它只包含默认权限。如果你在第 2 章加了自定义权限声明,标准基座不会自动带上,需要先打一个自定义调试基座,再运行到手机上。否则你会碰到权限配置了但运行时报 SecurityException 的怪事。
第一次验收不要急着扫描设备,先验证蓝牙适配器能不能拿到。在 js/bluetooth.js 里加一段初始化代码:
var BluetoothAdapter = plus.android.importClass("android.bluetooth.BluetoothAdapter"); var adapter = BluetoothAdapter.getDefaultAdapter(); function getAdapterState() { if (!adapter) { log("当前设备没有蓝牙模块"); return; } log("蓝牙是否开启:" + adapter.isEnabled()); }如果页面能输出“蓝牙是否开启:true”,说明 importClass 成功、适配器对象拿到了、权限也没卡住。这一步通过后,再往下走扫描和连接才有意义。很多开发者跳过这个验收直接写连接代码,出了问题根本分不清是适配器没拿到还是 Socket 建立失败。
4. 核心收发包:用 Native.js 调 SPP 蓝牙的最小实现
整个 demo 的核心都在这一章。SPP 蓝牙通讯的逻辑链路是:打开蓝牙 → 扫描设备 → 选择设备建立 Socket → 通过输入输出流收发数据。每一步在 Android 原生层都是一个标准动作,在 Native.js 里只是把调用方式从 Java 换成 JS。
4.1 打开蓝牙:getDefaultAdapter 与 enable 的返回值陷阱
打开蓝牙在原生层很简单,BluetoothAdapter.getDefaultAdapter().enable() 一行。但这里有个容易误判的点:enable() 的返回值不等于蓝牙已经打开,它只表示系统是否接收了打开请求,真正的状态变化要监听 STATE_ON 广播。不过对于调试页来说,轮询 isEnabled() 就够了:
function openBluetooth() { if (!adapter) { log("当前设备没有蓝牙模块"); return false; } if (adapter.isEnabled()) { log("蓝牙已经是开启状态"); return true; } var result = adapter.enable(); log("enable 返回:" + result + ",等待系统弹窗确认"); return result; }这里注意 enable() 在部分国产 ROM 上可能弹不出系统确认框,表现为返回 true 但蓝牙没开。遇到这种设备,常见做法是跳系统设置页让用户手动打开:
var Intent = plus.android.importClass("android.content.Intent"); var mainActivity = plus.android.runtimeMainActivity(); var settingsIntent = new Intent("android.settings.BLUETOOTH_SETTINGS"); mainActivity.startActivity(settingsIntent);这段代码放在 enable() 失败后作为兜底。runtimeMainActivity() 拿到的是当前 5+ 的宿主 Activity,startActivity 是它自带的方法,直接传 Intent 即可。
4.2 扫描附近设备:广播接收器与设备列表
Android 蓝牙扫描是异步的过程,结果通过系统广播返回。在 JS 里要做的,是用 plus.android.registerBroadcastReceiver 注册一个广播接收器,监听蓝牙设备发现动作:
var receiverId = null; function startScan() { if (!adapter || !adapter.isEnabled()) { log("请先打开蓝牙"); return; } // 先取消上次扫描,避免重复触发 if (adapter.isDiscovering()) { adapter.cancelDiscovery(); } receiverId = plus.android.registerBroadcastReceiver({ action: "android.bluetooth.device.action.FOUND" }, function(intent) { var device = intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE"); var name = device.getName(); var address = device.getAddress(); log("发现设备:" + name + " (" + address + ")"); }, function(err) { log("广播注册失败:" + JSON.stringify(err)); }); adapter.startDiscovery(); log("开始扫描,持续约 12 秒"); }这段代码要解释几个关键点。intent.getParcelableExtra 是 Java 方法,Native.js 桥接后可以直接调用,不需要 importClass;返回的 device 对象自动被包装成 JObject,getName 和 getAddress 都是实例方法。广播接收器的回调是异步触发的,每发现一个设备就回调一次,所以设备列表的展示逻辑要放在回调里而不是 startDiscovery 之后。action 字符串这里用全限定名,不同基座版本对这个字符串的兼容性不太一样,如果在你的环境里收不到回调,可以改成不带包名前缀的写法试一次,但不要同时注册两个接收器。
扫描结束的广播是 android.bluetooth.device.action.DISCOVERY_FINISHED,需要的话可以再注册一个接收器来更新 UI 状态,demo 里一般用不到,但做完整工具页时建议加上。
4.3 建立 BluetoothSocket 连接:SPP UUID 与阻塞式 connect
扫描到设备后,连接的核心调用是 createRfcommSocketToServiceRecord,它需要传一个 UUID。SPP 服务的标准 UUID 是 00001101-0000-1000-8000-00805F9B34FB,绝大多数串口蓝牙模块都走这个值。有些设备自定义了 UUID,那就得问模块厂商要具体的服务 UUID。
var SPP_UUID = "00001101-0000-1000-8000-00805F9B34FB"; var deviceList = {}; function connectDevice(address) { if (!address) { log("缺少设备地址"); return; } // 通过地址拿到远端设备对象 var device = adapter.getRemoteDevice(address); log("正在连接 " + address + " ..."); // 创建 SPP Socket 并连接 socket = device.createRfcommSocketToServiceRecord(SPP_UUID); socket.connect(); log("连接成功"); }注意 socket.connect() 是一个阻塞方法,在 JS 里调用它会卡住当前 WebView 的 UI 线程,表现是页面上的按钮点了没反应,连接过程可能要两三秒。这是正常现象,不是死机。但如果超过 10 秒还没有成功或报错,就要排查 UUID 是否正确、对端设备是否支持 SPP 服务。还有一类模块需要的是不安全的快速连接,这时改用 createInsecureRfcommSocketToServiceRecord,参数和用法一样,只是在建立连接时不要求配对流程。遇到连上就断的情况,我会优先换这个接口重试。
这里有个我自己踩过的坑:connect() 抛出的异常是 Java 层异常,JS 里用 try/catch 能捕获,但错误信息很模糊,常见是 socket closed 或 connection refused。捕获异常后至少要把错误码和 message 打到日志里,不要吞掉:
try { socket.connect(); log("连接成功"); } catch (e) { log("连接失败:" + (e.message || JSON.stringify(e))); socket = null; }4.4 发送与接收:OutputStream 写入与 InputStream 轮询读
连接成功后,socket 里有两个流:getOutputStream 用于发送,getInputStream 用于接收。发送相对简单,构造字节数组后 write 出去:
function sendText(text) { if (!socket) { log("未连接设备"); return; } try { var outputStream = socket.getOutputStream(); var payload = plus.android.newObject("java.lang.String", text).getBytes("UTF-8"); outputStream.write(payload); outputStream.flush(); log("已发送:" + text); } catch (e) { log("发送失败:" + (e.message || e)); } }newObject 的作用是创建一个 Java 对象,这里创建的是 String,然后调用 getBytes("UTF-8") 转成字节数组。如果你想发十六进制数据,比如发 01 03 00 02,就要自己构造 byte[] 数组,这也是练习 Native.js 的好场景:
function sendHex(hexStr) { var len = Math.floor(hexStr.length / 2); var bytes = plus.android.newObject("byte[]", len); for (var i = 0; i < len; i++) { bytes[i] = parseInt(hexStr.substr(i * 2, 2), 16) & 0xff; } var outputStream = socket.getOutputStream(); outputStream.write(bytes); outputStream.flush(); }接收这一端有个设计抉择:inputStream.read() 也是阻塞的,直接调用会让 WebView 卡死。我在这里用轮询的方式,每秒读取缓冲区里已经到达的字节:
var readTimer = null; function startReceiving() { if (readTimer) clearInterval(readTimer); readTimer = setInterval(function() { if (!socket) return; try { var inputStream = socket.getInputStream(); var available = inputStream.available(); if (available > 0) { var buf = plus.android.newObject("byte[]", available); var len = inputStream.read(buf); if (len > 0) { var text = plus.android.newObject("java.lang.String", buf, "UTF-8").toString(); log("收到:" + text); } } } catch (e) { log("接收异常,连接可能已断开"); disconnect(); } }, 100); }轮询间隔设为 100 毫秒,对大多数串口透传场景足够了。available() 返回当前缓冲区内可读字节数,read 只读这么多,不会阻塞。转字符串时用 newObject 创建 String 对象并传入字节数组和字符集,如果对端设备发送的是 GBK 编码,把 UTF-8 换成 GBK 即可。这里要强调:蓝牙串口是流式协议,没有消息边界,一帧数据可能分两次到达,两帧数据也可能粘在一起。demo 里先直接显示,生产环境必须加帧头帧尾或定长解析,后面的避坑章会展开讲。
4.5 断开连接与生命周期回收
收发包的最后一步是资源回收。很多人调通了收发就收工,结果第二次连接要么收不到数据,要么后台切回来状态错乱,都是因为没有做清理。Socket 关闭和广播反注册要做在同一个方法里:
function disconnect() { if (readTimer) { clearInterval(readTimer); readTimer = null; } if (receiverId) { plus.android.unregisterBroadcastReceiver(receiverId); receiverId = null; } if (socket) { try { socket.close(); } catch (e) { // Socket 已关闭时忽略 } socket = null; } log("已断开连接并释放资源"); } // 页面切后台或应用关闭时主动断开 document.addEventListener("pause", disconnect); document.addEventListener("close", disconnect);pause 事件是 5+ Runtime 的生命周期事件,页面切到后台时触发;close 是页面销毁时触发。这两个事件加挂后,能保证连接不会残留到下一次会话。Socket 不主动 close,下次连接同样的设备可能报 already connected 或 address already in use,这是高频坑之一。
5. 避坑记录:扫不到设备、连上就断、数据乱码的 5 个真机坑
蓝牙通讯最大的问题不是不会写,而是写了以后真机表现和预期不一样。下面五条都是我在调试过程中真实撞上过的,按频率从高到低排列。
5.1 扫描列表一直为空:先开定位,再谈蓝牙
现象:页面代码没问题,按钮也点了,日志里就是没有“发现设备”,等待 12 秒后什么也没有。
原因:Android 6.0 以后,蓝牙扫描结果依赖定位服务。只申请了蓝牙权限、没开系统定位开关,扫描功能会被系统静默屏蔽,不报错、不回调。另外,华为、小米等厂商在省电策略上会限制后台扫描。
解决:把 ACCESS_FINE_LOCATION 权限申请放到页面加载时,同时引导用户打开系统定位开关;在 scan 前检查 locationManager 的 isProviderEnabled 状态。还可以在日志里明确提示“如果扫描不到,先检查系统定位开关”,这能减少一半的无效排查。
5.2 列表有设备,点击连接就报告连接失败或连上一秒就断
现象:扫描正常,但 createRfcommSocketToServiceRecord 后 connect() 报错,或者连接成功但立刻收到流关闭异常。
原因:最常见的是对端设备的 UUID 不是标准的 00001101。很多蓝牙模块出厂固件会自定义服务 UUID,尤其是一些国产透传模块。第二个常见原因是对端设备是 BLE 模式,根本不提供 SPP 服务,压根连不上。
解决:先用手机自带的蓝牙设置页手动配对一次,如果能配对成功说明设备本身支持经典蓝牙;再用厂商提供的 UUID 替换 SPP_UUID 重试;如果设备手册里写的是 BLE,就改用第 6 章的 BLE 通道处理。第三个办法是换成 createInsecureRfcommSocketToServiceRecord,跳过安全层连接,有时候能绕过配对机制导致的连接中断。
5.3 iOS 上 HTML5 蓝牙完全失效
现象:同一份代码在 Android 上跑得很顺,换成 iPhone 后页面报 plus.android is undefined,或者 navigator.bluetooth 不存在。
原因:plus.android 桥是 Android 平台的专属能力,iOS 上没有对应实现;Web Bluetooth API 在 Safari 和 iOS WebView 里一直没有开放。HTML5 的这套方案在 iOS 上是没有可用的原生通道的。
解决:如果需求必须覆盖 iOS,就不要再在 HTML5 里找办法了。iOS 上做蓝牙通讯只能走 CoreBluetooth,要写原生插件或者改用 uni-app 的 BLE API,并且确认外设支持 BLE。如果只是做调试工具,建议先锁定 Android,把一个平台做透比两个平台都半吊子强。
5.4 数据乱码或粘包:字节流不是消息
现象:收到内容偶尔有乱码,或者明明发了两帧,页面收到的却是合并在一起的一长串,再或者一帧数据在中间被切断。
原因:蓝牙 SPP 是流式传输,没有消息边界。对端设备发送的数据可能被系统拆成多个包到达,也可能多个包合并后同时到达。read 能读到多少只取决于缓冲区里当时有多少。
解决:收发双方约定一种简单的帧协议,固定帧头(比如 0xAA 0x55)、帧长字段、数据区和校验位。接收端先读入字节流,拼包到完整帧后再解析。调试阶段可以把接收模式切换成 hex 显示,这样能直接看到原始字节流,比看乱码文本好定位得多。我之前就在某个设备上遇到文本模式下乱码、切到 hex 才发现是字符集不匹配的问题。
5.5 HBuilderX 真机运行后权限不生效:记得重新打自定义基座
现象:manifest.json 里加了权限,代码也写了 requestPermissions,但手机上运行依然提示权限被拒绝,或者蓝牙调用直接抛异常。
原因:HBuilderX 运行到手机默认使用标准基座,标准基座不含你在 manifest 里声明的自定义权限。你改的权限声明只对自定义基座和正式打包生效。
解决:在 HBuilderX 里选择“制作自定义调试基座”,重新生成本地基座后再运行到手机。换过一次基座后,手机上要卸载旧基座重新安装,否则可能加载的还是旧包。这个坑最隐蔽,因为它不报错,只是功能不符合预期,一旦踩到会浪费大量时间在代码上找原因。
6. 进阶:给同一个工程加一条 BLE 验证通道,以及三个调试习惯
SPP 链路跑通之后,我建议在这个 HTML5 工程里顺手加一个 BLE 验证通道。一方面,现在不少外设只支持 BLE,你迟早要碰上;另一方面,Web Bluetooth API 写起来比 Native.js 干净,适合用来快速验证“设备是否在广播、服务是否可读”这类前置问题。
在页面里加一个“BLE 扫描”按钮,挂上这段代码:
function startBleScan() { if (!navigator.bluetooth) { log("当前 WebView 不支持 Web Bluetooth"); return; } navigator.bluetooth.requestDevice({ acceptAllDevices: true }) .then(function(device) { log("BLE 设备:" + device.name || device.id); return device.gatt.connect(); }) .then(function(server) { log("GATT 已连接"); return server.getPrimaryService("0000FFE0-0000-1000-8000-00805F9B34FB"); }) .then(function(service) { return service.getCharacteristic("0000FFE1-0000-1000-8000-00805F9B34FB"); }) .then(function(characteristic) { characteristic.startNotifications(); characteristic.addEventListener("characteristicvaluechanged", function(event) { var value = event.target.value; log("BLE 通知:" + new Uint8Array(value.buffer)); }); }) .catch(function(err) { log("BLE 连接失败:" + err.message); }); }这段代码跑在支持 Web Bluetooth 的 Android Chrome 或 HBuilderX 内置浏览器里。注意它和 Native.js 是两套完全独立的机制,互不干扰,在一个页面里共存没问题。这样同一个工程就同时覆盖了 SPP 工具场景和 BLE 验证场景。
日常调试时我给自己定了三个固定动作,每次接手蓝牙需求都先过一遍:第一,确认对端是 SPP 还是 BLE,这决定整条技术路线;第二,确认数据语义是流式还是命令式,流式就必须先定帧协议;第三,确认目标平台是否包含 iOS,包含就提前评估原生改造量。这三个问题能过滤掉大半的返工。后来再遇到蓝牙需求,我不会急着写连接代码,而是先跟对端设备做两次手工配对,把设备行为摸清楚再动手。这套 html5-bluetooth-demo 现在成了我本地工具库的常驻项目,任何新硬件到手,先拿它验证收发特性,再决定要不要做正式产品。希望帮到你。
本文还有配套的精品资源,点击获取