diff --git a/firmware/main.py b/firmware/main.py index c6d0ba3..6b02bed 100644 --- a/firmware/main.py +++ b/firmware/main.py @@ -105,17 +105,24 @@ MOOD_COLORS = { "sleep": (20, 22, 35), } -JAW_COLOR = (0, 200, 255) +# Jaw color breakpoints for amplitude-based color shift +# (threshold 0-100, r, g, b) +JAW_COLOR_LO = (0, 200, 255) # Cyan at low amplitude +JAW_COLOR_MID = (200, 240, 255) # White-ish at mid amplitude +JAW_COLOR_HI = (255, 220, 180) # Warm at high amplitude +JAW_CENTER = JAW_OFFSET + 6 # Center LED index in strip (index 7) # === Animation state === mood_mode = "idle" jaw_mode = "off" jaw_amplitude = 0 # 0-100, set by bare integer commands +jaw_peak = 0 # Current display level with peak+decay (0-100) # Per-animation persistent state larson_pos = 0 larson_dir = 1 +larson_tick = 0 wave_offset = 0.0 # Sparkle buffer: store per-LED brightness as fixed-point (0-255) sparkle_buf = array.array("B", [0] * MOOD_LEDS) @@ -179,12 +186,12 @@ def tick_wave(color): brightness = 51 + ((204 * raw) >> 8) ar[i] = scale_color_grb(r, g, b, brightness) mood.show() - wave_offset += 0.5 + wave_offset += 0.25 def tick_larson(color): - """Larson scanner (bouncing dot with trail).""" - global larson_pos, larson_dir + """Larson scanner (bouncing dot with trail). Advances every 2nd frame.""" + global larson_pos, larson_dir, larson_tick r, g, b = color ar = mood.ar # Clear @@ -201,13 +208,17 @@ def tick_larson(color): fade = _sine_table[(SINE_TABLE_SIZE // 4 - (t * SINE_TABLE_SIZE // 32)) % SINE_TABLE_SIZE] ar[idx] = scale_color_grb(r, g, b, fade) mood.show() - larson_pos += larson_dir - if larson_pos >= MOOD_LEDS - 1: - larson_pos = MOOD_LEDS - 1 - larson_dir = -1 - elif larson_pos <= 0: - larson_pos = 0 - larson_dir = 1 + # Advance position every 2nd frame + larson_tick += 1 + if larson_tick >= 2: + larson_tick = 0 + larson_pos += larson_dir + if larson_pos >= MOOD_LEDS - 1: + larson_pos = MOOD_LEDS - 1 + larson_dir = -1 + elif larson_pos <= 0: + larson_pos = 0 + larson_dir = 1 def tick_sparkle(color): @@ -216,10 +227,10 @@ def tick_sparkle(color): ar = mood.ar buf = sparkle_buf for i in range(MOOD_LEDS): - # Decay existing brightness by ~15%: multiply by 217/256 - v = (buf[i] * 217) >> 8 - # Random spawn: ~15% chance = getrandbits check - if getrandbits(3) == 0: # 1/8 = 12.5%, close enough to 15% + # Decay existing brightness by ~10%: multiply by 230/256 + v = (buf[i] * 230) >> 8 + # Random spawn: ~6% chance + if getrandbits(4) == 0: # 1/16 = 6.25% v = 255 buf[i] = v ar[i] = scale_color_grb(r, g, b, v) @@ -237,37 +248,84 @@ def tick_mood(now_ms): return if mode == "idle": - tick_pulse(now_ms, color, 500, 51, 204) # 0.5 Hz, 20-80% + tick_pulse(now_ms, color, 250, 51, 204) # 0.25 Hz, 20-80% elif mode == "listening": - tick_pulse(now_ms, color, 2000, 153, 255) # 2.0 Hz, 60-100% + tick_pulse(now_ms, color, 1000, 153, 255) # 1.0 Hz, 60-100% elif mode == "responding": tick_wave(color) elif mode == "pleasure": - tick_pulse(now_ms, color, 800, 77, 230) # 0.8 Hz, 30-90% + tick_pulse(now_ms, color, 400, 77, 230) # 0.4 Hz, 30-90% elif mode == "thinking": tick_larson(color) elif mode == "playful": tick_sparkle(color) elif mode == "commanding": - tick_pulse(now_ms, color, 3000, 128, 255) # 3.0 Hz, 50-100% + tick_pulse(now_ms, color, 1500, 128, 255) # 1.5 Hz, 50-100% elif mode == "love": - tick_pulse(now_ms, color, 600, 102, 230) # 0.6 Hz, 40-90% + tick_pulse(now_ms, color, 300, 102, 230) # 0.3 Hz, 40-90% elif mode == "sleep": - tick_pulse(now_ms, color, 200, 26, 77) # 0.2 Hz, 10-30% + tick_pulse(now_ms, color, 100, 26, 77) # 0.1 Hz, 10-30% # === Jaw animation ticks === +def _jaw_color_for_amp(amp): + """Interpolate jaw color based on amplitude 0-100. Returns (r, g, b).""" + if amp <= 33: + # Blend lo -> mid over 0-33 + t = (amp * 256) // 33 # 0-256 + r = JAW_COLOR_LO[0] + ((JAW_COLOR_MID[0] - JAW_COLOR_LO[0]) * t >> 8) + g = JAW_COLOR_LO[1] + ((JAW_COLOR_MID[1] - JAW_COLOR_LO[1]) * t >> 8) + b = JAW_COLOR_LO[2] + ((JAW_COLOR_MID[2] - JAW_COLOR_LO[2]) * t >> 8) + else: + # Blend mid -> hi over 34-100 + t = ((amp - 34) * 256) // 66 # 0-256 + r = JAW_COLOR_MID[0] + ((JAW_COLOR_HI[0] - JAW_COLOR_MID[0]) * t >> 8) + g = JAW_COLOR_MID[1] + ((JAW_COLOR_HI[1] - JAW_COLOR_MID[1]) * t >> 8) + b = JAW_COLOR_MID[2] + ((JAW_COLOR_HI[2] - JAW_COLOR_MID[2]) * t >> 8) + return (r, g, b) + + def jaw_set_amplitude(amp): - """Set jaw LEDs based on amplitude 0-100.""" - num_lit = (amp * JAW_USABLE) // 100 - r, g, b = JAW_COLOR - on = pack_grb(r, g, b) + """Symmetric VU bar from center with peak+decay and color shift.""" + global jaw_peak + # Peak+decay: snap up, smooth decay down + if amp >= jaw_peak: + jaw_peak = amp + else: + jaw_peak = jaw_peak - (jaw_peak - amp) * 0.3 + if jaw_peak < 0.5: + jaw_peak = 0 + + display = int(jaw_peak) ar = jaw.ar ar[0] = 0 # Damaged LED always off + + if display == 0: + for i in range(JAW_OFFSET, JAW_LEDS): + ar[i] = 0 + jaw.show() + return + + r, g, b = _jaw_color_for_amp(display) + on = pack_grb(r, g, b) + + # How many LEDs per side (0-6), plus center = 1 + 2*num_side + # At amp 100: num_side = 6 (all 13 lit). At amp ~8: num_side = 0 (center only) + num_side_256 = (display * 6 * 256) // 100 # fixed-point 0..6*256 + num_side_full = num_side_256 >> 8 # whole LEDs per side + edge_brightness = num_side_256 & 0xFF # fractional part for edge LED + for i in range(JAW_OFFSET, JAW_LEDS): - if i - JAW_OFFSET < num_lit: + dist = abs(i - JAW_CENTER) # distance from center (0-6) + if dist == 0: + # Center always on when amp > 0 ar[i] = on + elif dist <= num_side_full: + ar[i] = on + elif dist == num_side_full + 1 and edge_brightness > 0: + # Edge LED with fractional brightness + ar[i] = scale_color_grb(r, g, b, edge_brightness) else: ar[i] = 0 jaw.show() @@ -293,10 +351,10 @@ def tick_jaw(now_ms): amp = (val * 100) >> 8 # Scale to 0-100 jaw_set_amplitude(amp) elif mode == "idle": - # Subtle dim glow + # Subtle dim glow from center ar = jaw.ar ar[0] = 0 - dim = scale_color_grb(JAW_COLOR[0], JAW_COLOR[1], JAW_COLOR[2], 15) + dim = scale_color_grb(JAW_COLOR_LO[0], JAW_COLOR_LO[1], JAW_COLOR_LO[2], 15) for i in range(JAW_OFFSET, JAW_LEDS): ar[i] = dim jaw.show() @@ -358,9 +416,10 @@ def process_command(line): def _reset_mood_state(): """Reset per-animation state on mode switch.""" - global larson_pos, larson_dir, wave_offset + global larson_pos, larson_dir, larson_tick, wave_offset larson_pos = 0 larson_dir = 1 + larson_tick = 0 wave_offset = 0.0 for i in range(MOOD_LEDS): sparkle_buf[i] = 0