#!/usr/bin/env python3
########################################################################
# Filename    : Keypad.py
# Description : The module of matrix keypad 
# Author      : freenove
# modification: 2016/07/13
########################################################################
import RPi.GPIO as GPIO 
import time
#class Key:Define some of the properties of Key
class Key(object):
    NO_KEY = '\0'
    #Defines the four states of Key
    IDLE = 0
    PRESSED = 1
    HOLD = 2
    RELEASED = 3
    #define OPEN and CLOSED
    OPEN = 0
    CLOSED =1
    #constructor
    def __init__(self):
        self.kchar = self.NO_KEY
        self.kstate = self.IDLE
        self.kcode = -1
        self.stateChanged = False

class Keypad(object):
    NULL = '\0'
    LIST_MAX = 10   #Max number of keys on the active list.
    MAPSIZE = 10    #MAPSIZE is the number of rows (times 16 columns)
    bitMap = [0]*MAPSIZE
    key = [Key()]*LIST_MAX
    holdTime = 500      #key hold time
    holdTimer = 0
    startTime = 0
    #Allows custom keymap, pin configuration, and keypad sizes.
    def __init__(self,usrKeyMap,row_Pins,col_Pins,num_Rows,num_Cols):
        GPIO.setmode(GPIO.BOARD)
        self.rowPins = row_Pins
        self.colPins = col_Pins
        self.numRows = num_Rows
        self.numCols = num_Cols
        
        self.keymap = usrKeyMap
        self.setDebounceTime(10)
    #Returns a single key only. Retained for backwards compatibility.   
    def getKey(self):
        single_key = True
        if(self.getKeys() and self.key[0].stateChanged and (self.key[0].kstate == self.key[0].PRESSED)):
            return self.key[0].kchar
        single_key = False
        return self.key[0].NO_KEY
    #Populate the key list. 
    def getKeys(self):
        keyActivity = False
        #Limit how often the keypad is scanned.
        if((time.time() - self.startTime) > self.debounceTime*0.001):
            self.scanKeys()
            keyActivity = self.updateList()
            self.startTime = time.time()
        return keyActivity
    #Hardware scan ,the result store in bitMap  
    def scanKeys(self):
        #Re-intialize the row pins. Allows sharing these pins with other hardware.
        for pin_r in self.rowPins:
            GPIO.setup(pin_r,GPIO.IN,pull_up_down = GPIO.PUD_UP)
        #bitMap stores ALL the keys that are being pressed.
        for pin_c in self.colPins:
            GPIO.setup(pin_c,GPIO.OUT)
            GPIO.output(pin_c,GPIO.LOW)
            for r in self.rowPins: #keypress is active low so invert to high.
                self.bitMap[self.rowPins.index(r)] = self.bitWrite(self.bitMap[self.rowPins.index(r)],self.colPins.index(pin_c),not GPIO.input(r))
            #Set pin to high impedance input. Effectively ends column pulse.
            GPIO.output(pin_c,GPIO.HIGH)
            GPIO.setup(pin_c,GPIO.IN)
    #Manage the list without rearranging the keys. Returns true if any keys on the list changed state.      
    def updateList(self):
        anyActivity = False
        kk = Key()
        #Delete any IDLE keys
        for i in range(self.LIST_MAX):
            if(self.key[i].kstate == kk.IDLE):
                self.key[i].kchar = kk.NO_KEY
                self.key[i].kcode = -1
                self.key[i].stateChanged = False
        # Add new keys to empty slots in the key list.
        for r in range(self.numRows):
            for c in range(self.numCols):
                button = self.bitRead(self.bitMap[r],c)
                keyChar = self.keymap[r * self.numCols +c]
                keyCode = r * self.numCols +c
                idx = self.findInList(keyCode)
                #Key is already on the list so set its next state.
                if(idx > -1):
                    self.nextKeyState(idx,button)
                #Key is NOT on the list so add it.
                if((idx == -1) and button):
                    for i in range(self.LIST_MAX):
                        if(self.key[i].kchar == kk.NO_KEY): #Find an empty slot or don't add key to list.
                            self.key[i].kchar = keyChar
                            self.key[i].kcode = keyCode
                            self.key[i].kstate = kk.IDLE    #Keys NOT on the list have an initial state of IDLE.
                            self.nextKeyState(i,button)
                            break   #Don't fill all the empty slots with the same key.
        #Report if the user changed the state of any key.
        for i in range(self.LIST_MAX):
            if(self.key[i].stateChanged):
                anyActivity = True
        return anyActivity      
    #This function is a state machine but is also used for debouncing the keys. 
    def nextKeyState(self,idx, button):
        self.key[idx].stateChanged = False
        kk = Key()
        if(self.key[idx].kstate == kk.IDLE):
            if(button == kk.CLOSED):
                self.transitionTo(idx,kk.PRESSED)
                self.holdTimer = time.time()    #Get ready for next HOLD state.
        elif(self.key[idx].kstate == kk.PRESSED):
            if((time.time() - self.holdTimer) > self.holdTime*0.001):   #Waiting for a key HOLD...  
                self.transitionTo(idx,kk.HOLD)
            elif(button == kk.OPEN):        # or for a key to be RELEASED.
                self.transitionTo(idx,kk.RELEASED)
        elif(self.key[idx].kstate == kk.HOLD):
            if(button == kk.OPEN):
                self.transitionTo(idx,kk.RELEASED)
        elif(self.key[idx].kstate == kk.RELEASED):
            self.transitionTo(idx,kk.IDLE)
            
    def transitionTo(self,idx,nextState):
        self.key[idx].kstate = nextState
        self.key[idx].stateChanged = True
    #Search by code for a key in the list of active keys.
    #Returns -1 if not found or the index into the list of active keys.
    def findInList(self,keyCode):
        for i in range(self.LIST_MAX):
            if(self.key[i].kcode == keyCode):
                return i
        return -1
    #set Debounce Time, The default is 50ms                 
    def setDebounceTime(self,ms):
        self.debounceTime = ms
    #set HoldTime,The default is 500ms
    def setHoldTime(self,ms):
        self.holdTime = ms
    #   
    def isPressed(keyChar):
        for i in range(self.LIST_MAX):
            if(self.key[i].kchar == keyChar):
                if(self.key[i].kstate == self.self.key[i].PRESSED and self.key[i].stateChanged):
                    return True
        return False
    #           
    def waitForKey():
        kk = Key()
        waitKey = kk.NO_KEY
        while(waitKey == kk.NO_KEY):
            waitKey = getKey()
        return waitKey
    
    def getState():
        return self.key[0].kstate
    #   
    def keyStateChanged():
        return self.key[0].stateChanged
    
    def bitWrite(self,x,n,b):
        if(b):
            x |= (1<<n)
        else:
            x &=(~(1<<n))
        return x
    def bitRead(self,x,n):
        if((x>>n)&1 == 1):
            return True
        else:
            return False

#######################EXAMPLE##################################        
ROWS = 4
COLS = 4
keys =  [   '1','2','3','A',
            '4','5','6','B',
            '7','8','9','C',
            '*','0','#','D'     ]
rowsPins = [12,16,18,22]
colsPins = [19,15,13,11]    

def loop():
    keypad = Keypad(keys,rowsPins,colsPins,ROWS,COLS)
    keypad.setDebounceTime(50)
    while(True):
        key = keypad.getKey()
        if(key != keypad.NULL):
            print ("You Pressed Key : %c "%(key) )
            
if __name__ == '__main__':     # Program start from here
    print ("Program is starting ... ")
    try:
        loop()
    except KeyboardInterrupt:  # When 'Ctrl+C' is pressed, the child program destroy() will be  executed.
        pass
        GPIO.cleanup()  
            
            
            
            

