Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
1839f1973This file is pinned to the selected release. Download the original to open it in your design tool.
import machine
import neopixel
import time
import random
# --- Hardware Configuration ---
PIN = 25
WIDTH = 4
HEIGHT = 4
PIXELS = WIDTH * HEIGHT
LED_ORDER = 3
GLOBAL_BRIGHTNESS = 0.05 # Essential safeguard for the Pico's regulator
# Initialize the NeoPixel matrix
np = neopixel.NeoPixel(machine.Pin(PIN), PIXELS, bpp=LED_ORDER)
# --- The Thermodynamics Engine State ---
# We maintain a "heat" map instead of a color map.
# 0 is cold (black), 255 is intensely hot (yellow/white).
heat = [[0 for _ in range(WIDTH)] for _ in range(HEIGHT)]
def hsv_to_rgb(h, s, v):
"""Converts a Hue (0.0 to 1.0) into standard RGB."""
if s == 0.0: return (int(v * 255), int(v * 255), int(v * 255))
i = int(h * 6.0)
f = (h * 6.0) - i
p = v * (1.0 - s)
q = v * (1.0 - s * f)
t = v * (1.0 - s * (1.0 - f))
i %= 6
if i == 0: r, g, b = v, t, p
elif i == 1: r, g, b = q, v, p
elif i == 2: r, g, b = p, v, t
elif i == 3: r, g, b = p, q, v
elif i == 4: r, g, b = t, p, v
else: r, g, b = v, p, q
return (int(r * 255), int(g * 255), int(b * 255))
def heat_to_color(temperature):
"""
Translates a raw temperature (0-255) into a realistic fire color.
0 = Black
Low Temp = Dim Red
Medium Temp = Bright Orange
High Temp = Blinding Yellow
"""
# Safety clamp to ensure temperature stays within 0-255 bounds
temperature = max(0, min(255, temperature))
# If it's too cold, it doesn't emit any light
if temperature < 30:
return (0, 0, 0)
# 1. Calculate Hue: 0.0 is Red, 0.15 is Yellow.
# The hotter it gets, the closer the hue shifts toward yellow.
hue = (temperature / 255.0) * 0.15
# 2. Calculate Brightness: The hotter it is, the brighter it glows.
# We multiply by GLOBAL_BRIGHTNESS to prevent blowing out the board.
val = (temperature / 255.0) * GLOBAL_BRIGHTNESS
# Return fully saturated (1.0) fire colors
return hsv_to_rgb(hue, 1.0, val)
def update_fire():
"""The Core Thermodynamics Step."""
# 1. Cool down the entire grid slightly so the fire doesn't run out of control.
for y in range(HEIGHT):
for x in range(WIDTH):
cooling_amount = random.randint(0, 30)
heat[y][x] = max(0, heat[y][x] - cooling_amount)
# 2. Move the heat UPWARDS.
# We iterate from the top row (0) down to the second-to-last row (2).
# We don't touch the bottom row (3) here, because that is our fuel source.
for y in range(HEIGHT - 1):
for x in range(WIDTH):
# To make the flames flicker naturally, the heat doesn't just go straight up.
# It occasionally drifts slightly to the left or right.
drift = random.randint(-1, 1)
source_x = x + drift
# Keep the drift within the 4x4 physical bounds
if source_x < 0: source_x = 0
if source_x >= WIDTH: source_x = WIDTH - 1
# Pull the heat from the cell below it and move it to the current cell
heat[y][x] = heat[y+1][source_x]
# 3. Ignite the fuel source (The bottom row).
for x in range(WIDTH):
# We give the bottom row a 60% chance to spark with intense heat every frame.
if random.randint(0, 100) < 60:
heat[HEIGHT-1][x] = random.randint(160, 255)
else:
# If it doesn't spark, it cools down rapidly.
heat[HEIGHT-1][x] = max(0, heat[HEIGHT-1][x] - 40)
def run_fire_demo():
"""The main simulation loop."""
try:
while True:
# 1. Calculate the physics for this frame
update_fire()
# 2. Render the heat map to the physical LEDs
for y in range(HEIGHT):
for x in range(WIDTH):
idx = y * WIDTH + x
np[idx] = heat_to_color(heat[y][x])
np.write()
# 3. Fire needs to move faster than falling sand to look kinetic and violent.
time.sleep(0.06)
except KeyboardInterrupt:
# Clean shutdown via Thonny
for i in range(PIXELS):
np[i] = (0, 0, 0)
np.write()
# Ignite the simulation
run_fire_demo()