update clue plotter
This commit is contained in:
parent
6ac07e72fb
commit
198a22c672
2 changed files with 243 additions and 192 deletions
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue