diff --git a/lib/logitech/unifying_receiver/base.py b/lib/logitech/unifying_receiver/base.py index a2330389..e56242cb 100644 --- a/lib/logitech/unifying_receiver/base.py +++ b/lib/logitech/unifying_receiver/base.py @@ -266,7 +266,7 @@ def make_notification(devnumber, data): return a Notification tuple if it is.""" sub_id = ord(data[:1]) if sub_id & 0x80 == 0x80: - # if this is a HID++1.0 register r/w, bail out + # this is either a HID++1.0 register r/w, or an error reply return address = ord(data[1:2]) diff --git a/lib/logitech/unifying_receiver/hidpp10.py b/lib/logitech/unifying_receiver/hidpp10.py index 65ba3c50..9cb88c93 100644 --- a/lib/logitech/unifying_receiver/hidpp10.py +++ b/lib/logitech/unifying_receiver/hidpp10.py @@ -158,6 +158,7 @@ def get_serial(device): else: dev_id = 0x30 + device.number - 1 receiver = device.receiver + assert receiver.unifying_supported serial = read_register(receiver, 0x2B5, dev_id) if serial is not None: diff --git a/lib/logitech/unifying_receiver/receiver.py b/lib/logitech/unifying_receiver/receiver.py index 75ba20c5..eabb9ff7 100644 --- a/lib/logitech/unifying_receiver/receiver.py +++ b/lib/logitech/unifying_receiver/receiver.py @@ -116,22 +116,17 @@ class PairedDevice(object): @property def protocol(self): if self._protocol is None: - self._protocol = _base.ping(self.receiver.handle, self.number) - # if the ping failed, the peripheral is (almost) certainly offline - self.online = self._protocol is not None - - # use the descriptor only as a fallback, because it may not be 100% correct descriptor = _descriptors.DEVICES.get(self.codename) - if self._protocol is None: - if descriptor and descriptor.protocol is not None: + if descriptor: + if descriptor.protocol: self._protocol = descriptor.protocol - else: - if descriptor: - if descriptor.protocol is None: - _log.info("%s: descriptor has no protocol, should be %0.1f", self, self._protocol) - elif descriptor.protocol != self._protocol: - _log.error("%s: descriptor has wrong protocol %0.1f, should be %0.1f", - self, descriptor.protocol, self._protocol) + else: + _log.warn("%s: descriptor has no protocol, should be %0.1f", self, self._protocol) + + if self._protocol is None: + self._protocol = _base.ping(self.receiver.handle, self.number) + # if the ping failed, the peripheral is (almost) certainly offline + self.online = self._protocol is not None # _log.debug("device %d protocol %s", self.number, self._protocol) return self._protocol or 0 @@ -160,15 +155,12 @@ class PairedDevice(object): @property def kind(self): if self._kind is None: - # already handled in the constructor - # if self.receiver.unifying_supported: - # pair_info = self.receiver.read_register(0x2B5, 0x20 + self.number - 1) - # if pair_info: - # kind = ord(pair_info[7:8]) & 0x0F - # self._kind = _hidpp10.DEVICE_KIND[kind] - # if self.wpid is None: - # self.wpid = _strhex(pair_info[3:5]) - if self.protocol >= 2.0 and self.online: + if self.receiver.unifying_supported: + pair_info = self.receiver.read_register(0x2B5, 0x20 + self.number - 1) + if pair_info: + kind = ord(pair_info[7:8]) & 0x0F + self._kind = _hidpp10.DEVICE_KIND[kind] + if self._kind is None and self.protocol >= 2.0 and self.online: self._kind = _hidpp20.get_kind(self) if self._kind is None: descriptor = _descriptors.DEVICES.get(self.codename) @@ -287,13 +279,13 @@ class PairedDevice(object): __int__ = __index__ def __eq__(self, other): - return other is not None and self.kind == other.kind and self.serial == other.serial + return other is not None and self.kind == other.kind and self.wpid == other.wpid def __ne__(self, other): - return other is None or self.kind != other.kind or self.serial != other.serial + return other is None or self.kind != other.kind or self.wpid != other.wpid def __hash__(self): - return self.serial.__hash__() + return self.wpid.__hash__() __bool__ = __nonzero__ = lambda self: self.wpid is not None and self.number in self.receiver diff --git a/lib/logitech/unifying_receiver/status.py b/lib/logitech/unifying_receiver/status.py index c021a5f9..a60ce5c2 100644 --- a/lib/logitech/unifying_receiver/status.py +++ b/lib/logitech/unifying_receiver/status.py @@ -262,7 +262,7 @@ class DeviceStatus(dict): _log.debug("polling status of %s", d) # read these from the device, the UI may need them later - d.protocol, d.firmware, d.kind, d.name, d.settings, None + d.protocol, d.serial, d.firmware, d.kind, d.name, d.settings, None # make sure we know all the features of the device # if d.features: diff --git a/lib/solaar/listener.py b/lib/solaar/listener.py index 82123982..e12bf3b1 100644 --- a/lib/solaar/listener.py +++ b/lib/solaar/listener.py @@ -18,7 +18,7 @@ from logitech.unifying_receiver import (Receiver, # from collections import namedtuple -_GHOST_DEVICE = namedtuple('_GHOST_DEVICE', ['receiver', 'number', 'name', 'kind', 'serial', 'status', 'online']) +_GHOST_DEVICE = namedtuple('_GHOST_DEVICE', ['receiver', 'number', 'name', 'kind', 'status', 'online']) _GHOST_DEVICE.__bool__ = lambda self: False _GHOST_DEVICE.__nonzero__ = _GHOST_DEVICE.__bool__ del namedtuple @@ -29,7 +29,6 @@ def _ghost(device): number=device.number, name=device.name, kind=device.kind, - serial=device.serial, status=None, online=False) diff --git a/lib/solaar/ui/action.py b/lib/solaar/ui/action.py index 4ce6e89f..6055a30e 100644 --- a/lib/solaar/ui/action.py +++ b/lib/solaar/ui/action.py @@ -44,7 +44,7 @@ about = make('help-about', 'About ' + NAME, _show_about_window) from . import pair_window def pair(window, receiver): - assert receiver is not None + assert receiver assert receiver.kind is None pair_dialog = pair_window.create(receiver) @@ -58,7 +58,7 @@ def pair(window, receiver): from ..ui import error_dialog def unpair(window, device): - assert device is not None + assert device assert device.kind is not None qdialog = Gtk.MessageDialog(window, 0, diff --git a/lib/solaar/ui/config_panel.py b/lib/solaar/ui/config_panel.py index 87e2dabe..18dc71aa 100644 --- a/lib/solaar/ui/config_panel.py +++ b/lib/solaar/ui/config_panel.py @@ -81,9 +81,8 @@ def _combo_notify(cbbox, setting, spinner): # return True -def _create_sbox(s, device_id): +def _create_sbox(s): sbox = Gtk.HBox(homogeneous=False, spacing=6) - sbox.set_name(device_id) sbox.pack_start(Gtk.Label(s.label), False, False, 0) spinner = Gtk.Spinner() @@ -170,21 +169,24 @@ def create(): def update(device, is_online): assert _box is not None assert device + device_id = (device.receiver.path, device.number) # if the device changed since last update, clear the box first - if device.serial != _box._last_device: + if device_id != _box._last_device: _box.set_visible(False) - _box._last_device = device.serial + _box._last_device = device_id - # hide - _box.foreach(lambda x, s: x.set_visible(x.get_name() == s), device.serial) + # hide controls belonging to other devices + for k, sbox in _items.items(): + sbox = _items[k] + sbox.set_visible(k[0:2] == device_id) for s in device.settings: - k = device.serial + '_' + s.name + k = (device_id[0], device_id[1], s.name) if k in _items: sbox = _items[k] else: - sbox = _items[k] = _create_sbox(s, device.serial) + sbox = _items[k] = _create_sbox(s) _box.pack_start(sbox, False, False, 0) if is_online: @@ -194,16 +196,16 @@ def update(device, is_online): _box.set_visible(True) -def clean(device_id): +def clean(device): """Remove the controls for a given device serial. Needed after the device has been unpaired. """ assert _box is not None + device_id = (device.receiver.path, device.number) for k in list(_items.keys()): - sbox = _items[k] - if sbox.get_name() == device_id: + if k[0:2] == device_id: + _box.remove(_items[k]) del _items[k] - _box.remove(sbox) def destroy(): diff --git a/lib/solaar/ui/tray.py b/lib/solaar/ui/tray.py index 1ee49929..b065b8f5 100644 --- a/lib/solaar/ui/tray.py +++ b/lib/solaar/ui/tray.py @@ -24,6 +24,7 @@ from .window import popup as _window_popup, toggle as _window_toggle _TRAY_ICON_SIZE = 32 # pixels _MENU_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR +_RECEIVER_SEPARATOR = ('~', None, None, None) # # @@ -93,8 +94,8 @@ try: if not info[1]: # only conside peripherals continue - # compare peripheral serials - if info[1] == _picked_device[1]: + # compare peripherals + if info[0:2] == _picked_device[0:2]: if direction == ScrollDirection.UP and candidate: # select previous device break @@ -151,7 +152,7 @@ try: def _update_tray_icon(): if _picked_device: - _, _, name, _, device_status = _picked_device + _, _, name, device_status = _picked_device battery_level = device_status.get(_K.BATTERY_LEVEL) battery_charging = device_status.get(_K.BATTERY_CHARGING) tray_icon_name = _icons.battery(battery_level, battery_charging) @@ -209,7 +210,7 @@ except ImportError: _icon.set_tooltip_markup(tooltip) if _picked_device: - _, _, name, _, device_status = _picked_device + _, _, name, device_status = _picked_device battery_level = device_status.get(_K.BATTERY_LEVEL) battery_charging = device_status.get(_K.BATTERY_CHARGING) tray_icon_name = _icons.battery(battery_level, battery_charging) @@ -253,11 +254,10 @@ def _generate_tooltip_lines(): yield '%s' % NAME yield '' - for _, serial, name, _, status in _devices_info: - if serial is None: # receiver + for _, number, name, status in _devices_info: + if number is None: # receiver continue - p = str(status) if p: # does it have any properties to print? yield '%s' % name @@ -306,7 +306,7 @@ def _add_device(device): assert receiver_path index = None - for idx, (path, _, _, _, _) in enumerate(_devices_info): + for idx, (path, _, _, _) in enumerate(_devices_info): if path == receiver_path: # the first entry matching the receiver serial should be for the receiver itself index = idx + 1 @@ -315,8 +315,8 @@ def _add_device(device): # proper ordering (according to device.number) for a receiver's devices while True: - path, _, _, number, _ = _devices_info[index] - if path == '-': + path, number, _, _ = _devices_info[index] + if path == _RECEIVER_SEPARATOR[0]: break assert path == receiver_path assert number != device.number @@ -324,7 +324,8 @@ def _add_device(device): break index = index + 1 - new_device_info = (receiver_path, device.serial, device.name, device.number, device.status) + new_device_info = (receiver_path, device.number, device.name, device.status) + assert len(new_device_info) == len(_RECEIVER_SEPARATOR) _devices_info.insert(index, new_device_info) # label_prefix = b'\xE2\x94\x84 '.decode('utf-8') @@ -333,7 +334,7 @@ def _add_device(device): new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix + device.name) new_menu_item.set_image(Gtk.Image()) new_menu_item.show_all() - new_menu_item.connect('activate', _window_popup, receiver_path, device.serial) + new_menu_item.connect('activate', _window_popup, receiver_path, device.number) _menu.insert(new_menu_item, index) return index @@ -347,7 +348,7 @@ def _remove_device(index): removed_device = _devices_info.pop(index) global _picked_device - if _picked_device and _picked_device[1] == removed_device[1]: + if _picked_device and _picked_device[0:2] == removed_device[0:2]: # the current pick was unpaired _picked_device = None @@ -355,8 +356,9 @@ def _remove_device(index): def _add_receiver(receiver): index = len(_devices_info) - device_info = (receiver.path, None, receiver.name, None, None) - _devices_info.append(device_info) + new_receiver_info = (receiver.path, None, receiver.name, None) + assert len(new_receiver_info) == len(_RECEIVER_SEPARATOR) + _devices_info.append(new_receiver_info) new_menu_item = Gtk.ImageMenuItem.new_with_label(receiver.name) _menu.insert(new_menu_item, index) @@ -365,7 +367,7 @@ def _add_receiver(receiver): new_menu_item.show_all() new_menu_item.connect('activate', _window_popup, receiver.path) - _devices_info.append(('-', None, None, None, None)) + _devices_info.append(_RECEIVER_SEPARATOR) separator = Gtk.SeparatorMenuItem.new() separator.set_visible(True) _menu.insert(separator, index + 1) @@ -379,11 +381,11 @@ def _remove_receiver(receiver): # remove all entries in devices_info that match this receiver while index < len(_devices_info): - path, _, _, _, _ = _devices_info[index] + path, _, _, _ = _devices_info[index] if path == receiver.path: found = True _remove_device(index) - elif found and path == '-': + elif found and path == _RECEIVER_SEPARATOR[0]: # the separator after this receiver _remove_device(index) break @@ -411,10 +413,13 @@ def _update_menu_item(index, device): # # for which device to show the battery info in systray, if more than one +# it's actually an entry in _devices_info _picked_device = None # cached list of devices and some of their properties +# contains tuples of (receiver path, device number, name, status) _devices_info = [] + _menu = None _icon = None @@ -449,7 +454,7 @@ def update(device=None): receiver_path = device.path if is_alive: index = None - for idx, (path, _, _, _, _) in enumerate(_devices_info): + for idx, (path, _, _, _) in enumerate(_devices_info): if path == receiver_path: index = idx break @@ -464,8 +469,8 @@ def update(device=None): is_paired = bool(device) receiver_path = device.receiver.path index = None - for idx, (path, serial, name, _, _) in enumerate(_devices_info): - if path == receiver_path and serial == device.serial: + for idx, (path, number, _, _) in enumerate(_devices_info): + if path == receiver_path and number == device.number: index = idx if is_paired: diff --git a/lib/solaar/ui/window.py b/lib/solaar/ui/window.py index 2716c2c1..e077cbb4 100644 --- a/lib/solaar/ui/window.py +++ b/lib/solaar/ui/window.py @@ -8,7 +8,7 @@ from logging import getLogger, DEBUG as _DEBUG _log = getLogger(__name__) del getLogger -from gi.repository import Gtk, Gdk +from gi.repository import Gtk, Gdk, GLib from gi.repository.GObject import TYPE_PYOBJECT from solaar import NAME @@ -32,9 +32,9 @@ _INFO_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR _DEVICE_ICON_SIZE = Gtk.IconSize.DND # tree model columns -_COLUMN = _NamedInts(ID=0, ACTIVE=1, NAME=2, ICON=3, STATUS_ICON=4, DEVICE=5) -_COLUMN_TYPES = (str, bool, str, str, str, TYPE_PYOBJECT) -_TREE_SEPATATOR = (None, False, None, None, None, None) +_COLUMN = _NamedInts(PATH=0, NUMBER=1, ACTIVE=2, NAME=3, ICON=4, STATUS_ICON=5, DEVICE=6) +_COLUMN_TYPES = (str, int, bool, str, str, str, TYPE_PYOBJECT) +_TREE_SEPATATOR = (None, 0, False, None, None, None, None) _TOOLTIP_LINK_SECURE = 'The wireless link between this device and its receiver is not encrypted.' _TOOLTIP_LINK_INSECURE = ('The wireless link between this device and its receiver is not encrypted.\n' @@ -169,6 +169,7 @@ def _create_buttons_box(): assert _find_selected_device_id() is not None receiver = _find_selected_device() assert receiver is not None + assert bool(receiver) assert receiver.kind is None _action.pair(_window, receiver) @@ -179,6 +180,7 @@ def _create_buttons_box(): assert _find_selected_device_id() is not None device = _find_selected_device() assert device is not None + assert bool(device) assert device.kind is not None _action.unpair(_window, device) @@ -239,7 +241,7 @@ def _create_tree(model): tree.set_model(model) def _is_separator(model, item, _=None): - return model.get_value(item, _COLUMN.ID) is None + return model.get_value(item, _COLUMN.PATH) is None tree.set_row_separator_func(_is_separator, None) icon_cell_renderer = Gtk.CellRendererPixbuf() @@ -348,7 +350,8 @@ def _find_selected_device(): def _find_selected_device_id(): selection = _tree.get_selection() model, item = selection.get_selected() - return model.get_value(item, _COLUMN.ID) if item else None + if item: + return _model.get_value(item, _COLUMN.PATH), _model.get_value(item, _COLUMN.NUMBER) # triggered by changing selection in the tree @@ -363,17 +366,18 @@ def _receiver_row(receiver_path, receiver=None): item = _model.get_iter_first() while item: - if _model.get_value(item, _COLUMN.ID) == receiver_path: + # first row matching the path must be the receiver one + if _model.get_value(item, _COLUMN.PATH) == receiver_path: return item item = _model.iter_next(item) if not item and receiver: icon_name = _icons.device_icon_name(receiver.name) pairing_icon_name = '' - row_data = (receiver_path, True, receiver.name, icon_name, pairing_icon_name, receiver) - if _log.isEnabledFor(_DEBUG): - _log.debug("new receiver row %s", row_data) - # _log.debug("receiver %s", receiver) + row_data = (receiver_path, 0, True, receiver.name, icon_name, pairing_icon_name, receiver) + assert len(row_data) == len(_TREE_SEPATATOR) + # if _log.isEnabledFor(_DEBUG): + # _log.debug("new receiver row %s", row_data) item = _model.append(None, row_data) if _TREE_SEPATATOR: _model.append(None, _TREE_SEPATATOR) @@ -381,24 +385,25 @@ def _receiver_row(receiver_path, receiver=None): return item or None -def _device_row(receiver_path, device_serial, device=None): +def _device_row(receiver_path, device_number, device=None): assert receiver_path - assert device_serial + assert device_number is not None receiver_row = _receiver_row(receiver_path, None if device is None else device.receiver) item = _model.iter_children(receiver_row) while item: - if _model.get_value(item, _COLUMN.ID) == device_serial: + if ((_model.get_value(item, _COLUMN.PATH) == receiver_path) and + (_model.get_value(item, _COLUMN.NUMBER) == device_number)): return item item = _model.iter_next(item) if not item and device: icon_name = _icons.device_icon_name(device.name, device.kind) battery_icon_name = '' - row_data = (device_serial, bool(device.online), device.codename, icon_name, battery_icon_name, device) - if _log.isEnabledFor(_DEBUG): - _log.debug("new device row %s", row_data) - # _log.debug("device %s", device) + row_data = (receiver_path, device_number, bool(device.online), device.codename, icon_name, battery_icon_name, device) + assert len(row_data) == len(_TREE_SEPATATOR) + # if _log.isEnabledFor(_DEBUG): + # _log.debug("new device row %s", row_data) item = _model.append(receiver_row, row_data) return item or None @@ -407,18 +412,18 @@ def _device_row(receiver_path, device_serial, device=None): # # -def select(receiver_path, device_id=None): +def select(receiver_path, device_number=None): assert _window assert receiver_path is not None - if device_id is None: + if device_number is None: item = _receiver_row(receiver_path) else: - item = _device_row(receiver_path, device_id) + item = _device_row(receiver_path, device_number) if item: selection = _tree.get_selection() selection.select_iter(item) else: - _log.warn("select(%s, %s) failed to find an item", receiver_path, device_id) + _log.warn("select(%s, %s) failed to find an item", receiver_path, device_number) def _hide(w, _=None): @@ -451,8 +456,6 @@ def toggle(trigger=None): def _update_details(button): assert button visible = button.get_active() - device = _find_selected_device() - assert device if visible: # _details._text.set_markup('reading...') @@ -480,7 +483,9 @@ def _update_details(button): flag_names = ('(none)',) if flag_bits == 0 else _hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits) yield ('Notifications', ('\n%15s' % ' ').join(flag_names)) - items = _details_items(device) + selected_device = _find_selected_device() + assert selected_device + items = _details_items(selected_device) text = '\n'.join('%-13s: %s' % i for i in items if i) _details._text.set_markup('' + text + '') @@ -502,7 +507,7 @@ def _update_receiver_panel(receiver, panel, buttons, full=False): else: panel._count.set_markup(_NANO_RECEIVER_TEXT[1]) - is_pairing = receiver and receiver.status.lock_open + is_pairing = receiver.status.lock_open if is_pairing: panel._scanning.set_visible(True) if not panel._spinner.get_visible(): @@ -683,12 +688,13 @@ def update(device, need_popup=False): assert item if is_alive and item: - _model.set_value(item, _COLUMN.ACTIVE, True) - is_pairing = is_alive and device.status.lock_open + was_pairing = bool(_model.get_value(item, _COLUMN.STATUS_ICON)) + is_pairing = bool(device.status.lock_open) _model.set_value(item, _COLUMN.STATUS_ICON, 'network-wireless' if is_pairing else '') - if selected_device_id == device.path: - _update_info_panel(device, need_popup) + if selected_device_id == (device.path, 0): + full_update = need_popup or was_pairing != is_pairing + _update_info_panel(device, full=full_update) elif item: if _TREE_SEPATATOR: @@ -700,8 +706,8 @@ def update(device, need_popup=False): # peripheral is_paired = bool(device) assert device.receiver - assert device.serial - item = _device_row(device.receiver.path, device.serial, device if is_paired else None) + assert device.number is not None and device.number > 0, "invalid device number" + str(device.number) + item = _device_row(device.receiver.path, device.number, device if is_paired else None) if is_paired and item: was_online = _model.get_value(item, _COLUMN.ACTIVE) @@ -717,14 +723,14 @@ def update(device, need_popup=False): _model.set_value(item, _COLUMN.STATUS_ICON, icon_name) if selected_device_id is None: - select(device.receiver.path, device.serial) - elif selected_device_id == device.serial: + select(device.receiver.path, device.number) + elif selected_device_id == (device.receiver.path, device.number): full_update = need_popup or was_online != is_online _update_info_panel(device, full=full_update) elif item: _model.remove(item) - _config_panel.clean(device.serial) + _config_panel.clean(device) # make sure all rows are visible _tree.expand_all()