尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MiroFish:ESP32+树莓派本地鱼缸监测与告警实践

MiroFish:ESP32+树莓派本地鱼缸监测与告警实践 养鱼这件事看着简单实际上是个典型的信息黑箱问题。鱼缸摆在那里水温多少、水质如何、鱼今天活跃不活跃全靠人凑过去看一眼凭感觉猜。我做 MiroFish 这个项目的初衷很直接把鱼缸里那些看得见摸不着的东西变成一条条能被记录、能被回看、能被报警的数据。MiroFish 不是某个市面上能买到的成品它是我自己攒出来的一套本地化鱼缸监测系统名字里 Miro 取的是注视、观察的意思Fish 就是字面意义的鱼——盯着鱼看并且记住看到的一切。整套东西由三部分组成一块 ESP32 负责采水温、TDS、pH 和光照一台树莓派挂着摄像头负责看鱼的活动量一台常开的小主机跑本地服务收数据、存数据库、算告警。总成本压在五百块以内不需要任何云服务也不需要你懂深度学习。它适合几类人养鱼老是翻车想搞清楚原因的、想入门物联网但不知道拿什么练手的、以及单纯觉得给鱼缸装个监控这件事好玩的人。下面我按从需求到落地、从硬件到代码、从采集到告警的顺序把整套东西拆开讲一遍包括我踩过的那些坑。1. MiroFish 是什么把鱼缸里看不见的东西变成数据1.1 起因连续翻缸之后我才意识到自己在瞎养最早养的是孔雀鱼一缸十几条活蹦乱跳了两周然后开始陆续掉尾巴、趴缸、翻肚。我当时的处理方式是经典的三板斧换水、加盐、祈祷。结果就是今天死两条、明天死三条一周下来全军覆没。第二次养斑马鱼我自认为做足了功课买了测试纸、买了加热棒还是没撑过一个月。第三次是草缸水草倒是长得不错鱼依然保不住。转折点是有一次我用测试纸测了一下水质发现亚硝酸盐那一格颜色深得离谱。问题是我完全不知道这个数值是从哪天开始升上去的。如果每天测测试纸成本高、麻烦而且颜色比对我自己都看不太准如果不测那永远是出事之后才知道。那一刻我想明白一件事我需要的不是更多的经验而是连续的数据。测试纸给我的是一个时间点上的快照而养鱼真正要盯的是趋势——温度是不是在慢慢爬、TDS 是不是每周都在涨、pH 是不是在夜间波动。这些东西只有连续采集才看得出来。MiroFish 就是从我要一条温度曲线这个最朴素的念头长出来的。先是加了个 DS18B20 水温探头接在一块吃灰很久的 ESP32 上数据发到本地一台旧笔记本。跑了几天之后我发现光有温度不够于是加了 TDS又加了 pH最后觉得鱼到底活得怎么样这件事还没解决就把摄像头也挂了上去。整套系统就这样一点点拼起来没有一开始就画架构图。1.2 这个项目要解决的三类问题把需求收敛之后MiroFish 其实就干三件事。第一类是环境参数记录。水温、TDS、pH、光照强度这四项是家庭养鱼里最容易出问题、也最容易量化的指标。水温决定了鱼的代谢速率和免疫能力TDS 大致反映水体里溶解性固体的累积程度pH 决定了氨氮的毒性形态光照则影响水草和藻类的生长节奏。这四项记下来至少你能回答上周三到底发生了什么。第二类是异常告警。注意我说的是异常告警而不是实时监控。这两者的区别很大实时监控要求你盯着看异常告警要求系统在你没看的时候替你盯着。养鱼出问题往往发生在半夜或者你出差的时候加热棒坏了水温在六小时内从 26 度掉到 18 度这种事人是不可能守着的。第三类是行为观测。这是摄像头部分要干的事也是最有意思的部分。鱼的活跃度和它的健康状态强相关——生病或者水质不适的鱼会趴缸、躲角落、减少游动。人眼很难判断今天的鱼比昨天安静了 20%但通过计算画面里运动像素的占比可以把这个感觉变成一个数字形成一条活跃度曲线。这条曲线单独看没什么用但和环境数据放在一起看往往能提前几天发现苗头。提示行为观测的定位是辅助参考不是诊断结论。鱼的活跃度受喂食、光照周期、繁殖行为影响很大波动是正常的。真正有价值的信号是活跃度持续下滑并且环境参数同时异常这种组合情况。1.3 架构选型为什么我不上云先做本地这里要说清楚一个取舍。市面上能买到的智能鱼缸方案绝大多数走的是设备上报到云平台App 查看的路线。这种方案的好处是开箱即用、远程可看但它有三个我不太能接受的地方。一是依赖网络。鱼缸这种东西是要常年运行的家里路由器抖一下、光猫重启一下数据就断了。而且一旦厂商停止服务你手里的设备基本变成砖头。二是数据归属。我不想把自己鱼缸的数据随便放到别人的服务器上虽然这些数据没什么敏感性但主观上不舒服。三是成本。走云平台的方案通常要买它家的专用设备单套价格普遍在几百到上千而且传感器精度往往还不如图自己配的。所以我选择全本地ESP32 通过局域网把数据 POST 到本地服务服务存在旧笔记本或者树莓派上数据库用 SQLite告警走本地消息通道。整个系统的运行不依赖任何外部服务断网也不影响采集和存储ESP32 会本地缓存联网后补传。这里有一个我做过的妥协远程查看。如果你出差在外想看数据全本地方案天然做不到。我的处理方式是在家里路由器上做了端口映射配合一个简单的反向代理和访问控制只对我自己的设备开放。这个方案对技术要求稍高一点但它把要不要暴露到公网的决定权留在了你自己手里。2. 硬件清单与传感器选型预算 500 元怎么花2.1 主控与传感器清单先上清单这是我实际用的一套价格按我下单时候的行情算会有波动仅供参考。部件型号/规格参考单价数量作用主控ESP32-WROOM-32 开发板30 元1采集、上传水温DS18B20 防水探头8 元2主缸 备用溶解性固体TDS 模拟模块 探头30 元1水质累积度酸碱度pH 模块BNC 接口 探头75 元1水质酸碱度光照BH1750 数字光照模块6 元1光照周期记录视觉树莓派 Zero 2 W 摄像头模块200 元1行为观测存储主机旧笔记本或树莓派 4B已有1服务与数据库杂项防水盒、杜邦线、电源、扎带60 元—安装算下来大概在 400 到 500 元之间。这个价位里pH 模块是最大头也是最能省也最不能省的一项。我一开始图便宜买了三十多块的 pH 模块读数飘得离谱同一个水样连续测五次能给你五个不同的值误差能到 0.5 以上这种精度做趋势图毫无意义。后来换成带温度补偿和 BNC 接口的模块配合标准缓冲液校准稳定在 ±0.1 以内才算能用。2.2 温度、TDS、pH 三个传感器的真实表现DS18B20是这三个里最省心的。它走单总线协议一根数据线可以挂多个探头防水封装直接扔水里就行。分辨率可以配 9 到 12 位12 位对应 0.0625 度的分辨率转换时间约 750 毫秒。实际使用中我建议用 11 位或者 12 位因为水温变化本来就慢多花几百毫秒换来的精度是值得的。它的绝对精度标称 ±0.5 度实际用下来同一批次的两根探头之间会有 0.1 到 0.2 度的差异如果你很在意绝对值可以用冰水浴做一次单点校准。TDS 模块的原理是测水体电导率再换算。它输出的是模拟电压你需要自己在代码里算。常见的换算公式是先把电压做温度补偿再套一个三次多项式。这个模块最大的问题是它对水温极其敏感——同一盆水温度从 20 度升到 28 度读数能差出 20 到 30 ppm。所以温度补偿这一步绝对不能省否则你看到的 TDS 曲线里混进去的其实是水温的变化。另外探头的两个金属针脚长期泡在水里会氧化读数会逐渐偏高我一般两三个月拿出来用酒精棉擦一次。pH 模块是三兄弟里最娇气的。它的探头是玻璃电极需要保持湿润长期干放会报废它的信号是高阻抗的微弱电压走线太长或者靠近电源线就会被干扰。实际使用中我总结了三条校准频率至少两周一次用 4.00 和 6.86 两点校准探头和主控之间的距离尽量短实在要延长就用带屏蔽层的线读数要取多次中位数而不是平均因为干扰往往是脉冲式的平均值会被拉偏。2.3 供电、防水与走线翻车率最高的环节硬件本身出问题的概率远低于安装环节出问题的概率。我在这上面栽过好几次说几个关键的。供电方面ESP32 对电源质量比较敏感尤其是 Wi-Fi 发射瞬间会有几百毫安的电流尖峰。我一开始用了一个杂牌的 5V/1A 充电头结果就是设备每隔几小时重启一次日志里看不到任何错误纯粹是电压跌落导致的复位。换成 5V/2A 的优质电源之后就再没出现过。如果你要用长 USB 线线径也要够细线在大电流下的压降很可观。防水方面探头本身是防水的但接线处不是。DS18B20 的探头线接出去的那一小段热缩管时间长了会进水。我的做法是在接线处缠两到三层自粘硅胶带外面再套一层热缩管最后用扎带固定在鱼缸边框上让线有一段自然下垂的弧度水汽凝结后会顺着弧度滴下去而不是倒流进接头。走线方面最容易忽视的是强弱电分离。pH 探头的信号线如果和加热棒的电源线捆在一起走读数会明显漂移。我的做法是把传感器线统一走鱼缸背面左侧电源线走右侧中间隔开至少十厘米。摄像头的数据线也别和电源线并排尤其是 USB 线它对信号的干扰比你想的大。注意所有伸进鱼缸的线材和接头都要考虑到某天会掉进水里这个可能性。我的做法是在鱼缸外部的线路上留一个滴水弯也就是让线的最低点低于接头位置这样即使有水沿线下流也会在最低点滴落而不会进入接头。3. 采集端固件让 ESP32 稳定跑三个月不重启3.1 非阻塞采集循环的写法新手写 ESP32 固件最常见的写法是delay()一把梭读一个传感器睡一秒再读下一个。这种写法在只有一个传感器的时候没问题但一旦你要同时处理 Wi-Fi 重连、多传感器、本地缓存就会立刻崩掉——因为delay()期间整个程序是停住的网络断了它也不知道。我的写法是基于millis()的非阻塞轮询每个任务有自己的时间戳到点了才执行。核心结构大概是这样// 各任务的周期毫秒 const uint32_t T_TEMP 30000; // 温度 30 秒一次 const uint32_t T_TDS 60000; // TDS 60 秒一次 const uint32_t T_PH 300000; // pH 5 分钟一次 const uint32_t T_WIFI 10000; // 网络状态检查 10 秒一次 uint32_t lastTemp 0, lastTds 0, lastPh 0, lastWifi 0; void loop() { uint32_t now millis(); if (now - lastTemp T_TEMP) { lastTemp now; readTemperature(); // 内部不做任何 delay } if (now - lastTds T_TDS) { lastTds now; readTds(); } if (now - lastPh T_PH) { lastPh now; readPh(); } if (now - lastWifi T_WIFI) { lastWifi now; checkWifi(); } // 上传队列在后台滚动发送 pumpUploadQueue(); }这里的关键在于每个读取函数内部都不能有阻塞操作。DS18B20 的转换需要时间处理方式是发起转换后立刻返回下一次轮询时再取结果。pH 模块的模拟读取本身很快但因为要取多次中位数我会分成几个轮询周期完成而不是在一个周期里连读二十次。3.2 传感器读数滤波与异常值剔除周期怎么定这不是拍脑袋。水温变化的时间尺度是分钟到小时级别30 秒采一次已经完全够用。TDS 变化更慢60 秒一次合理。pH 探头响应本身就慢而且频繁读取会加速电极老化5 分钟一次是折中。摄像头那边是另外一套逻辑后面单独讲。滤波方面我在固件层做的是最轻量的一层滑动窗口取中位数。每个传感器维护一个长度为 5 的环形缓冲取中位数作为本次读数上报。选 5 是因为它在去掉单个离群点的同时延迟只有一个采样周期。如果你用平均值一次干扰产生的尖峰会污染接下来四五个读数。原始序列: 26.1 26.2 28.9 26.2 26.1 平均值: 26.70 (被拉高) 中位数: 26.2 (正确)异常值剔除是在服务端做的第二层。固件上报的数据里带一个时间戳服务端收到后会和上一个有效值比较如果变化幅度超过物理上可能的范围比如水温在 30 秒内变化超过 2 度就标记为可疑但不立即丢弃连续三次可疑才判定为异常段。这样做的好处是真实的水温骤变比如加热棒故障不会因为阈值设置而漏报。3.3 断网重连与本地缓存这部分是被低估的。ESP32 的 Wi-Fi 在长时间运行后掉线是常态不是异常。所以固件必须假设网络随时会断并且断网时数据不能丢。我的做法是在 ESP32 上开一块 SPIFFS 或者 LittleFS 分区用来做离线缓存。每次采集完成后数据先写入本地队列再由上传任务从队列头部取出、尝试发送、成功后才删除。队列用固定长度的环形缓冲实现容量大概能存 2000 条记录按上面说的采集频率算足够撑过一整天的断网。struct Reading { uint32_t ts; // 采集时间戳秒 float temp; float tds; float ph; float lux; uint8_t flags; // 位标记哪些字段有效 };flags这个字段很有用。如果某次采集时 pH 探头正好在自检读数是无效的这时把对应位置 0 上报服务端就不会把 0 当成真实 pH 值存进去。这个设计避免了我早期版本里出现过的一个经典 bug——pH 读数为 0 触发了严重偏酸的告警半夜把人吵醒。Wi-Fi 重连我用的是WiFi.onEvent()回调配合一个状态机而不是在 loop 里轮询WiFi.status()然后WiFi.reconnect()。原因是后者在信号弱的时候会产生大量无效重连请求反而拖慢恢复。正确的做法是检测到断开后先等待 5 秒再尝试重连连续失败后逐步退避到 30 秒一次。4. 数据落地SQLite 够不够用什么时候该换4.1 表结构设计与数据量估算先说结论家庭鱼缸场景下SQLite 完全够用而且大概率你一辈子都用不到它的性能上限。我把这个算给你看。按前面的采集频率温度 30 秒一条TDS 60 秒一条pH 5 分钟一条。为了简化存储我实际是统一成一个宽表按最短周期 30 秒写一条每条记录包含当前所有字段的最新值。这样一天是 2880 条一年是 105 万条。每条记录按 8 个字段、平均 8 字节算加上索引开销一年数据大约 30 到 50 MB。SQLite 单表处理千万级记录的查询毫无压力磁盘占用也完全在可接受范围内。所以那种必须上时序数据库否则撑不住的说法在这个量级上是不成立的。我用的是最简单的单表结构CREATE TABLE readings ( ts INTEGER PRIMARY KEY, -- Unix 时间戳秒 temp REAL, tds REAL, ph REAL, lux REAL, act REAL, -- 活跃度指数0~1 flags INTEGER DEFAULT 0 ); CREATE INDEX idx_ts ON readings(ts);ts直接做主键因为采集周期是固定的同一秒不会产生两条记录。这样天然实现了幂等——ESP32 断网重传时如果记录已经存在INSERT OR REPLACE会直接覆盖不会产生重复。4.2 写入接口与幂等处理服务端我用 Python 写的框架是 FastAPI理由是它的请求校验开箱即用对 ESP32 发过来的 JSON 做字段校验非常省事。核心接口就一个from fastapi import FastAPI from pydantic import BaseModel import sqlite3 app FastAPI() class Reading(BaseModel): ts: int temp: float | None None tds: float | None None ph: float | None None lux: float | None None act: float | None None flags: int 0 app.post(/ingest) def ingest(items: list[Reading]): conn sqlite3.connect(mirofish.db) cur conn.cursor() cur.executemany( INSERT OR REPLACE INTO readings (ts, temp, tds, ph, lux, act, flags) VALUES (?,?,?,?,?,?,?), [(i.ts, i.temp, i.tds, i.ph, i.lux, i.act, i.flags) for i in items] ) conn.commit() conn.close() return {ok: True, count: len(items)}接口设计成批量接收是一个关键决定。ESP32 从断网状态恢复后本地可能积压了几百上千条记录如果一条一条发每次都要建立连接、等待响应恢复过程会长得离谱而且在恢复期间新数据还在产生队列会越积越多。批量发送配合executemany一次事务提交几千条记录一两秒就写完了。INSERT OR REPLACE配合ts主键的组合让整个写入过程天然幂等。这一点在重传场景下非常重要——ESP32 发出去之后没收到响应它不知道服务端到底写没写成功只能重发。如果服务端用的是普通INSERT就会因为主键冲突报错或者产生重复数据。4.3 降采样与长期存储数据存下来之后有个现实问题你要看一年的温度曲线总不能把 105 万个点全画到一张图上。浏览器渲染不了人眼也看不出区别。我的做法是加一层降采样。原始数据保留完整的 30 秒粒度保留最近 90 天超过 90 天的数据按小时聚合取平均、最大、最小三个值后存入readings_hourly表原始表只保留每天的整点值。这样既能满足查最近三天的细节也能满足看一年的趋势。-- 每小时聚合由定时任务每天执行一次 INSERT OR REPLACE INTO readings_hourly SELECT (ts / 3600) * 3600 AS hour_ts, AVG(temp), MAX(temp), MIN(temp), AVG(ph), MAX(ph), MIN(ph), AVG(tds), AVG(act) FROM readings WHERE ts ? AND ts ? GROUP BY ts / 3600;MAX和MIN这两个聚合值不能省。温度曲线的平均值看起来很平稳但实际短时间内可能有过一次剧烈波动只看均值就完全发现不了。我在早期的版本里只存了平均值结果一次加热棒接触不良导致的两小时温度骤降在降采样后的曲线上几乎看不出来。提示降采样任务一定要设置成先写聚合表、再删原始数据的顺序并且两步之间要有事务保护。我有一次因为任务在中途被中断聚合没写完但原始数据已经删了丢了一整天的细粒度数据。5. 摄像头看鱼从有没有动到活跃度曲线5.1 摄像头选型与补光方案摄像头这部分我用的成本最低的方案树莓派 Zero 2 W 加官方摄像头模块总价两百出头。不用 USB 摄像头的原因是树莓派对 USB 摄像头的支持不如 CSI 接口稳定长时间运行容易掉设备。CSI 接口的摄像头模块在 OpenCV 里的读取非常稳定我连续跑过两个月没断过。摆放位置决定成败。摄像头要正对鱼缸长边镜头距离缸壁大概 30 到 50 厘米视野里要包含尽可能大的水体面积但要避开缸壁上的反光。我一开始把摄像头放在缸的斜上方结果画面里一半是屋顶的灯鱼反而很小。后来改成水平正对效果好很多。补光是必须解决的。鱼缸在晚上关灯后画面全黑什么都拍不到。我用了一圈红色 LED 补光理由有三个红光对鱼的干扰比白光小很多鱼类对红光不敏感红光在水中衰减比蓝光慢红色 LED 成本低。补光强度不能太高我用的是可调恒流源实际调到了大概 10 流明左右足以让画面有可用的对比度又不会让鱼产生应激反应。5.2 用帧差法算活跃度指数这里我要先泼一盆冷水在这个场景下上深度学习是过度设计。我见过不少类似项目一上来就 YOLO 检测鱼的位置、DeepSORT 做跟踪结果是树莓派跑不动或者跑得动但帧率低到 2 帧根本反映不了活动量。而且鱼缸里鱼的数量、大小、种类都是变化的预训练模型不一定适用。我用的是最朴素的帧差法。原理很简单把连续两帧转成灰度图做差分得到一张变化图然后统计变化幅度超过阈值的像素占整幅画面的比例。这个比例就是活跃度指数。import cv2 import numpy as np cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) BG_SUBTRACTOR cv2.createBackgroundSubtractorMOG2( history500, varThreshold25, detectShadowsFalse ) prev_gray None motion_buffer [] while True: ok, frame cap.read() if not ok: continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray cv2.GaussianBlur(gray, (7, 7), 0) # 只统计鱼缸内的区域排除画面边缘的干扰 roi gray[80:440, 60:580] if prev_gray is not None: diff cv2.absdiff(prev_gray, roi) _, mask cv2.threshold(diff, 18, 255, cv2.THRESH_BINARY) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) ratio np.count_nonzero(mask) / mask.size motion_buffer.append(ratio) prev_gray roi # 每 30 秒汇报一次中位数与采样周期对齐 if len(motion_buffer) 90: act float(np.median(motion_buffer)) motion_buffer.clear() report(act)几个参数值得说明。roi那个裁剪区域是我手动标定的把画面里鱼缸以外的部分墙壁、桌子、水族灯的支架全部排除掉否则一只飞过的小虫或者灯光闪烁都可能被算成鱼在动。18这个阈值是灰度差分的经验值太小会把水面波纹和噪点算进去太大则漏掉缓慢游动的鱼。np.median而不是np.mean同样是为了抗单帧干扰。5.3 白天模式与夜间红外模式的分离处理这里有个坑我踩了很久。白天的画面是彩色的晚上开了红光补光之后画面偏暗灰度分布的均值会明显偏低。如果白天和晚上用同一个差分阈值晚上的时候要么把噪点全算成运动要么把鱼的运动全过滤掉。我的处理方式是按补光状态切换两组参数。系统里有一个光照传感器就是前面提到的 BH1750当光照低于阈值时判定为夜间模式切换到另一组参数差分阈值从 18 降到 12形态学操作的结构元素从 3×3 放大到 5×5因为夜间噪点更碎同时把活跃度上报周期从 30 秒延长到 60 秒因为夜间鱼本来就动得少采样太密没有意义。BASE_LIGHT_THRESHOLD 8.0 # lux低于这个值判定为夜间 NIGHT_DIFF_THRESHOLD 12 DAY_DIFF_THRESHOLD 18还有一个隐蔽问题鱼缸玻璃的反光会随时间变化。白天窗外光线移动玻璃上会映出不同的影子这些影子在差分图里会被算成运动。这个问题的解决办法是在 ROI 里再挖掉几个固定的高反光区域也就是把那些区域涂黑像素值置为 0。这需要你对着画面手动标定一次虽然麻烦但标定完之后一劳永逸。活跃度指数的绝对值没有意义有意义的是它相对基线的偏离。我在服务端维护了一个7 天同一时段的中位数作为基线实际展示的时候给的是当前值相对基线的百分比。这样即使你换了个摄像头、换了摆放角度只要基线重新学习几天整套逻辑就还能用。6. 告警策略怎么让系统别一天到晚瞎叫6.1 连续触发与静默窗口告警系统最大的失败模式不是漏报而是误报太多导致你把它静音。我第一版的时候只要水温超过 28 度就发通知结果夏天连续两周每天发几十条最后我直接把通知权限关了。等到真正出事那天我什么都没收到。解决方式是引入两个机制连续触发和静默窗口。连续触发是指单次超阈值不算数必须连续 N 次采集都超阈值才触发。对于温度这种 30 秒采一次的指标我设成连续 4 次也就是两分钟足以滤掉大部分的瞬时抖动和探头噪声。对于 pH 这种 5 分钟采一次的指标设成连续 2 次也就是 10 分钟。这个 N 值的选择逻辑是告警的延迟必须显著小于异常造成损害的时间尺度。水温对鱼造成实质伤害大概需要几十分钟到几小时所以延迟两分钟完全没问题。ALARM_RULES { temp_high: {field: temp, op: , value: 30.0, need: 4}, temp_low: {field: temp, op: , value: 22.0, need: 4}, ph_low: {field: ph, op: , value: 6.2, need: 2}, ph_high: {field: ph, op: , value: 8.4, need: 2}, tds_high: {field: tds, op: , value: 600, need: 6}, } def evaluate(rows): fired [] for name, rule in ALARM_RULES.items(): recent [r for r in rows[-rule[need]:]] if len(recent) rule[need]: continue cond lambda v: ( v is not None and ( v rule[value] if rule[op] else v rule[value] ) ) if all(cond(r.get(rule[field])) for r in recent): fired.append(name) return fired静默窗口是指同一个告警在触发之后一段时间内不再重复发通知。我的设置是 30 分钟。也就是说如果水温持续偏高你会在开始的时候收到一条半小时后如果还没恢复再收到一条而不是每两分钟收一条。这个数字不能设得太长否则你会以为问题已经解决了。6.2 阈值怎么定从实测基线出发所有阈值都应该是从你自己的缸里跑出来的网上的推荐值只能当起点参考。我的做法是系统刚上线的头两周只记录、不告警收集足够的数据之后看一下每个指标的实际分布中位数是多少90 分位是多少最大值出现在什么时段。以我自己的缸为例水温的中位数是 25.6 度一天内的波动范围大概是 ±0.8 度。那我把低温阈值定在 22 度、高温阈值定在 30 度就留出了足够的安全余量不会因为正常的昼夜波动误报。这个余量的逻辑是**阈值应该卡在正常波动的边界之外但又在造成伤害之前。pH 的阈值设定要特别小心因为 pH 的绝对值和你的水质类型强相关。草缸因为通二氧化碳白天 pH 会偏低晚上会回升一天波动 0.5 都是正常的而三湖缸的 pH 本来就偏碱。所以 pH 告警阈值必须按缸来定同一个数值放在不同的缸里一个是正常一个是危险。活跃度告警我用的不是绝对阈值而是相对基线的跌幅。规则是连续 3 个小时的活跃度中位数低于同期基线的 60%触发行为异常提醒。这个告警的优先级我设得比水质告警低只推送到日志里不弹通知——因为它的误报率天然更高。6.3 通知渠道与消息格式通知渠道我试过几种最后采用的是本地服务推送加邮件备份的组合。本地的部分负责即时性邮件负责你不在场时的兜底。通知的内容要包含足够的信息让人一眼能判断严重程度。我的格式是这样的[MiroFish 告警] 水温偏高 当前值: 31.2 度 阈值: 30.0 度连续 4 次超限 触发时间: 14:32 最近一小时: 28.9 → 31.2 建议: 检查加热棒是否卡在开启状态必要时断电这里最近一小时那一行是我后来加的它直接告诉你趋势是快速上升还是缓慢爬升这个信息对判断紧急程度非常关键。快速上升一小时涨 2 度以上通常意味着设备故障缓慢爬升则可能是环境温度的影响。最后那句建议是硬编码的规则文案虽然简单但在半夜被吵醒的时候有一个明确的动作指引比让你自己去想强得多。7. 常见问题排查实录7.1 传感器漂移与校准节奏三个传感器里pH 的漂移速度是最快的也最需要规律校准。我的经验值是新探头前两周每周校准一次稳定之后每两到三周一次长期不用超过一个月之后再启用必须重新校准。校准用两点法就够4.00 和 6.86 两瓶标准缓冲液成本很低能用很久。校准的操作细节有几条容易出错。缓冲液从瓶子里倒出来之后用过一次就倒掉绝对不要倒回去因为探头上的水会污染整瓶。校准前探头要用纯水冲洗并轻轻吸干不要用纸擦擦会产生静电影响读数。探头插进缓冲液之后要等读数稳定通常需要 30 到 60 秒急着记录会带来很大误差。TDS 探头的漂移主要来自电极氧化。表现是读数缓慢上升而且上升趋势在清洗后会回落。判断方法是拿探头在纯净水里测一次理论上应该在 10 ppm 以下如果你的读数明显高于这个值就该清洗了。清洗用酒精棉轻轻擦拭两个金属针脚不要用砂纸砂纸会磨掉镀层。DS18B20 基本上不需要校准。它出厂时的精度对养鱼来说绰绰有余如果两根探头读数有差异优先怀疑是安装位置不同比如一根靠近加热棒而不是探头本身的问题。传感器漂移速度校准/维护周期常见症状DS18B20极慢基本不需校准多探头读数差异来自位置TDS中2-3 个月清洗读数持续偏高清洗后回落pH快2-3 周校准读数漂移、响应变慢7.2 数据断流的三层排查法数据断流是最高频的故障。我总结的排查顺序是先看设备有没有在跑再看网络通不通最后看服务端有没有在收。第一层看 ESP32 的板载 LED。我在固件里让 LED 按状态闪烁常亮表示正常慢闪1 秒一次表示 Wi-Fi 断开快闪200 毫秒一次表示本地缓存队列超过 80%。这样你不用连串口瞟一眼就知道大概是什么问题。第二层看路由器的设备列表确认 ESP32 拿到 IP 了。如果没拿到多半是 DHCP 池满了或者信号太弱。信号弱的话看 RSSI低于 -75 dBm 就应该考虑调整位置或者加个中继。第三层直接在服务端看数据库最新的ts是多少。如果最新记录是几小时前但设备状态正常那就是上传环节的问题。这时候检查服务端日志看是不是接口报错了。我遇到过最诡异的一次是设备正常、网络正常、接口也没报错但数据就是不进库。排查了两个小时才发现是磁盘满了SQLite 写入失败但异常被吞掉了。从那以后我在写入接口里加了显式的异常捕获和日志并且在磁盘使用率超过 90% 的时候发告警。这个坑很典型——存储层的故障往往最安静。7.3 问题速查表现象最可能的原因处理方式数据全断LED 快闪本地缓存积压上传失败检查服务端接口和网络数据全断LED 慢闪Wi-Fi 断开检查路由器和信号强度水温曲线有规律尖刺电磁干扰或电源噪声传感器线远离电源线pH 读数缓慢上升电极老化或污染重新校准必要时更换探头TDS 与实测值差很多未做温度补偿检查补偿公式里的温度项活跃度夜间飙高补光导致的噪点被误判切换夜间参数组告警频繁误报阈值太窄或未做连续判定放宽阈值增加 need 值服务端写入失败磁盘满或权限问题检查磁盘使用率8. 成本、扩展方向与我的几条经验8.1 总成本与性价比分析把账算一下。硬件一次性投入大概 450 元其中摄像头那部分树莓派加摄像头占了一半。如果你不需要行为观测只做环境采集成本能压到 200 元以内。运行成本基本为零——整套系统功耗不到 10 瓦一年电费算下来也就几十块。对比市面上的成品方案这个价格并不便宜。但差异在于可控性传感器坏了换传感器探头老化了换探头代码想改哪里改哪里数据完全在自己手里。而且在这个过程中你会真的搞清楚 TDS 和 pH 到底是怎么测出来的这种东西是买不来的。如果你只是想解决水温报警这一个问题那我建议别搞这么复杂一个 DS18B20 加一块 ESP32 加一个简单的阈值判断一百块以内搞定两小时能上线。MiroFish 这套东西的复杂度是奔着完整的鱼缸数据记录去的值不值得取决于你到底想解决什么问题。8.2 可以继续加的东西系统跑顺之后我列了一些想加但还没加的东西也分享给你作为参考。自动补水和换水记录。现在是纯手工记录我打算加一个流量计在换水管路上每次换水自动记下换水量和换水时间。这个数据和水质变化关联起来会很有意思能看出换水 30% 之后 TDS 多久恢复上升这类规律。喂食记录联动。喂食后鱼的活动量和氨氮都会变化如果能把喂食时间点标记到曲线上很多波动就解释得通了。最简单的实现是在服务端加一个手动打点接口配合手机上的快捷指令按一下就记录一次。多缸支持。现在的表结构是单缸的加一个tank_id字段就能扩展。这个改动对数据库层几乎没影响但要改的地方是告警规则——每口缸的阈值不一样规则表要按缸来配。数据导出。现在数据只在我自己的数据库里我想做一个导出功能把任意时间段的数据导成 CSV。这样换设备或者想用别的工具分析的时候不至于被绑死。这件事的重要性在于永远给你的数据留一条出逃路径。8.3 几条踩坑总结最后说几条我在这个项目里最深的体会都是花了时间才明白的。传感器的精度远不如安装方式和校准频率重要。我一开始总想着买更贵的传感器后来发现同一个探头换个位置、校准做勤一点效果提升比换探头明显得多。尤其是 pH探头本身再好一个月不校准照样给你飘到没法用。告警宁少勿多。这条我说过但我还想再说一遍。一个每次都能引起你重视的告警系统价值远高于一个每天发五十条然后被静音的告警系统。少设几条规则把每条规则都调到很少误报这是唯一能让它长期发挥作用的方式。先跑起来再优化。我这套东西一开始只有一个温度探头代码只有几十行。后面加的每一样东西都是因为前面那一样跑起来之后我发现了新的需求。如果一开始就画一个包含十几个模块的架构图我大概率到现在还在画图。数据要能被看到才有意义。存了几百万条记录但没有任何可视化那这些数据就等于不存在。我建议你在采集跑通的第一天就把图画出来哪怕只是最简单的静态 HTML看着那条曲线一天天变长你才有力气继续往下做。记录环境也记录你的操作。换水、喂食、加药、清洗滤材这些人为操作对水质的影响往往比设备故障更大。如果只记传感器数据不记自己的操作很多时候你会看着曲线上的突变一头雾水怎么都想不起来那天干了什么。我现在养成了一个习惯做完任何和鱼缸有关的操作都顺手在系统里打个点这个习惯带来的排查效率提升比多加三个传感器还大。
返回列表