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
|
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)
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
21
wasp/wasp.py
21
wasp/wasp.py
|
@ -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:
|
||||||
|
self._tick()
|
||||||
|
machine.deepsleep()
|
||||||
|
|
||||||
while True:
|
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
|
# 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
|
||||||
|
|
Loading…
Reference in a new issue