diff --git a/src/bar_config.py b/src/bar_config.py new file mode 100644 index 0000000..fd1fcb6 --- /dev/null +++ b/src/bar_config.py @@ -0,0 +1,38 @@ +# LED Bar Configuration +# Modify these names as needed for your setup + +# LED Bar Names/IDs - 4 left bars + 4 right bars +LED_BAR_NAMES = [ + "100", # Left Bar 1 + "101", # Left Bar 2 + "102", # Left Bar 3 + "103", # Left Bar 4 + "104", # Right Bar 1 + "105", # Right Bar 2 + "106", # Right Bar 3 + "107", # Right Bar 4 +] + +# Left and right bar groups for spatial control +LEFT_BARS = ["100", "101", "102", "103"] +RIGHT_BARS = ["104", "105", "106", "107"] + +# Number of LED bars +NUM_BARS = len(LED_BAR_NAMES) + +# Default settings for all bars +DEFAULT_BAR_SETTINGS = { + "pattern": "pulse", + "delay": 100, + "colors": [(0, 255, 0)], # Default green + "brightness": 100, + "num_leds": 200, + "n1": 10, + "n2": 10, + "n3": 1, + "n": 0, +} + +# ESP-NOW broadcast settings +ESP_NOW_CHANNEL = 1 +ESP_NOW_ENCRYPTION = False diff --git a/src/main.py b/src/main.py index cceebf8..602f701 100644 --- a/src/main.py +++ b/src/main.py @@ -199,14 +199,16 @@ class App: "pulse": "💥", "flicker": "✨", "alternating": "↔️", - "n_chase": "🏃", + "n chase": "🏃", "rainbow": "🌈", "radiate": "🌟", + "sequential\npulse": "🔄", + "alternating\nphase": "⚡", "-": "", } bank1_patterns = [ - "pulse", "flicker", "alternating", "n_chase", - "rainbow", "radiate", "-", "-", + "pulse", "flicker", "alternating", "n chase", + "rainbow", "radiate", "sequential\npulse", "alternating\nphase", "-", "-", "-", "-", "-", "-", "-", "-", ] diff --git a/src/midi.py b/src/midi.py index 182ed89..7dc0675 100644 --- a/src/midi.py +++ b/src/midi.py @@ -7,6 +7,19 @@ import logging # Added logging import import time # Added for initial state read import tkinter as tk from tkinter import ttk, messagebox # Import messagebox for confirmations +from bar_config import LED_BAR_NAMES, DEFAULT_BAR_SETTINGS + +# Pattern name mapping for shorter JSON payloads +PATTERN_NAMES = { + "flicker": "f", + "fill_range": "fr", + "n_chase": "nc", + "alternating": "a", + "pulse": "p", + "rainbow": "r", + "specto": "s", + "radiate": "rd", +} # Configure logging @@ -46,6 +59,9 @@ class MidiHandler: self.current_bpm: float | None = None self.current_pattern: str = "" self.beat_index: int = 0 + # Sequential pulse pattern state + self.sequential_pulse_enabled: bool = False + self.sequential_pulse_step: int = 0 def _current_color_rgb(self) -> tuple: r = max(0, min(255, int(self.color_r))) @@ -53,6 +69,128 @@ class MidiHandler: b = max(0, min(255, int(self.color_b))) return (r, g, b) + async def _handle_sequential_pulse(self): + """Handle sequential pulse pattern: each bar pulses for 1 beat, then next bar, mirrored""" + from bar_config import LEFT_BARS, RIGHT_BARS + + # Calculate which bar should pulse based on beat (1 beat per bar) + bar_index = self.beat_index % 4 # 0-3, cycles every 4 beats + + # Create minimal payload - only send pattern + payload = { + "d": { # Defaults - only pattern + "pt": "p", # pulse + } + } + + # Set specific bars to pulse + left_bar = LEFT_BARS[bar_index] + right_bar = RIGHT_BARS[bar_index] + + payload[left_bar] = {"pt": "p"} # pulse + payload[right_bar] = {"pt": "p"} # pulse + + logging.debug(f"[Sequential Pulse] Beat {self.beat_index}, pulsing bars {left_bar} and {right_bar}") + await self.ws_client.send_data(payload) + + async def _handle_alternating_phase(self): + """Handle alternating pattern with phase offset: every second bar swaps n1 and n2""" + from bar_config import LED_BAR_NAMES + + # Create minimal payload - only send what changes + payload = { + "d": { # Defaults - only pattern and n1/n2 + "pt": "a", # alternating + "n1": self.n1, + "n2": self.n2, + } + } + + # Set n1/n2 swap for every second bar (bars 101, 103, 105, 107) + swap_bars = ["101", "103", "105", "107"] + for bar_name in LED_BAR_NAMES: + if bar_name in swap_bars: + # Swap n1 and n2 for out-of-phase bars + payload[bar_name] = {"n1": self.n2, "n2": self.n1} + else: + # In-phase bars use defaults (no override needed) + payload[bar_name] = {} + + logging.debug(f"[Alternating Phase] Beat {self.beat_index}, n1/n2 swap for bars {swap_bars}") + await self.ws_client.send_data(payload) + + async def _send_full_parameters(self): + """Send all parameters to bars - may require multiple packets due to size limit""" + from bar_config import LED_BAR_NAMES + + # Calculate packet size for full parameters + full_payload = { + "d": { + "pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern), + "dl": self.delay, + "cl": [self._current_color_rgb()], + "br": self.brightness, + "n1": self.n1, + "n2": self.n2, + "n3": self.n3, + } + } + + # Estimate size: ~200 bytes for defaults + 8 bars * 2 bytes = ~216 bytes + # This should fit in one packet, but let's be safe + payload_size = len(str(full_payload)) + + if payload_size > 200: # Split into 2 packets if too large + # Packet 1: Pattern and timing parameters + payload1 = { + "d": { + "pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern), + "dl": self.delay, + "br": self.brightness, + } + } + for bar_name in LED_BAR_NAMES: + payload1[bar_name] = {} + + # Packet 2: Color and pattern parameters + payload2 = { + "d": { + "cl": [self._current_color_rgb()], + "n1": self.n1, + "n2": self.n2, + "n3": self.n3, + } + } + for bar_name in LED_BAR_NAMES: + payload2[bar_name] = {} + + logging.debug(f"[Full Params] Sending in 2 packets due to size ({payload_size} bytes)") + await self.ws_client.send_data(payload1) + await asyncio.sleep(0.01) # Small delay between packets + await self.ws_client.send_data(payload2) + else: + # Single packet + for bar_name in LED_BAR_NAMES: + full_payload[bar_name] = {} + + logging.debug(f"[Full Params] Sending single packet ({payload_size} bytes)") + await self.ws_client.send_data(full_payload) + + async def _send_normal_pattern(self): + """Send normal pattern to all bars - minimal payload for beats""" + payload = { + "d": { # Defaults - only pattern + "pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern), + } + } + + # Add empty entries for each bar (they'll use defaults) + for bar_name in LED_BAR_NAMES: + payload[bar_name] = {} + + logging.debug(f"[Beat] Triggering '{self.current_pattern}' for {len(LED_BAR_NAMES)} bars using defaults") + await self.ws_client.send_data(payload) + async def _send_reset_to_sound(self): try: reader, writer = await asyncio.open_connection(self.sound_control_host, self.sound_control_port) @@ -90,22 +228,20 @@ class MidiHandler: logging.debug("[Beat] No pattern selected yet; ignoring beat") else: self.beat_index = (self.beat_index + 1) % 1000000 - if self.current_pattern: - payload1 = { - "0": { - "pattern": self.current_pattern, - "delay": self.delay, - "colors": [self._current_color_rgb()], - "brightness": self.brightness, - "num_leds": 200, - "n1": self.n1, - "n2": self.n2, - "n3": self.n3, - "n": self.beat_index, - } - } - logging.debug(f"[Beat] Triggering '{self.current_pattern}' with payload: {payload1}") - await self.ws_client.send_data(payload1) + + # Send periodic parameter updates every 8 beats + if self.beat_index % 8 == 0: + await self._send_full_parameters() + + if self.current_pattern == "sequential_pulse": + # Sequential pulse pattern: each bar pulses for 1 beat, then next bar, mirrored + await self._handle_sequential_pulse() + elif self.current_pattern == "alternating_phase": + # Alternating pattern with phase offset: every second bar is out of phase + await self._handle_alternating_phase() + elif self.current_pattern: + # Normal pattern mode - run on all bars + await self._send_normal_pattern() except ValueError: logging.warning(f"[MidiHandler - TCP Server] Received non-BPM message from {addr}, not forwarding: {message}") # Changed to warning except Exception as e: @@ -207,10 +343,10 @@ class MidiHandler: ("flicker", {}), ("alternating", {"n1": 6, "n2": 6}), ("n_chase", {"n1": 5, "n2": 5}), - # fill_range intentionally omitted from buttons ("rainbow", {}), - # specto intentionally omitted from buttons ("radiate", {"n1": 8}), + ("sequential_pulse", {}), + ("alternating_phase", {}), ] idx = msg.note - 36 if 0 <= idx < len(pattern_bindings): @@ -222,6 +358,9 @@ class MidiHandler: if "n2" in extra: self.n2 = extra["n2"] logging.info(f"[Select] Pattern selected via note {msg.note}: {self.current_pattern} (n1={self.n1}, n2={self.n2})") + + # Send full parameters when pattern changes + await self._send_full_parameters() else: logging.debug(f"Note {msg.note} not bound to patterns")