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/ledwaterparticles.py
from machine import Pin, SPI
import math, time, struct, neopixel, random
# ───────── LED-string setup ─────────
LED_PIN = 6
NUM_LEDS = 42 # 7 × 6 matrix, row-major
COLS, ROWS = 7, 6
np = neopixel.NeoPixel(Pin(LED_PIN), NUM_LEDS)
# ───── Visual parameters ─────
MAX_LEVEL = 35 # cap (0-15) → tweak for brightness
COLOR_CH = (0, 0, 1) # per-channel multipliers (R,G,B). (1,0,0) = red only
# ───── Matrix parameters ─────
MATRIX_WIDTH = COLS # 7 units wide
MATRIX_HEIGHT = ROWS # 6 units tall
PARTICLE_COUNT = 25 # Reduced for better performance with collisions
# ───── Accelerometer (LIS3DHTR) setup - for gravity direction ─────
SCK_PIN, MOSI_PIN, MISO_PIN, CS_PIN = 10, 11, 12, 17
spi = SPI(1, baudrate=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 _w(addr, val): # write 1 byte
cs(0); spi.write(bytearray([addr & 0x3F, val & 0xFF])); cs(1)
def _r(addr, n=1): # read n bytes
cmd = addr | 0x80 | (0x40 if n > 1 else 0)
cs(0); spi.write(bytearray([cmd])); data = spi.read(n); cs(1); return data
if _r(0x0F)[0] != 0x33:
raise RuntimeError("No LIS3DHTR")
_w(0x20, 0x57) # ODR 100 Hz, XYZ on
_w(0x23, 0x08) # hi-res, ±2 g
# ───── Mapping constants ─────
LSB_G = 0.00098 # 1 mg / LSB (hi-res ±2 g)
G_CLAMP = 15 # your observed ±15 range → ±90 °
# ───── Simplified Particle System ─────
class Particle:
def __init__(self):
self.x = random.uniform(1.0, MATRIX_WIDTH - 1.0)
self.y = random.uniform(1.0, MATRIX_HEIGHT - 1.0)
self.vx = random.uniform(-0.2, 0.2)
self.vy = random.uniform(-0.2, 0.2)
self.radius = 0.4 # Increased particle size
def update(self, gravity_x, gravity_y, particles):
# Apply gravity
self.vx += gravity_x * 0.05
self.vy += gravity_y * 0.05
# Simple collision detection with other particles
for other in particles:
if other is self:
continue
dx = other.x - self.x
dy = other.y - self.y
distance_sq = dx * dx + dy * dy
min_distance = self.radius + other.radius
if distance_sq < min_distance * min_distance and distance_sq > 0.01:
# Push particles apart
distance = math.sqrt(distance_sq)
overlap = min_distance - distance
# Normalize and apply separation
if distance > 0:
dx_norm = dx / distance
dy_norm = dy / distance
# Move particles apart
self.x -= dx_norm * overlap * 0.5
self.y -= dy_norm * overlap * 0.5
# Add some velocity exchange
self.vx -= dx_norm * 0.1
self.vy -= dy_norm * 0.1
# Update position
self.x += self.vx
self.y += self.vy
# Bounce off walls with damping
if self.x < self.radius or self.x > MATRIX_WIDTH - self.radius:
self.vx *= -0.7
self.x = max(self.radius, min(MATRIX_WIDTH - self.radius, self.x))
if self.y < self.radius or self.y > MATRIX_HEIGHT - self.radius:
self.vy *= -0.7
self.y = max(self.radius, min(MATRIX_HEIGHT - self.radius, self.y))
# Simple damping
self.vx *= 0.9
self.vy *= 0.9
# Initialize particles
particles = [Particle() for _ in range(PARTICLE_COUNT)]
# Pre-allocate LED brightness array
led_brightness = [0] * NUM_LEDS
# ─── LED update helper ───
def update_leds(gravity_x, gravity_y):
"""
Update particles and calculate LED brightness based on particle positions.
"""
# Update all particles
for particle in particles:
particle.update(gravity_x, gravity_y, particles)
# Reset LED brightness
for i in range(NUM_LEDS):
led_brightness[i] = 0
# Calculate LED brightness based on particle positions
for particle in particles:
# Find which LED grid cell this particle is in
grid_x = int(particle.x)
grid_y = int(particle.y)
# Make sure we're within bounds
if 0 <= grid_x < COLS and 0 <= grid_y < ROWS:
led_index = grid_y * COLS + grid_x
led_brightness[led_index] += 1
# Also add influence to neighboring LEDs for smoother effect
for dx in [-1, 0, 1]:
for dy in [-1, 0, 1]:
nx, ny = grid_x + dx, grid_y + dy
if 0 <= nx < COLS and 0 <= ny < ROWS:
led_index = ny * COLS + nx
# Calculate distance influence (bigger radius for larger particles)
dist_x = abs(particle.x - nx - 0.5)
dist_y = abs(particle.y - ny - 0.5)
if dist_x < 1.2 and dist_y < 1.2: # Increased influence radius
influence = max(0, 1.0 - (dist_x + dist_y) * 0.7)
led_brightness[led_index] += influence * 0.8 # Stronger influence
# Update LEDs
for i in range(NUM_LEDS):
# Scale brightness and clamp
brightness = led_brightness[i] * 6 # Reduced multiplier since particles have more influence
lvl = int(min(brightness, MAX_LEVEL))
np[i] = (lvl * COLOR_CH[0],
lvl * COLOR_CH[1],
lvl * COLOR_CH[2])
np.write()
# ─── Main loop ───
while True:
# Read accelerometer for gravity direction
raw = _r(0x28, 6)
x_raw, y_raw, z_raw = struct.unpack("<hhh", raw)
# Convert to g‑units and clamp
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
# Use accelerometer data as gravity for particles
# Normalize gravity vector
gravity_mag = math.sqrt(ax*ax + ay*ay)
if gravity_mag > 0.05:
gravity_x = ax / gravity_mag # Negative because we want "down" direction
gravity_y = -ay / gravity_mag
else:
# If board is nearly flat, use gz to determine direction
gravity_x = 0.0
gravity_y = 1.0 if az > 0 else -1.0
update_leds(gravity_x, gravity_y)
#time.sleep(0.01) # ~33 fps