目录
一、项目意义和项目简介
二、硬件设计
1.原理图设计
2.PCB设计
3、主要器件
3.1三轴加速度传感器
3.2可燃气体传感器
3.3GPS模块
4、MQTT通信协议设计
三、微信小程序设计
3.1、连接EMQX服务器步骤
3.2、MQTT发布订阅测试
3.3程序界面设计
1)APP主页面:
2)地图界面设计
3)设备信息界面设计
4)设备日志页面设计
3.3程序界面设计
总体流程图如下图:
1)APP主页面:
APP主页面流程图
界面布局设计:
index.wxss
.background{
background-color: #dddddd;
width: 100%;
height: 100vh;
}
.swiper{
height: 300rpx;
}
.img{
width: 100%;
height: 300rpx;
}
.BoxsBar{
display: flex;
flex-wrap: wrap;
}
.Boxs{
width: 50%;
height: 260rpx;
display: flex;
margin-top: 20px;
justify-content: center;
text-align: center;
}
.Box{
display: flex;
justify-content: space-around;
width: 350rpx;
height: 260rpx;
border-radius: 20rpx;
background-color: #FFFFFF;
}
.image{
margin-top: 10rpx;
height: 220rpx;
width: 180rpx;
}
.List{
margin-top: 50rpx;
}
.list{
margin-top: 30rpx;
}
.text{
width: 54%;
}
.switch{
zoom:0.8;
width: 46%;
text-align:right;
}
index.wxml
<view>
<view class="background">
<van-cell-group>
<van-cell bindtap="scanQRCode" style="font-weight:bold" title="井盖编号" value="{{scanResult}}" size="large">
<van-icon name="scan" bindtap="onScan" size="20px" />
</van-cell>
</van-cell-group>
<button bindtap="subscribeButtonTap">授权订阅</button>
<view class="BoxsBar" style="width: 677rpx; height: 102rpx; display: flex; box-sizing: content-box">
<!-- cstx节点1 -->
<view class="Boxs" style="width: 706rpx; height: 100rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx">
<view class="Box" style="width: 800rpx; height: 102rpx; display: flex; box-sizing: border-box; position: relative; left: 24rpx; top: -6rpx">
<image class="image" src="/images/dianchi.png" style="width: 54rpx; height: 62rpx; display: block; box-sizing: border-box; position: relative; left: -89rpx; top: 6rpx"></image>
<view class="List" style="width: 220rpx; height: 62rpx; display: block; box-sizing: border-box">
<view style="font-weight: bold; position: relative; left: -318rpx; top: -18rpx">电池电压</view>
<view class="list">
<view style="position: relative; left: 3rpx; top: -92rpx">{{volt}}V</view>
</view>
</view>
</view>
</view>
<view class="Boxs" style="width: 706rpx; height: 100rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx">
<view class="Box" style="width: 800rpx; height: 102rpx; display: flex; box-sizing: border-box; position: relative; left: 24rpx; top: -33rpx">
<image class="image" src="/images/gongdian.png" style="width: 54rpx; height: 62rpx; display: block; box-sizing: border-box; position: relative; left: -83rpx; top: 6rpx">
</image>
<view class="List">
<view style="font-weight: bold; position: relative; left: -318rpx; top: -18rpx">板载电压</view>
<view class="list">
<view style="position: relative; left: 3rpx; top: -99rpx">{{vbat}}</view>
</view>
</view>
</view>
</view>
<!-- cstx节点1 -->
<view class="Boxs" style="width: 706rpx; height: 100rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx">
<view class="Box" style="width: 800rpx; height: 102rpx; display: flex; box-sizing: border-box; position: relative; left: 25rpx; top: -31rpx">
<image class="image" src="/images/qingxie.png" style="width: 54rpx; height: 62rpx; display: block; box-sizing: border-box; position: relative; left: -83rpx; top: 6rpx"></image>
<view class="List1">
<view style="font-weight: bold; position: relative; left: -276rpx; top: 0rpx">X与Y轴的倾斜度</view>
<view class="list">
<view style="display: flex; flex-direction: column;">
<view style="position: relative; left: 15rpx; top: -78rpx">{{accelDisplay}}</view>
</view>
</view>
</view>
</view>
</view>
<view class="Boxs" style="width: 706rpx; height: 100rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx">
<view class="Box" style="width: 800rpx; height: 102rpx; display: flex; box-sizing: border-box; position: relative; left: 24rpx; top: -54rpx">
<image class="image" src="/images/qingxie.png" style="width: 54rpx; height: 62rpx; display: block; box-sizing: border-box; position: relative; left: -83rpx; top: 6rpx"></image>
<view class="List1">
<view style="font-weight: bold; position: relative; left: -276rpx; top: 0rpx">X与Z轴的倾斜度</view>
<view class="list">
<view style="display: flex; flex-direction: column;">
<view style="position: relative; left: 15rpx; top: -73rpx">{{accelDisplay1}}</view>
</view>
</view>
</view>
</view>
</view>
<view class="Boxs" style="width: 706rpx; height: 100rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx">
<view class="Box" style="width: 800rpx; height: 102rpx; display: flex; box-sizing: border-box; position: relative; left: 24rpx; top: -38rpx">
<image class="image" src="/images/dingwei.png" style="width: 54rpx; height: 62rpx; display: block; box-sizing: border-box; position: relative; left: -83rpx; top: 6rpx"></image>
<view class="List">
<view style="font-weight: bold; position: relative; left: -318rpx; top: -18rpx">纬度 N°</view>
<view class="list">
<view style="position: relative; left: -12rpx; top: -101rpx">{{latitude}}°</view>
</view>
</view>
</view>
</view>
<view class="Boxs" style="width: 706rpx; height: 100rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx">
<view class="Box" style="width: 800rpx; height: 102rpx; display: flex; box-sizing: border-box; position: relative; left: 26rpx; top: -52rpx">
<image class="image" src="/images/dingwei.png" style="width: 54rpx; height: 62rpx; display: block; box-sizing: border-box; position: relative; left: -83rpx; top: 6rpx"></image>
<view class="List">
<view style="font-weight: bold; position: relative; left: -318rpx; top: -18rpx">经度 E°</view>
<view class="list">
<view style="position: relative; left: -22rpx; top: -87rpx">{{longitude}}°</view>
</view>
</view>
</view>
</view>
<view class="Boxs" style="width: 706rpx; height: 100rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx">
<view class="Box" style="width: 800rpx; height: 102rpx; display: flex; box-sizing: border-box; position: relative; left: 26rpx; top: -61rpx">
<image class="image" src="/images/qiti.png" style="width: 54rpx; height: 62rpx; display: block; box-sizing: border-box; position: relative; left: -83rpx; top: 6rpx"></image>
<view class="List">
<view style="font-weight: bold; position: relative; left: -318rpx; top: -18rpx">可燃气体</view>
<view class="list">
<view style="position: relative; left: -22rpx; top: -87rpx">{{gas}}</view>
</view>
</view>
</view>
</view>
<view class="Boxs" style="width: 706rpx; height: 100rpx; display: flex; box-sizing: border-box; left: 0rpx; top: 0rpx">
<view class="Box" style="width: 706rpx; height: 104rpx; display: flex; box-sizing: border-box; position: relative; left: 24rpx; top: -75rpx">
<image class="image" src="/images/temp.png" style="width: 54rpx; height: 62rpx; display: block; box-sizing: border-box; position: relative; left: -83rpx; top: 6rpx"></image>
<view class="List">
<view style="font-weight: bold; position: relative; left: -318rpx; top: -18rpx">温度</view>
<view class="list">
<view style="position: relative; left: -22rpx; top: -87rpx">{{temp}}°C</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<page-meta>
<navigation-bar title="{{nbTitle}}" loading="{{nbLoading}}" front-color="{{nbFrontColor}}" background-color="{{nbBackgroundColor}}" color-animation-duration="0" color-animation-timing-func="easeIn" />
</page-meta>
数据显示设计:
这里由于我的板子发送上来的数据有乱码,所以用了另外的方法解决,我先提取{}里面的数据,再把里面的数据进行JSON格式化,然后用setData进行将数据展示在页面上
//接收消息监听
client.on('message', (topic, message) => {
// 数据加载完成,隐藏Toast
wx.hideToast();
let msg = message.toString();
const data = JSON.parse(msg);
const timestamp = new Date().toISOString().substr(0, 19).replace('T', ' ');
// 假设lon和lat是浮点数,我们将它们乘以100并保留两位小数
const lonFormatted = (data.lon / 100).toFixed(6);
const latFormatted = (data.lat / 100).toFixed(6);
console.log('纬度:', latFormatted, '经度', lonFormatted, '角度:', accelDisplay, '气体:', data.gas, "电池电压:", data.volt, "4G模块电池电压", data.vbat, "板子温度:", data.temp, '角度:', accelDisplay1);
let data1 = data.volt;
let data2 = data.temp;
let roundedData2 = Math.round(data2 * 1e3) / 1e3;
let roundedData1 = Math.round(data1 * 1e3) / 1e3;
// 假设 data.accel 是您的定位数据
let accelDisplay = "";
let accelDisplay1 = '';
// 遍历并处理加速度数据
let accel = data.accel;
let pitch = 0,
roll = 0; // 假设roll是绕y轴的旋转(横滚角),pitch是绕x轴的旋转(俯仰角)
// 注意:在实际应用中,你可能需要使用Math.atan2而不是Math.atan,以获取正确的象限信息
// 这里为了简化示例,我们仅使用Math.atan并假设设备静止且没有旋转到负值区域
if (accel.x !== 0 && accel.y !== 0 && accel.z !== 0) {
// 俯仰角pitch(绕x轴旋转,与y轴和z轴构成的平面之间的角度)
pitch = Math.atan(accel.y / Math.sqrt(Math.pow(accel.x, 2) + Math.pow(accel.z, 2))) * (180 / Math.PI);
roll = Math.atan(accel.x / Math.sqrt(Math.pow(accel.y, 2) + Math.pow(accel.z, 2))) * (180 / Math.PI);
// 根据你的需求调整角度的正负号和范围(0-360或-180到180)
// 这里我们假设pitch和roll的范围是-90到90度
pitch = pitch > 180 ? 180 : pitch < -180 ? -180 : pitch;
roll = roll > 180 ? 180 : roll < -180 ? -180 : roll;
}
// 将加速度数据添加到accelDisplay中
accelDisplay += `${pitch.toFixed(2)}°`;
accelDisplay1 += `${roll.toFixed(2)}°`
// 设置数据
this.setData({
isLoading:false,
longitude: lonFormatted,
latitude: latFormatted,
gas: data.gas,
accelDisplay: accelDisplay,
accelDisplay1: accelDisplay1,
volt: roundedData1,
vbat: data.vbat,
temp: roundedData2
});
appInstance.globalData.sharedLocation = {
latitude: latFormatted,
longitude: lonFormatted
};
const updatedData = {
...data,
timestamp: timestamp
};
app.emitGlobalEvent('mqttDataUpdatedWithTime', updatedData);
if (data.gas > 2000 && !app.globalData.hasShownGasAlarm) {
wx.showModal({
title: '报警',
content: '气体浓度超过阈值!',
showCancel: true, // 不显示取消按钮
success(res) {
if (res.confirm) {
// 用户点击确定后的回调,这里可以什么都不做或者执行其他逻辑
}
// 更新全局标志位
app.globalData.hasShownGasAlarm = true;
}
});
}
if (pitch.toFixed(2) > 10 && !app.globalData.hasShownGasAlarm1) {
wx.showModal({
title: '报警',
content: '有抖动现象!',
showCancel: true, // 不显示取消按钮
success(res) {
if (res.confirm) {
// 用户点击确定后的回调,这里可以什么都不做或者执行其他逻辑
}
// 更新全局标志位
app.globalData.hasShownGasAlarm1 = true;
}
});
}
if (pitch.toFixed(2) < -10 && !app.globalData.hasShownGasAlarm1) {
wx.showModal({
title: '报警',
content: '有抖动现象!',
showCancel: true, // 不显示取消按钮
success(res) {
if (res.confirm) {
// 用户点击确定后的回调,这里可以什么都不做或者执行其他逻辑
}
// 更新全局标志位
app.globalData.hasShownGasAlarm1 = true;
}
});
}
if (roll.toFixed(2) > 10 && !app.globalData.hasShownGasAlarm2) {
wx.showModal({
title: '报警',
content: '有抖动现象!',
showCancel: true, // 不显示取消按钮
success(res) {
if (res.confirm) {
// 用户点击确定后的回调,这里可以什么都不做或者执行其他逻辑
}
// 更新全局标志位
app.globalData.hasShownGasAlarm2 = true;
}
});
}
if (roll.toFixed(2) < -10 && !app.globalData.hasShownGasAlarm1) {
wx.showModal({
title: '报警',
content: '有抖动现象!',
showCancel: true, // 不显示取消按钮
success(res) {
if (res.confirm) {
// 用户点击确定后的回调,这里可以什么都不做或者执行其他逻辑
}
// 更新全局标志位
app.globalData.hasShownGasAlarm1 = true;
}
});
}
if (roundedData1 === 0) {
// 调用云函数发送订阅消息(这里需要确保你有调用云函数的权限)
wx.cloud.callFunction({
name: 'sendSubscriptionMessage', // 云函数名称
data: {
volt: roundedData1 // 传递需要的数据给云函数
},
success: res => {
console.log('云函数调用成功', res)
},
fail: err => {
console.error('云函数调用失败', err)
}
})
}
})
},
// 按钮点击事件处理函数
subscribeButtonTap: function () {
wx.requestSubscribeMessage({
tmplIds: ['BChyKXOjPqeDNQnq8POD3DMFxAxmpam38Hk3FjawP98'], // 替换为你的模板ID
success(res) {
// 用户同意订阅
console.log('用户同意订阅', res);
// 这里可以保存用户的订阅状态到数据库等
// 或者在符合发送条件时直接调用云函数发送订阅消息
},
fail(err) {
// 用户拒绝订阅
console.log('用户拒绝订阅', err);
}
});
},
假设你们收到的数据中存在[][]{传感器的数据},以下代码是我的解决方法,首先我在数据中看到的是[][]{传感器的数据},然后进行找到并提取一个括号里面的内容作为对象。
client.on('message', (topic, message) => {
let msg = message.toString();
var str = msg;
var regex = /\{([^}]*)\}/;
var match = str.match(regex);
if (match) {
// match[0] 是整个匹配到的字符串,包括{}
// match[1] 是第一个括号内的内容,即{}里面的内容
var contentInsideBrackets = match[1];
// 将提取的内容转换为对象,方便访问
try {
var timestamp = new Date().toISOString();
var data = JSON.parse('{' + contentInsideBrackets + '}');
// 假设提取的内容是键值对形式的字符串,并且可以直接转换为对象
// 这里我们定义变量来存储温度和湿度数据
var temperture= data.temperture;
var humidity = data.humidity;
console.log("Temperature:",temperture);
console.log("Humidity:", humidity);
console.log("timestamp",timestamp);
} catch (e) {
// 如果转换失败,可能是因为内容格式不正确
console.error("Failed to parse the content inside brackets:", e);
}
this.setData({
temperture:temperture,
humidity:humidity,
timestamp:timestamp
});
};
通信功能程序:
我们用的是MQTT通信方式进行连接EMQX服务器
const options = {
keepalive: 60, //60s
clean: true, //cleanSession不保持持久会话
protocolVersion: 4 ,//MQTT v3.1.1
clientId:Math.random().toString(36).substr(2)
};
console.log(options)
let url = "wx://www.visionexpand.com.cn:8083/mqtt";//这个地址是emq官方的公开免费地址,请换成自己服务器的地址
console.log(url);
const client = mqtt.connect(url,options)
client.on('connect', function () {
console.log('连接emqx服务器成功')
client.subscribe('$thing/up/property/IQMPOB8BI9/temp/humi',{qos:2},function(err){
if(!err)
{console.log('订阅成功')}
})
})
二维码扫描识别功能:
用户使用手机点击扫码,进行实现扫描二维码功能
data:{
scanResult: ''
},
scanQRCode: function() {
const that = this;
wx.scanCode({
success: function(res) {
console.log('扫描结果', res.result);
that.setData({
scanResult: res.result
});
},
fail: function(err) {
console.error('扫描失败', err);
wx.showToast({
title: '扫描失败',
icon: 'none'
});
}
});
}
2)地图界面设计
流程图:
数据显示设计:
先在app.js定义一个全局变量,然后再index.js用这个函数取出经纬度变量的值,然后再map.js初始化这个函数,调用函数,setData实时更新显示数据,定位就出来了!
map.js
getFuzzyLocation: function (message) {
const sharedLocation = appInstance.globalData.sharedLocation;
const marker = {
id: 1, // 标记的id,可以根据需要设置
latitude: sharedLocation.latitude, // 纬度
longitude: sharedLocation.longitude, // 经度
name: '井盖保镖',
desc: '我现在的位置'
};
this.setData({
markers: [marker], // 设置markers数组,只包含一个标记
latitude: sharedLocation.latitude, // 可选:更新页面数据中的纬度值
longitude: sharedLocation.longitude, // 可选:更新页面数据中的经度值
});
},
地图插件:
浏览器搜搜微信小程序公众平台,点击微信公众平台
接下来的内容还有很多,就不一一列出了。
对本项目感兴趣的同学、朋友,或者需要定制、学术论文使用的都可以扫码联系: