Implement full parameter sending on pattern change and periodic updates
- Send all parameters when pattern changes (may require 2 packets if >200 bytes) - Send periodic parameter updates every 8 beats to keep bars synchronized - Beat packets remain minimal for performance - All packets stay under 230-byte limit
This commit is contained in:
38
src/bar_config.py
Normal file
38
src/bar_config.py
Normal file
@@ -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
|
@@ -199,14 +199,16 @@ class App:
|
|||||||
"pulse": "💥",
|
"pulse": "💥",
|
||||||
"flicker": "✨",
|
"flicker": "✨",
|
||||||
"alternating": "↔️",
|
"alternating": "↔️",
|
||||||
"n_chase": "🏃",
|
"n chase": "🏃",
|
||||||
"rainbow": "🌈",
|
"rainbow": "🌈",
|
||||||
"radiate": "🌟",
|
"radiate": "🌟",
|
||||||
|
"sequential\npulse": "🔄",
|
||||||
|
"alternating\nphase": "⚡",
|
||||||
"-": "",
|
"-": "",
|
||||||
}
|
}
|
||||||
bank1_patterns = [
|
bank1_patterns = [
|
||||||
"pulse", "flicker", "alternating", "n_chase",
|
"pulse", "flicker", "alternating", "n chase",
|
||||||
"rainbow", "radiate", "-", "-",
|
"rainbow", "radiate", "sequential\npulse", "alternating\nphase",
|
||||||
"-", "-", "-", "-",
|
"-", "-", "-", "-",
|
||||||
"-", "-", "-", "-",
|
"-", "-", "-", "-",
|
||||||
]
|
]
|
||||||
|
175
src/midi.py
175
src/midi.py
@@ -7,6 +7,19 @@ import logging # Added logging import
|
|||||||
import time # Added for initial state read
|
import time # Added for initial state read
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox # Import messagebox for confirmations
|
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
|
# Configure logging
|
||||||
@@ -46,6 +59,9 @@ class MidiHandler:
|
|||||||
self.current_bpm: float | None = None
|
self.current_bpm: float | None = None
|
||||||
self.current_pattern: str = ""
|
self.current_pattern: str = ""
|
||||||
self.beat_index: int = 0
|
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:
|
def _current_color_rgb(self) -> tuple:
|
||||||
r = max(0, min(255, int(self.color_r)))
|
r = max(0, min(255, int(self.color_r)))
|
||||||
@@ -53,6 +69,128 @@ class MidiHandler:
|
|||||||
b = max(0, min(255, int(self.color_b)))
|
b = max(0, min(255, int(self.color_b)))
|
||||||
return (r, g, 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):
|
async def _send_reset_to_sound(self):
|
||||||
try:
|
try:
|
||||||
reader, writer = await asyncio.open_connection(self.sound_control_host, self.sound_control_port)
|
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")
|
logging.debug("[Beat] No pattern selected yet; ignoring beat")
|
||||||
else:
|
else:
|
||||||
self.beat_index = (self.beat_index + 1) % 1000000
|
self.beat_index = (self.beat_index + 1) % 1000000
|
||||||
if self.current_pattern:
|
|
||||||
payload1 = {
|
# Send periodic parameter updates every 8 beats
|
||||||
"0": {
|
if self.beat_index % 8 == 0:
|
||||||
"pattern": self.current_pattern,
|
await self._send_full_parameters()
|
||||||
"delay": self.delay,
|
|
||||||
"colors": [self._current_color_rgb()],
|
if self.current_pattern == "sequential_pulse":
|
||||||
"brightness": self.brightness,
|
# Sequential pulse pattern: each bar pulses for 1 beat, then next bar, mirrored
|
||||||
"num_leds": 200,
|
await self._handle_sequential_pulse()
|
||||||
"n1": self.n1,
|
elif self.current_pattern == "alternating_phase":
|
||||||
"n2": self.n2,
|
# Alternating pattern with phase offset: every second bar is out of phase
|
||||||
"n3": self.n3,
|
await self._handle_alternating_phase()
|
||||||
"n": self.beat_index,
|
elif self.current_pattern:
|
||||||
}
|
# Normal pattern mode - run on all bars
|
||||||
}
|
await self._send_normal_pattern()
|
||||||
logging.debug(f"[Beat] Triggering '{self.current_pattern}' with payload: {payload1}")
|
|
||||||
await self.ws_client.send_data(payload1)
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logging.warning(f"[MidiHandler - TCP Server] Received non-BPM message from {addr}, not forwarding: {message}") # Changed to warning
|
logging.warning(f"[MidiHandler - TCP Server] Received non-BPM message from {addr}, not forwarding: {message}") # Changed to warning
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -207,10 +343,10 @@ class MidiHandler:
|
|||||||
("flicker", {}),
|
("flicker", {}),
|
||||||
("alternating", {"n1": 6, "n2": 6}),
|
("alternating", {"n1": 6, "n2": 6}),
|
||||||
("n_chase", {"n1": 5, "n2": 5}),
|
("n_chase", {"n1": 5, "n2": 5}),
|
||||||
# fill_range intentionally omitted from buttons
|
|
||||||
("rainbow", {}),
|
("rainbow", {}),
|
||||||
# specto intentionally omitted from buttons
|
|
||||||
("radiate", {"n1": 8}),
|
("radiate", {"n1": 8}),
|
||||||
|
("sequential_pulse", {}),
|
||||||
|
("alternating_phase", {}),
|
||||||
]
|
]
|
||||||
idx = msg.note - 36
|
idx = msg.note - 36
|
||||||
if 0 <= idx < len(pattern_bindings):
|
if 0 <= idx < len(pattern_bindings):
|
||||||
@@ -222,6 +358,9 @@ class MidiHandler:
|
|||||||
if "n2" in extra:
|
if "n2" in extra:
|
||||||
self.n2 = extra["n2"]
|
self.n2 = extra["n2"]
|
||||||
logging.info(f"[Select] Pattern selected via note {msg.note}: {self.current_pattern} (n1={self.n1}, n2={self.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:
|
else:
|
||||||
logging.debug(f"Note {msg.note} not bound to patterns")
|
logging.debug(f"Note {msg.note} not bound to patterns")
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user