import random import utime # Default warm palette: ember → orange → yellow → pale hot (RGB) _DEFAULT_PALETTE = ( (90, 8, 8), (200, 40, 12), (255, 120, 30), (255, 220, 140), ) def _clamp(x, lo, hi): if x < lo: return lo if x > hi: return hi return x def _lerp_chan(a, b, t): return a + ((b - a) * t >> 8) def _lerp_rgb(c0, c1, t): return ( _lerp_chan(c0[0], c1[0], t), _lerp_chan(c0[1], c1[1], t), _lerp_chan(c0[2], c1[2], t), ) def _palette_sample(palette, pos256): n = len(palette) if n == 0: return (255, 160, 60) if n == 1: return palette[0] span = (n - 1) * pos256 seg = span >> 8 if seg >= n - 1: return palette[n - 1] frac = span & 0xFF return _lerp_rgb(palette[seg], palette[seg + 1], frac) def _triangle_255(elapsed_ms, period_ms): period_ms = max(period_ms, 400) p = elapsed_ms % period_ms half = period_ms >> 1 if half <= 0: return 128 if p < half: return (p * 255) // half return ((period_ms - p) * 255) // (period_ms - half) class Flame: def __init__(self, driver): self.driver = driver def _build_palette(self, preset): colors = preset.c if not colors: return list(_DEFAULT_PALETTE) out = [] for c in colors: if isinstance(c, (list, tuple)) and len(c) == 3: out.append( ( _clamp(int(c[0]), 0, 255), _clamp(int(c[1]), 0, 255), _clamp(int(c[2]), 0, 255), ) ) return out if out else list(_DEFAULT_PALETTE) def _draw_frame(self, preset, palette, ticks_now, breath_el_ms, rise, cluster_jit, breath_ms, lo, hi, spark_state): """spark_state: (active: bool, start_ticks, duration_ms). ticks_now for sparks; breath_el_ms for slow wave.""" num = self.driver.num_leds denom = num - 1 if num > 1 else 1 breathe = _triangle_255(breath_el_ms, breath_ms) base_level = lo + (((hi - lo) * breathe) >> 8) micro = 232 + random.randint(0, 35) level = (base_level * micro) >> 8 level = _clamp(level, lo, hi) spark_boost = 0 spark_white = (0, 0, 0) active, s0, dur = spark_state if active and dur > 0: el = utime.ticks_diff(ticks_now, s0) if el < 0: el = 0 if el >= dur: spark_boost = 0 else: env = 255 - ((el * 255) // dur) spark_boost = (env * 90) >> 8 spark_white = ((env * 55) >> 8, (env * 50) >> 8, (env * 40) >> 8) for i in range(num): h = (i * 256) // denom flow = (h + rise + ((i // max(1, num >> 3)) * 17)) & 255 pos = (flow + cluster_jit[(i >> 2) & 7]) & 255 rgb = _palette_sample(palette, pos) if spark_boost: rgb = ( _clamp(rgb[0] + spark_white[0] + (spark_boost * 3 >> 2), 0, 255), _clamp(rgb[1] + spark_white[1] + (spark_boost >> 1), 0, 255), _clamp(rgb[2] + spark_white[2] + (spark_boost >> 2), 0, 255), ) self.driver.n[i] = self.driver.apply_brightness(rgb, level) self.driver.n.write() def run(self, preset): """Salt-lamp / hearth-style flame: warm gradient, breathing, jitter, drift, rare sparks.""" palette = self._build_palette(preset) lo = max(0, min(255, int(preset.n1))) hi = max(0, min(255, int(preset.b))) if lo > hi: lo, hi = hi, lo bp = int(preset.n2) breath_ms = max(800, bp if bp > 0 else 2500) gap_lo = int(preset.n3) gap_hi = int(preset.n4) # n3 < 0 disables sparks; n3=n4=0 uses ~10–30 s gaps (hearth pops). if gap_lo < 0: sparks_on = False else: sparks_on = True if gap_lo == 0 and gap_hi == 0: gap_lo, gap_hi = 10000, 30000 else: gap_lo = max(gap_lo, 500) if gap_hi < gap_lo: gap_hi = gap_lo delay_ms = max(16, int(preset.d)) rise = random.randint(0, 255) cluster_jit = [random.randint(-18, 18) for _ in range(8)] last_draw = utime.ticks_ms() breath_origin = last_draw last_cluster = last_draw spark_active = False spark_start = 0 spark_dur = 0 next_spark = utime.ticks_add(last_draw, random.randint(gap_lo, gap_hi)) if sparks_on else 0 if not preset.a: now = utime.ticks_ms() self._draw_frame( preset, palette, now, utime.ticks_diff(now, breath_origin), rise, cluster_jit, breath_ms, lo, hi, (False, 0, 0), ) yield return while True: now = utime.ticks_ms() if utime.ticks_diff(now, last_draw) < delay_ms: yield continue last_draw = utime.ticks_add(last_draw, delay_ms) rise = (rise + random.randint(-10, 12)) & 255 if utime.ticks_diff(now, last_cluster) >= (delay_ms * 4): last_cluster = now cluster_jit = [random.randint(-18, 18) for _ in range(8)] spark_state = (spark_active, spark_start, spark_dur) if sparks_on: if spark_active: if utime.ticks_diff(now, spark_start) >= spark_dur: spark_active = False next_spark = utime.ticks_add( now, random.randint(gap_lo, gap_hi), ) elif utime.ticks_diff(now, next_spark) >= 0: spark_active = True spark_start = now spark_dur = random.randint(180, 360) self._draw_frame( preset, palette, now, utime.ticks_diff(now, breath_origin), rise, cluster_jit, breath_ms, lo, hi, (spark_active, spark_start, spark_dur), ) yield