Skip to content

Commit 0317ec3

Browse files
authored
Merge pull request #3230 from firepixie/Ripple_Slippers
Create code.py
2 parents 3184a56 + 92fecf4 commit 0317ec3

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed

Ripple_Slippers/code.py

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# SPDX-FileCopyrightText: 2026 Erin St Blaine for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
import time
6+
import board
7+
import digitalio
8+
import neopixel
9+
10+
# =========================================================
11+
# Ripple Footstep Lights with Color Cycling
12+
# Pylint-friendly version
13+
#
14+
# What this project does:
15+
# - Reads an FSR on pin A0
16+
# - Triggers a ripple of light from the center of the strip
17+
# - Changes color on each new press
18+
# - If pressed again while running:
19+
# - the timer extends
20+
# - the color changes immediately
21+
# - the ripple restarts from the center
22+
# =========================================================
23+
24+
25+
# -----------------------------
26+
# USER SETTINGS
27+
# -----------------------------
28+
NUM_PIXELS = 20
29+
ACTIVE_SECONDS = 3.0
30+
PIXEL_BRIGHTNESS = 0.3
31+
32+
# Lower = faster ripple, higher = slower ripple
33+
RIPPLE_DELAY = 0.05
34+
35+
# Lower = shorter trail, higher = longer trail
36+
TRAIL_FADE = 0.6
37+
38+
# End fade settings
39+
FADE_DELAY = 0.03
40+
FADE_STEPS = 20
41+
42+
# Color cycle: white -> pink -> purple -> blue
43+
COLOR_SEQUENCE = [
44+
(255, 255, 255), # white
45+
(255, 100, 180), # pink
46+
(180, 0, 255), # purple
47+
(0, 120, 255), # blue
48+
]
49+
50+
print("boot")
51+
52+
53+
# -----------------------------
54+
# SENSOR SETUP
55+
# -----------------------------
56+
# FSR wired between A0 and GND.
57+
# With internal pull-up enabled:
58+
# - unpressed = True
59+
# - pressed = False
60+
motion = digitalio.DigitalInOut(board.A0)
61+
motion.direction = digitalio.Direction.INPUT
62+
motion.pull = digitalio.Pull.UP
63+
print("motion ready")
64+
65+
66+
# -----------------------------
67+
# EXTERNAL POWER SETUP
68+
# -----------------------------
69+
# The Prop-Maker Feather needs EXTERNAL_POWER enabled
70+
# to power the external NeoPixel terminal.
71+
external_power = digitalio.DigitalInOut(board.EXTERNAL_POWER)
72+
external_power.direction = digitalio.Direction.OUTPUT
73+
external_power.value = True
74+
print("external power enabled")
75+
76+
77+
# -----------------------------
78+
# NEOPIXEL SETUP
79+
# -----------------------------
80+
pixels = neopixel.NeoPixel(
81+
board.EXTERNAL_NEOPIXELS,
82+
NUM_PIXELS,
83+
brightness=PIXEL_BRIGHTNESS,
84+
auto_write=False
85+
)
86+
print("pixels ready")
87+
88+
89+
# -----------------------------
90+
# HELPER FUNCTIONS
91+
# -----------------------------
92+
def clear():
93+
"""Turn all pixels off."""
94+
pixels.fill((0, 0, 0))
95+
pixels.show()
96+
97+
98+
def dim_rgb(rgb_value, factor):
99+
"""Return a dimmed version of an RGB color tuple."""
100+
return (
101+
int(rgb_value[0] * factor),
102+
int(rgb_value[1] * factor),
103+
int(rgb_value[2] * factor),
104+
)
105+
106+
107+
def get_next_color(sequence_index):
108+
"""
109+
Advance to the next color in the sequence.
110+
111+
Returns:
112+
tuple: (new_index, new_rgb)
113+
"""
114+
new_index = (sequence_index + 1) % len(COLOR_SEQUENCE)
115+
return new_index, COLOR_SEQUENCE[new_index]
116+
117+
118+
def ripple_frame(center_pixel, radius, ripple_rgb):
119+
"""
120+
Draw one frame of the ripple animation.
121+
122+
center_pixel: where the ripple starts
123+
radius: how far the wave has expanded
124+
ripple_rgb: current ripple color
125+
"""
126+
for pixel_index in range(NUM_PIXELS):
127+
distance = abs(pixel_index - center_pixel)
128+
129+
# Bright wave front
130+
if distance == radius:
131+
pixels[pixel_index] = ripple_rgb
132+
133+
# Optional thicker wave front:
134+
# Uncomment these two lines and comment out the line above
135+
# if abs(distance - radius) <= 1:
136+
# pixels[pixel_index] = ripple_rgb
137+
138+
# Fade the trail behind the wave
139+
elif distance < radius:
140+
red, green, blue = pixels[pixel_index]
141+
pixels[pixel_index] = (
142+
int(red * TRAIL_FADE),
143+
int(green * TRAIL_FADE),
144+
int(blue * TRAIL_FADE),
145+
)
146+
147+
# Pixels ahead of the wave stay off
148+
else:
149+
pixels[pixel_index] = (0, 0, 0)
150+
151+
pixels.show()
152+
153+
154+
def ripple_for(seconds, start_rgb, starting_index):
155+
"""
156+
Run the ripple animation for a set amount of time.
157+
158+
If the sensor is pressed again while the animation is running:
159+
- extend the timer
160+
- change to the next color
161+
- restart the ripple from the center
162+
163+
Returns:
164+
int: updated color sequence index
165+
"""
166+
center_pixel = NUM_PIXELS // 2
167+
active_rgb = start_rgb
168+
sequence_index = starting_index
169+
end_time = time.monotonic() + seconds
170+
was_pressed = False
171+
radius = 0
172+
173+
while time.monotonic() < end_time:
174+
is_pressed = not motion.value
175+
176+
# Detect a new press during the active animation
177+
if is_pressed and not was_pressed:
178+
sequence_index, active_rgb = get_next_color(sequence_index)
179+
print("extended, new color:", active_rgb)
180+
181+
end_time = time.monotonic() + seconds
182+
radius = 0
183+
184+
ripple_frame(center_pixel, radius, active_rgb)
185+
186+
radius += 1
187+
if radius > NUM_PIXELS:
188+
radius = 0
189+
190+
time.sleep(RIPPLE_DELAY)
191+
was_pressed = is_pressed
192+
193+
return sequence_index
194+
195+
196+
def fade_out():
197+
"""Fade the current pixels smoothly to black."""
198+
current_pixels = [pixels[pixel_index] for pixel_index in range(NUM_PIXELS)]
199+
200+
for step in range(FADE_STEPS, -1, -1):
201+
factor = step / FADE_STEPS
202+
203+
for pixel_index in range(NUM_PIXELS):
204+
pixels[pixel_index] = dim_rgb(current_pixels[pixel_index], factor)
205+
206+
pixels.show()
207+
time.sleep(FADE_DELAY)
208+
209+
clear()
210+
211+
212+
# -----------------------------
213+
# STARTUP FLASH
214+
# -----------------------------
215+
startup_colors = [
216+
(255, 0, 0),
217+
(0, 255, 0),
218+
(0, 0, 255),
219+
]
220+
221+
for startup_rgb in startup_colors:
222+
pixels.fill(startup_rgb)
223+
pixels.show()
224+
time.sleep(0.2)
225+
226+
clear()
227+
print("starting loop")
228+
229+
230+
# -----------------------------
231+
# MAIN LOOP
232+
# -----------------------------
233+
last_state = motion.value
234+
current_sequence_index = -1
235+
236+
while True:
237+
current_state = motion.value
238+
239+
if current_state != last_state:
240+
print("changed:", current_state)
241+
242+
# Trigger on press: True -> False
243+
if not current_state:
244+
print("TRIGGERED")
245+
246+
current_sequence_index, trigger_rgb = get_next_color(
247+
current_sequence_index
248+
)
249+
print("color:", trigger_rgb)
250+
251+
current_sequence_index = ripple_for(
252+
ACTIVE_SECONDS,
253+
trigger_rgb,
254+
current_sequence_index
255+
)
256+
257+
fade_out()
258+
259+
last_state = current_state
260+
261+
time.sleep(0.01)

0 commit comments

Comments
 (0)