Add auto-reconnect and diagnostic hints for serial Access Denied errors
The previous fix (port access lock) only addressed contention between our own read/write threads. A recurring "ClearCommError failed - Acesso negado" after the lock fix indicates the OS is invalidating the port handle itself (USB power management suspending the port, the RFID module browning out under high RF output power, or another app holding the port) rather than an internal race. Add: - A "Reconectar automaticamente" checkbox (on by default) that retries opening the port with backoff (up to 5 attempts) after any fatal serial error, resuming the scan automatically if one was active. - A generation counter so stale scheduled reconnects are discarded if the user manually connects/disconnects in the meantime. - An actionable hint logged on fatal errors explaining the likely causes and how to fix each one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
d93be84bda
commit
22fca0246c
151
uhf_rfid_gui.py
151
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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user