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 tower is supposed to represent the Cathedral of Learning and other decorations were added to reflect some of the iconic aspects of Pittsburgh, like the bridges, incline, and rivers. Photo courtesy of Professor Garth.

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. 

The curved spikes help the wheel pick up marbles from the ramp before releasing them onto the path leading to the accumulator.
Users can interact with the marble run by spinning the gear to move the wheel and control how many marbles there are in the structure. Photo courtesy of Professor Garth.

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.

After getting released by the wheel, the marbles fall onto the path that connects the wheel to the accumulator. When enough marbles are present and the sensor is covered, the gate is raised to release the marbles into one of the four paths.

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. 

The tower path is in purple, the funnel path is in red, the zig zag path is in green, and the drawbridge path is in yellow. Those four paths all lead to the main path in blue that brings marbles back to 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: