summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Thompson <daniel@redfelineninja.org.uk>2020-04-11 19:14:30 (GMT)
committerDaniel Thompson <daniel@redfelineninja.org.uk>2020-04-11 19:15:02 (GMT)
commitf68eb610c5d77bb71d3952e0dc9ca70a472ebfae (patch)
treeca0b7a1174dc7e7013ce5e67fc18231d1d6ea0c3
parent8cf9369efa9083cccee36e7596d52e12ace362d3 (diff)
wasp: On-device crash reporting
If an application crashes let's report it on the device so it can be distinguished from a hang (if nothing else it should mean we get better bug reports).
m---------micropython0
-rw-r--r--res/bomb.pngbin0 -> 305 bytes
-rw-r--r--wasp/apps/pager.py119
-rw-r--r--wasp/apps/testapp.py22
-rw-r--r--wasp/boards/pinetime/manifest.py1
-rw-r--r--wasp/boards/simulator/watch.py6
-rw-r--r--wasp/draw565.py29
-rw-r--r--wasp/icons.py15
-rw-r--r--wasp/wasp.py21
9 files changed, 208 insertions, 5 deletions
diff --git a/micropython b/micropython
-Subproject 2e5cb3eb32bcd4d72a328697db5442a9950969c
+Subproject 7f8eda310df53a086ea55281bc9361ef386ec01
diff --git a/res/bomb.png b/res/bomb.png
new file mode 100644
index 0000000..b9979d2
--- /dev/null
+++ b/res/bomb.png
Binary files differ
diff --git a/wasp/apps/pager.py b/wasp/apps/pager.py
new file mode 100644
index 0000000..7da8f5a
--- /dev/null
+++ b/wasp/apps/pager.py
@@ -0,0 +1,119 @@
+# SPDX-License-Identifier: LGPL-3.0-or-later
+# Copyright (C) 2020 Daniel Thompson
+
+import wasp
+import icons
+
+import io
+import sys
+
+
+class PagerApp():
+ """Show long text in a pager.
+
+ This is used to present text based information to the user. It is primarily
+ intended for notifications but is also used to provide debugging
+ information when applications crash.
+ """
+ NAME = 'Pager'
+ ICON = icons.app
+
+ def __init__(self, msg):
+ self._msg = msg
+ self._scroll = wasp.widgets.ScrollIndicator()
+
+ 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()
+
+ def background(self):
+ del self._chunks
+ del self._numpages
+
+ def swipe(self, event):
+ mute = wasp.watch.display.mute
+
+ if event[0] == wasp.EventType.UP:
+ if self._page >= self._numpages:
+ wasp.system.navigate(wasp.EventType.BACK)
+ return
+ self._page += 1
+ else:
+ if self._page <= 0:
+ wasp.watch.vibrator.pulse()
+ return
+ self._page -= 1
+ mute(True)
+ self._draw()
+ mute(False)
+
+ def _draw(self):
+ """Draw the display from scratch."""
+ draw = wasp.watch.drawable
+ draw.fill()
+
+ page = self._page
+ i = page * 9
+ j = i + 11
+ chunks = self._chunks[i:j]
+ for i in range(len(chunks)-1):
+ sub = self._msg[chunks[i]:chunks[i+1]].rstrip()
+ draw.string(sub, 0, 24*i)
+
+ scroll = self._scroll
+ scroll.up = page > 0
+ scroll.down = page < self._numpages
+ scroll.draw()
+
+class CrashApp():
+ """Crash handler application.
+
+ This application is launched automatically whenever another
+ application crashes. Our main job it to indicate as loudly as
+ possible that the system is no longer running correctly. This
+ app deliberately enables inverted video mode in order to deliver
+ that message as strongly as possible.
+ """
+ def __init__(self, exc):
+ """Capture the exception information.
+
+ This app does not actually display the exception information
+ but we need to capture the exception info before we leave
+ the except block.
+ """
+ msg = io.StringIO()
+ sys.print_exception(exc, msg)
+ self._msg = msg.getvalue()
+ msg.close()
+
+ def foreground(self):
+ """Indicate the system has crashed by drawing a couple of bomb icons.
+
+ If you owned an Atari ST back in the mid-eighties then I hope you
+ recognise this as a tribute a long forgotten home computer!
+ """
+ wasp.watch.display.invert(False)
+ draw = wasp.watch.drawable
+ draw.blit(icons.bomb, 0, 104)
+ draw.blit(icons.bomb, 32, 104)
+
+ wasp.system.request_event(wasp.EventMask.SWIPE_UPDOWN |
+ wasp.EventMask.SWIPE_LEFTRIGHT)
+
+ def background(self):
+ """Restore a normal display mode.
+
+ Conceal the display before the transition otherwise the inverted
+ bombs get noticed by the user.
+ """
+ wasp.watch.display.mute(True)
+ wasp.watch.display.invert(True)
+
+ def swipe(self, event):
+ """Show the exception message in a pager.
+ """
+ wasp.system.switch(PagerApp(self._msg))
diff --git a/wasp/apps/testapp.py b/wasp/apps/testapp.py
index 789454f..0df4ee7 100644
--- a/wasp/apps/testapp.py
+++ b/wasp/apps/testapp.py
@@ -12,7 +12,7 @@ class TestApp():
ICON = icons.app
def __init__(self):
- self.tests = ('Touch', 'String', 'Button', 'Crash', 'RLE')
+ self.tests = ('Touch', 'String', 'Wrap', 'Button', 'Crash', 'RLE')
self.test = self.tests[0]
self.scroll = wasp.widgets.ScrollIndicator()
@@ -56,6 +56,8 @@ class TestApp():
event[1], event[2]), 0, 108, width=240)
elif self.test == 'String':
self.benchmark_string()
+ elif self.test == 'Wrap':
+ self.benchmark_wrap()
elif self.test == 'RLE':
self.benchmark_rle()
@@ -88,6 +90,24 @@ class TestApp():
del t
draw.string('{}s'.format(elapsed / 1000000), 12, 24+192)
+ def benchmark_wrap(self):
+ draw = wasp.watch.drawable
+ draw.fill(0, 0, 30, 240, 240-30)
+ self.scroll.draw()
+ t = machine.Timer(id=1, period=8000000)
+ t.start()
+ draw = wasp.watch.drawable
+ s = 'This\nis a very long string that will need to be wrappedinmultipledifferentways!'
+ chunks = draw.wrap(s, 240)
+
+ for i in range(len(chunks)-1):
+ sub = s[chunks[i]:chunks[i+1]].rstrip()
+ draw.string(sub, 0, 48+24*i)
+ elapsed = t.time()
+ t.stop()
+ del t
+ draw.string('{}s'.format(elapsed / 1000000), 12, 24+192)
+
def draw(self):
"""Redraw the display from scratch."""
wasp.watch.display.mute(True)
diff --git a/wasp/boards/pinetime/manifest.py b/wasp/boards/pinetime/manifest.py
index 3d8cd9a..d22f360 100644
--- a/wasp/boards/pinetime/manifest.py
+++ b/wasp/boards/pinetime/manifest.py
@@ -7,6 +7,7 @@ freeze('../..',
'apps/clock.py',
'apps/flashlight.py',
'apps/launcher.py',
+ 'apps/pager.py',
'apps/settings.py',
'apps/testapp.py',
'boot.py',
diff --git a/wasp/boards/simulator/watch.py b/wasp/boards/simulator/watch.py
index ce281f4..178b1eb 100644
--- a/wasp/boards/simulator/watch.py
+++ b/wasp/boards/simulator/watch.py
@@ -6,6 +6,12 @@ def sleep_ms(ms):
time.sleep(ms / 1000)
time.sleep_ms = sleep_ms
+import sys, traceback
+def print_exception(exc, file=sys.stdout):
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ traceback.print_exception(exc_type, exc_value, exc_traceback, file=file)
+sys.print_exception = print_exception
+
import draw565
from machine import I2C
diff --git a/wasp/draw565.py b/wasp/draw565.py
index 93032ac..1b1ce11 100644
--- a/wasp/draw565.py
+++ b/wasp/draw565.py
@@ -235,3 +235,32 @@ class Draw565(object):
if width:
display.fill(0, x, y, rightpad, h)
+
+ def wrap(self, s, width):
+ font = self._font
+ max = len(s)
+ chunks = [ 0, ]
+ end = 0
+
+ while end < max:
+ start = end
+ l = 0
+
+ for i in range(start, max+1):
+ if i >= len(s):
+ break
+ ch = s[i]
+ if ch == '\n':
+ end = i+1
+ break
+ if ch == ' ':
+ end = i+1
+ (_, h, w) = font.get_ch(ch)
+ l += w + 1
+ if l > width:
+ break
+ if end <= start:
+ end = i
+ chunks.append(end)
+
+ return chunks
diff --git a/wasp/icons.py b/wasp/icons.py
index 02c1b91..9bfa92a 100644
--- a/wasp/icons.py
+++ b/wasp/icons.py
@@ -4,6 +4,19 @@
# 1-bit RLE, generated from res/battery.png, 189 bytes
battery = (36, 48, b'\x97\x0e\x14\x12\x11\x14\x10\x14\x0c\x08\x0c\x08\x08\x08\x0c\x08\x08\x08\x0c\x08\x08\x08\x0c\x08\x08\x04\x14\x04\x08\x04\x14\x04\x08\x04\x0c\x04\x04\x04\x08\x04\x0b\x05\x04\x04\x08\x04\n\x06\x04\x04\x08\x04\t\x07\x04\x04\x08\x04\x08\x07\x05\x04\x08\x04\x07\x07\x06\x04\x08\x04\x06\x07\x07\x04\x08\x04\x05\x07\x08\x04\x08\x04\x04\x0e\x02\x04\x08\x04\x03\x0f\x02\x04\x08\x04\x02\x10\x02\x04\x08\x04\x02\x10\x02\x04\x08\x04\x02\x0f\x03\x04\x08\x04\x02\x0e\x04\x04\x08\x04\x08\x07\x05\x04\x08\x04\x07\x07\x06\x04\x08\x04\x06\x07\x07\x04\x08\x04\x05\x07\x08\x04\x08\x04\x04\x07\t\x04\x08\x04\x04\x06\n\x04\x08\x04\x04\x05\x0b\x04\x08\x04\x04\x04\x0c\x04\x08\x04\x14\x04\x08\x04\x14\x04\x08\x04\x14\x04\x08\x04\x14\x04\x08\x1c\x08\x1c\x08\x1c\x08\x1c\x98')
+# 2-bit RLE, generated from res/bomb.png, 100 bytes
+bomb = (
+ b'\x02'
+ b' '
+ b'\x15\xc2\x06\xc22\xc3\x03\xc2\x02\xc2\x13\xc1\x03\xc1\x1a\xc1'
+ b'\x05\xc5\x15\xc1\x1c\xc7\x04\xc2\x02\xc2\x0f\xc7\x19\xc7\x02\xc2'
+ b'\x06\xc2\r\xc7\x17\xcb\x13\xcf\x10\xc6\x02\xc9\x0e\xd3\r\xd3'
+ b'\x0c\xc5\x02\xce\x0b\xc4\x02\xcf\x0b\xd5\n\xc4\x01\xd2\t\xc3'
+ b'\x02\xd2\t\xc3\x01\xd3\t\xc3\x02\xd2\t\xc4\x01\xd2\n\xd5'
+ b'\x0b\xd5\x0b\xd5\x0c\xd3\r\xd3\x0e\xd1\x10\xcf\x13\xcb\x18\xc5'
+ b'\x0e'
+)
+
# 2-bit RLE, generated from res/app_icon.png, 460 bytes
app = (
b'\x02'
@@ -98,6 +111,7 @@ settings = (
b'C,t-r/p2l?X\x80m\xa6;\xa4'
b'<\xa4<\xa4\x1e'
)
+
# 2-bit RLE, generated from res/torch_icon.png, 247 bytes
torch = (
b'\x02'
@@ -125,4 +139,3 @@ up_arrow = (16, 9, b'\x07\x02\r\x04\x0b\x06\t\x08\x07\n\x05\x0c\x03\x0e\x01 ')
# 1-bit RLE, generated from res/down_arrow.png, 17 bytes
down_arrow = (16, 9, b'\x00 \x01\x0e\x03\x0c\x05\n\x07\x08\t\x06\x0b\x04\r\x02\x07')
-
diff --git a/wasp/wasp.py b/wasp/wasp.py
index d336729..e5315a1 100644
--- a/wasp/wasp.py
+++ b/wasp/wasp.py
@@ -16,6 +16,7 @@ import widgets
from apps.clock import ClockApp
from apps.flashlight import FlashlightApp
from apps.launcher import LauncherApp
+from apps.pager import CrashApp
from apps.settings import SettingsApp
from apps.testapp import TestApp
@@ -32,6 +33,7 @@ class EventType():
TOUCH = 5
HOME = 256
+ BACK = 257
class EventMask():
"""Enumerated event masks.
@@ -179,7 +181,7 @@ class Manager():
self.switch(app_list[0])
else:
watch.vibrator.pulse()
- elif direction == EventType.HOME:
+ elif direction == EventType.HOME or direction == EventType.BACK:
if self.app != app_list[0]:
self.switch(app_list[0])
else:
@@ -298,7 +300,7 @@ class Manager():
if 1 == self._button.get_event() or self.charging != charging:
self.wake()
- def run(self):
+ def run(self, no_except=True):
"""Run the system manager synchronously.
This allows all watch management activities to handle in the
@@ -312,8 +314,21 @@ class Manager():
# been set running again.
print('Watch is running, use Ctrl-C to stop')
+ if not no_except:
+ # This is a simplified (uncommented) version of the loop
+ # below
+ while True:
+ self._tick()
+ machine.deepsleep()
+
while True:
- self._tick()
+ try:
+ self._tick()
+ except KeyboardInterrupt:
+ raise
+ except Exception as e:
+ self.switch(CrashApp(e))
+
# Currently there is no code to control how fast the system
# ticks. In other words this code will break if we improve the
# power management... we are currently relying on no being able