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,44 +43,56 @@ import gc
import board import board
from plotter import Plotter from plotter import Plotter
from plot_source import PlotSource, TemperaturePlotSource, PressurePlotSource, \ from plot_source import (
HumidityPlotSource, ColorPlotSource, ProximityPlotSource, \ TemperaturePlotSource,
IlluminatedColorPlotSource, VolumePlotSource, \ PressurePlotSource,
AccelerometerPlotSource, GyroPlotSource, \ HumidityPlotSource,
MagnetometerPlotSource, PinPlotSource ColorPlotSource,
ProximityPlotSource,
IlluminatedColorPlotSource,
VolumePlotSource,
AccelerometerPlotSource,
GyroPlotSource,
MagnetometerPlotSource,
PinPlotSource,
)
from adafruit_clue import clue from adafruit_clue import clue
debug = 1 debug = 1
# A list of all the data sources for plotting # A list of all the data sources for plotting
sources = [TemperaturePlotSource(clue, mode="Celsius"), # NOTE: Due to memory contraints, the total number of data sources
TemperaturePlotSource(clue, mode="Fahrenheit"), # is limited. Can try adding more until a memory limit is hit. At that
PressurePlotSource(clue, mode="Metric"), # point, decide what to keep and what to toss. Can comment/uncomment lines
PressurePlotSource(clue, mode="Imperial"), # below as desired.
HumidityPlotSource(clue), sources = [
ColorPlotSource(clue), TemperaturePlotSource(clue, mode="Celsius"),
ProximityPlotSource(clue), # TemperaturePlotSource(clue, mode="Fahrenheit"),
IlluminatedColorPlotSource(clue, mode="Red"), PressurePlotSource(clue, mode="Metric"),
IlluminatedColorPlotSource(clue, mode="Green"), # PressurePlotSource(clue, mode="Imperial"),
IlluminatedColorPlotSource(clue, mode="Blue"), HumidityPlotSource(clue),
IlluminatedColorPlotSource(clue, mode="Clear"), ColorPlotSource(clue),
VolumePlotSource(clue), ProximityPlotSource(clue),
AccelerometerPlotSource(clue), # IlluminatedColorPlotSource(clue, mode="Red"),
GyroPlotSource(clue), # IlluminatedColorPlotSource(clue, mode="Green"),
MagnetometerPlotSource(clue), # IlluminatedColorPlotSource(clue, mode="Blue"),
PinPlotSource([board.P0, board.P1, board.P2]) # IlluminatedColorPlotSource(clue, mode="Clear"),
] # VolumePlotSource(clue),
AccelerometerPlotSource(clue),
# GyroPlotSource(clue),
# MagnetometerPlotSource(clue),
# PinPlotSource([board.P0, board.P1, board.P2])
]
# The first source to select when plotting starts # The first source to select when plotting starts
current_source_idx = 0 current_source_idx = 0
# The various plotting styles - scroll is currently a jump scroll # The various plotting styles - scroll is currently a jump scroll
stylemodes = (("lines", "scroll"), # draws lines between points stylemodes = (
("lines", "wrap"), ("lines", "scroll"), # draws lines between points
("dots", "scroll"), # just points - slightly quicker ("lines", "wrap"),
("dots", "wrap") ("dots", "scroll"), # just points - slightly quicker
) ("dots", "wrap"),
)
current_sm_idx = 0 current_sm_idx = 0
@ -94,7 +106,7 @@ def d_print(level, *args, **kwargs):
def select_colors(plttr, src, def_palette): def select_colors(plttr, src, def_palette):
"""Choose the colours based on the particular PlotSource """Choose the colours based on the particular PlotSource
or forcing use of default palette.""" or forcing use of default palette."""
# otherwise use defaults # otherwise use defaults
channel_colidx = [] channel_colidx = []
palette = plttr.get_colors() palette = plttr.get_colors()
@ -109,7 +121,7 @@ def select_colors(plttr, src, def_palette):
def ready_plot_source(plttr, srcs, def_palette, index=0): def ready_plot_source(plttr, srcs, def_palette, index=0):
"""Select the plot source by index from srcs list and then setup the """Select the plot source by index from srcs list and then setup the
plot parameters by retrieving meta-data from the PlotSource object.""" plot parameters by retrieving meta-data from the PlotSource object."""
src = srcs[index] src = srcs[index]
# Put the description of the source on screen at the top # Put the description of the source on screen at the top
source_name = str(src) source_name = str(src)
@ -132,12 +144,12 @@ def ready_plot_source(plttr, srcs, def_palette, index=0):
def wait_release(func, menu): def wait_release(func, menu):
"""Calls func repeatedly waiting for it to return a false value """Calls func repeatedly waiting for it to return a false value
and goes through menu list as time passes. and goes through menu list as time passes.
The menu is a list of menu entries where each entry is a The menu is a list of menu entries where each entry is a
two element list of time passed in seconds and text to display two element list of time passed in seconds and text to display
for that period. for that period.
The entries must be in ascending time order.""" The entries must be in ascending time order."""
start_t_ns = time.monotonic_ns() start_t_ns = time.monotonic_ns()
menu_option = None menu_option = None
@ -162,7 +174,7 @@ def wait_release(func, menu):
def popup_text(plttr, text, duration=1.0): def popup_text(plttr, text, duration=1.0):
"""Place some text on the screen using info property of Plotter object """Place some text on the screen using info property of Plotter object
for duration seconds.""" for duration seconds."""
plttr.info = text plttr.info = text
time.sleep(duration) time.sleep(duration)
plttr.info = None plttr.info = None
@ -175,13 +187,15 @@ initial_title = "CLUE Plotter"
# displayio has some static limits on text - pre-calculate the maximum # displayio has some static limits on text - pre-calculate the maximum
# length of all of the different PlotSource objects # length of all of the different PlotSource objects
max_title_len = max(len(initial_title), max([len(str(so)) for so in sources])) max_title_len = max(len(initial_title), max([len(str(so)) for so in sources]))
plotter = Plotter(board.DISPLAY, plotter = Plotter(
style=stylemodes[current_sm_idx][0], board.DISPLAY,
mode=stylemodes[current_sm_idx][1], style=stylemodes[current_sm_idx][0],
title=initial_title, mode=stylemodes[current_sm_idx][1],
max_title_len=max_title_len, title=initial_title,
mu_output=mu_plotter_output, max_title_len=max_title_len,
debug=debug) mu_output=mu_plotter_output,
debug=debug,
)
# If set to true this forces use of colour blindness friendly colours # If set to true this forces use of colour blindness friendly colours
use_def_pal = False 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() plotter.display_on()
# Using left and right here in case the CLUE is cased hiding A/B labels # Using left and right here in case the CLUE is cased hiding A/B labels
popup_text(plotter, popup_text(
"\n".join(["Button Guide", plotter,
"Left: next source", "\n".join(
" 2secs: palette", [
" 4s: Mu plot", "Button Guide",
" 6s: range lock", "Left: next source",
"Right: style change"]), duration=10) " 2secs: palette",
" 4s: Mu plot",
" 6s: range lock",
"Right: style change",
]
),
duration=10,
)
count = 0 count = 0
while True: while True:
# Set the source and start items # Set the source and start items
(source, channels) = ready_plot_source(plotter, sources, (source, channels) = ready_plot_source(
use_def_pal, plotter, sources, use_def_pal, current_source_idx
current_source_idx) )
while True: while True:
# Read data from sensor or voltage from pad # Read data from sensor or voltage from pad
@ -213,25 +234,22 @@ while True:
# Check for left (A) and right (B) buttons # Check for left (A) and right (B) buttons
if clue.button_a: if clue.button_a:
# Wait for button release with time-based menu # Wait for button release with time-based menu
opt, _ = wait_release(lambda: clue.button_a, opt, _ = wait_release(
[(2, "Next\nsource"), lambda: clue.button_a,
(4, [
("Source" if use_def_pal else "Default") (2, "Next\nsource"),
+ "\npalette"), (4, ("Source" if use_def_pal else "Default") + "\npalette"),
(6, (6, "Mu output " + ("off" if mu_plotter_output else "on")),
"Mu output " (8, "Range lock\n" + ("off" if range_lock else "on")),
+ ("off" if mu_plotter_output else "on")), ],
(8, )
"Range lock\n" + ("off" if range_lock else "on"))
])
if opt == 0: # change plot source if opt == 0: # change plot source
current_source_idx = (current_source_idx + 1) % len(sources) current_source_idx = (current_source_idx + 1) % len(sources)
break # to leave inner while and select the new source break # to leave inner while and select the new source
elif opt == 1: # toggle palette elif opt == 1: # toggle palette
use_def_pal = not use_def_pal use_def_pal = not use_def_pal
plotter.channel_colidx = select_colors(plotter, source, plotter.channel_colidx = select_colors(plotter, source, use_def_pal)
use_def_pal)
elif opt == 2: # toggle Mu output elif opt == 2: # toggle Mu output
mu_plotter_output = not mu_plotter_output mu_plotter_output = not mu_plotter_output
@ -244,8 +262,7 @@ while True:
if clue.button_b: # change plot style and mode if clue.button_b: # change plot style and mode
current_sm_idx = (current_sm_idx + 1) % len(stylemodes) current_sm_idx = (current_sm_idx + 1) % len(stylemodes)
(new_style, new_mode) = stylemodes[current_sm_idx] (new_style, new_mode) = stylemodes[current_sm_idx]
wait_release(lambda: clue.button_b, wait_release(lambda: clue.button_b, [(2, new_style + "\n" + new_mode)])
[(2, new_style + "\n" + new_mode)])
d_print(1, "Graph change", new_style, new_mode) d_print(1, "Graph change", new_style, new_mode)
plotter.change_stylemode(new_style, new_mode) plotter.change_stylemode(new_style, new_mode)
@ -256,7 +273,7 @@ while True:
plotter.data_add(all_data) plotter.data_add(all_data)
# An occasional print of free heap # An occasional print of free heap
if debug >=3 and count % 15 == 0: if debug >= 3 and count % 15 == 0:
gc.collect() # must collect() first to measure free memory gc.collect() # must collect() first to measure free memory
print("Free memory:", gc.mem_free()) print("Free memory:", gc.mem_free())

View file

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