# drv8833.py
#
# Raspberry Pi Pico - dual H-bridge motor driver support
#
# This module provides a class for controlling a DRV8833 dual H-bridge DC motor driver.
# This device can drive two low-power DC motor bidirectionally with variable speed.
#
# A typical usage requires four digital outputs.  The defaults 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
#
# Driver lifecycle documentation:
# https://circuitpython.readthedocs.io/en/latest/docs/design_guide.html#lifetime-and-contextmanagers
#
################################################################################
# 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

#--------------------------------------------------------------------------------
class DRV8833:
    def __init__(self,
                 AIN1=board.GP18, AIN2=board.GP19,  # control pins for motor A
                 BIN2=board.GP20, BIN1=board.GP21,  # control pins for motor B
                 pwm_rate=20000):
        """This class represents a single dual H-bridge driver.  It configures four pins
        for PWM output and can be used to control two DC motors bidirectionally
        at variable speed. 

        N.B. this does not implement any other timing process, it simply sets
        motor PWM levels but does not apply feedback, duration, or trajectory.
        """
        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 int channel:  0 for motor A, 1 for motor B
        :param float 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 or channel == 'A' or channel == 'a':
            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

    def deinit(self):
        """Manage resource release as part of object lifecycle."""
        self.ain1.deinit()
        self.ain2.deinit()
        self.bin1.deinit()
        self.bin2.deinit()
        self.ain1 = None
        self.ain2 = None
        self.bin1 = None
        self.bin2 = None
        
    def __enter__(self):
        return self

    def __exit__(self):
        # Automatically deinitializes the hardware when exiting a context. 
        self.deinit()
    
