update clue plotter

This commit is contained in:
caternuson 2022-04-05 14:07:03 -07:00
parent 6ac07e72fb
commit 198a22c672
2 changed files with 243 additions and 192 deletions

View file

@ -43,43 +43,55 @@ import gc
import board
from plotter import Plotter
from plot_source import PlotSource, TemperaturePlotSource, PressurePlotSource, \
HumidityPlotSource, ColorPlotSource, ProximityPlotSource, \
IlluminatedColorPlotSource, VolumePlotSource, \
AccelerometerPlotSource, GyroPlotSource, \
MagnetometerPlotSource, PinPlotSource
from plot_source import (
TemperaturePlotSource,
PressurePlotSource,
HumidityPlotSource,
ColorPlotSource,
ProximityPlotSource,
IlluminatedColorPlotSource,
VolumePlotSource,
AccelerometerPlotSource,
GyroPlotSource,
MagnetometerPlotSource,
PinPlotSource,
)
from adafruit_clue import clue
debug = 1
# A list of all the data sources for plotting
sources = [TemperaturePlotSource(clue, mode="Celsius"),
TemperaturePlotSource(clue, mode="Fahrenheit"),
# NOTE: Due to memory contraints, the total number of data sources
# is limited. Can try adding more until a memory limit is hit. At that
# point, decide what to keep and what to toss. Can comment/uncomment lines
# below as desired.
sources = [
TemperaturePlotSource(clue, mode="Celsius"),
# TemperaturePlotSource(clue, mode="Fahrenheit"),
PressurePlotSource(clue, mode="Metric"),
PressurePlotSource(clue, mode="Imperial"),
# PressurePlotSource(clue, mode="Imperial"),
HumidityPlotSource(clue),
ColorPlotSource(clue),
ProximityPlotSource(clue),
IlluminatedColorPlotSource(clue, mode="Red"),
IlluminatedColorPlotSource(clue, mode="Green"),
IlluminatedColorPlotSource(clue, mode="Blue"),
IlluminatedColorPlotSource(clue, mode="Clear"),
VolumePlotSource(clue),
# IlluminatedColorPlotSource(clue, mode="Red"),
# IlluminatedColorPlotSource(clue, mode="Green"),
# IlluminatedColorPlotSource(clue, mode="Blue"),
# IlluminatedColorPlotSource(clue, mode="Clear"),
# VolumePlotSource(clue),
AccelerometerPlotSource(clue),
GyroPlotSource(clue),
MagnetometerPlotSource(clue),
PinPlotSource([board.P0, board.P1, board.P2])
# GyroPlotSource(clue),
# MagnetometerPlotSource(clue),
# PinPlotSource([board.P0, board.P1, board.P2])
]
# The first source to select when plotting starts
current_source_idx = 0
# The various plotting styles - scroll is currently a jump scroll
stylemodes = (("lines", "scroll"), # draws lines between points
stylemodes = (
("lines", "scroll"), # draws lines between points
("lines", "wrap"),
("dots", "scroll"), # just points - slightly quicker
("dots", "wrap")
("dots", "wrap"),
)
current_sm_idx = 0
@ -175,13 +187,15 @@ initial_title = "CLUE Plotter"
# displayio has some static limits on text - pre-calculate the maximum
# length of all of the different PlotSource objects
max_title_len = max(len(initial_title), max([len(str(so)) for so in sources]))
plotter = Plotter(board.DISPLAY,
plotter = Plotter(
board.DISPLAY,
style=stylemodes[current_sm_idx][0],
mode=stylemodes[current_sm_idx][1],
title=initial_title,
max_title_len=max_title_len,
mu_output=mu_plotter_output,
debug=debug)
debug=debug,
)
# If set to true this forces use of colour blindness friendly colours
use_def_pal = False
@ -190,21 +204,28 @@ clue.pixel[0] = clue.BLACK # turn off the NeoPixel on the back of CLUE board
plotter.display_on()
# Using left and right here in case the CLUE is cased hiding A/B labels
popup_text(plotter,
"\n".join(["Button Guide",
popup_text(
plotter,
"\n".join(
[
"Button Guide",
"Left: next source",
" 2secs: palette",
" 4s: Mu plot",
" 6s: range lock",
"Right: style change"]), duration=10)
"Right: style change",
]
),
duration=10,
)
count = 0
while True:
# Set the source and start items
(source, channels) = ready_plot_source(plotter, sources,
use_def_pal,
current_source_idx)
(source, channels) = ready_plot_source(
plotter, sources, use_def_pal, current_source_idx
)
while True:
# Read data from sensor or voltage from pad
@ -213,25 +234,22 @@ while True:
# Check for left (A) and right (B) buttons
if clue.button_a:
# Wait for button release with time-based menu
opt, _ = wait_release(lambda: clue.button_a,
[(2, "Next\nsource"),
(4,
("Source" if use_def_pal else "Default")
+ "\npalette"),
(6,
"Mu output "
+ ("off" if mu_plotter_output else "on")),
(8,
"Range lock\n" + ("off" if range_lock else "on"))
])
opt, _ = wait_release(
lambda: clue.button_a,
[
(2, "Next\nsource"),
(4, ("Source" if use_def_pal else "Default") + "\npalette"),
(6, "Mu output " + ("off" if mu_plotter_output else "on")),
(8, "Range lock\n" + ("off" if range_lock else "on")),
],
)
if opt == 0: # change plot source
current_source_idx = (current_source_idx + 1) % len(sources)
break # to leave inner while and select the new source
elif opt == 1: # toggle palette
use_def_pal = not use_def_pal
plotter.channel_colidx = select_colors(plotter, source,
use_def_pal)
plotter.channel_colidx = select_colors(plotter, source, use_def_pal)
elif opt == 2: # toggle Mu output
mu_plotter_output = not mu_plotter_output
@ -244,8 +262,7 @@ while True:
if clue.button_b: # change plot style and mode
current_sm_idx = (current_sm_idx + 1) % len(stylemodes)
(new_style, new_mode) = stylemodes[current_sm_idx]
wait_release(lambda: clue.button_b,
[(2, new_style + "\n" + new_mode)])
wait_release(lambda: clue.button_b, [(2, new_style + "\n" + new_mode)])
d_print(1, "Graph change", new_style, new_mode)
plotter.change_stylemode(new_style, new_mode)

View file

@ -49,7 +49,7 @@ import array
import displayio
import terminalio
from adafruit_display_text.label import Label
from adafruit_display_text.bitmap_label import Label
def mapf(value, in_min, in_max, out_min, out_max):
@ -59,6 +59,7 @@ def mapf(value, in_min, in_max, out_min, out_max):
# This creates ('{:.0f}', '{:.1f}', '{:.2f}', etc
_FMT_DEC_PLACES = tuple("{:." + str(x) + "f}" for x in range(10))
def format_width(nchars, value):
"""Simple attempt to generate a value within nchars characters.
Return value can be too long, e.g. for nchars=5, bad things happen
@ -76,23 +77,24 @@ def format_width(nchars, value):
return text_value
class Plotter():
_DEFAULT_SCALE_MODE = {"lines": "onscroll",
"dots": "screen"}
class Plotter:
_DEFAULT_SCALE_MODE = {"lines": "onscroll", "dots": "screen"}
# Palette for plotting, first one is set transparent
TRANSPARENT_IDX = 0
# Removed one colour to get number down to 8 for more efficient
# bit-packing in displayio's Bitmap
_PLOT_COLORS = (0x000000,
0x0000ff,
0x00ff00,
0x00ffff,
0xff0000,
_PLOT_COLORS = (
0x000000,
0x0000FF,
0x00FF00,
0x00FFFF,
0xFF0000,
# 0xff00ff,
0xffff00,
0xffffff,
0xff0080)
0xFFFF00,
0xFFFFFF,
0xFF0080,
)
POS_INF = float("inf")
NEG_INF = float("-inf")
@ -110,8 +112,8 @@ class Plotter():
_GRAPH_TOP = 30 # y position for the graph placement
INFO_FG_COLOR = 0x000080
INFO_BG_COLOR = 0xc0c000
LABEL_COLOR = 0xc0c0c0
INFO_BG_COLOR = 0xC0C000
LABEL_COLOR = 0xC0C0C0
def _display_manual(self):
"""Intention was to disable auto_refresh here but this needs a
@ -129,18 +131,26 @@ class Plotter():
else:
return True
def __init__(self, output,
style="lines", mode="scroll", scale_mode=None,
screen_width=240, screen_height=240,
plot_width=192, plot_height=201,
x_divs=4, y_divs=4,
def __init__(
self,
output,
style="lines",
mode="scroll",
scale_mode=None,
screen_width=240,
screen_height=240,
plot_width=192,
plot_height=201,
x_divs=4,
y_divs=4,
scroll_px=50,
max_channels=3,
est_rate=50,
title="",
max_title_len=20,
mu_output=False,
debug=0):
debug=0,
):
"""scroll_px of greater than 1 gives a jump scroll."""
# pylint: disable=too-many-locals,too-many-statements
self._output = output
@ -167,8 +177,8 @@ class Plotter():
self._data_value = []
for _ in range(self._max_channels):
# 'i' is 32 bit signed integer
self._data_y_pos.append(array.array('i', [0] * self._data_size))
self._data_value.append(array.array('f', [0.0] * self._data_size))
self._data_y_pos.append(array.array("i", [0] * self._data_size))
self._data_value.append(array.array("f", [0.0] * self._data_size))
# begin-keep-pylint-happy
self._data_mins = None
@ -243,14 +253,18 @@ class Plotter():
for ch_idx in range(self._channels):
# intentional use of negative array indexing
for data_idx in range(self._data_idx - 1,
self._data_idx - 1 - self._data_values,
-1):
self._data_y_pos[ch_idx][data_idx] = round(mapf(self._data_value[ch_idx][data_idx],
for data_idx in range(
self._data_idx - 1, self._data_idx - 1 - self._data_values, -1
):
self._data_y_pos[ch_idx][data_idx] = round(
mapf(
self._data_value[ch_idx][data_idx],
self._plot_min,
self._plot_max,
self._plot_height_m1,
0))
0,
)
)
def get_colors(self):
return self._PLOT_COLORS
@ -288,16 +302,16 @@ class Plotter():
self._display_manual()
def _make_empty_tg_plot_bitmap(self):
plot_bitmap = displayio.Bitmap(self._plot_width, self._plot_height,
len(self._PLOT_COLORS))
plot_bitmap = displayio.Bitmap(
self._plot_width, self._plot_height, len(self._PLOT_COLORS)
)
# Create a colour palette for plot dots/lines
plot_palette = displayio.Palette(len(self._PLOT_COLORS))
for idx in range(len(self._PLOT_COLORS)):
plot_palette[idx] = self._PLOT_COLORS[idx]
plot_palette.make_transparent(0)
tg_plot_data = displayio.TileGrid(plot_bitmap,
pixel_shader=plot_palette)
tg_plot_data = displayio.TileGrid(plot_bitmap, pixel_shader=plot_palette)
tg_plot_data.x = self._screen_width - self._plot_width - 1
tg_plot_data.y = self._GRAPH_TOP
return (tg_plot_data, plot_bitmap)
@ -325,24 +339,24 @@ class Plotter():
a_plot_grid[0, y] = 1
right_line = displayio.Bitmap(1, grid_height, 2)
tg_right_line = displayio.TileGrid(right_line,
pixel_shader=grid_palette)
tg_right_line = displayio.TileGrid(right_line, pixel_shader=grid_palette)
for y in range(0, grid_height, self.GRID_DOT_SPACING):
right_line[0, y] = 1
bottom_line = displayio.Bitmap(grid_width + 1, 1, 2)
tg_bottom_line = displayio.TileGrid(bottom_line,
pixel_shader=grid_palette)
tg_bottom_line = displayio.TileGrid(bottom_line, pixel_shader=grid_palette)
for x in range(0, grid_width + 1, self.GRID_DOT_SPACING):
bottom_line[x, 0] = 1
# Create a TileGrid using the Bitmap and Palette
# and tiling it based on number of divisions required
tg_plot_grid = displayio.TileGrid(a_plot_grid,
tg_plot_grid = displayio.TileGrid(
a_plot_grid,
pixel_shader=grid_palette,
width=self._x_divs,
height=self._y_divs,
default_tile = 0)
default_tile=0,
)
tg_plot_grid.x = self._screen_width - self._plot_width - 1
tg_plot_grid.y = self._GRAPH_TOP
tg_right_line.x = tg_plot_grid.x + grid_width
@ -360,32 +374,39 @@ class Plotter():
def _make_empty_graph(self, tg_and_plot=None):
font_w, font_h = self._font.get_bounding_box()
self._displayio_title = Label(self._font,
self._displayio_title = Label(
self._font,
text=self._title,
scale=2,
line_spacing=1,
color=self._y_lab_color)
color=self._y_lab_color,
)
self._displayio_title.x = self._screen_width - self._plot_width
self._displayio_title.y = font_h // 2
self._displayio_y_axis_lab = Label(self._font,
text=self._y_axis_lab,
line_spacing=1,
color=self._y_lab_color)
self._displayio_y_axis_lab = Label(
self._font, text=self._y_axis_lab, line_spacing=1, color=self._y_lab_color
)
self._displayio_y_axis_lab.x = 0 # 0 works here because text is ""
self._displayio_y_axis_lab.y = font_h // 2
plot_y_labels = []
# y increases top to bottom of screen
for y_div in range(self._y_divs + 1):
plot_y_labels.append(Label(self._font,
plot_y_labels.append(
Label(
self._font,
text=" " * self._y_lab_width,
line_spacing=1,
color=self._y_lab_color))
plot_y_labels[-1].x = (self._screen_width - self._plot_width
- self._y_lab_width * font_w - 5)
plot_y_labels[-1].y = (round(y_div * self._plot_height / self._y_divs)
+ self._GRAPH_TOP - 1)
color=self._y_lab_color,
)
)
plot_y_labels[-1].x = (
self._screen_width - self._plot_width - self._y_lab_width * font_w - 5
)
plot_y_labels[-1].y = (
round(y_div * self._plot_height / self._y_divs) + self._GRAPH_TOP - 1
)
self._displayio_y_labs = plot_y_labels
# Three items (grid, axis label, title) plus the y tick labels
@ -429,8 +450,7 @@ class Plotter():
pass
def _draw_vline(self, x1, y1, y2, colidx):
"""Draw a clipped vertical line at x1 from pixel one along from y1 to y2.
"""
"""Draw a clipped vertical line at x1 from pixel one along from y1 to y2."""
if y2 == y1:
if 0 <= y2 <= self._plot_height_m1:
self._displayio_plot[x1, y2] = colidx
@ -439,9 +459,11 @@ class Plotter():
# For y2 above y1, on screen this translates to being below
step = 1 if y2 > y1 else -1
for line_y_pos in range(max(0, min(y1 + step, self._plot_height_m1)),
for line_y_pos in range(
max(0, min(y1 + step, self._plot_height_m1)),
max(0, min(y2, self._plot_height_m1)) + step,
step):
step,
):
self._displayio_plot[x1, line_y_pos] = colidx
# def _clear_plot_bitmap(self): ### woz here
@ -460,7 +482,9 @@ class Plotter():
for x_pos in range(x_cols):
# "jump" the gap in the circular buffer for wrap mode
if wrapMode and x_pos == self._x_pos:
data_idx = (data_idx + self._data_size - self._plot_width) % self._data_size
data_idx = (
data_idx + self._data_size - self._plot_width
) % self._data_size
# ideally this should inhibit lines between wrapped data
y_pos = self._data_y_pos[ch_idx][data_idx]
@ -484,12 +508,10 @@ class Plotter():
self._redraw_all_col_idx([self.TRANSPARENT_IDX] * self._channels)
self._plot_dirty = False
def _redraw_all(self):
self._redraw_all_col_idx(self._channel_colidx)
self._plot_dirty = True
def _undraw_column(self, x_pos, data_idx):
"""Undraw a single column at x_pos based on data from data_idx."""
colidx = self.TRANSPARENT_IDX
@ -529,7 +551,7 @@ class Plotter():
"""Update the statistics for minimum and maximum."""
for idx, value in enumerate(values):
# Occasionally check if we need to add a new bucket to stats
if idx == 0 and self._values & 0xf == 0:
if idx == 0 and self._values & 0xF == 0:
now_ns = time.monotonic_ns()
if now_ns - self._data_start_ns[-1] > 1e9:
self._data_start_ns.append(now_ns)
@ -563,9 +585,9 @@ class Plotter():
for ch_idx, value in enumerate(values):
# Last two parameters appear "swapped" - this deals with the
# displayio screen y coordinate increasing downwards
y_pos = round(mapf(value,
self._plot_min, self._plot_max,
self._plot_height_m1, 0))
y_pos = round(
mapf(value, self._plot_min, self._plot_max, self._plot_height_m1, 0)
)
if y_pos < 0 or y_pos >= self._plot_height:
offscale = True
@ -575,8 +597,7 @@ class Plotter():
if self._style == "lines" and self._x_pos != 0:
# Python supports negative array index
prev_y_pos = self._data_y_pos[ch_idx][data_idx - 1]
self._draw_vline(x_pos, prev_y_pos, y_pos,
self._channel_colidx[ch_idx])
self._draw_vline(x_pos, prev_y_pos, y_pos, self._channel_colidx[ch_idx])
self._plot_dirty = True # bit wrong if whole line is off screen
else:
if not offscale:
@ -602,8 +623,10 @@ class Plotter():
headroom = recent_range * self.ZOOM_HEADROOM
# No zoom if the range of data is near the plot range
if (self._plot_min > recent_min - headroom
and self._plot_max < recent_max + headroom):
if (
self._plot_min > recent_min - headroom
and self._plot_max < recent_max + headroom
):
return ()
new_plot_min = max(recent_min - headroom, self._abs_min)
@ -630,11 +653,10 @@ class Plotter():
# set new range if the data does not fit on the screen
# this will also redo y tick labels if necessary
if (new_plot_min < self._plot_min or new_plot_max > self._plot_max):
if new_plot_min < self._plot_min or new_plot_max > self._plot_max:
if self._debug >= 2:
print("Zoom out")
self._change_y_range(new_plot_min, new_plot_max,
redraw_plot=redraw_plot)
self._change_y_range(new_plot_min, new_plot_max, redraw_plot=redraw_plot)
zoom_out = True
else: # otherwise check if zoom in is warranted
@ -642,8 +664,11 @@ class Plotter():
if rescale_zoom_range:
if self._debug >= 2:
print("Zoom in")
self._change_y_range(rescale_zoom_range[0], rescale_zoom_range[1],
redraw_plot=redraw_plot)
self._change_y_range(
rescale_zoom_range[0],
rescale_zoom_range[1],
redraw_plot=redraw_plot,
)
zoom_in = True
if zoom_in or zoom_out:
@ -664,8 +689,11 @@ class Plotter():
changed = self._auto_plot_range(redraw_plot=False)
# Undraw any previous data at current x position
if (not changed and self._data_values >= self._plot_width
and self._values >= self._plot_width):
if (
not changed
and self._data_values >= self._plot_width
and self._values >= self._plot_width
):
self._undraw_column(self._x_pos, data_idx - self._plot_width)
elif self._mode == "scroll":
@ -674,12 +702,13 @@ class Plotter():
if not changed:
self._undraw_bitmap() # Need to cls for the scroll
sc_data_idx = ((data_idx + self._scroll_px - self._plot_width)
% self._data_size)
sc_data_idx = (
data_idx + self._scroll_px - self._plot_width
) % self._data_size
self._data_values -= self._scroll_px
self._redraw_for_scroll(0,
self._plot_width - 1 - self._scroll_px,
sc_data_idx)
self._redraw_for_scroll(
0, self._plot_width - 1 - self._scroll_px, sc_data_idx
)
x_pos = self._plot_width - self._scroll_px
elif self._scale_mode == "pixel":
@ -776,20 +805,25 @@ class Plotter():
text_lines = value.split("\n")
max_word_chars = max([len(word) for word in text_lines])
# If too large reduce the scale
if (max_word_chars * font_scale * font_w > self._screen_width
or len(text_lines) * font_scale * font_h * line_spacing > self._screen_height):
if (
max_word_chars * font_scale * font_w > self._screen_width
or len(text_lines) * font_scale * font_h * line_spacing
> self._screen_height
):
font_scale -= 1
self._displayio_info = Label(self._font, text=value,
self._displayio_info = Label(
self._font,
text=value,
line_spacing=line_spacing,
scale=font_scale,
background_color=None,
color=self.INFO_BG_COLOR)
self._displayio_info.palette[0] = self.INFO_FG_COLOR
self._displayio_info.palette.make_opaque(0)
background_color=self.INFO_FG_COLOR,
color=self.INFO_BG_COLOR,
)
# centre the (left justified) text
self._displayio_info.x = (self._screen_width
- font_scale * font_w * max_word_chars) // 2
self._displayio_info.x = (
self._screen_width - font_scale * font_w * max_word_chars
) // 2
self._displayio_info.y = self._screen_height // 2
self._displayio_graph.append(self._displayio_info)