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:
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
BIN
res/bomb.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 305 B |
119
wasp/apps/pager.py
Normal file
119
wasp/apps/pager.py
Normal 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))
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
21
wasp/wasp.py
21
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
|
||||
|
|
Loading…
Reference in a new issue