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:
2025-09-18 21:58:39 +12:00
parent 36dfda74b2
commit fcbe9e9094
3 changed files with 200 additions and 21 deletions

38
src/bar_config.py Normal file
View 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

View File

@@ -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",
"-", "-", "-", "-",
"-", "-", "-", "-",
]

View File

@@ -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")