From f288f441c72f574b5c277637c3a7f65320800e11 Mon Sep 17 00:00:00 2001 From: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com> Date: Tue, 16 May 2023 16:18:49 +0200 Subject: [PATCH] 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 --- README.rst | 4 + apps/pomodoro.py | 308 ++++++++++++++++++++++++++++++++ docs/apps.rst | 2 + res/icons/pomodoro_icon.png | Bin 0 -> 12333 bytes res/screenshots/PomodoroApp.png | Bin 0 -> 6936 bytes 5 files changed, 314 insertions(+) create mode 100644 apps/pomodoro.py create mode 100644 res/icons/pomodoro_icon.png create mode 100644 res/screenshots/PomodoroApp.png diff --git a/README.rst b/README.rst index 809641c..bf916f6 100644 --- a/README.rst +++ b/README.rst @@ -262,6 +262,10 @@ Other apps: (The "blank" white screenshot is a flashlight app) :alt: Morse translator/notepad application running on the wasp-os simulator :width: 179 +.. image:: res/screenshots/PomodoroApp.png + :alt: Customizable pomodoro app with randomized vibration patterns to make sure you notice + :width: 179 + .. image:: res/screenshots/PhoneFinderApp.png :alt: Find your phone by causing it to ring :width: 179 diff --git a/apps/pomodoro.py b/apps/pomodoro.py new file mode 100644 index 0000000..11fef1e --- /dev/null +++ b/apps/pomodoro.py @@ -0,0 +1,308 @@ +# 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() diff --git a/docs/apps.rst b/docs/apps.rst index ac75070..409a4e4 100644 --- a/docs/apps.rst +++ b/docs/apps.rst @@ -65,6 +65,8 @@ Applications .. automodule:: phone_finder +.. automodule:: pomodoro + .. automodule:: sports .. automodule:: stopwatch diff --git a/res/icons/pomodoro_icon.png b/res/icons/pomodoro_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..56cd298bc7782a8a0491eecf9c686e6e8dc8fbdb GIT binary patch literal 12333 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 I|MJa$0FV?B;s5{u literal 0 HcmV?d00001