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
This commit is contained in:
189
src/main.py
189
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("<<ComboboxSelected>>", 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
|
||||
|
Reference in New Issue
Block a user