这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » [物联网系列][Aduino]Arduino智能灌溉系统(二) 完整版

共4条 1/1 1 跳转至

[物联网系列][Aduino]Arduino智能灌溉系统(二) 完整版

工程师
2025-05-17 23:28:23     打赏

简介

在上一篇文章中我对当前系统的整个构成进行了概述, 虽然系统的传感器使用的比较多,但是使用的是Arduino 平台,所以对每个传感器的驱动并不复杂, 因此在本篇文章中我将完成所有组件的构建.  


传感器外围部件清单

1 - 土壤湿度传感器(Analog)

2- 水流量传感器YF-S401

3- OLED 0.96

4- SHT30

5- LTR-329

6- NMOS 用于PWM驱动LED灯板

7- LED灯板

8- 继电器 (如果使用5V可以使用NMOS控制开关, 不需要使用继电器)

9- 12V 水泵 (可以调整为5V)


实物俯视图

image.png

实物平视图

image.png


OLED 显示image.png


Flask上位机监控界面

Snipaste_2025-05-16_14-04-53.png图图表一为系统上电时 PID动态调整灯光亮度的曲线图.


系统稳定时即PID调整后的光照输出 (稳定在了亮度200)

Snipaste_2025-05-16_14-05-38.png


Arduino程序设计

#include <Wire.h>
#include "Adafruit_SSD1306.h"
#include "Adafruit_GFX.h"
#include "Adafruit_LTR329_LTR303.h"
#include "Adafruit_SHT4x.h"
#include <ArduinoJson.h>


// OLED 屏幕配置
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire1, OLED_RESET);

// 传感器对象
Adafruit_LTR329 ltr = Adafruit_LTR329();
Adafruit_SHT4x sht4;

// PID 控制相关
const int pwmPin = 3;         // IO2 PWM 输出
const int targetCh0 = 200;    // 目标 CH0 值

float Kp = 0.18, Ki = 0.0825, Kd = 0.002;
float integral = 0, last_error = 0;
unsigned long lastTime = 0;

volatile unsigned int pulseCount = 0;
unsigned long lastFlowCalcTime = 0;
float flowRate_mLps = 0;   // 毫升每秒
float totalMilliLiters = 0;


const int pumpPin = 4;  // IO4 控制继电器
const int soilThreshold = 600;  // 土壤湿度阈值(需根据实际情况调整)
bool pumping = false;
float pumpStartVolume = 0;
volatile bool isTrunonLight = true;

#define FLOW_SENSOR_PIN 2

void flowPulseISR() {
  pulseCount++;
}


void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("Initializing...");

  pinMode(pwmPin, OUTPUT);

  // 初始化 OLED
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 initialization failed!"));
    while (true);
  }
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  // 初始化 LTR329
  if (!ltr.begin(&Wire1)) {
    Serial.println("Couldn't find LTR sensor!");
    while (1) delay(10);
  }
  ltr.setGain(LTR3XX_GAIN_2);
  ltr.setIntegrationTime(LTR3XX_INTEGTIME_100);
  ltr.setMeasurementRate(LTR3XX_MEASRATE_200);

  // 初始化 SHT40
  if (!sht4.begin(&Wire1)) {
    Serial.println(F("SHT40 sensor not found!"));
    while (1);
  }
  sht4.setPrecision(SHT4X_HIGH_PRECISION);
  sht4.setHeater(SHT4X_NO_HEATER);

  pinMode(FLOW_SENSOR_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), flowPulseISR, RISING);

  pinMode(pumpPin, OUTPUT);
  digitalWrite(pumpPin, LOW);  // 默认关闭水泵

}

void loop() {
   if(Serial.available() >0)
    {
      char c = Serial.read();
      Serial.println(c);
      if(c == '1')
      {
        isTrunonLight = !isTrunonLight;
      }
    
    }
  uint16_t ch0 = 0, ch1 = 0;
  float temperature = 0, humidity = 0;

  // 读取光照
  if (ltr.newDataAvailable()) {
     StaticJsonDocument<256> doc;
    bool valid = ltr.readBothChannels(ch0, ch1);
    if (valid && isTrunonLight) {
      // PID 计算
      float error = targetCh0 - ch0;
      unsigned long now = millis();
      float dt = (now - lastTime) / 1000.0;
      lastTime = now;

      integral += error * dt;
      float derivative = (error - last_error) / dt;
      last_error = error;

      float output = Kp * error + Ki * integral + Kd * derivative;
      output = constrain(output, 0, 255);
      analogWrite(pwmPin, (int)output);
      // 每 1000ms 计算一次水流量
      if (millis() - lastFlowCalcTime >= 1000) {
        noInterrupts();
        unsigned int pulses = pulseCount;
        pulseCount = 0;
        interrupts();

        // 每脉冲 ≈ 2.22 毫升(YF401)
        flowRate_mLps = pulses * 2.22;
        totalMilliLiters += flowRate_mLps;

        lastFlowCalcTime = millis();
      }
          
        display.print("PWM: ");
        display.println((int)output);
        doc["pwm"] = (int)output;

    }else{
       analogWrite(pwmPin, 0);
    }
     // 读取温湿度
      sensors_event_t temp_event, hum_event;
      sht4.getEvent(&hum_event, &temp_event);
      temperature = temp_event.temperature;
      humidity = hum_event.relative_humidity;

      // 读取土壤湿度
      int soilValue = analogRead(A5);

      // 显示到 OLED
      display.clearDisplay();
      display.setCursor(0, 0);
      display.print("Temp: ");
      display.print(temperature, 1);
      display.println(" C");

      display.print("Humidity: ");
      display.print(humidity, 1);
      display.println(" %");

      display.print("Light CH0: ");
      display.println(ch0);

      display.print("Target: ");
      display.println(targetCh0);


      display.print("Flow: ");
      display.print(flowRate_mLps, 1);
      display.println(" mL/s");

      display.print("Total: ");
      display.print(totalMilliLiters, 0);
      display.println(" mL");

      if (!pumping && soilValue > soilThreshold) {
        digitalWrite(pumpPin, HIGH);
        pumping = true;
        pumpStartVolume = totalMilliLiters;
      }

      // 已在泵水 -> 检查是否达到100mL
      if (pumping && (totalMilliLiters - pumpStartVolume >= 100.0)) {
        digitalWrite(pumpPin, LOW);
        pumping = false;
        
      }

      int barLength = map(soilValue, 1023, 0, 0, 100);  // 显示湿度条(反映湿度)
      display.drawRect(0, 58, 100, 5, SSD1306_WHITE);      // 条边框
      display.fillRect(0, 58, barLength, 5, SSD1306_WHITE); // 填充条
      display.display();
     

      doc["temperature"] = temperature;
      doc["humidity"] = humidity;
      doc["light_ch0"] = ch0;
      doc["light_ch1"] = ch1;
      doc["target"] = targetCh0;
      doc["flow_mLps"] = flowRate_mLps;
      doc["total_mL"] = totalMilliLiters;
      doc["soil"] = soilValue;
      doc["pump"] = pumping;

      serializeJson(doc, Serial);
      Serial.println();
  }

  delay(500);  // 避免刷新过快
}


在上述的程序中对PID的调整稍微有一点麻烦, 这里有一个调整的思路就是, 首先将KI和KD都设置为0, 首先调整KP, 这里调整KP的时候可能会出现抖动的情况. 当抖动并不是很大的时候(LED闪烁频率没有那么大), 然后调整KI用来消除稳态误差. 当光照逐渐变稳定的时候再尝试调整KD来减缓超调与振荡.  此时LED的亮度已经已经稳定了. 如果觉得LED的亮度上升太慢或者下降太慢的话再逐渐改变KP的值. 到最后我系统中所调整的值为:

float Kp = 0.18, Ki = 0.0825, Kd = 0.002;

使其程序在运行的时候光照强度(可见光 + 红外光) 的亮度稳定在了 200. (上述的PID算法同样可以用于温控系统, 比如说恒温箱等)

核心代码如下所示

      float error = targetCh0 - ch0;
      unsigned long now = millis();
      float dt = (now - lastTime) / 1000.0;
      lastTime = now;

      integral += error * dt;
      float derivative = (error - last_error) / dt;
      last_error = error;

      float output = Kp * error + Ki * integral + Kd * derivative;
      output = constrain(output, 0, 255);
      analogWrite(pwmPin, (int)output);


Python 上位机程序设计

python的上位机是运行在虚拟的树莓派OS中的, 其中和Arduino的通讯主要是采用串口通讯. 其后端框架采用的是Flask, 支持将数据保存到数据库中. 

image.png

其主要的核心逻辑即读取串口的JSON输入, 然后通过HTML的定时器定时发送HTTP请求来读取串口数据,然后使用chart.js进行绘图.

串口线程(读取Arduino串口数据)


# 串口线程函数
def read_serial():
    global latest_data
    try:
        while True:
            line = ser.readline().decode('utf-8').strip()
            if line:
                try:
                    data = json.loads(line)  # 解析 JSON 数据
                    latest_data = data  # 更新最新的数据
                    print("Received:", data)
                    insert_data_to_db(data)  # 插入数据到数据库
                except json.JSONDecodeError:
                    print("Invalid JSON:", line)
    except serial.SerialException as e:
        print("Serial error:", e)


# 启动串口线程
serial_thread = threading.Thread(target=read_serial, daemon=True)
serial_thread.start()


返回串口数据用于前端绘图


@app.route('/data')
def get_data():
    return jsonify(latest_data)


定时请求串口数据:

 async function fetchData() {
        try {
            const res = await fetch('/data');
            const data = await res.json();
            document.getElementById("json").textContent = JSON.stringify(data, null, 2);

            const now = new Date().toLocaleTimeString();
            if (labels.length >= maxDataPoints) {
                labels.shift();
                lightData.shift();
                tempData.shift();
                humidityData.shift();
                soilData.shift();
            }

            labels.push(now);
            lightData.push(data.light_ch0);
            tempData.push(data.temperature);
            humidityData.push(data.humidity);
            soilData.push(data.soil);

            [lightChart, tempChart, humidityChart, soilChart].forEach((chart, i) => {
                chart.data.labels = labels;
                chart.data.datasets[0].data = [lightData, tempData, humidityData, soilData][i];
                chart.update();
            });
        } catch (err) {
            console.error("Failed to fetch data:", err);
        }
    }

    setInterval(() => {
        fetchData();
        fetchStats();
    }, 2000);


附件

project_without_env.zip

注意: 系统并不需要使用上位机程序进行监控, 不运行上位机对Arduino的执行没有任何影响




关键词: Arduino     智能灌溉系统     DIY    

专家
2025-05-18 11:39:07     打赏
2楼

感谢分享


专家
2025-05-18 11:42:57     打赏
3楼

感谢分享


专家
2025-05-18 11:44:44     打赏
4楼

学习一下


共4条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]