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:
parent
7b41ac054b
commit
d93be84bda
@ -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:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user