diff --git a/app/pairing.py b/app/pairing.py index 60a6a170..15b24d12 100644 --- a/app/pairing.py +++ b/app/pairing.py @@ -4,9 +4,6 @@ from logging import getLogger as _Logger -from receiver import DeviceInfo as _DeviceInfo -from logitech.devices.constants import (STATUS, NAMES) - _l = _Logger('pairing') @@ -70,31 +67,8 @@ class State(object): _l.debug("event for new device? %s", event) if event.code == 0x10 and event.data[0:2] == b'\x41\x04': - state_code = ord(event.data[2:3]) & 0xF0 - state = STATUS.UNAVAILABLE if state_code == 0x60 else \ - STATUS.CONNECTED if state_code == 0xA0 else \ - STATUS.CONNECTED if state_code == 0x20 else \ - None - if state is None: - _l.warn("don't know how to handle status 0x%02x: %s", state_code, event) - elif event.devnumber < 1 or event.devnumber > self.max_devices: - _l.warn("got event for invalid device number %d: %s", event.devnumber, event) - else: - dev = _DeviceInfo(self._watcher.receiver, event.devnumber, state) - if state == STATUS.CONNECTED: - n, k = dev.name, dev.kind - _l.debug("detected active device %s", dev) - else: - # we can query the receiver for the device short name - dev_id = self.request(0xFF, b'\x83\xB5', event.data[4:5]) - if dev_id: - shortname = str(dev_id[2:].rstrip(b'\x00')) - if shortname in NAMES: - dev._name, dev._kind = NAMES[shortname] - _l.debug("detected new device %s", dev) - else: - _l.warn("could not properly detect inactive device %d: %s", event.devnumber, shortname) - self.detected_device = dev + self.detected_device = self._watcher.receiver.make_device(event) + return True return True diff --git a/app/receiver.py b/app/receiver.py index 6bd2b6de..b978e6e2 100644 --- a/app/receiver.py +++ b/app/receiver.py @@ -6,6 +6,7 @@ from logging import getLogger as _Logger _LOG_LEVEL = 6 from threading import Event as _Event +from binascii import hexlify as _hexlify from logitech.unifying_receiver import base as _base from logitech.unifying_receiver import api as _api @@ -26,6 +27,7 @@ class DeviceInfo(object): self.number = number self._name = None self._kind = None + self._serial = None self._firmware = None self._features = None @@ -80,6 +82,13 @@ class DeviceInfo(object): self._kind = self.receiver.call_api(_api.get_device_kind, self.number, self.features) return self._kind or '?' + @property + def serial(self): + if self._serial is None: + if self._status >= STATUS.CONNECTED: + pass + return self._serial or '?' + @property def firmware(self): if self._firmware is None: @@ -219,13 +228,14 @@ class Receiver(_listener.EventsListener): def serial(self): if self._serial is None: if self: - self._serial, firmware, bootloader = self.call_api(_api.get_receiver_info) - self._firmware = (firmware, bootloader) + self._serial, self._firmware = self.call_api(_api.get_receiver_info) return self._serial or '?' @property def firmware(self): - s = self.serial + if self._firmware is None: + if self: + self._serial, self._firmware = self.call_api(_api.get_receiver_info) return self._firmware or ('?', '?') @@ -239,38 +249,25 @@ class Receiver(_listener.EventsListener): return if event.code == 0x10 and event.data[0:2] == b'\x41\x04': - state_code = ord(event.data[2:3]) & 0xF0 - state = STATUS.UNAVAILABLE if state_code == 0x60 else \ - STATUS.CONNECTED if state_code == 0xA0 else \ - STATUS.CONNECTED if state_code == 0x20 else \ - None - if state is None: - self.LOG.warn("don't know how to handle status 0x%02x: %s", state_code, event) - return - if event.devnumber in self.devices: - self.devices[event.devnumber].status = state + state_code = ord(event.data[2:3]) & 0xF0 + state = STATUS.UNAVAILABLE if state_code == 0x60 else \ + STATUS.CONNECTED if state_code == 0xA0 else \ + STATUS.CONNECTED if state_code == 0x20 else \ + None + if state is None: + self.LOG.warn("don't know how to handle status 0x%02x: %s", state_code, event) + else: + self.devices[event.devnumber].status = state return - if event.devnumber < 1 or event.devnumber > self.max_devices: - self.LOG.warn("got event for invalid device number %d: %s", event.devnumber, event) - return - - dev = DeviceInfo(self, event.devnumber, state) - if state == STATUS.CONNECTED: - n, k = dev.name, dev.kind + dev = self.make_device(event) + if dev is None: + self.LOG.warn("failed to make new device from %s", event) else: - # we can query the receiver for the device short name - dev_id = self.request(0xFF, b'\x83\xB5', event.data[4:5]) - if dev_id: - shortname = str(dev_id[2:].rstrip(b'\x00')) - if shortname in NAMES: - dev._name, dev._kind = NAMES[shortname] - else: - self.LOG.warn("could not properly detect inactive device %d: %s", event.devnumber, shortname) - self.devices[event.devnumber] = dev - self.LOG.info("new device ready %s", dev) - self.status = STATUS.CONNECTED + len(self.devices) + self.devices[event.devnumber] = dev + self.LOG.info("new device ready %s", dev) + self.status = STATUS.CONNECTED + len(self.devices) return if event.devnumber == 0xFF: @@ -290,6 +287,40 @@ class Receiver(_listener.EventsListener): self.LOG.warn("don't know how to handle event %s", event) + def make_device(self, event): + if event.devnumber < 1 or event.devnumber > self.max_devices: + self.LOG.warn("got event for invalid device number %d: %s", event.devnumber, event) + return None + + state_code = ord(event.data[2:3]) & 0xF0 + state = STATUS.UNAVAILABLE if state_code == 0x60 else \ + STATUS.CONNECTED if state_code == 0xA0 else \ + STATUS.CONNECTED if state_code == 0x20 else \ + None + if state is None: + self.LOG.warn("don't know how to handle device status 0x%02x: %s", state_code, event) + return None + + dev = DeviceInfo(self, event.devnumber, state) + if state == STATUS.CONNECTED: + n, k = dev.name, dev.kind + else: + # we can query the receiver for the device short name + dev_id = self.request(0xFF, b'\x83\xB5', event.data[4:5]) + if dev_id: + shortname = dev_id[2:].rstrip(b'\x00').decode('ascii') + if shortname in NAMES: + dev._name, dev._kind = NAMES[shortname] + else: + self.LOG.warn("could not identify inactive device %d: %s", event.devnumber, shortname) + + b = bytearray(event.data[4:5]) + b[0] -= 0x10 + serial = self.request(0xFF, b'\x83\xB5', bytes(b)) + if serial: + dev._serial = _hexlify(serial[1:5]).decode('ascii').upper() + return dev + def __str__(self): return 'Receiver(%s,%x,%d:%d)' % (self.path, self._handle, self._active, self._status) diff --git a/app/ui/action.py b/app/ui/action.py index 11ff8bf4..35f681b3 100644 --- a/app/ui/action.py +++ b/app/ui/action.py @@ -56,3 +56,4 @@ def _pair_device(action, window, state): # pair_dialog.set_modal(True) pair_dialog.present() pair = _action('add', 'Pair new device', None) +pair.set_sensitive(False) diff --git a/app/ui/main_window.py b/app/ui/main_window.py index 797e2428..49d806b0 100644 --- a/app/ui/main_window.py +++ b/app/ui/main_window.py @@ -10,24 +10,198 @@ from logitech.devices.constants import (STATUS, PROPS) _SMALL_DEVICE_ICON_SIZE = Gtk.IconSize.BUTTON _DEVICE_ICON_SIZE = Gtk.IconSize.DIALOG -_STATUS_ICON_SIZE = Gtk.IconSize.DND +_STATUS_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR _PLACEHOLDER = '~' +# +# +# + +def _show_info(action, widget): + widget.set_visible(action.get_active()) + +def _receiver_box(name): + icon = Gtk.Image.new_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE) + + label = Gtk.Label('Initializing...') + label.set_name('status-label') + label.set_alignment(0, 0.5) + + toolbar = Gtk.Toolbar() + toolbar.set_name('buttons') + toolbar.set_style(Gtk.ToolbarStyle.ICONS) + toolbar.set_icon_size(Gtk.IconSize.MENU) + toolbar.set_show_arrow(False) + + hbox = Gtk.HBox(homogeneous=False, spacing=8) + hbox.pack_start(icon, False, False, 0) + hbox.pack_start(label, True, True, 0) + hbox.pack_end(toolbar, False, False, 0) + + info_label = Gtk.Label() + info_label.set_name('info-label') + info_label.set_alignment(0, 0.5) + info_label.set_padding(32, 2) + info_label.set_selectable(True) + + info_action = ui.action._toggle_action('info', 'Receiver info', _show_info, info_label) + toolbar.insert(info_action.create_tool_item(), 0) + toolbar.insert(ui.action.pair.create_tool_item(), -1) + + vbox = Gtk.VBox(homogeneous=False, spacing=4) + vbox.set_border_width(4) + vbox.pack_start(hbox, True, True, 0) + vbox.pack_start(info_label, True, True, 0) + + frame = Gtk.Frame() + frame.add(vbox) + frame.show_all() + info_label.set_visible(False) + return frame + + +def _device_box(): + icon = Gtk.Image.new_from_icon_name('image-missing', _DEVICE_ICON_SIZE) + icon.set_name('icon') + icon.set_alignment(0.5, 0) + + label = Gtk.Label('Initializing...') + label.set_name('label') + label.set_alignment(0, 0.5) + label.set_padding(0, 2) + + battery_icon = Gtk.Image.new_from_icon_name('battery_unknown', _STATUS_ICON_SIZE) + + battery_label = Gtk.Label() + battery_label.set_width_chars(6) + battery_label.set_alignment(0, 0.5) + + light_icon = Gtk.Image.new_from_icon_name('light_unknown', _STATUS_ICON_SIZE) + + light_label = Gtk.Label() + light_label.set_alignment(0, 0.5) + light_label.set_width_chars(8) + + toolbar = Gtk.Toolbar() + toolbar.set_name('buttons') + toolbar.set_style(Gtk.ToolbarStyle.ICONS) + toolbar.set_icon_size(Gtk.IconSize.MENU) + toolbar.set_show_arrow(False) + + status_box = Gtk.HBox(homogeneous=False, spacing=0) + status_box.set_name('status') + status_box.pack_start(battery_icon, False, True, 0) + status_box.pack_start(battery_label, False, True, 0) + status_box.pack_start(light_icon, False, True, 0) + status_box.pack_start(light_label, False, True, 0) + status_box.pack_end(toolbar, False, False, 0) + + info_label = Gtk.Label() + info_label.set_name('info-label') + info_label.set_alignment(0, 0.5) + info_label.set_padding(6, 2) + info_label.set_selectable(True) + + info_action = ui.action._toggle_action('info', 'Device info', _show_info, info_label) + toolbar.insert(info_action.create_tool_item(), 0) + + unpair_action = ui.action._action('remove', 'Unpair', None) + toolbar.insert(unpair_action.create_tool_item(), -1) + + vbox = Gtk.VBox(homogeneous=False, spacing=8) + vbox.pack_start(label, True, True, 0) + vbox.pack_start(status_box, True, True, 0) + vbox.pack_start(info_label, True, True, 0) + + box = Gtk.HBox(homogeneous=False, spacing=10) + box.set_border_width(4) + box.pack_start(icon, False, False, 0) + box.pack_start(vbox, True, True, 0) + box.show_all() + + frame = Gtk.Frame() + frame.add(box) + info_label.set_visible(False) + return frame + + +def toggle(window, trigger): + # print 'window toggle', window, trigger + if window.get_visible(): + position = window.get_position() + window.hide() + window.move(*position) + else: + if trigger and type(trigger) == Gtk.StatusIcon: + x, y = window.get_position() + if x == 0 and y == 0: + x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), trigger) + window.move(x, y) + window.present() + return True + + +def create(title, name, max_devices, systray=False): + window = Gtk.Window() + window.set_title(title) + window.set_icon_name(ui.appicon(0)) + window.set_role('status-window') + + vbox = Gtk.VBox(homogeneous=False, spacing=4) + vbox.set_border_width(4) + + rbox = _receiver_box(name) + vbox.add(rbox) + for i in range(1, 1 + max_devices): + dbox = _device_box() + vbox.add(dbox) + vbox.set_visible(True) + + window.add(vbox) + + geometry = Gdk.Geometry() + geometry.min_width = 360 + geometry.min_height = 20 + window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE) + window.set_resizable(False) + + window.toggle_visible = lambda i: toggle(window, i) + + if systray: + window.set_keep_above(True) + window.connect('delete-event', toggle) + else: + window.connect('delete-event', Gtk.main_quit) + + return window + +# +# +# + +def _info_text(dev): + fw_text = '\n'.join(['%-12s\t%s%s%s' % + (f.kind, f.name, ' ' if f.name else '', f.version) for f in dev.firmware]) + return ('' + 'Serial \t\t%s\n' + '%s' + '' % (dev.serial, fw_text)) + + +def _update_receiver_box(frame, receiver): + label, toolbar, info_label = ui.find_children(frame, 'status-label', 'buttons', 'info-label') -def _update_receiver_box(box, receiver): - button, label, frame, info = ui.find_children(box, - 'info-button', 'status-label', 'info-frame', 'info-label') label.set_text(receiver.status_text or '') if receiver.status < STATUS.CONNECTED: - button.set_sensitive(False) - button.set_active(False) - frame.set_visible(False) - info.set_text('') + toolbar.set_sensitive(False) + info_label.set_visible(False) + info_label.set_text('') else: - button.set_sensitive(True) - if not info.get_text(): - info.set_text('Serial:\t\t%s\nFirmware: \t%s\nBootloader: \t%s\nMax devices:\t%s' % - (receiver.serial, receiver.firmware[0], receiver.firmware[1], receiver.max_devices)) + toolbar.set_sensitive(True) + if not info_label.get_text(): + info_label.set_markup(_info_text(receiver)) + info_label.set_visible(toolbar.get_children()[0].get_active()) + def _update_device_box(frame, dev): if dev is None: @@ -35,27 +209,29 @@ def _update_device_box(frame, dev): frame.set_name(_PLACEHOLDER) return - icon, label = ui.find_children(frame, 'icon', 'label') + icon, label, toolbar, info_label = ui.find_children(frame, 'icon', 'label', 'buttons', 'info-label') frame.set_visible(True) if frame.get_name() != dev.name: frame.set_name(dev.name) + icon.set_tooltip_text('') icon.set_from_icon_name(ui.get_icon(dev.name, dev.kind), _DEVICE_ICON_SIZE) - icon.set_tooltip_text(dev.name) label.set_markup('' + dev.name + '') status = ui.find_children(frame, 'status') if dev.status < STATUS.CONNECTED: - icon.set_sensitive(False) - icon.set_tooltip_text(dev.status_text) label.set_sensitive(False) - status.set_visible(False) + status.set_sensitive(False) + info_label.set_visible(False) return - icon.set_sensitive(True) - icon.set_tooltip_text('') label.set_sensitive(True) - status.set_visible(True) + status.set_sensitive(True) + info_label.set_visible(toolbar.get_children()[0].get_active()) + + if not info_label.get_text(): + info_label.set_markup(_info_text(dev)) + status_icons = status.get_children() battery_icon, battery_label = status_icons[0:2] @@ -103,158 +279,3 @@ def update(window, receiver): for index in range(1, len(controls)): dev = receiver.devices[index] if index in receiver.devices else None _update_device_box(controls[index], dev) - -# -# -# - -def _receiver_box(name): - info_button = Gtk.ToggleButton() - info_button.set_name('info-button') - info_button.set_alignment(0.5, 0) - info_button.set_image(Gtk.Image.new_from_icon_name(name, _SMALL_DEVICE_ICON_SIZE)) - info_button.set_relief(Gtk.ReliefStyle.NONE) - info_button.set_tooltip_text(name) - info_button.set_sensitive(False) - - label = Gtk.Label('Initializing...') - label.set_name('status-label') - label.set_alignment(0, 0.5) - - toolbar = Gtk.Toolbar() - toolbar.set_name('buttons') - toolbar.set_style(Gtk.ToolbarStyle.ICONS) - toolbar.set_icon_size(Gtk.IconSize.MENU) - toolbar.set_show_arrow(False) - toolbar.insert(ui.action.pair.create_tool_item(), 0) - - info_label = Gtk.Label('') - info_label.set_name('info-label') - info_label.set_alignment(0, 0.5) - info_label.set_padding(24, 4) - info_label.set_selectable(True) - - info_frame = Gtk.Frame() - info_frame.set_name('info-frame') - info_frame.set_label(name) - info_frame.add(info_label) - - info_button.connect('toggled', lambda b: info_frame.set_visible(b.get_active())) - - hbox = Gtk.HBox(homogeneous=False, spacing=8) - hbox.pack_start(info_button, False, False, 0) - hbox.pack_start(label, True, True, 0) - hbox.pack_end(toolbar, False, False, 0) - - vbox = Gtk.VBox(homogeneous=False, spacing=4) - vbox.set_border_width(4) - vbox.pack_start(hbox, True, True, 0) - vbox.pack_start(info_frame, True, True, 0) - vbox.show_all() - - info_frame.set_visible(False) - return vbox - - -def _device_box(has_status_icons=True, has_frame=True): - box = Gtk.HBox(homogeneous=False, spacing=10) - box.set_border_width(4) - - icon = Gtk.Image() - icon.set_name('icon') - icon.set_from_icon_name('image-missing', _DEVICE_ICON_SIZE) - icon.set_alignment(0.5, 0) - box.pack_start(icon, False, False, 0) - - vbox = Gtk.VBox(homogeneous=False, spacing=8) - box.pack_start(vbox, True, True, 0) - - label = Gtk.Label('Initializing...') - label.set_name('label') - label.set_alignment(0, 0.5) - - status_box = Gtk.HBox(homogeneous=False, spacing=0) - status_box.set_name('status') - - if has_status_icons: - vbox.pack_start(label, True, True, 0) - - battery_icon = Gtk.Image.new_from_icon_name('battery_unknown', _STATUS_ICON_SIZE) - status_box.pack_start(battery_icon, False, True, 0) - - battery_label = Gtk.Label() - battery_label.set_width_chars(6) - battery_label.set_alignment(0, 0.5) - status_box.pack_start(battery_label, False, True, 0) - - light_icon = Gtk.Image.new_from_icon_name('light_unknown', _STATUS_ICON_SIZE) - status_box.pack_start(light_icon, False, True, 0) - - light_label = Gtk.Label() - light_label.set_alignment(0, 0.5) - light_label.set_width_chars(8) - status_box.pack_start(light_label, False, True, 0) - else: - status_box.pack_start(label, True, True, 0) - - vbox.pack_start(status_box, True, True, 0) - - box.show_all() - - if has_frame: - frame = Gtk.Frame() - frame.add(box) - return frame - else: - return box - - -def toggle(window, trigger): - # print 'window toggle', window, trigger - if window.get_visible(): - position = window.get_position() - window.hide() - window.move(*position) - else: - if trigger and type(trigger) == Gtk.StatusIcon: - x, y = window.get_position() - if x == 0 and y == 0: - x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), trigger) - window.move(x, y) - window.present() - return True - - -def create(title, name, max_devices, systray=False): - window = Gtk.Window() - window.set_title(title) - window.set_icon_name(ui.appicon(0)) - window.set_role('status-window') - - vbox = Gtk.VBox(homogeneous=False, spacing=4) - vbox.set_border_width(4) - - rbox = _receiver_box(name) - vbox.add(rbox) - for i in range(1, 1 + max_devices): - dbox = _device_box() - vbox.add(dbox) - vbox.set_visible(True) - - window.add(vbox) - - geometry = Gdk.Geometry() - geometry.min_width = 300 - geometry.min_height = 40 - window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE) - window.set_resizable(False) - - window.toggle_visible = lambda i: toggle(window, i) - - if systray: - window.set_keep_above(True) - window.connect('delete-event', toggle) - else: - window.connect('delete-event', Gtk.main_quit) - - return window diff --git a/app/watcher.py b/app/watcher.py index 4f718ee6..0609a0be 100644 --- a/app/watcher.py +++ b/app/watcher.py @@ -5,16 +5,21 @@ from threading import Thread import time from logging import getLogger as _Logger -from collections import namedtuple from logitech.devices.constants import STATUS from receiver import Receiver -_DUMMY_RECEIVER = namedtuple('_DUMMY_RECEIVER', ['NAME', 'kind', 'status', 'status_text', 'max_devices', 'devices']) -_DUMMY_RECEIVER.__nonzero__ = lambda _: False -_DUMMY_RECEIVER.device_name = Receiver.NAME -DUMMY = _DUMMY_RECEIVER(Receiver.NAME, Receiver.NAME, STATUS.UNAVAILABLE, 'Receiver not found.', Receiver.max_devices, {}) +class _DUMMY_RECEIVER: + NAME = Receiver.NAME + device_name = NAME + kind = Receiver.NAME + status = STATUS.UNAVAILABLE + status_text = 'Receiver not found.' + max_devices = Receiver.max_devices + devices = {} + def __nonzero__(self): return False +DUMMY = _DUMMY_RECEIVER() _l = _Logger('watcher') diff --git a/lib/logitech/devices/constants.py b/lib/logitech/devices/constants.py index 3fb3bcfd..32da3911 100644 --- a/lib/logitech/devices/constants.py +++ b/lib/logitech/devices/constants.py @@ -34,9 +34,11 @@ NAMES = { 'M325': ('Wireless Mouse M325', 'mouse'), 'M510': ('Wireless Mouse M510', 'mouse'), 'M515': ('Couch Mouse M515', 'mouse'), + 'M525': ('Wireless Mouse M525', 'mouse'), 'M570': ('Wireless Trackball M570', 'trackball'), 'K270': ('Wireless Keyboard K270', 'keyboard'), 'K350': ('Wireless Keyboard K350', 'keyboard'), 'K750': ('Wireless Solar Keyboard K750', 'keyboard'), 'K800': ('Wireless Illuminated Keyboard K800', 'keyboard'), + 'T650': ('Wireless Rechargeable Touchpad T650', 'touchpad'), } diff --git a/lib/logitech/unifying_receiver/api.py b/lib/logitech/unifying_receiver/api.py index 4bc38611..27a76cc4 100644 --- a/lib/logitech/unifying_receiver/api.py +++ b/lib/logitech/unifying_receiver/api.py @@ -42,23 +42,21 @@ def get_receiver_info(handle): if reply and reply[0:1] == b'\x03': serial = _hexlify(reply[1:5]).decode('ascii').upper() - firmware = '??.??' - reply = _base.request(handle, 0xFF, b'\x81\xF1', b'\x01') - if reply and reply[0:1] == b'\x01': - fw_version = _hexlify(reply[1:3]).decode('ascii') - firmware = '%s.%s' % (fw_version[0:2], fw_version[2:4]) + firmware = [] - reply = _base.request(handle, 0xFF, b'\x81\xF1', b'\x02') + reply = _base.request(handle, 0xFF, b'\x83\xB5', b'\x02') if reply and reply[0:1] == b'\x02': - firmware += '.' + _hexlify(reply[1:3]).decode('ascii') + fw_version = _hexlify(reply[1:5]).decode('ascii') + fw_version = '%s.%s.%s' % (fw_version[0:2], fw_version[2:4], fw_version[4:8]) + firmware.append(_FirmwareInfo(0, FIRMWARE_KIND[0], '', fw_version, None)) - bootloader = None reply = _base.request(handle, 0xFF, b'\x81\xF1', b'\x04') if reply and reply[0:1] == b'\x04': bl_version = _hexlify(reply[1:3]).decode('ascii') - bootloader = '%s.%s' % (bl_version[0:2], bl_version[2:4]) + bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4]) + firmware.append(_FirmwareInfo(1, FIRMWARE_KIND[1], '', bl_version, None)) - return (serial, firmware, bootloader) + return (serial, tuple(firmware)) def count_devices(handle): @@ -270,8 +268,8 @@ def get_device_firmware(handle, devnumber, features=None): :returns: a list of FirmwareInfo tuples, ordered by firmware layer. """ - def _makeFirmwareInfo(level, kind, name=None, version=None, build=None, extras=None): - return _FirmwareInfo(level, kind, name, version, build, extras) + def _makeFirmwareInfo(level, kind, name='', version='', extras=None): + return _FirmwareInfo(level, kind, name, version, extras) fw_count = request(handle, devnumber, FEATURE.FIRMWARE, features=features) if fw_count: @@ -286,14 +284,16 @@ def get_device_firmware(handle, devnumber, features=None): kind = FIRMWARE_KIND[level] name, = _unpack('!3s', fw_info[1:4]) name = name.decode('ascii') - version = _hexlify(fw_info[4:6]) + version = _hexlify(fw_info[4:6]).decode('ascii') version = '%s.%s' % (version[0:2], version[2:4]) build, = _unpack('!H', fw_info[6:8]) + if build: + version += ' b%d' % build extras = fw_info[9:].rstrip(b'\x00') if extras: - fw_info = _makeFirmwareInfo(level, kind, name, version, build, extras) + fw_info = _makeFirmwareInfo(level, kind, name, version, extras) else: - fw_info = _makeFirmwareInfo(level, kind, name, version, build) + fw_info = _makeFirmwareInfo(level, kind, name, version) elif level == 2: fw_info = _makeFirmwareInfo(2, FIRMWARE_KIND[2], version=ord(fw_info[1:2])) else: diff --git a/lib/logitech/unifying_receiver/base.py b/lib/logitech/unifying_receiver/base.py index 799ca06a..eb871307 100644 --- a/lib/logitech/unifying_receiver/base.py +++ b/lib/logitech/unifying_receiver/base.py @@ -105,7 +105,7 @@ def try_open(path): # otherwise, the read should produce nothing reply = _hid.read(receiver_handle, _MAX_REPLY_SIZE, DEFAULT_TIMEOUT) if reply: - if reply[:4] == b'\x10\x00\x8F\x00': + if reply[:5] == b'\x10\x00\x8F\x00\x10': # 'device 0 unreachable' is the expected reply from a valid receiver handle _l.log(_LOG_LEVEL, "[%s] success: handle %x", path, receiver_handle) return receiver_handle @@ -114,9 +114,9 @@ def try_open(path): if _l.isEnabledFor(_LOG_LEVEL): if reply == b'\x01\x00\x00\x00\x00\x00\x00\x00': # no idea what this is, but it comes up occasionally - _l.log(_LOG_LEVEL, "[%s] %x mistery reply [%s]", path, receiver_handle, _hexlify(reply)) + _l.log(_LOG_LEVEL, "[%s] %x mistery reply [%s]", path, receiver_handle, _hexlify(reply).decode('ascii')) else: - _l.log(_LOG_LEVEL, "[%s] %x unknown reply [%s]", path, receiver_handle, _hexlify(reply)) + _l.log(_LOG_LEVEL, "[%s] %x unknown reply [%s]", path, receiver_handle, _hexlify(reply).decode('ascii')) else: _l.log(_LOG_LEVEL, "[%s] %x no reply", path, receiver_handle) @@ -171,7 +171,7 @@ def write(handle, devnumber, data): wdata = _pack('!BB18s' if len(data) > 5 else '!BB5s', 0x10, devnumber, data) if _l.isEnabledFor(_LOG_LEVEL): - hexs = _hexlify(wdata) + hexs = _hexlify(wdata).decode('ascii') _l.log(_LOG_LEVEL, "(%d) <= w[%s %s %s %s]", devnumber, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:]) if not _hid.write(handle, wdata): @@ -208,7 +208,7 @@ def read(handle, timeout=DEFAULT_TIMEOUT): if len(data) > _MAX_REPLY_SIZE: _l.warn("(%d) => r[%s] read packet too long: %d bytes", ord(data[1:2]), _hexlify(data), len(data)) if _l.isEnabledFor(_LOG_LEVEL): - hexs = _hexlify(data) + hexs = _hexlify(data).decode('ascii') _l.log(_LOG_LEVEL, "(%d) => r[%s %s %s %s]", ord(data[1:2]), hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:]) code = ord(data[:1]) devnumber = ord(data[1:2]) @@ -235,7 +235,9 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None :raisees FeatureCallError: if the feature call replied with an error. """ if _l.isEnabledFor(_LOG_LEVEL): - _l.log(_LOG_LEVEL, "(%d) request {%s} params [%s]", devnumber, _hexlify(feature_index_function), _hexlify(params)) + _l.log(_LOG_LEVEL, "(%d) request {%s} params [%s]", devnumber, + _hexlify(feature_index_function).decode('ascii'), + _hexlify(params).decode('ascii')) if len(feature_index_function) != 2: raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hexlify(feature_index_function)) @@ -264,18 +266,20 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function: # device not present - _l.log(_LOG_LEVEL, "(%d) request ping failed on {%s} call: [%s]", devnumber, _hexlify(feature_index_function), _hexlify(reply_data)) + _l.log(_LOG_LEVEL, "(%d) request ping failed on {%s} call: [%s]", devnumber, + _hexlify(feature_index_function).decode('ascii'), + _hexlify(reply_data).decode('ascii')) return None if reply_code == 0x10 and reply_data[:1] == b'\x8F': # device not present - _l.log(_LOG_LEVEL, "(%d) request ping failed: [%s]", devnumber, _hexlify(reply_data)) + _l.log(_LOG_LEVEL, "(%d) request ping failed: [%s]", devnumber, _hexlify(reply_data).decode('ascii')) return None if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function: # the feature call returned with an error error_code = ord(reply_data[3]) - _l.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hexlify(reply_data)) + _l.warn("(%d) request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hexlify(reply_data).decode('ascii')) feature_index = ord(feature_index_function[:1]) feature_function = feature_index_function[1:2] feature = None if features is None else features[feature_index] if feature_index < len(features) else None @@ -283,14 +287,16 @@ def request(handle, devnumber, feature_index_function, params=b'', features=None if reply_code == 0x11 and reply_data[:2] == feature_index_function: # a matching reply - # _l.log(_LOG_LEVEL, "(%d) matched reply with feature-index-function [%s]", devnumber, _hexlify(reply_data[2:])) + # _l.log(_LOG_LEVEL, "(%d) matched reply with feature-index-function [%s]", devnumber, _hexlify(reply_data[2:]).decode('ascii')) return reply_data[2:] if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function: # direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10 - # _l.log(_LOG_LEVEL, "(%d) matched reply with feature-index-function [%s]", devnumber, _hexlify(reply_data[2:])) + # _l.log(_LOG_LEVEL, "(%d) matched reply with feature-index-function [%s]", devnumber, _hexlify(reply_data[2:]).decode('ascii')) return reply_data[2:] - # _l.log(_LOG_LEVEL, "(%d) unmatched reply {%s} (expected {%s})", devnumber, _hexlify(reply_data[:2]), _hexlify(feature_index_function)) + # _l.log(_LOG_LEVEL, "(%d) unmatched reply {%s} (expected {%s})", devnumber, + # _hexlify(reply_data[:2]).decode('ascii'), + # _hexlify(feature_index_function).decode('ascii')) if unhandled_hook: unhandled_hook(reply_code, reply_devnumber, reply_data) diff --git a/lib/logitech/unifying_receiver/common.py b/lib/logitech/unifying_receiver/common.py index 6a181e2a..d13081ba 100644 --- a/lib/logitech/unifying_receiver/common.py +++ b/lib/logitech/unifying_receiver/common.py @@ -36,7 +36,6 @@ FirmwareInfo = namedtuple('FirmwareInfo', [ 'kind', 'name', 'version', - 'build', 'extras']) """Reprogrammable keys informations.""" diff --git a/lib/logitech/unifying_receiver/constants.py b/lib/logitech/unifying_receiver/constants.py index 98669b4b..802a4fea 100644 --- a/lib/logitech/unifying_receiver/constants.py +++ b/lib/logitech/unifying_receiver/constants.py @@ -55,7 +55,7 @@ _DEVICE_KINDS = ('keyboard', 'remote control', 'numpad', 'mouse', DEVICE_KIND = FallbackDict(lambda x: 'unknown', list2dict(_DEVICE_KINDS)) -_FIRMWARE_KINDS = ('Main (HID)', 'Bootloader', 'Hardware', 'Other') +_FIRMWARE_KINDS = ('Firmware', 'Bootloader', 'Hardware', 'Other') """Names of different firmware levels possible, indexed by level.""" FIRMWARE_KIND = FallbackDict(lambda x: 'Unknown', list2dict(_FIRMWARE_KINDS)) diff --git a/lib/logitech/ur_scanner.py b/lib/logitech/ur_scanner.py index a2e1d6d8..21de2a0c 100644 --- a/lib/logitech/ur_scanner.py +++ b/lib/logitech/ur_scanner.py @@ -13,11 +13,11 @@ from .unifying_receiver.constants import * def print_receiver(receiver): print ("Unifying Receiver") - serial, firmware, bootloader = api.get_receiver_info(receiver) + serial, firmware = api.get_receiver_info(receiver) - print (" Serial: %s" % serial) - print (" Firmware version: %s" % firmware) - print (" Bootloader: %s" % bootloader) + print (" Serial : %s" % serial) + for f in firmware: + print (" %-10s: %s" % (f.kind, f.version)) print ("--------") @@ -36,7 +36,7 @@ def scan_devices(receiver): firmware = api.get_device_firmware(receiver, devinfo.number, features=devinfo.features) for fw in firmware: - print (" %s firmware: %s version %s build %d" % (fw.kind, fw.name, fw.version, fw.build)) + print (" %-10s: %s %s" % (fw.kind, fw.name, fw.version)) for index in range(0, len(devinfo.features)): feature = devinfo.features[index] @@ -62,7 +62,7 @@ if __name__ == '__main__': import argparse arg_parser = argparse.ArgumentParser() arg_parser.add_argument('-v', '--verbose', action='count', default=0, - help='increase the logger verbosity') + help='log the HID data traffic with the receiver') args = arg_parser.parse_args() log_level = logging.root.level - 10 * args.verbose