简介:本资源是一套完整的微信小程序蓝牙控制继电器及串口调试实战项目源码,面向嵌入式开发初学者、物联网爱好者及微信小程序硬件交互实践者,解决蓝牙通信、STM32外设控制与小程序端调试工具集成等典型IoT开发痛点。压缩包共56个文件,含10个wxss样式文件(定义UI主题与响应式布局)、8个js逻辑文件(封装蓝牙连接、设备发现、指令发送与串口数据解析)、5个wxml页面结构文件(涵盖设备列表、继电器开关面板、串口调试终端等核心界面),以及17张png界面截图和README.md说明文档,整体仅133KB,轻量易部署。已有3845人学习下载,资源目录结构清晰,包含miniprogram源码主目录、colorui组件库、cloudfunctions云函数模块(支持状态存储与用户管理)及static静态资源,开箱即用,可快速复现小程序直连STM32蓝牙模块、远程控制继电器通断,并实时收发串口指令完成双向调试验证。
1. 项目缘起:一个硬件工程师的微信小程序蓝牙控制实践
作为一名长期在嵌入式硬件和物联网领域摸爬滚打的工程师,我经常需要调试各种传感器、执行器与微控制器之间的通信。继电器作为最经典的开关控制元件,其调试和控制是家常便饭。过去,我习惯于在电脑上打开串口调试助手,敲击AT指令或者发送十六进制数据包来控制蓝牙模块,进而驱动继电器。这种方法在开发阶段没问题,但一旦设备部署到现场,或者需要给客户、测试人员一个简单的演示界面时,就显得非常笨拙和不便。
后来,我开始尝试用手机App来控制,但为每一个小项目都开发一个安卓或iOS应用,无论是用原生还是Flutter,都显得过于沉重,开发和分发成本都太高。直到我开始深入研究微信小程序,发现其蓝牙API已经相当成熟,能够满足大多数低速率、间歇性通信的物联网设备控制需求。最关键的是,它无需安装,扫码即用,分享极其方便,这完美契合了硬件调试、演示和轻量级控制场景的需求。
于是,我决定将“电脑上的串口调试助手”和“手机上的蓝牙控制App”这两个需求,融合进一个微信小程序里。这个项目的核心目标,就是打造一个能够通过蓝牙连接常见模块(如HC-05、JDY-31等),实现类似串口调试助手的数据收发功能,并能直接发送预定义指令控制继电器状态的小工具。它不仅是一个可用的工具,更是一个完整的、可二次开发的源代码框架,希望能为同样有硬件蓝牙控制需求的开发者提供一个清晰的参考。
2. 核心架构解析:小程序如何与蓝牙硬件“对话”
要理解这个小程序如何工作,我们需要先拆解其技术栈和通信链路。整个系统可以看作一个三层结构:微信小程序(前端界面与逻辑)、手机系统蓝牙栈(中介)、蓝牙模块及下位机硬件(执行端)。
2.1 微信小程序蓝牙API概览与选型
微信小程序提供了wx.openBluetoothAdapter等一系列蓝牙API,属于低功耗蓝牙(BLE)协议栈。但这里有一个关键的认知点:我们常用的HC-05、JDY-31这类经典蓝牙串口模块,通常工作在经典蓝牙(SPP)模式,而非BLE模式。这是很多初学者第一个容易“踩坑”的地方。
为什么小程序BLE API能连接经典蓝牙模块?这依赖于手机系统的一个特性:蓝牙协议兼容层。安卓和iOS系统会将传统的SPP服务,映射成一个虚拟的BLE服务,通常有一个固定的服务UUID(如00001101-0000-1000-8000-00805F9B34FB,这是SPP服务的标准UUID)。小程序通过发现并连接这个服务,就可以像操作一个BLE设备一样,通过特征值(Characteristic)进行数据的读写,从而实现与经典蓝牙模块的通信。这解释了为什么在搜索设备时,我们往往需要根据这个特定的服务UUID来过滤设备,而不是设备名称。
基于此,小程序的蓝牙操作流程就清晰了:
- 初始化蓝牙适配器(
wx.openBluetoothAdapter):检查手机蓝牙是否可用,并初始化小程序蓝牙模块。 - 开始搜索设备(
wx.startBluetoothDevicesDiscovery):通常需要指定services参数为上述SPP服务UUID,以精准过滤出我们需要的蓝牙串口模块,避免搜到一堆手环、耳机等无关设备。 - 监听寻找到新设备(
wx.onBluetoothDeviceFound):将找到的设备信息(如deviceId,name,RSSI等)展示在列表中。 - 连接目标设备(
wx.createBLEConnection):使用设备的deviceId进行连接。 - 获取服务与特征值(
wx.getBLEDeviceServices->wx.getBLEDeviceCharacteristics):连接成功后,获取设备的所有服务,并找到我们需要的服务下的“写”特征和“通知”特征。写特征用于发送数据(控制继电器),通知特征用于接收数据(读取串口数据)。 - 启用特征值通知(
wx.notifyBLECharacteristicValueChange):启用接收数据的通道,并监听特征值变化事件 (wx.onBLECharacteristicValueChange) 来获取硬件返回的数据。 - 数据读写:通过
wx.writeBLECharacteristicValue发送指令,通过监听事件接收数据。
2.2 下位机硬件方案:从蓝牙模块到继电器驱动
小程序是“大脑”,而硬件是“手脚”。一个典型的硬件链路如下:手机小程序->手机蓝牙->HC-05/JDY-31蓝牙模块->串口(TX/RX)->微控制器(如STM32/Arduino)->GPIO口->继电器模块。
- 蓝牙模块:充当无线串口透传模块。它接收小程序发来的数据,通过串口原封不动地转发给单片机;同时将单片机串口发送的数据,通过蓝牙回传给小程序。其配置(如波特率、配对码)通常需要通过AT指令在连接前用USB转TTL工具在电脑上设置好。
- 微控制器:核心处理器。它解析从串口收到的指令。例如,小程序发送字符串
“RELAY1_ON\r\n”,单片机程序识别到这个字符串后,就将控制继电器1的GPIO引脚置为高电平(或低电平,取决于继电器模块的驱动逻辑)。 - 继电器模块:执行机构。通常是一个集成了光耦隔离和晶体管驱动的电路板,输入是单片机的3.3V或5V GPIO信号,输出是一组可以通断220V交流或更高电压直流电的继电器触点。
这里的一个关键设计是通信协议。为了可靠控制,我们不能只发送一个简单的字符,而需要设计一个包含帧头、指令、校验和帧尾的简单协议。例如:0xAA(帧头) +0x01(继电器编号) +0x01(开) +CRC(校验和) +0x55(帧尾)。 单片机程序需要按照这个协议解析,校验通过后才执行动作,这能极大提高抗干扰能力。在小程序端,我们需要将指令封装成对应的十六进制数组(ArrayBuffer)进行发送。
2.3 数据格式转换:字符串、十六进制与ArrayBuffer的“三角恋”
小程序蓝牙API读写数据操作的都是ArrayBuffer类型。这与我们习惯的字符串或十六进制显示有巨大差异,是开发中的第二个“坑点”。
- 发送(字符串 -> ArrayBuffer):用户在界面输入“AABBCC”或者“OPEN”,我们需要将其转换为
ArrayBuffer。- 如果输入是十六进制字符串,需要将其每两个字符解析为一个字节。例如,“AABBCC” ->
[0xAA, 0xBB, 0xCC]->ArrayBuffer。 - 如果输入是普通字符串(如AT指令),则需要通过
TextEncoder或String.charCodeAt()将其转换为UTF-8编码的字节数组,再转为ArrayBuffer。
- 如果输入是十六进制字符串,需要将其每两个字符解析为一个字节。例如,“AABBCC” ->
- 接收(ArrayBuffer -> 可显示格式):从蓝牙收到的数据是
ArrayBuffer,我们需要将其转换为十六进制字符串或文本字符串,才能显示在界面的接收框中。- 转换为十六进制字符串:将
ArrayBuffer转为Uint8Array,然后遍历每个字节,通过toString(16)转换为两位十六进制字符串,不足两位补零。 - 转换为文本:使用
TextDecoder将Uint8Array解码为字符串(前提是数据本身是文本)。
- 转换为十六进制字符串:将
在串口调试功能中,通常需要同时提供“十六进制发送/显示”和“文本发送/显示”的选项,以应对不同的调试场景。
3. 微信小程序前端界面与逻辑实现详解
有了架构认知,我们开始动手实现。小程序的代码主要分布在三个部分:.wxml(界面)、.wxss(样式)、.js(逻辑)。我们重点关注逻辑实现。
3.1 设备扫描、连接与状态管理
这是所有蓝牙操作的基础,必须做到状态清晰、反馈及时。
// index.js - 简化示例 Page({ data: { devices: [], // 发现的设备列表 connectedDeviceId: '', // 当前连接设备的ID connectedDeviceName: '', state: 'uninitialized', // 状态:uninitialized, ready, scanning, connecting, connected log: '', // 运行日志 }, // 1. 初始化蓝牙适配器 initBluetooth() { const that = this; wx.openBluetoothAdapter({ success(res) { that.setData({ state: 'ready' }); that.addLog('蓝牙适配器初始化成功'); // 监听适配器状态变化 wx.onBluetoothAdapterStateChange((stateRes) => { that.addLog(`蓝牙适配器状态变化: ${stateRes.available ? '可用' : '不可用'}`); }); }, fail(err) { that.addLog(`初始化失败: ${err.errMsg}`); } }); }, // 2. 开始扫描设备 startScan() { if (this.data.state !== 'ready') return; this.setData({ state: 'scanning', devices: [] }); // 关键:指定服务UUID,过滤SPP设备 wx.startBluetoothDevicesDiscovery({ services: ['00001101-0000-1000-8000-00805F9B34FB'], allowDuplicatesKey: false, success: (res) => { this.addLog('开始扫描设备...'); // 设置扫描超时,例如10秒后自动停止 setTimeout(() => this.stopScan(), 10000); }, fail: (err) => { /* 错误处理 */ } }); // 3. 监听找到新设备 wx.onBluetoothDeviceFound((devicesRes) => { const newDevices = devicesRes.devices; let updated = false; const oldDevices = this.data.devices; newDevices.forEach(newDev => { // 去重,只添加未在列表中的设备 if (!oldDevices.some(d => d.deviceId === newDev.deviceId)) { oldDevices.push({ deviceId: newDev.deviceId, name: newDev.name || '未知设备', RSSI: newDev.RSSI, advertisData: newDev.advertisServiceUUIDs // 可查看广播信息 }); updated = true; } }); if (updated) { // 按信号强度排序 oldDevices.sort((a, b) => (b.RSSI || -100) - (a.RSSI || -100)); this.setData({ devices: oldDevices }); } }); }, // 4. 连接设备 connectDevice(e) { const deviceId = e.currentTarget.dataset.id; const deviceName = e.currentTarget.dataset.name; this.setData({ state: 'connecting' }); wx.createBLEConnection({ deviceId, timeout: 10000, // 连接超时设置 success: (res) => { this.addLog(`连接成功: ${deviceName}`); this.setData({ connectedDeviceId: deviceId, connectedDeviceName: deviceName, state: 'connected' }); this.stopScan(); // 连接成功后停止扫描 this.discoverServices(deviceId); // 发现服务 }, fail: (err) => { this.addLog(`连接失败: ${err.errMsg}`); this.setData({ state: 'ready' }); } }); }, })注意:设备发现事件
onBluetoothDeviceFound在iOS和安卓上的触发频率和行为有差异。安卓设备可能会持续上报,即使设备信息未变化;而iOS可能只在状态变化时上报。因此,在列表展示时做好去重和排序(按RSSI信号强度)非常重要,能提升用户体验。
3.2 服务发现、特征值读写与数据收发
连接成功只是万里长征第一步,找到正确的“管道”(特征值)才能通信。
// 续 index.js discoverServices(deviceId) { const that = this; wx.getBLEDeviceServices({ deviceId, success: (servicesRes) => { const services = servicesRes.services; that.addLog(`发现 ${services.length} 个服务`); // 通常我们需要的SPP服务UUID是固定的 const targetServiceId = '00001101-0000-1000-8000-00805F9B34FB'; const service = services.find(s => s.uuid.toUpperCase() === targetServiceId.toUpperCase()); if (service) { that.addLog(`找到目标服务: ${service.uuid}`); that.getCharacteristics(deviceId, service.uuid); } else { that.addLog('未找到目标SPP服务,尝试列出所有服务特征...'); // 有些模块的服务UUID可能略有不同,可以遍历所有服务寻找可写可通知的特征 for (let srv of services) { that.getCharacteristics(deviceId, srv.uuid, true); // 传入标志位进行探索 } } } }); }, getCharacteristics(deviceId, serviceId, isExplore = false) { const that = this; wx.getBLEDeviceCharacteristics({ deviceId, serviceId, success: (charsRes) => { const characteristics = charsRes.characteristics; that.addLog(`服务 ${serviceId} 下有 ${characteristics.length} 个特征`); let writeChar = null; let notifyChar = null; characteristics.forEach(char => { const props = char.properties; // 寻找可写且可通知的特征 if (props.write && props.notify) { // 理想情况:同一个特征同时支持写和通知 writeChar = char; notifyChar = char; } else if (props.write && !writeChar) { writeChar = char; } else if (props.notify && !notifyChar) { notifyChar = char; } }); // 如果探索模式,只打印信息 if (isExplore) { that.addLog(`探索模式: 写特征=${writeChar?.uuid}, 通知特征=${notifyChar?.uuid}`); return; } // 正常模式,启用通知并保存特征值ID if (notifyChar) { that.enableNotification(deviceId, serviceId, notifyChar.uuid); } else { that.addLog('警告:未找到可通知的特征,将无法接收数据'); } if (writeChar) { // 保存写特征ID,用于后续发送数据 that.setData({ writeServiceId: serviceId, writeCharacteristicId: writeChar.uuid }); that.addLog(`写特征已设置: ${writeChar.uuid}`); } else { that.addLog('错误:未找到可写的特征,无法发送数据'); } } }); }, enableNotification(deviceId, serviceId, characteristicId) { const that = this; wx.notifyBLECharacteristicValueChange({ deviceId, serviceId, characteristicId, state: true, // 启用通知 success: (res) => { that.addLog('已启用特征值通知'); // 监听特征值变化,接收数据 wx.onBLECharacteristicValueChange((charRes) => { const arrayBuffer = charRes.value; const hexStr = that.ab2hex(arrayBuffer); // 转换为十六进制字符串 const textStr = that.ab2str(arrayBuffer); // 转换为文本字符串 // 更新UI,显示接收到的数据 that.updateReceivedData(hexStr, textStr); }); } }); },实操心得:并非所有蓝牙串口模块都会将“写”和“通知”属性放在同一个特征值上。有些模块会分开,比如一个特征只写(
0xFFE1),另一个特征只通知(0xFFE2)。因此,在getCharacteristics函数中,更健壮的做法是分别寻找write属性和notify/indicate属性最强的特征,并记录下来。上述代码中的遍历逻辑就是为此准备的。
3.3 串口调试助手功能实现:数据发送与接收显示
这是工具的核心功能,需要处理多种输入格式和显示模式。
// 续 index.js data: { sendText: '', // 文本发送框内容 sendHex: '', // 十六进制发送框内容 sendMode: 'text', // 发送模式:text / hex receiveData: [], // 接收到的数据历史 {time, hex, text, dir: 'in'} autoNewLine: true, // 发送时自动添加换行符(如\r\n) hexDisplay: false, // 接收区是否以十六进制显示 }, // 发送数据 sendData() { if (!this.data.writeServiceId || !this.data.writeCharacteristicId) { this.addLog('错误:未找到可写的特征,请先连接设备'); return; } let buffer; if (this.data.sendMode === 'hex') { // 处理十六进制字符串,如 "AABB 0C 0D" 或 "AA BB CC" const hexStr = this.data.sendHex.replace(/\s+/g, ''); // 去除空格 if (!/^[0-9A-Fa-f]*$/.test(hexStr)) { this.addLog('错误:十六进制格式不正确'); return; } if (hexStr.length % 2 !== 0) { this.addLog('警告:十六进制字符串长度不是偶数,可能被截断'); } buffer = this.hex2ab(hexStr); } else { // 处理文本 let text = this.data.sendText; if (this.data.autoNewLine) { text += '\r\n'; // 添加回车换行,这是串口调试中常见的行结束符 } buffer = this.str2ab(text); } if (buffer.byteLength === 0) { this.addLog('警告:发送数据为空'); return; } wx.writeBLECharacteristicValue({ deviceId: this.data.connectedDeviceId, serviceId: this.data.writeServiceId, characteristicId: this.data.writeCharacteristicId, value: buffer, success: (res) => { this.addLog(`发送成功,长度: ${buffer.byteLength} 字节`); // 将发送的数据也显示在接收区,方便对比(标记为发送方向) this.updateReceivedData( this.data.sendMode === 'hex' ? this.data.sendHex : this.ab2hex(buffer), this.data.sendMode === 'text' ? this.data.sendText : this.ab2str(buffer), 'out' // 方向:发出 ); // 清空发送框(可选) // this.setData({ sendText: '', sendHex: '' }); }, fail: (err) => { this.addLog(`发送失败: ${err.errMsg}`); } }); }, // ArrayBuffer 转 十六进制字符串 ab2hex(buffer) { const hexArr = Array.from(new Uint8Array(buffer)); return hexArr.map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' '); }, // 十六进制字符串 转 ArrayBuffer hex2ab(hexStr) { const cleanStr = hexStr.replace(/\s+/g, ''); const len = cleanStr.length; const buffer = new ArrayBuffer(len / 2); const view = new Uint8Array(buffer); for (let i = 0; i < len; i += 2) { view[i / 2] = parseInt(cleanStr.substr(i, 2), 16); } return buffer; }, // 字符串 转 ArrayBuffer (UTF-8) str2ab(str) { const encoder = new TextEncoder(); return encoder.encode(str).buffer; }, // ArrayBuffer 转 字符串 (UTF-8) ab2str(buffer) { const decoder = new TextDecoder('utf-8'); return decoder.decode(new Uint8Array(buffer)); }, updateReceivedData(hexStr, textStr, direction = 'in') { const newEntry = { time: new Date().toLocaleTimeString(), hex: hexStr, text: textStr, dir: direction // 'in' 或 'out' }; const newData = [newEntry, ...this.data.receiveData.slice(0, 199)]; // 保留最近200条 this.setData({ receiveData: newData }); },在界面(.wxml)中,我们需要设计两个主要的输入区域和一个显示区域。发送区可以通过选项卡切换“文本”和“十六进制”模式。接收区则是一个可滚动的列表,每条记录显示时间、数据(根据hexDisplay标志显示十六进制或文本)以及一个方向标识(← 表示接收,→ 表示发送),这能极大地方便调试。
4. 继电器控制功能的集成与协议设计
串口调试是通用功能,而继电器控制是具体的业务功能。我们需要在小程序前端设计一个直观的控制面板,并与后端协议对接。
4.1 控制面板UI与交互设计
对于多路继电器控制,一个清晰直观的UI至关重要。我们可以采用卡片式或按钮矩阵式设计。
<!-- index.wxml 部分代码 --> <view class="control-panel"> <text class="panel-title">继电器控制面板</text> <view class="relay-list"> <view wx:for="{{relays}}" wx:key="index" class="relay-item"> <text class="relay-name">继电器 {{item.id}}</text> <text class="relay-status">{{item.status ? '开启' : '关闭'}}</text> <view class="btn-group"> <button size="mini" type="primary" bindtap="controlRelay">// index.js // 生成控制指令的ArrayBuffer generateRelayCommand(relayId, isOn) { const CMD_CONTROL = 0x01; const HEADER = 0xAA; const FOOTER = 0x55; const relayByte = relayId & 0xFF; // 继电器编号,假设1-255 const stateByte = isOn ? 0x01 : 0x00; // 数据部分:指令码 + 继电器编号 + 状态 const dataPart = [CMD_CONTROL, relayByte, stateByte]; const lengthByte = dataPart.length; // 计算校验和(简单累加和示例) let checksum = HEADER + lengthByte; dataPart.forEach(byte => checksum += byte); checksum = checksum & 0xFF; // 取低8位 // 组装完整帧 const frame = [HEADER, lengthByte, ...dataPart, checksum, FOOTER]; return new Uint8Array(frame).buffer; }, // 控制继电器 controlRelay(e) { const relayId = parseInt(e.currentTarget.dataset.id); const cmd = parseInt(e.currentTarget.dataset.cmd); // 1开,0关 const isOn = (cmd === 1); const buffer = this.generateRelayCommand(relayId, isOn); this.sendBuffer(buffer, `控制继电器${relayId} ${isOn ? '开启' : '关闭'}`); // 乐观更新UI状态 this.updateRelayUIS状态(relayId, isOn); }, // 发送Buffer的通用函数 sendBuffer(buffer, logMsg) { if (!this.checkConnection()) return; wx.writeBLECharacteristicValue({ deviceId: this.data.connectedDeviceId, serviceId: this.data.writeServiceId, characteristicId: this.data.writeCharacteristicId, value: buffer, success: (res) => { this.addLog(`指令发送成功: ${logMsg}`); }, fail: (err) => { this.addLog(`指令发送失败: ${logMsg} - ${err.errMsg}`); // 发送失败,可能需要回滚UI状态 } }); }, // 更新本地UI状态 updateRelayUIS状态(id, status) { const relays = this.data.relays.map(relay => { if (relay.id === id) { return { ...relay, status }; } return relay; }); this.setData({ relays }); },在单片机(下位机)端,则需要编写相应的协议解析程序。以Arduino为例,其伪代码如下:
// Arduino 伪代码 byte rxBuffer[32]; byte rxIndex = 0; bool frameStarted = false; void parseBluetoothData(byte incomingByte) { if (incomingByte == 0xAA && !frameStarted) { // 找到帧头,开始接收 rxIndex = 0; rxBuffer[rxIndex++] = incomingByte; frameStarted = true; } else if (frameStarted) { rxBuffer[rxIndex++] = incomingByte; // 检查是否收到帧尾 if (incomingByte == 0x55) { // 一帧接收完成 processFrame(rxBuffer, rxIndex); frameStarted = false; } // 防止缓冲区溢出 if (rxIndex >= sizeof(rxBuffer)) { frameStarted = false; } } } void processFrame(byte* frame, int len) { // 1. 基本长度检查 if (len < 6) return; // 至少帧头+长度+指令+数据+校验+帧尾 // 2. 校验和验证 byte calcChecksum = 0; for (int i = 0; i < len - 2; i++) { // 不包含校验和本身和帧尾 calcChecksum += frame[i]; } calcChecksum &= 0xFF; if (calcChecksum != frame[len - 2]) { // 校验失败,丢弃 return; } // 3. 解析指令 byte cmd = frame[2]; // 假设结构:[AA][len][cmd][data...][checksum][55] switch (cmd) { case 0x01: // 控制继电器 if (len >= 5) { // 确保有数据 byte relayNum = frame[3]; byte state = frame[4]; controlRelay(relayNum, state); // 可选:发送一个应答帧回小程序 sendAck(relayNum, state); } break; // 其他指令... } } void controlRelay(byte num, byte state) { // 根据继电器模块的驱动逻辑,设置对应GPIO高低电平 // 例如: digitalWrite(relayPins[num-1], state == 0x01 ? HIGH : LOW); }这种协议虽然简单,但包含了帧边界识别、长度校验和和校验,已经能够应对大多数蓝牙通信中的粘包、错包问题。
5. 项目实战:从零构建与深度避坑指南
有了代码和原理,我们还需要关注整个项目的构建流程和实际开发中必然会遇到的“坑”。
5.1 微信小程序项目配置与权限申请
首先,你需要一个微信公众平台账号,并创建一个小程序。在项目配置文件app.json中,必须声明蓝牙权限:
{ "pages": ["index/index"], "window": { ... }, "permission": { "scope.bluetooth": { "desc": "用于连接和控制蓝牙设备" } }, "requiredPrivateInfos": ["getBluetoothDevices", "onBluetoothDeviceFound", "createBLEConnection", "writeBLECharacteristicValue"] // 根据实际使用的API添加 }在project.config.json中,确保"miniprogramRoot"路径正确。然后,在开发者工具中,你可以直接使用真机调试功能,在手机上预览和测试蓝牙功能,这比模拟器可靠得多。
5.2 真机调试与常见问题排查
问题一:搜索不到蓝牙设备。
- 检查点1:手机系统蓝牙是否已开启,并且蓝牙模块已进入配对模式(通常指示灯快闪)。
- 检查点2:小程序是否已获取蓝牙权限。首次使用时会弹窗请求,如果用户拒绝,后续需要引导用户去设置页手动开启。
- 检查点3:搜索时是否指定了正确的
servicesUUID。如果不指定,可能会搜到大量无关设备。可以尝试先不指定UUID,看能否搜到设备名,确认基础通信正常。 - 检查点4:部分安卓手机对蓝牙扫描有后台限制,请确保小程序在前台运行。
问题二:连接失败(fail: 10003或fail: 10004)。
- 10003/10004通常表示超时或连接被拒绝。首先确认设备没有被其他手机或应用连接(蓝牙模块一般只允许一个主设备连接)。尝试重启蓝牙模块和手机蓝牙。检查设备
deviceId是否正确。
问题三:能连接,但找不到服务或特征值。
- 这是最常遇到的问题。首先,在
getBLEDeviceServices成功后,将获取到的所有服务UUID打印到控制台。查看是否有00001101-...这个服务。如果没有,说明模块可能不是标准的SPP,或者需要特定的初始化指令。有些模块需要先通过“写”特征发送一个激活指令(例如AT+...),另一个“通知”特征才会出现。 - 使用“探索模式”(如我们代码中的
isExplore标志),遍历所有服务的所有特征,把它们的UUID和属性都打印出来。你可能会发现写特征UUID是0000ffe1-...,通知特征是0000ffe2-...。这时就需要修改代码,分别记录这两个特征ID。
问题四:发送数据成功,但硬件无反应;或接收不到数据。
- 发送无反应:首先用电脑端的串口调试助手连接蓝牙模块(通过USB转TTL),确认单片机程序能正确响应相同指令。排除硬件问题后,检查小程序发送的
ArrayBuffer数据是否正确。可以在writeBLECharacteristicValue的success回调里,将发送的buffer再转换回十六进制打印出来,与预期指令对比。 - 接收不到数据:确认已成功调用
notifyBLECharacteristicValueChange并启用了通知。确认单片机端确实有数据通过串口发送给蓝牙模块。可以在单片机程序里固定间隔发送一个测试字符串(如"TEST\r\n"),看小程序能否收到。
问题五:iOS和安卓兼容性问题。
- 设备发现:iOS可能对设备名过滤更严格,且
onBluetoothDeviceFound事件触发机制不同。确保你的设备列表更新逻辑是增量添加而非每次替换。 - UUID格式:iOS返回的服务/特征UUID可能是全大写且带连字符的格式(如
00001101-0000-1000-8000-00805F9B34FB),而安卓可能返回不带连字符或大小写混合。比较时最好统一转为大写并去除连字符:uuid.replace(/-/g, '').toUpperCase()。 - 后台运行:小程序切到后台后,蓝牙连接可能会被系统挂起或断开。需要监听
wx.onBLEConnectionStateChange来处理连接状态变化,并在onShow生命周期中尝试重连或更新状态。
5.3 性能优化与体验提升
- 连接缓存:将已成功连接过的设备
deviceId和name缓存到本地存储(wx.setStorageSync)。下次打开小程序时,可以直接在界面提供一个“历史设备”列表,点击后尝试快速重连,省去扫描步骤。 - 自动重连机制:在连接断开事件 (
onBLEConnectionStateChange) 中,如果不是主动断开,可以尝试自动重连几次,提升稳定性。 - 数据接收处理:蓝牙数据是分包到达的,
onBLECharacteristicValueChange可能被频繁触发。避免在每次触发时都直接更新UI(setData),这可能导致界面卡顿。可以设置一个缓冲数组,累积一定数据或在一定时间间隔后(如100ms)再统一更新接收显示区。 - 发送队列:快速连续点击控制按钮可能导致发送指令堆积。可以建立一个简单的发送队列,确保前一个指令发送完成(收到success回调)后再发送下一个,避免写入错误。
6. 源代码结构与扩展思路
一个完整的项目源代码应该结构清晰,便于他人理解和二次开发。一个推荐的结构如下:
miniprogram-ble-relay-control/ ├── pages/ │ └── index/ │ ├── index.js // 页面逻辑,包含蓝牙操作、数据收发、继电器控制 │ ├── index.json // 页面配置 │ ├── index.wxml // 页面结构,含设备列表、控制面板、调试终端 │ └── index.wxss // 页面样式 ├── utils/ │ ├── bleCore.js // 蓝牙核心操作封装(初始化、扫描、连接、服务发现) │ ├── protocol.js // 通信协议封装(生成指令、解析数据) │ └── utils.js // 通用工具函数(AB转Hex、Hex转AB等) ├── app.js // 小程序入口,全局状态管理 ├── app.json // 全局配置,声明蓝牙权限和页面 └── project.config.json // 项目配置文件扩展思路:
- 多协议支持:除了自定义协议,可以增加对常见智能硬件协议(如Modbus RTU over Bluetooth)的解析支持,使其成为一个通用的蓝牙调试工具。
- 指令模板:允许用户保存常用的指令(如特定的AT指令、查询指令),一键发送,避免重复输入。
- 数据图表:对于接收到的传感器数据(如温度、湿度),可以尝试用
wx.createCanvasContext绘制简单的实时曲线图。 - 云端同步:将设备列表、指令模板同步到云端,实现多端共享。
- 安全增强:为控制指令增加简单的密码验证或Token机制,防止被附近的其他手机误连接误操作。
这个项目麻雀虽小,五脏俱全。它串联起了微信小程序前端开发、蓝牙通信、硬件串口协议、单片机编程等多个知识点。当你真正动手把它做出来,并成功点亮第一盏由小程序控制的灯时,那种跨越软硬件鸿沟的成就感,是纯软件或纯硬件开发难以比拟的。希望这份详细的解读和源代码思路,能为你打开一扇物联网快速原型开发的大门。
本文还有配套的精品资源,点击获取