文章目录

  • 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. 硬件、接线、环境配置

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 土壤湿度监控 微信小程序端演示