注:以下内容疑似为ai生成

ai太厉害了

以下为零件说明:

(注:需轻微焊接)

ESP32开发板(Type-C口)1个

DHT22温湿度传感器(3针模块)1个

HW-390 电容式土壤湿度传感器(v2.0版) 1个

BH1750 光照传感器 1个(购买回来可能需要自己把引脚焊接)

0.96 寸 OLED 显示屏(4 针 I2C) 1个

5V 单路光耦隔离继电器模块(高电平触发)1个

DC 3W 潜水泵(3~6V,150~370mA, 内径0.5mm)1个 + 内径0.6mm硅胶管(购买回来水泵可能只有两根电线,需要自己把一根杜邦线去皮焊接)

5V 2A 电源适配器(5.5mm 大头)1个 + dc母头转杜线带针

面包板 1个 

杜邦线(公对公,母对母,公对母)若干

1kΩ电阻 (只用了一个)

4.7kΩ电阻 (只用了一个)

二极管 (只用了一个)

第一步

使用软件:

vscode+platformIO

1.在vscode的拓展里安装platformIO

2.点开platformIO里的界面open处

3.点击+New Project新建项目

输入Name:输入一个项目名称

Board:选择esp32开发板

Framework:Arduino

Location:文件路径,取消Use default location默认路径,之后自己选择一个地方

之后点击右下角蓝色Finish

以上操作会在Location所选的地址下自动创建一个以项目名称为文件名字的文件夹

之后等待一会(第一次应该会等待很长时间 )就创建成功了

第二步

接线(AI给出):

注意:所有GND(负极)都要连接在一起,以给正极提供相同的0V参考线

1.DHT22接线

**解释**:DHT22 是 3 针模块,三根脚分别是 VCC(+)、DATA(数据)、GND(−)。它的 DATA 脚上电后不断输出一串脉冲,ESP32 通过 GPIO4 读这串脉冲,解析出温度和湿度。3 针模块板上已带 4.7k 上拉电阻,所以不用外接。

**操作**:

| DHT22 引脚 | 接 ESP32 | 杜邦线类型 |

|---|---|---|

| VCC(+) | 3V3(3.3V) | 母对母 |

| DATA(out) | GPIO4 | 母对母 |

| GND(−) | GND | 母对母 |

如果模块上印的是 `+ / out / −`,就按同样的对应关系接。

**接反会怎样**:VCC 和 GND 接反 → 传感器发热甚至烧坏;DATA 接 5V → 可能损坏 GPIO。所以先看清丝印再插。

```

DHT22                                           ESP32

┌──────┐

│ VCC       ├─────────────→ 3V3

│ DATA     ├─────────────→ GPIO4

│ GND      ├─────────────→ GND

└──────┘

```

2.HW-390 土壤湿度传感器接线

**解释**:HW-390 是电容式传感器,利用土壤含水量改变电容来测量,不怕腐蚀(比电阻式寿命长)。板上一般有 4 个针:VCC、GND、AO(模拟输出)、DO(数字输出)。本系统只用 AO。

**操作**:

| HW-390 引脚 | 接 ESP32 | 说明 |

|---|---|---|

| VCC | 3V3 | ⚠️ 用 3.3V,不要用 5V |

| GND | GND | 共地 |

| AO | GPIO34 | 模拟输出 → ADC 引脚 |

| DO | 不接 | 数字输出用不到 |

**为什么用 3.3V 供电**:AO 输出电压和供电电压有关。ESP32 的 ADC 最高只能承受约 3.3V,5V 供电的 AO 可能超过 3.3V,会损坏 ADC 引脚。用 3.3V 供电就绝对安全。

**为什么用 GPIO34**:GPIO34 属于 ADC1,而且只能当输入(不会误输出),是模拟输入的理想选择。GPIO32~39 都是 ADC1,避开 ADC2(与 WiFi 冲突)。

**插土注意事项**:

- 探针竖直插进土里,插到金属探针完全没入土壤即可,别插到盆底。

- 探针别碰到花盆金属边、石头、根系粗壮部分,会读数不准。

- 插稳后不要频繁拔插,位置固定读数才稳定。

```

HW-390                                      ESP32

┌────────┐

│ VCC           ├──────────→ 3V3

│ GND          ├──────────→ GND

│ AO             ├──────────→ GPIO34

│ DO             ├──── 不接

└────────┘

```

3.BH1750 光照传感器接线

**解释**:BH1750 用 I2C 通信。ESP32 的 I2C 引脚可以软件指定,我们统一用默认的 GPIO21(SDA)和 GPIO22(SCL)。模块板上带 I2C 上拉电阻,不用外接。

**操作**:

| BH1750 引脚 | 接 ESP32 |

|---|---|

| VCC | 3V3 |

| GND | GND |

| SDA | GPIO21 |

| SCL | GPIO22 |

**插反会怎样**:SDA 和 SCL 接反 → 读数失败(库初始化失败),不会烧硬件,换回来即可。VCC/GND 接反 → 可能烧模块。

```

BH1750                                        ESP32

┌────────┐

│ VCC           ├──────────→ 3V3

│ GND          ├──────────→ GND

│ SDA           ├──────────→ GPIO21

│ SCL            ├──────────→ GPIO22

│ ADDR        ├──── 悬空(默认地址)

└────────┘

```

4.OLED 显示屏接线

**解释**: OLED 是 0.96 寸、SSD1306 驱动、4 针版本,4 根针分别是 VCC、GND、SDA、SCL,属于 I2C 接口。它和 BH1750 **共用 GPIO21/22 两根线**——这就是 I2C 总线的特点:一根线上可以挂多个设备,只要地址不同就不会打架(BH1750 地址 0x23,OLED 地址 0x3C)。屏幕上显示中文用的是 u8g2 库自带的中文字库。

**操作**:

| OLED 引脚 | 接 ESP32 | 说明 |

|---|---|---|

| VCC | 3V3 | 3.3V 供电 |

| GND | GND | 共地 |

| SDA | GPIO21 | 和 BH1750 的 SDA 接同一路 |

| SCL | GPIO22 | 和 BH1750 的 SCL 接同一路 |

接线方法:在面包板上把 BH1750 的 SDA 和 OLED 的 SDA 都插到 GPIO21 那一列,SCL 同理都插到 GPIO22 那一列;没有面包板就用杜邦线把两个模块的 SDA 并联。

**插反会怎样**:VCC/GND 接反可能烧屏;SDA/SCL 接反屏幕不亮(不会烧),换回来即可。

```

OLED                                                          ESP32

┌────────────┐

│ VCC                     ├────────────→ 3V3

│ GND                    ├────────────→ GND

│ SDA                     ├────────────→ GPIO21(和 BH1750 共用)

│ SCL                      ├────────────→ GPIO22(和 BH1750 共用)

└────────────┘

```

5.继电器模块接线

**操作**:

| 继电器引脚 | 接哪里 |

|---|---|

| VCC | 5V(电源适配器 5V+) |

| GND | 共地(适配器 GND,同时连 ESP32 GND,也就是要共地) |

| IN | ESP32 GPIO16 |

**重要:IN 加 4.7kΩ 下拉电阻**:

- 把 4.7kΩ 电阻一端插在 GPIO16 这一路的杜邦线上(或面包板对应孔),另一端插到 GND。

- 作用:ESP32 上电的一瞬间,GPIO 还没被代码控制,处于"悬空"状态,继电器可能乱吸合、水泵乱转。下拉电阻让悬空时 IN 稳定在低电平,泵不会自己转。

**跳线帽**:保持出厂默认位置(插在 VCC-JD 之间)即可,新手不用动。

```

继电器模块 ESP32 / 电源

┌────────────┐

│ VCC                     ├──→ 适配器 5V+

│ GND                    ├──→ 适配器 GND ──┼── ESP32 GND(共地)

│ IN                        ├──→ GPIO16

│                             │                                            4.7kΩ 电阻:GPIO16 ──电阻── GND

│ COM                    ├──→ 适配器 5V+

│ NO                      ├──→ 水泵 +

└────────────┘

```

6.水泵与供电

| 连接 | 说明 |

|---|---|

| 适配器 5V+ → 端子排 → 继电器 VCC | 给继电器供电 |

| 适配器 5V+ → 端子排 → 继电器 COM | 给水泵供电的"总入口" |

| 继电器 NO → 水泵 + | 继电器吸合时,5V 送到水泵 |

| 适配器 GND → 水泵 − | 水泵负极 |

| 适配器 GND → ESP32 GND | 共地(关键!) |

**水泵并联二极管(推荐)**:

把二极管并在水泵两根线上:**二极管负极(有横杠那头)接水泵 +,正极接水泵 −**。

水泵是电机,断电瞬间会产生反向电压(反电动势),二极管把它泄放掉,保护继电器触点。你已有二极管,顺手接上。

```

电源适配器 5V 2A(5.5mm 公头)

├─ DC母头转杜邦线 ─→ 端子排

│ ├─ 5V+ ──→ 继电器 VCC

│ ├─ 5V+ ──→ 继电器 COM

│ ├─ GND ──→ 继电器 GND

│ ├─ GND ──→ 水泵 −

│ └─ GND ──→ ESP32 GND(共地)

└─(方案B才接)5V+ ──→ ESP32 5V/VIN

继电器 NO ──→ 水泵 +

水泵 + ──二极管──→ 水泵 −(二极管负极朝 +)

```

第三步

代码:

1.下载依赖库

点开platform的libraries处

搜索dht22,作者为Adafruit

点进去后,点击Add to Project来添加到自己的项目

选择后添加之后点击Add添加

之后重复再添加

BH1750 by claws

U8g2 by oliver

添加好后platformio.ini这个文件的lib_deps处会有这三个库

2.之后在src下创建文件夹和文件(自己创建)

代码为ai给出

路径src/main.cpp

#include <Arduino.h>
#include "esp_task_wdt.h" // 看门狗:程序卡死时自动重启(安全保护)

#include "config.h"
#include "calibration_store.h"
#include "sensors/dht_sensor.h"
#include "sensors/hw_soil_moisture_sensor.h"
#include "sensors/light_sensor.h"
#include "actuators/relay_pump.h"
#include "controllers/watering_controller.h"
#include "display/oled_display.h"

// ================================================================
// 金华佛手(香橼)自动浇水系统 —— 主程序
// 结构:main 只负责"组装零件",具体逻辑在各模块里
// ================================================================

// ===== 全局对象创建(按依赖顺序)=====
CalibrationStore cal_store;                         // 校准值存储(NVS)
DHTSensor dht_sensor(PIN_DHT22, DHTModel::DHT22);   // DHT22
LightSensor light_sensor(PIN_I2C_SDA, PIN_I2C_SCL); // BH1750
SoilMoistureSensor soil_sensor(PIN_SOIL_ADC,        // HW-390
                               SOIL_DEFAULT_DRY,
                               SOIL_DEFAULT_WET,
                               16,          // 采样次数
                               10);         // 采样间隔 ms
RelayPump relay_pump(PIN_RELAY_IN);         // 继电器+水泵
WateringController controller(relay_pump);  // 自动浇水控制器
OledDisplay oled(PIN_I2C_SDA, PIN_I2C_SCL); // 0.96 寸 OLED 显示屏

// ===== 串口命令帮助 =====
void print_help()
{
    Serial.println();
    Serial.println("════════ 串口命令帮助 ════════");
    Serial.println("h → 显示本帮助");
    Serial.println("d → 把当前土壤读数记为【干燥】(传感器放空气中/干土时发)");
    Serial.println("w → 把当前土壤读数记为【湿润】(传感器插浇透的土时发)");
    Serial.println("c → 查看当前参数和校准值");
    Serial.println("t → 测试水泵 3 秒");
    Serial.println("o → 急停(立即关泵)");
    Serial.println("r → 恢复默认校准值");
    Serial.println("══════════════════════════════");
    Serial.println();
}

// ===== 打印参数 =====
void print_params()
{
    Serial.println("──── 当前参数 ────");
    Serial.printf("校准: 干燥 %d / 湿润 %d\n",
                  soil_sensor.get_dry_value(), soil_sensor.get_wet_value());
    Serial.printf("浇水阈值: 启动 %d%% / 停止 %d%% / 极干 %d%%\n",
                  WATER_START_PCT, WATER_STOP_PCT, WATER_EMERGENCY_PCT);
    Serial.printf("连续 %lu 秒 / 渗水 %lu 秒 / 单次累计 %lu 秒\n",
                  (unsigned long)(PUMP_BURST_MS / 1000),
                  (unsigned long)(PUMP_SOAK_MS / 1000),
                  (unsigned long)(SESSION_MAX_ON_MS / 1000));
    Serial.printf("最小间隔 %lu 小时 / 每日上限 %lu 分钟\n",
                  MIN_INTERVAL_MS / 3600000UL, DAILY_LIMIT_MS / 60000UL);
    Serial.println("──────────────────");
}

// ===== 串口命令处理 =====
void handle_serial_command()
{
    if (!Serial.available())
        return;

    String cmd = Serial.readStringUntil('\n');
    cmd.trim();
    cmd.toLowerCase();

    if (cmd == "h")
    {
        print_help();
    }
    else if (cmd == "d")
    {
        // 把"当前土壤读数"记为干燥值并保存
        int raw = soil_sensor.get_last_raw();
        soil_sensor.set_dry_value(raw);
        cal_store.save_dry(raw);
        Serial.printf("✅ 已把当前读数 %d 记为【干燥】并保存\n", raw);
    }
    else if (cmd == "w")
    {
        int raw = soil_sensor.get_last_raw();
        soil_sensor.set_wet_value(raw);
        cal_store.save_wet(raw);
        Serial.printf("✅ 已把当前读数 %d 记为【湿润】并保存\n", raw);
    }
    else if (cmd == "c")
    {
        print_params();
    }
    else if (cmd == "t")
    {
        controller.test_pump(3000); // 测试水泵 3 秒
    }
    else if (cmd == "o")
    {
        controller.emergency_stop();
    }
    else if (cmd == "r")
    {
        cal_store.clear();
        soil_sensor.set_calibration(SOIL_DEFAULT_DRY, SOIL_DEFAULT_WET);
        Serial.println("✅ 已恢复默认校准值(干燥3310 / 湿润1080)");
    }
    else if (cmd.length() > 0)
    {
        Serial.printf("未知命令: %s(输入 h 看帮助)\n", cmd.c_str());
    }
}

// ===== 看门狗初始化(兼容旧版/新版 Arduino-ESP32 内核)=====
static void setup_watchdog()
{
#if ESP_ARDUINO_VERSION_MAJOR >= 3
    esp_task_wdt_config_t cfg;
    cfg.timeout_ms = 30000; // 30 秒没喂狗就重启
    cfg.idle_core_mask = 0;
    cfg.trigger_panic = true;
    esp_task_wdt_init(&cfg);
#else
    esp_task_wdt_init(30, true); // 30 秒,panic 模式
#endif
    esp_task_wdt_add(NULL); // 把当前主任务加入喂狗名单
    Serial.println("✅ 看门狗已开启:程序卡死 30 秒会自动重启");
}

// ===== setup:上电只跑一次 =====
void setup()
{
    Serial.begin(SERIAL_BAUD);
    delay(300);

    Serial.println("════════════════════════════════");
    Serial.println("  金华佛手 自动浇水系统 v1.1 (OLED)");
    Serial.println("════════════════════════════════");

    // 1. 先读保存的校准值(没有则用默认值)
    cal_store.begin();
    int dry = cal_store.load_dry(SOIL_DEFAULT_DRY);
    int wet = cal_store.load_wet(SOIL_DEFAULT_WET);
    soil_sensor.set_calibration(dry, wet);

    // 2. 初始化所有模块
    dht_sensor.begin();
    soil_sensor.begin();
    light_sensor.begin();
    oled.begin();
    relay_pump.begin();
    controller.begin();

    // 3. 看门狗 + 帮助
    setup_watchdog();
    print_help();
}

// ===== loop:不断循环 =====
void loop()
{
    esp_task_wdt_reset(); // 喂狗:告诉看门狗"我还活着"

    int soil_pct = 0;
    float humidity = 0.0f, temperature = 0.0f, lux = 0.0f;

    // 读取所有传感器(失败时返回 false,控制器会自动保护)
    bool dht_ok = dht_sensor.read(humidity, temperature);
    bool soil_ok = soil_sensor.read(soil_pct);
    bool light_ok = light_sensor.read(lux); // 光照只做提醒,不参与浇水保护

    // BH1750 偶尔失败时提示一下(节流,避免刷屏)
    if (!light_ok)
    {
        static unsigned long last_light_warn_ms = 0;
        if (millis() - last_light_warn_ms > 30000)
        {
            last_light_warn_ms = millis();
            Serial.println("⚠️ BH1750 读取失败(继续用上次缓存值,请检查接线)");
        }
    }

    // 自动浇水控制
    controller.update(soil_pct, temperature, humidity, lux, soil_ok, dht_ok);

    // OLED 刷新(每 1 秒一次,减少 I2C 负担)
    static unsigned long last_oled_ms = 0;
    if (millis() - last_oled_ms >= OLED_REFRESH_MS)
    {
        last_oled_ms = millis();
        oled.update(soil_pct, temperature, humidity, lux,
                    controller.get_state_name());
    }

    // 串口命令(校准/测试/急停)
    handle_serial_command();

    delay(50); // 给串口一点处理时间
}

src下创建的actuators文件夹(用于实现水泵开关)

src/actuators/relay_pump.cpp

#include "relay_pump.h"
#include <Arduino.h>

// ===== 构造函数 =====
RelayPump::RelayPump(uint8_t pin)
    : relay_pin(pin),
      pump_on(false)
{
}

// ===== 初始化:先断电 =====
void RelayPump::begin()
{
    pinMode(relay_pin, OUTPUT);
    // 上电默认低电平 → 继电器不吸合 → 水泵不转(安全保护)
    digitalWrite(relay_pin, LOW);

    Serial.printf("✅ RelayPump: 继电器 IN = GPIO%d,已设为关闭状态\n", relay_pin);
    Serial.println("   接线提醒:继电器 VCC→5V,GND→共地,IN→GPIO16");
    Serial.println("   5V+ → 继电器 COM,继电器 NO → 水泵+,电源 GND → 水泵−");
}

// ===== 开泵 =====
void RelayPump::turn_on()
{
    digitalWrite(relay_pin, HIGH);
    pump_on = true;
}

// ===== 关泵 =====
void RelayPump::turn_off()
{
    digitalWrite(relay_pin, LOW);
    pump_on = false;
}

src/actuators/relay_pump.h

#ifndef RELAY_PUMP_H
#define RELAY_PUMP_H

#include <stdint.h>

/**
 * @brief 继电器 + 潜水泵执行器类
 *
 * 继电器是"电控开关":给它一个高电平,它就吸合(内部触点接通),
 * 水泵的 5V 供电回路就通了;给低电平就断开。
 *
 * ⚠️ 本类只管"开/关",不管浇多久——浇水时长和节奏
 *    由 WateringController(控制器)负责。
 */
class RelayPump
{
private:
    uint8_t relay_pin; // 继电器 IN 引脚
    bool pump_on;      // 当前泵是否通电

public:
    /**
     * @brief 构造函数
     * @param pin 继电器 IN 引脚(建议 GPIO16)
     */
    RelayPump(uint8_t pin);

    /**
     * @brief 初始化:上电强制断电(安全第一)
     */
    void begin();

    /**
     * @brief 开泵(继电器吸合)
     */
    void turn_on();

    /**
     * @brief 关泵(继电器断开)
     */
    void turn_off();

    /**
     * @brief 查询当前是否开泵
     */
    inline bool is_on() const { return pump_on; }

    /**
     * @brief 获取继电器引脚号
     */
    inline uint8_t get_pin() const { return relay_pin; }
};

#endif

src下创建的controllers文件夹

src/controllers/watering_controller.cpp

#include "watering_controller.h"
#include "config.h"
#include <Arduino.h>

// ===== 构造函数 =====
WateringController::WateringController(RelayPump &pump_ref)
    : pump(pump_ref),
      state(WateringState::MONITOR),
      burst_start_ms(0),
      session_on_ms(0),
      daily_on_ms(0),
      last_water_end_ms(0),
      last_day_index(0),
      last_tick_ms(0),
      last_log_ms(0),
      low_light_start_ms(0)
{
}

// ===== 初始化 =====
void WateringController::begin()
{
    last_tick_ms = millis();
    last_day_index = last_tick_ms / DAY_LENGTH_MS;
    set_pump(false); // 上电默认不浇水(安全保护)
    Serial.println("✅ WateringController: 自动浇水控制就绪(上电默认不浇水)");
}

// ===== 统一开关泵 =====
void WateringController::set_pump(bool on)
{
    if (on)
        pump.turn_on();
    else
        pump.turn_off();
}

// ===== 主更新函数 =====
void WateringController::update(int soil_pct, float temp_c, float humidity,
                                float lux, bool soil_ok, bool dht_ok)
{
    unsigned long now = millis();
    unsigned long delta = now - last_tick_ms;
    last_tick_ms = now;

    // 每日上限:按开机后的 24 小时滚动重置
    // (ESP32 没有实时时钟,重启会重新计时,见教程说明)
    unsigned long day_index = now / DAY_LENGTH_MS;
    if (day_index != last_day_index)
    {
        last_day_index = day_index;
        daily_on_ms = 0;
        Serial.println("🔄 新的 24 小时周期开始,每日浇水上限已重置");
    }

    // ===== 安全保护 1:传感器故障 → 立即停泵,禁用自动浇水 =====
    if (!soil_ok || !dht_ok)
    {
        if (state == WateringState::PUMP_ON || state == WateringState::SOAKING)
        {
            set_pump(false);
            Serial.println("🚨 浇水过程中传感器故障!已紧急停泵");
        }
        if (state != WateringState::FAULT)
        {
            state = WateringState::FAULT;
            Serial.println("🚨 传感器故障:自动浇水已禁用(读数恢复后自动解除)");
        }
        return;
    }

    // 故障恢复 → 回到监测
    if (state == WateringState::FAULT)
    {
        state = WateringState::MONITOR;
        Serial.println("✅ 传感器恢复正常,自动浇水已重新启用");
    }

    // 低光照计时:连续低于阈值才提示,避免开关灯瞬间刷屏
    if (lux < LIGHT_WARN_LUX)
    {
        if (low_light_start_ms == 0)
            low_light_start_ms = now;
    }
    else
    {
        low_light_start_ms = 0;
    }

    // ===== 温度修正(高温更勤快、低温少浇防冻)=====
    int start_pct = WATER_START_PCT;
    if (temp_c >= TEMP_HOT)
        start_pct = min(WATER_START_PCT + 10, 60);
    if (temp_c <= TEMP_COLD)
        start_pct = max(WATER_START_PCT - 10, 25);

    switch (state)
    {
    // ---------- 监测状态 ----------
    case WateringState::MONITOR:
    {
        bool interval_ok = (now - last_water_end_ms) >= MIN_INTERVAL_MS;
        bool daily_ok = daily_on_ms < DAILY_LIMIT_MS;
        bool emergency_dry = soil_pct < WATER_EMERGENCY_PCT;
        bool need_water = soil_pct < start_pct;

        // 启动条件:土壤偏干 + 距上次浇水够久 + 今日没超上限
        if (daily_ok && (emergency_dry || (interval_ok && need_water)))
        {
            state = WateringState::PUMP_ON;
            burst_start_ms = now;
            session_on_ms = 0;
            set_pump(true);
            Serial.printf("💧 开始浇水:土壤 %d%% %s(启动阈值 %d%%)\n",
                          soil_pct, emergency_dry ? "极干🚨" : "偏干", start_pct);
        }
        else if (now - last_log_ms >= STATUS_PRINT_MS)
        {
            last_log_ms = now;
            log_status(soil_pct, temp_c, humidity, lux);
        }
        break;
    }

    // ---------- 正在浇水 ----------
    case WateringState::PUMP_ON:
    {
        // 土壤达到目标湿度 → 结束本次浇水
        if (soil_pct >= WATER_STOP_PCT)
        {
            finish_session("土壤已达标");
            break;
        }

        // 连续运行达到上限 → 转渗水停顿
        if (now - burst_start_ms >= PUMP_BURST_MS)
        {
            set_pump(false);
            state = WateringState::SOAKING;
            Serial.printf("⏸️ 连续浇水 %lu 秒,暂停 %lu 秒让水渗入\n",
                          (unsigned long)(PUMP_BURST_MS / 1000),
                          (unsigned long)(PUMP_SOAK_MS / 1000));
            break;
        }

        // 单次累计上限 / 每日上限
        if (session_on_ms + delta >= SESSION_MAX_ON_MS)
        {
            finish_session("已达单次浇水上限");
            break;
        }
        if (daily_on_ms + delta >= DAILY_LIMIT_MS)
        {
            finish_session("已达每日浇水上限");
            break;
        }

        // 正常累计运行时长
        session_on_ms += delta;
        daily_on_ms += delta;
        break;
    }

    // ---------- 渗水停顿 ----------
    case WateringState::SOAKING:
    {
        // 停顿期间土壤达标 → 结束
        if (soil_pct >= WATER_STOP_PCT)
        {
            finish_session("土壤已达标");
            break;
        }

        // 累计达到上限 → 结束
        if (session_on_ms >= SESSION_MAX_ON_MS || daily_on_ms >= DAILY_LIMIT_MS)
        {
            finish_session("已达浇水上限");
            break;
        }

        // 停顿时间够了 → 继续浇水
        if (now - burst_start_ms >= PUMP_BURST_MS + PUMP_SOAK_MS)
        {
            burst_start_ms = now;
            state = WateringState::PUMP_ON;
            set_pump(true);
            Serial.println("▶️ 渗水结束,继续浇水");
        }
        break;
    }

    // ---------- 冷却(等最小间隔)----------
    case WateringState::COOLDOWN:
        if ((now - last_water_end_ms) >= MIN_INTERVAL_MS)
        {
            state = WateringState::MONITOR;
        }
        break;

    case WateringState::FAULT:
        // 已在上面统一处理
        break;
    }
}

// ===== 结束本次浇水 =====
void WateringController::finish_session(const char *reason)
{
    set_pump(false);
    state = WateringState::COOLDOWN;
    last_water_end_ms = millis();
    Serial.printf("🛑 本次浇水结束(%s)\n", reason);
    Serial.printf("   本次累计运行 %lu 秒 | 今日累计 %lu 秒\n",
                  session_on_ms / 1000, daily_on_ms / 1000);
}

// ===== 急停 =====
void WateringController::emergency_stop()
{
    set_pump(false);
    state = WateringState::COOLDOWN;
    last_water_end_ms = millis();
    Serial.println("🛑 急停已执行:水泵关闭,进入冷却间隔");
}

// ===== 状态中文名(OLED 显示用)=====
const char *WateringController::get_state_name() const
{
    switch (state)
    {
    case WateringState::MONITOR:
        return "监测中";
    case WateringState::PUMP_ON:
        return "浇水中";
    case WateringState::SOAKING:
        return "渗水中";
    case WateringState::COOLDOWN:
        return "冷却中";
    case WateringState::FAULT:
        return "故障!";
    }
    return "未知";
}

// ===== 测试水泵 =====
void WateringController::test_pump(unsigned long ms)
{
    if (state != WateringState::MONITOR)
    {
        Serial.println("⚠️ 当前不在空闲状态,请先输入 o 急停,再测试水泵");
        return;
    }

    Serial.println("⚠️ 测试模式:水泵将开启数秒!");
    Serial.println("   请确认:泵泡在水里、出水管对着花盆或杯子、人站在旁边");
    set_pump(true);
    delay(ms);
    set_pump(false);
    daily_on_ms += ms; // 测试时间也算进每日上限,保持诚实
    Serial.println("✅ 测试结束,水泵已关闭");
}

// ===== 打印状态 =====
void WateringController::log_status(int soil_pct, float temp_c, float humidity, float lux)
{
    Serial.println("──────────────────────────────");
    Serial.printf("🌱 土壤湿度: %d%%\n", soil_pct);
    Serial.printf("🌡️  温度: %.1f℃  空气湿度: %.1f%%\n", temp_c, humidity);
    Serial.printf("☀️  光照: %.0f lx\n", lux);
    Serial.printf("⏱️  今日已浇水: %lu 秒 / 上限 %lu 秒\n",
                  daily_on_ms / 1000,
                  (unsigned long)(DAILY_LIMIT_MS / 1000));
    if (low_light_start_ms != 0 &&
        millis() - low_light_start_ms >= LIGHT_WARN_MS)
        Serial.println("💡 光照已持续偏低 30 分钟,佛手需要充足阳光(建议移到窗边)");
    if (temp_c >= TEMP_HOT)
        Serial.println("🔥 温度偏高,注意补水");
    if (temp_c <= TEMP_COLD)
        Serial.println("❄️ 温度偏低,注意防冻,减少浇水");
    Serial.println("──────────────────────────────");
}

src/controllers/watering_controller.h

#ifndef WATERING_CONTROLLER_H
#define WATERING_CONTROLLER_H

#include <stdint.h>
#include "actuators/relay_pump.h"

/**
 * @brief 浇水状态(自动浇水状态机的五种状态)
 */
enum class WateringState
{
    MONITOR,  // 监测中,泵关;判断土壤湿度是否该浇水
    PUMP_ON,  // 正在浇水(泵开)
    SOAKING,  // 间歇停顿(泵关),让水渗进土壤
    COOLDOWN, // 本次浇水结束,等待最小间隔(防频繁浇水)
    FAULT     // 传感器故障,禁止自动浇水(安全保护)
};

/**
 * @brief 浇水状态机(自动浇水的大脑)
 *
 * 状态说明:
 * - MONITOR   监测中,泵关;判断土壤湿度是否该浇水
 * - PUMP_ON   正在浇水(泵开)
 * - SOAKING   间歇停顿(泵关),让水渗进土壤
 * - COOLDOWN  本次浇水结束,等待最小间隔(防频繁浇水)
 * - FAULT     传感器故障,禁止自动浇水(安全保护)
 *
 * 保护机制(全部自动执行):
 * 1. 传感器故障 → 立即停泵,禁用自动浇水
 * 2. 每次连续浇水最多 10 秒,停 20 秒渗水
 * 3. 一次浇水累计最多 60 秒
 * 4. 两次浇水最小间隔 4 小时
 * 5. 每天累计浇水最多 10 分钟
 * 6. 上电默认不浇水
 */
class WateringController
{
private:
    RelayPump &pump;     // 引用继电器/水泵对象
    WateringState state; // 当前状态

    unsigned long burst_start_ms;     // 本次连续浇水开始时间
    unsigned long session_on_ms;      // 本次浇水累计泵运行毫秒
    unsigned long daily_on_ms;        // 今日累计泵运行毫秒
    unsigned long last_water_end_ms;  // 上次浇水结束时间(最小间隔用)
    unsigned long last_day_index;     // 24 小时滚动重置每日上限
    unsigned long last_tick_ms;       // 上一帧时间(累计时长用)
    unsigned long last_log_ms;        // 状态打印节流
    unsigned long low_light_start_ms; // 低光照开始时间(连续 30 分钟才提示)

    /**
     * @brief 开关泵(内部统一入口,方便以后加日志/保护)
     */
    void set_pump(bool on);

    /**
     * @brief 结束本次浇水:关泵 + 进入冷却 + 打印总结
     */
    void finish_session(const char *reason);

    /**
     * @brief 打印一行状态(每 60 秒一次)
     */
    void log_status(int soil_pct, float temp_c, float humidity, float lux);

public:
    /**
     * @brief 构造函数
     * @param pump_ref 继电器/水泵对象(引用,本类只控制它)
     */
    explicit WateringController(RelayPump &pump_ref);

    /**
     * @brief 初始化:上电默认关泵
     */
    void begin();

    /**
     * @brief 每一帧调用一次(主循环里)
     * @param soil_pct   土壤湿度百分比(0~100)
     * @param temp_c     温度(℃)
     * @param humidity   空气湿度(%)
     * @param lux        光照(lx)
     * @param soil_ok    土壤传感器读数是否有效
     * @param dht_ok     DHT22 读数是否有效
     */
    void update(int soil_pct, float temp_c, float humidity, float lux,
                bool soil_ok, bool dht_ok);

    /**
     * @brief 急停(串口命令 o):立即关泵并进入冷却
     */
    void emergency_stop();

    /**
     * @brief 测试水泵(串口命令 t):开泵若干毫秒后关闭
     * @param ms 测试时长(默认 3000)
     */
    void test_pump(unsigned long ms);

    /**
     * @brief 获取当前状态
     */
    inline WateringState get_state() const { return state; }

    /**
     * @brief 获取当前状态的中文名称(供 OLED/串口显示)
     * @return 例如 "监测中" / "浇水中" / "渗水中" / "冷却中" / "故障!"
     */
    const char *get_state_name() const;

    /**
     * @brief 获取今日累计浇水毫秒数
     */
    inline unsigned long get_daily_on_ms() const { return daily_on_ms; }
};

#endif

src下创建的display文件夹

src/display/oled_display.cpp

#include "oled_display.h"
#include <Arduino.h>

// ===== 构造函数 =====
OledDisplay::OledDisplay(uint8_t sda, uint8_t scl)
    : u8g2(U8G2_R0, U8X8_PIN_NONE, scl, sda),
      sda_pin(sda),
      scl_pin(scl),
      ready(false)
{
}

// ===== 初始化 =====
bool OledDisplay::begin()
{
    // 1. 开启 I2C 总线
    Wire.begin(sda_pin, scl_pin);

    // 2. 探测 OLED 地址:多数模块是 0x3C,个别模块是 0x3D
    uint8_t found_addr = 0;
    Wire.beginTransmission(0x3C);
    if (Wire.endTransmission() == 0)
    {
        found_addr = 0x3C;
    }
    else
    {
        Wire.beginTransmission(0x3D);
        if (Wire.endTransmission() == 0)
            found_addr = 0x3D;
    }

    if (found_addr == 0)
    {
        Serial.println("❌ OLED 初始化失败!");
        Serial.println("   请检查:VCC→3.3V,GND→GND,SDA→GPIO21,SCL→GPIO22");
        ready = false;
        return false;
    }

    // 3. 如果是 0x3D,告诉 u8g2 改用这个地址(u8g2 内部用 8 位地址)
    if (found_addr == 0x3D)
        u8g2.setI2CAddress(0x3D << 1);

    // 4. 初始化显示,选择 12px 中文字体(覆盖完整 GB2312 字库)
    u8g2.begin();
    u8g2.setFont(u8g2_font_wqy12_t_gb2312);
    u8g2.setFontPosTop();
    ready = true;

    Serial.printf("✅ OLED 初始化成功(地址 0x%02X,u8g2 库)\n", found_addr);

    // 5. 开机画面
    u8g2.firstPage();
    do
    {
        u8g2.drawUTF8(0, 24, "佛手系统 启动中");
    } while (u8g2.nextPage());

    return true;
}

// ===== 刷新一屏数据 =====
void OledDisplay::update(int soil_pct, float temp_c, float humidity,
                         float lux, const char *state_text)
{
    if (!ready)
        return;

    u8g2.firstPage();
    do
    {
        char line[40];

        // 第 1 行:土壤湿度 + 当前状态(12px 字高,每行间隔 14px)
        snprintf(line, sizeof(line), "土壤:%3d%% %s", soil_pct, state_text);
        u8g2.drawUTF8(0, 0, line);

        // 第 2 行:温度
        snprintf(line, sizeof(line), "温度:%5.1f C", temp_c);
        u8g2.drawUTF8(0, 14, line);

        // 第 3 行:空气湿度
        snprintf(line, sizeof(line), "空气:%4.0f %%", humidity);
        u8g2.drawUTF8(0, 28, line);

        // 第 4 行:光照
        snprintf(line, sizeof(line), "光照:%5.0f lx", lux);
        u8g2.drawUTF8(0, 42, line);
    } while (u8g2.nextPage());
}

src/display/oled_display.h

#ifndef OLED_DISPLAY_H
#define OLED_DISPLAY_H

#include <stdint.h>
#include <Wire.h>
#include <U8g2lib.h>

/**
 * @brief 0.96 寸 OLED 显示屏模块(SSD1306 驱动,4 针 I2C)
 *
 * 接线:VCC→3.3V,GND→GND,SDA→GPIO21,SCL→GPIO22
 * 它和 BH1750 共用同一组 I2C 引脚:BH1750 地址 0x23,OLED 地址 0x3C,
 * 两个设备地址不同,互不干扰,这就是 I2C 总线"一线挂多设备"的好处。
 *
 * 显示库:u8g2(作者 Oliver Kraus,GitHub: github.com/olikraus/u8g2),
 * 自带完整 GB2312 中文字库,所以屏幕能直接显示中文。
 */
class OledDisplay
{
private:
    // u8g2 显示对象:128x64 分辨率,硬件 I2C
    // 参数顺序:旋转方向、复位脚(无)、SCL、SDA
    U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2;

    uint8_t sda_pin; // SDA 引脚(GPIO21)
    uint8_t scl_pin; // SCL 引脚(GPIO22)
    bool ready;      // 是否初始化成功

public:
    /**
     * @brief 构造函数
     * @param sda SDA 引脚(GPIO21)
     * @param scl SCL 引脚(GPIO22)
     */
    OledDisplay(uint8_t sda, uint8_t scl);

    /**
     * @brief 初始化 OLED(探测地址 → 开机画面),必须在 update() 前调用
     * @return true 成功,false 失败(先检查接线)
     */
    bool begin();

    /**
     * @brief 刷新一屏数据(土壤 / 温度 / 空气湿度 / 光照 / 状态)
     * @param soil_pct   土壤湿度百分比
     * @param temp_c     温度(℃)
     * @param humidity   空气湿度(%)
     * @param lux        光照(lx)
     * @param state_text 浇水状态文字(如"监测中""浇水中")
     */
    void update(int soil_pct, float temp_c, float humidity, float lux,
                const char *state_text);

    /**
     * @brief 是否初始化成功
     */
    inline bool is_ready() const { return ready; }
};

#endif

src下创建的sensors文件夹

src/sensors/dht_sensor.cpp

#include "dht_sensor.h"
#include <Arduino.h> // 用到delay()、isnan()等Arduino API

// ===== 构造函数实现(对应C的 `DHTSensor_Init` 函数逻辑)=====
DHTSensor::DHTSensor(uint8_t pin, DHTModel model)
    // 初始化列表:在构造函数体执行前初始化成员变量(对应C的Init函数里的成员赋值)
    : dht(
          pin,
          model == DHTModel::DHT11 ? DHT11 : DHT22 // 转换枚举为Adafruit库需要的宏
          ),
      last_humidity(0.0f),    // 初始化湿度缓存为0
      last_temperature(0.0f), // 初始化温度缓存为0
      data_pin(pin),          // 保存引脚编号
      model(model)            // 保存传感器型号
{
    // 构造函数体:无额外逻辑(初始化已在初始化列表完成)
}

// ===== begin()实现 =====
void DHTSensor::begin()
{
    dht.begin(); // 启动底层DHT驱动
    delay(2500); // 传感器预热:DHT22上电后需要2秒稳定时间
    Serial.printf("✅ DHTSensor: DHT22 数据线 = GPIO%d 初始化完成\n", data_pin);
}

// ===== read() =====
bool DHTSensor::read(float &humidity, float &temperature)
{
    // 重试3次
    for (uint8_t retry = 0; retry < 3; ++retry)
    {
        // 1. 先读湿度
        humidity = dht.readHumidity();
        // DHT22转换数据需要时间,等它转换完再读温度
        delay(250);

        // 2. 再读温度
        temperature = dht.readTemperature();

        // 3. 判断是否成功(库里如果读到错误会返回 NAN)
        if (!isnan(humidity) && !isnan(temperature))
        {
            last_humidity = humidity;
            last_temperature = temperature;
            return true;
        }

        // 失败了就延时一会再重试
        delay(200);
    }

    // 3次都失败,返回旧缓存
    humidity = last_humidity;
    temperature = last_temperature;
    return false;
}

src/sensors/dht_sensor.h

#ifndef DHT_SENSOR_H
#define DHT_SENSOR_H

#include <DHT.h> // 必须包含:类成员使用了DHT类型,编译器需要知道其定义

/**
 * @brief DHT传感器型号枚举(对应C的 `typedef enum { ... } DHTModel_t`)
 * @note C++的enum class比C的enum更安全,不会和其他枚举值重名
 */
enum class DHTModel
{
    DHT11, // DHT11型号
    DHT22  // DHT22型号(AM2302)
};

/**
 * @brief DHT传感器类(对应C的 `typedef struct { ... } DHTSensor_t`)
 */
class DHTSensor
{
private:
    // ===== 私有成员(对应C结构体里的内部成员,仅类内部可访问)=====
    DHT dht;                // DHT底层驱动对象(对应C的 `void* dht_handle`)
    float last_humidity;    // 缓存上一次读取的湿度值(%RH)
    float last_temperature; // 缓存上一次读取的温度值(℃)
    uint8_t data_pin;       // DATA引脚编号
    DHTModel model;         // 传感器型号

public:
    // ===== 公有接口(对应C的对外函数声明,所有文件都可调用)=====

    /**
     * @brief 构造函数(对应C的 `DHTSensor_Init` 函数)
     * @param pin DATA引脚编号
     * @param model 传感器型号(DHT11/DHT22)
     */
    DHTSensor(uint8_t pin, DHTModel model);

    /**
     * @brief 启动传感器(对应C的 `DHTSensor_Begin` 函数)
     * @note 会等待传感器预热,必须在使用read()前调用
     */
    void begin();

    /**
     * @brief 读取温湿度(带3次重试机制,对应C的 `DHTSensor_Read` 函数)
     * @param humidity 输出:湿度值(%RH),读取失败时不变
     * @param temperature 输出:温度值(℃),读取失败时不变
     * @return true 读取成功,false 3次重试后仍失败
     */
    bool read(float &humidity, float &temperature);

    // ===== 内联getter(对应C的 `static inline` 函数,直接写在.h里)=====
    /**
     * @brief 获取缓存的湿度值
     * @return 上一次成功读取的湿度值(%RH)
     */
    inline float get_humidity() const { return last_humidity; }

    /**
     * @brief 获取缓存的温度值
     * @return 上一次成功读取的温度值(℃)
     */
    inline float get_temperature() const { return last_temperature; }
};

#endif // PLANT_MONITOR_DHT_SENSOR_H

src/sensors/hw_soil_moisture_sensor.cpp

#include "hw_soil_moisture_sensor.h"
#include <Arduino.h>

// ===== 构造函数实现 =====
SoilMoistureSensor::SoilMoistureSensor(
    uint8_t pin,
    int dry_value,
    int wet_value,
    uint8_t sample_count,
    uint16_t sample_delay_ms) // sample:样本
    // 初始化列表
    : adc_pin(pin),
      dry_value(dry_value),
      wet_value(wet_value),
      last_raw(0),
      last_percent(0),
      sample_count(sample_count > 0 ? sample_count : 1),
      sample_delay_ms(sample_delay_ms)
{
    // 构造函数体:无额外逻辑
    // ⚠️ 注意:begin() 中才能安全调用 analogSetWidth 等,
    //   构造函数阶段硬件可能尚未就绪
}

// ===== 检查是否为 ADC1 引脚 =====
bool SoilMoistureSensor::is_adc1_pin(uint8_t pin) const
{
    // Arduino-ESP32 中,ADC1 对应 GPIO32~39
    // GPIO32=CH4, GPIO33=CH5, GPIO34=CH6, GPIO35=CH7,
    // GPIO36=CH0, GPIO37=CH1, GPIO38=CH2, GPIO39=CH3
    switch (pin)
    {
    case 32:
    case 33:
    case 34:
    case 35:
    case 36:
    case 37:
    case 38:
    case 39:
        return true;
    default:
        return false;
    }
}

// ===== begin():用 Arduino 层 API 配置 ADC =====
void SoilMoistureSensor::begin()
{
    // 1. 检查引脚是否安全(必须是 ADC1,避开 ADC2 与 WiFi 冲突)
    if (!is_adc1_pin(adc_pin))
    {
        Serial.printf("❌ SoilMoistureSensor: GPIO%d 不是 ADC1 引脚!\n",
                      adc_pin);
        Serial.println("   ⚠️ ADC2 引脚(GPIO0/2/4/12~15/25~27)与 WiFi 冲突");
        Serial.println("   → 请改用 GPIO32~39");
        return;
    }

    // 2. 设置 ADC 位宽:12 位 → 量程 0~4095
    analogSetWidth(12);

    // 3. 设置该引脚的衰减(Attenuation)
    //    11dB → 约 0~3.1V,覆盖 HW-390 全输出范围
    analogSetPinAttenuation(adc_pin, ADC_11db);

    // 4. 配置引脚为输入
    pinMode(adc_pin, INPUT);

    Serial.printf("✅ SoilMoistureSensor: GPIO%d 初始化完成\n", adc_pin);
    Serial.printf("   位宽: 12-bit (0~4095) | 衰减: 11dB (~0~3.1V)\n");
    Serial.printf("   校准值 → 干燥:%d  湿润:%d\n",
                  dry_value, wet_value);
}

// ===== 单次原始读取 =====
int SoilMoistureSensor::read_raw_once()
{
    return analogRead(adc_pin);
}

// ===== 读取(带滤波 + 重试)=====
bool SoilMoistureSensor::read(int &humidity_percent)
{
    // 校准值合法性检查
    if (dry_value == wet_value)
    {
        Serial.println("❌ SoilMoistureSensor: dry_value == wet_value,无法映射");
        humidity_percent = last_percent;
        return false;
    }

    // 重试 3 次(与 DHT 模块保持一致的重试策略)
    for (uint8_t retry = 0; retry < 3; ++retry)
    {
        long sum = 0;
        int valid_samples = 0;

        // 多点采样滑动平均
        for (uint8_t i = 0; i < sample_count; ++i)
        {
            int val = read_raw_once();
            // analogRead 失败时会返回 0 或负值(取决于 core 版本),
            // 我们用 >0 做简单有效性判断
            if (val > 0)
            {
                sum += val;
                ++valid_samples;
            }
            if (sample_delay_ms > 0)
                delay(sample_delay_ms);
        }

        if (valid_samples == 0)
        {
            delay(50);
            continue; // 全部失败,重试
        }

        int raw = sum / valid_samples;

        // 限制在 [干, 湿] 两端之间(防止越界)
        int lo = (wet_value < dry_value) ? wet_value : dry_value;
        int hi = (wet_value > dry_value) ? wet_value : dry_value;
        int constrained = constrain(raw, lo, hi);

        // ⚠️ 方向自适应映射:
        //   常见批次:越湿 → ADC 越小 → wet=100%, dry=0%
        //   少数批次:越湿 → ADC 越大 → 反向映射
        int percent;
        if (dry_value > wet_value)
        {
            percent = map(constrained, wet_value, dry_value, 100, 0);
        }
        else
        {
            percent = map(constrained, dry_value, wet_value, 0, 100);
        }
        percent = constrain(percent, 0, 100);

        // 缓存
        last_raw = raw;
        last_percent = percent;
        humidity_percent = percent;
        return true;
    }

    // 3 次都失败,返回旧缓存
    humidity_percent = last_percent;
    return false;
}

// ===== 更新校准值 =====
void SoilMoistureSensor::set_calibration(int dry, int wet)
{
    dry_value = dry;
    wet_value = wet;
    Serial.printf("✅ 校准值更新 → 干燥:%d  湿润:%d\n", dry_value, wet_value);
}

void SoilMoistureSensor::set_dry_value(int value)
{
    dry_value = value;
}

void SoilMoistureSensor::set_wet_value(int value)
{
    wet_value = value;
}

// ===== 获取湿度等级 =====
SoilMoistureLevel SoilMoistureSensor::get_level() const
{
    if (last_percent <= 30)
        return SoilMoistureLevel::SEVERE_DRY;
    if (last_percent <= 44)
        return SoilMoistureLevel::SLIGHT_DRY;
    if (last_percent <= 65)
        return SoilMoistureLevel::OPTIMAL;
    if (last_percent <= 75)
        return SoilMoistureLevel::SLIGHT_WET;
    return SoilMoistureLevel::OVER_WET;
}

// ===== 打印佛手浇水建议 =====
void SoilMoistureSensor::print_advice() const
{
    Serial.println();
    Serial.println("🌿 [佛手养护建议]");

    switch (get_level())
    {
    case SoilMoistureLevel::SEVERE_DRY:
        Serial.println("   🚨 严重干旱! 佛手根系细弱,久旱必落叶落果");
        Serial.println("   → 立即浇透水(浇则浇透)");
        Serial.println("   → 配合叶面喷水,空气湿度保持 60%~75%");
        break;

    case SoilMoistureLevel::SLIGHT_DRY:
        Serial.println("   💧 偏干,接近浇水时机");
        Serial.println("   → 1~2 天内浇水");
        break;

    case SoilMoistureLevel::OPTIMAL:
        Serial.println("   ✅ 湿度适宜(45%~65%),无需干预");
        Serial.println("   → 遵循'不干不浇,浇则浇透'");
        break;

    case SoilMoistureLevel::SLIGHT_WET:
        Serial.println("   ⚠️ 偏湿,暂停浇水");
        Serial.println("   → 加强通风,检查盆底排水孔");
        Serial.println("   → 佛手怕涝,宁可偏干不可过湿");
        break;

    case SoilMoistureLevel::OVER_WET:
        Serial.println("   🚨 过湿! 烂根风险!");
        Serial.println("   → 立即停止浇水!");
        Serial.println("   → 检查土壤透气性(推荐配比:腐殖土60%+河沙30%+珍珠岩10%)");
        Serial.println("   → 盆底加陶粒/碎瓦片滤水层");
        break;
    }
    Serial.println();
}

src/sensors/hw_soil_moisture_sensor.h

#ifndef HW_SOIL_MOISTURE_SENSOR_H
#define HW_SOIL_MOISTURE_SENSOR_H

#include <stdint.h> // uint8_t, int, uint16_t

/**
 * @brief 土壤湿度等级枚举
 * @note  该百分比是 HW-390 经干/湿土两点校准后线性映射的值,
 *        并非农业气象意义上的"土壤相对含水量",仅用于本系统阈值判断。
 *
 *        阈值依据(结合金华佛手栽培特性):
 *        - 佛手"怕涝忌积水、不耐旱",浅根盆栽烂根风险高
 *        - 参考《金华佛手》地方标准"不干不浇、浇则浇透"
 *        - 参考农业墒情 0~20cm 相对含水量 60%~80% 适宜区间,
 *          因佛手怕涝,本系统将上限适当下调
 */
enum class SoilMoistureLevel
{
    SEVERE_DRY, // ≤30%  严重干旱,必须立即浇水
    SLIGHT_DRY, // 31~44% 偏干,建议 1~2 天内浇水
    OPTIMAL,    // 45~65% ✅ 佛手最适宜区间
    SLIGHT_WET, // 66~75% 偏湿,暂停浇水、加强通风
    OVER_WET    // >75%  过湿,烂根风险,立即停止浇水
};

/**
 * @brief 电容式土壤湿度传感器类(HW-390 v2.0)
 *
 * @details
 *  HW-390 利用土壤介电常数变化→电容值变化→输出电压变化。
 *  常见批次:越湿润 → 电压越低 → ADC 值越小(你的实测:
 *  空气 3310、水中 1080,就是这种方向)。
 *  少数批次方向相反,本类已做了方向自适应,两种都能用。
 *
 *  本类封装了:
 *  - ADC 配置(Arduino 层 API:analogSetWidth / analogSetPinAttenuation)
 *  - 多点采样滑动平均滤波
 *  - 干/湿土两点校准 → 百分比映射(支持运行时更新校准值)
 *  - 佛手栽培适宜区间判断
 *
 *  ⚠️ 引脚必须用 ADC1(GPIO32~39),因 ADC2 与 WiFi 冲突
 */
class SoilMoistureSensor
{
private:
    // ===== 私有成员 =====
    uint8_t adc_pin;          // ADC 引脚编号(推荐 GPIO34 = ADC1_CH6)
    int dry_value;            // 校准:干燥时(空气中/干土)的 ADC 值
    int wet_value;            // 校准:湿润时(水中/湿土)的 ADC 值
    int last_raw;             // 缓存上一次成功读取的原始 ADC 值
    int last_percent;         // 缓存上一次成功计算的湿度百分比
    uint8_t sample_count;     // 每次读取的采样次数(滑动平均)
    uint16_t sample_delay_ms; // 每次采样间隔(ms),用于降低噪声

    // ===== 内部辅助函数 =====
    /**
     * @brief 检查引脚是否属于 ADC1(GPIO32~39)
     * @return true 是 ADC1 引脚,false 不是(ADC2 或与 WiFi 冲突)
     */
    bool is_adc1_pin(uint8_t pin) const;

    /**
     * @brief 单次读取原始 ADC 值(调用 Arduino 的 analogRead)
     */
    int read_raw_once();

public:
    // ===== 构造函数 =====
    /**
     * @brief 构造土壤湿度传感器对象
     * @param pin        ADC 引脚(推荐 GPIO34 = ADC1_CH6)
     * @param dry_value  干燥基准 ADC 值(默认 3310,可运行时校准)
     * @param wet_value  湿润基准 ADC 值(默认 1080,可运行时校准)
     * @param sample_count  采样次数,默认 16
     * @param sample_delay_ms 采样间隔 ms,默认 10
     *
     * @note ⚠️ 引脚必须用 ADC1(GPIO32~39),否则 WiFi 启用后读数异常
     */
    SoilMoistureSensor(
        uint8_t pin,
        int dry_value,
        int wet_value,
        uint8_t sample_count = 16,
        uint16_t sample_delay_ms = 10);

    // ===== 公有接口 =====

    /**
     * @brief 初始化 ADC(配置位宽 + 衰减,全部使用 Arduino 层 API)
     * @note  必须在 read() 前调用
     */
    void begin();

    /**
     * @brief 读取土壤湿度(带滤波和重试)
     * @param humidity_percent 输出:湿度百分比 0~100%
     * @return true 读取并映射成功,false 校准值非法或连续失败
     */
    bool read(int &humidity_percent);

    /**
     * @brief 更新干/湿校准值(串口校准命令 d / w 会调用)
     * @param dry 干燥基准 ADC 值
     * @param wet 湿润基准 ADC 值
     */
    void set_calibration(int dry, int wet);

    /**
     * @brief 单独更新干燥校准值
     */
    void set_dry_value(int value);

    /**
     * @brief 单独更新湿润校准值
     */
    void set_wet_value(int value);

    /**
     * @brief 获取当前湿度等级(供主程序做浇水决策)
     */
    SoilMoistureLevel get_level() const;

    /**
     * @brief 打印佛手浇水建议到 Serial
     */
    void print_advice() const;

    // ===== 内联 getter =====
    inline int get_last_raw() const { return last_raw; }
    inline int get_last_percent() const { return last_percent; }
    inline int get_dry_value() const { return dry_value; }
    inline int get_wet_value() const { return wet_value; }
};

#endif

src/sensors/light_sensor.cpp

#include "light_sensor.h"
#include <Arduino.h>

// ===== 构造函数 =====
LightSensor::LightSensor(uint8_t sda, uint8_t scl)
    : sda_pin(sda),
      scl_pin(scl),
      last_lux(0.0f),
      initialized(false)
{
}

// ===== 初始化 =====
bool LightSensor::begin()
{
    // 1. 让 ESP32 的 I2C 硬件使用我们指定的两个引脚
    //    (claws 库只负责读写 BH1750,不负责初始化 Wire)
    Wire.begin(sda_pin, scl_pin);

    // 2. 检测 BH1750 是否在线(库内部向地址 0x23 发握手信号)
    if (!light_meter.begin())
    {
        Serial.println("❌ BH1750 初始化失败!");
        Serial.println("   请检查:VCC→3.3V,GND→GND,SDA→GPIO21,SCL→GPIO22");
        initialized = false;
        return false;
    }

    // 3. 高分辨率连续模式首次测量需要约 120ms,等它准备好再读
    delay(200);

    initialized = true;
    Serial.println("✅ BH1750 光照传感器初始化成功(claws/BH1750 库)");
    return true;
}

// ===== 读取光照 =====
bool LightSensor::read(float &lux)
{
    if (!initialized)
    {
        lux = last_lux;
        return false;
    }

    for (uint8_t retry = 0; retry < 3; ++retry)
    {
        float value = light_meter.readLightLevel();

        // 有效判断:库在 I2C 读失败时返回负值(-1),不是离谱大数。
        // BH1750 最大合法读数约 54612 lx(饱和值),用 60000 兜底,
        // 模块没接好时可能读到 0xFFFF 转换出的异常值
        if (value >= 0.0f && value <= 60000.0f)
        {
            last_lux = value;
            lux = value;
            return true;
        }

        delay(100); // 稍等再重试
    }

    // 3 次失败,返回旧缓存
    lux = last_lux;
    return false;
}

src/sensors/light_sensor.h

#ifndef LIGHT_SENSOR_H
#define LIGHT_SENSOR_H

#include <stdint.h>
#include <Wire.h>
#include <BH1750.h>

/**
 * @brief BH1750 光照强度传感器类(I2C 接口)
 *
 * 用途:测量环境光照(单位 lx 勒克斯),在本系统中只做"提醒":
 * 光照长期不足时提示你把佛手挪到更亮的地方,不直接控制浇水。
 *
 * 底层使用 claws/BH1750 库(作者 Christopher Laws,MIT 协议),
 * 已在 platformio.ini 的 lib_deps 里声明,第一次编译会自动下载;
 * 如果下载失败,把库压缩包手动放进 lib/BH1750/ 文件夹即可(教程 7.3)。
 *
 * 对应知识点:
 * - I2C 总线:只用 SDA(数据)和 SCL(时钟)两根线,
 *   就能和一个或多个设备通信;每根线需要接上拉电阻
 *   (BH1750 模块板上通常已自带)。
 * - lx(勒克斯):照度单位。晴天户外可达几万 lx,
 *   室内窗边通常几千 lx,普通室内灯光只有几百 lx。
 */
class LightSensor
{
private:
    uint8_t sda_pin;    // SDA 引脚(默认 GPIO21)
    uint8_t scl_pin;    // SCL 引脚(默认 GPIO22)
    BH1750 light_meter; // BH1750 库对象(默认 I2C 地址 0x23)
    float last_lux;     // 缓存上次成功读数
    bool initialized;   // 是否初始化成功

public:
    /**
     * @brief 构造函数
     * @param sda SDA 引脚(GPIO21)
     * @param scl SCL 引脚(GPIO22)
     */
    LightSensor(uint8_t sda, uint8_t scl);

    /**
     * @brief 初始化 I2C 与 BH1750,必须在 read() 前调用
     * @return true 成功,false 失败(先检查接线)
     */
    bool begin();

    /**
     * @brief 读取光照强度
     * @param lux 输出:光照值(lx)
     * @return true 成功,false 失败(使用缓存值)
     */
    bool read(float &lux);

    /**
     * @brief 获取缓存的最近一次光照值
     */
    inline float get_lux() const { return last_lux; }

    /**
     * @brief 是否初始化成功
     */
    inline bool is_ready() const { return initialized; }
};

#endif

src下的文件

src/calibration_store.cpp

#include "calibration_store.h"
#include <Arduino.h>

// ===== 构造函数:只初始化状态,不操作硬件 =====
CalibrationStore::CalibrationStore()
    : ready(false)
{
}

// ===== 打开 NVS 命名空间 =====
void CalibrationStore::begin()
{
    // 第一个参数是"命名空间"(最多15个字符),相当于文件柜的抽屉名
    // 第二个参数 false 表示"可读可写"
    prefs.begin("foshou", false);
    ready = true;
    Serial.println("✅ CalibrationStore: 校准值存储(NVS)就绪");
}

int CalibrationStore::load_dry(int default_value)
{
    if (!ready)
        return default_value;
    return prefs.getInt("soil_dry", default_value);
}

int CalibrationStore::load_wet(int default_value)
{
    if (!ready)
        return default_value;
    return prefs.getInt("soil_wet", default_value);
}

void CalibrationStore::save_dry(int value)
{
    if (ready)
        prefs.putInt("soil_dry", value);
}

void CalibrationStore::save_wet(int value)
{
    if (ready)
        prefs.putInt("soil_wet", value);
}

void CalibrationStore::clear()
{
    if (ready)
    {
        prefs.remove("soil_dry");
        prefs.remove("soil_wet");
    }
}

src/calibration_store.h

#ifndef CALIBRATION_STORE_H
#define CALIBRATION_STORE_H

#include <Preferences.h>

/**
 * @brief 校准值持久化存储(ESP32 内置 NVS 闪存)
 *
 * 作用:把土壤传感器"干燥/湿润"校准值保存起来,
 *       断电重启后不用重新校准。
 *
 * 对应知识点:NVS(Non-Volatile Storage,非易失性存储)
 * 是 ESP32 芯片里的一小块闪存,断电后数据不丢失,
 * 类似 U 盘;Preferences 是 Arduino 封装好的读写接口。
 */
class CalibrationStore
{
private:
    Preferences prefs; // Preferences 底层对象
    bool ready;        // 是否初始化成功

public:
    CalibrationStore();

    /**
     * @brief 打开存储(必须在 load/save 之前调用)
     */
    void begin();

    /**
     * @brief 读取保存的干燥值;没有保存过则返回 default_value
     */
    int load_dry(int default_value);

    /**
     * @brief 读取保存的湿润值;没有保存过则返回 default_value
     */
    int load_wet(int default_value);

    /**
     * @brief 保存干燥值
     */
    void save_dry(int value);

    /**
     * @brief 保存湿润值
     */
    void save_wet(int value);

    /**
     * @brief 清除保存的校准值(恢复出厂默认)
     */
    void clear();
};

#endif

src/config.h

#ifndef CONFIG_H
#define CONFIG_H

// ================================================================
// 金华佛手自动浇水系统 —— 配置中心
// 所有"以后想改的数值"都集中在这里,新手只改这个文件就够了
// ================================================================

// ---------- 引脚定义(改这里之前先看教程接线图) ----------
#define PIN_DHT22 4     // DHT22 数据线 → GPIO4
#define PIN_I2C_SDA 21  // BH1750 + OLED 的 SDA → GPIO21
#define PIN_I2C_SCL 22  // BH1750 + OLED 的 SCL → GPIO22
#define PIN_SOIL_ADC 34 // HW-390 AO → GPIO34(ADC1_CH6,只能输入)
#define PIN_RELAY_IN 16 // 继电器 IN → GPIO16(高电平触发)
#define PIN_LED_TEST 17 // 测试 LED(点灯测试用,最终系统可不接)

// ---------- 土壤湿度校准默认值(你 2026-7-31 实测) ----------
// 空气(干燥): 3310    水中(湿润): 1080
// 注意:用串口命令 d / w 校准后,新值会保存到 ESP32 内部存储(NVS),
//       以保存值为准;想恢复默认值,串口发 r
#define SOIL_DEFAULT_DRY 3247
#define SOIL_DEFAULT_WET 1070

// ---------- 浇水参数(含义和理由见教程第七节) ----------
#define WATER_START_PCT 45       // 土壤湿度低于此值 → 准备浇水(%)
#define WATER_STOP_PCT 70        // 土壤湿度高于此值 → 停止浇水(%)
#define WATER_EMERGENCY_PCT 20   // 低于此值 = 极干,无视最小间隔
#define PUMP_BURST_MS 10000      // 泵每次连续运行最多 10 秒
#define PUMP_SOAK_MS 20000       // 每次停 20 秒,让水渗入土壤
#define SESSION_MAX_ON_MS 60000  // 一次浇水累计泵运行最多 60 秒
#define MIN_INTERVAL_MS 14400000 // 两次浇水最小间隔 4 小时
#define DAILY_LIMIT_MS 600000    // 每天累计浇水最多 10 分钟
#define DAY_LENGTH_MS 86400000   // 一天 24 小时(毫秒),用于重置每日上限

// ---------- 温度 / 光照 ----------
#define TEMP_HOT 35.0f        // 超过此温度 → 浇水阈值调高(更勤快)
#define TEMP_COLD 10.0f       // 低于此温度 → 浇水阈值调低(少浇防冻)
#define LIGHT_WARN_LUX 3000   // 光照低于此值 → 提示光照不足
#define LIGHT_WARN_MS 1800000 // 低光照持续 30 分钟才提示一次

// ---------- 串口 ----------
#define SERIAL_BAUD 9600
#define STATUS_PRINT_MS 60000 // 每 60 秒打印一次状态

// ---------- OLED 显示屏(0.96 寸 SSD1306,4 针 I2C)----------
// 和 BH1750 共用 GPIO21(SDA)/GPIO22(SCL),地址不同互不冲突
#define OLED_REFRESH_MS 1000 // 屏幕每 1 秒刷新一次

#endif

第四步:

type-C数据线连接esp32和电脑

烧录(使用别的文件演示):

点击到之前写完代码的main.cpp

之后vscode左下角有一行图标

点击箭头Upload进行烧录

烧录成功之后可以点击

Serial Monitor进行查看数据

第五步

如何使用:

上述完成代码烧录

将上面连接完成的电源适配器插在插板上(小心,我也不知道会不会被电awa

将type-C线连接esp32和一个充电头后,把充电头插在插板上

水泵连接橡胶管进行吸水和浇水(右边较低的口是进水,左边较高的口是出水)

都做好后就可以使用了

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐