# noise_module.py
# Versão: 7.2 (Build com Correção de Fonte)
# Data: 01/09/2025 21:46 (São Paulo, Brasil)
# Descrição: Módulo de Análise de Ruído.
#            - CORREÇÃO: Removida a tentativa de obter a fonte do ttk.Treeview que causava um erro TclError.
#            - Edição in-line do nome do teste no histórico com duplo-clique.

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import ctypes, os, sys, struct, threading, time, json
from datetime import datetime, timedelta
from decimal import Decimal, getcontext

try:
    import matplotlib
    matplotlib.use("TkAgg")
    from matplotlib.figure import Figure
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
    LIBS_DISPONIVEIS = True
except ImportError:
    LIBS_DISPONIVEIS = False

from .app_console import fc_log
from .shell_open import open_path_with_default_app

try:
    from .noise_database import NoiseDatabase

    DATABASE_AVAILABLE = True
except ImportError:
    try:
        from noise_database import NoiseDatabase
        DATABASE_AVAILABLE = True
    except ImportError:
        DATABASE_AVAILABLE = False
        fc_log("noise", "⚠️ Módulo de banco de dados não disponível - histórico desabilitado")

try:
    from .state_persistence import StatePersistence
    STATE_PERSISTENCE_AVAILABLE = True
except ImportError:
    try:
        from state_persistence import StatePersistence
        STATE_PERSISTENCE_AVAILABLE = True
    except ImportError:
        STATE_PERSISTENCE_AVAILABLE = False
        fc_log("noise", "⚠️ Sistema de persistência de estado não disponível")

try:
    from .i18n import get_translator, t
    TRANSLATOR_AVAILABLE = True
except ImportError:
    try:
        from i18n import get_translator, t
        TRANSLATOR_AVAILABLE = True
    except ImportError:
        TRANSLATOR_AVAILABLE = False
        get_translator = None
        t = None
        fc_log("noise", "⚠️ Sistema de traduções indisponível para NoiseModule")

# --- CONFIGURAÇÕES E CONSTANTES DO MÓULO ---
getcontext().prec = 10
DLL_NAME = "UHFRFID.dll"
# COM_PORT = 4 ### ALTERADO: Removido
BAUD_RATE = 115200
FIXED_FREQ_MHZ = 915.0
COMMAND_CODE_SET_FREQ = 0x14
RFID_CMD_GET_RSSIVALU = 0x64
RFID_CMD_GET_TEMPERATURE = 0x34
TEMPERATURE_LIMIT = 50.0

try:
    if hasattr(sys, '_MEIPASS'):
        dll_path = os.path.join(sys._MEIPASS, DLL_NAME)
    else:
        dll_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), DLL_NAME)
    rfid_sdk = ctypes.CDLL(dll_path)
    rfid_sdk.UHF_RFID_Open.argtypes = [ctypes.c_ubyte, ctypes.c_int]
    rfid_sdk.UHF_RFID_Open.restype = ctypes.c_int
    rfid_sdk.UHF_RFID_Close.argtypes = [ctypes.c_ubyte]
    rfid_sdk.UHF_RFID_Close.restype = ctypes.c_int
    rfid_sdk.UHF_RFID_Set.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint)]
    rfid_sdk.UHF_RFID_Set.restype = ctypes.c_int
except OSError:
    rfid_sdk = None

class NoiseModule(ttk.Frame):
    ### ALTERADO: Adicionado com_port=4 ###
    def __init__(self, master=None, is_licensed=False, app_shell=None, demo_mode=False, com_port=4):
        super().__init__(master)
        self.master = master
        self.is_licensed = is_licensed
        self.app_shell = app_shell
        self.demo_mode = demo_mode
        self.com_port = com_port ### NOVO: Armazena a porta COM ###
        
        # NOVO: Define o nome do módulo para identificação
        self.module_name = "Noise Check"
        
        # NOVO: Dicionário para persistir cores dos testes
        self.test_colors = {}
        self._load_test_colors()

        # CORREÇÃO: Sistema de Limites de Licença
        self.license_limits = {'min_freq': 800, 'max_freq': 1000, 'min_power': 5, 'max_power': 25, 'is_licensed': False}
        
        # CORREÇÃO: Atualiza limites da licença se app_shell estiver disponível
        if app_shell and hasattr(app_shell, 'license_limits'):
            self.license_limits = app_shell.license_limits
            fc_log("noise", f"🔍 NoiseModule: License_limits do app_shell: {self.license_limits}")
        elif app_shell and hasattr(app_shell, '_calculate_license_limits'):
            # Tenta obter limites da licença ativa
            try:
                valid_lics = ["NoiseCheck", "FastChecker"]
                license_limits = app_shell._calculate_license_limits(valid_lics)
                if license_limits.get('is_licensed', False):
                    self.license_limits = license_limits
                    fc_log("noise", f"✅ NoiseCheck: Limites da licença aplicados - Freq: {license_limits.get('min_freq')}-{license_limits.get('max_freq')}MHz, Power: {license_limits.get('min_power')}-{license_limits.get('max_power')}dBm")
                    fc_log("noise", f"✅ NoiseCheck: Licença ativa: '{license_limits.get('license_name', 'N/A')}'")
                else:
                    fc_log("noise", "⚠️ NoiseCheck: Usando limites padrão (sem licença ativa)")
                    fc_log("noise", f"🔍 NoiseCheck: License_limits recebidos: {license_limits}")
            except Exception as e:
                fc_log("noise", f"⚠️ NoiseCheck: Erro ao obter limites da licença: {e}")
                fc_log("noise", "⚠️ NoiseCheck: Usando limites padrão")
        
        # CORREÇÃO: Atualiza o status da licença baseado nos limites obtidos
        if self.license_limits.get('is_licensed', False):
            self.is_licensed = True
            fc_log("noise", "✅ NoiseModule: Status da licença definido como True baseado nos limites")
        else:
            self.is_licensed = False
            fc_log("noise", "⚠️ NoiseModule: Status da licença definido como False (sem licença válida)")

        self.test_name_var = tk.StringVar(value="Noise")
        self.scan_time_var = tk.StringVar(value="60")
        self.run_mode_var = tk.StringVar(value="Single")
        # Último tempo válido em modo Único (para restaurar ao sair de Contínuo)
        self._noise_scan_time_backup = "60"
        # CORREÇÃO: Status inicial contextual baseado na licença
        is_licensed = self.is_licensed
        initial_status = "Modo browser - funcionalidade limitada" if not is_licensed else "Pronto para iniciar teste de ruído"
        self.status_var = tk.StringVar(value=initial_status)

        self.test_is_running = False
        self.stop_requested = False
        self.worker_thread = None

        self.live_data, self.imported_data = {}, {}
        self.multiple_tests = {}
        self.current_test_name = ""
        self.annot = None
        self.test_start_time = None
        self.current_sort_column = None
        self.current_sort_reverse = False  # NOVO: Armazena o horário de início do teste
        self._last_plot_update = 0.0
        self._plot_update_interval = 0.3  # segundos entre atualizações completas do gráfico
        
        # Sistema de traduções
        self.translator = get_translator() if TRANSLATOR_AVAILABLE and callable(get_translator) else None
        self._widget_refs = {}
        self._translation_cache = {}
        self._last_status_message = None
        self._last_status_time = 0.0
        self._status_update_interval = 0.4  # segundos entre atualizações frequentes
        # Variáveis de zoom
        self.zoom_factor = 1.0
        self.original_xlim = None
        self.original_ylim = None

        self.database = None
        self.test_history = []
        if DATABASE_AVAILABLE:
            try:
                self.database = NoiseDatabase()
                # NOVO: Executa migração de dados antigos
                self.database.migrate_old_data()
                self.test_history = self.database.get_test_history()
                fc_log("noise", f"📊 Carregados {len(self.test_history)} testes de ruído do banco de dados")
            except Exception as e:
                fc_log("noise", f"⚠️ Erro ao carregar banco de dados: {e}")
                self.database = None
        
        # NOVO: Sistema de Persistência de Estado
        self.state_persistence = None
        if STATE_PERSISTENCE_AVAILABLE:
            try:
                # Cria sistema de persistência específico para o noise module
                state_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "noise_state.json")
                self.state_persistence = StatePersistence(state_file)
                fc_log("noise", "✅ Sistema de persistência de estado inicializado para Noise Module")
            except Exception as e:
                fc_log("noise", f"⚠️ Erro ao inicializar persistência de estado: {e}")
                self.state_persistence = None
        
        self.create_widgets()
        self.update_ui_state(False)
        self._apply_translations()
        
        # NOVO: Carrega estado anterior
        self._load_previous_state()
        self._sync_scan_time_field_with_mode()
        
        self.bind("<Destroy>", self.on_destroy)

        if not rfid_sdk or not LIBS_DISPONIVEIS:
            self.show_dependency_error()

    def on_destroy(self, event=None):
        # NOVO: Salva estado antes de destruir
        self._save_current_state()
        
        if self.test_is_running:
            self.stop_requested = True
            self.test_is_running = False
            if self.worker_thread and self.worker_thread.is_alive():
                self.worker_thread.join(timeout=0.5)
        if rfid_sdk:
            ### ALTERADO: usa self.com_port ###
            try: rfid_sdk.UHF_RFID_Close(self.com_port)
            except Exception as e: fc_log("noise", f"Erro ao fechar COM no módulo de ruído: {e}")

    def show_dependency_error(self):
        for widget in self.winfo_children(): widget.destroy()
        error_msg = ""
        if not LIBS_DISPONIVEIS: error_msg += "Biblioteca 'matplotlib' não encontrada.\nInstale com: pip install matplotlib\n\n"
        if not rfid_sdk: error_msg += f"Não foi possível carregar a DLL '{DLL_NAME}'.\nEste módulo não funcionará."
        ttk.Label(self, text=error_msg, justify="center", font=("Segoe UI", 12, "bold"), foreground="red").pack(expand=True, padx=20, pady=20)

    def _load_previous_state(self):
        """Carrega o estado anterior da interface"""
        if not self.state_persistence:
            return
        
        try:
            state = self.state_persistence.load_state()
            if state and "interface_config" in state:
                config = state["interface_config"]
                
                # Carrega valores da interface
                if "test_name" in config:
                    self.test_name_var.set(config["test_name"])
                if "scan_time" in config:
                    self.scan_time_var.set(config["scan_time"])
                if "run_mode" in config:
                    self.run_mode_var.set(config["run_mode"])
                
                fc_log("noise", "✅ Noise Module: Estado anterior carregado com sucesso")
        except Exception as e:
            fc_log("noise", f"⚠️ Erro ao carregar estado anterior: {e}")

    def _save_current_state(self):
        """Salva o estado atual da interface"""
        if not self.state_persistence:
            return
        
        try:
            state = {
                "interface_config": {
                    "test_name": self.test_name_var.get(),
                    "scan_time": self.scan_time_var.get(),
                    "run_mode": self.run_mode_var.get()
                },
                "last_session": {
                    "timestamp": datetime.now().isoformat(),
                    "module": "Noise Check"
                }
            }
            
            success = self.state_persistence.save_state(state)
            if success:
                fc_log("noise", "💾 Noise Module: Estado salvo com sucesso")
        except Exception as e:
            fc_log("noise", f"⚠️ Erro ao salvar estado: {e}")

    def _schedule_auto_save(self):
        """Agenda salvamento automático após mudanças"""
        if hasattr(self, '_save_timer'):
            self.after_cancel(self._save_timer)
        
        self._save_timer = self.after(5000, self._auto_save_state)  # 5 segundos

    def _auto_save_state(self):
        """Executa salvamento automático"""
        try:
            if hasattr(self, 'state_persistence') and self.state_persistence:
                self._save_current_state()
        except Exception as e:
            fc_log("noise", f"⚠️ Erro no auto-save: {e}")

    def _setup_auto_save_callbacks(self):
        """Configura callbacks para salvamento automático quando valores mudam"""
        if not self.state_persistence:
            return
        
        try:
            def on_variable_change(*args):
                self._schedule_auto_save()

            def on_run_mode_change(*args):
                self._sync_scan_time_field_with_mode()
                self._schedule_auto_save()
            
            self.test_name_var.trace('w', on_variable_change)
            self.scan_time_var.trace('w', on_variable_change)
            self.run_mode_var.trace('w', on_run_mode_change)
            
            fc_log("noise", "✅ Callbacks de auto-save configurados para Noise Module")
        except Exception as e:
            fc_log("noise", f"⚠️ Erro ao configurar callbacks de auto-save: {e}")

    def create_widgets(self):
        # Layout principal: barra lateral + área principal
        main_container = ttk.Frame(self)
        main_container.pack(fill="both", expand=True, padx=5, pady=5)
        
        # Configuração do grid
        main_container.columnconfigure(1, weight=1)  # Área principal ocupa espaço restante
        main_container.rowconfigure(0, weight=1)
        
        # BARRA LATERAL (esquerda)
        self.sidebar = ttk.Frame(main_container, width=300)
        self.sidebar.grid(row=0, column=0, sticky="nsew", padx=(0, 5))
        self.sidebar.grid_propagate(False)  # Mantém largura fixa
        
        # ÁREA PRINCIPAL (direita)
        self.main_area = ttk.Frame(main_container)
        self.main_area.grid(row=0, column=1, sticky="nsew")
        self.main_area.columnconfigure(0, weight=1)
        self.main_area.rowconfigure(0, weight=1)
        
        # Construir componentes da barra lateral
        self._build_sidebar()
        
        # Construir área principal
        self._build_main_area()
        
        # Mensagem de modo browser (se necessário)
        self._setup_browser_mode_message()
        
        # NOVO: Adiciona callbacks de salvamento automático
        self._setup_auto_save_callbacks()

    def _t(self, key, default=None, **kwargs):
        """Retorna a tradução para a chave informada."""
        if self.translator:
            try:
                current_lang = getattr(self.translator, 'get_language', lambda: None)()
                cache_key = (current_lang, key)
                if cache_key in self._translation_cache:
                    text = self._translation_cache[cache_key]
                else:
                    text = self.translator.translate(key, default if default is not None else key)
                    self._translation_cache[cache_key] = text
            except Exception:
                text = default if default is not None else key
        else:
            text = default if default is not None else key
        
        if kwargs:
            try:
                return text.format(**kwargs)
            except Exception as e:
                fc_log("noise", f"⚠️ NoiseModule: Erro ao formatar texto '{key}': {e}")
                return text
        return text
    
    def _set_status(self, key, default=None, *, force=False, **kwargs):
        """Atualiza o texto de status traduzido."""
        if hasattr(self, 'status_var'):
            message = self._t(key, default, **kwargs)
            now = time.time()
            should_update = force or (message != self._last_status_message) or ((now - self._last_status_time) >= self._status_update_interval)
            if should_update:
                self.status_var.set(message)
                self._last_status_message = message
                self._last_status_time = now

    def refresh_language(self):
        """Contrato com app_shell: reaplica traduções quando o idioma muda."""
        self._translation_cache.clear()
        self._last_status_message = None
        self._last_status_time = 0.0
        self.after_idle(self._apply_translations)

    def _apply_translations(self):
        """Aplica traduções aos elementos da interface."""
        try:
            # Frames principais
            if hasattr(self, 'config_frame'):
                self.config_frame.configure(text=self._t('noise.test_config', 'Configuração do Teste'))
            if hasattr(self, 'controls_frame'):
                self.controls_frame.configure(text=self._t('noise.controls', 'Controles'))
            if hasattr(self, 'history_frame'):
                self.history_frame.configure(text=self._t('noise.test_history', 'Histórico de Testes'))
            if hasattr(self, 'plot_title_label'):
                self.plot_title_label.configure(text=self._t('noise.monitor_title', 'Monitor de Ruído'))

            # Labels de configuração
            if hasattr(self, 'test_name_label'):
                self.test_name_label.configure(text=self._t('noise.name', 'Nome:'))
            if hasattr(self, 'scan_time_label'):
                self.scan_time_label.configure(text=self._t('noise.time_s', 'Tempo (s):'))
            if hasattr(self, 'mode_label'):
                self.mode_label.configure(text=self._t('noise.mode', 'Modo:'))
            if hasattr(self, 'radio_single'):
                self.radio_single.configure(text=self._t('noise.single', 'Único'))
            if hasattr(self, 'radio_continuous'):
                self.radio_continuous.configure(text=self._t('noise.continuous', 'Contínuo'))

            # Botões da barra lateral
            if hasattr(self, 'btn_start'):
                self.btn_start.configure(text=self._t('noise.test', 'Testar'))
            if hasattr(self, 'btn_stop'):
                self.btn_stop.configure(text=self._t('noise.stop', 'Parar'))
            if hasattr(self, 'btn_clear_plot'):
                self.btn_clear_plot.configure(text=self._t('noise.clear_plot', 'Limpar Gráfico'))
            if hasattr(self, 'btn_save_selected'):
                self.btn_save_selected.configure(text=self._t('noise.save_selected', 'Salvar Selecionados'))
            if hasattr(self, 'btn_import_tests'):
                self.btn_import_tests.configure(text=self._t('noise.import_tests', 'Importar Testes'))
            if hasattr(self, 'btn_generate_report'):
                self.btn_generate_report.configure(text=self._t('noise.report_pdf', 'Relatório Selecionados (PDF)'))

            # Histórico - cabeçalhos
            if hasattr(self, 'history_stats_label'):
                self.update_history_stats()
            if hasattr(self, 'history_tree'):
                self._update_history_headings()
            if hasattr(self, 'select_all_button'):
                self.select_all_button.configure(text=self._t('noise.select_all', 'Selecionar Todos'))
            if hasattr(self, 'deselect_all_button'):
                self.deselect_all_button.configure(text=self._t('noise.deselect_all', 'Deselecionar Todos'))
            if hasattr(self, 'delete_selected_button'):
                self.delete_selected_button.configure(text=self._t('noise.delete_selected', 'Excluir Selecionados...'))

            # Mensagem de modo browser
            if hasattr(self, 'browser_mode_label'):
                self.browser_mode_label.configure(text=self._t('noise.browser_mode', ' modo browser'))

            # Eixos do gráfico
            if hasattr(self, 'ax'):
                self.ax.set_xlabel(self._t('noise.x_axis_label', 'Tempo (s)'))
                self.ax.set_ylabel(self._t('noise.y_axis_label', 'Intensidade (dBm)'))

            # Status inicial
            if hasattr(self, 'status_var') and not self.status_var.get():
                key = 'noise.status_ready' if self.is_licensed else 'noise.status_browser_mode'
                default = 'Pronto para iniciar teste de ruído' if self.is_licensed else 'Modo browser - funcionalidade limitada'
                self._set_status(key, default)

        except Exception as e:
            fc_log("noise", f"⚠️ NoiseModule: Erro ao aplicar traduções: {e}")

    def _update_history_headings(self):
        """Atualiza os títulos das colunas do histórico conforme o idioma."""
        try:
            headings = {
                "Plot": self._t('noise.plot', 'Plot'),
                "ID": self._t('noise.id', 'ID'),
                "Nome": self._t('noise.test_name', 'Nome do Teste'),
                "Duração": self._t('noise.duration', 'Duração (s)'),
                "Ruído Médio": self._t('noise.avg_noise', 'Ruído Médio (dBm)'),
                "Ruído Mínimo": self._t('noise.min_noise', 'Ruído Mínimo (dBm)'),
                "Ruído Máximo": self._t('noise.max_noise', 'Ruído Máximo (dBm)'),
                "Hora Ruído Máximo": self._t('noise.max_noise_time', 'Hora Ruído Máximo'),
                "Data/Hora": self._t('noise.date_time', 'Data/Hora'),
                "Severidade": self._t('noise.severity', 'Severidade'),
            }
            for column, label in headings.items():
                if column in ("Plot", "ID"):
                    suffix = " ↕"
                else:
                    suffix = " ↕"
                try:
                    self.history_tree.heading(column, text=f"{label}{suffix}", command=lambda col=column: self.sort_treeview(col))
                except Exception as e:
                    fc_log("noise", f"⚠️ NoiseModule: Erro ao atualizar heading '{column}': {e}")
        except Exception as exc:
            fc_log("noise", f"⚠️ NoiseModule: Falha ao atualizar cabeçalhos do histórico: {exc}")

    def _build_sidebar(self):
        """Constrói a barra lateral com todas as seções"""
        # 1. Configuração do Teste
        self._build_test_config_section()
        
        # 2. Controles
        self._build_controls_section()

    def _build_test_config_section(self):
        """Seção de configuração do teste"""
        self.config_frame = ttk.LabelFrame(self.sidebar, text=self._t('noise.test_config', 'Configuração do Teste'), padding=10)
        self.config_frame.pack(fill="x", padx=5, pady=5)
        
        # Nome do teste
        name_frame = ttk.Frame(self.config_frame)
        name_frame.pack(fill="x", pady=2)
        self.test_name_label = ttk.Label(name_frame, text=self._t('noise.name', 'Nome:'))
        self.test_name_label.pack(anchor="w")
        self.test_name_entry = ttk.Entry(name_frame, textvariable=self.test_name_var, width=20)
        self.test_name_entry.pack(fill="x", pady=(2, 0))
        
        # Tempo de varredura
        time_frame = ttk.Frame(self.config_frame)
        time_frame.pack(fill="x", pady=2)
        self.scan_time_label = ttk.Label(time_frame, text=self._t('noise.time_s', 'Tempo (s):'))
        self.scan_time_label.pack(anchor="w")
        self.scan_time_entry = ttk.Entry(time_frame, textvariable=self.scan_time_var, width=20)
        self.scan_time_entry.pack(fill="x", pady=(2, 0))
        
        # Modo de execução
        mode_frame = ttk.Frame(self.config_frame)
        mode_frame.pack(fill="x", pady=2)
        self.mode_label = ttk.Label(mode_frame, text=self._t('noise.mode', 'Modo:'))
        self.mode_label.pack(anchor="w")
        
        mode_options_frame = ttk.Frame(mode_frame)
        mode_options_frame.pack(fill="x", pady=(2, 0))
        
        self.radio_single = ttk.Radiobutton(mode_options_frame, text=self._t('noise.single', 'Único'), variable=self.run_mode_var, value="Single")
        self.radio_single.pack(side="left")
        self.radio_continuous = ttk.Radiobutton(mode_options_frame, text=self._t('noise.continuous', 'Contínuo'), variable=self.run_mode_var, value="Continuous")
        self.radio_continuous.pack(side="left", padx=(10, 0))

    def _build_controls_section(self):
        """Seção de controles"""
        self.controls_frame = ttk.LabelFrame(self.sidebar, text=self._t('noise.controls', 'Controles'), padding=10)
        self.controls_frame.pack(fill="x", padx=5, pady=5)
        
        # Botão de teste
        self.btn_start = ttk.Button(self.controls_frame, text=self._t('noise.test', 'Testar'), command=self.start_scan, state="disabled")
        self.btn_start.pack(fill="x", pady=2)
        
        # Botão de parar
        self.btn_stop = ttk.Button(self.controls_frame, text=self._t('noise.stop', 'Parar'), command=self.stop_scan, state="disabled")
        self.btn_stop.pack(fill="x", pady=2)
        
        # Botão de limpar
        self.btn_clear_plot = ttk.Button(self.controls_frame, text=self._t('noise.clear_plot', 'Limpar Gráfico'), command=self.clear_plot_and_selection)
        self.btn_clear_plot.pack(fill="x", pady=2)
        
        # Botão de salvar
        self.btn_save_selected = ttk.Button(self.controls_frame, text=self._t('noise.save_selected', 'Salvar Selecionados'), command=self.save_selected_tests)
        self.btn_save_selected.pack(fill="x", pady=2)
        
        # Botão de importar
        self.btn_import_tests = ttk.Button(self.controls_frame, text=self._t('noise.import_tests', 'Importar Testes'), command=self.import_tests)
        self.btn_import_tests.pack(fill="x", pady=2)
        
        # Botão de relatório PDF
        self.btn_generate_report = ttk.Button(self.controls_frame, text=self._t('noise.report_pdf', 'Relatório Selecionados (PDF)'), command=self.generate_pdf_report, state="disabled")
        self.btn_generate_report.pack(fill="x", pady=2)


    def _build_main_area(self):
        """Constrói a área principal com gráfico e histórico"""
        # Área do gráfico
        self._build_plot_area()
        
        # Área do histórico (se disponível)
        if DATABASE_AVAILABLE:
            self._build_history_area()

    def _build_plot_area(self):
        """Constrói a área do gráfico"""
        plot_container = ttk.Frame(self.main_area)
        plot_container.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
        plot_container.columnconfigure(0, weight=1)
        plot_container.rowconfigure(1, weight=1)
        
        self.plot_title_label = ttk.Label(plot_container, text=self._t('noise.monitor_title', 'Monitor de Ruído'), font=("Segoe UI", 11, "bold"))
        self.plot_title_label.grid(row=0, column=0, sticky="w", padx=5, pady=(0, 4))
        
        canvas_frame = ttk.Frame(plot_container)
        canvas_frame.grid(row=1, column=0, sticky="nsew")
        canvas_frame.columnconfigure(0, weight=1)
        canvas_frame.rowconfigure(0, weight=1)
        
        # NOVO: Armazena referência ao frame do plot
        self.plot_frame = plot_container
        
        self.fig = Figure(figsize=(6, 4), dpi=100)
        self.ax = self.fig.add_subplot(111)
        self.fig.subplots_adjust(right=0.75, bottom=0.15)
        
        self.canvas = FigureCanvasTkAgg(self.fig, master=canvas_frame)
        self.canvas.draw()
        self.canvas.get_tk_widget().grid(row=0, column=0, sticky="nsew")
        
        # Controles de zoom e pan na parte superior esquerda
        self._setup_zoom_controls(canvas_frame)
        
        self.setup_plot()

    def _build_history_area(self):
        """Constrói a área do histórico"""
        self.history_frame = ttk.LabelFrame(self.main_area, text=self._t('noise.test_history', 'Histórico de Testes'), padding=10)
        self.history_frame.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
        self.history_frame.columnconfigure(0, weight=1)
        
        stats_frame = ttk.Frame(self.history_frame)
        stats_frame.pack(fill="x", pady=(0, 5))
        self.history_stats_label = ttk.Label(stats_frame, text=self._t('noise.stats_loading', 'Estatísticas: Carregando...'), font=("Segoe UI", 9))
        self.history_stats_label.pack(side="left")
        
        # NOVO: Botão de ajuda para severidade (lupa igual ao Simulador)
        self.severity_help_button = ttk.Label(stats_frame, text="🔍", font=("Helvetica", 12), cursor="hand2", foreground="#0066cc")
        self.severity_help_button.pack(side="left", padx=(10, 0))
        self.severity_help_button.bind('<Button-1>', lambda e: self.show_severity_help())

        history_tree_frame = ttk.Frame(self.history_frame)
        history_tree_frame.pack(fill="both", expand=True, pady=(5, 0))
        
        columns = ("Plot", "ID", "Nome", "Duração", "Ruído Médio", "Ruído Mínimo", "Ruído Máximo", "Hora Ruído Máximo", "Data/Hora", "Severidade")
        display_columns = ("Plot", "Nome", "Duração", "Ruído Médio", "Ruído Mínimo", "Ruído Máximo", "Hora Ruído Máximo", "Data/Hora", "Severidade")
        
        self.history_tree = ttk.Treeview(history_tree_frame, columns=columns, show="headings", height=6, displaycolumns=display_columns)
        
        self.history_tree.column("Plot", width=40, anchor="center")
        self.history_tree.column("ID", width=50, anchor="center")
        self.history_tree.column("Nome", width=150, anchor="w")
        self.history_tree.column("Duração", width=100, anchor="center")
        self.history_tree.column("Ruído Médio", width=120, anchor="center")
        self.history_tree.column("Ruído Mínimo", width=120, anchor="center")
        self.history_tree.column("Ruído Máximo", width=120, anchor="center")
        self.history_tree.column("Hora Ruído Máximo", width=140, anchor="center")
        self.history_tree.column("Data/Hora", width=150, anchor="center")
        self.history_tree.column("Severidade", width=80, anchor="center")
        self._update_history_headings()
        
        history_scrollbar = ttk.Scrollbar(history_tree_frame, orient="vertical", command=self.history_tree.yview)
        self.history_tree.configure(yscrollcommand=history_scrollbar.set)
        self.history_tree.pack(side="left", fill="both", expand=True)
        history_scrollbar.pack(side="right", fill="y")
        
        history_actions_frame = ttk.Frame(self.history_frame)
        history_actions_frame.pack(fill="x", pady=(5, 0))

        self.select_all_button = ttk.Button(history_actions_frame, text=self._t('noise.select_all', 'Selecionar Todos'), command=self.select_all_tests, width=18)
        self.select_all_button.pack(side="left", padx=(0, 5))
        self.deselect_all_button = ttk.Button(history_actions_frame, text=self._t('noise.deselect_all', 'Deselecionar Todos'), command=self.deselect_all_tests, width=18)
        self.deselect_all_button.pack(side="left", padx=(0, 5))
        self.delete_selected_button = ttk.Button(history_actions_frame, text=self._t('noise.delete_selected', 'Excluir Selecionados...'), command=self.delete_selected_tests, width=20)
        self.delete_selected_button.pack(side="left", padx=(0, 5))
        
        # Eventos do treeview
        self.history_tree.bind('<Button-1>', self.on_history_tree_click)
        
        # NOVO: Sistema de clique/duplo-clique com timer
        self._click_timer = None
        self._click_count = 0
        
        self.update_history_stats()
        self.load_history_to_tree()
        self.after(100, self.auto_plot_selected_tests)

    def _setup_browser_mode_message(self):
        """Configura mensagem de modo browser"""
        # --- Mensagem de Modo Browser ---
        # Posiciona dentro da seção "Configuração do Teste" se existir
        target_parent = getattr(self, 'config_frame', self)
        self.browser_frame = ttk.Frame(target_parent)
        
        # Bullet point azul
        bullet_label = ttk.Label(self.browser_frame, text="•", foreground="blue", font=("Helvetica", 10))
        bullet_label.pack(side='left', anchor='w')
        
        # Texto "modo browser"
        self.browser_mode_label = ttk.Label(self.browser_frame, text=self._t('noise.browser_mode', ' modo browser'), foreground="blue", font=("Helvetica", 10))
        self.browser_mode_label.pack(side='left', anchor='w')
        
        # CORREÇÃO: Controle inicial da visibilidade baseado na licença
        fc_log("noise", f"🔍 NoiseModule: Status inicial da licença: {self.is_licensed}")
        if self.is_licensed:
            self.browser_frame.pack_forget()  # Esconde quando há licença
            fc_log("noise", "✅ NoiseModule: Mensagem de modo browser ocultada (licença ativa)")
        else:
            self.browser_frame.pack(fill="x", pady=(5, 5), padx=5)  # Mostra quando não há licença
            fc_log("noise", "⚠️ NoiseModule: Mensagem de modo browser visível (sem licença)")

    def update_license_status(self, new_license_status):
        """CORREÇÃO: Atualiza o status da licença e a interface do usuário"""
        try:
            fc_log("noise", f"🔔 NoiseModule: Atualizando status da licença de {self.is_licensed} para {new_license_status}")
            
            # CORREÇÃO: Verifica se a licença é realmente válida para o módulo Noise
            # Mesmo que o app_shell diga que há licença, verifica se é específica para Noise
            if new_license_status and self.app_shell:
                try:
                    # Verifica se há licença específica para Noise
                    valid_lics = ["NoiseCheck", "FastChecker"]
                    license_limits = self.app_shell._calculate_license_limits(valid_lics)
                    if not license_limits.get('is_licensed', False):
                        fc_log("noise", "⚠️ NoiseModule: AppShell diz que há licença, mas não é válida para Noise")
                        new_license_status = False
                    else:
                        fc_log("noise", f"✅ NoiseModule: Licença válida confirmada: {license_limits.get('license_name', 'N/A')}")
                except Exception as e:
                    fc_log("noise", f"⚠️ NoiseModule: Erro ao verificar licença específica: {e}")
                    new_license_status = False
            
            # Atualiza o status interno
            self.is_licensed = new_license_status
            
            # Atualiza o status inicial
            if new_license_status:
                self._set_status('noise.status_ready', "Pronto para iniciar teste de ruído")
            else:
                self._set_status('noise.status_browser_mode', "Modo browser - funcionalidade limitada")
            
            # Atualiza a interface
            self._update_ui_for_license_status()
            
            fc_log("noise", f"✅ NoiseModule: Status da licença atualizado para: {self.is_licensed}")
            
        except Exception as e:
            fc_log("noise", f"❌ Erro ao atualizar status da licença no NoiseModule: {e}")
    
    def _update_ui_for_license_status(self):
        """CORREÇÃO: Atualiza a interface baseada no status da licença (padrão Antenna)"""
        try:
            # CORREÇÃO: Verifica se os widgets já foram criados
            if not hasattr(self, 'browser_frame'):
                fc_log("noise", "⚠️ NoiseModule: Widgets ainda não criados, aguardando...")
                return
            
            fc_log("noise", f"🔍 NoiseModule: Atualizando interface para licença: {self.is_licensed}")
            
            # CORREÇÃO: Controla visibilidade do frame de modo browser (como no módulo Antenna)
            if self.is_licensed:
                # Esconde mensagem de modo browser quando há licença
                if hasattr(self, 'browser_frame'):
                    try:
                        self.browser_frame.pack_forget()
                    except tk.TclError:
                        self.browser_frame.grid_forget()
                    fc_log("noise", "✅ NoiseModule: Frame de modo browser ocultado")
            else:
                # Mostra mensagem de modo browser quando não há licença
                if hasattr(self, 'browser_frame'):
                    if not self.browser_frame.winfo_manager():
                        try:
                            self.browser_frame.pack(fill="x", pady=(5, 5), padx=5)
                        except tk.TclError:
                            self.browser_frame.grid(row=1, column=0, sticky="ew", pady=(5, 5), padx=5)
                    fc_log("noise", "✅ NoiseModule: Frame de modo browser exibido")
            
            # Atualiza o status dos botões
            if hasattr(self, 'btn_start'):
                self.btn_start.config(state='normal' if self.is_licensed else 'disabled')
            
            if hasattr(self, 'btn_stop'):
                self.btn_stop.config(state='normal' if self.is_licensed else 'disabled')
            
            # Ações de browser devem permanecer habilitadas em modo browser
            if hasattr(self, 'btn_clear_plot'):
                self.btn_clear_plot.config(state='normal')
            if hasattr(self, 'btn_save_selected'):
                self.btn_save_selected.config(state='normal')
            if hasattr(self, 'btn_import_tests'):
                self.btn_import_tests.config(state='normal')
            
            # Atualiza campos de entrada
            if hasattr(self, 'test_name_entry'):
                self.test_name_entry.config(state='normal' if self.is_licensed else 'disabled')
            
            if hasattr(self, 'scan_time_entry'):
                self._apply_scan_time_entry_state()
            
            if hasattr(self, 'run_mode_combobox'):
                self.run_mode_combobox.config(state='readonly' if self.is_licensed else 'disabled')
            
            # Atualiza botões do histórico (liberados em modo browser)
            if hasattr(self, 'delete_selected_button'):
                self.delete_selected_button.config(state='normal')
            
            fc_log("noise", f"✅ NoiseModule: Interface atualizada para status da licença: {self.is_licensed}")
            
        except Exception as e:
            fc_log("noise", f"❌ Erro ao atualizar interface do NoiseModule: {e}")

    def _sync_scan_time_field_with_mode(self, *args):
        """Modo Contínuo: tempo indeterminado — campo vazio e desativado. Modo Único: restaura duração."""
        if not hasattr(self, 'scan_time_entry'):
            return
        if self.run_mode_var.get() == "Continuous":
            try:
                cur = (self.scan_time_var.get() or "").strip()
                if cur:
                    n = int(cur)
                    if n > 0:
                        self._noise_scan_time_backup = str(n)
            except ValueError:
                pass
            self.scan_time_var.set("")
        else:
            cur = (self.scan_time_var.get() or "").strip()
            if not cur:
                self.scan_time_var.set(self._noise_scan_time_backup)
            else:
                try:
                    n = int(cur)
                    if n > 0:
                        self._noise_scan_time_backup = str(n)
                except ValueError:
                    self.scan_time_var.set(self._noise_scan_time_backup)
        self._apply_scan_time_entry_state()

    def _apply_scan_time_entry_state(self):
        """Estado do campo tempo: desativado em Contínuo; caso contrário segue licença e execução."""
        if not hasattr(self, 'scan_time_entry'):
            return
        if self.run_mode_var.get() == "Continuous":
            self.scan_time_entry.config(state='disabled')
            return
        is_running = getattr(self, 'test_is_running', False)
        if self.is_licensed:
            self.scan_time_entry.config(state='disabled' if is_running else 'normal')
        else:
            self.scan_time_entry.config(state='normal')

    def get_scan_duration(self):
        if self.run_mode_var.get() == "Continuous":
            return 0
        try:
            raw = (self.scan_time_var.get() or "").strip()
            if not raw:
                return int(self._noise_scan_time_backup) if int(self._noise_scan_time_backup) > 0 else 60
            n = int(raw)
            return n if n > 0 else 60
        except ValueError:
            try:
                b = int(str(self._noise_scan_time_backup).strip())
                return b if b > 0 else 60
            except ValueError:
                return 60

    def check_duplicate_test_name(self, test_name):
        """
        Verifica se o nome do teste já existe no histórico
        
        Args:
            test_name: Nome do teste a verificar
            
        Returns:
            bool: True se o nome já existe, False caso contrário
        """
        if not DATABASE_AVAILABLE or not self.database:
            return False
        
        try:
            history = self.database.get_test_history()
            # NOVO: Compara nomes normalizados (sem espaços extras, case-insensitive)
            test_name_normalized = test_name.strip().lower()
            existing_names = [test.get('test_name', '').strip().lower() for test in history]
            return test_name_normalized in existing_names
        except Exception as e:
            fc_log("noise", f"❌ Erro ao verificar nomes duplicados: {e}")
            return False

    def check_duplicate_test_name_excluding_current(self, test_name, current_test_id):
        """
        Verifica se o nome do teste já existe no histórico, excluindo o teste atual
        
        Args:
            test_name: Nome do teste a verificar
            current_test_id: ID do teste atual que está sendo editado
            
        Returns:
            bool: True se o nome já existe em outro teste, False caso contrário
        """
        if not DATABASE_AVAILABLE or not self.database:
            return False
        
        try:
            history = self.database.get_test_history()
            # NOVO: Compara nomes normalizados (sem espaços extras, case-insensitive)
            test_name_normalized = test_name.strip().lower()
            
            for test in history:
                if test.get('id') != current_test_id:  # Exclui o teste atual
                    existing_name = test.get('test_name', '').strip().lower()
                    if existing_name == test_name_normalized:
                        return True
            return False
        except Exception as e:
            fc_log("noise", f"❌ Erro ao verificar nomes duplicados: {e}")
            return False

    def get_test_color(self, test_id):
        """
        Gera uma cor fixa e consistente baseada no ID do teste
        
        Args:
            test_id: ID único do teste
            
        Returns:
            str: Código hexadecimal da cor
        """
        # Paleta de cores com alto contraste e boa distinção visual
        colors = [
            '#1f77b4',  # Azul
            '#d62728',  # Vermelho
            '#2ca02c',  # Verde
            '#ff7f0e',  # Laranja
            '#9467bd',  # Roxo
            '#8c564b',  # Marrom
            '#e377c2',  # Rosa
            '#7f7f7f',  # Cinza
            '#bcbd22',  # Verde-amarelado
            '#17becf',  # Ciano
            '#aec7e8',  # Azul claro
            '#ffbb78',  # Laranja claro
            '#98df8a',  # Verde claro
            '#ff9896',  # Vermelho claro
            '#c5b0d5',  # Roxo claro
            '#c49c94',  # Marrom claro
            '#f7b6d3',  # Rosa claro
            '#c7c7c7',  # Cinza claro
            '#dbdb8d',  # Amarelo claro
            '#9edae5'   # Ciano claro
        ]
        
        # Converte test_id para string para usar como chave
        test_key = str(test_id)
        
        # Se já existe uma cor atribuída a este teste, retorna ela
        if test_key in self.test_colors:
            return self.test_colors[test_key]
        
        # Se não existe, atribui uma nova cor baseada no ID
        if isinstance(test_id, (int, str)):
            try:
                test_id_int = int(test_id)
                color = colors[test_id_int % len(colors)]
            except (ValueError, TypeError):
                # Se ID inválido, usa hash do ID como string
                hash_value = hash(str(test_id)) % len(colors)
                color = colors[hash_value]
        else:
            # Fallback para IDs não numéricos
            hash_value = hash(str(test_id)) % len(colors)
            color = colors[hash_value]
        
        # Salva a cor atribuída para persistência
        self.test_colors[test_key] = color
        self._save_test_colors()
        return color
    
    def _load_test_colors(self):
        """Carrega as cores persistentes dos testes"""
        try:
            if STATE_PERSISTENCE_AVAILABLE and self.state_persistence:
                colors_data = self.state_persistence.get_state_value('test_colors', {})
                if isinstance(colors_data, dict):
                    self.test_colors = colors_data
                    fc_log("noise", f"✅ Cores dos testes carregadas: {len(self.test_colors)} cores")
        except Exception as e:
            fc_log("noise", f"⚠️ Erro ao carregar cores dos testes: {e}")
            self.test_colors = {}
    
    def _save_test_colors(self):
        """Salva as cores persistentes dos testes"""
        try:
            if STATE_PERSISTENCE_AVAILABLE and self.state_persistence:
                self.state_persistence.set_state_value('test_colors', self.test_colors)
        except Exception as e:
            fc_log("noise", f"⚠️ Erro ao salvar cores dos testes: {e}")

    def get_dynamic_x_scale(self):
        """
        Calcula a escala dinâmica do eixo X baseada nos dados sendo plotados
        
        Returns:
            tuple: (x_min, x_max) para o eixo X
        """
        max_time = 0
        active_data_sources = 0
        
        # Verifica dados ao vivo
        if self.live_data:
            live_times = [float(t) for t in self.live_data.keys()]
            if live_times:
                max_time = max(max_time, max(live_times))
                active_data_sources += 1
        
        # Verifica múltiplos testes do histórico
        if self.multiple_tests:
            for test_name, test_info in self.multiple_tests.items():
                if test_info:
                    # NOVO: Verifica se é a nova estrutura com ID
                    if isinstance(test_info, dict) and 'data' in test_info and 'id' in test_info:
                        test_data = test_info['data']
                    else:
                        # Estrutura antiga - mantém compatibilidade
                        test_data = test_info
                    
                    if test_data:
                        test_times = [float(t) for t in test_data.keys()]
                        if test_times:
                            max_time = max(max_time, max(test_times))
                            active_data_sources += 1
        
        # Verifica dados importados
        if self.imported_data:
            imported_times = [float(t) for t in self.imported_data.keys()]
            if imported_times:
                max_time = max(max_time, max(imported_times))
                active_data_sources += 1
        
        # Se não há dados, usa a duração configurada
        if max_time == 0:
            max_time = self.get_scan_duration()
        
        # NOVO: Ajusta margem baseada no número de fontes de dados
        if active_data_sources == 1:
            # Apenas um teste: margem menor para melhor visualização
            margin = max(2, max_time * 0.02)  # 2% ou mínimo 2 segundos
        else:
            # Múltiplos testes: margem maior para acomodar todos
            margin = max(5, max_time * 0.05)  # 5% ou mínimo 5 segundos
        
        return 0, max_time + margin

    def setup_plot(self):
        self.ax.clear()
        self.ax.set_xlabel("Tempo (s)"); self.ax.set_ylabel("Intensidade (dBm)")
        self.ax.grid(True, which='both', linestyle='--', linewidth=0.5)
        
        # NOVO: Usa escala dinâmica do eixo X
        x_min, x_max = self.get_dynamic_x_scale()
        self.ax.set_xlim(x_min, x_max)
        self.ax.set_ylim(-80, -40)
        
        self.annot = self.ax.annotate("", xy=(0, 0), xytext=(20, 20), textcoords="offset points",
                                       bbox=dict(boxstyle="round,pad=0.5", 
                                                facecolor="lightblue", 
                                                alpha=0.9,
                                                edgecolor="navy",
                                                linewidth=1),
                                       arrowprops=dict(arrowstyle="->", 
                                                      connectionstyle="arc3,rad=0",
                                                      color="navy",
                                                      lw=1),
                                       fontsize=9, 
                                       ha='left',
                                       va='bottom',
                                       wrap=True)
        self.annot.set_visible(False)
        self.setup_interactivity()

    def setup_interactivity(self):
        if hasattr(self, 'canvas'):
            self.canvas.mpl_connect('motion_notify_event', self._on_hover)

    def _on_hover(self, event):
        if event.inaxes != self.ax:
            if self.annot and self.annot.get_visible():
                self.annot.set_visible(False); self.canvas.draw_idle()
            return
        for line in self.ax.lines:
            cont, ind = line.contains(event)
            if cont:
                pos = line.get_xydata()[ind["ind"][0]]
                x_coord, y_coord = pos[0], pos[1]
                tooltip_text = f"{x_coord:.2f}s -> {y_coord:.2f} dBm"
                self._position_tooltip_smartly(event, x_coord, y_coord, tooltip_text)
                return
        if self.annot and self.annot.get_visible():
            self.annot.set_visible(False); self.canvas.draw_idle()

    def _position_tooltip_smartly(self, event, x_pos, y_pos, tooltip_text):
        """Posiciona o tooltip de forma inteligente baseado na posição do mouse"""
        try:
            # Obtém dimensões do canvas em pixels
            canvas_width = self.canvas.get_width_height()[0]
            canvas_height = self.canvas.get_width_height()[1]
            
            # Converte coordenadas do evento para pixels do canvas
            x_pixel = event.x
            y_pixel = event.y
            
            # Margens mais conservadoras para evitar cortes
            margin_right = 300   # Margem para borda direita
            margin_top = 150     # Margem para borda superior
            margin_bottom = 150  # Margem para borda inferior
            margin_left = 250    # Margem para borda esquerda
            
            # Calcula posição do tooltip baseada na posição do mouse
            if x_pixel > canvas_width - margin_right:  # Próximo à borda direita
                # Posiciona à esquerda do mouse com margem maior
                xytext = (-150, 20)
                ha = 'right'
            elif x_pixel < margin_left:  # Próximo à borda esquerda
                # Posiciona à direita do mouse com margem maior
                xytext = (150, 20)
                ha = 'left'
            else:  # Posição normal (centro)
                # Posiciona à direita do mouse
                xytext = (20, 20)
                ha = 'left'
            
            # Ajusta posição vertical se necessário
            if y_pixel < margin_top:  # Próximo ao topo
                xytext = (xytext[0], 150)  # Margem maior para baixo
            elif y_pixel > canvas_height - margin_bottom:  # Próximo à parte inferior
                xytext = (xytext[0], -150)  # Margem maior para cima
            
            # Atualiza o tooltip com posicionamento inteligente
            self.annot.xy = (x_pos, y_pos)
            self.annot.xytext = xytext
            self.annot.set_text(tooltip_text)
            self.annot.set_ha(ha)
            self.annot.set_visible(True)
            
            # Força redesenho para garantir que o tooltip apareça
            self.canvas.draw_idle()
            
        except Exception as e:
            fc_log("noise", f"⚠️ Erro no posicionamento do tooltip: {e}")
            # Fallback para posicionamento padrão
            try:
                self.annot.xy = (x_pos, y_pos)
                self.annot.set_text(tooltip_text)
                self.annot.set_visible(True)
                self.canvas.draw_idle()
            except Exception as fallback_error:
                fc_log("noise", f"⚠️ Erro no fallback do tooltip: {fallback_error}")

    def update_plot(self):
        self.ax.clear(); self.setup_plot()
        all_data_plotted = False
        if self.imported_data:
            times = list(self.imported_data.keys()); values = list(self.imported_data.values())
            self.ax.plot(times, values, 'g-', marker='o', markersize=2, linewidth=0.8, alpha=0.6, label='Dados Importados', picker=5)
            all_data_plotted = True
        if self.multiple_tests:
            for test_name, test_info in self.multiple_tests.items():
                if test_info:
                    # NOVO: Verifica se é a nova estrutura com ID
                    if isinstance(test_info, dict) and 'data' in test_info:
                        test_data = test_info['data']
                    else:
                        # Estrutura antiga - mantém compatibilidade
                        test_data = test_info
                    
                    # Usa SEMPRE uma cor consistente baseada no ID do teste,
                    # garantindo que a cor não mude entre sessões.
                    test_id = test_info.get('id') if isinstance(test_info, dict) else None
                    if test_id is not None:
                        color = self.get_test_color(test_id)
                    else:
                        # Fallback estável (sem hash Python): chave por nome
                        color = self.get_test_color(f"noise_legacy:{test_name}")
                    
                    if test_data and len(test_data) > 0:
                        times = list(test_data.keys())
                        values = list(test_data.values())
                        self.ax.plot(times, values, '-', marker='o', markersize=2, linewidth=0.8, alpha=0.7, label=test_name, picker=5, color=color)
                        all_data_plotted = True
        if self.live_data:
            times = list(self.live_data.keys())
            # NOVO: Extrai valores da nova estrutura de dados
            if isinstance(list(self.live_data.values())[0], dict):
                values = [v["value"] for v in self.live_data.values()]
            else:
                values = list(self.live_data.values())
            # Usa a mesma cor baseada no ID do teste atual (se disponível)
            # Para teste em tempo real, usa um ID temporário baseado no nome
            if self.current_test_name:
                # Chave estável por nome (evita hash instável entre processos)
                live_color = self.get_test_color(f"noise_live:{self.current_test_name.strip()}")
            else:
                live_color = '#d62728'  # Vermelho padrão
            self.ax.plot(times, values, '-', marker='o', markersize=2, linewidth=0.8, alpha=0.9, label=self.current_test_name, picker=5, color=live_color)
            all_data_plotted = True
        
        # NOVO: Recalcula e ajusta a escala do eixo X após plotar todos os dados
        if all_data_plotted:
            x_min, x_max = self.get_dynamic_x_scale()
            self.ax.set_xlim(x_min, x_max)
            self.ax.legend(bbox_to_anchor=(1.02, 1), loc='upper left', 
                          bbox_transform=self.ax.transAxes, frameon=True, 
                          borderpad=1.0, columnspacing=1.0, fontsize=9)
            
            # NOVO: Atualiza status com informação da escala
            if hasattr(self, 'status_var'):
                test_count = len(self.multiple_tests) if self.multiple_tests else 0
                if test_count == 1:
                    self._set_status('noise.status_scale_single', "Escala X: 0-{max_time:.1f}s (teste único)", max_time=x_max)
                elif test_count > 1:
                    self._set_status('noise.status_scale_multiple', "Escala X: 0-{max_time:.1f}s ({count} testes)", max_time=x_max, count=test_count)
                else:
                    self._set_status('noise.status_scale', "Escala X: 0-{max_time:.1f}s", max_time=x_max)
        
        self.canvas.draw()

    def update_ui_state(self, is_running):
        # Estado base para ações de browser (independente de licença)
        browser_btn_state = 'disabled' if is_running else 'normal'

        # Ações que acionam o reader dependem de licença
        self.btn_start.config(state='disabled' if is_running else ('normal' if self.is_licensed else 'disabled'))
        self.btn_stop.config(state='normal' if is_running else 'disabled')
        
        # CORREÇÃO: Botões "Limpar Gráfico" e "Importar Testes" sempre habilitados
        self.btn_clear_plot.config(state='normal')
        self.btn_import_tests.config(state='normal')
        
        # Ações de browser: manter habilitadas independentemente da licença
        if hasattr(self, 'btn_save_selected'):
            self.btn_save_selected.config(state=browser_btn_state)
        if hasattr(self, 'delete_selected_button'):
            self.delete_selected_button.config(state=browser_btn_state)
        
        if DATABASE_AVAILABLE:
            for child in self.winfo_children():
                if isinstance(child, ttk.Button) and child.cget('text') in ["Selecionar Todos", "Deselecionar Todos", "Excluir Selecionados..."]:
                    child.config(state='normal')
        


        # Controle dos campos: em modo browser, permitir edição (não aciona reader)
        if self.is_licensed:
            for child in [self.radio_single, self.radio_continuous, self.test_name_entry]:
                child.config(state='disabled' if is_running else 'normal')
            self._apply_scan_time_entry_state()
        else:
            for child in [self.radio_single, self.radio_continuous, self.test_name_entry]:
                child.config(state='normal')
            self._apply_scan_time_entry_state()
            
    def start_scan(self):
        if self.demo_mode:
            messagebox.showinfo(
                self._t('noise.demo_mode', "Modo Demo"),
                self._t('noise.demo_mode_msg', "Este módulo está funcionando em modo demo.\n\nPara funcionalidade completa, adicione uma licença válida no módulo License."),
                parent=self
            )
            return
        if not self.is_licensed:
            messagebox.showwarning(
                self._t('noise.invalid_license', "Licença Inválida"),
                self._t('noise.invalid_license_msg', "Funcionalidade desabilitada."),
                parent=self
            )
            return
        
        # NOVO: Validação dos limites da licença
        if not self.license_limits.get('is_licensed', False):
            messagebox.showerror(
                self._t('noise.invalid_license', "Licença Inválida"),
                self._t('noise.invalid_license_not_found', "Nenhuma licença ativa encontrada.\n\nPara usar este módulo, ative uma licença válida no módulo License."),
                parent=self
            )
            return
        
        # NOVO: Validação da frequência fixa (915 MHz) contra limites da licença
        fixed_freq = FIXED_FREQ_MHZ  # 915.0 MHz
        min_freq = self.license_limits.get('min_freq', 800)
        max_freq = self.license_limits.get('max_freq', 1000)
        
        if not (min_freq <= fixed_freq <= max_freq):
            messagebox.showerror(
                self._t('noise.freq_outside_license', "Frequência fora da faixa da licença"),
                self._t(
                    'noise.freq_outside_license_msg',
                    "A frequência de operação do Noise Check ({freq} MHz) está fora da faixa permitida pela licença.\n\nFaixa permitida: {min_freq} - {max_freq} MHz\nFrequência do módulo: {freq} MHz\n\nEste módulo não pode operar com a licença atual.",
                    freq=fixed_freq,
                    min_freq=min_freq,
                    max_freq=max_freq
                ),
                parent=self
            )
            return
        temp = self.get_temperature()
        if temp is not None and temp >= TEMPERATURE_LIMIT:
            messagebox.showerror(
                self._t('noise.safety_stop_temp', "Parada de Segurança"),
                self._t('noise.safety_stop_temp_msg', "Temperatura ({temp}°C) excedeu o limite de {limit}°C.", temp=temp, limit=TEMPERATURE_LIMIT),
                parent=self
            )
            return
        
        # NOVO: Validação de nome duplicado
        self.current_test_name = self.test_name_var.get().strip()
        if not self.current_test_name: 
            self.current_test_name = f"Noise_{len(self.multiple_tests) + 1}"
        
        # Verifica se o nome já existe no histórico
        if self.check_duplicate_test_name(self.current_test_name):
            messagebox.showerror(
                self._t('noise.test_name_duplicate', "Nome Duplicado"),
                self._t('noise.test_name_duplicate_msg', "Já existe um teste com o nome '{name}' no histórico.\n\nPor favor, escolha um nome diferente para este teste.", name=self.current_test_name),
                parent=self
            )
            return
        self.live_data = {}
        self.test_start_time = datetime.now()  # NOVO: Registra horário de início
        self.test_is_running = True; self.stop_requested = False
        if self.app_shell: self.app_shell.set_test_running(True, "Noise Check")
        self.update_ui_state(True)
        self.worker_thread = threading.Thread(target=self.worker_scan, daemon=True); self.worker_thread.start()

    def stop_scan(self):
        self._set_status('noise.status_stop_requested', "Parada solicitada...")
        self.stop_requested = True
        self.test_is_running = False
        if self.live_data and self.current_test_name:
            # NOVO: Converte dados para formato compatível com múltiplos testes
            if isinstance(list(self.live_data.values())[0], dict):
                converted_data = {t: v["value"] for t, v in self.live_data.items()}
            else:
                converted_data = self.live_data.copy()
            self.multiple_tests[self.current_test_name] = converted_data
            self.save_test_to_history()
        if self.app_shell: self.app_shell.set_test_running(False, "Noise Check")
        
    def worker_scan(self):
        try:
            scan_duration, run_mode = self.get_scan_duration(), self.run_mode_var.get()
            ### ALTERADO: usa self.com_port ###
            if rfid_sdk.UHF_RFID_Open(self.com_port, BAUD_RATE) != 0:
                self._set_status('noise.status_error_open_com', "Erro CRÍTICO: Falha ao abrir a porta COM{port}", port=self.com_port)
                self.after(0, self.update_ui_state, False)
                return
            self._set_status('noise.status_config_frequency', "Configurando frequência para {freq} MHz...", freq=FIXED_FREQ_MHZ)
            dummy_buffer, dummy_len = ctypes.create_string_buffer(256), ctypes.c_uint(0)
            freq_data = (1).to_bytes(1, 'big') + int(FIXED_FREQ_MHZ * 1000).to_bytes(3, 'big')
            rfid_sdk.UHF_RFID_Set(COMMAND_CODE_SET_FREQ, ctypes.c_char_p(freq_data), 4, dummy_buffer, ctypes.byref(dummy_len))
            time.sleep(0.1)
            start_time = time.time()
            self._set_status('noise.status_starting_measurement', "Iniciando medição...")
            while self.test_is_running:
                current_time = time.time(); elapsed_time = current_time - start_time
                if run_mode == "Single" and elapsed_time > scan_duration: 
                    self.test_is_running = False; 
                    if self.live_data and self.current_test_name: self.save_test_to_history()
                    break
                # Modo Contínuo - mede indefinidamente até ser parado pelo operador
                if run_mode == "Continuous":
                    # Não há condição de parada automática - continua até stop_scan() ser chamado
                    # Mantém os dados acumulados sem resetar
                    pass
                output_buffer, output_len = ctypes.create_string_buffer(64), ctypes.c_uint(0)
                status_noise = rfid_sdk.UHF_RFID_Set(RFID_CMD_GET_RSSIVALU, bytes([0,0]), 2, output_buffer, ctypes.byref(output_len))
                noise_dbm = 0.0
                if status_noise == 0 and output_len.value >= 3:
                    noise_dbm = Decimal(struct.unpack('>h', output_buffer.raw[1:3])[0]) / Decimal(10.0)
                    # NOVO: Registra timestamp absoluto junto com o valor
                    absolute_time = self.test_start_time + timedelta(seconds=elapsed_time)
                    self.live_data[elapsed_time] = {
                        "value": float(noise_dbm),
                        "absolute_time": absolute_time.strftime("%H:%M:%S")  # Formato HH:MM:SS (sem milissegundos)
                    }
                now = time.time()
                if (now - self._last_plot_update) >= self._plot_update_interval:
                    self._last_plot_update = now
                    self.after(0, self.update_plot)
                if run_mode == "Continuous":
                    self._set_status(
                        'noise.status_continuous',
                        "Medição Contínua... Tempo: {elapsed:.1f}s | Intensidade: {value:.2f} dBm | Pressione PARAR para finalizar",
                        elapsed=elapsed_time,
                        value=float(noise_dbm)
                    )
                else:
                    self._set_status(
                        'noise.status_running',
                        "Medindo... Tempo: {elapsed:.1f}s | Intensidade: {value:.2f} dBm",
                        elapsed=elapsed_time,
                        value=float(noise_dbm)
                    )
                time.sleep(0.05)
        except Exception as e:
            self._set_status('noise.status_execution_error', "Erro durante a execução: {error}", error=e)
        finally:
            ### ALTERADO: usa self.com_port ###
            if rfid_sdk: rfid_sdk.UHF_RFID_Close(self.com_port)
            self.after(0, self.update_plot)
            if self.stop_requested:
                self._set_status('noise.status_measurement_stopped', "Medição parada.")
            else:
                self._set_status('noise.status_measurement_finished', "Medição finalizada.")
            if self.app_shell: self.app_shell.set_test_running(False, "Noise Check")
            self.after(0, self.update_ui_state, False)

    def clear_plot_and_selection(self):
        self.live_data.clear(); self.imported_data.clear(); self.multiple_tests.clear()
        self.current_test_name = ""
        self.setup_plot(); self.update_plot()
        
        if DATABASE_AVAILABLE and hasattr(self, 'history_tree'):
            self.deselect_all_tests()
        
                    # CORREÇÃO: Status contextual baseado na licença
            is_licensed = self.is_licensed
            if is_licensed:
                self._set_status('noise.status_plot_cleared_ready', "Gráfico limpo. Pronto para iniciar.")
            else:
                self._set_status('noise.status_plot_cleared_browser', "Gráfico limpo. Modo browser ativo.")

    def save_selected_tests(self):
        if not DATABASE_AVAILABLE or not self.database: return

        selected_ids = []
        for item in self.history_tree.get_children():
            if self.history_tree.set(item, 'Plot') == "☑":
                selected_ids.append(self.history_tree.item(item)['values'][1])
        
        if not selected_ids:
            messagebox.showwarning(
                self._t('noise.no_test_selected_save', "Nenhum Teste Selecionado"),
                self._t('noise.no_test_selected_save_msg', "Por favor, selecione um ou mais testes no histórico para salvar."),
                parent=self
            )
            return

        tests_to_save = [self.database.get_test_by_id(test_id) for test_id in selected_ids]
        
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        default_filename = f"Noise_tests_{timestamp}.json"
        
        filepath = filedialog.asksaveasfilename(
            defaultextension=".json",
            filetypes=[(self._t('export_popup.filetype_json', 'JSON files'), "*.json")],
            title=self._t('noise.save_tests_title', "Salvar Testes Selecionados"),
            initialfile=default_filename,
            parent=self
        )
        if not filepath: return
            
        try:
            export_data = {
                "metadata": { "export_date": datetime.now().strftime('%d-%m-%Y %H:%M:%S'), "source": "Noise Module - FastChecker II" },
                "test_data": tests_to_save
            }
            with open(filepath, "w", encoding='utf-8') as f:
                json.dump(export_data, f, indent=2, ensure_ascii=False)
            messagebox.showinfo(
                self._t('noise.success', "Sucesso"),
                self._t('noise.tests_saved_success', "{count} teste(s) salvos com sucesso em:\n{filename}", count=len(tests_to_save), filename=os.path.basename(filepath)),
                parent=self
            )
        except Exception as e: 
            messagebox.showerror(
                self._t('noise.error_saving_tests', "Erro ao Salvar"),
                self._t('noise.error_saving_tests_msg', "Não foi possível salvar os testes.\nErro: {error}", error=e),
                parent=self
            )

    def import_tests(self):
        if not DATABASE_AVAILABLE or not self.database: return

        # NOVO: Ao importar, apaga o teste ativo (limpa gráfico e dados atuais)
        try:
            if getattr(self, 'test_is_running', False):
                # Solicita parada graciosa
                self.stop_requested = True
                self.test_is_running = False
                if hasattr(self, 'worker_thread') and self.worker_thread and self.worker_thread.is_alive():
                    self.worker_thread.join(timeout=0.5)
                if self.app_shell:
                    self.app_shell.set_test_running(False, "Noise Check")
            # Limpa dados e seleção atuais
            self.clear_plot_and_selection()
        except Exception as _e:
            fc_log("noise", f"⚠️ NoiseModule: Falha ao limpar teste ativo antes da importação: {_e}")

        filepath = filedialog.askopenfilename(
            filetypes=[(self._t('export_popup.filetype_json', 'JSON files'), "*.json")],
            title=self._t('noise.import_tests_title', "Importar Testes para o Histórico"),
            parent=self
        )
        if not filepath: return
        
        try:
            with open(filepath, "r", encoding='utf-8') as f:
                report_data = json.load(f)
            
            tests_to_import = report_data.get("test_data", [])
            if not tests_to_import:
                messagebox.showwarning(
                    self._t('noise.no_tests_in_file', "Nenhum Teste Encontrado"),
                    self._t('noise.no_tests_in_file_msg', "O arquivo selecionado não contém dados de teste no formato esperado."),
                    parent=self
                )
                return

            imported_count = 0
            for test_data in tests_to_import:
                if 'id' in test_data: del test_data['id']
                test_data['show_in_graph'] = True
                if self.database.add_test_to_history(test_data):
                    imported_count += 1
            
            if imported_count > 0:
                self._set_status('noise.status_import_success', "{count} teste(s) importado(s) com sucesso.", count=imported_count)
                self.load_history_to_tree()
                self.update_plot_from_history()
            else:
                self._set_status('noise.status_import_none', "Nenhum teste foi importado.")

        except Exception as e: 
            messagebox.showerror(
                self._t('noise.import_error', "Erro de Importação"),
                self._t('noise.import_error_msg', "Não foi possível ler o arquivo.\nErro: {error}", error=e),
                parent=self
            )

    def generate_pdf_report(self):
        """Gera relatório PDF com diálogo para salvar arquivo - apenas testes selecionados"""
        try:
            # Verifica se há dados para relatório
            if not DATABASE_AVAILABLE or not self.database:
                messagebox.showwarning(
                    self._t('noise.db_unavailable_report', "Aviso"),
                    self._t('noise.db_unavailable_report_msg', "Banco de dados não disponível para gerar relatório."),
                    parent=self
                )
                return
            
            # Obtém apenas os testes selecionados
            selected_ids = self.get_selected_test_ids()
            if not selected_ids:
                messagebox.showwarning(
                    self._t('noise.no_test_selected_report', "Nenhum Teste Selecionado"),
                    self._t('noise.no_test_selected_report_msg', "Por favor, selecione um ou mais testes no histórico para gerar o relatório."),
                    parent=self
                )
                return
            
            # Carrega apenas os testes selecionados
            history_data = []
            for test_id in selected_ids:
                test_data = self.database.get_test_by_id(test_id)
                if test_data:
                    history_data.append(test_data)
            
            if not history_data:
                messagebox.showwarning(
                    self._t('noise.no_valid_test', "Aviso"),
                    self._t('noise.no_valid_test_msg', "Nenhum teste válido encontrado entre os selecionados."),
                    parent=self
                )
                return
            
            # Gera nome do arquivo com data e hora no formato solicitado
            now = datetime.now()
            filename = f"Noise_Check_Report_{now.strftime('%d.%m.%y_%H.%M.%S')}.pdf"
            
            # Diálogo para salvar arquivo
            filepath = filedialog.asksaveasfilename(
                defaultextension=".pdf",
                filetypes=[(self._t('fastthreshold.pdf_files', 'PDF files'), "*.pdf")],
                initialfile=filename,
                title=self._t('noise.report_pdf', "Relatório Selecionados (PDF)"),
                parent=self
            )
            
            if not filepath:
                return  # Usuário cancelou
            
            # Mostra mensagem de progresso
            self._set_status('noise.status_generating_pdf', "Gerando relatório PDF...", force=True)
            self.update()
            
            # Gera o PDF com dados selecionados
            result = self._generate_pdf_with_selected_tests(filepath, history_data)
            
            if result['success']:
                self._set_status('noise.status_pdf_success', "Relatório PDF dos testes selecionados gerado com sucesso!")
                messagebox.showinfo(
                    self._t('noise.success', "Sucesso"),
                    self._t('noise.success_report_generated', "Relatório PDF dos testes selecionados gerado com sucesso!\n\nArquivo: {filepath}\n\nTestes incluídos: {count}", filepath=filepath, count=len(history_data)),
                    parent=self
                )
                
                # Abre o PDF automaticamente (fallbacks se startfile falhar)
                open_path_with_default_app(filepath, log_tag="noise")
            else:
                error_msg = result.get('error', 'Erro desconhecido')
                self._set_status('noise.status_pdf_error', "Erro ao gerar relatório PDF")
                messagebox.showerror(
                    self._t('noise.error_generating_pdf_title', "Erro ao gerar relatório PDF"),
                    self._t('noise.error_generating_pdf', "Erro ao gerar relatório PDF:\n{error}", error=error_msg),
                    parent=self
                )
                
        except Exception as e:
            self._set_status('noise.status_pdf_error', "Erro ao gerar relatório PDF")
            messagebox.showerror(
                self._t('noise.error_generating_pdf_title', "Erro ao gerar relatório PDF"),
                self._t('noise.error_unexpected_pdf', "Erro inesperado ao gerar relatório:\n{error}", error=str(e)),
                parent=self
            )

    def get_selected_test_ids(self):
        """Retorna uma lista com os IDs dos testes selecionados no histórico"""
        if not hasattr(self, 'history_tree'):
            return []
        
        selected_ids = []
        for item in self.history_tree.get_children():
            if self.history_tree.set(item, 'Plot') == "☑":
                test_id = self.history_tree.item(item)['values'][1]
                selected_ids.append(test_id)
        return selected_ids

    def update_report_button_state(self):
        """Atualiza o estado do botão de relatório baseado na seleção de testes"""
        if not hasattr(self, 'btn_generate_report'):
            return
        
        selected_ids = self.get_selected_test_ids()
        has_selection = len(selected_ids) > 0
        
        # O botão só fica ativo se houver testes selecionados
        self.btn_generate_report.config(state='normal' if has_selection else 'disabled')

    def _generate_pdf_with_selected_tests(self, filepath, selected_tests):
        """Gera PDF apenas com os testes selecionados"""
        try:
            return self._generate_pdf_with_reportlab(filepath, selected_tests)
        except ImportError:
            try:
                from fastchecker.pdf_generator import generate_pdf_only
            except ImportError:
                from pdf_generator import generate_pdf_only
            return generate_pdf_only(filepath)
        except Exception as e:
            return {'success': False, 'error': str(e)}

    def _generate_pdf_with_reportlab(self, filepath, selected_tests):
        """Gera PDF usando ReportLab com dados selecionados"""
        try:
            from reportlab.lib.pagesizes import A4
            from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
            from reportlab.lib.units import inch
            from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
            from .pdf_pagination_utils import (
                cond_page_break_before_medium_figure,
                cond_page_break_before_footer_block,
            )
            from reportlab.lib import colors
            from reportlab.lib.enums import TA_CENTER, TA_LEFT
            try:
                from .html_report_generator import _calculate_severity  # type: ignore
            except ImportError:
                from html_report_generator import _calculate_severity  # type: ignore
            try:
                from .image_chart_generator import generate_all_charts  # type: ignore
            except ImportError:
                from image_chart_generator import generate_all_charts  # type: ignore

            translator = self.translator
            if not translator and TRANSLATOR_AVAILABLE and callable(get_translator):
                translator = get_translator()
            current_lang = 'pt'
            if translator and hasattr(translator, 'get_language'):
                try:
                    current_lang = (translator.get_language() or 'pt').lower()
                except Exception:
                    current_lang = 'pt'
            lang_prefix = (current_lang or 'pt')[:2]

            # Função para formatar datas conforme idioma atual
            def _format_datetime(value: str) -> str:
                if not value or not isinstance(value, str):
                    return '-'
                try:
                    from datetime import datetime
                    timestamp_dt = None
                    if 'T' in value:
                        timestamp_dt = datetime.fromisoformat(value.replace('Z', '+00:00'))
                    else:
                        formats_to_try = [
                            '%d/%m/%Y %H:%M:%S',
                            '%d/%m/%Y %H:%M',
                            '%d-%m-%Y %H:%M:%S',
                            '%d-%m-%Y %H:%M',
                            '%Y-%m-%d %H:%M:%S',
                            '%Y-%m-%d %H:%M'
                        ]
                        for fmt in formats_to_try:
                            try:
                                timestamp_dt = datetime.strptime(value, fmt)
                                break
                            except ValueError:
                                continue
                    if timestamp_dt:
                        has_seconds = value.count(':') >= 2
                        if lang_prefix == 'en':
                            return timestamp_dt.strftime('%m/%d/%y %I:%M:%S %p' if has_seconds else '%m/%d/%y %I:%M %p')
                        return timestamp_dt.strftime('%d/%m/%Y %H:%M:%S' if has_seconds else '%d/%m/%Y %H:%M')
                except Exception:
                    pass
                return value

            # Função para obter informações do sistema da licença ativa
            def _get_system_info():
                """Obtém informações do sistema da licença ativa"""
                date_format = '%d/%m/%Y %H:%M:%S' if lang_prefix == 'pt' else '%m/%d/%Y %I:%M:%S %p'
                try:
                    if hasattr(self, 'app_shell') and self.app_shell and hasattr(self.app_shell, 'license_manager'):
                        system_info = self.app_shell.license_manager.get_active_license_system_info(self.com_port)
                        system_info = system_info or {}
                        system_info.setdefault('generated_at', datetime.now().strftime(date_format))
                        return system_info

                    try:
                        from .license_module import LicenseManager
                    except ImportError:
                        from license_module import LicenseManager
                    import os
                    LICENSE_DB_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "licenses.json")
                    license_manager = LicenseManager(LICENSE_DB_FILE)
                    system_info = license_manager.get_active_license_system_info(self.com_port)
                    system_info = system_info or {}
                    system_info.setdefault('generated_at', datetime.now().strftime(date_format))
                    return system_info
                except Exception as e:
                    fc_log("noise", f"⚠️ Erro geral ao obter informações do sistema: {e}")
                    return {
                        'software': '4.0.0',
                        'hardware': 'N/A',
                        'firmware': 'N/A',
                        'serial_number': 'N/A',
                        'license': 'N/A',
                        'generated_at': datetime.now().strftime(date_format)
                    }

            def _display_name(test: dict, fallback_index: int) -> str:
                name = test.get('test_name')
                if name:
                    return name
                fallback_label = self._t('threshold.test', 'Teste')
                return f"{fallback_label} {test.get('id', fallback_index)}"

            import tempfile
            import base64

            doc = SimpleDocTemplate(
                filepath,
                pagesize=A4,
                rightMargin=72,
                leftMargin=72,
                topMargin=72,
                bottomMargin=18
            )

            styles = getSampleStyleSheet()
            title_style = ParagraphStyle(
                'CustomTitle',
                parent=styles['Heading1'],
                fontSize=24,
                spaceAfter=30,
                alignment=TA_CENTER,
                textColor=colors.HexColor('#2c3e50')
            )

            heading_style = ParagraphStyle(
                'CustomHeading',
                parent=styles['Heading2'],
                fontSize=16,
                spaceAfter=12,
                textColor=colors.HexColor('#2c3e50')
            )
            footer_style = ParagraphStyle(
                'FooterStyle',
                parent=styles['Normal'],
                fontSize=10,
                alignment=TA_CENTER,
                textColor=colors.grey
            )

            story = []

            try:
                root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
                logo_path = os.path.join(root, 'assets', 'images', 'fasttag_logo.png')
                if os.path.exists(logo_path):
                    logo_size = 1.5 * inch
                    logo_img = Image(logo_path, width=logo_size, height=logo_size)
                    story.append(logo_img)
                    story.append(Spacer(1, 10))
                    fc_log("noise", "Logo Fasttag adicionado ao PDF (proporção circular)")
                else:
                    fc_log("noise", "Logo Fasttag não encontrado")
            except Exception as e:
                fc_log("noise", f"Erro ao adicionar logo: {e}")

            story.append(Paragraph(self._t('pdf.report_title_noise', 'Relatório de Testes - Noise Check'), title_style))
            story.append(Spacer(1, 20))

            sysinfo = _get_system_info()
            story.append(Paragraph(self._t('pdf.system_info', 'Informações do Sistema'), heading_style))
            info_text = (
                f"<b>{self._t('pdf.software', 'Software')}</b> {sysinfo.get('software', 'N/A')}<br/>"
                f"<b>{self._t('pdf.hardware', 'Hardware')}</b> {sysinfo.get('hardware', 'N/A')}<br/>"
                f"<b>{self._t('pdf.firmware', 'Firmware')}</b> {sysinfo.get('firmware', 'N/A')}<br/>"
                f"<b>{self._t('pdf.serial_number', 'Serial Number')}</b> {sysinfo.get('serial_number', 'N/A')}<br/>"
                f"<b>{self._t('pdf.license', 'Licença')}</b> {sysinfo.get('license', 'N/A')}<br/>"
                f"<b>{self._t('pdf.generated_at', 'Gerado em:')}</b> {sysinfo.get('generated_at', 'N/A')}"
            )
            story.append(Paragraph(info_text, styles['Normal']))
            story.append(Spacer(1, 20))

            story.append(Paragraph(self._t('pdf.test_description_title_noise', 'Descrição do Teste'), heading_style))
            story.append(Paragraph(self._t(
                'pdf.test_description_text_noise',
                'Este relatório apresenta os resultados das medições de ruído ambiente coletadas pelo módulo Noise Check do FastChecker II.'
            ), styles['Normal']))
            story.append(Spacer(1, 20))

            story.append(Paragraph(self._t('pdf.test_summary_noise', 'Resumo dos Testes'), heading_style))

            table_headers = [
                self._t('pdf.test_name_col', 'Nome'),
                self._t('pdf.duration_col', 'Duração (s)'),
                self._t('pdf.average_col', 'Médio (dBm)'),
                self._t('pdf.min_col', 'Mín (dBm)'),
                self._t('pdf.max_col', 'Máx (dBm)'),
                self._t('pdf.date_time_col', 'Data/Hora'),
                self._t('pdf.severity_col', 'Severidade')
            ]
            table_data = [table_headers]
            test_stats = {}

            for index, test in enumerate(selected_tests):
                display_name = _display_name(test, index + 1)
                duration = test.get('duration', 0)
                timestamp_formatted = _format_datetime(test.get('timestamp'))
                noise_data = test.get('noise_data', {})

                values = []
                if isinstance(noise_data, dict) and noise_data:
                    first_val = next(iter(noise_data.values()))
                    if isinstance(first_val, dict) and 'value' in first_val:
                        values = [v['value'] for v in noise_data.values()]
                    else:
                        values = list(noise_data.values())

                if values:
                    avg_val = sum(values) / len(values)
                    min_val = min(values)
                    max_val = max(values)
                    severity = _calculate_severity(max_val, avg_val)
                else:
                    avg_val = min_val = max_val = 0
                    severity = '-'

                table_data.append([
                    display_name,
                    f"{duration}",
                    f"{avg_val:.1f}",
                    f"{min_val:.1f}",
                    f"{max_val:.1f}",
                    timestamp_formatted,
                    severity
                ])

                test_stats[display_name] = {
                    'duration': duration,
                    'avg': avg_val,
                    'min': min_val,
                    'max': max_val,
                    'severity': severity,
                    'timestamp': timestamp_formatted
                }

            test_table = Table(
                table_data,
                colWidths=[1.8 * inch, 0.9 * inch, 1 * inch, 1 * inch, 1 * inch, 1.4 * inch, 1.0 * inch],
                repeatRows=1,
            )
            test_table.setStyle(TableStyle([
                ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#007bff')),
                ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
                ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
                ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
                ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
                ('FONTSIZE', (0, 0), (-1, -1), 8),
                ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
                ('TOPPADDING', (0, 0), (-1, -1), 8),
                ('GRID', (0, 0), (-1, -1), 1, colors.black),
                ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f8f9fa')])
            ]))
            story.append(test_table)
            story.append(Spacer(1, 16))

            charts = generate_all_charts(selected_tests)
            temp_files = []

            if charts:
                for chart_name, image_base64 in charts:
                    story.append(cond_page_break_before_medium_figure(2.5))
                    with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_file:
                        tmp_file.write(base64.b64decode(image_base64))
                        tmp_path = tmp_file.name
                        temp_files.append(tmp_path)

                    img = Image(tmp_path, width=7 * inch, height=2.5 * inch)
                    stats = test_stats.get(chart_name, {})
                    chart_info_text = (
                        f"<b>{self._t('pdf.test_name_col', 'Nome')}:</b> {chart_name}<br/>"
                        f"<b>{self._t('pdf.duration_col', 'Duração (s)')}:</b> {stats.get('duration', '-')}<br/>"
                        f"<b>{self._t('pdf.date_time_col', 'Data/Hora')}:</b> {stats.get('timestamp', '-')}<br/>"
                        f"<b>{self._t('pdf.average_col', 'Médio (dBm)')}:</b> {stats.get('avg', 0):.1f} dBm | "
                        f"<b>{self._t('pdf.min_col', 'Mín (dBm)')}:</b> {stats.get('min', 0):.1f} dBm | "
                        f"<b>{self._t('pdf.max_col', 'Máx (dBm)')}:</b> {stats.get('max', 0):.1f} dBm<br/>"
                        f"<b>{self._t('pdf.severity_col', 'Severidade')}:</b> {stats.get('severity', '-')}"
                    )

                    story.append(Paragraph(f"{self._t('pdf.noise_chart_title', 'Noise Check')} - {chart_name}", heading_style))
                    story.append(Paragraph(chart_info_text, styles['Normal']))
                    story.append(img)
                    story.append(Spacer(1, 16))
            else:
                story.append(Paragraph(self._t('pdf.no_data_available', 'Nenhum dado disponível para este tipo de gráfico'), styles['Normal']))

            story.append(Spacer(1, 12))
            story.append(Paragraph(self._t('pdf.auto_report_footer', 'Relatório automático gerado pelo FastChecker'), footer_style))
            story.append(cond_page_break_before_footer_block())
            story.append(Spacer(1, 12))
            story.append(Paragraph(self._t('pdf.additional_info', 'Informações Adicionais'), heading_style))
            story.append(Paragraph(self._t('pdf.footer_text', 'Este relatório foi gerado automaticamente pelo FastChecker II.'), styles['Normal']))

            timestamp_format = "%d/%m/%Y às %H:%M:%S" if lang_prefix == 'pt' else "%m/%d/%Y at %I:%M:%S %p"
            story.append(Paragraph(
                f"<b>{self._t('pdf.document_generated_at', 'Documento gerado em:')}</b> {datetime.now().strftime(timestamp_format)}",
                styles['Normal']
            ))

            doc.build(story)

            for temp_file in temp_files:
                try:
                    if os.path.exists(temp_file):
                        os.unlink(temp_file)
                except Exception:
                    pass

            return {'success': True, 'filepath': filepath}

        except Exception as e:
            return {'success': False, 'error': str(e)}

    def _generate_pdf_fallback(self, filepath, selected_tests, html_path=None):
        """Mantido apenas por compatibilidade (não é mais usado)"""
        try:
            from fastchecker.pdf_generator import generate_pdf_only
            return generate_pdf_only(filepath)
        except Exception as e:
            return {'success': False, 'error': str(e)}

    def _generate_html_content_for_selected_tests(self, selected_tests):
        """Gera conteúdo HTML apenas com os testes selecionados"""
        try:
            from html_report_generator import _calculate_severity
            from image_chart_generator import generate_all_charts
            
            # Função para obter informações do sistema da licença ativa
            def _get_system_info():
                """Obtém informações do sistema da licença ativa"""
                try:
                    # Tenta obter informações via app_shell (método preferido)
                    if hasattr(self, 'app_shell') and self.app_shell and hasattr(self.app_shell, 'license_manager'):
                        system_info = self.app_shell.license_manager.get_active_license_system_info(self.com_port)
                        # Mantém a versão do software (4.0.0) sem sobrescrever com nome do módulo
                        return system_info
                    
                    # Fallback: cria LicenseManager temporário
                    try:
                        from .license_module import LicenseManager
                        import os
                        LICENSE_DB_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "licenses.json")
                        license_manager = LicenseManager(LICENSE_DB_FILE)
                        system_info = license_manager.get_active_license_system_info(self.com_port)
                        # Mantém a versão do software (4.0.0) sem sobrescrever com nome do módulo
                        return system_info
                    except Exception as fallback_error:
                        fc_log("noise", f"⚠️ Erro no fallback de informações do sistema: {fallback_error}")
                    
                    # Fallback final: informações básicas
                    from datetime import datetime
                    return {
                        'software': '4.0.0',
                        'hardware': 'N/A',
                        'firmware': 'N/A',
                        'serial_number': 'N/A',
                        'license': 'N/A',
                        'generated_at': datetime.now().strftime('%d/%m/%Y %H:%M:%S')
                    }
                    
                except Exception as e:
                    fc_log("noise", f"⚠️ Erro geral ao obter informações do sistema: {e}")
                    from datetime import datetime
                    return {
                        'software': '4.0.0',
                        'hardware': 'N/A',
                        'firmware': 'N/A',
                        'license': 'N/A',
                        'generated_at': datetime.now().strftime('%d/%m/%Y %H:%M:%S')
                    }
            
            sysinfo = _get_system_info()
            
            # Logo path
            root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
            logo_path = os.path.join(root, 'assets', 'images', 'fasttag_logo.png')
            logo_exists = os.path.exists(logo_path)
            
            # Logo base64 se existir
            logo_base64 = ""
            if logo_exists:
                import base64
                with open(logo_path, 'rb') as logo_file:
                    logo_base64 = base64.b64encode(logo_file.read()).decode()
            
            # Gera gráficos apenas com os testes selecionados
            charts = generate_all_charts(selected_tests)
            
            # HTML template
            html_content = f"""
            <!DOCTYPE html>
            <html lang="pt-BR">
            <head>
                <meta charset="UTF-8">
                <title>Relatório de Testes Selecionados - Noise Check</title>
                <style>
                    body {{ font-family: Arial, sans-serif; margin: 20px; }}
                    .header {{ text-align: center; margin-bottom: 30px; }}
                    .logo {{ max-width: 200px; height: auto; }}
                    .info {{ background: #f5f5f5; padding: 15px; border-radius: 5px; margin-bottom: 20px; }}
                    .test-table {{ width: 100%; border-collapse: collapse; margin-bottom: 30px; }}
                    .test-table th, .test-table td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
                    .test-table th {{ background-color: #f2f2f2; }}
                    .chart {{ text-align: center; margin: 20px 0; }}
                    .chart img {{ max-width: 100%; height: auto; }}
                </style>
            </head>
            <body>
                <div class="header">
                    <h1>Relatório de Testes Selecionados - Noise Check</h1>
                    {f'<img src="data:image/png;base64,{logo_base64}" class="logo" alt="Logo">' if logo_base64 else ''}
                </div>
                
                <div class="info">
                    <h3>Informações do Sistema</h3>
                    <p><strong>Software:</strong> {sysinfo['software']}</p>
                    <p><strong>Hardware:</strong> {sysinfo['hardware']}</p>
                    <p><strong>Firmware:</strong> {sysinfo['firmware']}</p>
                    <p><strong>Serial Number:</strong> {sysinfo['serial_number']}</p>
                    <p><strong>Licença:</strong> {sysinfo['license']}</p>
                    <p><strong>Gerado em:</strong> {sysinfo['generated_at']}</p>
                    <p><strong>Testes incluídos:</strong> {len(selected_tests)}</p>
                </div>
                
                <h3>Resumo dos Testes Selecionados</h3>
                <table class="test-table">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>Nome do Teste</th>
                            <th>Duração (s)</th>
                            <th>Ruído Médio (dBm)</th>
                            <th>Ruído Mínimo (dBm)</th>
                            <th>Ruído Máximo (dBm)</th>
                            <th>Hora do Pico</th>
                            <th>Status</th>
                            <th>Data/Hora</th>
                        </tr>
                    </thead>
                    <tbody>
            """
            
            # Adiciona dados dos testes selecionados
            for test in selected_tests:
                noise_data = test.get("noise_data", {})
                if isinstance(noise_data, dict) and noise_data:
                    if isinstance(list(noise_data.values())[0], dict):
                        noise_values = [v["value"] for v in noise_data.values()]
                    else:
                        noise_values = list(noise_data.values())
                    avg_noise = sum(noise_values) / len(noise_values)
                    min_noise = min(noise_values)
                    max_noise = max(noise_values)
                else:
                    avg_noise = min_noise = max_noise = 0.0
                
                max_noise_time = test.get("max_noise_time", "-")
                noise_difference = max_noise - avg_noise
                
                if noise_difference <= 3:
                    status = "Baixa"
                elif 3 < noise_difference <= 6:
                    status = "Média"
                elif 6 < noise_difference <= 9:
                    status = "Alta"
                else:
                    status = "Muito Alta"
                
                html_content += f"""
                        <tr>
                            <td>{test.get('id', 'N/A')}</td>
                            <td>{test.get('test_name', 'Sem nome')}</td>
                            <td>{test.get('duration', 0)}</td>
                            <td>{avg_noise:.1f}</td>
                            <td>{min_noise:.1f}</td>
                            <td>{max_noise:.1f}</td>
                            <td>{max_noise_time}</td>
                            <td>{status}</td>
                            <td>{test.get('timestamp', 'N/A')}</td>
                        </tr>
                """
            
            html_content += """
                    </tbody>
                </table>
            """
            
            # Adiciona gráficos
            for name, image_base64 in charts:
                html_content += f"""
                <div class="chart">
                    <h4>Noise Check - {name}</h4>
                    <img src="data:image/png;base64,{image_base64}" alt="{name}">
                </div>
                """
            
            html_content += """
            </body>
            </html>
            """
            
            return html_content
            
        except Exception as e:
            fc_log("noise", f"❌ Erro ao gerar HTML para testes selecionados: {e}")
            return f"<html><body><h1>Erro ao gerar relatório</h1><p>{str(e)}</p></body></html>"

    def delete_selected_tests(self):
        if not DATABASE_AVAILABLE or not self.database: return

        selected_items = [item for item in self.history_tree.get_children() if self.history_tree.set(item, 'Plot') == "☑"]
        
        if not selected_items:
            messagebox.showwarning(
                self._t('noise.no_test_selected_delete', "Nenhum Teste Selecionado"),
                self._t('noise.no_test_selected_delete_msg', "Por favor, selecione um ou mais testes no histórico para excluir."),
                parent=self
            )
            return



        deleted_count = 0
        for item in selected_items:
            test_id = self.history_tree.item(item)['values'][1]
            if self.database.delete_test_from_history(test_id):
                deleted_count += 1
        
        messagebox.showinfo(
            self._t('noise.tests_deleted', "Operação Concluída"),
            self._t('noise.tests_deleted_msg', "{count} teste(s) foram excluídos com sucesso.", count=deleted_count),
            parent=self
        )
        self.load_history_to_tree()
        self.update_history_stats()
        self.update_plot_from_history()
    
    def save_test_to_history(self):
        if not DATABASE_AVAILABLE or not self.database or not self.live_data: return
        try:
            # NOVO: Calcula a hora do ruído máximo
            max_noise_time = "-"
            if self.live_data:
                # Encontra o valor máximo de ruído
                if isinstance(list(self.live_data.values())[0], dict):
                    max_noise_value = max(v["value"] for v in self.live_data.values())
                    # Encontra o timestamp correspondente ao valor máximo
                    for time_key, data in self.live_data.items():
                        if data["value"] == max_noise_value:
                            max_noise_time = data["absolute_time"]
                            break
                else:
                    # Para dados antigos, calcula baseado no timestamp relativo
                    max_noise_value = max(self.live_data.values())
                    max_time_key = max(self.live_data.keys(), key=lambda k: self.live_data[k])
                    if self.test_start_time:
                        max_absolute_time = self.test_start_time + timedelta(seconds=float(max_time_key))
                        max_noise_time = max_absolute_time.strftime("%H:%M:%S")
            
            # Calcula duração real do teste (em segundos) com base nos dados coletados
            real_duration = self.get_scan_duration()
            try:
                if self.live_data:
                    first_val = next(iter(self.live_data.values()))
                    # As chaves de self.live_data são tempos relativos em segundos
                    durations = [float(t) for t in self.live_data.keys()]
                    if durations:
                        computed_duration = max(durations)
                        if computed_duration > 0:
                            # Armazena com 1 casa decimal para maior precisão
                            real_duration = round(computed_duration, 1)
            except Exception:
                # Em caso de qualquer problema, mantém a duração configurada
                pass

            test_data = {
                "test_name": self.current_test_name, "duration": real_duration,
                "run_mode": self.run_mode_var.get(), "frequency": FIXED_FREQ_MHZ,
                "noise_data": self.live_data.copy(), "timestamp": datetime.now().strftime("%d-%m-%Y %H:%M"),
                "max_noise_time": max_noise_time, "show_in_graph": True
            }
            if self.database.add_test_to_history(test_data):
                fc_log("noise", f"✅ Teste '{self.current_test_name}' salvo no histórico")
                self.test_history = self.database.get_test_history()
                self.update_history_stats()
                self.load_history_to_tree()
                # CORREÇÃO: Limpa dados ao vivo após salvar no histórico para evitar duplicação no gráfico
                self.live_data.clear()
                self.update_plot_from_history()  # Atualiza o gráfico com apenas os dados do histórico
            else:
                fc_log("noise", f"⚠️ Erro ao salvar teste '{self.current_test_name}' no histórico")
        except Exception as e:
            fc_log("noise", f"❌ Erro ao salvar teste no histórico: {e}")
    
    def get_temperature(self):
        ### ALTERADO: usa self.com_port ###
        if not rfid_sdk or rfid_sdk.UHF_RFID_Open(self.com_port, BAUD_RATE) != 0: return None
        try:
            output_buffer, output_len = ctypes.create_string_buffer(64), ctypes.c_uint(0)
            status = rfid_sdk.UHF_RFID_Set(RFID_CMD_GET_TEMPERATURE, None, 0, output_buffer, ctypes.byref(output_len))
            if status == 0 and output_len.value >= 3: return struct.unpack('>h', output_buffer.raw[1:3])[0] / 100.0
        finally: 
            ### ALTERADO: usa self.com_port ###
            rfid_sdk.UHF_RFID_Close(self.com_port)
        return None

    def update_history_stats(self):
        if not hasattr(self, 'history_stats_label'):
            return
        
        if not DATABASE_AVAILABLE or not self.database:
            stats_text = self._t('noise.stats_db_unavailable', 'Estatísticas: Banco de dados não disponível')
        else:
            try:
                stats = self.database.get_statistics()
                count = stats.get('total_tests', 0)
                duration = stats.get('total_duration', 0.0)
                avg_noise = stats.get('avg_noise_level', 0.0)
                key = 'noise.stats_format_single' if count == 1 else 'noise.stats_format'
                default = "Estatísticas: {count} testes | Duração total: {duration:.1f}s | Ruído médio: {avg_noise:.1f} dBm"
                stats_text = self._t(key, default, count=count, duration=duration, avg_noise=avg_noise)
            except Exception as e:
                stats_text = self._t('noise.stats_load_error', f"Estatísticas: Erro ao carregar ({e})", error=e)
        
        self.history_stats_label.config(text=stats_text)

    def show_severity_help(self):
        """Mostra popup com tabela de definição de severidade"""
        help_window = tk.Toplevel(self)
        help_window.title("Definição de Severidade do Ruído")
        help_window.geometry("650x450")
        help_window.resizable(False, False)
        help_window.transient(self)
        help_window.grab_set()
        
        # Centraliza a janela
        help_window.update_idletasks()
        x = (help_window.winfo_screenwidth() // 2) - (650 // 2)
        y = (help_window.winfo_screenheight() // 2) - (450 // 2)
        help_window.geometry(f"650x450+{x}+{y}")
        
        # Frame principal
        main_frame = ttk.Frame(help_window, padding="20")
        main_frame.pack(fill="both", expand=True)
        
        # Título
        title_label = ttk.Label(main_frame, text="Critério de Classificação de Severidade", 
                               font=("Segoe UI", 14))
        title_label.pack(pady=(0, 20))
        
        # Explicação simplificada
        explanation_text = """Severidade = Ruído Máximo - Ruído Médio"""
        
        explanation_label = ttk.Label(main_frame, text=explanation_text, 
                                    font=("Segoe UI", 10), justify="left")
        explanation_label.pack(pady=(0, 20))
        
        # Frame para a tabela com Canvas para dots coloridos
        table_frame = ttk.Frame(main_frame)
        table_frame.pack(fill="both", expand=True)
        
        # Cria um Canvas para desenhar os dots coloridos
        canvas = tk.Canvas(table_frame, height=200, bg="white")
        canvas.pack(fill="both", expand=True)
        
        # Dados de severidade com cores
        severity_data = [
            ("≤ 3 dBm", "Baixa", "Ruído estável, pouca variação - Ambiente ideal", "#00AA00"),
            ("3 < x ≤ 6 dBm", "Média", "Variação moderada de ruído - Aceitável", "#FFD700"),
            ("6 < x ≤ 9 dBm", "Alta", "Variação significativa de ruído - Pode afetar performance", "#FF8C00"),
            ("> 9 dBm", "Muito Alta", "Variação extrema de ruído - Operação comprometida", "#FF0000")
        ]
        
        # Desenha os dots coloridos e texto
        y_start = 20
        for i, (diff, severity, desc, color) in enumerate(severity_data):
            y_pos = y_start + (i * 45)
            
            # Desenha o dot colorido (círculo) - 50% menor (10x10 em vez de 20x20)
            canvas.create_oval(20, y_pos + 5, 30, y_pos + 15, fill=color, outline="black", width=1)
            
            # Adiciona o texto da severidade (sem negrito)
            canvas.create_text(40, y_pos + 10, text=severity, anchor="w", font=("Segoe UI", 10))
            
            # Adiciona o texto da diferença
            canvas.create_text(150, y_pos + 10, text=diff, anchor="w", font=("Segoe UI", 9))
            
            # Adiciona a descrição
            canvas.create_text(300, y_pos + 10, text=desc, anchor="w", font=("Segoe UI", 9))
        
        # Frame para botões
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill="x", pady=(20, 0))
        
        # Botão Fechar
        close_button = ttk.Button(button_frame, text="Fechar", command=help_window.destroy)
        close_button.pack(side="right")
        
        # Informação adicional
        info_text = """• Ruído Médio: Média aritmética de todos os valores coletados
• Ruído Máximo: Maior valor registrado durante o teste
• A classificação é aplicada automaticamente aos testes"""
        
        info_label = ttk.Label(main_frame, text=info_text, 
                              font=("Segoe UI", 9), justify="left", 
                              foreground="gray")
        info_label.pack(pady=(10, 0))
    
    def load_history_to_tree(self, tree=None):
        if tree is None: tree = self.history_tree
        for item in tree.get_children(): tree.delete(item)
        if not DATABASE_AVAILABLE or not self.database: return
        try:
            self.test_history = self.database.get_test_history()
            for test in self.test_history:
                noise_data = test.get("noise_data", {})
                if isinstance(noise_data, dict) and noise_data:
                    # NOVO: Extrai valores da nova estrutura de dados
                    if isinstance(list(noise_data.values())[0], dict):
                        noise_values = [v["value"] for v in noise_data.values()]
                    else:
                        noise_values = list(noise_data.values())
                    avg_noise = sum(noise_values) / len(noise_values)
                    min_noise = min(noise_values); max_noise = max(noise_values)
                else: avg_noise = 0.0; min_noise = 0.0; max_noise = 0.0
                
                # NOVO: Obtém a hora do ruído máximo
                max_noise_time = test.get("max_noise_time", "-")
                
                # NOVO: Calcula a diferença entre ruído máximo e médio para classificação
                noise_difference = max_noise - avg_noise
                
                # NOVO: Critérios baseados na diferença ruído máximo - ruído médio
                if noise_difference <= 3:
                    status = "Baixa"
                elif 3 < noise_difference <= 6:
                    status = "Média"
                elif 6 < noise_difference <= 9:
                    status = "Alta"
                else:  # noise_difference > 9
                    status = "Muito Alta"
                
                plot_state = "☑" if test.get('show_in_graph', False) else "☐"
                
                tree.insert("", "end", values=(
                    plot_state, test.get("id", ""), test.get("test_name", "Sem nome"),
                    test.get("duration", 0), f"{avg_noise:.1f}", f"{min_noise:.1f}", f"{max_noise:.1f}",
                    max_noise_time, test.get("timestamp", ""), status
                ))
            
            # NOVO: Interface light sem cores nas linhas
            # Atualiza o estado do botão de relatório após carregar o histórico
            self.update_report_button_state()
            
            # Scroll automático para o último teste (mais recente)
            if tree.get_children():
                last_item = tree.get_children()[-1]  # Pega o último item
                tree.see(last_item)  # Faz scroll para o último item
                tree.selection_set(last_item)  # Seleciona o último item
        except Exception as e:
            messagebox.showerror(
                self._t('noise.error_loading_history'),
                self._t('noise.error_loading_history_msg', error=str(e)),
                parent=self)

    def on_history_tree_click(self, event):
        """Manipula cliques na árvore de histórico - detecta clique simples e duplo clique"""
        self._click_count += 1
        
        if self._click_count == 1:
            # Primeiro clique - inicia timer
            self._click_timer = self.after(300, self._handle_single_click, event)
        elif self._click_count == 2:
            # Segundo clique - cancela timer e trata como duplo clique
            if self._click_timer:
                self.after_cancel(self._click_timer)
            self._click_timer = None
            self._click_count = 0
            self._handle_double_click(event)
    
    def _handle_single_click(self, event):
        """Trata clique simples - toggle do checkbox Plot"""
        self._click_count = 0
        region = self.history_tree.identify("region", event.x, event.y)
        # Só aceita cliques na coluna do checkbox (coluna 1)
        if region == "cell":
            column_id = self.history_tree.identify_column(event.x)
            # Coluna 1 é o checkbox (Plot)
            if column_id == '#1':
                item = self.history_tree.identify_row(event.y)
                if item:
                    test_id = self.history_tree.item(item)['values'][1]
                    if DATABASE_AVAILABLE and self.database:
                        test_data = self.database.get_test_by_id(test_id)
                        if test_data:
                            current_state = self.history_tree.set(item, 'Plot')
                            new_state = "☑" if current_state == "☐" else "☐"
                            self.history_tree.set(item, 'Plot', new_state)
                        test_data['show_in_graph'] = (new_state == "☑")
                        self.database.update_test_in_history(test_id, test_data)
                        self.update_plot_from_history()
                        # Atualiza o estado do botão de relatório
                        self.update_report_button_state()
    
    def _handle_double_click(self, event):
        """Trata duplo clique - edição do nome do teste"""
        # Destrói qualquer widget de edição que já exista
        if hasattr(self, '_edit_entry') and self._edit_entry.winfo_exists():
            self._edit_entry.destroy()

        region = self.history_tree.identify("region", event.x, event.y)
        if region != "cell":
            return

        column_id = self.history_tree.identify_column(event.x)
        # As colunas de exibição são ("Plot", "Nome", ...), então "Nome" é a coluna #2
        if column_id != '#2':
            return
            
        item_id = self.history_tree.identify_row(event.y)
        if not item_id:
            return

        # Obtém as coordenadas da célula para posicionar o widget de edição
        x, y, width, height = self.history_tree.bbox(item_id, column_id)

        # Cria e posiciona o widget de edição
        current_name = self.history_tree.set(item_id, 'Nome')
        self._edit_var = tk.StringVar(value=current_name)
        # --- LINHA CORRIGIDA ---
        self._edit_entry = ttk.Entry(self.history_tree, textvariable=self._edit_var)
        self._edit_entry.place(x=x, y=y, width=width, height=height)
        self._edit_entry.focus_set()
        self._edit_entry.selection_range(0, 'end')

        # Associa eventos para salvar ou cancelar a edição
        self._edit_entry.bind("<Return>", lambda e: self.save_edited_name(item_id))
        self._edit_entry.bind("<FocusOut>", lambda e: self.save_edited_name(item_id))
        self._edit_entry.bind("<Escape>", lambda e: self.cancel_edit_name())
    
    # --- MÉTODOS PARA EDIÇÃO IN-LINE ---

    def save_edited_name(self, item_id):
        """Salva o nome do teste que foi editado."""
        if not (hasattr(self, '_edit_entry') and self._edit_entry.winfo_exists()):
            return
            
        try:
            new_name = self._edit_var.get().strip()
            self.cancel_edit_name() # Destrói o widget de edição

            if not new_name:
                self._set_status('noise.status_empty_name_error', "Erro: O nome do teste não pode ser vazio.")
                return

            # Obtém o ID do teste a partir dos valores da linha
            test_id = self.history_tree.item(item_id)['values'][1]

            if DATABASE_AVAILABLE and self.database:
                # NOVO: Verifica se o novo nome já existe (excluindo o teste atual)
                if self.check_duplicate_test_name_excluding_current(new_name, test_id):
                    messagebox.showerror(
                        self._t('noise.name_duplicate_inline', "Nome Duplicado"),
                        self._t('noise.name_duplicate_inline_msg', "Já existe um teste com o nome '{name}' no histórico.\n\nPor favor, escolha um nome diferente.", name=new_name),
                        parent=self
                    )
                    return
                
                test_data = self.database.get_test_by_id(test_id)
                if test_data:
                    test_data['test_name'] = new_name
                    if self.database.update_test_in_history(test_id, test_data):
                        self.history_tree.set(item_id, 'Nome', new_name)
                        self._set_status('noise.status_name_updated', "Nome do teste ID {test_id} atualizado.", test_id=test_id)
                        fc_log("noise", f"✅ Nome do teste ID {test_id} atualizado para '{new_name}'")
                        # NOVO: Atualiza o gráfico imediatamente após editar o nome
                        self.update_plot_from_history()
                    else:
                        self._set_status('noise.status_db_update_error', "Erro ao atualizar o teste no banco de dados.")
                else:
                    self._set_status('noise.status_test_not_found', "Erro: Teste com ID {test_id} não encontrado.", test_id=test_id)
        except Exception as e:
            fc_log("noise", f"❌ Erro ao salvar nome editado: {e}")
            self._set_status('noise.status_save_error', "Erro ao salvar: {error}", error=e)

    def cancel_edit_name(self, event=None):
        """Cancela o processo de edição e destrói o widget."""
        if hasattr(self, '_edit_entry') and self._edit_entry.winfo_exists():
            self._edit_entry.destroy()
            delattr(self, '_edit_entry')

    # --- FIM DOS MÉTODOS DE EDIÇÃO ---

    def update_plot_from_history(self):
        if not DATABASE_AVAILABLE or not self.database: return
        try:
            self.multiple_tests.clear()
            selected_tests = []
            for item in self.history_tree.get_children():
                if self.history_tree.set(item, 'Plot') == "☑":
                    test_id = self.history_tree.item(item)['values'][1]
                    test_data = self.database.get_test_by_id(test_id)
                    if test_data: selected_tests.append(test_data)
            
            for test in selected_tests:
                test_name = test.get('test_name', f'Teste_{test.get("id")}')
                test_id = test.get('id')
                noise_data = test.get('noise_data', {})
                if isinstance(noise_data, dict) and noise_data:
                    # NOVO: Converte dados para formato compatível com gráfico e inclui ID
                    if isinstance(list(noise_data.values())[0], dict):
                        converted_data = {float(t): v["value"] for t, v in noise_data.items()}
                    else:
                        converted_data = {float(t): v for t, v in noise_data.items()}
                    
                    # NOVO: Armazena dados com ID para uso nas cores
                    self.multiple_tests[test_name] = {
                        'data': converted_data,
                        'id': test_id
                    }
            
            self.update_plot()
            if selected_tests:
                self._set_status('noise.status_plotting_tests', "Plotando {count} teste(s) do histórico", count=len(selected_tests))
            else:
                self._set_status('noise.status_no_tests_to_plot', "Nenhum teste selecionado para plotagem")
        except Exception as e:
            self._set_status('noise.status_plot_error', "Erro ao atualizar gráfico: {error}", error=e)

    def select_all_tests(self):
        if not DATABASE_AVAILABLE or not self.database: return
        for item in self.history_tree.get_children():
            test_id = self.history_tree.item(item)['values'][1]
            test_data = self.database.get_test_by_id(test_id)
            if test_data and not test_data.get('show_in_graph', False):
                self.history_tree.set(item, 'Plot', '☑')
                test_data['show_in_graph'] = True
                self.database.update_test_in_history(test_id, test_data)
        self.update_plot_from_history()
        # Atualiza o estado do botão de relatório
        self.update_report_button_state()

    def deselect_all_tests(self):
        if not DATABASE_AVAILABLE or not self.database: return
        for item in self.history_tree.get_children():
            test_id = self.history_tree.item(item)['values'][1]
            test_data = self.database.get_test_by_id(test_id)
            if test_data and test_data.get('show_in_graph', False):
                self.history_tree.set(item, 'Plot', '☐')
                test_data['show_in_graph'] = False
                self.database.update_test_in_history(test_id, test_data)
        self.update_plot_from_history()
        # Atualiza o estado do botão de relatório
        self.update_report_button_state()

    def sort_treeview(self, column):
        """
        Ordena a tabela de histórico pela coluna especificada
        """
        try:
            # Obtém todos os itens da tabela
            items = [(self.history_tree.set(item, column), item) for item in self.history_tree.get_children('')]
            
            # Verifica se é a mesma coluna para alternar direção
            if self.current_sort_column == column:
                self.current_sort_reverse = not self.current_sort_reverse
            else:
                self.current_sort_column = column
                self.current_sort_reverse = False
            
            # Determina o tipo de ordenação baseado na coluna
            if column in ["Duração", "ID"]:
                # Ordenação numérica
                items.sort(key=lambda x: int(x[0]) if x[0].isdigit() else 0, reverse=self.current_sort_reverse)
            elif column in ["Ruído Médio", "Ruído Mínimo", "Ruído Máximo"]:
                # Ordenação numérica decimal
                items.sort(key=lambda x: float(x[0].replace(" dBm", "")) if x[0] != "" else 0, reverse=self.current_sort_reverse)
            elif column == "Hora Ruído Máximo":
                # Ordenação de horário
                def time_key(x):
                    if x[0] == "-" or x[0] == "":
                        return "00:00:00"
                    return x[0]
                items.sort(key=time_key, reverse=self.current_sort_reverse)
            elif column == "Data/Hora":
                # Ordenação de data/hora
                items.sort(key=lambda x: x[0], reverse=self.current_sort_reverse)
            elif column == "Plot":
                # Ordenação por estado (☐ primeiro, depois ☑)
                def plot_key(x):
                    return 0 if x[0] == "☐" else 1
                items.sort(key=plot_key, reverse=self.current_sort_reverse)
            elif column == "Severidade":
                # NOVO: Ordenação hierárquica por severidade
                def severity_key(x):
                    severity_order = {"Baixa": 1, "Média": 2, "Alta": 3, "Muito Alta": 4}
                    return severity_order.get(x[0], 0)
                items.sort(key=severity_key, reverse=self.current_sort_reverse)
            else:
                # Ordenação alfabética para outras colunas
                items.sort(key=lambda x: x[0].lower(), reverse=self.current_sort_reverse)
            
            # Reorganiza os itens na tabela
            for index, (val, item) in enumerate(items):
                self.history_tree.move(item, '', index)
            
            # Atualiza o símbolo de ordenação no cabeçalho
            self.update_sort_indicator(column)
            
        except Exception as e:
            fc_log("noise", f"Erro na ordenação: {e}")
    
    def update_sort_indicator(self, column):
        """
        Atualiza os símbolos de ordenação nos cabeçalhos
        """
        try:
            # Remove símbolos de todas as colunas
            for col in ["Plot", "ID", "Nome", "Duração", "Ruído Médio", "Ruído Mínimo", "Ruído Máximo", "Hora Ruído Máximo", "Data/Hora", "Severidade"]:
                current_text = self.history_tree.heading(col)['text']
                # Remove símbolos existentes
                if " ↑" in current_text:
                    current_text = current_text.replace(" ↑", "")
                elif " ↓" in current_text:
                    current_text = current_text.replace(" ↓", "")
                # Adiciona símbolo neutro
                if not current_text.endswith(" ↕"):
                    current_text += " ↕"
                self.history_tree.heading(col, text=current_text)
            
            # Adiciona símbolo de direção na coluna atual
            current_text = self.history_tree.heading(column)['text']
            current_text = current_text.replace(" ↕", "")
            if self.current_sort_reverse:
                current_text += " ↓"
            else:
                current_text += " ↑"
            self.history_tree.heading(column, text=current_text)
            
        except Exception as e:
            fc_log("noise", f"Erro ao atualizar indicador de ordenação: {e}")

    def auto_plot_selected_tests(self):
        self.update_plot_from_history()

    def _setup_zoom_controls(self, parent):
        """Configura os controles de zoom na parte superior esquerda do gráfico"""
        # Frame para os controles de zoom
        zoom_frame = ttk.Frame(parent)
        zoom_frame.grid(row=0, column=0, sticky="nw", padx=5, pady=5)
        
        # Botão Zoom In
        zoom_in_btn = tk.Button(zoom_frame, text="🔍+", width=3, height=1, 
                               command=self._zoom_in, font=("Arial", 10))
        zoom_in_btn.grid(row=0, column=0, padx=2)
        
        # Botão Zoom Out  
        zoom_out_btn = tk.Button(zoom_frame, text="🔍-", width=3, height=1,
                                command=self._zoom_out, font=("Arial", 10))
        zoom_out_btn.grid(row=0, column=1, padx=2)
        
        # Botão Reset Zoom
        reset_btn = tk.Button(zoom_frame, text="↻", width=3, height=1,
                             command=self._reset_zoom, font=("Arial", 10))
        reset_btn.grid(row=0, column=2, padx=2)
        
        # Botão Fit to Data
        fit_btn = tk.Button(zoom_frame, text="📐", width=3, height=1,
                           command=self._fit_to_data, font=("Arial", 10))
        fit_btn.grid(row=0, column=3, padx=2)
        
        # Botão Pan (arrastar)
        self.pan_active = False
        self.pan_btn = tk.Button(zoom_frame, text="✋", width=3, height=1,
                                 command=self._toggle_pan, font=("Arial", 10),
                                 relief=tk.RAISED)
        self.pan_btn.grid(row=0, column=4, padx=2)
        
        # Label do fator de zoom
        self.zoom_label = tk.Label(zoom_frame, text="Zoom: 1.0x", font=("Arial", 9))
        self.zoom_label.grid(row=0, column=5, padx=5)

    def _zoom_in(self):
        """Aplica zoom in no gráfico"""
        try:
            # Obtém limites atuais
            xlim = self.ax.get_xlim()
            ylim = self.ax.get_ylim()
            
            # Calcula centro dos limites atuais
            x_center = (xlim[0] + xlim[1]) / 2
            y_center = (ylim[0] + ylim[1]) / 2
            
            # Calcula nova amplitude (zoom in = amplitude menor)
            x_range = xlim[1] - xlim[0]
            y_range = ylim[1] - ylim[0]
            zoom_factor = 0.8  # Reduz amplitude em 20%
            
            new_x_range = x_range * zoom_factor
            new_y_range = y_range * zoom_factor
            
            # Define novos limites
            new_xlim = (x_center - new_x_range/2, x_center + new_x_range/2)
            new_ylim = (y_center - new_y_range/2, y_center + new_y_range/2)
            
            self.ax.set_xlim(new_xlim)
            self.ax.set_ylim(new_ylim)
            self.canvas.draw()
            
            # Atualiza fator de zoom
            self.zoom_factor *= 1.25
            self.zoom_label.config(text=f"Zoom: {self.zoom_factor:.1f}x")
            
            fc_log("noise", "✅ Zoom in aplicado no Noise Check")
            
        except Exception as e:
            fc_log("noise", f"❌ Erro ao aplicar zoom in: {e}")

    def _zoom_out(self):
        """Aplica zoom out no gráfico"""
        try:
            # Obtém limites atuais
            xlim = self.ax.get_xlim()
            ylim = self.ax.get_ylim()
            
            # Calcula centro dos limites atuais
            x_center = (xlim[0] + xlim[1]) / 2
            y_center = (ylim[0] + ylim[1]) / 2
            
            # Calcula nova amplitude (zoom out = amplitude maior)
            x_range = xlim[1] - xlim[0]
            y_range = ylim[1] - ylim[0]
            zoom_factor = 1.25  # Aumenta amplitude em 25%
            
            new_x_range = x_range * zoom_factor
            new_y_range = y_range * zoom_factor
            
            # Define novos limites
            new_xlim = (x_center - new_x_range/2, x_center + new_x_range/2)
            new_ylim = (y_center - new_y_range/2, y_center + new_y_range/2)
            
            self.ax.set_xlim(new_xlim)
            self.ax.set_ylim(new_ylim)
            self.canvas.draw()
            
            # Atualiza fator de zoom
            self.zoom_factor *= 0.8
            self.zoom_label.config(text=f"Zoom: {self.zoom_factor:.1f}x")
            
            fc_log("noise", "✅ Zoom out aplicado no Noise Check")
            
        except Exception as e:
            fc_log("noise", f"❌ Erro ao aplicar zoom out: {e}")

    def _reset_zoom(self):
        """Reseta o zoom para os limites originais"""
        try:
            if self.original_xlim and self.original_ylim:
                self.ax.set_xlim(self.original_xlim)
                self.ax.set_ylim(self.original_ylim)
            else:
                # Se não há limites originais salvos, usa a escala dinâmica
                x_min, x_max = self.get_dynamic_x_scale()
                self.ax.set_xlim(x_min, x_max)
                self.ax.set_ylim(-80, -40)
            
            self.canvas.draw()
            
            # Reseta fator de zoom
            self.zoom_factor = 1.0
            self.zoom_label.config(text="Zoom: 1.0x")
            
            fc_log("noise", "✅ Zoom resetado no Noise Check")
            
        except Exception as e:
            fc_log("noise", f"❌ Erro ao resetar zoom: {e}")

    def _fit_to_data(self):
        """Ajusta a visualização automaticamente aos dados"""
        try:
            # Obtém todos os dados plotados
            lines = self.ax.get_lines()
            if not lines:
                return
                
            # Encontra limites dos dados (excluindo linhas de limite)
            x_data = []
            y_data = []
            
            for line in lines:
                # Pula linhas de limite (linhas horizontais ou com poucos pontos)
                x_line = line.get_xdata()
                y_line = line.get_ydata()
                
                # Debug: mostra informações de cada linha
                fc_log("noise", f"🔍 Linha encontrada: {len(x_line)} pontos, X: {min(x_line) if len(x_line) > 0 else 'N/A'}-{max(x_line) if len(x_line) > 0 else 'N/A'} s")
                
                # Filtra apenas linhas com dados de ruído (mais de 2 pontos e range X > 1 s)
                if len(x_line) > 2:
                    x_range_line = max(x_line) - min(x_line)
                    if x_range_line > 1:  # Linha de dados de ruído
                        x_data.extend(x_line)
                        y_data.extend(y_line)
                        fc_log("noise", f"✅ Linha de dados incluída: {len(x_line)} pontos, range X: {x_range_line:.1f} s")
            
            if not x_data or not y_data:
                fc_log("noise", "❌ Nenhum dado de ruído encontrado")
                return
                
            # Calcula limites com margem
            x_min, x_max = min(x_data), max(x_data)
            y_min, y_max = min(y_data), max(y_data)
            
            fc_log("noise", f"🔍 Dados filtrados: X({x_min:.1f}-{x_max:.1f} s), Y({y_min:.1f}-{y_max:.1f} dBm)")
            
            # Margem maior para melhor visualização
            x_range = x_max - x_min
            y_range = y_max - y_min
            
            # Se o range for muito pequeno, usa margem absoluta menor
            if x_range < 10:  # Menos de 10 segundos de range
                x_margin = 1  # 1 segundo de margem para ranges pequenos
            else:
                x_margin = x_range * 0.05  # 5% de margem para ranges maiores
                
            if y_range < 5:  # Menos de 5 dBm de range
                y_margin = 1  # 1 dBm de margem
            else:
                y_margin = y_range * 0.05  # 5% de margem
            
            # Define novos limites
            new_x_min = max(0, x_min - x_margin)  # Não vai abaixo de 0
            new_x_max = x_max + x_margin
            new_y_min = y_min - y_margin
            new_y_max = y_max + y_margin
            
            self.ax.set_xlim(new_x_min, new_x_max)
            self.ax.set_ylim(new_y_min, new_y_max)
            self.canvas.draw()
            
            fc_log("noise", f"✅ Visualização ajustada aos dados no Noise Check: X({new_x_min:.1f}-{new_x_max:.1f}s), Y({new_y_min:.1f}-{new_y_max:.1f}dBm)")
            
        except Exception as e:
            fc_log("noise", f"❌ Erro ao ajustar visualização: {e}")

    def _toggle_pan(self):
        """Ativa/desativa o modo pan (arrastar gráfico)"""
        self.pan_active = not self.pan_active
        
        if self.pan_active:
            # Ativa modo pan
            self.pan_btn.config(relief=tk.SUNKEN, bg='#d0d0d0')
            self.canvas.mpl_connect('button_press_event', self._on_pan_press)
            self.canvas.mpl_connect('button_release_event', self._on_pan_release)
            self.canvas.mpl_connect('motion_notify_event', self._on_pan_motion)
            self.pan_start = None
            fc_log("noise", "✅ Modo Pan ativado - clique e arraste para mover o gráfico")
        else:
            # Desativa modo pan
            self.pan_btn.config(relief=tk.RAISED, bg='SystemButtonFace')
            # Desconecta eventos (matplotlib mantém conexões internamente)
            fc_log("noise", "✅ Modo Pan desativado")

    def _on_pan_press(self, event):
        """Callback quando pressiona o mouse no modo pan"""
        if self.pan_active and event.inaxes == self.ax:
            self.pan_start = (event.xdata, event.ydata)
            self.pan_xlim = self.ax.get_xlim()
            self.pan_ylim = self.ax.get_ylim()

    def _on_pan_release(self, event):
        """Callback quando solta o mouse no modo pan"""
        if self.pan_active:
            self.pan_start = None

    def _on_pan_motion(self, event):
        """Callback quando move o mouse no modo pan"""
        if self.pan_active and self.pan_start and event.inaxes == self.ax:
            # Calcula deslocamento
            dx = self.pan_start[0] - event.xdata
            dy = self.pan_start[1] - event.ydata
            
            # Aplica deslocamento aos limites
            new_xlim = (self.pan_xlim[0] + dx, self.pan_xlim[1] + dx)
            new_ylim = (self.pan_ylim[0] + dy, self.pan_ylim[1] + dy)
            
            self.ax.set_xlim(new_xlim)
            self.ax.set_ylim(new_ylim)
            self.canvas.draw_idle()

if __name__ == '__main__':
    root = tk.Tk()
    root.title("Teste Isolado - Módulo de Ruído")
    root.geometry("1100x750")
    
    from port_manager import get_com_port_number
    com_port = get_com_port_number()
    if com_port is None:
        # CORREÇÃO: Não exibe erro de hardware quando sem licença (modo browser)
        fc_log("noise", "ℹ️ Modo browser - hardware não encontrado")
        # Cria uma instância com porta padrão para modo browser
        app = NoiseModule(root, is_licensed=False, com_port=4)  # COM4 como padrão
        app.pack(fill="both", expand=True)
        root.mainloop()
    else:
        app = NoiseModule(root, is_licensed=True, com_port=com_port)
        app.pack(fill="both", expand=True)
        root.mainloop()