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.
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.
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.
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.
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.
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
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.
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:
Gabriel Prado
The goal of this project was to build a music visualizer, Headbanger, that would incorporate marbles and light to correspond with the mood and rhythm of songs. We built a two-axis pan-tilt machine, which included a face character with colored LED lights. We choreographed the machine to two contrasting songs via motorized movements and light sequences.
We wanted to create a music visualizer to blend physical motion with creative expression. To achieve this effect, we made use of a pan/tilt mechanism and neopixels lights.
The machine could be choreographed to match diverse song types.
The combination of the pan/tilt movement and neopixels control expression of marble plate. Lights give the marble plate character and are refracted by the marbles. The pan tilt induces marble motion in sync with the music, and the marbles rhythmically roll across the plate and interact with each other.
We built a two-axis pan tilt device that utilizes DC motors for each degree of freedom. The bottom layer is the pan motor assembly. There is a 20 tooth gear on the DC motor, connected to a 40 tooth gear attached to the ring bearing case. The top of the ring bearing case is mounted onto the panning plan. We chose to use the ring bearing case so that wires could be passed through from under the pan plate. The gearing helps us achieve slower pans.
The panning plate has tabs in it for struts for the tilt mechanism. The tilt mechanism is driven by a cam, which is powered by another DC motor with a 20 tooth (on motor) to 30 tooth (on cam) gear reduction.
We choreographed two songs to Headbanger: Tourette’s by Nirvana and To Build A Home by The Cinematic Orchestra. The former is a heavy noise rock song while the latter is a soft, melodic piece. We purposefully chose opposite song types in order to provoke diverse reactions by the audience at the show.
We constructed two prototypes prior to our final build.
For our first prototype, we incorporated all the planned fundamental functionality into the build. However, we had to use LEDs instead of neopixels because there was not enough space for the neopixel wires to go through the smile platform. We also included a return spring for the smile so that it would return to its original state when the cam activated.
Unfortunately, the pan mechanism of the first prototype malfunctioned often due to the ring bearing partially breaking during the build. The tilt mechanism was more reliable, but the return spring seemed to be too strong to have any realistic control of the tilt via code.
For our second prototype, we kept the fundamental mechanisms of the first prototype and mainly focused on making the motion more reliable and controllable. To do this, we used a second iteration of the ring bearing to provide better pan motion. We also removed the pullback spring and instead glued the smile platform such that its own weight would return it to the original state. We were also able to add the neopixels by giving more room in the smile to feed their wires through.
This prototype was a major improvement from the first one in terms of reliability and motion, and we were able to run code for a song on this prototype. However, there were still slight issues in its sturdiness. The glue that fixed the smile to the tilt mechanism was not strong enough, our cam was slightly oversized, and we had to improvise a counterweight for the entire assembly.
Taking the lessons learned from the creations of our previous prototypes, we approached the final build with further mechanical refinement. We also put a lot of focus on choreographing songs via our code. The major mechanical changes we made were adjusting the cam size and its attachment to the corresponding gear mechanism as well as using trapezoidal inserts to attach the smile platform to the tilt mechanism. These changes significantly improved the reliability and performance of Headbanger. Due to time constraints, we were only able to choreograph two songs. However, there was very little trouble mechanically in achieving this.
Here is our project video demonstrating the final build:
To further improve upon this project, we could choreograph routines to more songs across different genres. We could add sensing such as an accelerometer that would allow for more fine tuned control of the pan and tilt, or photoelectric sensors that can allow the lights to change color when a marble triggers them.
We can also clean up the build in terms of loose wires and other aesthetic detail. In addition, the gear assembly could be more robust.
The following photos were taken by Professor Garth Zeglin at our final show:
Jasmine wrote all of the software and designed the pan and tilt mechanisms while Gabriel designed the smile platform, conducted wiring testing, and made the project video.
Sections of software were taken from Professor Garth’s class website:
Fall 2021 Course Guide — 16-223 Creative Kinetic Systems (cmu.edu)
The crooked smile was inspired by rock band Nirvana’s smiley logo, The CAD for it was downloaded on GrabCad, and it was designed by user Javier López.
Nirvana | 3D CAD Model Library | GrabCAD
Below is the source code. The files are:
code.py: Contains main event loop and input recorder
lights.py: Contains Light object that sets LED to various light sequences
motor.py: Contains motor objects that send power commands to two motors
timer.py: Contains timer object that gives elapsed time since start of the code running
motor_actions.py: Contains motor motion primitives and their respective key bindings, handles commanding motor actions, calls Motor object to set motor powers for a given primitive
code.py
import time import math import board import digitalio from motor_actions import MotorActions import neopixel_write import pwmio from lights import NeoPixelLights from motor import DRV8833 import remote import timer # ------------------------------------------------------------------- # Initialize motor_driver = DRV8833() lights = NeoPixelLights() remote = remote.RemoteSerial() timer = timer.Timer() class EventObject: def __init__(self, event_end): self.event_end = event_end event = EventObject(0) actions = MotorActions(motor_driver, event, timer) log_name = "log.txt" def log_input(): def keypress_handler(key_in): actions.command_action(key_in) remote.add_keypress_handler(keypress_handler) lights.sequence = lights.flash while True: lights.poll(timer.now()) remote.poll(0) actions.poll() def countdown(countdown_time): # Countdown countup_time = 0 while True: elasped_s = timer.elasped_s() if elasped_s > countup_time: print("Starting in", countdown_time - countup_time) countup_time += 1 if elasped_s > countdown_time: break def do_performance(log_name): f = open(log_name,'r',encoding = 'utf-8') event_list = [] for line in f: words = line.split() action_time = float(words[0]) action = words[1] event_list.append((action_time, action)) # ------------------------------------------------------------ # Main Event Loop timer.reset() i = 0 while True: if i % 10000 == 0: pass #print("Time elasped", elasped_s) i += 1 if timer.elasped_s() > event.event_end: motor_driver.set_pan_motor(0) motor_driver.set_tilt_motor(0) if len(event_list) > 0 and timer.elasped_s() > event_list[0][0]: next_event = event_list.pop(0) print("Event: ", next_event) # command_action will set the motor speeds and modify event.event_end actions.command_action(next_event[1]) # Lights lights.color_sequence(timer.now()) # Remote remote.poll(timer.elasped_s()) countdown(3) log_input() #do_performance(log_name)
motor.py
import time import board import digitalio import neopixel_write import math import pwmio class DRV8833(): def __init__(self, AIN1=board.GP18, AIN2=board.GP19, BIN2=board.GP20, BIN1=board.GP21, 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) def set_pan_motor(self, rate): # Pan motor is Motor A self.write(0, rate) def set_tilt_motor(self, rate): # Tilt motor is Motor B self.write(1, 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 else: if rate < 0: self.bin1.duty_cycle = pwm self.bin2.duty_cycle = 0 else: self.bin1.duty_cycle = 0 self.bin2.duty_cycle = pwm
motor_actions.py
import timer # import pdb class MotorActions: def __init__(self, motor_driver, event_obj, in_timer): self.motor_driver = motor_driver self.event_obj = event_obj self.timer = in_timer self.tilt_set_up = True self.action_lookup = { 'a': self.pan_right_slow, 'd': self.pan_left_slow, 'w': self.tilt_up, 's': self.tilt_down, 'h': self.tilt_up_left, 'j': self.tilt_up_right, 'n': self.tilt_down_left, 'm': self.tilt_down_right, 'l': self.swirl_movement } self.action_queue = [] self.action_queue_start = 0 self.action_timer = timer.Timer() self.current_action = None def poll(self): if self.current_action is not None: if self.action_timer.elasped_s() > self.current_action[2]: # Get next action if len(self.action_queue) > 0: self.set_next_action() else: self.motor_driver.set_pan_motor(0) self.motor_driver.set_tilt_motor(0) def set_next_action(self): if len(self.action_queue) > 0: self.current_action = self.action_queue.pop(0) pan, tilt, action_end = self.current_action self.motor_driver.set_pan_motor(pan) self.motor_driver.set_tilt_motor(tilt) self.action_timer.reset() def command_action(self, action_key): action = self.action_lookup.get(action_key) if action is not None: print(f"{self.timer.elasped_s()} {action_key}") action() self.set_next_action() def pan_right_slow(self): print('pan right') self.action_queue = [(-0.7, 0, 0.5)] def swirl_movement(self): if self.tilt_set_up: self.action_queue = [ (0.75, -0.8, 0.75), (0.75, 0.8, 0.75), (0.75, -0.8, 0.75) ] self.tilt_set_up = False def pan_left_slow(self): print('pan left') self.action_queue = [(0.7, 0, 0.5)] def tilt_up(self): if not self.tilt_set_up: self.tilt_set_up = True self.action_queue = [(0, 0.75, 0.6)] def tilt_down(self): if self.tilt_set_up: self.tilt_set_up = False self.action_queue = [(0, -0.75, 0.5)] def tilt_up_left(self): if not self.tilt_set_up: self.tilt_set_up = True self.action_queue = [(0.7, 0.75, 0.7)] def tilt_down_right(self): if self.tilt_set_up: self.tilt_set_up = False self.action_queue = [(-0.7, -0.75, 0.5)] def tilt_up_right(self): if not self.tilt_set_up: self.tilt_set_up = True self.action_queue = [(-0.7, 0.75, 0.7)] def tilt_down_left(self): if self.tilt_set_up: self.tilt_set_up = False self.action_queue = [(0.7, -0.75, 0.5)] def set_action(self, pan_motor, tilt_motor, time): self.motor_driver.set_pan_motor(pan_motor) self.motor_driver.set_tilt_motor(tilt_motor) self.event_obj.event_end = self.timer.elasped_s() + time
lights.py
import digitalio import neopixel_write import board class Color(): def __init__(self, r, g, b): self.r = r self.g = g self.b = b def rgb(self): return self.r, self.g, self.b RED = Color(255, 0, 0) GREEN = Color(0, 255, 0) BLUE = Color(0, 0, 255) MAGENTA = Color(255, 0, 255) YELLOW = Color(255, 255, 0) CYAN = Color(0, 255, 255) class NeoPixelLights(): def __init__(self): self.pixels = digitalio.DigitalInOut(board.GP13) self.pixels.direction = digitalio.Direction.OUTPUT self.num_pixels = 40 self.frame_buffer = bytearray(3*self.num_pixels) self.dim_factor = 50 self.sequence = self.color_sequence def set_sequence(self, sequence): self.sequence = sequence def poll(self, now): self.sequence(now) def color_sequence(self, now): # generate a temporal color sequence with each component out of phase red = int((now//11000) % 256) grn = int((now//33000) % 256) blu = int((now//55000) % 256) # print(f"{red}, {grn}, {blu}") # update the entire frame buffer including an additional position-dependent term # to create spatial variation for p in range(self.num_pixels): self.frame_buffer[3*p] = (grn + 12*p) % 256 self.frame_buffer[3*p+1] = (red + 12*p) % 256 self.frame_buffer[3*p+2] = (blu + 12*p) % 256 for i in range(self.num_pixels * 3): self.frame_buffer[i] = round(self.frame_buffer[i]/self.dim_factor) # transfer the new frame to the NeoPixel LED strand neopixel_write.neopixel_write(self.pixels, self.frame_buffer) def eye_swirl(self, now): speed = 2 red = int((now//(speed * 11000)) % 256) grn = int((now//(speed * 33000)) % 256) blu = int((now//(speed * 55000)) % 256) for i in range(self.num_pixels * 3): self.frame_buffer[i] = 0 swirl_speed = 4 for p in range(12): self.frame_buffer[3*p] = (grn + swirl_speed*12*p) % 256 self.frame_buffer[3*p+1] = (red + swirl_speed*12*p) % 256 self.frame_buffer[3*p+2] = (blu + swirl_speed*12*p) % 256 self.frame_buffer[3*(24 - p)] = (grn + swirl_speed*12*p) % 256 self.frame_buffer[3*(24 - p)+1] = (red + swirl_speed*12*p) % 256 self.frame_buffer[3*(24 - p)+2] = (blu + swirl_speed*12*p) % 256 for i in range(self.num_pixels * 3): self.frame_buffer[i] = round(self.frame_buffer[i]/self.dim_factor) # transfer the new frame to the NeoPixel LED strand neopixel_write.neopixel_write(self.pixels, self.frame_buffer) def flash(self, now): flash_on = 1000 flash_off = 1000 on = now % (flash_on + flash_off) < flash_off grn = 0 red = 255 blu = 0 if on: for p in range(self.num_pixels): self.frame_buffer[3*p] = grn self.frame_buffer[3*p+1] = red self.frame_buffer[3*p+2] = blu else: for i in range(self.num_pixels * 3): self.frame_buffer[i] = 0 for i in range(self.num_pixels * 3): self.frame_buffer[i] = round(self.frame_buffer[i]/self.dim_factor) # transfer the new frame to the NeoPixel LED strand neopixel_write.neopixel_write(self.pixels, self.frame_buffer) def set_led_rgb(self, led_num, r, g, b): self.frame_buffer[3*led_num] = (g + 12*led_num) % 256 self.frame_buffer[3*led_num+1] = (r + 12*led_num) % 256 self.frame_buffer[3*led_num+2] = (b + 12*led_num) % 256 def set_led_color(self, led_num, color): r, g, b = color.rgb self.set_led_rgb(led_num, r, g, b) def write_to_neopixel(self): for i in range(self.num_pixels * 3): self.frame_buffer[i] = round(self.frame_buffer[i]/self.dim_factor) neopixel_write.neopixel_write(self.pixels, self.frame_buffer)
timer.py
import time class Timer: def __init__(self): self.start_time = self.now_s() def reset(self): self.start_time = self.now_s() def elasped_s(self): return self.now_s() - self.start_time def now(self): return time.monotonic_ns() / 1000 def now_s(self): return self.now()/float(1000000)]]>
Our goal was to create an interactive game where a user could use marble projectiles to shoot at targets and score points, in an engaging or addictive fashion. eyeDrop Attack is an engaging, on-the-floor game that allows players to drop, throw or otherwise launch a marble projectile at any of the six eyes of the monster. Players score points based on how long the “eyes” or limit switches are pressed, which incentivizes strategies that target multiple eyes or target a single eye for a continuous period of time.
While our original goal was to create an arcade-style shooter which uses a horizontal launch trajectory to target multiple vertical targets, we ended up simplifying our vision to a top-to-bottom or horizontal target surface, to improve the feasibility of the project. By simplifying our project we were able to focus our efforts and achieve a functional marble return system, LCD text and score display, and target movement system capable of 360-degree rotation.
Our final implementation consisted of the hit registration system, the rotating movement system, and the control and scoring system. The hit registration system was designed based on a limit switch and hinge combo. This system proved difficult to implement because the original hinges we used had very loose tolerances which introduced more errors and made hit registration inconsistent. The newer metal hinges we purchased had much tighter tolerances and produced much better results.
The rotational motion system produced the most interesting challenge as we wanted to produce a system with full 360-degree rotation and electronic components on both the rotating piece and the box. We were able to solve this issue using a slip ring which allowed us to connect our rotating switches to our fixed microcontroller. This slip ring needed to be at the axis of rotation which presented an issue as this would take the place of any axle-based system we might have otherwise used. Garth’s open hub design helped us solve this problem, by allowing us to create a rotational movement system with an open center, allowing us to pass wires through the center.
We controlled the motion system by tuning the speed of the motor to suit the state of the game. In demo mode, the motor will follow a simple sinusoidal wave. During play, the motor’s speed is the sum of two sinusoidal waves of differing periods creating a randomized effect. After a hit, the motor will operate with a higher speed floor, and a shorter sinusoidal period to create the illusion that the monster is recoiling from the hit.
The electronics system also included the scoring system. The scoring system increments the score based on how long the switches on the eye have been pressed. This score is then displayed on the LCD display. The system also supports multiple games as the player can end the game by pressing the “Start/Stop” button to enter demo mode, and then pressing the “Start/Stop” button again to enter play mode once again. This action will set the score to zero.
Our main successes were the use of the slip ring, switch mounting, and the long-running nature of our project. The early incorporation of the slip ring in our design allowed us to manage our wiring in one place and, most importantly, gave us full rotation on a single axis. The switch mounting also introduced another degree of freedom by allowing us to fine-tune the hit recognition system. The mounting was critical in determining which position would offer the most consistent actuation. Another important success was the persistence of our project. The centralized design and mounting ensured the rotating mechanism was able to keep moving for relatively long periods of time.
Unexpected successes arose when new players interacted with our project. Many of our users were captivated by both the crazy visuals and bizarre mechanical behavior of our machine. Their experience was enhanced through the self-imposed difficulty of our game as well as the feedback from scoring. Over time, users developed their own game modes, one of which was a co-op mode of sorts that involved various users dropping marbles.
Jason Perez – Helped come up with ideas during brainstorming, introduced hinges as a component to the target system, developed central rotator piece through CAD, developed switch mounting through CAD, programmed initial code for testing switches-motor-LCD, graphic design/theme of the project
Michael Nguyen – created Box Full Assembly CAD model, converted code to polling states to allow for simulated multitasking, motion profiled rotator for demo, play, and freakout states, designed gearbox and debugged LCD display.
Video
Gallery
Motor Sample Code:
https://courses.ideate.cmu.edu/16-223/f2021/text/code/pico-motor.html#id7
Hub:
https://courses.ideate.cmu.edu/16-223/f2021/text/mechanism/open-hub.html
# dual_spin.py # # Raspberry Pi Pico - DC motor motion demo # # Demonstrates operating two DC motors driven by a DRV8833. # # 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 dual_spin 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 import busio import adafruit_ht16k33.segments 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, 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) 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 else: if rate < 0: self.bin1.duty_cycle = pwm self.bin2.duty_cycle = 0 else: self.bin1.duty_cycle = 0 self.bin2.duty_cycle = pwm #-------------------------------------------------------------------------------- # Create an object to represent a dual motor driver. print("Creating driver object.") driver = DRV8833() speed = 0.65 #LED led = DigitalInOut(board.LED) led.direction = Direction.OUTPUT led.value = True #7 Segment Display i2c = busio.I2C(scl=board.GP5, sda=board.GP4) display = adafruit_ht16k33.segments.Seg7x4(i2c) #Switches switchStart = DigitalInOut(board.GP9) switchStart.pull = Pull.DOWN switchStart.direction = Direction.OUTPUT wasPressed = False switch0 = DigitalInOut(board.GP10) switch0.pull = Pull.DOWN switch0.direction = Direction.OUTPUT switch1 = DigitalInOut(board.GP11) switch1.pull = Pull.DOWN switch1.direction = Direction.OUTPUT switch2 = DigitalInOut(board.GP12) switch2.pull = Pull.DOWN switch2.direction = Direction.OUTPUT switch3 = DigitalInOut(board.GP13) switch3.pull = Pull.DOWN switch3.direction = Direction.OUTPUT switch4 = DigitalInOut(board.GP14) switch4.pull = Pull.DOWN switch4.direction = Direction.OUTPUT switch5 = DigitalInOut(board.GP15) switch5.pull = Pull.DOWN switch5.direction = Direction.OUTPUT #States? demoMode = True #-------------------------------------------------------------------------------- # 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. class Game: def __init__(self): self.score = 0 self.scoreScale = 5 self.motorTime = 0.01 self.nextMotorTime = 0.01 self.demoMode = True self.wasReleased = True self.startTime = time.time() self.hurtTime = 0 def getSpeed(self,scale): currTime = time.time() - self.startTime speed = math.sin(currTime/scale) return min(max(abs(speed), 0.6), 0.8) * (speed / (abs(speed) + 0.001)) def pollMainSwitches(self): if not self.demoMode: if switch0.value or switch1.value or switch2.value or switch3.value or switch4.value or switch5.value: self.score += self.scoreScale self.hurtTime = time.time() + 1 def pollMotors(self): currTime = time.time() if (currTime >= self.nextMotorTime): self.nextMotorTime = currTime + self.motorTime if (self.demoMode): speed = self.getSpeed(2) driver.write(1, speed) elif (currTime <= self.hurtTime): speed = 4 * self.getSpeed(0.001) driver.write(1, speed) else: speed = self.getSpeed(2) driver.write(1, speed) def pollStart(self): if not switchStart.value: self.wasReleased = True if self.wasReleased and switchStart.value: self.demoMode = not self.demoMode led.value = False self.wasReleased = False def pollDisplay(self): if self.demoMode: #Press display.set_digit_raw(0, 0b01110011) #P display.set_digit_raw(1, 0b01010000) #r display[2] = "e" display[3] = "5" display.scroll(1) display[3] = "5" display.show() display.scroll(1) display[3] = " " display.show() display.scroll(1) display.set_digit_raw(3, 0b01111000) #t display.show() display.scroll(1) display.set_digit_raw(3, 0b01011100) #o display.show() display.scroll(1) display[3] = " " display.show() display.scroll(1) display[3] = "5" display.show() display.scroll(1) display.set_digit_raw(1, 0b01111000) display.show() display.scroll(1) display[3] = "a" display.show() display.scroll(1) display.set_digit_raw(3, 0b01010000) display.show() display.scroll(1) display.set_digit_raw(1, 0b01111000) display.show() display.scroll(1) display[3] = " " #Start #display[0] = "5" #display.set_digit_raw(1, 0b01111000) #display[2] = "A" #display.set_digit_raw(3, 0b01010000) #display.set_digit_raw(1, 0b01111000) else: display.print("%04d" % self.score) print("Starting main script.") scale = 2.0 startTime = time.time() game = Game(); while True: currTime = time.time() - startTime led.value = game.demoMode game.pollMotors() game.pollDisplay() game.pollMainSwitches() game.pollStart();
CAD Files:
https://drive.google.com/file/d/1EFqiocY5mWaKRPtHlE2MzSBPyFFcaLiz/view?usp=sharing
]]>https://drive.google.com/file/d/1l34aYv7CRfREJyPeC3UP6gdpZpHGwltG/view?usp=sharing
]]>Video:
https://drive.google.com/file/d/1nsp35weg4MCzrwo7Uk-kEdKQg_moJ2l0/view?usp=sharing
]]>The original intent was to create an instrument whose sound production relies on differences in materiality. The structure is a mirrored water wheel design that rotates regularly, dropping marbles on altering paddles to create sound. The first half is the control- a side of all wood paddles to provide consistency. The user is then able to alter the other half of the wheel. The first method is mechanically, physically switching the order of the materials that the ball bearings will hit. The second is through the console, where they can switch the direction that the wheel is spinning, creating an opposite descent of tones. The result is a device that can be used to experiment with multiple materials and the specific sounds that each can make.
Photo:
https://drive.google.com/file/d/1IhyuiksY9cI05_Z0S7TpE9anxvwzo7ZN/view?usp=sharing
Code:
https://drive.google.com/file/d/1l8lhA-4YXRlHUJtgQnlsOEgS9Hdo1Hcn/view?usp=sharing
]]>If we could redo aspects of this project, we would work out the spacing of the pegs better so that not all the balls would just flow down the sides when the tube is turned over slowly. We can also experiment with different modes of user input, such as the user pressing a button when they want the tube to turn. As well as implementing more interactive sensing which might give the user angle control in addition to speed control. We would also like to experiment with networking and other interfaces to increase the compatibility of the music we can create
Video: https://drive.google.com/file/d/1IrCWwKjV1mRumPzaRAGrWUcxnvIRX1Br/view?usp=sharing
# 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 dual_spin 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 import analogio import digitalio import random #-------------------------------------------------------------------------------- # 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, 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) 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 else: if rate < 0: self.bin1.duty_cycle = pwm self.bin2.duty_cycle = 0 else: self.bin1.duty_cycle = 0 self.bin2.duty_cycle = pwm #-------------------------------------------------------------------------------- # 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. # Set up built-in green LED for output. led = DigitalInOut(board.LED) # GP25 led.direction = Direction.OUTPUT # Set up an analog input on ADC0 (GP26), which is physically pin 31. # E.g., this may be attached to photocell or photointerrupter with associated pullup resistor. # sensor = analogio.AnalogIn(board.A0) sensor_direction = analogio.AnalogIn(board.A1) # These may be need to be adjusted for your particular hardware. The Pico has # 12-bit analog-to-digital conversion so the actual conversion has 4096 possible # values, but the results are scaled to a 16-bit unsigned integer with range # from 0 to 65535. lower_threshold = 8000 upper_threshold = 45000 state_index = False #switch = DigitalInOut(board.GP15) #switch.direction = Direction.INPUT last_change_time = time.time() start_time = time.time() print("Starting main script.") change_action_time = 0 prev_direction = 1 SENSOR_THRESHOLD = 1000 state_index = False while True: motor_power = 1 sensor_direction_level = sensor_direction.value if state_index is False: if sensor_direction_level < lower_threshold: led.value = True state_index = True print("On") elif state_index is True: if sensor_direction_level > upper_threshold: led.value = False state_index = False print("Off") prev_direction *= -1 driver.write(1, prev_direction * motor_power) time.sleep(0.5) driver.write(1, prev_direction * motor_power) # uncomment the following to print tuples to plot with the mu editor # print((sensor_level, motor_power)) # time.sleep(0.02) # slow sampling to avoid flooding]]>
Gabriel Prado and Emilie Zhou
Description
Our main intent for this musical instrument was to allow users to be able to change the rhythm produced by the sound of the marbles and motors. The final outcome is an instrument with three components producing different sounds and users can interact with two of those components. When the program first starts, the metronome attached to the left motor produces a short sequence of beats that indicates the start of the program. The right motor is connected to a wheel-like container and a photoreceptor sensor that controls the direction the wheel spins in. The wheel also has a track on it that holds marbles and makes another sound when the marbles hit each other. The motor produces a continuous sound as it spins but when it changes directions, it also makes a deeper, grittier sound and the rhythm of that sound can be controlled by covering and uncovering the sensor. There’s also a tower with peg attachments and dropping marbles down the tower produces a pattern of clinking sounds as the marbles bounce off of the tower attachments. Half of the peg attachments have holes in them to provide sounds of a different pitch and all the peg attachments can be rearranged to produce a desired pattern of sounds. Our initial concept also included the metronome hitting a row of marbles to produce a clearer and more distinct sound but we decided to only have the metronome moving at the beginning of the program as the metronome arm was too strong and kept knocking the marbles off the board.
]]>