Karen Abruzzo and Emilie Zhou
Abstract
Our goal for Marble City was to create a visually appealing marble-run with structural features and components based loosely on the buildings and architecture of Pittsburgh. The resulting structure has a wheel mechanism, accumulator, and four distinct paths. The cascade of events is initiated by spinning the wheel mechanism, which also provides a way for people to interact with the marble run, and results in marbles falling into four distinct paths before they are returned back to the wheel.
Objectives
We wanted to include a variety of interesting paths centered on different components and also wanted users to be able to interact with the structure in some way. To fit all of our ideas into the scope for this project, we ended up creating a marble run with four distinct paths and a hand cranked wheel mechanism that would act as a return system to move marbles back onto the paths. We used both physical and computational components throughout the structure to incorporate different kinds of motion and try to add unexpected elements of surprise into the experience of interacting with the structure.
Implementation
To bring the different elements of our project together and give us a direction to work towards, we centered our paths and pieces around a Pittsburgh city theme.
The core of our marble structure is the wheel mechanism, which was inspired by a past group’s CKS project. The wheel has curved protrusions with spiked ends and a gear attached to the back of it. The wheel can be moved by spinning the gear next to the wheel. We chose to use this mechanism because it gave us the height that we needed for our structure, as the wheel could pick up marbles collected at the bottom of the wheel and then deposit them to an accumulator near the top of the wheel. For our final implementation of the wheel mechanism, we placed a curved piece of wood in the middle of the two wheel pieces to help force marbles onto the wheel and another piece of wood at the top of the wheel that would allow marbles to roll off the wheel and onto a desired path.
For the accumulator, our initial idea was to have a 3D printed part that would have slots for marbles to roll into and then rotate to release the marbles once all the slots were filled. Due to 3D printer problems, we ended up laser printing the accumulator path and gate. Marbles are deposited by the wheel onto a small path that feeds into the accumulator path. There’s a sensor in the accumulator path floor and once the accumulator is full and the sensor is covered, the gate opens and releases the marbles down the four paths. We found that the moment when all the marbles were suddenly released was satisfying and mesmerizing to both see and hear.
There are four different paths that the marbles can fall into. Two of the paths, the tower and zig zag paths, only have physical components, while the other two paths, the funnel and drawbridge paths, have more actuation and computational pieces. We hoped to balance the physical and computational elements by using different components for the paths. For the funnel path, when a marble moves over the sensor, the funnel spins and creates a swirling motion for the marbles that land in it. Marbles in the drawbridge path will initially get stuck by a gate in the middle of the path, but when a marble moves over the sensor the gate will rotate to allow the marble to continue to the rest of the path. All four paths connect to a main path that brings the marbles back to the bottom of the wheel.
Outcomes
The use of the wheel, accumulator, and different paths were successful in creating a marble run with some satisfying and unexpected moments. We tried to create a balance between physical components and components that had more computation. We also found that some people were surprised by the accumulator release and the moving funnel and that most people liked the overall look of the structure. We were able to keep reusing most of the marbles as they cycled through the marble run, but a portion of marbles also fell off the paths or got stuck. Another problem we encountered was that the wheel didn’t continuously pick up the marbles. However, we were able to avoid complications from these problems by adding more marbles onto the marble run to make up for the marbles that were lost.
Future Work
For the next iteration of this project, we could do more testing and fine tune the paths and how the different components fit together to decrease the number of marbles that fall off the paths. We could also connect a hand crank to the gear to make it easier for people to move the wheel. Adding bridge-like cut outs to the walls of the paths could also contribute to the Pittsburgh theme of the structure and make it more visually cohesive and appealing.
Contributions
Karen: Helping throughout the stages of the project, brainstorming ideas, designing CAD pieces, assembling the project, designing the circuit, writing code
Emilie: Helping throughout the stages of the project, brainstorming ideas, designing CAD pieces and layout, assembling how the CAD components fit together, assembling the project, adding visual elements and paint, final video
Media
Citations
For our project, we were inspired by Ben Tardif’s Marble Mountain. The wheel mechanism was inspired by the Marble Maze Melody Machine made by Wenqing Yin and Hagan Miller in a previous iteration of this course. Code from Professor Garth’s website was also used as a template.
Supporting Material
Source Code:
# marble_city.py # # Raspberry Pi Pico # # # # This assumes a Pololu DRV8833 dual motor driver has been wired up to the Pico as follows: # Pico pin 24, GPIO18 -> AIN1 # Pico pin 25, GPIO19 -> AIN2 # Pico pin 26, GPIO20 -> BIN2 # Pico pin 27, GPIO21 -> BIN1 # any Pico GND -> GND # DRV8833 carrier board: https://www.pololu.com/product/2130 ################################################################ # CircuitPython module documentation: # time https://circuitpython.readthedocs.io/en/latest/shared-bindings/time/index.html # math https://circuitpython.readthedocs.io/en/latest/shared-bindings/math/index.html # board https://circuitpython.readthedocs.io/en/latest/shared-bindings/board/index.html # pwmio https://circuitpython.readthedocs.io/en/latest/shared-bindings/pwmio/index.html ################################################################################ # print a banner as reminder of what code is loaded print("Starting marble_city script.") # load standard Python modules import math, time # load the CircuitPython hardware definition module for pin definitions import board # load the CircuitPython pulse-width-modulation module for driving hardware import pwmio from digitalio import DigitalInOut, Direction, Pull #-------------------------------------------------------------------------------- # Class to represent a single dual H-bridge driver. class DRV8833(): def __init__(self, AIN1=board.GP18, AIN2=board.GP19, BIN2=board.GP20, BIN1=board.GP21, CIN2=board.GP16, CIN1=board.GP17, pwm_rate=20000): # Create a pair of PWMOut objects for each motor channel. self.ain1 = pwmio.PWMOut(AIN1, duty_cycle=0, frequency=pwm_rate) self.ain2 = pwmio.PWMOut(AIN2, duty_cycle=0, frequency=pwm_rate) self.bin1 = pwmio.PWMOut(BIN1, duty_cycle=0, frequency=pwm_rate) self.bin2 = pwmio.PWMOut(BIN2, duty_cycle=0, frequency=pwm_rate) self.cin1 = pwmio.PWMOut(CIN1, duty_cycle=0, frequency=pwm_rate) self.cin2 = pwmio.PWMOut(CIN2, duty_cycle=0, frequency=pwm_rate) def write(self, channel, rate): """Set the speed and direction on a single motor channel. :param channel: 0 for motor A, 1 for motor B :param rate: modulation value between -1.0 and 1.0, full reverse to full forward.""" # convert the rate into a 16-bit fixed point integer pwm = min(max(int(2**16 * abs(rate)), 0), 65535) if channel == 0: if rate < 0: self.ain1.duty_cycle = pwm self.ain2.duty_cycle = 0 else: self.ain1.duty_cycle = 0 self.ain2.duty_cycle = pwm elif channel == 1: if rate < 0: self.bin1.duty_cycle = pwm self.bin2.duty_cycle = 0 else: self.bin1.duty_cycle = 0 self.bin2.duty_cycle = pwm else: if rate < 0: self.cin1.duty_cycle = pwm self.cin2.duty_cycle = 0 else: self.cin1.duty_cycle = 0 self.cin2.duty_cycle = pwm class BridgeSensor(): def __init__(self, SWITCH=board.GP15): self.input = DigitalInOut(SWITCH) self.input.direction = Direction.INPUT self.state_index = False self.sampling_interval = 10000000 # period of 100 Hz in nanoseconds self.sampling_timer = 0 self.switch = False def poll(self, elapsed): """Polling function to be called as frequently as possible from the event loop to read and process new samples. :param elapsed: nanoseconds elapsed since the last cycle. """ self.sampling_timer -= elapsed if self.sampling_timer < 0: self.sampling_timer += self.sampling_interval self.switch = self.input.value class FunnelSensor(): def __init__(self, SWITCH=board.GP14): self.input = DigitalInOut(SWITCH) self.input.direction = Direction.INPUT self.state_index = False self.sampling_interval = 10000000 # period of 100 Hz in nanoseconds self.sampling_timer = 0 self.switch = False def poll(self, elapsed): """Polling function to be called as frequently as possible from the event loop to read and process new samples. :param elapsed: nanoseconds elapsed since the last cycle. """ self.sampling_timer -= elapsed if self.sampling_timer < 0: self.sampling_timer += self.sampling_interval self.switch = self.input.value class StartSensor(): def __init__(self, SWITCH=board.GP13): self.input = DigitalInOut(SWITCH) self.input.direction = Direction.INPUT self.state_index = False self.sampling_interval = 10000000 # period of 100 Hz in nanoseconds self.sampling_timer = 0 self.switch = False def poll(self, elapsed): """Polling function to be called as frequently as possible from the event loop to read and process new samples. :param elapsed: nanoseconds elapsed since the last cycle. """ self.sampling_timer -= elapsed if self.sampling_timer < 0: self.sampling_timer += self.sampling_interval self.switch = self.input.value class Motor: def __init__(self, channel, rate, timeOpen, timeWait, timeClose, timeWait2): self.update_timer = 0 self.timeOpen = timeOpen self.timeWait = timeWait self.timeWait2 = timeWait2 self.timeClose = timeClose self.isOn = False self.channel = channel self.rate = rate self.time = time def poll(self, elapsed): """Polling function to be called as frequently as possible from the event loop with the nanoseconds elapsed since the last cycle.""" if (self.isOn): self.update_timer += elapsed if self.update_timer < self.timeOpen: driver.write(self.channel, self.rate) #print("opening") elif self.update_timer < self.timeWait: driver.write(self.channel, 0) #print("waiting") elif self.update_timer < self.timeClose: driver.write(self.channel, -(self.rate)) #print("closing") elif self.update_timer < self.timeWait2: driver.write(self.channel, 0) #print("closing") else: driver.write(self.channel, 0) self.update_timer = 0 self.isOn = False #print("done") #-------------------------------------------------------------------------------- # Create an object to represent a dual motor driver. print("Creating driver object.") driver = DRV8833() #-------------------------------------------------------------------------------- # Begin the main processing loop. This is structured as a looping script, since # each movement primitive 'blocks', i.e. doesn't return until the action is # finished. bridge = BridgeSensor() funnel = FunnelSensor() start = StartSensor() motorBridge = Motor(0, 1.1, 21000000, 1500000000, 1520800000, 1525000000) gateBridge = Motor(1, -1, 70000000, 3000000000, 3070000000, 0) funnelBridge = Motor(2, -0.85, 2650000000, 0, 0, 0) gateBridge.isOn = False print("Starting main script.") last_clock = time.monotonic_ns() while True: #print(gateBridge.update_timer); now = time.monotonic_ns() elapsed = now - last_clock last_clock = now bridge.poll(elapsed) motorBridge.poll(elapsed) gateBridge.poll(elapsed) funnelBridge.poll(elapsed) start.poll(elapsed) funnel.poll(elapsed) if bridge.state_index is False: if bridge.switch is True: #led.value = True bridge.state_index = True #print("On") elif bridge.state_index is True: if bridge.switch is False: #led.value = False bridge.state_index = False #print("Off") motorBridge.isOn = True gateBridge.poll(elapsed) if funnel.state_index is False: if funnel.switch is True: #led.value = True funnel.state_index = True print("On") elif funnel.state_index is True: if funnel.switch is False: #led.value = False funnel.state_index = False funnelBridge.isOn = True print("Off") if start.state_index is False: if start.switch is True: #led.value = True start.state_index = True #print("On") elif start.state_index is True: if start.switch is False: #led.value = False start.state_index = False gateBridge.isOn = True #rint("Off")
CAD:
Drawings:
Leave a Reply
You must be logged in to post a comment.