seveibar/led-water-accelerometer
This is an LED matrix with an accelerometer and a PICO. When you tilt the PCB, the LEDs dim and change color to simulate water flowing.
- Version
- 1.0.18
- License
- unset
- Stars
- 7
firmware/animation_switcher.py
from machine import Pin, Timer, SPI
import neopixel
import time
import math
import struct
import random
import rp2
import micropython
micropython.alloc_emergency_exception_buf(100)
# ───── LED Setup ─────
LED_PIN = 6
COLS, ROWS = 8, 6
NUM_LEDS = COLS * ROWS
np = neopixel.NeoPixel(Pin(LED_PIN), NUM_LEDS)
# Initialize onboard LED for feedback
onboard_led = Pin('LED', Pin.OUT)
# Global variables for BOOTSEL button handling
last_button_state = 0
current_mode = 0
switch_requested = False
total_presses = 0
# Accelerometer globals
spi, cs, _w, _r = None, None, None, None
accelerometer_ok = False
LSB_G = 0.00098
G_CLAMP = 15
def check_bootsel_button(timer):
"""Timer callback to check BOOTSEL button state"""
global last_button_state, current_mode, switch_requested, total_presses
current_state = rp2.bootsel_button()
if current_state == 1 and last_button_state == 0:
total_presses += 1
current_mode = (current_mode + 1) % 3 # Switch between 0, 1, and 2
switch_requested = True
print(f"🎬 BOOTSEL pressed! Switching to mode {current_mode} (Press #{total_presses})")
onboard_led.on()
# show_switch_feedback()
elif current_state == 0 and last_button_state == 1:
print("🔄 BOOTSEL button released")
onboard_led.off()
last_button_state = current_state
def show_switch_feedback():
"""Visual feedback when switching modes"""
for _ in range(2):
for i in range(NUM_LEDS):
np[i] = (10, 10, 10)
np.write()
time.sleep(0.1)
for i in range(NUM_LEDS):
np[i] = (0, 0, 0)
np.write()
time.sleep(0.1)
def init_accelerometer():
"""Initialize accelerometer and set global variables."""
global spi, cs, _w, _r, accelerometer_ok
SCK_PIN, MOSI_PIN, MISO_PIN, CS_PIN = 10, 11, 12, 17
try:
spi = SPI(1, 1_000_000, polarity=1, phase=1,
sck=Pin(SCK_PIN), mosi=Pin(MOSI_PIN), miso=Pin(MISO_PIN))
cs = Pin(CS_PIN, Pin.OUT, value=1)
def write_reg(a, v):
cs(0); spi.write(bytearray([a & 0x3F, v & 0xFF])); cs(1)
def read_reg(a, n=1):
cmd = a | 0x80 | (0x40 if n > 1 else 0)
cs(0); spi.write(bytearray([cmd])); d = spi.read(n); cs(1); return d
_w = write_reg
_r = read_reg
if _r(0x0F)[0] != 0x33:
raise RuntimeError("No LIS3DHTR")
_w(0x20, 0x57)
_w(0x23, 0x08)
accelerometer_ok = True
print("✅ Accelerometer initialized successfully")
except Exception as e:
print(f"⚠️ Accelerometer not found: {e}")
accelerometer_ok = False
# ═══════════════════════════════════════════════════════════════════════════════
# ANIMATION 0: SCROLLING TEXT
# ═══════════════════════════════════════════════════════════════════════════════
class NeoPixelMatrix:
def __init__(self, neopixel_strip, cols, rows):
self.np = neopixel_strip
self.cols = cols
self.rows = rows
self.clear_matrix()
def xy_to_index(self, x, y):
return y * self.cols + x
def clear_matrix(self):
for i in range(NUM_LEDS):
self.np[i] = (0, 0, 0)
self.np.write()
def set_pixel(self, x, y, color):
if 0 <= x < self.cols and 0 <= y < self.rows:
idx = self.xy_to_index(x, y)
self.np[idx] = color
LETTER_PATTERNS = {
'T': [[1,1,1,1,1],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0]],
'S': [[0,1,1,1,1],[1,0,0,0,0],[0,1,1,1,0],[0,0,0,0,1],[0,0,0,0,1],[1,1,1,1,0]],
'C': [[0,1,1,1,1],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[1,0,0,0,0],[0,1,1,1,1]],
'I': [[1,1,1,1,1],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[0,0,1,0,0],[1,1,1,1,1]],
'R': [[1,1,1,1,0],[1,0,0,0,1],[1,1,1,1,0],[1,0,1,0,0],[1,0,0,1,0],[1,0,0,0,1]],
'U': [[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[1,0,0,0,1],[0,1,1,1,0]]
}
def draw_letter_to_buffer(buffer, letter, start_x, start_y, color):
if letter not in LETTER_PATTERNS: return
pattern = LETTER_PATTERNS[letter]
for row_idx, row_pattern in enumerate(pattern):
for col_idx, pixel in enumerate(row_pattern):
if pixel == 1:
screen_x, screen_y = start_x + col_idx, start_y + row_idx
if 0 <= screen_x < COLS and 0 <= screen_y < ROWS:
buffer[screen_y][screen_x] = color
def update_matrix_from_buffer(matrix, buffer):
for y in range(ROWS):
for x in range(COLS):
matrix.set_pixel(x, y, buffer[y][x] if buffer[y][x] != 0 else (0,0,0))
matrix.np.write()
def scroll_text_continuous_animation():
global switch_requested
matrix = NeoPixelMatrix(np, COLS, ROWS)
text = "TSCIRCUIT"
color, speed, letter_spacing = (0, 0, 50), 0.05, 1
print("🎬 Running Mode 0: Scrolling Text")
while not switch_requested:
letter_width = 5
total_width = len(text) * (letter_width + letter_spacing) - letter_spacing
start_x, end_x = COLS, -total_width
frame_buffer = [[0 for _ in range(COLS)] for _ in range(ROWS)]
position = float(start_x)
while position > end_x and not switch_requested:
for y in range(ROWS):
for x in range(COLS):
frame_buffer[y][x] = 0
current_x = position
for char in text:
if char in LETTER_PATTERNS:
draw_letter_to_buffer(frame_buffer, char, int(current_x), 0, color)
current_x += letter_width + letter_spacing
update_matrix_from_buffer(matrix, frame_buffer)
position -= 1.0
time.sleep(speed)
if not switch_requested:
time.sleep(0.2)
# ═══════════════════════════════════════════════════════════════════════════════
# ANIMATION 1: PARTICLE PHYSICS (Water)
# ═══════════════════════════════════════════════════════════════════════════════
def particle_physics_animation():
global switch_requested, accelerometer_ok, _r, LSB_G, G_CLAMP
print("🎬 Running Mode 1: Particle Physics (Water)")
MAX_LEVEL = 50
COLOR_CH = (0, 0, 1)
PARTICLE_COUNT = 25
PARTICLE_RADIUS = 0.5
class Particle:
__slots__ = ("x", "y", "vx", "vy")
def __init__(self):
self.x = random.uniform(1, COLS - 1)
self.y = random.uniform(1, ROWS - 1)
self.vx = random.uniform(-0.2, 0.2)
self.vy = random.uniform(-0.2, 0.2)
def update(self, gravity_x, gravity_y, buckets):
self.vx += gravity_x * 0.2
self.vy += gravity_y * 0.2
self.x += self.vx
self.y += self.vy
if self.x < PARTICLE_RADIUS or self.x > COLS - PARTICLE_RADIUS:
self.vx *= -0.7
self.x = max(PARTICLE_RADIUS, min(COLS - PARTICLE_RADIUS, self.x))
if self.y < PARTICLE_RADIUS or self.y > ROWS - PARTICLE_RADIUS:
self.vy *= -0.7
self.y = max(PARTICLE_RADIUS, min(ROWS - PARTICLE_RADIUS, self.y))
self.vx *= 0.92
self.vy *= 0.92
gx, gy = int(self.x), int(self.y)
for dx in (-1, 0, 1):
for dy in (-1, 0, 1):
nx, ny = int(gx + dx), int(gy + dy)
if 0 <= nx < COLS and 0 <= ny < ROWS:
for other in buckets[ny][nx]:
if other is self: continue
dx_dist = other.x - self.x
dy_dist = other.y - self.y
dist_sq = dx_dist*dx_dist + dy_dist*dy_dist
min_dist = PARTICLE_RADIUS * 2
if 0.01 < dist_sq < min_dist * min_dist:
dist = math.sqrt(dist_sq)
if dist > 0:
overlap = min_dist - dist
nxn, nyn = dx_dist / dist, dy_dist / dist
self.x -= nxn * overlap * 0.5
self.y -= nyn * overlap * 0.5
self.vx -= nxn * 0.1
self.vy -= nyn * 0.1
def update_leds(gx, gy, particles, brightness, buckets):
for y in range(ROWS):
for x in range(COLS):
brightness[y][x] = 0.0
buckets[y][x].clear()
for p in particles:
px, py = int(p.x), int(p.y)
if 0 <= px < COLS and 0 <= py < ROWS:
buckets[py][px].append(p)
for p in particles:
p.update(gx, gy, buckets)
for p in particles:
gx_led, gy_led = int(p.x), int(p.y)
for dx in (-1, 0, 1):
for dy in (-1, 0, 1):
nx, ny = gx_led + dx, gy_led + dy
if 0 <= nx < COLS and 0 <= ny < ROWS:
dist_x, dist_y = abs(p.x - (nx+0.5)), abs(p.y - (ny+0.5))
if dist_x < 1.2 and dist_y < 1.2:
influence = max(0, 1.0 - (dist_x + dist_y) * 0.7)
brightness[ny][nx] += influence * 1.5
for y in range(ROWS):
for x in range(COLS):
idx = y * COLS + x
lvl = int(min(brightness[y][x] * 6, MAX_LEVEL))
np[idx] = (lvl * COLOR_CH[0], lvl * COLOR_CH[1], lvl * COLOR_CH[2])
np.write()
particles = [Particle() for _ in range(PARTICLE_COUNT)]
brightness = [[0.0 for _ in range(COLS)] for _ in range(ROWS)]
buckets = [[[] for _ in range(COLS)] for _ in range(ROWS)]
sim_time = 0
while not switch_requested:
if accelerometer_ok:
try:
raw = _r(0x28, 6)
x_raw, y_raw, z_raw = struct.unpack("<hhh", raw)
ax = max(min(x_raw*LSB_G, G_CLAMP), -G_CLAMP)
ay = max(min(y_raw*LSB_G, G_CLAMP), -G_CLAMP)
az = z_raw * LSB_G
mag = math.sqrt(ax*ax + ay*ay)
if mag > 0.05:
gx, gy = ax / mag, -ay / mag
else:
gx, gy = 0.0, 1.0 if az > 0 else -1.0
except:
gx, gy = 0.0, 1.0 # Fallback
else:
sim_time += 0.1
gx = math.sin(sim_time * 0.3) * 0.5
gy = math.cos(sim_time * 0.2) * 0.5
update_leds(gx, gy, particles, brightness, buckets)
# ═══════════════════════════════════════════════════════════════════════════════
# ANIMATION 2: FLAME
# ═══════════════════════════════════════════════════════════════════════════════
def flame_animation():
global switch_requested, accelerometer_ok, _r, LSB_G, G_CLAMP
print("🎬 Running Mode 2: Flame")
if not accelerometer_ok:
print("Flame animation requires an accelerometer.")
# Could show a message on the matrix here.
time.sleep(2) # Show message before switching
return
PARTICLE_COUNT = 12
TURBULENCE = 50.0
LIFETIME = 18
HEAT_INTENSITY = 0.3
FLAME_WIDTH = 2.0
SPAWN_RATE = 1
GRAVITY_STRENGTH = 0.8
x_off = (COLS - 1) / 2
y_off = (ROWS - 1) / 2
coords = tuple((c-x_off, y_off-r) for idx in range(NUM_LEDS) for r,c in (divmod(idx,COLS),))
class Particle:
__slots__ = ("x","y","z","vx","vy","vz","life","max_life","temperature")
def __init__(self, x, y, gx, gy):
self.x, self.y, self.z = x, y, (random.random()-0.5)*0.5
s = 0.05 + random.random()*0.08
self.vx = gx*s + (random.random()-0.5)*0.02
self.vy = gy*s + (random.random()-0.5)*0.02
self.vz = (random.random()-0.5)*0.01
self.life = random.randint(LIFETIME//2, LIFETIME)
self.max_life = self.life
self.temperature = random.random()*0.5 + 0.5
__temperature_map = ((0.25,(255,150,100)),(0.15,(255,100,0)),(0.15,(255,150,0)),(0.0,(255,15,0)))
@micropython.native
def get_color(self):
tr = self.temperature
for t,(r,g,b) in self.__temperature_map:
if tr > t: return r, g, b
return 0, 0, 0
_particles = []
@micropython.native
def _spawn_particle(gx: float, gy: float):
d = 4.0
return Particle(gx*d+(random.random()-0.5)*FLAME_WIDTH, gy*d+(random.random()-0.5)*FLAME_WIDTH*0.5, gx, gy)
@micropython.native
def _update_particles(gx: float, gy: float):
i = 0
while i < len(_particles):
if _particles[i].life <= 0: _particles.pop(i)
else: i += 1
if len(_particles) < PARTICLE_COUNT:
for _ in range(SPAWN_RATE):
_particles.append(_spawn_particle(gx, gy))
if len(_particles) >= PARTICLE_COUNT: break
for p in _particles:
p.vx -= gx*GRAVITY_STRENGTH*0.1; p.vy -= gy*GRAVITY_STRENGTH*0.1
p.vx += (random.random()-0.5)*TURBULENCE*0.001
p.vy += (random.random()-0.5)*TURBULENCE*0.001
p.vz += (random.random()-0.5)*TURBULENCE*0.0005
p.x += p.vx; p.y += p.vy; p.z += p.vz
p.vx *= 0.95; p.vy *= 0.95; p.vz *= 0.92
p.life -= 1; p.temperature = (p.life / p.max_life)*HEAT_INTENSITY
@micropython.native
def _led_color_for(x: float, y: float):
tot_i = r_acc = g_acc = b_acc = 0.0
for p in _particles:
dx, dy, dz = p.x-x, p.y-y, p.z
dist2 = dx*dx + dy*dy + dz*dz
if dist2 < 4.0:
inv = 1.0 - dist2*0.25
weight = inv * (p.life/p.max_life)
r,g,b = p.get_color()
tot_i += weight; r_acc += r*weight; g_acc += g*weight; b_acc += b*weight
if tot_i <= 0.0: return 0,0,0
r_base, g_base, b_base = r_acc/tot_i, g_acc/tot_i, b_acc/tot_i
brightness = (tot_i if tot_i < 1.0 else 1.0) * 0.3
r,g,b = int(r_base*brightness), int(g_base*brightness), int(b_base*brightness)
if r>255:r=255
if g>255:g=255
if b>255:b=255
return r,g,b
@micropython.native
def update(gx: float, gy: float, gz: float):
mag = (gx*gx + gy*gy)**0.5
if mag < 0.05:
gy = 1.0 if gz > 0 else -1.0; gx = 0.0
else:
inv = 1.0 / mag; gx *= inv; gy *= inv
_update_particles(gx, gy)
for i, (x, y) in enumerate(coords):
np[i] = _led_color_for(x, y)
np.write()
while not switch_requested:
try:
raw = _r(0x28, 6)
ax, ay, az = struct.unpack("<hhh", raw)
ax = max(min(ax * LSB_G, G_CLAMP), -G_CLAMP)
ay = max(min(ay * LSB_G, G_CLAMP), -G_CLAMP)
update(ax, ay, az * LSB_G)
except Exception as e:
print("err:", e)
time.sleep(0.1)
# ═══════════════════════════════════════════════════════════════════════════════
# MAIN PROGRAM & MODE SWITCHING
# ═══════════════════════════════════════════════════════════════════════════════
def main():
global switch_requested, current_mode
init_accelerometer()
button_timer = Timer()
button_timer.init(freq=50, mode=Timer.PERIODIC, callback=check_bootsel_button)
animations = [scroll_text_continuous_animation, particle_physics_animation, flame_animation]
animation_names = ["Scrolling Text", "Particle Physics (Water)", "Flame"]
print("\n🎮 BOOTSEL Button Animation Switcher")
print("Press BOOTSEL button to switch between animations:")
print(" Mode 0: Scrolling Text (TSCIRCUIT)")
print(" Mode 1: Particle Physics with Accelerometer (Water)")
print(" Mode 2: Flame with Accelerometer")
print("-" * 50)
try:
while True:
print(f"\n🎬 Starting Mode {current_mode}: {animation_names[current_mode]}")
switch_requested = False
animations[current_mode]()
time.sleep(0.1)
except KeyboardInterrupt:
print(f"\n🛑 Program stopped by user. Total BOOTSEL presses: {total_presses}")
finally:
for i in range(NUM_LEDS):
np[i] = (0, 0, 0)
np.write()
onboard_led.off()
button_timer.deinit()
print("✅ Cleanup done.")
if __name__ == "__main__":
main()