diff --git a/Woodpecker Chirps - QuickSounds.com.mp3 b/Woodpecker Chirps - QuickSounds.com.mp3 new file mode 100644 index 0000000..4124c83 Binary files /dev/null and b/Woodpecker Chirps - QuickSounds.com.mp3 differ diff --git a/alarm_off.png b/alarm_off.png new file mode 100644 index 0000000..1de8366 Binary files /dev/null and b/alarm_off.png differ diff --git a/alarm_on.png b/alarm_on.png new file mode 100644 index 0000000..d27f147 Binary files /dev/null and b/alarm_on.png differ diff --git a/clock.py b/clock.py new file mode 100644 index 0000000..40a8536 --- /dev/null +++ b/clock.py @@ -0,0 +1,349 @@ +# Many thanks to https://stackoverflow.com/questions/18923321/making-a-clock-in-kivy +import collections +import datetime +import math +import sys +import traceback + +from kivy.app import App +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.label import Label +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.graphics import Color, Line, Rectangle + +from multiprocessing import Process +from playsound import playsound + +Builder.load_string(''' +: + on_pos: self.update_clock() + on_size: self.update_clock() + + FloatLayout + id: face + size_hint: None, None + pos_hint: {"center_x":0.5, "center_y":0.5} + size: 0.9*min(root.size), 0.9*min(root.size) + canvas: + Color: + rgb: 0.1, 0.1, 0.1 + Ellipse: + size: self.size + pos: self.pos + + FloatLayout + id: hands + size_hint: None, None + pos_hint: {"center_x":0.5, "center_y":0.5} + size: 0.9*min(root.size), 0.9*min(root.size) + + FloatLayout + id: set_alarm_button + size_hint: None, None + pos_hint: {"center_x":0.9, "center_y":0.1} + size: 0.1*min(root.size), 0.1*min(root.size) +''') + +Position = collections.namedtuple('Position', 'x y') + +global sound_process +sound_process = None + +def play_sound(source): + playsound(source) + +class MyClockWidget(FloatLayout): + set_alarm_mode = False + alarm_time = datetime.datetime(2022, 12, 10, 7, 30, 0, 0) + alarm_activated = False + + grabbed = "" + face_numbers = [] + set_alarm_timeout_counter = 0 + alarm_modified = False + seconds_to_next_alarm = False + led_color = [0, 0, 0] + # sound_source = "https://icecast.omroep.nl/radio1-bb-mp3" + sound_source = "Woodpecker Chirps - QuickSounds.com.mp3" + + def draw_face(self): + """ + Add number labels when added in widget hierarchy + """ + if self.set_alarm_mode: + time = self.alarm_time + else: + time = datetime.datetime.now() + + for i in range(1, 13): + if time.hour < 12: + offset = 0 + else: + offset = 12 + + self.face_numbers.append(Label( + text=str(i + offset), + pos_hint={ + # pos_hint is a fraction in range (0, 1) + "center_x": 0.5 + 0.45*math.sin(2 * math.pi * i/12), + "center_y": 0.5 + 0.45*math.cos(2 * math.pi * i/12), + } + )) + self.ids["face"].add_widget(self.face_numbers[i - 1]) + + def update_face(self): + if self.set_alarm_mode: + time = self.alarm_time + else: + time = datetime.datetime.now() + + for i in range(0, 12): + if time.hour < 12: + offset = 0 + else: + offset = 12 + + self.face_numbers[i].text=str(i + 1 + offset) + + + def on_parent(self, myclock, parent): + self.draw_face() + + def position_on_clock(self, fraction, length): + """ + Calculate position in the clock using trygonometric functions + """ + center_x = self.size[0]/2 + center_y = self.size[1]/2 + return Position( + center_x + length * math.sin(2 * math.pi * fraction), + center_y + length * math.cos(2 * math.pi * fraction), + ) + + def update_set_alarm_button(self): + set_alarm_button = self.ids["set_alarm_button"] + + if self.set_alarm_mode or self.alarm_activated: + source = 'alarm_on.png' + r = 0.9 + g = 0.0 + b = 0.0 + else: + source = 'alarm_off.png' + r = 1.0 + g = 1.0 + b = 1.0 + + set_alarm_button = self.ids["set_alarm_button"] + set_alarm_button.canvas.clear() + with set_alarm_button.canvas: + Color(r, g, b) + Rectangle(size=set_alarm_button.size, pos=set_alarm_button.pos, source=source) + + def sun_rise(self): + # to do: calculate brightness and color according to sun rise instead of linear increment + intensity = round((1.0 - self.seconds_to_next_alarm / (30.0 * 60.0)) * 256.0) + led_color = [0, 0, 0] + for i in range(3): + led_color[i] = intensity + + if self.led_color != led_color: + self.led_color = led_color + + def check_sun_rise(self): + if self.alarm_activated == False: + return + + if self.seconds_to_next_alarm < 30 * 60: + self.sun_rise() + + def check_play_sound(self): + global sound_process + + if self.alarm_activated == False: + return + + if self.seconds_to_next_alarm < 0.1 and sound_process is None: + print("beep beep!") + sound_process = Process(target=play_sound, args=(self.sound_source,)) + sound_process.start() + + def calc_seconds_to_next_alarm(self): + if self.alarm_activated == False: + return + + # Make sure alarm_time is in the future but not more than 24 h from now + now = datetime.datetime.now() + d = self.alarm_time - now + self.alarm_time -= datetime.timedelta(days=d.days) + + # Calculate number of seconds until next alarm + d = self.alarm_time - now + self.seconds_to_next_alarm = d.days * 24 * 3600 + d.seconds + d.microseconds / 1000000.0 + + def check_alarm(self): + self.calc_seconds_to_next_alarm() + self.check_sun_rise() + self.check_play_sound() + + def update_clock(self, *args): + self.check_alarm() + """ + Redraw clock hands + """ + if self.set_alarm_mode: + time = self.alarm_time + else: + time = datetime.datetime.now() + + hands = self.ids["hands"] + seconds_hand = self.position_on_clock(time.second/60, length=0.45*hands.size[0]) + minutes_hand = self.position_on_clock(time.minute/60+time.second/3600, length=0.40*hands.size[0]) + hours_hand = self.position_on_clock(time.hour/12 + time.minute/720, length=0.35*hands.size[0]) + + self.update_face() + self.update_set_alarm_button() + + hands.canvas.clear() + with hands.canvas: + if self.set_alarm_mode: + if self.grabbed != "" or self.set_alarm_timeout_counter < 1 * 60 or self.set_alarm_timeout_counter % 60 <= 30 or self.alarm_modified == False: + Color(0.9, 0.0, 0.0) + Line(points=[hands.center_x, hands.center_y, hours_hand.x, hours_hand.y], width=3, cap="round") + Color(0.8, 0.0, 0.0) + Line(points=[hands.center_x, hands.center_y, minutes_hand.x, minutes_hand.y], width=2, cap="round") + + if self.grabbed == "": + self.set_alarm_timeout_counter += 1 + + if self.set_alarm_timeout_counter >= 4 * 60 + 30: + self.set_alarm_mode = False + self.set_alarm_timeout_counter = 0 + + if self.alarm_modified: + self.alarm_activated = True + self.alarm_modified = False + else: + Color(0.9, 0.9, 0.9) + Line(points=[hands.center_x, hands.center_y, hours_hand.x, hours_hand.y], width=3, cap="round") + Color(0.8, 0.8, 0.8) + Line(points=[hands.center_x, hands.center_y, minutes_hand.x, minutes_hand.y], width=2, cap="round") + Color(0.7, 0.7, 0.7) + Line(points=[hands.center_x, hands.center_y, seconds_hand.x, seconds_hand.y], width=1, cap="round") + + def on_alarm_button_pressed(self): + self.alarm_modified = False + self.set_alarm_timeout_counter = 0 + + if self.set_alarm_mode: + self.set_alarm_mode = False + self.alarm_activated = False + else: + self.set_alarm_mode = True + self.alarm_activated = True + + def on_touch_up(self, touch): + self.grabbed = "" + if self.set_alarm_mode and (self.grabbed == "hour" or self.grabbed == "minute"): + self.set_alarm_timeout_counter = 0 + + def on_touch_move(self, touch): + self.alarm_set_timeout = 0 + x = touch.pos[0] - self.size[0]/2 + y = touch.pos[1] - self.size[1]/2 + angle = math.atan2(y, x) / math.pi; # angle is between -1 and 1 + if self.grabbed == "minute": + self.alarm_modified = True + self.set_alarm_timeout_counter = 0 + minute = round(-angle * 30 + 15) + if minute < 0: + minute += 60 + if minute == 60: + minute = 59 + + # hour correction + hour = self.alarm_time.hour + if self.alarm_time.minute >= 55 and minute <= 5: + hour += 1 + elif self.alarm_time.minute <= 5 and minute >= 55: + hour -= 1 + + if hour == 24: + hour = 0 + elif hour == -1: + hour = 23 + + self.alarm_time = datetime.datetime(self.alarm_time.year, self.alarm_time.month, self.alarm_time.day, \ + hour, minute, self.alarm_time.second, 0) + elif self.grabbed == "hour": + self.alarm_modified = True + self.set_alarm_timeout_counter = 0 + hour = round(-angle * 6 + 3) + if hour < 0: + hour += 12 + if hour == 12: + hour = 0 + + if self.alarm_time.hour >= 12: + hour += 12 + + # AM / PM correction + if self.alarm_time.hour == 11 and hour == 0: + hour = 12 + elif self.alarm_time.hour == 23 and hour == 12: + hour = 0 + elif self.alarm_time.hour == 0 and hour == 11: + hour = 23 + elif self.alarm_time.hour == 12 and hour == 23: + hour = 11 + + # AM / PM boundary + self.alarm_time = datetime.datetime(self.alarm_time.year, self.alarm_time.month, self.alarm_time.day, \ + hour, self.alarm_time.minute, self.alarm_time.second, 0) + + def on_touch_down(self, touch): + global sound_process + + time = self.alarm_time + hands = self.ids["hands"] + minutes_hand = self.position_on_clock(time.minute/60+time.second/3600, length=0.40*hands.size[0]) + hours_hand = self.position_on_clock(time.hour/12 + time.minute/720, length=0.35*hands.size[0]) + + self.grabbed = "" + + if (0.85 <= touch.spos[0] <= 0.95) and (0.05 <= touch.spos[1] <= 0.15): + self.on_alarm_button_pressed() + elif sound_process is not None: + kill_sound_process() + elif self.set_alarm_mode: + self.set_alarm_timeout_counter = 0 + if (minutes_hand.x - 0.1 * self.size[0] <= touch.pos[0] <= minutes_hand.x + 0.1 * self.size[0]) and \ + (minutes_hand.y - 0.1 * self.size[1] <= touch.pos[1] <= minutes_hand.y + 0.1 * self.size[1]): + self.grabbed = "minute" + elif (hours_hand.x - 0.1 * self.size[0] <= touch.pos[0] <= hours_hand.x + 0.1 * self.size[0]) and \ + (hours_hand.y - 0.1 * self.size[1] <= touch.pos[1] <= hours_hand.y + 0.1 * self.size[1]): + self.grabbed = "hour" + +class MyApp(App): + def build(self): + clock_widget = MyClockWidget() + # update initially, just after construction of the widget is complete + Clock.schedule_once(clock_widget.update_clock, 0) + # then update 60 times per second + Clock.schedule_interval(clock_widget.update_clock, 1.0/60.0) + return clock_widget + +def kill_sound_process(): + global sound_process + if sound_process is not None: + sound_process.kill() + sound_process = None + +def except_hook(type, value, tb): + kill_sound_process() + +if __name__ == '__main__': + # sys.excepthook = except_hook + MyApp().run() +