Martha Cryan, Xin Hui Lim, Tara Molesworth

Our project explored the dynamics of fabric together with wind, using kites as inspiration. Chaotic as well as floaty gestures could arise from small parameter changes, showcasing the stretchability and lightness of the material. All together, the effect of the performance was a dynamic creature-like-kite, flying and diving through the air.

To fly the fabric, 4 motors were fitted with aluminum arms. The arms were each tied at the end with fishing wire, which held a small piece of fabric (like a four-stringed kite) over a horizontal fan. The fan was controlled by a dmx, allowing for control of the fan speed as well as arm movement parameters.

The setup:

We used a micro-stepper CNC Shield board that was fitted over an Arduino Uno. The CNC Shield was able to hold 4 stepper drivers, which was just enough for our purposes. Each stepper motor, with extended wiring, was connected to one of the four ports – X, Y, Z and A. The Arduino Uno was powered by a 12V supply. We also connected the fan to a DMX box, that was connected to a DMX/USB interface (ENTTEC), and plugged into a power source. Both the USB ports from the Arduino and DMX were then connected to a Raspberry Pi. Finally, the Raspberry Pi was connected to the laptop which ran the code.

 

We cut 7/32” aluminum tubes into 15” lengths and drilled three 3/32” holes for each “arm” – two of which would be screwed onto the hub connector that was then secured onto the stepper motor with a mini counterscrew, one of which we tied a fishing line with the other end attached to one of the corners of the fabric.

 

We also lasercut two wooden stands that was painted with black acrylic paint, which the fan could be secured on.

 

The process:
We had previously experimented with the the type of mechanism (spooling, same string between two corners), the length of arms, as well as fabric sizes. Some things were considered included whether the arms created enough force, the sound of the stepper motors and fan, and the tone of the piece. Ultimately, we went for a more playful piece with a smaller fabric that had a larger range of motion, rather than a big fabric with more subtle movements because the single fan setup made it harder for the viewer to distinguish between different states.

During the experiment phase, we had used 4’ long wooden planks to keep the stepper motors at the same distance, but in the final presentation we secured them onto linoleum blocks, minimizing the distraction from the kinetic fabric piece.

 

We had also previously coded in a way for us to quickly change the stepper motor target positions, the stepper motor speeds, and the fan speed, by connecting a MIDI/Alias controller to our laptop. It was useful to learn about the hardware interface that could be implemented for experimentation, although we ultimately found it more useful to code the actual performances since it was not that hard to translate what we conceptualized for each performance state/behavior into code.

 

The performances:
At the start, all arms move inwards, hitting the ground and moving back to vertical position. The fan speed slowly increases. (State 0)

The code then runs itself in a loop (State 1 – 10).

State 1: Warming up – Opposite corners took turns to move slowly and slightly

State 2: Breathing – Fabric is held up vertically, and arms move slightly inwards and outwards at the same time.

State 3: Walking – All four arms were moving, with one opposite pair moving inwards and the other moving outwards.

State 4: Parabola – Opposite corners moving inwards at the same time, or outwards.

State 5: Jumping – Quick movements, randomly generated.

State 6: Swaying – Opposite corners moved slowly in a choreographed manner such that the fabric would jump between corners.

State 7: Resting – Fan speed slowly decreases, until just before the fabric falls, and quickly increases.

State 8: Jumping – Quick movements, randomly generated.

State 9: Spiral – Each arm took turns to wave/jerk at high speeds.

State 10: Flailing – Arms were held in fixed position such that the fabric was tilted upwards on one side, with only the fan speed changing to create movements in the fabric.

Source Code:


#!/usr/bin/env python

"""\
test_client.py : sample code in Python to communicate with an Arduino running CNC_Shield_Server

Copyright (c) 2015, Garth Zeglin. All rights reserved. Licensed under the terms
of the BSD 3-clause license.

"""

#================================================================
from __future__ import print_function
import argparse
import time

# This requires a pySerial installation.
# Package details: https://pypi.python.org/pypi/pyserial,
# Documentation: http://pythonhosted.org/pyserial/
import serial
import numpy as np

# This requires a pySerial installation.
# Package details: https://pypi.python.org/pypi/pyserial
# Documentation: http://pythonhosted.org/pyserial/
import serial
from serial import Serial

# from rtmidi.midiutil import open_midiinput

class MidiInputHandler(object):
def __init__(self, port, dmx, motors):
self.port = port
self._wallclock = time.time()
self.dmx = dmx
self.motors = motors
self.pX = 0
self.pY = 0
self.pZ = 0
self.pA = 0
self.eventQueue = []
self.updateTime = time.time()

## TO EDIT THE SLIDERS
## message is a 3 element array:
## first element doesn't matter
## second element is which slider
## third element is the value
def __call__(self, event, data=None):
message, deltatime = event
self._wallclock += deltatime
# if deltatime < 0.2:
# self.eventQueue.append(event)
# return
# print("[%s] @%0.6f %r" % (self.port, self._wallclock, message))
# self.updateTime = self._wallclock
if message[1] == 8:
self.motors.state = 0
elif message[1] == 9:
self.motors.state = 1
elif message[1] == 10:
self.motors.state = 2
elif message[1] == 11:
self.motors.state = 3
elif message[1] == 11:
self.motors.state = 4

#================================================================
class DMXUSBPro(object):
"""Class to manage a connection to a serial-connected Enttec DMXUSB Pro
interface. This only supports output.

:param port: the name of the serial port device
:param verbose: flag to increase console output
:param debug: flag to print raw inputs on sconsole
:param kwargs: collect any unused keyword arguments
"""

def __init__(self, port=None, verbose=False, debug=False, universe_size=25, **kwargs ):

# Initialize a default universe. This publicly readable and writable.
# The Enttec requires a minimum universe size of 25.
self.universe = np.zeros((universe_size), dtype=np.uint8)

# Initialize internal state.
self.verbose = verbose
self.debug = debug
self.portname = port
self.port = None
self.output = None
self.input = None

return

def is_connected(self):
"""Return true if the serial port device is open."""
return self.port is not None

def set_serial_port_name(self, name):
"""Set the name of the serial port device."""
self.portname = name
return

def open_serial_port(self,port):
"""Open the serial connection to the controller."""

# open the serial port
self.port = serial.Serial( port, 115200 )
if self.verbose:
print("Opened serial port named", self.port.name)

# save separate copies of the file object; this will ease simulation using other sources
self.output = self.port
self.input = self.port
return

def flush_serial_input(self):
"""Clear the input buffer."""
if self.input is not None:
self.input.flushInput()

def close_serial_port(self):
"""Shut down the serial connection, after which this object may no longer be used."""
self.port.close()
self.port = None
return

def send_universe(self):
"""Issue a DMX universe update."""
if self.output is None:
print("Port not open for output.")
else:
message = np.ndarray((6 + self.universe.size), dtype=np.uint8)
message[0:2] = [126, 6] # Send DMX Packet header
message[2] = (self.universe.size+1) % 256 # data length LSB
message[3] = (self.universe.size+1) >> 8 # data length MSB
message[4] = 0 # zero 'start code' in first universe position
message[5:5+self.universe.size] = self.universe
message[-1] = 231 # end of message delimiter

if self.debug:
print("Sending: '%s'" % message)
self.output.write(message)
return

def speed_change(self,speed):
print("dmx changing speed to "+str(speed))
dmx.universe[0] = speed
dmx.universe[2] = speed-50
dmx.send_universe()

#================================================================
class CncShieldClient(object):
"""Class to manage a connection to a CNC_Shield_Server running on a
serial-connected Arduino.

:param port: the name of the serial port device
:param verbose: flag to increase console output
:param debug: flag to print raw inputs on sconsole
:param kwargs: collect any unused keyword arguments
"""

def __init__(self, port=None, verbose=False, debug=False, **kwargs ):
# initialize the client state
self.arduino_time = 0
self.position = [0, 0, 0, 0]
self.target = [0, 0, 0, 0]
self.verbose = verbose
self.debug = debug
self.awake = False

# open the serial port, which should also reset the Arduino

self.port = serial.Serial( "/dev/ttyACM0", 115200, timeout=5 )
# self.port = serial.Serial( "/dev/tty.usbmodem1421", 115200, timeout=5 )
# self.port = serial.Serial( "COM4", 115200, timeout=5 )

if self.verbose:
print("Opened serial port named", self.port.name)
print("Sleeping briefly while Arduino boots...")

# wait briefly for the Arduino to finish booting
time.sleep(2) # units are seconds

# throw away any extraneous input
self.port.flushInput()

return

def close(self):
"""Shut down the serial connection to the Arduino, after which this object may no longer be used."""
self.port.close()
self.port = None
return

def _wait_for_input(self):
line = self.port.readline().rstrip().decode('utf-8')

if line:
elements = line.split(' ')
if self.debug:
print("Received: ")
print(elements)
print("Position:")
print(self.position)

if elements[0] == 'txyz':
self.arduino_time = int(elements[1])
self.position = [int(s) for s in elements[2:]]

elif elements[0] == 'awake':
self.awake = True

elif elements[0] == 'dbg':
print("Received debugging message:", line)

else:
if self.debug:
print("Unknown status message: ", line)

return

def _send_command(self, string):
if self.verbose:
print("Sending: ", string)
self.port.write( str.encode(string+'\n'))
self.port.flushOutput()
self.port.flushInput()
return

def motor_enable( self, value=True):
"""Issue a command to enable or disable the stepper motor drivers."""

self._send_command( "enable 1" if value is True else "enable 0" )
return

def wait_for_wakeup(self):
"""Issue a status query and wait until an 'awake' status has been received."""
while self.awake is False:
self._send_command( "ping" )
self._wait_for_input()

def move_to(self, position):
"""Issue a command to move to a [x, y, z, a] absolute position (specified in microsteps) and wait until completion.

:param position: a list or tuple with at least three elements
"""
self._send_command( "goto %d %d %d %d" % tuple(position))
# self.target = position

# while self.position[0] != position[0] or self.position[1] != position[1] or self.position[2] != position[2] or self.position[3] != position[3]:
# try:
# self._wait_for_input()
# except:
# print("Error reading!!")
# if self.verbose:
# print ("Position:", self.position)

# self.moving = False
return

def speed_change(self, speed):
self._send_command( "sc %d %d %d %d" % (speed,speed,speed,speed))

#================================================================

# The following section is run when this is loaded as a script.
if __name__ == "__main__":

# Initialize the command parser.
parser = argparse.ArgumentParser( description = """Simple test client to send data to the CNC_Shield_Server on an Arduino.""")
parser.add_argument( '-v', '--verbose', action='store_true', help='Enable more detailed output.' )
parser.add_argument( '--debug', action='store_true', help='Enable debugging output.' )

# Parse the command line, returning a Namespace.
args = parser.parse_args()

dmx = DMXUSBPro(**vars(args))
dmx.open_serial_port("/dev/ttyUSB0")
# dmx.open_serial_port("/dev/tty.usbserial-EN199298")

client = CncShieldClient(**vars(args))
client.moving = False

print("Waiting for wakeup.")
client.wait_for_wakeup()

print("Beginning movement sequence.")
client.motor_enable()

# Begin the lighting sequence. This may be safely interrupted by the user pressing Control-C.
try:
print("Beginning lighting sequence.")

speed = 150
direction = 1
motorspeed=100
posX = posY = posZ = posA = 0
client.state=9
new=[0,0,0,0]
count=0
x=200
client.move_to([0,0,0,0])

#reset position and slowly increase fan speed
seq0= [[0,0,0,0],[-80,80,80,-80],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]
fan0 = [100,100,100,120,140,150,160,170,160,140]

# breathing low
seq1=[[-20,20,20,-20],[-20,20,20,-20],[-10,10,10,-10],[-10,10,10,-10],[10,-10,-10,10]]
speed1 = [20,20,2,2,2]
fan1=[100,100,100,120,120]

#breathing high
seq4=[[-50,50,50,-10],[0,0,0,15],[30,-30,-30,15],[0,0,0,-15],[30,-10,-10,30],[0,0,0,-15],[30,-30,-30,15],[0,0,0,-15],[30,-10,-10,15],[0,0,0,0],[30,-10,-10,30]]
speed4 = [50,50,25,25,25,25,25,25,25,25,25,25]
fan4 = [160,160,150,150,140,140,130,130,120,120,140,140]

# walking
seq3=[[40,60,60,30],[-20,-10,-10,-30],[40,60,60,30],[-20,-10,-10,-30],[40,60,60,30],[-20,-10,-10,-30]]
speed3 = [10,30,30,30,10]
# fan3=[150,150,140,140,120,180]
time3=[5,3,2,3,5]
fan3=[140,180,140,180,140,180]

#x corner
seq2=[[30,0,0,0],[0,0,0,0],[-30,0,0,0],[0,0,0,0],[30,0,0,-30],[0,0,0,-30],[-30,0,0,-30],[0,0,0,-30],[20,0,0,-30],[0,0,0,-30],[-10,0,0,-30],[0,0,0,-30]]
# speed4 = [20,20,20,20,20,20,20,20,20,20,20,20]
speed2 = [50,30,30]
fan2=[120,120,120,120,120,120,120,120,120,120,120,120]

#moving quickly between corners, x&a
seq5=[[-20,0,0,20],[0,0,0,0],[40,0,0,-40],[0,0,0,0],[-40,0,0,40],[0,0,0,0],[60,0,0,-60],[0,0,0,0],[60,0,0,-60],[0,0,0,0],[60,0,0,-60],[20,0,0,20],[40,-20,0,40],[40,0,0,40],[40,-40,-10,40],[40,0,-20,40],[40,40,40,40],[40,0,0,40],[40,60,-60,40],[40,0,0,40],[40,60,-60,40],[40,0,0,40],[40,60,-60,40],[40,0,0,40]]
speed5 = [40,100,40]
fan5=[140,140,140,140,140,140,120,120,120,120,120,120,140,140,140,140,140,140,120,120,120,120,120,120]

#falling and getting up
seq7=[[-80,80,80,-80],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]
speed7=[50,5,5,5,5,5]
fan7=[200,200,160,120,120,100]

#fan speed change only
seq10=[[-20,20,60,-60],[-20,20,60,-60],[-20,20,60,-60],[-20,20,60,-60],[-20,20,60,-60],[-20,20,60,-60]]
fan10=[80,110,140,80,110,140]

# motion directly dependent on fan speed
while True:

if client.state==0:
print("-------------state0-------------")
client.speed_change(150)

for i in range(len(seq0)):
dmx.speed_change(fan0[i])
client.move_to(seq0[i])
time.sleep(i*0.5)
# maybe time.sleep can vary with sensor input

client.state=6

if client.state==1:
print("-------------state1-------------")
client.speed_change(20)
# falling

for j in range(3):
for i in range(len(seq1)):
dmx.speed_change(fan1[i])
client.speed_change(speed1[i])
client.move_to(seq1[i])
time.sleep(i*2)

client.state=2

# moving between corners
if client.state==2:
print("-------------state2-------------")
for j in range(len(speed2)):
client.speed_change(speed2[j])
for i in range(len(seq2)):
dmx.speed_change(fan2[i])
client.move_to(seq2[i])
time.sleep(2)

client.state=3

#walking
if client.state==3:
print("-------------state3-------------")
for j in range(len(speed3)):
client.speed_change(speed3[j])
for i in range(len(seq3)):
dmx.speed_change(fan3[i])
client.move_to(seq3[i])
time.sleep(time3[j])

client.state=4

# all moving in slightly / breathing
if client.state==4:
print("-------------state4-------------")
for j in range(3):
for i in range(len(seq4)):
dmx.speed_change(fan4[i])
client.speed_change(speed4[i])
client.move_to(seq4[i])
time.sleep(1)
client.state=8

if client.state==5:
print("-------------state5-------------")

client.move_to([0,0,0,0])
for i in range(50):
client.speed_change(200)
client.move_to([-np.random.randint(50),np.random.randint(80),np.random.randint(80),-np.random.randint(50)])
time.sleep(0.5)

client.move_to([0,0,0,0])
client.state=6

#low fan, quick movement
if client.state==6:
print("-------------state6-------------")
for j in range(len(speed5)):
client.speed_change(speed5[j])

for i in range(len(seq5)):
dmx.speed_change(fan5[i])
client.move_to(seq5[i])
time.sleep(0.5)
# time.sleep(np.random.random_sample()*3)
client.state=7

if client.state==7:
print("-------------state7-------------")

client.move_to([0,0,0,0])
for i in range(50):
client.speed_change(200)
client.move_to([-np.random.randint(50),np.random.randint(80),np.random.randint(80),-np.random.randint(50)])
time.sleep(0.5)

client.move_to([0,0,0,0])
client.state=8

if client.state==8:
print("-------------state8-------------")
for i in range(len(seq7)):
dmx.speed_change(fan7[i])
client.move_to(seq7[i])
time.sleep(4)
client.state=9

if client.state==9:
print("-------------state9-------------")
#get it to fall
dmx.speed_change(200)
client.speed_change(200)
client.move_to([0,0,0,0])
time.sleep(0.5)
client.move_to([-80,80,80,-80])
time.sleep(1)
dmx.speed_change(10)
time.sleep(3)
fanSpeed = 10
#increase fan speed
while fanSpeed < 200:
fanSpeed += 10
dmx.speed_change(fanSpeed)
time.sleep(0.5)

#spiral movements progressively get wider
pos = 0
dmx.speed_change(140)
for i in range(15):
#X moves
client.move_to([-5*i,0,0,0])
time.sleep(0.25)

#Y moves
client.move_to([0,5*i,0,0])
time.sleep(0.25)

#A moves
client.move_to([0,0,0,-5*i])
time.sleep(0.25)

#Z moves
client.move_to([0,0,5*i,0])
time.sleep(0.25)

client.state=10

if client.state==10:
print("-------------state10-------------")
# client.speed_change(100)
# client.move_to([-80,80,80,-80])
time.sleep(1.5)
client.speed_change(20)

for j in range(3):
for i in range(len(seq10)):
dmx.speed_change(fan10[i])
client.move_to(seq10[i])
time.sleep(2.5)
client.state=1

except KeyboardInterrupt:
client.move_to([0,0,0,0])
print("User interrupted motion.")

# Close the port. This will not the stop the dmx if still in motion.
dmx.close_serial_port()

# Begin the motion sequence. This may be safely interrupted by the user pressing Control-C.

# Issue a command to turn off the drivers, then shut down the connection.
client.motor_enable(False)
client.close()