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:
parent
6686f17e72
commit
a01fb7df57
10 changed files with 172 additions and 13 deletions
|
@ -1 +1 @@
|
||||||
Subproject commit df61f43d562ec33388bdb27b07fb6f0bbdbe8b8b
|
Subproject commit 13f086deeb8b9195886fbbda0751302a67ff3c15
|
|
@ -35,6 +35,7 @@ class ClockApp():
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.meter = wasp.widgets.BatteryMeter()
|
self.meter = wasp.widgets.BatteryMeter()
|
||||||
|
self.notifier = wasp.widgets.Notifier()
|
||||||
|
|
||||||
def foreground(self):
|
def foreground(self):
|
||||||
"""Activate the application."""
|
"""Activate the application."""
|
||||||
|
@ -87,4 +88,5 @@ class ClockApp():
|
||||||
0, 180, width=240)
|
0, 180, width=240)
|
||||||
|
|
||||||
self.meter.update()
|
self.meter.update()
|
||||||
|
self.notifier.update()
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -26,16 +26,13 @@ class PagerApp():
|
||||||
|
|
||||||
def foreground(self):
|
def foreground(self):
|
||||||
"""Activate the application."""
|
"""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)
|
wasp.system.request_event(wasp.EventMask.SWIPE_UPDOWN)
|
||||||
self._draw()
|
self._redraw()
|
||||||
|
|
||||||
def background(self):
|
def background(self):
|
||||||
"""De-activate the application."""
|
"""De-activate the application."""
|
||||||
del self._chunks
|
self._chunks = None
|
||||||
del self._numpages
|
self._numpages = None
|
||||||
|
|
||||||
def swipe(self, event):
|
def swipe(self, event):
|
||||||
"""Swipe to page up/down."""
|
"""Swipe to page up/down."""
|
||||||
|
@ -55,8 +52,15 @@ class PagerApp():
|
||||||
self._draw()
|
self._draw()
|
||||||
mute(False)
|
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):
|
def _draw(self):
|
||||||
"""Draw the display from scratch."""
|
"""Draw a page from scratch."""
|
||||||
draw = wasp.watch.drawable
|
draw = wasp.watch.drawable
|
||||||
draw.fill()
|
draw.fill()
|
||||||
|
|
||||||
|
@ -73,6 +77,22 @@ class PagerApp():
|
||||||
scroll.down = page < self._numpages
|
scroll.down = page < self._numpages
|
||||||
scroll.draw()
|
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():
|
class CrashApp():
|
||||||
"""Crash handler application.
|
"""Crash handler application.
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ class TestApp():
|
||||||
ICON = icons.app
|
ICON = icons.app
|
||||||
|
|
||||||
def __init__(self):
|
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.test = self.tests[0]
|
||||||
self.scroll = wasp.widgets.ScrollIndicator()
|
self.scroll = wasp.widgets.ScrollIndicator()
|
||||||
|
|
||||||
|
@ -70,6 +70,19 @@ class TestApp():
|
||||||
self._update_colours()
|
self._update_colours()
|
||||||
elif self.test.startswith('Fill'):
|
elif self.test.startswith('Fill'):
|
||||||
self._benchmark_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':
|
elif self.test == 'RLE':
|
||||||
self._benchmark_rle()
|
self._benchmark_rle()
|
||||||
elif self.test == 'String':
|
elif self.test == 'String':
|
||||||
|
@ -166,6 +179,10 @@ class TestApp():
|
||||||
for s in self._sliders:
|
for s in self._sliders:
|
||||||
s.draw()
|
s.draw()
|
||||||
self._update_colours()
|
self._update_colours()
|
||||||
|
elif self.test == 'Notifications':
|
||||||
|
draw.string('+', 24, 100)
|
||||||
|
draw.string('-', 210, 100)
|
||||||
|
self._update_notifications()
|
||||||
elif self.test == 'RLE':
|
elif self.test == 'RLE':
|
||||||
draw.blit(self.ICON, 120-48, 120-32)
|
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.string('RGB565 #{:04x}'.format(rgb), 0, 6, width=240)
|
||||||
draw.fill(rgb, 60, 35, 120, 50)
|
draw.fill(rgb, 60, 35, 120, 50)
|
||||||
|
|
||||||
|
def _update_notifications(self):
|
||||||
|
wasp.watch.drawable.string(str(len(wasp.system.notifications)), 0, 140, 240)
|
||||||
|
|
|
@ -28,6 +28,7 @@ freeze('../..',
|
||||||
'fonts/sans24.py',
|
'fonts/sans24.py',
|
||||||
'fonts/sans28.py',
|
'fonts/sans28.py',
|
||||||
'fonts/sans36.py',
|
'fonts/sans36.py',
|
||||||
|
'gadgetbridge.py',
|
||||||
'icons.py',
|
'icons.py',
|
||||||
'ppg.py',
|
'ppg.py',
|
||||||
'shell.py',
|
'shell.py',
|
||||||
|
|
60
wasp/gadgetbridge.py
Normal file
60
wasp/gadgetbridge.py
Normal 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()
|
||||||
|
|
||||||
|
|
|
@ -150,3 +150,16 @@ knob = (
|
||||||
b'\x05\xe2\x07\xe0\x08\xe0\t\xde\x0b\xdc\r\xda\x10\xd6\x13\xd4'
|
b'\x05\xe2\x07\xe0\x08\xe0\t\xde\x0b\xdc\r\xda\x10\xd6\x13\xd4'
|
||||||
b'\x16\xd0\x1c\xc8\x10'
|
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'
|
||||||
|
)
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
# Copyright (C) 2020 Daniel Thompson
|
# Copyright (C) 2020 Daniel Thompson
|
||||||
|
|
||||||
import wasp
|
import wasp
|
||||||
|
from gadgetbridge import *
|
||||||
wasp.system.schedule()
|
wasp.system.schedule()
|
||||||
|
|
19
wasp/wasp.py
19
wasp/wasp.py
|
@ -25,7 +25,7 @@ from apps.clock import ClockApp
|
||||||
from apps.flashlight import FlashlightApp
|
from apps.flashlight import FlashlightApp
|
||||||
from apps.heart import HeartApp
|
from apps.heart import HeartApp
|
||||||
from apps.launcher import LauncherApp
|
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.settings import SettingsApp
|
||||||
from apps.steps import StepCounterApp
|
from apps.steps import StepCounterApp
|
||||||
from apps.stopwatch import StopwatchApp
|
from apps.stopwatch import StopwatchApp
|
||||||
|
@ -101,6 +101,8 @@ class Manager():
|
||||||
self.quick_ring = []
|
self.quick_ring = []
|
||||||
self.launcher = LauncherApp()
|
self.launcher = LauncherApp()
|
||||||
self.launcher_ring = []
|
self.launcher_ring = []
|
||||||
|
self.notifier = NotificationApp()
|
||||||
|
self.notifications = {}
|
||||||
|
|
||||||
self.blank_after = 15
|
self.blank_after = 15
|
||||||
|
|
||||||
|
@ -202,13 +204,26 @@ class Manager():
|
||||||
if self.app != app_list[0]:
|
if self.app != app_list[0]:
|
||||||
self.switch(app_list[0])
|
self.switch(app_list[0])
|
||||||
else:
|
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:
|
elif direction == EventType.HOME or direction == EventType.BACK:
|
||||||
if self.app != app_list[0]:
|
if self.app != app_list[0]:
|
||||||
self.switch(app_list[0])
|
self.switch(app_list[0])
|
||||||
else:
|
else:
|
||||||
self.sleep()
|
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):
|
def request_event(self, event_mask):
|
||||||
"""Subscribe to events.
|
"""Subscribe to events.
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,11 @@ shared between applications.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import icons
|
import icons
|
||||||
|
import wasp
|
||||||
import watch
|
import watch
|
||||||
from micropython import const
|
from micropython import const
|
||||||
|
|
||||||
class BatteryMeter(object):
|
class BatteryMeter:
|
||||||
"""Battery meter widget.
|
"""Battery meter widget.
|
||||||
|
|
||||||
A simple battery meter with a charging indicator, will draw at the
|
A simple battery meter with a charging indicator, will draw at the
|
||||||
|
@ -69,7 +70,33 @@ class BatteryMeter(object):
|
||||||
|
|
||||||
self.level = level
|
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.
|
"""Scrolling indicator.
|
||||||
|
|
||||||
A simple battery meter with a charging indicator, will draw at the
|
A simple battery meter with a charging indicator, will draw at the
|
||||||
|
|
Loading…
Reference in a new issue