物联网技术(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

二、硬件设计

1.原理图设计

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

图片2

2.PCB设计

PCB的设计采用模块化器件,同时支持网关和终端,同时布局了4G模块和STM32。附上了项目名称,小程序二维码,学校LOGO。

图片33、主要器件

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终端发布消息到服务器上的一个主题,其他客户端(如小程序或移动说明)可以订阅这个主题以接收数据。

关键部分代码:

PROJECT = "mqttdemo"

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服务器步骤

  1. 新建mqtt.js库

先新建文件夹utils,再新建文件mqtt.min.js

  1. 找到mqtt.min.js库

官网下载地址:https://unpkg.com/mqtt@4.2.0/dist/mqtt.min.js

  1. 复制

进行ctrl+a复制

(4)粘贴到mqtt.js库

  1. 引用mqtt.js库

  1. 连接EMQX服务器

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'  

        });  

      }  

    });  

  }

})

点击“编译“

  1. 查看是否连接上EMQX

可以看到我们的小程序已经连上EMQX服务器了

3.2、MQTT发布订阅测试

1、注册EMQX服务器

登录EMQX服务器官网http://193.112.181.228:18083/#/login?to=/websocket,注册自己的帐号和密码,点击“登录”。

2、找到WebSocket客户端

3、连接客户端

点击“连接”

显示“已连接”

4、下载MQTTX

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

点击“EMQX社区|EMQX”

点击“文档“

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

点击“MQTTX“

点击“下载MQTTX“

根据自己的电脑型号进行下载

5、使用MQTTX

自己注册一个名称,自己的服务器地址和端口号,用户名和密码与你自己MQTTX的账号密码一致,点击“连接”

6、MQTTX连接

显示“已连接”

7、查看MQTTX是否连接上EMQX

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

8、添加订阅

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

9、复制订阅

复制自己刚刚新建的Topic

10、粘贴到EMQX

分别粘贴到订阅和发布

11、点击“订阅”

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

12、把以下代码复制到Payload

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

13、点击“发布“

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

14、查看MQTTX是否接收

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

15、查看小程序是否接收到数据

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

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

流程图:

界面布局设计:

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;

}

数据设计:

app.js

// 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);  

      });  

    }  

  },

.js

  // 处理带有时间戳的更新数据  

    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

            });

        }

对本项目感兴趣的同学、朋友,或者需要定制、学术论文使用的都可以微信扫码联系:

 

 

posted @ 2024-07-04 21:52  leida_3669  阅读(52)  评论(0)    收藏  举报