# mesa_movimento.py (VERSÃO FINAL: FORÇANDO RESET POR DTR/RTS)

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import threading
import time
import serial
import sys
import queue
from datetime import datetime
import csv
import random 

# --- CONFIGURAÇÕES DE HARDWARE ---
ARDUINO_PORT = 'COM5' 
ARDUINO_BAUD = 9600
PASSOS_PER_REVOLUCAO = 2038 

# --- CLASSE DE CONTROLE PRINCIPAL ---
class MovimentoMesa(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        self.master = master
        self.master.title(f"Controle de Movimento Puro (Arduino {ARDUINO_PORT})")
        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.results_queue = queue.Queue()
        self.test_history = []
        self.current_position = 0.0 

        # Variáveis de entrada (APENAS ÂNGULOS)
        self.start_angle_var = tk.DoubleVar(value=0.0)
        self.end_angle_var = tk.DoubleVar(value=360.0)
        self.step_angle_var = tk.DoubleVar(value=45.0)

        self._create_widgets()
        self._check_hardware() 
        self.process_queue() 

    def _create_widgets(self):
        # Frame de Configuração
        config_frame = ttk.LabelFrame(self, text="Configuração do Movimento")
        config_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=(0, 10))
        config_frame.columnconfigure(1, weight=1)

        # Entradas
        ttk.Label(config_frame, text="Ângulo Inicial (°):").grid(row=0, column=0, sticky="w", pady=2, padx=5)
        ttk.Entry(config_frame, textvariable=self.start_angle_var, width=15).grid(row=0, column=1, sticky="ew", pady=2, padx=5)
        
        ttk.Label(config_frame, text="Ângulo Final (°):").grid(row=1, column=0, sticky="w", pady=2, padx=5)
        ttk.Entry(config_frame, textvariable=self.end_angle_var, width=15).grid(row=1, column=1, sticky="ew", pady=2, padx=5)
        
        ttk.Label(config_frame, text="Step (°):").grid(row=2, column=0, sticky="w", pady=2, padx=5)
        ttk.Entry(config_frame, textvariable=self.step_angle_var, width=15).grid(row=2, column=1, sticky="ew", pady=2, padx=5)

        # Botões
        command_button_frame = tk.Frame(config_frame)
        command_button_frame.grid(row=0, column=2, padx=10, rowspan=3, sticky="nswe")
        
        self.start_button = ttk.Button(command_button_frame, text="Iniciar Movimento", command=self.start_test, width=15)
        self.start_button.pack(pady=3, fill="x")

        self.export_button = ttk.Button(command_button_frame, text="Exportar Log CSV", command=self.export_to_csv, width=15)
        self.export_button.pack(pady=3, fill="x")
        
        # Frame de Status
        status_frame = ttk.LabelFrame(self, text="Status e Controle") 
        status_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 10))
        self.status_label = ttk.Label(status_frame, text="Aguardando inicialização...", foreground="blue")
        self.status_label.pack(fill="x")
        self.cancel_button = ttk.Button(status_frame, text="Cancelar", command=self._request_cancel, state="disabled")
        self.cancel_button.pack(pady=5)

        # Frame do Histórico (Tabela)
        history_frame = ttk.LabelFrame(self, text="Log de Movimento")
        history_frame.grid(row=2, column=0, sticky="nsew", padx=10, pady=(0, 10))
        self.rowconfigure(2, weight=1) 

        # Tabela Treeview
        cols = ('Angulo', 'Status', 'Passos', 'Data/Hora')
        self.history_tree = ttk.Treeview(history_frame, columns=cols, show='headings', height=10)
        self.history_tree.heading('Angulo', text='Ângulo (°)')
        self.history_tree.heading('Status', text='Status')
        self.history_tree.heading('Passos', text='Passos')
        self.history_tree.heading('Data/Hora', text='Data/Hora')
        self.history_tree.column('Angulo', width=80, anchor='center')
        self.history_tree.column('Status', width=200, anchor='center')
        self.history_tree.column('Passos', width=120, anchor='center')
        self.history_tree.column('Data/Hora', width=120, anchor='center')

        # Scrollbar
        scrollbar = ttk.Scrollbar(history_frame, orient=tk.VERTICAL, command=self.history_tree.yview)
        self.history_tree.configure(yscrollcommand=scrollbar.set)
        
        self.history_tree.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
    def _check_hardware(self):
        """Verifica se a porta COM5 existe."""
        try:
            # Tenta APENAS abrir e fechar a porta COM5 para verificar sua existência
            temp_serial = serial.Serial(ARDUINO_PORT, ARDUINO_BAUD, timeout=1)
            temp_serial.close()
            self.status_label.config(text=f"Pronto. Arduino conectado em {ARDUINO_PORT}.", foreground="green")
            self.start_button.config(state="normal")
        except serial.SerialException:
            self.status_label.config(text=f"ERRO: Arduino não encontrado em {ARDUINO_PORT}.", foreground="red")
            self.start_button.config(state="disabled")
            
    def _request_cancel(self):
        """Solicita o cancelamento do teste."""
        self.cancel_requested = True
        self.status_label.config(text="Cancelamento solicitado...", foreground="orange")
        self.cancel_button.config(state="disabled")

    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.status_label.config(text="Teste em andamento...", foreground="blue")

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

    def start_test(self):
        """Valida entradas antes de iniciar o teste."""
        if self.test_running: return
        
        try:
            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("Erro de Entrada", "Ângulo inicial deve ser menor que o final.")
                 return
            if step_angle <= 0.0:
                 messagebox.showerror("Erro de Entrada", "O Step deve ser maior que 0.")
                 return

            if self.start_button.cget('state') == 'disabled':
                messagebox.showerror("Erro de Hardware", "O hardware não está pronto. Verifique as conexões.")
                return

            # Limpa o histórico anterior
            for item in self.history_tree.get_children():
                self.history_tree.delete(item)
            self.test_history = []
            
            # Define current_position para o ângulo inicial para começar a varredura
            self.current_position = start_angle 
            
            self._start_test_thread()

        except ValueError as e:
            messagebox.showerror("Erro de Entrada", f"Verifique os valores de entrada. {e}")
        except Exception as e:
            messagebox.showerror("Erro", f"Ocorreu um erro inesperado: {e}")

    # --- FUNÇÕES DE COMUNICAÇÃO ---
    def _calcular_passos(self, graus):
        """Converte o ângulo desejado para passos."""
        passos = int((graus / 360) * PASSOS_PER_REVOLUCAO)
        return passos

    def _mover_mesa(self, graus_relativos):
        """Abre, FORÇA RESET, envia comando, espera PRONTO, fecha."""
        
        if self.cancel_requested:
            return False
            
        passos = self._calcular_passos(graus_relativos)
        
        try:
            # 1. Abre a porta serial para este comando
            # rtscts=False, dsrdtr=False (para não interferir com a comunicação)
            arduino = serial.Serial(
                port=ARDUINO_PORT, 
                baudrate=ARDUINO_BAUD, 
                timeout=3,
                rtscts=False, 
                dsrdtr=False  
            )
            
            # 2. ** FORÇAR RESET DE SOFTWARE **: Simula o reset do Monitor Serial
            # Abaixa e levanta o pino DTR (Data Terminal Ready), forçando o reset do Arduino.
            arduino.setDTR(False)
            time.sleep(0.02) # Espera 20ms para o reset
            arduino.setDTR(True) 
            time.sleep(1.5) # Espera o Arduino rebotar e inicializar (Serial.begin)

            # Limpeza
            arduino.reset_input_buffer()
            arduino.reset_output_buffer()
            time.sleep(0.1) 
            
            # Limpa o Buffer Inicial do Arduino (Geralmente "ARDUINO-PRONTO\r\n")
            while arduino.in_waiting > 0:
                arduino.readline()
                
            # 3. Envia o comando
            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'))
            self.results_queue.put({'type': 'STATUS', 'text': f"Movendo mesa {graus_relativos:.1f} graus ({passos} passos)..."})

            # 4. Espera pela resposta 'PRONTO'
            start_time = time.time()
            TIMEOUT = 5.0 
            full_buffer = "" 

            while time.time() - start_time < TIMEOUT:
                if self.cancel_requested: 
                    arduino.close()
                    return False
                
                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! Buffer Total: {repr(full_buffer)}")
                        arduino.close()
                        return True
                        
                time.sleep(0.01)
            
            # Se atingir o timeout
            arduino.close()
            print(f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] *** FALHA DE MOVIMENTO ***")
            print(f"Buffer Parcial Final: {repr(full_buffer)}")
            
            self.results_queue.put({'type': 'ERROR', 'msg': f"FALHA DE MOVIMENTO: Mesa não girou em {graus_relativos:.1f} graus. Falha de Driver/DTR/RTS."})
            return False

        except serial.SerialException as e:
            self.results_queue.put({'type': 'ERROR', 'msg': f"ERRO SERIAL (COM5) - Falha ao abrir a porta: {e}"})
            return False
        except Exception as e:
            self.results_queue.put({'type': 'ERROR', 'msg': f"Erro inesperado durante _mover_mesa: {e}"})
            return False


    # --- WORKFLOW PRINCIPAL DE MOVIMENTO ---
    def _movement_workflow(self):
        """Workflow para movimentar a mesa e logar o sucesso/falha."""
        
        # 1. Parâmetros do Teste
        start_angle = self.start_angle_var.get()
        end_angle = self.end_angle_var.get()
        step_angle = self.step_angle_var.get()
        
        current_angle = self.current_position
        success = True
        
        try:
            # 1.1. Reset Inicial (Para o ângulo inicial)
            # Executamos o primeiro movimento para zerar a posição e confirmar a conexão
            rel_move_init = start_angle - self.current_position
            if not self._mover_mesa(rel_move_init):
                 self.results_queue.put({'type': 'ERROR', 'msg': "Falha crítica no reset de posição. Verifique a COM5."})
                 return 

            self.current_position = start_angle
            current_angle = self.current_position
            
            # 2. Loop de Movimento
            while current_angle <= end_angle and success and not self.cancel_requested:
                
                # 2.1. Adiciona Resultado
                status_log = "PRONTO (Mesa Parada)"
                self.results_queue.put({
                    'type': 'RESULT',
                    'angle': current_angle,
                    'status_log': status_log,
                    'passos': self._calcular_passos(step_angle) if current_angle != end_angle else 0,
                    'timestamp': datetime.now().strftime('%H:%M:%S')
                })
                
                # 2.2. Verifica Cancelamento e Próximo Passo
                if self.cancel_requested:
                    break
                    
                # 2.3. Prepara para o próximo ângulo
                next_angle = current_angle + step_angle
                
                if next_angle > end_angle:
                    break 

                # 2.4. Movimenta a mesa
                if not self._mover_mesa(step_angle):
                    self.results_queue.put({'type': 'ERROR', 'msg': "Movimento falhou. Verifique conexão."})
                    success = False
                    break
                
                self.current_position += step_angle
                current_angle = self.current_position
            
            # 3. Movimento Final (volta para 0)
            if success and not self.cancel_requested:
                self.results_queue.put({'type': 'STATUS', 'text': "Teste concluído. Voltando para 0°."})
                rel_move_final = -self.current_position
                if not self._mover_mesa(rel_move_final): 
                    self.results_queue.put({'type': 'STATUS', 'text': "Falha ao mover para 0°, encerrando."})
                else:
                    self.current_position = 0.0
                    self.results_queue.put({'type': 'STATUS', 'text': "Posição final 0° atingida."})
                
        except Exception as e:
            self.results_queue.put({'type': 'ERROR', 'msg': f"Erro no workflow de teste: {e}"})
            success = False
            
        finally:
            self.results_queue.put({'type': 'FINISH', 'status': 'Cancelled' if self.cancel_requested else ('Success' if success else 'Error')})


    # --- PROCESSAMENTO DA FILA DA GUI ---
    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':
                    self.status_label.config(text=result['text'], foreground="blue")
                
                elif result.get('type') == 'ERROR':
                    error_msg = result['msg']
                    self.status_label.config(text=f"ERRO: {error_msg}", foreground="red")
                    messagebox.showerror("Erro de Execução", error_msg)
                    self._end_test_ui(status='Error')

                elif result.get('type') == 'RESULT':
                    # Adiciona resultado à tabela
                    self.history_tree.insert('', 'end', values=(
                        f"{result['angle']:.1f}", 
                        result['status_log'], 
                        result['passos'], 
                        result['timestamp']
                    ))
                    self.history_tree.see('end') 
                    self.test_history.append(result)

                elif result.get('type') == 'FINISH':
                    self._end_test_ui(status='Success')

        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")

        if status == 'Success':
            self.status_label.config(text="Teste concluído com Sucesso.", foreground="green")
        elif status == 'Cancelled':
            self.status_label.config(text=f"Teste Cancelado. Mesa em {self.current_position:.1f}°.", foreground="orange")
        else:
            self.status_label.config(text="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."""
        if not self.test_history:
            messagebox.showinfo("Exportar Dados", "Não há dados no histórico para exportar.", parent=self.master)
            return

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

        try:
            with open(filename, mode='w', newline='', encoding='utf-8') as file:
                writer = csv.writer(file, delimiter=';')
                
                writer.writerow(["Angulo (°)", "Status", "Passos Enviados", "Data_Hora"])
                
                for entry in self.test_history:
                    writer.writerow([
                        f"{entry['angle']:.1f}",
                        entry['status_log'],
                        entry['passos'],
                        entry['timestamp']
                    ])

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


# --- INICIALIZAÇÃO DA GUI ---
if __name__ == "__main__":
    root = tk.Tk()
    app = MovimentoMesa(master=root)
    root.protocol("WM_DELETE_WINDOW", lambda: sys.exit(0))
    root.mainloop()