跳转到内容

水泵控制逻辑

水泵控制逻辑

本节介绍 ESP32 控制投料泵的核心逻辑——基于枚举状态机的泵控制实现。学习完成后,您将能够:

  • 使用 Enum + Switch 结构设计 ESP32 状态机
  • 实现非阻塞的定时泵控制
  • 理解状态机在 IoT 项目中的优势
┌──────────────┐
│ WAIT │ ← 上电初始,等待 MQTT 连接和命令
│ 等待命令 │ MQTT 订阅: esp32/dosing/control
└──────┬───────┘
│ 收到 MQTT 消息 "get_data"
┌──────────────┐
│ GET_DATA │ ← 读取超声波传感器液位
│ 获取液位 │ 发布 MQTT: esp32/dosing/info
└──────┬───────┘ 等待 Node-RED 返回指令
│ MQTT 收到 "1" (启动投料)
┌──────────────┐
│ PUMPING │ ← 继电器打开,非阻塞计时 3 秒
│ 投料中 │ Blink Without Delay 模式
└──────┬───────┘
│ 3 秒计时结束
┌──────────────┐
│ TIME_FOR_SLEEP│ ← 关闭继电器,发布状态
│ 准备睡眠 │ 进入深度睡眠或等待
└──────┬───────┘
│ esp_deep_sleep_start()
┌──────────────┐
│ DEEP SLEEP │ ← 1 小时后定时唤醒
│ 深度睡眠 │ 从头执行 setup()
└──────────────┘
// 状态枚举 (enum states)
// 定义系统的所有运行状态
enum States {
STATE_WAIT, // 等待命令
STATE_GET_DATA, // 获取液位数据
STATE_PUMPING, // 投料中
STATE_TIME_FOR_SLEEP, // 准备睡眠
STATE_WAITING // 等待间隙
};
// 全局状态变量
States currentState = STATE_WAIT;
void loop() {
// MQTT 连接维护
if (!client.connected()) {
connectMQTT();
}
client.loop();
// 状态机主循环
switch (currentState) {
case STATE_WAIT:
// 等待 MQTT 回调改变状态
// 空闲状态,不做任何操作
break;
case STATE_GET_DATA:
// 读取液位数据并发送
handleGetData();
break;
case STATE_PUMPING:
// 控制投料泵 (非阻塞)
handlePumping();
break;
case STATE_TIME_FOR_SLEEP:
// 准备进入深度睡眠
handleSleep();
break;
case STATE_WAITING:
// 等待间隙,不做操作
break;
}
}
// 投料泵控制参数
#define RELAY_PUMP_PIN 20 // 继电器控制引脚
#define PUMP_DURATION 3000 // 投料持续时间 (毫秒)
// 投料泵控制状态
unsigned long pumpStartTime = 0;
bool pumpActive = false;
// 处理投料 (非阻塞)
void handlePumping() {
if (!pumpActive) {
// 首次进入投料状态
Serial.println("Pump: Starting dosing");
digitalWrite(RELAY_PUMP_PIN, HIGH); // 打开继电器
pumpStartTime = millis();
pumpActive = true;
// 发布投料启动状态
client.publish("esp32/dosing/status", "pumping");
}
// 非阻塞等待投料完成
unsigned long currentMillis = millis();
if (currentMillis - pumpStartTime >= PUMP_DURATION) {
// 投料完成
digitalWrite(RELAY_PUMP_PIN, LOW); // 关闭继电器
pumpActive = false;
Serial.println("Pump: Dosing complete (3s)");
client.publish("esp32/dosing/status", "complete");
// 切换到睡眠状态
currentState = STATE_TIME_FOR_SLEEP;
}
}
// MQTT 回调:根据接收到的指令切换状态
void callback(char* topic, byte* payload, unsigned int length) {
String message;
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.print("MQTT received: ");
Serial.println(message);
// 泵控制指令 (来自 Node-RED 时间逻辑)
if (strcmp(topic, "esp32/dosing/control") == 0) {
if (message == "1") {
// Node-RED 判定需要投料
Serial.println("Command: Start dosing");
currentState = STATE_PUMPING;
} else if (message == "0") {
// Node-RED 判定不需要投料
Serial.println("Command: No dosing needed");
currentState = STATE_TIME_FOR_SLEEP;
}
}
// 数据获取指令
if (strcmp(topic, "esp32/dosing/command") == 0) {
if (message == "get_data") {
Serial.println("Command: Get sensor data");
currentState = STATE_GET_DATA;
}
}
}
// 获取液位数据
void handleGetData() {
// 读取超声波传感器 (详见 12-04)
long distance = measureDistance(TRIG_PIN, ECHO_PIN);
Serial.print("Distance measured: ");
Serial.println(distance);
// 构建 JSON 数据
String payload = "{\"distance\":" + String(distance) +
",\"state\":\"get_data\"}";
// 发布到 MQTT → 触发 Node-RED 时间逻辑
client.publish("esp32/dosing/info", payload.c_str());
// 切换到等待状态,等待 Node-RED 返回控制指令
currentState = STATE_WAIT;
// 设置超时: 如果 15 秒内未收到回复,自动睡眠
lastCommandTime = millis();
}
// 准备睡眠
void handleSleep() {
// 发布睡眠前状态
client.publish("esp32/dosing/status", "going_to_sleep");
// 发布深度睡眠记录
String sleepPayload = "{\"timestamp\":" + String(millis()) + "}";
client.publish("esp32/dosing/sleep_record", sleepPayload.c_str());
delay(1000); // 等待 MQTT 消息发送完成
client.disconnect();
// 配置定时唤醒 (1 小时)
esp_sleep_enable_timer_wakeup(3600 * 1000000); // 微秒
Serial.println("Entering deep sleep for 1 hour...");
Serial.flush();
esp_deep_sleep_start();
}
// globals.h — 多文件共享的全局变量定义
#ifndef GLOBALS_H
#define GLOBALS_H
// 状态枚举
enum States {
STATE_WAIT,
STATE_GET_DATA,
STATE_PUMPING,
STATE_TIME_FOR_SLEEP,
STATE_WAITING
};
// 全局状态
extern States currentState;
// 引脚定义
extern const int RELAY_PUMP_PIN;
extern const int TRIG_PIN;
extern const int ECHO_PIN;
// 泵控制变量
extern unsigned long pumpStartTime;
extern bool pumpActive;
extern unsigned long lastCommandTime;
// 传感器数据
extern long distance1;
extern long distance2;
#endif
Terminal window
# 1. 烧录后测试状态切换
# 串口监视器输出:
WiFi connected
MQTT connected
State: WAIT
mosquitto_pub -t "esp32/dosing/command" -m "get_data"
# 输出: Command: Get sensor data
# Distance measured: 16
# MQTT published: {"distance":16,"state":"get_data"}
# 2. 测试泵控制
mosquitto_pub -t "esp32/dosing/control" -m "1"
# 输出: Command: Start dosing
# Pump: Starting dosing
# (3秒后) Pump: Dosing complete (3s)
# State: TIME_FOR_SLEEP
# 3. 测试不需要投料
mosquitto_pub -t "esp32/dosing/control" -m "0"
# 输出: Command: No dosing needed
# State: TIME_FOR_SLEEP

Q1: 为什么用状态机而不是直接在回调中执行泵控制?

Section titled “Q1: 为什么用状态机而不是直接在回调中执行泵控制?”

因为 MQTT 回调函数运行在中断上下文中,不适合执行耗时操作(如等待 3 秒)。状态机模式将控制逻辑移到主循环,确保非阻塞运行。

可以。修改 PUMP_DURATION 常量即可。也可以通过 MQTT 远程设置:

// 在 MQTT 回调中支持远程配置
if (strcmp(topic, "esp32/dosing/config") == 0) {
// 解析 JSON: {"pump_duration":5000}
// 更新 PUMP_DURATION
}

Q3: 如果 Node-RED 没有返回指令怎么办?

Section titled “Q3: 如果 Node-RED 没有返回指令怎么办?”

实现超时机制,默认自动进入睡眠:

// 在 loop() 中添加超时检查
if (currentState == STATE_WAIT &&
millis() - lastCommandTime > 15000) { // 15 秒超时
Serial.println("Timeout: No command received, going to sleep");
currentState = STATE_TIME_FOR_SLEEP;
}

推荐做法:

  • 使用 Enum 定义清晰的状态集合
  • Switch-Case 处理状态分发
  • Blink Without Delay 模式实现非阻塞等待
  • 为每个状态添加超时保护和出错处理
  • 状态切换时打印调试信息

避免做法:

  • 在 MQTT 回调中使用 delay() 阻塞
  • 状态变量定义在多个文件中重复
  • 忘记在状态切换时复位相关变量
  • 忽略 MQTT 断开时的状态处理
  1. 状态机架构: WAIT → GET_DATA → PUMPING → TIME_FOR_SLEEP
  2. 枚举定义: enum States 清晰列出所有状态
  3. 非阻塞泵控制: Blink Without Delay 模式,不阻塞主循环
  4. MQTT 回调: 仅切换状态,不执行耗时操作
  5. 超时保护: 防止 Node-RED 无响应时系统卡死