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