diff --git a/lib/logitech_receiver/receiver.py b/lib/logitech_receiver/receiver.py
index 86840f7e..0943b5a3 100644
--- a/lib/logitech_receiver/receiver.py
+++ b/lib/logitech_receiver/receiver.py
@@ -249,6 +249,9 @@ class Receiver:
if bool(self):
return _base.request(self.handle, 0xFF, request_id, *params)
+ def reset_pairing(self):
+ self.pairing = Pairing()
+
read_register = hidpp10.read_register
write_register = hidpp10.write_register
diff --git a/lib/solaar/ui/pair_window.py b/lib/solaar/ui/pair_window.py
index 5899be32..4ea1fc60 100644
--- a/lib/solaar/ui/pair_window.py
+++ b/lib/solaar/ui/pair_window.py
@@ -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
@@ -27,97 +28,125 @@ from . import icons as _icons
logger = logging.getLogger(__name__)
-#
-#
-#
_PAIRING_TIMEOUT = 30 # seconds
_STATUS_CHECK = 500 # milliseconds
-address = kind = authentication = name = passcode = None
+
+def create(receiver):
+ receiver.reset_pairing() # clear out any information on previous pairing
+ title = _("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name}
+ if receiver.receiver_kind == "bolt":
+ text = _("Bolt receivers are only compatible with Bolt devices.")
+ text += "\n\n"
+ text += _("Press a pairing button or key until the pairing light flashes quickly.")
+ else:
+ if receiver.receiver_kind == "unifying":
+ text = _("Unifying receivers are only compatible with Unifying devices.")
+ else:
+ text = _("Other receivers are only compatible with a few devices.")
+ text += "\n\n"
+ text += _("Turn on the device you want to pair.")
+ text += _("The device must not be paired with a nearby powered-on receiver.")
+ text += "\n"
+ text += _("If the device is already turned on, turn it off and on again.")
+ if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0:
+ text += (
+ ngettext(
+ "\n\nThis receiver has %d pairing remaining.",
+ "\n\nThis receiver has %d pairings remaining.",
+ receiver.remaining_pairings(),
+ )
+ % receiver.remaining_pairings()
+ )
+ text += _("\nCancelling at this point will not use up a pairing.")
+ ok = prepare(receiver)
+ assistant = _create_assistant(receiver, ok, _finish, title, text)
+ if ok:
+ GLib.timeout_add(_STATUS_CHECK, check_lock_state, assistant, receiver)
+ return assistant
-def _create_page(assistant, kind, header=None, icon_name=None, text=None):
- p = Gtk.VBox(homogeneous=False, spacing=8)
- assistant.append_page(p)
- assistant.set_page_type(p, kind)
-
- if header:
- item = Gtk.HBox(homogeneous=False, spacing=16)
- p.pack_start(item, False, True, 0)
-
- label = Gtk.Label(label=header)
- # deprecated - not needed label.set_alignment(0, 0)
- label.set_line_wrap(True)
- item.pack_start(label, True, True, 0)
-
- if icon_name:
- icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
- # deprecated - not needed icon.set_alignment(1, 0)
- item.pack_start(icon, False, False, 0)
-
- if text:
- label = Gtk.Label(label=text)
- # deprecated - not needed label.set_alignment(0, 0)
- label.set_line_wrap(True)
- p.pack_start(label, False, False, 0)
-
- p.show_all()
- return p
+def prepare(receiver):
+ if receiver.receiver_kind == "bolt":
+ if receiver.discover(timeout=_PAIRING_TIMEOUT):
+ return True
+ else:
+ receiver.pairing.error = "discovery did not start"
+ return False
+ elif receiver.set_lock(False, timeout=_PAIRING_TIMEOUT):
+ return True
+ else:
+ receiver.pairing.error = "the pairing lock did not open"
+ return False
-def _check_lock_state(assistant, receiver, count=2):
- global address, kind, authentication, name, passcode
-
+def check_lock_state(assistant, receiver, count=2):
if not assistant.is_drawable():
if logger.isEnabledFor(logging.DEBUG):
logger.debug("assistant %s destroyed, bailing out", assistant)
return False
+ return _check_lock_state(assistant, receiver, count)
+
+def _check_lock_state(assistant, receiver, count):
if receiver.pairing.error:
- # receiver.pairing.new_device = _fake_device(receiver)
_pairing_failed(assistant, receiver, receiver.pairing.error)
- receiver.pairing.error = None
return False
-
- if receiver.pairing.new_device:
+ elif receiver.pairing.new_device:
receiver.remaining_pairings(False) # Update remaining pairings
- device, receiver.pairing.new_device = receiver.pairing.new_device, None
- _pairing_succeeded(assistant, receiver, device)
+ _pairing_succeeded(assistant, receiver, receiver.pairing.new_device)
return False
- elif receiver.pairing.device_address and receiver.pairing.device_name and not address:
- address = receiver.pairing.device_address
- name = receiver.pairing.device_name
- kind = receiver.pairing.device_kind
- authentication = receiver.pairing.device_authentication
- name = receiver.pairing.device_name
- entropy = 10
- if kind == _hidpp10_constants.DEVICE_KIND.keyboard:
- entropy = 20
- if receiver.pair_device(
- address=address,
- authentication=authentication,
- entropy=entropy,
- ):
+ elif not receiver.pairing.lock_open and not receiver.pairing.discovering:
+ if count > 0:
+ # the actual device notification may arrive later so have a little patience
+ GLib.timeout_add(_STATUS_CHECK, check_lock_state, assistant, receiver, count - 1)
+ else:
+ _pairing_failed(assistant, receiver, "failed to open pairing lock")
+ return False
+ elif receiver.pairing.lock_open and receiver.pairing.device_passkey:
+ _show_passcode(assistant, receiver, receiver.pairing.device_passkey)
+ return True
+ elif receiver.pairing.discovering and receiver.pairing.device_address and receiver.pairing.device_name:
+ add = receiver.pairing.device_address
+ ent = 20 if receiver.pairing.device_kind == _hidpp10_constants.DEVICE_KIND.keyboard else 10
+ if receiver.pair_device(address=add, authentication=receiver.pairing.device_authentication, entropy=ent):
return True
else:
_pairing_failed(assistant, receiver, "failed to open pairing lock")
return False
- elif address and receiver.pairing.device_passkey and not passcode:
- passcode = receiver.pairing.device_passkey
- _show_passcode(assistant, receiver, passcode)
- return True
-
- if not receiver.pairing.lock_open and not receiver.pairing.discovering:
- if count > 0:
- # the actual device notification may arrive later so have a little patience
- GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver, count - 1)
- else:
- _pairing_failed(assistant, receiver, "failed to open pairing lock")
- return False
-
return True
+def _pairing_failed(assistant, receiver, error):
+ assistant.remove_page(0) # needed to reset the window size
+ if logger.isEnabledFor(logging.DEBUG):
+ logger.debug("%s fail: %s", receiver, error)
+ _create_failure_page(assistant, error)
+
+
+def _pairing_succeeded(assistant, receiver, device):
+ assistant.remove_page(0) # needed to reset the window size
+ if logger.isEnabledFor(logging.DEBUG):
+ logger.debug("%s success: %s", receiver, device)
+ _create_success_page(assistant, device)
+
+
+def _finish(assistant, receiver):
+ if logger.isEnabledFor(logging.DEBUG):
+ logger.debug("finish %s", assistant)
+ assistant.destroy()
+ receiver.pairing.new_device = None
+ if receiver.pairing.lock_open:
+ if receiver.receiver_kind == "bolt":
+ receiver.pair_device("cancel")
+ else:
+ receiver.set_lock()
+ if receiver.pairing.discovering:
+ receiver.discover(True)
+ if not receiver.pairing.lock_open and not receiver.pairing.discovering:
+ receiver.pairing.error = None
+
+
def _show_passcode(assistant, receiver, passkey):
if logger.isEnabledFor(logging.DEBUG):
logger.debug("%s show passkey: %s", receiver, passkey)
@@ -138,57 +167,57 @@ def _show_passcode(assistant, receiver, passkey):
assistant.next_page()
-def _prepare(assistant, page, receiver):
- index = assistant.get_current_page()
- if logger.isEnabledFor(logging.DEBUG):
- logger.debug("prepare %s %d %s", assistant, index, page)
-
- if index == 0:
- if receiver.receiver_kind == "bolt":
- if receiver.discover(timeout=_PAIRING_TIMEOUT):
- assert receiver.pairing.new_device is None
- assert receiver.pairing.error is None
- spinner = page.get_children()[-1]
- spinner.start()
- GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver)
- assistant.set_page_complete(page, True)
- else:
- GLib.idle_add(_pairing_failed, assistant, receiver, "discovery did not start")
- elif receiver.set_lock(False, timeout=_PAIRING_TIMEOUT):
- assert receiver.pairing.new_device is None
- assert receiver.pairing.error is None
- spinner = page.get_children()[-1]
- spinner.start()
- GLib.timeout_add(_STATUS_CHECK, _check_lock_state, assistant, receiver)
- assistant.set_page_complete(page, True)
- else:
- GLib.idle_add(_pairing_failed, assistant, receiver, "the pairing lock did not open")
+def _create_assistant(receiver, ok, finish, title, text):
+ assistant = Gtk.Assistant()
+ assistant.set_title(title)
+ assistant.set_icon_name("list-add")
+ assistant.set_size_request(400, 240)
+ assistant.set_resizable(False)
+ assistant.set_role("pair-device")
+ if ok:
+ page_intro = _create_page(assistant, Gtk.AssistantPageType.PROGRESS, title, "preferences-desktop-peripherals", text)
+ spinner = Gtk.Spinner()
+ spinner.set_visible(True)
+ spinner.start()
+ page_intro.pack_end(spinner, True, True, 24)
+ assistant.set_page_complete(page_intro, True)
else:
- assistant.remove_page(0)
+ page_intro = _create_failure_page(assistant, receiver.pairing.error)
+ assistant.connect("cancel", finish, receiver)
+ assistant.connect("close", finish, receiver)
+ return assistant
-def _finish(assistant, receiver):
- if logger.isEnabledFor(logging.DEBUG):
- logger.debug("finish %s", assistant)
- assistant.destroy()
- receiver.pairing.new_device = None
- if receiver.pairing.lock_open:
- if receiver.receiver_kind == "bolt":
- receiver.pair_device("cancel")
- else:
- receiver.set_lock()
- if receiver.pairing.discovering:
- receiver.discover(True)
- if not receiver.pairing.lock_open and not receiver.pairing.discovering:
- receiver.pairing.error = None
-
-
-def _pairing_failed(assistant, receiver, error):
- if logger.isEnabledFor(logging.DEBUG):
- logger.debug("%s fail: %s", receiver, error)
+def _create_success_page(assistant, device):
+ def _check_encrypted(device, assistant, hbox):
+ if assistant.is_drawable() and device.link_encrypted is False:
+ hbox.pack_start(Gtk.Image.new_from_icon_name("security-low", Gtk.IconSize.MENU), False, False, 0)
+ hbox.pack_start(Gtk.Label(label=_("The wireless link is not encrypted")), False, False, 0)
+ hbox.show_all()
+ return False
+ page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY)
+ header = Gtk.Label(label=_("Found a new device:"))
+ page.pack_start(header, False, False, 0)
+ device_icon = Gtk.Image()
+ icon_name = _icons.device_icon_name(device.name, device.kind)
+ device_icon.set_from_icon_name(icon_name, _icons.LARGE_SIZE)
+ page.pack_start(device_icon, True, True, 0)
+ device_label = Gtk.Label()
+ device_label.set_markup(f"{device.name}")
+ page.pack_start(device_label, True, True, 0)
+ hbox = Gtk.HBox(homogeneous=False, spacing=8)
+ hbox.pack_start(Gtk.Label(label=" "), False, False, 0)
+ hbox.set_property("expand", False)
+ hbox.set_property("halign", Gtk.Align.CENTER)
+ page.pack_start(hbox, False, False, 0)
+ GLib.timeout_add(_STATUS_CHECK, _check_encrypted, device, assistant, hbox) # wait a bit to check link status
+ page.show_all()
+ assistant.next_page()
assistant.commit()
+
+def _create_failure_page(assistant, error):
header = _("Pairing failed") + ": " + _(str(error)) + "."
if "timeout" in str(error):
text = _("Make sure your device is within range, and has a decent battery charge.")
@@ -199,111 +228,26 @@ def _pairing_failed(assistant, receiver, error):
else:
text = _("No further details are available about the error.")
_create_page(assistant, Gtk.AssistantPageType.SUMMARY, header, "dialog-error", text)
-
assistant.next_page()
assistant.commit()
-def _pairing_succeeded(assistant, receiver, device):
- assert device
- if logger.isEnabledFor(logging.DEBUG):
- logger.debug("%s success: %s", receiver, device)
-
- page = _create_page(assistant, Gtk.AssistantPageType.SUMMARY)
-
- header = Gtk.Label(label=_("Found a new device:"))
- # deprecated - not needed header.set_alignment(0.5, 0)
- page.pack_start(header, False, False, 0)
-
- device_icon = Gtk.Image()
- icon_name = _icons.device_icon_name(device.name, device.kind)
- device_icon.set_from_icon_name(icon_name, _icons.LARGE_SIZE)
- # deprecated - not needed device_icon.set_alignment(0.5, 1)
- page.pack_start(device_icon, True, True, 0)
-
- device_label = Gtk.Label()
- device_label.set_markup(f"{device.name}")
- # deprecated - not needed device_label.set_alignment(0.5, 0)
- page.pack_start(device_label, True, True, 0)
-
- hbox = Gtk.HBox(homogeneous=False, spacing=8)
- hbox.pack_start(Gtk.Label(label=" "), False, False, 0)
- hbox.set_property("expand", False)
- hbox.set_property("halign", Gtk.Align.CENTER)
- page.pack_start(hbox, False, False, 0)
-
- def _check_encrypted(dev):
- if assistant.is_drawable():
- if device.link_encrypted is False:
- hbox.pack_start(Gtk.Image.new_from_icon_name("security-low", Gtk.IconSize.MENU), False, False, 0)
- hbox.pack_start(Gtk.Label(label=_("The wireless link is not encrypted") + "!"), False, False, 0)
- hbox.show_all()
- else:
- return True
-
- GLib.timeout_add(_STATUS_CHECK, _check_encrypted, device)
-
- page.show_all()
-
- assistant.next_page()
- assistant.commit()
-
-
-def create(receiver):
- assert receiver is not None
- assert receiver.kind is None
-
- global address, kind, authentication, name, passcode
- address = name = kind = authentication = passcode = None
-
- assistant = Gtk.Assistant()
- assistant.set_title(_("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name})
- assistant.set_icon_name("list-add")
-
- assistant.set_size_request(400, 240)
- assistant.set_resizable(False)
- assistant.set_role("pair-device")
-
- if receiver.receiver_kind == "unifying":
- page_text = _("Unifying receivers are only compatible with Unifying devices.")
- elif receiver.receiver_kind == "bolt":
- page_text = _("Bolt receivers are only compatible with Bolt devices.")
- else:
- page_text = _("Other receivers are only compatible with a few devices.")
- page_text += "\n"
- page_text += _("The device must not be paired with a nearby powered-on receiver.")
- page_text += "\n\n"
-
- if receiver.receiver_kind == "bolt":
- page_text += _("Press a pairing button or key until the pairing light flashes quickly.")
- page_text += "\n"
- page_text += _("You may have to first turn the device off and on again.")
- else:
- page_text += _("Turn on the device you want to pair.")
- page_text += "\n"
- page_text += _("If the device is already turned on, turn it off and on again.")
- if receiver.remaining_pairings() and receiver.remaining_pairings() >= 0:
- page_text += (
- ngettext(
- "\n\nThis receiver has %d pairing remaining.",
- "\n\nThis receiver has %d pairings remaining.",
- receiver.remaining_pairings(),
- )
- % receiver.remaining_pairings()
- )
- page_text += _("\nCancelling at this point will not use up a pairing.")
-
- intro_text = _("%(receiver_name)s: pair new device") % {"receiver_name": receiver.name}
-
- page_intro = _create_page(
- assistant, Gtk.AssistantPageType.PROGRESS, intro_text, "preferences-desktop-peripherals", page_text
- )
- spinner = Gtk.Spinner()
- spinner.set_visible(True)
- page_intro.pack_end(spinner, True, True, 24)
-
- assistant.connect("prepare", _prepare, receiver)
- assistant.connect("cancel", _finish, receiver)
- assistant.connect("close", _finish, receiver)
-
- return assistant
+def _create_page(assistant, kind, header=None, icon_name=None, text=None):
+ p = Gtk.VBox(homogeneous=False, spacing=8)
+ assistant.append_page(p)
+ assistant.set_page_type(p, kind)
+ if header:
+ item = Gtk.HBox(homogeneous=False, spacing=16)
+ p.pack_start(item, False, True, 0)
+ label = Gtk.Label(label=header)
+ label.set_line_wrap(True)
+ item.pack_start(label, True, True, 0)
+ if icon_name:
+ icon = Gtk.Image.new_from_icon_name(icon_name, Gtk.IconSize.DIALOG)
+ item.pack_start(icon, False, False, 0)
+ if text:
+ label = Gtk.Label(label=text)
+ label.set_line_wrap(True)
+ p.pack_start(label, False, False, 0)
+ p.show_all()
+ return p