【物联网初探】- 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)
这个小项目涉及的基础知识主要有:
那么整体的技术路线主要包括以下两部分内容:
在ESP32上编写土壤湿度传感器读取、UDP/TCP通信的代码,并将读取后的信息以UDP或TCP的通信方式发送至手机小程序端;
小程序端接受 UDP/TCP 发送来的数据,简单画一点界面显示当前实时湿度,配合 ECharts 动态显示历史测量数据;
湿度传感器与ESP32的连接已经在上一篇讲解了,这里主要涉及到一个问题是供电方案的设计,该项目主要的需求是,尽可能长时间的监控土壤湿度,尽量不需要总去插拔电路,如果能够24小时供电是最好的,另外,要便宜。
基于上述考虑,我一开始尝试了下面这种两节 18650 供电的方案,因为手头有一些闲置的 18650 充电电池,所以第一时间想到利用起来,但是实际使用的问题是,柠檬一般放在阳光充足的地方,电池不可避免的会晒到一些太阳,长时间使用有一定的风险,另外,电池容量有限,如果没电了,还得给电池充电,虽然支持边充边放,但是也比较麻烦。
通过进一步在TB上搜索,发现了一个便宜又好用的东西,就是下面这种太阳能充电宝,本身带有一定容量,同时太阳能也可充电,如果白天阳光的强度和时长充足的话,应该能够实现24H不间断监控,并且价格也在能接受的范围内。
在ESP32 上运行的代码主要是读取传感器数据,利用之前标定的参数计算相对湿度参考值,最后通过 UDP 发送至指定的远程 IP 和端口。
//for this esp32 , pin4 = G32
#include
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
}
由于 Arduino 下写ESP32程序,这个
库里没提供 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 的编译方案,以及具有更高超的小程序编写技巧都可以从别的角度解决上述问题,本文仅是讨论了一种简单、可行的方式。
设置 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) => {},
})
})
小程序内部的源码较多,这里我主要开发了一个单页面的程序,页面上半部分显示湿度值,下半部分显示湿度变化曲线,该页面名为 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;
}
esp32 土壤湿度监控 微信小程序端演示