Duplo clique fantasma no mouse Logitech: contornei no Linux com Python (evdev)
Meu mouse Logitech começou a dar o famoso duplo clique fantasma: selecionar texto virava “abre/fecha”, arrastar arquivo “soltava” no meio, etc.
Em vez de trocar switch/soldar ou tentar limpar e não ter garantia de resolução, resolvi ver se o amigo GPT consegue tratar isso via software, pra minha sorte meu SO é o Linux então deve ser mais capaz, já que eu vi não é viavel no Windows: capturar os eventos do mouse real, filtrar o bounce e reenviar tudo por um mouse virtual “limpo”.
O que está acontecendo
Isso quase sempre é desgaste do switch mecânico: o contato “vibra” por alguns milissegundos antes de estabilizar, e o sistema interpreta como cliques separados.
A correção clássica é debounce: ignorar cliques repetidos dentro de uma janela pequena como no caso foi usado o 100ms.
A ideia da solução
- Ler eventos do mouse físico via
evdev - Dar grab no dispositivo físico (o sistema para de receber eventos dele)
- Filtrar “press” repetido em menos de
100ms - Repassar eventos válidos para um mouse virtual via
uinput
Aviso:
grab()captura o dispositivo no nível do sistema.
A partir daí o mouse físico para de enviar eventos diretamente para o desktop — só o mouse virtual (viauinput) continua funcionando.
Se o processo cair/travar, o mouse pode ficar “mudo” até você encerrar o processo que está segurando o device.
Como rodar
Instale a lib:
pip install evdev
Descubra o caminho do seu mouse:
ls -l /dev/input/by-id/ | grep -i mouse
você vai ver linhas parecidas com:
usb-Logitech_USB_Receiver-event-mouse -> ../event7
usb-Logitech_USB_Receiver-mouse -> ../mouse1
O que você faz com isso:
- Use o que termina em
-event-mouse(é o que oevdevlê melhor pra eventos):
-
Exemplo:
/dev/input/by-id/usb-Logitech_USB_Receiver-event-mouse
Cole esse caminho no script:
MOUSE_DEV = '/dev/input/by-id/usb-Logitech_USB_Receiver-event-mouse'
Depois rode (geralmente precisa de root por causa de /dev/input e uinput):
sudo python3 debounced_mouse.py
O código (completo)
Só precisa ajustar o
MOUSE_DEVpro seu caso.
#!/usr/bin/env python3
"""
Filtro global de mouse para eliminar "duplo clique fantasma" (debounce).
Ideia: Lê eventos do mouse físico, cria um mouse virtual via uinput e
repassa tudo, EXCETO cliques repetidos dentro do intervalo de debounce.
"""
from evdev import InputDevice, ecodes, UInput
import datetime
import signal
import sys
# Identificação do dispositivo físico (ajuste para o seu modelo)
MOUSE_DEV = '/dev/input/by-id/usb-Logitech_Wireless_Receiver-event-mouse'
# 100ms: qualquer segundo clique no mesmo botão abaixo disso é descartado
DEBOUNCE_SECONDS = 0.10
def main():
try:
dev = InputDevice(MOUSE_DEV)
except FileNotFoundError:
print(f"Erro: Dispositivo {MOUSE_DEV} não encontrado.")
print("Dica: rode `ls -l /dev/input/by-id/ | grep -i mouse` e ajuste o caminho.")
return
# Capacidades do mouse virtual (botões e eixos de movimento)
uinput_caps = {
ecodes.EV_KEY: [
ecodes.BTN_LEFT,
ecodes.BTN_RIGHT,
ecodes.BTN_MIDDLE,
ecodes.BTN_SIDE,
ecodes.BTN_EXTRA,
],
ecodes.EV_REL: [
ecodes.REL_X,
ecodes.REL_Y,
ecodes.REL_WHEEL,
ecodes.REL_HWHEEL,
],
}
ui = UInput(
uinput_caps,
name='debounced-mouse',
vendor=dev.info.vendor,
product=dev.info.product,
)
def cleanup_and_exit(_signum=None, _frame=None):
try:
dev.ungrab()
except Exception:
pass
try:
ui.close()
except Exception:
pass
try:
dev.close()
except Exception:
pass
print("\nEncerrado. Mouse físico liberado.")
sys.exit(0)
signal.signal(signal.SIGINT, cleanup_and_exit)
signal.signal(signal.SIGTERM, cleanup_and_exit)
# Dá um "grab" no device real (o SO para de ouvir o mouse físico defeituoso)
dev.grab()
last_press_time = {}
ignore_buttons = set()
# Botões que vamos filtrar (pode incluir SIDE/EXTRA se quiser)
watched_buttons = {ecodes.BTN_LEFT, ecodes.BTN_RIGHT, ecodes.BTN_MIDDLE}
print(f"Monitorando {MOUSE_DEV} (Threshold: {DEBOUNCE_SECONDS * 1000:.0f}ms)...")
try:
for event in dev.read_loop():
# Tratamos apenas botões (EV_KEY), movimento (EV_REL) e sincronização (EV_SYN)
if event.type not in (ecodes.EV_KEY, ecodes.EV_REL, ecodes.EV_SYN):
continue
if event.type == ecodes.EV_KEY and event.code in watched_buttons:
btn = event.code
t = event.sec + event.usec / 1_000_000.0
if event.value == 1: # 1 = Press
last = last_press_time.get(btn, 0.0)
dt = t - last
dt_ms = dt * 1000.0
if dt < DEBOUNCE_SECONDS:
now_str = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
print(f"[{now_str}] ⚡ Filtro ativado! Clique fantasma no botão {btn} ({dt_ms:.1f} ms)")
ignore_buttons.add(btn)
continue
else:
last_press_time[btn] = t
# Se estamos ignorando esse botão, descartamos até o Release
if btn in ignore_buttons:
if event.value == 0: # 0 = Release
ignore_buttons.discard(btn)
continue
# Repassa o evento filtrado para o mouse virtual
if event.type == ecodes.EV_SYN:
ui.syn()
else:
ui.write_event(event)
finally:
cleanup_and_exit()
if __name__ == '__main__':
main()
O “log da vergonha” do hardware
Sem filtro, isso virava clique errado na prática:
[09:43:36.184] ⚡ Filtro ativado! Clique fantasma no botão 272 (96.1 ms)
[09:47:34.983] ⚡ Filtro ativado! Clique fantasma no botão 272 (32.0 ms)
[09:55:25.742] ⚡ Filtro ativado! Clique fantasma no botão 272 (31.7 ms)
[11:13:20.114] ⚡ Filtro ativado! Clique fantasma no botão 272 (31.4 ms)
[11:17:26.591] ⚡ Filtro ativado! Clique fantasma no botão 272 (27.9 ms)
Resultado
- Economizei um mouse novo (por enquanto kkkk).
- Voltei a ter paz pra trabalhar sem “duplo clique do nada”.
- Não obtive muito bons resultados em arrastar arquivos sem "soltar" inesperadamente mas resolveu o duplo click que acava executando outras ações.