# Cyclist(TT) clock

# software v1.20.6
# raspberry pi pico LiPo 4MB
# pico display pack 240 x 135 SPI LCD
# Waveshare GPIO Expander For Raspberry Pi Pico
# rv3028 rtc

import math
import utime
from pimoroni import Button, RGBLED
from picographics import PicoGraphics, DISPLAY_PICO_DISPLAY, PEN_P4
disp = PicoGraphics(display=DISPLAY_PICO_DISPLAY, pen_type=PEN_P4, rotate=0)
from breakout_rtc import BreakoutRTC
from pimoroni_i2c import PimoroniI2C
from machine import Pin, PWM

# display setup
width, height = disp.get_bounds()
disp_buffer = bytearray(width * height * 2)
#disp.init(disp_buffer)
disp.set_backlight(0.5)  # backlight - 50%
disp.set_font("bitmap8")
green = disp.create_pen(0, 255, 0) 
black = disp.create_pen(0, 0, 0)
blue = disp.create_pen(0, 0, 255)
red = disp.create_pen(255, 0, 0)
white = disp.create_pen(255, 255, 255)

button_a = Button(12)
button_b = Button(13)
button_x = Button(14)
button_y = Button(15)

led_rgb = RGBLED(6, 7, 8)

# rtc setup
PINS_BREAKOUT_GARDEN = {"sda": 0, "scl": 1}
i2c_3 = PimoroniI2C(**PINS_BREAKOUT_GARDEN)
rtc = BreakoutRTC(i2c_3)
rtc.set_backup_switchover_mode(3)
rtc.set_24_hour()
rtc.update_time()

switch = Pin(11, Pin.IN, Pin.PULL_DOWN) # set enable
buzzer = PWM(Pin(21))

beatlist = [[1,0,0,0],[1,1,0,0],[0,1,0,0],[0,1,1,0],[0,0,1,0],[0,0,1,1],[0,0,0,1],[1,0,0,1],[0,0,0,0]] # stepper motor pattern
motor_pins = [2, 3, 4, 5, 9, 10] # Select [9(Hrs), 10(Mins)], motor [2, 3, 4, 5]
sense_pins = [27, 28] # Hall sensors 27(Hrs), 28(Mins)

for pin in motor_pins:
    machine.Pin(pin, machine.Pin.OUT)
    
for pin in sense_pins:
    machine.Pin(pin, machine.Pin.IN, Pin.PULL_UP)
    
# global variables
sum_hrs = 0
sum_min = 0
tim = 0
ttim = 0
mim = 0
mtim = 0
rtc_seconds = 0
rtc_minutes = 0
zero_mins = 0
zero_hrs = 0 
hours = 0
old_hrs = -1
old_mins = -1
old_secs = -1
diff_hrs = 0
diff_mins = 0
diff_secs = 0
home = False
gear_ratio = 65/29
spiral_hgt = 1 # height of spiral in circular rotations
deg_hr = (spiral_hgt*(360*gear_ratio))/12
deg_min = (spiral_hgt*(360*gear_ratio))/60
blank = False

# functions
def clkwise(sel):
    # motor direction of rotation
    for index in range(0, 8):
     set_bit(index, sel)
    return


def anti_clkwise(sel):
    # motor direction of rotation
    for index in range(7, -1, -1):
     set_bit(index, sel)
    return
    

def set_bit(num, sel):
    # num - motor pattern, sel - motor selector
    global beatlist, motor_pins
    # motor select
    if sel == 0:
        machine.Pin(motor_pins[4]).value(0) # motor1 off
        machine.Pin(motor_pins[5]).value(0) # motor2 off
    elif sel == 1:
        machine.Pin(motor_pins[4]).value(1) # motor1 on 
        machine.Pin(motor_pins[5]).value(0) # motor2 off
    elif sel == 2:
        machine.Pin(motor_pins[4]).value(0) # motor1 off
        machine.Pin(motor_pins[5]).value(1) # motor2 on
    elif sel == 3:
        machine.Pin(motor_pins[4]).value(1) # motor1 on
        machine.Pin(motor_pins[5]).value(1) # motor2 on       
    utime.sleep_us(500)
    # motor pattern    
    motor_bits = beatlist[num]
    # print (motor_bits[0], motor_bits[1], motor_bits[2], motor_bits[3])
    for index in range(0, 4):
        machine.Pin(motor_pins[index]).value(motor_bits[index])
    utime.sleep_us(500)
    return


def motor_off():
    set_bit(8, 1) # motor1
    set_bit(8, 2) # motor2
    return


def pos_min(mins, secs):
    # rotation for minute intervals
    step_min = int(((mins+(secs/60))*deg_min)*1.422)
    if step_min > 0:
     # print (step_min, mins)
     for index in range(step_min + 1):
      clkwise(2)
    
    set_bit(8, 2) # motor2 off
    return


def pos_hrs(hrs, mins):
    # rotation for hour intervals
    # step_hrs = int(((hrs+(mins/60))*deg_hr)*1.42222)
    step_hrs = int(((hrs+(mins/60))*deg_hr)*1.4)
    if step_hrs > 0:
     # print (step_hrs, hrs)   
     for index in range(step_hrs + 1):
      clkwise(1)

    set_bit(8, 1) # motor1 off
    return


def spiral_time():
    # update spiral positions
    global diff_hrs, diff_mins, diff_secs
    spiral_hrs = diff_hrs
    if diff_hrs >= 12:
        spiral_hrs = diff_hrs - 12
    pos_hrs(spiral_hrs, diff_mins) 
    pos_min(diff_mins, diff_secs)
    return

def sprint():
    # both wheels turning
    for index in range(1129):
        clkwise(3)
    set_bit(8, 3)    
    return

def homing():
    # home all motors
    global old_hrs, old_mins, old_secs
    old_hrs = 0
    old_mins = 0
    old_secs = 0
    seeking(1, 0) # motor1
    seeking(2, 1) # motor2
    return


def seeking(motor, sensor):
    # ensure spirals are aligned at start up
    global home, sense_pins
    # print ("homing", motor)
    home = False
    timer = utime.ticks_ms()
    index = 0
    # rotate spiral to home position before timeout
    if machine.Pin(sense_pins[sensor]).value() == 0:
        index = 0
    else:
        while (machine.Pin(sense_pins[sensor]).value() == 1 and (utime.ticks_ms() - timer) < 60000):
            clkwise(motor)
            index += 1
            if index > 512:
                index = 0
        if motor == 1:
            pos_hrs(0, 15)
        if motor == 2:    
            pos_min(1, 0)
    
    utime.sleep_ms(100)
    if machine.Pin(sense_pins[sensor]).value() == 0:
        home = True
    motor_off()
    return


def tgraph():
    # display digital time
    global tim, ttim, mim, mtim
    intime = str(tim) + str(ttim) + ":" + str(mim) + str(mtim) 
    disp.text(intime, int(width/6), int(height/4), scale=8)
    return


def hrs_adj():
    global tim, ttim, sum_hrs
    sum_hrs += 1
    if sum_hrs > 24:
        sum_hrs = 0
    tim, ttim = split_time(sum_hrs)
    tgraph()
    disp.update()
    return


def mins_adj():
    global mim, mtim, sum_min
    sum_min += 1
    if sum_min > 59:
        sum_min = 0
    mim, mtim = split_time(sum_min)    
    tgraph()
    disp.update()
    return


def split_time(value):
    if value > 0:
        new_value = str(value)
        if value < 10:
            tens = 0
            unit = new_value[0]
        else:
            tens = new_value[0]
            unit = new_value[1]
    else:
        tens = unit = 0
    return tens, unit


def clean_disp():
    disp.set_pen(black)
    disp.clear()
    disp.update()
    return


def read_rtc():
    global tim, ttim, mim, mtim, rtc_seconds, rtc_minutes, old_hrs, old_mins, old_secs, diff_hrs, diff_mins, zero_mins, zero_hrs
    if rtc.read_periodic_update_interrupt_flag():
        rtc.clear_periodic_update_interrupt_flag()

        if rtc.update_time():
            # rtc_date = rtc.string_date()
            # rtc_time = rtc.string_time()
            rtc_hours = rtc.get_hours()
            rtc_minutes = rtc.get_minutes()
            rtc_seconds = rtc.get_seconds() # used in cycle_led
            # print (rtc_time, rtc_hours, rtc_minutes, rtc_seconds)
            
            diff_hrs = rtc_hours - old_hrs
            zero_hrs = diff_hrs
            if diff_hrs < 0:
                diff_hrs = 0
            if old_hrs != rtc_hours:
                old_hrs = rtc_hours
            
            diff_mins = rtc_minutes - old_mins
            zero_mins = diff_mins            
            if diff_mins <= 0:
                diff_mins = 0
            if old_mins != rtc_minutes:
                old_mins = rtc_minutes
            if zero_mins == -59: 
                if rtc_hours == 0 or rtc_hours == 12:
                    zero_hrs = -23
                       
            diff_secs = rtc_seconds - old_secs
            if diff_secs < 0:
                diff_secs = 0
            if old_secs != rtc_seconds:
                old_secs = rtc_seconds                
            
            # print (diff_hrs, zero_hrs)

            if zero_mins == -59:
                chime(4) # tone on the hour
                sprint()
                homing()
            
            tim, ttim = split_time(rtc_hours)
            mim, mtim = split_time(rtc_minutes)
     
    return


def set_time():
    # seconds, minutes, hours, weekday, day, month, year
    # rtc.set_time(55,59,22,0,14,2,2022)
    global sum_min, sum_hrs, old_hrs, old_mins
    rtc.set_time(0, sum_min, sum_hrs, 0, 1, 1, 2023)
    utime.sleep(1)
    # reset old time values
    old_hrs = -1
    old_mins = -1
    homing()
    return


def cycle_led(sec):
    global width, height
    # cycle_led: represents 0 to 60 seconds as a varying horizontal line
    disp.set_clip(0, 120, width, 5)
    tick = int((sec / 60) * width) # horizontal line as a %age of screen width
    disp.pixel_span(0, 120, tick)
    disp.update()
    disp.remove_clip()
    utime.sleep(1)
    return


def chime(count):
    for b in range(1,count):
        for a in range(500, 800):
            buzzer.duty_u16(1000)
            buzzer.freq(a)
            utime.sleep(0.005)
        
        buzzer.duty_u16(0)
        return

def beep():
    buzzer.freq(1000)
    buzzer.duty_u16(1000)
    utime.sleep(0.25)
    buzzer.duty_u16(0)
    return

def menu():
    # button labels
    disp.text("H", 10, 20, 24, 2)   # hours
    disp.text("M", 220, 20, 24, 2)  # minutes
    disp.text("U", 220, 100, 24, 2) # update time
    disp.text("B", 10, 100, 24, 2)  # display mode (blank or time)
    return
   

# initialization
read_rtc()
rtc.enable_periodic_update_interrupt(True)
clean_disp()
disp.set_pen(white)
disp.text("Cyclist(TT) Time",10,25,220,3)
disp.update()
utime.sleep(2)
clean_disp()
beep()
disp.set_pen(green)
disp.text("Please Wait" + " Homing....",10,25,220,3)
disp.update()
homing()
clean_disp()
if home == False:
    disp.set_pen(red)
    disp.text("Homing Failed!",10,50,220,3)
    beep()
else:
    disp.set_pen(green) 
    disp.text("Homing Passed",10,50,220,3)    
disp.update()
utime.sleep(2)

while True:
    if switch.value():         # enable set time
        led_rgb.set_rgb(0, 0, 4) # set mode
        disp.set_pen(blue)
        menu()
        tgraph()
         
        if button_a.read():    # enter hours
            beep()
            clean_disp()
            hrs_adj()
  
        elif button_b.read():
            beep()
            blank = not(blank)
            utime.sleep(1)
           
        elif button_x.read():  # enter minutes
            beep()
            clean_disp() 
            mins_adj()
           
        elif button_y.read():  # set time
            beep()
            set_time()
        
        disp.update()
    
    else:
               
        read_rtc()
        clean_disp()
        if home == True:
            led_rgb.set_rgb(0, 4, 0) # homing good
            if blank == False:                  
                disp.set_pen(green)
        else:
            led_rgb.set_rgb(4, 0, 0) # homing error 
            if blank == False:    
                disp.set_pen(red)            
        menu()
        tgraph()
        disp.update()
        if home == True:
            spiral_time()
            if zero_mins == -59:
                homing()
                # print(zero_mins)
        cycle_led(rtc_seconds)
