简介:本资源是一套完整的微信蓝牙小程序实战项目代码,面向前端开发者及新能源IoT应用学习者,聚焦充电桩场景下的蓝牙通信与微信生态集成。项目实现了附近桩搜索、实时状态查看、扫码启动、在线支付与预约充电等核心功能,解决了新能源车主找桩难、操作繁琐的痛点,适用于智慧出行、能源管理类项目开发参考。压缩包含262个文件,总大小2.27MB,其中82个JS文件承载蓝牙连接、设备发现与指令交互逻辑,44个WXSS与33个WXML构成响应式UI组件,58张PNG图标与6张JPG图示覆盖设备界面与二维码展示,另有38个JSON配置与1份README说明文档。已有76人下载学习,代码结构清晰、模块职责分明,附带实际运行效果视频(B站链接),便于理解蓝牙协议对接细节与小程序生命周期管理实践。 最近在做一个新能源充电桩的配套微信小程序,从协议梳理到蓝牙通信再到业务状态机,踩了不少坑,也沉淀下来一套能直接用的方案。项目源码统一打成了rar包,里面是一套完整的前端小程序工程,包含蓝牙扫描、连接、指令下发、充电状态上报这些核心逻辑。这篇文章就把整套方案的思路和关键实现拆开讲清楚,给正在做硬件类小程序开发的朋友参考。内容既涉及微信小程序的BLE接口使用细节,也覆盖充电桩业务层的协议设计,适合有基础前端能力、想快速上手物联网场景的开发者。
1. 项目全貌:为什么是"微信小程序+蓝牙+充电桩"这个组合
1.1 充电桩蓝牙方案的适用场景
很多人一听到充电桩,第一反应是4G联网、云平台远程控制那套方案。确实,大型公共充电站走4G+云平台是主流,因为要接入电网调度、计费清分和跨区域运营。但充电桩不只是公共场所有需求,还有大量场景其实用不着上云,比如家用慢充桩、小区地下车库的车位桩、单位内部停车场、临时的移动充电设备,这些场景如果非要走4G,反而会带来一系列麻烦:SIM卡要续费、地下车库信号不好、云平台开发周期长、数据安全也要考虑。
蓝牙方案在这个场景下优势非常明显。手机和充电桩之间直接建立BLE连接,不依赖任何外部网络,哪怕是地下三层车库、手机完全没有蜂窝信号的极端环境,也能正常完成启动充电、停止充电、读取状态这些操作。成本上,一颗BLE蓝牙模块(比如国产的泰凌微、Nordic方案)比4G模组便宜不少,而且不需要流量资费,对家用充电桩这种对成本敏感的产品来说,是一个很务实的选型。
另外一个容易忽略的点是安全性。蓝牙是点对点通信,充电桩和手机绑定之后,别的设备扫描到了也拿不到控制权,天然有设备鉴权的属性。相比之下,如果设备直连公网,反而要花大力气去做加密、防重放、防控制劫持这些安全机制。所以我的结论是:公共运营型充电桩做云端方案,私有充电场景做蓝牙方案,两者不是替代关系,而是互补关系。
1.2 微信小程序相比App的核心优势
既然选蓝牙,那手机端为什么不用原生App,非要用小程序?我从实际开发和用户转化两个角度聊。
开发成本上,一套小程序代码同时覆盖iOS和Android两端,不用各自写一套原生逻辑。BLE API在小程序平台上的封装非常完整,wx.openBluetoothAdapter、wx.createBLEConnection、wx.writeBLECharacteristicValue这些接口在两端表现基本一致,不用处理底层系统差异,这对小团队和硬件初创公司来说太重要了。我见过太多团队花两个月做原生App,结果用户下载率感人,而且蓝牙权限、系统兼容性问题要比小程序多得多。
用户触达上,小程序"扫码即用"是杀手级场景。充电桩外壳贴一个二维码,用户微信扫一扫,直接进入小程序,授权蓝牙权限,10秒内完成连接和启动充电。不需要去应用商店搜索下载、注册账号、登录,整个链条短得舒服。而且小程序有潜力直接关联"附近充电桩"这种LBS能力,后续扩展预约、分享、消息通知(充电完成推送)都非常自然。
技术栈上来讲,小程序对BLE的支持已经非常成熟,不只是简单的收发数据,还包括获取信号强度(RSSI)做距离判断、监听设备广播、后台断线重连。这套能力组合足够覆盖充电桩业务90%的需求。唯一要注意的是小程序在系统蓝牙开关关闭时,需要引导用户去设置页打开蓝牙,这个后面我会专门讲。
2. 蓝牙BLE通信原理与充电桩侧适配
2.1 BLE协议基础回顾:服务、特征值、UUID
做小程序蓝牙开发之前,务必要把BLE的四层模型搞明白:物理层负责2.4GHz无线收发,链路层负责连接广播和数据分组,L2CAP层做逻辑通道复用,再往上ATT/GATT层就是应用开发者接触最多的了。GATT层定义了服务(Service)和特征值(Characteristic)两个核心概念。
理解GATT结构的一个生活化类比:把蓝牙设备想象成一栋楼,楼里有很多房间(服务),每个房间里有不同的开关和显示屏(特征值),你通过大楼前台(GATT Server)去访问某个房间里的某个开关,就可以控制设备了。比如说充电桩设备定义了"充电控制服务",里面有两个特征值:一个用来接收手机下发的控制指令(可写),一个用来向手机推送充电状态(可通知)。
每个特征值都有一个UUID标识,16位UUID是蓝牙SIG标准定义好的(比如电池服务0x180F),而自定义业务功能要使用128位UUID,比如0000FFE0-0000-1000-8000-00805F9B34FB这种格式。这里有一个很常见的坑:很多硬件工程师直接用标准蓝牙调试助手测试时使用的是16位UUID的简写,比如FFE0、FFE1,拿到小程序里却发现services和characteristics的UUID全带上了128位前缀。实际上这是同一回事,微信API返回时会自动把16位UUID扩展成128位格式,你判断时要么都转成完整字符串比较,要么统一取后四位,否则很容易出现"明明连接上了但找不到服务"的诡异问题。
2.2 充电桩蓝牙模块选型与硬件适配
充电桩的MCU和蓝牙模块之间,最常见的是串口(UART)透传方式。蓝牙模块上电后自动转发串口数据和蓝牙通道之间的双向数据。我在这个项目里用到的模块支持AT指令配置,可以设置广播间隔、设备名称、发射功率这些参数。硬件选型时要注意几个点:模块要支持BLE 4.2及以上协议,保证传输速率和稳定性;供电电压要匹配MCU逻辑电平;必须支持GATT的多个连接参数协商,避免iOS和Android的连接行为差异导致兼容问题。
特别提一下广播参数配置对小程序扫描的影响。广播间隔建议设置在100ms到300ms之间,太密费电且容易造成信道拥塞,太疏会导致手机扫描命中率低。发射功率根据充电桩安装位置来调,室内空旷环境0dBm就够了,室外可能要调到+6dBm以确保10米以上的连接距离。另外,广播包中建议携带一个独特的Service UUID,这样小程序扫描时可以根据Service UUID直接过滤,而不是扫描到一堆蓝牙音箱、手环之后再逐个判断,这对用户体验影响极大。
2.3 自定义服务与特征值定义:协议表先行
做蓝牙项目,我强烈建议先画协议表,再写代码。协议表是连接硬件工程师和前端工程师的桥梁,也是后期排查问题的最重要文档。下面是一个简化版的充电桩GATT协议示例:
| 服务UUID | 特征值UUID | 属性 | 说明 |
|---|---|---|---|
| 0000FFE0-0000-1000-8000-00805F9B34FB | 0000FFE1-0000-1000-8000-00805F9B34FB | 写(Write) | 手机下发控制指令 |
| 0000FFE0-0000-1000-8000-00805F9B34FB | 0000FFE2-0000-1000-8000-00805F9B34FB | 通知(Notify) | 充电桩主动上报状态 |
| 0000FFE0-0000-1000-8000-00805F9B34FB | 0000FFE3-0000-1000-8000-00805F9B34FB | 读(Read) | 手机读取设备出厂信息 |
特征值FFE1承载下行指令,FFE2承载上行状态上报,FFE3用来读取设备序列号和固件版本。有读者可能会问,为什么要单独搞一个读的特征值?因为有些状态在连接建立时就需要主动查询一次,比如充电桩当前是否在充电、上次充电有没有故障记录,这时候用读比等设备上报更可靠。
报文结构方面,我采用的是精简的帧格式:帧头(0xA5)+ 长度(1字节)+ 指令(1字节)+ 数据区(N字节)+ 校验(1字节)。校验位用累加和,就是把长度、指令、数据区所有字节相加取低8位。这套格式虽然简单,但足够应对充电桩场景的数据量。启动充电指令数据区包含充电模式(快充/慢充)、目标电量百分比;停止充电指令数据区为空;状态查询指令数据区为空。充电桩回复的状态帧数据区固定15个字节,包含当前电压、电流、功率、已充百分比、电池温度、充电状态、故障码,设备端每隔2秒主动上报一次,正好满足小程序实时展示的刷新频率。
3. 小程序蓝牙核心流程实现
3.1 初始化与权限处理:先过系统关卡
小程序里的蓝牙开发,第一步不是写代码,而是处理系统的权限政策。这里我把iOS和Android的差异一并说清楚。
iOS从13.0开始强制要求蓝牙权限描述,小程序manifest里要在IOSPrivilegedPlugins或permission描述中体现NSBluetoothAlwaysUsageDescription。Android从6.0开始,蓝牙扫描需要定位权限(ACCESS_FINE_LOCATION),Android 12及以上还要进一步申请"附近的设备"权限(BLUETOOTH_SCAN和BLUETOOTH_CONNECT)。很多安卓用户反馈"点了没反应",八成就是权限没申请全。
小程序的权限申请流程是:先调用wx.getSetting获取当前的授权状态,如果蓝牙权限未开启,弹窗引导用户去scope.bluetooth对应的授权页。这里有一个体验优化的细节:当用户之前拒绝过权限,再调用wx.authorize时不会重新弹窗,而是直接进入fail回调,这时需要使用wx.openSetting引导用户手动开启权限。我封装了一个权限准备函数,在页面的onLoad里调用,核心逻辑如下:
function ensureBluetoothPermission() { return new Promise((resolve, reject) => { wx.getSetting({ success: (res) => { if (res.authSetting['scope.bluetooth']) { resolve(); } else if (res.authSetting['scope.bluetooth'] === false) { wx.showModal({ title: '提示', content: '需要蓝牙权限才能连接充电桩,是否前往设置开启?', success: (modalRes) => { if (modalRes.confirm) { wx.openSetting({ success: (settingRes) => { if (settingRes.authSetting['scope.bluetooth']) { resolve(); } else { reject(new Error('用户未授权蓝牙')); } } }); } else { reject(new Error('用户取消授权')); } } }); } else { wx.authorize({ scope: 'scope.bluetooth', success: () => resolve(), fail: () => reject(new Error('授权失败')) }); } } }); }); }权限申请通过之后,再调用wx.openBluetoothAdapter初始化蓝牙适配器。如果失败,大概率是手机系统蓝牙没有打开,这时候要提示用户去系统设置打开。注意fail回调里除了提示信息,最好把错误码也打出来,方便线上排查。
3.2 设备扫描:用Service UUID过滤,别做全量扫描
扫描是用户感知最直观的一步,也是性能优化空间最大的地方。很多同学一上来就调用wx.startBluetoothDevicesDiscovery,然后把所有扫描到的设备都展示在列表里,连接的时候再逐个判断名称。这种写法不是不行,但扫描结果会非常脏:同一区域可能有几十个蓝牙设备在广播,用户要在一堆乱码设备名里找到充电桩,体验很糟糕。
正确的做法是让扫描接口直接带上services参数,这样系统层就会按广播包里的Service UUID做过滤。小程序startBluetoothDevicesDiscovery支持services数组参数,示例:
const PILE_SERVICE_UUID = '0000FFE0-0000-1000-8000-00805F9B34FB'; wx.startBluetoothDevicesDiscovery({ services: [PILE_SERVICE_UUID], allowDuplicatesKey: true, interval: 0, success: () => { wx.onBluetoothDeviceFound((res) => { const devices = res.devices || []; devices.forEach((device) => { if (device.advertisServiceUUIDs.includes(PILE_SERVICE_UUID)) { updateDeviceList(device); } }); }); } });allowDuplicatesKey设为true时,同一个设备会重复触发onBluetoothDeviceFound,这样我们可以利用RSSI信号强度来优化列表排序。充电桩的蓝牙距离一般不超过10米,如果RSSI低于-90dBm,说明设备太远或者中间有遮挡,可以在UI上提示用户靠近设备,避免用户在一个基本连不上的状态里反复操作。
扫描超时控制也要做。建议扫描持续8到10秒后主动调用wx.stopBluetoothDevicesDiscovery,避免蓝牙模块持续工作带来的耗电问题。如果10秒内没有发现设备,给用户一个"未发现充电桩,请确认蓝牙已开启并靠近设备后重试"的友好提示。实测下来,这个超时设定在绝大多数场景都能兼顾体验和功耗。
一个小提示:Android上如果没有执行stopBluetoothDevicesDiscovery就立刻去createBLEConnection,经常会出现连接失败。官方文档虽然没有明确说明,但实测在Android 11以上的机型上,停止扫描后再发起连接的成功率会高很多。所以我在项目里统一做成了"停止扫描 -> 等待500ms -> 发起连接"的流程。
3.3 连接、发现服务与订阅通知:顺序不能乱
蓝牙连接这个环节看起来简单,但却是排查时长最多的部分。我先画出标准流程,再逐个说明:
- wx.createBLEConnection(建立基础连接)
- wx.getBLEDeviceServices(获取服务列表)
- wx.getBLEDeviceCharacteristics(获取特征值详情)
- wx.notifyBLECharacteristicValueChange(订阅通知)
- wx.onBLECharacteristicValueChange(监听数据上报)
第一步建立连接后,不要马上调用getBLEDeviceServices,因为系统可能还没准备好GATT信息。实测在连接成功的success回调里直接获取服务,偶发会返回空数组。稳妥做法是延迟300ms再获取,或者使用setTimeout包裹,这不算hack,而是给协议栈一点反应时间。网上很多把这个问题归咎于"手机兼容性",其实多半是时序问题。
第二步获取服务列表后,遍历查找目标Service UUID。这里要特别注意大小写和格式:设备固件返回的UUID可能是全大写,我们代码里定义的是小写,比较时要统一toUpperCase()之后再比对。我后来把所有UUID比较都封装成函数,避免在多个地方手动比较产生遗漏:
function isTargetService(uuid) { return uuid.toUpperCase() === PILE_SERVICE_UUID.toUpperCase(); }第三步获取特征值后,先判断特征值的properties是否包含notify属性。如果设备定义了通知特征值,但小程序侧没订阅,那设备上报状态时小程序完全收不到。正式订阅调用:
wx.notifyBLECharacteristicValueChange({ deviceId: this.deviceId, serviceId: serviceId, characteristicId: notifyCharId, state: true, success: () => { wx.onBLECharacteristicValueChange((res) => { const data = ArrayBufferToBytes(res.value); this.handlePileData(data); }); } });还有一点是重复订阅问题。如果页面onShow时又执行了一次notify,设备端可能重复上报状态,导致UI刷新频繁。我在代码里用了一个布尔标志位,只在连接并且未订阅的状态下才执行订阅,避免重复绑定。
3.4 连接状态管理:全局单例还是页面局部?
连接状态管理是小程序蓝牙项目里一个很容易被轻视的点。如果在每一个页面里都维护蓝牙对象的生命周期,页面跳转、退到后台再回来,很容易出现连接丢失、回调混乱的问题。
我的方案是在App.js里挂一个全局的BluetoothManager单例,统一管理适配器状态、设备ID、服务ID、特征值ID、订阅状态,以及对外暴露connect、disconnect、writeData等方法。页面只依赖这个单例的接口,不做底层API调用。这样带来的好处是:页面A发起连接并启动充电后,用户跳转到页面B查看充电进度,B可以直接复用连接,不需要重新握手,充电桩也不会因为页面切换而断开连接。
全局管理还有一个重要任务是监听系统蓝牙断开事件:wx.onBLEConnectionStateChange。当充电桩断电、距离超出信号范围时,系统会回调这个事件,这时候要主动更新全局状态为disconnected,并通知当前页面弹出"连接已断开"提示。如果不做状态同步,用户看到UI上还显示"充电中",实际上充电桩早就不理你了,这种体验属于严重bug级别。
下面给一个简化的断开重连策略:收到断线事件后,自动尝试重连一次,如果在3秒内重连成功,静默继续当前业务;如果失败,主动提示用户重新扫码或者手动连接。重连次数不要超过一次,否则在信号不稳定的区域会造成反复弹窗,适得其反。
wx.onBLEConnectionStateChange((res) => { if (!res.connected) { this.connectionStatus = 'disconnected'; this.autoReconnectIfNeeded(); } });4. 充电桩业务逻辑与协议处理
4.1 充电流程状态机设计
充电业务不能简单理解成"发指令、收状态",它天然是一个状态机:待机、启动中、充电中、暂停、完成、异常。如果代码里不做状态机管理,而是满屏的if else判断当前界面显示什么,后面需求一旦增加(比如预约充电、错峰充电)就会迅速失控。
我把状态机定义如下:
| 状态 | 触发事件 | 动作 | 下一状态 |
|---|---|---|---|
| IDLE | 用户点击启动 | 发送启动指令 | STARTING |
| STARTING | 收到充电桩确认 | 等待状态上报 | CHARGING |
| CHARGING | 达到目标电量 | 发送停止指令 | STOPPING |
| CHARGING | 用户点击停止 | 发送停止指令 | STOPPING |
| STOPPING | 收到充电桩停止确认 | 展示费用与电量 | IDLE |
| FAULT | 收到故障码 | 展示故障信息 | IDLE |
在小程序里,我用一个currentStatus字段保存状态值,每次收到设备上报都会走一个统一的状态处理函数。该函数首先解析帧数据里的状态字节,更新全局状态,然后根据状态值决定UI跳转和弹窗。这样逻辑集中,排查问题时只要关注状态机的入口和出口,不需要在多个页面里到处翻回调。
简单描述状态机处理的核心代码结构:
handlePileData(bytes) { const status = bytes[4]; const payload = bytes.slice(5, bytes.length - 1); switch (status) { case PILE_STATUS.IDLE: this.changeStatus('IDLE'); break; case PILE_STATUS.CHARGING: this.changeStatus('CHARGING', payload); break; case PILE_STATUS.FAULT: this.changeStatus('FAULT', payload); break; } }4.2 指令协议封装:字节序、校验、超时重试
蓝牙数据传输本质是字节流,指令的拼装与解析必须非常严谨。我这套协议规定了小端字节序(低字节在前),所以在用JavaScript处理时,需要明确使用DataView并设置littleEndian参数。这个很容易踩坑:JavaScript本身没有字节序概念,ArrayBuffer里的数据只是字节序列,只有用DataView按特定格式读取时才有大小端之分。如果硬件工程师的协议文档没写明字节序,强烈建议先做一个握手指令测试,确认字节序方向后再批量开发。
校验和的计算也必须和硬件严格一致。我的场景里校验和是整个数据包所有字节的和取低8位。在PC端调试小程序时,能用console打印每个字节和校验结果,但在真机上就麻烦一些,所以我在代码里封装了debug函数,把收发内容统一打印到面板,开发时开启,上线时关闭。
发指令给设备后,设备可能因为蓝牙模块忙、总线冲突等原因没有响应。这时候必须做超时重试机制。我做了一个简单可靠的方式:每次writeData之前记录当前时间,并启动一个2秒的setTimeout,如果2秒内没有收到对应的ACK帧,就自动重发,最多重发3次。超时重试要和业务逻辑解耦,做成一个纯工具函数。这里有一个细节:发送后的setTimeout要保存在实例上,收到ACK时清除,否则重发和ACK响应顺序错乱会导致业务重复触发。
实际项目中用的启动充电指令是一个7字节的帧:A5 06 01 00 00 64 0A,其中A5是帧头,06是数据长度(从指令到校验之前的字节数),01是启动充电指令码,00是模式(快充),00是预留,64是目标电量100%,0A是校验和。小程序发送时,先构造ArrayBuffer,再用DataView写入字节,最后通过writeBLECharacteristicValue发送。注意微信API要求写入的数据类型是ArrayBuffer,不能直接传普通数组:
function buildFrame(cmd, payload) { const length = 2 + payload.length + 1; const buffer = new ArrayBuffer(length + 1); const view = new DataView(buffer); view.setUint8(0, 0xA5); view.setUint8(1, payload.length + 2); view.setUint8(2, cmd); for (let i = 0; i < payload.length; i++) { view.setUint8(3 + i, payload[i]); } let checksum = 0; for (let i = 1; i < length; i++) { checksum += view.getUint8(i); } view.setUint8(length, checksum & 0xff); return buffer; }4.3 状态上报解析与UI数据绑定
充电桩通过Notify特征值每2秒上报一次状态帧。状态帧的数据区包含电压、电流、功率等数值,但注意这些值都是原始值,需要在业务层换算成展示单位。比如电压字段是16位无符号整数,单位0.1V,实际展示时除以10;电流是16位无符号整数,单位0.01A,展示时除以100;功率可以直接由电压和电流相乘得到,也可以设备端上报。
这里有一个容易出错的地方:由于DataView只有getUint8、getInt16这些方法,读取多字节整型时要正确选择调用方式。我用getUint16(offset, true)来读小端序的16位数据。很多同事直接用getUint8拿两个字节再手动拼接,容易漏掉符号位和大小端问题,代码还难看。
UI绑定方面,小程序本身是数据驱动模式,setData是单向数据流。我在页面的data里定义了一个pileStatus对象,每次状态解析完成后,更新这个对象并调用setData。为了避免频繁的全量setData影响性能,我只更新变化的字段,不做整对象重建。充电进度条则用一个环形进度组件绑定已充百分比,进度动画用CSS transition实现,平滑好看又不用额外引入图表库。
收到充电桩上报的故障码时,需要根据故障码表映射成中文提示。比如故障码01表示"过温保护",02表示"输出过流",03表示"电池连接异常"。故障弹窗设计成模态框,用户点击确认后进入待机状态。
4.4 费用估算与充电记录展示
蓝牙充电桩一般不是计费终点,但小程序里依然要有费用估算能力,方便用户在充电途中了解大概费用。我用的是分时电价模型:深谷、低谷、平时、高峰、尖峰五个时段电价不同,每收到一次状态上报,就根据当时时间和当前功率,计算这一段时间(2秒)的电量消耗,累加到本次充电的总电量中,再乘以对应时段电价。
这个算法很简单,但要注意用户充电可能跨越多个电价时段,不能拿一个固定电价去套整个充电过程。我把每个上报周期单独计算,代码上用reduce累加。费用展示保留两位小数,并在页面底部标注"估算费用,以实际账单为准",规避误解。当然,如果产品后续接入支付,这里就会替换成云端的真实计费结果,小程序端负责展示即可。
充电结束后,把本次充电的开始时间、结束时间、总电量、总费用、平均功率组装成记录,通过wx.setStorageSync存在本地。开发者工具里也可以扩展成调用后端接口上报,方便运营侧做统计分析。我展示记录的原生页面用了一个简单的列表,从本地缓存读取后渲染。
5. 常见问题与排查技巧实录
5.1 连接不稳定:80%是广播与权限的锅
蓝牙连接不稳定的现象非常普遍,但大部分情况下不是模块硬件坏了,而是软件配置或环境因素。我遇到过两个典型场景。
第一个是安卓手机扫描不到设备。排查了很久发现,安卓系统要求定位服务处于开启状态才能扫描低功耗蓝牙。很多用户的"定位权限已授权"其实只是应用授权,但系统定位服务被用户关掉了。所以小程序的提示文案要写清楚:"请开启系统定位服务后重试",不能只提示"请确保蓝牙已开启"。
第二个是设备连接后十几秒内自动断开,重连一次又断开。用逻辑分析仪抓设备端数据后发现,是设备端的连接参数(Connection Interval)和手机期望的不匹配,手机侧请求了较小的连接间隔,设备没有响应,系统在超时后主动断开。解决方法是把设备端的连接参数调整到兼容区间(比如连接间隔30ms到50ms,从机延迟0,超时时间2000ms),并在小程序里做好断线重连兜底。硬件端的同事如果看到类似问题,先查这两处,比反复换手机测试效率高。
5.2 数据丢失或乱码:字节序、分包、写入频率
蓝牙BLE单次数据包的理论上限是MTU大小,一般为23字节,扣除ATT协议头,实际有效数据只有20字节左右。如果充电桩上报的状态帧超过20字节,就会出现截断或分片,小程序端收到的是不完整数据。我的解决方案有两个方向:一是精简协议,把状态帧压缩到20字节以内;二是启用MTU协商,在连接成功后通过wx.setBLEMTU请求更大的MTU值(比如247字节),但注意Android和iOS对这个接口的支持有差异,Android可以通过setBLEMTU扩展,iOS一般自动协商且不开放手动设置。
写入频率过高也是丢包的重灾区。有些开发者为了UI刷新流畅,每500ms就发一次查询指令,这会给BLE链路造成压力,设备端如果处理不过来,就会丢失指令或响应延迟。我实测下来,查询类指令间隔1.5秒以上比较稳妥;状态上报由设备主动推送,理论上不吃查询指令,所以充电场景下发指令频率很低,真正考验的是设备端上报的连续性。另外,连续两条写指令之间建议间隔至少100ms,蓝牙芯片在Received数据后需要时间处理,连续写入会导致缓冲区溢出。
乱码问题大概率是字节序或错位解析。排查时最有效的工具不是盲改代码,而是提前打印原始字节流。我在项目里写了一个helper:ArrayBuffer转十六进制字符串,网络上的抓包数据也能直接对得上。出现乱码时,先确认收到的字节和硬件测试工具收到的字节是否一致,如果一致,说明传输没问题,问题一定在解析逻辑上;如果不一致,再去查分包和MTU。这个排查思路能省掉大量时间。
5.3 多机型兼容性:Android碎片化避坑指南
小程序BLE开发最大的痛点不是iOS,而是Android的碎片化。不同厂商的ROM对BLE的支持天差地别,我整理了以下常见问题:
| 机型/系统 | 现象 | 解决方案 |
|---|---|---|
| 小米/Redmi | 扫描不到设备 | 确认系统定位服务开启,尝试关闭MIUI优化后再试 |
| 华为/荣耀 | 连接后获取服务为空 | 连接后延迟500ms再getBLEDeviceServices |
| OPPO/vivo | 连接成功率低 | 关闭省电模式,蓝牙优化策略设为"兼容模式" |
| Android 13+ | 权限提示不弹出 | 在app.json中声明androidPermissions需要的蓝牙权限 |
| Android 14 | 后台扫描被限制 | 前台运行时扫描,避免页面hide后仍持续扫描 |
针对Android扫描不到的常见解决方法,我习惯在进入扫码页时先调用wx.getBluetoothAdapterState获取系统蓝牙状态,再开始扫描。如果返回蓝牙未打开,直接引导去设置页打开。这样能拦截掉一大批"底锅"问题,让后续流程更专注。
还有一个开发环境的小提示:微信开发者工具中模拟蓝牙扫描是走电脑的蓝牙适配器,很多场景下电脑没有BLE能力,所以模拟器里调用openBluetoothAdapter会失败,这是正常现象。真正的蓝牙联调必须在真机上。很多新手在这里卡住,以为代码写错了,实际上只是开发环境的物理限制。我建议从一开始就把蓝牙相关页面设为"真机调试模式",在工具栏选择"预览"扫码,能够极大减少误判。
5.4 真机调试与定位问题的实用技巧
小程序开发工具自带"真机调试"和"预览"两个入口,第一次跑蓝牙项目时务必选"真机调试"模式,因为它会连接开发机,把console日志实时同步过来,真机上的蓝牙日志直接在开发工具的Console面板可见,排查效率非常高。
还有一个技巧是善用第三方BLE调试助手做"标准答案"。手机上装一个nRF Connect或者LightBlue,先手动连接充电桩,一步步操作:写入指令、接收通知,看回复是否和预期帧一致。如果调试助手能正常收发、小程序不行,那问题在小程序代码;如果调试助手也不行,问题大概率在设备端或协议定义。这个二分法几乎适用于所有蓝牙联调场景。
最后再说一个我在项目里做得比较晚但效果显著的改进:把蓝牙上报数据的解析和页面UI解耦,做成独立的library模块。这样即使不打开页面,小程序在后台(用户切到其他页面)也能继续接收状态数据并更新全局状态。后来用户从充电进度页切到首页再切回来,状态不会丢,体验提升了一个等级。我强烈建议所有做蓝牙小程序的朋友都往这个方向重构代码。
6. 想清楚这几点,你的充电桩小程序能少走一半弯路
在这套项目真正跑起来之前,有几个问题我希望再强调一遍,因为它们决定项目的整体走向,比单纯写代码更重要。
先想清楚产品定位:你的充电桩是面向私人家庭用户,还是面向园区、停车场这类准公共场景?家庭用户对蓝牙方案非常友好,扫码即用、数据本地化,几乎没有运维成本。但如果你计划做共享运营,就一定要在架构上预留云端同步通道,蓝牙负责本地控制,云平台负责计费和远程运维,两者并存而不是二选一。这个架构决策需要在项目早期确定,否则后期切换成本极高。
其次是安全与容错设计。充电桩控制的是大功率设备,指令下发必须可靠。我在协议中加入了指令ACK机制,设备收到指令后无论成功失败都要回一个确认帧,失败要带原因码。小程序的超时重试必须设置上限,避免在故障状态下反复给设备发指令。用户侧的权限管控也要做:连接蓝牙前判断设备MAC地址是否在白名单内,或者要求用户在页面上输入充电桩侧贴的配对码,这两种方式都可以有效防止陌生设备被"试探"控制。
然后是维护与升级思路。蓝牙设备端的固件升级是绕不开的话题。小程序在技术上是可以直接给设备做OTA的,通过蓝牙通道把固件包分片传输给设备的Bootloader。虽然这里没有展开执行细节,但建议前期就在协议中预留固件升级服务和特征值,哪怕一开始不开发升级页面,也要把协议位占好,否则后期追加升级功能时发现特征值不够用或者适配表冲突,改动会牵一发而动全身。
我个人在实际操作中最深刻的体会是:硬件类小程序开发和纯前端开发完全是两种节奏。纯前端你只要拿到接口文档就能开写,但蓝牙项目里前端工程师必须理解设备端的工作方式、广播行为、连接参数、数据协议,甚至要会看一点逻辑分析仪的输出。不要怕这些"跨界知识",它们恰恰是硬件小程序开发中最有价值的部分。多和硬件工程师吵几次协议,你写出来的代码会比闷头开发健壮得多。
这套充电桩小程序的思路不仅适用于新能源充电桩,所有基于BLE的低功耗硬件控制场景——便携设备、健康设备、智能家居——几乎都可以复用。你只要把Service UUID换成自己的、协议帧按硬件规格调整,剩下整个蓝牙连接管理、状态机设计、断线重连、真机调试方法论,都能平移过去。这也是我把这套结构分享出来最大的价值所在。
本文还有配套的精品资源,点击获取