diff --git a/uhf_rfid_gui.py b/uhf_rfid_gui.py index 74e6339..d67c883 100644 --- a/uhf_rfid_gui.py +++ b/uhf_rfid_gui.py @@ -256,6 +256,12 @@ class RfidApp(tk.Tk): self.stop_event = threading.Event() self.event_queue = queue.Queue() self.parser = FrameParser() + # No Windows, ler e escrever na porta serial ao mesmo tempo a partir + # de threads diferentes sem sincronizacao costuma gerar + # PermissionError/WinError 5 ("Acesso negado") em GetOverlappedResult + # ou WriteFile, dependendo do driver USB-serial. Este lock serializa + # todo acesso a self.ser (leitura e escrita) para evitar a corrida. + self._port_lock = threading.RLock() self.tags = {} # epc -> {"count": int, "rssi": int, "last_seen": str} @@ -409,9 +415,15 @@ class RfidApp(tk.Tk): return try: - self.ser = serial.Serial(port, baudrate=baud, timeout=0.2) + self.ser = serial.Serial(port, baudrate=baud, timeout=0.2, write_timeout=1.0) except Exception as exc: - messagebox.showerror("Erro ao conectar", str(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 return @@ -434,10 +446,11 @@ class RfidApp(tk.Tk): if self.reader_thread is not None: self.reader_thread.join(timeout=1.0) if self.ser is not None: - try: - self.ser.close() - except Exception: - pass + with self._port_lock: + try: + self.ser.close() + except Exception: + pass self.ser = None self._set_controls_connected(False) self._log("Desconectado") @@ -448,32 +461,58 @@ class RfidApp(tk.Tk): finally: self.destroy() + 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. + """ + self._log(reason) + self.stop_event.set() + if self.ser is not None: + with self._port_lock: + try: + self.ser.close() + except Exception: + pass + self.ser = None + self._set_controls_connected(False) + self.status_var.set("Desconectado (erro)") + # -------------------------------------------------------------- serial def _send(self, frame: bytes): if self.ser is None: return try: - self.ser.write(frame) + with self._port_lock: + self.ser.write(frame) except Exception as exc: - self._log(f"Erro ao enviar: {exc}") + self._fatal_disconnect(f"Erro ao enviar: {exc}") return self._log(f"TX: {frame.hex(' ').upper()}") def _reader_loop(self): + # Le em pequenos pedaços e mantem o lock so durante a chamada + # in_waiting+read (nao-bloqueante, pois so le o que ja esta + # disponivel), para nao competir por muito tempo com escritas + # feitas pela thread principal. while not self.stop_event.is_set(): try: - if self.ser is None: - break - waiting = self.ser.in_waiting - data = self.ser.read(waiting if waiting else 1) + with self._port_lock: + if self.ser is None: + break + waiting = self.ser.in_waiting + data = self.ser.read(waiting) if waiting else b"" except Exception as exc: - self.event_queue.put(("log", f"Erro de leitura serial: {exc}")) + self.event_queue.put(("fatal", f"Erro de leitura serial: {exc}")) break if data: frames = self.parser.feed(data) for frame in frames: self.event_queue.put(("frame", frame)) + else: + time.sleep(0.02) # ------------------------------------------------------------- actions def _set_power(self): @@ -519,6 +558,8 @@ class RfidApp(tk.Tk): self._log(payload) elif kind == "frame": self._handle_frame(payload) + elif kind == "fatal": + self._fatal_disconnect(payload) except queue.Empty: pass finally: