微信小程序设计思路
本帖最后由 dql2016 于 2021-7-18 11:04 编辑<p>本次项目中的重要环节之一就是微信小程序的设计,系统采集的各种传感器数据都需要送到微信小程序端显示,并进行后续的分析,逻辑控制。由于我是第一次接触小程序设计,期间踩了很多坑,不过掌握诀窍后,只要实现了一个功能的设计,其它类似功能都变得很容易了。</p>
<p>先来看一下系统结构图,本次项目打算设计4个功能节点,分别是照明控制节点、植物管家节点、环境监测节点、用电管理节点,各个节点的设计这里不再啰嗦,之前的帖子说明了各个功能节点的设计过程。既然微信小程序作为功能节点的数据显示和控制的人机交互界面,那么显然有什么功能节点就需要有对应的界面。这里我的设计思路是,一个界面上摆放多个功能模块,点击进去后就是对应的功能节点详细界面。然后需要有一个控制蓝牙连接的界面,方便后续对设备进行一些参数的设定,比如重命名设备(比如,一个设备可能存在多个就需要用户设定不同的名字来区分)。</p>
<p>微信小程序项目的结构如下,小程序包含一个描述整体程序的 <code>app</code> 和多个描述各自页面的 <code>page。</code>一个小程序主体部分由三个文件组成,app.js小程序逻辑,app.json小程序公共配置,app.wxss小程序公共样式表,必须放在项目的根目录。一个小程序页面由四个文件组成,分别是页面逻辑xx.js,页面结构xx.wxml,页面配置xx.json,页面样式表xx.wxss,<strong>描述页面的四个文件必须具有相同的路径与文件名(不一定跟文件夹同名)。</strong></p>
<p></p>
<p>如下图是环境监测页面的结构:</p>
<p>设置界面设计如下:主要由4个按钮组成。</p>
<p></p>
<p>点击搜索蓝牙设备按钮后,就开始搜索周围正在广播的蓝牙设备,并出现在蓝牙设备列表中,可以选中一个蓝牙设备进行连接。</p>
<p>选择蓝牙设备后,然后点击连接蓝牙设备按钮,就会去连接蓝牙设备了,一旦连接成功就会获取蓝牙设备的服务列表和特征值列表,若发现有通知权限的特征值,则会注册通知回调函数,这样一来就会收到功能节点采集的传感器数据了。设置页面的js逻辑代码如下:</p>
<pre>
<code class="language-javascript">// pages/bluetoothconfig/bluetoothconfig.js
const util = require('../../utils/util.js')
var event = require('../../utils/event.js')
var delayTimer; //用来控制是否持续服务发现
var isFound = false;
var app = getApp()
Page({
/**
* 页面的初始数据
*/
data: {
logs: [],
deviceArray: [],
currDeviceID: '请选择...',
currDeviceName:'',
},
//屏幕打开时执行的函数
onLoad: function () {
let that = this;
//接收别的页面传过来的数据
event.on('EnvMonitorSendData2Device', this, function(data) {
//另外一个页面传过来的data是16进制字符串形式
console.log("要发送给蓝牙设备的数据:"+data);
//var buffer = new ArrayBuffer(5)
var buffer=that.stringToBytes(data);
var dataView = new Uint8Array(buffer)
dataView = data;
wx.writeBLECharacteristicValue({
deviceId: app.globalData._deviceId,//蓝牙设备 id
serviceId: app.globalData._serviceId,//蓝牙特征值对应服务的 uuid
characteristicId: app.globalData._writeCharacteristicId,//蓝牙特征值的 uuid
value: buffer,//ArrayBuffer 蓝牙设备特征值对应的二进制值
success: function (res) {//接口调用成功的回调函数
console.log('发送成功')
},
fail: function(res) {//接口调用失败的回调函数
//发送蓝牙数据失败
console.log('发送失败')
}
}
)
}),
//灯光控制页面发来的RGB数据
event.on('LightControlSendData2Device', this, function(data) {
//另外一个页面传过来的data是16进制字符串形式
console.log("要发送给蓝牙设备的数据:"+data);
var a=data;
var b = a.indexOf("(")
var c = a.indexOf(")")
var d = a.substring(c,b+1)//100,200,230
var e=d.substring(0,d.indexOf(","));
console.log("分词R:"+parseInt(e,10))
var f=d.substring(d.indexOf(",")+1,d.lastIndexOf(",")+1);
console.log("分词G:"+parseInt(f,10))
var g=d.substring(d.length,d.lastIndexOf(",")+1);
console.log("分词B:"+parseInt(g,10))
var myrgb=this.myStringToHex(parseInt(e,10).toString(16))+this.myStringToHex(parseInt(f,10).toString(16))+this.myStringToHex(parseInt(g,10).toString(16));
console.log("myrgb:"+myrgb)
//var buffer = new ArrayBuffer(5)
//var dataView = new Uint8Array(buffer)
var buffer=that.stringToBytes(myrgb);
wx.writeBLECharacteristicValue({
deviceId: app.globalData._deviceId,//蓝牙设备 id
serviceId: app.globalData._serviceId,//蓝牙特征值对应服务的 uuid
characteristicId: app.globalData._writeCharacteristicId,//蓝牙特征值的 uuid
value: buffer,//ArrayBuffer 蓝牙设备特征值对应的二进制值
success: function (res) {//接口调用成功的回调函数
console.log('发送成功')
},
fail: function(res) {//接口调用失败的回调函数
//发送蓝牙数据失败
console.log('发送失败')
}
}
)
})
},
onUnload: function() {
event.remove('EnvMonitorSendData2Device', this);
event.remove('LightControlSendData2Device', this);
},
//按钮回调
clickMe: function() {
wx.showModal({
title: '确定发送数据吗?',
content: "01 02 03",
showCancel: true,
success (res) {
if (res.confirm) {
console.log('用户点击确定')
////////////////////////////////////////////
var buffer = new ArrayBuffer(3)
var dataView = new Uint8Array(buffer)
dataView = 1;
dataView = 2;
dataView = 3;
var that =this;
wx.writeBLECharacteristicValue({
deviceId: app.globalData._deviceId,//蓝牙设备 id
serviceId: app.globalData._serviceId,//蓝牙特征值对应服务的 uuid
characteristicId: app.globalData._writeCharacteristicId,//蓝牙特征值的 uuid
value: buffer,//ArrayBuffer 蓝牙设备特征值对应的二进制值
success: function (res) {//接口调用成功的回调函数
this.printLog('发送成功')
},
fail: function(res) {//接口调用失败的回调函数
//发送蓝牙数据失败
this.printLog('发送失败')
}
}
)
//////////////////////////
} else if (res.cancel) {
console.log('用户点击取消')
}
}
})
},
bindPickerChange: function(ret){
var array = this.data.deviceArray;
console.log(array);
this.setData({
currDeviceID: array
})
this.printLog("选中:" + array);
},
bleSearchEvent: function(ret){
this.initBLE();
},
bleConfigEvent: function (ret) {
var deviceID = this.data.currDeviceID;
console.log("选中:" + deviceID);
if (util.isEmpty(deviceID) || deviceID == "请选择..."){
util.toastError("请先搜索设备");
return ;
}
var device = deviceID.split('[');
if(device.length <= 1){
util.toastError("请先搜索设备");
return ;
}
var l=deviceID.split('[');
this.printLog("选中蓝牙设备名字:" + l);
this.setData({
currDeviceName: l
})
var id = device.replace("]", "");
console.log(id);
util.toastError("连接" + id);
this.createBLE(id);
},
bleCloseEvent: function(ret){
var deviceId = this.data.currDeviceID;
wx.showModal({
title: '确定断开设备吗?',
content: deviceId,
showCancel: true
})
var device = deviceId.split('[');
var id = device.replace("]", "");
this.printLog("断开设备:[" + id+"]...");
this.closeBLE(id,null);
},
initBLE: function() {
this.printLog("启动蓝牙适配器, 蓝牙初始化")
this.setData("启动蓝牙适配器, 蓝牙初始化")
var that = this;
wx.openBluetoothAdapter({
success: function(res) {
console.log(res);
that.findBLE();
},
fail: function(res) {
util.toastError('请先打开蓝牙');
}
})
},
findBLE: function() {
this.printLog("打开蓝牙成功.")
var that = this
wx.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false,
interval: 0,
success: function(res) {
wx.showLoading({
title: '正在搜索设备',
})
console.log(res);
delayTimer = setInterval(function(){
that.discoveryBLE() //3.0 //这里的discovery需要多次调用
}, 1000);
setTimeout(function () {
if (isFound) {
return;
} else {
wx.hideLoading();
console.log("搜索设备超时");
wx.stopBluetoothDevicesDiscovery({
success: function (res) {
console.log('连接蓝牙成功之后关闭蓝牙搜索');
}
})
clearInterval(delayTimer)
wx.showModal({
title: '搜索设备超时',
content: '请检查蓝牙设备是否正常工作,Android手机请打开GPS定位.',
showCancel: false
})
util.toastError("搜索设备超时,请打开GPS定位,再搜索")
return
}
}, 15000);
},
fail: function(res) {
that.printLog("蓝牙设备服务发现失败: " + res.errMsg);
}
})
},
discoveryBLE: function() {
var that = this
wx.getBluetoothDevices({
success: function(res) {
var list = res.devices;
console.log(list);
if(list.length <= 0){
return ;
}
var devices = [];
for (var i = 0; i < list.length; i++) {
//that.data.inputValue:表示的是需要连接的蓝牙设备ID,
//简单点来说就是我想要连接这个蓝牙设备,
//所以我去遍历我搜索到的蓝牙设备中是否有这个ID
/*var name = list.name || list.localName;
if(util.isEmpty(name)){
continue;
}
if(name.indexOf('MI') >= 0 && list.RSSI != 0){
console.log(list);
devices.push(list);
}*/
var deviceId = list.deviceId;
if(util.isEmpty(deviceId)){
continue;
}
if(list.RSSI != 0){
console.log(list);
devices.push(list);
}
}
console.log('总共有' + devices.length + "个设备需要设置")
if (devices.length <= 0) {
return;
}
that.connectBLE(devices);
},
fail: function() {
util.toastError('搜索蓝牙设备失败');
}
})
},
connectBLE: function(devices){
this.printLog('总共有' + devices.length + "个设备需要设置")
var that = this;
wx.hideLoading();
isFound = true;
clearInterval(delayTimer);
wx.stopBluetoothDevicesDiscovery({
success: function (res) {
that.printLog('搜索蓝牙设备成功之后关闭蓝牙搜索');
}
})
//两个的时候需要选择
var list = [];
for (var i = 0; i < devices.length; i++) {
var name = devices.name || devices.localName;
list.push(name + "[" + devices.deviceId + "]")
}
this.setData({
deviceArray: list
})
//默认选择
this.setData({
currDeviceID: list
})
},
createBLE: function(deviceId){
var app = getApp()
this.printLog("全局变量av0="+ app.globalData.av0);
this.printLog("连接: [" + deviceId+"]...");
var that = this;
//连接之前,先断开一下,防止设备已连接了
this.closeBLE(deviceId, function(res){
console.log("预先关闭,再打开");
setTimeout(function(){
wx.createBLEConnection({
deviceId: deviceId,
success: function (res) {
that.printLog("设备连接成功!");
event.emit('deviceConnectStatus',"在线");
app.globalData.deviceMac=deviceId;
that.getBLEServiceId(deviceId);
},
fail: function (res) {
that.printLog("设备连接失败:" + res.errMsg);
}
})
}, 2000)
});
},
//获取服务UUID
getBLEServiceId: function(deviceId){
this.printLog("获取设备[" + deviceId + "]服务列表")
var that = this;
wx.getBLEDeviceServices({
deviceId: deviceId,
success: function(res) {
console.log(res);
var services = res.services;
if (services.length <= 0){
that.printLog("未找到主服务列表")
return;
}
that.printLog('找到设备服务列表个数: ' + services.length);
if (services.length == 1)
{
var service = services;
that.printLog("服务UUID:["+service.uuid+"] Primary:" + service.isPrimary);
that.getBLECharactedId(deviceId, service.uuid);
}
else//多个主服务
{
for(var i=0;i<services.length;i++)
{
if(services.uuid=="E093F3B5-00A3-A9E5-9ECA-40016E0EDC24")
{
var service = services;
that.printLog("服务UUID:["+service.uuid+"] Primary:" + service.isPrimary);
app.globalData._deviceId=deviceId;
app.globalData._serviceId=service.uuid;
that.getBLECharactedId(deviceId, service.uuid);
}
}
}
},
fail: function(res){
that.printLog("获取设备服务列表失败" + res.errMsg);
}
})
},
getBLECharactedId: function(deviceId, serviceId){
this.printLog("获取设备特征值")
var that = this;
wx.getBLEDeviceCharacteristics({
deviceId: deviceId,
serviceId: serviceId,
success: function(res) {
console.log(res);
//这里会获取到两个特征值,一个用来写,一个用来读
var chars = res.characteristics;
if(chars.length <= 0){
that.printLog("未找到设备特征值")
return ;
}
that.printLog("找到"+serviceId+"特征值个数:" + chars.length);
if(chars.length >=1){
for(var i=0; i<chars.length; i++){
var char = chars;
that.printLog("特征值[" + char.uuid + "]")
var prop = char.properties;
if(prop.notify == true)
{
that.printLog("该特征值属性: Notify");
that.recvBLECharacterNotice(deviceId, serviceId, char.uuid);
}
else if(prop.write == true)
{
that.printLog("该特征值属性: Write");
app.globalData._writeCharacteristicId=char.uuid;
that.sendBLECharacterNotice(deviceId, serviceId, char.uuid);
}
else if(prop.read == true)
{
that.printLog("该特征值属性: Read");
that.sendBLECharacterNotice(deviceId, serviceId, char.uuid);
}
else if(prop.indicate == true)
{
that.printLog("该特征值属性: Indicate");
that.sendBLECharacterNotice(deviceId, serviceId, char.uuid);
}
else
{
that.printLog("该特征值属性: 其他");
}
}
}else{
//TODO
}
},
fail: function(res){
that.printLog("获取设备特征值失败")
}
})
},
recvBLECharacterNotice: function(deviceId, serviceId, charId){
//接收设置是否成功
this.printLog("注册Notice 回调函数");
var that = this;
//设备一旦发送数据在此通道,就会立刻收到通知
wx.notifyBLECharacteristicValueChange({
deviceId: deviceId,
serviceId: serviceId,
characteristicId: charId,
state: true, //启用Notify功能
success: function(res) {
wx.onBLECharacteristicValueChange(function(res){
console.log(res);
that.printLog("收到Notify数据: " + that.ab2hex(res.value));
//var app = getApp();
var tmp =that.ab2hex(res.value);
//app.globalData.av0=parseInt(tmp,16);
//event.emit('DataChanged',app.globalData.av0);
var cdn = that.data.currDeviceName;
that.printLog("当前蓝牙设备名字: " + cdn);
//发送数据到其它页面
if(cdn=='环境监测')
{
event.emit('environmetDataChanged',tmp);
}
else if(cdn=='用电管理')
{
event.emit('energyManageDataChanged',tmp);
}
else if(cdn=='照明控制')
{
event.emit('lightControlDataChanged',tmp);
}
else if(cdn=='植物管家')
{
event.emit('plantKeeperDataChanged',tmp);
}
//关闭蓝牙
/*wx.showModal({
title: '配网成功',
content: that.ab2hex(res.value),
showCancel: false
})*/
});
},
fail: function(res){
console.log(res);
that.printLog("特征值Notice 接收数据失败: " + res.errMsg);
}
})
},
sendBLECharacterNotice: function (deviceId, serviceId, charId){
var that = this;
var buffer = this.string2buffer(JSON.stringify(cell));
setTimeout(function(){
wx.writeBLECharacteristicValue({
deviceId: deviceId,
serviceId: serviceId,
characteristicId: charId,
value: buffer,
})
}, 1000);
},
closeBLE: function(deviceId, callback){
var that = this;
wx.closeBLEConnection({
deviceId: deviceId,
success: function(res) {
that.printLog("断开设备[" + deviceId + "]成功.");
event.emit('deviceConnectStatus',"离线");
console.log(res)
},
fail: function(res){
//that.printLog("断开设备[" + deviceId + "]失败!");
},
complete: callback//接口调用结束的回调函数(调用成功、失败都会执行)
})
},
printLog: function(msg){
var logs = this.data.logs;
logs.push(msg);
this.setData({ logs: logs })
},
/**
* 将字符串转换成ArrayBufer
*/
string2buffer(str) {
if (!str) return;
var val = "";
for (var i = 0; i < str.length; i++) {
val += str.charCodeAt(i).toString(16);
}
console.log(val);
str = val;
val = "";
let length = str.length;
let index = 0;
let array = []
while (index < length) {
array.push(str.substring(index, index + 2));
index = index + 2;
}
val = array.join(",");
// 将16进制转化为ArrayBuffer
return new Uint8Array(val.match(/[\da-f]{2}/gi).map(function (h) {
return parseInt(h, 16)
})).buffer
},
/**
* 将ArrayBuffer转换成字符串
*/
ab2hex(buffer) {
var hexArr = Array.prototype.map.call(
new Uint8Array(buffer),
function (bit) {
return ('00' + bit.toString(16)).slice(-2)
}
)
return hexArr.join('');
},
stringToBytes(str) {
var array = new Uint8Array(str.length);
for (var i = 0, l = str.length; i < l; i++) {
array = str.charCodeAt(i);
}
console.log(array);
return array.buffer;
},
myStringToHex(str){
var a='';
if(str.length == 1){
a += "0" + str;
}
else{
a=str;
}
return a.toUpperCase();// 统一大写格式输出
},
inputSSID: function(res) {
var ssid = res.detail.value;
this.setData({
ssid: ssid
})
},
inputPASS: function(res) {
var pass = res.detail.value;
this.setData({
pass: pass
})
}
})</code></pre>
<p>设置页面的逻辑代码除了控制蓝牙连接断开外,还会接收其它页面发来的数据(发给蓝牙设备),一开始在如何实现小程序不同页面之间的通信的时候踩了许多坑,后来在github上找到了一个很好用的开源库,用类似mqtt发布订阅的方式,一个页面订阅某个topic然后注册回调函数,另一个页面直接往这个topic发数据就行了。</p>
<p> </p>
<p>首页用九宫格的方式展示各个功能节点,如下图所示:</p>
<p></p>
<p>点击各个九宫格就可以跳转到对应的功能界面,如下图是环境监测功能界面:提供了温度、湿度、气压、空气质量、光照、声音6个表征环境的参数。还提供了一个按钮用于控制风扇运转,模拟通风功能。</p>
<p></p>
<p>环境监测功能界面各个图标点击后就会进入对应数据的曲线图界面(由于暂时还没有实现数据库存储采集数据的功能,这里曲线所用数据均为模拟数据):</p>
<p></p>
<p> </p>
<p></p>
<p> </p>
<p></p>
<p> </p>
<p></p>
<p> </p>
<p></p>
<p> </p>
<p></p>
<p>照明控制页面提供了3个按钮,2个按钮分别独立控制寝室灯和卫生间灯,另一个图标按钮则用于控制所有灯的开关。</p>
<p></p>
<p>氛围灯的颜色控制通过一个拾色器实现,点击后弹出拾色器,选择颜色后及时将数据发送给蓝牙设备。拾色器也不是微信小程序原生的功能,这里也是使用了github上的开源库。</p>
<p>植物管家界面提供了土壤湿度的显示,光照的显示,以及一个按钮用于控制浇水。</p>
<p></p>
<p>用电管理界面提供了电压、电流、功率的展示,并提供一个按钮用于总电源的通断,点击图标可进入曲线统计界面,可以显示最近用电量报表。</p>
<p></p>
<p>健康管理界面目前设想的几个功能点如下,限于时间和资源原因,仅仅设计好了微信小程序原型,没有实现相应的设备端。</p>
<p></p>
<p> </p>
<p>以上就是本次项目微信小程序各个界面的说明,其中的难点主要有3个,一是如何与蓝牙设备进行通信、读写特征值、读取通知数据、发送数据;二是不同页面间的通信,比如我在设置界面进行蓝牙数据收发,这个数据如何跟其它的页面传递;三是九宫格展示各个功能节点,点击后进入各个界面,之前一直找不到思路,直到在尝试使用echarts开源库进行曲线图表的绘制时候,发现echarts的demo就是九宫格,查看了一下源码廓然开朗。</p>
<p>首页九宫格实现代码主要如下:</p>
<p> </p>
<p>在index.wxml中绘制图标按钮,指定回调函数open并将index.js中定义的页面id作为参数传进去。</p>
<p></p>
<p>index.js的回调函数open中就根据id跳转到指定页面,这里的id值就是各个page的名字。</p>
<p>以环境监测界面为例看看逻辑功能如何实现:</p>
<pre>
<code class="language-javascript">// index.js
//加载事件通知库,用于不同页面发布和订阅事件
var event = require('../../utils/event.js')
var app=getApp()
Page({
data: {
charts: [{
id: 'wendu',
name: '温度',
data:'0',//温度值
unit:'℃'//温度单位
},{
id: 'shidu',
name: '湿度',
data:'0',
unit:'%'
},
{
id: 'pressure',
name: '气压',
data:'0',
unit:''
},
{
id: 'air',
name: 'AQI',
data:'0'
},
{
id: 'lux',
name: '光照',
data:'0',
unit:'Lux'
},
{
id: 'dmic',
name: '声音',
data:'0'
}],
/*
chartsWithoutImg: [{
id: 'aa',
name: '今日提醒'
}, {
id: 'bb',
name: '移动侦测'
}],*/
device_id:123,
device_status:"在线",// 显示是否在线的字符串
checked: false,//风扇的状态,默认关闭
fanIcon:"/utils/img/fanOff.png",//显示风扇图标的状态,默认是关闭状态图标
},
//点击图标的回调函数,这里就触发跳转到对应的曲线图界面
open: function (e) {
wx.navigateTo({
url: '../' + e.target.dataset.chart.id + '/' + 'line'
});
},
//屏幕打开时执行的函数
onLoad: function () {
//接收别的页面传过来的数据
event.on('deviceConnectStatus', this, function(data) {
//另外一个页面传过来的data是bool
console.log("蓝牙设备连接状态:"+data)
this.setData({
device_status:data
});
})
//接收别的页面传过来的数据
event.on('environmetDataChanged', this, function(data) {
//另外一个页面传过来的data是16进制字符串形式
console.log("接收到蓝牙设备发来的数据:"+data)
//温度 1byte
var a=parseInt(data+data,16);
//将温度放到云数据库,供曲线图界面读取
var newarray = [{
timestamp:"10:15",
v:89
}];
app.globalData.wendus.push(newarray);
//湿度 1byte
var b=parseInt(data+data,16);
//气压 4byte
var c=parseInt(data+data,16);
var d=parseInt(data+data,16);
var e=c*256+d;
//空气质量 4byte
var f=parseInt(data+data,16);
var g=parseInt(data+data,16);
var h=f*256+g;
//光照 4byte
var i=parseInt(data+data,16);
var j=parseInt(data+data,16);
var k=i*256+j;
//声音 4byte
var l=parseInt(data+data,16);
var m=parseInt(data+data,16);
var n=l*256+m;
//实时修改显示值
var up0 = "charts[" + 0 + "].data";
var up1 = "charts[" + 1 + "].data";
var up2 = "charts[" + 2 + "].data";
var up3 = "charts[" + 3 + "].data";
var up4 = "charts[" + 4 + "].data";
var up5 = "charts[" + 5 + "].data";
this.setData({
//wendu:app.globalData.v,//赋值全局变量
:a,
:b,
:e,
:h,
:k,
:n,
});
})
},
onUnload: function() {
event.remove('environmetDataChanged', this);
event.remove('deviceConnectStatus', this);
},
//控制风扇的函数,小滑块点击后执行的函数
onChange({ detail }){
this.setData({
checked: detail,
});
if(detail == true){
//发送'fefe'给蓝牙设备 开
event.emit('EnvMonitorSendData2Device','fefe');
this.setData({
fanIcon: "/utils/img/fanOn.png",
});
}else{
//发送'0101'给蓝牙设备 关
event.emit('EnvMonitorSendData2Device','0101');
this.setData({
fanIcon: "/utils/img/fanOff.png",
});
}
},
})
</code></pre>
<p>启动后注册设置页面蓝牙数据通知的回调,接收到通知数据后就解析数据,主要是注意数据格式的问题,蓝牙设置页面传过来的数据是16进制字符串形式。发送数据给蓝牙设备则是调用不同页面通信机制 event.emit('EnvMonitorSendData2Device','fefe');实现。由于各个图标点击后需要跳转到各个曲线图界面,因此这里的还是相当于一个”九宫格“。实现原理就不再赘述。</p>
<p> </p>
<p>以后我也玩玩微信小程序!!!</p>
页:
[1]