# youknow.py
#
# Library for use with EduShields YouKnow board for layering on top of
#   MrYsLab's PyMata API for SJSU ME30.
# Provides interfaces to each of the board's components using objects for each
#  rather than the lower-level Arduino-like API.
#
# Version history:
#   1.3 31-Jan-2019 add GPIO_In and GPIO_Out classes
#   1.2 31-Oct-2018 add ButtonAsync
#                   change Button(internalPull) to True/False from 0/!0
#   1.1 24-Sep-2018 Added optional parameter to Led constructor for "activeLevel"
#   1.0 01-Jan-2018 Initial Revision (eric@edushields.com)

from pymata_aio.constants import Constants

# Digital pin assignments
PIN_SW0     = 12
PIN_SW1     =  8
PIN_SW2     =  7
PIN_SW3     =  4
PIN_LED0    = 11
PIN_LED1    =  9
PIN_LED2    =  6
PIN_LED3    =  3
PIN_SPEAKER =  5
PIN_SERVO   = 10
PIN_LASER   = 13
PIN_ENC_CH1 = 17
PIN_ENC_CH2 = 18
PIN_ENC_SW  = 19

PIN_RGB_RED    = 11
PIN_RGB_GREEN  =  9
PIN_RGB_BLUE   =  6
PIN_RGB_RED_V3 =  3 # location of RED LED on version<4 boards

PIN_TEMP = 0
PIN_POT  = 1
PIN_ALS  = 2

class Button:
    """Class for the (four) momentary tactile buttons
    
    Note: The buttons on the YouKnow are activeLevel=0 and require internalPull=true
    """

    def __init__(self, board, pin, activeLevel=1, internalPull=False, bounceDelay=0.002):
        """Create button object connected to the specified Arduino pin
    
        Required arguments:
        board -- the PyMata3 object for the board
        pin -- the Arduino pin number to which the button is connected, usually PIN_SWn
    
        Keyword arguments:
        activeLevel -- 0 if button is wired active-low, 1 if active-high (default: 1)
        internalPull -- true to turn on internal pullup/down resistors to opposite of activeLevel, false otherwise (default: false)
        bounceDelay -- period of time (in seconds) between samples to mitigate switch bounce (default: 0.002)
        """
        self.board       = board
        self.pin         = pin
        self.activeLevel = activeLevel
        self.bounceDelay = bounceDelay

        board.set_pin_mode(pin, Constants.INPUT)
        if internalPull:
            if self.activeLevel == 0:
                board.digital_write(pin, 1)  # on ATmegas this turns on internal pullup resistor
            else:
                # as-is this module only supports MCUs with internal pullups (not pulldowns)
                raise valueError("pulldown incapable")
        else:
            return

    def isPressed(self):
        """Detect (software-debounced) button press, blocking for >= self.bounceDelay"""
        while True:
            sample1 = self.board.digital_read(self.pin)
            self.board.sleep(self.bounceDelay)
            sample2 = self.board.digital_read(self.pin)

            if (sample1 != self.activeLevel) and (sample2 != self.activeLevel):
                return False
            if (sample1 == self.activeLevel) and (sample2 == self.activeLevel):
                return True

class Led:
    """Class for a single-channel LED
    
    Note: All LEDs on the YouKnow are activeLevel=1
    """

    def __init__(self, board, pin, activeLevel=1):
        """Create LED object connected to the specified Arduino pin
    
        Required arguments:
        board -- the PyMata3 object for the board
        pin -- the Arduino pin number to which the LED is connected, usually PIN_LEDn or PIN_RGB_xxx
    
        Keyword arguments:
        activeLevel -- 0 if LED is wired active-low, 1 if active-high (default: 1)
        """
        self.board       = board
        self.pin         = pin
        self.activeLevel = activeLevel

        board.set_pin_mode(pin, Constants.OUTPUT)

    def on(self, val=True):
        """Turn on (or off) LED
        
        Keyword arguments:
        val -- True to turn on LED, False to turn off (default: True)
        """
        if self.activeLevel == 0:
            self.board.digital_write(self.pin, 0)
        else:
            self.board.digital_write(self.pin, 1)

    def off(self):
        """Turn off LED"""
        if (self.activeLevel == 0):
            self.board.digital_write(self.pin, 1)
        else:
            self.board.digital_write(self.pin, 0)

class LedPWM:
    """Class for a single-channel LED with PWM capability

    PWM resolution is 2 ** analogOutBits.
    Note: All LEDs on the YouKnow are connected to PWM-able pins
    """

    def __init__(self, board, pin, analogOutBits, activeLevel=1):
        """Create PWM-able LED object connected to the specified Arduino pin
    
        Required arguments:
        board -- the PyMata3 object for the board
        pin -- the Arduino pin number to which the LED is connected, usually PIN_LEDn or PIN_RGB_xxx
        analogOutBits -- number of bits of resolution of PWM peripheral (8 for the Arduino Uno)

        Keyword arguments:
        activeLevel -- 0 if LED is wired active-low, 1 if active-high (default: 1)
        """
        self.board        = board
        self.pin          = pin
        self.analogOutMax = (2**analogOutBits)-1
        self.activeLevel  = activeLevel

        board.set_pin_mode(pin, Constants.PWM)

    def on(self, val=True):
        """Turn on LED to full brightness"""
        if self.activeLevel == 0:
            if val:
                self.board.analog_write(self.pin, 0)
            else:
                self.board.analog_write(self.pin, self.analogOutMax)
        else:
            if val:
                self.board.analog_write(self.pin, self.analogOutMax)
            else:
                self.board.analog_write(self.pin, 0)
            
    def off(self):
        """Turn off LED"""
        if self.activeLevel == 0:
            self.board.analog_write(self.pin, self.analogOutMax)
        else:
            self.board.analog_write(self.pin, 0)
        
    def fraction(self, fract):
        """Turn on LED to fractional brightness"""
        if self.activeLevel == 0:
            self.board.analog_write(self.pin, int(self.analogOutMax*(1.0-fract)))
        else:
            self.board.analog_write(self.pin, int(self.analogOutMax*fract))

class Encoder:
    """Class for two-channel rotary encoder"""

    def __init__(self, board, enc1, enc2):
        """Create encoder object connected to the specified (two) Arduino pins
    
        Required arguments:
        board -- the PyMata3 object for the board
        enc1 -- pin to which one input channel of encoder is connected, usually PIN_ENC_CH1
        enc2 -- pin to which the other input channel of encoder is connected, usually PIN_ENC_CH2
        """
        self.board = board
        self.ch1   = enc1
        self.ch2   = enc2
        self.cnt   = 0
        board.encoder_config(enc1, enc2, self.cb_encoder, Constants.CB_TYPE_DIRECT)
        
    def cb_encoder(self, data):
        """Private, internal callback function"""
        if (type(data) != list):     # strangely returns list at start
            self.cnt = data

    def count(self):
        """Return (absolute) running count of encoder"""
        return self.cnt

class Pot:
    """Class for potentiometer"""

    def __init__(self, board, pin, analogInBits):
        """Create potentiometer object connected to the specified Arduino ADC pin
    
        Required arguments:
        board -- the PyMata3 object for the board
        pin -- the Arduino pin number to which the LED is connected, usually PIN_POT
        analogInBits -- number of bits of resolution of ADC peripheral (10 for the Arduino Uno)
        """
        self.board       = board
        self.pin         = pin
        self.analogInMax = (2**analogInBits)-1

        board.set_pin_mode(pin, Constants.ANALOG)

    def fraction(self):
        """Returns value between 0.0 and 1.0 denoting position"""
        return self.board.analog_read(self.pin)/self.analogInMax

class ALS:
    """Class for ambient light sensor"""

    def __init__(self, board, pin, analogInBits):
        """Create ALS (ambient light sensor) object connected to the specified Arduino ADC pin
    
        Required arguments:
        board -- the PyMata3 object for the board
        pin -- the Arduino pin number to which the LED is connected, usually PIN_ALS
        analogInBits -- number of bits of resolution of ADC peripheral (10 for the Arduino Uno)
        """
        self.board       = board
        self.pin         = pin
        self.analogInMax = (2**analogInBits)-1

        board.set_pin_mode(pin, Constants.ANALOG)

    def fraction(self):
        """Returns value between 0.0 and 1.0 denoting position"""
        return self.board.analog_read(self.pin)/self.analogInMax

class Servo:
    """Class for servo motor"""

    def __init__(self, board, pin):
        """Create servo motor object connected to the specified Arduino pin
    
        Required arguments:
        board -- the PyMata3 object for the board
        pin -- the Arduino pin number to which the LED is connected, usually PIN_SERVO
        """
        self.board = board
        self.pin   = pin
        board.servo_config(pin)

    def write(self, degrees):
        """Move servo to specified angle, in degrees (usually between 0 and 180)"""
        self.board.analog_write(self.pin, int(degrees))

class LM34:
    """Class for (10mV/degF) temperature sensor"""

    def __init__(self, board, pin, volts_per_adc_count):
        """Create servo motor object connected to the specified Arduino pin
    
        Required arguments:
        board -- the PyMata3 object for the board
        pin -- the Arduino pin number to which the LED is connected, usually PIN_TEMP
        volts_per_adc_count -- volts per ADC count (on a 5V Arduino Uno, use (5.0/(2**10))
        """
        self.board     = board
        self.pin       = pin
        self.v_per_adc = volts_per_adc_count
        board.set_pin_mode(pin, Constants.ANALOG)

    def degreesF(self):
        """Return degrees in Fahrenheit"""
        adc = self.board.analog_read(self.pin)
        return (adc*self.v_per_adc)/.010 # 0.010V/degF

class Speaker:
    """Class for speaker"""

    def __init__(self, board, pin):
        """Create speaker object connected to the specified Arduino pin
    
        Required arguments:
        board -- the PyMata3 object for the board
        pin -- the Arduino pin number to which the LED is connected, usually PIN_TEMP
        """
        self.board = board
        self.pin   = pin
    
    def play(self, freq, duration=None):
        """Play tone
        
        Required arguments:
        freq -- frequency
        duration -- duration of tone in seconds (default: forever)
        """ 
        self.board.play_tone(self.pin, Constants.TONE_TONE, freq, duration)

    def playStop(self):
        self.board.play_tone(self.pin, Constants.TONE_NO_TONE, 0)

class GPIO_In:
    """Class for a generic GPIO input pin
    """

    def __init__(self, board, pin, internalPull=0):
        """Create GPIO Input object connected to the specified Arduino pin
    
        Required arguments:
        board -- the PyMata3 object for the board
        pin -- the Arduino pin number
    
        Keyword arguments:
        internalPull -- true to turn on internal pullup/down resistors (0=none, 1=up, -1=down (default: 0)
        """
        self.board        = board
        self.pin          = pin
        self.internalPull = internalPull

        board.set_pin_mode(pin, Constants.INPUT)

        if internalPull == 0:
            pass
        elif internalPull == 1:
            board.digital_write(pin, 1)  # on ATmegas this turns on internal pullup resistor
        elif internalPull == -1:
            # as-is this module only supports MCUs with no internal pulldown resistors
            raise valueError("pulldown incapable")
        else:
            raise valueError("internalPull")

    def value(self):
        """Return current state of GPIO input pin (True=high, False=low)"""
        val = self.board.digital_read(self.pin)
        if val == 0:
            return False
        else:
            return True

    def pullUp(self, on=True):
        """Turn on/off pullup resistor manually"""
        if (on):
            board.digital_write(pin, 1)  # on ATmegas this turns on internal pullup resistor
        else:
            board.digital_write(pin, 0)  # on ATmegas this turns off internal pullup resistor

class GPIO_Out:
    """Class for a generic GPIO output pin
    """

    def __init__(self, board, pin):
        """Create GPIO Output object connected to the specified Arduino pin
    
        Required arguments:
        board -- the PyMata3 object for the board
        pin -- the Arduino pin number
        """
        self.board = board
        self.pin   = pin

        board.set_pin_mode(pin, Constants.OUTPUT)

    def value(self, on):
        """Set current state of GPIO output pin (True=high, False=low)"""
        if (on):
            self.board.digital_write(self.pin, 1)
        else:
            self.board.digital_write(self.pin, 0)
