文章目录
1. 硬件、接线、环境配置
2. 项目简介
2.1 初衷
- 终于,物联网初探系列来到了完结篇,也就是将之前所学的进行集成,展现一个完整的小项目。本次小项目的主要内容是实现基于ESP32和微信小程序的土壤湿度监测,这也是本专栏的初衷,为家里养殖的柠檬监控湿度,适时浇水。
2.2 技术路线
-
这个小项目涉及的基础知识主要有:
- Arduino 下的 ESP32 基本编程,UDP/TCP通信;
- 微信小程序的基本开发技能,账号注册使用、开发工具使用、能够进行基本调试测试;
- 一点点 ECharts 的知识,一点点 JS/HTML/CSS 基础;
- 如果会 3D 打印更好,可以利用 Fusion360等建模工具简单设计并打印一个外壳;
-
那么整体的技术路线主要包括以下两部分内容:
-
在ESP32上编写土壤湿度传感器读取、UDP/TCP通信的代码,并将读取后的信息以UDP或TCP的通信方式发送至手机小程序端;
-
小程序端接受 UDP/TCP 发送来的数据,简单画一点界面显示当前实时湿度,配合 ECharts 动态显示历史测量数据;
-
3. 实现方法
3.1 接线及电源选型
-
湿度传感器与ESP32的连接已经在上一篇讲解了,这里主要涉及到一个问题是供电方案的设计,该项目主要的需求是,尽可能长时间的监控土壤湿度,尽量不需要总去插拔电路,如果能够24小时供电是最好的,另外,要便宜。
-
基于上述考虑,我一开始尝试了下面这种两节 18650 供电的方案,因为手头有一些闲置的 18650 充电电池,所以第一时间想到利用起来,但是实际使用的问题是,柠檬一般放在阳光充足的地方,电池不可避免的会晒到一些太阳,长时间使用有一定的风险,另外,电池容量有限,如果没电了,还得给电池充电,虽然支持边充边放,但是也比较麻烦。
-
通过进一步在TB上搜索,发现了一个便宜又好用的东西,就是下面这种太阳能充电宝,本身带有一定容量,同时太阳能也可充电,如果白天阳光的强度和时长充足的话,应该能够实现24H不间断监控,并且价格也在能接受的范围内。
3.2 ESP32 端程序
3.2.1 源码
-
在ESP32 上运行的代码主要是读取传感器数据,利用之前标定的参数计算相对湿度参考值,最后通过 UDP 发送至指定的远程 IP 和端口。
//for this esp32 , pin4 = G32 #include <WiFi.h> const char *ssid = "**"; const char *password = "**"; float c_min = 2590.0; //readings in air float c_max = 1090.0; //readings in water float m_min = 0.0; //min soil moisture float m_max = 100.0; //max soil moisture const int m_Pin = 32; //与wifi不冲突的pin //声明一个本地udp,和两个远程udp对象 WiFiUDP Udp_Local, Udp_Remote; IPAddress remote_IP(192, 168, **, **);//远程设备的局域网IP unsigned int remote_UdpPort = 6060; // 远程监听端口,先初始化为任意值 unsigned int local_UdpPort = 23415; // 本地监听端口,自定义 void setup() { Serial.begin(9600); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(200); Serial.print("."); } Serial.println("Connected"); Serial.print("IP Address:"); Serial.println(WiFi.localIP()); //开启本地UDP端口监听,用于接收小程序发回来的“远程UDP端口号”。 Udp_Local.begin(local_UdpPort); } void loop() { char buf[10]; Udp_Local.parsePacket();//解析UDP数据 Udp_Local.read(buf, 10);//存如字符数组 String str_port = buf;//转为字符串 if (str_port.length() >= 4)//简单判断端口号长度 { remote_UdpPort = str_port.toInt();//得到远程UDP端口号 Serial.println(remote_UdpPort); } Udp_Remote.beginPacket(remote_IP, remote_UdpPort);//配置远端ip地址和端口 int c_cur = analogRead(m_Pin);//读取GPIO4上的模拟数据 int m_cur = (c_cur - c_min) * (m_max - m_min) / (c_max - c_min);//公式(1) String str_m_cur(m_cur);//转字符串 Udp_Remote.println(str_m_cur);//把数据写入发送缓冲区 Udp_Remote.endPacket();//发送数据 Serial.println(str_m_cur); delay(1000);//1s }
3.2.2 特别说明
-
由于 Arduino 下写ESP32程序,这个
<WiFi.h>
库里没提供 UDP 广播的操作,也就是说,在这种编程环境下,只能跟已知 IP 和端口的远程端进行通信,一开始我的处理办法是,ESP32 向手机小程序端的固定 IP 和端口发消息,但是貌似是微信小程序自己的 bug ,每次在小程序里绑定一个固定端口时,下一次再进入小程序,就会发现该端口被占用了,无论何种方式都不能正确的释放该端口,造成收不到 ESP32 发送的数据。 -
针对上一问题,思考了一种折衷的办法,小程序的 UDP 类中的
bind()
绑定端口是可以不指定端口号的,由系统随机分配一个可用的端口,该函数执行后会返回这个端口号,那么我们要做的就是让 ESP32 也知道这个可用的端口号,并且以这一新端口号进行 UDP 通信。 -
如此这般,上面的程序就呈现出这个样子, 我们先随便定义一个端口号,然后在 loop() 中等待
Udp_Local.parsePacket()
获取小程序发来的端口号,在小程序上我写了一个下拉刷新的函数,每次下拉刷新就会重新绑定端口并发送至 ESP32 。unsigned int remote_UdpPort = 6060; // 远程监听端口,先初始化为任意值 //...... void loop() { char buf[10]; Udp_Local.parsePacket();//解析UDP数据 Udp_Local.read(buf, 10);//存如字符数组 String str_port = buf;//转为字符串 remote_UdpPort = str_port.toInt();//得到远程UDP端口号 //....... }
-
上述操作的基础是,手机和ESP32都在同一个局域网下,对于常见的路由器,每个设备只要连过一次该 WIFI ,它的 IP 一般是不会变的,在这点基础上,我们在 ESP32 上是把手机端 IP 写死的,而端口是根据小程序发回来的值设定的;在小程序端,我们是把 ESP32 端的 IP 和端口都写死的(小程序发送固定端口没有问题,仅接收有问题)。
-
当前,采用非 Arduino 的编译方案,以及具有更高超的小程序编写技巧都可以从别的角度解决上述问题,本文仅是讨论了一种简单、可行的方式。
3.3 微信小程序端
3.3.1 参考例程
- 本项目大量参考了learn-esp8266-sdk 这个项目中的 1.02 部分,该程序虽然是 ESP8266 的,但是实现的功能跟我的需求完全一致,本项目的需求也仅仅是在手机上查看实时的土壤湿度,大家也可以在该开源代码的基础上自行修改自己想要的功能,如果对小程序不了解,强烈建议去 B 站先刷一点小程序开发的基础教学视频。
3.3.2 ECharts 集成
- 为了进一步追求一点点可用性,想在小程序端看实时的湿度变化曲线,这里使用了 ECharts 实现图表显示,对于微信小程序,我参考了echarts-for-weixin 这个项目中的源码,这个项目中也详细解释了怎么在小程序中使用 ECharts,详见该项目。
3.3.3 小程序下拉刷新发送 udp 端口号
-
设置 app.json 中的参数
"enablePullDownRefresh": true
-
在需要的页面 page.js 中的 Page 函数部分,重载 onPullDownRefresh() 函数
Page({ onPullDownRefresh() { udp.send({ address: '192.168.xx.xx', port: 23415, message: port.toString() }) console.log(port) wx.stopPullDownRefresh({ success: (res) => {}, }) })
3.3.4 源码
-
小程序内部的源码较多,这里我主要开发了一个单页面的程序,页面上半部分显示湿度值,下半部分显示湿度变化曲线,该页面名为 main_page ,相关的四个文件为
.js .json .wxml .wxss
,源码如下: -
main_page.js
//index.js //获取应用实例 import * as echarts from '../ec-canvas/echarts'; var util = require("../utils/utils.js"); const app = getApp() var udp; var port; var mychart = null; //chart 实例 var myoption = null; //option 实例 //echart function initChart(canvas, width, height, dpr) { mychart = echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr // new }); canvas.setChart(mychart); myoption = { title: { text: '土壤湿度变化曲线', left: 'center' }, legend: { data: ['mosi (%)'], top: 30, left: 'center', z: 200 }, grid: { containLabel: true }, tooltip: { show: true, trigger: 'axis' }, xAxis: { type: 'category', boundaryGap: 5, data: [], // show: false }, yAxis: { x: 'center', type: 'value', splitLine: { lineStyle: { type: 'dashed' } } // show: false }, series: [{ name: 'mosi (%)', type: 'line', smooth: true, data: [0] }] }; mychart.setOption(myoption); return mychart; } Page({ data: { ec: { onInit: initChart }, humidity: "0", //湿度 mos_color: "blue" }, onLoad() { udp = wx.createUDPSocket() console.log("create") port = udp.bind() }, onUnload() { udp.close() }, onPullDownRefresh() { udp.send({ address: '192.168.31.201', port: 23415, message: port.toString() }) console.log(port) wx.stopPullDownRefresh({ success: (res) => {}, }) }, onShow: function () { let _this = this; this.setData({ humiditytext: this.data.humidity, }) //UDP接收到消息 var that = this; udp.onMessage(function (res) { let str = util.newAb2Str(res.message); //接收消息 that.setData({ humiditytext: str }); //arduino 上 1 秒一个数,最大计算1天也就是 7*24*360 = 60480 if (myoption.series[0].data.length > 60480) { myoption.series[0].data.shift() myoption.series[0].data.push(str) } else { myoption.series[0].data.push(str) } mychart.setOption(myoption) if (Number(str) <= 40) { that.setData({ mos_color: "red" }) } else { that.setData({ mos_color: "green" }) } }); } })
-
main_page.json
{ "usingComponents": { "ec-canvas": "../ec-canvas/ec-canvas" }, "navigationBarTitleText": "土壤湿度监控" }
-
main_page.wxml
<view class='main'> <view class='title_view'> <text class='title_text'> 实时土壤湿度 Real Time Mositure </text> </view> <view class="temperature_humidity"> <view class='humidity_view'> <image class="humidity" src="/images/humidity.png "></image> <text class='humiditytext' style="color: {{mos_color}};"> = {{humiditytext}} % </text> </view> </view> <view class='note_view'> <text class='note_text'> (提示:该土壤湿度为参考值,0% 对应空气中测量值,100% 对应水中测量值,低于 40% 可浇水。) </text> </view> <view class="container"> <ec-canvas id="mychart-dom-line" canvas-id="mychart-line" ec="{{ ec }}"></ec-canvas> </view> </view>
-
main_page.wxss
.main{ width:100%; height:100%; display: flex;/*main这个框里面的元素使用flex布局方式*/ flex-direction: column; /*里面的元素这样从上到下排列*/ position:fixed; background-color: #f0ffff } .title_view{ display:block;/*这个框里面的元素使用flex布局方式*/ flex-direction:row;/*左右排列控件,从左到右,水平线就叫做主轴,竖直的就叫做交叉轴*/ text-align: center; margin-top: 40rpx; } .title_text{ padding-top: 25px; font-size:30px; text-align: center; height: 80rpx; font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; } .note_view{ display:block;/*这个框里面的元素使用flex布局方式*/ flex-direction:row;/*左右排列控件,从左到右,水平线就叫做主轴,竖直的就叫做交叉轴*/ text-align: center; margin-top: 40rpx; margin-left: 8%; margin-right: 8%; margin-bottom: 0rpx; } .note_text{ font-size:15px; } .temperature_humidity{ display: flex;/*这个框里面的元素使用flex布局方式*/ flex-direction:row;/*左右排列控件,从左到右,水平线就叫做主轴,竖直的就叫做交叉轴*/ } /*温湿度 View*/ .humidity_view{ display: flex;/*这个框里面的元素使用flex布局方式*/ flex-direction:block;/*左右排列控件,从左到右,水平线就叫做主轴,竖直的就叫做交叉轴*/ margin-top: 30rpx; margin-left: 25%; } /*温湿度 图片大小*/ .humidity{ margin-right: 30rpx; width: 100rpx; height: 100rpx } /*温湿度 显示的文字设置*/ .humiditytext{ padding-top: 0px; font-size:40px; text-align: center; color: mos_color; } /**index.wxss**/ ec-canvas { width: 100%; height: 100%; } .container { position: relative; display:inline-flexbox; margin-top: 0rpx; }
4. 实际运行效果
-
实物图如下,测试时充电宝还没到货,先用了 18650 的电源。目前为了测试,接线都是裸露的,可根据需要订制3D打印外壳,或打点热熔胶防水。
-
手机端小程序
-
演示视频
esp32 土壤湿度监控 微信小程序端演示