> 技术文档 > 智能家居:基于蓝牙连接的微信小程序与 ESP32 智能家居控制系统_esp32 蓝牙 小程序

智能家居:基于蓝牙连接的微信小程序与 ESP32 智能家居控制系统_esp32 蓝牙 小程序


引言

本系统是利用微信小程序设置闹钟时间,然后通过蓝牙传递给esp32,最后在esp32的编写好的LVGL界面显示。(本文主要是笔记提醒,望大家多多包涵)具体效果图如下:

闹钟设定(屏幕显示)

 主界面(esp32屏幕显示)

闹钟设定

 微信小程序首页

微信小程序

微信小程序简单介绍

微信小程序开发的基本框架和一些配置就先不介绍了,只需记得一般是在pages文件夹里面书写,然后记住基本的几种文件:.wxml、 .wxss、 .js 、 .json。(有点像前端的三剑客)

微信小程序——首页代码 

    {{time}} {{date}}   <image class=\"weather-icon\" src=\"{{weatherIcon}}\" wx:if=\"{{weatherData}}\" /> {{weatherData ? weatherData.temp + \'°\' : \'加载中...\'}} {{weatherData ? weatherData.text : \'\'}}      智能闹钟    个人中心                          <view class=\"cube\" style=\"transform: rotateX({{cube.rotateX}}deg) rotateY({{cube.rotateY}}deg);\">    客厅主灯 <switch checked=\"{{devices.light}}\" bindchange=\"toggleDevice\" data-device=\"light\" />     空调 <slider value=\"{{devices.temperature}}\" min=\"16\" max=\"30\" bindchange=\"setTemperature\"/>     窗帘         安防摄像头 <switch checked=\"{{devices.camera}}\" bindchange=\"toggleDevice\" data-device=\"camera\" />     嘻嘻 <slider value=\"{{devices.volume}}\" min=\"0\" max=\"100\" bindchange=\"setVolume\"/>     智能闹钟 <switch checked=\"{{devices.humidifier}}\" bindchange=\"toggleDevice\" data-device=\"humidifier\" />   

.wxss文件

/* 全局样式 */.container { padding: 20rpx; background-color: #f5f7fa; min-height: 100vh; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, \'Segoe UI\', Roboto, \'Helvetica Neue\', Arial, sans-serif;}/* 顶部状态栏样式 */.status-bar { display: flex; justify-content: space-between; padding: 20rpx; /* 合并重复的padding声明 */ margin-bottom: 10rpx; /* 缩小间距关键参数 */ background-color: #42b983; color: white; border-radius: 10rpx; box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);}.time-info { text-align: left;}.time { font-size: 48rpx; font-weight: bold;}.date { font-size: 28rpx; margin-top: 5rpx;}.weather-info { text-align: right;}.weather-icon { width: 60rpx; height: 60rpx; vertical-align: middle;}.weather-temp { font-size: 36rpx; margin-right: 10rpx; vertical-align: middle;}.weather-desc { font-size: 28rpx; vertical-align: middle;}/* 导航菜单 */#navView{ display: flex; /* 应用flex布局 ,所有的子元素排在一行*/ height: 232rpx; align-content: space-around; /* 多行垂直排列*/font-size: 26rpx; font-weight: 600;}.navItemView{ width: 150rpx; text-align: center; margin: 0 50rpx;}.navItemView > image{ width: 150rpx; height: 150rpx;}/* 3D轮播图 */.memoirs-container { width: 280rpx; height: 150rpx; margin-top: 10rpx; /* 添加顶部负向间距补偿 */ animation: rotate 12s linear infinite; transform-style: preserve-3d; position: relative;}.img { width: 320rpx; height: 220rpx; margin-top: 10rpx; position: absolute; border-radius: 10rpx; box-shadow: 0 5rpx 15rpx rgba(0, 0, 0, 0.3); /* 添加阴影效果 */}.img:nth-of-type(1) { transform: rotateY(0deg) translateZ(300rpx) scale(1); /* 缩放效果*/}.img:nth-of-type(2){ transform: rotateY(60deg) translateZ(300rpx) scale(1);}.img:nth-of-type(3){ transform: rotateY(120deg) translateZ(300rpx) scale(1);} .img:nth-of-type(4){ transform: rotateY(180deg) translateZ(300rpx) scale(1);}.img:nth-of-type(5){ transform: rotateY(240deg) translateZ(300rpx) scale(1);}.img:nth-of-type(6){ transform: rotateY(300deg) translateZ(300rpx) scale(1);} @keyframes rotate { 0% { transform: rotateY(0deg); } 100% { transform: rotateY(360deg); }}/* 内容区域 */.device-controls { display: flex; flex-wrap: wrap;}.device-item { display: flex; align-items: center; width: 50%; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0;}.device-item:nth-child(2n) { border-right: 1rpx solid #f0f0f0;}.device-icon { width: 60rpx; height: 60rpx; margin-right: 15rpx;}.device-name { font-size: 28rpx; color: #333; flex: 1;}/* 底部菜单栏样式 */.bottom-nav { display: flex; /* 应用flex布局 ,所有的子元素排在一行*/ height: 464rpx; align-content: space-around; /* 多行垂直排列*/ font-size: 26rpx; font-weight: 600; bottom: 0; left: 0; right: 0; background-color: white; justify-content: space-around;}.nav-item { display: flex; flex-direction: column; align-items: center;}.nav-icon { width: 60rpx; height: 60rpx; margin-bottom: 10rpx;}.nav-text { font-size: 24rpx; color: #333; text-align: center;}/* 立方体样式 */.cube-container { width: 300rpx; height: 300rpx; margin: 40rpx auto; perspective: 1000rpx;}.cube { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; transition: transform 0.5s ease-out;}.cube-face { position: absolute; width: 100%; height: 100%; background: rgba(255,255,255,0.95); border: 2rpx solid #eee; border-radius: 16rpx; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.1); display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20rpx; backface-visibility: hidden;}/* 各面定位 */.cube-front { transform: translateZ(150rpx); }.cube-back { transform: rotateY(180deg) translateZ(150rpx); }.cube-left { transform: rotateY(-90deg) translateZ(150rpx); }.cube-right { transform: rotateY(90deg) translateZ(150rpx); }.cube-top { transform: rotateX(90deg) translateZ(150rpx); }.cube-bottom { transform: rotateX(-90deg) translateZ(150rpx); }/* 设备控制样式 */.device-icon { width: 80rpx; height: 80rpx; margin-bottom: 20rpx;}.device-name { font-size: 28rpx; color: #333; margin-bottom: 20rpx;}

.js部分代码

Page({ data: { time: \'00:00\', date: \'2023年1月1日 星期一\', weatherData: null, weatherIcon: \'\', location: \'\', lightStatus: false, motto: \'欢迎来到智能家居控制中心\', cube: { rotateX: -15, rotateY: -45, startX: 0, startY: 0, isDragging: false }, devices: { light: false, camera: true, humidifier: false, temperature: 26, volume: 50 }, userInfo: { avatarUrl: \'\', nickName: \'\' } }, // 触摸开始 cubeTouchStart(e) { const touch = e.touches[0] this.setData({ \'cube.startX\': touch.clientX, \'cube.startY\': touch.clientY, \'cube.isDragging\': true }) }, // 触摸移动 cubeTouchMove(e) { if (!this.data.cube.isDragging) return const touch = e.touches[0] const deltaX = touch.clientX - this.data.cube.startX const deltaY = touch.clientY - this.data.cube.startY this.setData({ \'cube.rotateY\': this.data.cube.rotateY + deltaX * 0.5, \'cube.rotateX\': this.data.cube.rotateX - deltaY * 0.3, \'cube.startX\': touch.clientX, \'cube.startY\': touch.clientY }) }, // 触摸结束 cubeTouchEnd() { this.setData({\'cube.isDragging\': false}) }, // 设备控制方法 toggleDevice(e) { const device = e.currentTarget.dataset.device this.setData({ [`devices.${device}`]: !this.data.devices[device] }) }, setTemperature(e) { this.setData({\'devices.temperature\': e.detail.value}) }, setVolume(e) { this.setData({\'devices.volume\': e.detail.value}) }, controlCurtain(e) { const action = e.currentTarget.dataset.action wx.showToast({ title: action === \'open\' ? \'正在打开窗帘\' : \'正在关闭窗帘\', icon: \'none\' }) }, onLoad() { this.updateTime(); this.startInterval(); this.loadWeatherData(); this.checkLocationPermission(); }, startInterval() { this.timeInterval = setInterval(() => { this.updateTime(); }, 1000); }, updateTime() { const now = new Date(); const hours = now.getHours().toString().padStart(2, \'0\'); const minutes = now.getMinutes().toString().padStart(2, \'0\'); const timeStr = `${hours}:${minutes}`; const year = now.getFullYear(); const month = (now.getMonth() + 1).toString().padStart(2, \'0\'); const day = now.getDate().toString().padStart(2, \'0\'); const weekdays = [\'日\', \'一\', \'二\', \'三\', \'四\', \'五\', \'六\']; const weekday = weekdays[now.getDay()]; const dateStr = `${year}年${month}月${day}日 星期${weekday}`; this.setData({ time: timeStr, date: dateStr }); }, loadWeatherData() { // 这里可以使用天气API加载实际天气数据 setTimeout(() => { this.setData({ weatherData: { temp: 25, text: \'晴\' }, weatherIcon: \'\' // 请确保这个链接有效,或者使用本地图片 }); }, 1000); }, // 检查位置权限 checkLocationPermission() { const that = this; wx.getSetting({ success(res) { if (!res.authSetting[\'scope.userLocation\']) { wx.authorize({ scope: \'scope.userLocation\', success() {  that.getLocation(); }, fail() {  wx.showModal({ title: \'提示\', content: \'需要获取位置权限,请在设置中开启\', success(res) {  if (res.confirm) {  wx.openSetting({success(settingRes) { if (settingRes.authSetting[\'scope.userLocation\']) { that.getLocation(); }}  });  } }  }); } }); } else { that.getLocation(); } } }); }, // 获取位置 getLocation() { const that = this; wx.getLocation({ type: \'wgs84\', success(res) { const latitude = res.latitude; const longitude = res.longitude; that.setData({ location: `纬度: ${latitude}, 经度: ${longitude}` }); // 这里可以调用地图API获取详细地址 }, fail(err) { console.error(\'获取位置失败\', err); wx.showToast({ title: \'获取位置失败\', icon: \'none\', duration: 2000 }); } }); }, bindGetLocation() { this.checkLocationPermission(); }, toggleLight(e) { this.setData({ lightStatus: e.detail.value }); }, navToPrediction() { wx.navigateTo({ url: \'/pages/mqtt/mqtt\' }); }, navToChat() { wx.navigateTo({ url: \'/pages/naozhong/naozhong\' }); }, onUnload() { if (this.timeInterval) { clearInterval(this.timeInterval); } }});

 微信小程序——闹钟页代码

 效果图

 代码如下:

.wxml代码:

    {{time}} {{date}}   <image class=\"weather-icon\" src=\"{{weatherIcon}}\" wx:if=\"{{weatherData}}\" /> {{weatherData ? weatherData.temp + \'°\' : \'加载中...\'}} {{weatherData ? weatherData.text : \'\'}}    闹钟  <block wx:for=\"{{alarms}}\" wx:key=\"id\">  {{item.time}} {{item.desc}}  <button class=\"action-btn edit-btn\" bindtap=\"editAlarm\" data-id=\"{{item.id}}\">修改 <button class=\"action-btn delete-btn\" bindtap=\"deleteAlarm\" data-id=\"{{item.id}}\">删除        <view class=\"modal\" wx:if=\"{{showModal}}\">  设置闹钟   <picker mode=\"time\" class=\"time-picker\" value=\"{{editAlarmTime}}\" bindchange=\"onTimeChange\"> {{editAlarmTime}}     <input type=\"text\" value=\"{{editAlarmDesc}}\" bindinput=\"onDescChange\" placeholder=\"输入闹钟名称\" class=\"input\" />       

.wxss代码:

/* 全局样式 */.container { padding: 20rpx; background-color: #f5f7fa; min-height: 100vh; box-sizing: border-box;}/* 顶部状态栏样式 */.status-bar { display: flex; justify-content: space-between; padding: 30rpx 0; margin-bottom: 30rpx; border-bottom: 1rpx solid #e8e8e8;}.time-info { text-align: left;}.time { font-size: 48rpx; font-weight: bold; color: #333;}.date { font-size: 28rpx; color: #666; margin-top: 5rpx;}.weather-info { text-align: right;}.weather-icon { width: 60rpx; height: 60rpx; vertical-align: middle;}.weather-temp { font-size: 36rpx; color: #333; margin-right: 10rpx; vertical-align: middle;}.weather-desc { font-size: 28rpx; color: #666; vertical-align: middle;}/* 标题样式 */.section-title { font-size: 32rpx; font-weight: bold; color: #333; margin: 30rpx 0 20rpx;}/* 闹钟列表样式 */.alarm-list { background-color: #fff; border-radius: 10rpx; margin-bottom: 30rpx; box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); overflow: hidden;}.alarm-item { display: flex; align-items: center; padding: 25rpx; border-bottom: 1rpx solid #f0f0f0;}.alarm-item:last-child { border-bottom: none;}.alarm-time { flex: 1; font-size: 32rpx; color: #333; font-weight: bold;}.alarm-desc { flex: 2; font-size: 28rpx; color: #666; padding-left: 20rpx;}.alarm-actions { display: flex;}.action-btn { padding: 8rpx 16rpx; border-radius: 10rpx; font-size: 24rpx; margin-left: 10rpx;}.edit-btn { background-color: #ffe66d; color: #333;}.delete-btn { background-color: #ff6b6b; color: #fff;}/* 添加按钮样式 */.add-alarm-btn { width: 100%; padding: 20rpx 0; background-color: #42b983; color: white; border: none; border-radius: 10rpx; font-size: 32rpx; box-shadow: 0 2rpx 10rpx rgba(66, 185, 131, 0.3);}/* 弹窗样式 */.modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 100;}.modal-content { background-color: #fff; width: 90%; max-width: 600rpx; border-radius: 10rpx; padding: 30rpx;}.modal-title { font-size: 36rpx; font-weight: bold; color: #333; margin-bottom: 30rpx; text-align: center;}.form-group { margin-bottom: 25rpx;}.form-group label { display: block; margin-bottom: 10rpx; font-size: 28rpx; color: #333;}.picker { font-size: 30rpx; color: #333; border: 1rpx solid #e8e8e8; border-radius: 8rpx; padding: 10rpx; width: 100%; box-sizing: border-box; background-color: #f9f9f9;}.input { width: 100%; padding: 10rpx; border: 1rpx solid #e8e8e8; border-radius: 8rpx; font-size: 28rpx; box-sizing: border-box;}.modal-buttons { display: flex; justify-content: flex-end; margin-top: 30rpx;}.modal-btn { padding: 15rpx 30rpx; border-radius: 8rpx; font-size: 28rpx; margin-left: 20rpx;}.cancel-btn { background-color: #f5f5f5; color: #666;}.save-btn { background-color: #42b983; color: white;}.list-item { display: flex; align-items: center; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0;}.list-item { display: flex; align-items: center; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0;}.list-item:last-child { border-bottom: none;}.no-padding { padding: 20rpx 0;}.item-icon { width: 48rpx; height: 48rpx; margin-right: 20rpx;}.item-text { flex: 1; font-size: 28rpx;}.item-status { color: #999; font-size: 24rpx; margin-right: 20rpx;}/* 蓝牙功能栏样式 */.list-item { display: flex; justify-content: space-between; align-items: center; padding: 20rpx; margin-bottom: 10rpx; background-color: #fff; border-radius: 10rpx; box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);}.item-text { font-size: 32rpx; color: #333;}.item-status { font-size: 32rpx; color: #666;}.mini-btn { padding: 8rpx 16rpx; font-size: 24rpx; color: #fff; background-color: #007aff; border-radius: 8rpx;}.mini-btn:active { background-color: #005cb0;}/* 蓝牙设备列表样式 *//* alarm-page.wxss */.status-section { padding: 20rpx; background-color: #f5f5f5; margin-bottom: 20rpx;}.device-list { padding: 20rpx; background-color: #fff; border-radius: 10rpx; margin-bottom: 20rpx;}.device-item { padding: 15rpx; border-bottom: 1rpx solid #eee;}/* 闹钟管理区域样式 */.section-title { font-size: 36rpx; font-weight: bold; color: #333; margin: 30rpx 0 20rpx;}.alarm-list { margin-bottom: 20rpx;}.alarm-item { display: flex; justify-content: space-between; align-items: center; padding: 20rpx; margin-bottom: 10rpx; background-color: #fff; border-radius: 10rpx; box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);}.alarm-time { font-size: 36rpx; font-weight: bold; color: #333;}.alarm-desc { font-size: 28rpx; color: #666;}.alarm-actions { display: flex;}.action-btn { padding: 8rpx 16rpx; margin-left: 10rpx; font-size: 24rpx; border-radius: 8rpx;}.edit-btn { color: #007aff; border: 1rpx solid #007aff;}.delete-btn { color: #ff3b30; border: 1rpx solid #ff3b30;}/*蓝牙列表*/.function-list { padding: 20rpx 30rpx;}.list-item { display: flex; align-items: center; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0;}.list-item:last-child { border-bottom: none;}.no-padding { padding: 20rpx 0;}.item-icon { width: 48rpx; height: 48rpx; margin-right: 20rpx;}.item-text { flex: 1; font-size: 28rpx;}.item-status { color: #999; font-size: 24rpx; margin-right: 20rpx;}.mini-btn { background-color: #7367F0; color: white; border-radius: 4rpx; padding: 4rpx 8rpx; font-size: 24rpx;}.item-input { flex: 1; padding: 10rpx 0; font-size: 28rpx; border-bottom: 1rpx solid #f0f0f0;}.bind-btn { background-color: #7367F0; color: white; border: none; padding: 6rpx 20rpx; border-radius: 4rpx; margin-left: 20rpx;}.arrow { width: 24rpx; height: 24rpx;}.devices-list { margin: 20rpx 0;}.device-item { display: flex; align-items: center; padding: 20rpx 0; border-bottom: 1rpx solid #f0f0f0;}.device-icon { width: 40rpx; height: 40rpx; margin-right: 16rpx;}.device-name { flex: 1; font-size: 28rpx; margin-right: 10rpx;}.device-status { color: #7367F0; font-size: 24rpx;}

.js代码:

// alarm-page.jsvar app = getApp()const config = { serviceId: \'000018f3-0000-1000-8000-00805f9b34fb\', // 服务2的UUID characteristics: { alarm1: \'00002af1-0000-1000-8000-00805f9b34fb\', // 闹钟1特征 alarm2: \'00002af2-0000-1000-8000-00805f9b34fb\', // 闹钟2特征 alarm3: \'00002af3-0000-1000-8000-00805f9b34fb\' // 闹钟3特征 }};Page({ data: { time: \'00:00\', date: \'2023年1月1日 星期一\', weatherData: null, weatherIcon: \'\', alarms: [ { id: 1, time: \'07:00\', desc: \'起床\' }, { id: 2, time: \'12:00\', desc: \'午餐\' }, { id: 3, time: \'18:30\', desc: \'晚餐\' } ], nextAlarmId: 4, showModal: false, editAlarmTime: \'07:00\', editAlarmDesc: \'新闹钟\', editingAlarmId: null, bluetooth: { connected: false, deviceId: \'\', serviceId: config.serviceId, characteristics: { alarm1: null, alarm2: null, alarm3: null } } }, onLoad() { this.initBluetooth(); this.updateTime(); this.fetchWeatherData(); this.startInterval(); }, onUnload() { if (this.timeInterval) { clearInterval(this.timeInterval); } this.closeConnection(); }, // 初始化蓝牙适配器 async initBluetooth() { try { const res = await wx.getSystemInfo(); if (!res.bluetoothEnabled) { throw new Error(\'蓝牙未启用\'); } await this.openAdapter(); this.startDiscovery(); } catch (err) { wx.showToast({ title: err.message, icon: \'none\' }); } }, // 打开蓝牙适配器 openAdapter() { return new Promise((resolve, reject) => { wx.openBluetoothAdapter({ success: resolve, fail: err => reject(new Error(\'请开启手机蓝牙\')) }); }); }, // 开始设备发现 startDiscovery() { wx.startBluetoothDevicesDiscovery({ services: [config.serviceId], allowDuplicatesKey: false, success: () => { this.listenDevices(); setTimeout(() => this.stopDiscovery(), 10000); } }); }, // 监听发现设备 listenDevices() { wx.onBluetoothDeviceFound(devices => { const target = devices.devices.find(d => d.name && d.name.includes(\'ESP32-Clock\') ); if (target) { this.connectDevice(target.deviceId); } }); }, // 创建蓝牙连接 createConnection(deviceId) { return new Promise((resolve, reject) => { wx.createBLEConnection({ deviceId, timeout: 5000, success: resolve, fail: () => reject(new Error(\'连接超时\')) }); }); }, // 获取服务 getServices(deviceId) { return new Promise((resolve, reject) => { wx.getBLEDeviceServices({ deviceId, success: res => { const target = res.services.find(s => s.uuid.toLowerCase() === config.serviceId.toLowerCase() ); target ? resolve([target]) : reject(new Error(\'未找到服务\')); }, fail: reject }); }); }, // 修改后的特征值获取方法 async getCharacteristics(deviceId, serviceId) { const res = await wx.getBLEDeviceCharacteristics({ deviceId, serviceId }); const chars = { alarm1: res.characteristics.find(c => c.uuid.toLowerCase() === config.characteristics.alarm1.toLowerCase() ), alarm2: res.characteristics.find(c => c.uuid.toLowerCase() === config.characteristics.alarm2.toLowerCase() ), alarm3: res.characteristics.find(c => c.uuid.toLowerCase() === config.characteristics.alarm3.toLowerCase() ) }; if (!chars.alarm1 || !chars.alarm2 || !chars.alarm3) { throw new Error(\'缺少必要特征值\'); } this.setData({ \'bluetooth.characteristics.alarm1\': chars.alarm1, \'bluetooth.characteristics.alarm2\': chars.alarm2, \'bluetooth.characteristics.alarm3\': chars.alarm3 }); }, // 新增的闹钟数据发送方法 async sendAlarmsToBluetooth() { if (!this.data.bluetooth.connected) { console.warn(\'蓝牙未连接,跳过发送\'); return; } const { alarms } = this.data; const alarmsToSend = alarms.slice(0, 3); try { for (let i = 0; i  { this.updateTime(); }, 1000); }, // 更新时间显示 updateTime() { const now = new Date(); const hours = now.getHours().toString().padStart(2, \'0\'); const minutes = now.getMinutes().toString().padStart(2, \'0\'); const timeStr = `${hours}:${minutes}`; const year = now.getFullYear(); const month = (now.getMonth() + 1).toString().padStart(2, \'0\'); const day = now.getDate().toString().padStart(2, \'0\'); const weekdays = [\'日\', \'一\', \'二\', \'三\', \'四\', \'五\', \'六\']; const weekday = weekdays[now.getDay()]; const dateStr = `${year}年${month}月${day}日 星期${weekday}`; this.setData({ time: timeStr, date: dateStr }); }, // 获取天气数据 fetchWeatherData() { // 这里只是示例,实际使用时需要接入真实的天气API // 例如:和风天气、彩云天气等 // 模拟天气数据 setTimeout(() => { const weatherData = { temp: 25, text: \'晴\', // 使用本地图片资源或确保图片链接有效 icon: \'/images/sunny.png\' // 使用本地图片路径 }; this.setData({ weatherData, weatherIcon: weatherData.icon }); }, 1000); }, // 打开添加闹钟弹窗 addAlarm() { this.setData({ showModal: true, editAlarmTime: \'07:00\', editAlarmDesc: \'新闹钟\', editingAlarmId: null }); }, // 编辑闹钟 editAlarm(e) { const alarmId = e.currentTarget.dataset.id; const alarm = this.data.alarms.find(alarm => alarm.id === alarmId); this.setData({ showModal: true, editAlarmTime: alarm.time, editAlarmDesc: alarm.desc, editingAlarmId: alarmId }); }, // 删除闹钟 deleteAlarm(e) { const alarmId = e.currentTarget.dataset.id; const newAlarms = this.data.alarms.filter(alarm => alarm.id !== alarmId); this.setData({ alarms: newAlarms }); }, // 关闭弹窗 closeModal() { this.setData({ showModal: false }); }, // 保存闹钟 saveAlarm() { const newAlarm = { id: this.data.editingAlarmId || this.data.nextAlarmId++, time: this.data.editAlarmTime, desc: this.data.editAlarmDesc }; let newAlarms; if (this.data.editingAlarmId) { newAlarms = this.data.alarms.map(alarm => alarm.id === newAlarm.id ? newAlarm : alarm ); } else { newAlarms = [...this.data.alarms, newAlarm]; } this.setData({ alarms: newAlarms, showModal: false }); // 4. 更新全局数据(关键修复) // 提取小时和分钟 const [hour, minute] = newAlarm.time.split(\':\').map(Number); // 更新全局数据 赋值给全局变量 app.globalData.alarmTime = { hour: hour, minute: minute }; // 修复:调用正确的批量发送方法 if (this.data.bluetooth.connected) { this.sendAlarmsToBluetooth().catch(err => { console.error(\'蓝牙发送失败:\', err); }); } }, // 时间选择器变化事件 onTimeChange(e) { this.setData({ editAlarmTime: e.detail.value }); }, // 描述输入事件 onDescChange(e) { this.setData({ editAlarmDesc: e.detail.value }); }, // 停止设备发现 stopDiscovery() { wx.stopBluetoothDevicesDiscovery(); }, // 关闭连接 closeConnection() { if (this.data.bluetooth.connected) { wx.closeBLEConnection({ deviceId: this.data.bluetooth.deviceId }); this.setData({ \'bluetooth.connected\': false, \'bluetooth.deviceId\': \'\' }); } }});

微信小程序——蓝牙部分

效果图 

蓝牙思路:初始化、寻找蓝牙、连接蓝牙、传输数据(读取特征值然后把闹钟设置的时间当做特征值写入)。本系统是有俩个服务,服务一是传递时间;服务二是传递设定的闹钟。蓝牙部分的逻辑是连接完蓝牙后先进行服务一,若服务一出现异常就断开蓝牙;若服务一成功就进行服务二。

.js部分代码(每个项目的具体要求不同,可能需要修改相关参数与逻辑,但是基本逻辑是一样的)

// 蓝牙相关 async initBluetooth() { try { const { bluetoothEnabled } = await wx.getSystemInfo(); if (!bluetoothEnabled) throw { errCode: 10001, errMsg: \'设备不支持蓝牙\' }; await this.checkBluetoothAuth(); await wx.openBluetoothAdapter(); this.listenDeviceFound(); } catch (err) { this.handleBluetoothError(err); } }, listenDeviceFound() { wx.onBluetoothDeviceFound(result => { try { const devices = (Array.isArray(result.devices) ? result.devices : [result.devices]) .filter(device => device?.deviceId) .map(device => ( { deviceId: device.deviceId, name: this.generateDeviceName(device), RSSI: device.RSSI || 0, connected: device.deviceId === this.data.bluetooth.connectedDeviceId })); this.setData({ \'bluetooth.devices\': this.safeMergeDevices(this.data.bluetooth.devices, devices) }); } catch (err) { console.error(\'设备处理异常:\', err); } }); }, async connectDevice(e) { try { const device = e.currentTarget.dataset.device; if (!device?.deviceId) return; wx.showLoading({ title: \'连接中...\' }); await this.stopScan(); // 首次连接只处理服务1 await this.connectToService(device.deviceId, configService1); this.setData({ \'bluetooth.connected\': true, \'bluetooth.connectedDeviceId\': device.deviceId, \'bluetooth.currentService\': \'service1\' }); wx.showToast({ title: \'服务一连接成功\' }); } catch (err) { console.error(\'连接失败:\', err); wx.showToast({ title: err.message || \'连接失败\', icon: \'none\' }); } finally { wx.hideLoading(); } }, // 新增服务连接核心方法 async connectToService(deviceId, config) { try { // 创建BLE连接 await wx.createBLEConnection({ deviceId, timeout: 10000 }); // 获取目标服务 const { services } = await wx.getBLEDeviceServices({ deviceId }); const targetService = services.find(s => s.uuid.toLowerCase() === config.serviceId.toLowerCase() ); if (!targetService) throw new Error(\'未找到服务\'); // 验证特征值 const { characteristics } = await wx.getBLEDeviceCharacteristics({ deviceId, serviceId: targetService.uuid }); const validChars = Object.entries(config.characteristics).reduce((acc, [key, uuid]) => { acc[key] = characteristics.find(c =>  c.uuid.toLowerCase() === uuid.toLowerCase() && (c.properties.write || c.properties.writeWithoutResponse) ); return acc; }, {}); if (Object.values(validChars).some(v => !v)) { throw new Error(\'特征值验证失败\'); } return validChars; } catch (err) { await wx.closeBLEConnection({ deviceId }); throw err; } }, // 修改后的时间同步方法 async syncTimeToDevice(hours, minutes, seconds) { try { const deviceId = this.data.bluetooth.connectedDeviceId; // 写入服务1数据 await this.writeChunkedData( deviceId, configService1.serviceId, configService1.characteristics.hour, new Uint8Array([hours]).buffer ); await this.writeChunkedData( deviceId, configService1.serviceId, configService1.characteristics.minute, new Uint8Array([minutes]).buffer ); await this.writeChunkedData( deviceId, configService1.serviceId, configService1.characteristics.second, new Uint8Array([seconds]).buffer ); // 断开服务1连接 await wx.closeBLEConnection({ deviceId }); this.setData({ \'bluetooth.connected\': false, \'bluetooth.currentService\': null }); // 连接服务2并设置闹钟 await this.connectToService2(deviceId); } catch (err) { console.error(\'闹钟时间同步失败:\', err); wx.showToast({ title: \'闹钟同步失败\', icon: \'none\' }); } }, // 新增服务2连接方法 async connectToService2(deviceId) { wx.showLoading({ title: \'连接服务二...\' }); try { // 连接服务2 const validChars = await this.connectToService(deviceId, configService2); // 写入闹钟数据 const alarmBuffer = this.formatAlarmTime( this.data.naozhong.hour, this.data.naozhong.minute ); await this.writeChunkedData( deviceId, configService2.serviceId, configService2.characteristics.alarmTime, alarmBuffer ); this.setData({ \'bluetooth.connected\': true, \'bluetooth.currentService\': \'service2\' }); wx.showToast({ title: \'闹钟设置成功\' }); } catch (err) { console.error(\'服务二连接失败:\', err); wx.showToast({ title: \'设置失败\', icon: \'none\' }); } finally { wx.hideLoading(); } },

结语

由于篇幅有限,关于硬件部分的内容就先记了 。当然这个项目由于完成的时间较急且时间有限,因此存在较多问题,大家可以在此基础上多多完善。主要存在的问题问题如下:

1. 蓝牙连接稳定性不足

  • 问题描述:蓝牙传输逻辑目前部署在“个人中心”界面。当用户退出该界面时,蓝牙连接会中断。若用户随后在“智能闹钟”界面修改闹钟设置,新的闹钟时间将无法通过蓝牙同步至 ESP32 设备。

  • 改进建议

    • 多界面蓝牙支持:在“智能闹钟”界面增设蓝牙传输功能,确保用户在该界面修改闹钟设置时,数据能直接通过蓝牙发送,减少因切换界面导致的同步问题。

    • 持续连接机制:优化蓝牙逻辑,力求建立持久连接。通过在程序后台设置连接维持机制,如定时发送心跳包等策略,使蓝牙在不同界面切换时仍能保持稳定连接,保障数据实时同步至 ESP32。

PHP技术研究