# Many thanks to https://stackoverflow.com/questions/18923321/making-a-clock-in-kivy import collections import datetime import math import sys import traceback import copy import os from kivy.config import Config from kivy.properties import ObjectProperty import time from sounds import WakeUpSounds def is_arm(): if (os.uname()[4][:3] == 'arm') or (os.uname()[4][:7] == 'aarch64'): return True else: return False if is_arm(): # Do not forget to create udev rule: # echo 'SUBSYSTEM=="backlight",RUN+="/bin/chmod 666 /sys/class/backlight/%k/brightness /sys/class/backlight/%k/bl_power"' | sudo tee -a /etc/udev/rules.d/backlight-permissions.rules from rpi_backlight import Backlight from rgbw_colorspace_converter.colors.converters import HSV, RGB from hsluv import hsluv_to_rgb Config.set('graphics', 'width', '800') Config.set('graphics', 'height', '480') #Config.set('graphics', 'width', '750') #Config.set('graphics', 'height', '450') import board import neopixel pixel_pin = board.D10 num_pixels = 144 ORDER = neopixel.GRBW pixels = neopixel.NeoPixel( pixel_pin, num_pixels, brightness=0.2, auto_write=False, pixel_order=ORDER ) pixels.fill((0, 0, 0, 0)) pixels.show() backlight = Backlight() else: Config.set('graphics', 'width', '1200') Config.set('graphics', 'height', '720') pixels = None backlight = None Config.set('graphics', 'maxfps', '60') from kivy.app import App from kivy.uix.floatlayout import FloatLayout from kivy.uix.label import Label from kivy.uix.slider import Slider from kivy.uix.button import Button from kivy.uix.gridlayout import GridLayout from kivy.uix.checkbox import CheckBox from kivy.clock import Clock from kivy.lang import Builder from kivy.graphics import Color, Line, Rectangle, Ellipse from kivy.core.window import Window if is_arm(): Window.show_cursor = False import pulsectl import vlc Builder.load_file('clock.kv') Position = collections.namedtuple('Position', 'x y') class Theme(): def __init__(self, x="Dark"): self.name = x if x == "Dark": self.color_background = [0, 0, 0] self.color_shade = [.1, .1, .1] self.color_clock_hands_hours = [.9, .9, .9] self.color_clock_hands_minutes = [.8, .8, .8] self.color_clock_hands_seconds = [.7, .7, .7] self.color_alarm_hands_hours = [.9, .0, .0] self.color_alarm_hands_minutes = [.8, .0, .0] self.color_alarm_hands_seconds = [.7, .0, .0] self.color_font = [1, 1, 1] self.color_numbers = [1, 1, 1] self.color_button = [.2, .2, .2, 1] self.color_funky = [.5, .2, .1, 0.2] self.icon_light_off = 'themes/Dark/light_off.png' self.icon_light_reading = 'themes/Dark/light_reading.png' self.icon_light_sunrise = 'themes/Dark/light_sunrise.png' self.icon_light_on = 'themes/Dark/light_on.png' self.icon_media_playing = 'themes/Dark/media_playing.png' self.icon_media_stopped = 'themes/Dark/media_stopped.png' self.icon_alarm_on = 'themes/Dark/alarm_on.png' self.icon_alarm_off = 'themes/Dark/alarm_off.png' self.icon_settings_visible = 'themes/Dark/settings_visible.png' self.icon_settings_not_visible = 'themes/Dark/settings_not_visible.png' elif x == "Light": self.color_background = [1, 1, 1] self.color_shade = [.9, .9, .9] self.color_clock_hands_hours = [.1, .1, .1] self.color_clock_hands_minutes = [.2, .2, .2] self.color_clock_hands_seconds = [.3, .3, .3] self.color_alarm_hands_hours = [.9, .0, .0] self.color_alarm_hands_minutes = [.8, .0, .0] self.color_alarm_hands_seconds = [.7, .0, .0] self.color_font = [0, 0, 0] self.color_numbers = [0, 0, 0] self.color_button = [.8, .8, .8, 1] self.color_funky = [.1, .2, .5, 0.2] self.icon_light_off = 'themes/Light/light_off.png' self.icon_light_reading = 'themes/Light/light_reading.png' self.icon_light_sunrise = 'themes/Light/light_sunrise.png' self.icon_light_on = 'themes/Light/light_on.png' self.icon_media_playing = 'themes/Light/media_playing.png' self.icon_media_stopped = 'themes/Light/media_stopped.png' self.icon_alarm_on = 'themes/Light/alarm_on.png' self.icon_alarm_off = 'themes/Light/alarm_off.png' self.icon_settings_visible = 'themes/Light/settings_visible.png' self.icon_settings_not_visible = 'themes/Light/settings_not_visible.png' class AlarmSettings(): alarm_time = datetime.datetime(2022, 12, 10, 7, 30, 0, 0) alarm_activated = False alarm_playing = False alarm_modified = False seconds_to_sunrise = 30 * 60 # 30 minutes volume = 15 wake_up_brightness = 20 reading_light_brightness = 1 display_brightness = 10 class Touch(): def __init__(self, x=None): if x is None: self.pos = [] self.spos = [] self.is_empty = True self.angle = 0 self.corrected = False else: self.pos = [i for i in x.pos] self.spos = [i for i in x.spos] self.is_empty = False x = self.spos[0] - 0.5 y = self.spos[1] - 0.5 self.angle = math.atan2(y, x) / math.pi; # angle is between -1 and 1 self.corrected = False def __repr__(self): return ".is_empty: " + str(self.is_empty) + ", .pos: " + str(self.pos) + ", .spos: " + str(self.spos), ", .corrected: " + str(self.corrected) def copy(self, x): self = Touch(x) def clear(self): self = Touch() class TouchEvent(): def __init__(self, x=None): self.time = time.time() self.touch = copy.copy(x) self.processed = False class MyClockWidget(FloatLayout): theme_selected = "Dark" theme = ObjectProperty(Theme(theme_selected)) grabbed = "" face_numbers = [] # if not grabbed, set_alarm_timeout_counter is incremented at every update call; # this is used to blink the hands at 1 Hz when setting the alarm and releasing the hand set_alarm_timeout_counter = 0 seconds_to_next_alarm = 0 light_state = "off" # "off", "reading", "sunrise" or "on" intensity_target = 0 intensity_prev = 0 intensity_curr = 0 intensity_target_prev = None rgbw_prev = None # Ugly workaround for issue with Kivy and Raspberry Pi 3 + touch screen # For each type of touch event, handling of the event is ignored until the last event is more than touch_delay_time seconds old touch_delay_time = 0.1 touch_down_event_prev = None touch_move_event_prev = None touch_up_event_prev = None touch_down_event_curr = None touch_move_event_curr = None touch_up_event_curr = None # view can be one of the following strings: # - "clock" # - "set_alarm" # - "settings_menu" # - "settings_menu_wake_up_sound" # - "settings_menu_theme" view = "clock" view_prev = "" view_active_since = time.time() # ugly workaround for issue where input is processed twice (specifically: # pressing Ok in wake up sound view switches to settings view but also # adjusts display brightness slider) time_to_ignore_inputs_after_view_change = 0.5 # ugly workaround for issue where graphics are not drawn correctly after view change time_to_force_redraws_after_view_change = 0.5 # we need a dirty hack to sensure that radio button is always in sync with selected setting settings_menu_wake_up_sound_select_button_cb_hack = False settings_menu_theme_select_button_cb_hack = False # defer drawing to improve application speed: if list of draw calls for # this frame is identical to list of previous frame then nothing needs to # be drawed draw_list_last_frame = [] draw_list_curr_frame = [] touch_prev = Touch() light_button_move_init = [] vlc = vlc.Instance() player = vlc.media_player_new() is_arm = is_arm() def play_sound(self, source): alarm_settings = App.get_running_app().alarm_settings if (self.player.get_state() == vlc.State.NothingSpecial) or \ (self.player.get_state() == vlc.State.Paused) or \ (self.player.get_state() == vlc.State.Stopped) or \ (self.player.get_state() == vlc.State.Ended) or \ (self.player.get_state() == vlc.State.Error): print("beep beep! " + source) media = self.vlc.media_new(source) self.player.set_media(media) self.player.play() alarm_settings.alarm_playing = True def stop_sound(self): self.player.stop() alarm_settings = App.get_running_app().alarm_settings alarm_settings.alarm_playing = False def set_backlight(self, x): offset = 3 # 20 steps to from 1 to 100 - offset c = (100 - offset)**(1/20) y = c**x + offset z = max(1, round(y)) if self.is_arm: if backlight.brightness != z: backlight.brightness = z def hide_widget(self, widget, hide=True): if hasattr(widget, 'saved_attrs'): if not hide: widget.height, widget.size_hint_y, widget.opacity, widget.disabled = widget.saved_attrs del widget.saved_attrs elif hide: widget.saved_attrs = widget.height, widget.size_hint_y, widget.opacity, widget.disabled widget.height, widget.size_hint_y, widget.opacity, widget.disabled = 0, None, 0, True def draw_face(self): """ Add number labels when added in widget hierarchy """ alarm_settings = App.get_running_app().alarm_settings if self.view == "set_alarm": t = alarm_settings.alarm_time else: t = datetime.datetime.now() for i in range(1, 13): if t.hour < 12: offset = 0 else: offset = 12 self.face_numbers.append(Label( text=str(i + offset), font_size=float(Config.get('graphics', 'height'))*0.05, color=self.theme.color_numbers, 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_background(self): background = self.ids["background"] self.draw_list_curr_frame.append(["canvas.clear()", background]) with background.canvas: self.draw_list_curr_frame.append(["Color", background.canvas, self.theme.color_background[0], self.theme.color_background[1], self.theme.color_background[2]]) self.draw_list_curr_frame.append(["Rectangle", background.canvas, background.size, background.pos, None]) def update_face(self): alarm_settings = App.get_running_app().alarm_settings face_plate = self.ids["face_plate"] self.draw_list_curr_frame.append(["canvas.clear()", face_plate]) with face_plate.canvas: self.draw_list_curr_frame.append(["Color", face_plate.canvas, self.theme.color_shade[0], self.theme.color_shade[1], self.theme.color_shade[2]]) self.draw_list_curr_frame.append(["Ellipse", face_plate.canvas, face_plate.size, face_plate.pos]) if self.view == "set_alarm": t = alarm_settings.alarm_time else: t = datetime.datetime.now() for i in range(0, 12): if t.hour < 12: offset = 0 else: offset = 12 self.face_numbers[i].color=self.theme.color_numbers 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 + round(length * math.sin(2 * math.pi * fraction)), center_y + round(length * math.cos(2 * math.pi * fraction)), ) def update_light_button(self): if (self.light_state == "off"): source = self.theme.icon_light_off rgb = [1.0, 1.0, 1.0] elif (self.light_state == "reading"): source = self.theme.icon_light_reading rgb = [1.0, 1.0, 1.0] elif (self.light_state == "sunrise"): source = self.theme.icon_light_sunrise rgb = [1.0, 1.0, 1.0] elif (self.light_state == "on"): source = self.theme.icon_light_on rgb = [1.0, 1.0, 1.0] light_button = self.ids["light_button"] self.draw_list_curr_frame.append(["canvas.clear()", light_button]) with light_button.canvas: self.draw_list_curr_frame.append(["Color", light_button.canvas, rgb[0], rgb[1], rgb[2]]) self.draw_list_curr_frame.append(["Rectangle", light_button.canvas, light_button.size, light_button.pos, source]) def update_play_button(self): alarm_settings = App.get_running_app().alarm_settings if alarm_settings.alarm_playing: source = self.theme.icon_media_playing rgb = [1.0, 1.0, 1.0] else: source = self.theme.icon_media_stopped rgb = [1.0, 1.0, 1.0] play_button = self.ids["play_button"] self.draw_list_curr_frame.append(["canvas.clear()", play_button]) with play_button.canvas: self.draw_list_curr_frame.append(["Color", play_button.canvas, rgb[0], rgb[1], rgb[2]]) self.draw_list_curr_frame.append(["Rectangle", play_button.canvas, play_button.size, play_button.pos, source]) def update_set_alarm_button(self): alarm_settings = App.get_running_app().alarm_settings if (self.view == "set_alarm") or alarm_settings.alarm_activated: source = self.theme.icon_alarm_on rgb = [1.0, 1.0, 1.0] else: source = self.theme.icon_alarm_off rgb = [1.0, 1.0, 1.0] set_alarm_button = self.ids["set_alarm_button"] self.draw_list_curr_frame.append(["canvas.clear()", set_alarm_button]) with set_alarm_button.canvas: self.draw_list_curr_frame.append(["Color", set_alarm_button.canvas, rgb[0], rgb[1], rgb[2]]) self.draw_list_curr_frame.append(["Rectangle", set_alarm_button.canvas, set_alarm_button.size, set_alarm_button.pos, source]) def update_settings_button(self): alarm_settings = App.get_running_app().alarm_settings if (self.view.startswith("settings_menu")): source = self.theme.icon_settings_visible rgb = [1.0, 1.0, 1.0] else: source = self.theme.icon_settings_not_visible rgb = [1.0, 1.0, 1.0] settings_button = self.ids["settings_button"] self.draw_list_curr_frame.append(["canvas.clear()", settings_button]) with settings_button.canvas: self.draw_list_curr_frame.append(["Color", settings_button.canvas, rgb[0], rgb[1], rgb[2]]) self.draw_list_curr_frame.append(["Rectangle", settings_button.canvas, settings_button.size, settings_button.pos, source]) def intensity_to_rgbw(self, intensity): if intensity < 0: intensity = 0 elif intensity > 1: intensity = 1 h = max(0, min(75, (75 - 12) * intensity + 12)) s = max(0, min(100, 250 - 250 * intensity)) l = 100 * intensity rgb = hsluv_to_rgb([h, s, l]) led_color = RGB(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255) return led_color def update_backlight(self): alarm_settings = App.get_running_app().alarm_settings # make sure brightness of display is never lower than intensity of leds brightness_min = 20 * self.intensity_curr if alarm_settings.display_brightness < brightness_min: self.set_backlight(brightness_min) else: self.set_backlight(alarm_settings.display_brightness) def set_leds(self): if self.intensity_prev != self.intensity_curr: self.intensity_prev = self.intensity_curr if self.is_arm: led_color = self.intensity_to_rgbw(self.intensity_curr) if self.rgbw_prev != led_color.rgbw: self.rgbw_prev = led_color.rgbw # print(self.light_state + ", t: " + str(self.seconds_to_next_alarm) + ", i: " + str(self.intensity_curr) + ", rgbw: " + str(led_color.rgbw)) pixels.fill(led_color.rgbw) pixels.show() else: # On non-arm rgbw_colorspace_converter and hsluv are not available; fake rgbw by setting w component to scaled value of intensity_curr rgbw = [0, 0, 0, round(self.intensity_curr * 255)] if self.rgbw_prev != rgbw: self.rgbw_prev = rgbw # print(self.light_state + ", t: " + str(self.seconds_to_next_alarm) + ", i: " + str(self.intensity_curr) + ", rgbw: " + str(rgbw)) def sunrise(self): alarm_settings = App.get_running_app().alarm_settings if self.view == "set_alarm": # Do not simulate sunrise when adjusting alarm time return if self.seconds_to_next_alarm < 0.1: intensity_target = (alarm_settings.wake_up_brightness / 20.0) new_state = "on" else: intensity_target = (1.0 - self.seconds_to_next_alarm / alarm_settings.seconds_to_sunrise) * (alarm_settings.wake_up_brightness / 20.0) new_state = "sunrise" # only adjust intensity_target if new intensity_target is higher than current intesity, to avoid dimming light when reading mode is on if self.intensity_target < intensity_target: self.intensity_target = intensity_target self.light_state = new_state def process_led_state(self): alarm_settings = App.get_running_app().alarm_settings if (alarm_settings.alarm_activated) and (self.seconds_to_next_alarm < alarm_settings.seconds_to_sunrise): self.sunrise() else: if (self.light_state == "off"): self.intensity_target = 0 elif (self.light_state == "reading"): self.intensity_target = alarm_settings.reading_light_brightness / 20.0 elif (self.light_state == "on"): self.intensity_target = alarm_settings.wake_up_brightness / 20.0 weight = 0.05 intensity_next = weight * self.intensity_target + (1 - weight) * self.intensity_curr step = intensity_next - self.intensity_curr if abs(step) < 0.001: step = self.intensity_target - self.intensity_curr self.intensity_curr = self.intensity_curr + step def check_play_sound(self): alarm_settings = App.get_running_app().alarm_settings if alarm_settings.alarm_activated == False: return if (self.seconds_to_next_alarm < 0.1) or (alarm_settings.alarm_playing == True): if alarm_settings.sound_source != "": self.play_sound(alarm_settings.sound_source) def calc_seconds_to_next_alarm(self): alarm_settings = App.get_running_app().alarm_settings if alarm_settings.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 = alarm_settings.alarm_time - now alarm_settings.alarm_time -= datetime.timedelta(days=d.days) # Calculate number of seconds until next alarm d = alarm_settings.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.process_led_state() self.set_leds() self.update_backlight() self.check_play_sound() def update_clock(self): self.draw_list_curr_frame.append(["self.hide_widget", self.ids["face_plate"], False]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["face"], False]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["hands"], False]) alarm_settings = App.get_running_app().alarm_settings if self.view == "set_alarm": t = alarm_settings.alarm_time else: t = datetime.datetime.now() hands = self.ids["hands"] seconds_hand = self.position_on_clock(t.second/60, length=0.45*hands.size[0]) minutes_hand = self.position_on_clock(t.minute/60+t.second/3600, length=0.40*hands.size[0]) hours_hand = self.position_on_clock(t.hour/12 + t.minute/720, length=0.35*hands.size[0]) self.draw_list_curr_frame.append(["canvas.clear()", hands]) update_rate = App.get_running_app().update_rate with hands.canvas: if self.view == "set_alarm": if self.grabbed != "" or self.set_alarm_timeout_counter < 1 * update_rate or \ self.set_alarm_timeout_counter % update_rate <= update_rate / 2 or alarm_settings.alarm_modified == False: self.draw_list_curr_frame.append(["Color", hands.canvas, self.theme.color_alarm_hands_hours[0], self.theme.color_alarm_hands_hours[1], self.theme.color_alarm_hands_hours[2]]) self.draw_list_curr_frame.append(["Line", hands.canvas, [hands.center_x, hands.center_y, hours_hand.x, hours_hand.y], 3, "round"]) self.draw_list_curr_frame.append(["Color", hands.canvas, self.theme.color_alarm_hands_minutes[0], self.theme.color_alarm_hands_minutes[1], self.theme.color_alarm_hands_minutes[2]]) self.draw_list_curr_frame.append(["Line", hands.canvas, [hands.center_x, hands.center_y, minutes_hand.x, minutes_hand.y], 2, "round"]) if self.grabbed == "": self.set_alarm_timeout_counter += 1 if self.set_alarm_timeout_counter >= 4.5 * update_rate: self.view = "clock" self.set_alarm_timeout_counter = 0 self.view_active_since = time.time() if alarm_settings.alarm_modified: alarm_settings.alarm_activated = True alarm_settings.alarm_modified = False else: self.draw_list_curr_frame.append(["Color", hands.canvas, self.theme.color_clock_hands_hours[0], self.theme.color_clock_hands_hours[1], self.theme.color_clock_hands_hours[2]]) self.draw_list_curr_frame.append(["Line", hands.canvas, [hands.center_x, hands.center_y, hours_hand.x, hours_hand.y], 3, "round"]) self.draw_list_curr_frame.append(["Color", hands.canvas, self.theme.color_clock_hands_minutes[0], self.theme.color_clock_hands_minutes[1], self.theme.color_clock_hands_minutes[2]]) self.draw_list_curr_frame.append(["Line", hands.canvas, [hands.center_x, hands.center_y, minutes_hand.x, minutes_hand.y], 2, "round"]) self.draw_list_curr_frame.append(["Color", hands.canvas, self.theme.color_clock_hands_seconds[0], self.theme.color_clock_hands_seconds[1], self.theme.color_clock_hands_seconds[2]]) self.draw_list_curr_frame.append(["Line", hands.canvas, [hands.center_x, hands.center_y, seconds_hand.x, seconds_hand.y], 1, "round"]) self.update_face() def update_settings_menu(self): self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu_background"], False]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu"], False]) background = self.ids["settings_menu_background"] self.draw_list_curr_frame.append(["canvas.clear()", background]) with background.canvas: rgb = self.theme.color_shade self.draw_list_curr_frame.append(["Color", background.canvas, rgb[0], rgb[1], rgb[2]]) self.draw_list_curr_frame.append(["Rectangle", background.canvas, background.size, background.pos, ""]) for s in ["label_0", "label_1", "label_2", "label_3", "label_4", "label_5", "theme_select_button", "wake_up_sound_select_button"]: i = self.ids["settings_menu_" + s] i.color = self.theme.color_font i = self.ids["settings_menu_theme_select_button"] i.background_color = self.theme.color_button i = self.ids["settings_menu_wake_up_sound_select_button"] i.background_color = self.theme.color_button def update_settings_menu_wake_up_sound(self): self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu_wake_up_sound_background"], False]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu_wake_up_sound"], False]) background = self.ids["settings_menu_wake_up_sound_background"] self.draw_list_curr_frame.append(["canvas.clear()", background]) with background.canvas: rgb = self.theme.color_shade self.draw_list_curr_frame.append(["Color", background.canvas, rgb[0], rgb[1], rgb[2]]) self.draw_list_curr_frame.append(["Rectangle", background.canvas, background.size, background.pos, ""]) for i in self.wake_up_sound_labels: i.color = self.theme.color_font self.settings_menu_wake_up_sound_Ok_button.background_color = self.theme.color_button def update_settings_menu_theme(self): self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu_theme_background"], False]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu_theme"], False]) background = self.ids["settings_menu_theme_background"] self.draw_list_curr_frame.append(["canvas.clear()", background]) with background.canvas: rgb = self.theme.color_shade self.draw_list_curr_frame.append(["Color", background.canvas, rgb[0], rgb[1], rgb[2]]) self.draw_list_curr_frame.append(["Rectangle", background.canvas, background.size, background.pos, ""]) for s in ["label_top", "label_0", "label_1", "label_2", "Ok_button"]: i = self.ids["settings_menu_theme_" + s] i.color = self.theme.color_font i = self.ids["settings_menu_theme_Ok_button"] i.background_color = self.theme.color_button def update_settings(self): if self.view == "settings_menu": self.update_settings_menu() elif self.view == "settings_menu_wake_up_sound": self.update_settings_menu_wake_up_sound() elif self.view == "settings_menu_theme": self.update_settings_menu_theme() def draw_display(self): t = time.time() if (self.view != self.view_prev) or (t - self.view_active_since < self.time_to_force_redraws_after_view_change): self.draw_list_last_frame = [] # force redraw when view has changed self.view_prev = self.view if self.draw_list_curr_frame != self.draw_list_last_frame: for i in self.draw_list_curr_frame: if i[0] == "self.hide_widget": self.hide_widget(i[1], i[2]) elif i[0] == "canvas.clear()": i[1].canvas.clear() elif i[0] == "Color": with i[1]: if len(i) == 5: Color(i[2], i[3], i[4]) else: Color(i[2], i[3], i[4], i[5]) elif i[0] == "Rectangle": with i[1]: Rectangle(size=i[2], pos=i[3], source=i[4]) elif i[0] == "Ellipse": with i[1]: Ellipse(size=i[2], pos=i[3]) elif i[0] == "Line": with i[1]: Line(points=i[2], width=i[3], cap=i[4]) else: print("Unknown draw command: " + i[0]) self.draw_list_last_frame = self.draw_list_curr_frame self.draw_list_curr_frame = [] def process_touch_events(self): t = time.time() if (self.touch_down_event_curr is not None) and (self.touch_down_event_curr.processed == False) and \ (t - self.touch_down_event_curr.time > self.touch_delay_time): self.touch_down_function(self.touch_down_event_curr.touch) self.touch_down_event_curr.processed = True self.touch_down_event_prev = copy.copy(self.touch_down_event_curr) self.touch_down_event_curr = None if (self.touch_move_event_curr is not None) and (self.touch_move_event_curr.processed == False) and \ (t - self.touch_move_event_curr.time > self.touch_delay_time): self.touch_move_function(self.touch_move_event_curr.touch) self.touch_move_event_curr.processed = True self.touch_move_event_prev = copy.copy(self.touch_move_event_curr) self.touch_move_event_curr = None if (self.touch_up_event_curr is not None) and (self.touch_up_event_curr.processed == False) and \ (t - self.touch_up_event_curr.time > self.touch_delay_time): self.touch_up_function(self.touch_up_event_curr.touch) self.touch_up_event_curr.processed = True self.touch_up_event_prev = copy.copy(self.touch_up_event_curr) self.touch_up_event_curr = None def update_display(self, *args): self.process_touch_events() self.check_alarm() # Hide all dynamic widgets; will be enabled when updating respecive view self.draw_list_curr_frame.append(["self.hide_widget", self.ids["face_plate"], True]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["face"], True]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["hands"], True]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu_background"], True]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu"], True]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu_wake_up_sound_background"], True]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu_wake_up_sound"], True]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu_theme_background"], True]) self.draw_list_curr_frame.append(["self.hide_widget", self.ids["settings_menu_theme"], True]) self.update_background() self.update_light_button() self.update_play_button() self.update_set_alarm_button() self.update_settings_button() if self.view == "clock" or self.view == "set_alarm": self.update_clock() elif self.view.startswith("settings_menu"): self.update_settings() else: print("unknown view: " + self.view) self.draw_display() def settings_menu_wake_up_sound_select_button_cb(self): self.settings_menu_wake_up_sound_select_button_cb_hack = True alarm_settings = App.get_running_app().alarm_settings for c in self.wake_up_sound_checkboxes: c.active = False print("sound selected: " + alarm_settings.sound_selected) n = 0 for i in self.wake_up_sounds: if i == alarm_settings.sound_selected: self.wake_up_sound_checkboxes[n].active = True n = n + 1 self.view = "settings_menu_wake_up_sound" self.view_active_since = time.time() def settings_menu_wake_up_sound_Ok_button_cb(self, event): alarm_settings = App.get_running_app().alarm_settings self.ids["settings_menu_wake_up_sound_select_button"].text = alarm_settings.sound_selected self.view = "settings_menu" self.view_active_since = time.time() def settings_menu_wake_up_sound_cb(self, instance, value): n = 0 for c in self.wake_up_sound_checkboxes: if c == instance: break n = n + 1 k = 0 for sound in self.wake_up_sounds: if k == n: break k = k + 1 alarm_settings = App.get_running_app().alarm_settings if self.settings_menu_wake_up_sound_select_button_cb_hack: self.settings_menu_wake_up_sound_select_button_cb_hack = False if not (alarm_settings.sound_selected == "" and sound != ""): return if value == True: print("You selected " + sound) else: print("You deselected " + sound) alarm_settings.sound_source = self.wake_up_sounds[sound] alarm_settings.sound_selected = sound def settings_menu_theme_select_button_cb(self): self.settings_menu_wake_up_theme_button_cb_hack = True alarm_settings = App.get_running_app().alarm_settings self.ids["settings_menu_theme_Dark"].active = False self.ids["settings_menu_theme_Light"].active = False print("theme selected: " + self.theme.name) if self.theme.name == "Dark": self.ids["settings_menu_theme_Dark"].active = True elif self.theme.name == "Light": self.ids["settings_menu_theme_Light"].active = True self.view = "settings_menu_theme" self.view_active_since = time.time() def settings_menu_theme_Ok_button_cb(self): alarm_settings = App.get_running_app().alarm_settings self.ids["settings_menu_theme_select_button"].text = self.theme.name self.view = "settings_menu" self.view_active_since = time.time() def settings_menu_theme_cb(self, instance, value, theme): alarm_settings = App.get_running_app().alarm_settings if self.settings_menu_theme_select_button_cb_hack: self.settings_menu_theme_select_button_cb_hack = False if not (self.theme.name == "" and theme != ""): return if value == True: print("You selected " + theme) else: print("You deselected " + theme) self.theme_selected = theme if theme == "Automatic": self.theme = Theme("Light") else: self.theme = Theme(theme) def volume_slider_value(self, *args): alarm_settings = App.get_running_app().alarm_settings alarm_settings.volume = int(args[1]) print("Volume changed to " + str(alarm_settings.volume)) with pulsectl.Pulse('volume-increaser') as pulse: for sink in pulse.sink_list(): # Volume is usually in 0-1.0 range, with >1.0 being soft-boosted old_vol = pulse.volume_get_all_chans(sink) pulse.volume_set_all_chans(sink, alarm_settings.volume / 20.0) new_vol = pulse.volume_get_all_chans(sink) print("HW volume changed from " + str(old_vol) + " to " + str(new_vol)) def wake_up_brightness_slider_value(self, *args): alarm_settings = App.get_running_app().alarm_settings alarm_settings.wake_up_brightness = int(args[1]) print("Wake up brightness changed to " + str(alarm_settings.wake_up_brightness)) def reading_light_brightness_slider_value(self, *args): alarm_settings = App.get_running_app().alarm_settings alarm_settings.reading_light_brightness = int(args[1]) if (self.light_state == "reading"): self.intensity_target = alarm_settings.reading_light_brightness / 20.0 print("Reading light brightness changed to " + str(alarm_settings.reading_light_brightness)) def display_brightness_slider_value(self, *args): alarm_settings = App.get_running_app().alarm_settings alarm_settings.display_brightness = int(args[1]) def on_light_button_pressed(self): print("light button pressed from view " + self.view) alarm_settings = App.get_running_app().alarm_settings if self.light_state == "off": self.light_state = "reading" self.intensity_target = alarm_settings.reading_light_brightness / 20.0 elif self.light_state == "reading": self.light_state = "off" self.intensity_target = 0 elif self.light_state == "sunrise": # allow enabling reading mode when sunrise has not yet reached that level if self.intensity_target < alarm_settings.reading_light_brightness / 20.0: self.light_state = "reading" self.intensity_target = alarm_settings.reading_light_brightness / 20.0 else: self.light_state = "off" elif self.light_state == "on": self.light_state = "reading" self.intensity_target = 0 if alarm_settings.alarm_playing: self.stop_sound() def on_play_button_pressed(self): print("play button pressed from view " + self.view) alarm_settings = App.get_running_app().alarm_settings if alarm_settings.alarm_playing: self.stop_sound() else: self.play_sound(alarm_settings.sound_source) def on_alarm_button_pressed(self): print("alarm button pressed from view " + self.view) alarm_settings = App.get_running_app().alarm_settings alarm_settings.alarm_modified = False self.set_alarm_timeout_counter = 0 if self.view == "set_alarm": self.view = "clock" self.view_active_since = time.time() alarm_settings.alarm_activated = False else: self.view = "set_alarm" self.view_active_since = time.time() alarm_settings.alarm_activated = True def on_settings_button_pressed(self): print("settings button pressed from view " + self.view) if self.view != "settings_menu": self.view = "settings_menu" self.view_active_since = time.time() else: self.view = "clock" self.view_active_since = time.time() print("view updated to " + self.view) def touch_up_function(self, touch): self.grabbed = "" self.light_button_move_init = [] alarm_settings = App.get_running_app().alarm_settings if (self.view == "set_alarm") and (self.grabbed == "hour" or self.grabbed == "minute"): self.set_alarm_timeout_counter = 0 super(MyClockWidget, self).on_touch_up(touch) def touch_move_function(self, touch): if self.grabbed == "": return alarm_settings = App.get_running_app().alarm_settings self.alarm_set_timeout = 0 touch_curr = Touch(touch) # Ugly workaround for issue with Kivy and Raspberry Pi 3 + touch screen tol = 0.5 # 0.5 is equal to 90 degrees inc = 1 # 1 is equal to 180 degrees if (self.touch_prev.is_empty == False) and (touch_curr.angle - self.touch_prev.angle >= tol): touch_curr.spos[0] = 1 - touch_curr.spos[0] touch_curr.spos[1] = 1 - touch_curr.spos[1] touch_curr.corrected = True while touch_curr.angle - self.touch_prev.angle >= tol: touch_curr.angle -= inc elif (self.touch_prev.is_empty == False) and (touch_curr.angle - self.touch_prev.angle <= -tol): touch_curr.spos[0] = 1 - touch_curr.spos[0] touch_curr.spos[1] = 1 - touch_curr.spos[1] touch_curr.corrected = True while touch_curr.angle - self.touch_prev.angle <= -tol: touch_curr.angle += inc self.touch_prev = copy.deepcopy(touch_curr) if self.grabbed == "minute": alarm_settings.alarm_modified = True self.set_alarm_timeout_counter = 0 minute = round(-touch_curr.angle * 30 + 15) while minute < 0: minute += 60 while minute >= 60: minute -= 60 # Sometimes the hand is 30 minutes ahead / behind the place where the user touches the screen --> correct for this behavior if (((minute - alarm_settings.alarm_time.minute) >= 15) and ((minute - alarm_settings.alarm_time.minute) <= 45)): minute = minute - 30 elif (((minute - alarm_settings.alarm_time.minute) <= -15) and ((minute - alarm_settings.alarm_time.minute) >= -45)): minute = minute + 30 while minute < 0: minute += 60 while minute >= 60: minute -= 60 # hour correction hour = alarm_settings.alarm_time.hour if alarm_settings.alarm_time.minute >= 55 and minute <= 5: hour += 1 elif alarm_settings.alarm_time.minute <= 5 and minute >= 55: hour -= 1 while hour < 0: hour += 24 while hour >= 24: hour -= 24 alarm_settings.alarm_time = datetime.datetime(alarm_settings.alarm_time.year, \ alarm_settings.alarm_time.month, alarm_settings.alarm_time.day, \ hour, minute, alarm_settings.alarm_time.second, 0) elif self.grabbed == "hour": alarm_settings.alarm_modified = True self.set_alarm_timeout_counter = 0 hour = round(-touch_curr.angle * 6 + 3) while hour < 0: hour += 12 while hour >= 12: hour -= 12 # Sometimes the hand is 6 hours ahead / behind the place where the user touches the screen --> correct for this behavior if (((hour - alarm_settings.alarm_time.hour) >= 3) and ((hour - alarm_settings.alarm_time.hour) <= 9)): hour = hour - 6 if (((hour - alarm_settings.alarm_time.hour) <= -3) and ((hour - alarm_settings.alarm_time.hour) >= -9)): hour = hour + 6 while hour < 0: hour += 12 while hour >= 12: hour -= 12 if alarm_settings.alarm_time.hour >= 12: hour += 12 # AM / PM correction if alarm_settings.alarm_time.hour == 11 and hour == 0: hour = 12 elif alarm_settings.alarm_time.hour == 23 and hour == 12: hour = 0 elif alarm_settings.alarm_time.hour == 0 and hour == 11: hour = 23 elif alarm_settings.alarm_time.hour == 12 and hour == 23: hour = 11 minute = alarm_settings.alarm_time.minute alarm_settings.alarm_time = datetime.datetime(alarm_settings.alarm_time.year, \ alarm_settings.alarm_time.month, alarm_settings.alarm_time.day, \ hour, alarm_settings.alarm_time.minute, alarm_settings.alarm_time.second, 0) elif self.grabbed == "light_button": if len(self.light_button_move_init) == 0: self.light_button_move_init = touch_curr.spos d = touch_curr.spos[0] - self.light_button_move_init[0] threshold = 0.05 # Ugly workaround for issue with Kivy and Raspberry Pi 3 + touch screen: mirror d if position is on other side if self.light_button_move_init[0] > 0.5: d = -d if d > 0.05: # move to the right: set light to wake up level self.light_state = "on" elif d < -0.05: # move to the left: set light off self.light_state = "off" super(MyClockWidget, self).on_touch_move(touch) def touch_down_function(self, touch): alarm_settings = App.get_running_app().alarm_settings t = alarm_settings.alarm_time hands = self.ids["hands"] minutes_hand = self.position_on_clock(t.minute/60+t.second/3600, length=0.40*hands.size[0]) hours_hand = self.position_on_clock(t.hour/12 + t.minute/720, length=0.35*hands.size[0]) if (0.05 <= touch.spos[0] <= 0.25) and (0.85 <= touch.spos[1] <= 0.95): if self.grabbed == "": self.grabbed = "light_button" self.on_light_button_pressed() elif (0.05 <= touch.spos[0] <= 0.15) and (0.05 <= touch.spos[1] <= 0.15): self.on_play_button_pressed() elif (0.85 <= touch.spos[0] <= 0.95) and (0.05 <= touch.spos[1] <= 0.15): self.on_alarm_button_pressed() elif (0.85 <= touch.spos[0] <= 0.95) and (0.85 <= touch.spos[1] <= 0.95): self.on_settings_button_pressed() elif self.view == "set_alarm": self.set_alarm_timeout_counter = 0 if self.grabbed == "": 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" self.touch_prev.clear() 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" self.touch_prev.clear() else: self.grabbed = "" elif self.view == "settings_menu": pass elif self.view == "settings_menu_wake_up_sound": pass elif self.view == "settings_menu_theme_select": pass elif self.view == "clock": self.stop_sound() super(MyClockWidget, self).on_touch_down(touch) def on_touch_down(self, touch, after=False): t = time.time() if t - self.view_active_since <= self.time_to_ignore_inputs_after_view_change: return True """ if self.touch_down_event_prev is None: print("touch down at " + str(t) + " on " + str(touch.spos) + ", after: " + str(after) + ", prev event: None") else: print("touch down at " + str(t) + " on " + str(touch.spos) + ", after: " + str(after) + ", prev event at " + str(self.touch_down_event_prev.time) + " on " + str(self.touch_down_event_prev.touch.spos)) """ if after: self.touch_down_event_curr = TouchEvent(touch) return True else: Clock.schedule_once(lambda dt: self.on_touch_down(touch, True)) return super(MyClockWidget, self).on_touch_down(touch) def on_touch_move(self, touch, after=False): t = time.time() if t - self.view_active_since <= self.time_to_ignore_inputs_after_view_change: return True """ if self.touch_move_event_prev is None: print("touch move at " + str(t) + " on " + str(touch.spos) + ", after: " + str(after) + ", prev event: None") else: print("touch move at " + str(t) + " on " + str(touch.spos) + ", after: " + str(after) + ", prev event at " + str(self.touch_move_event_prev.time) + " on " + str(self.touch_move_event_prev.touch.spos)) """ if after: # Do not delay processing of move events self.touch_move_function(touch) return True else: Clock.schedule_once(lambda dt: self.on_touch_move(touch, True)) return super(MyClockWidget, self).on_touch_move(touch) def on_touch_up(self, touch, after=False): t = time.time() if t - self.view_active_since <= self.time_to_ignore_inputs_after_view_change: return True """ if self.touch_up_event_prev is None: print("touch up at " + str(t) + " on " + str(touch.spos) + ", after: " + str(after) + ", prev event: None") else: print("touch up at " + str(t) + " on " + str(touch.spos) + ", after: " + str(after) + ", prev event at " + str(self.touch_up_event_prev.time) + " on " + str(self.touch_up_event_prev.touch.spos)) """ if after: self.touch_up_event_curr = TouchEvent(touch) return True else: Clock.schedule_once(lambda dt: self.on_touch_up(touch, True)) return super(MyClockWidget, self).on_touch_up(touch) class MyApp(App): alarm_settings = AlarmSettings() update_rate = 60.0 # apply volume setting with pulsectl.Pulse('volume-increaser') as pulse: for sink in pulse.sink_list(): # Volume is usually in 0-1.0 range, with >1.0 being soft-boosted old_vol = pulse.volume_get_all_chans(sink) pulse.volume_set_all_chans(sink, alarm_settings.volume / 20.0) new_vol = pulse.volume_get_all_chans(sink) print("HW volume changed from " + str(old_vol) + " to " + str(new_vol)) def build(self): clock_widget = MyClockWidget() x = clock_widget.ids["settings_menu_wake_up_sound_boxlayout"] gl = GridLayout( cols=2, ) clock_widget.wake_up_sounds = WakeUpSounds self.alarm_settings.sound_selected = next(iter(clock_widget.wake_up_sounds)) self.alarm_settings.sound_source = clock_widget.wake_up_sounds[self.alarm_settings.sound_selected] clock_widget.ids["settings_menu_wake_up_sound_select_button"].text = self.alarm_settings.sound_selected clock_widget.wake_up_sound_checkboxes = [] clock_widget.wake_up_sound_labels = [] i = 0 for w in clock_widget.wake_up_sounds: c = CheckBox( group = "settings_menu_wake_up_sound", size = [gl.size[0] * 0.1, gl.size[1]], ) c.bind(active=clock_widget.settings_menu_wake_up_sound_cb) gl.add_widget(c) clock_widget.wake_up_sound_checkboxes.append(c) if i == 0: a = True else: a = False l = Label( text = w, halign = "left", valign = "middle", size = [gl.size[0] * 2.5, c.size[1]], text_size = Window.size, font_size = Window.height*0.05, color = clock_widget.theme.color_font, active = a ) l.text_size = l.size gl.add_widget(l) clock_widget.wake_up_sound_labels.append(l) i = i + 1 x.add_widget(gl) b = Button( text = "Ok", font_size = clock_widget.height*0.3, color = clock_widget.theme.color_font, background_normal = '', background_color = clock_widget.theme.color_button ) b.bind(on_press = clock_widget.settings_menu_wake_up_sound_Ok_button_cb) x.add_widget(b) clock_widget.settings_menu_wake_up_sound_Ok_button = b update_rate = App.get_running_app().update_rate # update initially, just after construction of the widget is complete Clock.schedule_once(clock_widget.update_display, 0) # then update at update_rate times per second Clock.schedule_interval(clock_widget.update_display, 1.0/update_rate) if is_arm(): Window.borderless = True return clock_widget def except_hook(type, value, tb): if is_arm(): pixels.fill((0, 0, 0, 0)) pixels.show() backlight.brightness = 50 return if __name__ == '__main__': if is_arm(): sys.excepthook = except_hook MyApp().run()