基于树莓派与Arduino的智能浇水系统:从传感器到云端的物联网实践

1. 项目概述:从一盆枯萎的绿萝到WaterPi

几年前,我因为一次为期两周的出差,回家后面对的是一盆彻底枯萎、救无可救的绿萝。作为一个热爱折腾硬件的开发者,那种无力感促使我开始思考:能不能做一个系统,让我无论身在何处,都能像在家一样照料我的植物?这就是WaterPi项目的起点。它不仅仅是一个自动浇水装置,更是一个完整的远程监控与决策系统,核心目标是将种植经验数据化、操作远程化,让植物养护变得智能且省心。

简单来说,WaterPi是一个基于树莓派(Raspberry Pi)和微控制器(如Arduino或ESP32)的物联网(IoT)项目。它通过传感器实时采集土壤湿度、环境温湿度、光照强度等关键数据,经由树莓派处理后,一方面可以在本地进行逻辑判断(比如土壤太干就启动水泵),另一方面将所有数据上传到云端(例如AWS IoT),供你通过网页或手机App远程查看。你可以在办公室的电脑前,轻松查看家里龟背竹的“健康状况”,并手动点击按钮为它补充水分。

这个项目非常适合有一定动手能力的硬件爱好者、物联网初学者,或者单纯是受够了植物“杀手”称号,想用科技拯救绿植的朋友。它涉及的知识面较广,包括硬件电路搭建、传感器驱动、嵌入式编程、服务器端开发以及前端界面设计,是一个非常好的全栈实践项目。接下来,我将拆解整个系统的设计思路、硬件选型、软件实现以及那些只有实际动手才会遇到的“坑”。

2. 系统架构与核心组件选型

一个可靠的远程浇水监控系统,其架构设计决定了稳定性、可扩展性和成本。经过多次迭代,我最终确定了分层式架构,将感知、控制、计算和展示分离。

2.1 整体架构设计思路

系统主要分为三层: 感知控制层 边缘计算与网关层 云平台与应用层

感知控制层 是系统的“手”和“感觉器官”,部署在花盆旁边。它的核心是一个微控制器,负责驱动各类传感器和执行器。我选择了Arduino Uno作为核心,原因在于其生态成熟、引脚简单、编程容易,非常适合快速原型开发。这一层需要完成以下任务:

  1. 数据采集 :周期性地读取土壤湿度传感器、温湿度传感器(如DHT11/DHT22)、光照传感器的模拟或数字信号。
  2. 设备控制 :根据来自上层(树莓派)的指令或本地预设的简单逻辑,控制继电器模块来开关水泵、补光灯等设备。
  3. 初步处理与上报 :将采集到的原始数据进行简单的格式化,然后通过串口(UART)通信发送给树莓派。

边缘计算与网关层 是系统的“大脑”和“通讯员”,由树莓派担任。树莓派在这里扮演了多重角色:

  • 协议转换网关 :接收Arduino通过串口发送的简单数据,将其封装成符合MQTT等物联网协议的消息。
  • 边缘计算节点 :可以运行更复杂的判断逻辑。例如,可以设置一个规则:“如果连续3次检测到土壤湿度低于30%,且环境温度高于28度,则立即浇水5秒”。这种复杂规则放在树莓派上比放在Arduino上更易于管理和修改。
  • 本地数据缓存与转发 :将数据持久化到本地数据库(如SQLite),并在网络恢复后同步到云端,防止数据丢失。
  • 本地服务提供者 :可以运行一个轻量的Web服务器,提供局域网内的控制界面,即使外网断开,在家庭内部仍可操作。

云平台与应用层 是系统的“仪表盘”和“远程遥控器”。我选择了AWS IoT Core,因为它提供了完善的设备管理、消息路由和安全认证机制。树莓派作为“设备”连接到AWS IoT,将数据发布到指定的MQTT主题(Topic)。同时,在AWS上可以部署一个无服务器架构:

  • 使用AWS Lambda函数订阅MQTT主题,处理传入的数据,并将其存入Amazon DynamoDB或Timestream(时序数据库)。
  • 通过Amazon API Gateway提供RESTful API。
  • 前端可以是一个简单的React或Vue.js单页应用,托管在Amazon S3上,通过API获取数据并展示图表,同时发送控制指令(通过API Gateway触发Lambda,再通过IoT Core下发到设备)。

注意 :对于个人项目,云服务成本是需要考虑的因素。AWS IoT Core对于小流量应用有免费额度,通常足够使用。你也可以选择其他物联网平台,如阿里云物联网平台、腾讯云物联网开发平台,或者使用开源的MQTT Broker(如EMQX)自建服务器,但这需要你具备公网IP和一定的运维能力。

2.2 硬件组件深度解析

硬件是项目的基石,选对组件事半功倍。

  1. 主控单元对比:Arduino Uno vs ESP32

    • Arduino Uno :经典入门选择。ATmega328P芯片,5V工作电压,数字和模拟I/O引脚足够本项目使用。其最大优势是稳定性高、社区资源极其丰富,任何传感器几乎都有现成的库。缺点是本身不带网络功能,需依赖树莓派联网。
    • ESP32 :更现代的选择。双核处理器,主频更高,集成了Wi-Fi和蓝牙。这意味着你可以用一块ESP32直接连接Wi-Fi并上传数据到云端,省去树莓派,简化系统架构。但对于新手,其配置环境(Arduino IDE或PlatformIO)和网络编程会稍复杂一些。
    • 我的选择与理由 :在WaterPi的当前架构中,我依然推荐使用 Arduino Uno 。原因在于职责分离:让Arduino专心、稳定地处理实时性要求高的传感器读取和IO控制;让树莓派处理复杂的网络通信、逻辑判断和数据上行。这种分工降低了单个节点的复杂度,提高了系统整体的可靠性。ESP32更适合作为一体化的精简方案。
  2. 传感器选型与避坑指南

    • 土壤湿度传感器 :最常见的是电阻式传感器(如FC-28)。它通过两个探针间的电阻变化来检测湿度。 这里有一个大坑 :如果一直通电插入土中,探针会因电解作用而快速腐蚀,寿命可能只有一两个月。解决方案有两种:一是购买带有镀金探针的“电容式”土壤湿度传感器,它通过检测电容变化来工作,不与土壤直接发生电化学反应,寿命更长;二是即使使用电阻式,也仅在测量时通过数字引脚为其供电,测量完毕立即断电。
    • 温湿度传感器 :DHT11(低成本,精度一般)或DHT22(精度更高,范围更广)。它们使用单总线协议,只需要一个数据引脚。注意要正确连接上拉电阻,并且两次读取之间需要至少2秒的间隔。
    • 光照传感器 :BH1750是一个数字光照强度传感器,使用I2C接口,精度高,使用方便,直接输出lux值,比光敏电阻更稳定。
  3. 执行器与电源管理

    • 水泵与继电器 :小型潜水泵或直流隔膜泵(12V)即可。 绝对不要直接用Arduino的5V引脚驱动水泵 !Arduino的IO引脚只能提供约40mA电流,而水泵启动电流可能高达数百mA,会瞬间烧毁芯片。必须使用继电器模块或MOS管驱动模块进行隔离控制。继电器模块相当于一个电子开关,用Arduino的5V信号控制一个线圈,来接通或断开水泵的12V供电电路。
    • 电源 :这是系统稳定的关键。Arduino、传感器、继电器模块的驱动部分可以用一个5V/2A的手机充电器供电。水泵则需要独立的12V电源适配器。务必确保电源功率充足,特别是当水泵启动时,电压跌落可能导致树莓派或Arduino重启。

3. 下位机固件开发:让Arduino“活”起来

下位机(Arduino)的代码是整个系统的数据源头和控制终端,其稳定性和准确性至关重要。

3.1 传感器数据采集与滤波

直接读取的传感器数据往往带有噪声,需要进行软件滤波才能得到稳定值。

// 示例:读取土壤湿度并做滑动平均滤波
#define SOIL_SENSOR_PIN A0
#define POWER_PIN 7 // 控制传感器电源的引脚,用于防腐蚀

const int numReadings = 10; // 滤波窗口大小
int readings[numReadings];
int readIndex = 0;
int total = 0;
int average = 0;

void setup() {
  Serial.begin(9600);
  pinMode(POWER_PIN, OUTPUT);
  digitalWrite(POWER_PIN, LOW); // 初始断电
  for (int thisReading = 0; thisReading < numReadings; thisReading++) {
    readings[thisReading] = 0;
  }
}

int readSoilMoisture() {
  digitalWrite(POWER_PIN, HIGH); // 给传感器供电
  delay(20); // 等待传感器稳定,非常重要!
  int rawValue = analogRead(SOIL_SENSOR_PIN);
  digitalWrite(POWER_PIN, LOW); // 立即断电
  // 将模拟值(0-1023)转换为百分比(0-100%),注意:值越小越湿
  // 具体映射关系需要根据传感器在空气中和水中的读数进行校准
  int moisturePercent = map(rawValue, AIR_VALUE, WATER_VALUE, 0, 100);
  moisturePercent = constrain(moisturePercent, 0, 100);
  return moisturePercent;
}

void loop() {
  // 滑动平均滤波计算
  total = total - readings[readIndex]; // 减去最旧的读数
  int currentMoisture = readSoilMoisture();
  readings[readIndex] = currentMoisture;
  total = total + readings[readIndex]; // 加上最新的读数
  readIndex = readIndex + 1;
  if (readIndex >= numReadings) {
    readIndex = 0;
  }
  average = total / numReadings;

  // 将平均后的数据通过串口发送出去,格式化为JSON便于解析
  Serial.print("{\"soil_moisture\":");
  Serial.print(average);
  Serial.print(",\"temp\":");
  Serial.print(readDHTTemperature()); // 假设有读取DHT的函数
  Serial.print(",\"humidity\":");
  Serial.print(readDHTHumidity());
  Serial.println("}");

  delay(5000); // 每5秒采集一次
}

实操要点

  • 校准是关键 AIR_VALUE WATER_VALUE 需要实测。将传感器探针完全置于空气中,读取稳定的模拟值作为 AIR_VALUE (代表0%湿度);将探针插入水中(注意不要淹到电路部分),读取值作为 WATER_VALUE (代表100%湿度)。
  • 防腐蚀策略 :通过 POWER_PIN 控制传感器电源,仅在测量时通电,极大延长了电阻式传感器的寿命。
  • 通信协议 :使用JSON格式通过串口输出,方便树莓派端的Node.js程序用 JSON.parse() 直接解析,比自定义二进制协议或纯字符串分割更健壮。

3.2 串口通信协议与指令解析

树莓派不仅要接收数据,还需要向Arduino发送控制指令(如“浇水10秒”)。我们需要定义一个简单的双向通信协议。

// 在loop()函数中添加指令解析
void loop() {
  // ... 数据采集和发送代码 ...

  // 检查串口是否有指令传入
  if (Serial.available() > 0) {
    String command = Serial.readStringUntil('\n'); // 以换行符为命令结束符
    command.trim();
    if (command.startsWith("PUMP_ON:")) {
      int duration = command.substring(8).toInt(); // 解析出时长,如"PUMP_ON:10"
      controlPump(duration);
    } else if (command.equals("GET_STATUS")) {
      // 立即返回一次所有传感器状态
      reportImmediateStatus();
    }
  }
}

void controlPump(int seconds) {
  digitalWrite(PUMP_RELAY_PIN, HIGH); // 打开继电器,启动水泵
  delay(seconds * 1000); // 注意:这里使用delay会阻塞整个循环!
  digitalWrite(PUMP_RELAY_PIN, LOW);
  Serial.println("{\"event\":\"pump_activated\", \"duration\":" + String(seconds) + "}");
}

重要缺陷与改进 :上面的 controlPump 函数使用了 delay() ,在浇水期间整个Arduino会卡住,无法采集传感器数据或响应其他指令。这是 必须避免的 。正确的做法是使用非阻塞的定时方式,例如利用 millis() 函数。

unsigned long pumpStartTime = 0;
int pumpDuration = 0;
bool pumpRunning = false;

void controlPumpNonBlocking(int seconds) {
  pumpStartTime = millis();
  pumpDuration = seconds * 1000;
  digitalWrite(PUMP_RELAY_PIN, HIGH);
  pumpRunning = true;
  Serial.println("{\"event\":\"pump_started\", \"duration\":" + String(seconds) + "}");
}

void checkPumpStatus() {
  if (pumpRunning && (millis() - pumpStartTime >= pumpDuration)) {
    digitalWrite(PUMP_RELAY_PIN, LOW);
    pumpRunning = false;
    Serial.println("{\"event\":\"pump_stopped\"}");
  }
}

// 在loop()中,用checkPumpStatus()替换delay
void loop() {
  // ... 数据采集 ...
  checkPumpStatus(); // 非阻塞检查水泵是否该关闭
  // ... 指令解析 ...
}

4. 网关与云端桥梁:树莓派上的Node.js服务

树莓派运行Raspbian系统,我们使用Node.js来编写网关程序。选择Node.js是因为其异步非阻塞特性非常适合处理I/O密集型的网络通信和串口数据读取。

4.1 环境搭建与核心依赖

首先在树莓派上安装Node.js。建议使用NodeSource的仓库安装LTS版本。

# 安装Node.js
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs

# 验证安装
node --version
npm --version

创建一个项目目录并初始化,安装必要的npm包:

mkdir waterpi-gateway && cd waterpi-gateway
npm init -y
npm install serialport aws-iot-device-sdk-v2 dotenv
  • serialport : 用于与Arduino进行串口通信。
  • aws-iot-device-sdk-v2 : AWS IoT设备SDK,用于连接和认证AWS IoT Core。
  • dotenv : 管理环境变量,将敏感信息(如证书路径)从代码中分离。

4.2 串口数据读取与解析

编写一个 serialReader.js 模块来稳定地读取和解析Arduino发来的JSON数据。

const { SerialPort } = require('serialport');
const { ReadlineParser } = require('@serialport/parser-readline');

class SerialReader {
  constructor(portPath = '/dev/ttyACM0', baudRate = 9600) {
    this.port = new SerialPort({ path: portPath, baudRate: baudRate });
    const parser = this.port.pipe(new ReadlineParser({ delimiter: '\n' }));

    parser.on('data', (line) => {
      try {
        const data = JSON.parse(line.trim());
        console.log('Parsed sensor data:', data);
        // 触发一个事件,让主程序可以处理这些数据
        this.emit('data', data);
      } catch (error) {
        console.error('Failed to parse JSON from serial:', line, error);
        // 可能是数据不完整或乱码,可以选择忽略或记录
      }
    });

    this.port.on('error', (err) => {
      console.error('Serial port error:', err.message);
      this.emit('error', err);
    });

    this.port.on('open', () => {
      console.log(`Serial port ${portPath} opened.`);
    });
  }

  // 发送指令到Arduino
  sendCommand(command) {
    if (this.port.isOpen) {
      this.port.write(command + '\n');
      console.log(`Command sent: ${command}`);
    } else {
      console.error('Serial port is not open.');
    }
  }
}

// 使用EventEmitter让类可以触发事件
const EventEmitter = require('events');
Object.assign(SerialReader.prototype, EventEmitter.prototype);

module.exports = SerialReader;

注意事项

  • 串口设备路径 :树莓派上Arduino通常连接为 /dev/ttyACM0 /dev/ttyUSB0 。如果连接不上,可以使用 ls /dev/tty* 命令查看。
  • 权限问题 :普通用户可能无权访问串口设备。可以将用户加入 dialout 组: sudo usermod -a -G dialout $USER ,然后 需要重新登录 生效。
  • 错误处理 :串口通信易受干扰,必须做好 try...catch 和错误监听,防止程序因单次解析失败而崩溃。

4.3 连接AWS IoT Core并发布数据

在AWS IoT控制台创建设备“事物”(Thing),并下载其连接证书(设备证书、私钥)和根CA证书。将这些文件安全地存放在树莓派上。

创建一个 awsPublisher.js 模块:

const awsIot = require('aws-iot-device-sdk-v2');
const iot = awsIot.iot;
const mqtt = awsIot.mqtt;

class AwsIoTPublisher {
  constructor(config) {
    const clientConfig = {
      keyPath: config.keyPath, // 私钥文件路径
      certPath: config.certPath, // 设备证书文件路径
      caPath: config.caPath, // 根CA证书路径
      clientId: config.clientId, // 设备客户端ID,需唯一
      host: config.host, // AWS IoT端点,形如 xxxxxx-ats.iot.region.amazonaws.com
    };

    this.client = mqtt.ClientConnection = iot.MqttClient.newClient(clientConfig);
    this.topic = `waterpi/${config.clientId}/sensor/data`; // 发布数据的主题

    this.client.on('connect', () => {
      console.log('Connected to AWS IoT Core');
      this.connected = true;
    });

    this.client.on('error', (error) => {
      console.error('AWS IoT Connection error:', error);
      this.connected = false;
    });

    this.client.on('interrupt', (error) => {
      console.log('Connection interrupted:', error);
    });

    this.client.on('resume', (returnCode, sessionPresent) => {
      console.log('Connection resumed:', returnCode, sessionPresent);
    });
  }

  async connect() {
    try {
      const connection = await this.client.connect();
      console.log('MQTT connection established.');
      return connection;
    } catch (err) {
      console.error('Failed to connect:', err);
      throw err;
    }
  }

  publishData(data) {
    if (!this.connected) {
      console.warn('Not connected to AWS IoT, skipping publish.');
      return;
    }
    const payload = JSON.stringify({
      ...data,
      timestamp: new Date().toISOString(), // 添加服务器端时间戳
    });

    this.client.publish(this.topic, payload, mqtt.QoS.AtLeastOnce, (error) => {
      if (error) {
        console.error('Publish error:', error);
      } else {
        console.log('Data published to topic:', this.topic);
      }
    });
  }

  // 订阅控制主题,接收来自云端的指令
  subscribeToControlTopic(topic, callback) {
    this.client.subscribe(topic, mqtt.QoS.AtLeastOnce, (error, granted) => {
      if (error) {
        console.error('Subscribe error:', error);
      } else {
        console.log('Subscribed to topic:', granted[0].topic);
        this.client.on('message', (receivedTopic, payload) => {
          if (receivedTopic === topic) {
            const message = payload.toString();
            callback(message);
          }
        });
      }
    });
  }
}

module.exports = AwsIoTPublisher;

在主程序 index.js 中整合这两个模块:

require('dotenv').config();
const SerialReader = require('./serialReader');
const AwsIoTPublisher = require('./awsPublisher');

const serialReader = new SerialReader(process.env.SERIAL_PORT, 9600);
const publisher = new AwsIoTPublisher({
  keyPath: process.env.AWS_KEY_PATH,
  certPath: process.env.AWS_CERT_PATH,
  caPath: process.env.AWS_CA_PATH,
  clientId: process.env.AWS_CLIENT_ID,
  host: process.env.AWS_IOT_ENDPOINT,
});

(async () => {
  try {
    await publisher.connect();
    // 订阅控制主题,例如 waterpi/device001/control
    publisher.subscribeToControlTopic(`waterpi/${process.env.AWS_CLIENT_ID}/control`, (message) => {
      console.log('Control command received:', message);
      // 解析指令并转发给Arduino
      serialReader.sendCommand(message);
    });

    // 当串口收到数据时,发布到AWS IoT
    serialReader.on('data', (sensorData) => {
      publisher.publishData(sensorData);
    });

    serialReader.on('error', (err) => {
      console.error('Serial reader error, attempting to reconnect...', err);
      // 可以实现重连逻辑
    });

  } catch (error) {
    console.error('Initialization failed:', error);
    process.exit(1);
  }
})();

// 优雅退出
process.on('SIGINT', () => {
  console.log('Shutting down...');
  // 关闭串口和MQTT连接
  process.exit(0);
});

环境变量文件 .env 示例

SERIAL_PORT=/dev/ttyACM0
AWS_KEY_PATH=/home/pi/certs/private.pem.key
AWS_CERT_PATH=/home/pi/certs/certificate.pem.crt
AWS_CA_PATH=/home/pi/certs/AmazonRootCA1.pem
AWS_CLIENT_ID=waterpi_raspberry_01
AWS_IOT_ENDPOINT=xxxxxxxxxxxx-ats.iot.us-east-1.amazonaws.com

5. 云端逻辑与前端展示

数据上了云,我们就可以做更多事情:持久化存储、复杂分析、远程控制以及可视化。

5.1 AWS IoT规则引擎与数据流转

在AWS IoT控制台,我们可以创建一条规则(Rule),将设备发布到 sensor/data 主题的消息,自动转发到其他服务进行处理。

一个典型的规则动作可以是:

  1. 存入DynamoDB :用于记录最新的传感器状态,便于快速查询。
  2. 存入Timestream :这是一个专门为时序数据优化的数据库,非常适合存储带时间戳的传感器读数,用于绘制历史趋势图。
  3. 触发Lambda函数 :执行更复杂的业务逻辑。例如,检查土壤湿度是否低于阈值,如果是,则通过IoT Core向设备下发浇水指令,或者向你的手机发送一条SNS通知。

创建规则的SQL语句示例(在IoT控制台规则查询语句中):

SELECT
  state.reported.soil_moisture as moisture,
  state.reported.temp as temperature,
  state.reported.humidity as humidity,
  timestamp() as timestamp,
  clientid() as deviceId
FROM
  'waterpi/+/sensor/data'

这条规则会监听所有匹配 waterpi/+/sensor/data 主题的消息( + 是通配符),并提取出有用的字段。

5.2 构建简易监控Web界面

前端界面可以直接托管在AWS S3上,实现静态网站托管。使用Chart.js来绘制温湿度和土壤湿度的历史曲线,并添加一个手动浇水的按钮。

一个简化的HTML/JS示例 ( index.html ):

<!DOCTYPE html>
<html>
<head>
    <title>WaterPi Dashboard</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
    <h1>我的植物状态</h1>
    <div>
        <p>土壤湿度: <span id="moisture">--</span>%</p>
        <p>温度: <span id="temperature">--</span>°C</p>
        <p>湿度: <span id="humidity">--</span>%</p>
    </div>
    <div>
        <button onclick="waterPlant(5)">浇水5秒</button>
        <button onclick="waterPlant(10)">浇水10秒</button>
    </div>
    <canvas id="sensorChart" width="800" height="400"></canvas>

    <script>
        const apiBaseUrl = 'https://your-api-gateway-url.amazonaws.com/prod'; // 替换为你的API网关地址
        const deviceId = 'waterpi_raspberry_01';
        let sensorChart;

        async function fetchLatestData() {
            try {
                const response = await axios.get(`${apiBaseUrl}/sensor/latest?deviceId=${deviceId}`);
                const data = response.data;
                document.getElementById('moisture').textContent = data.moisture;
                document.getElementById('temperature').textContent = data.temperature;
                document.getElementById('humidity').textContent = data.humidity;
                updateChart(data); // 假设这个函数会更新图表
            } catch (error) {
                console.error('Failed to fetch data:', error);
            }
        }

        async function waterPlant(seconds) {
            try {
                await axios.post(`${apiBaseUrl}/control/water`, {
                    deviceId: deviceId,
                    duration: seconds
                });
                alert(`已发送浇水${seconds}秒指令`);
            } catch (error) {
                console.error('Failed to send command:', error);
                alert('指令发送失败');
            }
        }

        // 初始化图表和定时刷新
        function initChart() {
            const ctx = document.getElementById('sensorChart').getContext('2d');
            sensorChart = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: [], // 时间标签
                    datasets: [{
                        label: '土壤湿度 (%)',
                        data: [],
                        borderColor: 'rgb(75, 192, 192)',
                        tension: 0.1
                    }, {
                        label: '温度 (°C)',
                        data: [],
                        borderColor: 'rgb(255, 99, 132)',
                        tension: 0.1
                    }]
                },
                options: { responsive: true }
            });
        }

        function updateChart(newDataPoint) {
            // 简化逻辑:将新数据点添加到图表中
            const now = new Date().toLocaleTimeString();
            sensorChart.data.labels.push(now);
            sensorChart.data.datasets[0].data.push(newDataPoint.moisture);
            sensorChart.data.datasets[1].data.push(newDataPoint.temperature);
            // 只保留最近20个数据点
            if (sensorChart.data.labels.length > 20) {
                sensorChart.data.labels.shift();
                sensorChart.data.datasets.forEach(dataset => dataset.data.shift());
            }
            sensorChart.update();
        }

        window.onload = function() {
            initChart();
            fetchLatestData();
            setInterval(fetchLatestData, 10000); // 每10秒刷新一次数据
        };
    </script>
</body>
</html>

后端API可以通过API Gateway + Lambda来实现。Lambda函数从DynamoDB查询最新数据,或者向一个控制主题发布MQTT消息来触发浇水。

6. 系统部署、优化与故障排查

将代码写好只是第一步,让系统7x24小时稳定运行才是真正的挑战。

6.1 树莓派生产环境部署

不要让Node.js程序在前台运行,使用进程管理工具 pm2

# 全局安装pm2
sudo npm install -g pm2

# 在项目目录下,用pm2启动你的网关应用
pm2 start index.js --name waterpi-gateway

# 设置开机自启动
pm2 startup
# 执行pm2给出的命令,然后保存当前进程列表
pm2 save

系统服务化 :更进一步,可以创建一个systemd服务文件,管理得更规范。

6.2 电源与连接稳定性优化

  • 电源问题 :树莓派对电源非常敏感。使用官方电源或质量可靠的5V/3A电源适配器。供电不足会导致树莓派重启、网络断开或USB设备识别异常,这是许多奇怪问题的根源。
  • 看门狗与自动重启 :编写一个简单的“看门狗”脚本,定期检查网关进程和串口连接是否正常,如果异常则自动重启服务甚至重启树莓派。
  • 网络重连 :在Node.js的AWS IoT SDK中,已经内置了断线重连机制。但你需要确保树莓派的Wi-Fi连接稳定。可以考虑使用 connman dhcpcd 的配置来增强Wi-Fi稳定性,或者直接使用有线网络。

6.3 常见问题排查实录

  1. 问题 :串口读取不到数据,或者数据是乱码。

    • 排查
      • 检查串口设备路径是否正确 ( ls /dev/tty* )。
      • 检查串口波特率是否与Arduino代码中设置的 Serial.begin(9600) 一致。
      • 使用 sudo screen /dev/ttyACM0 9600 命令直接连接串口,看能否看到Arduino打印的原始数据。这是最直接的调试方法。
      • 检查接地(GND)。确保树莓派和Arduino的GND引脚连接在一起,共地是通信的基础。
  2. 问题 :AWS IoT连接失败,提示证书或策略错误。

    • 排查
      • 确认证书文件路径正确,且Node.js进程有读取权限。
      • 在AWS IoT控制台,检查该“事物”的证书是否已激活(Active)。
      • 检查该证书附加的策略(Policy)是否正确。一个最小化的策略示例如下:
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": "iot:Connect",
              "Resource": "arn:aws:iot:region:account:client/${iot:ClientId}"
            },
            {
              "Effect": "Allow",
              "Action": "iot:Publish",
              "Resource": "arn:aws:iot:region:account:topic/waterpi/${iot:ClientId}/*"
            },
            {
              "Effect": "Allow",
              "Action": "iot:Subscribe",
              "Resource": "arn:aws:iot:region:account:topicfilter/waterpi/${iot:ClientId}/*"
            },
            {
              "Effect": "Allow",
              "Action": "iot:Receive",
              "Resource": "arn:aws:iot:region:account:topic/waterpi/${iot:ClientId}/*"
            }
          ]
        }
        
      • 检查AWS IoT终端节点(Endpoint)是否正确,不同区域端点不同。
  3. 问题 :水泵不工作,但继电器有“咔嗒”声。

    • 排查
      • 最常见原因 :水泵电源功率不足或未接。用万用表测量水泵两端的电压,在继电器吸合时是否达到12V。
      • 检查继电器模块的接线:控制端(IN)是否接对了Arduino引脚;常开端(NO)和公共端(COM)是否串接在水泵的电源回路中。
      • 水泵本身是否损坏?可以直接将水泵接上12V电源测试。
  4. 问题 :土壤湿度读数不准,或者一直不变。

    • 排查
      • 校准!校准!校准! 这是最重要的一步。空气中的值和水中的值必须重新校准,特别是换了一种土或传感器后。
      • 检查传感器探针是否与土壤接触良好。过于松散的土壤可能导致接触电阻不稳定。
      • 如果使用防腐蚀方案(间歇供电),确保供电后等待了足够的时间( delay(20) )再读取,让传感器电路稳定。

这个项目从构思到稳定运行,我花了差不多两个月的时间,期间最大的收获不是代码或硬件本身,而是对“可靠性”的理解。硬件项目与纯软件不同,一个松动的接头、一个不稳定的电源,都可能让整个系统瘫痪。我的建议是,分模块测试,每连接一个传感器就写一段代码验证它,最后再整合。当你在手机上第一次看到家里植物真实的土壤湿度曲线时,那种成就感远超乎想象。现在,我的绿萝再也没枯过,而我的阳台,已经变成了一个小型的、自给自足的智能绿洲。你可以从控制一个花盆开始,然后扩展到整个阳台的灌溉系统,甚至加入摄像头做图像识别,判断植物是否生病。乐趣,才刚刚开始。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值