Jason Perez, Gabriel Prado, Michael Nguyen

Statement

Our initial concept was to create a balancing game where the player had to keep as many marbles in the see-saw as they could. We decided to make an accelerometer the player input for controlling the see-saw movement. A DC motor was used to actuate the see-saw. We also wanted to include levels of difficulty for the player through random bias in the motor rotation that fought with the input. After our first iteration, we decided to add some feedback in the form of LEDs that signaled to the player the current difficulty. We also added a funnel that could be used to insert more marbles into the game. Our final outcome was a marble balancing game with 3 levels of difficulty that progress through time and increase the random bias in the system.

Code

# demo.py

# Raspberry Pi Pico - Signal Processing Demo

# Read an analog input with the ADC, apply various filters, and print filtered
# data to the console for plotting.

# Import CircuitPython modules.
import board
import math
import pwmio
import time
import analogio
import digitalio
import random

# Import every filter sample.  These files should be copied to the top-level
# directory of the CIRCUITPY filesystem on the Pico.

import biquad
import hysteresis
import linear
import median
import smoothing
import statistics

#---------------------------------------------------------------
# Set up the hardware.

# 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)
red = digitalio.DigitalInOut(board.GP3)
red.direction = digitalio.Direction.OUTPUT
yellow = digitalio.DigitalInOut(board.GP2)
yellow.direction = digitalio.Direction.OUTPUT
green = digitalio.DigitalInOut(board.GP4)
green.direction = digitalio.Direction.OUTPUT

red.value = True
yellow.value = True
green.value = True
#---------------------------------------------------------------
# Initialize filter objects as global variables.

stats        = statistics.CentralMeasures()
hysteresis   = hysteresis.Hysteresis(lower=0.25, upper=0.75)
average      = smoothing.MovingAverage()
smoothing    = smoothing.Smoothing()
median       = median.MedianFilter()
lowpass      = biquad.BiquadFilter(biquad.low_pass_10_1)
highpass     = biquad.BiquadFilter(biquad.high_pass_10_1)
bandpass     = biquad.BiquadFilter(biquad.band_pass_10_1)
bandstop     = biquad.BiquadFilter(biquad.band_stop_10_1)

# Collect all the filters in a list for efficient multiple updates.

all_filters = [ stats, hysteresis, average, smoothing, median,
                lowpass, highpass, bandpass, bandstop, ]

#---------------------------------------------------------------
# Run the main event loop.

# Use the high-precision clock to regulate a precise *average* sampling rate.
sampling_interval  = 100000000           # 0.1 sec period of 10 Hz in nanoseconds
next_sample_time   = time.monotonic_ns()

#--------------------------------------------------------------------------------
# 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


red_time = 15
yellow_time = 5

driver = DRV8833()
startTime = time.time()
while True:
    # read the current nanosecond clock
    now = time.monotonic_ns()
    current = time.time() - startTime
    if current < yellow_time:
        k_random = 0.00
        green.value = True
        red.value = False
        yellow.value = False
    elif current < red_time:
        k_random = 0.3
        green.value = False
        red.value = False
        yellow.value = True
    else:
        k_random = 0.6
        green.value = False
        red.value = True
        yellow.value = False
    if now &gt;= next_sample_time:
        # Advance the next event time; by spacing out the timestamps at precise
        # intervals, the individual sample times may have 'jitter', but the
        # average rate will be exact.
        next_sample_time += sampling_interval

        # Read the sensor once per sampling cycle.
        raw = sensor.value

        # Apply calibration to map integer ADC values to meaningful units.  The
        # exact scaling and offset will depend on both the individual device and
        # application.
        calib  = linear.map(raw, 59000, 4000, -4.0, 4.0) + 0.19

        # Pipe the calibrated value through all the filters.
        #filtered = [filt.update(calib) for filt in all_filters]
        #filtered = median.update(calib)
        filtered = calib
        if filtered &gt; -0.09 and filtered < 0.09:
            out = 0
        else:
            out = max(math.log(abs(filtered) + 0.9)+ 0.4, 0.55)
            out = min(out, 0.9) * (filtered / (abs(filtered) + 1e-10))
        # Selectively report results for plotting.
        # print((raw, calib))                            # raw and calibrated input signal
        # print((calib, filtered[0][0], filtered[0][1])) # calibrated, average, variance
        # print((calib, 1*filtered[1], filtered[2]))     # calibrated, thresholded, moving average
        print((calib, filtered, out))         # calibrated, smoothed, median-filtered
        # print((calib, filtered[5], filtered[6]))       # calibrated, low-pass, high-pass
        # print((calib, filtered[7], filtered[8]))       # calibrated, band-pass, band-stop
    rand_bias = 2*random.random() - 1; #random number from -1 to 1
    if out == 0 and current &gt;= red_time:
        driver.write(1,rand_bias)
    else:
        driver.write(1, k_random * rand_bias + (1 - k_random) * -out )