# spin_player.py

# Raspberry Pi Pico - step sequencer demo using DC motors.

# This sample demonstrates mapping step sequencer events to DC motor
# activations.  This assumes a DRV8833 driver has been attached to the default
# pins.  Please see the drv8833 module documentation for details.

#--------------------------------------------------------------------------------
# Import standard modules.
import time

# Load the CircuitPython hardware definition module for pin definitions.
import board

# Import course modules.  These files should be copied to the top-level
# directory of the CIRCUITPY filesystem on the Pico.
import drv8833
import sequencer
import remote

#---------------------------------------------------------------
class BeatMotor:
    def __init__(self, motor):
        """Create musical beats on two DC motors using sequencer event callbacks.  This
         will need to be customized to produce appropriate movements for the
         attached mechanical hardware.

        :param drv8833 motor: motor driver object to use for output
        """
        self.motor = motor
        self.timeout_timer = 0       # nanosecond timer for timing out motions

    def note_event(self, char):
        """Callback to receive sequencer events encoded as single characters."""

        # Whitespace or period will be treated as a rest.
        if (char.isspace() or char == '.'):
            self.motor.write(0, 0.0)
            self.motor.write(1, 0.0)
        else:
            # Use the character value to set the movement magnitude.  This could
            # be considerably elaborated to produce varied motions based on the
            # 'note' value.
            if char in '+abcdefg':
                self.motor.write(0, 0.75)
                self.motor.write(1, -0.75)
                self.timeout_timer = int(1e9)
            else:
                self.motor.write(0, -1.0)
                self.motor.write(1, 1.0)
                self.timeout_timer = int(1e9)                

    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."""

        # Apply a duration limit to any movement.
        if self.timeout_timer > 0:
            self.timeout_timer -= elapsed
            if self.timeout_timer <= 0:
                self.timeout_timer = 0
                self.motor.write(0, 0.0)
                self.motor.write(1, 0.0)

#--------------------------------------------------------------------------------
# Create an object to represent the motor driver on the default hardware pins.
motor = drv8833.DRV8833()

# Create beat motion control connected to the servo.
motion = BeatMotor(motor)

# Create a sequencer and connect it to the motor motion control.
sequencer = sequencer.Sequencer()
sequencer.set_note_handler(motion.note_event)

# Set a test pattern to loop.
sequencer.set_pattern("#   #   #   +   +   + + +   #+++")   

# Set up communication interface and callbacks.
remote  = remote.RemoteSerial()

def default_handler(msgtype, *args):
    print(f"Warning: received unknown message {msgtype} {args}")

remote.add_default_handler(default_handler)    
remote.add_handler('tempo', sequencer.set_tempo)
remote.add_handler('pattern', sequencer.set_pattern)

#---------------------------------------------------------------
# Main event loop to run each non-preemptive thread.

last_clock = time.monotonic_ns()

while True:
    # read the current nanosecond clock
    now = time.monotonic_ns()
    elapsed = now - last_clock
    last_clock = now

    # poll each thread
    remote.poll(elapsed)    
    sequencer.poll(elapsed)    
    motion.poll(elapsed)
