Add rate-limited parameter updates and message type system

- Rate limit parameter updates to 100ms minimum interval
- Send immediate updates if rate limit allows, otherwise queue
- Process pending updates during beat handling
- All knob changes (CC30-37) now trigger parameter updates
- Add message type field: 'b' for beats, 'u' for updates
- Optimize message type to single letters to save packet space
- Prevents ESP-NOW network flooding during rapid knob adjustments
- All packets stay under 230-byte limit with automatic splitting
This commit is contained in:
2025-09-18 22:11:17 +12:00
parent fcbe9e9094
commit 5f7db51851

View File

@@ -59,6 +59,12 @@ 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
# Rate limiting for parameter updates
self.last_param_update: float = 0.0
self.param_update_interval: float = 0.1 # 100ms minimum between updates
self.pending_param_update: bool = False
# Sequential pulse pattern state # Sequential pulse pattern state
self.sequential_pulse_enabled: bool = False self.sequential_pulse_enabled: bool = False
self.sequential_pulse_step: int = 0 self.sequential_pulse_step: int = 0
@@ -79,6 +85,7 @@ class MidiHandler:
# Create minimal payload - only send pattern # Create minimal payload - only send pattern
payload = { payload = {
"d": { # Defaults - only pattern "d": { # Defaults - only pattern
"t": "b", # Message type: beat
"pt": "p", # pulse "pt": "p", # pulse
} }
} }
@@ -100,6 +107,7 @@ class MidiHandler:
# Create minimal payload - only send what changes # Create minimal payload - only send what changes
payload = { payload = {
"d": { # Defaults - only pattern and n1/n2 "d": { # Defaults - only pattern and n1/n2
"t": "b", # Message type: beat
"pt": "a", # alternating "pt": "a", # alternating
"n1": self.n1, "n1": self.n1,
"n2": self.n2, "n2": self.n2,
@@ -126,6 +134,7 @@ class MidiHandler:
# Calculate packet size for full parameters # Calculate packet size for full parameters
full_payload = { full_payload = {
"d": { "d": {
"t": "u", # Message type: update
"pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern), "pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern),
"dl": self.delay, "dl": self.delay,
"cl": [self._current_color_rgb()], "cl": [self._current_color_rgb()],
@@ -144,6 +153,7 @@ class MidiHandler:
# Packet 1: Pattern and timing parameters # Packet 1: Pattern and timing parameters
payload1 = { payload1 = {
"d": { "d": {
"t": "u", # Message type: update
"pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern), "pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern),
"dl": self.delay, "dl": self.delay,
"br": self.brightness, "br": self.brightness,
@@ -155,6 +165,7 @@ class MidiHandler:
# Packet 2: Color and pattern parameters # Packet 2: Color and pattern parameters
payload2 = { payload2 = {
"d": { "d": {
"t": "u", # Message type: update
"cl": [self._current_color_rgb()], "cl": [self._current_color_rgb()],
"n1": self.n1, "n1": self.n1,
"n2": self.n2, "n2": self.n2,
@@ -176,10 +187,26 @@ class MidiHandler:
logging.debug(f"[Full Params] Sending single packet ({payload_size} bytes)") logging.debug(f"[Full Params] Sending single packet ({payload_size} bytes)")
await self.ws_client.send_data(full_payload) await self.ws_client.send_data(full_payload)
async def _request_param_update(self):
"""Request a parameter update with rate limiting"""
import time
current_time = time.time()
if current_time - self.last_param_update >= self.param_update_interval:
# Can send immediately
self.last_param_update = current_time
await self._send_full_parameters()
logging.debug("[Rate Limit] Parameter update sent immediately")
else:
# Rate limited - mark as pending
self.pending_param_update = True
logging.debug("[Rate Limit] Parameter update queued (rate limited)")
async def _send_normal_pattern(self): async def _send_normal_pattern(self):
"""Send normal pattern to all bars - minimal payload for beats""" """Send normal pattern to all bars - minimal payload for beats"""
payload = { payload = {
"d": { # Defaults - only pattern "d": { # Defaults - only pattern
"t": "b", # Message type: beat
"pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern), "pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern),
} }
} }
@@ -233,6 +260,16 @@ class MidiHandler:
if self.beat_index % 8 == 0: if self.beat_index % 8 == 0:
await self._send_full_parameters() await self._send_full_parameters()
# Check for pending parameter updates (rate limited)
if self.pending_param_update:
import time
current_time = time.time()
if current_time - self.last_param_update >= self.param_update_interval:
self.last_param_update = current_time
self.pending_param_update = False
await self._send_full_parameters()
logging.debug("[Rate Limit] Pending parameter update sent")
if self.current_pattern == "sequential_pulse": if self.current_pattern == "sequential_pulse":
# Sequential pulse pattern: each bar pulses for 1 beat, then next bar, mirrored # Sequential pulse pattern: each bar pulses for 1 beat, then next bar, mirrored
await self._handle_sequential_pulse() await self._handle_sequential_pulse()
@@ -369,9 +406,11 @@ class MidiHandler:
case 36: case 36:
self.n3 = max(1, msg.value) # Update n3 step rate self.n3 = max(1, msg.value) # Update n3 step rate
logging.info(f"n3 set to {self.n3} by MIDI controller (CC36)") logging.info(f"n3 set to {self.n3} by MIDI controller (CC36)")
await self._request_param_update()
case 37: case 37:
self.delay = msg.value * 4 # Update instance delay self.delay = msg.value * 4 # Update instance delay
logging.info(f"Delay set to {self.delay} ms by MIDI controller (CC37)") logging.info(f"Delay set to {self.delay} ms by MIDI controller (CC37)")
await self._request_param_update()
case 27: case 27:
if msg.value == 127: if msg.value == 127:
self.beat_sending_enabled = True self.beat_sending_enabled = True
@@ -387,24 +426,30 @@ class MidiHandler:
# Map 0-127 to 0-100 brightness scale # Map 0-127 to 0-100 brightness scale
self.brightness = round((msg.value / 127) * 100) self.brightness = round((msg.value / 127) * 100)
logging.info(f"Brightness set to {self.brightness} by MIDI controller (CC33)") logging.info(f"Brightness set to {self.brightness} by MIDI controller (CC33)")
await self._request_param_update()
case 30: case 30:
# Red 0-127 -> 0-255 # Red 0-127 -> 0-255
self.color_r = round((msg.value / 127) * 255) self.color_r = round((msg.value / 127) * 255)
logging.info(f"Red set to {self.color_r}") logging.info(f"Red set to {self.color_r}")
await self._request_param_update()
case 31: case 31:
# Green 0-127 -> 0-255 # Green 0-127 -> 0-255
self.color_g = round((msg.value / 127) * 255) self.color_g = round((msg.value / 127) * 255)
logging.info(f"Green set to {self.color_g}") logging.info(f"Green set to {self.color_g}")
await self._request_param_update()
case 32: case 32:
# Blue 0-127 -> 0-255 # Blue 0-127 -> 0-255
self.color_b = round((msg.value / 127) * 255) self.color_b = round((msg.value / 127) * 255)
logging.info(f"Blue set to {self.color_b}") logging.info(f"Blue set to {self.color_b}")
await self._request_param_update()
case 34: case 34:
self.n1 = int(msg.value) self.n1 = int(msg.value)
logging.info(f"n1 set to {self.n1} by MIDI controller (CC34)") logging.info(f"n1 set to {self.n1} by MIDI controller (CC34)")
await self._request_param_update()
case 35: case 35:
self.n2 = int(msg.value) self.n2 = int(msg.value)
logging.info(f"n2 set to {self.n2} by MIDI controller (CC35)") logging.info(f"n2 set to {self.n2} by MIDI controller (CC35)")
await self._request_param_update()
await asyncio.sleep(0.001) # Important: Yield control to asyncio event loop await asyncio.sleep(0.001) # Important: Yield control to asyncio event loop