1
0
Fork 0
wasp-os/apps/pomodoro.py
thiswillbeyourgithub f288f441c7
new: add pomodoro app (third trial) (#418)
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>
2023-05-16 15:18:49 +01:00

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()