Fix Windows PermissionError (WinError 5) during continuous scan

Reading and writing the serial port from different threads without
synchronization can trigger "GetOverlappedResult failed / WriteFile
failed - Acesso negado" on Windows with some USB-serial drivers. Add a
lock guarding all access to the Serial object, make the reader loop
non-blocking (in_waiting + short sleep instead of blocking read), and
recover cleanly to a reconnectable UI state on any fatal serial error
instead of leaving the app stuck in a broken "connected" state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
lronetto 2026-07-16 15:25:36 +00:00 committed by Claude
parent 7b41ac054b
commit d93be84bda

View File

@ -256,6 +256,12 @@ class RfidApp(tk.Tk):
self.stop_event = threading.Event() self.stop_event = threading.Event()
self.event_queue = queue.Queue() self.event_queue = queue.Queue()
self.parser = FrameParser() 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} self.tags = {} # epc -> {"count": int, "rssi": int, "last_seen": str}
@ -409,9 +415,15 @@ class RfidApp(tk.Tk):
return return
try: 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: 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 self.ser = None
return return
@ -434,10 +446,11 @@ class RfidApp(tk.Tk):
if self.reader_thread is not None: if self.reader_thread is not None:
self.reader_thread.join(timeout=1.0) self.reader_thread.join(timeout=1.0)
if self.ser is not None: if self.ser is not None:
try: with self._port_lock:
self.ser.close() try:
except Exception: self.ser.close()
pass except Exception:
pass
self.ser = None self.ser = None
self._set_controls_connected(False) self._set_controls_connected(False)
self._log("Desconectado") self._log("Desconectado")
@ -448,32 +461,58 @@ class RfidApp(tk.Tk):
finally: finally:
self.destroy() 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 # -------------------------------------------------------------- serial
def _send(self, frame: bytes): def _send(self, frame: bytes):
if self.ser is None: if self.ser is None:
return return
try: try:
self.ser.write(frame) with self._port_lock:
self.ser.write(frame)
except Exception as exc: except Exception as exc:
self._log(f"Erro ao enviar: {exc}") self._fatal_disconnect(f"Erro ao enviar: {exc}")
return return
self._log(f"TX: {frame.hex(' ').upper()}") self._log(f"TX: {frame.hex(' ').upper()}")
def _reader_loop(self): 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(): while not self.stop_event.is_set():
try: try:
if self.ser is None: with self._port_lock:
break if self.ser is None:
waiting = self.ser.in_waiting break
data = self.ser.read(waiting if waiting else 1) waiting = self.ser.in_waiting
data = self.ser.read(waiting) if waiting else b""
except Exception as exc: 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 break
if data: if data:
frames = self.parser.feed(data) frames = self.parser.feed(data)
for frame in frames: for frame in frames:
self.event_queue.put(("frame", frame)) self.event_queue.put(("frame", frame))
else:
time.sleep(0.02)
# ------------------------------------------------------------- actions # ------------------------------------------------------------- actions
def _set_power(self): def _set_power(self):
@ -519,6 +558,8 @@ class RfidApp(tk.Tk):
self._log(payload) self._log(payload)
elif kind == "frame": elif kind == "frame":
self._handle_frame(payload) self._handle_frame(payload)
elif kind == "fatal":
self._fatal_disconnect(payload)
except queue.Empty: except queue.Empty:
pass pass
finally: finally: