前面已经写了三篇博客关于智能家居的,服务器全都是使用ONENET中国移动,他最大的优点就是作为数据收发的中转站是免费的。本篇使用专门适配MQTT协议的MQTT服务器,有公用的,也可以自己搭建(应该要钱),项目源码在最后
前言
本篇博客实现的功能和之前的智能家居系列类似,仅仅是把服务器换成了公用的mqtt服务器,在经过实测之后,个人觉得智能家居这种场景还是比较适合使用mqtt协议,仅仅是个人感觉。其实功能这一块我想到一个比较有意思的,就是用雨滴传感器和步进电机来模拟自动晾晒衣服的功能,大家应该能想象出来是啥意思,就是下雨了通过步进电机将晒衣服的架子给缩回来,不下雨了又给推出去,这还蛮有意思的。
前几篇博客:
一、项目总览
由于面包板还没到,线接的很乱就不拍照了,找到了之前做类似项目的照片,用这个代替一下:
微信小程序:
基本功能首先是检测家庭内部环境信息,其次就是微信小程序控制外设动作。
上面的实物图中有一个sim900A模块,是以前用到的,可以用于拨打电话和发送短信,要是有兴趣可以评论或者私信,我单独出一期。
可以看到本次微信小程序大改动,不再是前几篇那样简陋了,但是实现的功能还是一样的,这里就不再介绍了,请移步前几篇博客。
二、总体流程分析
1、了解mqtt协议
mqtt协议我单独出了一篇博客,参考:https://blog.csdn.net/m0_71523511/article/details/135905690
2、测试下位机与服务器的通信(mqtt.fx)
可以看到免费的mqtt服务器的域名是:broker.emqx.io,下位机上传数据使用TCP端口:1883。
通常当下位机上传数据逻辑写好之后不会直接用微信小程序进行测试,而是使用软件连接服务器来观察数据是否成功上传给了服务器,如果成功即可开始微信小程序的代码编写,没成功就调试,还是比较方便的。
这里使用mqtt.fx来进行测试:
在网上已经不好找到这个软件的早些版本了,这里给出网盘资源,下载后直接安装即可:
我用夸克网盘分享了「mqttfx-1.7.1-windows-x64.exe」,点击链接即可保存。打开「夸克APP」,无需下载在线播放视频,畅享原画5倍速,支持电视投屏。
链接:https://pan.quark.cn/s/8013f5c2151c
安装完成之后按照下面步骤设置:
首先点击那个蓝色的设置按钮,按下面这样设置好:
然后订阅单片机向服务器发送数据的topic:/smarthome/pub
此时只要下载在本篇文章结尾开源的代码(记得修改esp8266.c中的wifi名称和密码,其他的不用修改)并烧录,一会儿就可以在mqtt.fx软件的右边那个框看到上传上去的数据了。由于是公用的服务器,所以可能会导致数据流紊乱。如果测试没问题就可以打开微信小程序愉快的调试了。
3、搭建自己的MQTT服务器
这里我还没有尝试,但是有一个比较靠谱的视频:https://b23.tv/p8RkhU5
一般来说自己搭建多少是要钱的,如果免费的能满足尽量不要自己搭建,或者可以淘宝租一个月玩玩。
三、代码
1、下位机:
代码结构没变,还是那四个文件组成网络通信部分的代码:
①上传数据也没啥可以说的,直接参照上一篇博客即可(这次代码写的更漂亮,有参考价值)。
②接收数据在这里做了比较大的改动,这里使用两个键值对的逻辑进行发送和解析:
小程序下发的指令:
实际上对应的就是:{target:"fen",value:"1"}
下位机解析:
首先解析出前面这个键值对的值,后面的逻辑就是判断这个值是多少,比如是leds的话就再进行第二个键值对的解析,此时判断第二个键值对的值是0还是1以控制对应的外设。这种逻辑相较于前几次博客的项目而言是可以减少内存占用的,因为只需要初始化一个:cJSON *json , *json_value;
2、微信小程序:
代码风格跟之前的完全不一样,如果想详细了解的话可以学一学javascript,或者直接gpt问各行代码的意思。本设计用微信开发者工具原生框架进行小程序设计,用到了多个不同的文件来设计小程序,其中app.json文件是对小程序进行全局配置的,可以配置顶部文字和底部tabBar栏;以.wxml为后缀的文件用来设置小程序的整个页面及页面各部分内部的文字、数据、图片等;以wxss为后缀的文件用来在.wxml文件构建好的部分中设置规则,比如字体大小、颜色等;以.js为后缀的文件使用JavaScript语言处理小程序和用户的交互(与单片机的数据接收和发送)
①index.js
const app = getApp()
const{ connect } = require('../../utils/mqtt')
const mqttHost = 'broker.emqx.io'
const mqttPort = 8084
const deviceSub = '/mysmarthome/sub' //小程序发布指令的Topic
const devicePub = '/smarthome/pub' //小程序接收数据的Topic
const mpSubTopic = devicePub //mp指小程序(这样更直观)
const mpPubTopic = deviceSub //小程序发布指令的Topic
Page({
data: {
client: null,
Humi:0,
Temp:0,
huoyan:1,
gas:0,
leds:false,
ranqi:0,
fen:false,
water:false
},
//下发指令:led
onledsChange(event){
const that = this
console.log(event.detail.value);
const sw = event.detail.value
that.setData({leds:sw})
if(sw){
that.data.client.publish(mpPubTopic,JSON.stringify({
target:"leds",
value:1
}),function (err) {
if(!err){
console.log('成功下发指令打开led')
console.log('{target:"leds",value:1}')
}
})
}else{
that.data.client.publish(mpPubTopic,JSON.stringify({
target:"leds",
value:0
}),function (err) {
if(!err){
console.log('成功下发指令关闭led')
console.log('{target:"leds",value:0}')
}
})
}
},
//下发指令:风扇
onfenChange(event){
const that = this
console.log(event.detail.value);
const sw = event.detail.value
that.setData({fen:sw})
if(sw){
that.data.client.publish(mpPubTopic,JSON.stringify({
target:"fen",
value:1
}),function (err) {
if(!err){
console.log('成功下发指令打开排气扇')
}
})
}else{
that.data.client.publish(mpPubTopic,JSON.stringify({
target:"fen",
value:0
}),function (err) {
if(!err){
console.log('成功下发指令关闭排气扇')
}
})
}
},
//下发指令:水泵
onwaterChange(event){
const that = this
console.log(event.detail.value);
const sw = event.detail.value
that.setData({water:sw})
if(sw){
that.data.client.publish(mpPubTopic,JSON.stringify({
target:"water",
value:1
}),function (err) {
if(!err){
console.log('成功下发指令打开水泵')
}
})
}else{
that.data.client.publish(mpPubTopic,JSON.stringify({
target:"water",
value:0
}),function (err) {
if(!err){
console.log('成功下发指令关闭水泵')
}
})
}
},
//显示接受到的数据
onShow(){
const that = this
that.setData({
client:connect(`wxs://${mqttHost}:${mqttPort}/mqtt`)
})
that.data.client.on('connect',function (params) {
console.log('成功连接到mqtt服务器')
wx.showToast({
title: '连接成功',
icon:'success',
mask:true
})
that.data.client.subscribe(mpSubTopic,function (err) {
if(!err){
console.log('成功订阅设备上行数据Topic')
}
})
})
that.data.client.on('message',function (topic,message) {
console.log(topic);
let dataFromDev = {}
try {
dataFromDev = JSON.parse(message)
console.log(dataFromDev);
that.setData({
Temp:dataFromDev.Temp,
Humi:dataFromDev.Humi,
huoyan:dataFromDev.huoyan,
gas:dataFromDev.gas,
ranqi:dataFromDev.ranqi,
})
} catch (error) {
console.log('JSON解析失败',error)
}
})
},
})
②index.wxml
<!--index.wxml-->
<view class="page-container">
<!--头部部分-->
<view class="header-container">
<view class="header-one">
<view>
战损版
</view>
</view>
<view class="header-two">
<view>
智能家居
</view>
</view>
</view>
<!--数据部分-->
<view class="data-container">
<!--温度-->
<view class="data-card">
<image class="data-card__icon" src="/static/wendu.png" />
<view class="data-card__text">
<view class="data-card__title">
温度
</view>
<view class="data-card__value">
{{ Temp }} ℃
</view>
</view>
</view>
<!--湿度-->
<view class="data-card">
<image class="data-card__icon" src="/static/shidu.png" />
<view class="data-card__text">
<view class="data-card__title">
湿度
</view>
<view class="data-card__value">
{{ Humi }}%rh
</view>
</view>
</view>
<!--可燃气体浓度-->
<view class="data-card">
<image class="data-card__icon" src="/static/gas.png" />
<view class="data-card__text">
<view class="data-card__title">
可燃气浓度
</view>
<view class="data-card__value">
{{ ranqi }} ppm
</view>
</view>
</view>
<!--烟雾浓度-->
<view class="data-card">
<image class="data-card__icon" src="/static/yanwu.png" />
<view class="data-card__text">
<view class="data-card__title">
天然气浓度
</view>
<view class="data-card__value">
{{ gas }} ppm
</view>
</view>
</view>
<view class="data-card">
<image class="data-card__icon" src="/static/huoyan.png" />
<view class="data-card__text">
<view class="data-card__title">
火焰
</view>
<view class="data-card__value">
<view wx:if="{{huoyan == 1}}">
无
</view>
<view wx:elif="{{huoyan == 0}}">
有
</view>
</view>
</view>
</view>
<!--雨水-->
<view class="data-card">
<image class="data-card__icon" src="/static/yudi.png" />
<view class="data-card__text">
<view class="data-card__title">
雨
</view>
<view class="data-card__value">
<view wx:if="{{xiayu==1}}">
下雨
</view>
<view wx:elif="{{xiayu==0}}">
未下雨
</view>
</view>
</view>
</view>
<!--开启or关闭排气扇-->
<view class="data-card">
<image class="data-card__icon" src="/static/fen.png"/>
<view class="data-card__text">
<view class="data-card__title">
排气扇
</view>
<view class="data-card__value">
<switch checked="{{fen}}" bindchange="onfenChange" color="#3d7ef9"/>
</view>
</view>
</view>
<!--开启or关闭水泵-->
<view class="data-card">
<image class="data-card__icon" src="/static/water.png"/>
<view class="data-card__text">
<view class="data-card__title">
水泵
</view>
<view class="data-card__value">
<switch checked="{{water}}" bindchange="onwaterChange" color="#3d7ef9"/>
</view>
</view>
</view>
<!--开启or关闭卧室灯-->
<view class="data-card">
<image class="data-card__icon" src="/static/leds.png" />
<view class="data-card__text">
<view class="data-card__title">
卧室灯
</view>
<view class="data-card__value">
<switch checked="{{ leds }}" bindchange="onledsChange" color="#3d7ef9"/>
</view>
</view>
</view>
</view>
</view>
③index.wxss
/**小程序整个页面的设置**/
.page-container{
padding: 36rpx;
}
/**设置顶部大筐筐**/
.header-container{
background-color: #fff;
color: black;
box-shadow: #d6d6d6 0 0 20rpx;
border-radius: 36rpx;
padding: 3rpx 3rpx;
}
/**设置顶部大筐筐内的第一行字**/
.header-container .header-one{
display: flex;
justify-content: center;
padding: 10rpx;
font-size: 50rpx;
font: bold;
}
/**设置顶部大筐筐内的第二行字**/
.header-container .header-two{
display: flex;
justify-content: center;
padding: 10rpx;
font-size: 40rpx;
font: bold;
}
.choice {
display: flex;
flex-direction: row;
font-size: 20px;
justify-content: center;
color: rgb(38, 38, 39);
margin-top: 30rpx;
margin-bottom: 20rpx;
top: 3rpx;
box-shadow:0px 0px 10px #bfbfc0 inset;
}
.btn1 {
/* float:left;
background-color: rgb(112, 107, 107);
color: rgb(66, 63, 63); */
float:left;
top: 3rpx;
box-shadow:0px 0px 8px #999 inset;
}
.btn-class{
float:left;
top: 3rpx;
box-shadow:0px 0px 10px #b8b9bd inset;
}
.btn2 {
float:right;
top: 3rpx;
box-shadow:0px 0px 8px #999 inset;
}
.btn-class2{
float:right;
top: 3rpx;
box-shadow:0px 0px 10px #b8b9bd inset;
}
.btn3 {
float:left;
top: 3rpx;
box-shadow:0px 0px 8px #999 inset;
}
.btn-class3{
float:left;
top: 3rpx;
box-shadow:0px 0px 10px #b8b9bd inset;
}
.btn4 {
float:right;
top: 3rpx;
box-shadow:0px 0px 8px #999 inset;
}
.btn-class4{
float:right;
top: 3rpx;
box-shadow:0px 0px 10px #b8b9bd inset;
}
.s_view{
bottom: 0rpx;
width: 100%;
}
/**设置数据部分总体布局**/
.data-container{
margin-top: 30rpx;
display: grid;
justify-content: center;
grid-template-columns: repeat(auto-fill,300rpx);
grid-gap: 30rpx;
}
/**设置数据部分小卡片内的格式**/
.data-container .data-card{
position: relative;
background-color: #fff;
height: 140rpx;
box-shadow: #d6d6d6 0 0 8rpx;
border-radius: 36rpx;
display: flex;
justify-content: space-between;
padding: 16rpx;
}
/**以下是小卡片内的文本、图片、标题、数值设置**/
.data-container .data-card .data-card__text{
position: absolute;
top: 20rpx;
right: 24rpx;
text-align: right;
white-space: nowrap;
}
.data-container .data-card .data-card__icon{
position: absolute;
height: 72rpx;
width: 72rpx;
left: 32rpx;
top: 16rpx;
}
.data-container .data-card .data-card__value{
font-size: 40rpx;
margin-top: 20rpx;
}
.data-container .data-card .data-card__title{
font-size: 34rpx;
}
至此整个项目的关键点介绍完毕,可以想象出裸机还是可能会漏消息的,下一篇就是使用freertos来进行编程,使得整个系统的实时性再一次提高。
四、项目获取
提取码:2Nmg