f288f441c7
Add a pomodoro timer * added pomodoro.py * added icon * mention pomodoro in the readme * updated docstring * make compatible with lack of get/set attribute * fix wrong offset when swiping up * added screenshot* added pomodoro.py Icon design by Plan5. Thanks! Co-Authored-By: Plan5 Signed-off-by: thiswillbeyourgithub <github@32mail.33mail.com>
308 lines
12 KiB
Python
308 lines
12 KiB
Python
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
# Copyright (C) 2022 github user thiswillbeyourgithub
|
|
"""Pomodoro Application
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
A pomodoro app, forked from timer.py. Swipe laterally to load presets and vertically
|
|
to change number of vibration. Vibration patterns are randomized if vibrating
|
|
more than 4 times to make sure you notice.
|
|
|
|
.. figure:: res/screenshots/PomodoroApp.png
|
|
:width: 179
|
|
"""
|
|
|
|
import wasp
|
|
import fonts
|
|
import widgets
|
|
import math
|
|
import random
|
|
from micropython import const
|
|
|
|
# 2-bit RLE, 57x56, generated from png provided by plan5, 211 bytes
|
|
icon = (
|
|
b'\x02'
|
|
b'98'
|
|
b'\x1d@\x1eD3E3E5C:\x80\xb4\x82)C'
|
|
b'\x01\x83\x05B\x86$I\x03C\x89#F\x05C\xc0\xd2'
|
|
b'\xc1\x89-D\x8a\x18\x82\x0c\x82\x04C\x01\x8b\x15\x84\x08'
|
|
b'\x85\n\x8a\x13\x92\x05\x83\x03\x8a\x11\x93\x03\x88\x01\x8a\x0f'
|
|
b'\x93\x03\x95\r\x94\x02\x97\x0b\x95\x01\x99\t\xb1\x08\xb1\x07'
|
|
b'\xa7\x01\x8b\x06\xa5\x03\x8b\x05\xa4\x04\x8d\x04\xa1\x06\x8e\x03'
|
|
b'\xa0\x07\x90\x02\x9e\x08\x91\x02\x9b\x0b\x91\x02\x99\x0c\x92\x02'
|
|
b'\x98\x0c\x93\x01\x99\x0b\xad\x03\x84\x04\xae\x03\x84\x03\xaf\x03'
|
|
b'\x84\x03\xb0\x02\x84\x02\x82\x01\xaf\x05\x83\x02\x95\x01\x9a\x03'
|
|
b'\x83\x04\x93\x02\xa0\x05\x92\x02\xa1\x04\x92\x02\xa2\x04\x91\x02'
|
|
b'\xa4\x03\x90\x03\xa4\x03\x8e\x04\xa5\x02\x8e\x05\xa6\x01\x8c\x06'
|
|
b'\xa7\x01\x8b\x07\xb1\x08\xb1\t\xaf\x0b\xad\r\xab\x0f\xa9\x11'
|
|
b'\xa7\x13\xa5\x15\xa3\x18\x9f\x1c\x9b \x97%\x91,\x89\x18'
|
|
)
|
|
|
|
|
|
_STOPPED = const(0)
|
|
_RUNNING = const(1)
|
|
_RINGING = const(2)
|
|
_REPEAT_MAX = const(99) # auto stop repeat after 99 runs
|
|
_FIELDS = const(1234567890)
|
|
_MAX_VIB = const(15) # max number of second the watch you vibrate
|
|
|
|
# you can add your own presets here:
|
|
_PRESETS = ['2,10', '10,2', '20,5']
|
|
_TIME_MODE = const(1) # if 0: duration of vibration will be discounted
|
|
# from timer. if 1: not discounted
|
|
|
|
|
|
class PomodoroApp():
|
|
"""Allows the user to set a periodic vibration alarm, Pomodoro style."""
|
|
NAME = 'Pomodoro'
|
|
ICON = icon
|
|
|
|
def __init__(self):
|
|
self._already_initialized = False
|
|
|
|
def _actual_init(self):
|
|
self.current_alarm = None
|
|
self.nb_vibrat_total = _STOPPED # number of time it vibrated
|
|
self.last_preset = _STOPPED
|
|
self.last_run = -1 # to keep track of where in the queue we are
|
|
self.state = _STOPPED
|
|
|
|
# reloading last value
|
|
try:
|
|
last_val = wasp.system.get("pomodoro")
|
|
self.nb_vibrat_per_alarm = int(last_val[0])
|
|
self.queue = last_val[1]
|
|
except:
|
|
self.queue = _PRESETS[0]
|
|
self.nb_vibrat_per_alarm = 10 # number of times to vibrate each time
|
|
return True
|
|
|
|
def foreground(self):
|
|
if not self._already_initialized:
|
|
self._already_initialized = self._actual_init()
|
|
self._draw()
|
|
wasp.system.request_event(wasp.EventMask.TOUCH |
|
|
wasp.EventMask.SWIPE_LEFTRIGHT |
|
|
wasp.EventMask.SWIPE_UPDOWN)
|
|
wasp.system.request_tick(1000)
|
|
|
|
def background(self):
|
|
if self.state == _RINGING:
|
|
self._start()
|
|
elif self.state == _STOPPED:
|
|
self.__init__()
|
|
|
|
def sleep(self):
|
|
"""don't exit when screen turns off"""
|
|
return True
|
|
|
|
def tick(self, ticks):
|
|
if self.state == _RINGING:
|
|
if self.nb_vibrat_per_alarm <= 3:
|
|
wasp.watch.vibrator.pulse(duty=50, ms=650)
|
|
else:
|
|
if random.random() > 0.7: # one very long vibration
|
|
wasp.watch.vibrator.pulse(duty=random.randint(3, 60),
|
|
ms=900)
|
|
else: # burst of vibration
|
|
max_dur = 900
|
|
done = 0
|
|
while done <= max_dur:
|
|
new_vibr = random.randint(20, 200)
|
|
new_sleep = random.randint(0, 300)
|
|
while done + new_vibr + new_sleep > max_dur * 1.1:
|
|
new_vibr = random.randint(20, 200)
|
|
new_sleep = random.randint(0, 300)
|
|
wasp.watch.vibrator.pulse(duty=3, ms=new_vibr)
|
|
wasp.watch.time.sleep(new_sleep * 0.001)
|
|
done += new_vibr + new_sleep
|
|
wasp.system.keep_awake()
|
|
self.nb_vibrat_total += 1
|
|
if self.nb_vibrat_total % self.nb_vibrat_per_alarm == 0:
|
|
# vibrated self.nb_vibrat_per_alarm times so
|
|
# no more repeat needed
|
|
if self.nb_vibrat_total // self.nb_vibrat_per_alarm // len(self.squeue) < _REPEAT_MAX:
|
|
# restart another
|
|
self._start()
|
|
else: # avoid running for days if forgotten
|
|
self._stop()
|
|
|
|
else:
|
|
self._update()
|
|
|
|
def swipe(self, event):
|
|
if self.state == _STOPPED:
|
|
draw = wasp.watch.drawable
|
|
draw.set_font(fonts.sans24)
|
|
if event[0] == wasp.EventType.UP:
|
|
self.nb_vibrat_per_alarm = max(1, (self.nb_vibrat_per_alarm + 1) % (_MAX_VIB + 1))
|
|
elif event[0] == wasp.EventType.DOWN:
|
|
self.nb_vibrat_per_alarm -= 1
|
|
if self.nb_vibrat_per_alarm == 0:
|
|
self.nb_vibrat_per_alarm = _MAX_VIB
|
|
else:
|
|
if event[0] == wasp.EventType.RIGHT:
|
|
self.last_preset += 1
|
|
elif event[0] == wasp.EventType.LEFT:
|
|
self.last_preset -= 1
|
|
self.last_preset %= len(_PRESETS)
|
|
self.queue = _PRESETS[self.last_preset]
|
|
draw.string(self.queue, 0, 35, right=True, width=240)
|
|
draw.string("V{}".format(self.nb_vibrat_per_alarm), 0, 35)
|
|
|
|
def touch(self, event):
|
|
if self.state == _RINGING:
|
|
mute = wasp.watch.display.mute
|
|
mute(False)
|
|
elif self.state == _RUNNING:
|
|
if self.btn_stop.touch(event):
|
|
self._stop()
|
|
elif self.btn_add.touch(event):
|
|
wasp.system.cancel_alarm(self.current_alarm, self._alert)
|
|
self.current_alarm += 60
|
|
wasp.system.set_alarm(self.current_alarm, self._alert)
|
|
wasp.watch.vibrator.pulse(duty=25, ms=50)
|
|
self._update()
|
|
elif self.state == _STOPPED:
|
|
if self.btn_del.touch(event):
|
|
if len(self.queue) > 1:
|
|
self.queue = self.queue[:-1]
|
|
else:
|
|
self.queue = ""
|
|
elif self.btn_start.touch(event):
|
|
if self.queue != "" and not self.queue.endswith(","):
|
|
self.squeue = [int(x) for x in self.queue.split(",")]
|
|
self._start(first_run=True)
|
|
return
|
|
elif len(self.queue) < 13:
|
|
if self.btn_add.touch(event):
|
|
if len(self.queue) >= 1 and self.queue[-1] != ",":
|
|
self.queue += ","
|
|
else:
|
|
for i, b in enumerate(self.btns):
|
|
if b.touch(event):
|
|
self.queue += str((i + 1) % 10)
|
|
break
|
|
draw = wasp.watch.drawable
|
|
draw.set_font(fonts.sans24)
|
|
draw.string(self.queue, 0, 35, right=True, width=240)
|
|
draw.string("V{}".format(self.nb_vibrat_per_alarm), 0, 35)
|
|
|
|
def _start(self, first_run=False):
|
|
if self.btns is not None: # save some memory
|
|
for b in self.btns:
|
|
b = None
|
|
del b
|
|
self.btns = None
|
|
wasp.gc.collect()
|
|
|
|
self.state = _RUNNING
|
|
now = wasp.watch.rtc.time()
|
|
self.last_run += 1
|
|
if self.last_run >= len(self.squeue):
|
|
self.last_run = 0
|
|
m = self.squeue[self.last_run]
|
|
|
|
# reduce by one second if repeating, to avoid growing offset
|
|
if first_run or _TIME_MODE == 1:
|
|
self.current_alarm = now + max(m * 60, 1)
|
|
else:
|
|
self.current_alarm = now + max(m * 60 - self.nb_vibrat_per_alarm, 1)
|
|
wasp.system.set_alarm(self.current_alarm, self._alert)
|
|
self._draw()
|
|
if hasattr(wasp.system, "set") and callable(wasp.system.set):
|
|
wasp.system.set("pomodoro", [self.nb_vibrat_per_alarm, self.queue])
|
|
|
|
def _stop(self):
|
|
self.state = _STOPPED
|
|
wasp.system.cancel_alarm(self.current_alarm, self._alert)
|
|
self.last_run = -1
|
|
self.nb_vibrat_total = 0
|
|
self._draw()
|
|
|
|
def _draw(self):
|
|
"""Draw the display from scratch."""
|
|
draw = wasp.watch.drawable
|
|
draw.fill()
|
|
|
|
if self.state == _RINGING:
|
|
draw.set_font(fonts.sans24)
|
|
draw.string(self.NAME, 0, 150, width=240)
|
|
draw.blit(icon, 73, 50)
|
|
elif self.state == _RUNNING:
|
|
self.btn_stop = widgets.Button(x=0, y=200, w=160, h=40,
|
|
label="STOP")
|
|
self.btn_stop.draw()
|
|
self.btn_add = widgets.Button(x=160, y=200, w=80, h=40,
|
|
label="+1")
|
|
self.btn_add.draw()
|
|
draw.reset()
|
|
t = "Timer #{}/{} ({})".format(self.last_run + 1,
|
|
len(self.squeue),
|
|
self.nb_vibrat_total // self.nb_vibrat_per_alarm // len(self.squeue))
|
|
draw.string(t, 10, 60)
|
|
draw.set_font(fonts.sans28)
|
|
draw.string(':', 110, 106, width=20)
|
|
|
|
self._update()
|
|
draw.set_font(fonts.sans18)
|
|
elif self.state == _STOPPED:
|
|
self.btns = []
|
|
fields = str(_FIELDS)
|
|
for y in range(2):
|
|
for x in range(5):
|
|
btn = widgets.Button(x=x*48,
|
|
y=y*65+60,
|
|
w=49,
|
|
h=59,
|
|
label=fields[x + 5*y])
|
|
btn.draw()
|
|
self.btns.append(btn)
|
|
self.btn_del = widgets.Button(x=0,
|
|
y=190,
|
|
w=76,
|
|
h=50,
|
|
label="Del.")
|
|
self.btn_del.draw()
|
|
self.btn_add = widgets.Button(x=80,
|
|
y=190,
|
|
w=76,
|
|
h=50,
|
|
label="Then")
|
|
self.btn_add.draw()
|
|
self.btn_start = widgets.Button(x=160,
|
|
y=190,
|
|
w=80,
|
|
h=50,
|
|
label="Go")
|
|
self.btn_start.update(txt=0, frame=wasp.system.theme('mid'),
|
|
bg=0xf800)
|
|
draw.reset()
|
|
draw.set_font(fonts.sans24)
|
|
draw.string(self.queue, 0, 35, right=True, width=240)
|
|
draw.string("V{}".format(self.nb_vibrat_per_alarm), 0, 35)
|
|
sbar = wasp.system.bar
|
|
sbar.clock = True
|
|
sbar.draw()
|
|
|
|
def _update(self):
|
|
wasp.system.bar.update()
|
|
draw = wasp.watch.drawable
|
|
if self.state == _RUNNING:
|
|
now = wasp.watch.rtc.time()
|
|
s = max(self.current_alarm - now, 0)
|
|
m = math.floor(s // 60)
|
|
s = math.floor(s) % 60
|
|
if len(str(m)) > 5:
|
|
prefix = "+"
|
|
m = int(str(m)[-4:])
|
|
else:
|
|
prefix = ""
|
|
draw.set_font(fonts.sans28)
|
|
draw.string("{}{:02d}:{:02d}".format(prefix, m, s), 90, 106,
|
|
width=60)
|
|
|
|
def _alert(self):
|
|
self.state = _RINGING
|
|
wasp.system.wake()
|
|
wasp.system.switch(self)
|
|
self._draw()
|