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.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: