ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ESP32+MQTT的智慧鱼缸控制器:从传感器选型到异常保护全记录

ESP32+MQTT的智慧鱼缸控制器:从传感器选型到异常保护全记录

做“智慧鱼缸控制器”这个项目,起因特别朴素:我要出差一周,回来发现缸里的鱼少了三条,水面上浮了一层油膜,加热棒还在不知疲倦地烧。那一刻我就意识到,养鱼这件事,真正缺的不是勤快,而是一套能在人不在场时依然看得住水、喂得上食、保得了温的系统。智慧鱼缸控制器解决的就是这个问题:用传感器实时感知水温、水位、水质状态,用单片机控制加热、过滤、喂食、灯光这些设备,再通过网络把状态推到手机端,让我在几百公里外也能知道鱼缸发生了什么。

如果正在养鱼,又经常出差、加班,或者单纯想把手边的鱼缸变成一块嵌入式练手项目,这篇文章就是按这个目标写的。它不是一套量产产品的说明书,更像是一份从需求拆解到硬件选型、从软件框架到联调排障的完整现场记录。内容覆盖了主控和传感器怎么挑、控制逻辑怎么写、断网断电怎么办、哪些坑我踩过,尽量把每一步为什么这样做讲清楚,方便直接参考复现。

1. 需求拆解:智慧鱼缸控制器到底要解决什么

1.1 场景痛点:鱼缸管理不只在“喂食”

大多数人想到自动鱼缸,第一反应是自动喂食器。实际养过鱼之后才会发现,喂食只是最表面的一层。鱼缸是一个小型生态系统,真正影响鱼生存状态的是水温波动、水质恶化、过滤循环中断、光照紊乱,以及人不在场时设备故障没人发现。

举几个真实场景。夏天白天气温高,晚上空调一开室温又降下来,鱼缸水温可能在一天内波动三四度,很多鱼就受不了。出差一周没人换水,硝酸盐浓度慢慢升高,鱼开始蔫,等回来再处理往往已经晚了。热带鱼缸的加热棒如果失控,水温一路升到30多度,致死率极高。还有停电后过滤器恢复供电但水泵空转、气管被异物堵住、水位蒸发下降导致加热棒露出水面干烧,这些都是没人盯就会出大事的问题。

所以智慧鱼缸控制器的核心目标不是“自动喂食”,而是三件事:实时感知环境、自动维持稳定、异常时远程告警。感知对象包括温度、水位、水质状态;维持稳定的手段包括加热、补光、定时投喂、过滤循环控制;告警则是把缺水和超温这类风险第一时间推送到手机。人工参与度越低,对可靠性要求越高,这就是本项目和普通DIY玩具最不一样的地方。

1.2 设计原则:先做到可控,再谈全自动

项目设计之初,我给自己定了几条原则,实际操作中帮了大忙。

第一条是模块化。主控板、传感器、执行器、供电完全分开,哪一块坏了就换哪一块,不用整体重做。传感器全部用独立模块,杜邦线或接线端子连接,不用焊接固定,方便以后升级和排查。

第二条是安全优先。鱼缸是水电共存的场景,220V加热棒控制不能有一点含糊。所有强电控制都通过带隔离的继电器模块,弱电部分与强电部分物理隔离;缺水、超温、传感器异常这些故障状态,每一条都要在代码里有明确的兜底行为,而不是等到出问题再处理。

第三条是低成本、可复现。第一版不追求一次到位设计PCB,全部用市面常见的开发板和模块搭起来,总成本控制在合理范围,绝大多数嵌入式爱好者都能复刻。第四条是留出扩展接口。比如这次先不装PH传感器,但预留IO口和通信协议里的数据字段,后面想加水质检测或者自动换水,不用重写系统。

为什么不做成完全自动的“养鱼机器人”?因为全自动意味着大量的冗余设计和昂贵传感器,可靠性要求也指数级上升。对家庭用户来说,最合适的产品形态是“人参与决策、设备负责执行、异常及时通知”:日常的加热、喂食、照明交给控制器,换水洗棉这类高风险操作依旧由人来完成。这个定位让项目复杂度可控,又实实在在解决痛点。

2. 硬件选型:主控、传感器、执行器怎么选

2.1 主控选型:ESP32为什么是首选

主控是整个系统的核心。我对比过几类常见方案,最终选了ESP32系列开发板。原因并不复杂:自带WiFi和蓝牙,不需要外接通信模块;双核160MHz以上,跑传感器任务和网络任务不费力;有ADC、PWM、I2C、单总线等丰富外设;功耗在可接受范围;开发工具链和社区资料相当成熟。

对比一下就知道为什么没选其他方案:

方案优点缺点适合场景
ESP32开发板WiFi内置、生态好、性能足够引脚电平3.3V,驱动执行器需要加电路首选,尤其是需要远程通信
STM32稳定、外设强、实时性好网络需要额外模块,开发门槛稍高对稳定性和实时性要求极高的工业级方案
Arduino UNO上手快、资料多性能弱、无网络能力纯本地逻辑教学演示
树莓派性能强、可做复杂运算成本高、启动慢、不适合直接控制硬件图像识别、视频监控等重负载场景

ESP32具体型号推荐使用常规的开发板即可,注意选择带USB转串口芯片的版本,方便烧录和调试。内存和Flash容量足够存储MQTT客户端、传感器驱动和OTA升级固件,不用刻意追求高配。

选好主控只是开始。真正影响系统稳定性的,是传感器和执行器的选型。很多DIY项目死在传感器读数不准和执行器驱动不稳上,这两部分值得单独展开。

2.2 传感器选型:温度、水位、水质的组合方案

温度传感器用了DS18B20防水探头。这款传感器精度±0.5℃,测量范围覆盖鱼缸场景绰绰有余,走单总线协议,一个引脚就能挂多个探头,而且出厂前做过校准,不需要用户自己标定。探头不锈钢外壳适合长期泡在水里,但要避免贴在加热棒表面或者缸底水循环死角,最好放在过滤泵出水口附近,那里水流充分,测出来的温度更接近真实水体温度。

水位传感器是个容易踩坑的点。电极式浮球开关便宜,但长期泡水后电极会电解腐蚀,表面结垢导致误判。我试过之后换成了电容式液位传感器,贴在鱼缸外壁或安装在固定水位高度,非接触式检测,不接触水体就不会有腐蚀和污染问题。它输出开关量,放在“缺水警戒线”上,一旦水位低于该点,立即触发保护逻辑:切断加热棒和水泵电源,同时本地蜂鸣器和远程告警同时动作。

水质检测用TDS探针。TDS反映的是溶解性固体总量,不能直接代表水质好坏,但趋势变化很有参考价值:鱼缸水体蒸发后TDS缓慢上升,换水后明显下降,投喂过多也会让TDS走高。探针有两个注意点:一是长时间直流供电会导致电极极化,读数漂移严重,所以测量时应短时供电,测完立刻断电;二是TDS值受温度影响,需要做温度补偿,代码里要根据当前温度修正读数。

至于PH传感器,虽然对养鱼很有用,但可维护成本不低。PH电极需要定期校准和保存液维护,玻璃电极也比较脆弱。我的建议是第一版不装,先把温度、水位、TDS跑通,系统稳定后再扩展。

2.3 执行机构选型与安全回路设计

执行机构包括加热棒、水泵、LED灯具、自动喂食器四类,每一类都有自己的脾气。

加热棒控制是安全级别的需求。家用加热棒大多是220V供电,我用了带光耦隔离的继电器模块,由ESP32的一个GPIO控制,低电平触发。关键点在于:继电器只控制火线通断,零线不经过继电器;所有220V接线用绝缘端子固定,并且与控制板保持物理距离。虽然很多成品加热棒自带温控,但我的系统里只把它当作执行器,温度保护逻辑由自己的温度传感器和代码实现,形成双重保险。

水泵选择12V直流潜水泵,功率由水位和缸体大小决定,一般5到10W足够循环。低压系统操作更安全,也方便用MOS管或继电器做开关控制。水泵连续运转容易在叶轮处积聚杂质,所以过滤棉要定期清洗;更重要的是,水位低于水泵进水口时不能空转,否则容易烧毁电机,这个问题在控制逻辑里必须处理。

LED灯具用12V灯带,通过MOS管模块做PWM调光,模拟日出日落。纯定时开关的灯比较呆板,有了PWM渐入渐出后,观赏效果好很多,鱼的应激反应也小一些。

自动喂食器用SG90舵机加一个3D打印的储料仓。舵机旋转角度控制出料口开合,转动时间越长落料越多。这里有个细节:饲料颗粒大小要适配出料口直径,太大容易卡住,太小会一次性漏出一大堆。我最后加了一片硅胶挡板在出料口,减缓饲料下落速度,实测效果好很多。

所有执行器的供电要分开。主控板和传感器用5V供电,水泵、灯带这类功率器件用12V供电,加热棒走220V独立回路。混合用一个电源会互相干扰,继电器吸合的瞬间电压跌落会把ADC采样数据搞乱,甚至让主控重启。

3. 软件架构:让设备“离线也能自动、在线才能远程”

3.1 任务划分:传感采集、决策控制、通信上报分离

硬件搭好只是第一步,软件架构决定这个系统能跑多久、稳不稳。第一版我用Arduino框架加FreeRTOS,把不同功能拆成独立任务,避免一个阻塞操作卡死整个循环。

任务划分大致是:传感器采集任务负责按周期读取DS18B20、TDS探针、水位传感器;控制决策任务负责执行温度滞回控制、水位保护、喂食计划、灯光渐变;通信任务负责MQTT连接、心跳上报、命令接收;告警任务独立监听异常标志并触发本地蜂鸣器和远程推送。

周期设置要合理:温度变化慢,5秒读一次;水位1秒检查一次;TDS测量耗电且会造成极化,30秒以上才测量一次。控制决策每200毫秒运行,这样继电器动作的响应时间可控。上报频率结合通信开销,10秒一次状态推送完全足够,也可以用事件触发方式,状态突变时立即上报。

这样的分层让代码逻辑清晰,排查问题时可以直接定位是采集问题、控制问题还是网络问题。更重要的是,即使WiFi断开、MQTT服务器不可用,本地控制任务依然在独立运行,鱼缸不会因为断网变成睁眼瞎。

3.2 通信协议:MQTT到底强在哪

鱼缸控制器和手机App之间通信,我选了MQTT协议。前提是先把一个消息中间件跑起来,可以用云上的服务,也可以部署在局域网内的服务器上。MQTT是发布订阅模式,设备端和手机端不直接互相连接,而是通过Broker中转,这对家庭场景特别合适。

MQTT的优势在于轻量、可靠、适合不可靠网络。一条连接叠加一个保活心跳,服务器和客户端能互相感知对方是否在线;消息有QoS等级可选,控制类命令用QoS1保证至少送达一次;设备断开时可以通过遗嘱消息通知服务器“我掉线了”,手机端马上就能收到离线提醒。

在设备端我设计了三个主题:tank/telemetry上传状态数据,tank/command接收手机端下发的控制指令,tank/alarm发布告警事件。消息格式用JSON,方便调试和扩展。温度、水位、TDS这些字段按固定结构填充,手机端解析起来很轻松。

有个细节值得提一下:不要把命令和执行状态混在同一个主题里。下发命令和上报状态分开,排查时一眼能看出是消息没送达、命令被拒绝还是执行后状态没更新。我在本地调试时抓包对比过,分开之后问题定位快很多。

3.3 状态机设计:异常情况下的安全兜底

系统运行状态不能只有“正常工作”一种模式,还要显式定义故障模式下的行为。我用了一个简单状态机,包含自动模式、手动模式、安全保护模式三种。

自动模式是默认状态:加热、喂食、灯光按设定逻辑运行。手动模式用于临时操作,比如手机端手动喂食、临时关灯,手动操作超时后自动回到自动模式,避免“开着手动忘了切回来”。安全保护模式是系统兜底,触发条件包括水位低、温度超上限、传感器连续多次读取失败、主控看门狗复位等。进入安全保护模式后,加热棒强制断电、水泵断电、告警推送,直到操作人员手动确认故障排除才复位。

这个状态机的价值在于,所有异常路径都有明确的退出条件,不会出现“设备不知道自己在干嘛”的死循环。比如温度超限时如果同时检测到水位低,系统需要判断先执行哪一个保护动作,我在这里用了一个简单的优先级表:水位低优先于温度超限,因为加热棒露出水面干烧的后果比水温偏高更严重。

代码实现不复杂,但思路要对。先画状态转移,再写代码,不要边写边想。实际运行中,这套状态机帮我避免了好几次“加热棒失控还傻傻继续加热”的险情。

4. 实操记录:从接线到联调的完整流程

4.1 供电与电气接线:水电共存的注意事项

接线是整个项目里最需要耐心的一步。电源分配上,我用一个12V适配器作为总输入,通过DC-DC降压模块分出5V给ESP32和传感器,12V直接给水泵和LED灯带;220V加热棒接继电器模块的负载端。注意ESP32的GPIO输出能力有限,直接驱动继电器线圈或MOS管可能带不动,需要用三极管或光耦模块做中间驱动。

水位传感器安装在缸体外壁指定高度,DS18B20探头从缸沿引入水中,引线走缸外壁,避免在水中穿来穿去。TDS探针固定在水流比较均匀的区域,不要贴缸壁,否则会影响读数稳定性。

接线顺序有讲究:先接弱电部分,再接强电部分,最后才通电测试。强电端子和弱电端子分开放置,线缆用不同颜色区分,12V用红黑线,220V用花线或标注胶带,穿上热缩管。如果条件允许,在220V进线处加一个漏电保护插头,不是小题大做,鱼缸漏水到插座附近的场景并不少见。

电源走线时要避开传感器信号线,否则继电器吸合瞬间的电流浪涌会在信号线上感应出噪声,让ADC读数抖动。我第一版就是没注意这点,温度读数每隔几分钟就跳一下,排查了很久才发现是走线太近。

4.2 传感器校准与数据滤波:实测填坑

DS18B20不需要额外校准,出厂已经校准过,直接用就行。TDS探针则需要校准。市售TDS模块一般会带标定液,标准做法是配置对应的TDS标准溶液,把探针放进去,读取ADC原始值,按说明书算出K值写入程序。没有标定液的话,我用一个笨办法:拿一杯已知TDS值的水(比如纯净水稀释盐水),用另一支已校准的TDS笔做对照,调K值直到读数一致。

TDS读数非常容易跳变,主要原因是探针极化、水流动不稳定、ADC参考电压波动。我做了两层处理。第一层是测量瞬间供电,代码里先给探针通电商,延时100毫秒后再采集,采集完马上断电,避免极化和发热问题。

第二层是软件滤波。温度数据用中值滤波加滑动平均,TDS数据用限幅滤波加滑动平均,实测能把偶尔的尖刺完全削掉。核心代码片段如下:

// 中值滤波:取5次采样中间的1个值,去除偶发尖刺 float medianFilter(float arr[], int len) { for (int i = 0; i < len - 1; i++) { for (int j = i + 1; j < len; j++) { if (arr[i] > arr[j]) { float t = arr[i]; arr[i] = arr[j]; arr[j] = t; } } } return arr[len / 2]; } // 滑动平均:保留最近N个值,取平均,平滑曲线 float sampleBuf[10] = {0}; int bufIdx = 0; float exponentialOrMovingAverage(float newVal) { sampleBuf[bufIdx] = newVal; bufIdx = (bufIdx + 1) % 10; float sum = 0; for (int i = 0; i < 10; i++) sum += sampleBuf[i]; return sum / 10; }

DS18B20在探头接触不良时会返回一个固定错误值,比如-127或85,不能直接当正常数据处理,读取后要做上下限判断,连续失败三次就上报传感器异常。

4.3 关键控制逻辑实现

温度控制我用了滞回控制,而不是简单的“低于25度就开,高于25度就关”。滞回控制的原理是设置两个阈值,避免继电器在目标温度附近频繁切换。我的参数是:目标25℃,低于24.5℃开启加热,高于25.5℃关闭加热,同时限制最短开关间隔为60秒,进一步保护继电器触点。

// 滞回控制 + 最小开关间隔 if (temperature <= 24.5 && heaterState == HEATER_OFF && millis() - lastSwitchTime > 60000) { turnHeaterOn(); lastSwitchTime = millis(); } else if (temperature >= 25.5 && heaterState == HEATER_ON && millis() - lastSwitchTime > 60000) { turnHeaterOff(); lastSwitchTime = millis(); }

喂食逻辑用了定时器驱动,不是delay阻塞。舵机从待机角度转到投喂角度,保持一段时间再转回来,每次投喂的饲料量由这个保持时间控制。为了防止漏料,我设置了最小投喂间隔,两次投喂至少间隔4小时,避免有人手滑连点投喂把鱼撑死。水位保护逻辑放在最前面,只要水位触发低水位信号,不管当前温度多少,加热和水泵一律先断电。这个判断放在所有控制逻辑之前,确保异常时第一时间生效。

灯光控制用PWM输出模拟日出日落:每天早上6点开始缓慢增加亮度,30分钟内从0升到目标值,晚上22点反向渐变到熄灭。渐变曲线用线性插值就够,不需要更复杂的算法。

4.4 联调与验收清单

全部模块装完后,不要急着入缸,分四步联调。第一步单独测试每个传感器,串口打印原始值和滤波值,确认读数稳定且符合预期。第二步单独测试每个执行器,通过调试指令强制开关加热继电器、水泵、灯带、舵机,确认动作正确。第三步联调MQTT,设备端上报数据,手机端能看到状态,手动下发命令能控制设备。第四步跑联动逻辑,人为模拟水位低、温度超限、断网等异常情况,观察系统是否进入保护模式。

我列了一张验收清单,逐项打勾:

验收项测试方法通过标准
温度采集串口观察10分钟温度波动不超过0.3℃
TDS采集对比标准液读数误差在可接受范围内
水位低保护人为降低水位加热和水泵立即断电并告警
温度超限保护调低超限阈值模拟加热断电并推送告警
喂食动作手动触发喂食舵机转动顺畅,出料量合适
MQTT指令手机下发开关灯延迟小于几秒
断网重连关闭WiFi再恢复自动重连,恢复后状态同步
看门狗复位代码模拟死循环设备自动重启并进入自动模式

这套清单看起来简单,但漏掉任何一项,后面长期运行都会付出代价。尤其是异常模拟,一定要在真实场景下测试,处于安全考虑所有强电测试都应在断电监护下进行。

5. 常见问题与排查技巧实录

5.1 从现象到根因:三个真实排查案例

第一个案例是温度读数周期性跳变。现象是每隔几分钟温度值突然跳高2到3度,然后又恢复正常。一开始怀疑传感器坏了,换了一个新的还是这样。后来用示波器观察信号线,发现跳变时刻和继电器吸合时刻完全重合,判断是继电器吸合瞬间电流变化在信号线上感应出噪声。处理办法是把传感器信号线换成双绞屏蔽线,并远离电源线,跳变消失。

第二个案例是设备每隔几小时自动重启一次。排查发现不是看门狗触发,而是供电电压在继电器吸合瞬间从5V掉到4.2V,低于ESP32最低工作电压。原因是我用一个5V的电源同时给主控和继电器模块供电,功率余量不够。解决办法是继电器模块的驱动电源单独用12V转5V的稳压模块,主控独立供电,并用一个470μF电解电容做缓冲,再也没重启过。

第三个案例是MQTT连接偶发掉线。现象是设备在线,但手机端经常收到离线通知又马上恢复在线。排查发现是MQTT保活时间设置太短,一旦网络波动,客户端和服务器还没完成重连就判定对方掉线。调整保活间隔为60秒,并实现指数退避重连,问题消除。

这三个案例有一个共性:问题并不总出在你最初怀疑的模块上,排查时一定要带着怀疑去看数据,而不是猜。先把现象、时间点、触发条件记录下来,再对照日志定位,效率高很多。

5.2 高频问题速查表

把一段时间运行中遇到的高频问题整理成表格,方便遇到类似情况时快速对照。

现象可能原因解决措施
温度读数跳变数据线靠近强电、探头接触不良换屏蔽线、远离电源线、检查接头
继电器频繁吸合温度控制阈值太小使用滞回控制,限制最小开关间隔
继电器触点打火负载为感性设备,起停电流大加RC吸收电路,或换固态继电器
TDS读数漂移电极极化、水流太急测量瞬间通电,采集后断开
水位传感器误报传感器表面水垢或气泡选用电容非接触式传感器,定期清洁
舵机卡料、不转饲料颗粒卡住出料口调整出料口口径,加装挡板
设备反复重启电源余量不足、电压跌落分开供电,加大滤波电容
WiFi频繁掉线信道干扰、路由器配置固定独特信道,调整网关位置
手机收不到告警通知权限、告警主题未推送检查App订阅主题和系统通知权限

排查故障的通用思路是:先看现象发生频率,再看供电是否稳定,然后看传感器数据是否可信,最后才怀疑通信链路和代码逻辑。百分之八十的稳定性问题其实出在供电和信号干扰上,这个比例在低速单片机项目里相当有代表性。

6. 扩展方向与个人体会

6.1 这个控制器还能怎么扩展

硬件预留的IO口和接口让扩展变得很简单。手头这个项目已经跑了一段时间后,我盘算过几个升级方向。

自动换水是玩家普遍想要的:加装一个进水电磁阀和一个排水泵,配合超声波或者浮球传感器精确控制水量。核心难点不是泵选型,而是防止虹吸和换水速度控制。进水侧需要加装防虹吸阀,排水泵运行时间要控制,避免排空后空转烧泵。

多缸集中管理是另一个实用方向:每个鱼缸独立节点,通过MQTT上报到同一套看板,手机端按缸分组查看数据和告警。数据可视化方面,可以把历史温度、TDS数据写入本地数据库,生成趋势曲线,观察水质变化趋势比看单点读数有价值得多。

对于观赏性需求,可以在鱼缸上方加一个摄像头,定时拍照推送到手机,出差时看一眼鱼的生活状态。摄像头选型注意夜视功能,配合灯光定时策略,可以做到夜间画面依然可辨。

6.2 个人实操体会与建议

第一版完工后运行了几个月,最大的体会不是技术难度,而是分寸感。智能设备不是功能越多越好,每个功能背后都是一份可靠性负担。自动喂食器可以提升便利性,但如果因为误操作把整包饲料漏进水里,损失比手动投喂更大。所以在有限的开发精力里,优先把安全保护做扎实,远比堆新功能更重要。

在实际使用中我发现,真正能降低缸主焦虑的不是远程开启某个灯,而是一份及时准确的“一切正常”状态。状态上报的意义在于异常时第一时间通知,而不是让人时刻盯着数值看。所以告警推送的稳定性和准确性,比数据曲线的美观程度重要得多。

另一个容易被忽视的点是售后维护视角。鱼缸控制器长期泡在湿润环境里,接头氧化、线路老化、设备积垢都是潜在风险。设计时就要想着“半年后怎么拆下来检修”,线缆留足余量,接插件用可拆卸端子而不是焊死。改造一个邻居的鱼缸时,我发现他的控制器因为接线全部焊死,坏了一个传感器只能整个模块换掉,成本和麻烦程度完全不一样。

如果现在有人要复刻这套系统,我会建议从最小可用版本开始:ESP32加温度传感器加加热继电器加水位报警,先跑通一整套安全逻辑。本地控制稳定后,再加MQTT远程监控,最后才加喂食和灯光。这个顺序投入成本低,每一步都能独立验证,不会把问题堆到最后一次性排查。

这项目后续可以持续打磨,比如断电后自动恢复的策略、多个传感器冗余设计、更精准的水质判断逻辑。但对我来说,它最核心的价值已经达成:人不在鱼缸旁边的时候,系统知道怎么保护鱼缸,也知道怎么通知我。养鱼这件事,终于可以少操一点心了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进