midi: init read of CCs on startup (delay, brightness, RGB, beat enable); track bpm/pattern for GUI\nmain: integrate MidiHandler; add status panel for delay/brightness/RGB/pattern/BPM
This commit is contained in:
734
src/main.py
734
src/main.py
@@ -5,8 +5,9 @@ import json
|
|||||||
from async_tkinter_loop import async_handler, async_mainloop
|
from async_tkinter_loop import async_handler, async_mainloop
|
||||||
from networking import WebSocketClient
|
from networking import WebSocketClient
|
||||||
import color_utils
|
import color_utils
|
||||||
from settings import Settings
|
|
||||||
import time
|
import time
|
||||||
|
from midi import MidiHandler # Import MidiHandler
|
||||||
|
|
||||||
# Dark theme colors (unchanged)
|
# Dark theme colors (unchanged)
|
||||||
bg_color = "#2e2e2e"
|
bg_color = "#2e2e2e"
|
||||||
@@ -24,28 +25,22 @@ active_palette_color_border = "#FFD700" # Gold color
|
|||||||
|
|
||||||
class App:
|
class App:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.settings = Settings()
|
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
self.root.attributes("-fullscreen", True)
|
# self.root.attributes("-fullscreen", True)
|
||||||
self.root.configure(bg=bg_color)
|
self.root.configure(bg=bg_color)
|
||||||
|
|
||||||
# Debouncing variables (remain the same)
|
|
||||||
self.last_rgb_update_time = 0
|
|
||||||
self.rgb_update_interval_ms = 100
|
|
||||||
self.pending_rgb_update_id = None
|
|
||||||
|
|
||||||
self.last_brightness_update_time = 0
|
|
||||||
self.brightness_update_interval_ms = 100
|
|
||||||
self.pending_brightness_update_id = None
|
|
||||||
|
|
||||||
self.last_delay_update_time = 0
|
|
||||||
self.delay_update_interval_ms = 100
|
|
||||||
self.pending_delay_update_id = None
|
|
||||||
|
|
||||||
# --- WebSocketClient ---
|
# --- WebSocketClient ---
|
||||||
self.websocket_client = WebSocketClient("ws://192.168.4.1:80/ws")
|
self.websocket_client = WebSocketClient("ws://192.168.4.1:80/ws")
|
||||||
self.root.after(100, async_handler(self.websocket_client.connect))
|
self.root.after(100, async_handler(self.websocket_client.connect))
|
||||||
|
|
||||||
|
# --- MIDI Handler ---
|
||||||
|
MIDI_PORT_INDEX = 1 # Adjust as needed
|
||||||
|
WEBSOCKET_SERVER_URI = "ws://192.168.4.1:80/ws"
|
||||||
|
self.midi_handler = MidiHandler(MIDI_PORT_INDEX, WEBSOCKET_SERVER_URI)
|
||||||
|
self.midi_task: asyncio.Task | None = None
|
||||||
|
# Start MIDI in background
|
||||||
|
self.root.after(0, async_handler(self.start_midi))
|
||||||
|
|
||||||
# Configure ttk style (unchanged)
|
# Configure ttk style (unchanged)
|
||||||
style = ttk.Style()
|
style = ttk.Style()
|
||||||
style.theme_use("alt")
|
style.theme_use("alt")
|
||||||
@@ -54,691 +49,68 @@ class App:
|
|||||||
style.configure(
|
style.configure(
|
||||||
"TNotebook.Tab", background=bg_color, foreground=fg_color, font=("Arial", 30), padding=[10, 5]
|
"TNotebook.Tab", background=bg_color, foreground=fg_color, font=("Arial", 30), padding=[10, 5]
|
||||||
)
|
)
|
||||||
style.map("TNotebook.Tab", background=[("selected", active_bg_color)], foreground=[("selected", fg_color)])
|
|
||||||
style.configure("TFrame", background=bg_color)
|
|
||||||
|
|
||||||
# Create Notebook for tabs (unchanged)
|
# --- Status Frame ---
|
||||||
self.notebook = ttk.Notebook(self.root)
|
status_frame = ttk.Frame(self.root)
|
||||||
self.notebook.pack(expand=1, fill="both")
|
status_frame.pack(padx=16, pady=16, fill="x")
|
||||||
|
|
||||||
self.tabs = {}
|
self.lbl_delay = ttk.Label(status_frame, text="Delay: -")
|
||||||
self.create_tabs()
|
self.lbl_brightness = ttk.Label(status_frame, text="Brightness: -")
|
||||||
|
self.lbl_r = ttk.Label(status_frame, text="R: -")
|
||||||
|
self.lbl_g = ttk.Label(status_frame, text="G: -")
|
||||||
|
self.lbl_b = ttk.Label(status_frame, text="B: -")
|
||||||
|
self.lbl_pattern = ttk.Label(status_frame, text="Pattern: -")
|
||||||
|
self.lbl_bpm = ttk.Label(status_frame, text="BPM: -")
|
||||||
|
|
||||||
self.notebook.bind("<<NotebookTabChanged>>", self.on_tab_change)
|
for w in (self.lbl_delay, self.lbl_brightness, self.lbl_r, self.lbl_g, self.lbl_b, self.lbl_pattern, self.lbl_bpm):
|
||||||
|
w.pack(anchor="w")
|
||||||
|
|
||||||
# Add Reload Config Button (unchanged)
|
# schedule periodic UI updates
|
||||||
reload_button = tk.Button(
|
self.root.after(200, self.update_status_labels)
|
||||||
self.root,
|
|
||||||
text="Reload Config",
|
|
||||||
command=self.reload_config,
|
|
||||||
bg=active_bg_color,
|
|
||||||
fg=fg_color,
|
|
||||||
font=("Arial", 20),
|
|
||||||
padx=20,
|
|
||||||
pady=10,
|
|
||||||
relief=tk.FLAT,
|
|
||||||
)
|
|
||||||
reload_button.pack(side=tk.BOTTOM, pady=20)
|
|
||||||
|
|
||||||
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||||
|
|
||||||
async_mainloop(self.root)
|
async_mainloop(self.root)
|
||||||
|
|
||||||
|
@async_handler
|
||||||
|
async def start_midi(self):
|
||||||
|
# Launch MidiHandler.run() as a background task
|
||||||
|
if self.midi_task is None or self.midi_task.done():
|
||||||
|
self.midi_task = asyncio.create_task(self.midi_handler.run())
|
||||||
|
|
||||||
def on_closing(self):
|
def on_closing(self):
|
||||||
print("Closing application...")
|
print("Closing application...")
|
||||||
|
if self.midi_task and not self.midi_task.done():
|
||||||
|
self.midi_task.cancel()
|
||||||
asyncio.create_task(self.websocket_client.close())
|
asyncio.create_task(self.websocket_client.close())
|
||||||
self.root.destroy()
|
self.root.destroy()
|
||||||
|
|
||||||
def create_tabs(self):
|
|
||||||
for tab_name in list(self.tabs.keys()):
|
|
||||||
self.notebook.forget(self.tabs[tab_name])
|
|
||||||
del self.tabs[tab_name]
|
|
||||||
|
|
||||||
for key, value in self.settings["lights"].items():
|
|
||||||
tab = ttk.Frame(self.notebook)
|
|
||||||
self.notebook.add(tab, text=key)
|
|
||||||
self.create_light_control_widgets(tab, key, value["names"], value["settings"])
|
|
||||||
self.tabs[key] = tab
|
|
||||||
|
|
||||||
def create_light_control_widgets(self, tab, tab_name, ids, initial_settings):
|
|
||||||
slider_length = 800
|
|
||||||
slider_width = 50
|
|
||||||
|
|
||||||
# Extract initial color, brightness, and delay
|
|
||||||
initial_colors = initial_settings.get("colors", ["#000000"])
|
|
||||||
initial_hex_color = initial_colors[0] if initial_colors else "#000000"
|
|
||||||
initial_brightness = initial_settings.get("brightness", 127)
|
|
||||||
initial_delay = initial_settings.get("delay", 0)
|
|
||||||
initial_pattern = initial_settings.get("pattern", "on")
|
|
||||||
|
|
||||||
initial_r, initial_g, initial_b = color_utils.hex_to_rgb(initial_hex_color)
|
|
||||||
|
|
||||||
# Main frame to hold everything within the tab
|
|
||||||
main_tab_frame = tk.Frame(tab, bg=bg_color)
|
|
||||||
main_tab_frame.pack(expand=True, fill="both", padx=10, pady=10)
|
|
||||||
|
|
||||||
# Left panel for sliders
|
|
||||||
slider_panel_frame = tk.Frame(main_tab_frame, bg=bg_color)
|
|
||||||
slider_panel_frame.pack(side=tk.LEFT, padx=10, pady=10)
|
|
||||||
|
|
||||||
# Common slider configuration
|
|
||||||
slider_config = {
|
|
||||||
"from_": 255,
|
|
||||||
"to": 0,
|
|
||||||
"orient": tk.VERTICAL,
|
|
||||||
"length": slider_length,
|
|
||||||
"width": slider_width,
|
|
||||||
"bg": bg_color,
|
|
||||||
"fg": fg_color,
|
|
||||||
"highlightbackground": bg_color,
|
|
||||||
"activebackground": active_bg_color,
|
|
||||||
"resolution": 1,
|
|
||||||
"sliderlength": 70,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Red Slider
|
|
||||||
red_slider = tk.Scale(slider_panel_frame, label="Red", troughcolor=trough_color_red, **slider_config)
|
|
||||||
red_slider.set(initial_r)
|
|
||||||
red_slider.pack(side=tk.LEFT, padx=10)
|
|
||||||
red_slider.bind("<B1-Motion>", lambda _: self.schedule_update_rgb(tab))
|
|
||||||
red_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_rgb(tab, force_send=True))
|
|
||||||
|
|
||||||
# Green Slider
|
|
||||||
green_slider = tk.Scale(slider_panel_frame, label="Green", troughcolor=trough_color_green, **slider_config)
|
|
||||||
green_slider.set(initial_g)
|
|
||||||
green_slider.pack(side=tk.LEFT, padx=10)
|
|
||||||
green_slider.bind("<B1-Motion>", lambda _: self.schedule_update_rgb(tab))
|
|
||||||
green_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_rgb(tab, force_send=True))
|
|
||||||
|
|
||||||
# Blue Slider
|
|
||||||
blue_slider = tk.Scale(slider_panel_frame, label="Blue", troughcolor=trough_color_blue, **slider_config)
|
|
||||||
blue_slider.set(initial_b)
|
|
||||||
blue_slider.pack(side=tk.LEFT, padx=10)
|
|
||||||
blue_slider.bind("<B1-Motion>", lambda _: self.schedule_update_rgb(tab))
|
|
||||||
blue_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_rgb(tab, force_send=True))
|
|
||||||
|
|
||||||
# Brightness Slider
|
|
||||||
brightness_slider = tk.Scale(
|
|
||||||
slider_panel_frame, label="Brightness", troughcolor=trough_color_brightness, **slider_config
|
|
||||||
)
|
|
||||||
brightness_slider.set(initial_brightness)
|
|
||||||
brightness_slider.pack(side=tk.LEFT, padx=10)
|
|
||||||
brightness_slider.bind("<B1-Motion>", lambda _: self.schedule_update_brightness(tab))
|
|
||||||
brightness_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_brightness(tab, force_send=True))
|
|
||||||
|
|
||||||
# Delay Slider
|
|
||||||
delay_slider_config = slider_config.copy()
|
|
||||||
delay_slider_config.update(
|
|
||||||
{
|
|
||||||
"from_": 1000,
|
|
||||||
"to": 0,
|
|
||||||
"resolution": 10,
|
|
||||||
"label": "Delay (ms)",
|
|
||||||
"troughcolor": trough_color_delay,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
delay_slider = tk.Scale(slider_panel_frame, **delay_slider_config)
|
|
||||||
delay_slider.set(initial_delay)
|
|
||||||
delay_slider.pack(side=tk.LEFT, padx=10)
|
|
||||||
delay_slider.bind("<B1-Motion>", lambda _: self.schedule_update_delay(tab))
|
|
||||||
delay_slider.bind("<ButtonRelease-1>", lambda _: self.schedule_update_delay(tab, force_send=True))
|
|
||||||
|
|
||||||
# Store references to widgets for this tab
|
|
||||||
tab.widgets = {
|
|
||||||
"red_slider": red_slider,
|
|
||||||
"green_slider": green_slider,
|
|
||||||
"blue_slider": blue_slider,
|
|
||||||
"brightness_slider": brightness_slider,
|
|
||||||
"delay_slider": delay_slider,
|
|
||||||
"selected_color_index": 0, # Default to the first color
|
|
||||||
}
|
|
||||||
tab.colors_in_palette = initial_colors.copy() # Store the list of hex colors for this tab
|
|
||||||
tab.color_swatch_frames = [] # To hold references to the color swatches
|
|
||||||
|
|
||||||
# Right panel for IDs, Patterns, and NEW Color Palette
|
|
||||||
right_panel_frame = tk.Frame(main_tab_frame, bg=bg_color)
|
|
||||||
right_panel_frame.pack(side=tk.LEFT, padx=20, pady=10, anchor="n", expand=True, fill="both")
|
|
||||||
|
|
||||||
# IDs section - MODIFIED TO BE SIDE-BY-SIDE
|
|
||||||
ids_frame = tk.Frame(right_panel_frame, bg=bg_color)
|
|
||||||
ids_frame.pack(pady=10, fill=tk.X)
|
|
||||||
tk.Label(ids_frame, text="Associated Names:", font=("Arial", 20), bg=bg_color, fg=fg_color).pack(pady=10)
|
|
||||||
|
|
||||||
# New inner frame for the IDs to be displayed horizontally
|
|
||||||
ids_inner_frame = tk.Frame(ids_frame, bg=bg_color)
|
|
||||||
ids_inner_frame.pack(fill=tk.X, expand=True) # Pack this frame to fill available width
|
|
||||||
|
|
||||||
for light_id in ids:
|
|
||||||
tk.Label(ids_inner_frame, text=str(light_id), font=("Arial", 18), bg=bg_color, fg=fg_color).pack(
|
|
||||||
side=tk.LEFT, padx=5, pady=2
|
|
||||||
) # Pack labels horizontally
|
|
||||||
|
|
||||||
# --- New Frame to hold Patterns and Color Palette side-by-side ---
|
|
||||||
patterns_and_palette_frame = tk.Frame(right_panel_frame, bg=bg_color)
|
|
||||||
patterns_and_palette_frame.pack(pady=20, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Patterns section
|
|
||||||
patterns_frame = tk.Frame(patterns_and_palette_frame, bg=bg_color, bd=2, relief=tk.GROOVE)
|
|
||||||
patterns_frame.pack(side=tk.LEFT, padx=10, pady=5, fill=tk.BOTH, expand=True) # Pack to the left
|
|
||||||
tk.Label(patterns_frame, text="Patterns:", font=("Arial", 20), bg=bg_color, fg=fg_color).pack(pady=10)
|
|
||||||
|
|
||||||
tab.pattern_buttons = {}
|
|
||||||
patterns = self.settings.get("patterns", [])
|
|
||||||
for pattern_name in patterns:
|
|
||||||
button = tk.Button(
|
|
||||||
patterns_frame,
|
|
||||||
text=pattern_name,
|
|
||||||
command=lambda p=pattern_name: self.send_pattern(tab_name, p),
|
|
||||||
bg=active_bg_color,
|
|
||||||
fg=fg_color,
|
|
||||||
font=("Arial", 18),
|
|
||||||
padx=15,
|
|
||||||
pady=5,
|
|
||||||
relief=tk.FLAT,
|
|
||||||
)
|
|
||||||
button.pack(pady=5, fill=tk.X)
|
|
||||||
tab.pattern_buttons[pattern_name] = button
|
|
||||||
|
|
||||||
self.highlight_pattern_button(tab, initial_pattern)
|
|
||||||
|
|
||||||
# --- Color Palette Editor Section ---
|
|
||||||
color_palette_editor_frame = tk.Frame(patterns_and_palette_frame, bg=bg_color, bd=2, relief=tk.GROOVE)
|
|
||||||
color_palette_editor_frame.pack(side=tk.LEFT, padx=10, pady=5, fill=tk.BOTH, expand=True) # Pack to the left
|
|
||||||
tab.color_palette_editor_frame = color_palette_editor_frame # Store reference for update_ui_for_pattern
|
|
||||||
|
|
||||||
tk.Label(color_palette_editor_frame, text="Color Palette:", font=("Arial", 20), bg=bg_color, fg=fg_color).pack(
|
|
||||||
pady=10
|
|
||||||
)
|
|
||||||
|
|
||||||
# Frame to hold color swatches (will be dynamic)
|
|
||||||
tab.color_swatches_container = tk.Frame(color_palette_editor_frame, bg=bg_color)
|
|
||||||
tab.color_swatches_container.pack(pady=5, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Buttons for Add/Remove Color
|
|
||||||
palette_buttons_frame = tk.Frame(color_palette_editor_frame, bg=bg_color)
|
|
||||||
palette_buttons_frame.pack(pady=10, fill=tk.X)
|
|
||||||
|
|
||||||
add_color_button = tk.Button(
|
|
||||||
palette_buttons_frame,
|
|
||||||
text="Add Color",
|
|
||||||
command=lambda t=tab: self.add_color_to_palette(t),
|
|
||||||
bg=active_bg_color,
|
|
||||||
fg=fg_color,
|
|
||||||
font=("Arial", 16),
|
|
||||||
padx=10,
|
|
||||||
pady=5,
|
|
||||||
relief=tk.FLAT,
|
|
||||||
)
|
|
||||||
add_color_button.pack(side=tk.LEFT, expand=True, padx=5)
|
|
||||||
|
|
||||||
remove_color_button = tk.Button(
|
|
||||||
palette_buttons_frame,
|
|
||||||
text="Remove Selected",
|
|
||||||
command=lambda t=tab: self.remove_selected_color_from_palette(t),
|
|
||||||
bg=active_bg_color,
|
|
||||||
fg=fg_color,
|
|
||||||
font=("Arial", 16),
|
|
||||||
padx=10,
|
|
||||||
pady=5,
|
|
||||||
relief=tk.FLAT,
|
|
||||||
)
|
|
||||||
remove_color_button.pack(side=tk.RIGHT, expand=True, padx=5)
|
|
||||||
|
|
||||||
# Initial population of the color palette
|
|
||||||
self.refresh_color_palette_display(tab)
|
|
||||||
|
|
||||||
# The initial call to update_ui_for_pattern now only sets slider values and highlights
|
|
||||||
self.update_ui_for_pattern(tab, initial_pattern)
|
|
||||||
|
|
||||||
def refresh_color_palette_display(self, tab):
|
|
||||||
"""Clears and repopulates the color swatches in the palette display."""
|
|
||||||
# Clear existing swatches
|
|
||||||
for frame in tab.color_swatch_frames:
|
|
||||||
frame.destroy()
|
|
||||||
tab.color_swatch_frames.clear()
|
|
||||||
|
|
||||||
for i, hex_color in enumerate(tab.colors_in_palette):
|
|
||||||
swatch_frame = tk.Frame(
|
|
||||||
tab.color_swatches_container, bg=hex_color, width=100, height=50, bd=2, relief=tk.SOLID
|
|
||||||
)
|
|
||||||
swatch_frame.pack(pady=3, padx=5, fill=tk.X)
|
|
||||||
# Bind click to select this color for editing
|
|
||||||
swatch_frame.bind("<Button-1>", lambda event, idx=i, t=tab: self.select_color_in_palette(t, idx))
|
|
||||||
|
|
||||||
# Add a label inside to make it clickable too
|
|
||||||
swatch_label = tk.Label(
|
|
||||||
swatch_frame,
|
|
||||||
text=f"Color {i+1}",
|
|
||||||
bg=hex_color,
|
|
||||||
fg=color_utils.get_contrast_text_color(hex_color),
|
|
||||||
font=("Arial", 14),
|
|
||||||
width=5,
|
|
||||||
height=3,
|
|
||||||
)
|
|
||||||
swatch_label.pack(expand=True, fill=tk.BOTH)
|
|
||||||
swatch_label.bind("<Button-1>", lambda event, idx=i, t=tab: self.select_color_in_palette(t, idx))
|
|
||||||
|
|
||||||
tab.color_swatch_frames.append(swatch_frame)
|
|
||||||
|
|
||||||
# Re-highlight the currently selected color
|
|
||||||
self._highlight_selected_color_swatch(tab)
|
|
||||||
|
|
||||||
def _highlight_selected_color_swatch(self, tab):
|
|
||||||
"""Applies/removes highlight border to the selected color swatch."""
|
|
||||||
current_index = tab.widgets["selected_color_index"]
|
|
||||||
for i, swatch_frame in enumerate(tab.color_swatch_frames):
|
|
||||||
if i == current_index:
|
|
||||||
swatch_frame.config(highlightbackground=active_palette_color_border, highlightthickness=3)
|
|
||||||
else:
|
|
||||||
swatch_frame.config(highlightbackground=swatch_frame.cget("bg"), highlightthickness=0) # Reset to no highlight
|
|
||||||
|
|
||||||
def select_color_in_palette(self, tab, index: int):
|
|
||||||
"""Selects a color in the palette, updates sliders, and highlights swatch.
|
|
||||||
This now also triggers an RGB update to the device."""
|
|
||||||
if not (0 <= index < len(tab.colors_in_palette)):
|
|
||||||
return
|
|
||||||
|
|
||||||
tab.widgets["selected_color_index"] = index
|
|
||||||
self._highlight_selected_color_swatch(tab)
|
|
||||||
|
|
||||||
# Update RGB sliders with the selected color
|
|
||||||
hex_color = tab.colors_in_palette[index]
|
|
||||||
r, g, b = color_utils.hex_to_rgb(hex_color)
|
|
||||||
tab.widgets["red_slider"].set(r)
|
|
||||||
tab.widgets["green_slider"].set(g)
|
|
||||||
tab.widgets["blue_slider"].set(b)
|
|
||||||
|
|
||||||
print(f"Selected color index {index}: {hex_color}")
|
|
||||||
|
|
||||||
# Immediately send the update, as changing the selected color implies
|
|
||||||
# a desire to change the light's current color, regardless of pattern.
|
|
||||||
# This will also save the settings.
|
|
||||||
self.schedule_update_rgb(tab, force_send=True)
|
|
||||||
|
|
||||||
def add_color_to_palette(self, tab):
|
|
||||||
"""Adds a new black color to the palette and selects it, with a limit of 10 colors."""
|
|
||||||
MAX_COLORS = 8 # Define the maximum number of colors allowed
|
|
||||||
|
|
||||||
if len(tab.colors_in_palette) >= MAX_COLORS:
|
|
||||||
messagebox.showwarning("Color Limit Reached", f"You can add a maximum of {MAX_COLORS} colors to the palette.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Simplified: just add black. If unique colors were required globally,
|
|
||||||
# more complex logic would be needed here.
|
|
||||||
tab.colors_in_palette.append("#000000") # Add black as default
|
|
||||||
self.refresh_color_palette_display(tab)
|
|
||||||
# Select the newly added color
|
|
||||||
self.select_color_in_palette(tab, len(tab.colors_in_palette) - 1)
|
|
||||||
self.save_current_tab_settings() # Save changes to settings.json
|
|
||||||
|
|
||||||
def remove_selected_color_from_palette(self, tab):
|
|
||||||
"""Removes the currently selected color from the palette."""
|
|
||||||
current_index = tab.widgets["selected_color_index"]
|
|
||||||
if len(tab.colors_in_palette) <= 1:
|
|
||||||
messagebox.showwarning("Cannot Remove", "There must be at least one color in the palette.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if messagebox.askyesno("Confirm Delete", f"Are you sure you want to remove Color {current_index + 1}?"):
|
|
||||||
del tab.colors_in_palette[current_index]
|
|
||||||
# Adjust selected index if the removed color was the last one
|
|
||||||
if current_index >= len(tab.colors_in_palette):
|
|
||||||
tab.widgets["selected_color_index"] = len(tab.colors_in_palette) - 1
|
|
||||||
if tab.widgets["selected_color_index"] < 0: # Should not happen with 1-color check
|
|
||||||
tab.widgets["selected_color_index"] = 0
|
|
||||||
|
|
||||||
self.refresh_color_palette_display(tab)
|
|
||||||
# Update sliders with the new selected color (if any)
|
|
||||||
if tab.colors_in_palette:
|
|
||||||
self.select_color_in_palette(tab, tab.widgets["selected_color_index"])
|
|
||||||
else: # If palette became empty (shouldn't happen with 1-color check)
|
|
||||||
tab.widgets["red_slider"].set(0)
|
|
||||||
tab.widgets["green_slider"].set(0)
|
|
||||||
tab.widgets["blue_slider"].set(0)
|
|
||||||
|
|
||||||
self.save_current_tab_settings() # Save changes to settings.json
|
|
||||||
|
|
||||||
def update_ui_for_pattern(self, tab, current_pattern: str):
|
|
||||||
"""
|
|
||||||
Manages the state of the UI elements based on the selected pattern.
|
|
||||||
The Color Palette Editor is always visible. RGB sliders update
|
|
||||||
based on the currently selected color in the palette, or the first
|
|
||||||
color if the palette is empty or not in transition mode and a new tab/pattern is selected.
|
|
||||||
"""
|
|
||||||
# The color_palette_editor_frame is always packed, so no visibility control needed here.
|
|
||||||
|
|
||||||
# When the pattern changes, we need to ensure the RGB sliders reflect
|
|
||||||
# the appropriate color based on the context.
|
|
||||||
|
|
||||||
if tab.colors_in_palette:
|
|
||||||
# If in 'transition' mode, set sliders to the currently selected color in the palette.
|
|
||||||
if current_pattern == "transition":
|
|
||||||
self.select_color_in_palette(tab, tab.widgets["selected_color_index"])
|
|
||||||
else:
|
|
||||||
# If not in 'transition' mode, but a color is selected, update sliders to that.
|
|
||||||
# Or, if this is a fresh load/tab change, ensure it's the first color.
|
|
||||||
# This ensures the sliders consistently show the color that will be sent
|
|
||||||
# for 'on'/'blink' based on the palette's first entry.
|
|
||||||
hex_color = tab.colors_in_palette[tab.widgets["selected_color_index"]]
|
|
||||||
r, g, b = color_utils.hex_to_rgb(hex_color)
|
|
||||||
tab.widgets["red_slider"].set(r)
|
|
||||||
tab.widgets["green_slider"].set(g)
|
|
||||||
tab.widgets["blue_slider"].set(b)
|
|
||||||
self._highlight_selected_color_swatch(tab) # Re-highlight even if index didn't change
|
|
||||||
else:
|
|
||||||
# Handle empty palette scenario (shouldn't happen with default ["#000000"])
|
|
||||||
tab.widgets["red_slider"].set(0)
|
|
||||||
tab.widgets["green_slider"].set(0)
|
|
||||||
tab.widgets["blue_slider"].set(0)
|
|
||||||
tab.widgets["selected_color_index"] = 0 # Ensure index is valid
|
|
||||||
self._highlight_selected_color_swatch(tab)
|
|
||||||
|
|
||||||
# Brightness and Delay sliders are always visible.
|
|
||||||
|
|
||||||
def highlight_pattern_button(self, tab_widget, active_pattern_name):
|
|
||||||
if hasattr(tab_widget, "pattern_buttons"):
|
|
||||||
for pattern_name, button in tab_widget.pattern_buttons.items():
|
|
||||||
if pattern_name == active_pattern_name:
|
|
||||||
button.config(bg=highlight_pattern_color)
|
|
||||||
else:
|
|
||||||
button.config(bg=active_bg_color)
|
|
||||||
|
|
||||||
def on_tab_change(self, event):
|
|
||||||
selected_tab_name = self.notebook.tab(self.notebook.select(), "text")
|
|
||||||
current_tab_widget = self.notebook.nametowidget(self.notebook.select())
|
|
||||||
|
|
||||||
initial_settings = self.settings["lights"][selected_tab_name]["settings"]
|
|
||||||
|
|
||||||
# Ensure current_tab_widget has the necessary attributes
|
|
||||||
if not hasattr(current_tab_widget, "colors_in_palette"):
|
|
||||||
# This tab might not have been fully initialized yet, or recreated
|
|
||||||
# In a full reload, create_tabs ensures it is.
|
|
||||||
return
|
|
||||||
|
|
||||||
# Update the local colors_in_palette list for the tab
|
|
||||||
current_tab_widget.colors_in_palette = initial_settings.get("colors", ["#000000"]).copy()
|
|
||||||
current_tab_widget.widgets["selected_color_index"] = 0 # Default to first color
|
|
||||||
|
|
||||||
# Refresh the color palette display and select the first color
|
|
||||||
self.refresh_color_palette_display(current_tab_widget)
|
|
||||||
if current_tab_widget.colors_in_palette:
|
|
||||||
self.select_color_in_palette(current_tab_widget, 0)
|
|
||||||
else: # If palette became empty (shouldn't happen with default ["#000000"])
|
|
||||||
current_tab_widget.widgets["red_slider"].set(0)
|
|
||||||
current_tab_widget.widgets["green_slider"].set(0)
|
|
||||||
current_tab_widget.widgets["blue_slider"].set(0)
|
|
||||||
|
|
||||||
# Update brightness and delay sliders
|
|
||||||
current_tab_widget.widgets["brightness_slider"].set(initial_settings.get("brightness", 127))
|
|
||||||
current_tab_widget.widgets["delay_slider"].set(initial_settings.get("delay", 0))
|
|
||||||
|
|
||||||
# Highlight the active pattern button
|
|
||||||
initial_pattern = initial_settings.get("pattern", "on")
|
|
||||||
self.highlight_pattern_button(current_tab_widget, initial_pattern)
|
|
||||||
|
|
||||||
# Update UI visibility based on the current pattern
|
|
||||||
self.update_ui_for_pattern(current_tab_widget, initial_pattern)
|
|
||||||
|
|
||||||
def reload_config(self):
|
|
||||||
print("Reloading configuration...")
|
|
||||||
self.settings = Settings()
|
|
||||||
self.create_tabs()
|
|
||||||
# After recreating, ensure the currently selected tab's sliders are updated
|
|
||||||
# Trigger on_tab_change manually for the currently selected tab
|
|
||||||
self.on_tab_change(None)
|
|
||||||
|
|
||||||
# --- Debouncing functions (no change to core logic, just how they call update_rgb) ---
|
|
||||||
def schedule_update_rgb(self, tab, force_send=False):
|
|
||||||
current_time = time.time() * 1000
|
|
||||||
if force_send:
|
|
||||||
if self.pending_rgb_update_id:
|
|
||||||
self.root.after_cancel(self.pending_rgb_update_id)
|
|
||||||
self.pending_rgb_update_id = None
|
|
||||||
self.update_rgb(tab)
|
|
||||||
self.last_rgb_update_time = current_time
|
|
||||||
elif current_time - self.last_rgb_update_time >= self.rgb_update_interval_ms:
|
|
||||||
if self.pending_rgb_update_id:
|
|
||||||
self.root.after_cancel(self.pending_rgb_update_id)
|
|
||||||
self.pending_rgb_update_id = None
|
|
||||||
self.update_rgb(tab)
|
|
||||||
self.last_rgb_update_time = current_time
|
|
||||||
else:
|
|
||||||
if self.pending_rgb_update_id:
|
|
||||||
self.root.after_cancel(self.pending_rgb_update_id)
|
|
||||||
time_to_wait = int(self.rgb_update_interval_ms - (current_time - self.last_rgb_update_time))
|
|
||||||
self.pending_rgb_update_id = self.root.after(time_to_wait, lambda: self.update_rgb(tab))
|
|
||||||
|
|
||||||
def schedule_update_brightness(self, tab, force_send=False):
|
|
||||||
current_time = time.time() * 1000
|
|
||||||
if force_send:
|
|
||||||
if self.pending_brightness_update_id:
|
|
||||||
self.root.after_cancel(self.pending_brightness_update_id)
|
|
||||||
self.pending_brightness_update_id = None
|
|
||||||
self.update_brightness(tab)
|
|
||||||
self.last_brightness_update_time = current_time
|
|
||||||
elif current_time - self.last_brightness_update_time >= self.brightness_update_interval_ms:
|
|
||||||
if self.pending_brightness_update_id:
|
|
||||||
self.root.after_cancel(self.pending_brightness_update_id)
|
|
||||||
self.pending_brightness_update_id = None
|
|
||||||
self.update_brightness(tab)
|
|
||||||
self.last_brightness_update_time = current_time
|
|
||||||
else:
|
|
||||||
if self.pending_brightness_update_id:
|
|
||||||
self.root.after_cancel(self.pending_brightness_update_id)
|
|
||||||
time_to_wait = int(self.brightness_update_interval_ms - (current_time - self.last_brightness_update_time))
|
|
||||||
self.pending_brightness_update_id = self.root.after(time_to_wait, lambda: self.update_brightness(tab))
|
|
||||||
|
|
||||||
def schedule_update_delay(self, tab, force_send=False):
|
|
||||||
current_time = time.time() * 1000
|
|
||||||
if force_send:
|
|
||||||
if self.pending_delay_update_id:
|
|
||||||
self.root.after_cancel(self.pending_delay_update_id)
|
|
||||||
self.pending_delay_update_id = None
|
|
||||||
self.update_delay(tab)
|
|
||||||
self.last_delay_update_time = current_time
|
|
||||||
elif current_time - self.last_delay_update_time >= self.delay_update_interval_ms:
|
|
||||||
if self.pending_delay_update_id:
|
|
||||||
self.root.after_cancel(self.pending_delay_update_id)
|
|
||||||
self.pending_delay_update_id = None
|
|
||||||
self.update_delay(tab)
|
|
||||||
self.last_delay_update_time = current_time
|
|
||||||
else:
|
|
||||||
if self.pending_delay_update_id:
|
|
||||||
self.root.after_cancel(self.pending_delay_update_id)
|
|
||||||
time_to_wait = int(self.delay_update_interval_ms - (current_time - self.last_delay_update_time))
|
|
||||||
self.pending_delay_update_id = self.root.after(time_to_wait, lambda: self.update_delay(tab))
|
|
||||||
|
|
||||||
# --- Asynchronous Update Functions ---
|
# --- Asynchronous Update Functions ---
|
||||||
@async_handler
|
@async_handler
|
||||||
async def update_rgb(self, tab):
|
async def update_rgb(self, tab):
|
||||||
"""Update the currently selected color in the palette and send to the server."""
|
pass
|
||||||
try:
|
|
||||||
red_slider = tab.widgets["red_slider"]
|
|
||||||
green_slider = tab.widgets["green_slider"]
|
|
||||||
blue_slider = tab.widgets["blue_slider"]
|
|
||||||
|
|
||||||
r = red_slider.get()
|
def update_status_labels(self):
|
||||||
g = green_slider.get()
|
# Pull values from midi_handler
|
||||||
b = blue_slider.get()
|
delay = self.midi_handler.delay
|
||||||
|
brightness = self.midi_handler.brightness
|
||||||
|
r = getattr(self.midi_handler, 'color_r', 0)
|
||||||
|
g = getattr(self.midi_handler, 'color_g', 0)
|
||||||
|
b = getattr(self.midi_handler, 'color_b', 0)
|
||||||
|
pattern = getattr(self.midi_handler, 'current_pattern', '') or '-'
|
||||||
|
bpm = getattr(self.midi_handler, 'current_bpm', None)
|
||||||
|
bpm_text = f"{bpm:.2f}" if isinstance(bpm, (float, int)) else "-"
|
||||||
|
|
||||||
hex_color = f"#{r:02x}{g:02x}{b:02x}"
|
self.lbl_delay.config(text=f"Delay: {delay}")
|
||||||
print(f"Updating selected color to: {hex_color}")
|
self.lbl_brightness.config(text=f"Brightness: {brightness}")
|
||||||
|
self.lbl_r.config(text=f"R: {r}")
|
||||||
|
self.lbl_g.config(text=f"G: {g}")
|
||||||
|
self.lbl_b.config(text=f"B: {b}")
|
||||||
|
self.lbl_pattern.config(text=f"Pattern: {pattern}")
|
||||||
|
self.lbl_bpm.config(text=f"BPM: {bpm_text}")
|
||||||
|
|
||||||
selected_color_index = tab.widgets["selected_color_index"]
|
# reschedule
|
||||||
if 0 <= selected_color_index < len(tab.colors_in_palette):
|
self.root.after(200, self.update_status_labels)
|
||||||
tab.colors_in_palette[selected_color_index] = hex_color
|
|
||||||
self.refresh_color_palette_display(tab) # Update swatch immediately
|
|
||||||
|
|
||||||
selected_server = self.notebook.tab(self.notebook.select(), "text")
|
|
||||||
names = self.settings["lights"][selected_server]["names"]
|
|
||||||
|
|
||||||
# Determine which colors to send based on the current pattern.
|
|
||||||
current_pattern = self.settings["lights"][selected_server]["settings"].get("pattern", "on")
|
|
||||||
colors_to_send = []
|
|
||||||
|
|
||||||
if current_pattern == "transition":
|
|
||||||
colors_to_send = tab.colors_in_palette.copy()
|
|
||||||
elif current_pattern in ["on", "blink", "theater_chase", "flicker"]: # Add other patterns that use a single color
|
|
||||||
if tab.colors_in_palette:
|
|
||||||
# For non-transition patterns, the device typically uses only the first color.
|
|
||||||
# However, if a user picks a color from the palette, we want THAT color to be the one
|
|
||||||
# sent and active. So, the selected color from the palette *becomes* the first color
|
|
||||||
# in the list we send to the device for these modes.
|
|
||||||
# This ensures the light matches the selected palette color.
|
|
||||||
colors_to_send = [hex_color] # Send the color currently set by the sliders
|
|
||||||
else:
|
|
||||||
colors_to_send = ["#000000"] # Default if palette is empty
|
|
||||||
else: # For other patterns like "off", "rainbow" where colors might not be primary
|
|
||||||
# We still want to send the *current* palette state for saving,
|
|
||||||
# but the device firmware might ignore it for these patterns.
|
|
||||||
colors_to_send = tab.colors_in_palette.copy()
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"save": True, # Always save this change to config
|
|
||||||
"names": names,
|
|
||||||
"settings": {
|
|
||||||
"colors": colors_to_send, # This now dynamically changes based on pattern
|
|
||||||
"brightness": tab.widgets["brightness_slider"].get(),
|
|
||||||
"delay": tab.widgets["delay_slider"].get(),
|
|
||||||
"pattern": current_pattern, # Always send the current pattern
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update the settings object with the new color list (and potentially other synced values)
|
|
||||||
self.settings["lights"][selected_server]["settings"]["colors"] = tab.colors_in_palette.copy()
|
|
||||||
self.settings["lights"][selected_server]["settings"]["brightness"] = tab.widgets["brightness_slider"].get()
|
|
||||||
self.settings["lights"][selected_server]["settings"]["delay"] = tab.widgets["delay_slider"].get()
|
|
||||||
self.settings.save()
|
|
||||||
|
|
||||||
await self.websocket_client.send_data(payload)
|
|
||||||
print(f"Sent RGB payload: {payload}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error updating RGB: {e}")
|
|
||||||
|
|
||||||
@async_handler
|
|
||||||
async def update_brightness(self, tab):
|
|
||||||
try:
|
|
||||||
brightness_slider = tab.widgets["brightness_slider"]
|
|
||||||
brightness = brightness_slider.get()
|
|
||||||
print(f"Brightness: {brightness}")
|
|
||||||
|
|
||||||
selected_server = self.notebook.tab(self.notebook.select(), "text")
|
|
||||||
names = self.settings["lights"][selected_server]["names"]
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"save": True,
|
|
||||||
"names": names,
|
|
||||||
"settings": {
|
|
||||||
"brightness": brightness,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
# Update the settings object with the new brightness
|
|
||||||
self.settings["lights"][selected_server]["settings"]["brightness"] = brightness
|
|
||||||
self.settings.save()
|
|
||||||
await self.websocket_client.send_data(payload)
|
|
||||||
print(f"Sent brightness payload: {payload}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error updating brightness: {e}")
|
|
||||||
|
|
||||||
@async_handler
|
|
||||||
async def update_delay(self, tab):
|
|
||||||
try:
|
|
||||||
delay_slider = tab.widgets["delay_slider"]
|
|
||||||
delay = delay_slider.get()
|
|
||||||
print(f"Delay: {delay}")
|
|
||||||
|
|
||||||
selected_server = self.notebook.tab(self.notebook.select(), "text")
|
|
||||||
names = self.settings["lights"][selected_server]["names"]
|
|
||||||
payload = {
|
|
||||||
"save": True,
|
|
||||||
"names": names,
|
|
||||||
"settings": {
|
|
||||||
"delay": delay,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
# Update the settings object with the new delay
|
|
||||||
self.settings["lights"][selected_server]["settings"]["delay"] = delay
|
|
||||||
self.settings.save()
|
|
||||||
await self.websocket_client.send_data(payload)
|
|
||||||
print(f"Sent delay payload: {payload}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error updating delay: {e}")
|
|
||||||
|
|
||||||
@async_handler
|
|
||||||
async def send_pattern(self, tab_name: str, pattern_name: str):
|
|
||||||
try:
|
|
||||||
names = self.settings["lights"][tab_name]["names"]
|
|
||||||
# Get the actual tab widget to access its `colors_in_palette` and other attributes
|
|
||||||
current_tab_widget = None
|
|
||||||
for key, tab_widget in self.tabs.items():
|
|
||||||
if key == tab_name:
|
|
||||||
current_tab_widget = tab_widget
|
|
||||||
break
|
|
||||||
|
|
||||||
if not current_tab_widget:
|
|
||||||
print(f"Error: Could not find tab widget for {tab_name}")
|
|
||||||
return
|
|
||||||
|
|
||||||
current_settings_for_tab = self.settings["lights"][tab_name]["settings"]
|
|
||||||
|
|
||||||
payload_settings = {
|
|
||||||
"pattern": pattern_name,
|
|
||||||
"brightness": current_settings_for_tab.get("brightness", 127),
|
|
||||||
"delay": current_settings_for_tab.get("delay", 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Determine colors to send based on the *newly selected* pattern
|
|
||||||
if pattern_name == "transition":
|
|
||||||
payload_settings["colors"] = current_tab_widget.colors_in_palette.copy()
|
|
||||||
elif pattern_name in ["on", "blink"]: # Add other patterns that use a single color here
|
|
||||||
# When switching TO 'on' or 'blink', ensure the color sent is the one
|
|
||||||
# currently displayed on the sliders (which reflects the selected palette color).
|
|
||||||
r = current_tab_widget.widgets["red_slider"].get()
|
|
||||||
g = current_tab_widget.widgets["green_slider"].get()
|
|
||||||
b = current_tab_widget.widgets["blue_slider"].get()
|
|
||||||
hex_color_from_sliders = f"#{r:02x}{g:02x}{b:02x}"
|
|
||||||
payload_settings["colors"] = [hex_color_from_sliders]
|
|
||||||
else:
|
|
||||||
# For other patterns, send the full palette, device might ignore or use default
|
|
||||||
payload_settings["colors"] = current_tab_widget.colors_in_palette.copy()
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"save": True,
|
|
||||||
"names": names,
|
|
||||||
"settings": payload_settings,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update the settings object with the new pattern and current colors/brightness/delay
|
|
||||||
self.settings["lights"][tab_name]["settings"]["pattern"] = pattern_name
|
|
||||||
# Always save the full current palette state in settings.
|
|
||||||
self.settings["lights"][tab_name]["settings"]["colors"] = current_tab_widget.colors_in_palette.copy()
|
|
||||||
self.settings.save()
|
|
||||||
|
|
||||||
self.highlight_pattern_button(current_tab_widget, pattern_name)
|
|
||||||
self.update_ui_for_pattern(current_tab_widget, pattern_name) # Update UI based on new pattern
|
|
||||||
|
|
||||||
await self.websocket_client.send_data(payload)
|
|
||||||
print(f"Sent pattern payload: {payload}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error sending pattern: {e}")
|
|
||||||
|
|
||||||
def save_current_tab_settings(self):
|
|
||||||
"""Saves the current state of the active tab's settings (colors, brightness, delay, pattern) to config."""
|
|
||||||
selected_server = self.notebook.tab(self.notebook.select(), "text")
|
|
||||||
current_tab_widget = self.notebook.nametowidget(self.notebook.select())
|
|
||||||
|
|
||||||
if not hasattr(current_tab_widget, "colors_in_palette"):
|
|
||||||
return # Tab not fully initialized yet
|
|
||||||
|
|
||||||
# Update settings for the current tab in the self.settings object
|
|
||||||
self.settings["lights"][selected_server]["settings"]["colors"] = current_tab_widget.colors_in_palette
|
|
||||||
self.settings["lights"][selected_server]["settings"]["brightness"] = current_tab_widget.widgets["brightness_slider"].get()
|
|
||||||
self.settings["lights"][selected_server]["settings"]["delay"] = current_tab_widget.widgets["delay_slider"].get()
|
|
||||||
# The pattern is updated in send_pattern already, but ensure consistency
|
|
||||||
# For simplicity, we assume send_pattern is the primary way to change pattern.
|
|
||||||
|
|
||||||
self.settings.save()
|
|
||||||
print(f"Saved settings for {selected_server}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
66
src/midi.py
66
src/midi.py
@@ -4,6 +4,7 @@ import networking
|
|||||||
import socket
|
import socket
|
||||||
import json
|
import json
|
||||||
import logging # Added logging import
|
import logging # Added logging import
|
||||||
|
import time # Added for initial state read
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
DEBUG_MODE = True # Set to False for INFO level logging
|
DEBUG_MODE = True # Set to False for INFO level logging
|
||||||
@@ -29,6 +30,19 @@ class MidiHandler:
|
|||||||
self.beat_sending_enabled = True # New: Local flag for beat sending
|
self.beat_sending_enabled = True # New: Local flag for beat sending
|
||||||
self.sound_control_host = SOUND_CONTROL_HOST
|
self.sound_control_host = SOUND_CONTROL_HOST
|
||||||
self.sound_control_port = SOUND_CONTROL_PORT
|
self.sound_control_port = SOUND_CONTROL_PORT
|
||||||
|
# RGB controlled by CC 30/31/32 (default green)
|
||||||
|
self.color_r = 0
|
||||||
|
self.color_g = 255
|
||||||
|
self.color_b = 0
|
||||||
|
# Current state for GUI display
|
||||||
|
self.current_bpm: float | None = None
|
||||||
|
self.current_pattern: str = ""
|
||||||
|
|
||||||
|
def _current_color_hex(self) -> str:
|
||||||
|
r = max(0, min(255, int(self.color_r)))
|
||||||
|
g = max(0, min(255, int(self.color_g)))
|
||||||
|
b = max(0, min(255, int(self.color_b)))
|
||||||
|
return f"#{r:02x}{g:02x}{b:02x}"
|
||||||
|
|
||||||
async def _send_reset_to_sound(self):
|
async def _send_reset_to_sound(self):
|
||||||
try:
|
try:
|
||||||
@@ -61,13 +75,14 @@ class MidiHandler:
|
|||||||
try:
|
try:
|
||||||
# Attempt to parse as float (BPM) from sound.py
|
# Attempt to parse as float (BPM) from sound.py
|
||||||
bpm_value = float(message)
|
bpm_value = float(message)
|
||||||
|
self.current_bpm = bpm_value
|
||||||
# Construct JSON message using the current MIDI-controlled delay and brightness
|
# Construct JSON message using the current MIDI-controlled delay and brightness
|
||||||
json_message = {
|
json_message = {
|
||||||
"names": ["0"],
|
"names": ["0"],
|
||||||
"settings": {
|
"settings": {
|
||||||
"pattern": "pulse",
|
"pattern": "pulse",
|
||||||
"delay": self.delay, # Use MIDI-controlled delay
|
"delay": self.delay, # Use MIDI-controlled delay
|
||||||
"colors": ["#00ff00"],
|
"colors": [self._current_color_hex()],
|
||||||
"brightness": self.brightness,
|
"brightness": self.brightness,
|
||||||
"num_leds": 200,
|
"num_leds": 200,
|
||||||
},
|
},
|
||||||
@@ -100,6 +115,32 @@ class MidiHandler:
|
|||||||
async with server:
|
async with server:
|
||||||
await server.serve_forever()
|
await server.serve_forever()
|
||||||
|
|
||||||
|
async def _read_initial_cc_state(self, port, timeout_s: float = 0.5):
|
||||||
|
"""Read initial CC values from the MIDI device for a short period to populate state."""
|
||||||
|
start = time.time()
|
||||||
|
while time.time() - start < timeout_s:
|
||||||
|
msg = port.receive(block=False)
|
||||||
|
if msg and msg.type == 'control_change':
|
||||||
|
if msg.control == 36:
|
||||||
|
self.delay = msg.value * 4
|
||||||
|
logging.info(f"[Init] Delay set to {self.delay} ms from CC36")
|
||||||
|
elif msg.control == 37:
|
||||||
|
self.brightness = round((msg.value / 127) * 100)
|
||||||
|
logging.info(f"[Init] Brightness set to {self.brightness} from CC37")
|
||||||
|
elif msg.control == 30:
|
||||||
|
self.color_r = round((msg.value / 127) * 255)
|
||||||
|
logging.info(f"[Init] Red set to {self.color_r} from CC30")
|
||||||
|
elif msg.control == 31:
|
||||||
|
self.color_g = round((msg.value / 127) * 255)
|
||||||
|
logging.info(f"[Init] Green set to {self.color_g} from CC31")
|
||||||
|
elif msg.control == 32:
|
||||||
|
self.color_b = round((msg.value / 127) * 255)
|
||||||
|
logging.info(f"[Init] Blue set to {self.color_b} from CC32")
|
||||||
|
elif msg.control == 27:
|
||||||
|
self.beat_sending_enabled = (msg.value == 127)
|
||||||
|
logging.info(f"[Init] Beat sending {'ENABLED' if self.beat_sending_enabled else 'DISABLED'} from CC27")
|
||||||
|
await asyncio.sleep(0.001)
|
||||||
|
|
||||||
async def _midi_listener(self):
|
async def _midi_listener(self):
|
||||||
logging.info("Midi function") # Changed to info
|
logging.info("Midi function") # Changed to info
|
||||||
"""
|
"""
|
||||||
@@ -125,6 +166,8 @@ class MidiHandler:
|
|||||||
try:
|
try:
|
||||||
with mido.open_input(midi_port_name) as port:
|
with mido.open_input(midi_port_name) as port:
|
||||||
logging.info(f"MIDI port '{midi_port_name}' opened. Press Ctrl+C to stop.") # Changed to info
|
logging.info(f"MIDI port '{midi_port_name}' opened. Press Ctrl+C to stop.") # Changed to info
|
||||||
|
# Read initial controller state briefly
|
||||||
|
await self._read_initial_cc_state(port)
|
||||||
while True:
|
while True:
|
||||||
msg = port.receive(block=False) # Non-blocking read
|
msg = port.receive(block=False) # Non-blocking read
|
||||||
if msg:
|
if msg:
|
||||||
@@ -137,24 +180,26 @@ class MidiHandler:
|
|||||||
match msg.note:
|
match msg.note:
|
||||||
case 48: # Original Note 48 for 'pulse'
|
case 48: # Original Note 48 for 'pulse'
|
||||||
pattern_name = "pulse"
|
pattern_name = "pulse"
|
||||||
|
self.current_pattern = pattern_name
|
||||||
await self.ws_client.send_data({
|
await self.ws_client.send_data({
|
||||||
"names": ["0"],
|
"names": ["0"],
|
||||||
"settings": {
|
"settings": {
|
||||||
"pattern": pattern_name,
|
"pattern": pattern_name,
|
||||||
"delay": self.delay, # Use MIDI-controlled delay
|
"delay": self.delay, # Use MIDI-controlled delay
|
||||||
"colors": ["#00ff00"],
|
"colors": [self._current_color_hex()],
|
||||||
"brightness": self.brightness,
|
"brightness": self.brightness,
|
||||||
"num_leds": 120,
|
"num_leds": 120,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
case 49: # Original Note 49 for 'theater_chase'
|
case 49: # Original Note 49 for 'theater_chase'
|
||||||
pattern_name = "theater_chase"
|
pattern_name = "theater_chase"
|
||||||
|
self.current_pattern = pattern_name
|
||||||
await self.ws_client.send_data({
|
await self.ws_client.send_data({
|
||||||
"names": ["0"],
|
"names": ["0"],
|
||||||
"settings": {
|
"settings": {
|
||||||
"pattern": pattern_name,
|
"pattern": pattern_name,
|
||||||
"delay": self.delay, # Use MIDI-controlled delay
|
"delay": self.delay, # Use MIDI-controlled delay
|
||||||
"colors": ["#00ff00"],
|
"colors": [self._current_color_hex()],
|
||||||
"brightness": self.brightness,
|
"brightness": self.brightness,
|
||||||
"num_leds": 120,
|
"num_leds": 120,
|
||||||
"on_width": 10,
|
"on_width": 10,
|
||||||
@@ -165,13 +210,14 @@ class MidiHandler:
|
|||||||
})
|
})
|
||||||
case 50: # Original Note 50 for 'alternating'
|
case 50: # Original Note 50 for 'alternating'
|
||||||
pattern_name = "alternating"
|
pattern_name = "alternating"
|
||||||
|
self.current_pattern = pattern_name
|
||||||
logging.debug("Triggering Alternating Pattern") # Changed to debug
|
logging.debug("Triggering Alternating Pattern") # Changed to debug
|
||||||
await self.ws_client.send_data({
|
await self.ws_client.send_data({
|
||||||
"names": ["0"],
|
"names": ["0"],
|
||||||
"settings": {
|
"settings": {
|
||||||
"pattern": pattern_name,
|
"pattern": pattern_name,
|
||||||
"delay": self.delay, # Use MIDI-controlled delay
|
"delay": self.delay, # Use MIDI-controlled delay
|
||||||
"colors": ["#00ff00", "#0000ff"],
|
"colors": [self._current_color_hex(), "#0000ff"],
|
||||||
"brightness": self.brightness,
|
"brightness": self.brightness,
|
||||||
"num_leds": 120,
|
"num_leds": 120,
|
||||||
"n1": 10,
|
"n1": 10,
|
||||||
@@ -199,6 +245,18 @@ 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")
|
logging.info(f"Brightness set to {self.brightness} by MIDI controller")
|
||||||
|
case 30:
|
||||||
|
# Red 0-127 -> 0-255
|
||||||
|
self.color_r = round((msg.value / 127) * 255)
|
||||||
|
logging.info(f"Red set to {self.color_r}")
|
||||||
|
case 31:
|
||||||
|
# Green 0-127 -> 0-255
|
||||||
|
self.color_g = round((msg.value / 127) * 255)
|
||||||
|
logging.info(f"Green set to {self.color_g}")
|
||||||
|
case 32:
|
||||||
|
# Blue 0-127 -> 0-255
|
||||||
|
self.color_b = round((msg.value / 127) * 255)
|
||||||
|
logging.info(f"Blue set to {self.color_b}")
|
||||||
|
|
||||||
await asyncio.sleep(0.001) # Important: Yield control to asyncio event loop
|
await asyncio.sleep(0.001) # Important: Yield control to asyncio event loop
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user