diff --git a/uhf_rfid_gui.py b/uhf_rfid_gui.py index d67c883..9d99d82 100644 --- a/uhf_rfid_gui.py +++ b/uhf_rfid_gui.py @@ -112,6 +112,9 @@ CMD_STOP_MULTI_POLL = 0x28 BAUDRATES = ["9600", "19200", "38400", "57600", "115200"] DEFAULT_BAUDRATE = "115200" +MAX_RECONNECT_ATTEMPTS = 5 +RECONNECT_DELAY_MS = 1500 + # -------------------------------------------------------------------------- # Construcao / interpretacao de quadros @@ -263,6 +266,19 @@ class RfidApp(tk.Tk): # todo acesso a self.ser (leitura e escrita) para evitar a corrida. self._port_lock = threading.RLock() + # Estado usado pela reconexao automatica: guarda os ultimos + # parametros usados com sucesso, quantas tentativas silenciosas ja + # foram feitas, se uma varredura estava ativa (para retoma-la), e um + # "numero de geracao" que invalida tentativas de reconexao agendadas + # quando o usuario toma uma acao manual (conectar/desconectar) antes + # delas dispararem. + self._last_port = None + self._last_baud = None + self._reconnect_attempts = 0 + self._pending_resume_scan = False + self._conn_generation = 0 + self.scanning = False + self.tags = {} # epc -> {"count": int, "rssi": int, "last_seen": str} self._build_ui() @@ -315,6 +331,13 @@ class RfidApp(tk.Tk): ) self.set_power_btn.grid(row=0, column=8, **pad) + self.auto_reconnect_var = tk.BooleanVar(value=True) + ttk.Checkbutton( + conn_frame, + text="Reconectar automaticamente em caso de erro na porta", + variable=self.auto_reconnect_var, + ).grid(row=1, column=0, columnspan=6, sticky="w", padx=6, pady=(0, 4)) + # ------------------------------------------------------ Scan controls scan_frame = ttk.LabelFrame(self, text="Varredura") scan_frame.pack(fill="x", **pad) @@ -394,39 +417,80 @@ class RfidApp(tk.Tk): # ------------------------------------------------------------ connect def _toggle_connection(self): if self.ser is None: + self._conn_generation += 1 # invalida qualquer reconexao automatica antiga pendente + self._reconnect_attempts = 0 self._connect() else: self._disconnect() - def _connect(self): + def _connect(self, silent=False, expected_gen=None): + """ + Abre a porta serial. Quando silent=True, esta sendo chamado pela + rotina de reconexao automatica: usa os ultimos parametros que + funcionaram, nao exibe caixas de dialogo de erro (so registra no + log) e agenda uma nova tentativa em caso de falha. + + expected_gen e o "numero de geracao" capturado no momento em que a + tentativa (silenciosa) foi agendada; se o usuario tomou uma acao + manual (conectar/desconectar) enquanto ela esperava, a geracao atual + ja mudou e esta tentativa e descartada sem tocar na porta. + """ + if silent and expected_gen is not None and expected_gen != self._conn_generation: + self._log("Reconexao automatica cancelada (nova acao do usuario).") + return + + gen = self._conn_generation + if serial is None: - messagebox.showerror("Erro", "pyserial nao instalado. Rode: pip install pyserial") + if not silent: + messagebox.showerror("Erro", "pyserial nao instalado. Rode: pip install pyserial") return - port = self.port_var.get() + port = self._last_port if silent else self.port_var.get() + baud_source = self._last_baud if silent else self.baud_var.get() + if not port: - messagebox.showwarning("Atencao", "Selecione uma porta serial.") + if not silent: + messagebox.showwarning("Atencao", "Selecione uma porta serial.") return try: - baud = int(self.baud_var.get()) - except ValueError: - messagebox.showwarning("Atencao", "Baudrate invalido.") + baud = int(baud_source) + except (TypeError, ValueError): + if not silent: + messagebox.showwarning("Atencao", "Baudrate invalido.") return try: - self.ser = serial.Serial(port, baudrate=baud, timeout=0.2, write_timeout=1.0) + ser = serial.Serial(port, baudrate=baud, timeout=0.2, write_timeout=1.0) except Exception as exc: - messagebox.showerror( - "Erro ao conectar", - f"{exc}\n\n" - "Se o erro for 'Acesso negado'/PermissionError, verifique se a porta\n" - "nao esta aberta em outro programa (Monitor Serial da Arduino IDE,\n" - "outra instancia deste app, PuTTY, etc.) e tente novamente.", - ) - self.ser = None + if silent: + self._log(f"Falha ao reconectar: {exc}") + self._schedule_reconnect(gen) + else: + messagebox.showerror( + "Erro ao conectar", + f"{exc}\n\n" + "Se o erro for 'Acesso negado'/PermissionError, verifique se a porta\n" + "nao esta aberta em outro programa (Monitor Serial da Arduino IDE,\n" + "outra instancia deste app, PuTTY, etc.) e tente novamente.", + ) return + # se o usuario desconectou/reconectou manualmente enquanto esta + # tentativa (silenciosa) estava em andamento, descarta o resultado + if gen != self._conn_generation: + try: + ser.close() + except Exception: + pass + return + + self.ser = ser + self._last_port = port + self._last_baud = baud + self._reconnect_attempts = 0 + self.stop_event.clear() self.parser = FrameParser() self.reader_thread = threading.Thread(target=self._reader_loop, daemon=True) @@ -440,7 +504,15 @@ class RfidApp(tk.Tk): self._send(frame_get_info(INFO_SW_VERSION)) self._send(frame_get_power()) + if self._pending_resume_scan: + self._pending_resume_scan = False + self._log("Retomando varredura apos reconexao automatica...") + self.after(300, self._start_scan) + def _disconnect(self): + self._conn_generation += 1 # cancela qualquer reconexao automatica pendente + self._reconnect_attempts = 0 + self._pending_resume_scan = False self._stop_scan(silent=True) self.stop_event.set() if self.reader_thread is not None: @@ -464,10 +536,24 @@ class RfidApp(tk.Tk): def _fatal_disconnect(self, reason): """ Encerra a conexao apos um erro irrecuperavel (ex.: PermissionError - do Windows) e devolve a interface a um estado consistente, para que - o usuario possa simplesmente clicar em "Conectar" de novo. + do Windows) e devolve a interface a um estado consistente. Se + "Reconectar automaticamente" estiver marcado, agenda novas + tentativas; senao, o usuario so precisa clicar em "Conectar" de novo. """ self._log(reason) + self._log( + "Dica: 'Acesso negado' (WinError 5) na porta serial normalmente vem de " + "(1) gerenciamento de energia USB suspendendo a porta - em Gerenciador de " + "Dispositivos, na porta COM, aba Gerenciamento de Energia, desmarque 'Permitir " + "que o computador desligue este dispositivo para economizar energia'; " + "(2) o modulo RFID sofrendo queda de energia/reset ao transmitir em potencia " + "alta - tente reduzir a potencia ou usar um cabo/hub USB com alimentacao " + "externa; ou (3) outro programa (Monitor Serial, outra instancia deste app) " + "usando a mesma porta ao mesmo tempo." + ) + + self._pending_resume_scan = self.scanning + self.scanning = False self.stop_event.set() if self.ser is not None: with self._port_lock: @@ -477,7 +563,32 @@ class RfidApp(tk.Tk): pass self.ser = None self._set_controls_connected(False) - self.status_var.set("Desconectado (erro)") + + gen = self._conn_generation + if self.auto_reconnect_var.get(): + self.status_var.set("Erro na porta - reconectando...") + self._schedule_reconnect(gen) + else: + self.status_var.set("Desconectado (erro)") + + def _schedule_reconnect(self, gen): + if gen != self._conn_generation or not self.auto_reconnect_var.get(): + return + if self._reconnect_attempts >= MAX_RECONNECT_ATTEMPTS: + self._log( + f"Numero maximo de tentativas de reconexao automatica ({MAX_RECONNECT_ATTEMPTS}) " + "atingido. Verifique a causa (veja a dica acima) e clique em Conectar manualmente." + ) + self._reconnect_attempts = 0 + self.status_var.set("Desconectado (erro)") + return + self._reconnect_attempts += 1 + self._log( + f"Tentando reconectar automaticamente " + f"({self._reconnect_attempts}/{MAX_RECONNECT_ATTEMPTS}) em " + f"{RECONNECT_DELAY_MS / 1000:.1f}s..." + ) + self.after(RECONNECT_DELAY_MS, lambda: self._connect(silent=True, expected_gen=gen)) # -------------------------------------------------------------- serial def _send(self, frame: bytes): @@ -529,12 +640,14 @@ class RfidApp(tk.Tk): def _start_scan(self): self._send(frame_start_multi_poll(count=4095)) + self.scanning = True self.start_btn.configure(state="disabled") self.stop_btn.configure(state="normal") self.status_var.set("Varredura em andamento...") self._log("Varredura continua iniciada") def _stop_scan(self, silent=False): + self.scanning = False if self.ser is not None: self._send(frame_stop_multi_poll()) self.start_btn.configure(state="normal" if self.ser is not None else "disabled")