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

#####################################################################
#  optimised_flame.py – same API, faster with @micropython.native   #
#  Modified: LED brightness now scales with local heat density      #
#####################################################################
from machine import Pin, SPI
import micropython, neopixel, time, struct, math, random

micropython.alloc_emergency_exception_buf(100)

# ───────── Constants ─────────
micropython.const(1)
LED_PIN   = const(6)
NUM_LEDS  = const(42)
COLS      = const(7)
ROWS      = const(6)

PARTICLE_COUNT   = const(12)
TURBULENCE       = 50.0
LIFETIME         = const(18)
HEAT_INTENSITY   = 0.3
FLAME_WIDTH      = 2.0
SPAWN_RATE       = const(1)           # integer → simpler native loop
GRAVITY_STRENGTH = 0.8

LSB_G   = 0.00098
G_CLAMP = 15

# ───────── LED & geometry setup (unchanged) ─────────
np = neopixel.NeoPixel(Pin(LED_PIN), NUM_LEDS)
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),))

# ───────── LIS3DHTR setup (unchanged) ─────────
SCK_PIN, MOSI_PIN, MISO_PIN, CS_PIN = 10, 11, 12, 17
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 _w(a, v):
    cs(0); spi.write(bytearray([a & 0x3F, v & 0xFF])); cs(1)

def _r(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

if _r(0x0F)[0] != 0x33:
    raise RuntimeError("No LIS3DHTR")
_w(0x20, 0x57)               # 100 Hz, all axes
_w(0x23, 0x08)               # hi-res, ±2 g

# ───────── Particle class ─────────
class Particle:
    __slots__ = ("x","y","z","vx","vy","vz","life","max_life","temperature")
    def __init__(self, x, y, gx, gy):
        self.x = x
        self.y = y
        self.z = (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):
        # Look‑up instead of nested if/elif increases speed
        tr = self.temperature
        for t,(r,g,b) in self.__temperature_map:
            if tr > t:
                return r, g, b
        return 0, 0, 0  # fallback – should not occur

_particles = []          # global list – keep name short for native code

# ───────── native helpers ─────────
@micropython.native
def _spawn_particle(gx: float, gy: float):
    """Return *new* Particle spawned opposite gravity vector."""
    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):
    """Maintain list _particles in‑place, spawn & update."""
    plist = _particles
    i = 0
    # remove dead – faster than list‑comp in native
    while i < len(plist):
        if plist[i].life <= 0:
            plist.pop(i)
        else:
            i += 1
    # spawn
    if len(plist) < PARTICLE_COUNT:
        for _ in range(SPAWN_RATE):
            plist.append(_spawn_particle(gx, gy))
            if len(plist) >= PARTICLE_COUNT:
                break
    # update
    for p in plist:
        # gravity
        p.vx -= gx*GRAVITY_STRENGTH*0.1
        p.vy -= gy*GRAVITY_STRENGTH*0.1
        # turbulence
        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
        # integrate
        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

# ───────── LED colour helper ─────────
#   ‼️  Updated: brightness now scales with local heat density
@micropython.native
def _led_color_for(x: float, y: float):
    """Blend contribution of nearby particles and scale brightness.

    The cumulative heat density (tot_i) is used as the brightness
    factor (0‥1).  Hue is taken from the average particle colour so
    hue stays stable while brightness varies with the flame’s vigour.
    """
    tot_i = r_acc = g_acc = b_acc = 0.0
    for p in _particles:
        dx = p.x - x; dy = p.y - y; dz = p.z
        dist2 = dx*dx + dy*dy + dz*dz
        if dist2 < 4.0:                  # 2.0**2
            inv = 1.0 - dist2*0.25       # 1 - dist²/4 (0‥1)
            life_ratio = p.life / p.max_life
            weight = inv * life_ratio    # heat contribution (0‥1)
            r, g, b = p.get_color()      # ints 0‑255
            tot_i += weight
            r_acc += r * weight
            g_acc += g * weight
            b_acc += b * weight

    if tot_i <= 0.0:
        return 0, 0, 0

    # Base colour (average of contributing particles)
    r_base = r_acc / tot_i
    g_base = g_acc / tot_i
    b_base = b_acc / tot_i

    # Brightness scales with heat density (clamp ≤1)
    brightness = (tot_i if tot_i < 1.0 else 1.0) * 0.3

    r_val = int(r_base * brightness)
    g_val = int(g_base * brightness)
    b_val = int(b_base * brightness)

    # Ensure values stay within 0‑255
    if r_val > 255: r_val = 255
    if g_val > 255: g_val = 255
    if b_val > 255: b_val = 255

    return r_val, g_val, b_val

# ───────── High‑level update (native) ─────────
@micropython.native
def update(gx: float, gy: float, gz: float):
    """Public: advance simulation one frame & push to LEDs."""
    # normalise g vector in‑plane
    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)

    # paint LEDs
    for i, (x, y) in enumerate(coords):
        np[i] = _led_color_for(x, y)
    np.write()

# ───────── Main loop ─────────
print("Starting flame simulation (native)…")
while True:
    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)