物联网技术(AIR780E cat1-4G模块+小程序+腾讯云MQTT)驱动的智能井盖系统设计
目录
一、项目意义和项目简介 3
二、硬件设计 3
1.原理图设计 3
2.PCB设计 4
3、主要器件 4
3.1三轴加速度传感器 4
3.2可燃气体传感器 5
3.3GPS模块 5
4、MQTT通信协议设计 6
三、微信小程序设计 8
3.1、连接EMQX服务器步骤 8
3.2、MQTT发布订阅测试 12
3.3程序界面设计 19
1)APP主页面: 21
2)地图界面设计 36
3)设备信息界面设计 41
4)设备日志页面设计 51
本项目的开发的目的是为了实现井盖状态的实时监控和管理,提高城市管理的效率和安全性。随着城市化进程的加快,传统的井盖管理模式已经无法满足现代城市的需求。传统的管理模式不仅效率低下,而且无法实时有效地获得设备的信息,面对异常情况无法实现实时监控和快速高效的管理。通过采用先进的物联网技术,智能井盖系统不仅能够实时监测井盖的状态,还能够通过数据分析预测潜在的安全风险,从而实现对城市井盖的高效、智能化管理。这不仅是对传统井盖管理模式的一种技术革新,更是向智慧城市迈进的重要一步。
终端设备采用AIR780E模块作为主控器,电子模块有三轴加速度传感器、可燃气体传感器,AIR780E模块(4G模块)通过MQTT通信协议进行数据打包上传至EMQX服务器,在EMQX服务器中,用户可以自行订阅主题和发布主题,小程序通过MQTT通信协议订阅EMQX服务器的主题,服务器通过接收到硬件发布的数据,转发到微信小程序。

二、硬件设计
1.原理图设计
本项目硬件有两种设备角色,网关角色:AIR780E模块4G通信+lora通信+采集传感器信息;终端角色:stm32主控mcu+lora通信+采集传感器信息。同一个原理图和PCB,兼顾这两个角色,利于降低生产和管理、物流成本。

2.PCB设计
PCB的设计采用模块化器件,同时支持网关和终端,同时布局了4G模块和STM32。附上了项目名称,小程序二维码,学校LOGO。
3、主要器件
3.1三轴加速度传感器
工作原理:三轴加速度传感器通过检测物体在空间三个正交方向(X、Y、Z轴)上的加速度,来测量物体的运动状态。这种传感器能够将加速度转化为电阻、电压或电容的变化,进而通过电路处理输出为模拟或数字信号。
故障诊断:在井盖设备中部署三轴加速度传感器,实时监测设备的振动情况,通过分析振动数据并且上传到服务器来预测并防止潜在的机械故障。
local accel = mpu6xxx.get_accel()--获取加速度
data_pub["accel"] = accel
--local jsonaccle = json.encode(accel)
log.info("6050accel", "accel.x",accel.x,"accel.y",accel.y,"accel.z",accel.z)
log.info("gyro.x",gyro.x,"gyro.y",gyro.y,"gyro.z",gyro.z)
local json_data_pub = json.encode(data_pub)
3.2可燃气体传感器
工作原理:可燃气体传感器主要通过检测特定气体的浓度变化来工作,当气体浓度超过预设的安全水平时,传感器会触发警报系统。
故障诊断:可燃气体传感器的主要优点是能够提供实时监控,快速响应和高灵敏度,,这使它们能够迅速检测到气体泄漏并及时采取措施以防止事故发生。
log.debug("adc", "adc" .. tostring(adc_pin_1), adc.get(adc_pin_1))
local gas = adc.get(adc_pin_1)
data_pub["gas"] = gas
local jsongas = json.encode(gas)
log.info("gas", "jsongas", jsongas)
3.3GPS模块
说明: 代码中使用了libgnss库来获取GPS数据,并通过uart接口与GPS硬件通信。当GPS状态变为“FIXED”(定位成功)时,收集的位置信息被记录并用于发布。
关键部分代码:
local locStr = libgnss.locStr()
log.info("gnss", "locStr", locStr)
lat = string.sub(locStr, 1, 8)
lon = string.sub(locStr, 12, 19)
data_pub["lat"] = lat
data_pub["lon"] = lon
--local location= lat ..','.. lon
--local jsonloc = json.encode(location)
log.info("gnss", "jsonloc", lat)
log.info("gnss", "jsonloc", lon)
4、MQTT通信协议设计
说明: 在这个项目中,AIR780E模块使用MQTT协议,用于将传感器数据上传到云服务器。设备作为MQTT终端发布消息到服务器上的一个主题,其他客户端(如小程序或移动说明)可以订阅这个主题以接收数据。
关键部分代码:
VERSION = "1.0.0"
PROJECT = "gnsstest"
VERSION = "1.0.1"
-- LuaTools需要PROJECT和VERSION这两个信息
PROJECT = "adcdemo"
_G.sys = require("sys")
_G.sysplus = require("sysplus")
local mpu6xxx = require "mpu6xxx"
i2cid = 0
i2c_speed = i2c.FAST
local mqtt_host = "www.visionexpand.com.cn"
local mqtt_port = 1883
local mqtt_isssl = false
local client_id = "emqx"
local pub_topic = "$thing/up/property/IQMPOB8BI9/temp/humi"
local sub_topic = "mqtt"
local uart2_data = "text"
local adc_pin_1=1
local data_pub={ }
-- libgnss库初始化
libgnss.clear() -- 清空数据,兼初始化
if wdt then
wdt.init(9000) sys.timerLoopStart(wdt.feed, 3000) end
local uartid = 2
local result = uart.setup(
uartid,
115200,
8,
1
)
sys.taskInit(function()
if rtos.bsp() == "AIR780E" then
device_id = mobile.imei()
sys.waitUntil("IP_READY", 30000)
pub_topic = "$thing/up/property/IQMPOB8BI9/temp/humi"
sub_topic = "mqtt"
end
log.info("mqtt", "pub", pub_topic)
log.info("mqtt", "sub", sub_topic)
local mqttc = mqtt.create(nil, mqtt_host, mqtt_port, mqtt_isssl, nil)
mqttc:auth(client_id, nil, nil)
mqttc:autoreconn(true, 3000)
mqttc:on(function(mqtt_client, event, data, payload)
if event == "conack" then
sys.publish("mqtt_conack")
mqtt_client:subscribe(sub_topic)
elseif event == "recv" then
log.info("mqtt", "received", "topic", data, "payload", payload)
elseif event == "sent" then
log.info("mqtt", "sent", "pkgid", data)
end
end)
mqttc:connect()
sys.waitUntil("mqtt_conack")
将数据打包至EMQX服务器:
local mqtt_host = "www.visionexpand.com.cn"
local mqtt_port = 1883
local mqtt_isssl = false
local client_id = "emqx"
local pub_topic = "$thing/up/property/IQMPOB8BI9/temp/humi"
local sub_topic = "mqtt"
local uart2_data = "text"
-- libgnss库初始化
libgnss.clear() -- 清空数据,兼初始化
if wdt then
wdt.init(9000) sys.timerLoopStart(wdt.feed, 3000) end
sys.taskInit(function()
if rtos.bsp() == "AIR780E" then
device_id = mobile.imei()
sys.waitUntil("IP_READY", 30000)
pub_topic = "$thing/up/property/IQMPOB8BI9/temp/humi"
sub_topic = "mqtt"
end
log.info("mqtt", "pub", pub_topic)
log.info("mqtt", "sub", sub_topic)
local mqttc = mqtt.create(nil, mqtt_host, mqtt_port, mqtt_isssl, nil)
mqttc:auth(client_id, nil, nil)
mqttc:autoreconn(true, 3000)
mqttc:on(function(mqtt_client, event, data, payload)
if event == "conack" then
sys.publish("mqtt_conack")
mqtt_client:subscribe(sub_topic)
elseif event == "recv" then
log.info("mqtt", "received", "topic", data, "payload", payload)
elseif event == "sent" then
log.info("mqtt", "sent", "pkgid", data)
end
end)
mqttc:connect()
sys.waitUntil("mqtt_conack")
三、微信小程序设计
项目使用微信开发者工具进行设计与开发,使用的是JavaStript语言编写小程序代码。地图界面使用的是腾讯地图WebService API进行架构,数据界面使用MQTT通信协议和订阅主题实现数据接收功能,服务器使用的是在腾讯服务器上使用开源EMQX自主搭建的MQTT服务器,登陆界面将用户信息上传在云数据库上。
主要实现的功能:在数据界面通过EMQX服务器将STM32单片机上的可燃气、倾角、震动、报警、水位监测、井盖定位数据用MQTT通信协议和订阅主题进行接收,但是井盖定位数据则是发送到地图界面上,并在腾讯地图中获取实时定位,还有数据页面加上实时时间一一发送到设备日志页面上。
3.1、连接EMQX服务器步骤
- 新建mqtt.js库
先新建文件夹utils,再新建文件mqtt.min.js

官网下载地址:https://unpkg.com/mqtt@4.2.0/dist/mqtt.min.js
进行ctrl+a复制



const app = getApp()
var mqtt = require('../../utils/mqtt.min.js') //根据自己存放的路径修改
const appInstance = getApp();
Page({
data: {
motto: 'Hello World',
userInfo: {},
hasUserInfo: false,
canIUse: wx.canIUse('button.open-type.getUserInfo'),
canIUseGetUserProfile: false,
canIUseOpenData: wx.canIUse('open-data.type.userAvatarUrl') && wx.canIUse('open-data.type.userNickName'), // 如需尝试获取用户信息可改为false
temperture:'0',
humidity:'0',
displayData1: '0',
displayData2: '0',
displayData3: '0',
wl:'0',
gas:'0',
longitude:'0',
latitude:'0',
scanResult: ''
},
onLoad(){
this.doConnect()
if (wx.getUserProfile) {
this.setData({
canIUseGetUserProfile: true
})
}
this.setData({
nbTitle: '设备参数',
nbLoading: false,
nbFrontColor: '#ffffff',
nbBackgroundColor: '#D53e37',
})
},
doConnect(){
//如果你服务器开了连接验证,这里的参数要自己加上username和password等
const options = {
keepalive: 60, //60s
clean: true, //cleanSession不保持持久会话
protocolVersion: 4 ,//MQTT v3.1.1
clientId:Math.random().toString(36).substr(2)
};
let url = "wx://www.visionexpand.com.cn:8083/mqtt";//这个地址是emq官方的公开免费地址,请换成自己服务器的地址
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('订阅成功')}
})
})
//接收消息监听
client.on('message', (topic, message) => {
let msg=message.toString();
const data=JSON.parse(msg);
const timestamp = new Date().toISOString().substr(0, 19).replace('T', ' ');
console.log('温度:', data.temperture, '湿度:', data.humidity,'纬度:',data.longitude,'经度',data.latitude,'x轴:',data.x,'y轴:',data.y,'z轴:',data.z);
let data1=data.x;
let data2=data.y;
let data3=data.z;
let roundedData1=Math.round(data1*1e3)/1e3;
let roundedData2=Math.round(data2*1e3)/1e3;
let roundedData3=Math.round(data3*1e3)/1e3;
// 设置数据
this.setData({
temperture: data.temperture,
humidity: data.humidity,
longitude:data.longitude,
latitude:data.latitude,
displayData1: roundedData1,
displayData2: roundedData2,
displayData3: roundedData3,
});
appInstance.globalData.sharedLocation = {
latitude: data.latitude,
longitude: data.longitude
};
const updatedData = {
...data,
timestamp: timestamp
};
app.emitGlobalEvent('mqttDataUpdatedWithTime', updatedData);
})
},
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'
});
}
});
}
})
点击“编译“
可以看到我们的小程序已经连上EMQX服务器了

3.2、MQTT发布订阅测试
登录EMQX服务器官网http://193.112.181.228:18083/#/login?to=/websocket,注册自己的帐号和密码,点击“登录”。


点击“连接”

显示“已连接”

我们在浏览器中搜索emqx官网


点击“文档“

找到“开发者指南-MQTT客户端工具演示“

点击“MQTTX“

点击“下载MQTTX“

根据自己的电脑型号进行下载
自己注册一个名称,自己的服务器地址和端口号,用户名和密码与你自己MQTTX的账号密码一致,点击“连接”

显示“已连接”

可以看得到我的MQTTX已经连接上了EMQX

自行添加Topic,点击“确定”

复制自己刚刚新建的Topic

分别粘贴到订阅和发布

显示到已经订阅我们的主题了

{ "temperture":"90","humidity":"90","longitude":"113.970473","latitude":"22.556992"}

可以看到数据已经发布上去了

可以看到从EMQX把数据发送到MQTTX

可以看到我们的小程序已经接收到数据了

3.3程序界面设计
总体流程图如下图:

1)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 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)地图界面设计

流程图:
界面布局设计:
map.wxml
<view>
<map id="map"
class="map"
style="width:100%; height:1270rpx;"
latitude="{{latitude}}"
longitude="{{longitude}}"
scale="16"
markers="{{markers}}"
circles="{{circles}}"
polyline="{{polyline}}"
enable-3D="{{enable3d}}"
show-scale="{{showscale}}"
show-location="{{showLocation}}"
show-compass="{{showCompass}}"
enable-zoom="{{enableZoom}}"
enable-rotate="{{enableRotate}}"
enable-satellite="{{enableSatellite}}"
enable-traffic="{{enableTraffic}}"
enable-overlooking="{{enableOverlooking}}"
bindregionchange="regionChange"
bindtap="getLocation">
</map>
</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>
map.wxss
page{
height:200%;
}
#map {
padding: 10rpx;
}
.searchbox{
display:flex;
flex-direction:row;
align-items:center;
}
先在app.js定义一个全局变量,然后再index.js用这个函数取出经纬度变量的值,然后再map.js初始化这个函数,调用函数,setData实时更新显示数据,定位就出来了!
index.js
appInstance.globalData.sharedLocation = {
latitude: data.latitude,
longitude: data.longitude
};
app.js
globalData: {
sharedLocation: null
},
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, // 可选:更新页面数据中的经度值
});
},
浏览器搜搜微信小程序公众平台,点击微信公众平台

扫二维码登录

点击文档

找到组件-map并且往下滑找到地图服务API在小程序中的使用方法,里面有详细步骤和代码

3)设备信息界面设计

流程图:

wxml:
<form bindsubmit="submitDeviceInfo">
<view class="form-item">
<van-cell-group>
<label>设备编号:</label>
<van-row>
<van-col span="12" offset="21">
<van-icon name="scan" bindtap="scanQRCode" size="20px" />
</van-col>
</van-row>
<input name="deviceId" value="{{deviceId}}" />
</van-cell-group>
</view>
<view class="form-item">
<label>设备名称:</label>
<input name="name" placeholder="请输入设备名称"/>
</view>
<view class="form-item">
<label>安装地址:</label>
<input name="location" placeholder="请输入安装地址" />
</view>
<view class="form-item">
<label>联系方式:</label>
<input name="phone" placeholder="请输入联系方式"/>
</view>
<view class="form-item">
<label>安装时间:</label>
<input name="time" placeholder="请输入安装时间"/>
</view>
<view class="form-item">现场照片:
<button bindtap="upimage">上传图片</button>
<image src="{{src}}" alt=""></image>
</view>
<button bindtap="getOpenid">获取openid</button>
<button formType="submit">提交</button>
</form>
<button bindtap="shouquan">获取用户授权</button>
<button bindtap="sendOne">发送消息给单个用户</button>
<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>
调用云函数,实现用户填写信息扫码提交上传到云数据库中,并且授权用户信息,并且将用户信息根据模板发送到用户微信上,用户将可以看到订阅消息模板。
js:
Page({
data:{
openid: '',
},
onLoad(options) {
this.setData({
nbTitle: '设备报警',
nbLoading: false,
nbFrontColor: '#ffffff',
nbBackgroundColor: '#D53e37',
})
},
// 获取openid的函数
getOpenid: function() {
const that = this;
wx.login({
success: function(res) {
if (res.code) {
// 发送 res.code 到后台换取 openId, sessionKey, unionId
wx.cloud.callFunction({
name: "callloginopenid", // 假设这是您用来从code获取openid的云函数
data: {
code: res.code
},
success: function(res) {
if (res.result && res.result.openid) {
that.setData({
openid: res.result.openid // 存储openid到页面data中
});
console.log("获取openid成功", res.result.openid);
} else {
console.log("获取openid失败", res);
}
},
fail: function(err) {
console.log("调用云函数获取openid失败", err);
}
});
} else {
console.log('登录失败!' + res.errMsg);
}
}
});
},
//获取用户授权
shouquan(){
wx.requestSubscribeMessage({
//模板id,可以有多个模板参数(数组形式)
tmplIds: ['CGBtt8n1HS3M_ItMWUFraVXEgaG2GQV5XNAqWpZgvH0'], //自己的模板id
success(res){
console.log('授权成功',res)
},
fail(res){
console.log('授权失败',res)
}
})
},
scanQRCode: function() {
const that = this;
wx.scanCode({
success: function(res) {
console.log('扫描结果', res.result);
that.setData({
deviceId: res.result
});
},
fail: function(err) {
console.error('扫描失败', err);
wx.showToast({
title: '扫描失败',
icon: 'none'
});
}
});
} ,
submitDeviceInfo: function(e) {
const that = this;
// 获取表单数据
const formData = e.detail.value;
const deviceId = that.data.deviceId;
that.setData({
location: formData.location
});
// 调用云函数保存数据
wx.cloud.callFunction({
name: 'sendMsg1', // 云函数名称
data: {
deviceId,
location: formData.location,
image:formData.image,
phone:formData.phone,
name:formData.name,
time:formData.time
},
success: res => {
// 保存成功处理
wx.showToast({
title: '保存成功',
icon: 'success',
duration: 2000
});
},
fail: err => {
// 保存失败处理
wx.showToast({
title: '保存失败',
icon: 'none',
duration: 2000
});
console.error(err);
}
});
that.sendOne();
},
//发送消息给单个用户
sendOne:function(){
const that=this;
const deviceId = that.data.deviceId;
const location = that.data.location;
const openid = that.data.openid;
if (!openid) {
wx.showToast({
title: '请先获取openid',
icon: 'none',
duration: 2000
});
return; // 如果没有openid,则不继续执行
}
//调用编辑好的sendMsg1云函数
wx.cloud.callFunction({
//云函数的名字
name:"sendMsg1",
//传入openid
data:{
//把第一步获取到的openid粘过来
openid,//获取到的openid
deviceId,
location
}
}).then(res=>{
console.log("发送单条消息成功",res)
}).catch(res=>{
console.log("发送单条消息失败",res)
})
},
upimage(){
let that=this
wx.chooseImage({
count: 1,
sizeType:['original'],
sourceType:['album','camera'],
success(res){
console.log(res.tempFilePaths)
that.setData({
src:res.tempFilePaths
})
}
})
},
})
js:
const cloud = require('wx-server-sdk')
cloud.init({ env:cloud.DYNAMIC_CURRENT_ENV }) // 使用当前云环境
const db = cloud.database()
const _ = db.command
exports.main = async (event, context) => {
try {
const {deviceId, location, image, phone, name,time } = event
const wxContext = cloud.getWXContext();
const openid = wxContext.OPENID;
// 假设你有一个名为 device_info 的集合
const collection = db.collection('device_info')
// 添加记录
const docId = await collection.add({
data: {
deviceId: deviceId,
location: location,
image: image,
phone: phone,
name: name,
time:time,
// 其他字段,如创建时间等
createTime: db.serverDate()
}
})
console.log('数据库写入成功', docId)
// 获取当前的 UTC 时间并格式化为 "YYYY-MM-DD HH:mm" 格式
const currentDate = new Date();
const utcDate = new Date(Date.UTC(
currentDate.getUTCFullYear(),
currentDate.getUTCMonth(),
currentDate.getUTCDate(),
currentDate.getUTCHours(),
currentDate.getUTCMinutes(),
currentDate.getUTCSeconds()
));
const formattedUTCDate = utcDate.toISOString().substr(0, 19).replace('T', ' ');
// 数据库写入成功后,发送订阅消息给用户
const subscribeResult = await cloud.openapi.subscribeMessage.send({
touser: openid,
page: 'pages/xiaoxi/xiaoxi',
"data": {
thing1: {
value: location
},
thing2: {
value: "损坏"
},
character_string6: {
value:deviceId
},
phrase3: {
value: "成功修好"
},
date5: {
value: formattedUTCDate
}
},
templateId: 'CGBtt8n1HS3M_ItMWUFraVXEgaG2GQV5XNAqWpZgvH0' // 写入自己的模板id
})
return{
subscribeResult,// 返回发送结果
event,
openid: wxContext.OPENID,
appid: wxContext.APPID,
unionid: wxContext.UNIONID,
}
} catch (error) {
// 捕获并处理异常
console.error('发生错误', error)
return {
errcode: -1,
errmsg: '内部错误:' + error.message
}
}
}
app.js:
这里的env:”要填写自己的云开发环境”
onLaunch:function () {
if (!wx.cloud) {
console.error('请使用 2.2.3 或以上的基础库以使用云能力')
} else {
wx.cloud.init({
enx:"shenzhen-4gwpt20uc8316be1",
traceUser:true
})
}
},
在浏览器中搜索微信小程序官网,点击找到微信公众平台

扫描二维码

进入到页面

找到订阅消息并且点击

找到自己需要用到的,这样就可以找到自己的模板名称啦!

4)设备日志页面设计

流程图:

.wxml
<view class="boxs">
<view wx:for="{{messageLog}}" wx:key="*this">
<text>时间:{{item}}</text>
<!-- 显示报警信息 -->
<text>X轴与Y轴的角度: {{accelDisplay}},X轴与Z轴的角度: {{accelDisplay1}}</text>
<view wx:for="{{alarmMessages}}" wx:key="index">
<text style="color: red;">报警:{{item}}</text>
</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>
.wxss
.boxs{
margin-top: 10rpx;
}
// onGlobalEvent函数:用于注册全局事件的处理函数
onGlobalEvent: function(eventName, callback) {
// 如果当前对象没有globalEventHandlers属性(即还没有注册过任何事件),则初始化它为一个空对象
if (!this.globalEventHandlers) {
this.globalEventHandlers = {};
}
// 如果在globalEventHandlers对象中,指定的eventName事件还没有对应的处理函数数组,则初始化它为一个空数组
if (!this.globalEventHandlers[eventName]) {
this.globalEventHandlers[eventName] = [];
}
// 将传入的callback(处理函数)添加到指定eventName的事件处理函数数组中
this.globalEventHandlers[eventName].push(callback);
},
// emitGlobalEvent函数:用于触发全局事件,并调用所有注册在该事件上的处理函数
emitGlobalEvent: function(eventName, data) {
// 如果当前对象有globalEventHandlers属性,并且该属性中有指定的eventName事件,则执行以下操作
if (this.globalEventHandlers && this.globalEventHandlers[eventName]) {
// 遍历指定eventName事件的处理函数数组
this.globalEventHandlers[eventName].forEach(function(handler) {
// 调用每一个处理函数,并传入data参数
handler(data);
});
}
},
handleMqttDataUpdatedWithTime: function (data) {
// 将接收到的数据添加到日志中
const newMessage = `${data.timestamp}: 纬度:${(data.lat/100).toFixed(6)}, 经度:${(data.lon/100).toFixed(6)},气体${data.gas},电池电压:${data.volt},板载电压:${data.vbat},温度${data.temp}`;
const messageLog = this.data.messageLog.concat(newMessage);
// 检查pitch和roll并更新报警信息
let newAlarmMessages = [];
let data1 = data.volt;
let data2 = data.temp;
let roundedData2 = Math.round(data2 * 1e3) / 1e3;
let roundedData1 = Math.round(data1 * 1e3) / 1e3;
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({
latitude: (data.lat / 100).toFixed(6),
longitude: (data.lon / 100).toFixed(6),
lastUpdateTime: data.timestamp,
accelDisplay: accelDisplay,
accelDisplay1: accelDisplay1,
volt: roundedData2,
temp: roundedData1,
vbat: data.vbat,
messageLog: messageLog.slice(-100) // 假设只展示最近的10条消息
});
if (data.gas > 2000) {
if (!this.data.alarmMessages.some(msg => msg.includes('gas'))) {
newAlarmMessages.push('气体浓度超过阈值!');
}
}
if (pitch > 10|| pitch<-10) {
// 如果pitch报警且之前未显示过,则添加新的报警信息
if (!this.data.alarmMessages.some(msg => msg.includes('pitch'))) {
newAlarmMessages.push('X与Y轴有抖动现象!');
}
}
if (roll > 10 || roll<-10) {
// 如果roll报警且之前未显示过,则添加新的报警信息
if (!this.data.alarmMessages.some(msg => msg.includes('roll'))) {
newAlarmMessages.push('X与Z轴有抖动现象!');
}
}
// 如果新的报警信息和之前的相同(即没有新的报警),则不更新alarmMessages
if (newAlarmMessages.length > 0) {
this.setData({
alarmMessages: newAlarmMessages
});
}
对本项目感兴趣的同学、朋友,或者需要定制、学术论文使用的都可以微信扫码联系:


3、主要器件
流程图:
浙公网安备 33010602011771号