# fast_orientation_v10.py (VERSÃO COM RECRIAÇÃO TOTAL DO EIXO POLAR)
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import threading
import time
import serial
from serial.tools import list_ports
import sys
import queue 
import ctypes
import os
import struct
import math
import json
import tempfile
from datetime import datetime
import csv 
import logging
from typing import Dict, Optional, Tuple, Union

# Ajusta sys.path para permitir execução standalone (python fast_orientation.py)
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.abspath(os.path.join(CURRENT_DIR, "..", ".."))
SRC_DIR = os.path.join(PROJECT_ROOT, "src")
CORE_DIR = os.path.join(SRC_DIR, "core")
if PROJECT_ROOT not in sys.path:
    sys.path.insert(0, PROJECT_ROOT)
if SRC_DIR not in sys.path:
    sys.path.insert(0, SRC_DIR)
if CORE_DIR not in sys.path:
    sys.path.insert(0, CORE_DIR)

try:
    from src.core.port_manager import get_port_manager
except ImportError:
    from port_manager import get_port_manager  # type: ignore

try:
    from fastchecker.license_module import HardwareManager, LicenseManager  # type: ignore
except ImportError:
    alt_root = os.path.abspath(os.path.join(CURRENT_DIR, ".."))
    if alt_root not in sys.path:
        sys.path.insert(0, alt_root)
    from fastchecker.license_module import HardwareManager, LicenseManager  # type: ignore

try:
    from fastchecker.orientation_database import OrientationDatabase  # type: ignore
except ImportError:
    from orientation_database import OrientationDatabase  # type: ignore

try:
    from fastchecker.orientation_report_generator import generate_orientation_report  # type: ignore
except ImportError:
    from orientation_report_generator import generate_orientation_report  # type: ignore

try:
    from fastchecker.i18n import get_translator  # type: ignore
except ImportError:
    from i18n import get_translator  # type: ignore

try:
    from fastchecker.port_manager import get_com_port_number as _get_rfid_com_number  # type: ignore
except ImportError:
    try:
        from port_manager import get_com_port_number as _get_rfid_com_number  # type: ignore
    except ImportError:
        _get_rfid_com_number = None

STATE_STORAGE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fast_orientation_state.json")
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fast_orientation_config.json")
LOG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fast_orientation.log")


def _load_orientation_config() -> Dict[str, Union[str, int]]:
    default_config: Dict[str, Union[str, int]] = {
        "arduino_port": "COM5",
        "rfid_com": 4
    }
    try:
        if os.path.exists(CONFIG_FILE):
            with open(CONFIG_FILE, "r", encoding="utf-8") as config_file:
                loaded = json.load(config_file)
                if isinstance(loaded, dict):
                    default_config.update({
                        "arduino_port": str(loaded.get("arduino_port", default_config["arduino_port"])),
                        "rfid_com": int(loaded.get("rfid_com", default_config["rfid_com"]))
                    })
    except Exception as exc:
        print(f"[AVISO] Falha ao carregar configuração de hardware: {exc}")
    return default_config


_ORIENTATION_CONFIG = _load_orientation_config()

# Configuração do logger dedicado do módulo
logger = logging.getLogger("fast_orientation")
if not logger.handlers:
    logger.setLevel(logging.INFO)
    log_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
    log_formatter = logging.Formatter(
        fmt="%(asctime)s [%(levelname)s] %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )
    log_handler.setFormatter(log_formatter)
    logger.addHandler(log_handler)
    logger.propagate = False


FAST_ORIENTATION_TEXTS = {
    "Configuração do Teste": {"en": "Test Configuration"},
    "Nome do Teste:": {"en": "Test Name:"},
    "Frequência (MHz):": {"en": "Frequency (MHz):"},
    "Ângulo Inicial (°):": {"en": "Start Angle (°):"},
    "Ângulo Final (°):": {"en": "End Angle (°):"},
    "Step (°):": {"en": "Step (°):"},
    "Modo de Operação": {"en": "Operation Mode"},
    "Automático": {"en": "Automatic"},
    "Manual": {"en": "Manual"},
    "EPC Alvo": {"en": "Target EPC"},
    "Registrar EPC": {"en": "Register EPC"},
    "Nenhum EPC selecionado": {"en": "No EPC selected"},
    "Controles": {"en": "Controls"},
    "Iniciar Teste": {"en": "Start Test"},
    "Exportar CSV/Excel": {"en": "Export CSV/Excel"},
    "Salvar": {"en": "Save"},
    "Importar": {"en": "Import"},
    "Relatório PDF": {"en": "PDF Report"},
    "Cancelar": {"en": "Cancel"},
    "Confirmar Posição": {"en": "Confirm Position"},
    "Aguardando inicialização...": {"en": "Waiting for initialization..."},
    "Gráfico Polar do Threshold": {"en": "Threshold Polar Chart"},
    "Threshold vs. Ângulo (°)": {"en": "Threshold vs. Angle (°)"},
    "Histórico de Testes": {"en": "Test History"},
    "Nenhum teste salvo.": {"en": "No tests saved."},
    "Plot": {"en": "Plot"},
    "ID": {"en": "ID"},
    "Nome": {"en": "Name"},
    "Nome do Teste": {"en": "Test Name"},
    "EPC da Tag": {"en": "Tag EPC"},
    "Ângulo Inicial (°)": {"en": "Start Angle (°)"},
    "Ângulo Final (°)": {"en": "End Angle (°)"},
    "Step": {"en": "Step"},
    "Freq (MHz)": {"en": "Freq (MHz)"},
    "Ângulo de Abertura (°)": {"en": "Beamwidth (°)"},
    "Direção Lobular (°)": {"en": "Main Lobe Direction (°)"},
    "Data/Hora": {"en": "Date/Time"},
    "Selecionar Todos": {"en": "Select All"},
    "Desmarcar Todos": {"en": "Deselect All"},
    "Mostrar Tabela": {"en": "Show Table"},
    "Destacar +3 dBm": {"en": "Highlight +3 dBm"},
    "Excluir Selecionados": {"en": "Delete Selected"},
    "Erro de Dependência": {"en": "Dependency Error"},
    "A biblioteca 'matplotlib' não foi encontrada.\nInstale com: pip install matplotlib\nErro: {error}": {
        "en": "The 'matplotlib' library was not found.\nInstall with: pip install matplotlib\nError: {error}"
    },
    "Selecione ao menos um teste no histórico para salvar.": {"en": "Select at least one test in the history to save."},
    "Não foi possível localizar os testes selecionados no banco.": {"en": "Could not find the selected tests in the database."},
    "Dados salvos com {count} teste(s).\nArquivo: {filename}": {
        "en": "Saved {count} test(s).\nFile: {filename}"
    },
    "Erro ao salvar: {error}": {"en": "Error while saving: {error}"},
    "Erro ao ler o arquivo selecionado: {error}": {"en": "Error reading the selected file: {error}"},
    "O arquivo não contém testes para importar.": {"en": "The file does not contain tests to import."},
    "Importação concluída: {count} teste(s) adicionados.": {
        "en": "Import completed: {count} test(s) added."
    },
    "Relatório PDF": {"en": "PDF Report"},
    "Não há testes selecionados ou dados recentes para gerar o relatório.": {
        "en": "No selected tests or recent data to generate the report."
    },
    "Relatório gerado com sucesso.\nArquivo: {filename}": {
        "en": "Report generated successfully.\nFile: {filename}"
    },
    "Dependência ausente para gerar PDF: {error}": {
        "en": "Missing dependency to generate PDF: {error}"
    },
    "Erro ao gerar relatório: {error}": {"en": "Error generating report: {error}"},
    "Tabela de Teste": {"en": "Test Table"},
    "Selecione um teste no histórico.": {"en": "Select a test in the history."},
    "Selecione apenas um teste para visualizar a tabela.": {
        "en": "Select only one test to view the table."
    },
    "Não foi possível carregar os dados do teste selecionado.": {
        "en": "Could not load the selected test data."
    },
    "Este teste não possui medições salvas.": {"en": "This test has no saved measurements."},
    "Destaque +3 dBm": {"en": "Highlight +3 dBm"},
    "Selecione um teste no histórico para destacar.": {
        "en": "Select a test in the history to highlight."
    },
    "Escolha apenas um teste para destacar.": {"en": "Select only one test to highlight."},
    "Não foi possível carregar os dados do teste selecionado para destaque.": {
        "en": "Could not load data for the selected test to highlight."
    },
    "O teste selecionado não possui valores numéricos de threshold para calcular o destaque.": {
        "en": "The selected test does not have numeric threshold values to calculate the highlight."
    },
    "Fechar": {"en": "Close"},
    "Histórico": {"en": "History"},
    "Nenhum teste selecionado para excluir.": {"en": "No test selected to delete."},
    "Excluir Testes": {"en": "Delete Tests"},
    "Deseja realmente remover os testes selecionados do histórico?": {
        "en": "Do you really want to remove the selected tests from the history?"
    },
    "Não foi possível salvar o teste no histórico.": {
        "en": "Could not save the test to history."
    },
    "RFID": {"en": "RFID"},
    "DLL RFID não carregada.": {"en": "RFID DLL not loaded."},
    "O registro de EPC já está em andamento. Aguarde.": {
        "en": "EPC registration is already in progress. Please wait."
    },
    "Erro de Entrada": {"en": "Input Error"},
    "Informe o Nome do Teste antes de iniciar.": {"en": "Enter the test name before starting."},
    "O nome de teste '{test_name}' já existe. Escolha outro nome.": {
        "en": "The test name '{test_name}' already exists. Choose another name."
    },
    "Ângulo inicial deve ser menor que o final.": {
        "en": "Start angle must be less than the end angle."
    },
    "O Step deve ser maior que 0.": {"en": "Step must be greater than 0."},
    "Erro de Hardware": {"en": "Hardware Error"},
    "O hardware não está pronto. Verifique as conexões.": {
        "en": "Hardware is not ready. Check the connections."
    },
    "Verifique os valores de entrada. {error}": {"en": "Check the input values. {error}"},
    "Erro": {"en": "Error"},
    "Ocorreu um erro inesperado: {error}": {"en": "An unexpected error occurred: {error}"},
    "Teste em Andamento": {"en": "Test in Progress"},
    "Erro de Execução": {"en": "Execution Error"},
    "Aguarde o teste atual terminar.": {"en": "Wait for the current test to finish."},
    "Teste de Ângulos": {"en": "Angles Test"},
    "Este teste moverá a mesa para os ângulos: 0°, 90°, 180°, 270° e 360°.\n\nVerifique visualmente se os ângulos estão corretos.\n\nDeseja continuar?": {
        "en": "This test will move the table to the angles: 0°, 90°, 180°, 270° and 360°.\n\nVisually verify if the angles are correct.\n\nDo you want to continue?"
    },
    "não identificado": {"en": "not identified"},
    "indefinida": {"en": "undefined"},
    "não calculada": {"en": "not calculated"},
    "Condições mínimas não atendidas.": {"en": "Minimum conditions not met."},
    "Abertura angular: {value}": {"en": "Angular width: {value}"},
    "Direção lobular: {value}": {"en": "Main lobe direction: {value}"},
    "Abertura angular: análise indisponível": {"en": "Angular width: analysis unavailable"},
    "Direção lobular: análise indisponível ({reason})": {
        "en": "Main lobe direction: analysis unavailable ({reason})"
    },
    "Círculo pontilhado adicionado em {radius:.1f} dBm (mínimo registrado: {min_value:.1f} dBm @ {center}).\nInterseções: {intersections}\n{opening_line}\n{direction_line}": {
        "en": "Dashed circle added at {radius:.1f} dBm (minimum recorded: {min_value:.1f} dBm @ {center}).\nIntersections: {intersections}\n{opening_line}\n{direction_line}"
    },
    "Exportar Dados": {"en": "Export Data"},
    "Selecione um teste no histórico para exportar.": {"en": "Select a test in the history to export."},
    "Dados exportados com sucesso para:\n{filename}": {
        "en": "Data exported successfully to:\n{filename}"
    },
    "Erro de Exportação": {"en": "Export Error"},
    "Falha ao salvar o arquivo:\n{error}": {"en": "Failed to save the file:\n{error}"},
    "Teste concluído com Sucesso.": {"en": "Test completed successfully."},
    "Teste interrompido pelo usuário.": {"en": "Test interrupted by the user."},
    "Teste Encerrado devido a um ERRO.": {"en": "Test ended due to an error."},
    "Pronto. Arduino conectado em {port}.": {"en": "Ready. Arduino connected on {port}."},
    "Leitor RFID COM{port} LIVRE": {"en": "RFID reader COM{port} FREE"},
    "Leitor RFID COM{port} OCUPADO": {"en": "RFID reader COM{port} BUSY"},
    "Leitura falhou {count}x na COM{port}. Retentando...": {
        "en": "Read failed {count}x on COM{port}. Retrying..."
    },
    "Resetando posição...": {"en": "Resetting position..."},
    "Movendo para próximo ponto...": {"en": "Moving to next point..."},
    "Movimento concluído.": {"en": "Movement completed."},
    "Retornando à referência...": {"en": "Returning to reference..."},
    "Executando movimento do motor...": {"en": "Executing motor movement..."},
    "Posicionando no início...": {"en": "Positioning at start..."},
    "Iniciando varredura rápida...": {"en": "Starting quick scan..."},
    "Teste concluído. Retornando ao ponto de origem...": {"en": "Test finished. Returning to home..."},
    "Posição final atingida com precisão.": {"en": "Final position reached precisely."},
    "Posição final ajustada com pequeno desvio.": {"en": "Final position adjusted with minor offset."},
    "Teste cancelado. Retornando ao ponto de origem...": {"en": "Test cancelled. Returning to home..."},
    "Teste de ângulos concluído.": {"en": "Angle test finished."},
    "manual.prompt.initial_angle": {
        "en": "Place jig at {angle:.1f}° and confirm."
    },
    "manual.prompt.next_angle": {
        "en": "Move jig to {angle:.1f}° and confirm."
    },
}


def _autodetect_hardware_ports(config: Dict[str, Union[str, int]]) -> Tuple[str, int, Tuple[str, ...]]:
    """Detecta automaticamente as portas do Arduino e do reader RFID."""
    arduino_port = str(config.get("arduino_port", "COM5")).upper()
    rfid_port_number = int(config.get("rfid_com", 4))
    notes = []

    detected_rfid = None
    if _get_rfid_com_number:
        try:
            detected_rfid = _get_rfid_com_number(refresh=True)
        except Exception as exc:
            notes.append(f"Falha ao detectar porta do reader automaticamente: {exc}")
    else:
        notes.append("Detecção do reader via helper indisponível; mantendo configuração salva.")

    if isinstance(detected_rfid, int) and detected_rfid > 0:
        if detected_rfid != rfid_port_number:
            notes.append(f"Porta do reader definida automaticamente para COM{detected_rfid} (antes COM{rfid_port_number}).")
        rfid_port_number = detected_rfid
    else:
        notes.append(f"Porta do reader mantida em COM{rfid_port_number}.")

    detected_arduino = None
    try:
        available_ports = list(list_ports.comports())
    except Exception as exc:
        notes.append(f"Falha ao listar portas seriais: {exc}. Porta da mesa mantida em {arduino_port}.")
        available_ports = []

    if available_ports:
        def _port_matches_keywords(port_obj, keywords) -> bool:
            text = " ".join(
                filter(
                    None,
                    [
                        getattr(port_obj, "description", "") or "",
                        getattr(port_obj, "manufacturer", "") or "",
                        getattr(port_obj, "hwid", "") or ""
                    ]
                )
            ).lower()
            return any(keyword in text for keyword in keywords)

        arduino_keywords = ("arduino", "ch340", "wchusbserial", "wch.cn", "ft232", "ft231", "usb serial device")
        fallback_keywords = ("usb serial", "usb-serial", "usb2.0-serial", "usb to uart")

        for port in available_ports:
            if port.device and port.device.upper() == f"COM{rfid_port_number}":
                continue
            if _port_matches_keywords(port, arduino_keywords):
                detected_arduino = port.device
                break

        if not detected_arduino:
            for port in available_ports:
                if port.device and port.device.upper() == f"COM{rfid_port_number}":
                    continue
                if _port_matches_keywords(port, fallback_keywords):
                    detected_arduino = port.device
                    break

        if not detected_arduino:
            for port in available_ports:
                if port.device and port.device.upper() != f"COM{rfid_port_number}":
                    detected_arduino = port.device
                    break

    if detected_arduino:
        normalized = detected_arduino.upper()
        if normalized != arduino_port:
            notes.append(f"Porta da mesa giratória definida automaticamente para {normalized} (antes {arduino_port}).")
        arduino_port = normalized
    else:
        notes.append(f"Porta da mesa giratória mantida em {arduino_port}.")

    return arduino_port, rfid_port_number, tuple(notes)


ARDUINO_PORT, RFID_READER_COM_NUM, _PORT_AUTODETECT_NOTES = _autodetect_hardware_ports(_ORIENTATION_CONFIG)
for _note in _PORT_AUTODETECT_NOTES:
    logger.info(_note)


# --- Importações Matplotlib ---
try:
    from matplotlib.figure import Figure
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
    import numpy as np
except ImportError as e:
    translator = None
    try:
        translator = get_translator()
    except Exception:
        translator = None
    lang = translator.get_language() if translator else "pt"
    title_template = FAST_ORIENTATION_TEXTS.get("Erro de Dependência", {})
    message_template = FAST_ORIENTATION_TEXTS.get(
        "A biblioteca 'matplotlib' não foi encontrada.\nInstale com: pip install matplotlib\nErro: {error}",
        {},
    )
    title = title_template.get(lang, "Erro de Dependência")
    message = message_template.get(
        lang,
        "A biblioteca 'matplotlib' não foi encontrada.\nInstale com: pip install matplotlib\nErro: {error}",
    ).format(error=e)
    messagebox.showerror(title, message)
    sys.exit()

# --- CONFIGURAÇÃO DA DLL E CONSTANTES (MIGRADO DO FASTSURANCE) ---
dll_name = "UHFRFID.dll"
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)
except OSError as e:
    print(f"ERRO CRÍTICO: Falha ao carregar DLL '{dll_name}'. Razão: {e}")
    rfid_sdk = None
    DLL_LOADED = False
else:
    DLL_LOADED = True
    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

# Comandos da DLL (MIGRADO DO FASTSURANCE)
RFID_CMD_SET_TXPOWER = 0x10
RFID_CMD_SET_FREQ_TABLE = 0x14
RFID_CMD_INV_TAG = 0x80
RFID_CMD_SET_CW_STATUS = 0x24
RFID_CMD_STOP_INVENTORY = 0x8C  # Comando para parar inventário (reset)
RFID_CMD_SET_SOFTRESET = 0x68  # Comando de Soft Reset (gera bip)

# Parâmetros de Limite
DEFAULT_MAX_POWER_DBM = 25.0
DEFAULT_MIN_POWER_DBM = 5.0
HARD_TIMEOUT_S_PER_FREQ = 9.0 # Timeout ajustado para permitir tentativas adicionais

# --- CONFIGURAÇÕES DE HARDWARE ---
ARDUINO_BAUD = 9600
# CORREÇÃO: Valor ajustado para melhor precisão nos ângulos retos
# Se os ângulos retos estiverem visualmente incorretos, este valor pode precisar ser calibrado
# Valor original do Arduino: 2038
# CORREÇÃO: Valor real medido no serial monitor - 2050 passos para uma volta completa
# Testado em mesa_movimento.py e confirmado no serial monitor
PASSOS_PER_REVOLUCAO = 2050  # Valor real medido (2050 passos = 360°) 

# --- CLASSE DE CONTROLE PRINCIPAL ---
class OrientationTester(tk.Frame):
    def __init__(self, master=None, app_shell=None):
        super().__init__(master)
        self.master = master
        self.app_shell = app_shell
        self.translator = get_translator()
        self._translatable_widgets = []
        self._translator_listener_registered = False
        self.port_manager = get_port_manager()
        self._log(f"Iniciando módulo com Arduino em {ARDUINO_PORT} e RFID COM{RFID_READER_COM_NUM}")
        self.mode_var = tk.StringVar(value="auto")
        self.manual_step_event = threading.Event()
        self.manual_waiting_confirm = False
        self.ignore_live_updates = True
        try:
            if self.port_manager:
                self.port_manager.com_port = RFID_READER_COM_NUM
        except Exception:
            pass
        
        # CORREÇÃO: Obtém a janela principal (toplevel)
        self.root_window = self.master.winfo_toplevel()
        
        # CORREÇÃO: Só configura se for a janela principal (não dentro do app_shell)
        if self.root_window == self.master:
            self.master.title(f"Fast Orientation Tester (Arduino {ARDUINO_PORT} / RFID COM{RFID_READER_COM_NUM})")
            # Largura mínima
            self.master.minsize(width=1200, height=700)
            # NÃO define mais resizable(False, True)
        else:
            # Dentro do app_shell: garante que o frame não cause mudanças
            self.root_window.minsize(width=1200, height=700)
        
        # --- NOVA FUNÇÃO PARA CORRIGIR MAXIMIZAÇÃO ---
        self._setup_window_manager()
        # --- FIM DA NOVA FUNÇÃO ---
        
        self.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
        self.master.columnconfigure(0, weight=1)
        self.master.rowconfigure(0, weight=1)

        self.test_running = False
        self.cancel_requested = False
        self._cancelled_run = False
        self.results_queue = queue.Queue()
        self.test_history = []
        self.current_position = 0.0
        self.passos_per_revolucao_calibrado = PASSOS_PER_REVOLUCAO
        self._state_file_path = STATE_STORAGE_FILE
        self._state_save_job = None
        self._state_loading = False
        
        self.contador_passos_absoluto = 0
        self.tabela_passos_por_angulo = self._criar_tabela_passos()

        self.freq_var = tk.DoubleVar(value=915.0)
        self.start_angle_var = tk.DoubleVar(value=0.0)
        self.end_angle_var = tk.DoubleVar(value=360.0)
        self.step_options = (5.0, 10.0, 15.0, 30.0)
        self.step_angle_var = tk.DoubleVar(value=self.step_options[0])
        self.selected_epc_var = tk.StringVar(value="")
        self.test_name_var = tk.StringVar(value="")

        self._tooltip_points = []
        self._polar_motion_cid = None
        self._polar_leave_cid = None
        self.test_name_var = tk.StringVar(value="")

        self.database = OrientationDatabase()
        self.historical_tests = {}
        self.selected_history_ids = set()
        self._history_tree_item_map = {}
        self.test_colors = {}
        self._highlight_overlay = None
        self._color_palette = [
            "#1f77b4", "#d62728", "#2ca02c", "#ff7f0e", "#9467bd",
            "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"
        ]
        self._history_sort_state = {"column": None, "reverse": False}
        self._history_edit_entry = None

        self.current_test_metadata = {}
        self.epc_registering = False
        
        self._execute_factory_reset_if_needed()

        self._create_widgets()
        self._attach_state_watchers()
        self._register_translatable_widgets()
        self._apply_translations()
        self._attach_language_listener()
        self._load_session_state()
        self._check_hardware() 
        self.load_history_to_tree()
        self.update_history_stats()
        self.process_queue() 

    def _log(self, message: str, level: str = "info") -> None:
        log_func = getattr(logger, level.lower(), logger.info)
        log_func(message)
        print(f"[LOG] {message}")

    def _translate_text(self, default_text: str, lang: Optional[str] = None) -> str:
        if not default_text:
            return default_text
        current_lang = lang or (self.translator.get_language() if self.translator else None)
        if not current_lang or current_lang == "pt":
            return default_text
        translations = FAST_ORIENTATION_TEXTS.get(default_text)
        if translations:
            return translations.get(current_lang, translations.get("en", default_text))
        return default_text

    def _trf(self, default_text: str, **kwargs) -> str:
        translated = self._translate_text(default_text)
        if kwargs:
            try:
                return translated.format(**kwargs)
            except Exception:
                return translated
        return translated

    def _register_translatable_widgets(self) -> None:
        self._translatable_widgets = []

        def collect(widget: tk.Widget) -> None:
            try:
                keys = widget.keys()
            except Exception:
                keys = []
            if "text" in keys:
                try:
                    text_value = widget.cget("text")
                except Exception:
                    text_value = None
                if text_value and text_value in FAST_ORIENTATION_TEXTS:
                    if not hasattr(widget, "_fo_original_text"):
                        setattr(widget, "_fo_original_text", text_value)
                    self._translatable_widgets.append(widget)
            for child in widget.winfo_children():
                collect(child)

        collect(self)

    def _apply_translations(self) -> None:
        if not getattr(self, "_translatable_widgets", None):
            return
        current_lang = self.translator.get_language() if self.translator else "pt"
        for widget in self._translatable_widgets:
            original_text = getattr(widget, "_fo_original_text", None)
            if not original_text:
                continue
            translated = self._translate_text(original_text, current_lang)
            try:
                widget.config(text=translated)
            except Exception:
                continue
        self._apply_treeview_translations(current_lang)

    def _apply_treeview_translations(self, lang: Optional[str] = None) -> None:
        if not hasattr(self, "history_tree"):
            return
        defaults = getattr(self, "_history_headings_defaults", None)
        if not defaults:
            return
        current_lang = lang or (self.translator.get_language() if self.translator else "pt")
        for column_id, default_text in defaults.items():
            translated = self._translate_text(default_text, current_lang)
            try:
                self.history_tree.heading(column_id, text=translated)
            except Exception:
                continue

    def _attach_language_listener(self) -> None:
        if self.translator and not self._translator_listener_registered:
            try:
                self.translator.add_language_change_listener(self._on_language_changed)
                self._translator_listener_registered = True
            except Exception as exc:
                self._log(f"Erro ao registrar listener de idioma: {exc}", "warning")

    def _detach_language_listener(self) -> None:
        if self.translator and self._translator_listener_registered:
            try:
                self.translator.remove_language_change_listener(self._on_language_changed)
            except Exception as exc:
                self._log(f"Erro ao remover listener de idioma: {exc}", "warning")
            finally:
                self._translator_listener_registered = False

    def _on_language_changed(self, old_language: str, new_language: str) -> None:
        try:
            self._apply_translations()
        except Exception as exc:
            self._log(f"Falha ao aplicar traduções: {exc}", "warning")

    def _setup_window_manager(self):
        """
        NOVA FUNÇÃO: Corrige o bug de maximização do Tkinter/Windows.
        Intercepta o evento de maximizar e aplica manualmente a "Work Area".
        """
        try:
            # Importa a biblioteca 'ctypes' para falar com o Windows
            from ctypes import windll, wintypes, byref, sizeof, Structure

            # Define a estrutura RECT que o Windows usa para posições
            class RECT(Structure):
                _fields_ = [("left", wintypes.LONG),
                            ("top", wintypes.LONG),
                            ("right", wintypes.LONG),
                            ("bottom", wintypes.LONG)]

            # Constantes do Windows
            SPI_GETWORKAREA = 0x0030
            WM_GETMINMAXINFO = 0x0024

            # Define a estrutura MINMAXINFO
            class MINMAXINFO(Structure):
                _fields_ = [
                    ("ptReserved", wintypes.POINT),
                    ("ptMaxSize", wintypes.POINT),
                    ("ptMaxPosition", wintypes.POINT),
                    ("ptMinTrackSize", wintypes.POINT),
                    ("ptMaxTrackSize", wintypes.POINT),
                ]

            # Pega o 'ponteiro' (HWND) da nossa janela
            hwnd = windll.user32.GetParent(self.root_window.winfo_id())

            def get_work_area(hmonitor):
                """Obtém a área de trabalho do monitor"""
                mi = wintypes.MONITORINFO()
                mi.cbSize = sizeof(wintypes.MONITORINFO)
                if windll.user32.GetMonitorInfoW(hmonitor, byref(mi)):
                    return mi.rcWork  # Retorna o RECT da área de trabalho
                return None

            def on_getminmaxinfo(hwnd, lparam):
                """
                Esta função é chamada pelo Windows SEGUNDOS ANTES de maximizar.
                Nós interceptamos e corrigimos os valores.
                """
                try:
                    # Pega a informação de tamanho do evento
                    mmi = ctypes.cast(lparam, ctypes.POINTER(MINMAXINFO)).contents
                    
                    # Pega o monitor onde a janela está
                    hmonitor = windll.user32.MonitorFromWindow(hwnd, 0x00000002) # MONITOR_DEFAULTTONEAREST
                    
                    if hmonitor:
                        # Pega a área de trabalho (sem a barra de tarefas)
                        work_area = RECT()
                        if windll.user32.SystemParametersInfoW(SPI_GETWORKAREA, 0, byref(work_area), 0):
                            # Informa ao Windows que a posição máxima é o canto da área de trabalho
                            mmi.ptMaxPosition.x = work_area.left
                            mmi.ptMaxPosition.y = work_area.top
                            
                            # Informa ao Windows o tamanho máximo
                            mmi.ptMaxSize.x = work_area.right - work_area.left
                            mmi.ptMaxSize.y = work_area.bottom - work_area.top
                            return 0
                except Exception as e:
                    print(f"Erro ao calcular minmax: {e}")
                
                # Se falhar, deixa o Windows continuar
                return windll.user32.DefWindowProcW(hwnd, WM_GETMINMAXINFO, hwnd, lparam)

            # --- MÁGICA FINAL: Substitui o handler de eventos do Windows ---
            
            # Define o tipo de callback que o Windows espera
            WNDPROC = ctypes.WINFUNCTYPE(wintypes.LPARAM, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM)
            
            # Pega o handler de eventos atual da janela
            self._original_wndproc = windll.user32.GetWindowLongPtrW(hwnd, -4) # GWL_WNDPROC

            def new_wndproc(hwnd_int, msg, wparam, lparam):
                """Nosso novo handler que filtra as mensagens"""
                if msg == WM_GETMINMAXINFO: # Se a mensagem for WM_GETMINMAXINFO
                    # Chama nossa função corrigida
                    return on_getminmaxinfo(hwnd_int, lparam)
                
                # Para todas as outras mensagens, chama a função original
                return windll.user32.CallWindowProcW(self._original_wndproc, hwnd_int, msg, wparam, lparam)

            # Salva nossa função (para o Python não 'esquecê-la')
            self._new_wndproc = WNDPROC(new_wndproc)
            
            # Diz ao Windows para usar nossa função em vez da original
            windll.user32.SetWindowLongPtrW(hwnd, -4, self._new_wndproc)
            print("INFO: Gerenciador de maximização corrigido.")

        except Exception as e:
            print(f"AVISO: Não foi possível aplicar a correção de maximização (pode não estar no Windows): {e}")
            # Se não for Windows ou der erro, permite redimensionar
            self.root_window.resizable(True, True)
            
    def _attach_state_watchers(self):
        """Adiciona observadores nas variáveis de configuração para persistir alterações."""
        self._watched_vars = [
            self.freq_var,
            self.start_angle_var,
            self.end_angle_var,
            self.step_angle_var,
            self.selected_epc_var,
            self.test_name_var,
            self.mode_var,
        ]
        for var in self._watched_vars:
            try:
                var.trace_add("write", self._on_setup_var_changed)
            except Exception:
                pass

    def _on_setup_var_changed(self, *_):
        if self._state_loading:
            return
        self._schedule_state_save()

    def _schedule_state_save(self, delay_ms: int = 400):
        if self._state_loading:
            return
        try:
            if self._state_save_job is not None:
                self.after_cancel(self._state_save_job)
        except Exception:
            pass
        self._state_save_job = self.after(delay_ms, self._save_session_state)

    def _load_session_state(self):
        if not self._state_file_path or not os.path.exists(self._state_file_path):
            return
        try:
            with open(self._state_file_path, "r", encoding="utf-8") as fp:
                state_data = json.load(fp)
        except Exception as e:
            print(f"[AVISO] Não foi possível carregar estado anterior do Fast Orientation: {e}")
            return

        self._state_loading = True
        try:
            setup = state_data.get("setup", {})
            if isinstance(setup, dict):
                if "frequency_mhz" in setup:
                    try:
                        self.freq_var.set(float(setup["frequency_mhz"]))
                    except (TypeError, ValueError):
                        pass
                if "start_angle" in setup:
                    try:
                        self.start_angle_var.set(float(setup["start_angle"]))
                    except (TypeError, ValueError):
                        pass
                if "end_angle" in setup:
                    try:
                        self.end_angle_var.set(float(setup["end_angle"]))
                    except (TypeError, ValueError):
                        pass
                if "step_angle" in setup:
                    try:
                        self.step_angle_var.set(float(setup["step_angle"]))
                    except (TypeError, ValueError):
                        pass
                if self.step_angle_var.get() not in self.step_options:
                    self.step_angle_var.set(self.step_options[0])
                if "selected_epc" in setup:
                    self.selected_epc_var.set(setup.get("selected_epc") or "")
                if "test_name" in setup:
                    self.test_name_var.set(setup.get("test_name") or "")
                if "mode" in setup:
                    mode_value = setup.get("mode")
                    if mode_value in ("auto", "manual"):
                        self.mode_var.set(mode_value)

            self.current_test_metadata = state_data.get("current_test_metadata") or {}
            self.test_history = state_data.get("test_history") or []

            selected_ids = state_data.get("selected_history_ids", [])
            if isinstance(selected_ids, list):
                try:
                    self.selected_history_ids = {int(test_id) for test_id in selected_ids}
                except ValueError:
                    self.selected_history_ids = set()

            colors_map = state_data.get("test_colors")
            if isinstance(colors_map, dict):
                converted = {}
                for key, value in colors_map.items():
                    try:
                        converted[int(key)] = value
                    except (ValueError, TypeError):
                        continue
                if converted:
                    self.test_colors.update(converted)

            sort_state = state_data.get("history_sort_state")
            if isinstance(sort_state, dict):
                column = sort_state.get("column", self._history_sort_state.get("column"))
                reverse = sort_state.get("reverse", self._history_sort_state.get("reverse", False))
                self._history_sort_state = {"column": column, "reverse": reverse}
        finally:
            self._state_loading = False

    def _collect_setup_snapshot(self) -> Dict:
        return {
            "frequency_mhz": self.freq_var.get(),
            "start_angle": self.start_angle_var.get(),
            "end_angle": self.end_angle_var.get(),
            "step_angle": self.step_angle_var.get(),
            "selected_epc": self.selected_epc_var.get(),
            "test_name": self.test_name_var.get(),
            "mode": self.mode_var.get(),
        }

    def _collect_system_info_for_report(self, language: Optional[str] = None) -> Dict[str, str]:
        translator = get_translator()
        lang_code = language or (self.translator.get_language() if hasattr(self, "translator") and self.translator else translator.get_language())
        date_format = '%d/%m/%Y %H:%M:%S' if lang_code == 'pt' else '%m/%d/%y %I:%M:%S %p'
        info: Dict[str, str] = {
            "software": "FastChecker II",
            "hardware": "N/A",
            "firmware": "N/A",
            "serial_number": "N/A",
            "license": "N/A",
            "generated_at": datetime.now().strftime(date_format),
        }

        com_port = getattr(self.port_manager, "com_port", None) or RFID_READER_COM_NUM

        def _merge_info(source: Optional[Dict[str, str]]) -> None:
            if not source:
                return
            for key, value in source.items():
                if value not in (None, "", "N/A", "-"):
                    info[key] = value

        try:
            if self.app_shell and hasattr(self.app_shell, "license_manager") and self.app_shell.license_manager:
                fetched = self.app_shell.license_manager.get_active_license_system_info(com_port)
                _merge_info(fetched)
        except Exception as exc:
            self._log(f"Falha ao obter informações de licença via app_shell: {exc}", "warning")

        try:
            license_db_path = os.path.join(PROJECT_ROOT, "licenses.json")
            if os.path.exists(license_db_path):
                license_manager = LicenseManager(license_db_path)
                fetched = license_manager.get_active_license_system_info(com_port)
                _merge_info(fetched)
        except Exception as exc:
            self._log(f"Falha ao obter informações de licença via arquivo local: {exc}", "warning")

        try:
            from .version_config import get_software_info  # type: ignore
            sw_info = get_software_info()
            if isinstance(sw_info, dict):
                name = sw_info.get("name", "FastChecker II")
                version = sw_info.get("version", "")
                info["software"] = f"{name} v{version}".strip()
        except Exception:
            pass

        info["generated_at"] = datetime.now().strftime(date_format)

        return info

    def _save_session_state(self):
        if self._state_loading:
            return
        payload = {
            "setup": self._collect_setup_snapshot(),
            "current_test_metadata": self.current_test_metadata,
            "test_history": self.test_history,
            "selected_history_ids": sorted(self.selected_history_ids),
            "test_colors": {str(k): v for k, v in self.test_colors.items()},
            "history_sort_state": self._history_sort_state,
            "saved_at": datetime.now().isoformat(timespec="seconds"),
        }
        try:
            with open(self._state_file_path, "w", encoding="utf-8") as fp:
                json.dump(payload, fp, indent=2, ensure_ascii=False)
        except Exception as e:
            print(f"[AVISO] Não foi possível salvar estado do Fast Orientation: {e}")
        finally:
            self._state_save_job = None

    def _default_bundle_filename(self) -> str:
        timestamp = datetime.now().strftime('%d.%m.%y_%H.%M.%S')
        return f"Fast Orientation {timestamp}.json"

    def _default_pdf_filename(self) -> str:
        timestamp = datetime.now().strftime('%d.%m.%y_%H.%M.%S')
        return f"Relatório_Fast Orientation {timestamp}.pdf"

    def _get_selected_history_ids(self):
        return sorted(self.selected_history_ids)

    def _save_bundle(self):
        selected_ids = self._get_selected_history_ids()
        if not selected_ids:
            messagebox.showwarning(
                self._trf("Salvar"),
                self._trf("Selecione ao menos um teste no histórico para salvar."),
                parent=self.master
            )
            return

        tests_to_export = []
        missing_ids = []
        for test_id in selected_ids:
            test_payload = self.database.get_test_by_id(test_id) if self.database else None
            if test_payload:
                tests_to_export.append(test_payload)
            else:
                missing_ids.append(test_id)

        if not tests_to_export:
            messagebox.showwarning(
                self._trf("Salvar"),
                self._trf("Não foi possível localizar os testes selecionados no banco."),
                parent=self.master
            )
            return

        if missing_ids:
            print(f"[AVISO] Alguns IDs não foram encontrados para exportação: {missing_ids}")

        default_filename = self._default_bundle_filename()
        filepath = filedialog.asksaveasfilename(
            defaultextension=".json",
            filetypes=[("JSON Files", "*.json")],
            title="Salvar dados do Fast Orientation",
            initialfile=default_filename,
            parent=self.master
        )
        if not filepath:
            return

        bundle_payload = {
            "metadata": {
                "exported_at": datetime.now().strftime('%d-%m-%Y %H:%M:%S'),
                "source": "Fast Orientation - FastChecker II",
                "format_version": "1.0"
            },
            "setup": self._collect_setup_snapshot(),
            "current_session": {
                "current_test_metadata": self.current_test_metadata,
                "test_history": self.test_history,
            },
            "selected_history_ids": selected_ids,
            "tests": tests_to_export,
        }

        try:
            with open(filepath, "w", encoding="utf-8") as fp:
                json.dump(bundle_payload, fp, indent=2, ensure_ascii=False)
            messagebox.showinfo(
                self._trf("Salvar"),
                self._trf(
                    "Dados salvos com {count} teste(s).\nArquivo: {filename}",
                    count=len(tests_to_export),
                    filename=os.path.basename(filepath),
                ),
                parent=self.master
            )
        except Exception as e:
            messagebox.showerror(
                self._trf("Salvar"),
                self._trf("Erro ao salvar: {error}", error=e),
                parent=self.master
            )

    def _import_bundle(self):
        filepath = filedialog.askopenfilename(
            defaultextension=".json",
            filetypes=[("JSON Files", "*.json")],
            title="Importar dados do Fast Orientation",
            parent=self.master
        )
        if not filepath:
            return

        try:
            with open(filepath, "r", encoding="utf-8") as fp:
                bundle_data = json.load(fp)
        except Exception as e:
            messagebox.showerror(
                self._trf("Importar"),
                self._trf("Erro ao ler o arquivo selecionado: {error}", error=e),
                parent=self.master
            )
            return

        tests_data = bundle_data.get("tests") or []
        if not isinstance(tests_data, list) or not tests_data:
            messagebox.showwarning(
                self._trf("Importar"),
                self._trf("O arquivo não contém testes para importar."),
                parent=self.master
            )
            return

        imported_count = 0
        imported_ids = []
        existing_names = {
            (test.get('test_name') or '').strip().lower()
            for test in (self.database.get_test_history() if self.database else [])
        }

        for test_payload in tests_data:
            if not isinstance(test_payload, dict):
                continue
            payload = dict(test_payload)
            payload.pop("id", None)
            test_name = (payload.get("test_name") or "Teste Importado").strip()
            base_name = test_name or "Teste Importado"
            duplicate_index = 1
            while test_name.lower() in existing_names:
                test_name = f"{base_name} ({duplicate_index})"
                duplicate_index += 1
            payload["test_name"] = test_name
            existing_names.add(test_name.lower())

            if self.database:
                new_id = self.database.add_test(payload)
                if new_id is not None:
                    imported_count += 1
                    imported_ids.append(new_id)
                else:
                    print(f"[AVISO] Falha ao adicionar teste importado: {test_name}")

        self._state_loading = True
        try:
            setup_data = bundle_data.get("setup")
            if isinstance(setup_data, dict):
                if "frequency_mhz" in setup_data:
                    try:
                        self.freq_var.set(float(setup_data["frequency_mhz"]))
                    except (TypeError, ValueError):
                        pass
                if "start_angle" in setup_data:
                    try:
                        self.start_angle_var.set(float(setup_data["start_angle"]))
                    except (TypeError, ValueError):
                        pass
                if "end_angle" in setup_data:
                    try:
                        self.end_angle_var.set(float(setup_data["end_angle"]))
                    except (TypeError, ValueError):
                        pass
                if "step_angle" in setup_data:
                    try:
                        self.step_angle_var.set(float(setup_data["step_angle"]))
                    except (TypeError, ValueError):
                        pass
                if "selected_epc" in setup_data:
                    self.selected_epc_var.set(setup_data.get("selected_epc") or "")
                if "test_name" in setup_data:
                    self.test_name_var.set(setup_data.get("test_name") or "")
                if "mode" in setup_data and setup_data.get("mode") in ("auto", "manual"):
                    self.mode_var.set(setup_data.get("mode"))

            session_data = bundle_data.get("current_session") or {}
            if isinstance(session_data, dict):
                self.current_test_metadata = session_data.get("current_test_metadata") or {}
                self.test_history = session_data.get("test_history") or []
            else:
                self.current_test_metadata = {}
                self.test_history = []
        finally:
            self._state_loading = False

        if imported_ids:
            self.selected_history_ids.update(imported_ids)

        self.load_history_to_tree()
        self.update_history_stats()
        self.update_polar_plot()
        self._schedule_state_save()

        messagebox.showinfo(
            self._trf("Importar"),
            self._trf("Importação concluída: {count} teste(s) adicionados.", count=imported_count),
            parent=self.master
        )

    def _generate_pdf_report(self):
        selected_ids = self._get_selected_history_ids()
        if not selected_ids and self.database:
            selected_ids = [
                test.get("id")
                for test in self.database.get_test_history()
                if test.get("id") is not None
            ]

        tests_to_include = []
        if self.database:
            for test_id in selected_ids:
                test_payload = self.database.get_test_by_id(test_id)
                if test_payload:
                    tests_to_include.append(test_payload)

        if not tests_to_include and not self.test_history:
            messagebox.showwarning(
                self._trf("Relatório PDF"),
                self._trf("Não há testes selecionados ou dados recentes para gerar o relatório."),
                parent=self.master
            )
            return

        default_filename = self._default_pdf_filename()
        filepath = filedialog.asksaveasfilename(
            defaultextension=".pdf",
            filetypes=[("PDF Files", "*.pdf")],
            title="Salvar Relatório PDF do Fast Orientation",
            initialfile=default_filename,
            parent=self.master
        )
        if not filepath:
            return

        polar_image_path = None
        try:
            if hasattr(self, "polar_fig") and self.polar_fig is not None:
                with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
                    self.polar_fig.savefig(tmp.name, dpi=220)
                    polar_image_path = tmp.name
        except Exception as e:
            print(f"[AVISO] Não foi possível gerar imagem do gráfico polar: {e}")
            polar_image_path = None

        session_snapshot = {
            "current_test_metadata": self.current_test_metadata,
            "test_history": self.test_history,
        }

        current_language = self.translator.get_language() if hasattr(self, "translator") and self.translator else get_translator().get_language()
        system_info = self._collect_system_info_for_report(current_language)

        try:
            generate_orientation_report(
                output_pdf_path=filepath,
                setup=self._collect_setup_snapshot(),
                tests=tests_to_include,
                session_data=session_snapshot,
                polar_chart_path=polar_image_path,
                system_info=system_info,
                language=current_language
            )
            messagebox.showinfo(
                self._trf("Relatório PDF"),
                self._trf(
                    "Relatório gerado com sucesso.\nArquivo: {filename}",
                    filename=os.path.basename(filepath),
                ),
                parent=self.master
            )
            try:
                os.startfile(filepath)  # type: ignore[attr-defined]
            except Exception:
                pass
        except ImportError as e:
            messagebox.showerror(
                self._trf("Relatório PDF"),
                self._trf("Dependência ausente para gerar PDF: {error}", error=e),
                parent=self.master
            )
        except Exception as e:
            messagebox.showerror(
                self._trf("Relatório PDF"),
                self._trf("Erro ao gerar relatório: {error}", error=e),
                parent=self.master
            )
        finally:
            if polar_image_path and os.path.exists(polar_image_path):
                try:
                    os.unlink(polar_image_path)
                except Exception:
                    pass

    def _create_widgets(self):
        # Estrutura principal seguindo padrão RSSI x Power
        main_frame = tk.Frame(self)
        main_frame.pack(fill='both', expand=True, padx=10, pady=10)
        main_frame.grid_columnconfigure(1, weight=1)
        main_frame.grid_rowconfigure(0, weight=1)
        main_frame.grid_rowconfigure(1, weight=0)
        
        # Painel superior com left e right panels
        top_panels_frame = tk.Frame(main_frame)
        top_panels_frame.grid(row=0, column=0, columnspan=2, sticky='nsew')
        top_panels_frame.grid_columnconfigure(1, weight=1)
        top_panels_frame.grid_rowconfigure(0, weight=1)
        
        # Painel esquerdo (configurações e controles)
        # CORREÇÃO: Define largura fixa para evitar mudanças de layout
        left_panel = tk.Frame(top_panels_frame)
        left_panel.config(width=260, height=540)  # Largura fixa e altura ampliadas
        left_panel.grid_propagate(False)  # Previne que filhos redimensionem o frame
        left_panel.grid(row=0, column=0, sticky='ns', padx=(0, 10))
        
        # Painel direito (gráfico)
        right_panel = tk.Frame(top_panels_frame)
        right_panel.grid(row=0, column=1, sticky='nsew')
        
        # Constrói os painéis
        config_frame = self._build_config_panel(left_panel)
        mode_frame = self._build_mode_panel(left_panel)
        epc_frame = self._build_epc_panel(left_panel)
        actions_frame = self._build_actions_panel(left_panel)
        self._apply_fixed_width([config_frame, mode_frame, epc_frame, actions_frame])
        self._build_graph_panel(right_panel)
        self._build_history_panel(main_frame)
        
        # CORREÇÃO: Força tamanho fixo após criar todos os widgets
        self.master.update_idletasks()
        if self.root_window == self.master:
            # Janela independente: define geometry fixo
            self.master.geometry("1200x700")
            # self.master.resizable(False, True) # Removido pela nova função de maximização
        else:
            # Dentro do app_shell: garante largura mínima
            try:
                current_width = self.root_window.winfo_width()
                if current_width < 1200:
                    self.root_window.geometry(f"1200x{self.root_window.winfo_height()}")
                self.root_window.minsize(width=1200, height=600)
                # self.root_window.resizable(False, True) # Removido
            except:
                pass
    
    def _build_config_panel(self, parent):
        """Painel de configuração do teste - seguindo padrão RSSI x Power"""
        config_frame = tk.LabelFrame(parent, text="Configuração do Teste", padx=10, pady=5)
        config_frame.pack(fill='x', pady=(0, 10))
        
        ttk.Label(config_frame, text="Nome do Teste:").pack(anchor='w')
        name_entry = ttk.Entry(config_frame, textvariable=self.test_name_var)
        name_entry.pack(fill='x', pady=(0, 5))
        
        ttk.Label(config_frame, text="Frequência (MHz):").pack(anchor='w')
        freq_entry = ttk.Entry(config_frame, textvariable=self.freq_var)
        freq_entry.pack(fill='x', pady=(0, 5))
        
        ttk.Label(config_frame, text="Ângulo Inicial (°):").pack(anchor='w')
        start_entry = ttk.Entry(config_frame, textvariable=self.start_angle_var)
        start_entry.pack(fill='x', pady=(0, 5))
        
        ttk.Label(config_frame, text="Ângulo Final (°):").pack(anchor='w')
        end_entry = ttk.Entry(config_frame, textvariable=self.end_angle_var)
        end_entry.pack(fill='x', pady=(0, 5))
        
        ttk.Label(config_frame, text="Step (°):").pack(anchor='w')
        self.step_combobox = ttk.Combobox(
            config_frame,
            textvariable=self.step_angle_var,
            values=self.step_options,
            state="readonly"
        )
        self.step_combobox.pack(fill='x')
        return config_frame

    def _build_mode_panel(self, parent):
        mode_frame = tk.LabelFrame(parent, text="Modo de Operação", padx=10, pady=5)
        mode_frame.pack(fill='x', pady=(0, 10))
        self.automatic_radio = ttk.Radiobutton(
            mode_frame, text="Automático", variable=self.mode_var, value="auto",
            command=self._on_mode_changed,
            state="disabled"
        )
        self.automatic_radio.pack(anchor='w')
        ttk.Radiobutton(
            mode_frame, text="Manual", variable=self.mode_var, value="manual",
            command=self._on_mode_changed
        ).pack(anchor='w')
        return mode_frame
    
    def _build_epc_panel(self, parent):
        """Painel de seleção de EPC - seguindo padrão RSSI x Power"""
        epc_frame = tk.LabelFrame(parent, text="EPC Alvo", padx=10, pady=5)
        epc_frame.pack(fill='x', pady=(0, 10))
        
        self.capture_epc_button = ttk.Button(epc_frame, text="Registrar EPC", command=self._start_register_epc)
        self.capture_epc_button.pack(fill='x', pady=(0, 5))
        
        # CORREÇÃO: Usa Label com wraplength para mostrar EPC completo sem quebrar layout
        # EPC completo tem ~24 caracteres hexadecimais, que cabem confortavelmente no painel
        # Largura do painel esquerdo é 250px, então wraplength de 220px é seguro
        self.epc_label = tk.Label(epc_frame, text="Nenhum EPC selecionado", fg="gray", wraplength=220, anchor='w', justify='left')
        self.epc_label.pack(fill='x', anchor='w')
        return epc_frame
    
    def _build_actions_panel(self, parent):
        """Painel de ações/controles - seguindo padrão RSSI x Power"""
        actions_frame = tk.LabelFrame(parent, text="Controles", padx=10, pady=5)
        actions_frame.pack(fill='both', expand=True, pady=(0, 10))
        actions_frame.pack_propagate(False)
        actions_frame.config(height=360)
        
        self.start_button = ttk.Button(actions_frame, text="Iniciar Teste", command=self.start_test)
        self.start_button.pack(fill='x', pady=2)
        
        self.save_bundle_button = ttk.Button(actions_frame, text="Salvar", command=self._save_bundle)
        self.save_bundle_button.pack(fill='x', pady=2)

        self.import_bundle_button = ttk.Button(actions_frame, text="Importar", command=self._import_bundle)
        self.import_bundle_button.pack(fill='x', pady=2)

        self.pdf_button = ttk.Button(actions_frame, text="Relatório PDF", command=self._generate_pdf_report)
        self.pdf_button.pack(fill='x', pady=2)

        self.cancel_button = ttk.Button(actions_frame, text="Cancelar", command=self._request_cancel, state="disabled")
        self.cancel_button.pack(fill='x', pady=2)

        self.manual_confirm_button = ttk.Button(
            actions_frame,
            text="Confirmar Posição",
            command=self._manual_confirm_position,
            state="disabled"
        )
        self.manual_confirm_button.pack(fill='x', pady=2)

        self.status_label = ttk.Label(
            actions_frame,
            text="Aguardando inicialização...",
            foreground="blue",
            wraplength=230,
            anchor='w',
            justify='left'
        )
        self.status_label.pack(fill='x', pady=(8, 0))
        return actions_frame

    def _apply_fixed_width(self, frames, width=230):
        for frame in frames:
            if not frame:
                continue
            frame.update_idletasks()
            desired_height = frame.winfo_reqheight()
            frame.config(width=width, height=desired_height)
            frame.pack_propagate(False)

    def _on_mode_changed(self):
        mode = self.mode_var.get()
        if mode == "manual":
            self.manual_confirm_button.config(state="disabled")
        else:
            self.manual_confirm_button.config(state="disabled")
            self.manual_step_event.set()
            self.manual_waiting_confirm = False

    def _manual_confirm_position(self):
        if not self.manual_waiting_confirm:
            return
        self.manual_waiting_confirm = False
        self.manual_confirm_button.config(state="disabled")
        self.status_label.config(text="Posição confirmada. Medindo...", foreground="blue")
        self.manual_step_event.set()

    def _build_graph_panel(self, parent):
        """Painel do gráfico polar"""
        self.graph_frame = tk.LabelFrame(parent, text="Gráfico Polar do Threshold", padx=10, pady=5)
        self.graph_frame.pack(fill='both', expand=True)
        
        try:
            self.polar_fig = Figure(figsize=(5, 5), dpi=100)
            
            # Inicializa o eixo (será recriado em update_polar_plot)
            self.polar_ax = self.polar_fig.add_subplot(111, projection='polar')
            self.polar_ax.set_theta_direction(-1) 
            self.polar_ax.set_theta_zero_location("N") 
            self.polar_ax.set_rlim(DEFAULT_MAX_POWER_DBM, DEFAULT_MIN_POWER_DBM)
            r_ticks = np.arange(DEFAULT_MIN_POWER_DBM, DEFAULT_MAX_POWER_DBM + 1, 5)
            self.polar_ax.set_rticks(r_ticks)
            self.polar_ax.set_yticklabels([str(int(t)) for t in r_ticks])
            self.polar_ax.set_rlabel_position(22.5)
            self.polar_ax.set_thetagrids(np.arange(0, 360, 15))
            self.polar_ax.set_title("Threshold vs. Ângulo (°)", va='bottom')
            
            # Canvas embutido no painel
            self.polar_canvas = FigureCanvasTkAgg(self.polar_fig, self.graph_frame)
            self.polar_canvas.draw()
            self.polar_canvas.get_tk_widget().pack(fill='both', expand=True)
            
            # Chama a atualização uma vez para definir o estado inicial (vazio)
            self.update_polar_plot()
            
        except Exception as e:
            print(f"ERRO DE PLOTAGEM CRÍTICO: {e}")
            tk.Label(self.graph_frame, text=f"ERRO ao carregar o gráfico:\n{e}", fg="red").pack(pady=10)
            self.polar_fig = None
    
    def _build_history_panel(self, parent):
        """Painel de histórico de testes (formato alinhado ao módulo RSSI x Power)."""
        history_frame = ttk.LabelFrame(parent, text="Histórico de Testes")
        history_frame.grid(row=1, column=0, columnspan=2, sticky='ew', padx=0, pady=(10, 0))

        stats_frame = ttk.Frame(history_frame)
        stats_frame.pack(fill='x', pady=2, padx=5)
        self.stats_label = ttk.Label(stats_frame, text="Nenhum teste salvo.")
        self.stats_label.pack(side='left')

        tree_frame = ttk.Frame(history_frame)
        tree_frame.pack(fill='both', expand=True, padx=5)
        tree_frame.grid_rowconfigure(0, weight=1)
        tree_frame.grid_columnconfigure(0, weight=1)

        columns = (
            "Plot", "ID", "Nome", "EPC", "Ângulo Inicial", "Ângulo Final",
            "Step", "Freq (MHz)", "Ângulo Abertura", "Direção Lobular", "Data/Hora"
        )
        display_columns = (
            "Plot", "Nome", "EPC", "Ângulo Inicial", "Ângulo Final",
            "Step", "Freq (MHz)", "Ângulo Abertura", "Direção Lobular", "Data/Hora"
        )

        self.history_tree = ttk.Treeview(
            tree_frame,
            columns=columns,
            displaycolumns=display_columns,
            show='headings',
            height=6
        )

        self.history_tree.heading("Plot", text="Plot", command=lambda: self.sort_history_tree("Plot"))
        self.history_tree.column("Plot", width=45, anchor='center', minwidth=45)
        self.history_tree.heading("Nome", text="Nome do Teste", command=lambda: self.sort_history_tree("Nome"))
        self.history_tree.column("Nome", width=160, anchor='w', minwidth=140)
        self.history_tree.heading("EPC", text="EPC da Tag", command=lambda: self.sort_history_tree("EPC"))
        self.history_tree.column("EPC", width=200, anchor='w', minwidth=180)
        self.history_tree.heading("Ângulo Inicial", text="Ângulo Inicial (°)", command=lambda: self.sort_history_tree("Ângulo Inicial"))
        self.history_tree.column("Ângulo Inicial", width=110, anchor='center', minwidth=100)
        self.history_tree.heading("Ângulo Final", text="Ângulo Final (°)", command=lambda: self.sort_history_tree("Ângulo Final"))
        self.history_tree.column("Ângulo Final", width=100, anchor='center', minwidth=90)
        self.history_tree.heading("Step", text="Step (°)", command=lambda: self.sort_history_tree("Step"))
        self.history_tree.column("Step", width=80, anchor='center', minwidth=70)
        self.history_tree.heading("Freq (MHz)", text="Freq (MHz)", command=lambda: self.sort_history_tree("Freq (MHz)"))
        self.history_tree.column("Freq (MHz)", width=90, anchor='center', minwidth=80)
        self.history_tree.heading("Ângulo Abertura", text="Ângulo de Abertura (°)", command=lambda: self.sort_history_tree("Ângulo Abertura"))
        self.history_tree.column("Ângulo Abertura", width=130, anchor='center', minwidth=120)
        self.history_tree.heading("Direção Lobular", text="Direção Lobular (°)", command=lambda: self.sort_history_tree("Direção Lobular"))
        self.history_tree.column("Direção Lobular", width=130, anchor='center', minwidth=120)
        self.history_tree.heading("Data/Hora", text="Data/Hora", command=lambda: self.sort_history_tree("Data/Hora"))
        self.history_tree.column("Data/Hora", width=140, anchor='center', minwidth=120)

        self._history_headings_defaults = {
            "Plot": "Plot",
            "ID": "ID",
            "Nome": "Nome do Teste",
            "EPC": "EPC da Tag",
            "Ângulo Inicial": "Ângulo Inicial (°)",
            "Ângulo Final": "Ângulo Final (°)",
            "Step": "Step",
            "Freq (MHz)": "Freq (MHz)",
            "Ângulo Abertura": "Ângulo de Abertura (°)",
            "Direção Lobular": "Direção Lobular (°)",
            "Data/Hora": "Data/Hora",
        }

        scrollbar = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.history_tree.yview)
        self.history_tree.configure(yscrollcommand=scrollbar.set)

        h_scrollbar = ttk.Scrollbar(tree_frame, orient=tk.HORIZONTAL, command=self.history_tree.xview)
        self.history_tree.configure(xscrollcommand=h_scrollbar.set)

        self.history_tree.grid(row=0, column=0, sticky='nsew')
        scrollbar.grid(row=0, column=1, sticky='ns')
        h_scrollbar.grid(row=1, column=0, sticky='ew')

        actions_frame = ttk.Frame(history_frame)
        actions_frame.pack(fill='x', padx=5, pady=5)

        select_all_btn = ttk.Button(actions_frame, text="Selecionar Todos", command=self.select_all_tests)
        select_all_btn.pack(side='left', padx=(0, 5))

        deselect_all_btn = ttk.Button(actions_frame, text="Desmarcar Todos", command=self.deselect_all_tests)
        deselect_all_btn.pack(side='left', padx=(0, 5))

        show_table_btn = ttk.Button(actions_frame, text="Mostrar Tabela", command=self.show_selected_table)
        show_table_btn.pack(side='left', padx=(0, 5))

        highlight_btn = ttk.Button(actions_frame, text="Destacar +3 dBm", command=self.highlight_min_margin)
        highlight_btn.pack(side='left', padx=(0, 5))

        delete_btn = ttk.Button(actions_frame, text="Excluir Selecionados", command=self.delete_selected_tests)
        delete_btn.pack(side='left', padx=(0, 5))

        self.export_button = ttk.Button(actions_frame, text="Exportar CSV/Excel", command=self.export_to_csv)
        self.export_button.pack(side='left', padx=(0, 5))

        self.history_tree.bind('<Button-1>', self.on_history_tree_click)
        self.history_tree.bind('<Double-1>', self.on_history_tree_double_click)

    def show_selected_table(self):
        """Abre popup com tabela de ângulos e thresholds do primeiro teste selecionado."""
        if not self.selected_history_ids:
            messagebox.showinfo(
                self._trf("Tabela de Teste"),
                self._trf("Selecione um teste no histórico."),
            )
            return
        if len(self.selected_history_ids) > 1:
            messagebox.showwarning(
                self._trf("Tabela de Teste"),
                self._trf("Selecione apenas um teste para visualizar a tabela."),
                parent=self.master
            )
            return

        test_id = next(iter(self.selected_history_ids))
        test = self.historical_tests.get(test_id)

        if not test:
            messagebox.showwarning(
                self._trf("Tabela de Teste"),
                self._trf("Não foi possível carregar os dados do teste selecionado."),
            )
            return

        measurements = test.get('measurements') or []
        if not measurements:
            messagebox.showinfo(
                self._trf("Tabela de Teste"),
                self._trf("Este teste não possui medições salvas."),
            )
            return

        popup = tk.Toplevel(self.master)
        popup.title(f"Tabela - {test.get('test_name', f'Teste #{test_id}')}")
        popup.geometry("420x360")
        popup.transient(self.master)
        popup.grab_set()

        table_frame = ttk.Frame(popup)
        table_frame.pack(fill='both', expand=True, padx=10, pady=10)
        table_frame.grid_rowconfigure(0, weight=1)
        table_frame.grid_columnconfigure(0, weight=1)

        table = ttk.Treeview(
            table_frame,
            columns=("Ângulo", "Threshold"),
            show="headings",
            height=12
        )
        table.heading("Ângulo", text="Ângulo (°)")
        table.heading("Threshold", text="Threshold (dBm)")
        table.column("Ângulo", width=120, anchor='center')
        table.column("Threshold", width=200, anchor='center')
        y_scroll = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=table.yview)
        x_scroll = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=table.xview)
        table.configure(yscrollcommand=y_scroll.set, xscrollcommand=x_scroll.set)

        table.grid(row=0, column=0, sticky='nsew')
        y_scroll.grid(row=0, column=1, sticky='ns')
        x_scroll.grid(row=1, column=0, sticky='ew')

        for entry in sorted(measurements, key=lambda e: float(e.get('angle', 0.0))):
            ang = f"{float(entry.get('angle', 0.0)):.1f}"
            threshold = entry.get('threshold_display') or entry.get('threshold_raw') or "-"
            table.insert('', 'end', values=(ang, threshold))

        ttk.Button(popup, text=self._trf("Fechar"), command=popup.destroy).pack(pady=(0, 10))
        popup.focus_force()

    def highlight_min_margin(self):
        """Destaca no gráfico um círculo a +3 dBm do menor threshold do teste selecionado."""
        if not self.selected_history_ids:
            messagebox.showinfo(
                self._trf("Destaque +3 dBm"),
                self._trf("Selecione um teste no histórico para destacar."),
            )
            return
        if len(self.selected_history_ids) > 1:
            messagebox.showwarning(
                self._trf("Destaque +3 dBm"),
                self._trf("Escolha apenas um teste para destacar."),
                parent=self.master
            )
            return

        test_id = next(iter(self.selected_history_ids))
        test_data = self.historical_tests.get(test_id)
        if not test_data:
            messagebox.showwarning(
                self._trf("Destaque +3 dBm"),
                self._trf("Não foi possível carregar os dados do teste selecionado para destaque."),
            )
            return

        measurements = test_data.get('measurements') or []
        metrics = self._compute_threshold_metrics(measurements)
        min_value = metrics.get('min_value')
        if min_value is None:
            messagebox.showinfo(
                self._trf("Destaque +3 dBm"),
                self._trf("O teste selecionado não possui valores numéricos de threshold para calcular o destaque.")
            )
            return

        min_angle = metrics.get('min_angle')
        target_radius = metrics.get('target_radius')
        intersections = metrics.get('intersections') or []
        abertura = metrics.get('angular_opening')
        analysis_allowed = metrics.get('analysis_allowed', True)
        analysis_block_reason = metrics.get('analysis_block_reason')

        self._highlight_overlay = {
            "test_id": test_id,
            "radius": target_radius,
            "base_value": min_value,
            "label": f"{test_data.get('test_name', f'Teste #{test_id}')} (+3 dBm)",
            "intersections_deg": intersections,
            "abertura_deg": abertura if analysis_allowed else None,
            "angle_min_deg": min_angle,
            "analysis_allowed": analysis_allowed,
            "analysis_block_reason": analysis_block_reason
        }

        inter_debug = ", ".join(f"{float(val):.2f}°" for val in intersections) if intersections else "nenhuma interseção"
        abertura_debug = f"{abertura:.2f}°" if abertura is not None else "N/A"
        self._log(
            f"Destaque de +3 dBm aplicado ao teste {test_id}: mínimo={min_value:.2f} dBm, "
            f"círculo em {target_radius:.2f} dBm, interseções: {inter_debug}, abertura: {abertura_debug}. "
            f"Análise {'habilitada' if analysis_allowed else 'bloqueada'}."
        )
        self.update_polar_plot()

        if intersections:
            inter_text = ", ".join(f"{val:.1f}°" for val in intersections)
        else:
            inter_text = self._trf("não identificado")

        centro_text = f"{min_angle:.1f}°" if min_angle is not None else self._trf("indefinida")

        if analysis_allowed:
            abertura_text = f"{abertura:.1f}°" if abertura is not None else self._trf("não calculada")
            direcao_text = centro_text
            abertura_line = self._trf("Abertura angular: {value}", value=abertura_text)
            direcao_line = self._trf("Direção lobular: {value}", value=direcao_text)
        else:
            reason = analysis_block_reason or self._trf("Condições mínimas não atendidas.")
            abertura_line = self._trf("Abertura angular: análise indisponível")
            direcao_line = self._trf("Direção lobular: análise indisponível ({reason})", reason=reason)

        messagebox.showinfo(
            self._trf("Destaque +3 dBm"),
            self._trf(
                "Círculo pontilhado adicionado em {radius:.1f} dBm (mínimo registrado: {min_value:.1f} dBm @ {center}).\nInterseções: {intersections}\n{opening_line}\n{direction_line}",
                radius=target_radius,
                min_value=min_value,
                center=centro_text,
                intersections=inter_text,
                opening_line=abertura_line,
                direction_line=direcao_line,
            ),
            parent=self.master
        )

    def _extract_numeric_threshold(self, entry: Dict) -> Tuple[Optional[float], Optional[str]]:
        """Obtém o valor numérico e a relação do threshold de uma medição."""
        raw_value = entry.get('threshold_raw')
        if isinstance(raw_value, (int, float)):
            return float(raw_value), None

        candidates = [
            raw_value,
            entry.get('threshold_display')
        ]

        for candidate in candidates:
            if isinstance(candidate, (int, float)):
                return float(candidate), None
            if not isinstance(candidate, str):
                continue

            relation = None
            text = candidate.strip().replace(',', '.').upper()
            if not text:
                continue

            if text[0] in ('>', '<'):
                relation = text[0]
                text = text[1:].strip()

            try:
                value = float(text)
            except ValueError:
                continue
            return value, relation
        return None, None

    def _compute_threshold_metrics(self, measurements: list) -> Dict[str, Optional[Union[float, list]]]:
        """Calcula valores auxiliares (mínimo, direção, interseções e abertura) para um conjunto de medições."""
        if not measurements:
            return {
                "min_value": None,
                "min_angle": None,
                "target_radius": None,
                "intersections": [],
                "angular_opening": None,
                "analysis_allowed": False,
                "analysis_block_reason": "Nenhum ponto válido para análise."
            }

        measurement_records: list = []
        for entry in measurements:
            if isinstance(entry, dict) and entry.get('is_progress'):
                continue
            angle = None
            if isinstance(entry, dict):
                angle = entry.get('angle', entry.get('raw_angle'))
            elif isinstance(entry, (list, tuple)) and len(entry) >= 2:
                angle = entry[0]
                entry = {'threshold_raw': entry[1]}
            if angle is None:
                continue
            value, relation = self._extract_numeric_threshold(entry) if isinstance(entry, dict) else (None, None)
            if value is None:
                continue
            measurement_records.append((float(angle) % 360.0, float(value), relation))

        if not measurement_records:
            return {
                "min_value": None,
                "min_angle": None,
                "target_radius": None,
                "intersections": [],
                "angular_opening": None,
                "analysis_allowed": False,
                "analysis_block_reason": "Nenhum ponto válido para análise."
            }

        measurement_records.sort(key=lambda item: item[0])
        measurement_points = [(angle, value) for angle, value, _ in measurement_records]
        measurement_relations = [relation for _, _, relation in measurement_records]
        points_with_relation = list(zip(measurement_points, measurement_relations))
        min_value = min(val for _, val in measurement_points)
        tolerance = 1e-6
        min_angle = None
        for angle, val in measurement_points:
            if math.isclose(val, min_value, abs_tol=tolerance):
                min_angle = angle
                break
        if min_angle is None:
            min_angle = measurement_points[0][0]

        target_radius = max(min(min_value + 3.0, DEFAULT_MAX_POWER_DBM), DEFAULT_MIN_POWER_DBM)
        intersections = self._calculate_circle_intersections(measurement_points, target_radius)
        angular_opening = self._compute_angular_opening(min_angle, intersections)

        analysis_allowed = True
        analysis_block_reason: Optional[str] = None

        lower_threshold_limit = 5.0
        upper_threshold_limit = 25.0
        neighbor_tolerance = 1.5

        for (_, value), relation in points_with_relation:
            if relation == '<' or value < lower_threshold_limit:
                analysis_allowed = False
                analysis_block_reason = "Pontos abaixo de 5 dBm."
                break

        if analysis_allowed:
            total_points = len(points_with_relation)
            if total_points >= 3:
                for idx, ((_, value), relation) in enumerate(points_with_relation):
                    is_high = relation == '>' or value > upper_threshold_limit
                    if not is_high:
                        continue

                    prev_point = points_with_relation[idx - 1]
                    next_point = points_with_relation[(idx + 1) % total_points]

                    prev_value = prev_point[0][1]
                    prev_relation = prev_point[1]
                    prev_adjusted = upper_threshold_limit if prev_relation == '>' else prev_value

                    next_value = next_point[0][1]
                    next_relation = next_point[1]
                    next_adjusted = upper_threshold_limit if next_relation == '>' else next_value

                    if (
                        abs(prev_adjusted - upper_threshold_limit) > neighbor_tolerance
                        and abs(next_adjusted - upper_threshold_limit) > neighbor_tolerance
                    ):
                        analysis_allowed = False
                        analysis_block_reason = "Valor isolado acima de 25 dBm."
                        break
            else:
                high_detected = any(
                    relation == '>' or value > upper_threshold_limit
                    for (_, value), relation in points_with_relation
                )
                if high_detected:
                    analysis_allowed = False
                    analysis_block_reason = "Valor acima de 25 dBm sem vizinhos para comparação."

        if not analysis_allowed:
            angular_opening = None

        return {
            "min_value": min_value,
            "min_angle": min_angle,
            "target_radius": target_radius,
            "intersections": intersections,
            "angular_opening": angular_opening,
            "analysis_allowed": analysis_allowed,
            "analysis_block_reason": analysis_block_reason
        }

    def _compute_angular_opening(self, min_angle: float, intersections: list) -> Optional[float]:
        """Calcula a abertura angular relativa ao ângulo mínimo."""
        if min_angle is None or not intersections or len(intersections) < 2:
            return None

        sorted_intersections = sorted(angle % 360.0 for angle in intersections)
        min_angle_norm = min_angle % 360.0
        tol = 1e-6

        right_angle = None
        for angle in sorted_intersections:
            if angle + tol >= min_angle_norm:
                right_angle = angle
                break
        if right_angle is None:
            right_angle = sorted_intersections[0] + 360.0

        left_angle = None
        for angle in reversed(sorted_intersections):
            if angle - tol <= min_angle_norm:
                left_angle = angle
                break
        if left_angle is None:
            left_angle = sorted_intersections[-1] - 360.0

        abertura = right_angle - left_angle
        if abertura < 0:
            abertura += 360.0
        return abertura

    def _calculate_circle_intersections(
        self,
        measurement_points: list,
        target_radius: float
    ) -> list:
        """Calcula ângulos onde a curva cruza o círculo de +3 dBm."""
        if not measurement_points:
            return []

        sorted_points = sorted(measurement_points, key=lambda item: item[0])
        intersections: list = []

        def _register(angle: float) -> None:
            if math.isnan(angle):
                return
            for existing in intersections:
                if abs(existing - angle) <= 1e-3:
                    return
            intersections.append(angle)

        for idx, (angle_deg, value) in enumerate(sorted_points):
            if math.isclose(value, target_radius, rel_tol=1e-6, abs_tol=1e-6):
                _register(angle_deg)

            if idx == len(sorted_points) - 1:
                continue

            next_angle, next_value = sorted_points[idx + 1]
            if math.isclose(next_value, target_radius, rel_tol=1e-6, abs_tol=1e-6):
                _register(next_angle)

            diff_current = value - target_radius
            diff_next = next_value - target_radius

            if diff_current == 0 and diff_next == 0:
                continue

            if diff_current * diff_next < 0:
                try:
                    ratio = (target_radius - value) / (next_value - value)
                    interpolated_angle = angle_deg + ratio * (next_angle - angle_deg)
                    _register(interpolated_angle)
                except ZeroDivisionError:
                    continue

        intersections.sort()
        return intersections

    def _draw_highlight_circle(self, overlay: Dict[str, Union[int, float, str]]) -> None:
        """Desenha o círculo pontilhado vermelho correspondente ao destaque."""
        radius = overlay.get('radius')
        if radius is None:
            return

        try:
            radius_val = float(radius)
        except (TypeError, ValueError):
            return

        radius_val = max(min(radius_val, DEFAULT_MAX_POWER_DBM), DEFAULT_MIN_POWER_DBM)
        theta = np.linspace(0, 2 * np.pi, 361)
        circle_label = overlay.get('label')

        self.polar_ax.plot(theta, np.full_like(theta, radius_val), linestyle='--', color='red', linewidth=1.5, label=None)
        self.polar_ax.plot([0], [radius_val], marker='o', color='red', markersize=5)

        intersections = overlay.get('intersections_deg') or []
        for angle_deg in intersections:
            try:
                ang_rad = math.radians(float(angle_deg))
            except (TypeError, ValueError):
                continue
            self.polar_ax.plot([ang_rad], [radius_val], marker='o', color='red', markersize=6)

        if circle_label:
            self.polar_ax.plot([], [], linestyle='--', color='red', linewidth=1.5, label=circle_label)

    def _clear_highlight_if_invalid(self):
        """Garante que o destaque só permaneça quando apenas um teste válido estiver selecionado."""
        if not self._highlight_overlay:
            return

        test_id = self._highlight_overlay.get('test_id')
        if test_id is None:
            self._highlight_overlay = None
            return

        if (
            len(self.selected_history_ids) != 1
            or test_id not in self.selected_history_ids
            or test_id not in self.historical_tests
        ):
            self._highlight_overlay = None


    def _register_tooltip_point(self, angle_rad: float, value: float, display_text: str):
        try:
            value_float = float(value)
        except (TypeError, ValueError):
            text_upper = str(display_text).strip().upper()
            if text_upper.startswith('>'):
                value_float = DEFAULT_MAX_POWER_DBM
            elif text_upper.startswith('<'):
                value_float = DEFAULT_MIN_POWER_DBM
            else:
                value_float = DEFAULT_MIN_POWER_DBM
        else:
            value_float = float(value)

        self._tooltip_points.append((angle_rad, value_float, display_text))
        self._all_r_values.append(value_float)

    def _init_polar_tooltip(self, force_rebind: bool = False):
        try:
            if not hasattr(self, 'polar_annot') or self.polar_annot is None:
                self.polar_annot = self.polar_ax.annotate(
                    "",
                    xy=(0, 0),
                    xytext=(15, 15),
                    textcoords="offset points",
                    bbox=dict(boxstyle="round,pad=0.4", facecolor="white", alpha=0.85),
                    arrowprops=dict(arrowstyle="->", color="black"),
                    fontsize=9
                )
            self.polar_annot.set_visible(False)

            if force_rebind and self._polar_motion_cid is not None:
                try:
                    self.polar_canvas.mpl_disconnect(self._polar_motion_cid)
                except Exception:
                    pass
                self._polar_motion_cid = None
            if force_rebind and self._polar_leave_cid is not None:
                try:
                    self.polar_canvas.mpl_disconnect(self._polar_leave_cid)
                except Exception:
                    pass
                self._polar_leave_cid = None

            if self._polar_motion_cid is None:
                self._polar_motion_cid = self.polar_canvas.mpl_connect('motion_notify_event', self._on_polar_motion)
            if self._polar_leave_cid is None:
                self._polar_leave_cid = self.polar_canvas.mpl_connect('figure_leave_event', self._on_polar_leave)
        except Exception:
            pass

    def _on_polar_motion(self, event):
        try:
            if event.inaxes != self.polar_ax or not self._tooltip_points:
                if self.polar_annot.get_visible():
                    self.polar_annot.set_visible(False)
                    self.polar_canvas.draw_idle()
                return

            theta = event.xdata
            radius = event.ydata
            if theta is None or radius is None:
                if self.polar_annot.get_visible():
                    self.polar_annot.set_visible(False)
                    self.polar_canvas.draw_idle()
                return

            event_x = radius * math.cos(theta)
            event_y = radius * math.sin(theta)

            best_point = None
            best_distance = float('inf')

            for point_theta, point_radius, display_text in self._tooltip_points:
                px = point_radius * math.cos(point_theta)
                py = point_radius * math.sin(point_theta)
                distance = math.hypot(event_x - px, event_y - py)
                if distance < best_distance:
                    best_distance = distance
                    best_point = (point_theta, point_radius, display_text)

            if best_point and best_distance <= 1.2:
                theta_point, radius_point, display_text = best_point
                angle_deg = (math.degrees(theta_point)) % 360
                if not display_text:
                    display_text = f"{radius_point:.1f} dBm"
                tooltip_text = f"Ângulo: {angle_deg:.1f}°\nThreshold: {display_text}"
                self.polar_annot.xy = (theta_point, radius_point)
                self.polar_annot.set_text(tooltip_text)
                self.polar_annot.set_visible(True)
                self.polar_canvas.draw_idle()
            elif self.polar_annot.get_visible():
                self.polar_annot.set_visible(False)
                self.polar_canvas.draw_idle()
        except Exception:
            pass

    def _on_polar_leave(self, _event):
        try:
            if hasattr(self, 'polar_annot') and self.polar_annot.get_visible():
                self.polar_annot.set_visible(False)
                self.polar_canvas.draw_idle()
        except Exception:
            pass

    # ------------------------------------------------------------------
    # Histórico de testes (persistência + UI)
    # ------------------------------------------------------------------
    def _ensure_color_for_test(self, test_id: int) -> str:
        if test_id in self.test_colors:
            return self.test_colors[test_id]

        for color in self._color_palette:
            if color not in self.test_colors.values():
                self.test_colors[test_id] = color
                return color

        # Fallback: recicla cores caso todas já tenham sido usadas
        color = self._color_palette[len(self.test_colors) % len(self._color_palette)]
        self.test_colors[test_id] = color
        return color

    def load_history_to_tree(self):
        if not hasattr(self, 'history_tree'):
            return

        for item in self.history_tree.get_children():
            self.history_tree.delete(item)

        self.historical_tests = {}
        self._history_tree_item_map = {}

        if not self.database:
            return

        history = self.database.get_test_history()

        for test in history:
            test_id = test.get('id')
            if test_id is None:
                continue
            sanitized = self._sanitize_measurements(test)
            if sanitized and self.database:
                try:
                    self.database.update_test(test_id, test)
                except Exception as e:
                    print(f"⚠️ Não foi possível atualizar teste {test_id} com ângulos normalizados: {e}")

            metrics = self._compute_threshold_metrics(test.get('measurements', []))
            analysis_allowed = metrics.get('analysis_allowed', True)
            updated = False

            if not analysis_allowed:
                if test.get('angular_opening_deg') is not None:
                    test['angular_opening_deg'] = None
                    updated = True
                if test.get('lobular_direction_deg') is not None:
                    test['lobular_direction_deg'] = None
                    updated = True
            else:
                abertura_calc = metrics.get('angular_opening')
                direcao_calc = metrics.get('min_angle')
                if test.get('angular_opening_deg') is None and abertura_calc is not None:
                    test['angular_opening_deg'] = abertura_calc
                    updated = True
                if test.get('lobular_direction_deg') is None and direcao_calc is not None:
                    test['lobular_direction_deg'] = direcao_calc
                    updated = True

            if updated and self.database:
                try:
                    self.database.update_test(test_id, test)
                except Exception as e:
                    print(f"⚠️ Não foi possível atualizar métricas do teste {test_id}: {e}")

            self.historical_tests[test_id] = test
            checkbox = "☑" if test_id in self.selected_history_ids else "☐"
            abertura_val = test.get('angular_opening_deg')
            lobular_val = test.get('lobular_direction_deg')
            abertura_str = f"{float(abertura_val):.1f}" if abertura_val is not None else "-"
            lobular_str = f"{float(lobular_val):.1f}" if lobular_val is not None else "-"
            values = (
                checkbox,
                test_id,
                test.get('test_name', f"Teste #{test_id}"),
                test.get('epc', 'N/A'),
                f"{test.get('start_angle', 0.0):.1f}",
                f"{test.get('end_angle', 0.0):.1f}",
                f"{test.get('step_angle', 0.0):.1f}",
                f"{test.get('frequency_mhz', 0.0):.1f}",
                abertura_str,
                lobular_str,
                test.get('timestamp', '-')
            )
            item_id = self.history_tree.insert('', 'end', values=values)
            self._history_tree_item_map[item_id] = test_id
            self._ensure_color_for_test(test_id)

        self._clear_highlight_if_invalid()
        self.update_history_stats()
        self.update_plot_from_history()

    def update_history_stats(self):
        if not hasattr(self, 'stats_label') or not self.database:
            return

        stats = self.database.get_statistics()
        total_tests = stats.get('total_tests', 0)
        unique_tags = stats.get('unique_tags', 0)
        self.stats_label.config(text=f"Total de testes: {total_tests} | Tags únicas: {unique_tags}")

    def select_all_tests(self):
        self.selected_history_ids = set(self.historical_tests.keys())
        self.load_history_to_tree()
        self._schedule_state_save()

    def deselect_all_tests(self):
        self.selected_history_ids.clear()
        self._highlight_overlay = None
        self.load_history_to_tree()
        self._schedule_state_save()

    def delete_selected_tests(self):
        if not self.selected_history_ids:
            messagebox.showinfo(
                self._trf("Histórico"),
                self._trf("Nenhum teste selecionado para excluir."),
            )
            return

        resposta = messagebox.askyesno(
            self._trf("Excluir Testes"),
            self._trf("Deseja realmente remover os testes selecionados do histórico?"),
            parent=self.master
        )
        if not resposta:
            return

        for test_id in list(self.selected_history_ids):
            if self.database.delete_test(test_id):
                self.selected_history_ids.discard(test_id)
                self.historical_tests.pop(test_id, None)
                self.test_colors.pop(test_id, None)

        self._clear_highlight_if_invalid()
        self.load_history_to_tree()
        self._schedule_state_save()

    def sort_history_tree(self, column: str):
        if not hasattr(self, 'history_tree'):
            return

        current = self._history_sort_state.get('column')
        reverse = self._history_sort_state.get('reverse', False)

        if column == current:
            reverse = not reverse
        else:
            reverse = False

        self._history_sort_state = {'column': column, 'reverse': reverse}

        items = []
        for item_id in self.history_tree.get_children(''):
            cell_value = self.history_tree.set(item_id, column)
            items.append((cell_value, item_id))

        def _convert(value):
            try:
                return float(value)
            except (ValueError, TypeError):
                return str(value)

        items.sort(key=lambda x: _convert(x[0]), reverse=reverse)

        for index, (_, item_id) in enumerate(items):
            self.history_tree.move(item_id, '', index)

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

        column = self.history_tree.identify_column(event.x)
        if column != '#1':
            return

        item_id = self.history_tree.identify_row(event.y)
        if not item_id:
            return 'break'

        test_id = self._history_tree_item_map.get(item_id)
        if test_id is None:
            return 'break'

        if test_id in self.selected_history_ids:
            self.selected_history_ids.remove(test_id)
            new_value = "☐"
        else:
            self.selected_history_ids.add(test_id)
            new_value = "☑"

        self.history_tree.set(item_id, 'Plot', new_value)
        self._clear_highlight_if_invalid()
        self.update_plot_from_history()
        self._schedule_state_save()
        return 'break'

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

        column = self.history_tree.identify_column(event.x)
        if column != '#2':  # Coluna "Nome"
            return

        item_id = self.history_tree.identify_row(event.y)
        if not item_id:
            return

        bbox = self.history_tree.bbox(item_id, column)
        if not bbox:
            return

        x, y, width, height = bbox
        test_id = self._history_tree_item_map.get(item_id)
        if test_id is None:
            return

        current_name = self.history_tree.set(item_id, 'Nome')
        self._history_edit_var = tk.StringVar(value=current_name)
        self._history_edit_entry = ttk.Entry(self.history_tree, textvariable=self._history_edit_var)
        self._history_edit_entry.place(x=x, y=y, width=width, height=height)
        self._history_edit_entry.focus_set()
        self._history_edit_entry.bind('<Return>', lambda e: self._finish_history_name_edit(item_id, test_id))
        self._history_edit_entry.bind('<FocusOut>', lambda e: self._finish_history_name_edit(item_id, test_id))

    def _finish_history_name_edit(self, item_id, test_id):
        if not self._history_edit_entry:
            return

        new_name = self._history_edit_var.get().strip()
        self._history_edit_entry.destroy()
        self._history_edit_entry = None

        if not new_name:
            return

        test_data = self.historical_tests.get(test_id)
        if not test_data:
            return

        test_data = dict(test_data)
        test_data['test_name'] = new_name
        if self.database.update_test(test_id, test_data):
            self.historical_tests[test_id] = test_data
            self.history_tree.set(item_id, 'Nome', new_name)
            self._schedule_state_save()

    def update_plot_from_history(self):
        self.update_polar_plot()

    def _save_current_test_to_history(self):
        if not self.database:
            return

        final_points = [entry for entry in self.test_history if not entry.get('is_progress', False)]
        if not final_points:
            return

        metadata = dict(self.current_test_metadata or {})
        if not metadata.get('test_name'):
            metadata['test_name'] = f"Teste {datetime.now().strftime('%H:%M:%S')}"

        sanitized_points = []
        for entry in final_points:
            sanitized_points.append({
                'angle': float(entry.get('angle', 0.0)),
                'threshold_raw': entry.get('threshold_raw'),
                'threshold_display': entry.get('threshold_display'),
                'timestamp': entry.get('timestamp')
            })

        metrics = self._compute_threshold_metrics(sanitized_points)
        analysis_allowed = metrics.get('analysis_allowed', True)
        abertura = metrics.get('angular_opening') if analysis_allowed else None
        direcao = metrics.get('min_angle') if analysis_allowed else None

        payload = {
            'test_name': metadata.get('test_name'),
            'epc': metadata.get('epc', 'N/A'),
            'frequency_mhz': float(metadata.get('frequency_mhz', 0.0)),
            'start_angle': float(metadata.get('start_angle', 0.0)),
            'end_angle': float(metadata.get('end_angle', 0.0)),
            'step_angle': float(metadata.get('step_angle', 0.0)),
            'measurements': sanitized_points,
            'angular_opening_deg': abertura,
            'lobular_direction_deg': direcao
        }

        new_id = self.database.add_test(payload)
        if new_id is None:
            messagebox.showwarning(
                self._trf("Histórico"),
                self._trf("Não foi possível salvar o teste no histórico."),
            )
            return

        payload['id'] = new_id
        self.historical_tests[new_id] = payload
        self.selected_history_ids.add(new_id)
        self._ensure_color_for_test(new_id)
        self.test_history = []
        self.current_test_metadata = {}
        self.load_history_to_tree()
        self._schedule_state_save()
        try:
            last_item = self.history_tree.get_children()[-1]
            self.history_tree.see(last_item)
            self.history_tree.selection_set(last_item)
        except Exception:
            pass
        
    def _check_hardware(self):
        """Verifica se a porta COM5 existe."""
        global DLL_LOADED
        arduino_ok = False
        
        try:
            temp_serial = serial.Serial(ARDUINO_PORT, ARDUINO_BAUD, timeout=1)
            temp_serial.close()
            arduino_ok = True
        except serial.SerialException:
            pass
        
        if not arduino_ok:
            self.mode_var.set("manual")
            if hasattr(self, 'automatic_radio'):
                self.automatic_radio.config(state="disabled")
            self._log(f"Arduino não encontrado na porta {ARDUINO_PORT}. Alternando para modo manual.", "warning")
        else:
            if hasattr(self, 'automatic_radio'):
                self.automatic_radio.config(state="normal")
        
        if not DLL_LOADED:
            self.status_label.config(text=f"ERRO: DLL RFID não carregada.", foreground="red")
            self.start_button.config(state="disabled")
            return
            
        if not arduino_ok:
            self.status_label.config(text=f"ERRO: Arduino não encontrado em {ARDUINO_PORT}.", foreground="red")
            self.start_button.config(state="disabled")
            self._log(f"Erro de hardware: Arduino não encontrado em {ARDUINO_PORT}", "error")
            return

        status_text = self._trf("Pronto. Arduino conectado em {port}.", port=ARDUINO_PORT)

        try:
            temp_serial_rfid = serial.Serial(f'COM{RFID_READER_COM_NUM}', ARDUINO_BAUD, timeout=0.1)
            temp_serial_rfid.close()
            status_text += f"\n({self._trf('Leitor RFID COM{port} LIVRE', port=RFID_READER_COM_NUM)})"
        except serial.SerialException:
            status_text += f"\n({self._trf('Leitor RFID COM{port} OCUPADO', port=RFID_READER_COM_NUM)})"
            self._log(f"Leitor RFID COM{RFID_READER_COM_NUM} ocupado durante checagem de hardware.", "warning")
        
        self.status_label.config(text=status_text, foreground="green")
        self.start_button.config(state="normal")
        self._log("Hardware validado com sucesso.")
            
    def _request_cancel(self):
        """Solicita o cancelamento do teste."""
        self.cancel_requested = True
        self._cancelled_run = True
        self.ignore_live_updates = True
        self.status_label.config(text="Cancelamento solicitado...", foreground="orange")
        self.cancel_button.config(state="disabled")
        self.manual_waiting_confirm = False
        self.manual_confirm_button.config(state="disabled")
        self.manual_step_event.set()
        self.test_history = []
        self.current_test_metadata = {}
        self.update_polar_plot()
        self._schedule_state_save()
        self._schedule_state_save()

    def _start_test_thread(self):
        """Inicia a thread principal do workflow de teste."""
        self.start_button.config(state="disabled")
        self.cancel_button.config(state="normal")
        self.test_running = True
        self.cancel_requested = False
        self._cancelled_run = False
        self.status_label.config(text="Teste em andamento...", foreground="blue")

        worker_thread = threading.Thread(target=self._test_workflow_integrated, daemon=True) 
        worker_thread.start()

    # --- FUNÇÃO PARA ATUALIZAR O GRÁFICO POLAR (REVISÃO v10) ---
    def update_polar_plot(self):
        """Atualiza o gráfico polar com dados do teste atual e dos testes selecionados."""
        if self.polar_fig is None:
            return

        try:
            self.polar_fig.clear()
            self.polar_ax = self.polar_fig.add_subplot(111, projection='polar')
            self._configure_polar_axis()

            self._tooltip_points = []
            self._all_r_values = []

            datasets = self._collect_datasets_for_plot()
            plotted_ids = set()
            for dataset in datasets:
                test_id = dataset.get('test_id')
                if test_id is not None:
                    plotted_ids.add(test_id)
                self._plot_dataset_on_axis(dataset)

            overlay = getattr(self, "_highlight_overlay", None)
            if overlay and overlay.get('test_id') in plotted_ids:
                self._draw_highlight_circle(overlay)
            elif overlay and overlay.get('test_id') is not None and overlay.get('test_id') not in plotted_ids:
                self._highlight_overlay = None

            handles, labels = self.polar_ax.get_legend_handles_labels()
            if handles:
                legend = self.polar_ax.legend(handles, labels, framealpha=0.9, fontsize='small',
                                              loc='center left', bbox_to_anchor=(1.05, 0.5), borderaxespad=0.)
                self.polar_fig.subplots_adjust(right=0.82)

            self._init_polar_tooltip(force_rebind=True)
            self.polar_canvas.draw_idle()

        except Exception as e:
            print(f"Erro ao atualizar gráfico polar: {e}")
            import traceback
            traceback.print_exc()

    def _execute_factory_reset_if_needed(self):
        """
        Executa o factory reset do leitor RFID apenas uma vez por sessão,
        reutilizando a mesma lógica do módulo de licença.
        """
        if not DLL_LOADED:
            return

        try:
            already_done = getattr(HardwareManager, "_factory_reset_global_done", False)
            print(f"[INFO] FastOrientation: executando factory reset na porta COM{RFID_READER_COM_NUM}.")
            hw_manager = HardwareManager(com_port=RFID_READER_COM_NUM)
            success = hw_manager.factory_reset(preserve_profile=True, emit_beep=False)
            hw_manager.close()

            if success:
                print("[OK] FastOrientation: factory reset concluído com sucesso.")
            else:
                print("[AVISO] FastOrientation: factory reset não pôde ser concluído.")
        except Exception as e:
            print(f"[AVISO] FastOrientation: erro ao executar factory reset: {e}")

    def _configure_polar_axis(self):
        self.polar_ax.set_theta_direction(-1)
        self.polar_ax.set_theta_zero_location("N")
        self.polar_ax.set_rlim(DEFAULT_MAX_POWER_DBM, DEFAULT_MIN_POWER_DBM)
        if getattr(self, "_polar_current_rlim", None):
            self.polar_ax.set_rlim(*self._polar_current_rlim)

        r_ticks = np.arange(DEFAULT_MIN_POWER_DBM, DEFAULT_MAX_POWER_DBM + 1, 5)
        self.polar_ax.set_rticks(r_ticks)
        self.polar_ax.set_yticklabels([str(int(t)) for t in r_ticks])
        self.polar_ax.set_rlabel_position(22.5)
        theta_min, theta_max = getattr(self, "_polar_current_thetalim", (0, 360))
        try:
            if theta_max - theta_min >= 15:
                theta_grid = np.arange(theta_min, theta_max, 15)
                if len(theta_grid) == 0:
                    theta_grid = [theta_min]
            else:
                theta_grid = [theta_min, theta_max]
            self.polar_ax.set_thetalim(theta_min, theta_max)
            self.polar_ax.set_thetagrids(theta_grid)
        except Exception:
            self.polar_ax.set_thetagrids(np.arange(0, 360, 15))
        self.polar_ax.set_title("Threshold vs. Ângulo (°)", va='bottom')

    def _collect_datasets_for_plot(self):
        datasets = []

        for test_id in sorted(self.selected_history_ids):
            test = self.historical_tests.get(test_id)
            if not test:
                continue
            datasets.append({
                'label': test.get('test_name', f"Teste #{test_id}"),
                'color': self._ensure_color_for_test(test_id),
                'measurements': test.get('measurements', []),
                'is_live': False,
                'step': float(test.get('step_angle', 0.0) or 0.0),
                'test_id': test_id
            })

        if self.test_history:
            datasets.append({
                'label': self.current_test_metadata.get('test_name', 'Teste Atual'),
                'color': '#ff7f0e',
                'measurements': list(self.test_history),
                'is_live': True,
                'step': float(self.current_test_metadata.get('step_angle', 0.0) or 0.0),
                'test_id': None
            })

        return datasets

    def _snap_angle_to_grid(self, raw_angle: float, start_angle: float, end_angle: float, step_angle: float) -> float:
        if step_angle <= 0.0 or math.isclose(step_angle, 0.0, abs_tol=1e-6):
            return float(round(max(min(raw_angle, end_angle), start_angle), 3))

        tolerance = max(0.3, step_angle * 0.15)
        clamped_angle = max(min(raw_angle, end_angle + tolerance), start_angle - tolerance)

        relative = clamped_angle - start_angle
        index = round(relative / step_angle)
        snapped = start_angle + index * step_angle

        if snapped < start_angle - tolerance:
            snapped = start_angle
        if snapped > end_angle + tolerance:
            snapped = end_angle

        if abs(raw_angle - end_angle) <= tolerance or snapped > end_angle:
            snapped = end_angle
        if abs(raw_angle - start_angle) <= tolerance or snapped < start_angle:
            snapped = start_angle

        return float(round(snapped, 3))

    def _normalize_measurement_angle(self, raw_angle: float) -> float:
        """
        Ajusta o ângulo medido para o grid teórico definido por start/end/step.
        Evita acumular frações de passo (ex.: 14.93°) e garante que o último ponto
        coincida exatamente com o ângulo final desejado.
        """
        try:
            start_angle = float(self.current_test_metadata.get('start_angle', 0.0) or 0.0)
        except Exception:
            start_angle = 0.0
        try:
            end_angle = float(self.current_test_metadata.get('end_angle', raw_angle) or raw_angle)
        except Exception:
            end_angle = raw_angle
        try:
            step_angle = float(self.current_test_metadata.get('step_angle', 0.0) or 0.0)
        except Exception:
            step_angle = 0.0

        return self._snap_angle_to_grid(raw_angle, start_angle, end_angle, step_angle)

    def _sanitize_measurements(self, test_payload: Dict) -> bool:
        """
        Normaliza ângulos de testes já salvos no banco (histórico) para manter
        consistência visual com os novos testes.
        Retorna True se houve alteração.
        """
        measurements = test_payload.get('measurements')
        if not isinstance(measurements, list):
            return False

        try:
            start_angle = float(test_payload.get('start_angle', 0.0) or 0.0)
        except Exception:
            start_angle = 0.0
        try:
            end_angle = float(test_payload.get('end_angle', start_angle) or start_angle)
        except Exception:
            end_angle = start_angle
        try:
            step_angle = float(test_payload.get('step_angle', 0.0) or 0.0)
        except Exception:
            step_angle = 0.0

        changed = False
        angle_index_map = {}
        normalized_list = []

        for entry in measurements:
            entry_dict = dict(entry)
            if entry_dict.get('is_progress'):
                changed = True
                continue
            raw_angle = float(entry_dict.get('raw_angle', entry_dict.get('angle', 0.0)) or 0.0)
            normalized = self._snap_angle_to_grid(raw_angle, start_angle, end_angle, step_angle)

            if not math.isclose(raw_angle, normalized, abs_tol=1e-3):
                changed = True

            entry_dict['raw_angle'] = raw_angle
            entry_dict['angle'] = normalized

            if normalized in angle_index_map:
                normalized_list[angle_index_map[normalized]] = entry_dict
                changed = True
            else:
                angle_index_map[normalized] = len(normalized_list)
                normalized_list.append(entry_dict)

        if len(normalized_list) != len(measurements):
            changed = True

        normalized_list.sort(key=lambda item: item.get('angle', 0.0))

        if changed:
            test_payload['measurements'] = normalized_list

        return changed

    def _plot_dataset_on_axis(self, dataset):
        measurements = dataset.get('measurements', [])
        if not measurements:
            return

        final_entries = [entry for entry in measurements if not entry.get('is_progress', False)]
        progress_entries = [entry for entry in measurements if entry.get('is_progress', False)]

        final_entries.sort(key=lambda e: float(e.get('angle', 0.0)))
        progress_entries.sort(key=lambda e: float(e.get('angle', 0.0)))

        display_lookup = {}
        for entry in final_entries:
            angle_key = round(float(entry.get('angle', 0.0)), 3)
            display_lookup[angle_key] = entry.get('threshold_display') or (
                entry.get('threshold_raw') if isinstance(entry.get('threshold_raw'), str)
                else (f"{float(entry.get('threshold_raw') or 0):.1f} dBm" if entry.get('threshold_raw') is not None else "-")
            )

        red_angles, red_values = [], []
        green_angles, green_values = [], []
        normal_segments = []
        current_segment_angles, current_segment_values = [], []
        label_used = False

        step_threshold = dataset.get('step', 0.0)
        if step_threshold <= 0:
            step_threshold = None

        previous_angle = None

        for entry in final_entries:
            angle_deg = float(entry.get('angle', 0.0))
            category, value = self._normalize_threshold_value(entry.get('threshold_raw'))
            if value is None:
                continue
            angle_rad = np.deg2rad(angle_deg)
            display_text = display_lookup.get(round(angle_deg, 3)) or (
                f"{value:.1f} dBm" if isinstance(value, (int, float)) else str(value)
            )

            if category == 'normal':
                if previous_angle is not None and step_threshold:
                    if abs(angle_deg - previous_angle) > max(step_threshold * 1.5, 1.0):
                        if current_segment_angles:
                            normal_segments.append((current_segment_angles, current_segment_values))
                        current_segment_angles, current_segment_values = [], []

                current_segment_angles.append(angle_rad)
                current_segment_values.append(value)
                previous_angle = angle_deg
                self._register_tooltip_point(angle_rad, value, display_text)
            else:
                if current_segment_angles:
                    normal_segments.append((current_segment_angles, current_segment_values))
                    current_segment_angles, current_segment_values = [], []
                previous_angle = None

                if category == 'red':
                    red_angles.append(angle_rad)
                    red_values.append(value)
                    self._register_tooltip_point(angle_rad, value, display_text)
                elif category == 'green':
                    green_angles.append(angle_rad)
                    green_values.append(value)
                    self._register_tooltip_point(angle_rad, value, display_text)

        if current_segment_angles:
            normal_segments.append((current_segment_angles, current_segment_values))

        # Plota segmentos normais
        for angles, values in normal_segments:
            if not angles:
                continue
            line, = self.polar_ax.plot(
                angles,
                values,
                'o-',
                color=dataset['color'],
                linewidth=2 if not dataset.get('is_live') else 1.5,
                markersize=4,
                alpha=0.9,
                label=dataset['label'] if not label_used else None
            )
            label_used = True

        # Se não houve pontos normais, adiciona handle invisível para legenda
        if not label_used:
            self.polar_ax.plot([], [], color=dataset['color'], label=dataset['label'])

        if red_angles:
            self.polar_ax.plot(red_angles, red_values, 'o', color='red', markersize=4, label=None)

        if green_angles:
            self.polar_ax.plot(green_angles, green_values, 'o', color='green', markersize=4, label=None)

        if dataset.get('is_live') and progress_entries:
            progress_angles = []
            progress_values = []
            for entry in progress_entries:
                category, value = self._normalize_threshold_value(entry.get('threshold_raw'))
                if value is None or category != 'normal':
                    continue
                progress_angles.append(np.deg2rad(float(entry.get('angle', 0.0))))
                progress_values.append(value)

            if progress_angles:
                self.polar_ax.plot(
                    progress_angles,
                    progress_values,
                    'o--',
                    color=dataset['color'],
                    linewidth=1,
                    markersize=3,
                    alpha=0.5,
                    label=None
                )
                for ang_rad, val in zip(progress_angles, progress_values):
                    display_text = f"{val:.1f} dBm"
                    self._register_tooltip_point(ang_rad, val, display_text)

    def _normalize_threshold_value(self, threshold_raw):
        if threshold_raw is None:
            return None, None

        if isinstance(threshold_raw, (int, float)):
            value = float(threshold_raw)
            value = max(min(value, DEFAULT_MAX_POWER_DBM), DEFAULT_MIN_POWER_DBM)
            return 'normal', value

        if isinstance(threshold_raw, str):
            text = threshold_raw.strip().upper()
            if text.startswith('>'):
                return 'red', DEFAULT_MAX_POWER_DBM
            if text.startswith('<'):
                return 'green', DEFAULT_MIN_POWER_DBM
            try:
                value = float(text.replace(',', '.'))
                value = max(min(value, DEFAULT_MAX_POWER_DBM), DEFAULT_MIN_POWER_DBM)
                return 'normal', value
            except ValueError:
                return None, None

        return None, None

    # --- NOVA FUNÇÃO DE MEDIÇÃO OTIMIZADA COM ATUALIZAÇÃO EM TEMPO REAL ---
    def _run_threshold_scan_worker(self, freq_mhz: float, filter_epc_hex: str, current_angle: float) -> Optional[Union[float, str]]:
        """
        Executa a varredura de potência controlada (Threshold) para uma única frequência.
        Retorna o Threshold (float) ou string (">max"/"<min") ou None/string de erro.
        Atualiza o gráfico em tempo real durante a varredura.
        
        CORREÇÃO: Adicionado STOP_INVENTORY antes de iniciar (igual ao FastSurance)
        para garantir que não há inventário em andamento que possa interferir.
        """
        last_successful_power = None
        inventory_failures = 0
        port_acquired = False
        reader_open = False
        max_failures_before_abort = 12
        consecutive_failures_log_threshold = (4, 8)
        
        try:
            if self.port_manager:
                port_acquired = self.port_manager.acquire_port("FastOrientation_RF", timeout=3.0)
                if not port_acquired:
                    warning_msg = "RFID ocupado por outro módulo."
                    self.results_queue.put({'type': 'STATUS', 'text': warning_msg})
                    self._log(f"Falha ao adquirir porta RFID: {warning_msg}", "warning")
                    return None
            
            if rfid_sdk.UHF_RFID_Open(RFID_READER_COM_NUM, 115200) != 0:
                self.results_queue.put({'type': 'STATUS', 'text': f"Falha ao abrir COM{RFID_READER_COM_NUM} para medição."})
                self._log(f"Não foi possível abrir COM{RFID_READER_COM_NUM} para varredura threshold.", "error")
                return None
            reader_open = True

            output_buffer, output_len = ctypes.create_string_buffer(256), ctypes.c_uint(0)
            
            freq_data = b'\x01' + int(freq_mhz * 1000).to_bytes(3, 'big')
            rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_FREQ_TABLE, ctypes.c_char_p(freq_data), 4, output_buffer, ctypes.byref(output_len))

            power_val = int(DEFAULT_MAX_POWER_DBM * 100)
            power_data = bytes([0x00, 0x00]) + power_val.to_bytes(2, 'big') * 2
            rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_TXPOWER, ctypes.c_char_p(power_data), 6, output_buffer, ctypes.byref(output_len))

            start_power_for_run = DEFAULT_MAX_POWER_DBM
            min_power_limit = DEFAULT_MIN_POWER_DBM
            stop_value = int(min_power_limit * 10) - 5
            start_time = time.time()

            self._log(f"Iniciando varredura threshold em {freq_mhz:.1f} MHz @ ângulo {current_angle:.2f}°")
            
            for power_int_x10 in range(int(start_power_for_run * 10), stop_value, -5):
                if self.cancel_requested:
                    self._log("Varredura cancelada pelo usuário durante ajuste de potência.", "warning")
                    return "CANCELADO"
                    
                elapsed = time.time() - start_time
                if elapsed > HARD_TIMEOUT_S_PER_FREQ:
                    self._log(f"Timeout de {HARD_TIMEOUT_S_PER_FREQ}s atingido na varredura threshold.", "warning")
                    break 
                
                power_dbm = power_int_x10 / 10.0
                power_val = int(power_dbm * 100)
                power_data = bytes([0x00, 0x00]) + power_val.to_bytes(2, 'big') * 2
                rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_TXPOWER, ctypes.c_char_p(power_data), 6, output_buffer, ctypes.byref(output_len))
                
                time.sleep(0.03)
                
                inv_tag_input_data = bytes([0x00, 0x1E]) 
                ret = rfid_sdk.UHF_RFID_Set(RFID_CMD_INV_TAG, ctypes.c_char_p(inv_tag_input_data), 2, output_buffer, ctypes.byref(output_len))
                
                is_tag_read = False
                if ret == 0 and output_len.value > 5:
                    detected_epc = output_buffer.raw[2:output_len.value - 3].hex().upper()
                    if filter_epc_hex and filter_epc_hex == detected_epc:
                        is_tag_read = True

                if is_tag_read:
                    last_successful_power = power_dbm
                    inventory_failures = 0
                    self._log(f"EPC {filter_epc_hex} detectado em {power_dbm:.1f} dBm (ângulo {current_angle:.2f}°).")
                    self.results_queue.put({
                        'type': 'PROGRESS_UPDATE',
                        'angle': current_angle,
                        'threshold_raw': power_dbm,
                        'threshold_display': f"{power_dbm:.1f}",
                        'epc': filter_epc_hex if filter_epc_hex else "N/A",
                        'is_progress': True
                    })
                else: 
                    inventory_failures += 1
                    if inventory_failures in consecutive_failures_log_threshold:
                        self._log(f"EPC {filter_epc_hex} não detectado após {inventory_failures} tentativas na potência {power_dbm:.1f} dBm.", "warning")
                        try:
                            rfid_sdk.UHF_RFID_Set(RFID_CMD_STOP_INVENTORY, None, 0, output_buffer, ctypes.byref(output_len))
                        except Exception:
                            pass
                        time.sleep(0.1)
                    if inventory_failures >= max_failures_before_abort:
                        self._log(f"Falhas consecutivas ({inventory_failures}) excederam limite. Abortando varredura no ângulo {current_angle:.2f}°.", "error")
                        break
                    backoff = 0.05 if inventory_failures < 6 else 0.12
                    time.sleep(backoff)
                    continue
            
            if last_successful_power is None:
                self._log(f"Nenhuma leitura obtida. Registrando resultado como >{start_power_for_run:.1f} dBm.", "warning")
                return f">{start_power_for_run:.1f}" 
            elif last_successful_power <= min_power_limit:
                self._log(f"EPC manteve leitura até o limite inferior ({min_power_limit:.1f} dBm).", "info")
                return f"<{min_power_limit:.1f}" 
            else:
                return last_successful_power
                
        except Exception as e:
            self.results_queue.put({'type': 'STATUS', 'text': f"Erro na varredura Threshold: {e}"})
            return "ERRO/DLL"
        finally:
            try:
                if reader_open:
                    cleanup_buf, cleanup_len = ctypes.create_string_buffer(64), ctypes.c_uint(0)
                    try:
                        rfid_sdk.UHF_RFID_Set(RFID_CMD_STOP_INVENTORY, None, 0, cleanup_buf, ctypes.byref(cleanup_len))
                    except Exception:
                        pass
                    try:
                        rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_CW_STATUS, ctypes.c_char_p(b'\x00'), 1, cleanup_buf, ctypes.byref(cleanup_len))
                    except Exception:
                        pass
                    time.sleep(0.05)
            finally:
                if reader_open:
                    rfid_sdk.UHF_RFID_Close(RFID_READER_COM_NUM)
                if port_acquired and self.port_manager:
                    self.port_manager.release_port("FastOrientation_RF")

    # --- FUNÇÃO DE REGISTRO DE EPC USANDO CTYPES ---
    def _soft_reset_reader(self):
        """Executa Soft Reset no leitor (mesmo processo do módulo License)."""
        if not DLL_LOADED or not rfid_sdk:
            print("[EPC] soft_reset: DLL não carregada. Reset não executado.")
            return False

        port_acquired = False
        reader_open = False
        try:
            if self.port_manager:
                port_acquired = self.port_manager.acquire_port("FastOrientation_Reset", timeout=2.0)
                if not port_acquired:
                    print("[EPC] soft_reset: Porta ocupada por outro módulo.")
                    return False

            print(f"[EPC] soft_reset: Abrindo porta COM{RFID_READER_COM_NUM}...")
            if rfid_sdk.UHF_RFID_Open(RFID_READER_COM_NUM, 115200) != 0:
                print(f"[EPC] soft_reset: Falha ao abrir porta COM{RFID_READER_COM_NUM}.")
                return False
            reader_open = True
            
            out_buf = ctypes.create_string_buffer(16)
            out_len = ctypes.c_uint(0)
            status = rfid_sdk.UHF_RFID_Set(
                RFID_CMD_SET_SOFTRESET,
                None,
                0,
                out_buf,
                ctypes.byref(out_len)
            )
            
            if status == 0:
                print("[EPC] soft_reset: Reset executado com sucesso (bip).")
                time.sleep(0.2)
                return True
            else:
                print(f"[EPC] soft_reset: Falha ao executar reset - status {status}")
                return False
        except Exception as exc:
            print(f"[EPC] soft_reset: Exceção durante reset: {exc}")
            return False
        finally:
            if reader_open:
                try:
                    cleanup_buf, cleanup_len = ctypes.create_string_buffer(64), ctypes.c_uint(0)
                    try:
                        rfid_sdk.UHF_RFID_Set(RFID_CMD_STOP_INVENTORY, None, 0, cleanup_buf, ctypes.byref(cleanup_len))
                    except Exception:
                        pass
                    try:
                        rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_CW_STATUS, ctypes.c_char_p(b'\x00'), 1, cleanup_buf, ctypes.byref(cleanup_len))
                    except Exception:
                        pass
                    time.sleep(0.05)
                except Exception:
                    pass
                rfid_sdk.UHF_RFID_Close(RFID_READER_COM_NUM)
                print("[EPC] soft_reset: Porta fechada.")
            if port_acquired and self.port_manager:
                self.port_manager.release_port("FastOrientation_Reset")

    def _start_register_epc(self):
        """Dispara o registro de EPC em thread separada para não travar a UI."""
        if not DLL_LOADED:
            messagebox.showerror(
                self._trf("RFID"),
                self._trf("DLL RFID não carregada."),
            )
            return

        if self.epc_registering:
            messagebox.showinfo(
                self._trf("RFID"),
                self._trf("O registro de EPC já está em andamento. Aguarde."),
            )
            return

        self.epc_registering = True
        self.capture_epc_button.config(state="disabled")
        self.status_label.config(text=f"Abrindo RFID na COM{RFID_READER_COM_NUM} para registrar EPC...", foreground="blue")

        worker = threading.Thread(target=self._register_epc_worker, daemon=True)
        worker.start()

        # Fallback: se algo travar, reativa o botão após timeout
        self.after(12000, self._check_epc_worker_timeout)

    def _check_epc_worker_timeout(self):
        if self.epc_registering:
            print("⚠️ Timeout aguardando registro de EPC. Reativando botão.")
            self.epc_registering = False
            self.capture_epc_button.config(state="normal")
            self.results_queue.put({
                'type': 'STATUS',
                'text': "Timeout no registro de EPC. Verifique o leitor e tente novamente.",
                'foreground': "orange"
            })

    def _register_epc_worker(self):
        result = {
            'type': 'EPC_RESULT',
            'success': False,
            'epc': None,
            'message': ""
        }

        if not DLL_LOADED or rfid_sdk is None:
            result['message'] = "DLL RFID não carregada."
            self.results_queue.put(result)
            self.results_queue.put({'type': 'EPC_FINISH'})
            return

        output_buffer = ctypes.create_string_buffer(256)
        output_len = ctypes.c_uint(0)
        rfid_opened = False
        port_acquired = False
        
        try:
            if self.port_manager:
                port_acquired = self.port_manager.acquire_port("FastOrientation_EPC", timeout=3.0)
                if not port_acquired:
                    result['message'] = "Leitor ocupado por outro módulo."
                    self.results_queue.put(result)
                    self.results_queue.put({'type': 'EPC_FINISH'})
                    return
            
            print("[EPC] Tentando abrir leitor na COM", RFID_READER_COM_NUM)
            if rfid_sdk.UHF_RFID_Open(RFID_READER_COM_NUM, 115200) != 0:
                result['message'] = f"Falha ao abrir COM{RFID_READER_COM_NUM}."
                print("[EPC] Falha ao abrir porta COM")
                return
            
            rfid_opened = True
            
            # Garante que não há inventário pendente
            try:
                print("[EPC] Enviando STOP_INVENTORY")
                rfid_sdk.UHF_RFID_Set(RFID_CMD_STOP_INVENTORY, None, 0, output_buffer, ctypes.byref(output_len))
                time.sleep(0.05)
            except Exception:
                pass
            
            # Configura potência e frequência padrão
            power_val = int(15.0 * 100)
            power_data = bytes([0x00, 0x00]) + power_val.to_bytes(2, 'big') * 2
            rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_TXPOWER, ctypes.c_char_p(power_data), 6, output_buffer, ctypes.byref(output_len))
            
            freq_data = b'\x01' + int(915.0 * 1000).to_bytes(3, 'big')
            rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_FREQ_TABLE, ctypes.c_char_p(freq_data), 4, output_buffer, ctypes.byref(output_len))
            
            found_epc = None
            for _ in range(40):
                inv_tag_input_data = bytes([0x00, 0x1E])
                ret = rfid_sdk.UHF_RFID_Set(RFID_CMD_INV_TAG, ctypes.c_char_p(inv_tag_input_data), 2, output_buffer, ctypes.byref(output_len))
                print(f"[EPC] Inventário retorno={ret}, len={output_len.value}")
                if ret == 0 and output_len.value > 5:
                    found_epc = output_buffer.raw[2:output_len.value - 3].hex().upper()
                    if found_epc:
                        print(f"[EPC] EPC encontrado: {found_epc}")
                        break
                time.sleep(0.05)
            
            if found_epc:
                result['success'] = True
                result['epc'] = found_epc
                result['message'] = "EPC registrado para filtro de threshold."
            else:
                result['message'] = "Nenhum EPC detectado no registro. Aproximar a tag e tentar novamente."
                print("[EPC] Nenhum EPC detectado durante as tentativas.")
        
        except Exception as exc:
            result['message'] = f"Erro ao registrar EPC: {exc}"
            print(f"[EPC] Exceção: {exc}")
        
        finally:
            try:
                if rfid_opened:
                    cleanup_buf, cleanup_len = ctypes.create_string_buffer(64), ctypes.c_uint(0)
                    try:
                        rfid_sdk.UHF_RFID_Set(RFID_CMD_STOP_INVENTORY, None, 0, cleanup_buf, ctypes.byref(cleanup_len))
                    except Exception:
                        pass
                    try:
                        rfid_sdk.UHF_RFID_Set(RFID_CMD_SET_CW_STATUS, ctypes.c_char_p(b'\x00'), 1, cleanup_buf, ctypes.byref(cleanup_len))
                    except Exception:
                        pass
                    time.sleep(0.05)
                    rfid_sdk.UHF_RFID_Close(RFID_READER_COM_NUM)
                    print("[EPC] Porta COM fechada.")
            except Exception:
                pass
            finally:
                if port_acquired and self.port_manager:
                    self.port_manager.release_port("FastOrientation_EPC")
        
            self.results_queue.put(result)
            self.results_queue.put({'type': 'EPC_FINISH'})


    # --- FUNÇÃO RESTAURADA ---
    def start_test(self):
        """Valida entradas antes de iniciar o teste."""
        if self.test_running: return
        
        try:
            test_name = self.test_name_var.get().strip()
            if not test_name:
                messagebox.showerror(
                    self._trf("Erro de Entrada"),
                    self._trf("Informe o Nome do Teste antes de iniciar."),
                )
                return
            # Verifica duplicidade de nome de teste (case-insensitive)
            existing_names = {
                (test.get('test_name') or '').strip().lower()
                for test in self.historical_tests.values()
            }
            if test_name.lower() in existing_names:
                messagebox.showerror(
                    self._trf("Erro de Entrada"),
                    self._trf("O nome de teste '{test_name}' já existe. Escolha outro nome.", test_name=test_name),
                )
                return

            start_angle = self.start_angle_var.get()
            end_angle = self.end_angle_var.get()
            step_angle = self.step_angle_var.get()

            if start_angle >= end_angle:
                 messagebox.showerror(
                     self._trf("Erro de Entrada"),
                     self._trf("Ângulo inicial deve ser menor que o final."),
                 )
                 return
            if step_angle <= 0.0:
                 messagebox.showerror(
                     self._trf("Erro de Entrada"),
                     self._trf("O Step deve ser maior que 0."),
                 )
                 return
            
            if self.start_button.cget('state') == 'disabled':
                messagebox.showerror(
                    self._trf("Erro de Hardware"),
                    self._trf("O hardware não está pronto. Verifique as conexões."),
                )
                return

            # Reseta dados do teste atual
            self.test_history = []
            self.current_test_metadata = {
                'test_name': test_name,
                'epc': self.selected_epc_var.get().strip(),
                'frequency_mhz': self.freq_var.get(),
                'start_angle': start_angle,
                'end_angle': end_angle,
                'step_angle': step_angle
            }

            # Limpa o gráfico polar (mantém histórico salvo)
            self.update_polar_plot()

            self.ignore_live_updates = False
            if self.app_shell:
                self.app_shell.set_test_running(True, "Fast Orientation")

            self._start_test_thread()
            self._schedule_state_save()

        except ValueError as e:
            if self.app_shell:
                self.app_shell.set_test_running(False, "Fast Orientation")
            messagebox.showerror(
                self._trf("Erro de Entrada"),
                self._trf("Verifique os valores de entrada. {error}", error=e),
            )
        except Exception as e:
            if self.app_shell:
                self.app_shell.set_test_running(False, "Fast Orientation")
            messagebox.showerror(
                self._trf("Erro"),
                self._trf("Ocorreu um erro inesperado: {error}", error=e),
            )

    # --- FUNÇÕES DE COMUNICAÇÃO (Arduino) ---
    def _calcular_passos(self, graus):
        """
        Converte o ângulo desejado para passos usando round() para precisão.
        
        CORREÇÃO: Usa round() em vez de ceil() para evitar acúmulo sistemático.
        O rastreamento da posição real (baseado no movimento executado) compensa
        pequenos erros de arredondamento sem causar acúmulo sistemático.
        """
        # Usa valor calibrado se disponível, senão usa o padrão
        passos_per_rev = getattr(self, 'passos_per_revolucao_calibrado', PASSOS_PER_REVOLUCAO)
        
        # Calcula passos exatos e arredonda para o mais próximo
        # round() evita acúmulo sistemático (ceil sempre ultrapassa, floor sempre falta)
        passos = round((graus / 360.0) * passos_per_rev)
        return int(passos)
    
    def _get_passos_per_revolucao(self):
        """Retorna o valor de passos por revolução (calibrado ou padrão)."""
        return getattr(self, 'passos_per_revolucao_calibrado', PASSOS_PER_REVOLUCAO)
    
    
    def _criar_tabela_passos(self):
        """
        Cria uma tabela de passos para ângulos principais.
        Retorna um dicionário {ângulo: número_de_passos}
        """
        passos_per_rev = self._get_passos_per_revolucao()
        tabela = {}
        
        # Gera tabela para ângulos de -360° a +720° em incrementos de 5°
        # Isso cobre posições negativas, uma volta completa e 2 voltas à frente
        for angulo in range(-360, 721, 5):
            passos = round((angulo / 360.0) * passos_per_rev)
            tabela[angulo] = passos
        
        # Garante entradas exatas para 360° e -360° (caso arredondamento arredonde diferente)
        tabela[360] = passos_per_rev
        tabela[-360] = -passos_per_rev
 
        print(f"📊 Tabela de passos criada: {len(tabela)} entradas")
        print(f"   Exemplos: -90°={tabela[-90]} passos, 0°={tabela[0]} passos, 90°={tabela[90]} passos, 360°={tabela[360]} passos")
        return tabela
    
    def _obter_passos_para_angulo(self, angulo_graus):
        """
        Obtém o número de passos necessário para um ângulo específico.
        Se o ângulo estiver na tabela, usa o valor da tabela.
        Caso contrário, calcula e arredonda.
        """
        passos_per_rev = self._get_passos_per_revolucao()

        # Se o ângulo for muito próximo de um inteiro (ex.: -15.0000), usa a tabela
        angulo_arredondado = int(round(angulo_graus))
        if abs(angulo_graus - angulo_arredondado) < 1e-6 and angulo_arredondado in self.tabela_passos_por_angulo:
            return self.tabela_passos_por_angulo[angulo_arredondado]

        # Caso contrário, calcula diretamente (suporta ângulos negativos ou maiores que 360°)
        passos = round((angulo_graus / 360.0) * passos_per_rev)
        return int(passos)
    
    def test_angles(self):
        """Testa os ângulos principais (90°, 180°, 270°, 360°) para verificar se estão corretos."""
        if self.test_running:
            messagebox.showwarning(
                self._trf("Teste em Andamento"),
                self._trf("Aguarde o teste atual terminar."),
            )
            return
        
        resposta = messagebox.askyesno(
            self._trf("Teste de Ângulos"),
            self._trf(
                "Este teste moverá a mesa para os ângulos: 0°, 90°, 180°, 270° e 360°.\n\nVerifique visualmente se os ângulos estão corretos.\n\nDeseja continuar?"
            ),
            parent=self.master
        )
        
        if not resposta:
            return
        
        # Inicia teste em thread separada
        self.start_button.config(state="disabled")
        self.test_running = True
        self.cancel_requested = False
        
        worker_thread = threading.Thread(target=self._test_angles_worker, daemon=True)
        worker_thread.start()
    
    def _test_angles_worker(self):
        """Worker thread para testar os ângulos principais."""
        try:
            angulos_teste = [0, 90, 180, 270, 360]
            
            # Reset inicial
            self.results_queue.put({'type': 'STATUS', 'text_key': "Resetando posição..."})
            rel_move_to_zero = -self.current_position
            movimento_real_reset = self._mover_mesa(rel_move_to_zero)
            if movimento_real_reset is False:
                self.results_queue.put({'type': 'ERROR', 'msg': "Falha ao resetar para 0°."})
                return
            self.current_position += movimento_real_reset
            if abs(self.current_position) < 0.1:
                self.current_position = 0.0
            
            # Testa cada ângulo
            for angulo_alvo in angulos_teste:
                if self.cancel_requested:
                    break
                
                self.results_queue.put({'type': 'STATUS', 'text_key': "Movendo para próximo ponto..."})
                
                # Calcula movimento necessário
                movimento_necessario = angulo_alvo - self.current_position
                passos_calculados = self._calcular_passos(movimento_necessario)
                passos_per_rev = self._get_passos_per_revolucao()
                graus_esperados = (passos_calculados / passos_per_rev) * 360.0
                
                print(f"TESTE ÂNGULO: Alvo={angulo_alvo}°, Posição atual={self.current_position:.3f}°, "
                      f"Movimento necessário={movimento_necessario:.3f}°, "
                      f"Passos={passos_calculados}, Graus reais={graus_esperados:.3f}°")
                
                movimento_real = self._mover_mesa(movimento_necessario)
                if movimento_real is False:
                    self.results_queue.put({'type': 'ERROR', 'msg': f"Falha ao mover para {angulo_alvo}°."})
                    break
                
                self.current_position += movimento_real
                posicao_final = self.current_position
                erro = abs(angulo_alvo - posicao_final)
                
                print(f"TESTE ÂNGULO: Chegou em {posicao_final:.3f}° (alvo: {angulo_alvo}°, erro: {erro:.3f}°)")
                self.results_queue.put({'type': 'STATUS', 'text_key': "Movimento concluído."})
                
                # Pausa para verificação visual
                time.sleep(2)
            
            # Retorna para 0°
            self.results_queue.put({'type': 'STATUS', 'text_key': "Retornando à referência..."})
            rel_move_final = -self.current_position
            movimento_real_final = self._mover_mesa(rel_move_final)
            if movimento_real_final is not False:
                self.current_position += movimento_real_final
                if abs(self.current_position) < 0.1:
                    self.current_position = 0.0
            
            self.results_queue.put({'type': 'STATUS', 'text_key': "Teste de ângulos concluído."})
            self.results_queue.put({'type': 'FINISH', 'status': 'Success'})
            
        except Exception as e:
            self.results_queue.put({'type': 'ERROR', 'msg': f"Erro no teste de ângulos: {e}"})
        finally:
            self._liberar_motor()
            self.test_running = False
            self.start_button.config(state="normal")

    def _liberar_motor(self):
        """Libera o motor do Arduino quando não estiver em uso."""
        try:
            arduino = serial.Serial(
                port=ARDUINO_PORT, baudrate=ARDUINO_BAUD, timeout=2, rtscts=False, dsrdtr=False
            )
            arduino.setDTR(False); time.sleep(0.02); arduino.setDTR(True); time.sleep(0.5)
            arduino.reset_input_buffer(); arduino.reset_output_buffer(); time.sleep(0.1)
            
            # Tenta enviar comando "RELEASE" para liberar o motor
            # Se o Arduino não reconhecer, tenta comando "0" seguido de "RELEASE"
            comandos_release = ["RELEASE\r\n", "0\r\nRELEASE\r\n"]
            
            for comando in comandos_release:
                try:
                    arduino.write(comando.encode('utf-8'))
                    time.sleep(0.2)
                    # Limpa qualquer resposta
                    if arduino.in_waiting > 0:
                        arduino.read(arduino.in_waiting)
                except Exception:
                    pass
            
            arduino.close()
            print(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] Motor liberado (solto)")
        except Exception as e:
            # Não exibe erro se não conseguir liberar - apenas loga
            print(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] Aviso: Não foi possível liberar motor: {e}")

    def _mover_mesa_para_angulo_absoluto(self, angulo_desejado):
        """
        NOVO: Move a mesa para um ângulo absoluto usando o sistema de tabela de passos.
        Calcula a diferença entre o ângulo desejado e a posição atual (em passos),
        executa essa diferença e atualiza o contador absoluto.
        
        Args:
            angulo_desejado: Ângulo absoluto desejado (0-360)
            
        Returns:
            True se sucesso, False se falha
        """
        if self.cancel_requested:
            return False
        
        passos_necessarios = self._obter_passos_para_angulo(angulo_desejado)
        passos_a_executar = passos_necessarios - self.contador_passos_absoluto
        
        print(f"🎯 MOVER PARA ÂNGULO ABSOLUTO: {angulo_desejado}°")
        print(f"   Passos necessários (tabela): {passos_necessarios}")
        print(f"   Contador atual: {self.contador_passos_absoluto}")
        print(f"   Passos a executar: {passos_a_executar}")
        
        # Se não precisa mover, retorna sucesso
        if passos_a_executar == 0:
            print(f"   ✅ Já está na posição correta")
            return True
        
        # Executa o movimento relativo
        sucesso = self._executar_passos(passos_a_executar)
        
        if sucesso:
            self.contador_passos_absoluto += passos_a_executar
            
            passos_per_rev = self._get_passos_per_revolucao()
            self.current_position = (self.contador_passos_absoluto / passos_per_rev) * 360.0
            
            print(f"   ✅ Movimento concluído. Contador atualizado: {self.contador_passos_absoluto} passos ({self.current_position:.3f}°)")
            print(f"   📊 Verificação: Passos executados: {passos_a_executar}, Contador: {self.contador_passos_absoluto}, Posição: {self.current_position:.3f}°")
            tolerancia = max(0.5, abs(self.step_angle_var.get()) * 0.5 if hasattr(self, 'step_angle_var') else 0.5)
            delta = abs(self.current_position - angulo_desejado)
            if delta > tolerancia:
                aviso = (
                    f"Desalinhamento após movimento absoluto. Esperado {angulo_desejado:.2f}°, "
                    f"obtido {self.current_position:.2f}° (Δ={delta:.2f}°)."
                )
                self._log(aviso, "warning")
            else:
                self._log(
                    f"Movimento absoluto concluído com alinhamento dentro da tolerância ({delta:.2f}° <= {tolerancia:.2f}°)."
                )
        else:
            self._log(f"Falha ao executar movimento para {angulo_desejado:.2f}°.", "error")
        
        return sucesso
    
    def _executar_passos(self, passos):
        """
        Executa um número de passos no Arduino.
        Esta é a função de baixo nível que realmente envia o comando.
        """
        if self.cancel_requested:
            return False
        
        base_timeout = 2.0 + 0.0065 * abs(passos)
        TIMEOUT_DYNAMIC = base_timeout if base_timeout > 6.0 else 6.0
        
        try:
            arduino = serial.Serial(
                port=ARDUINO_PORT, baudrate=ARDUINO_BAUD, timeout=3, rtscts=False, dsrdtr=False
            )
            arduino.setDTR(False); time.sleep(0.02); arduino.setDTR(True); time.sleep(1.5)
            arduino.reset_input_buffer(); arduino.reset_output_buffer(); time.sleep(0.1)
            while arduino.in_waiting > 0:
                try: _ = arduino.readline()
                except Exception: break

            comando = str(passos) + '\r\n'
            print(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] ENVIANDO COMANDO: {repr(comando)} | Passos: {passos}")
            
            arduino.write(comando.encode('utf-8'))
            arduino.flush()
            self.results_queue.put({'type': 'STATUS', 'text_key': "Executando movimento do motor..."})

            start_time = time.time(); TIMEOUT = TIMEOUT_DYNAMIC; full_buffer = ""; cancel_during_motion = False
            while time.time() - start_time < TIMEOUT:
                if self.cancel_requested:
                    cancel_during_motion = True
                if arduino.in_waiting > 0:
                    raw_data = arduino.read(arduino.in_waiting)
                    full_buffer += raw_data.decode('utf-8', errors='ignore')
                    if "PRONTO" in full_buffer:
                        print(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] RECEBIDO PRONTO!")
                        arduino.close()
                        self._liberar_motor()
                        if cancel_during_motion:
                            self._cancelled_run = True
                        return True
                time.sleep(0.01)
            
            print(f"⚠️ Timeout ao executar {passos} passos")
            arduino.close()
            self._liberar_motor()
            return False
            
        except Exception as e:
            print(f"❌ Erro ao executar passos: {e}")
            return False
    
    def _mover_mesa(self, graus_relativos):
        """
        Abre, envia comando, espera o tempo de movimento, e lê o PRONTO.
        Retorna o movimento REAL em graus (baseado nos passos executados) ou False em caso de falha.
        Isso garante que a posição seja rastreada com precisão, evitando erros acumulativos.
        
        NOTA: Esta função ainda é usada para movimentos relativos.
        Para movimentos absolutos, use _mover_mesa_para_angulo_absoluto().
        """
        if self.cancel_requested:
            return False
            
        passos = self._calcular_passos(graus_relativos)
        # CORREÇÃO: Calcula o movimento REAL baseado nos passos que serão executados
        # Isso evita erros acumulativos devido ao arredondamento
        passos_per_rev = self._get_passos_per_revolucao()
        graus_reais = (passos / passos_per_rev) * 360.0
        
        # Executa os passos
        sucesso = self._executar_passos(passos)
        
        if sucesso:
            # Atualiza contador absoluto
            self.contador_passos_absoluto += passos
            # Atualiza current_position em graus
            self.current_position = (self.contador_passos_absoluto / passos_per_rev) * 360.0
            
            return graus_reais
        else:
            return False
    
    def _resetar_posicao_zero(self):
        """
        NOVO: Reseta a posição para zero usando o sistema de tabela de passos.
        Move para 0° e zera o contador absoluto.
        
        CORREÇÃO CRÍTICA: Se há erro visual de 7°, isso corresponde a ~40 passos.
        Vamos fazer um reset mais robusto que verifica e corrige o erro.
        """
        print(f"🔄 RESETANDO PARA ZERO")
        print(f"   Contador atual: {self.contador_passos_absoluto} passos")
        passos_per_rev = self._get_passos_per_revolucao()
        angulo_atual_estimado = (self.contador_passos_absoluto / passos_per_rev) * 360.0
        print(f"   Ângulo atual estimado: {angulo_atual_estimado:.3f}°")

        # Se já está próximo de 0° ou 360°, apenas normaliza sem mover
        if abs(angulo_atual_estimado) <= 1.0 or abs(angulo_atual_estimado - 360.0) <= 1.0 or abs(angulo_atual_estimado + 360.0) <= 1.0:
            self.contador_passos_absoluto = 0
            self.current_position = 0.0
            print("   ✅ Dentro da tolerância. Nenhum movimento necessário.")
            return True

        resto = self.contador_passos_absoluto % passos_per_rev
        if resto > passos_per_rev / 2:
            resto -= passos_per_rev
        elif resto < -passos_per_rev / 2:
            resto += passos_per_rev

        passos_para_zero = -resto
        passos_para_zero_int = int(round(passos_para_zero))
        print(f"   Passos necessários para voltar a 0° (menor caminho): {passos_para_zero_int}")

        if abs(passos_para_zero_int) <= 1:
            sucesso = True
        else:
            sucesso = self._executar_passos(passos_para_zero_int)

        if sucesso:
            self.contador_passos_absoluto += passos_para_zero_int
            if abs(self.contador_passos_absoluto) <= 1:
                self.contador_passos_absoluto = 0
            self.current_position = (self.contador_passos_absoluto / passos_per_rev) * 360.0
            print(f"   ✅ Reset concluído. Contador zerado: {self.contador_passos_absoluto} passos")
            print(f"   ✅ Posição atual: {self.current_position:.3f}°")
        else:
            print(f"   ❌ Falha ao resetar para zero")

        return sucesso

    def _retornar_para_posicao_de_referencia(self):
        """
        Retorna a mesa para a posição equivalente a 0° pelo menor caminho possível.
        Implementação simples: volta diretamente para 0° (mesmo caminho do reset).
        """
        return self._resetar_posicao_zero()

    # --- WORKFLOW DE TESTE INTEGRADO (COM REPARO NO ENVIO DE DADOS) ---
    def _test_workflow_integrated(self):
        """Workflow de teste adaptado do FastSurance, com varredura rápida de threshold."""
        
        freq = self.freq_var.get()
        start_angle = self.start_angle_var.get()
        end_angle = self.end_angle_var.get()
        step_angle = self.step_angle_var.get()
        manual_mode = self.mode_var.get() == "manual"
        
        current_angle = self.current_position
        success = True
        self._log(
            f"Iniciando teste: freq={freq:.1f} MHz, start={start_angle:.2f}°, end={end_angle:.2f}°, step={step_angle:.2f}°, modo={'manual' if manual_mode else 'auto'}"
        )

        try:
            if manual_mode:
                self.contador_passos_absoluto = 0
                self.current_position = start_angle
                self.manual_step_event.clear()
                self.manual_waiting_confirm = True
                self.results_queue.put({
                    'type': 'MANUAL_PROMPT',
                    'text_key': "manual.prompt.initial_angle",
                    'text_params': {'angle': start_angle},
                })
                self._log(f"Aguardando confirmação manual para ângulo inicial {start_angle:.2f}°.")
                while not self.manual_step_event.wait(0.1):
                    if self.cancel_requested:
                        success = False
                        break
                if not success or self.cancel_requested:
                    return
                current_angle = start_angle
            else:
                sucesso_reset = self._resetar_posicao_zero()
                if not sucesso_reset:
                    self.results_queue.put({'type': 'ERROR', 'msg': f"Falha ao resetar para 0°. Verifique a {ARDUINO_PORT}."})
                    self._log("Falha no reset inicial para 0°. Teste abortado.", "error")
                    return
            
            # 2. Move para o ângulo inicial desejado - NOVO: Usa sistema de tabela de passos
            if start_angle != 0.0 and not manual_mode:
                self.results_queue.put({'type': 'STATUS', 'text_key': "Posicionando no início..."})
                sucesso_init = self._mover_mesa_para_angulo_absoluto(start_angle)
                if not sucesso_init:
                    self.results_queue.put({'type': 'ERROR', 'msg': f"Falha ao mover para posição inicial. Verifique a {ARDUINO_PORT}."})
                    self._log(f"Falha ao posicionar ângulo inicial {start_angle:.2f}°. Teste abortado.", "error")
                    return
                if abs(self.current_position - start_angle) > max(0.5, step_angle * 0.5):
                    mensagem = (
                        f"Desalinhamento detectado no ângulo inicial. "
                        f"Esperado {start_angle:.2f}°, atual {self.current_position:.2f}°."
                    )
                    self.results_queue.put({'type': 'ERROR', 'msg': mensagem})
                    self._log(mensagem, "error")
                    return
            
            current_angle = self.current_position
            
            # 3. Loop de Medição
            # CORREÇÃO: Sempre mede no ângulo atual ANTES de verificar se deve parar
            # Isso garante que meça até end_angle inclusive (ex: 360°)
            iteracao = 0
            max_iteracoes = int((end_angle - start_angle) / step_angle) + 10  # Limite de segurança
            print(f"DEBUG LOOP: Iniciando loop - start: {start_angle}°, end: {end_angle}°, step: {step_angle}°, max_iterações: {max_iteracoes}")
            
            while success and not self.cancel_requested:
                iteracao += 1
                print(f"DEBUG LOOP: Iteração {iteracao} - current_angle: {current_angle:.3f}°, end_angle: {end_angle:.3f}°")
                
                # PROTEÇÃO: Limite máximo de iterações para evitar loop infinito
                if iteracao > max_iteracoes:
                    print(f"DEBUG LOOP: ⚠️ ATINGIU LIMITE DE ITERAÇÕES ({max_iteracoes}). FORÇANDO PARADA.")
                    self.results_queue.put({'type': 'ERROR', 'msg': f"Loop atingiu limite de {max_iteracoes} iterações. Parando por segurança."})
                    break
                
                # CORREÇÃO CRÍTICA: Verifica se passou muito do end_angle (tolerância maior)
                # Mas permite medir se está dentro da faixa do end_angle
                if current_angle > end_angle + 0.5:  # Tolerância maior (0.5°) para permitir medição no end_angle
                    print(f"DEBUG POSIÇÃO: Loop finalizado - current_angle ({current_angle:.3f}°) muito além de end_angle ({end_angle:.3f}°)")
                    break
                
                # CORREÇÃO: Verifica ANTES de medir se já passou muito do end_angle
                # MAS permite medir se está no end_angle ou próximo (até 0.2° além)
                distancia_ate_end_inicial = end_angle - current_angle
                if current_angle > end_angle + 0.2:  # Só para se passou muito além (mais de 0.2°)
                    print(f"DEBUG POSIÇÃO: ✅ Passou muito além do end_angle ({end_angle:.3f}°). Current: {current_angle:.3f}°. PARANDO.")
                    break
                # Se está no end_angle ou próximo, mede primeiro e depois verifica se deve parar
                
                # SEMPRE mede no ângulo atual, mesmo que seja o end_angle
                threshold_result = None
                self.results_queue.put({'type': 'STATUS', 'text_key': "Iniciando varredura rápida..."})
                
                filter_epc = self.selected_epc_var.get().strip() or None
                self._log(f"Iniciando varredura no ângulo {current_angle:.2f}° (EPC alvo: {filter_epc or 'N/A'}).")
                if not filter_epc:
                    self.results_queue.put({'type': 'ERROR', 'msg': "EPC alvo não registrado. Não é possível realizar o teste de Threshold."})
                    success = False
                    break
                    
                threshold_result = self._run_threshold_scan_worker(freq, filter_epc, current_angle)
                
                
                # 2.4. Processamento (REPARADO PARA SEPARAR DADOS BRUTOS E DE DISPLAY)
                filter_epc_log = self.selected_epc_var.get().strip() or None 
                epc_to_log = "N/A"
                
                threshold_display_val = "ERRO"
                threshold_raw_val = None # O valor a ser salvo no histórico
                
                if threshold_result is None or threshold_result == "ERRO/DLL":
                    threshold_display_val = "ERRO COM"
                    epc_to_log = "FALHA/COM"
                    threshold_raw_val = "ERRO COM" # Salva a string de erro
                elif isinstance(threshold_result, float):
                    threshold_display_val = f"{threshold_result:.1f}" # String para a GUI
                    epc_to_log = filter_epc_log
                    threshold_raw_val = threshold_result # Salva o FLOAT
                elif isinstance(threshold_result, str) and threshold_result.startswith('>'):
                    threshold_display_val = threshold_result
                    epc_to_log = "NÃO DETECTADA"
                    threshold_raw_val = threshold_result # Salva a string ">25.0"
                elif isinstance(threshold_result, str) and threshold_result.startswith('<'):
                    threshold_display_val = threshold_result
                    epc_to_log = filter_epc_log
                    threshold_raw_val = threshold_result # Salva a string "<5.0"
                elif threshold_result == "CANCELADO":
                    success = False
                    break
                else:
                    threshold_display_val = str(threshold_result)
                    epc_to_log = "ERRO"
                    threshold_raw_val = str(threshold_result)

                # Adiciona o resultado final (será processado pela GUI que removerá progresso temporário)
                self.results_queue.put({
                    'type': 'RESULT',
                    'angle': current_angle,
                    'threshold_raw': threshold_raw_val, # O valor bruto (float ou string de erro)
                    'threshold_display': threshold_display_val, # O valor formatado para a tabela
                    'epc': epc_to_log,
                    'timestamp': datetime.now().strftime('%H:%M:%S'),
                    'is_progress': False  # Resultado final, não é progresso
                })

                # 2.5. Verifica Cancelamento
                if self.cancel_requested:
                    break
                
                # 2.6. Verifica se deve continuar DEPOIS de medir
                # CORREÇÃO: Verifica se já mediu no end_angle ou passou (com tolerância)
                distancia_ate_end = end_angle - current_angle
                
                print(f"DEBUG POSIÇÃO: Após medição - current_angle: {current_angle:.3f}°, end_angle: {end_angle:.3f}°, distância: {distancia_ate_end:.3f}°")
                self._log(
                    f"Resultado ângulo {current_angle:.2f}° -> {threshold_display_val} dBm (raw={threshold_raw_val}). Distância até final: {distancia_ate_end:.3f}°"
                )
                
                # CORREÇÃO CRÍTICA: Se já mediu no end_angle (dentro de tolerância de 0.3°), PARA
                # Tolerância menor (0.3°) para garantir que meça no end_angle mas pare logo após
                if distancia_ate_end <= 0.3:  # Tolerância de 0.3° para considerar que já mediu no end_angle
                    print(f"DEBUG POSIÇÃO: ✅ Medição completa - current_angle ({current_angle:.3f}°) está no end_angle ({end_angle:.3f}°) ou próximo. PARANDO.")
                    break
                
                # CORREÇÃO: Se passou do end_angle, também para
                if current_angle > end_angle + 0.3:  # Tolerância de 0.3°
                    print(f"DEBUG POSIÇÃO: ✅ Passou do end_angle - current_angle ({current_angle:.3f}°) > end_angle ({end_angle:.3f}°). PARANDO.")
                    break
                
                # CORREÇÃO ADICIONAL: Se end_angle é 360° e current_angle está próximo, verifica se já mediu
                # Se end_angle é 360° e está entre 359.5° e 360.5°, considera que já mediu e para
                if end_angle >= 360.0 and current_angle >= 359.5 and current_angle <= 360.5:
                    print(f"DEBUG POSIÇÃO: ✅ Chegou a 360° (end_angle). Já mediu. PARANDO.")
                    break
                
                # CORREÇÃO: Se está próximo do end_angle (menos de 30% do step), move o restante exato
                # MAS só se ainda não mediu no end_angle (distância > 0.3°)
                if manual_mode:
                    proximo_angulo = current_angle + step_angle
                    if proximo_angulo > end_angle:
                        proximo_angulo = end_angle
                    if proximo_angulo > current_angle and not self.cancel_requested:
                        self.manual_step_event.clear()
                        self.manual_waiting_confirm = True
                        self.results_queue.put({
                            'type': 'MANUAL_PROMPT',
                            'text_key': "manual.prompt.next_angle",
                            'text_params': {'angle': proximo_angulo},
                        })
                        while not self.manual_step_event.wait(0.1):
                            if self.cancel_requested:
                                success = False
                                break
                        if not success or self.cancel_requested:
                            break
                        self.current_position = proximo_angulo
                        current_angle = proximo_angulo
                    continue
                elif distancia_ate_end < step_angle * 0.3 and distancia_ate_end > 0.3:  # Está muito próximo do end_angle mas ainda não mediu
                    # NOVO: Usa sistema de tabela para mover exatamente para end_angle
                    print(f"DEBUG POSIÇÃO: Próximo ao end_angle. Movendo para {end_angle:.3f}° usando sistema de tabela")
                    
                    sucesso_final = self._mover_mesa_para_angulo_absoluto(end_angle)
                    if not sucesso_final:
                        self.results_queue.put({'type': 'ERROR', 'msg': "Falha ao mover para end_angle."})
                        success = False
                        break
                    
                    current_angle = self.current_position
                    print(f"DEBUG POSIÇÃO: Chegou ao end_angle - posição: {self.current_position:.3f}°")
                    
                    # Continua o loop para medir no end_angle na próxima iteração
                    continue
                elif distancia_ate_end <= 0.3:
                    # Já está no end_angle ou muito próximo - já mediu, então para
                    print(f"DEBUG POSIÇÃO: ✅ Já mediu no end_angle. PARANDO.")
                    break

                # 2.7. Movimenta a mesa (COM5) para o próximo ângulo - NOVO: Usa sistema de tabela
                proximo_angulo = current_angle + step_angle
                
                # CORREÇÃO: Se o próximo ângulo passaria do end_angle, vai direto para o end_angle
                # MAS NÃO PARA - deixa medir no end_angle na próxima iteração
                if proximo_angulo > end_angle:
                    # Se já passaria do end_angle, vai para o end_angle
                    proximo_angulo = end_angle
                    print(f"DEBUG POSIÇÃO: Próximo step ({current_angle + step_angle:.3f}°) passaria do end_angle. Ajustando para {end_angle:.3f}°")
                    
                    sucesso_step = self._mover_mesa_para_angulo_absoluto(proximo_angulo)
                    if not sucesso_step:
                        if self.cancel_requested:
                            print("⚠️ Movimento interrompido por cancelamento (movimento final)."); success = False; break
                        self.results_queue.put({'type': 'ERROR', 'msg': "Falha ao mover a mesa."})
                        success = False
                        break
                    
                    current_angle = self.current_position
                    print(f"DEBUG POSIÇÃO: Chegou ao end_angle - posição: {self.current_position:.3f}°. Vai medir na próxima iteração.")
                    # NÃO PARA AQUI - continua o loop para medir no end_angle
                    continue
                    
                elif proximo_angulo > 360:
                    # Se passar de 360°, normaliza (mas só se end_angle for 360°)
                    if end_angle >= 360:
                        proximo_angulo = proximo_angulo % 360
                        # Se normalizou para 0° e end_angle é 360°, já completou
                        if proximo_angulo == 0 and end_angle >= 360:
                            print(f"DEBUG POSIÇÃO: Completou 360°. PARANDO.")
                            break
                    else:
                        proximo_angulo = end_angle
                
                print(f"DEBUG POSIÇÃO: Movendo de {current_angle:.3f}° para {proximo_angulo:.3f}° (step: {step_angle:.3f}°)")
                
                sucesso_step = self._mover_mesa_para_angulo_absoluto(proximo_angulo)
                if not sucesso_step:
                    if self.cancel_requested:
                        print("⚠️ Movimento interrompido por cancelamento (movimento final)."); success = False; break
                    self.results_queue.put({'type': 'ERROR', 'msg': "Falha ao mover a mesa."})
                    success = False
                    break
                
                current_angle = self.current_position
                print(f"DEBUG POSIÇÃO: Após step - posição: {self.current_position:.3f}°")
                
                # CORREÇÃO: Verifica novamente se chegou ou passou do end_angle após o movimento
                # Se alcançou (ou passou ligeiramente) o end_angle, mede na próxima iteração
                if current_angle >= end_angle - 0.3:
                    print(f"DEBUG POSIÇÃO: ✅ Chegou ao end_angle após movimento. Vai medir e encerrar na próxima iteração.")
                    continue
            
            # 4. Movimento Final (volta para 0) - NOVO: Usa sistema de tabela
            if success and not self.cancel_requested and not manual_mode:
                self.results_queue.put({'type': 'STATUS', 'text_key': "Teste concluído. Retornando ao ponto de origem..."})
                
                passos_per_rev = self._get_passos_per_revolucao()
                print(f"🔍 DIAGNÓSTICO ANTES DO RESET:")
                print(f"   Contador atual: {self.contador_passos_absoluto} passos")
                print(f"   Posição atual (graus): {self.current_position:.3f}°")
                print(f"   Passos por revolução: {passos_per_rev}")
                print(f"   Se visualmente há erro, o número de passos por revolução pode estar incorreto")
                
                sucesso_zero = self._resetar_posicao_zero()
                if sucesso_zero:
                    self.results_queue.put({'type': 'STATUS', 'text_key': "Posição final atingida com precisão."})
                else:
                    self.results_queue.put({'type': 'STATUS', 'text_key': "Posição final ajustada com pequeno desvio."})
            
        except Exception as e:
            self.results_queue.put({'type': 'ERROR', 'msg': f"Erro no workflow de teste: {e}"})
            success = False
            
        finally:
            was_cancelled = self.cancel_requested or self._cancelled_run
            try:
                saved_flag = self.cancel_requested
                self.cancel_requested = False
                if was_cancelled:
                    self.results_queue.put({'type': 'STATUS', 'text_key': "Teste cancelado. Retornando ao ponto de origem..."})
                    if manual_mode:
                        self.contador_passos_absoluto = 0
                        self.current_position = 0.0
                    else:
                        self._retornar_para_posicao_de_referencia()
                else:
                    self.results_queue.put({'type': 'STATUS', 'text_key': "Teste concluído. Retornando ao ponto de origem..."})
                    if manual_mode:
                        self.contador_passos_absoluto = 0
                        self.current_position = 0.0
                    else:
                        self._retornar_para_posicao_de_referencia()
                self.cancel_requested = saved_flag
            except Exception as e:
                print(f"⚠️ Erro ao resetar após término/cancelamento: {e}")
            
            # Garante que o motor seja liberado ao final do teste
            self._liberar_motor()
            self.results_queue.put({'type': 'FINISH', 'status': 'Cancelled' if was_cancelled else ('Success' if success else 'Error')})
            self._cancelled_run = False


    # --- PROCESSAMENTO DA FILA DA GUI (COM REPARO NA LEITURA DE DADOS) ---
    def process_queue(self):
        """Processa a fila de resultados e eventos da thread de teste."""
        try:
            while not self.results_queue.empty():
                result = self.results_queue.get_nowait()
                
                if result.get('type') == 'STATUS':
                    foreground = result.get('foreground', "blue")
                    if 'text_key' in result:
                        text = self._trf(result['text_key'], **(result.get('text_params') or {}))
                    else:
                        text = self._translate_text(result.get('text', ""))
                    self.status_label.config(text=text, foreground=foreground)
                
                elif result.get('type') == 'ERROR':
                    error_msg = result['msg']
                    self.status_label.config(text=f"ERRO: {error_msg}", foreground="red")
                    messagebox.showerror(self._trf("Erro de Execução"), self._translate_text(error_msg))
                    self._end_test_ui(status='Error')

                elif result.get('type') == 'EPC_RESULT':
                    if result.get('success'):
                        epc_value = result.get('epc', '')
                        self.selected_epc_var.set(epc_value)
                        self.epc_label.config(text=epc_value or "Nenhum EPC selecionado", fg="blue" if epc_value else "gray")
                        status_message = result.get('message', 'EPC registrado.')
                        self.status_label.config(text=status_message, foreground="green")
                    else:
                        status_message = result.get('message', 'Falha ao registrar EPC.')
                        self.status_label.config(text=status_message, foreground="orange")
                        messagebox.showwarning(
                            self._trf("RFID"),
                            self._translate_text(status_message),
                            parent=self.master
                        )

                elif result.get('type') == 'EPC_FINISH':
                    self.epc_registering = False
                    self.capture_epc_button.config(state="normal")

                elif result.get('type') == 'MANUAL_PROMPT':
                    text = self._trf(result.get('text_key'), **(result.get('text_params') or {})) if result.get('text_key') else result.get('text', "Ajuste a posição e confirme.")
                    prompt_text = text
                    self.status_label.config(text=prompt_text, foreground="blue")
                    self.manual_waiting_confirm = True
                    self.manual_confirm_button.config(state="normal")

                elif result.get('type') == 'RESULT':
                    if self.ignore_live_updates:
                        continue
                    raw_angle = float(result.get('angle', 0.0) or 0.0)
                    normalized_angle = self._normalize_measurement_angle(raw_angle)
                    result['raw_angle'] = raw_angle
                    result['angle'] = normalized_angle

                    print(
                        f"DEBUG process_queue: RESULT - Ângulo bruto {raw_angle:.3f}° "
                        f"=> normalizado {normalized_angle:.3f}°, threshold_raw={result.get('threshold_raw')}"
                    )

                    # Remove entradas temporárias e finais anteriores para este ângulo normalizado
                    self.test_history = [
                        entry for entry in self.test_history
                        if entry.get('angle') != normalized_angle
                    ]

                    result['is_progress'] = False
                    self.test_history.append(result)
                    print(f"DEBUG process_queue: Histórico atual possui {len(self.test_history)} pontos finais")

                    self.update_polar_plot()
                    self._schedule_state_save()

                elif result.get('type') == 'PROGRESS_UPDATE':
                    if self.ignore_live_updates:
                        continue
                    # Atualização em tempo real durante a varredura de threshold
                    # Atualiza o gráfico sem adicionar à tabela ainda
                    angle = float(result.get('angle', 0.0) or 0.0)
                    normalized_angle = self._normalize_measurement_angle(angle)
                    threshold_raw = result['threshold_raw']
                    
                    existing_entry = None
                    for i, entry in enumerate(self.test_history):
                        if entry.get('angle') == normalized_angle and entry.get('is_progress', False):
                            existing_entry = i
                            break

                    progress_entry = {
                        'angle': normalized_angle,
                        'raw_angle': angle,
                        'threshold_raw': threshold_raw,
                        'threshold_display': result['threshold_display'],
                        'epc': result['epc'],
                        'timestamp': datetime.now().strftime('%H:%M:%S'),
                        'is_progress': True
                    }

                    if existing_entry is not None:
                        self.test_history[existing_entry] = progress_entry
                    else:
                        self.test_history.append(progress_entry)

                    self.update_polar_plot()
                    self._schedule_state_save()

                elif result.get('type') == 'FINISH':
                    finish_status = result.get('status', 'Success')
                    self._end_test_ui(status=finish_status)
                    if finish_status == 'Success':
                        self._save_current_test_to_history()
                    self.update_polar_plot() # Plota o resultado final

        except queue.Empty:
            pass
        except Exception as e:
            if "Item end not found" not in str(e):
                 print(f"Erro ao processar fila: {e}")

        # Agenda a próxima verificação da fila
        self.after(100, self.process_queue)
        
    def _end_test_ui(self, status):
        """Finaliza a interface após o teste."""
        self.test_running = False
        self.cancel_requested = False
        self.start_button.config(state="normal")
        self.cancel_button.config(state="disabled")
        self.manual_waiting_confirm = False
        self.manual_confirm_button.config(state="disabled")
        self.ignore_live_updates = True
        if self.app_shell:
            self.app_shell.set_test_running(False, "Fast Orientation")

        if status == 'Success':
            self.status_label.config(text=self._trf("Teste concluído com Sucesso."), foreground="green")
        elif status == 'Cancelled':
            self.status_label.config(text=self._trf("Teste interrompido pelo usuário."), foreground="orange")
        else:
            self.status_label.config(text=self._trf("Teste Encerrado devido a um ERRO."), foreground="red")


    def export_to_csv(self):
        """Salva os dados do histórico de testes em um arquivo CSV."""
        datasets = []

        if self.test_history:
            final_points = [entry for entry in self.test_history if not entry.get('is_progress', False)]
            if final_points:
                datasets.append({
                    'test_name': self.current_test_metadata.get('test_name', 'Teste Atual'),
                    'epc': self.current_test_metadata.get('epc', 'N/A'),
                    'frequency_mhz': self.current_test_metadata.get('frequency_mhz', 0.0),
                    'start_angle': self.current_test_metadata.get('start_angle', 0.0),
                    'end_angle': self.current_test_metadata.get('end_angle', 0.0),
                    'step_angle': self.current_test_metadata.get('step_angle', 0.0),
                    'measurements': final_points
                })

        for test_id in self.selected_history_ids:
            test = self.historical_tests.get(test_id)
            if test:
                datasets.append(test)

        if not datasets:
            messagebox.showinfo(
                self._trf("Exportar Dados"),
                self._trf("Selecione um teste no histórico para exportar."),
                parent=self.master
            )
            return

        filename = filedialog.asksaveasfilename(
            defaultextension=".csv",
            filetypes=[("Arquivos CSV", "*.csv"), ("Todos os Arquivos", "*.*")],
            title="Salvar Histórico de Testes"
        )
        
        if not filename:
            return

        try:
            with open(filename, mode='w', newline='', encoding='utf-8') as file:
                writer = csv.writer(file, delimiter=';') 
                
                writer.writerow([
                    "Teste",
                    "Angulo (°)", 
                    "Threshold (dBm)", 
                    "EPC Detectado", 
                    "Frequencia (MHz)",
                    "Data_Hora"
                ])
                
                for test in datasets:
                    test_name = test.get('test_name', 'Teste')
                    epc = test.get('epc', 'N/A')
                    freq = float(test.get('frequency_mhz', self.freq_var.get()))

                    for entry in test.get('measurements', []):
                        threshold = entry.get('threshold_display') or entry.get('threshold_raw', 'N/A')
                        timestamp = entry.get('timestamp', '-')

                        writer.writerow([
                            test_name,
                            f"{entry.get('angle', 0.0):.1f}",
                            threshold,
                            epc,
                            f"{freq:.1f}",
                            timestamp
                        ])

            messagebox.showinfo(
                self._trf("Exportar Dados"),
                self._trf("Dados exportados com sucesso para:\n{filename}", filename=filename),
                parent=self.master
            )
            
        except Exception as e:
            messagebox.showerror(
                self._trf("Erro de Exportação"),
                self._trf("Falha ao salvar o arquivo:\n{error}", error=e),
                parent=self.master
            )

    def destroy(self):
        """Libera recursos ao fechar a aplicação, incluindo o motor do Arduino."""
        try:
            self._save_session_state()
        except Exception as e:
            print(f"Aviso: Erro ao salvar estado ao destruir: {e}")
        try:
            self._detach_language_listener()
        except Exception:
            pass
        try:
            # Libera o motor antes de fechar
            self._liberar_motor()
        except Exception as e:
            print(f"Aviso: Erro ao liberar motor no destroy: {e}")
        try:
            super().destroy()
        except Exception:
            pass


# --- INICIALIZAÇÃO DA GUI ---
if __name__ == "__main__":
    if DLL_LOADED:
        root = tk.Tk()
        app = OrientationTester(master=root)
        def on_closing():
            """Handler de fechamento que libera o motor antes de sair."""
            try:
                app._save_session_state()
            except Exception:
                pass
            try:
                app._liberar_motor()
            except Exception:
                pass
            sys.exit(0)
        root.protocol("WM_DELETE_WINDOW", on_closing)
        root.mainloop()
    else:
        print("\n\nO programa não pode ser iniciado devido à falha da DLL RFID. Verifique o console.")