improve device detection when plugging/unplugging
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 19s
JS/TS / lint (pull_request) Successful in 19s
Python / fmt (pull_request) Successful in 36s
Python / lint (pull_request) Failing after 34s
Python / test (pull_request) Failing after 1m2s
Python / typecheck (pull_request) Failing after 1m11s
C++ / build (pull_request) Successful in 2m11s
Rust / fmt (pull_request) Successful in 52s
Rust / build (pull_request) Successful in 1m45s
Rust / clippy (pull_request) Successful in 1m35s
Python / buildcheck (pull_request) Successful in 2m44s
C++ / clang-tidy (pull_request) Successful in 3m37s

This commit is contained in:
2026-07-13 19:31:40 +02:00
parent bb50cf756d
commit a9bbb21f54
+139 -5
View File
@@ -292,6 +292,7 @@ class _ListenerRegistry:
self._rescan_lock = threading.Lock()
self._min_rescan_interval = 1.0
self._last_rescan_time = 0.0
self._hidpp_io_lock = threading.RLock()
def _touch_and_notify(self, dev_or_receiver, *args, **kwargs):
@@ -309,6 +310,7 @@ class _ListenerRegistry:
return
if not self._rescan_lock.acquire(blocking=False):
return
try:
self._last_rescan_time = now
with self._hidpp_io_lock:
@@ -321,9 +323,38 @@ class _ListenerRegistry:
if not listener._active
]
for p in dead:
dying_obj, _dying_listener = self._entries[p]
if dying_obj.isDevice:
ident = getattr(
dying_obj, "unitId", None
) or getattr(dying_obj, "serial", None)
info = getattr(dying_obj, "battery_info", None)
if ident and info is not None:
for (
other_path,
(other_obj, _other_listener),
) in self._entries.items():
if other_path == p or other_obj.isDevice:
continue
for child in other_obj:
child_ident = getattr(
child, "unitId", None
) or getattr(child, "serial", None)
if child_ident == ident:
child.set_battery_info(info)
break
del self._entries[p]
known_paths = set(self._entries.keys())
with self._lock:
id_to_entry = {}
for path, (obj, listener) in self._entries.items():
if obj.isDevice:
ident = obj.unitId or obj.serial
if ident:
id_to_entry[ident] = (path, obj, listener)
for dev_info in base.receivers_and_devices():
if dev_info.path in known_paths:
continue
@@ -350,17 +381,108 @@ class _ListenerRegistry:
continue
listener = ListenerClass(obj, self._touch_and_notify)
listener.start()
if obj.isDevice:
ident = None
try:
if obj.protocol >= 2.0:
obj.get_ids()
ident = obj.unitId or obj.serial
except Exception:
pass
if ident:
with self._lock:
existing = id_to_entry.get(ident)
if existing:
old_path, old_obj, old_listener = existing
if old_path != dev_info.path:
logger.info(
"replacing device %s (old path %s) with new path %s",
ident,
old_path,
dev_info.path,
)
if (
getattr(obj, "battery_info", None)
is None
and getattr(
old_obj, "battery_info", None
)
is not None
):
obj.set_battery_info(
old_obj.battery_info
)
old_listener.stop()
del self._entries[old_path]
self._entries[dev_info.path] = (
obj,
listener,
)
self._last_change[id(obj)] = time.time()
to_join = old_listener
break
else:
pass
else:
if (
getattr(obj, "battery_info", None)
is None
):
for (
other_path,
(other_obj, _other_listener),
) in self._entries.items():
if other_obj.isDevice:
continue
for child in other_obj:
child_ident = getattr(
child, "unitId", None
) or getattr(
child, "serial", None
)
if child_ident == ident:
obj.set_battery_info(
child.battery_info
)
break
if (
getattr(
obj,
"battery_info",
None,
)
is not None
):
break
with self._lock:
self._entries[dev_info.path] = (obj, listener)
self._last_change[id(obj)] = time.time()
if dev_info.path not in self._entries:
self._entries[dev_info.path] = (obj, listener)
self._last_change[id(obj)] = time.time()
listener.start()
logger.info(
"listening on %s (%s)",
dev_info.path,
"device" if dev_info.isDevice else "receiver",
)
if "to_join" in locals():
try:
to_join.join(timeout=1.0)
except Exception:
logger.exception("error joining replaced listener")
del to_join
finally:
self._rescan_lock.release()
@@ -610,10 +732,22 @@ def daemon(
pending_write_lock = threading.Lock()
WRITE_SETTLE_SECONDS = 0.3
write_seq_lock = threading.Lock()
write_seq = 0
last_written_seq = 0
def write_if_changed():
nonlocal last_payload
nonlocal last_payload, write_seq, last_written_seq
with write_seq_lock:
write_seq += 1
my_seq = write_seq
results = _snapshot(registry)
with write_lock:
results = _snapshot(registry)
with write_seq_lock:
if my_seq <= last_written_seq:
return
last_written_seq = my_seq
payload = [asdict(r) for r in results]
if payload != last_payload:
_write_json_atomic(out, results)