diff --git a/micropython b/micropython index df61f43..13f086d 160000 --- a/micropython +++ b/micropython @@ -1 +1 @@ -Subproject commit df61f43d562ec33388bdb27b07fb6f0bbdbe8b8b +Subproject commit 13f086deeb8b9195886fbbda0751302a67ff3c15 diff --git a/wasp/apps/clock.py b/wasp/apps/clock.py index f09f2d4..a3134a0 100644 --- a/wasp/apps/clock.py +++ b/wasp/apps/clock.py @@ -35,6 +35,7 @@ class ClockApp(): def __init__(self): self.meter = wasp.widgets.BatteryMeter() + self.notifier = wasp.widgets.Notifier() def foreground(self): """Activate the application.""" @@ -87,4 +88,5 @@ class ClockApp(): 0, 180, width=240) self.meter.update() + self.notifier.update() return True diff --git a/wasp/apps/pager.py b/wasp/apps/pager.py index 18322d2..edb5418 100644 --- a/wasp/apps/pager.py +++ b/wasp/apps/pager.py @@ -26,16 +26,13 @@ class PagerApp(): def foreground(self): """Activate the application.""" - self._page = 0 - self._chunks = wasp.watch.drawable.wrap(self._msg, 240) - self._numpages = (len(self._chunks) - 2) // 9 wasp.system.request_event(wasp.EventMask.SWIPE_UPDOWN) - self._draw() + self._redraw() def background(self): """De-activate the application.""" - del self._chunks - del self._numpages + self._chunks = None + self._numpages = None def swipe(self, event): """Swipe to page up/down.""" @@ -55,8 +52,15 @@ class PagerApp(): self._draw() mute(False) + def _redraw(self): + """Redraw from scratch (jump to the first page)""" + self._page = 0 + self._chunks = wasp.watch.drawable.wrap(self._msg, 240) + self._numpages = (len(self._chunks) - 2) // 9 + self._draw() + def _draw(self): - """Draw the display from scratch.""" + """Draw a page from scratch.""" draw = wasp.watch.drawable draw.fill() @@ -73,6 +77,22 @@ class PagerApp(): scroll.down = page < self._numpages scroll.draw() +class NotificationApp(PagerApp): + NAME = 'Notifications' + + def __init__(self): + super().__init__('') + + def foreground(self): + notes = wasp.system.notifications + + id = next(iter(notes)) + note = notes[id] + del notes[id] + self._msg = '{}\n\n{}'.format(note['title'], note['body']) + + super().foreground() + class CrashApp(): """Crash handler application. diff --git a/wasp/apps/testapp.py b/wasp/apps/testapp.py index a312530..e2e8828 100644 --- a/wasp/apps/testapp.py +++ b/wasp/apps/testapp.py @@ -16,7 +16,7 @@ class TestApp(): ICON = icons.app def __init__(self): - self.tests = ('Button', 'Crash', 'Colours', 'Fill', 'Fill-H', 'Fill-V', 'RLE', 'String', 'Touch', 'Wrap') + self.tests = ('Button', 'Crash', 'Colours', 'Fill', 'Fill-H', 'Fill-V', 'Notifications', 'RLE', 'String', 'Touch', 'Wrap') self.test = self.tests[0] self.scroll = wasp.widgets.ScrollIndicator() @@ -70,6 +70,19 @@ class TestApp(): self._update_colours() elif self.test.startswith('Fill'): self._benchmark_fill() + elif self.test == 'Notifications': + if event[1] < 120: + wasp.system.notify(wasp.watch.rtc.get_uptime_ms(), + { + "src":"Hangouts", + "title":"A Name", + "body":"message contents" + }) + else: + if wasp.system.notifications: + wasp.system.unnotify( + next(iter(wasp.system.notifications.keys()))) + self._update_notifications() elif self.test == 'RLE': self._benchmark_rle() elif self.test == 'String': @@ -166,6 +179,10 @@ class TestApp(): for s in self._sliders: s.draw() self._update_colours() + elif self.test == 'Notifications': + draw.string('+', 24, 100) + draw.string('-', 210, 100) + self._update_notifications() elif self.test == 'RLE': draw.blit(self.ICON, 120-48, 120-32) @@ -181,3 +198,6 @@ class TestApp(): draw.string('RGB565 #{:04x}'.format(rgb), 0, 6, width=240) draw.fill(rgb, 60, 35, 120, 50) + + def _update_notifications(self): + wasp.watch.drawable.string(str(len(wasp.system.notifications)), 0, 140, 240) diff --git a/wasp/boards/pinetime/manifest.py b/wasp/boards/pinetime/manifest.py index 0193fed..3a525e1 100644 --- a/wasp/boards/pinetime/manifest.py +++ b/wasp/boards/pinetime/manifest.py @@ -28,6 +28,7 @@ freeze('../..', 'fonts/sans24.py', 'fonts/sans28.py', 'fonts/sans36.py', + 'gadgetbridge.py', 'icons.py', 'ppg.py', 'shell.py', diff --git a/wasp/gadgetbridge.py b/wasp/gadgetbridge.py new file mode 100644 index 0000000..f1219dd --- /dev/null +++ b/wasp/gadgetbridge.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +# Copyright (C) 2020 Daniel Thompson +"""Gadgetbridge/Bangle.js protocol + +Currently implemented messages are: + + * t:"notify", id:int, src,title,subject,body,sender,tel:string - new + notification + * t:"notify-", id:int - delete notification + * t:"alarm", d:[{h,m},...] - set alarms + * t:"find", n:bool - findDevice + * t:"vibrate", n:int - vibrate + * t:"weather", temp,hum,txt,wind,loc - weather report + * t:"musicstate", state:"play/pause",position,shuffle,repeat - music + play/pause/etc + * t:"musicinfo", artist,album,track,dur,c(track count),n(track num) - + currently playing music track + * t:"call", cmd:"accept/incoming/outgoing/reject/start/end", name: "name", number: "+491234" - call +""" + +import io +import json +import sys +import wasp + +# JSON compatibility +null = None +true = True +false = False + +def _info(msg): + json.dump({'t':'info', 'msg':msg}, sys.stdout) + sys.stdout.write('\r\n') + +def _error(msg): + json.dump({'t':'error', 'msg':msg}, sys.stdout) + sys.stdout.write('\r\n') + +def GB(cmd): + task = cmd['t'] + del cmd['t'] + + try: + if task == 'find': + wasp.watch.vibrator.pin(not cmd['n']) + elif task == 'notify': + id = cmd['id'] + del cmd['id'] + wasp.system.notify(id, cmd) + elif task == 'notify-': + wasp.system.unnotify(cmd['id']) + else: + _info('Command "{}" is not implemented'.format(cmd)) + except Exception as e: + msg = io.StringIO() + sys.print_exception(e, msg) + _error(msg.getvalue()) + msg.close() + + diff --git a/wasp/icons.py b/wasp/icons.py index ff39f8a..2b55d9c 100644 --- a/wasp/icons.py +++ b/wasp/icons.py @@ -150,3 +150,16 @@ knob = ( b'\x05\xe2\x07\xe0\x08\xe0\t\xde\x0b\xdc\r\xda\x10\xd6\x13\xd4' b'\x16\xd0\x1c\xc8\x10' ) + +# 2-bit RLE, generated from res/notification.png, 105 bytes +notification = ( + b'\x02' + b' ' + b'\x0f\xc2\x1d\xc4\x1c\xc4\x19\xca\x14\xce\x11\xd0\x0f\xd2\x0e\xc5' + b'\x08\xc5\r\xc4\x0c\xc4\x0c\xc4\x0c\xc4\x0b\xc4\x0e\xc4\n\xc4' + b'\x0e\xc4\n\xc4\x0e\xc4\n\xc3\x0f\xc4\t\xc4\x10\xc3\t\xc4' + b'\x10\xc4\x08\xc4\x10\xc4\x08\xc4\x10\xc4\x08\xc4\x10\xc4\x08\xc3' + b'\x11\xc4\x07\xc4\x12\xc4\x06\xc4\x12\xc4\x06\xc4\x12\xc4\x05\xc5' + b'\x12\xc4\x05\xc4\x14\xc4\x03\xc5\x14\xc5\x02\xde\x01\xff\x01\x01' + b'\xde\x0f\xc4\x1d\xc2\x0f' +) diff --git a/wasp/main.py b/wasp/main.py index 8680219..c1db792 100644 --- a/wasp/main.py +++ b/wasp/main.py @@ -2,4 +2,5 @@ # Copyright (C) 2020 Daniel Thompson import wasp +from gadgetbridge import * wasp.system.schedule() diff --git a/wasp/wasp.py b/wasp/wasp.py index a9f1a30..5246718 100644 --- a/wasp/wasp.py +++ b/wasp/wasp.py @@ -25,7 +25,7 @@ from apps.clock import ClockApp from apps.flashlight import FlashlightApp from apps.heart import HeartApp from apps.launcher import LauncherApp -from apps.pager import PagerApp, CrashApp +from apps.pager import PagerApp, CrashApp, NotificationApp from apps.settings import SettingsApp from apps.steps import StepCounterApp from apps.stopwatch import StopwatchApp @@ -101,6 +101,8 @@ class Manager(): self.quick_ring = [] self.launcher = LauncherApp() self.launcher_ring = [] + self.notifier = NotificationApp() + self.notifications = {} self.blank_after = 15 @@ -202,13 +204,26 @@ class Manager(): if self.app != app_list[0]: self.switch(app_list[0]) else: - watch.vibrator.pulse() + if len(self.notifications): + self.switch(self.notifier) + else: + # Nothing to notify... we must handle that here + # otherwise the display will flicker. + watch.vibrator.pulse() + elif direction == EventType.HOME or direction == EventType.BACK: if self.app != app_list[0]: self.switch(app_list[0]) else: self.sleep() + def notify(self, id, msg): + self.notifications[id] = msg + + def unnotify(self, id): + if id in self.notifications: + del self.notifications[id] + def request_event(self, event_mask): """Subscribe to events. diff --git a/wasp/widgets.py b/wasp/widgets.py index 5d8cc9a..2fcac9e 100644 --- a/wasp/widgets.py +++ b/wasp/widgets.py @@ -9,10 +9,11 @@ shared between applications. """ import icons +import wasp import watch from micropython import const -class BatteryMeter(object): +class BatteryMeter: """Battery meter widget. A simple battery meter with a charging indicator, will draw at the @@ -69,7 +70,33 @@ class BatteryMeter(object): self.level = level -class ScrollIndicator(): +class Notifier: + """Show if there are pending notifications.""" + def __init__(self, x=8, y=8): + self._pos = (x, y) + + def draw(self): + """Update the notification widget. + + For this simple widget :py:meth:`~.draw` is simply a synonym for + :py:meth:`~.update`. + """ + self.update() + + def update(self): + """Update the widget. + + For this simple widget :py:meth:~.update` does nothing! + """ + draw = watch.drawable + (x, y) = self._pos + + if wasp.system.notifications: + draw.blit(icons.notification, x, y, fg=0x7bef) + else: + draw.fill(0, x, y, 32, 32) + +class ScrollIndicator: """Scrolling indicator. A simple battery meter with a charging indicator, will draw at the