文章目录
- 1. 硬件、接线、环境配置
- 2. 项目简介
- 2.1 初衷
- 2.2 技术路线
- 3. 实现方法
- 3.1 接线及电源选型
- 3.2 ESP32 端程序
- 3.2.1 源码
- 3.2.2 特别说明
- 3.3 微信小程序端
- 3.3.1 参考例程
- 3.3.2 ECharts 集成
- 3.3.3 小程序下拉刷新发送 udp 端口号
- 3.3.4 源码
- 4. 实际运行效果
1. 硬件、接线、环境配置
- 【物联网初探】- 01 - ESP32 开发环境搭建 (Arduino IDE)
- 【物联网初探】- 02 - ESP32 利用 SPI 联通 TFT 彩屏 (Arduino IDE)
- 【物联网初探】- 03 - ESP32 结合 TFT_eSPI 库标定 TFT 触摸屏 (Arduino IDE)
- 【物联网初探】- 04 - ESP32 结合 LVGL 库开发环境搭建 (Arduino IDE)
- 【物联网初探】- 05 - ESP32 上 LVGL 库的多个例程测试 (Arduino IDE)
- 【物联网初探】- 06 - ESP32 利用 wifi 进行 TCP 通信(Arduino IDE)
- 【物联网初探】- 07 - ESP32 利用 wifi 进行 UDP 通信(Arduino IDE)
- 【物联网初探】- 08 - ESP32 操作电容式土壤湿度传感器(Arduino IDE)
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 土壤湿度监控 微信小程序端演示