Skip to content

Commit 3093148

Browse files
Add CircuitPython_Goggles code
1 parent c04d2c7 commit 3093148

File tree

2 files changed

+375
-0
lines changed

2 files changed

+375
-0
lines changed

CircuitPython_Goggles/code.py

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
# NeoPixel goggles code for CircuitPython
2+
#
3+
# With a rotary encoder attached (pins are declred in the "Initialize
4+
# hardware" section of the code), you can select animation modes and
5+
# configurable attributes (color, brightness, etc.). TAP the encoder
6+
# button to switch between modes/settings, HOLD the encoder button to
7+
# toggle between PLAY and CONFIGURE states.
8+
#
9+
# With no rotary encoder attached, you can select an animation mode
10+
# and configure attributes in the "Configurable defaults" section
11+
# (including an option to auto-cycle through the animation modes).
12+
#
13+
# Things to Know:
14+
# - FancyLED library is NOT used here because it's a bit too much for the
15+
# Trinket M0 to handle (animation was very slow).
16+
# - Animation modes are all monochromatic (single color, varying only in
17+
# brightness). More a design decision than a technical one...of course
18+
# NeoPixels can be individual colors, but folks like customizing and the
19+
# monochromatic approach makes it easier to select a color. Also keeps the
20+
# code a bit simpler, since Trinket space & performance is limited.
21+
# - Animation is monotonic time driven; there are no sleep() calls. This
22+
# ensures that animation is constant-time regardless of the hardware or
23+
# CircuitPython performance over time, or other goings on (e.g. garbage
24+
# collection), only the frame rate (smoothness) varies; overall timing
25+
# remains consistent.
26+
27+
import adafruit_dotstar
28+
import board
29+
import neopixel
30+
from digitalio import DigitalInOut, Direction
31+
from math import modf, pi, pow, sin
32+
from random import getrandbits
33+
from richbutton import RichButton
34+
from rotaryio import IncrementalEncoder
35+
from time import monotonic
36+
37+
# Configurable defaults
38+
39+
pixel_hue = 0.0 # Red at start
40+
pixel_brightness = 0.4 # 40% brightness at start
41+
pixel_gamma = 2.6 # Controls brightness linearity
42+
ring_1_offset = 10 # Alignment of top pixel on 1st NeoPixel ring
43+
ring_2_offset = 10 # Alignment of top pixel on 2nd NeoPixel ring
44+
ring_2_flip = True # If True, reverse order of pixels on 2nd ring
45+
cycle_interval = 0 # If >0 auto-cycle through play modes @ this interval
46+
speed = 1.0 # Initial animation speed for modes that use it
47+
xray_bits = 0x0821 # Initial bit pattern for "X-ray" mode
48+
49+
# Things you probably don't want to change, unless adding new modes
50+
51+
PLAY_MODE_SPIN = 0 # Revolving pulse
52+
PLAY_MODE_XRAY = 1 # Watchmen-inspired "X-ray goggles"
53+
PLAY_MODE_SCAN = 2 # Scanline effect
54+
PLAY_MODE_SPARKLE = 3 # Random dots
55+
PLAY_MODES = 4 # Number of PLAY modes
56+
play_mode = PLAY_MODE_SPIN # Initial PLAY mode
57+
58+
CONFIG_MODE_COLOR = 0 # Setting color (hue)
59+
CONFIG_MODE_BRIGHTNESS = 1 # Setting brightness
60+
CONFIG_MODE_ALIGN = 2 # Top pixel indicator
61+
CONFIG_MODES = 3 # Number of CONFIG modes
62+
config_mode = CONFIG_MODE_COLOR # Initial CONFIG mode
63+
configuring = False # NOT configuring at start
64+
# CONFIG_MODE_ALIGN is only used to test the values of ring_1_offset and
65+
# ring_2_offset. The single lit pixel should appear at the top of each ring.
66+
# If it does not, adjust each of those two values (integer from 0 to 15)
67+
# until the pixel appears at the top (or physically reposition the rings).
68+
# Some of the animation modes rely on the two rings being aligned a certain
69+
# way. Once adjusted, you can reduce the value of CONFIG_MODES and this
70+
# mode will be skipped in config state.
71+
72+
# Initialize hardware - PIN DEFINITIONS APPEAR HERE
73+
74+
# Turn off onboard DotStar LED
75+
dotstar = adafruit_dotstar.DotStar(board.APA102_SCK, board.APA102_MOSI, 1)
76+
dotstar.brightness = 0
77+
78+
# Turn off onboard discrete LED
79+
led = DigitalInOut(board.D13)
80+
led.direction = Direction.OUTPUT
81+
led.value = 0
82+
83+
# Declare NeoPixels on pin D0, 32 pixels long. Set to max brightness because
84+
# on-the-fly brightness slows down NeoPixel lib, so we'll do our own here.
85+
pixels = neopixel.NeoPixel(board.D0, 32, brightness=1.0, auto_write=False)
86+
87+
# Declare rotary encoder on pins D4 and D3, and click button on pin D2.
88+
# If encoder behaves backwards from what you want, swap pins here.
89+
encoder = IncrementalEncoder(board.D4, board.D3)
90+
encoder_button = RichButton(board.D2)
91+
92+
93+
# Set one pixel in both 16-pixel rings. Pass in pixel index (0 to 15) and
94+
# relative brightness (0.0 to 1.0). Actual resulting brightness will be a
95+
# function of global brightness and gamma correction.
96+
def setPixel(pixel_num, brightness):
97+
# Clamp passed brightness to 0.0-1.0 range,
98+
# apply global brightness and gamma correction
99+
brightness = max(min(brightness, 1.0), 0.0) * pixel_brightness
100+
brightness = pow(brightness, pixel_gamma) * 255.0
101+
# local_color is adjusted brightness applied to global pixel_color
102+
local_color = (
103+
int(pixel_color[0] * brightness + 0.5),
104+
int(pixel_color[1] * brightness + 0.5),
105+
int(pixel_color[2] * brightness + 0.5))
106+
# Roll over pixel_num as needed to 0-15 range, then store color
107+
p = (pixel_num + ring_1_offset) & 15
108+
pixels[p] = local_color
109+
# Determine corresponding pixel for second ring. Mirror direction if
110+
# configured for such, correct for any rotational difference, then
111+
# perform similar roll-over as above before storing color.
112+
if ring_2_flip:
113+
pixel_num = 15 - pixel_num
114+
p = 16 + ((pixel_num + ring_2_offset) & 15)
115+
pixels[p] = local_color
116+
117+
118+
# Return a brightness level (0.0 to 1.0) corresponding to a position (0.0
119+
# to 1.0) within a triangle wave (spanning 0.0 to 1.0) with wave's peak
120+
# brightness at a given position (0.0 to 1.0) within its span. Positions
121+
# outside the wave's span return 0.0. Basically, this: _/\_
122+
def triangle_wave(x, peak = 0.5):
123+
if 0.0 <= x < 1.0:
124+
if x <= peak:
125+
return x / peak
126+
else:
127+
return (1.0 - x) / (1.0 - peak)
128+
else:
129+
return 0.0
130+
131+
132+
# Given a hue value as a float, where the fractional portion (0.0 to 1.0)
133+
# indicates the actual hue (starting from red at 0, to green at 1/3, to
134+
# blue at 2/3, and back to red at 1.0), return an RGB color as a 3-tuple
135+
# with values from 0.0 to 1.0.
136+
def hue_to_rgb(hue):
137+
hue = modf(hue)[0]
138+
n = (hue * 6.0) % 6.0
139+
f = modf(n)[0]
140+
if n < 1.0:
141+
return (1.0, f, 0.0)
142+
elif n < 2.0:
143+
return (1.0 - f, 1.0, 0.0)
144+
elif n < 3.0:
145+
return (0.0, 1.0, f)
146+
elif n < 4.0:
147+
return (0.0, 1.0 - f, 1.0)
148+
elif n < 5.0:
149+
return (f, 0.0, 1.0)
150+
else:
151+
return (1.0, 0.0, 1.0 - f)
152+
153+
154+
# Generate random bit pattern that avoids adjacent set bits (wraps around)
155+
def random_bits():
156+
pattern = getrandbits(16)
157+
pattern |= (pattern & 1) << 16 # Replicate bit 0 at bit 16
158+
return pattern & ~(pattern >> 1) # Mask out adjacent set bits
159+
160+
161+
# Some last-minute state initialization
162+
163+
pos = 0 # Initial swirl animation position
164+
pixel_color = hue_to_rgb(pixel_hue) # Initial color
165+
encoder_prior = encoder.position # Initial encoder position
166+
time_prior = monotonic() # Initial time
167+
last_cycle_time = time_prior # For mode auto-cycling
168+
sparkle_bits_prev = 0 # First bits for sparkle animation
169+
sparkle_bits_next = 0 # Next bits for sparkle animation
170+
prev_weight = 2 # Force initial sparkle refresh
171+
172+
173+
# Main loop
174+
175+
while True:
176+
action = encoder_button.action()
177+
if action is RichButton.TAP:
178+
# Encoder button tapped, cycle through play or config modes:
179+
if configuring:
180+
config_mode = (config_mode + 1) % CONFIG_MODES
181+
else:
182+
play_mode = (play_mode + 1) % PLAY_MODES
183+
elif action is RichButton.DOUBLE_TAP:
184+
# DOUBLE_TAP not currently used, but this is where it would go.
185+
pass
186+
elif action is RichButton.HOLD:
187+
# Encoder button held, toggle between PLAY and CONFIG modes:
188+
configuring = not configuring
189+
elif action is RichButton.RELEASE:
190+
# RELEASE not currently used (play/config state changes when HOLD
191+
# is detected), but this is where it would go.
192+
pass
193+
194+
# Process encoder input. Code always uses the encoder_change value
195+
# for relative adjustments.
196+
encoder_position = encoder.position
197+
encoder_change = encoder.position - encoder_prior
198+
encoder_prior = encoder_position
199+
200+
# Same idea, but for elapsed time (so time-based animation continues
201+
# at the next position, it doesn't jump around as when multiplying
202+
# monotonic() by speed.
203+
time_now = monotonic()
204+
time_change = time_now - time_prior
205+
time_prior = time_now
206+
207+
if configuring:
208+
# In config mode, different pixel patterns indicate which
209+
# adjustment is being made (e.g. alternating pixels = hue mode).
210+
if config_mode is CONFIG_MODE_COLOR:
211+
pixel_hue = modf(pixel_hue + encoder_change * 0.01)[0]
212+
pixel_color = hue_to_rgb(pixel_hue)
213+
for i in range(0, 16):
214+
setPixel(i, i & 1) # Turn on alternating pixels
215+
elif config_mode is CONFIG_MODE_BRIGHTNESS:
216+
pixel_brightness += encoder_change * 0.025
217+
pixel_brightness = max(min(pixel_brightness, 1.0), 0.0)
218+
for i in range(0, 16):
219+
setPixel(i, (i & 2) >> 1) # Turn on pixel pairs
220+
elif config_mode is CONFIG_MODE_ALIGN:
221+
c = 1 # First pixel on
222+
for i in range(0, 16):
223+
setPixel(i, c)
224+
c = 0 # All other pixels off
225+
else:
226+
# In play mode. Auto-cycle animations if cycle_interval is set.
227+
if cycle_interval > 0:
228+
if time_now - last_cycle_time > cycle_interval:
229+
play_mode = (play_mode + 1) % PLAY_MODES
230+
last_cycle_time = time_now
231+
232+
if play_mode is PLAY_MODE_XRAY:
233+
# In XRAY mode, encoder selects random bit patterns
234+
if abs(encoder_change) > 1:
235+
xray_bits = random_bits()
236+
# Unset bits pulsate ever-so-slightly
237+
dim = 0.42 + sin(monotonic() * 2) * 0.08
238+
for i in range(16):
239+
if xray_bits & (1 << i):
240+
setPixel(i, 1.0)
241+
else:
242+
setPixel(i, dim)
243+
else:
244+
# In all other modes, encoder adjusts speed/direction
245+
speed += encoder_change * 0.05
246+
speed = max(min(speed, 4.0), -4.0)
247+
pos += time_change * speed
248+
if play_mode is PLAY_MODE_SPIN:
249+
for i in range(16):
250+
frac = modf(pos + i / 15.0)[0] # 0.0-1.0 around ring
251+
if frac < 0:
252+
frac = 1.0 + frac
253+
setPixel(i, triangle_wave(frac, 0.5 - speed * 0.125))
254+
elif play_mode is PLAY_MODE_SCAN:
255+
if pos >= 0:
256+
s = 2.0 - modf(pos)[0] * 4.0
257+
else:
258+
s = 2.0 - (1.0 + modf(pos)[0]) * 4.0
259+
for i in range(16):
260+
y = sin((i / 7.5 + 0.5) * pi) # Pixel Y coord
261+
d = 0.5 - abs(y - s) * 0.6 # Distance to scanline
262+
setPixel(i, triangle_wave(d))
263+
elif play_mode is PLAY_MODE_SPARKLE:
264+
next_weight = modf(abs(pos * 2.0))[0]
265+
if speed < 0:
266+
next_weight = 1.0 - next_weight
267+
if next_weight < prev_weight:
268+
sparkle_bits_prev = sparkle_bits_next
269+
while True:
270+
sparkle_bits_next = random_bits()
271+
if not sparkle_bits_next & sparkle_bits_prev:
272+
break # No bits in common, good!
273+
prev_weight = 1.0 - next_weight
274+
for i in range(16):
275+
bit = 1 << i
276+
if sparkle_bits_prev & bit:
277+
result = prev_weight
278+
elif sparkle_bits_next & bit:
279+
result = next_weight
280+
else:
281+
result = 0
282+
setPixel(i, result)
283+
prev_weight = next_weight
284+
pixels.show()

CircuitPython_Goggles/richbutton.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from digitalio import DigitalInOut, Direction, Pull
2+
from time import monotonic
3+
4+
class RichButton(object):
5+
"""
6+
A button class handling more than basic taps: adds debounced tap,
7+
double-tap, hold and release.
8+
"""
9+
10+
TAP = 0
11+
DOUBLE_TAP = 1
12+
HOLD = 2
13+
RELEASE = 3
14+
15+
def __init__(self, pin, *, debounce_period = 0.05, hold_period = 0.75,
16+
double_tap_period = 0.3):
17+
"""
18+
Constructor for RichButton class.
19+
20+
Arguments:
21+
pin (int) : Digital pin connected to button
22+
(opposite leg to GND). Pin will be
23+
configured as INPUT with pullup.
24+
Keyword arguments:
25+
debounce_period (float) : interval, in seconds, in which multiple
26+
presses are ignored (debounced)
27+
(default = 0.05 seconds).
28+
hold_period (float) : interval, in seconds, when a held
29+
button will return a HOLD value from
30+
the action() function (default = 0.75).
31+
double_tap_period (float): interval, in seconds, when a double-
32+
tap can be sensed (vs returning
33+
a second single-tap) (default = 0.3).
34+
Longer double-tap periods will make
35+
single-taps less responsive.
36+
"""
37+
self.io = DigitalInOut(pin)
38+
self.io.direction = Direction.INPUT
39+
self.io.pull = Pull.UP
40+
self._debounce_period = debounce_period
41+
self._hold_period = hold_period
42+
self._double_tap_period = double_tap_period
43+
self._holding = False
44+
self._tap_time = -self._double_tap_period
45+
self._press_time = monotonic()
46+
self._prior_state = self.io.value
47+
48+
def action(self):
49+
"""
50+
Process pin input. This MUST be called frequently for debounce, etc.
51+
to work, since interrupts are not available.
52+
Returns:
53+
None, TAP, DOUBLE_TAP, HOLD or RELEASE.
54+
"""
55+
new_state = self.io.value
56+
if new_state != self._prior_state:
57+
# Button state changed since last call
58+
self._prior_state = new_state
59+
if not new_state:
60+
# Button initially pressed (TAP not returned until debounce)
61+
self._press_time = monotonic()
62+
else:
63+
# Button initially released
64+
if self._holding:
65+
# Button released after hold
66+
self._holding = False
67+
return self.RELEASE
68+
elif (monotonic() - self._press_time) >= self._debounce_period:
69+
# Button released after valid debounce time
70+
if monotonic() - self._tap_time < self._double_tap_period:
71+
# Followed another recent tap, reset double timer
72+
self._tap_time = 0
73+
return self.DOUBLE_TAP
74+
else:
75+
# Regular debounced release, maybe 1st tap, keep time
76+
self._tap_time = monotonic()
77+
else:
78+
# Button is in same state as last call
79+
if self._prior_state:
80+
# Is not pressed
81+
if (self._tap_time > 0 and
82+
(monotonic() - self._tap_time) > self._double_tap_period):
83+
# Enough time since last tap that it's not a double
84+
self._tap_time = 0
85+
return self.TAP
86+
elif (not self._holding and
87+
(monotonic() - self._press_time) >= self._hold_period):
88+
# Is pressed, and has been for the holding period
89+
self._holding = True
90+
return self.HOLD
91+
return None

0 commit comments

Comments
 (0)