Introduction: Self-sustaining Plant Wall

Many busy professionals want to have plants in their home, but don’t have the time to maintain them. This self-watering planter maintains the plant by itself, requiring only a tank refill every other week. It waters the plants when the soil is dry, turns on the lights when the room is dark but the owner is awake and away, and signals when it runs low on water.


  1. Case:
    • Wood finish Hinge (2 pack)
    • Latch (1x)
    • Screws (1 ½ and 1 ¾ inch)
    • 1x6 wood (3x, cut into 24 inch lengths)
    • 1x3 wood (1x, cut into 24 inch lengths)
    • Plywood (1x, 24”x24”)
  2. Tank:
    • Freezer ice cube tub
  3. Planters & Plants:
    • Foamcore
    • Plastic
    • Tape
    • Soil
    • Pothos plants (variety)
  4. Electronics:
  5. Tools: 3D printer; Soldering kit; Sander; Drill; Bandsaw; Hot glue gun; wire cutter

Step 1: Initial Case Construction

Used HomeDepots complimentary service to have three 1x6 planks and one 1x3 cut into 24 inch pieces (which were all a little different in actual length). NOTE: This was too short for the lights used, recommend at least 27 inches.

Four 1x6 lengths formed the frame, with the sides of the frame going outside the bottom and top boards for strength. Two additional pieces were used for shelves. Two 1x3 lengths formed the containing lip of the shelves. A seventh 1x6 lengths was used along with one of the 1x3 lengths to form a door for the water and electrical compartment.Two thin strips of wood were screwed onto the pieces to attach them. Once the frame and shelves and door are assembled, gently sand everything and apply two coats of stain. Attach the door with hinges and latch.

Step 2: Electronics - Hardware

  1. Do the easy soldering:
    1. stacking headers to 1 of the chirp sensors.
    2. some jumper wires to the other chirp sensor
  2. Some soldering where orientation matters
    1. Diodes across pumps
    2. Green and Blue wires to positive pump terminals
    3. Ground wire (shared) to negative pump terminals
  3. Wire it up!
    1. See Tinkercad picture above, and EAGLE .brd file here.
      1. moisture sensors are represented as gas sensors, with pin locations matching orientation of picture in Chirp! Documentation (link below).
      2. power relay is represented as a single relay off the board.
      3. RTC is represented as 8-pin header
      4. water level sensor is represented as potentiometer
    2. Chirp! documentation for which pin is which, reset is brown in the diagram.
    3. Elegoo Wiring Diagrams available on their download page, look for "37-in-1 V2".

Step 3: Integration and Final Case Assembly

Drill all holes for tubing and insert lights (since the lights themselves are 24 inches long, this requires digging holes into the side shelves to accommodate the power cables on the lights.

Perforate the tubing. To make a hole in the tube, make two 45-degree cuts with the wire-cutters going opposite directions. Put small holes in the tubes approximately every 1.5" along the lengths that will be in the planter.

Attach electronics in electronics compartment. Screw 24”x24” sheet onto back of box. Attach final two 1x6 lengths to create a gap between the wall and back of box for tubing and wires as well as for added stability and ease of mounting to a wall.

Step 4: Plant Plants

We selected Pothos plants because of their heartiness, visual diversity and tendency to continue growing in an aesthetically optimal, vine-like manner.

Create four planter boxes that will fit into the shelf and can be taken out as and if needed.

Cut pattern out of foam core

Hot glue boxes together

Line with plastic to waterproof and tape plastic down

Gently split apart plants and rearrange into planter boxes

Add soil to planter boxes as needed

Insert planter boxes into shelves and arrange watering tubes across them

Step 5: Code

The code uses the DS3231 library, which you can obtain through the Arduino IDE Tools -> Manage Libraries, search for DS3231.

/* Functionality:
* -- Activate pumps on moisture low * -- If photoresistor low, and valid hours of day, then turn lights on for 10 minutes * -- If water level low, then blink lights */

#include #include

// Assumption: Wire assumes that the two communication pins, which are here being used to multiplex // the two moisture sensors and the real-time clock, are plugged into SDA (A4) and SCL (A5)

/* * Future wishlist: * *** Use the moisture sensor's chirp mode as the alarm for low-tank level. * Make it reset after the water is refilled. */

#define NUM_PLANTERS 2

//#define DEBUG // Uncomment this to enable debug printing


#ifdef DEBUG #define DEBUG_PRINT(s) Serial.println(s) #else #define DEBUG_PRINT(s) #endif

/******************** Time ***************************/

#define TICK_TIME 5000 // how often to check moisture level. All times in ms unsigned int tickCount = 0;

/******************** Clock ***************************/

DS3231 clock;

/************************* Water level **********************/

#define LOW_WATER_THRESHOLD 300 int waterLevelPin = A0;

/*********************** Moisture sensing ************************/

typedef struct {int value;} MoistureLevel;

const int moistResetPin = 2; const byte moistureSensorAddresses[] = {0x20, 0x21}; // multiplexed; for both pins; AKA 32,33 #define MOISTURE_THRESHOLD 300;

/********************************* Pump control **************************/

int pumpPins[NUM_PLANTERS] = {6, 7};

#define PUMP_TIME 2000 // Time to run each pump for after detecting low moisture. All times in ms

/******************************** Light control ***************************/

#define DARK_THRESHOLD 600 int photoResistorPin = A1;

int growLightPin = 3;

typedef struct { int value; } OptionalNonnegInt;

OptionalNonnegInt lastDarkTime;

#ifdef SHORTEN_DAY_NIGHT_CYCLE #define TICKS_PER_LIGHT_CHECK 1 #else #define TICKS_PER_LIGHT_CHECK ((1000 * 10 / TICK_TIME) * 60) #endif

/*************************************************************************************************** *************************************** END HEADER ************************************************ *************************************************************************************************** */

/************************* Wire library ******************************************/

void writeI2CRegister8bit(int addr, int value) { // Serial.println("about to use wire"); Wire.beginTransmission(addr); DEBUG_PRINT("begun transmission"); // for an unknown reason, these print statements seem to help the moisture sensors initialize Wire.write(value); DEBUG_PRINT("wrote value to :"); DEBUG_PRINT(addr); Wire.endTransmission(); //for some reason this needs the reset pin connected? DEBUG_PRINT("ended transmission, returning to setup"); }

unsigned int readI2CRegister16bit(int addr, int reg) { Wire.beginTransmission(addr); Wire.write(reg); Wire.endTransmission(); delay(1000); Wire.requestFrom(addr, 2); unsigned int t = << 8; t = t |; return t; }

/***************************** Nonneg-ints ****************************/

OptionalNonnegInt makeEmptyInt() { OptionalNonnegInt ret; ret.value = -1; return ret; }

OptionalNonnegInt makeNonnegInt(int x) { OptionalNonnegInt ret; ret.value = x; return ret; }

int isEmpty(OptionalNonnegInt n) { return n.value == -1; }

int getValue(OptionalNonnegInt n) { return n.value; }

/***************************** Time ****************************/

void setupClock() { DEBUG_PRINT("setting up clock"); clock.begin(); DEBUG_PRINT("set up clock");

// This line resets the clock to the sketch compiling time // Uncomment this when configuring a device, then recomment it and upload again //clock.setDateTime(__DATE__, __TIME__); }

void printCurTime() { RTCDateTime dt = clock.getDateTime(); Serial.println("Printing time: Year, month, day, hour, minute"); Serial.println(dt.year); Serial.println(dt.month); Serial.println(; Serial.println(dt.hour); Serial.println(dt.minute); }

int getHourOfDay() { /* #ifdef SHORTEN_DAY_NIGHT_CYCLE RTCDateTime dt = clock.getDateTime(); return ((dt.minute % 2) * 60 + dt.second) / 5; #else return clock.getDateTime().hour; #endif */ return 12; }

OptionalNonnegInt timeSince(OptionalNonnegInt x) { if (isEmpty(x)) { return makeEmptyInt(); } else { int t1 = getValue(x); int t2 = millis(); return makeNonnegInt(t2 - t1); } }

/**************************** Light control ***********************/

void setupLights() { pinMode(growLightPin, OUTPUT); }

int isDark() { int darkLevel = analogRead(photoResistorPin); DEBUG_PRINT("light level: "); DEBUG_PRINT(darkLevel); return darkLevel > DARK_THRESHOLD; }

int forceDarkenLights() { int h = getHourOfDay(); DEBUG_PRINT("hour is"); DEBUG_PRINT(h); return !(h >= 10 && h <= 22); }

void trySetLights(int status) { if (forceDarkenLights()) { DEBUG_PRINT("forcing light to off b/c time of day"); digitalWrite(growLightPin, 0); } else { digitalWrite(growLightPin, status); } }

void updateDarkTime() { if ((tickCount % TICKS_PER_LIGHT_CHECK) == 0) {

/* This code solve the problem that it can't test if the room is dark * when the lights are on. * * But....when testing, this flicker is really annoying. */ #ifndef SHORTEN_DAY_NIGHT_CYCLE trySetLights(0); delay(100); #endif

if (isDark()) { lastDarkTime = makeNonnegInt(millis()); } else { lastDarkTime = makeEmptyInt(); } } }

/************************* Moisture sensing *********************************/

void moistureSetup() {

pinMode(moistResetPin, OUTPUT);

// reset the sensors to make them listen for I2C comms. digitalWrite(moistResetPin, LOW); delay(40); digitalWrite(moistResetPin, HIGH); delay(40); // This is used to get moisture sensors into sensor mode instead of chirp mode for (int i = 0; i < NUM_PLANTERS; i++) { // This simple reset should work, but doesn't: // writeI2CRegister8bit(moistures[i], 6); // delay(500); writeI2CRegister8bit(moistureSensorAddresses[i], 3); // delay(200); // readI2CRegister16bit(moistures[i], 0); // readMoisture(i); } }

MoistureLevel readMoisture(int i) { MoistureLevel ret; ret.value = readI2CRegister16bit(moistureSensorAddresses[i], 0); DEBUG_PRINT("Moisture level: "); DEBUG_PRINT(ret.value); return ret; }

int moistureLow(MoistureLevel l) { return l.value <= MOISTURE_THRESHOLD; }

/************************ Pump control *************************************/

void pumpSetup(){ for (int i = 0; i < NUM_PLANTERS; i++) { pinMode(pumpPins[i], OUTPUT); } }

void runPump(int i) { DEBUG_PRINT("watering plant"); digitalWrite(pumpPins[i], HIGH); delay(PUMP_TIME); digitalWrite(pumpPins[i], LOW); }

/************************ Water level *************************************/

int waterLow() { int waterLevel = analogRead(waterLevelPin); DEBUG_PRINT("Water level:"); DEBUG_PRINT(waterLevel); return waterLevel < LOW_WATER_THRESHOLD; }

/*********************** Main Arduino control *******************************/

void setup() { Wire.begin(); Serial.begin(9600); pumpSetup(); moistureSetup(); setupClock(); setupLights(); }

void loop() { DEBUG_PRINT("tick");

#ifdef DEBUG printCurTime(); #endif

pumpControl: { for (int i = 0; i < NUM_PLANTERS; i++) { DEBUG_PRINT("read sensor"); DEBUG_PRINT(readMoisture(i).value); if (moistureLow(readMoisture(i))) { runPump(i); } } }

lightControl: { updateDarkTime(); int lightShouldBeOn;

if (waterLow()) { // Hijack the light mechanism to flash the lights // to indicate low water lightShouldBeOn = tickCount % 2 == 0; } else { lightShouldBeOn = !isEmpty(lastDarkTime); }

DEBUG_PRINT("Dark time"); DEBUG_PRINT(lastDarkTime.value); DEBUG_PRINT("Light should be on?"); DEBUG_PRINT(lightShouldBeOn); trySetLights(lightShouldBeOn); } timeControl: { tickCount++; delay(TICK_TIME); } }

The code is divided into sections for each of the sensors it integrates with, and to encapsulate the various policies, such as timing, thresholds for the light and moisture sensors, and the time of the dark hours.

The main function gives the main functionality. It does these functions repeatedly:

Pump control:

Checks if each moisture sensor is reporting low moisture. If so, it will run the corresponding pump for 2 seconds.

Light control:

First, if the time is between 10 PM and 10 AM, then the planter is in dark hours. Ignore the rest of this part; the grow lights will not turn on under any circumstances during dark hours. Otherwise, it: Checks if the room is dark, and records the time if so. If the grow lights are on, it will briefly flicker them off to do the check If the water level is low, it will turn the lights on and off at roughly 5 second intervals. If the water level is not low, and the room is dark, activate the lights.

After performing light control, it waits 5 seconds, before beginning the next “tick” of the main loop.

A few other things about the code:

  • You will need to reset the RTC before using. Uncomment the relevant line in setupClock() to do so.
  • You can toggle the DEBUG flag to turn on debug printouts
  • You can toggle the SHORTEN_DAY_NIGHT_CYCLE flag to change the behavior so that it pretends to run through a full day-night cycle every 120 seconds. This is useful for testing purposes. When this flag is on, it will also no longer flicker the lights to check for darkness. Because it records the last time the room was dark, the code can easily be modified to wait several minutes before turning on the grow lights, as did a previous version.

Moisture sensor Address change:

Because the moisture sensors communicate via I2C on the same pins, they need unique addresses, and they can't be the same as the RTC. We couldn't find what the RTC uses in a cursory search, but the sensors start at 0x20. You will need to change one of them to something else (we used 0x21). The reason you don't solder a jumper directly between the two, and only a header on one, is so that you can change the address of only one of them and now have them on separate addresses. We modified the code available from the maker of Chirp!, because of problems getting it into sensor mode with the original code. Ours is available here.

Step 6: Future Work

The biggest improvement would be the ability to set the dark-hours. Right now, the planter is hardcoded to only turn the lights on between the hours of 10 AM and 10 PM.

Another improvement would be the low-water signal. It begins flashing its lights when it’s low water, which is an unintuitive signal. One alternative would be to use the "chirp" functionality in the Chirp! brand moisture sensors we are using. Another alternative would be to attach a separate speaker and play a sound.

The current code will periodically turn off the lights in order to check if the room is dark when the planter’s lights are off. This gives it an annoying flicker, and also means that, if you turn on the room lights, it will be a while before the planter turns its lights off. An alternative could be a more sophisticated photo-sensing setup. For example, it could determine that, if the area under the grow lights is significantly brighter than a more distant sensor, then there is no significant lighting in the room other than the grow lights. Another possibility is to use the different spectra of the grow-lights vs. normal home lighting.

On the physical side, it would be nice to make the tank more removable for easier cleaning.

Make it Glow Contest

Participated in the
Make it Glow Contest