1
0
Fork 0

Introduction basic notification support

This requires a modified version of Gadgetbridge and currently works by
implementing the BangleJS protocol.

In Gadgetbridge ensure "Sync time" is *not* set and choose "Don't pair"
when adding the PineTime device.
This commit is contained in:
Daniel Thompson 2020-07-19 20:50:33 +01:00
parent 6686f17e72
commit a01fb7df57
10 changed files with 172 additions and 13 deletions

@ -1 +1 @@
Subproject commit df61f43d562ec33388bdb27b07fb6f0bbdbe8b8b
Subproject commit 13f086deeb8b9195886fbbda0751302a67ff3c15

View file

@ -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

View file

@ -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.

View file

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

View file

@ -28,6 +28,7 @@ freeze('../..',
'fonts/sans24.py',
'fonts/sans28.py',
'fonts/sans36.py',
'gadgetbridge.py',
'icons.py',
'ppg.py',
'shell.py',

60
wasp/gadgetbridge.py Normal file
View file

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

View file

@ -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'
)

View file

@ -2,4 +2,5 @@
# Copyright (C) 2020 Daniel Thompson
import wasp
from gadgetbridge import *
wasp.system.schedule()

View file

@ -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.

View file

@ -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