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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
#!/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.
import serial
import numpy as np
 
# This requires a pySerial installation.
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()