########################################################################
#
# PixelClock.py
#
########################################################################
#
# What is a Pixel Clock? I define a Pixel Clock as an electronic
# timepiece that displays the current time in a non-traditional way,
# using pixels in a unique format (and not using alpha-numeric
# characters).
#
########################################################################
#
# MIT Licencse
#
# Copyright(c) 2016, Jason Matthew Hale
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation file (the
# "Software"), to deal in the Software without restricution, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject
# to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OR CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
########################################################################

import random
import sched
import time
from math import floor
import smbus
import logging

# /dev/i2c-0 = 0 for port I2CO, /dev/i2c-1 = 1 for port (I2C1)
# If your RPi supports 512MB or 1GB, then I2C is mapped to port 1.
# If your RPi supports 256MB, then I2C is mapped to port 0.
bus = smbus.SMBus(1)

DEVICE_ADDRESS = 0x70
DEVICE_REG_MODE1 = 0x00
DEVICE_REG_SYSSETUP = 0x20
DEVICE_REG_LEDOUT = 0x1d

# This array contains brightness settings for the LED Matrix.
# The first value, 0xE0, is the dimmest setting.
# The last value, 0xE7, is the brightest setting. 
brightnessArray = [0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7];

# The H_Array represents the pixel display for the hour digit, the 10's
# minute digit and the 1's minute digit.  In this implementation, the
# pixel display is consistent regardless of digit (and color).
# The first value represents the digit, '0' (no pixels).
# The last value represents the digit, '12' (almost all pixels).
H_Array = [ [ [0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0] ],
            [ [1,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0] ],
            [ [1,0],[0,1],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0] ],
            [ [1,0],[1,1],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0] ],
            [ [1,1],[1,1],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0] ],
            [ [1,1],[1,1],[0,0],[1,0],[0,0],[0,0],[0,0],[0,0] ],
            [ [1,1],[1,1],[0,0],[1,0],[0,1],[0,0],[0,0],[0,0] ],
            [ [1,1],[1,1],[0,0],[1,0],[1,1],[0,0],[0,0],[0,0] ],
            [ [1,1],[1,1],[0,0],[1,1],[1,1],[0,0],[0,0],[0,0] ],
            [ [1,1],[1,1],[0,0],[1,1],[1,1],[0,0],[1,0],[0,0] ],
            [ [1,1],[1,1],[0,0],[1,1],[1,1],[0,0],[1,0],[1,0] ],
            [ [1,1],[1,1],[0,0],[1,1],[1,1],[0,0],[1,0],[1,1] ],
            [ [1,1],[1,1],[0,0],[1,1],[1,1],[0,0],[1,1],[1,1] ]  ]

# The H_ArraySize represents the 'height' of the pixel display, where
# 'height' means the number of pixel rows between the top and bottom
# of the pixel display including blank rows.
# For example, the digit, '5', has a 'height' of '4' and a width
# of '2'. Graphically, the digit '5', is represented as:
# "X "
# "  "
# "XX"
# "XX"
# So this array, H_ArraySize, must match the array, H_Array.
H_ArraySize = [ [0,2], [1,2], [2,2], [2,2], [2,2], [4,2], [5,2], [5,2], [5,2], [7,2], [8,2], [8,2], [8,2] ]

# Define and clear the display buffer by writing a blank space in each
# 'pixel'. The valid contents of the display buffer is in the set
# {" ", "G" and "R"}, where "G" means 'Green Pixel', "R" means
# 'Red Pixel' and " " means 'No Pixel'. 
myDispBuffer = [[" "," "," "," "," "," "," "," "],
                [" "," "," "," "," "," "," "," "],
                [" "," "," "," "," "," "," "," "],
                [" "," "," "," "," "," "," "," "],
                [" "," "," "," "," "," "," "," "],
                [" "," "," "," "," "," "," "," "],
                [" "," "," "," "," "," "," "," "],
                [" "," "," "," "," "," "," "," "] ]

########################################################################
#
# The Python scheduler calls this function when its timer expires. This
# is the central function in this Python script. 
#
########################################################################
def my_sched_event1():
    """Determine the current hour and minute from the system."""
    curHour = time.strftime("%I")
    curMinu = time.strftime("%M")

    # Display the current time on the console as a reference.
    MyLog.debug("The current time is " + curHour + ":" + curMinu)
    
    # Clear the display buffer by writing a blank space in each 'pixel'.
    myDispBuffer = [[" "," "," "," "," "," "," "," "],
                    [" "," "," "," "," "," "," "," "],
                    [" "," "," "," "," "," "," "," "],
                    [" "," "," "," "," "," "," "," "],
                    [" "," "," "," "," "," "," "," "],
                    [" "," "," "," "," "," "," "," "],
                    [" "," "," "," "," "," "," "," "],
                    [" "," "," "," "," "," "," "," "] ]
    
    # Look-up the number of rows and columns for the current hour.
    numrows = H_ArraySize[int(curHour)][0]
    numcols = H_ArraySize[int(curHour)][1]

    # Randomly select a valid starting row at which the pattern
    # will be drawn.
    myStartingRow = random.randint(0,8-numrows)

    MyLog.debug("curHour=" + curHour + " numrows=" + str(numrows) +
                " numcols=" + str(numcols) + " myStartingRow=" +
                str(myStartingRow))

    # Draw the pattern for the current hour in Green.
    for row in range (0,numrows):
        for col in range (0,numcols):
            if H_Array[int(curHour)][row][col]==1:
                myDispBuffer[8-1-myStartingRow-row][col]="G"

    # Extract the 10's minute digit from the range [0,50] using the
    # 'floor' function and division by 10.
    myTensMinuteHand = int(floor(float(curMinu)/10.0))

    # Look-up the number of row and columns for the current 10's minute.   
    numrows = H_ArraySize[myTensMinuteHand][0]
    numcols = H_ArraySize[myTensMinuteHand][1]

    MyLog.debug("curMinu=" + curMinu + " Ten's Minute=" +
                str(myTensMinuteHand) + " numrows=" + str(numrows) +
                " numcols=" + str(numcols))

    # Randomly select a valid starting row at which the pattern
    # will be drawn.
    myStartingRow = random.randint(0,8-numrows)

    # Draw the pattern for the current 10's minute in Red.
    for row in range (0,numrows):
        for col in range (0,numcols):
            if H_Array[myTensMinuteHand][row][col]==1:
                myDispBuffer[8-1-myStartingRow-row][col+3]="R"

    # Extract the 1's minute digit from the range [0,59] by
    # decreasing in 10min increments.
    myOnesMinuteHand = int(curMinu)
    while ( (myOnesMinuteHand >=0) and (myOnesMinuteHand >= 10) ):
        myOnesMinuteHand -= 10

    # Look-up the number of row and columns for the current 1's minute.
    numrows = H_ArraySize[myOnesMinuteHand][0]
    numcols = H_ArraySize[myOnesMinuteHand][1]
    
    MyLog.debug("curMinu=" + curMinu + " One's Minute=" +
                str(myOnesMinuteHand) + " numrows=" + str(numrows) +
                " numcols=" + str(numcols))

    # Randomly select a valid starting row at which the pattern
    # will be drawn.
    myStartingRow = random.randint(0,8-numrows)

    # Draw the pattern for the current 1's minute in Red.
    for row in range (0,numrows):
        for col in range (0,numcols):
            if H_Array[myOnesMinuteHand][row][col]==1:
                myDispBuffer[8-1-myStartingRow-row][col+6]="R"

    # Now that the display buffer is full, it must be translated to
    # the 8x8 Bi-Color LED Matrix. The result will become visible
    # on the matrix.
    translate_dispbuf_to_8x8LED(myDispBuffer)

    # END my_sched_event1()

########################################################################
#
# This function translates the display buffer to the 8x8 Bi-Color LED
# Matrix. This function accesses the RPi GPIO.
#
########################################################################
def translate_dispbuf_to_8x8LED(myDispBuffer):
    """Translate the contents of the display buffer to the 8x8 LED Matrix."""

    # This variable holds the current I2C memory address for the 8x8
    # LED Matrix.  The initial value is 0x00 and will increment.
    address = 0x00

    # Define and clear an array whose contents will be sent to the
    # connected 8x8 Bi-Color LED Matrix.
    # Here, the upper bit range, 0x0F00, holds the Green data.
    # Here, the lower bit range, 0x000F, holds the Red data.
    # Any overlapping data will result in Yellow data
    #(i.e., Green and Red are simultaneously illuminated).
    LEDArray = [0x0000,
                0x0000,
                0x0000,
                0x0000,
                0x0000,
                0x0000,
                0x0000,
                0x0000]

    for row in range (0,8):
        MyLog.debug(myDispBuffer[row])
        
    # Translate the display buffer into the array, LEDArray. 
    for matrow in range (0,8):
        for matcol in range (0,8):
            if myDispBuffer[7-matrow][7-matcol] == "G":
                MyLog.debug("Found a Green... matrow=" + str(matrow) +
                            " matcol=" + str(matcol) +
                            " ((2**matcol)<<8=" + str(((2**matcol)<<8)))
                LEDArray[matrow] |= ((2**matcol)<<8)
            elif myDispBuffer[7-matrow][7-matcol] == "R":
                MyLog.debug("Found a Red...  matrow=" + str(matrow) +
                            " matcol=" + str(matcol) +
                            " (2**matcol)=" + str((2**matcol)))
                LEDArray[matrow] |= (2**matcol)

    # Send the contents of the array, LEDArray, to the connected
    # 8x8 Bi-Color LED Matrix via I2C.
    for column in range (0,8):
        try:
            bus.write_word_data(DEVICE_ADDRESS, address, LEDArray[column])
        except IOError:
            MyLog.error("Error: I2C Write Failed " +
                        "- The current time is " +
                        time.strftime ("%I:%M %x"))
            break
        else:
            address += 2

    # END translate_displaybuf_to_8x8LED()

########################################################################
#
# Note: Code Execution Starts Here
#
########################################################################

# Configure a basic logging function.   
logging.basicConfig(filename='PixelClock.log', level=logging.DEBUG,
                    format='%(asctime)s %(levelname)s LineNo=%(lineno)s MSG= %(message)s',
                    filemode="w", datefmt="%Y-%m-%d %H:%M:%S")

# Create a basic logging function.
MyLog = logging.getLogger()

# The logging level must be on from the set, {DEBUG, INFO, WARNING,
# ERROR, CRITICAL}.  For more information in the log, use the logging
# level, "DEBUG". For less information in the log, use the logging
# level, "ERROR".
MyLog.setLevel(logging.ERROR)
MyLog.debug('########################################################')
MyLog.debug('PixelClock.py')
MyLog.debug('Copyright(c) 2016, Jason Matthew Hale')
MyLog.debug('This python script realizes a Pixel Clock algorithm on')
MyLog.debug('custom hardware.  This script uses I2C protocol to')
MyLog.debug('generate pixel groups that represent digits, including')
MyLogdebug('(1) Hour, Minutes ((2) 10\'s digit and (3) 1\'s digit).')
MyLog.debug('########################################################')

# Seed the system's random number generator with an initial value.
random.seed()

# Instantiate a scheduler
scheduler = sched.scheduler(time.time, time.sleep)

MyLog.debug("Execution Start Time is " + time.strftime("%I:%M %x"))

# The following I2C commands are applicable to the Holtek HT16K33,
# the I2C backpack on the 8x8 Bi-Color LED Matrix.  Search the internet
# for a datasheet for more information abou these commands.

# Enable the system oscillator on the HolTek HT16K33
bus.write_byte(DEVICE_ADDRESS, 0x21)
# Enable the display with set the blinking on the Ho
bus.write_byte(DEVICE_ADDRESS, 0x81)
# Set the brightness of the 8x8 Bi-Color LED Matrix.
bus.write_byte(DEVICE_ADDRESS, brightnessArray[0])

# Infinitely loop by initializing and running the scheduler.
while (True):
    scheduler.enter(5,1, my_sched_event1, () )
    scheduler.run()
