简介

去年五一之前给三只小龟做了个自动喂食器:赶在五一之前给乌龟做个自动喂食器,刚好一年过去了(我这拖延症),升级到了 2.0 版本,主要是增加了自动水循环系统。

结构设计

在结构上增加了一个循环水箱,一个过滤水箱。水从循环水箱里抽出来,经过过滤水箱后给乌龟们洗澡喝水,再回流到了循环水箱里面。画了一个示意图:
schematic
在经过一段时间的循环后,水必然会减少,除了乌龟们用的,还有蒸发的,所以还得往循环水箱里面加水。既然自动化了,当然不能让我每天盯着水少了就加啊。于是我就增加了一个检测水位的装置:
Water-Level2
检测到了低水位后,水泵就打开,从水桶里往循环水箱里加水,一直加到高水位后水泵就停止了。
你问桶里没水了怎么办?当然是我手动加了,我可舍不得钱买一个能电动控制的水龙头,一个星期加一桶水,顺便清晰一下几个水箱,更换一下过滤棉。

硬件设计

主控还是 ESP8266 模块,除了自动喂食器的电机驱动,增加了两个水泵的电源控制继电器,再用两个 IO 来检测水位。
一个水泵是循环抽水的,可以通过云端设置开关的时间间隔,另一个水泵是加水的,当检测到低水位时,该水泵就打开,水位到最高点时自动关闭水泵。
监测水位的我用的是干簧管和磁铁,在防水上没什么问题,也不用多余的工作。
Water-Level1
中间的白色浮子里面嵌入了一个小磁铁,可以飘浮在水中,随着水位升降。当水面下降时,磁铁会靠近底下的干簧管,干簧管短路,IO 电平由低变高。水位上升的时候磁铁会离开底下的干簧管,干簧管开路,IO 由高变低,当靠近上面的干簧管后,IO由低变高,说明水面到了高水位。
开始本来的想法是用水的导电性来设计水位传感器的,淘宝上卖的也基本都是这类,因为水箱离主控板比较远,线缆一长,ADC 转换的时候容易出错,所以我就改成了用 IO 电平变化来监测水位了。
电路很简单,也没考虑很多,能用就行,仅供 DIY 用。
PCB

软件设计

软件设计还是 ESP8266 模块作为 Arduino 板联网控制,通过 Arduino IoT Cloud 连接设备。
Arduino Cloud IoT 使用入门指南

Arduino 代码很简单,注释我也写得很详细,流程图如下:
flowchart

/* 
Sketch generated by the Arduino IoT Cloud Thing "Autofeed2.0"
https://create.arduino.cc/cloud/things/a4043153-6bfb-4dfa-a055-151cba4627a4

Arduino IoT Cloud Variables description

The following variables are automatically generated and updated when changes are made to the Thing

CloudCounter motorCloudCycleHours;
bool motorCloudControl;
bool pumpCloudControl;
CloudTime pumpCloudOffSecond;
CloudTime pumpCloudOnSecond;

Variables which are marked as READ/WRITE in the Cloud Thing will also have functions
which are called when their values are changed from the Dashboard.
These functions are generated with the Thing and added at the end of this sketch.
*/
#include "thingProperties.h"

// 软件版本号
const char* softwareVersion = "2.1";
// 定义引脚
const int pwm1Pin = 12; // GPIO12 (D6)
const int pwm2Pin = 14; // GPIO14 (D7)
const int pumpPin = 13; // GPIO13 (D5)
const int pumpINPin = 16; // GPIO13 (D5)
const int pumpLowPin = 5; // GPIO4 (D2)
const int pumpHighPin = 4; // GPIO5 (D1)

// 系统故障状态
bool systemFault = false;

//进水泵状态变量
bool pumpINRunning = false;
unsigned long pumpINStartTime = 0; // 记录水泵开启的时间戳
const unsigned long PUMP_TIMEOUT_MS = 2 * 60 * 1000; // 进水泵运行超时2分钟

// --- 用于非阻塞去抖动的变量 ---
unsigned long lastLowPinDebounceTime = 0; // 上次低水位引脚状态变化的时间(毫秒)
unsigned long lastHighPinDebounceTime = 0; // 上次高水位引脚状态变化的时间(毫秒)
const long DEBOUNCE_DELAY_MS = 300; // 去抖动延迟时间 (毫秒)

int debouncedLowPinState = LOW; // 经过去抖动后的低水位引脚状态
int debouncedHighPinState = LOW; // 经过去抖动后的高水位引脚状态
int lastLowReading = -1;
int lastHighReading = -1;

// --- 通用的去抖动函数 ---
// pin: 要读取的引脚
// lastStableState: 存储该引脚上次稳定的状态 (需要传入引用&)
// lastDebounceTime: 存储该引脚上次去抖动的时间 (需要传入引用&)
// lastReading: 存储该引脚上次读取的状态
// 返回值: 经过去抖动后的稳定状态
int debounceRead(int pin, int& lastStableState, unsigned long& lastDebounceTime,int& lastReading) {
int reading = digitalRead(pin); // 读取引脚当前原始状态

// 只有当原始读取值与去抖动后的稳定状态不同时,才重置去抖动计时器
if (reading != lastReading) {
lastDebounceTime = millis();
}

// 如果从上次潜在状态变化到现在的时间超过了去抖动延迟
if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY_MS) {
if (reading != lastStableState){
lastStableState = reading;
}
}
lastReading = reading;
return lastStableState; // 返回去抖动后的稳定状态
}


// 喂食电机状态变量
bool motorRunning = false;
unsigned long motorStartTime = 0; // 记录电机本次启动的时间
const unsigned long motorRunDurationMillis = 25 * 1000; // 电机每次运行时间 秒

// 电机自动循环定时变量
unsigned long motorCycleStartTime = 0; // 记录上次自动循环启动的时间
unsigned long motorCycleDurationMillis = 0; // 自动循环间隔(从Cloud获取时间为小时,转换为毫秒)

// 循环水泵状态变量
bool pumpRunning = false; // 控制水泵的总开关(受Cloud和遥控按键影响)
bool pumpOnPeriod = false; // true表示处于水泵的ON阶段,false表示处于OFF阶段
unsigned long lastPumpToggleTime = 0; // 记录上次水泵ON/OFF切换的时间
unsigned long pumpOnDurationMillis = 0; // 水泵ON时长(从Cloud获取时间为秒,转换为毫秒)
unsigned long pumpOffDurationMillis = 0; // 水泵OFF时长(从Cloud获取时间为秒,转换为毫秒)

// --- 函数原型声明 ---
void startMotor();
void stopMotor();
void setPumpState(bool state);
void stopPump();


void setup() {
// Initialize serial and wait for port to open:
Serial.begin(115200);
// This delay gives the chance to wait for a Serial Monitor without blocking if none is found
Serial.println("Software Version: " + String(softwareVersion));
delay(100);

// Defined in thingProperties.h
initProperties();

// Connect to Arduino IoT Cloud
ArduinoCloud.begin(ArduinoIoTPreferredConnection);
// 设置引脚模式
pinMode(pwm1Pin, OUTPUT);
pinMode(pwm2Pin, OUTPUT);
pinMode(pumpPin, OUTPUT);
pinMode(pumpINPin, OUTPUT);
pinMode(pumpHighPin, INPUT);
pinMode(pumpLowPin, INPUT);

// 初始化输出状态
analogWrite(pwm1Pin, 0);
analogWrite(pwm2Pin, 0);
digitalWrite(pumpPin, LOW);
digitalWrite(pumpINPin, LOW);

// 初始化定时器(确保Cloud属性获取到初始值后才计算)
// 第一次启动时,假定自动循环从现在开始计时
motorCycleStartTime = millis(); //电机循环开始计时
lastPumpToggleTime = millis(); //水泵的循环也从现在开始计时(如果Cloud开关打开)


Serial.println("Arduino Cloud Connected!");
/*
The following function allows you to obtain more information
related to the state of network and IoT Cloud connection and errors
the higher number the more granular information you’ll get.
The default is 0 (only errors).
Maximum is 4
*/
setDebugMessageLevel(2);
ArduinoCloud.printDebugInfo();
}

void loop() {
ArduinoCloud.update();

//--- 处理进水检测和水泵开关逻辑 ---
// 读取水位状态并进行去抖动处理 (非阻塞方式)
debouncedLowPinState = debounceRead(pumpLowPin, debouncedLowPinState, lastLowPinDebounceTime, lastLowReading);
debouncedHighPinState = debounceRead(pumpHighPin, debouncedHighPinState, lastHighPinDebounceTime, lastHighReading);
// 打印当前所有相关变量的实时值
Serial.printf("实时状态 -> 低水位: %d, 高水位: %d, 水泵运行中: %d\n",
debouncedLowPinState, debouncedHighPinState, pumpINRunning);

// 当高位检测到高电平并且进水泵在运行状态,水泵马上停止
if ((debouncedHighPinState == HIGH && debouncedLowPinState == LOW) && pumpINRunning == true) {
digitalWrite(pumpINPin, LOW);
Serial.printf("水泵状态: 关闭\n");
// 设置电机状态为停止
pumpINRunning = false;
}

// 当低水位检测到高电平并且进水泵没有运行没有故障,则马上抽水
if ((debouncedLowPinState == HIGH && debouncedHighPinState == LOW) && pumpINRunning == false && !systemFault) {
digitalWrite(pumpINPin, HIGH);
Serial.printf("水泵状态: 开启\n");
pumpINStartTime = millis();
// 设置电机状态为运行
pumpINRunning = true;
}

// --- 进水泵超时保护 ---
if (pumpINRunning == true) {
if (millis() - pumpINStartTime >= PUMP_TIMEOUT_MS) {
digitalWrite(pumpINPin, LOW);
Serial.printf("进水泵状态: 关闭 (超时 - 系统可能故障!)\n");
pumpINRunning = false;
systemFault = true; // 设置系统故障标志
}
}

// --- 处理喂食电机逻辑 ---
// 自动循环定时启动
// 检查Cloud开关是否打开 AND 电机当前未运行 AND 距离上次自动启动已过去设定时间
motorCycleDurationMillis = (unsigned long)motorCloudCycleHours * 3600UL * 1000UL; // 实时更新循环周期,cloud 计数为小时
if (motorCloudControl && !motorRunning && motorCycleDurationMillis > 0 && (millis() - motorCycleStartTime >= motorCycleDurationMillis)) {
Serial.println("Motor triggered by automatic cycle.");
startMotor();
motorStartTime = millis(); // 记录本次启动时间
motorRunning = true;
motorCycleStartTime = millis(); // 重置自动循环计时
}

// 电机运行时间到期停止
if (motorRunning && (millis() - motorStartTime >= motorRunDurationMillis)) {
Serial.println("Motor run duration finished.");
stopMotor();
motorRunning = false;
// 如果是通过Cloud开关启动的,当运行时间到期时,Cloud开关状态保持ON
// 如果是通过自动循环或外部触发启动的,运行时间到期停止,Cloud开关状态不变
}

// --- 处理循环水泵逻辑 ---
// 只有当Cloud控制开启并且无故障时,水泵才进入循环模式
if (pumpCloudControl && !systemFault) {
// 实时更新泵的ON/OFF时长
pumpOnDurationMillis = (unsigned long)pumpCloudOnSecond * 1000UL; // 从Cloud获取并转换为毫秒
pumpOffDurationMillis = (unsigned long)pumpCloudOffSecond * 1000UL; // 从Cloud获取并转换为毫秒

// 检查当前处于ON还是OFF周期,并判断是否需要切换
if (pumpOnPeriod) { // 当前是ON周期
if (millis() - lastPumpToggleTime >= pumpOnDurationMillis && pumpOnDurationMillis > 0) {
Serial.println("Pump ON period finished, switching to OFF.");
pumpOnPeriod = false; // 切换到OFF周期
lastPumpToggleTime = millis(); // 记录切换时间
setPumpState(pumpOnPeriod); // 更新水泵引脚状态 (LOW)
}
} else { // 当前是OFF周期
if (millis() - lastPumpToggleTime >= pumpOffDurationMillis && pumpOffDurationMillis > 0) {
Serial.println("Pump OFF period finished, switching to ON.");
pumpOnPeriod = true; // 切换到ON周期
lastPumpToggleTime = millis(); // 记录切换时间
setPumpState(pumpOnPeriod); // 更新水泵引脚状态 (HIGH)
}
}
pumpRunning = true; // 只要Cloud开关打开,即使在OFF周期,逻辑上水泵是处于“运行中”循环模式
setPumpState(pumpOnPeriod); // 确保引脚状态与当前周期一致
} else {
// 如果Cloud开关关闭,停止水泵循环并关闭水泵
if(pumpRunning) { // 避免重复执行关闭操作
Serial.println("Pump Cloud control turned OFF, stopping pump cycle.");
stopPump();
pumpRunning = false;
pumpOnPeriod = false; // 确保回到初始状态
}
}
delay(50);
}

void startMotor() {
digitalWrite(pwm1Pin, HIGH); //逆时针旋转
digitalWrite(pwm2Pin, LOW);
Serial.println("Motor started.");
}

void stopMotor() {
digitalWrite(pwm1Pin, LOW);
digitalWrite(pwm2Pin, LOW); // 停止电机
Serial.println("Motor stopped.");
}

void setPumpState(bool state) {
digitalWrite(pumpPin, state ? HIGH : LOW);

}

void stopPump() {
setPumpState(false); // 直接关闭水泵
}


/*
Since PumpCloudControl is READ_WRITE variable, onPumpCloudControlChange() is
executed every time a new value is received from IoT Cloud.
*/
void onPumpCloudControlChange() {
Serial.printf("pumpCloudControl changed to: %s\n", pumpCloudControl ? "true" : "false");
if (pumpCloudControl) {
// 从Cloud开启水泵,立即开始循环
Serial.println("Starting pump cycle from Cloud.");
pumpRunning = true;
pumpOnPeriod = true;
lastPumpToggleTime = millis(); // 重置泵循环计时
setPumpState(pumpOnPeriod); // 立即打开水泵
} else {
// 从Cloud关闭水泵,立即停止水泵和循环
Serial.println("Stopping pump cycle from Cloud.");
stopPump();
pumpRunning = false;
pumpOnPeriod = false; // 确保回到初始状态
}
}

/*
Since MotorCloudControl is READ_WRITE variable, onMotorCloudControlChange() is
executed every time a new value is received from IoT Cloud.
*/
void onMotorCloudControlChange() {
Serial.printf("motorCloudControl changed to: %s\n", motorCloudControl ? "true" : "false");
if (motorCloudControl) {
// 从Cloud开启电机,运行25秒
startMotor();
motorStartTime = millis();
motorRunning = true;
} else {
// 从Cloud关闭电机,立即停止
stopMotor();
motorRunning = false;
}
}

/*
Since MotorCloudCycleHours is READ_WRITE variable, onMotorCloudCycleHoursChange() is
executed every time a new value is received from IoT Cloud.
*/
void onMotorCloudCycleHoursChange() {
motorCycleStartTime = millis();
}


/*
Since PumpCloudOffSecond is READ_WRITE variable, onPumpCloudOffSecondChange() is
executed every time a new value is received from IoT Cloud.
*/
void onPumpCloudOffSecondChange() {
// Add your code here to act upon PumpCloudOffSecond change
}

/*
Since PumpCloudOnSecond is READ_WRITE variable, onPumpCloudOnSecondChange() is
executed every time a new value is received from IoT Cloud.
*/
void onPumpCloudOnSecondChange() {
// Add your code here to act upon PumpCloudOnSecond change
}

可以看到上面的代码是 V2.1 版本,因为 V2.0 开始用的第一天,从乌龟槽流到循环水箱的下水管堵住了,导致水没法循环了,所以循环水箱里的水一直减少,加水的泵一直就加,一满桶水很快没了。我赶紧关闭了电源进行清理。
结果没过两天,早上五点多被嗡嗡声吵醒了,我一看,循环水箱没水了,水桶也没水了,导致加水泵一直开着,还不知道干烧了多久。我还在想昨晚桶里水不少,为什么这样,仔细一看,水桶底漏了一个洞。靠,刚烧了固件就连续出现两个意外,促使我完善了代码。
于是我就增加了加水泵保护的代码,持续工作时长超过两分钟自动停止,并报故障,直到系统重启。

下载 Arduino APP,登录,可以看到设备上线了,点击按键控制,因为 Arduino IoT Cloud 对免费版的 Cloud Variables 总数量有限制,所以开关项就这么几个了。
phone

以上所有原始文件在此,仅供参考,有问题自己搜索或问 AI:
https://github.com/harry10086/Autofeed