Skip to content

Commit 00a891b

Browse files
authored
Add advanced fidget spinner example for Circuit Playground Express boards.
1 parent 137b765 commit 00a891b

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed

examples/spinner_advanced.py

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# Circuit Playground Express CircuitPython Advanced Fidget Spinner
2+
#
3+
# This is a more advanced version of the fidget spinner which lets you change
4+
# color and animation type by pressing either the A or B buttons. NOTE: you
5+
# cannot run this example with a tool like ampy and MUST copy it to the board
6+
# and name it main.py to run at boot.
7+
#
8+
# This is meant to work with the Circuit Playground Express board:
9+
# https://www.adafruit.com/product/3333
10+
# Needs this LIS3DH module and the NeoPixel module installed:
11+
# https://github.com/adafruit/Adafruit_CircuitPython_LIS3DH
12+
# https://github.com/adafruit/Adafruit_CircuitPython_NeoPixel
13+
# Author: Tony DiCola
14+
# License: MIT License (https://opensource.org/licenses/MIT)
15+
import math
16+
import time
17+
import ustruct
18+
19+
import board
20+
import busio
21+
import digitalio
22+
23+
import adafruit_lis3dh
24+
import neopixel
25+
26+
27+
# Configuration:
28+
ACCEL_RANGE = adafruit_lis3dh.RANGE_16_G # Accelerometer range.
29+
TAP_THRESHOLD = 20 # Accelerometer tap threshold. Higher values
30+
# mean you need to tap harder to start a spin.
31+
SPINNER_DECAY = 0.5 # Decay rate for the spinner. Set to a value
32+
# from 0 to 1.0 where lower values mean the
33+
# spinner slows down faster.
34+
# Define list of color combinations. Pressing button A will cycle through
35+
# these combos. Each tuple entry (line) should be a 2-tuple of 3-tuple RGB
36+
# values (0-255).
37+
COLORS = (
38+
((255, 0, 0), (0, 0, 0)), # Red to black
39+
((0, 255, 0), (0, 0, 0)), # Green to black
40+
((0, 0, 255), (0, 0, 0)), # Blue to black
41+
((255, 0, 0), (0, 255, 0)), # Red to green
42+
((255, 0, 0), (0, 0, 255)), # Red to blue
43+
((0, 255, 0), (0, 0, 255)) # Green to blue
44+
)
45+
46+
47+
# Helper functions:
48+
def lerp(x, x0, x1, y0, y1):
49+
"""Linearly interpolate a value y given range y0...y1 that is proportional
50+
to x in range x0...x1 .
51+
"""
52+
return y0 + (x-x0)*((y1-y0)/(x1-x0))
53+
54+
def color_lerp(x, x0, x1, c0, c1):
55+
"""Linearly interpolate RGB colors (3-tuples of byte values) given x
56+
in range x0...x1.
57+
"""
58+
r0, g0, b0 = c0
59+
r1, g1, b1 = c1
60+
return (int(lerp(x, x0, x1, r0, r1)),
61+
int(lerp(x, x0, x1, g0, g1)),
62+
int(lerp(x, x0, x1, b0, b1)))
63+
64+
65+
# Define a class that represents the fidget spinner. The spinner only has a
66+
# concept of its current position, a continuous value from 0 to <10. You can
67+
# start spinning the spinner with an initial velocity by calling the spin
68+
# function, then periodically call get_position to get the current spinner
69+
# position. Since the position moves between values 0 to 10 it can easily map
70+
# to pixel positions around the Circuit Playground Express board.
71+
class FidgetSpinner:
72+
73+
def __init__(self, decay=0.5):
74+
"""Create an instance of the fidget spinner. Specify the decay rate
75+
as a value from 0 to 1 (continuous, floating point)--lower decay rate
76+
values will cause the spinner to slow down faster.
77+
"""
78+
self._decay = decay
79+
self._velocity = 0.0
80+
self._elapsed = 0.0
81+
self._position = 0.0
82+
83+
def spin(self, velocity):
84+
"""Start the spinner moving at the specified initial velocity (in
85+
positions/second).
86+
"""
87+
self._velocity = velocity
88+
self._elapsed = 0.0
89+
90+
def get_position(self, delta):
91+
"""Update the spinner position after the specified delta (in seconds)
92+
has elapsed. Will return the new spinner position, a continuous value
93+
from 0...<10.
94+
"""
95+
# Increment elapsed time and compute the current velocity after a
96+
# decay of the initial velocity.
97+
self._elapsed += delta
98+
current_velocity = self._velocity*math.pow(self._decay, self._elapsed)
99+
# Update position based on the current_velocity and elapsed time.
100+
self._position += current_velocity*delta
101+
# Make sure the position stays within values that range from 0 to <10.
102+
self._position = math.fmod(self._position, 10.0)
103+
if self._position < 0.0:
104+
self._position += 10.0
105+
return self._position
106+
107+
108+
# Define animation classes. Each animation needs to have an update function
109+
# which takes in the current spinner position and a selected primary and
110+
# secondary color (3-tuple of RGB bytes) and will render a frame of spinner
111+
# animation.
112+
class DiscreteDotAnimation:
113+
114+
def __init__(self, pixels, dots=2):
115+
"""Create an instance of a simple discrete dot animation. The dots
116+
parameter controls how many dots are rendered on the display (each
117+
evenly spaced apart).
118+
"""
119+
self._pixels = pixels
120+
self._dots = dots
121+
self._dot_offset = pixels.n / self._dots
122+
123+
def update(self, position, primary, secondary):
124+
"""Update the animation given the current spinner position and
125+
selected primary and secondary colors.
126+
"""
127+
# Clear all the pixels to secondary colors, then draw a number of
128+
# dots evenly spaced around the pixels and starting at the provided
129+
# position.
130+
self._pixels.fill(secondary)
131+
for i in range(self._dots):
132+
pos = int(position + i*self._dot_offset) % self._pixels.n
133+
self._pixels[pos] = primary
134+
self._pixels.write()
135+
136+
class SmoothAnimation:
137+
138+
def __init__(self, pixels, frequency=2.0):
139+
"""Create an instance of a smooth sine-wave based animation that sweeps
140+
around the board based on spinner position. Frequency specifies how
141+
many primary to secondary color bumps are shown around the board.
142+
"""
143+
self._pixels = pixels
144+
# Precompute some of the sine wave math factors so they aren't
145+
# recomputed in every loop iteration.
146+
self._sin_scale = 2.0*math.pi*frequency/pixels.n
147+
self._phase_scale = 2.0*math.pi/10.0
148+
149+
def update(self, position, primary, secondary):
150+
"""Update the animation given the current spinner position and
151+
selected primary and secondary colors.
152+
"""
153+
# Draw a smooth sine wave of primary and secondary color moving around
154+
# the pixels. Each pixel color is computed based on interpolating
155+
# color based on its position around the board, and a phase offset that
156+
# changes based on fidget spinner position.
157+
phase = self._phase_scale*position
158+
for i in range(self._pixels.n):
159+
x = math.sin(self._sin_scale*i - phase)
160+
self._pixels[i] = color_lerp(x, -1.0, 1.0, primary, secondary)
161+
self._pixels.write()
162+
163+
164+
# Initialize and turn off NeoPixels.
165+
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10)
166+
pixels.fill((0,0,0))
167+
pixels.write()
168+
169+
# Initialize buttons.
170+
button_a = digitalio.DigitalInOut(board.BUTTON_A)
171+
button_a.switch_to_input(pull=digitalio.DigitalInOut.Pull.DOWN)
172+
button_b = digitalio.DigitalInOut(board.BUTTON_B)
173+
button_b.switch_to_input(pull=digitalio.DigitalInOut.Pull.DOWN)
174+
175+
# Initialize the LIS3DH accelerometer.
176+
# Note that this is specific to Circuit Playground Express boards. For other
177+
# uses change the SCL and SDA pins below, and optionally the address of the
178+
# device if needed.
179+
i2c = busio.I2C(board.ACCELEROMETER_SCL, board.ACCELEROMETER_SDA)
180+
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c, address=25)
181+
182+
# Set accelerometer range.
183+
lis3dh.range = ACCEL_RANGE
184+
# Enable single click detection, but use a custom CLICK_CFG register value
185+
# to only detect clicks on the X axis (instead of all 3 X, Y, Z axes).
186+
lis3dh.set_click(1, TAP_THRESHOLD, click_cfg=0x01)
187+
# Enable LIS3DH FIFO in stream mode. This reaches in to the LIS3DH library to
188+
# call internal methods that change a few register values. This must be done
189+
# AFTER calling set_click above because the set_click function also changes
190+
# REG_CTRL5. The FIFO stream mode will keep track of the 32 last X,Y,Z accel
191+
# readings in a FIFO buffer so they can be read later to see a history of
192+
# recent acceleration. This is handy to look for the maximum/minimum impulse
193+
# after a click is detected.
194+
lis3dh._write_register_byte(adafruit_lis3dh.REG_CTRL5, 0b01001000)
195+
lis3dh._write_register_byte(0x2E, 0b10000000) # Set FIFO_CTRL to Stream mode.
196+
197+
# Create a fidget spinner object.
198+
spinner = FidgetSpinner(SPINNER_DECAY)
199+
200+
# Other global state for the spinner animation:
201+
last = time.monotonic() # Keep track of the last time the loop ran.
202+
color_index = 0 # Keep track of the currently selected color combo.
203+
animations = (DiscreteDotAnimation(pixels, 1), # Define list of animations.
204+
DiscreteDotAnimation(pixels, 2), # Button B presses cycle
205+
SmoothAnimation(pixels, 1), # through these animations.
206+
SmoothAnimation(pixels, 2))
207+
animation_index = 0 # Keep track of currently selected animation.
208+
209+
# Main loop will run forever checking for click/taps from accelerometer and
210+
# then spinning the spinner.
211+
while True:
212+
# Check for button press at the top and bottom of the loop so some time
213+
# elapses and the change in button state from pressed to released can be
214+
# detected.
215+
initial_a = button_a.value
216+
initial_b = button_b.value
217+
# Read the raw click detection register value and check if there was
218+
# a click detected. Remember only the X axis causes clicks because of
219+
# the register configuration set previously.
220+
clicksrc = lis3dh.read_click_raw()
221+
if clicksrc & 0b01000000 > 0:
222+
# Click was detected! Quickly read 32 values from the accelerometer
223+
# and look for the maximum magnitude values. Because the
224+
# accelerometer is in FIFO stream mode it will keep a history of the
225+
# 32 last accelerometer readings and return them when consecutively
226+
# read.
227+
maxval = lis3dh.acceleration[0] # Grab just the X acceleration value.
228+
for i in range(31):
229+
x = abs(lis3dh.acceleration[0])
230+
if x > maxval:
231+
maxval = x
232+
# Check if this was a positive or negative spin/click event.
233+
if clicksrc == 0b1010001:
234+
# Positive click, spin in a positive direction.
235+
spinner.spin(maxval)
236+
elif clicksrc == 0b1011001:
237+
# Negative click, spin in negative direction.
238+
spinner.spin(-maxval)
239+
# Update the amount of time that's passed since the last loop iteration.
240+
current = time.monotonic()
241+
delta = current - last
242+
last = current
243+
# Update fidget spinner position.
244+
position = spinner.get_position(delta)
245+
# Grab the currently selected primary and secondary colors.
246+
primary = COLORS[color_index][0]
247+
secondary = COLORS[color_index][1]
248+
# Draw the current animation on the pixels.
249+
animations[animation_index].update(position, primary, secondary)
250+
# Small delay to stay responsive but give time for interrupt processing.
251+
time.sleep(0.01)
252+
# Check button state again and compare to initial state to see if there
253+
# was a change (i.e. button was released).
254+
if not button_a.value and initial_a:
255+
# Button a released, i.e. it was true (high) and now is false (low).
256+
# Increment color and wrap back to zero when beyond total # of colors.
257+
color_index = (color_index + 1) % len(COLORS)
258+
if not button_b.value and initial_b:
259+
# Button b released, i.e. it was true (high) and now is false (low).
260+
# Increment animation (wrapping back around to zero as necessary).
261+
animation_index = (animation_index + 1) % len(animations)

0 commit comments

Comments
 (0)