1
0
Fork 0

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).
This commit is contained in:
Daniel Thompson 2020-04-11 20:14:30 +01:00
parent 8cf9369efa
commit f68eb610c5
9 changed files with 209 additions and 6 deletions

@ -1 +1 @@
Subproject commit 2e5cb3eb32bcd4d72a328697db5442a9950969c0 Subproject commit 7f8eda310df53a086ea55281bc9361ef386ec01a

BIN
res/bomb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

119
wasp/apps/pager.py Normal file
View file

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

View file

@ -12,7 +12,7 @@ class TestApp():
ICON = icons.app ICON = icons.app
def __init__(self): def __init__(self):
self.tests = ('Touch', 'String', 'Button', 'Crash', 'RLE') self.tests = ('Touch', 'String', 'Wrap', 'Button', 'Crash', 'RLE')
self.test = self.tests[0] self.test = self.tests[0]
self.scroll = wasp.widgets.ScrollIndicator() self.scroll = wasp.widgets.ScrollIndicator()
@ -56,6 +56,8 @@ class TestApp():
event[1], event[2]), 0, 108, width=240) event[1], event[2]), 0, 108, width=240)
elif self.test == 'String': elif self.test == 'String':
self.benchmark_string() self.benchmark_string()
elif self.test == 'Wrap':
self.benchmark_wrap()
elif self.test == 'RLE': elif self.test == 'RLE':
self.benchmark_rle() self.benchmark_rle()
@ -88,6 +90,24 @@ class TestApp():
del t del t
draw.string('{}s'.format(elapsed / 1000000), 12, 24+192) 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): def draw(self):
"""Redraw the display from scratch.""" """Redraw the display from scratch."""
wasp.watch.display.mute(True) wasp.watch.display.mute(True)

View file

@ -7,6 +7,7 @@ freeze('../..',
'apps/clock.py', 'apps/clock.py',
'apps/flashlight.py', 'apps/flashlight.py',
'apps/launcher.py', 'apps/launcher.py',
'apps/pager.py',
'apps/settings.py', 'apps/settings.py',
'apps/testapp.py', 'apps/testapp.py',
'boot.py', 'boot.py',

View file

@ -6,6 +6,12 @@ def sleep_ms(ms):
time.sleep(ms / 1000) time.sleep(ms / 1000)
time.sleep_ms = sleep_ms 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 import draw565
from machine import I2C from machine import I2C

View file

@ -235,3 +235,32 @@ class Draw565(object):
if width: if width:
display.fill(0, x, y, rightpad, h) 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

View file

@ -4,6 +4,19 @@
# 1-bit RLE, generated from res/battery.png, 189 bytes # 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') 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 # 2-bit RLE, generated from res/app_icon.png, 460 bytes
app = ( app = (
b'\x02' b'\x02'
@ -98,6 +111,7 @@ settings = (
b'C,t-r/p2l?X\x80m\xa6;\xa4' b'C,t-r/p2l?X\x80m\xa6;\xa4'
b'<\xa4<\xa4\x1e' b'<\xa4<\xa4\x1e'
) )
# 2-bit RLE, generated from res/torch_icon.png, 247 bytes # 2-bit RLE, generated from res/torch_icon.png, 247 bytes
torch = ( torch = (
b'\x02' 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 # 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') down_arrow = (16, 9, b'\x00 \x01\x0e\x03\x0c\x05\n\x07\x08\t\x06\x0b\x04\r\x02\x07')

View file

@ -16,6 +16,7 @@ import widgets
from apps.clock import ClockApp from apps.clock import ClockApp
from apps.flashlight import FlashlightApp from apps.flashlight import FlashlightApp
from apps.launcher import LauncherApp from apps.launcher import LauncherApp
from apps.pager import CrashApp
from apps.settings import SettingsApp from apps.settings import SettingsApp
from apps.testapp import TestApp from apps.testapp import TestApp
@ -32,6 +33,7 @@ class EventType():
TOUCH = 5 TOUCH = 5
HOME = 256 HOME = 256
BACK = 257
class EventMask(): class EventMask():
"""Enumerated event masks. """Enumerated event masks.
@ -179,7 +181,7 @@ class Manager():
self.switch(app_list[0]) self.switch(app_list[0])
else: else:
watch.vibrator.pulse() watch.vibrator.pulse()
elif direction == EventType.HOME: 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:
@ -298,7 +300,7 @@ class Manager():
if 1 == self._button.get_event() or self.charging != charging: if 1 == self._button.get_event() or self.charging != charging:
self.wake() self.wake()
def run(self): def run(self, no_except=True):
"""Run the system manager synchronously. """Run the system manager synchronously.
This allows all watch management activities to handle in the This allows all watch management activities to handle in the
@ -312,8 +314,21 @@ class Manager():
# been set running again. # been set running again.
print('Watch is running, use Ctrl-C to stop') 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: while True:
self._tick() self._tick()
machine.deepsleep()
while True:
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 # Currently there is no code to control how fast the system
# ticks. In other words this code will break if we improve the # ticks. In other words this code will break if we improve the
# power management... we are currently relying on no being able # power management... we are currently relying on no being able