简介:这份资源是一套完整的基于树莓派的智能鱼缸设计与实践项目,面向物联网、嵌入式开发及DIY智能硬件爱好者。包内共15个文件,包含6个Python控制脚本(如温度、电机、氧气、水质模拟)、3个C语言底层驱动(motor.c、oxygen.c、quality.c)、MD说明文档、YAML配置及依赖清单等,覆盖数据采集、控制逻辑、远程通讯多个层次。资源体积仅21KB,轻量便于快速部署。项目围绕树莓派4B实现水温调节、水质监测、光照控制、氧气供给等完整功能,结合MQTT协议与Web界面可进行远程监控和报警,适合从硬件连接到软件联调逐步上手。已有153人学习,可作为课程设计、毕业设计或智能硬件实践的参考范本。
1. 把智能鱼缸的控制权交给树莓派之前,先想清楚这一层值不值得做
出差一周回来,鱼缸温度冲到 31 度,水面漂着一层油膜,喂食器卡住没转。这时候你需要的不是多一个 App,而是一台能同时管温度、水位、灯光、喂食和循环泵的常驻小主机。树莓派 4B 做智能鱼缸控制器,核心优势是 Linux 生态和 GPIO 直连,Python 脚本可以直接读写传感器,也能把数据统一定向交给 MQTT。这套方案适合有 Python 和基础电路知识的开发者,不想每次改逻辑都重新烧固件的那类人。下面从接线、采集代码到 Home Assistant 自动化,按一条可复制的链路展开。
2. 硬件选型与接线规划:树莓派 4B 的 GPIO 引脚功能图怎么分
2.1 先把鱼缸拆成感知、控制、通信三层
一个智能鱼缸的自动化闭环只有三件事:感知环境参数、按规则动作、把状态告诉人。
感知层。水温是必须测的,DS18B20 一个探头几块钱,1-Wire 单总线,一条线上可以挂多个探头,同时测缸内和室温都够用。水位用浮球开关就够,低于设定位置时输出一个电平变化,不需要测精确高度;想做得更精细可以换 HC-SR04 超声波,但鱼缸水雾大、玻璃反光,超声波受干扰明显,我一般只把它当备用方案。
控制层。喂食器用 SG90 舵机加旋转出料机构,角度控制在 0 到 180 度内;LED 灯带和循环泵用两路光耦隔离继电器,树莓派 GPIO 只输出控制信号,不直接驱动大电流。执行器里最容易被低估的是舵机供电,SG90 堵转电流能到 700mA 左右,树莓派 4B 的 5V 引脚顶不住,常见做法是外接 5V 2A 电源单独给舵机,树莓派用自家 USB-C 供电,两边只共地。
通信层。树莓派内部跑一个 MQTT broker,传感器数据发布到本地主题,Home Assistant 或者手机端订阅。传感器采集、动作执行、界面展示三者解耦,任何一环重启都不影响其他环,这是裸跑 Python 脚本做不到的。
2.2 按树莓派 4B 引脚功能图分配 GPIO
下面是树莓派 4B 引脚功能图里常用的一组分配,编号以 BCM 为准。
| 功能 | 模块 | BCM 引脚 | 物理引脚 | 说明 |
|---|---|---|---|---|
| 水温 | DS18B20 | GPIO4 | 7 | 1-Wire 数据线,需 4.7kΩ 上拉到 3.3V |
| 水位 | 浮球开关 | GPIO17 | 11 | 内部上拉,低电平触发 |
| 喂食舵机 | SG90 | GPIO18 | 12 | 硬件 PWM 通道,外接 5V |
| LED 灯带 | 继电器 1 | GPIO22 | 15 | 光耦隔离,低电平吸合 |
| 循环泵 | 继电器 2 | GPIO23 | 16 | 与 LED 分开控制 |
| 备用风扇 | 继电器 3 | GPIO24 | 18 | 高温强制通风 |
| 心跳指示 | 普通 LED | GPIO25 | 22 | 脚本运行状态灯 |
GPIO2 和 GPIO3 默认自带 I2C 上拉,留着以后接 PH 或 TDS 探头;GPIO14/15 被串口占用,接蓝牙模块时就不要动。分配原则是「断电重启后不能误开设备」:继电器触发逻辑统一设计成低电平有效,树莓派重启瞬间引脚浮空,继电器默认断开,鱼缸不会出现水泵空转或者灯带半夜亮起的情况。
注意:接线有硬约束,先断电再接传感器。上电后碰 1-Wire 数据线,一旦误触 5V 引脚,对应 GPIO 直接报废。
2.3 打开内核层的 1-Wire 总线开关
DS18B20 在 Raspberry Pi OS 里不需要额外驱动,打开 device tree overlay 即可。
# 打开 1-Wire 总线,GPIOPIN 必须和实际接线一致 echo "dtoverlay=w1-gpio,gpiopin=4" | sudo tee -a /boot/firmware/config.txt sudo reboot # 重启后确认设备目录存在 ls /sys/bus/w1/devices/ # 应看到一堆 28-xxxxxxxxxxxx 开头的目录,28 是 DS18B20 的家族码这里踩过的一个坑是系统版本差异。Bookworm 之后的 Raspberry Pi OS 把 /boot 改成只读挂载,老教程里直接sudo echo写 config.txt 的写法会失败,必须用sudo nano或tee。另外 BCM4 同时是 SPI0 的 CE1 引脚,如果还插着 SPI 屏幕,1-Wire 会和它抢引脚,这类冲突排查起来非常隐蔽。
电气接线上有三件事必须确认:所有入水模块与树莓派共地,否则电平判断全是错的;数据线上拉到 3.3V 而不是 5V,树莓派 GPIO 不是 5V tolerant;接线头套热缩管再灌硅胶,鱼缸蒸汽会在端子上凝结水珠,这是最常见的隐性短路来源。
3. 数据采集与执行链路的 Python 实现:温度、水位和舵机喂食器
3.1 DS18B20 温度读取:从 1-Wire 设备文件拿数据
树莓派安装 Python 的步骤在 Raspberry Pi OS 上天然自带,直接用系统 Python 3。1-Wire 设备在/sys/bus/w1/devices下以目录形式暴露,读w1_slave文件就能拿到原始温度:
import glob import time W1_DIR = "/sys/bus/w1/devices" def find_probes(): # 每个 DS18B20 对应一个 28- 开头的目录,缓存起来避免每次扫描 return sorted(glob.glob(W1_DIR + "/28-*")) def read_temp(probe): with open(probe + "/w1_slave", "r") as f: lines = f.read().splitlines() if "YES" not in lines[0]: return None # CRC 校验失败,本次读数直接丢弃 # 第二行形如 t=24562,单位是 0.001 摄氏度 raw = lines[1].split("t=")[1] return int(raw) / 1000.0 probes = find_probes() while True: for p in probes: t = read_temp(p) if t is not None: print(time.strftime("%H:%M:%S"), t) time.sleep(30)这段代码有三个要点。第一,w1_slave首行的YES是 CRC 校验结果,线缆过长或接头氧化时会出现NO,这时的温度值是错的,返回None比拿脏数据去触发告警好得多。第二,采样间隔不要低于 5 秒,DS18B20 单次转换周期就要 750ms,水温变化又慢,30 秒一轮完全够用。第三,设备目录句柄要缓存,每轮循环都 glob 一次/sys会在高频调用下白白浪费 CPU。
3.2 浮球水位检测:数字输入加软件去抖
浮球开关本质是一个干接点,接在 GPIO17 和 GND 之间。启用内部上拉后平时读到 HIGH,浮球下沉闭合时读到 LOW。水面晃动会让电平连续跳变,轮询方式加一个去抖窗口:
import RPi.GPIO as GPIO import time WATER_LOW = 17 GPIO.setmode(GPIO.BCM) GPIO.setup(WATER_LOW, GPIO.IN, pull_up_down=GPIO.PUD_UP) def water_low(): low_count = 0 # 连续采样 5 次,累计 4 次低电平才判定缺水 for _ in range(5): if GPIO.input(WATER_LOW) == GPIO.LOW: low_count += 1 time.sleep(0.1) return low_count >= 4参数含义:连续采样 5 次、每次间隔 100ms,合计 500ms 判定窗口,能滤掉大部分水面波动。把窗口拉长到 1 秒会更保险,但缺水告警本身不要求秒级响应,延迟 1 秒完全可接受。这里刻意不用GPIO.add_event_detect中断模式,浮球抖动会积压大量回调,软件去抖在低频信号上明显更省心。
3.3 舵机喂食器:PWM 占空比与防卡死时间窗
SG90 是模拟舵机,50Hz 频率下占空比 2.5% 对应 0 度,12.5% 对应 180 度。树莓派 4B 的 GPIO18 支持硬件 PWM,抖动比软件 PWM 小一个数量级:
import RPi.GPIO as GPIO import time SERVO = 18 def angle_to_duty(angle): # SG90 实际有效角度通常不到 180 度,先按线性映射 return 2.5 + (angle / 180.0) * 10.0 GPIO.setmode(GPIO.BCM) GPIO.setup(SERVO, GPIO.OUT) pwm = GPIO.PWM(SERVO, 50) # 50Hz 是模拟舵机标准频率 pwm.start(0) def feed(amount_deg=30): pwm.ChangeDutyCycle(angle_to_duty(amount_deg)) time.sleep(0.8) # 等舵机转到位 pwm.ChangeDutyCycle(angle_to_duty(0)) time.sleep(0.8) pwm.ChangeDutyCycle(0) # 信号归零,让舵机卸力参数有两个不能照抄文档。喂食角度必须手动试:先把舵机臂摘下来,用脚本逐个角度扫,找到「刚好倒出一份饲料」的位置再填进amount_deg,不同出料机构的机械结构差别很大。0.8 秒停留是给舵机完成转动的余量,转不到位就加到 1.2 秒。最后把占空比归零是有讲究的,模拟舵机持续收到 PWM 信号会一直保持扭矩,长期通电发热并加速齿轮磨损,归零等于让舵机卸力。
防堵转要靠输出侧逻辑而不是电路侧。在feed()前后记录时间戳,单次耗时超过 3 秒就判定卡料,连续 3 次卡料直接关停喂食并往 MQTT 的 warning 主题发一条告警。SG90 没有位置反馈,堵转时只会表现为电流升高和持续嘶鸣,时间越界是成本最低的检测手段。
4. MQTT 接入 Home Assistant:把鱼缸数据落成自动化规则
4.1 为什么在树莓派本地跑 mosquitto
直连云平台最大的问题就是断网即失联,而鱼缸恰恰是最需要本地独立性的设备。树莓派上装 mosquitto,broker 和采集脚本跑在同一台机器上,断外网时 Home Assistant 的自动化照常执行;外网恢复后再做数据补同步。MQTT 的 retain 消息还能把传感器最新值留在 broker 上,新订阅端一上来就能拿到上次状态,不用干等下一个发布周期,这对 HA 重启后的体验影响很大。
4.2 安装 mosquitto 并用密码文件限制访问
sudo apt install -y mosquitto mosquitto-clients # 创建密码文件并写入一个专供采集脚本的用户 sudo mosquitto_passwd -c /etc/mosquitto/passwd fish # 写入本地监听配置 cat <<EOF | sudo tee /etc/mosquitto/conf.d/fish.conf listener 1883 127.0.0.1 allow_anonymous false password_file /etc/mosquitto/passwd EOF sudo systemctl restart mosquitto # 验证发布与订阅链路 mosquitto_sub -h 127.0.0.1 -u fish -P 你的密码 -t "fish/#" -v mosquitto_pub -h 127.0.0.1 -u fish -P 你的密码 -t "fish/test" -m "ok"参数说明:listener 绑定127.0.0.1是安全底线,broker 只对本机开放;Home Assistant 如果跑在别的机器,再单独加一条显式绑定的 listener,不要直接0.0.0.0裸奔。allow_anonymous false把无凭据连接全部拒掉,采集脚本用fish这个账号,Home Assistant 的凭据单独再建一个,方便单独吊销。
注意:mosquitto 默认会读
/etc/mosquitto/mosquitto.conf,里面如果已有 listener 配置会和你自定义的 conf.d 冲突,启动失败先看journalctl -u mosquitto的输出。
4.3 Python 发布端:Topic 分层与 retain 策略
paho-mqtt 的用法收敛成一个发布函数:
import json import time import paho.mqtt.publish as publish MQTT_CONF = { "hostname": "127.0.0.1", "auth": {"username": "fish", "password": "你的密码"}, "qos": 1, } def publish_status(temp, water_low, pump_on): payload = json.dumps({ "temp": round(temp, 2), "water_low": water_low, "pump_on": pump_on, "ts": int(time.time()), }) # retain=True 让 broker 保留最后一次完整状态 publish.single("fish/aquarium/status", payload, retain=True, **MQTT_CONF)Topic 按fish/{缸体名}/{属性}分层,多缸部署时不冲突。qos 取 1 保证至少投递一次,鱼缸状态上报频率低,这点网络开销不值一提。retain 只放在status这类的「最后一刻状态」上;未来加高频温度遥测,那部分不要 retain,否则订阅端容易把旧数据误认为新数据。
4.4 Home Assistant 订阅与告警自动化配置
HA 装好 MQTT 集成后,用 YAML 定义传感器实体:
mqtt: sensor: - name: "鱼缸水温" state_topic: "fish/aquarium/status" value_template: "{{ value_json.temp }}" unit_of_measurement: "°C" device_class: temperature binary_sensor: - name: "鱼缸缺水" state_topic: "fish/aquarium/status" value_template: "{{ value_json.water_low }}" device_class: moisture自动化里写两条最典型的规则:
automation: - alias: "鱼缸高温开风扇" trigger: - platform: numeric_state entity_id: sensor.fish_tank_temp above: 29 action: - service: switch.turn_on target: entity_id: switch.fish_fan - alias: "鱼缸缺水推送" trigger: - platform: state entity_id: binary_sensor.fish_tank_water to: "on" action: - service: notify.mobile_app_phone data: message: "鱼缸水位过低,请检查补水"Topic 与消息语义对照如下,排错时先查对应层级:
| Topic | QoS | Retain | 用途 |
|---|---|---|---|
| fish/aquarium/status | 1 | true | 完整传感器 JSON,保留最新值 |
| fish/warning/feed | 1 | false | 喂食器卡料告警 |
| fish/cmd/light | 0 | false | HA 下发的灯光指令 |
| fish/telemetry/temp | 0 | false | 高频温度采样,仅记录用 |
HA 侧最典型的坑在value_template的类型转换。water_low从 JSON 取出是布尔 true,binary_sensor 的状态却要匹配 "on" 而不是 "true",字符串对不上,告警永远不触发。排查告警不生效,先看开发工具里的实体实时状态,再回头检查模板。
5. 鱼缸系统长期运行的坑位:传感器校准、舵机保护和断线自愈
5.1 水温校准与舵机物理防护
DS18B20 出厂精度是正负 0.5 度,但探头长期泡在含盐和藻类的水里,封装表面会结垢,读数会缓慢偏移。每个月拿一支标准水银温度计对一次,差值记进配置,读取时做常数补偿:
# config 里维护一个校准偏移字段,例如 +0.3 度 def read_temp_compensated(probe): t = read_temp(probe) return None if t is None else round(t + CAL_OFFSET, 2)舵机在软件时间判定之外,还需要一道物理防线。供电回路上串一个 1A 自恢复保险丝,堵转电流超标时自动断开,冷却后恢复。SG90 的塑料齿轮在反复堵转下扫齿是必然的,自恢复保险丝能把损坏频率从「每次堵转坏一个」降到「堵转好几次才换一个」,成本比换金属齿舵机低得多。
5.2 systemd 看护和 MQTT 遗嘱消息
采集脚本用 systemd 托管,崩溃自动拉起:
[Unit] Description=Fish Tank Daemon After=network-online.target mosquitto.service [Service] ExecStart=/usr/bin/python3 /opt/fishtank/main.py Restart=always RestartSec=10 WatchdogSec=60 [Install] WantedBy=multi-user.targetWatchdogSec=60配合脚本里周期性调用systemd-notify WATCHDOG=1,脚本卡死超过 60 秒会被系统强制杀掉再拉起。这个机制的定位是「进程级自愈」,解决的是内存泄漏和 IO 阻塞,解决不了断电。
断电解法是用 MQTT 自带的遗嘱消息 LWT。采集脚本启动建连时注册遗嘱:
import paho.mqtt.client as mqtt client = mqtt.Client() # 连接断开瞬间由 broker 代发 offline,retain 让订阅端保留状态 client.will_set("fish/aquarium/online", payload="offline", qos=1, retain=True) client.connect("127.0.0.1", 1883, 60) client.loop_start()正常退出时脚本显式发布online=false;异常掉电时 broker 检测到连接断开,会替客户端自动补发遗嘱消息。订阅端拿这条消息配合通知服务,就能做到掉电后第一分钟知道鱼缸失联,而不是出差回来才从水面上看出问题。
本文还有配套的精品资源,点击获取