wasp-os/wasp/apps/alarm.py
Adam Blair e09b951017 Advanced alarm app
Features:
* Multiple alarms (up to 4)
* Day of the week support
* One time alarms
* Snooze

Changes to wasp-os for app support:
* Added + and - to the 28pt and 36pt fonts
* Checkboxes now require a click on the body of the checkbox if there is no label
* Added a Toggle Button class that extends Button and stores a state like checkbox

Signed-off-by: Adam Blair <adampblair@protonmail.com>
2021-07-25 08:56:14 +01:00

376 lines
13 KiB
Python

# SPDX-License-Identifier: LGPL-3.0-or-later
# Copyright (C) 2020 Daniel Thompson
# Copyright (C) 2020 Joris Warmbier
# Copyright (C) 2021 Adam Blair
"""Alarm Application
~~~~~~~~~~~~~~~~~~~~
An application to set a vibration alarm. All settings can be accessed from the Watch UI.
.. figure:: res/AlarmApp.png
:width: 179
Screenshot of the Alarm Application
"""
import wasp
import fonts
import time
import widgets
import array
from micropython import const
# 2-bit RLE, generated from res/alarm_icon.png, 390 bytes
icon = (
b'\x02'
b'`@'
b'\x17@\xd2G#G-K\x1fK)O\x1bO&O'
b'\n\x80\xb4\x89\x0bN$N\x08\x91\tM"M\x07\x97'
b'\x07M!L\x06\x9b\x07K K\x06\x9f\x06K\x1fJ'
b'\x05\xa3\x05J\x1eJ\x05\x91\xc0\xd0\xc3\x91\x05J\x1dI'
b'\x05\x8c\xcf\x8c\x05I\x1dH\x05\x8b\xd3\x8b\x05H\x1dG'
b'\x05\x8a\xd7\x8a\x05G\x1dG\x04\x89\xdb\x89\x05F\x1dF'
b'\x04\x89\xcc\x05\xcc\x89\x04F\x1dE\x04\x89\xcd\x05\xcd\x89'
b'\x04E\x1eD\x03\x88\xce\x07\xce\x88\x04C\x1fC\x04\x88'
b'\xce\x07\xce\x88\x04C\x1fC\x03\x88\xcf\x07\xcf\x88\x04A'
b'!A\x04\x87\xd0\x07\xd0\x87\x04A%\x87\xd1\x07\xd1\x87'
b")\x87\xd1\x07\xd1\x87(\x87\xd2\x07\xd2\x87'\x87\xd2\x07"
b"\xd2\x87'\x86\xd3\x07\xd3\x86&\x87\xd3\x07\xd3\x87%\x86"
b'\xd4\x07\xd4\x86%\x86\xd4\x07\xd4\x86%\x86\xd4\x07\xd4\x86'
b'$\x87\xd4\x07\xd4\x87#\x87\xd4\x07\xd4\x87#\x87\xd4\x07'
b'\xd4\x87#\x86\xd4\x08\xd5\x86#\x86\xd3\t\xd5\x86#\x86'
b'\xd2\t\xd6\x86#\x87\xd0\n\xd5\x87#\x87\xcf\n\xd6\x87'
b'#\x87\xce\n\xd7\x87$\x86\xce\t\xd8\x86%\x86\xce\x08'
b'\xd9\x86%\x86\xcd\x08\xda\x86%\x87\xcc\x07\xda\x87%\x87'
b"\xcc\x06\xdb\x86'\x87\xcc\x03\xdc\x87'\x87\xeb\x87(\x87"
b'\xe9\x87)\x87\xe9\x87*\x87\xe7\x87+\x88\xe5\x88,\x87'
b'\xe5\x87-\x88\xe3\x88.\x88\xe1\x880\x89\xdd\x892\x89'
b'\xdb\x893\x8b\xd7\x8b2\x8d\xd4\x8e0\x91\xcf\x91.\x97'
b'\xc5\x97,\xb5+\x88\x03\x9f\x03\x88*\x88\x05\x9d\x05\x88'
b')\x87\t\x97\t\x87*\x85\x0c\x93\x0c\x85,\x83\x11\x8b'
b'\x11\x83\x17'
)
# Enabled masks
_MONDAY = const(0x01)
_TUESDAY = const(0x02)
_WEDNESDAY = const(0x04)
_THURSDAY = const(0x08)
_FRIDAY = const(0x10)
_SATURDAY = const(0x20)
_SUNDAY = const(0x40)
_WEEKDAYS = const(0x1F)
_WEEKENDS = const(0x60)
_EVERY_DAY = const(0x7F)
_IS_ACTIVE = const(0x80)
# Alarm data indices
_HOUR_IDX = const(0)
_MIN_IDX = const(1)
_ENABLED_IDX = const(2)
# Pages
_HOME_PAGE = const(-1)
_RINGING_PAGE = const(-2)
class AlarmApp:
"""Allows the user to set a vibration alarm.
"""
NAME = 'Alarm'
ICON = icon
def __init__(self):
"""Initialize the application."""
self.page = _HOME_PAGE
self.alarms = (bytearray(3), bytearray(3), bytearray(3), bytearray(3))
self.pending_alarms = array.array('d', [0.0, 0.0, 0.0, 0.0])
# Set a nice default
self.num_alarms = 1
for alarm in self.alarms:
alarm[_HOUR_IDX] = 8
alarm[_MIN_IDX] = 0
alarm[_ENABLED_IDX] = 0
self.alarms[0][_ENABLED_IDX] = _WEEKDAYS
def foreground(self):
"""Activate the application."""
self.del_alarm_btn = widgets.Button(170, 204, 70, 35, 'DEL')
self.hours_wid = widgets.Spinner(50, 30, 0, 23, 2)
self.min_wid = widgets.Spinner(130, 30, 0, 59, 2)
self.day_btns = (widgets.ToggleButton(10, 145, 40, 35, 'Mo'),
widgets.ToggleButton(55, 145, 40, 35, 'Tu'),
widgets.ToggleButton(100, 145, 40, 35, 'We'),
widgets.ToggleButton(145, 145, 40, 35, 'Th'),
widgets.ToggleButton(190, 145, 40, 35, 'Fr'),
widgets.ToggleButton(10, 185, 40, 35, 'Sa'),
widgets.ToggleButton(55, 185, 40, 35, 'Su'))
self.alarm_checks = (widgets.Checkbox(200, 57), widgets.Checkbox(200, 102),
widgets.Checkbox(200, 147), widgets.Checkbox(200, 192))
self._deactivate_pending_alarms()
self._draw()
wasp.system.request_event(wasp.EventMask.TOUCH | wasp.EventMask.SWIPE_LEFTRIGHT | wasp.EventMask.BUTTON)
wasp.system.request_tick(1000)
def background(self):
"""De-activate the application."""
if self.page > _HOME_PAGE:
self._save_alarm()
self.page = _HOME_PAGE
del self.del_alarm_btn
del self.hours_wid
del self.min_wid
del self.alarm_checks
del self.day_btns
self._set_pending_alarms()
def tick(self, ticks):
"""Notify the application that its periodic tick is due."""
if self.page == _RINGING_PAGE:
wasp.watch.vibrator.pulse(duty=50, ms=500)
wasp.system.keep_awake()
else:
wasp.system.bar.update()
def press(self, button, state):
""""Notify the application of a button press event."""
if self.page == _RINGING_PAGE:
self._snooze()
wasp.system.navigate(wasp.EventType.HOME)
def swipe(self, event):
""""Notify the application of a swipe event."""
if self.page == _RINGING_PAGE:
self._silence_alarm()
elif self.page > _HOME_PAGE:
self._save_alarm()
self._draw()
else:
wasp.system.navigate(event[0])
def touch(self, event):
"""Notify the application of a touchscreen touch event."""
if self.page == _RINGING_PAGE:
self._silence_alarm()
elif self.page > _HOME_PAGE:
if self.hours_wid.touch(event) or self.min_wid.touch(event):
return
for day_btn in self.day_btns:
if day_btn.touch(event):
return
if self.del_alarm_btn.touch(event):
self._remove_alarm(self.page)
elif self.page == _HOME_PAGE:
for index, checkbox in enumerate(self.alarm_checks):
if checkbox.touch(event):
if checkbox.state:
self.alarms[index][_ENABLED_IDX] |= _IS_ACTIVE
else:
self.alarms[index][_ENABLED_IDX] &= ~_IS_ACTIVE
self._draw(index)
return
for index, alarm in enumerate(self.alarms):
# Open edit page for clicked alarms
if index < self.num_alarms and event[1] < 190 \
and 60 + (index * 45) < event[2] < 60 + ((index + 1) * 45):
self.page = index
self._draw()
return
# Add new alarm if plus clicked
elif index == self.num_alarms and 60 + (index * 45) < event[2]:
self.num_alarms += 1
self._draw(index)
return
def _remove_alarm(self, alarm_index):
# Shift alarm indices
for index in range(alarm_index, 3):
self.alarms[index][_HOUR_IDX] = self.alarms[index + 1][_HOUR_IDX]
self.alarms[index][_MIN_IDX] = self.alarms[index + 1][_MIN_IDX]
self.alarms[index][_ENABLED_IDX] = self.alarms[index + 1][_ENABLED_IDX]
self.pending_alarms[index] = self.pending_alarms[index + 1]
# Set last alarm to default
self.alarms[3][_HOUR_IDX] = 8
self.alarms[3][_MIN_IDX] = 0
self.alarms[3][_ENABLED_IDX] = 0
self.page = _HOME_PAGE
self.num_alarms -= 1
self._draw()
def _save_alarm(self):
alarm = self.alarms[self.page]
alarm[_HOUR_IDX] = self.hours_wid.value
alarm[_MIN_IDX] = self.min_wid.value
for day_idx, day_btn in enumerate(self.day_btns):
if day_btn.state:
alarm[_ENABLED_IDX] |= 1 << day_idx
else:
alarm[_ENABLED_IDX] &= ~(1 << day_idx)
self.page = _HOME_PAGE
def _draw(self, update_alarm_row=-1):
if self.page == _RINGING_PAGE:
self._draw_ringing_page()
elif self.page > _HOME_PAGE:
self._draw_edit_page()
else:
self._draw_home_page(update_alarm_row)
def _draw_ringing_page(self):
draw = wasp.watch.drawable
draw.set_color(wasp.system.theme('bright'))
draw.fill()
draw.set_font(fonts.sans24)
draw.string("Alarm", 0, 150, width=240)
draw.blit(icon, 73, 50)
draw.line(35, 1, 35, 239)
draw.string('Z', 10, 80)
draw.string('z', 10, 110)
draw.string('z', 10, 140)
def _draw_edit_page(self):
draw = wasp.watch.drawable
alarm = self.alarms[self.page]
draw.fill()
self._draw_system_bar()
self.hours_wid.value = alarm[_HOUR_IDX]
self.min_wid.value = alarm[_MIN_IDX]
draw.set_font(fonts.sans28)
draw.string(':', 110, 90-14, width=20)
self.del_alarm_btn.draw()
self.hours_wid.draw()
self.min_wid.draw()
for day_idx, day_btn in enumerate(self.day_btns):
day_btn.state = alarm[_ENABLED_IDX] & (1 << day_idx)
day_btn.draw()
def _draw_home_page(self, update_alarm_row=_HOME_PAGE):
draw = wasp.watch.drawable
if update_alarm_row == _HOME_PAGE:
draw.set_color(wasp.system.theme('bright'))
draw.fill()
self._draw_system_bar()
draw.line(0, 50, 240, 50, width=1, color=wasp.system.theme('bright'))
for index in range(len(self.alarms)):
if index < self.num_alarms and (update_alarm_row == _HOME_PAGE or update_alarm_row == index):
self._draw_alarm_row(index)
elif index == self.num_alarms:
# Draw the add button
draw.set_color(wasp.system.theme('bright'))
draw.set_font(fonts.sans28)
draw.string('+', 100, 60 + (index * 45))
def _draw_alarm_row(self, index):
draw = wasp.watch.drawable
alarm = self.alarms[index]
self.alarm_checks[index].state = alarm[_ENABLED_IDX] & _IS_ACTIVE
self.alarm_checks[index].draw()
if self.alarm_checks[index].state:
draw.set_color(wasp.system.theme('bright'))
else:
draw.set_color(wasp.system.theme('mid'))
draw.set_font(fonts.sans28)
draw.string("{:02d}:{:02d}".format(alarm[_HOUR_IDX], alarm[_MIN_IDX]), 10, 60 + (index * 45), width=120)
draw.set_font(fonts.sans18)
draw.string(self._get_repeat_code(alarm[_ENABLED_IDX]), 130, 70 + (index * 45), width=60)
draw.line(0, 95 + (index * 45), 240, 95 + (index * 45), width=1, color=wasp.system.theme('bright'))
def _draw_system_bar(self):
sbar = wasp.system.bar
sbar.clock = True
sbar.draw()
def _alert(self):
self.page = _RINGING_PAGE
wasp.system.wake()
wasp.system.switch(self)
def _snooze(self):
now = wasp.watch.rtc.get_localtime()
alarm = (now[0], now[1], now[2], now[3], now[4] + 10, now[5], 0, 0, 0)
wasp.system.set_alarm(time.mktime(alarm), self._alert)
def _silence_alarm(self):
mute = wasp.watch.display.mute
mute(True)
self._draw()
mute(False)
wasp.system.navigate(wasp.EventType.HOME)
def _set_pending_alarms(self):
now = wasp.watch.rtc.get_localtime()
for index, alarm in enumerate(self.alarms):
if index < self.num_alarms and alarm[_ENABLED_IDX] & _IS_ACTIVE:
yyyy = now[0]
mm = now[1]
dd = now[2]
HH = alarm[_HOUR_IDX]
MM = alarm[_MIN_IDX]
# If next alarm is tomorrow increment the day
if HH < now[3] or (HH == now[3] and MM <= now[4]):
dd += 1
pending_time = time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0))
# If this is not a one time alarm find the next day of the week that is enabled
if alarm[_ENABLED_IDX] & ~_IS_ACTIVE != 0:
for _i in range(7):
if (1 << time.localtime(pending_time)[6]) & alarm[_ENABLED_IDX] == 0:
dd += 1
pending_time = time.mktime((yyyy, mm, dd, HH, MM, 0, 0, 0, 0))
else:
break
self.pending_alarms[index] = pending_time
wasp.system.set_alarm(pending_time, self._alert)
else:
self.pending_alarms[index] = 0.0
def _deactivate_pending_alarms(self):
now = wasp.watch.rtc.get_localtime()
now = time.mktime((now[0], now[1], now[2], now[3], now[4], now[5], 0, 0, 0))
for index, alarm in enumerate(self.alarms):
pending_alarm = self.pending_alarms[index]
if not pending_alarm == 0.0:
wasp.system.cancel_alarm(pending_alarm, self._alert)
# If this is a one time alarm and in the past disable it
if alarm[_ENABLED_IDX] & ~_IS_ACTIVE == 0 and pending_alarm <= now:
alarm[_ENABLED_IDX] = 0
@staticmethod
def _get_repeat_code(days):
# Ignore the is_active bit
days = days & ~_IS_ACTIVE
if days == _WEEKDAYS:
return "wkds"
elif days == _WEEKENDS:
return "wkns"
elif days == _EVERY_DAY:
return "evry"
elif days == 0:
return "once"
else:
return "cust"