diff --git a/README.rst b/README.rst index 347d152..a6b9f98 100644 --- a/README.rst +++ b/README.rst @@ -258,6 +258,10 @@ application (and the "blank" white screen is a torch application): :alt: Shows a time as words in the wasp-os simulator :width: 179 +.. image:: res/LevelApp.png + :alt: Shows a time as words in the wasp-os simulator + :width: 179 + .. image:: res/BeaconApp.png :alt: Flash the relatively powerful HRS LED repeatedly - :width: 179 + :width: 179 \ No newline at end of file diff --git a/apps/Level.py b/apps/Level.py new file mode 100644 index 0000000..302d21d --- /dev/null +++ b/apps/Level.py @@ -0,0 +1,121 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +# Copyright (C) 2021 Francesco Gazzetta +"""Level application +~~~~~~~~~~~~~~~~~~~~ + +This app shows a dot that moves depending on the orientation of the watch. +A tap opens a menu with the option to calibrate or reset the level. +To calibrate, place the watch on a flat surface, then tap the "Calibrate" +button while ensuring the watch is stationary. + +.. figure:: res/LevelApp.png + :width: 179 +""" + +import wasp +import watch +import widgets +from micropython import const + +_X_MAX = const(240) +_Y_MAX = const(240) +_X_CENTER = const(120) +_Y_CENTER = const(120) + +class LevelApp(): + NAME = "Level" + # 2-bit RLE, 96x64, generated from res/level_icon.png, 410 bytes + ICON = ( + b'\x02' + b'`@' + b'?\xff\xff\xff\xff\xff\xd0@\xa8L\x80r\x82\xc0\xfd\xf0' + b'\x82L\x13M\x82@\xb0O\xc1\x05\xc1\x04\xc1\x05\xc1O' + b'\x82\x80\xa8\x8d\x11\x8e\xc0r\xc2O@\xfdA\x05A\x04' + b'A\x05A\x80\xb0\x8f\xc2\xc0\xa8\xce\x10\xce@rB\x8f' + b'\x80\xfd\x81\x05\x81\x04\x81\x05\x81\xc0\xb0\xcfB@\xa8N' + b'\x10N\x80r\x82\xcf\xc0\xfd\xc1\x05\xc1\x04\xc1\x05\xc1@' + b'\xb0O\x82\x80\xa8\x8e\x10\x8e\xc0r\xc2O@\xfdA\x05' + b'A\x04A\x05A\x80\xb0\x8f\xc2\xc0\xa8\xce\x10\xce@r' + b'B\x8f\x80\xfd\x81\xc0\xb0\xc1\x04\x81\x04\x81\x04\xc1\x81\xcf' + b'B@\xa8N\x10N\x80r\x82\xcf\xc0\xfd\xc1@\xb0C' + b'\x02\xc1\x04\xc1\x02C\xc1O\x82\x80\xa8\x8e\x10\x8e\xc0r' + b'\xc2O@\xfdA\x80\xb0\x85A\x84A\x85A\x8f\xc2\xc0' + b'\xa8\xce\x10\xce@rB\x8f\x80\xfd\x81\xc0\xb0\xc5\x81\xc4' + b'\x81\xc5\x81\xcfB@\xa8N\x10N\x80r\x82\xcf\xc0\xfd' + b'\xc1@\xb0E\xc1D\xc1E\xc1O\x82\x80\xa8\x8e\x10\x8e' + b'\xc0r\xc2O@\xfdA\x80\xb0\x85A\x84A\x85A\x8f' + b'\xc2\xc0\xa8\xce\x10\xce@rC\x8e\x80\xfd\x81\xc0\xb0\xc5' + b'\x81\xc4\x81\xc5\x81\xceC@\xa8N\x10O\x80r\x82\xce' + b'\xc0\xfd\xc1@\xb0E\xc1D\xc1E\xc1N\x82\x80\xa8\x8f' + b'\x10\x8f\xc0r\xc3M@\xfdA\x80\xb0\x85A\x84A\x85' + b'A\x8d\xc3\xc0\xa8\xcf\x10\xd0@rD\x8b\x80\xfd\x81\xc0' + b'\xb0\xc5\x81\xc4\x81\xc5\x81\xcbD@\xa8P\x10Q\x80r' + b'\xaeQ\x10S\xaaS\x10\x7f\x11\x10\x7f\x11\x10\x7f\x11\x10' + b'\x7f\x11\x10\x7f\x11\x10\x7f\x11\x10\x7f\x11\x10\x7f\x11\x10\x7f' + b'\x11\x10\x7f\x11\x10\x7f\x11\x10\x7f\x11\x11\x7f\x0f\x13\x7f\r' + b'?\xff\xff\xff\xff\xff\xd0' + ) + + def __init__(self): + self.old_xy = (0,0) + self.calibration = (0,0) + self.prompt = False + self.calibrate = widgets.Button(20, 20, 200, 60, 'Calibrate') + self.reset = widgets.Button(20, 90, 200, 60, 'Reset') + self.cancel = widgets.Button(20, 160, 200, 60, 'Cancel') + + def foreground(self): + self.prompt = False # in case the watch went to sleep with prompt on + self._draw() + wasp.system.request_event(wasp.EventMask.TOUCH) + wasp.system.request_tick(125) + + def _draw(self): + wasp.watch.drawable.fill() + self._update() + + def _update(self): + if not self.prompt: + draw = wasp.watch.drawable + # Clear the old bubble + draw.fill(None, self.old_xy[0] - 3 + _X_CENTER, self.old_xy[1] - 3 + _Y_CENTER, 6, 6) + # draw guide lines + draw.line(0, _Y_CENTER, _X_MAX, _Y_CENTER, color = wasp.system.theme('mid')) + draw.line(_X_CENTER, 0, _X_CENTER, _Y_MAX, color = wasp.system.theme('mid')) + (new_x, new_y, _) = watch.accel.accel_xyz() + # We clamp and scale the values down a bit to make them fit better, + # and apply the calibration. + # The scaling factor is negative because when gravity pulls in one + # direction we want the bubble to go the other direction. + new_x = min(_X_CENTER, max(-_X_CENTER, (new_x-self.calibration[0])//-3)) + new_y = min(_Y_CENTER, max(-_Y_CENTER, (new_y-self.calibration[1])//-3)) + # Draw the new bubble + draw.fill(wasp.system.theme('bright'), new_x - 3 + _X_CENTER, new_y - 3 + _Y_CENTER, 6, 6) + self.old_xy = (new_x, new_y) + + def tick(self, ticks): + self._update() + wasp.system.keep_awake() + + def touch(self, event): + if self.prompt: + # Handle buttons + if self.calibrate.touch(event): + (x, y, _) = watch.accel.accel_xyz() + self.calibration = (x, y) + if self.reset.touch(event): + self.calibration = (0,0) + #if self.cancel.touch(event): + # pass + + # reset the color (buttons set it to blue) and disable prompt + wasp.watch.drawable.set_color(wasp.system.theme('bright')) + self.prompt = False + self._draw() + else: + # Draw menu + self.prompt = True + wasp.watch.drawable.fill() + self.calibrate.draw() + self.reset.draw() + self.cancel.draw() diff --git a/docs/apps.rst b/docs/apps.rst index 75140bc..e6c0a63 100644 --- a/docs/apps.rst +++ b/docs/apps.rst @@ -59,6 +59,8 @@ Applications .. automodule:: apps.haiku +.. automodule:: Level + .. automodule:: Morse .. automodule:: apps.musicplayer diff --git a/res/LevelApp.png b/res/LevelApp.png new file mode 100644 index 0000000..b48ed1e Binary files /dev/null and b/res/LevelApp.png differ diff --git a/res/level_icon.png b/res/level_icon.png new file mode 100644 index 0000000..827e57f Binary files /dev/null and b/res/level_icon.png differ diff --git a/wasp/apps/flashlight.py b/wasp/apps/flashlight.py index fa734c7..3de7c21 100644 --- a/wasp/apps/flashlight.py +++ b/wasp/apps/flashlight.py @@ -4,16 +4,16 @@ """Flashlight ~~~~~~~~~~~~~ -Shows a pure white screen with the backlight set to maximum. +Shows a bright screen that you can tap to change brightness or switch to redlight. .. figure:: res/TorchApp.png :width: 179 """ import wasp - import icons + class TorchApp(object): """Trivial flashlight application.""" NAME = 'Torch' @@ -21,19 +21,27 @@ class TorchApp(object): def foreground(self): """Activate the application.""" - self.draw() - wasp.system.request_tick(1000) + wasp.system.request_tick(3000) + wasp.system.request_event(wasp.EventMask.TOUCH) self._brightness = wasp.system.brightness wasp.system.brightness = 3 + self._ntouch = 1 + wasp.watch.drawable.fill(0xffff) # white def background(self): - """De-activate the application (without losing state).""" + """De-activate the application (without losing original state).""" wasp.system.brightness = self._brightness def tick(self, ticks): wasp.system.keep_awake() - def draw(self): - """Redraw the display from scratch.""" - wasp.watch.drawable.fill(0xffff) + def touch(self, event): + self._ntouch += 1 + self._ntouch %= 6 + wasp.system.brightness = (-self._ntouch) % 3 + 1 + if (-self._ntouch + 1) % 3 == 0: + if -self._ntouch % 6 < 3: + wasp.watch.drawable.fill(0xf800) # red + else: + wasp.watch.drawable.fill(0xffff) # white diff --git a/wasp/boards/simulator/watch.py b/wasp/boards/simulator/watch.py index 7afc61a..860c414 100644 --- a/wasp/boards/simulator/watch.py +++ b/wasp/boards/simulator/watch.py @@ -48,6 +48,10 @@ class Accelerometer: def steps(self, value): self.reset() + def accel_xyz(self): + # TODO randomly alter the values over time + return (0,0,0) + class Backlight(object): def __init__(self, level=1): pass diff --git a/wasp/drivers/bma421.py b/wasp/drivers/bma421.py index 4901462..14562d4 100644 --- a/wasp/drivers/bma421.py +++ b/wasp/drivers/bma421.py @@ -8,17 +8,33 @@ import bma42x import time +# Sensor orientation definition. +# The 6 most significant bits define the indexes of the x, y, and z values +# in the acceleration tuple returned by the sensor, while the 3 least +# significant bits define their sign (1 = keep original sign, 0 = negate). +# +# Z index ─────────────────┐ +# Y index ───────────────┐ │ +# X index ─────────────┐ │ │ +# ├┐├┐├┐ +_DEFAULT_ORIENTATION = const(0b010010101) +# 1 = keep, 0 = negate │││ +# X sign ───────────────────┘││ +# Y sign ────────────────────┘│ +# Z sign ─────────────────────┘ + class BMA421: """BMA421 driver .. automethod:: __init__ """ - def __init__(self, i2c): + def __init__(self, i2c, orientation=_DEFAULT_ORIENTATION): """Configure the driver. :param machine.I2C i2c: I2C bus used to access the sensor. """ self._dev = bma42x.BMA42X(i2c) + self._orientation = orientation def reset(self): """Reset and reinitialize the sensor.""" @@ -49,3 +65,11 @@ class BMA421: if value != 0: raise ValueError() self._dev.reset_step_counter() + + def accel_xyz(self): + """Return a triple with acceleration values""" + raw = self._dev.read_accel_xyz() + x = raw[self._orientation >> 7 & 0b11] * ((self._orientation >> 1 & 0b10) - 1) + y = raw[self._orientation >> 5 & 0b11] * ((self._orientation & 0b10) - 1) + z = raw[self._orientation >> 3 & 0b11] * ((self._orientation << 1 & 0b10) - 1) + return (x, y, z)