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/waterledphysics.py

from machine import Pin, SPI
import math, time, struct, neopixel

# ───────── 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  = 30          # cap (0-15) → tweak for brightness
SOFT_WIDTH = 1.5         # fade distance (matrix units) under the line
COLOR_CH   = (0, 0, 1)   # per-channel multipliers (R,G,B). (1,0,0) = red only

# ───── Pre-compute LED coordinates (origin at matrix centre) ─────
x_off = (COLS - 1) / 2      # 3
y_off = (ROWS - 1) / 2      # 2.5
coords = []
for idx in range(NUM_LEDS):
    r, c = divmod(idx, COLS)
    coords.append((c - x_off, y_off - r))   # (x, y)

# ───── Accelerometer (LIS3DHTR) setup ─────
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 °
DEG_PER  = 90 / G_CLAMP # degrees per accel unit


# ─── LED update helper ───
def update(gx: float, gy: float, gz: float) -> None:
    """
    gx, gy, gz are gravity components in g‑units (‑1 … +1).
    Project gravity onto the LED plane (x,y) and fill pixels
    whose dot‑product with that vector is positive (below water line).
    """
    # 1. Project gravity onto board plane
    vx, vy = -gx, -gy           # minus sign because we want "down" direction
    mag = (vx * vx + vy * vy) ** 0.5 

    # 2. If board is nearly flat, fake gravity toward the edge selected by gz
    if mag < 0.05:             # ≈ 0.05 g threshold
        # choose +Y (top) when board is upside‑down, else ‑Y (bottom)
        vy = 1.0 if gz > 0 else -1.0
        vx = 0.0
        mag = 1.0

    # 3. Scaling for fade below the water‑line
    sat_step = (MAX_LEVEL / SOFT_WIDTH) / mag

    for i, (x, y) in enumerate(coords):
        depth = -(x * vx + y * vy)      # >0 means “below water‑line”
        if depth <= 0:
            lvl = 0
        else:
            lvl = int(min(depth * sat_step, MAX_LEVEL))

        np[i] = (lvl * COLOR_CH[0],
                 lvl * COLOR_CH[1],
                 lvl * COLOR_CH[2])

    np.write()


# ─── Main loop ───
while True:
    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               # no clamp needed here

    update(ax, ay, az)
    time.sleep(0.05)          # ~20 fps