【物联网初探】- 09 - 基于 ESP32 和微信小程序的土壤湿度监测【完结篇】

文章目录

      • 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 充电电池,所以第一时间想到利用起来,但是实际使用的问题是,柠檬一般放在阳光充足的地方,电池不可避免的会晒到一些太阳,长时间使用有一定的风险,另外,电池容量有限,如果没电了,还得给电池充电,虽然支持边充边放,但是也比较麻烦。

    【物联网初探】- 09 - 基于 ESP32 和微信小程序的土壤湿度监测【完结篇】_第1张图片

  • 通过进一步在TB上搜索,发现了一个便宜又好用的东西,就是下面这种太阳能充电宝,本身带有一定容量,同时太阳能也可充电,如果白天阳光的强度和时长充足的话,应该能够实现24H不间断监控,并且价格也在能接受的范围内。

    【物联网初探】- 09 - 基于 ESP32 和微信小程序的土壤湿度监测【完结篇】_第2张图片

3.2 ESP32 端程序
3.2.1 源码
  • 在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
    }
    
3.2.2 特别说明
  • 由于 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 的编译方案,以及具有更高超的小程序编写技巧都可以从别的角度解决上述问题,本文仅是讨论了一种简单、可行的方式。

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打印外壳,或打点热熔胶防水。

    【物联网初探】- 09 - 基于 ESP32 和微信小程序的土壤湿度监测【完结篇】_第3张图片

  • 手机端小程序

    【物联网初探】- 09 - 基于 ESP32 和微信小程序的土壤湿度监测【完结篇】_第4张图片

  • 演示视频

esp32 土壤湿度监控 微信小程序端演示

你可能感兴趣的:(嵌入式学习记录,esp32,arduino,物联网,微信小程序,udp通信)