solaar: use bluez dbus signals to disconnect and connect bluetooth devices
This commit is contained in:
parent
d7ce636917
commit
e667d41c7b
|
@ -133,21 +133,22 @@ for the step-by-step procedure for manual installation.
|
|||
|
||||
## Known Issues
|
||||
|
||||
- Bluez 5.73 interacts badly with Solaar (and with the Linux driver for Logitech devices).
|
||||
Bluetooth-connected devices will revert to default settings when reconnecting after going into power-saving mode or being turned off.
|
||||
One way to recover is to quit Solaar and restart it.
|
||||
|
||||
- Solaar expects that it has exclusive control over settings that are not ignored.
|
||||
Running other programs that modify these settings, such as logiops,
|
||||
will likely result in unexpected device behavior.
|
||||
- Bluez 5.73 does not remove Bluetooth devices when they disconnect.
|
||||
Solaar 1.1.12 processes the DBus disconnection and connection messages from Bluez and does re-initialize devices when they reconnect.
|
||||
The HID++ driver does not re-initialize devices, which causes problems with smooth scrolling.
|
||||
Until the problem is resolved having Scroll Wheel Resolution set to true (and not ignored) may be helpful.
|
||||
|
||||
- The Linux HID++ driver modifies the Scroll Wheel Resolution setting to
|
||||
implement smooth scrolling. If Solaar later changes this setting, scrolling
|
||||
implement smooth scrolling. If Solaar changes this setting, scrolling
|
||||
can be either very fast or very slow. To fix this problem
|
||||
click on the icon at the right edge of the setting to set it to
|
||||
"Ignore this setting", which is the default for new devices.
|
||||
The mouse has to be reset (e.g., by turning it off and on again) before this fix will take effect.
|
||||
|
||||
- Solaar expects that it has exclusive control over settings that are not ignored.
|
||||
Running other programs that modify these settings, such as logiops,
|
||||
will likely result in unexpected device behavior.
|
||||
|
||||
- The driver also sets the scrolling direction to its normal setting when implementing smooth scrolling.
|
||||
This can interfere with the Scroll Wheel Direction setting, requiring flipping this setting back and forth
|
||||
to restore reversed scrolling.
|
||||
|
|
|
@ -85,6 +85,7 @@ class Device:
|
|||
self.hidpp_short = device_info.hidpp_short if device_info else None
|
||||
self.hidpp_long = device_info.hidpp_long if device_info else None
|
||||
self.bluetooth = device_info.bus_id == 0x0005 if device_info else False # Bluetooth needs long messages
|
||||
self.hid_serial = device_info.serial if device_info else None
|
||||
self.setting_callback = setting_callback # for changes to settings
|
||||
self.status_callback = None # for changes to other potentially visible aspects
|
||||
self.wpid = pairing_info["wpid"] if pairing_info else None # the Wireless PID is unique per device model
|
||||
|
@ -111,6 +112,7 @@ class Device:
|
|||
self._settings_lock = _threading.Lock()
|
||||
self._persister_lock = _threading.Lock()
|
||||
self._notification_handlers = {} # See `add_notification_handler`
|
||||
self.cleanups = [] # functions to run on the device when it is closed
|
||||
|
||||
if not self.path:
|
||||
self.path = _hid.find_paired_node(receiver.path, number, 1) if receiver else None
|
||||
|
@ -510,6 +512,9 @@ class Device:
|
|||
handle, self.handle = self.handle, None
|
||||
if self in Device.instances:
|
||||
Device.instances.remove(self)
|
||||
if hasattr(self, "cleanups"):
|
||||
for cleanup in self.cleanups:
|
||||
cleanup(self)
|
||||
return handle and base.close(handle)
|
||||
|
||||
def __index__(self):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
## Copyright (C) 2014-2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
|
@ -18,64 +19,59 @@ import logging
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#
|
||||
# As suggested here: http://stackoverflow.com/a/13548984
|
||||
#
|
||||
try:
|
||||
import dbus
|
||||
|
||||
from dbus.mainloop.glib import DBusGMainLoop # integration into the main GLib loop
|
||||
|
||||
DBusGMainLoop(set_as_default=True)
|
||||
bus = dbus.SystemBus()
|
||||
assert bus
|
||||
|
||||
except Exception:
|
||||
# Either the dbus library is not available or the system dbus is not running
|
||||
logger.warning("failed to set up dbus")
|
||||
pass
|
||||
|
||||
|
||||
_suspend_callback = None
|
||||
|
||||
|
||||
def _suspend():
|
||||
if _suspend_callback:
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger.info("received suspend event")
|
||||
_suspend_callback()
|
||||
|
||||
|
||||
_resume_callback = None
|
||||
|
||||
|
||||
def _resume():
|
||||
if _resume_callback:
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger.info("received resume event")
|
||||
def _suspend_or_resume(suspend):
|
||||
if suspend is True and _suspend_callback:
|
||||
_suspend_callback()
|
||||
if suspend is False and _resume_callback:
|
||||
_resume_callback()
|
||||
|
||||
|
||||
def _suspend_or_resume(suspend):
|
||||
_suspend() if suspend else _resume()
|
||||
_LOGIND_PATH = "/org/freedesktop/login1"
|
||||
_LOGIND_INTERFACE = "org.freedesktop.login1.Manager"
|
||||
|
||||
|
||||
def watch(on_resume_callback=None, on_suspend_callback=None):
|
||||
def watch_suspend_resume(on_resume_callback=None, on_suspend_callback=None):
|
||||
"""Register callback for suspend/resume events.
|
||||
They are called only if the system DBus is running, and the Login daemon is available."""
|
||||
global _resume_callback, _suspend_callback
|
||||
_suspend_callback = on_suspend_callback
|
||||
_resume_callback = on_resume_callback
|
||||
|
||||
|
||||
try:
|
||||
import dbus
|
||||
|
||||
_LOGIND_BUS = "org.freedesktop.login1"
|
||||
_LOGIND_INTERFACE = "org.freedesktop.login1.Manager"
|
||||
|
||||
# integration into the main GLib loop
|
||||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
|
||||
DBusGMainLoop(set_as_default=True)
|
||||
|
||||
bus = dbus.SystemBus()
|
||||
assert bus
|
||||
|
||||
bus.add_signal_receiver(_suspend_or_resume, "PrepareForSleep", dbus_interface=_LOGIND_INTERFACE, bus_name=_LOGIND_BUS)
|
||||
|
||||
if on_resume_callback is not None or on_suspend_callback is not None:
|
||||
bus.add_signal_receiver(_suspend_or_resume, "PrepareForSleep", dbus_interface=_LOGIND_INTERFACE, path=_LOGIND_PATH)
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger.info("connected to system dbus, watching for suspend/resume events")
|
||||
|
||||
except Exception:
|
||||
# Either:
|
||||
# - the dbus library is not available
|
||||
# - the system dbus is not running
|
||||
logger.warning("failed to register suspend/resume callbacks")
|
||||
pass
|
||||
|
||||
_BLUETOOTH_PATH_PREFIX = "/org/bluez/hci0/dev_"
|
||||
_BLUETOOTH_INTERFACE = "org.freedesktop.DBus.Properties"
|
||||
|
||||
_bluetooth_callbacks = {}
|
||||
|
||||
|
||||
def watch_bluez_connect(serial, callback=None):
|
||||
if _bluetooth_callbacks.get(serial):
|
||||
_bluetooth_callbacks.get(serial).remove()
|
||||
path = _BLUETOOTH_PATH_PREFIX + serial.replace(":", "_").upper()
|
||||
if callback is not None:
|
||||
_bluetooth_callbacks[serial] = bus.add_signal_receiver(
|
||||
callback, "PropertiesChanged", path=path, dbus_interface=_BLUETOOTH_INTERFACE
|
||||
)
|
|
@ -30,11 +30,11 @@ from traceback import format_exc
|
|||
|
||||
import solaar.cli as _cli
|
||||
import solaar.configuration as _configuration
|
||||
import solaar.dbus as _dbus
|
||||
import solaar.i18n as _i18n
|
||||
import solaar.listener as _listener
|
||||
import solaar.ui as _ui
|
||||
import solaar.ui.common as _common
|
||||
import solaar.upower as _upower
|
||||
|
||||
from solaar import NAME
|
||||
from solaar import __version__
|
||||
|
@ -172,9 +172,9 @@ def main():
|
|||
_listener.setup_scanner(_ui.status_changed, _ui.setting_changed, _common.error_dialog)
|
||||
|
||||
if args.restart_on_wake_up:
|
||||
_upower.watch(_listener.start_all, _listener.stop_all)
|
||||
_dbus.watch_suspend_resume(_listener.start_all, _listener.stop_all)
|
||||
else:
|
||||
_upower.watch(lambda: _listener.ping_all(True))
|
||||
_dbus.watch_suspend_resume(lambda: _listener.ping_all(True))
|
||||
|
||||
_configuration.defer_saves = True # allow configuration saves to be deferred
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import subprocess
|
|||
import time
|
||||
|
||||
from collections import namedtuple
|
||||
from functools import partial
|
||||
|
||||
import gi
|
||||
import logitech_receiver.device as _device
|
||||
|
@ -33,6 +34,8 @@ from logitech_receiver import listener as _listener
|
|||
from logitech_receiver import notifications as _notifications
|
||||
|
||||
from . import configuration
|
||||
from . import dbus
|
||||
from . import i18n
|
||||
|
||||
gi.require_version("Gtk", "3.0") # NOQA: E402
|
||||
from gi.repository import GLib # NOQA: E402 # isort:skip
|
||||
|
@ -52,8 +55,8 @@ def _ghost(device):
|
|||
return _GHOST_DEVICE(receiver=device.receiver, number=device.number, name=device.name, kind=device.kind, online=False)
|
||||
|
||||
|
||||
class ReceiverListener(_listener.EventsListener):
|
||||
"""Keeps the status of a Receiver or Device."""
|
||||
class SolaarListener(_listener.EventsListener):
|
||||
"""Keeps the status of a Receiver or Device (member name is receiver but it can also be a device)."""
|
||||
|
||||
def __init__(self, receiver, status_changed_callback):
|
||||
assert status_changed_callback
|
||||
|
@ -226,7 +229,28 @@ class ReceiverListener(_listener.EventsListener):
|
|||
dev.ping()
|
||||
|
||||
def __str__(self):
|
||||
return f"<ReceiverListener({self.receiver.path},{self.receiver.handle})>"
|
||||
return f"<SolaarListener({self.receiver.path},{self.receiver.handle})>"
|
||||
|
||||
|
||||
def _process_bluez_dbus(device, path, dictionary, signature):
|
||||
"""Process bluez dbus property changed signals for device status changes to discover disconnections and connections"""
|
||||
if device:
|
||||
if dictionary.get("Connected") is not None:
|
||||
connected = dictionary.get("Connected")
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger.info("bluez dbus for %s: %s", device, "CONNECTED" if connected else "DISCONNECTED")
|
||||
device.changed(connected, reason=i18n._("connected") if connected else i18n._("disconnected"))
|
||||
elif device is not None:
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger.info("bluez cleanup for %s", device)
|
||||
_cleanup_bluez_dbus(device)
|
||||
|
||||
|
||||
def _cleanup_bluez_dbus(device):
|
||||
"""Remove dbus signal receiver for device"""
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger.info("bluez cleanup for %s", device)
|
||||
dbus.watch_bluez_connect(device.hid_serial, None)
|
||||
|
||||
|
||||
_all_listeners = {} # all known receiver listeners, listeners that stop on their own may remain here
|
||||
|
@ -239,10 +263,14 @@ def _start(device_info):
|
|||
receiver = _receiver.ReceiverFactory.create_receiver(device_info, _setting_callback)
|
||||
else:
|
||||
receiver = _device.DeviceFactory.create_device(device_info, _setting_callback)
|
||||
if receiver:
|
||||
configuration.attach_to(receiver)
|
||||
if receiver.bluetooth and receiver.hid_serial:
|
||||
dbus.watch_bluez_connect(receiver.hid_serial, partial(_process_bluez_dbus, receiver))
|
||||
receiver.cleanups.append(_cleanup_bluez_dbus)
|
||||
|
||||
if receiver:
|
||||
rl = ReceiverListener(receiver, _status_callback)
|
||||
rl = SolaarListener(receiver, _status_callback)
|
||||
rl.start()
|
||||
_all_listeners[device_info.path] = rl
|
||||
return rl
|
||||
|
@ -343,7 +371,7 @@ def _process_receiver_event(action, device_info):
|
|||
# whatever the action, stop any previous receivers at this path
|
||||
listener_thread = _all_listeners.pop(device_info.path, None)
|
||||
if listener_thread is not None:
|
||||
assert isinstance(listener_thread, ReceiverListener)
|
||||
assert isinstance(listener_thread, SolaarListener)
|
||||
listener_thread.stop()
|
||||
if action == "add":
|
||||
_process_add(device_info, 3)
|
||||
|
|
|
@ -44,6 +44,7 @@ class DeviceInfo:
|
|||
hidpp_short: bool = False
|
||||
hidpp_long: bool = True
|
||||
bus_id: int = 0x0003 # USB
|
||||
serial: str = "aa:aa:aa;aa"
|
||||
|
||||
|
||||
di_bad_handle = DeviceInfo(None, product_id=0xCCCC)
|
||||
|
@ -258,7 +259,7 @@ class TestDevice(device.Device): # a fully functional Device but its HID++ func
|
|||
],
|
||||
)
|
||||
def test_Device_complex(device_info, responses, protocol, led, keys, remap, gestures, backlight, profiles, mocker):
|
||||
test_device = TestDevice(responses, None, None, online=True, device_info=device_info)
|
||||
test_device = TestDevice(responses, None, None, True, device_info=device_info)
|
||||
test_device._name = "TestDevice"
|
||||
test_device._protocol = protocol
|
||||
spy_request = mocker.spy(test_device, "request")
|
||||
|
@ -297,7 +298,7 @@ def test_Device_complex(device_info, responses, protocol, led, keys, remap, gest
|
|||
)
|
||||
def test_Device_settings(device_info, responses, protocol, p, persister, settings, mocker):
|
||||
mocker.patch("solaar.configuration.persister", return_value=p)
|
||||
test_device = TestDevice(responses, None, None, online=True, device_info=device_info)
|
||||
test_device = TestDevice(responses, None, None, True, device_info=device_info)
|
||||
test_device._name = "TestDevice"
|
||||
test_device._protocol = protocol
|
||||
|
||||
|
|
Loading…
Reference in New Issue