水泵控制逻辑
水泵控制逻辑
本节介绍 ESP32 控制投料泵的核心逻辑——基于枚举状态机的泵控制实现。学习完成后,您将能够:
- 使用 Enum + Switch 结构设计 ESP32 状态机
- 实现非阻塞的定时泵控制
- 理解状态机在 IoT 项目中的优势
ESP32 Pump State Machine
Section titled “ESP32 Pump State Machine”┌──────────────┐│ 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-Based State Machine Implementation
Section titled “Enum-Based State Machine Implementation”// 状态枚举 (enum states)// 定义系统的所有运行状态enum States { STATE_WAIT, // 等待命令 STATE_GET_DATA, // 获取液位数据 STATE_PUMPING, // 投料中 STATE_TIME_FOR_SLEEP, // 准备睡眠 STATE_WAITING // 等待间隙};
// 全局状态变量States currentState = STATE_WAIT;Loop Function with Switch-Case
Section titled “Loop Function with Switch-Case”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; }}Pump Control Function
Section titled “Pump Control Function”// 投料泵控制参数#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; }}Complete Flow with Enum Switching
Section titled “Complete Flow with Enum Switching”// 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 (Shared Variables)
Section titled “Globals.h (Shared Variables)”// 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# 1. 烧录后测试状态切换# 串口监视器输出:WiFi connectedMQTT connectedState: 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_SLEEPCommon Customer Questions
Section titled “Common Customer Questions”Q1: 为什么用状态机而不是直接在回调中执行泵控制?
Section titled “Q1: 为什么用状态机而不是直接在回调中执行泵控制?”因为 MQTT 回调函数运行在中断上下文中,不适合执行耗时操作(如等待 3 秒)。状态机模式将控制逻辑移到主循环,确保非阻塞运行。
Q2: 投料时间 3 秒可以修改吗?
Section titled “Q2: 投料时间 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 断开时的状态处理
Summary
Section titled “Summary”- 状态机架构: WAIT → GET_DATA → PUMPING → TIME_FOR_SLEEP
- 枚举定义:
enum States清晰列出所有状态 - 非阻塞泵控制: Blink Without Delay 模式,不阻塞主循环
- MQTT 回调: 仅切换状态,不执行耗时操作
- 超时保护: 防止 Node-RED 无响应时系统卡死