Maker.io main logo

PyLeap CLUE Sensor Plotter

2024-11-01 | By Adafruit Industries

License: See Original Project Humidity Light Movement Sound Temperature

Courtesy of Adafruit

Guide by Liz Clark

Overview

sensors_allSensorsDbl

 

The CLUE has tons of onboard sensors that can sense movement, ‎light, sound, and environmental measures like temperature and ‎humidity. This project uploads code to the CLUE using PyLeap that ‎uses the CLUE's onboard display to plot all of the sensors' data.‎

This project was originally created by Kevin Walters. The original ‎Learn Guide can be found here.‎

Original CLUE Sensor Plotter in CircuitPython Learn Guide

Parts

Text editor powered by tinymce.‎

CircuitPython on CLUE

CircuitPython is a derivative of MicroPython designed to simplify ‎experimentation and education on low-cost microcontrollers. It ‎makes it easier than ever to get prototyping by requiring no upfront ‎desktop software downloads. Simply copy and edit files on ‎the CIRCUITPY flash drive to iterate.‎

The following instructions will show you how to install CircuitPython. ‎If you've already installed CircuitPython but are looking to update it ‎or reinstall it, the same steps work for that as well!‎

Set up CircuitPython Quick Start!‎

Follow this quick step-by-step for super-fast Python power :)‎

Download the latest version of CircuitPython for CLUE from ‎circuitpython.org

Click the link above to download the latest version of ‎CircuitPython for the CLUE.‎

Download and save it to your desktop (or wherever is handy).‎

download_1

Plug your CLUE into your computer using a known-good USB cable.‎

A lot of people end up using charge-only USB cables and it is very ‎frustrating! So, make sure you have a USB cable you know is good ‎for data sync.

Double-click the Reset button on the top (magenta arrow) on your ‎board, and you will see the NeoPixel RGB LED (green arrow) turn ‎green. If it turns red, check the USB cable, try another USB port, ‎etc. Note: The little red LED next to the USB connector will pulse red. ‎That's ok!‎

If double-clicking doesn't work the first time, try again. Sometimes it ‎can take a few tries to get the rhythm right!‎

board_2

You will see a new disk drive appear called CLUEBOOT.‎

Drag the adafruit-circuitpython-clue-etc.uf2 file to CLUEBOOT.‎

drive_3

drive_4

The LED will flash. Then, the CLUEBOOT drive will disappear, and a ‎new disk drive called CIRCUITPY will appear.‎

If this is the first time, you're installing CircuitPython or you're doing ‎a completely fresh install after erasing the filesystem, you will have ‎two files - boot_out.txt, and code.py, and one folder - lib on ‎your CIRCUITPY drive.‎

If CircuitPython was already installed, the files present before ‎reloading CircuitPython should still be present on ‎your CIRCUITPY drive. Loading CircuitPython will not create new ‎files if there was already a CircuitPython filesystem present.‎

That's it, you're done! :)‎

done_5

Text editor powered by tinymce.‎

Pairing

Now that you're done uploading the correct ‎firmware, disconnect your device from your computer and power it ‎via Li-Poly or AAA battery pack.‎

Pairing device to PyLeap

Once powered, press the small Reset button in the center of the ‎board (Circuit Playground Bluefruit) or on the top right of the board ‎‎(CLUE). When the blue light flashes, press the Reset button again.‎

circuit_playground_ezgif22

Circuit Playground Bluefruit with a small Reset button in the center of the board

reset_6

Adafruit CLUE Reset Button (Highlighted on the upper right)‎

When done correctly, the LEDs will flash yellow followed by solid ‎blue. Once this occurs, the board will continuously be in discovery ‎mode.‎

Scan & Connect

When your Circuit Playground Bluefruit or Adafruit CLUE is in ‎discovery mode, hold it very closely to your iPhone or iPadOS to pair.

circuitpython_ezgifcom-gif-maker

Below the spinning Blinka, you'll notice a status indicator that will let ‎you know your current pairing status.‎

Once you've found your device and received the Bluetooth Pairing ‎Request message, press Pair to pair your board to your iPhone or ‎iPadOS.‎

pair_7

If your Circuit Playground Bluefruit ‎doesn't appear:‎

  1. ‎Check to see if your Circuit Playground Bluefruit is powered on. ‎Verify that the green On light is lit.‎

  2. Make sure your Circuit Playground Bluefruit is running the ‎correct firmware. See the CircuitPython page in this guide.‎

  3. Try resetting the Circuit Playground Bluefruit by pressing the ‎small Reset button near the center of the board.‎

Text editor powered by tinymce.‎

Sensor Plotter Code

plotter_8

Download Project Bundle

Copy Code
# 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()

View on GitHub

Text editor powered by tinymce.‎

Usage

sensors_allSensorsDbl

After pairing your CLUE with the PyLeap app, scroll down to ‎the PyLeap CLUE Sensor Plotter project in the list. Tap to open it ‎and then tap on Run it! to download the project to your CLUE over ‎BLE.‎

sensors_00046

Once the project is uploaded, the code will begin running. First, you'll ‎see a "button guide" on the screen that tells you what the CLUE's A ‎and B buttons do. The A button has a few different functions ‎depending on the length of time that you press the button.‎

  • Short press: advance to the next sensor

  • Two seconds: change the palette (colors on the display)‎

  • Three seconds: opens the Mu plotter if your CLUE is connected ‎via USB to Mu

  • Four seconds: locks the range of the plot

The B button changes the style of the plot. You can choose between ‎plotting with a line or dots and having the plotter scroll or wrap.‎

clue_9

Toggle through plotting the different sensors on the CLUE. As the ‎sensors' values change, you'll see the onscreen plot change as well.‎

sensors_accel_1

Going Further

If you're curious about what makes the code tick, or plot, you can ‎check out the original Learn Guide by Kevin Walters. It goes into ‎depth on how everything is working.‎

Original CLUE Sensor Plotter in CircuitPython Learn Guide

Text editor powered by tinymce.

制造商零件编号 4500
CLUE NRF52840 EXPRESS
Adafruit Industries LLC
¥365.90
Details
制造商零件编号 592
CABLE A PLUG TO MCR B PLUG 3'
Adafruit Industries LLC
¥24.01
Details
制造商零件编号 3287
BATTERY HOLDER AA 3 CELL LEADS
Adafruit Industries LLC
¥26.12
Details
制造商零件编号 727
BATTERY HOLDER AAA 3 CELL LEADS
Adafruit Industries LLC
¥17.27
Details
Add all DigiKey Parts to Cart
TechForum

Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.

Visit TechForum