Skip to content

Commit

Permalink
solaar: use bluez dbus signals to disconnect and connect bluetooth de…
Browse files Browse the repository at this point in the history
…vices
  • Loading branch information
pfps committed Apr 18, 2024
1 parent d7ce636 commit c5c0758
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 55 deletions.
17 changes: 9 additions & 8 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions lib/logitech_receiver/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def __init__(self, receiver, number, online, pairing_info=None, handle=None, dev
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
Expand All @@ -111,6 +112,7 @@ def __init__(self, receiver, number, online, pairing_info=None, handle=None, dev
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
Expand Down Expand Up @@ -510,6 +512,8 @@ def close(self):
handle, self.handle = self.handle, None
if self in Device.instances:
Device.instances.remove(self)
for cleanup in self.cleanups:
cleanup(self)
return handle and base.close(handle)

def __index__(self):
Expand Down
72 changes: 34 additions & 38 deletions lib/solaar/upower.py → lib/solaar/dbus.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,64 +19,59 @@

logger = logging.getLogger(__name__)

#
# As suggested here: http://stackoverflow.com/a/13548984
#
try:
import dbus

_suspend_callback = None
from dbus.mainloop.glib import DBusGMainLoop # integration into the main GLib loop

DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
assert bus

def _suspend():
if _suspend_callback:
if logger.isEnabledFor(logging.INFO):
logger.info("received suspend event")
_suspend_callback()
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
_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
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")


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
_BLUETOOTH_PATH_PREFIX = "/org/bluez/hci0/dev_"
_BLUETOOTH_INTERFACE = "org.freedesktop.DBus.Properties"

bus.add_signal_receiver(_suspend_or_resume, "PrepareForSleep", dbus_interface=_LOGIND_INTERFACE, bus_name=_LOGIND_BUS)
_bluetooth_callbacks = {}

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
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
)
6 changes: 3 additions & 3 deletions lib/solaar/gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@

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__
Expand Down Expand Up @@ -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

Expand Down
40 changes: 34 additions & 6 deletions lib/solaar/listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import time

from collections import namedtuple
from functools import partial

import gi
import logitech_receiver.device as _device
Expand All @@ -33,6 +34,8 @@
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
Expand All @@ -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
Expand Down Expand Up @@ -226,7 +229,28 @@ def _notifications_handler(self, n):
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
Expand All @@ -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)
configuration.attach_to(receiver)
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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit c5c0758

Please sign in to comment.