Skip to content

Commit c9309bb

Browse files
author
Kevin J Walters
committed
Adding files for upcoming CLUE Sensor Plotter (to a new branch for different PR).
1 parent c4d275f commit c9309bb

File tree

6 files changed

+2204
-0
lines changed

6 files changed

+2204
-0
lines changed
Binary file not shown.

CLUE_Sensor_Plotter/clue-plotter.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
# clue-plotter v1.13
2+
# Sensor and input plotter for Adafruit CLUE in CircuitPython
3+
# This plots the sensors and three of the analogue inputs on
4+
# the LCD display either with scrolling or wrap mode which
5+
# approximates a slow timebase oscilloscope, left button selects
6+
# next source or with long press changes palette or longer press
7+
# turns on output for Mu plotting, right button changes plot style
8+
9+
# Tested with an Adafruit CLUE (Alpha) and CircuitPython and 5.0.0
10+
11+
# copy this file to CLUE board as code.py
12+
# needs companion plot_sensor.py and plotter.py files
13+
14+
# MIT License
15+
16+
# Copyright (c) 2020 Kevin J. Walters
17+
18+
# Permission is hereby granted, free of charge, to any person obtaining a copy
19+
# of this software and associated documentation files (the "Software"), to deal
20+
# in the Software without restriction, including without limitation the rights
21+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22+
# copies of the Software, and to permit persons to whom the Software is
23+
# furnished to do so, subject to the following conditions:
24+
25+
# The above copyright notice and this permission notice shall be included in all
26+
# copies or substantial portions of the Software.
27+
28+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
34+
# SOFTWARE.
35+
36+
import time
37+
38+
import gc
39+
import board
40+
41+
from plotter import Plotter
42+
from plot_source import PlotSource, TemperaturePlotSource, PressurePlotSource, \
43+
HumidityPlotSource, ColorPlotSource, ProximityPlotSource, \
44+
IlluminatedColorPlotSource, VolumePlotSource, \
45+
AccelerometerPlotSource, GyroPlotSource, \
46+
MagnetometerPlotSource, PinPlotSource
47+
from adafruit_clue import clue
48+
49+
50+
debug = 1
51+
52+
53+
# A list of all the data sources for plotting
54+
sources = [TemperaturePlotSource(clue, mode="Celsius"),
55+
TemperaturePlotSource(clue, mode="Fahrenheit"),
56+
PressurePlotSource(clue, mode="Metric"),
57+
PressurePlotSource(clue, mode="Imperial"),
58+
HumidityPlotSource(clue),
59+
ColorPlotSource(clue),
60+
ProximityPlotSource(clue),
61+
IlluminatedColorPlotSource(clue, mode="Red"),
62+
IlluminatedColorPlotSource(clue, mode="Green"),
63+
IlluminatedColorPlotSource(clue, mode="Blue"),
64+
IlluminatedColorPlotSource(clue, mode="Clear"),
65+
VolumePlotSource(clue),
66+
AccelerometerPlotSource(clue),
67+
GyroPlotSource(clue),
68+
MagnetometerPlotSource(clue),
69+
PinPlotSource([board.P0, board.P1, board.P2])
70+
]
71+
# The first source to select when plotting starts
72+
current_source_idx = 0
73+
74+
# The various plotting styles - scroll is currently a jump scroll
75+
stylemodes = (("lines", "scroll"), # draws lines between points
76+
("lines", "wrap"),
77+
("dots", "scroll"), # just points - slightly quicker
78+
("dots", "wrap")
79+
)
80+
current_sm_idx = 0
81+
82+
83+
def d_print(level, *args, **kwargs):
84+
"""A simple conditional print for debugging based on global debug level."""
85+
if not isinstance(level, int):
86+
print(level, *args, **kwargs)
87+
elif debug >= level:
88+
print(*args, **kwargs)
89+
90+
91+
def select_colors(plttr, src, def_palette):
92+
"""Choose the colours based on the particular PlotSource
93+
or forcing use of default palette."""
94+
# otherwise use defaults
95+
channel_colidx = []
96+
palette = plttr.get_colors()
97+
colors = PlotSource.DEFAULT_COLORS if def_palette else src.colors()
98+
for col in colors:
99+
try:
100+
channel_colidx.append(palette.index(col))
101+
except ValueError:
102+
channel_colidx.append(PlotSource.DEFAULT_COLORS.index(col))
103+
return channel_colidx
104+
105+
106+
def ready_plot_source(plttr, srcs, def_palette, index=0):
107+
"""Select the plot source by index from srcs list and then setup the
108+
plot parameters by retrieving meta-data from the PlotSource object."""
109+
src = srcs[index]
110+
# Put the description of the source on screen at the top
111+
source_name = str(src)
112+
d_print(1, "Selecting source:", source_name)
113+
plttr.clear_all()
114+
plttr.title = source_name
115+
plttr.y_axis_lab = src.units()
116+
# The range on graph will start at this value
117+
plttr.y_range = (src.initial_min(), src.initial_max())
118+
plttr.y_min_range = src.range_min()
119+
# Sensor/data source is expected to produce data between these values
120+
plttr.y_full_range = (src.min(), src.max())
121+
channels_from_src = src.values()
122+
plttr.channels = channels_from_src # Can be between 1 and 3
123+
plttr.channel_colidx = select_colors(plttr, src, def_palette)
124+
125+
src.start()
126+
return (src, channels_from_src)
127+
128+
129+
def wait_release(func, menu):
130+
"""Calls func repeatedly waiting for it to return a false value
131+
and goes through menu list as time passes.
132+
133+
The menu is a list of menu entries where each entry is a
134+
two element list of time passed in seconds and text to display
135+
for that period.
136+
The entries must be in ascending time order."""
137+
138+
start_t_ns = time.monotonic_ns()
139+
menu_option = None
140+
selected = False
141+
142+
for menu_option, menu_entry in enumerate(menu):
143+
menu_time_ns = start_t_ns + int(menu_entry[0] * 1e9)
144+
menu_text = menu_entry[1]
145+
if menu_text:
146+
plotter.info = menu_text
147+
while time.monotonic_ns() < menu_time_ns:
148+
if not func():
149+
selected = True
150+
break
151+
if menu_text:
152+
plotter.info = ""
153+
if selected:
154+
break
155+
156+
return (menu_option, (time.monotonic_ns() - start_t_ns) * 1e-9)
157+
158+
159+
def popup_text(plttr, text, duration=1.0):
160+
"""Place some text on the screen using info property of Plotter object
161+
for duration seconds."""
162+
plttr.info = text
163+
time.sleep(duration)
164+
plttr.info = None
165+
166+
167+
mu_plotter_output = False
168+
range_lock = False
169+
170+
initial_title = "CLUE Plotter"
171+
# displayio has some static limits on text - pre-calculate the maximum
172+
# length of all of the different PlotSource objects
173+
max_title_len = max(len(initial_title), max([len(str(so)) for so in sources]))
174+
plotter = Plotter(board.DISPLAY,
175+
style=stylemodes[current_sm_idx][0],
176+
mode=stylemodes[current_sm_idx][1],
177+
title=initial_title,
178+
max_title_len=max_title_len,
179+
mu_output=mu_plotter_output,
180+
debug=debug)
181+
182+
# If set to true this forces use of colour blindness friendly colours
183+
use_def_pal = False
184+
185+
clue.pixel[0] = clue.BLACK # turn off the NeoPixel on the back of CLUE board
186+
187+
plotter.display_on()
188+
# Using left and right here in case the CLUE is cased hiding A/B labels
189+
popup_text(plotter,
190+
"\n".join(["Button Guide",
191+
"Left: next source",
192+
" 2secs: palette",
193+
" 4s: Mu plot",
194+
" 6s: range lock",
195+
"Right: style change"]), duration=10)
196+
197+
count = 0
198+
199+
while True:
200+
# Set the source and start items
201+
(source, channels) = ready_plot_source(plotter, sources,
202+
use_def_pal,
203+
current_source_idx)
204+
205+
while True:
206+
# Read data from sensor or voltage from pad
207+
all_data = source.data()
208+
209+
# Check for left (A) and right (B) buttons
210+
if clue.button_a:
211+
# Wait for button release with time-based menu
212+
opt, _ = wait_release(lambda: clue.button_a,
213+
[(2, "Next\nsource"),
214+
(4,
215+
("Source" if use_def_pal else "Default")
216+
+ "\npalette"),
217+
(6,
218+
"Mu output "
219+
+ ("off" if mu_plotter_output else "on")),
220+
(8,
221+
"Range lock\n" + ("off" if range_lock else "on"))
222+
])
223+
if opt == 0: # change plot source
224+
current_source_idx = (current_source_idx + 1) % len(sources)
225+
break # to leave inner while and select the new source
226+
227+
elif opt == 1: # toggle palette
228+
use_def_pal = not use_def_pal
229+
plotter.channel_colidx = select_colors(plotter, source,
230+
use_def_pal)
231+
232+
elif opt == 2: # toggle Mu output
233+
mu_plotter_output = not mu_plotter_output
234+
plotter.mu_output = mu_plotter_output
235+
236+
else: # toggle range lock
237+
range_lock = not range_lock
238+
plotter.y_range_lock = range_lock
239+
240+
if clue.button_b: # change plot style and mode
241+
current_sm_idx = (current_sm_idx + 1) % len(stylemodes)
242+
(new_style, new_mode) = stylemodes[current_sm_idx]
243+
wait_release(lambda: clue.button_b,
244+
[(2, new_style + "\n" + new_mode)])
245+
d_print(1, "Graph change", new_style, new_mode)
246+
plotter.change_stylemode(new_style, new_mode)
247+
248+
# Display it
249+
if channels == 1:
250+
plotter.data_add((all_data,))
251+
else:
252+
plotter.data_add(all_data)
253+
254+
# An occasional print of free heap
255+
if debug >=3 and count % 15 == 0:
256+
gc.collect() # must collect() first to measure free memory
257+
print("Free memory:", gc.mem_free())
258+
259+
count += 1
260+
261+
source.stop()
262+
263+
plotter.display_off()

0 commit comments

Comments
 (0)