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.
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
Servo Feeder (GPIO18)
VCC (Red) → 5V (Pin 2)
Signal (Orange) → GPIO18 (Pin 12)
GND (Brown) → GND (Pin 6)
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)
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
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 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.
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).
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.
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 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