# SPDX-FileCopyrightText: 2020 Kevin J Walters for Adafruit Industries # # SPDX-License-Identifier: MIT # clue-plotter v1.14 # Sensor and input plotter for Adafruit CLUE in CircuitPython # This plots the sensors and three of the analogue inputs on # the LCD display either with scrolling or wrap mode which # approximates a slow timebase oscilloscope, left button selects # next source or with long press changes palette or longer press # turns on output for Mu plotting, right button changes plot style # Tested with an Adafruit CLUE (Alpha) and CircuitPython and 5.0.0 # copy this file to CLUE board as code.py # needs companion plot_sensor.py and plotter.py files # MIT License # Copyright (c) 2020 Kevin J. Walters # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import time import gc import board from adafruit_clue import clue from plotter import Plotter # pylint: disable=unused-import from plot_source import ( PlotSource, TemperaturePlotSource, PressurePlotSource, HumidityPlotSource, ColorPlotSource, ProximityPlotSource, IlluminatedColorPlotSource, VolumePlotSource, AccelerometerPlotSource, GyroPlotSource, MagnetometerPlotSource, PinPlotSource, ) debug = 1 # A list of all the data sources for plotting # 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"), HumidityPlotSource(clue), ColorPlotSource(clue), ProximityPlotSource(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]) ] # 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 ("lines", "wrap"), ("dots", "scroll"), # just points - slightly quicker ("dots", "wrap"), ) current_sm_idx = 0 def d_print(level, *args, **kwargs): """A simple conditional print for debugging based on global debug level.""" if not isinstance(level, int): print(level, *args, **kwargs) elif debug >= level: print(*args, **kwargs) def select_colors(plttr, src, def_palette): """Choose the colours based on the particular PlotSource or forcing use of default palette.""" # otherwise use defaults channel_colidx = [] palette = plttr.get_colors() colors = PlotSource.DEFAULT_COLORS if def_palette else src.colors() for col in colors: try: channel_colidx.append(palette.index(col)) except ValueError: channel_colidx.append(PlotSource.DEFAULT_COLORS.index(col)) return channel_colidx def ready_plot_source(plttr, srcs, def_palette, index=0): """Select the plot source by index from srcs list and then setup the plot parameters by retrieving meta-data from the PlotSource object.""" src = srcs[index] # Put the description of the source on screen at the top source_name = str(src) d_print(1, "Selecting source:", source_name) plttr.clear_all() plttr.title = source_name plttr.y_axis_lab = src.units() # The range on graph will start at this value plttr.y_range = (src.initial_min(), src.initial_max()) plttr.y_min_range = src.range_min() # Sensor/data source is expected to produce data between these values plttr.y_full_range = (src.min(), src.max()) channels_from_src = src.values() plttr.channels = channels_from_src # Can be between 1 and 3 plttr.channel_colidx = select_colors(plttr, src, def_palette) src.start() return (src, channels_from_src) def wait_release(func, menu): """Calls func repeatedly waiting for it to return a false value and goes through menu list as time passes. 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 for that period. The entries must be in ascending time order.""" start_t_ns = time.monotonic_ns() menu_option = None selected = False for menu_option, menu_entry in enumerate(menu): menu_time_ns = start_t_ns + int(menu_entry[0] * 1e9) menu_text = menu_entry[1] if menu_text: plotter.info = menu_text while time.monotonic_ns() < menu_time_ns: if not func(): selected = True break if menu_text: plotter.info = "" if selected: break return (menu_option, (time.monotonic_ns() - start_t_ns) * 1e-9) def popup_text(plttr, text, duration=1.0): """Place some text on the screen using info property of Plotter object for duration seconds.""" plttr.info = text time.sleep(duration) plttr.info = None mu_plotter_output = False range_lock = False 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, 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, ) # If set to true this forces use of colour blindness friendly colours use_def_pal = False 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", "Left: next source", " 2secs: palette", " 4s: Mu plot", " 6s: range lock", "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 ) while True: # Read data from sensor or voltage from pad all_data = source.data() # 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")), ], ) # pylint: disable=no-else-break 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) elif opt == 2: # toggle Mu output mu_plotter_output = not mu_plotter_output plotter.mu_output = mu_plotter_output else: # toggle range lock range_lock = not range_lock plotter.y_range_lock = range_lock 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)]) d_print(1, "Graph change", new_style, new_mode) plotter.change_stylemode(new_style, new_mode) # Display it if channels == 1: plotter.data_add((all_data,)) else: plotter.data_add(all_data) # An occasional print of free heap if debug >= 3 and count % 15 == 0: gc.collect() # must collect() first to measure free memory print("Free memory:", gc.mem_free()) count += 1 source.stop() plotter.display_off()