Introduction: Arduino Aquaponics: JSON Pump Controller
When we started developing the Aquaponics Controller we realized we wanted a single unit that could handle multiple situations. Some flood-and-drain aquaponics systems toggle the pump on and off on non-matching intervals, say five minutes on and fifteen minutes off and some use a bell siphon and let the main pump run continuously.
This controller does both and throws in a third mode for manual operation. There are a couple of challenges to building an aquaponics controller like this with Arduino, where we want to sync the operation information (mode, run time, idle time, and pump state) with a web application and yet operate independently if it should fail to make the connection. First, due to the inherent limit the Ethernet Shield can make requests - a maximum of 5 to 10 seconds - and the frequency we need to check whether the pump relay should be toggled on or off - once every second, we realized we would need two different TimerAlarms. Similarly, the Ethernet request frequency meant we had to find a way to sync the operating parameters (mode, run time, etc.) in one request so the Arduino could move on to checking the pump.
Enter JSON, a web standard for passing key-value pairs. We used the aJson library to parse the response from the web application. A ChronoDot (real-time-clock) is used to keep the system time and track when the pump toggles on and off.
Parts
1 x Arduino Mega R3
1 x Arduino Ethernet Shield R3
1 x ChronoDot
1 x PowerSwitch Tail II
1 x Bi-Color (Red/Green) 5mm LED
Jumper wires
CAT-5e cable
Arduino Libraries
You'll need a few libraries:
RTClib is used by the ChronoDot.
aJson is used to parse the JSON.
Time and TimeAlarms
This project is taken directly from Automating Aquaponics with Arduino.
This controller does both and throws in a third mode for manual operation. There are a couple of challenges to building an aquaponics controller like this with Arduino, where we want to sync the operation information (mode, run time, idle time, and pump state) with a web application and yet operate independently if it should fail to make the connection. First, due to the inherent limit the Ethernet Shield can make requests - a maximum of 5 to 10 seconds - and the frequency we need to check whether the pump relay should be toggled on or off - once every second, we realized we would need two different TimerAlarms. Similarly, the Ethernet request frequency meant we had to find a way to sync the operating parameters (mode, run time, etc.) in one request so the Arduino could move on to checking the pump.
Enter JSON, a web standard for passing key-value pairs. We used the aJson library to parse the response from the web application. A ChronoDot (real-time-clock) is used to keep the system time and track when the pump toggles on and off.
Parts
1 x Arduino Mega R3
1 x Arduino Ethernet Shield R3
1 x ChronoDot
1 x PowerSwitch Tail II
1 x Bi-Color (Red/Green) 5mm LED
Jumper wires
CAT-5e cable
Arduino Libraries
You'll need a few libraries:
RTClib is used by the ChronoDot.
aJson is used to parse the JSON.
Time and TimeAlarms
This project is taken directly from Automating Aquaponics with Arduino.
Step 1: Wiring Diagram
The wiring is very straight forward. Obviously the RTC breakout board in the graphic is not the ChronoDot but as there isn't a ChronoDot in Fritzing we used this as the graphic - the wiring is the same. Also note the pin connections for the Arduino Mega to the RTC are not the same as they would be on the Uno.
The bi-color LED is used to indicate a successful connection to the web application. When the connection is made, pin 8 is set to HIGH and pin 9 to LOW, which makes the LED green. When the connection fails, the flow is reversed and the LED shines red.
The bi-color LED is used to indicate a successful connection to the web application. When the connection is made, pin 8 is set to HIGH and pin 9 to LOW, which makes the LED green. When the connection fails, the flow is reversed and the LED shines red.
Step 2: Arduino Sketch
The Arduino sketch creates two TimerAlarms. The first checks the relay on one second intervals and is used for Toggle Mode. The second alarm calls the sync function on 30 second intervals.
The parsing function looks for the end of the response, creates an aJson object and then gets the Mode. Depending on the Mode value, one of three things happens. The first mode is Always On, so the relay is turned on. If you use Toggle, the run and idle times are synced. Finally, Manual is accompanied by the status you want the pump in.
#include <SPI.h>
#include <Ethernet.h> // Ethernet Shield
#include <aJSON.h> // Parse JSON. Credit: Interactive Matter
#include <Wire.h>
#include "RTClib.h" // ChronoDot. Credit: Adafruit Industries
#include <Time.h>
#include <TimeAlarms.h>
// RTC - ChronoDot
RTC_DS1307 RTC;
DateTime future;
unsigned long toggle;
unsigned long current;
// Ethernet Shield
byte mac[] = { 0x90, 0xA2, 0xD0, 0x0D, 0xA0, 0x00 };
//byte myIP[] = { 192, 168, 1, 15 }; // Uncomment here and line 52 if needed.
//byte gateway[] = { 192, 168, 1, 1 };
char server[] = "http://www.myapsystem.appspot.com"; // Change this to match the application identifier you setup in Step 3
EthernetClient client;
// Makes it easy to change app names
String webapp = "http://www.myapsystem.appspot.com/"; // Change this to match the application identifier you setup in Step 3
// Pins
int connectedON = 8;
int connectedOFF = 9;
int pumpRelay = 23;
// PumpRelay values
String current_mode = "Manual"; // The default mode
String pumpState = "off"; // The default state
int pumpRunTime = 20; // Run for 20 seconds
int pumpIdleTime = 40; // Sit idle for 40 seconds
void setup() {
// Turn relay off first
digitalWrite(pumpRelay, HIGH);
// Disable SD card if one in the slot
pinMode(4,OUTPUT);
digitalWrite(4,HIGH);
// Start Serial
Serial.begin(9600);
delay(1000);
// Start Ethernet
Ethernet.begin(mac);
//Ethernet.begin(mac, myIP);
//Ethernet.begin(mac, myIP, gateway);
delay(1000);
// Start RTC
Wire.begin();
RTC.begin();
if (! RTC.isrunning()) {
Serial.println("RTC is NOT running");
}
DateTime now = RTC.now();
DateTime compiled = DateTime(__DATE__, __TIME__);
if (now.unixtime() < compiled.unixtime()) {
Serial.println("RTC is older than compile time! Updating");
RTC.adjust(DateTime(__DATE__, __TIME__));
}
// Set pin modes
pinMode(connectedON, OUTPUT);
pinMode(connectedOFF, OUTPUT);
pinMode(pumpRelay, OUTPUT);
// Set alarms
Alarm.timerRepeat(30, sync);
Alarm.timerRepeat(1, checkRelay);
sync();
Serial.println("Setup Complete");
}
void loop() {
Alarm.delay(1000);
}
void sync() {
Serial.println("Syncing");
GAE(webapp + "adacs/sync?State=" + pumpState);
Serial.println();
}
void checkRelay() {
DateTime now = RTC.now();
current = now.unixtime();
if (current_mode == "Toggle") {
if (current < toggle) {
} else {
// Determine which state to toggle to
if (pumpState == "off") {
// The pump was off. Turn on and set next toggle time based on run time
digitalWrite(pumpRelay, LOW);
Serial.println("Turning pump on");
pumpState = "on";
// Set next toggle time
future = now.unixtime() + pumpRunTime;
toggle = now.unixtime() + pumpRunTime;
} else if (pumpState == "on") {
// The pump was on. Turn off and set next toggle time based on idle time
digitalWrite(pumpRelay, HIGH);
Serial.println("Turning pump off");
pumpState = "off";
// Set next toggle time
future = now.unixtime() + pumpIdleTime;
toggle = now.unixtime() + pumpIdleTime;
}
displayTime();
}
}
}
void displayTime() {
DateTime now = RTC.now();
current = now.unixtime();
// Display current time
Serial.print("Current time: ");
Serial.print(now.year(), DEC);
Serial.print('/');
Serial.print(now.month(), DEC);
Serial.print('/');
Serial.print(now.day(), DEC);
Serial.print(' ');
Serial.print(now.hour(), DEC);
Serial.print(':');
Serial.print(now.minute(), DEC);
Serial.print(':');
Serial.print(now.second(), DEC);
Serial.println();
// Display next toggle time
Serial.print("Future time: ");
Serial.print(future.year(), DEC);
Serial.print('/');
Serial.print(future.month(), DEC);
Serial.print('/');
Serial.print(future.day(), DEC);
Serial.print(' ');
Serial.print(future.hour(), DEC);
Serial.print(':');
Serial.print(future.minute(), DEC);
Serial.print(':');
Serial.print(future.second(), DEC);
Serial.println();
}
void GAE(String link) {
boolean success = httpRequest(link);
if (success == true) {
delay(5000);
boolean currentLineIsBlank = true;
String readString = "";
char newString[100];
while (client.connected()) {
if (client.available()) {
char c = client.read();
//Serial.write(c); // Dev mode
if (c == '\n' && currentLineIsBlank) {
while(client.connected()) {
char f = client.read();
readString += f;
}
}
if (c == '\n') {
currentLineIsBlank = true;
} else if (c != '\r') {
currentLineIsBlank = false;
}
}
}
client.stop();
readString.toCharArray(newString, 100);
// The full JSON object
aJsonObject* jsonObject = aJson.parse(newString);
// Get the mode
aJsonObject* mode = aJson.getObjectItem(jsonObject, "Mode");
current_mode = mode->valuestring; // Convert mode to string and assign to current_mode
Serial.println("Mode: " + current_mode);
// Mode conditional
if (current_mode == "Always On") {
// Turn the pump on
digitalWrite(pumpRelay, LOW);
// Update state
pumpState = "on";
} else if (current_mode == "Toggle") {
// Sync the run time
aJsonObject* rt = aJson.getObjectItem(jsonObject, "RunTime");
String rString = rt->valuestring;
pumpRunTime = rString.toInt();
// Sync the idle time
aJsonObject* it = aJson.getObjectItem(jsonObject, "IdleTime");
String iString = it->valuestring;
pumpIdleTime = iString.toInt();
// Optional output
// Serial.print("Run Time: ");
// Serial.print(pumpRunTime);
// Serial.println();
//
// Serial.print("Idle Time: ");
// Serial.print(pumpIdleTime);
// Serial.println();
} else if (current_mode == "Manual") {
// Sync the state
aJsonObject* st = aJson.getObjectItem(jsonObject, "Status");
pumpState = st->valuestring;
if (pumpState == "on") {
digitalWrite(pumpRelay, LOW);
} else if (pumpState == "off") {
digitalWrite(pumpRelay, HIGH);
} else {
Serial.println("Unknown pump state given in Manual mode.");
}
} else {
Serial.println("Uknown mode detected during sync.");
}
} else {
Serial.println("Not connected.");
}
// Delete the root object
aJson.deleteItem(jsonObject);
}
boolean httpRequest(String link) {
// If there is a successful connection
if (client.connect(server, 80)) {
client.println("GET " + link + " HTTP/1.0");
client.println();
// Turn connected LED on
digitalWrite(connectedOFF, LOW);
digitalWrite(connectedON, HIGH);
return true;
} else {
// You couldn't make the connection
Serial.println("Connection Failed");
//errors += 1;
client.stop();
// Turn connected LED on
digitalWrite(connectedON, LOW);
digitalWrite(connectedOFF, HIGH);
return false;
}
}
The parsing function looks for the end of the response, creates an aJson object and then gets the Mode. Depending on the Mode value, one of three things happens. The first mode is Always On, so the relay is turned on. If you use Toggle, the run and idle times are synced. Finally, Manual is accompanied by the status you want the pump in.
#include <SPI.h>
#include <Ethernet.h> // Ethernet Shield
#include <aJSON.h> // Parse JSON. Credit: Interactive Matter
#include <Wire.h>
#include "RTClib.h" // ChronoDot. Credit: Adafruit Industries
#include <Time.h>
#include <TimeAlarms.h>
// RTC - ChronoDot
RTC_DS1307 RTC;
DateTime future;
unsigned long toggle;
unsigned long current;
// Ethernet Shield
byte mac[] = { 0x90, 0xA2, 0xD0, 0x0D, 0xA0, 0x00 };
//byte myIP[] = { 192, 168, 1, 15 }; // Uncomment here and line 52 if needed.
//byte gateway[] = { 192, 168, 1, 1 };
char server[] = "http://www.myapsystem.appspot.com"; // Change this to match the application identifier you setup in Step 3
EthernetClient client;
// Makes it easy to change app names
String webapp = "http://www.myapsystem.appspot.com/"; // Change this to match the application identifier you setup in Step 3
// Pins
int connectedON = 8;
int connectedOFF = 9;
int pumpRelay = 23;
// PumpRelay values
String current_mode = "Manual"; // The default mode
String pumpState = "off"; // The default state
int pumpRunTime = 20; // Run for 20 seconds
int pumpIdleTime = 40; // Sit idle for 40 seconds
void setup() {
// Turn relay off first
digitalWrite(pumpRelay, HIGH);
// Disable SD card if one in the slot
pinMode(4,OUTPUT);
digitalWrite(4,HIGH);
// Start Serial
Serial.begin(9600);
delay(1000);
// Start Ethernet
Ethernet.begin(mac);
//Ethernet.begin(mac, myIP);
//Ethernet.begin(mac, myIP, gateway);
delay(1000);
// Start RTC
Wire.begin();
RTC.begin();
if (! RTC.isrunning()) {
Serial.println("RTC is NOT running");
}
DateTime now = RTC.now();
DateTime compiled = DateTime(__DATE__, __TIME__);
if (now.unixtime() < compiled.unixtime()) {
Serial.println("RTC is older than compile time! Updating");
RTC.adjust(DateTime(__DATE__, __TIME__));
}
// Set pin modes
pinMode(connectedON, OUTPUT);
pinMode(connectedOFF, OUTPUT);
pinMode(pumpRelay, OUTPUT);
// Set alarms
Alarm.timerRepeat(30, sync);
Alarm.timerRepeat(1, checkRelay);
sync();
Serial.println("Setup Complete");
}
void loop() {
Alarm.delay(1000);
}
void sync() {
Serial.println("Syncing");
GAE(webapp + "adacs/sync?State=" + pumpState);
Serial.println();
}
void checkRelay() {
DateTime now = RTC.now();
current = now.unixtime();
if (current_mode == "Toggle") {
if (current < toggle) {
} else {
// Determine which state to toggle to
if (pumpState == "off") {
// The pump was off. Turn on and set next toggle time based on run time
digitalWrite(pumpRelay, LOW);
Serial.println("Turning pump on");
pumpState = "on";
// Set next toggle time
future = now.unixtime() + pumpRunTime;
toggle = now.unixtime() + pumpRunTime;
} else if (pumpState == "on") {
// The pump was on. Turn off and set next toggle time based on idle time
digitalWrite(pumpRelay, HIGH);
Serial.println("Turning pump off");
pumpState = "off";
// Set next toggle time
future = now.unixtime() + pumpIdleTime;
toggle = now.unixtime() + pumpIdleTime;
}
displayTime();
}
}
}
void displayTime() {
DateTime now = RTC.now();
current = now.unixtime();
// Display current time
Serial.print("Current time: ");
Serial.print(now.year(), DEC);
Serial.print('/');
Serial.print(now.month(), DEC);
Serial.print('/');
Serial.print(now.day(), DEC);
Serial.print(' ');
Serial.print(now.hour(), DEC);
Serial.print(':');
Serial.print(now.minute(), DEC);
Serial.print(':');
Serial.print(now.second(), DEC);
Serial.println();
// Display next toggle time
Serial.print("Future time: ");
Serial.print(future.year(), DEC);
Serial.print('/');
Serial.print(future.month(), DEC);
Serial.print('/');
Serial.print(future.day(), DEC);
Serial.print(' ');
Serial.print(future.hour(), DEC);
Serial.print(':');
Serial.print(future.minute(), DEC);
Serial.print(':');
Serial.print(future.second(), DEC);
Serial.println();
}
void GAE(String link) {
boolean success = httpRequest(link);
if (success == true) {
delay(5000);
boolean currentLineIsBlank = true;
String readString = "";
char newString[100];
while (client.connected()) {
if (client.available()) {
char c = client.read();
//Serial.write(c); // Dev mode
if (c == '\n' && currentLineIsBlank) {
while(client.connected()) {
char f = client.read();
readString += f;
}
}
if (c == '\n') {
currentLineIsBlank = true;
} else if (c != '\r') {
currentLineIsBlank = false;
}
}
}
client.stop();
readString.toCharArray(newString, 100);
// The full JSON object
aJsonObject* jsonObject = aJson.parse(newString);
// Get the mode
aJsonObject* mode = aJson.getObjectItem(jsonObject, "Mode");
current_mode = mode->valuestring; // Convert mode to string and assign to current_mode
Serial.println("Mode: " + current_mode);
// Mode conditional
if (current_mode == "Always On") {
// Turn the pump on
digitalWrite(pumpRelay, LOW);
// Update state
pumpState = "on";
} else if (current_mode == "Toggle") {
// Sync the run time
aJsonObject* rt = aJson.getObjectItem(jsonObject, "RunTime");
String rString = rt->valuestring;
pumpRunTime = rString.toInt();
// Sync the idle time
aJsonObject* it = aJson.getObjectItem(jsonObject, "IdleTime");
String iString = it->valuestring;
pumpIdleTime = iString.toInt();
// Optional output
// Serial.print("Run Time: ");
// Serial.print(pumpRunTime);
// Serial.println();
//
// Serial.print("Idle Time: ");
// Serial.print(pumpIdleTime);
// Serial.println();
} else if (current_mode == "Manual") {
// Sync the state
aJsonObject* st = aJson.getObjectItem(jsonObject, "Status");
pumpState = st->valuestring;
if (pumpState == "on") {
digitalWrite(pumpRelay, LOW);
} else if (pumpState == "off") {
digitalWrite(pumpRelay, HIGH);
} else {
Serial.println("Unknown pump state given in Manual mode.");
}
} else {
Serial.println("Uknown mode detected during sync.");
}
} else {
Serial.println("Not connected.");
}
// Delete the root object
aJson.deleteItem(jsonObject);
}
boolean httpRequest(String link) {
// If there is a successful connection
if (client.connect(server, 80)) {
client.println("GET " + link + " HTTP/1.0");
client.println();
// Turn connected LED on
digitalWrite(connectedOFF, LOW);
digitalWrite(connectedON, HIGH);
return true;
} else {
// You couldn't make the connection
Serial.println("Connection Failed");
//errors += 1;
client.stop();
// Turn connected LED on
digitalWrite(connectedON, LOW);
digitalWrite(connectedOFF, HIGH);
return false;
}
}
Step 3: Web Application
The included web application runs on Google App Engine. You can learn how to create an application here and learn about Google App Engine's pricing here. This application is designed to fit inside of Google's free pricing scheme and does not accrue data, however, but note Google's prices are subject to change and not within our control.
To use the application, download and extract it. The only change you need to make is to modify app.yaml and rename the application with the identifier you used to create the application.
application: myapsystem -> {{ Your application identifier }}
The web application syncs with the server every 30 seconds (the same as the Arduino sketch). If you change the interval, make sure you change it for both programs, but be careful of running to many instances and exceeding the free quota.
Setting the pump state in Manual mode is done by clicking the image, but note that the image may change to the previous state before the Arduino syncs.
The toggle times are set in seconds, so running the pump for five minutes would mean entering 300 into the input box.
Finally, the web application is mobile friendly (Responsive Web Design) and will change layouts to handle phones and tablets.
To use the application, download and extract it. The only change you need to make is to modify app.yaml and rename the application with the identifier you used to create the application.
application: myapsystem -> {{ Your application identifier }}
The web application syncs with the server every 30 seconds (the same as the Arduino sketch). If you change the interval, make sure you change it for both programs, but be careful of running to many instances and exceeding the free quota.
Setting the pump state in Manual mode is done by clicking the image, but note that the image may change to the previous state before the Arduino syncs.
The toggle times are set in seconds, so running the pump for five minutes would mean entering 300 into the input box.
Finally, the web application is mobile friendly (Responsive Web Design) and will change layouts to handle phones and tablets.