diff --git a/micropython b/micropython index 2e5cb3e..7f8eda3 160000 --- a/micropython +++ b/micropython @@ -1 +1 @@ -Subproject commit 2e5cb3eb32bcd4d72a328697db5442a9950969c0 +Subproject commit 7f8eda310df53a086ea55281bc9361ef386ec01a diff --git a/res/bomb.png b/res/bomb.png new file mode 100644 index 0000000..b9979d2 Binary files /dev/null and b/res/bomb.png 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