wasp-os/wasp/apps/steps.py
Daniel Thompson 7a5990072c apps: steps: Add a history graph
The steplogger records steps but currently there is no way to see the data
recorded on the device itself. Make a first attempt at graphing the
step data.

Signed-off-by: Daniel Thompson <daniel@redfelineninja.org.uk>
2021-02-25 08:00:03 +00:00

178 lines
5.3 KiB
Python

# SPDX-License-Identifier: LGPL-3.0-or-later
# Copyright (C) 2020 Daniel Thompson
"""Step counter
~~~~~~~~~~~~~~~
Provide a daily step count.
.. figure:: res/StepsApp.png
:width: 179
The step counts automatically reset at midnight.
"""
import wasp
import fonts
import icons
import time
import watch
# 2-bit RLE, generated from res/feet.png, 240 bytes
feet = (
b'\x02'
b'00'
b'\x13\xc1-\xc4+\xc6*\xc6*\xc6&\xc3\x01\xc6\t\xc2'
b'\x1b\xc3\x02\xc5\x08\xc4\x1a\xc4\x01\xc5\x08\xc5\x19\xc4\x02\xc3'
b'\x08\xc6\x17\xc1\x02\xc3\x02\xc3\x08\xc6\x16\xc3\x02\xc1\x0e\xc6'
b'\x01\xc3\x12\xc3\x11\xc6\x01\xc3\x13\xc2\x05\xc2\n\xc5\x02\xc3'
b'\x10\xc2\x01\xc2\x02\xc6\n\xc4\x01\xc4\x10\xc2\x04\xc7\x0b\xc1'
b'\x03\xc3\x11\xc3\x02\xc8\x10\xc2\x01\xc3\r\xc2\x02\xc9\x13\xc3'
b'\x0b\xc1\x05\xc9\x0c\xc2\x05\xc3\x0b\xc2\x03\xc9\x0c\xc5\x03\xc2'
b'\x0c\xc2\x02\xca\x0c\xc6\x05\xc2\t\xc2\x02\xca\x0c\xc7\x03\xc3'
b'\r\xca\x0c\xc8\x02\xc3\x0c\xca\r\xc9\x02\xc1\r\xca\r\xc9'
b'\x04\xc2\n\xca\x0e\xc9\x02\xc3\n\xca\x0e\xc9\x02\xc2\x0b\xca'
b'\x0e\xca\x0e\xca\x0e\xca\x0f\xc9\x0e\xca\x0f\xca\r\xca\x0f\xca'
b'\r\xca\x10\xcb\x0b\xca\x10\xcc\n\xca\x10\xcd\t\xca\x11\xcc'
b'\x08\xca\x12\xcc\x07\xcb\x13\xcb\x06\xcb\x14\xcb\x05\xcc\x15\xca'
b'\x04\xcc\x16\xc9\x05\xcc\x17\xc7\x05\xcd\x17\xc7\x05\xcc\x1a\xc4'
b"\x07\xcb%\xca&\xca'\xc8)\xc6+\xc4\x0e"
)
class StepCounterApp():
"""Step counter application."""
NAME = 'Steps'
ICON = icons.app
def __init__(self):
watch.accel.reset()
self._scroll = wasp.widgets.ScrollIndicator()
self._wake = 0
def foreground(self):
"""Cancel the alarm and draw the application.
Cancelling the alarm has two effects. Firstly it ensures the
step count won't change whilst we are watching it and, secondly
it ensures that if the time of day has been set to a value in
the past that we reconfigure the alarm.
This does in the side effect that if the application of open at
midnight then the reset doesn't happen for that day.
"""
wasp.system.cancel_alarm(self._wake, self._reset)
wasp.system.bar.clock = True
self._page = -1
self._draw()
wasp.system.request_event(wasp.EventMask.SWIPE_UPDOWN)
wasp.system.request_tick(1000)
def background(self):
"""Set an alarm to trigger at midnight and reset the counter."""
now = watch.rtc.get_localtime()
yyyy = now[0]
mm = now[1]
dd = now[2]
then = (yyyy, mm, dd+1, 0, 0, 0, 0, 0, 0)
self._wake = time.mktime(then)
wasp.system.set_alarm(self._wake, self._reset)
def _reset(self):
""""Reset the step counter and re-arm the alarm."""
watch.accel.steps = 0
self._wake += 24 * 60 * 60
wasp.system.set_alarm(self._wake, self._reset)
def swipe(self, event):
if event[0] == wasp.EventType.DOWN:
if self._page == -1:
return
self._page -= 1
else:
self._page += 1
mute = wasp.watch.display.mute
mute(True)
self._draw()
mute(False)
def tick(self, ticks):
if self._page == -1:
self._update()
def _draw(self):
"""Draw the display from scratch."""
draw = wasp.watch.drawable
draw.fill()
if self._page == -1:
self._update()
wasp.system.bar.draw()
else:
self._update_graph()
def _update(self):
draw = wasp.watch.drawable
# Draw the icon
draw.blit(feet, 12, 132-24)
# Update the status bar
now = wasp.system.bar.update()
# Update the scroll indicator
scroll = self._scroll
scroll.up = False
scroll.draw()
# Update the step count
count = watch.accel.steps
t = str(count)
w = fonts.width(fonts.sans36, t)
draw.set_font(fonts.sans36)
draw.set_color(draw.lighten(wasp.system.theme('spot1'), wasp.system.theme('contrast')))
draw.string(t, 228-w, 132-18)
def _update_graph(self):
draw = watch.drawable
draw.set_font(fonts.sans24)
draw.set_color(0xffff)
# Draw the date
now = int(watch.rtc.time())
then = now - ((24*60*60) * self._page)
walltime = time.localtime(then)
draw.string('{:02d}-{:02d}'.format(walltime[2], walltime[1]), 0, 0)
# Get the iterable step date for the currently selected date
data = wasp.system.steps.data(then)
# Bail if there is no data
if not data:
draw.string('No data', 239-160, 0, 160, right=True)
return
color = wasp.system.theme('spot2')
# Draw the frame
draw.fill(0x3969, 0, 39, 240, 1)
draw.fill(0x3969, 0, 239, 240, 1)
for i in (0, 60, 90, 120, 150, 180, 239):
draw.fill(0x3969, i, 39, 1, 201)
total = 0
for x, d in enumerate(data):
if d == 0 or x < 2:
# TODO: the x < 2 conceals BUGZ
continue
total += d
d = d // 3
if d > 200:
draw.fill(0xffff, x, 239-200, 1, 200)
else:
draw.fill(color, x, 239-d, 1, d)
draw.string(str(total), 239-160, 0, 160, right=True)