1. 项目缘起:一个物联网小白的“求生”之路

大家好,我是老张,一个在物联网和硬件领域摸爬滚打了十来年的“老油条”。今天想和大家聊的,不是什么高深莫测的工业级方案,而是一个特别接地气、特别适合新手入门的项目——用Arduino和树莓派打造一个全自动的智能鱼缸。这个想法其实源于几年前我指导过的一个大学生毕设,当时那个学生的情况,我估计和现在看这篇文章的很多朋友很像:对物联网充满兴趣,但真动手时,面对一堆传感器、代码和莫名其妙的报错,头都大了。网上资料要么太零碎,要么太理论,缺的就是一份能“手把手”带着走完全程的指南。

所以,这篇文章我就打算扮演一次“保姆级”教练。咱们不空谈概念,就实实在在地,从零开始,把硬件怎么连、代码怎么写、数据怎么传、网页怎么显示,一步步全给捋清楚。你不需要有深厚的电子或编程功底,只要你有兴趣、有耐心,跟着我的步骤来,我敢保证,你不仅能顺利完成这个酷炫的智能鱼缸,更能把物联网项目从感知层到应用层的整个流程,扎扎实实地体验一遍。这可比单纯抄个代码交作业有意义多了,对吧?好了,闲话不多说,咱们卷起袖子,开始干活!

2. 开干之前:把整体思路和家当盘清楚

做任何项目,最怕的就是脑子一热直接开干,干到一半发现缺东少西,或者逻辑根本跑不通。咱们先花几分钟,把整个系统的“蓝图”和需要的“零件清单”搞清楚,这能帮你省下大量后期折腾的时间。

2.1 系统架构:一张图看懂数据流向

智能鱼缸的核心逻辑其实很简单,就是 “感知-决策-控制-展示” 一个闭环。咱们用一张简单的架构图来理解:

[鱼缸环境] --> (传感器感知) --> [Arduino采集] --(USB串口)--> [树莓派处理] --(网络)--> [Web服务器/数据库] --> [手机小程序查看]
      ^                                                                                        |
      |                                                                                        |
      +----------------------(继电器控制) <----------------[逻辑判断] <-------------------------+

具体来说,分四步走:

  1. 感知与采集(Arduino主场): Arduino像是一个兢兢业业的前线侦察兵。它负责连接并读取三个关键传感器:DS18B20防水温度传感器、水位传感器和用来模拟溶解氧传感器的电位器。同时,它还要根据我们设定的规则,控制四个继电器,分别管理加热棒、注水泵、抽水泵和充氧泵。
  2. 通信桥梁(USB串口): Arduino采集到的数据不能自己飞走,它通过最稳定简单的USB线,把数据源源不断地发送给“大脑”——树莓派。
  3. 处理与决策(树莓派主场): 树莓派在这里扮演指挥中心的角色。它接收Arduino发来的数据,一方面可以存入MySQL数据库留作记录,另一方面运行着核心控制逻辑(虽然本例逻辑主要在Arduino,但树莓派完全可以做更复杂的分析)。同时,它还用Python搭建了一个轻量的Web服务器(比如用Flask或Django),提供API接口。
  4. 展示与交互(云端与手机端): 最后,我们可以做一个微信小程序或者简单的网页。这个前端界面通过调用树莓派服务器提供的API,就能实时获取鱼缸的水温、水位等信息,并显示在手机上。你甚至可以增加手动控制按钮,远程开关设备。

2.2 硬件采购清单:照着买,不踩坑

工欲善其事,必先利其器。下面是我根据多年经验整理的必备和可选清单,兼顾了性价比和稳定性,你可以直接拿去抄作业。

类别名称说明参考价格(约)选购建议
核心控制器Arduino UNO R3 开发板主控,读取传感器、控制继电器。国产兼容板即可。30-50元选CH340芯片的,驱动安装方便。
树莓派3B/3B+/4B数据处理与服务器。3B足够,4B性能更强。200-400元需另配TF卡(16GB以上)、电源(5V/2.5A以上)。
传感器模块DS18B20 防水温度传感器测水温。务必买“防水封装”版。10-15元最好连同配套的4.7K上拉电阻模块一起买。
水位传感器(模拟款)检测水位高低。5-10元注意是模拟输出(AO引脚),非数字开关式。
10KΩ 电位器(旋钮式)用于模拟溶解氧传感器信号。1-2元三引脚,可调电阻。
执行器件5V 单路继电器模块控制大电流设备通断。需要4个。5元/个买“高电平触发”的,更常见易用。
USB接口 小型加热棒给水加热。20-40元功率不要太大(10W左右),注意安全。
USB接口 微型潜水泵抽水和注水,需要2个。15-25元/个注意扬程和流量,小型鱼缸够用即可。
USB接口 微型充氧泵给鱼缸增氧。15-25元带气管和气泡石。
连接与供电杜邦线(公对公、母对母)连接各模块,需要若干。10元/包建议买混合包,各种长度都备点。
面包板方便接线和测试,非必须但强烈推荐。10-20元中号即可,搭建原型时能避免反复焊接。
5V/2A 电源适配器给Arduino及执行器件独立供电。15-25元非常重要!树莓派USB口供电能力有限,必须外接。
Micro USB 数据线连接Arduino和树莓派进行通信。5-10元确保是数据线,而非仅充电线。
工具与其他焊锡、电烙铁、热缩管用于可靠连接水泵等设备的导线。50元内安全第一,接线务必牢固,避免短路。
万用表调试神器,检查通断、电压。50-100元非必须,但有它能极大提升排查效率。

几个踩坑提醒:

  • 供电是老大难:千万别指望树莓派通过USB线给Arduino以及一堆水泵、加热棒供电,绝对带不动!务必像我架构里强调的,给Arduino和执行器件准备一个独立的5V电源。树莓派自己用一个电源,Arduino和外设用另一个电源,但两者的GND(地线)一定要接在一起,这是通信和参考电平的基础。
  • USB设备改造:买USB接口的水泵、加热棒,是为了方便取电(5V)。你需要小心地剪开USB线,里面通常只有红(VCC)黑(GND)两根线。把这两根线分别接到继电器模块的“常开(NO)”端和电源GND上,就能通过继电器控制其开关了。
  • 传感器替代方案:原文章用电位器模拟溶解氧传感器,这是非常务实的做法,因为真传感器价格昂贵。如果你预算充足且想更真实,可以寻找一些廉价的模拟输出溶解氧传感器模块,但需要注意其输出特性和校准。

3. 动手实战一:让Arduino成为鱼缸的“感官与手脚”

这一部分是整个系统的基石,所有对物理世界的感知和控制都发生在这里。咱们的目标是让Arduino稳定地读取三个传感器的数据,并智能地控制四个继电器。

3.1 开发环境搭建与温度控制

首先,去Arduino官网下载并安装 Arduino IDE。这个软件是我们给Arduino写代码、上传程序的工具。安装好后,用USB线把Arduino连接到电脑。

第一步,搞定温度传感与加热。 我们使用DS18B20防水温度传感器。它的接线很简单,但有个关键点:它需要一颗4.7KΩ的上拉电阻接在数据线和VCC之间,以保证信号稳定。如果你买了配套的模块,模块上通常已经集成了这个电阻,直接使用VCC、GND、DATA三根线就行。

接线示意图(DS18B20模块为例):

  • DS18B20 VCC -> Arduino 5V
  • DS18B20 GND -> Arduino GND
  • DS18B20 DATA -> Arduino 数字引脚 2
  • 继电器模块1 IN -> Arduino 数字引脚 3
  • 继电器模块1 DC+ -> Arduino 5V (或外部电源5V)
  • 继电器模块1 DC- -> Arduino GND
  • 加热棒正极 -> 继电器1 NO (常开端)
  • 加热棒负极 -> 电源 GND (与Arduino共地)
  • 继电器1 COM -> 外部电源 5V

代码详解: 我们需要先安装一个库。在Arduino IDE中,点击「工具」->「管理库」,搜索“DallasTemperature”并安装,通常它会连带安装“OneWire”库。

#include <OneWire.h>
#include <DallasTemperature.h>

// 1. 定义引脚
#define TEMP_SENSOR_PIN 2 // 温度传感器数据线接引脚2
#define HEATER_RELAY_PIN 3 // 控制加热棒的继电器接引脚3

// 2. 初始化传感器对象
OneWire oneWire(TEMP_SENSOR_PIN);
DallasTemperature sensors(&oneWire);

float currentTemp; // 存储当前温度值

void setup() {
  Serial.begin(9600); // 启动串口通信,用于调试输出
  sensors.begin();    // 启动温度传感器
  pinMode(HEATER_RELAY_PIN, OUTPUT); // 设置继电器引脚为输出模式
  digitalWrite(HEATER_RELAY_PIN, LOW); // 初始状态关闭继电器(根据你的继电器是高/低电平触发调整)
  Serial.println("智能鱼缸系统启动 - 温度监控模块就绪");
}

void loop() {
  // 3. 请求并获取温度
  sensors.requestTemperatures(); // 发送读取命令
  currentTemp = sensors.getTempCByIndex(0); // 获取第一个传感器(索引0)的温度,单位摄氏度

  Serial.print("当前水温: ");
  Serial.print(currentTemp);
  Serial.println(" °C");

  // 4. 逻辑判断与控制
  if (currentTemp < 25.0) {
    digitalWrite(HEATER_RELAY_PIN, HIGH); // 温度低于25度,打开继电器(加热棒工作)
    Serial.println("状态:水温过低,加热棒已启动");
  } else {
    digitalWrite(HEATER_RELAY_PIN, LOW); // 温度高于等于25度,关闭继电器
    Serial.println("状态:水温正常,加热棒已关闭");
  }

  delay(2000); // 每2秒检测一次,避免频繁开关
}

上传代码后,打开IDE的「工具」->「串口监视器」,设置波特率为9600,你就能看到实时温度打印和加热棒的控制状态了。这里有个关键点:digitalWrite(HEATER_RELAY_PIN, HIGH); 这一行是让引脚输出高电平(5V)。如果你的继电器模块是“高电平触发”,那这就意味着“打开”。一定要确认你继电器的触发方式,如果是低电平触发,就需要把HIGH和LOW反过来写。

3.2 实现自动水位维持

水位传感器的工作原理是通过一组暴露的平行导线测量水的导电性来估算水位。但它的探针不能长期通电浸泡,否则会电解腐蚀。所以我们的策略是:仅在测量时给它短暂供电。

接线示意图:

  • 水位传感器 VCC -> Arduino 数字引脚 7 (注意,这里不接常电5V!)
  • 水位传感器 GND -> Arduino GND
  • 水位传感器 AO (模拟输出) -> Arduino 模拟引脚 A0
  • 继电器模块2 IN -> Arduino 数字引脚 4 (控制注水泵)
  • 继电器模块3 IN -> Arduino 数字引脚 5 (控制抽水泵)
  • 两个继电器的VCC和GND接好,COM端接外部电源5V,NO端分别接注水泵和抽水泵的正极。

代码逻辑解析: 我们定义一个函数 readWaterLevel(),在这个函数内部,先给水位传感器的VCC引脚(接数字7)一个高电平,短暂延时后读取A0的模拟值,然后立刻把VCC引脚拉低断电。这样既完成了测量,又保护了传感器。

#define WATER_SENSOR_POWER_PIN 7 // 水位传感器供电引脚
#define WATER_SENSOR_READ_PIN A0  // 水位传感器信号引脚
#define PUMP_IN_RELAY_PIN 4       // 注水泵(加水)继电器
#define PUMP_OUT_RELAY_PIN 5      // 抽水泵(排水)继电器

int waterLevelValue = 0; // 存储读取的原始模拟值

void setup() {
  Serial.begin(9600);
  pinMode(WATER_SENSOR_POWER_PIN, OUTPUT);
  digitalWrite(WATER_SENSOR_POWER_PIN, LOW); // 初始确保传感器断电
  pinMode(PUMP_IN_RELAY_PIN, OUTPUT);
  pinMode(PUMP_OUT_RELAY_PIN, OUTPUT);
  // 初始化关闭两个水泵
  digitalWrite(PUMP_IN_RELAY_PIN, LOW);
  digitalWrite(PUMP_OUT_RELAY_PIN, LOW);
}

// 专用的水位读取函数
int readWaterLevel() {
  digitalWrite(WATER_SENSOR_POWER_PIN, HIGH); // 给传感器供电
  delay(20); // 短暂稳定时间,可根据实际情况调整
  int value = analogRead(WATER_SENSOR_READ_PIN); // 读取模拟值 (0-1023)
  digitalWrite(WATER_SENSOR_POWER_PIN, LOW); // 立即断电
  return value;
}

void loop() {
  waterLevelValue = readWaterLevel(); // 获取水位值

  Serial.print("水位传感器原始值: ");
  Serial.println(waterLevelValue); // 这个值越小,表示水位越高(导电性越好)

  // 核心控制逻辑:根据实测值调整阈值
  if (waterLevelValue > 800) { // 值很大,说明探针露出多,水位很低
    Serial.println("状态:水位过低,启动注水泵");
    digitalWrite(PUMP_IN_RELAY_PIN, HIGH); // 打开注水泵
    digitalWrite(PUMP_OUT_RELAY_PIN, LOW);
  } else if (waterLevelValue < 200) { // 值很小,说明水快淹过探针,水位很高
    Serial.println("状态:水位过高,启动抽水泵");
    digitalWrite(PUMP_IN_RELAY_PIN, LOW);
    digitalWrite(PUMP_OUT_RELAY_PIN, HIGH); // 打开抽水泵
  } else { // 水位在正常区间
    Serial.println("状态:水位正常,水泵均关闭");
    digitalWrite(PUMP_IN_RELAY_PIN, LOW);
    digitalWrite(PUMP_OUT_RELAY_PIN, LOW);
  }

  delay(3000); // 每3秒检测一次水位,避免水泵频繁启停
}

阈值校准是关键! 代码里的 800 和 200 是我举例的阈值,你必须根据你的传感器安装高度和实际水质进行校准。把传感器固定在鱼缸理想水位的上下限位置,在串口监视器里观察对应的waterLevelValue,然后用这两个值替换代码中的阈值。

3.3 模拟溶解氧与增氧控制

真正的溶解氧传感器价格不菲,对于毕设和爱好者项目,用电位器模拟是一个聪明又低成本的选择。我们通过旋转电位器,改变输入Arduino的模拟电压(0-5V),映射为0-10 mg/L的溶解氧浓度。

接线示意图:

  • 电位器中间引脚(滑动端) -> Arduino 模拟引脚 A1
  • 电位器其余两引脚 -> Arduino 5V 和 GND (接法不同会影响旋转方向,任选一种即可)
  • 继电器模块4 IN -> Arduino 数字引脚 9
  • 继电器模块4接充氧泵,接法与加热棒相同。

代码实现:

#define OXYGEN_SIM_PIN A1        // 电位器模拟引脚
#define AIRPUMP_RELAY_PIN 9      // 充氧泵继电器引脚

void setup() {
  Serial.begin(9600);
  pinMode(AIRPUMP_RELAY_PIN, OUTPUT);
  digitalWrite(AIRPUMP_RELAY_PIN, LOW);
}

void loop() {
  int sensorValue = analogRead(OXYGEN_SIM_PIN); // 读取值 (0-1023)
  // 使用map函数将0-1023映射到0-10,模拟溶解氧浓度(mg/L)
  int oxygenLevel = map(sensorValue, 0, 1023, 0, 10);

  Serial.print("模拟溶解氧浓度: ");
  Serial.print(oxygenLevel);
  Serial.println(" mg/L");

  if (oxygenLevel < 5) { // 假设低于5mg/L需要增氧
    digitalWrite(AIRPUMP_RELAY_PIN, HIGH);
    Serial.println("状态:溶氧不足,充氧泵启动");
  } else {
    digitalWrite(AIRPUMP_RELAY_PIN, LOW);
    Serial.println("状态:溶氧充足,充氧泵关闭");
  }

  delay(2000); // 每2秒检测一次
}

在实际演示时,你可以通过旋转电位器来“模拟”溶解氧浓度的变化,看到继电器随之动作,非常直观。

3.4 代码整合与串口通信协议

现在,我们需要把温度、水位、溶解氧(模拟)三部分的代码整合到一个完整的Arduino程序中。同时,为了后续与树莓派通信,我们需要定义一个简单清晰的串口数据协议,而不是像调试时那样随意打印。

一个高效的协议是让Arduino定期(比如每3秒)向串口发送一行格式化的数据。树莓派收到后,很容易解析。我们采用 JSON格式,它结构清晰,易于扩展。

整合后的核心Loop函数与通信协议:

#include <OneWire.h>
#include <DallasTemperature.h>
#include <ArduinoJson.h> // 需要安装ArduinoJson库

// 引脚定义(整合所有)
#define TEMP_PIN 2
#define WATER_POWER_PIN 7
#define WATER_READ_PIN A0
#define OXYGEN_SIM_PIN A1
#define RELAY_HEATER 3
#define RELAY_PUMP_IN 4
#define RELAY_PUMP_OUT 5
#define RELAY_AIRPUMP 9

OneWire oneWire(TEMP_PIN);
DallasTemperature tempSensor(&oneWire);

void setup() {
  Serial.begin(115200); // 提高波特率,通信更流畅
  tempSensor.begin();
  // 初始化所有继电器引脚为输出,并关闭
  int relayPins[] = {RELAY_HEATER, RELAY_PUMP_IN, RELAY_PUMP_OUT, RELAY_AIRPUMP};
  for (int i = 0; i < 4; i++) {
    pinMode(relayPins[i], OUTPUT);
    digitalWrite(relayPins[i], LOW);
  }
  pinMode(WATER_POWER_PIN, OUTPUT);
  digitalWrite(WATER_POWER_PIN, LOW);
}

int readWaterLevel() {
  digitalWrite(WATER_POWER_PIN, HIGH);
  delay(20);
  int val = analogRead(WATER_READ_PIN);
  digitalWrite(WATER_POWER_PIN, LOW);
  return val;
}

void loop() {
  // 1. 读取所有传感器数据
  tempSensor.requestTemperatures();
  float temperature = tempSensor.getTempCByIndex(0);
  int waterLevelRaw = readWaterLevel();
  int oxygenSimValue = analogRead(OXYGEN_SIM_PIN);
  int oxygenLevel = map(oxygenSimValue, 0, 1023, 0, 10);

  // 2. 本地控制逻辑(与之前相同,此处简略)
  // 控制加热棒...
  // 控制水泵...
  // 控制充氧泵...

  // 3. 构建JSON字符串,准备发送给树莓派
  StaticJsonDocument<200> doc; // 创建一个JSON文档,预留200字节空间
  doc["temp"] = temperature;
  doc["water"] = waterLevelRaw;
  doc["oxygen"] = oxygenLevel;
  doc["heater"] = (digitalRead(RELAY_HEATER) == HIGH) ? "ON" : "OFF";
  doc["pump_in"] = (digitalRead(RELAY_PUMP_IN) == HIGH) ? "ON" : "OFF";
  doc["pump_out"] = (digitalRead(RELAY_PUMP_OUT) == HIGH) ? "ON" : "OFF";
  doc["airpump"] = (digitalRead(RELAY_AIRPUMP) == HIGH) ? "ON" : "OFF";

  // 4. 序列化JSON并打印到串口
  String output;
  serializeJson(doc, output);
  Serial.println(output); // 关键!树莓派将从这里读取数据

  delay(3000); // 每3秒发送一次数据包
}

安装 ArduinoJson 库后,上传这段代码。打开串口监视器,你会看到每隔3秒出现一行类似 {"temp":24.5,"water":450,"oxygen":6,"heater":"ON","pump_in":"OFF","pump_out":"OFF","airpump":"OFF"} 的数据。这就是我们和树莓派约定好的“语言”,结构清晰,包含了所有状态信息。

4. 动手实战二:树莓派搭建数据中枢与Web大脑

Arduino完成了前线的数据采集和基础控制,现在需要树莓派这个“大脑”来接收、处理、存储和展示数据。这部分会涉及到Linux操作、Python编程和一点网络知识,别怕,我们一步步来。

4.1 系统准备与Python环境配置

首先,确保你的树莓派已经安装了Raspberry Pi OS(原Raspbian)系统,并且能正常开机、联网。通过SSH远程登录或者直接接上显示器键盘操作都可以。

第一步,更新系统并安装必要的软件包: 打开终端,执行以下命令。这能确保你的系统是最新的,并且安装了后续需要的编译工具和Python包管理工具。

sudo apt update
sudo apt upgrade -y
sudo apt install python3-pip python3-venv git -y

第二步,为我们的项目创建一个独立的Python虚拟环境: 虚拟环境可以避免不同项目间的Python包版本冲突,是个好习惯。

mkdir ~/smart_aquarium
cd ~/smart_aquarium
python3 -m venv venv
source venv/bin/activate

当你看到命令行提示符前面出现 (venv),就说明已经激活了虚拟环境。

第三步,安装Python依赖库: 我们需要 pyserial 来读取Arduino的串口数据,用 Flask 来快速搭建Web服务器,用 pymysql 或 mysql-connector 来连接数据库。

pip install pyserial flask pymysql

4.2 串口通信:稳定读取Arduino数据

现在,把Arduino通过USB线连接到树莓派上。我们需要编写一个Python脚本,持续监听这个USB串口,解析我们刚才定义的JSON数据。

首先,确认Arduino在树莓派上对应的串口设备名。通常都是 /dev/ttyACM0 或 /dev/ttyUSB0。可以通过拔插USB线,使用 ls /dev/tty* 命令来观察哪个设备出现或消失。

创建并编写串口读取脚本 serial_reader.py:

#!/usr/bin/env python3
import serial
import json
import time
from datetime import datetime

# 配置串口参数,必须与Arduino代码中的Serial.begin()一致
SERIAL_PORT = '/dev/ttyACM0'  # 根据你的实际情况修改
BAUD_RATE = 115200

def main():
    try:
        # 打开串口
        ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=1)
        print(f"已连接到串口 {SERIAL_PORT},等待数据...")
        # 清空可能存在的旧数据缓冲区
        ser.reset_input_buffer()
        time.sleep(2)  # 等待Arduino重启

        while True:
            if ser.in_waiting > 0:
                # 读取一行数据(以换行符结尾)
                line = ser.readline().decode('utf-8').rstrip()
                try:
                    # 解析JSON数据
                    data = json.loads(line)
                    # 添加时间戳
                    data['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                    print(f"收到数据: {data}")

                    # 在这里,你可以将data写入数据库,或者传递给其他函数处理
                    # save_to_database(data)

                except json.JSONDecodeError as e:
                    print(f"JSON解析错误,原始数据: {line}, 错误: {e}")
                except UnicodeDecodeError as e:
                    print(f"字符解码错误: {e}")

    except serial.SerialException as e:
        print(f"无法打开串口 {SERIAL_PORT}: {e}")
    except KeyboardInterrupt:
        print("\n程序被用户中断")
        if 'ser' in locals():
            ser.close()

if __name__ == '__main__':
    main()

保存这个脚本,并在虚拟环境中运行它:python serial_reader.py。如果一切正常,你应该能看到树莓派终端开始规律地打印出从Arduino发来的、带时间戳的传感器数据。恭喜你,最关键的通信链路打通了!

4.3 数据持久化:使用MySQL存储历史记录

光在终端显示数据还不够,我们需要把数据存起来,方便后续查询和生成图表。我们在树莓派上安装MySQL数据库。

安装MySQL服务器:

sudo apt install mariadb-server -y # Raspberry Pi OS通常使用MariaDB,与MySQL兼容
sudo mysql_secure_installation # 运行安全配置脚本,建议设置root密码并移除测试用户等。

创建数据库和表: 登录MySQL:

sudo mysql -u root -p

执行SQL命令创建我们的数据库和表:

CREATE DATABASE smart_aquarium;
USE smart_aquarium;

CREATE TABLE sensor_data (
    id INT AUTO_INCREMENT PRIMARY KEY,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    temperature FLOAT,
    water_level INT,
    oxygen_level INT,
    heater_status VARCHAR(10),
    pump_in_status VARCHAR(10),
    pump_out_status VARCHAR(10),
    airpump_status VARCHAR(10)
);

exit 退出MySQL。

编写数据库操作模块 database.py:

import pymysql
import logging

# 数据库配置
DB_CONFIG = {
    'host': 'localhost',
    'user': 'root',          # 替换为你的用户名
    'password': 'your_password', # 替换为你的密码
    'database': 'smart_aquarium',
    'charset': 'utf8mb4'
}

def save_sensor_data(data):
    """将传感器数据存入数据库"""
    connection = None
    try:
        connection = pymysql.connect(**DB_CONFIG)
        with connection.cursor() as cursor:
            sql = """INSERT INTO sensor_data 
                     (timestamp, temperature, water_level, oxygen_level, 
                      heater_status, pump_in_status, pump_out_status, airpump_status) 
                     VALUES (%s, %s, %s, %s, %s, %s, %s, %s)"""
            cursor.execute(sql, (
                data.get('timestamp'),
                data.get('temp'),
                data.get('water'),
                data.get('oxygen'),
                data.get('heater'),
                data.get('pump_in'),
                data.get('pump_out'),
                data.get('airpump')
            ))
        connection.commit()
        logging.info("数据成功存入数据库")
    except pymysql.MySQLError as e:
        logging.error(f"数据库操作失败: {e}")
    finally:
        if connection:
            connection.close()

# 测试函数
if __name__ == '__main__':
    test_data = {
        'timestamp': '2023-10-27 14:30:00',
        'temp': 26.5,
        'water': 320,
        'oxygen': 7,
        'heater': 'OFF',
        'pump_in': 'OFF',
        'pump_out': 'ON',
        'airpump': 'OFF'
    }
    save_sensor_data(test_data)
    print("测试数据插入完成")

运行测试,确保数据库连接和插入操作正常。然后,修改之前的 serial_reader.py,在成功解析JSON后,取消注释 # save_to_database(data) 这行,并调用这个新的数据库函数,数据就会自动存入库中了。

4.4 构建Web API:用Flask提供数据接口

数据存好了,怎么给手机小程序或者网页用呢?我们需要一个API。Flask是一个轻量级的Python Web框架,非常适合做这个。

创建Flask应用 app.py:

from flask import Flask, jsonify, request, render_template
import pymysql
from datetime import datetime, timedelta

app = Flask(__name__)
DB_CONFIG = { ... } # 复用上面的数据库配置

def get_db_connection():
    return pymysql.connect(**DB_CONFIG)

@app.route('/')
def index():
    """提供一个简单的状态页面"""
    return """
    <h1>智能鱼缸监控系统</h1>
    <p>API接口:</p>
    <ul>
        <li><a href="/api/latest">/api/latest - 获取最新数据</a></li>
        <li><a href="/api/history?hours=24">/api/history?hours=24 - 获取最近24小时历史数据</a></li>
    </ul>
    """

@app.route('/api/latest', methods=['GET'])
def get_latest_data():
    """获取最新的一条传感器数据"""
    connection = get_db_connection()
    try:
        with connection.cursor(pymysql.cursors.DictCursor) as cursor:
            sql = "SELECT * FROM sensor_data ORDER BY timestamp DESC LIMIT 1"
            cursor.execute(sql)
            result = cursor.fetchone()
        return jsonify(result) if result else jsonify({'error': 'No data found'}), 404
    finally:
        connection.close()

@app.route('/api/history', methods=['GET'])
def get_history_data():
    """获取指定时间段的历史数据,例如 ?hours=6 获取最近6小时"""
    hours = request.args.get('hours', default=24, type=int)
    since_time = datetime.now() - timedelta(hours=hours)

    connection = get_db_connection()
    try:
        with connection.cursor(pymysql.cursors.DictCursor) as cursor:
            sql = "SELECT * FROM sensor_data WHERE timestamp > %s ORDER BY timestamp ASC"
            cursor.execute(sql, (since_time,))
            results = cursor.fetchall()
        return jsonify(results)
    finally:
        connection.close()

@app.route('/api/control', methods=['POST'])
def manual_control():
    """(扩展功能)接收手动控制指令,例如手动开关充氧泵"""
    # 注意:这需要额外的硬件支持(如树莓派GPIO控制继电器,或通过串口发送指令给Arduino)
    # 这里仅展示框架
    data = request.json
    device = data.get('device') # 'airpump', 'heater'
    action = data.get('action') # 'ON', 'OFF'
    # TODO: 实现控制逻辑,例如通过GPIO或串口发送指令
    return jsonify({'status': 'success', 'message': f'指令已接收: {device} -> {action}'})

if __name__ == '__main__':
    # 设置为0.0.0.0可以让同一网络下的其他设备访问
    app.run(host='0.0.0.0', port=5000, debug=True)

运行这个Flask应用:python app.py。然后在树莓派本身的浏览器里访问 http://127.0.0.1:5000,或者在同一局域网下的电脑/手机浏览器访问 http://[树莓派的IP地址]:5000,你应该能看到简单的页面。访问 http://[树莓派IP]:5000/api/latest 就能得到最新的JSON格式鱼缸数据了!

5. 系统集成、调试与优化心得

走到这一步,你的智能鱼缸系统已经具备了完整的数据流:传感器 -> Arduino -> 串口 -> 树莓派(Python脚本) -> 数据库 -> Web API。最后,我们把所有部分串联起来,并聊聊调试中可能遇到的坑。

5.1 让一切自动运行:使用Systemd服务

我们不能总是开着终端运行Python脚本。我们需要让 serial_reader.py 和 app.py 在树莓派开机后自动在后台运行。

为串口读取脚本创建系统服务:

  1. 创建服务文件:sudo nano /etc/systemd/system/aquarium-serial.service
  2. 写入以下内容(注意修改 User, WorkingDirectory 和 ExecStart 的路径为你自己的):
    [Unit]
    Description=Smart Aquarium Serial Reader
    After=multi-user.target
    
    [Service]
    Type=simple
    User=pi # 你的用户名
    WorkingDirectory=/home/pi/smart_aquarium
    ExecStart=/home/pi/smart_aquarium/venv/bin/python /home/pi/smart_aquarium/serial_reader.py
    Restart=on-failure
    RestartSec=10
    
    [Install]
    WantedBy=multi-user.target
    
  3. 保存退出,然后启用并启动服务:
    sudo systemctl daemon-reload
    sudo systemctl enable aquarium-serial.service
    sudo systemctl start aquarium-serial.service
    sudo systemctl status aquarium-serial.service # 检查状态
    

为Flask Web应用创建系统服务: 同理,创建另一个服务文件 aquarium-web.service,ExecStart 指向 app.py。这样,树莓派一开机,数据采集和Web服务就都准备好了。

5.2 常见问题与调试技巧

在整合过程中,你几乎一定会遇到一些问题。别慌,这是学习过程的一部分。

  1. 串口连接失败或数据乱码:

    • 检查端口:确认 serial_reader.py 中的 SERIAL_PORT 是否正确。可能是 /dev/ttyACM0 或 /dev/ttyUSB0。
    • 检查波特率:确保Arduino代码 Serial.begin(115200) 和Python代码 BAUD_RATE=115200 完全一致。
    • 权限问题:普通用户可能无权访问串口设备。将用户加入 dialout 组:sudo usermod -a -G dialout pi,然后重启。
    • USB线问题:确保使用的是数据线,而非仅能充电的线。
  2. 传感器读数不稳定或不准:

    • DS18B20:确保上拉电阻已接好(模块自带则无需担心)。尝试在 setup() 里增加 sensors.setResolution(12) 设置分辨率。
    • 水位传感器:阈值需要根据实际安装位置湿测校准。把传感器固定在理想水位高度,观察串口输出的原始值。水质(纯净水/自来水)也会影响导电性。
    • 供电干扰:电机(水泵、充氧泵)启停时会产生电压波动,可能干扰传感器。确保电源功率充足,线路连接牢固,传感器信号线远离电机电源线。
  3. 继电器状态混乱或设备不动作:

    • 触发电平:确认你的继电器模块是高电平触发还是低电平触发。代码中的 HIGH/LOW 要对应。
    • 共地:这是最容易被忽略的!Arduino、树莓派、外部电源的 GND(地线)必须连接在一起,否则控制信号无法形成回路。
    • 继电器负载:确认水泵、加热棒的电压(我们用的USB口都是5V)和电流在继电器模块的额定范围内(通常5V继电器模块能承受10A)。
  4. 数据库连接错误:

    • 检查 database.py 中的用户名、密码、数据库名是否正确。
    • 尝试在MySQL中直接用这些凭证登录测试。
    • 确保MySQL服务正在运行:sudo systemctl status mariadb。

5.3 项目扩展思路

基础系统跑通后,你可以玩出更多花样:

  • 增加可视化界面:用ECharts、Chart.js等库,为Flask应用制作一个漂亮的实时数据图表页面,显示温度、水位变化曲线。
  • 添加摄像头监控:使用树莓派官方摄像头或USB摄像头,通过 picamera 或 OpenCV 库定时抓拍鱼缸图片,并通过Flask提供图片流或最新快照。
  • 开发微信小程序:这是原作者的思路。小程序前端通过调用我们写好的 /api/latest 和 /api/control 接口,就能实现手机远程监控和控制。学习一下小程序开发基础,这是一个非常好的全栈实践。
  • 增加更多传感器:例如PH值传感器、TDS(总溶解固体)传感器,让鱼缸监控更全面。
  • 实现更智能的逻辑:将控制逻辑上移到树莓派。Arduino只负责原始数据采集和接收指令,树莓派分析历史数据,实现更复杂的控制策略,比如根据时间规律控制灯光。

这个项目最宝贵的不是最终那个能自动换水加热的鱼缸,而是你从硬件接线、代码调试、协议设计、服务部署到问题排查的完整经历。它把物联网的“感知、传输、处理、应用”四层架构实实在在地串了起来。我当年自己折腾的时候,光是一个串口数据丢包的问题就查了两天,但解决后的那种成就感,是无与伦比的。希望你在动手的过程中,也能享受到这种“从无到有”创造的乐趣,以及问题解决后豁然开朗的快感。如果遇到卡壳的地方,别犹豫,多查查资料,或者把错误信息贴到技术社区,很多时候,你离成功就差一次正确的搜索。祝你玩得开心,做出一个让你自豪的智能鱼缸!

更多推荐