Skip to main content

Automated Aquarium System

pH monitoring via ADS1115, scheduled feeding with servo, sunrise/sunset LED lighting, and Discord alerts.

Overview

Build a complete aquarium automation system with three subsystems: pH monitoring via an analog pH probe connected through an ADS1115 ADC, automated fish feeding using a servo motor on a schedule, and sunrise/sunset LED lighting controlled by PWM. Discord webhook alerts notify you when pH drifts outside the safe range for your fish.

Intermediate
Difficulty
6
Nodes Used
~25 min
Setup Time
I2C + PWM
Interfaces

What You'll Need

Hardware

  • * Raspberry Pi (any model with GPIO + I2C)
  • * ADS1115 16-bit ADC module (I2C)
  • * pH probe + BNC connector board (e.g., DFRobot SEN0161)
  • * SG90 or MG995 servo motor (fish feeder)
  • * 12V LED strip (white or RGB) with MOSFET driver
  • * IRLZ44N MOSFET (for LED PWM) + 10kΩ resistor
  • * 12V power supply for LED strip

Software / Accounts

  • * EdgeFlow installed on your Pi
  • * Discord server with a webhook URL
  • * I2C enabled on the Pi

Wiring Diagram

pH Probe via ADS1115

ADS1115 to Pi:

VCC → 3.3V (Pin 1)

GND → GND (Pin 9)

SDA → GPIO2 (Pin 3)

SCL → GPIO3 (Pin 5)

pH Board to ADS1115:

pH OUT → A0 (channel 0)

pH GND → GND

ADS1115 default address: 0x48 (ADDR pin to GND)

Servo Feeder (GPIO18)

VCC (Red) → 5V (Pin 2)

Signal (Orange) → GPIO18 (Pin 12)

GND (Brown) → GND (Pin 6)

GPIO18 supports hardware PWM. Use a separate 5V supply for larger servos.

LED Strip via MOSFET (GPIO12)

GPIO12 → 10kΩ → MOSFET Gate

MOSFET Source → GND

MOSFET Drain → LED Strip (-)

12V PSU (+) → LED Strip (+)

12V PSU (-) → Pi GND (shared)

GPIO12 supports hardware PWM. IRLZ44N is logic-level and works with 3.3V.

Importable Flow JSON

Copy this JSON and import it via Menu → Import in the EdgeFlow editor.

{
  "name": "Automated Aquarium System",
  "nodes": [
    {
      "id": "ads_ph",
      "type": "ads1x15",
      "name": "pH Probe (ADS1115)",
      "chip": "ADS1115",
      "address": "0x48",
      "channel": 0,
      "gain": 1,
      "interval": 10,
      "x": 120,
      "y": 140
    },
    {
      "id": "voltage_to_ph",
      "type": "function",
      "name": "Voltage to pH",
      "func": "// pH probe calibration: pH = 7 + (2.5 - voltage) / 0.18\n// Adjust offset and slope after calibrating with buffer solutions\nvar voltage = msg.payload.value;\nvar phValue = 7.0 + (2.5 - voltage) / 0.18;\nphValue = Math.round(phValue * 100) / 100;\nmsg.payload = {\n  ph: phValue,\n  voltage: Math.round(voltage * 1000) / 1000,\n  timestamp: Date.now()\n};\nmsg.topic = 'aquarium/ph';\nreturn msg;",
      "x": 340,
      "y": 140
    },
    {
      "id": "ph_check",
      "type": "switch",
      "name": "pH Safe Range?",
      "property": "payload.ph",
      "rules": [
        { "t": "lt", "v": 6.5 },
        { "t": "btwn", "v": 6.5, "v2": 7.5 },
        { "t": "gt", "v": 7.5 }
      ],
      "x": 560,
      "y": 140
    },
    {
      "id": "ph_alert_low",
      "type": "function",
      "name": "pH LOW Alert",
      "func": "msg.payload = {\n  content: '\u26a0\ufe0f **Aquarium pH Alert**\npH is too LOW: ' + msg.payload.ph + '\nTarget range: 6.5 - 7.5\nAction: Check CO2 injection or add buffer.'\n};\nreturn msg;",
      "x": 760,
      "y": 80
    },
    {
      "id": "ph_alert_high",
      "type": "function",
      "name": "pH HIGH Alert",
      "func": "msg.payload = {\n  content: '\u26a0\ufe0f **Aquarium pH Alert**\npH is too HIGH: ' + msg.payload.ph + '\nTarget range: 6.5 - 7.5\nAction: Check for ammonia or add driftwood.'\n};\nreturn msg;",
      "x": 760,
      "y": 200
    },
    {
      "id": "discord_alert",
      "type": "discord",
      "name": "Discord Webhook",
      "webhookUrl": "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN",
      "x": 980,
      "y": 140
    },
    {
      "id": "schedule_feed_am",
      "type": "schedule",
      "name": "Feed 8:00 AM",
      "cron": "0 8 * * *",
      "payload": "feed",
      "x": 120,
      "y": 320
    },
    {
      "id": "schedule_feed_pm",
      "type": "schedule",
      "name": "Feed 6:00 PM",
      "cron": "0 18 * * *",
      "payload": "feed",
      "x": 120,
      "y": 400
    },
    {
      "id": "servo_feed",
      "type": "function",
      "name": "Servo Feed Sequence",
      "func": "// Rotate servo to 90 degrees to dispense food\n// then return to 0 after 2 seconds\nvar open = { payload: 90 };\nvar close = { payload: 0 };\nsetTimeout(function() {\n  node.send(close);\n}, 2000);\nreturn open;",
      "x": 340,
      "y": 360
    },
    {
      "id": "pwm_servo",
      "type": "pwm",
      "name": "Feeder Servo",
      "pin": 18,
      "frequency": 50,
      "mode": "servo",
      "x": 560,
      "y": 360
    },
    {
      "id": "inject_light",
      "type": "inject",
      "name": "Every 1 Min",
      "interval": 60,
      "x": 120,
      "y": 520
    },
    {
      "id": "brightness_curve",
      "type": "function",
      "name": "Sunrise/Sunset Curve",
      "func": "// Simulate sunrise 7AM-9AM and sunset 7PM-9PM\n// Returns 0-100 brightness value\nvar now = new Date();\nvar hour = now.getHours() + now.getMinutes() / 60;\nvar brightness = 0;\n\nif (hour >= 7 && hour < 9) {\n  // Sunrise ramp: 0% at 7AM to 100% at 9AM\n  brightness = Math.round(((hour - 7) / 2) * 100);\n} else if (hour >= 9 && hour < 19) {\n  // Full daylight\n  brightness = 100;\n} else if (hour >= 19 && hour < 21) {\n  // Sunset ramp: 100% at 7PM to 0% at 9PM\n  brightness = Math.round(((21 - hour) / 2) * 100);\n} else {\n  // Night\n  brightness = 0;\n}\n\nmsg.payload = brightness;\nmsg.topic = 'aquarium/light';\nreturn msg;",
      "x": 340,
      "y": 520
    },
    {
      "id": "pwm_led",
      "type": "pwm",
      "name": "LED Strip",
      "pin": 12,
      "frequency": 1000,
      "mode": "duty",
      "range": 100,
      "x": 560,
      "y": 520
    },
    {
      "id": "debug_ph",
      "type": "debug",
      "name": "pH Monitor",
      "x": 560,
      "y": 60
    },
    {
      "id": "debug_light",
      "type": "debug",
      "name": "Light Monitor",
      "x": 560,
      "y": 600
    }
  ],
  "connections": [
    { "from": "ads_ph", "to": "voltage_to_ph" },
    { "from": "voltage_to_ph", "to": "ph_check" },
    { "from": "voltage_to_ph", "to": "debug_ph" },
    { "from": "ph_check", "to": "ph_alert_low", "fromPort": 0 },
    { "from": "ph_check", "to": "ph_alert_high", "fromPort": 2 },
    { "from": "ph_alert_low", "to": "discord_alert" },
    { "from": "ph_alert_high", "to": "discord_alert" },
    { "from": "schedule_feed_am", "to": "servo_feed" },
    { "from": "schedule_feed_pm", "to": "servo_feed" },
    { "from": "servo_feed", "to": "pwm_servo" },
    { "from": "inject_light", "to": "brightness_curve" },
    { "from": "brightness_curve", "to": "pwm_led" },
    { "from": "brightness_curve", "to": "debug_light" }
  ]
}

Step-by-Step Walkthrough

1

Wire the pH Probe via ADS1115

Connect the ADS1115 to the I2C bus (SDA on GPIO2, SCL on GPIO3). Connect the pH probe board output to channel A0 on the ADS1115. Verify the ADS1115 appears at address 0x48:

i2cdetect -y 1
# Should show "48" in the grid
2

Connect the Servo Feeder

Wire the servo signal to GPIO18 (hardware PWM capable). Use a separate 5V supply if the servo draws more than 500mA. Mount the servo on your fish feeder mechanism -- at 0° the opening is closed, at 90° it opens to dispense food.

3

Set Up LED PWM via MOSFET

Connect GPIO12 to the gate of an IRLZ44N MOSFET through a 10kΩ resistor. The LED strip positive goes to 12V, negative goes to the MOSFET drain. MOSFET source connects to ground (shared with Pi GND).

Always share ground between the Pi and the 12V LED power supply. Never connect 12V directly to the Pi.
4

Configure Discord Webhook

In your Discord server, go to Server Settings → Integrations → Webhooks → New Webhook. Copy the webhook URL and paste it into the "Discord Webhook" node configuration.

5

Calibrate the pH Probe

Submerge the pH probe in pH 7.0 buffer solution and note the voltage reading in the Debug panel. Then test with pH 4.0 buffer. Adjust the calibration formula in the "Voltage to pH" function node:

// Calibration formula:
// pH = 7.0 + (voltage_at_pH7 - measured_voltage) / slope
// slope = (voltage_at_pH7 - voltage_at_pH4) / 3.0
//
// Example: if pH7 = 2.50V and pH4 = 3.04V
// slope = (2.50 - 3.04) / 3.0 = -0.18
// pH = 7.0 + (2.50 - voltage) / 0.18
6

Deploy and Monitor

Click Deploy. The system will immediately start:

  • Reading pH every 10 seconds
  • Adjusting LED brightness every 60 seconds based on time of day
  • Waiting for 8:00 AM and 6:00 PM feeding schedules

Configuration Details

Parameter Value Description
ADS1115 Address 0x48 I2C address (ADDR pin to GND)
ADC Channel A0 (channel 0) pH probe analog signal input
ADC Gain 1x (±4.096V) Full-scale range for pH probe voltage
pH Read Interval 10 seconds Frequent polling for rapid pH change detection
Safe pH Range 6.5 - 7.5 Alert fires outside this range (adjust for your fish species)
Feeding Schedule 8:00 AM / 6:00 PM Servo dispenses food twice daily
Servo Open Angle 90° Opens feeder hatch for 2 seconds
Sunrise Period 7:00 - 9:00 AM LED ramps from 0% to 100% brightness
Sunset Period 7:00 - 9:00 PM LED ramps from 100% to 0% brightness

pH Voltage Conversion Formula

The pH probe outputs a voltage proportional to pH. The function node converts this using a linear equation calibrated with buffer solutions:

pH = 7.0 + (V_neutral - V_measured) / slope

Where:
  V_neutral  = voltage at pH 7.0 (typically ~2.5V)
  slope      = voltage change per pH unit (~0.18V/pH)
  V_measured = current ADS1115 reading

Example:
  V_measured = 2.14V
  pH = 7.0 + (2.5 - 2.14) / 0.18
  pH = 7.0 + 2.0 = 9.0  (too alkaline!)

LED Brightness Curve

The sunrise/sunset curve creates a natural lighting cycle for your fish:

Time   Brightness   Description
00:00  0%           Night (lights off)
07:00  0%           Sunrise begins
08:00  50%          Mid-sunrise ramp
09:00  100%         Full daylight
12:00  100%         Noon
19:00  100%         Sunset begins
20:00  50%          Mid-sunset ramp
21:00  0%           Night (lights off)

Expected Output

The pH monitoring subsystem outputs every 10 seconds:

{
  "payload": {
    "ph": 7.02,
    "voltage": 2.496,
    "timestamp": 1707750000000
  },
  "topic": "aquarium/ph",
  "_msgid": "b3c4d5e6"
}

When pH drops below 6.5, the Discord alert looks like:

{
  "content": "⚠️ **Aquarium pH Alert**\npH is too LOW: 6.32\nTarget range: 6.5 - 7.5\nAction: Check CO2 injection or add buffer."
}

The LED brightness controller outputs a duty-cycle value (0-100):

{
  "payload": 75,
  "topic": "aquarium/light",
  "_msgid": "c4d5e6f7"
}

Troubleshooting

pH readings are unstable or jumping
  • Add a 100nF capacitor between ADS1115 A0 and GND for noise filtering
  • Use shielded cable for the pH probe connection
  • Calibrate with fresh buffer solutions (pH 4.0 and pH 7.0)
  • Add averaging in the function node (keep a rolling window of 5 readings)
Servo doesn't move or jitters
  • Use a separate 5V power supply for the servo (Pi USB may not provide enough current)
  • Verify GPIO18 is set to PWM mode in the node config
  • Try different angle values (some servos have limited range)
  • Check servo frequency is 50 Hz for standard servos
LED strip flickers or doesn't light up
  • Verify the MOSFET gate is connected through the resistor (not directly)
  • Check that Pi GND and 12V supply GND are connected together
  • Test with a fixed brightness value (e.g., 50) before using the curve function
  • Ensure PWM frequency is 1000 Hz (not 50 Hz which is for servos)
Discord webhook not sending
  • Verify the webhook URL is complete (includes the token at the end)
  • Check the Pi has internet access
  • Discord webhook body must have a "content" field with a string value
  • Test the webhook manually with curl from the Pi terminal

Next Steps