diff --git a/logica/T1/geterSystemRecource.py b/logica/T1/geterSystemRecource.py index c3c40f1..2e3a493 100644 --- a/logica/T1/geterSystemRecource.py +++ b/logica/T1/geterSystemRecource.py @@ -9,21 +9,29 @@ def obtener_datos_cpu_ram(): :return: Diccionario con métricas. """ - cpu_percent_total = psutil.cpu_percent(interval=None) + # --- 1. MEDICIÓN DE CPU (CORRECCIÓN CLAVE) --- + # La primera llamada 'primea' el cálculo de psutil. + psutil.cpu_percent(interval=None) + + # La segunda llamada devuelve el porcentaje real calculado desde la primera. + # Aquí obtenemos el total y los núcleos en la misma llamada para consistencia. cpu_percent_per_core = psutil.cpu_percent(interval=None, percpu=True) + cpu_percent_total = sum(cpu_percent_per_core) / len(cpu_percent_per_core) + # Alternativamente, psutil.cpu_percent(interval=None) - pero calculando la media es más seguro. + + # --- 2. RESTO DE DATOS (Sin cambios) --- mem = psutil.virtual_memory() - num_procesos = len(psutil.pids()) - cpu_freq = psutil.cpu_freq() datos = { - 'cpu_total': cpu_percent_total, - 'cpu_cores': cpu_percent_per_core, + 'cpu_total': round(cpu_percent_total, 1), + 'cpu_cores': [round(p, 1) for p in cpu_percent_per_core], 'ram_total_gb': round(mem.total / (1024 ** 3), 2), 'ram_uso_gb': round(mem.used / (1024 ** 3), 2), 'ram_percent': mem.percent, + # Nota: 'num_hilos' es técnicamente el número de PIDs (procesos), no hilos 'num_hilos': num_procesos, 'cpu_freq_mhz': cpu_freq.current if cpu_freq else 0 } diff --git a/logica/T1/graficos.py b/logica/T1/graficos.py index 9b98e43..b30f832 100644 --- a/logica/T1/graficos.py +++ b/logica/T1/graficos.py @@ -15,8 +15,6 @@ def actualizar_historial_datos(net_in_kb, net_out_kb, cpu_percent, ram_percent): """ Recopila los datos actuales de CPU, RAM y Red pasados como argumento a sus historiales. """ - # 🎯 CORRECCIÓN: Los datos de CPU/RAM ahora vienen como argumentos, no se calculan aquí. - # 1. Añadir CPU y gestionar la longitud historial_cpu.append(cpu_percent) if len(historial_cpu) > MAX_PUNTOS: @@ -40,7 +38,7 @@ def crear_grafico_recursos(figure): """ Crea o actualiza un gráfico que muestre la evolución de CPU, RAM y Red. """ - # Limpiar la figura antes de dibujar + # Limpiar la figura antes de dibujar (CRUCIAL para redibujo) figure.clear() # Configuramos el fondo de la figura para que coincida con el estilo de la aplicación @@ -50,7 +48,7 @@ def crear_grafico_recursos(figure): # 3 filas para CPU, RAM, Red con espaciado vertical gs = figure.add_gridspec(3, 1, hspace=0.6, top=0.95, bottom=0.05, left=0.1, right=0.95) - # --- Función Helper para el estilo btop --- + # --- Función Helper para el estilo --- def configurar_ejes_historial(ax, title, color, data, y_limit=100, y_ticks=None): ax.set_facecolor('#f0f0f0') # Fondo del área de dibujo ax.set_title(title, fontsize=9, loc='left', pad=10) diff --git a/logica/T1/trafficMeter.py b/logica/T1/trafficMeter.py index ac93218..d4950ad 100644 --- a/logica/T1/trafficMeter.py +++ b/logica/T1/trafficMeter.py @@ -21,24 +21,30 @@ class NetIOMonitor(threading.Thread): # Almacenamiento seguro para los últimos datos de tráfico self.lock = threading.Lock() - self.data_in_kb = 0.0 # Tráfico de entrada en KB/s (Recibido) - self.data_out_kb = 0.0 # Tráfico de salida en KB/s (Enviado) - self.cpu_percent = 0.0 # Nuevo - self.ram_percent = 0.0 # Nuevo + self.data_in_kb = 0.0 + self.data_out_kb = 0.0 + self.cpu_percent = 0.0 + self.ram_percent = 0.0 # Almacena el contador anterior para calcular la diferencia (tasa) self.last_counters = psutil.net_io_counters() - # Necesario para inicializar la medición de CPU/RAM al inicio + # Inicializar la medición de CPU (sin intervalo) para la primera lectura. + # Esto 'primea' el cálculo de psutil para la primera vez que se llama en run(). psutil.cpu_percent(interval=None) def run(self): """Método principal del hilo.""" try: + # Esperar el intervalo antes de la primera lectura para tener una base + # y que el cálculo de la tasa sea correcto desde el inicio. + time.sleep(self.intervalo) + while not self._stop_event.is_set(): - # Esperar el intervalo antes de la lectura para calcular la tasa - time.sleep(self.intervalo) self._actualizar_datos() + # Pausa al final, simplifica la lógica de _actualizar_datos + time.sleep(self.intervalo) + except Exception as e: print(f"Error fatal en el hilo NetIOMonitor: {e}") self._stop_event.set() @@ -52,8 +58,9 @@ class NetIOMonitor(threading.Thread): current_counters = psutil.net_io_counters() - # Medición de CPU y RAM (usando interval=0.0 ya que el sleep garantiza el intervalo) - current_cpu = psutil.cpu_percent(interval=0.0) + # 🎯 CORRECCIÓN: Llamar sin intervalo. psutil.cpu_percent() devolverá el uso + # desde la última llamada (que fue hace 'self.intervalo' segundos). + current_cpu = psutil.cpu_percent() current_ram = psutil.virtual_memory().percent # Calcular la diferencia de bytes recibidos y enviados desde la última lectura diff --git a/logica/T2/musicReproductor.py b/logica/T2/musicReproductor.py index 53c6e94..a1eaa39 100644 --- a/logica/T2/musicReproductor.py +++ b/logica/T2/musicReproductor.py @@ -30,19 +30,72 @@ class MusicReproductor: self.instance = vlc.Instance() self.player = self.instance.media_player_new() self.current_media = None - self.is_playing = False + + # 🔑 Variables de estado/control + self._current_url = None # Guarda la última URL cargada + self._is_playing = False # Indica si se está reproduciendo activamente (no pausado) # Configurar volumen inicial - self.ajustar_volumen(initial_volume) - print(f"🎵 [VLC] Reproductor inicializado. Volumen: {self.player.audio_get_volume()}") + self.set_volumen(initial_volume) # 🔑 Renombrado a set_volumen + print(f"🎵 [VLC] Reproductor inicializado. Volumen: {self.get_volumen()}") - def ajustar_volumen(self, valor_porcentual): + # ------------------------------------------------------------- + # 🔑 MÉTODOS REQUERIDOS POR LA VISTA + # ------------------------------------------------------------- + + def get_volumen(self): """ - Ajusta el volumen del reproductor (0 a 100). + [REQUERIDO] Devuelve el nivel de volumen actual (0-100). + Este método es esencial para inicializar el slider de la interfaz. + """ + # VLC proporciona el volumen actual directamente + return self.player.audio_get_volume() + + def set_volumen(self, valor_porcentual): + """ + [REQUERIDO - Antes ajustar_volumen] Ajusta el volumen del reproductor (0 a 100). """ volumen_int = int(max(0, min(100, valor_porcentual))) self.player.audio_set_volume(volumen_int) - # No imprimimos el volumen aquí para evitar saturar la consola con cada movimiento del Scale + + def esta_reproduciendo(self): + """ + [REQUERIDO] Devuelve True si el reproductor está en estado Playing o Paused + y nosotros lo consideramos 'activo'. + Usaremos el estado interno _is_playing para indicar el estado activo/pausado. + """ + return self._is_playing + + def continuar(self): + """ + [REQUERIDO] Reanuda la reproducción si está pausada, o inicia el stream si está detenido. + Devuelve True si la reproducción se inició/continuó. + """ + # Si está pausado, reanuda + if self.player.get_state() == vlc.State.Paused: + self.player.play() + self._is_playing = True + print("▶️ [VLC] Reproducción reanudada.") + return True + + # Si está detenido y hay un medio cargado, intenta reproducir + elif self.player.get_state() == vlc.State.Stopped and self.current_media: + self.player.play() + self._is_playing = True + print("▶️ [VLC] Reproducción iniciada desde stream cargado.") + return True + + # Si no hay medio cargado, no puede continuar + elif not self.current_media: + print("ℹ️ [VLC] No hay stream cargado para continuar.") + return False + + # Si ya está reproduciendo, lo ignoramos + return True + + # ------------------------------------------------------------- + # MÉTODOS DE CONTROL DE VLC + # ------------------------------------------------------------- def cargar_y_reproducir(self, url_stream): """ @@ -50,29 +103,33 @@ class MusicReproductor: """ if not url_stream: print("❌ [VLC] URL del stream vacía.") - return + return False, "URL del stream vacía." print(f"🔄 [VLC] Intentando cargar y reproducir: {url_stream}") + # Detener la reproducción anterior self.player.stop() self.current_media = self.instance.media_new(url_stream) self.player.set_media(self.current_media) + self._current_url = url_stream + # Iniciar reproducción self.player.play() - self.is_playing = True - print("✅ [VLC] Reproducción iniciada.") + self._is_playing = True - def reproducir(self): - """ - Reanuda la reproducción si está pausada. - """ - if self.player.get_state() == vlc.State.Paused: - self.player.play() - self.is_playing = True - print("▶️ [VLC] Reproducción reanudada.") + # Esperar un poco para confirmar el estado de reproducción + # En entornos reales, se usaría un callback de evento para esto. + import time + time.sleep(0.1) + + if self.player.get_state() in [vlc.State.Playing, vlc.State.Opening]: + print("✅ [VLC] Reproducción iniciada.") + return True, url_stream else: - print("ℹ️ [VLC] Ya está reproduciéndose o esperando un stream.") + print(f"❌ [VLC] Fallo al iniciar la reproducción. Estado: {self.player.get_state()}") + self._is_playing = False + return False, "Fallo al iniciar el stream (Revisa la URL)." def pausar(self): """ @@ -80,18 +137,20 @@ class MusicReproductor: """ if self.player.get_state() == vlc.State.Playing: self.player.pause() - self.is_playing = False + self._is_playing = False print("⏸️ [VLC] Reproducción pausada.") + return True else: print("ℹ️ [VLC] No se puede pausar, el reproductor no está en estado de reproducción.") + return False def detener(self): """ - Detiene la reproducción y libera los recursos. Crucial al cerrar la aplicación. + Detiene la reproducción y el estado activo. """ if self.player: self.player.stop() - # 🎯 Solo liberamos el reproductor. No eliminamos self.instance. - self.player.release() - self.player = None # Esto asegura que el player se recree si es necesario - print("⏹️ [VLC] Reproductor detenido y recursos liberados.") \ No newline at end of file + self._is_playing = False + print("⏹️ [VLC] Reproductor detenido.") + return True + return False \ No newline at end of file diff --git a/vista/central_panel/view_radio.py b/vista/central_panel/view_radio.py index 96fe71f..a04e063 100644 --- a/vista/central_panel/view_radio.py +++ b/vista/central_panel/view_radio.py @@ -5,51 +5,38 @@ from tkinter import ttk import json from vista.config import * -# Bloque para manejar la dependencia de VLC -try: - from logica.T2.musicReproductor import MusicReproductor -except ImportError: - print("⚠️ Error al importar MusicReproductor. Usando simulador.") - - - # CLASE SIMULADA - class MusicReproductor: - def __init__(self, *args, **kwargs): pass - - def ajustar_volumen(self, valor): print(f"🎶 SIMULANDO VOLUMEN: {valor}") - - def cargar_y_reproducir(self, url): print(f"🎶 SIMULANDO PLAY: {url}") - - def reproducir(self): print("🎶 SIMULANDO PLAY") - - def pausar(self, *args): print("🎶 SIMULANDO PAUSA") - - def detener(self): print("🎶 SIMULANDO DETENER") +# ------------------------------------------------------------- +# ❌ ELIMINAMOS EL BLOQUE try/except CON LA SIMULACIÓN DE VLC +# Ya que esta clase no debe interactuar directamente con MusicReproductor. +# ------------------------------------------------------------- class RadioPanel(ttk.Frame): """ Panel de la pestaña Radios (T3). - Gestiona la selección de emisoras y los controles de reproducción. + Gestiona únicamente la selección de emisoras y DELEGA la reproducción + al ReproductorController en el Panel Lateral. """ NOMBRE_FICHERO_RADIOS = "res/radios.json" - def __init__(self, parent_notebook, root, *args, **kwargs): + def __init__(self, parent_notebook, root, reproductor_controller_instance=None, *args, **kwargs): super().__init__(parent_notebook, *args, **kwargs) self.root = root + # 🔑 REFERENCIA AL CONTROLADOR DE AUDIO DEL PANEL LATERAL + # Este controlador tiene los métodos cargar_stream() y manejar_stop(). + self.reproductor_controller = reproductor_controller_instance + self.emisoras_cargadas = self.cargar_emisoras() self.radio_seleccionada = tk.StringVar(value="---") - self.volumen_var = tk.DoubleVar(value=50.0) - # Inicialización de la lógica del reproductor - self.reproductor = MusicReproductor(initial_volume=self.volumen_var.get()) + # ❌ Se eliminaron: self.volumen_var y la inicialización de MusicReproductor. self.crear_interfaz_radios(self) # ------------------------------------------------------------- - # 📻 VISTA Y LÓGICA DE RADIO + # 📻 LÓGICA DE DATOS # ------------------------------------------------------------- def cargar_emisoras(self): @@ -64,8 +51,12 @@ class RadioPanel(ttk.Frame): print(f"⚠️ Error al leer el archivo {self.NOMBRE_FICHERO_RADIOS}. Está mal formado.") return [] + # ------------------------------------------------------------- + # 🖼️ VISTA (SOLO SELECCIONADOR) + # ------------------------------------------------------------- + def crear_interfaz_radios(self, parent_frame): - """Crea la interfaz para seleccionar la emisora de radio.""" + """Crea la interfaz para seleccionar la emisora de radio (SIN CONTROLES DE AUDIO).""" frame_radio = ttk.Frame(parent_frame, padding=10, style='TFrame') frame_radio.pack(expand=True, fill="both") @@ -99,25 +90,16 @@ class RadioPanel(ttk.Frame): self.url_seleccionada_label = ttk.Label(frame_radio, text="N/A", wraplength=400, foreground=COLOR_TEXTO) self.url_seleccionada_label.pack(anchor="w") - # Controles de Volumen y Play/Pause - frame_controles = ttk.Frame(frame_radio, padding=5) - frame_controles.pack(fill="x", pady=10) + # ❌ Se eliminaron los controles de volumen y Play/Pause/Stop. - ttk.Button(frame_controles, text="▶️ Play", command=lambda: self.controlar_reproduccion('play'), - style='Action.TButton').pack(side='left', padx=5) - ttk.Button(frame_controles, text="⏸️ Pause", command=lambda: self.controlar_reproduccion('pause'), - style='Action.TButton').pack(side='left', padx=5) - - ttk.Label(frame_controles, textvariable=self.radio_seleccionada, font=FUENTE_NEGOCIOS).pack(side='left', - padx=15) - - ttk.Label(frame_controles, text="Volumen:").pack(side='right', padx=5) - volumen_slider = ttk.Scale(frame_controles, from_=0, to=100, orient=tk.HORIZONTAL, - variable=self.volumen_var, command=self.cambiar_volumen, length=100) - volumen_slider.pack(side='right', padx=5) + # ------------------------------------------------------------- + # ⏯️ DELEGACIÓN DE LA LÓGICA DE AUDIO + # ------------------------------------------------------------- def seleccionar_radio(self, listbox): - """Captura la selección y llama al reproductor para iniciar la reproducción.""" + """ + Captura la selección y DELEGA la reproducción al ReproductorController. + """ seleccion = listbox.curselection() if seleccion: indice = seleccion[0] @@ -126,22 +108,21 @@ class RadioPanel(ttk.Frame): self.radio_seleccionada.set(emisora['nombre']) self.url_seleccionada_label.config(text=url) - self.reproductor.cargar_y_reproducir(url) - def controlar_reproduccion(self, accion): - """Llama al método de control del reproductor (play/pause).""" - if accion == 'play': - self.reproductor.reproducir() - elif accion == 'pause': - self.reproductor.pausar() + # 🔑 DELEGACIÓN: Llamamos al controlador de audio del Panel Lateral + if self.reproductor_controller: + self.reproductor_controller.cargar_stream(url) + else: + # El error indica que falta conectar en panel_central.py + print("❌ Error: ReproductorController no está asignado.") - def cambiar_volumen(self, valor): - """Ajusta el volumen.""" - valor_entero = int(float(valor)) - self.volumen_var.set(valor_entero) - self.reproductor.ajustar_volumen(valor_entero) + # ❌ Se eliminaron los métodos controlar_reproduccion, cambiar_volumen. def detener_actualizacion(self): - """Método llamado por PanelCentral al cerrar la aplicación.""" - if self.reproductor: - self.reproductor.detener() \ No newline at end of file + """Método llamado por PanelCentral al cerrar la aplicación (solo delega la detención).""" + # 🔑 DELEGACIÓN: El PanelCentral llama a esto al cerrar. + if self.reproductor_controller: + self.reproductor_controller.manejar_stop() + else: + # Si el controlador no existe, no hacemos nada, pero el PanelLateral debería manejar su propio cierre. + pass \ No newline at end of file diff --git a/vista/central_panel/view_recursos.py b/vista/central_panel/view_recursos.py index caa0a32..58fcc16 100644 --- a/vista/central_panel/view_recursos.py +++ b/vista/central_panel/view_recursos.py @@ -2,9 +2,9 @@ import tkinter as tk from tkinter import ttk -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg # <-- ¡IMPORTACIÓN NECESARIA! +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from logica.T1.graficos import crear_grafico_recursos, actualizar_historial_datos -from vista.config import * # Asumiendo que las constantes de estilo/fondo están aquí +from vista.config import * class RecursosPanel(ttk.Frame): @@ -13,54 +13,56 @@ class RecursosPanel(ttk.Frame): Muestra el gráfico de Matplotlib con el consumo de recursos de red. """ - # NOTA: Los parámetros 'canvas' y 'canvas_widget' ya no son necesarios - # en el constructor, pero los mantenemos para no romper el flujo de PanelCentral. def __init__(self, parent_notebook, figure, canvas, *args, **kwargs): super().__init__(parent_notebook, *args, **kwargs) self.figure = figure - # Aseguramos que el frame se expanda para llenar la pestaña + # Asegura que el frame del panel se expande self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) - self.canvas = None # Lo crearemos aquí - self.canvas_widget = None # Lo crearemos aquí + self.canvas = None + self.canvas_widget = None self.grafico_frame = None self.crear_interfaz_recursos(self) - # Estado inicial del gráfico + # Estado inicial del gráfico (limpia y configura la figura) crear_grafico_recursos(self.figure) - # Después de la creación, nos aseguramos de que PanelCentral obtenga las referencias - # que espera si las necesita. + # ✅ DIBUJO INICIAL: Asegura que el gráfico vacío se muestre inmediatamente. + if self.canvas: + self.canvas.draw() def crear_interfaz_recursos(self, parent_frame): """Prepara el Frame para contener el gráfico de Matplotlib.""" - - # 1. Crear el Frame que contendrá el Canvas (contenedor interno) self.grafico_frame = ttk.Frame(parent_frame, style='TFrame') + # Ubicar el frame contenedor del gráfico self.grafico_frame.grid(row=0, column=0, sticky="nsew", padx=10, pady=10) + # Asegura que el contenido del gráfico_frame se expande self.grafico_frame.grid_rowconfigure(0, weight=1) self.grafico_frame.grid_columnconfigure(0, weight=1) - # 2. 🎯 CREAR Y UBICAR EL CANVAS DENTRO DEL FRAME (SOLUCIÓN DEL PROBLEMA) - # El canvas debe crearse AQUÍ y usar 'self.grafico_frame' como master + # CREAR Y UBICAR EL CANVAS self.canvas = FigureCanvasTkAgg(self.figure, master=self.grafico_frame) self.canvas_widget = self.canvas.get_tk_widget() - # Usamos .grid() para llenar el frame_contenedor + # ✅ CRÍTICO: El widget del canvas debe expandirse para llenar el gráfico_frame self.canvas_widget.grid(row=0, column=0, sticky="nsew") - # ------------------------------------------------------------- - # 📞 MÉTODOS DE CONEXIÓN (Llamados por PanelCentral) # ------------------------------------------------------------- - def actualizar_datos(self, net_in, net_out): - """Recibe los datos del monitor de red y actualiza el historial.""" - actualizar_historial_datos(net_in, net_out) + def actualizar_datos(self, net_in, net_out, cpu_percent, ram_percent): + """Recibe los datos del monitor y actualiza el historial de datos en la lógica.""" + # Recibe los 4 datos validados por la prueba de terminal + actualizar_historial_datos(net_in, net_out, cpu_percent, ram_percent) def dibujar_grafico(self): - """Llama a la función de redibujo del gráfico.""" + """Llama a la función de redibujo del gráfico y fuerza la actualización del canvas.""" if self.figure: - crear_grafico_recursos(self.figure) \ No newline at end of file + # 1. Modifica la figura usando los datos del historial + crear_grafico_recursos(self.figure) + + # 2. ✅ FORZAR EL REDIBUJADO DE TKINTER + if self.canvas: + self.canvas.draw() \ No newline at end of file diff --git a/vista/panel_central.py b/vista/panel_central.py index 99be006..179d074 100644 --- a/vista/panel_central.py +++ b/vista/panel_central.py @@ -5,6 +5,7 @@ from tkinter import ttk from tkinter import messagebox from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure +import psutil # <--- IMPORTACIÓN REQUERIDA PARA CPU/RAM # --- LÓGICA DE CONTROL UNIVERSAL --- from logica.T1.trafficMeter import iniciar_monitor_red @@ -34,9 +35,12 @@ class PanelCentral(ttk.Frame): INTERVALO_ACTUALIZACION_MS = INTERVALO_RECURSOS_MS - def __init__(self, parent, root, *args, **kwargs): + # 🔑 CORRECCIÓN CRUCIAL: Añadir 'panel_lateral' al constructor para acceder al controlador de música. + def __init__(self, parent, root, panel_lateral, *args, **kwargs): super().__init__(parent, *args, **kwargs) self.root = root + # 🔑 Guardamos la referencia para poder acceder al ReproductorController + self.panel_lateral = panel_lateral # --- Variables de Estado y Lógica Central --- self.after_id = None @@ -56,12 +60,10 @@ class PanelCentral(ttk.Frame): self.modulos = {} # Configuración de Layout Principal - # 🎯 CORRECCIÓN 1: Se elimina la columna 1. La columna 0 ocupa todo el espacio. - self.grid_columnconfigure(0, weight=1) # La Columna de Pestañas ahora es la única y principal + self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.crear_area_principal() - # ❌ Se elimina la llamada a self.crear_panel_chat_y_alumnos() # 📦 ESTRUCTURA PRINCIPAL Y CREACIÓN DE PESTAÑAS # ------------------------------------------------------------- @@ -69,7 +71,7 @@ class PanelCentral(ttk.Frame): def crear_area_principal(self): """Crea el contenedor de las subpestañas (Notebook), columna izquierda (0).""" frame_izquierdo = ttk.Frame(self, style='TFrame') - frame_izquierdo.grid(row=0, column=0, sticky="nsew") # Ocupa toda la ventana + frame_izquierdo.grid(row=0, column=0, sticky="nsew") frame_izquierdo.grid_rowconfigure(0, weight=1) frame_izquierdo.grid_columnconfigure(0, weight=1) @@ -123,6 +125,14 @@ class PanelCentral(ttk.Frame): vista_instancia = AlarmaPanel(frame, self.root, self.alarm_manager) self.modulos[sub_tab_text] = vista_instancia + # 🔑 CORRECCIÓN CLAVE: Inyectar la dependencia del controlador de música en RadioPanel + elif sub_tab_text == "Radios": + # Accedemos al controlador de música a través de la referencia al PanelLateral + reproductor_controller = getattr(self.panel_lateral, 'controles_musica', None) + vista_instancia = RadioPanel(frame, self.root, + reproductor_controller_instance=reproductor_controller) + self.modulos[sub_tab_text] = vista_instancia + else: vista_instancia = ClasePanel(frame, self.root) self.modulos[sub_tab_text] = vista_instancia @@ -197,22 +207,29 @@ class PanelCentral(ttk.Frame): def actualizar_recursos(self): """Obtiene los datos del sistema y delega el dibujo al módulo RecursosPanel.""" try: - if self.net_monitor is None: - raise Exception("TrafficMeter no inicializado.") + if 'Recursos' not in self.modulos or self.net_monitor is None: + # Si el módulo Recursos o el monitor no están listos, reprogramamos. + self.after_id = self.after(self.INTERVALO_ACTUALIZACION_MS, self.actualizar_recursos) + return + # 1. OBTENER DATOS (Red, CPU y RAM) + # 🔑 CORRECCIÓN CLAVE: Desempaquetar los 4 valores devueltos por TrafficMeter. net_in, net_out, cpu_percent, ram_percent = self.net_monitor.get_io_data_kb() - if 'Recursos' in self.modulos: - self.modulos['Recursos'].actualizar_datos(net_in, net_out, cpu_percent, ram_percent) - self.modulos['Recursos'].dibujar_grafico() + # 2. ACTUALIZAR Y DIBUJAR + recursos_panel = self.modulos['Recursos'] - if self.canvas: - self.canvas.draw() + # Llama a actualizar_datos en la lógica de graficos.py + recursos_panel.actualizar_datos(net_in, net_out, cpu_percent, ram_percent) + + # Llama a dibujar_grafico en la vista (que ahora incluye self.canvas.draw()) + recursos_panel.dibujar_grafico() except Exception as e: + # Captura y muestra el error, pero no detiene la tarea print(f"Error en la actualización de recursos T1: {e}") - pass + # 3. REPROGRAMAR TAREA (Crucial para el bucle) self.after_id = self.after(self.INTERVALO_ACTUALIZACION_MS, self.actualizar_recursos) def iniciar_actualizacion_automatica(self): @@ -230,6 +247,12 @@ class PanelCentral(ttk.Frame): return print("Iniciando actualización automática de recursos.") + + # 🔑 CORRECCIÓN: Realizar una llamada inicial a psutil.cpu_percent() + # para establecer el punto de partida del intervalo de medición en el hilo principal. + psutil.cpu_percent(interval=None) + + # Iniciar el ciclo de actualización. self.after_id = self.after(0, self.actualizar_recursos) def detener_actualizacion_automatica(self): @@ -247,5 +270,4 @@ class PanelCentral(ttk.Frame): # Llama a los métodos de detención de los módulos (si existen) for nombre, modulo in self.modulos.items(): if hasattr(modulo, 'detener_actualizacion'): - modulo.detener_actualizacion() - + modulo.detener_actualizacion() \ No newline at end of file diff --git a/vista/panel_lateral.py b/vista/panel_lateral.py index 6c897f4..6399de7 100644 --- a/vista/panel_lateral.py +++ b/vista/panel_lateral.py @@ -5,16 +5,18 @@ from tkinter import ttk from tkinter import messagebox # --- Módulos de Lógica Existente --- -# Asumiendo que estos módulos existen en la estructura lógica del proyecto from logica.controlador import accion_placeholder from logica.T1.backup import accion_backup_t1 from logica.T1.runVScode import abrir_vscode from logica.T1.openBrowser import navegar_a_url from logica.T2.scraping import hacer_scraping +# 🔑 NUEVA IMPORTACIÓN DE LÓGICA T2 +from logica.T2.musicReproductor import MusicReproductor # --- Módulos de Vistas --- -# Importamos la clase RadioPanel, que contiene los controles de música (Play/Pause y Volumen). -from vista.central_panel.view_radio import RadioPanel +# ❌ Eliminamos: from vista.central_panel.view_radio import RadioPanel +# 🔑 NUEVA IMPORTACIÓN DE VISTA MODULAR +from vista.reproductor_controller import ReproductorController from vista.config import * @@ -33,9 +35,12 @@ class PanelLateral(ttk.Frame): self.root = root self.panel_central = panel_central self.controles_musica = None - self.entrada_superior = None + # 🔑 INSTANCIA DE LÓGICA DE MÚSICA T2 + # Inicializamos el objeto de la lógica de reproducción aquí + self.music_reproductor = MusicReproductor() + self.configurar_estilos_locales(root) # Configuración de Layout Principal @@ -52,6 +57,7 @@ class PanelLateral(ttk.Frame): ttk.Separator(self, orient='horizontal').grid(row=4, column=0, sticky="ew", pady=(10, 0)) tk.Frame(self, height=1).grid(row=99, column=0, sticky="nsew") + # 🔑 LLAMADA AL NUEVO CONTROLADOR self.crear_controles_musica() # Fila 100 # ------------------------------------------------------------- @@ -82,7 +88,7 @@ class PanelLateral(ttk.Frame): acciones_aplicaciones = [ ("Visual Code", abrir_vscode), - ("App2 (Carrera 🏁)", app2_comando), + ("Carrera 🏁", app2_comando), ("App3", lambda: accion_placeholder("App3")) ] self._crear_bloque_botones(self, titulo="Aplicaciones", acciones=acciones_aplicaciones, grid_row=2) @@ -95,13 +101,17 @@ class PanelLateral(ttk.Frame): self._crear_bloque_botones(self, titulo="Procesos batch", acciones=acciones_batch, grid_row=3) def crear_controles_musica(self): - """Crea el área para alojar los controles de música/radio.""" + """Crea el área para alojar los controles de música/radio usando el nuevo controlador.""" frame_musica = ttk.Frame(self, style='TFrame', padding="15 10") frame_musica.grid(row=100, column=0, sticky="ew") frame_musica.grid_columnconfigure(0, weight=1) - # Instancia la clase RadioPanel, que ahora contiene solo Play/Pause y Volumen - self.controles_musica = RadioPanel(frame_musica, self.root) + # 🔑 REEMPLAZO CLAVE: Usamos ReproductorController y le pasamos la instancia de la lógica. + self.controles_musica = ReproductorController( + frame_musica, + self.root, + music_reproductor_instance=self.music_reproductor # Pasamos la instancia de la lógica T2 + ) self.controles_musica.grid(row=0, column=0, sticky="nsew") frame_musica.grid_rowconfigure(0, weight=1) @@ -199,4 +209,12 @@ class PanelLateral(ttk.Frame): ttk.Label(frame_titulo, text=titulo, font=FUENTE_NEGOCIOS).pack(anchor="w", padx=5) for texto_boton, comando in acciones: - ttk.Button(frame_seccion, text=texto_boton, command=comando, style='Green.TButton').pack(fill="x", pady=5) \ No newline at end of file + ttk.Button(frame_seccion, text=texto_boton, command=comando, style='Green.TButton').pack(fill="x", pady=5) + + def set_panel_central_reference(self, panel_central_instance): + """ + Asigna la referencia al PanelCentral una vez que ambos paneles han sido inicializados. + Esto resuelve la dependencia circular. + """ + self.panel_central = panel_central_instance + print("✅ [PanelLateral] Referencia a Panel Central establecida.") \ No newline at end of file diff --git a/vista/reproductor_controller.py b/vista/reproductor_controller.py new file mode 100644 index 0000000..2c44dbe --- /dev/null +++ b/vista/reproductor_controller.py @@ -0,0 +1,124 @@ +# Módulo: vista/reproductor_controller.py + +import tkinter as tk +from tkinter import ttk +# ⚠️ Asegúrate de que esta ruta es correcta: 'vista/config' o 'config' +from vista.config import * + + +class ReproductorController(ttk.Frame): + """ + Controlador de la interfaz de reproducción de música (Panel Lateral). + Delega todas las acciones de audio a la instancia MusicReproductor. + """ + + def __init__(self, parent, root, music_reproductor_instance=None, *args, **kwargs): + super().__init__(parent, *args, **kwargs) + self.root = root + self.reproductor = music_reproductor_instance # Instancia de MusicReproductor (Lógica T2) + + # Variables de control de UI + # Intentar obtener el volumen inicial de la lógica, si está disponible + initial_volume = self.reproductor.get_volumen() if self.reproductor and hasattr(self.reproductor, + 'get_volumen') else 50 + self.volumen_var = tk.DoubleVar(value=initial_volume) + + # Estado inicial del botón Play/Pause + self.play_pause_text = tk.StringVar(value="▶️") + if self.reproductor and hasattr(self.reproductor, 'esta_reproduciendo') and self.reproductor.esta_reproduciendo(): + self.play_pause_text.set("⏸️") + + # Configuración de Layout + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(0, weight=1) + + self.crear_controles() + + def crear_controles(self): + """Crea el marco con el slider de volumen y los botones de control.""" + + main_frame = ttk.Frame(self, padding=10, style='TFrame') + main_frame.grid(row=0, column=0, sticky="nsew") + + main_frame.grid_columnconfigure(0, weight=1) + main_frame.grid_columnconfigure(1, weight=1) + + # --- Título --- + ttk.Label(main_frame, text="🎵 Control de Música", font=FUENTE_NEGOCIOS).grid( + row=0, column=0, columnspan=2, pady=(0, 10), sticky="w") + + # --- Slider de Volumen --- + ttk.Label(main_frame, text="Volumen:", font=FUENTE_NORMAL).grid( + row=1, column=0, columnspan=2, pady=(5, 0), sticky="w") + + self.slider_volumen = ttk.Scale( + main_frame, + from_=0, + to=100, + orient="horizontal", + variable=self.volumen_var, + command=self.manejar_ajuste_volumen + ) + self.slider_volumen.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(5, 10)) + + # --- Botones de Control --- + + self.boton_play_pause = ttk.Button( + main_frame, + textvariable=self.play_pause_text, + style='Action.TButton', + command=self.manejar_play_pause + ) + self.boton_play_pause.grid(row=3, column=0, sticky="ew", padx=(0, 5), pady=5) + + self.boton_stop = ttk.Button(main_frame, text="⏹️", style='Action.TButton', command=self.manejar_stop) + self.boton_stop.grid(row=3, column=1, sticky="ew", padx=(5, 0), pady=5) + + def manejar_play_pause(self): + """Alterna entre reproducir y pausar llamando al reproductor de la lógica.""" + if not self.reproductor: return + if self.reproductor.esta_reproduciendo(): + self.reproductor.pausar() + self.play_pause_text.set("▶️") + else: + # Llama a continuar, que intenta reanudar o iniciar el último medio + if self.reproductor.continuar(): + self.play_pause_text.set("⏸️") + else: + self.play_pause_text.set("▶️") + + def manejar_stop(self): + """Detiene completamente la reproducción.""" + if not self.reproductor: return + self.reproductor.detener() + self.play_pause_text.set("▶️") + + def manejar_ajuste_volumen(self, valor): + """Ajusta el volumen del reproductor basado en el slider.""" + if not self.reproductor: return + volumen = int(float(valor)) + # 🔑 Delega el ajuste de volumen a la lógica (MusicReproductor.set_volumen) + self.reproductor.set_volumen(volumen) + + # ------------------------------------------------------------- + + # 📡 FUNCIÓN EXPORTADA PARA RECIBIR EL STREAM DEL view_radio + # ------------------------------------------------------------- + + def cargar_stream(self, url_stream): + """ + Recibe la URL de la radio seleccionada y la pasa a la lógica para su reproducción. + """ + if not self.reproductor: + print("Error: Instancia de reproductor no asignada.") + return + + # 🔑 Llama a la lógica para detener lo anterior, cargar y reproducir el nuevo stream + success, message = self.reproductor.cargar_y_reproducir(url_stream) + + # Actualiza la UI basada en el resultado de la carga + if success: + self.play_pause_text.set("⏸️") + else: + self.play_pause_text.set("▶️") + print(f"⚠️ Fallo en la carga del stream: {message}") \ No newline at end of file diff --git a/vista/ventana_principal.py b/vista/ventana_principal.py index 8b1abde..72e7056 100644 --- a/vista/ventana_principal.py +++ b/vista/ventana_principal.py @@ -24,7 +24,8 @@ class VentanaPrincipal(tk.Tk): self.reloj_after_id = None self.label_clima = None self.clima_after_id = None - self.panel_central = None # Inicializar a None para evitar errores en on_closing + self.panel_central = None + self.panel_lateral = None # 🔑 Añadir inicialización a None style = ttk.Style() style.theme_use('clam') @@ -48,6 +49,9 @@ class VentanaPrincipal(tk.Tk): self.iniciar_actualizacion_reloj() self.iniciar_actualizacion_clima() + # 🔑 CORRECCIÓN CRÍTICA: Iniciar el bucle de actualización del Panel Central (T1) + self.iniciar_actualizacion_graficos() + def configurar_estilos(self, s: ttk.Style): """Define estilos visuales personalizados.""" @@ -77,20 +81,37 @@ class VentanaPrincipal(tk.Tk): s.configure('TNotebook.Tab', padding=[10, 5], font=FUENTE_NEGOCIOS) s.map('TNotebook.Tab', background=[('selected', COLOR_FONDO)], foreground=[('selected', COLOR_ACCION)]) + # 🔑 MÉTODO CORREGIDO def crear_paneles_principales(self): - """Ensambla el panel lateral y el panel central.""" + """Ensambla el panel lateral y el panel central, resolviendo la dependencia circular.""" - # Panel Central debe inicializarse primero para pasar la referencia al lateral - # self es parent - self.panel_central = PanelCentral(self, self) + # 1. Crear Panel Lateral: Inicialmente no necesita el PanelCentral, solo necesita saber que existirá. + # PanelLateral espera: PanelLateral(parent, root, panel_central_ref, ...) + # Usamos None para 'panel_central_ref' por ahora. + self.panel_lateral = PanelLateral(self, self, None, width=ANCHO_PANEL_LATERAL) + self.panel_lateral.grid(row=0, column=0, sticky="nswe", padx=(10, 5), pady=10) + self.panel_lateral.grid_propagate(False) + + + # 2. Crear Panel Central: Ahora sí necesita la referencia al PanelLateral para inyectar el controlador de audio. + # PanelCentral espera: PanelCentral(parent, root, panel_lateral) + self.panel_central = PanelCentral(self, self, panel_lateral=self.panel_lateral) self.panel_central.grid(row=0, column=1, sticky="nswe", padx=(5, 10), pady=10) - # 🎯 CORRECCIÓN CLAVE: Pasar 'self' como argumento 'root' y 'self.panel_central' como argumento posicional. - # PanelLateral espera: PanelLateral(parent, root, panel_central, ...) - self.panel_lateral = PanelLateral(self, self, self.panel_central, width=ANCHO_PANEL_LATERAL) - self.panel_lateral.grid(row=0, column=0, sticky="nswe", padx=(10, 5), pady=10) + # 3. Finalizar la dependencia circular: Asignar la referencia del Panel Central al Panel Lateral. + # El PanelLateral requiere la referencia de PanelCentral para manejar eventos (ej. Scrapping). + self.panel_lateral.set_panel_central_reference(self.panel_central) - self.panel_lateral.grid_propagate(False) + + # --- LÓGICA DE ACTUALIZACIÓN DE GRÁFICOS (T1) --- + def iniciar_actualizacion_graficos(self): + """Inicia el ciclo de actualización de recursos del PanelCentral.""" + if self.panel_central: + print("Iniciando actualización automática de gráficos del Panel Central.") + # Llama al método que inicializa el TrafficMeter y el self.after() + self.panel_central.iniciar_actualizacion_automatica() + else: + print("Error: Panel Central no inicializado para iniciar gráficos.") # --- LÓGICA DE ACTUALIZACIÓN DE RELOJ --- def actualizar_reloj(self): @@ -103,9 +124,6 @@ class VentanaPrincipal(tk.Tk): if self.label_reloj: self.label_reloj.config(text="Error al obtener la hora") print(f"Error en el reloj: {e}") - # CORRECCIÓN: Eliminamos la detención para que el after continúe intentando - # self.detener_actualizacion_reloj() - # return self.reloj_after_id = self.after(INTERVALO_RELOJ_MS, self.actualizar_reloj) @@ -132,9 +150,6 @@ class VentanaPrincipal(tk.Tk): if self.label_clima: self.label_clima.config(text="Error al obtener el clima") print(f"Error en la actualización del clima: {e}") - # CORRECCIÓN: Eliminamos la detención para que el after continúe intentando - # self.detener_actualizacion_clima() - # return self.clima_after_id = self.after(INTERVALO_CLIMA_MS, self.actualizar_clima) @@ -156,7 +171,7 @@ class VentanaPrincipal(tk.Tk): self.detener_actualizacion_reloj() self.detener_actualizacion_clima() - # Solo intenta detener el panel central si fue inicializado + # Detiene el hilo de TrafficMeter y el ciclo de repintado del gráfico if self.panel_central: self.panel_central.detener_actualizacion_automatica() @@ -188,135 +203,4 @@ class VentanaPrincipal(tk.Tk): frame_fecha.grid(row=0, column=2, sticky="e") self.label_reloj = ttk.Label(frame_fecha, text="Día y Hora: --/--/--", style='TLabel') - self.label_reloj.pack(side="left")# Módulo: vista/central_panel/view_radio.py - -import tkinter as tk -from tkinter import ttk -from tkinter import messagebox - -# 🎯 Asumo que MusicReproductor está importado aquí. -from logica.T2.musicReproductor import MusicReproductor -from vista.config import * - - -class RadioPanel(ttk.Frame): - """ - Panel de controles de Radio/Música. - Gestiona la interfaz de reproducción y volumen. - """ - - def __init__(self, parent_frame_musica, root, *args, **kwargs): - super().__init__(parent_frame_musica, *args, **kwargs) - self.root = root - - # 🎯 Instanciar la lógica del reproductor al inicializar la vista - self.reproductor = MusicReproductor() - - # Variables de control de UI - self.volumen_var = tk.DoubleVar(value=self.reproductor.get_volumen()) # Inicializa al volumen actual (por defecto 50) - - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) - - self.crear_interfaz_radio(self) - - # ------------------------------------------------------------- - # 🖼️ INTERFAZ DE USUARIO - # ------------------------------------------------------------- - - def crear_interfaz_radio(self, parent_frame): - """Crea los controles de reproducción y el slider de volumen.""" - - main_frame = ttk.Frame(parent_frame, padding=5, style='TFrame') - main_frame.grid(row=0, column=0, sticky="nsew") - main_frame.grid_columnconfigure(0, weight=1) # Columna de botones - main_frame.grid_columnconfigure(1, weight=1) # Columna de botones - main_frame.grid_columnconfigure(2, weight=1) # Columna de botones - main_frame.grid_columnconfigure(3, weight=1) # Columna de volumen - - # --- Título --- - ttk.Label(main_frame, text="Controles de Música", font=FUENTE_NEGOCIOS).grid( - row=0, column=0, columnspan=4, pady=(0, 10), sticky="w") - - - # --- Botones de Control (Fila 1) --- - - # Botón Play/Pause - self.boton_play_pause = ttk.Button(main_frame, text="▶️", style='Action.TButton', command=self.manejar_play_pause) - self.boton_play_pause.grid(row=1, column=1, sticky="ew", padx=5) - - # Botón Stop - ttk.Button(main_frame, text="⏹️", style='Action.TButton', command=self.manejar_stop).grid( - row=1, column=2, sticky="ew", padx=5) - - # Botón de Prueba de Carga (Para probar la reproducción de una URL) - ttk.Button(main_frame, text="📡 Cargar Stream", command=self.cargar_stream_prueba).grid( - row=1, column=0, sticky="ew", padx=5) - - # --- Slider de Volumen (Fila 2) --- - ttk.Label(main_frame, text="Volumen:", font=FUENTE_NORMAL).grid( - row=2, column=0, columnspan=4, pady=(10, 0), sticky="w") - - self.slider_volumen = ttk.Scale( - main_frame, - from_=0, - to=100, - orient="horizontal", - variable=self.volumen_var, - command=self.manejar_ajuste_volumen - ) - self.slider_volumen.grid(row=3, column=0, columnspan=4, sticky="ew", pady=(5, 0)) - - # Etiqueta de la estación actual (para estado) - self.label_estado = ttk.Label(main_frame, text="Estado: Detenido", anchor="center", font=('Arial', 9)) - self.label_estado.grid(row=4, column=0, columnspan=4, pady=(5, 0), sticky="ew") - - # ------------------------------------------------------------- - # ⏯️ MANEJO DE LA LÓGICA - # ------------------------------------------------------------- - - def manejar_play_pause(self): - """Alterna entre reproducir y pausar.""" - if self.reproductor.esta_reproduciendo(): - self.reproductor.pausar() - self.boton_play_pause.config(text="▶️") - self.label_estado.config(text="Estado: Pausado") - else: - # Si está pausado o detenido, intenta reproducir el último stream cargado. - if self.reproductor.continuar(): - self.boton_play_pause.config(text="⏸️") - self.label_estado.config(text="Estado: Reproduciendo") - else: - # Si no hay stream cargado, se mantiene detenido o se puede mostrar un error. - messagebox.showwarning("Advertencia", "No hay stream cargado para reproducir.") - - - def manejar_stop(self): - """Detiene completamente la reproducción.""" - self.reproductor.detener() - self.boton_play_pause.config(text="▶️") - self.label_estado.config(text="Estado: Detenido") - - def manejar_ajuste_volumen(self, valor): - """Ajusta el volumen del reproductor basado en el slider.""" - volumen = int(float(valor)) - self.reproductor.set_volumen(volumen) - # Puedes añadir una pequeña etiqueta para ver el volumen si es necesario. - print(f"Volumen ajustado a: {volumen}%") - - def cargar_stream_prueba(self): - """ - Carga y reproduce una URL de prueba o la última guardada. - Aquí usamos una URL de ejemplo que puede ser más estable. - """ - # Nota: La URL de tu log falló. Usamos una de prueba conocida (Radio Paradise) - URL_STREAM_PRUEBA = "http://stream.radioparadise.com/flac" - - success, mensaje = self.reproductor.cargar_y_reproducir(URL_STREAM_PRUEBA) - - if success: - self.boton_play_pause.config(text="⏸️") - self.label_estado.config(text=f"Estado: Reproduciendo {mensaje}") - else: - self.label_estado.config(text=f"Error: {mensaje}") - messagebox.showerror("Error de Stream", f"No se pudo cargar el stream. Revisa la URL y la conexión.\nDetalle: {mensaje}") \ No newline at end of file + self.label_reloj.pack(side="left") \ No newline at end of file