From f9c3d08b0f98524e1c597ee3b25eb56bfb21860d Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sat, 27 Sep 2025 23:11:42 +1200 Subject: [PATCH] Add MIDI controller dropdown selection - Add MIDI port detection and listing functionality - Create dropdown widget for MIDI controller selection in GUI - Implement dynamic MIDI controller switching without restart - Add refresh button to scan for new MIDI devices - Add status indicator showing connection status - Add comprehensive error handling for MIDI operations - Fix initialization timing issues with GUI widgets - Support graceful fallback when no MIDI devices available --- src/main.py | 189 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 180 insertions(+), 9 deletions(-) diff --git a/src/main.py b/src/main.py index 8d46e6e..d7b255f 100644 --- a/src/main.py +++ b/src/main.py @@ -5,6 +5,7 @@ import json from async_tkinter_loop import async_handler, async_mainloop from networking import WebSocketClient import color_utils +import mido # Import mido for MIDI port detection import time from midi import MidiHandler # Import MidiHandler @@ -33,15 +34,14 @@ class App: self.websocket_client = WebSocketClient("ws://192.168.4.1:80/ws") 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" - print(f"Initializing MIDI handler with port index {MIDI_PORT_INDEX}") - self.midi_handler = MidiHandler(MIDI_PORT_INDEX, WEBSOCKET_SERVER_URI) - print("MIDI handler initialized") + # --- MIDI Configuration --- + self.available_midi_ports = self.get_midi_ports() + self.current_midi_port_index = 0 # Default to first port + self.midi_handler: MidiHandler | None = None self.midi_task: asyncio.Task | None = None - # Start MIDI in background - self.root.after(0, async_handler(self.start_midi)) + + # Initialize MIDI handler with first available port (will be done after GUI is created) + self.pending_midi_init = True if self.available_midi_ports else False # Configure ttk style (unchanged) style = ttk.Style() @@ -54,6 +54,48 @@ class App: # (Status box removed per request) + # MIDI Controller Selection + midi_frame = ttk.LabelFrame(self.root, text="MIDI Controller") + midi_frame.pack(padx=16, pady=8, fill="x") + + # MIDI port dropdown + self.midi_port_var = tk.StringVar() + if self.available_midi_ports: + self.midi_port_var.set(self.available_midi_ports[0]) + + midi_dropdown = ttk.Combobox( + midi_frame, + textvariable=self.midi_port_var, + values=self.available_midi_ports, + state="readonly", + font=("Arial", 12) + ) + midi_dropdown.pack(padx=8, pady=4, fill="x") + midi_dropdown.bind("<>", self.on_midi_port_change) + + # Refresh MIDI ports button + refresh_button = ttk.Button( + midi_frame, + text="Refresh MIDI Ports", + command=self.refresh_midi_ports + ) + refresh_button.pack(padx=8, pady=4) + + # MIDI connection status + self.midi_status_label = tk.Label( + midi_frame, + text="Status: Disconnected", + bg=bg_color, + fg="red", + font=("Arial", 10) + ) + self.midi_status_label.pack(padx=8, pady=2) + + # Initialize MIDI handler now that GUI is ready + if self.pending_midi_init: + self.initialize_midi_handler() + self.pending_midi_init = False + # Controls overview (dials grid left, buttons grids right) controls_frame = ttk.Frame(self.root) controls_frame.pack(padx=16, pady=8, fill="both") @@ -188,11 +230,108 @@ class App: async_mainloop(self.root) + def get_midi_ports(self): + """Get list of available MIDI input ports""" + try: + port_names = mido.get_input_names() + return port_names + except Exception as e: + print(f"Error getting MIDI ports: {e}") + return [] + + def initialize_midi_handler(self): + """Initialize MIDI handler with current port""" + if not self.available_midi_ports: + print("No MIDI ports available") + return + + try: + WEBSOCKET_SERVER_URI = "ws://192.168.4.1:80/ws" + print(f"Initializing MIDI handler with port index {self.current_midi_port_index}") + self.midi_handler = MidiHandler(self.current_midi_port_index, WEBSOCKET_SERVER_URI) + print("MIDI handler initialized") + + # Update status + port_name = self.available_midi_ports[self.current_midi_port_index] + if hasattr(self, 'midi_status_label'): + self.midi_status_label.config(text=f"Status: Connected to {port_name}", fg="green") + + # Start MIDI in background + self.root.after(0, async_handler(self.start_midi)) + except Exception as e: + print(f"Error initializing MIDI handler: {e}") + messagebox.showerror("MIDI Error", f"Failed to initialize MIDI handler:\n{e}") + self.midi_handler = None + if hasattr(self, 'midi_status_label'): + self.midi_status_label.config(text="Status: Error", fg="red") + + def refresh_midi_ports(self): + """Refresh the list of available MIDI ports""" + old_ports = self.available_midi_ports.copy() + self.available_midi_ports = self.get_midi_ports() + + # Update dropdown values + midi_dropdown = None + for child in self.root.winfo_children(): + if isinstance(child, ttk.LabelFrame) and child.cget("text") == "MIDI Controller": + for widget in child.winfo_children(): + if isinstance(widget, ttk.Combobox): + midi_dropdown = widget + break + break + + if midi_dropdown: + midi_dropdown['values'] = self.available_midi_ports + if self.available_midi_ports and self.midi_port_var.get() not in self.available_midi_ports: + # Current selection is no longer available, select first available + self.midi_port_var.set(self.available_midi_ports[0]) + self.current_midi_port_index = 0 + self.restart_midi_handler() + elif not self.available_midi_ports: + # No ports available + self.midi_port_var.set("No MIDI ports found") + if hasattr(self, 'midi_status_label'): + self.midi_status_label.config(text="Status: No MIDI ports available", fg="orange") + # Stop current MIDI handler if running + if self.midi_task and not self.midi_task.done(): + self.midi_task.cancel() + self.midi_handler = None + + print(f"MIDI ports refreshed. Available: {self.available_midi_ports}") + + def on_midi_port_change(self, event): + """Handle MIDI port selection change""" + selected_port = self.midi_port_var.get() + if selected_port in self.available_midi_ports: + self.current_midi_port_index = self.available_midi_ports.index(selected_port) + print(f"MIDI port changed to: {selected_port} (index: {self.current_midi_port_index})") + self.restart_midi_handler() + + @async_handler + async def restart_midi_handler(self): + """Restart MIDI handler with new port""" + try: + # Stop current MIDI task + if self.midi_task and not self.midi_task.done(): + self.midi_task.cancel() + try: + await self.midi_task + except asyncio.CancelledError: + pass + + # Initialize new MIDI handler + self.initialize_midi_handler() + except Exception as e: + print(f"Error restarting MIDI handler: {e}") + messagebox.showerror("MIDI Error", f"Failed to restart MIDI handler:\n{e}") + @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(): + if self.midi_handler and (self.midi_task is None or self.midi_task.done()): self.midi_task = asyncio.create_task(self.midi_handler.run()) + elif not self.midi_handler: + print("Cannot start MIDI: no MIDI handler available") def on_closing(self): print("Closing application...") @@ -207,6 +346,38 @@ class App: await asyncio.sleep(0) # Yield control def update_status_labels(self): + # Check if MIDI handler is available + if not self.midi_handler: + # Update dial displays with placeholder values + placeholders = [ + ("n3", "-"), ("Delay", "-"), + ("n1", "-"), ("n2", "-"), + ("B", "-"), ("Brightness", "-"), + ("R", "-"), ("G", "-"), + ] + for idx, (label, value) in enumerate(placeholders): + if idx < len(self.dials_boxes): + self.dials_boxes[idx].config(text=f"{label}\n{value}") + + # Update knobs with placeholder values + knob_placeholders = [ + ("CC44", "-"), ("CC45", "-"), + ("Rad n1", "-"), ("Rad delay", "-"), + ("Alt n1", "-"), ("Alt n2", "-"), + ("Pulse n1", "-"), ("Pulse n2", "-"), + ] + for idx, (label, value) in enumerate(knob_placeholders): + if idx < len(self.knobs_boxes): + self.knobs_boxes[idx].config(text=f"{label}\n{value}") + + # Update buttons with no selection + for lbl in self.button1_cells: + lbl.config(text="", bg=bg_color) + + # Reschedule + self.root.after(200, self.update_status_labels) + return + # Pull values from midi_handler delay = self.midi_handler.delay brightness = self.midi_handler.brightness