Introduction: Water Curtain

About: I'm just a lady who likes making stuff. I got my degree in engineering but also enjoy cooking, sewing, knitting, gardening and backpacking, among other things.

In a class at our college, most of the semester is spent designing and building a project that has a mechanical component, an electrical component and a software component on a budget of $250.
We wanted to make a smaller scale water curtain like this one although on a much smaller, simpler scale.

Links to youtube videos:,

Step 1: Materials

For this project, we used
-8 gravity fed solenoid valves.
-PVC pipes and joints
-caulk/other waterproofing stuff
-tubs for water
-thick plastic

Step 2: Frame

For the frame, we used two tubs to hold the water.  One tub was a planter and one was just a regular storage bin we got for free.  We built a frame to hold the upper tank (basin) out of PVC pipe.
The legs of the structure were secured in the lower tank using elastic stretched under the tub to hold the legs firmly against the sides of the tub.

The size of your frame will really depend on how big you want your curtain to be.  Our frame was 5 feet tall.

Step 3: Water System and Valves

     To get water up to the basin, we bought a Rule 500 GPH Bilge pump, put 3/4" inside diameter clear vinyl tubing over it, and ran the tubing up one of the pvc legs and into the basin. For a 5 ft tall waterfall curtain, the pump was barely powerful enough to get the water all the way up, but we unfortunately didn't have the budget to buy a more powerful pump. Another inconvenience was that the pump ran off 12VDC instead of plugging in the wall. Fortunately, we had 12VDC power supply to use, but it would have been more convenient to get a plug-in pump.

     We used solenoid valves to create the waterfall, since they can be wired to a microcontroller that gives signals for them to open and close. While most solenoid valves are very expensive ($50-90), we were able to get cheaper, $16 valve from the valves4projects ebay store. We used a gravity feed valve as opposed to a pressurized valve because it was easier to have an open basin of water than a pressurized, enclosed system. While the gravity feed valves can only tolerate 7 ft of standing water above them, we didn't run into any problems with that.

     Each end of the valves was threaded to fit perfectly into a 1/2" female pvc pipe adapter. We could have drilled a threaded hole in the basin and screwed them in, but the machine shop we were using didn't have a tap with the correct thread. While that would have been the more waterproof way of attaching the valves, the simpler way was to buy pipe fittings, screw them onto all the valves after lubricating them with pipe thread sealant, and then press fit the valves into the basin.

     Regardless of which method was used, the bottom of the basin was too thin for anything to be attached to it and hold, so we screwed a 1.5" thick sheet of plastic with holes pre-machined for the valves, to the bottom of the basin. The plastic we used was similar to the type cutting boards are made out of, but any plastic sheet will do, as long as it is thick enough. After that, we stuck the valves in and sealed the area around the pipe and screws inside the basin. Selecting the right sealant is crucial-we tried epoxy putty but it still left little cracks. We're certain there are other epoxies or silicon-based adhesives that would work, but we ended up making do with hot glue.

Step 4: Circuit

     There were two different parts to the circuit: the circuit for the PIC which was on a breadboard and the circuit for the relays which was on a perfboard. When you make the code, you can test it with LED and replace the circuit for RB0 to RB7 with a 470 ohm resistor in series with an LED for each port. 

    The solenoid valves required 120V so make sure you are VERY careful with your wires and circuitry.  We are not responsible for any injuries or deaths caused by this project.That being said, we took every precaution possible in the valve circuit to make it safe. The relay circuit that connects to the valve is also shown below. The signal from the pic is sent to a relay which then sends a 120 V signal to a valve, causing it to open. The pic side of the relay is in series with a diode to prevent any current from traveling back to the pic. The valve is in series with a varistor (large green thing) to prevent voltage spikes when the valves are turned off.

    We used a 35x61 holed perfboard. The current runs straight down the perfboard on each copper strip, so we used a dremel to scrape off the copper to isolate each relay circuit. When we wired the circuit on perfboard, we isolated the 120 V signal, which came from an iec connector plugged into the wall, from any other part of the circuit. In addition, we attached wires to the solenoids using quick connects (female), covered by heat shrink tubing and electrical tape for a safe, easy way to connect the wires from the relay circuit to the solenoid.

Step 5: Code


In order for the PIC18F2445 to act as an integrated circuit, we created an MPLab project with a called python script to control the valve functions.

Original Proposed Function:
Our original intent with this project was to read images and translate them into water counterparts. We tried to find images with low pixelation so they were could be recreated. To upload and set the image as a 8X8 grid (since our apparatus had eight valves) we used the python imaging library (PIL) to upload our checkerboard jpeg image.

Sample Code:

import Image
import time

def loadPic(filename='checkerboard.jpeg', threshold = 3, size = (50,50)):
x_length,y_length= im.size
print [x_length, y_length]
x2_length = 2# gave an arbitrary value here, just so this works
x1_length= x1_length- x2_length
y1_length= y_length-y2_length

class TestTable():
def __init__(self):
self.rowLabels = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
self.colLabels = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]

def GetNumberRows(self):
return 10

def GetNumberCols(self):
return 10

def IsEmptyCell(self, row, col):
return False

def GetValue(self, row, col):
return 0

def SetValue(self, row, col, value):

def GetColLabelValue(self, col):
return self.colLabels[col]

def GetRowLabelValue(self, row):
return self.rowLabels[row]

We were able to divide the image into such a grid while assigning the rows and column characters so they could be referenced. This determined whether a spot(row,column) was empty (white) we assigned a get.color assignment to designate the number 1 for black and 0 for white. Each spot would have to go through an if else statement to make such assignments.

A example statement for the spot A1. Notice that after the assignment the command is also assigned. In this case it controls the pin RB0.

Sample Code:

if A1==(1,0):
print "white"

elif A1==(0,1):
print "white"

elif A1==(1,1):
print "white"

elif A1==(0,0):
print "black"

These commands repeat for this spot and for the remaining pins RB0-RB7.

After collecting the information from the image we had to send it through the usb to our MPLab code. We did this by initializing the usb and each command.

Sample Code:

from Tkinter import *
import ctypes

SET_RB0 = 1#self.col_grid[0(cell_value=1)] 
CLR_RB0 = 2#self.col_grid[0(cell_value=0)] 
SET_RB1 = 3#self.col_grid[1(cell_value=1)] 
CLR_RB1 = 4#self.col_grid[1(cell_value=0)] 
SET_RB2 = 5#self.col_grid[2(cell_value=1)] 
CLR_RB2 = 6#self.col_grid[2(cell_value=0)] 
SET_RB3 = 7#self.col_grid[3(cell_value=1)] 
CLR_RB3 = 8#self.col_grid[3(cell_value=0)] 
SET_RB4 = 9#self.col_grid[4(cell_value=1)] 
CLR_RB4 = 10#self.col_grid[4(cell_value=0)] 
SET_RB5 = 11#self.col_grid[5(cell_value=1)] 
CLR_RB5 = 12#self.col_grid[5(cell_value=0)] 
SET_RB6 = 13#self.col_grid[6(cell_value=1)] 
CLR_RB6 = 14#self.col_grid[6(cell_value=0)] 
SET_RB7 = 15#self.col_grid[7(cell_value=1)] 
CLR_RB7 = 16#self.col_grid[7(cell_value=0)]

 = ctypes.cdll.LoadLibrary('usb.dll')

buffer = ctypes.c_buffer(8)

root = Tk()
root.title('Water Curtain')
fm = Frame(root)

def update_status():
usb.control_transfer(dev, 0xC0, SET_RA8, 0, 0, 1, buffer)
status.configure(text = 'rb7 is currently %d.' % ord(buffer[0]))
root.after(50, update_status)

def set_rb0_callback():
usb.control_transfer(dev, 0x40, SET_RB0, 0, 0, 0, buffer)

def clr_rb0_callback():
usb.control_transfer(dev, 0x40, CLR_RB0, 0, 0, 0, buffer)

def set_rb1_callback():
usb.control_transfer(dev, 0x40, SET_RB1, 0, 0, 0, buffer)

def clr_rb1_callback():
usb.control_transfer(dev, 0x40, CLR_RB1, 0, 0, 0, buffer)

def set_rb2_callback():
usb.control_transfer(dev, 0x40, SET_RB2, 0, 0, 0, buffer)

def clr_rb2_callback():
usb.control_transfer(dev, 0x40, CLR_RB2, 0, 0, 0, buffer)

def set_rb3_callback():
usb.control_transfer(dev, 0x40, SET_RB3, 0, 0, 0, buffer)

def clr_rb3_callback():
usb.control_transfer(dev, 0x40, CLR_RB3, 0, 0, 0, buffer)

def set_rb4_callback():
usb.control_transfer(dev, 0x40, SET_RB4, 0, 0, 0, buffer)

def clr_rb4_callback():
usb.control_transfer(dev, 0x40, CLR_RB4, 0, 0, 0, buffer)

def set_rb5_callback():
usb.control_transfer(dev, 0x40, SET_RB5, 0, 0, 0, buffer)

def clr_rb5_callback():
usb.control_transfer(dev, 0x40, CLR_RB5, 0, 0, 0, buffer)

def set_rb6_callback():
usb.control_transfer(dev, 0x40, SET_RB6, 0, 0, 0, buffer)

def clr_rb6_callback():
usb.control_transfer(dev, 0x40, CLR_RB6, 0, 0, 0, buffer)

def set_rb7_callback():
usb.control_transfer(dev, 0x40, SET_RB7, 0, 0, 0, buffer)

def clr_rb7_callback():
usb.control_transfer(dev, 0x40, CLR_RB7, 0, 0, 0, buffer)

def set_duty_callback(value):
usb.control_transfer(dev, 0x40, SET_DUTY, int(value), 0, 0, buffer)

Now the problem that we faced from this point on was that we were not able to call a complex collection of pins with a timer request that was not cyclical. We were able to create our own simple images that used basic repeated delays that turned valves on and off, but none that could take in the inputs from the grid. Therefore, we not able to compile complex images. Therefore, we changed course to create simple images that could be translated into valve function.

New Software Goal

The new mission of our software was to take simple images that could be controlled with a delay cycle.

Import the appropriate python libraries and assign value to be read in MPLab.

Sample Code:

from Tkinter import *
import ctypes

##CLR_LED = 18
DOT = 18
LINE = 19

Initialize the USB.

usb = ctypes.cdll.LoadLibrary('usb.dll')

buffer = ctypes.c_buffer(8)

Define wanted images that will run under the timer cycle.

def dot_callback():
usb.control_transfer(dev, 0x40, DOT, 0, 0, 0, buffer)

def line_callback():
usb.control_transfer(dev, 0x40, LINE, 0, 0, 0, buffer)

def checkerboard_callback():
usb.control_transfer(dev, 0x40, CHECKERBOARD, 0, 0, 0, buffer)

def halfhalf_callback():
usb.control_transfer(dev, 0x40, HALF_HALF, 0, 0, 0, buffer)

def set_duty_callback(value):
usb.control_transfer(dev, 0x40, SET_DUTY, int(value), 0, 0, buffer)

def update_status():
root.after(50, update_status)

Make a text box button so we can control which images can displayed at what time (aka pressing the button!)

Sample Code:

root = Tk()
fm = Frame(root)
##my_textbox = Entry(fm)
##my_textbox.pack(side = LEFT)
##Button(fm, text = 'GO!', command = send_textbox_val).pack(side = LEFT)
##Button(fm, text = 'CLR', command = CLR_display).pack(side = RIGHT)
Button(fm, text = 'DOT', command = dot_callback).pack(side = RIGHT)
Button(fm, text = 'LINE', command = line_callback).pack(side = RIGHT)
Button(fm, text = 'CHECKERBOARD', command = checkerboard_callback).pack(side = RIGHT)
Button(fm, text = 'HALF AND HALF', command = halfhalf_callback).pack(side = RIGHT)
fm.pack(side = TOP)
dutyslider = Scale(root, from_ = 0, to = 255, orient = HORIZONTAL, showvalue = FALSE, command = set_duty_callback)
dutyslider.pack(side = TOP)

Write in a error if the USB device is not found.

Sample Code:

dev = usb.open_device(0x6666, 0x0003, 0)
if dev<0:
print "No matching device found...\n"
ret = usb.control_transfer(dev, 0x00, 0x09, 1, 0, 0, buffer)
if ret<0:
print "Unable to send SET_CONFIGURATION standard request.\n"
root.after(50, update_status)

Now to the MPLab code. We use a standard USB vendor request program, however we have to define all the commands in the beginning.

#define SET_RB0 0x01 // vendor-specific request to set (i.e., make high) RA0
#define CLEAR_RB0 0x02 // vendor-specific request to set (i.e., make ;low) RA0
#define SET_RB1 0x03 // vendor-specific request to set (i.e., make high) RA1
#define CLEAR_RB1 0x04 // vendor-specific request to set (i.e., make low) RA1
#define SET_RB2 0x05 // vendor-specific request to set (i.e., make high) RA2
#define CLEAR_RB2 0x06 // vendor-specific request to set (i.e., make low) RA2
#define SET_RB3 0x07 // vendor-specific request to set (i.e., make high) RA3
#define CLEAR_RB3 0x08 // vendor-specific request to set (i.e., make low) RA3
#define SET_RB4 0x09 // vendor-specific request to set (i.e., make high) RB4
#define CLEAR_RB4 0x0A // vendor-specific request to set (i.e., make low) RB4
#define SET_RB5 0x0B // vendor-specific request to set (i.e., make high) RB5
#define CLEAR_RB5 0x0C // vendor-specific request to set (i.e., make low) RB5
#define SET_RB6 0x0D // vendor-specific request to set (i.e., make high) RB6
#define CLEAR_RB6 0x0E // vendor-specific request to set (i.e., make low) RB6
#define SET_RB7 0x0F // vendor-specific request to set (i.e., make high) RB7
#define CLEAR_RB7 0x10 // vendor-specific request to set (i.e., make l0w) RB7
//#define TEXTBOX_VAL 0x11 // vendor-specific request to set 7 segment LED
//#define CLR_LED 0x12 // vendor-specific request to clear 7 segment LED
#define SET_DUTY 0x11
#define DOT 0x12
#define LINE 0x13
#define CHECKERBOARD 0x14
#define HALF_HALF 0x15

We then can proceed by writing our vendor requests for the USB. The following is a request for RB0 with the start of the entire function.

void VendorRequests(void) {
switch (USB_buffer_data[bRequest]) {
case SET_RB0:
PORTBbits.RB0 = 1; // set RA0 high
BD0I.bytecount = 0x00; // set EP0 IN byte count to 0
BD0I.status = 0xC8; // send packet as DATA1, set UOWN bit

We did this for each pin RB0-RB7 to enable us be able to test and ease the means of debugging.

In creating images such as the checkerboard we started in the same way as a specific pin request, but we assigned the on and off of the valves with 0's and 1's (each number going to one of the eight pins). This enable the multiple use of simultaneous pin operations.

Sample Code:

case SET_DUTY:

DUTY = USB_buffer_data[wValue];
BD0I.bytecount = 0x00; // set EP0 IN byte count to 0
BD0I.status = 0xC8; // send packet as DATA1, set UOWN bit


// PORTB=0b11001100;
// Delay10KTCYx(10);
// PORTB=0b00000000;

while (1){

temp = TMR0L; // latch the high order byte of the Timer0 counter into TMR0H by reading TMR0L

With these script modifications we were able to detail out a program that can translate grid images to water displays.

*see below for actual code. 


Step 6: Awesome Water Curtain!

Obviously, with a bigger budget, we would be able to produce a bigger, better, more refined curtain.  We also want to make some additions to our code and paint the frame to make it look a little nicer.
Stay tuned in the next few days for updates!

Epilog Challenge

Participated in the
Epilog Challenge