ui: handle wired devices

This commit is contained in:
Peter F. Patel-Schneider 2020-09-17 10:56:54 -04:00
parent aeb8588e06
commit 58823763ea
8 changed files with 133 additions and 74 deletions

View File

@ -55,6 +55,7 @@ DeviceInfo = namedtuple(
'product', 'product',
'interface', 'interface',
'driver', 'driver',
'isDevice',
] ]
) )
del namedtuple del namedtuple
@ -89,6 +90,7 @@ def _match(action, device, filter):
product_id = filter.get('product_id') product_id = filter.get('product_id')
interface_number = filter.get('usb_interface') interface_number = filter.get('usb_interface')
hid_driver = filter.get('hid_driver') hid_driver = filter.get('hid_driver')
isDevice = filter.get('isDevice')
usb_device = device.find_parent('usb', 'usb_device') usb_device = device.find_parent('usb', 'usb_device')
# print ("* parent", action, device, "usb:", usb_device) # print ("* parent", action, device, "usb:", usb_device)
@ -134,7 +136,8 @@ def _match(action, device, filter):
manufacturer=attrs.get('manufacturer'), manufacturer=attrs.get('manufacturer'),
product=attrs.get('product'), product=attrs.get('product'),
interface=usb_interface, interface=usb_interface,
driver=hid_driver_name driver=hid_driver_name,
isDevice=isDevice
) )
return d_info return d_info
@ -150,7 +153,8 @@ def _match(action, device, filter):
manufacturer=None, manufacturer=None,
product=None, product=None,
interface=None, interface=None,
driver=None driver=None,
isDevice=isDevice
) )
return d_info return d_info

View File

@ -104,7 +104,7 @@ def wired_devices():
def notify_on_receivers_glib(callback): def notify_on_receivers_glib(callback):
"""Watch for matching devices and notifies the callback on the GLib thread.""" """Watch for matching devices and notifies the callback on the GLib thread."""
_hid.monitor_glib(callback, *_RECEIVER_USB_IDS) _hid.monitor_glib(callback, *_RECEIVER_USB_IDS, *_WIRED_DEVICE_IDS)
# #

View File

@ -99,7 +99,7 @@ _ex100_receiver = lambda product_id: {
'ex100_27mhz_wpid_fix': True 'ex100_27mhz_wpid_fix': True
} }
_wired_device = lambda product_id: {'vendor_id': 0x046d, 'product_id': product_id, 'usb_interface': 2} _wired_device = lambda product_id: {'vendor_id': 0x046d, 'product_id': product_id, 'usb_interface': 2, 'isDevice': True}
# standard Unifying receivers (marked with the orange Unifying logo) # standard Unifying receivers (marked with the orange Unifying logo)
UNIFYING_RECEIVER_C52B = _unifying_receiver(0xc52b) UNIFYING_RECEIVER_C52B = _unifying_receiver(0xc52b)

View File

@ -1,5 +1,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals from __future__ import absolute_import, division, print_function, unicode_literals
import errno as _errno
from logging import INFO as _INFO from logging import INFO as _INFO
from logging import getLogger from logging import getLogger
@ -31,6 +33,7 @@ class Device(object):
def __init__(self, receiver, number, link_notification=None, info=None): def __init__(self, receiver, number, link_notification=None, info=None):
assert receiver or info assert receiver or info
self.receiver = receiver self.receiver = receiver
self.may_unpair = False
self.isDevice = True # some devices act as receiver so we need a property to distinguish them self.isDevice = True # some devices act as receiver so we need a property to distinguish them
if receiver: if receiver:
@ -153,6 +156,7 @@ class Device(object):
self.handle = _hid.open_path(self.path) self.handle = _hid.open_path(self.path)
self.product_id = info.product_id self.product_id = info.product_id
self._serial = ''.join(info.serial.split('-')).upper() self._serial = ''.join(info.serial.split('-')).upper()
self.online = True
if self._protocol is not None: if self._protocol is not None:
self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self) self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self)
@ -182,7 +186,7 @@ class Device(object):
# if _log.isEnabledFor(_DEBUG): # if _log.isEnabledFor(_DEBUG):
# _log.debug("device %d codename %s", self.number, self._codename) # _log.debug("device %d codename %s", self.number, self._codename)
else: else:
self._codename = '? (%s)' % self.wpid self._codename = '? (%s)' % (self.wpid or self.product_id)
return self._codename return self._codename
@property @property
@ -190,7 +194,7 @@ class Device(object):
if not self._name: if not self._name:
if self.online and self.protocol >= 2.0: if self.online and self.protocol >= 2.0:
self._name = _hidpp20.get_name(self) self._name = _hidpp20.get_name(self)
return self._name or self.codename or ('Unknown device %s' % self.wpid) return self._name or self.codename or ('Unknown device %s' % (self.wpid or self.product_id))
@property @property
def kind(self): def kind(self):
@ -369,7 +373,10 @@ class Device(object):
def __hash__(self): def __hash__(self):
return self.wpid.__hash__() return self.wpid.__hash__()
__bool__ = __nonzero__ = lambda self: self.wpid is not None and self.number in self.receiver def __bool__(self):
return self.wpid is not None and self.number in self.receiver if self.receiver else self.handle is not None
__nonzero__ = __bool__
def __str__(self): def __str__(self):
return '<Device(%d,%s,%s,%s)>' % ( return '<Device(%d,%s,%s,%s)>' % (
@ -377,3 +384,29 @@ class Device(object):
) )
__unicode__ = __repr__ = __str__ __unicode__ = __repr__ = __str__
def notify_devices(self): # no need to notify, as there are none
pass
@classmethod
def open(self, device_info):
"""Opens a Logitech Device found attached to the machine, by Linux device path.
:returns: An open file handle for the found receiver, or ``None``.
"""
try:
handle = _base.open_path(device_info.path)
if handle:
return Device(None, 0, info=device_info)
except OSError as e:
_log.exception('open %s', device_info)
if e.errno == _errno.EACCES:
raise
except Exception:
_log.exception('open %s', device_info)
def close(self):
handle, self.handle = self.handle, None
return (handle and _base.close(handle))
def __del__(self):
self.close()

View File

@ -26,15 +26,16 @@ from logging import INFO as _INFO
from logging import WARNING as _WARNING from logging import WARNING as _WARNING
from logging import getLogger from logging import getLogger
from logitech_receiver import Receiver from logitech_receiver import Device, Receiver
from logitech_receiver import base as _base from logitech_receiver import base as _base
from logitech_receiver import listener as _listener from logitech_receiver import listener as _listener
from logitech_receiver import notifications as _notifications from logitech_receiver import notifications as _notifications
from logitech_receiver import status as _status from logitech_receiver import status as _status
from solaar.i18n import _
from . import configuration from . import configuration
# from solaar.i18n import _
_log = getLogger(__name__) _log = getLogger(__name__)
del getLogger del getLogger
@ -94,7 +95,7 @@ class ReceiverListener(_listener.EventsListener):
# make sure to clean up in _all_listeners # make sure to clean up in _all_listeners
_all_listeners.pop(r.path, None) _all_listeners.pop(r.path, None)
r.status = _('The receiver was unplugged.') # this causes problems but what is it doing (pfps) - r.status = _('The receiver was unplugged.')
if r: if r:
try: try:
r.close() r.close()
@ -159,7 +160,7 @@ class ReceiverListener(_listener.EventsListener):
self.status_changed_callback(device, alert, reason) self.status_changed_callback(device, alert, reason)
return return
assert device.receiver == self.receiver # not true for wired devices - assert device.receiver == self.receiver
if not device: if not device:
# Device was unpaired, and isn't valid anymore. # Device was unpaired, and isn't valid anymore.
# We replace it with a ghost so that the UI has something to work # We replace it with a ghost so that the UI has something to work
@ -270,11 +271,19 @@ _all_listeners = {}
def _start(device_info): def _start(device_info):
assert _status_callback assert _status_callback
receiver = Receiver.open(device_info) isDevice = device_info.isDevice
if not isDevice:
receiver = Receiver.open(device_info)
else:
receiver = Device.open(device_info)
configuration.attach_to(receiver)
if receiver: if receiver:
rl = ReceiverListener(receiver, _status_callback) rl = ReceiverListener(receiver, _status_callback)
rl.start() rl.start()
_all_listeners[device_info.path] = rl _all_listeners[device_info.path] = rl
if isDevice: # (wired) devices start as active
receiver.status.changed(True)
return rl return rl
_log.warn('failed to open %s', device_info) _log.warn('failed to open %s', device_info)
@ -288,6 +297,8 @@ def start_all():
_log.info('starting receiver listening threads') _log.info('starting receiver listening threads')
for device_info in _base.receivers(): for device_info in _base.receivers():
_process_receiver_event('add', device_info) _process_receiver_event('add', device_info)
for device_info in _base.wired_devices():
_process_receiver_event('add', device_info)
def stop_all(): def stop_all():

View File

@ -509,7 +509,7 @@ def create():
def update(device, is_online=None): def update(device, is_online=None):
assert _box is not None assert _box is not None
assert device assert device
device_id = (device.receiver.path, device.number) device_id = (device.receiver.path if device.receiver else device.path, device.number)
if is_online is None: if is_online is None:
is_online = bool(device.online) is_online = bool(device.online)
@ -541,7 +541,7 @@ def clean(device):
Needed after the device has been unpaired. Needed after the device has been unpaired.
""" """
assert _box is not None assert _box is not None
device_id = (device.receiver.path, device.number) device_id = (device.receiver.path if device.receiver else device.path, device.number)
for k in list(_items.keys()): for k in list(_items.keys()):
if k[0:2] == device_id: if k[0:2] == device_id:
_box.remove(_items[k]) _box.remove(_items[k])

View File

@ -351,28 +351,29 @@ def _pick_device_with_lowest_battery():
def _add_device(device): def _add_device(device):
assert device assert device
assert device.receiver # not true for wired devices - assert device.receiver
receiver_path = device.receiver.path receiver_path = device.receiver.path if device.receiver is not None else device.path
assert receiver_path # not true for wired devices - assert receiver_path
index = None index = 0
for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info): for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info):
if path == receiver_path: if path == receiver_path:
# the first entry matching the receiver serial should be for the receiver itself # the first entry matching the receiver serial should be for the receiver itself
index = idx + 1 index = idx + 1
break break
assert index is not None # assert index is not None
# proper ordering (according to device.number) for a receiver's devices if device.receiver:
while True: # proper ordering (according to device.number) for a receiver's devices
path, number, _ignore, _ignore = _devices_info[index] while True:
if path == _RECEIVER_SEPARATOR[0]: path, number, _ignore, _ignore = _devices_info[index]
break if path == _RECEIVER_SEPARATOR[0]:
assert path == receiver_path break
assert number != device.number assert path == receiver_path
if number > device.number: assert number != device.number
break if number > device.number:
index = index + 1 break
index = index + 1
new_device_info = (receiver_path, device.number, device.name, device.status) new_device_info = (receiver_path, device.number, device.name, device.status)
assert len(new_device_info) == len(_RECEIVER_SEPARATOR) assert len(new_device_info) == len(_RECEIVER_SEPARATOR)
@ -381,7 +382,7 @@ def _add_device(device):
# label_prefix = b'\xE2\x94\x84 '.decode('utf-8') # label_prefix = b'\xE2\x94\x84 '.decode('utf-8')
label_prefix = ' ' label_prefix = ' '
new_menu_item = Gtk.ImageMenuItem.new_with_label(label_prefix + device.name) new_menu_item = Gtk.ImageMenuItem.new_with_label((label_prefix if device.number else '') + device.name)
new_menu_item.set_image(Gtk.Image()) new_menu_item.set_image(Gtk.Image())
new_menu_item.show_all() new_menu_item.show_all()
new_menu_item.connect('activate', _window_popup, receiver_path, device.number) new_menu_item.connect('activate', _window_popup, receiver_path, device.number)
@ -519,7 +520,7 @@ def update(device=None):
else: else:
# peripheral # peripheral
is_paired = bool(device) is_paired = bool(device)
receiver_path = device.receiver.path receiver_path = device.receiver.path if device.receiver is not None else device.path
index = None index = None
for idx, (path, number, _ignore, _ignore) in enumerate(_devices_info): for idx, (path, number, _ignore, _ignore) in enumerate(_devices_info):
if path == receiver_path and number == device.number: if path == receiver_path and number == device.number:
@ -529,9 +530,8 @@ def update(device=None):
if index is None: if index is None:
index = _add_device(device) index = _add_device(device)
_update_menu_item(index, device) _update_menu_item(index, device)
else: else: # was just unpaired or unplugged
# was just unpaired if index is not None:
if index:
_remove_device(index) _remove_device(index)
menu_items = _menu.get_children() menu_items = _menu.get_children()

View File

@ -433,6 +433,9 @@ def _device_row(receiver_path, device_number, device=None):
assert device_number is not None assert device_number is not None
receiver_row = _receiver_row(receiver_path, None if device is None else device.receiver) receiver_row = _receiver_row(receiver_path, None if device is None else device.receiver)
if receiver_row and device_number == 0: # wired device, receiver row is device row
return receiver_row
item = _model.iter_children(receiver_row) item = _model.iter_children(receiver_row)
new_child_index = 0 new_child_index = 0
while item: while item:
@ -450,9 +453,10 @@ def _device_row(receiver_path, device_number, device=None):
icon_name = _icons.device_icon_name(device.name, device.kind) icon_name = _icons.device_icon_name(device.name, device.kind)
status_text = None status_text = None
status_icon = None status_icon = None
row_data = ( codename = device.codename if device.codename and device.codename[0] != '?' else (
receiver_path, device_number, bool(device.online), device.codename, icon_name, status_text, status_icon, device device.name.split()[0] if device.name.split() else device.codename
) )
row_data = (receiver_path, device_number, bool(device.online), codename, icon_name, status_text, status_icon, device)
assert len(row_data) == len(_TREE_SEPATATOR) assert len(row_data) == len(_TREE_SEPATATOR)
if _log.isEnabledFor(_DEBUG): if _log.isEnabledFor(_DEBUG):
_log.debug('new device row %s at index %d', row_data, new_child_index) _log.debug('new device row %s at index %d', row_data, new_child_index)
@ -533,7 +537,10 @@ def _update_details(button):
else: else:
# yield ('Codename', device.codename) # yield ('Codename', device.codename)
yield (_('Index'), device.number) yield (_('Index'), device.number)
yield (_('Wireless PID'), device.wpid) if device.wpid:
yield (_('Wireless PID'), device.wpid)
if device.product_id:
yield (_('USB id'), '046d:' + device.product_id)
hid_version = device.protocol hid_version = device.protocol
yield (_('Protocol'), 'HID++ %1.1f' % hid_version if hid_version else _('Unknown')) yield (_('Protocol'), 'HID++ %1.1f' % hid_version if hid_version else _('Unknown'))
if read_all and device.polling_rate: if read_all and device.polling_rate:
@ -654,6 +661,9 @@ def _update_device_panel(device, panel, buttons, full=False):
is_online = bool(device.online) is_online = bool(device.online)
panel.set_sensitive(is_online) panel.set_sensitive(is_online)
if device.status.get(_K.BATTERY_LEVEL) is None:
device.status.read_battery(device)
battery_level = device.status.get(_K.BATTERY_LEVEL) battery_level = device.status.get(_K.BATTERY_LEVEL)
battery_next_level = device.status.get(_K.BATTERY_NEXT_LEVEL) battery_next_level = device.status.get(_K.BATTERY_NEXT_LEVEL)
battery_voltage = device.status.get(_K.BATTERY_VOLTAGE) battery_voltage = device.status.get(_K.BATTERY_VOLTAGE)
@ -736,7 +746,7 @@ def _update_device_panel(device, panel, buttons, full=False):
panel._lux.set_visible(False) panel._lux.set_visible(False)
buttons._pair.set_visible(False) buttons._pair.set_visible(False)
buttons._unpair.set_sensitive(device.receiver.may_unpair) buttons._unpair.set_sensitive(device.receiver.may_unpair if device.receiver else False)
buttons._unpair.set_visible(True) buttons._unpair.set_visible(True)
panel.set_visible(True) panel.set_visible(True)
@ -841,14 +851,14 @@ def update(device, need_popup=False):
selected_device_id = _find_selected_device_id() selected_device_id = _find_selected_device_id()
if device.kind is None: if device.kind is None: # receiver
# receiver # receiver
is_alive = bool(device) is_alive = bool(device)
item = _receiver_row(device.path, device if is_alive else None) item = _receiver_row(device.path, device if is_alive else None)
if is_alive and item: if is_alive and item:
was_pairing = bool(_model.get_value(item, _COLUMN.STATUS_ICON)) was_pairing = bool(_model.get_value(item, _COLUMN.STATUS_ICON))
is_pairing = bool(device.status.lock_open) is_pairing = (not device.isDevice) and bool(device.status.lock_open)
_model.set_value(item, _COLUMN.STATUS_ICON, 'network-wireless' if is_pairing else _CAN_SET_ROW_NONE) _model.set_value(item, _COLUMN.STATUS_ICON, 'network-wireless' if is_pairing else _CAN_SET_ROW_NONE)
if selected_device_id == (device.path, 0): if selected_device_id == (device.path, 0):
@ -862,44 +872,45 @@ def update(device, need_popup=False):
_model.remove(item) _model.remove(item)
else: else:
# peripheral path = device.receiver.path if device.receiver else device.path
is_paired = bool(device) assert device.number is not None and device.number >= 0, 'invalid device number' + str(device.number)
assert device.receiver item = _device_row(path, device.number, device if bool(device) 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)
is_online = bool(device.online)
_model.set_value(item, _COLUMN.ACTIVE, is_online)
battery_level = device.status.get(_K.BATTERY_LEVEL)
battery_voltage = device.status.get(_K.BATTERY_VOLTAGE)
if battery_level is None:
_model.set_value(item, _COLUMN.STATUS_TEXT, _CAN_SET_ROW_NONE)
_model.set_value(item, _COLUMN.STATUS_ICON, _CAN_SET_ROW_NONE)
else:
if battery_voltage is not None:
status_text = '%(battery_voltage)dmV' % {'battery_voltage': battery_voltage}
elif isinstance(battery_level, _NamedInt):
status_text = _(str(battery_level))
else:
status_text = '%(battery_percent)d%%' % {'battery_percent': battery_level}
_model.set_value(item, _COLUMN.STATUS_TEXT, status_text)
charging = device.status.get(_K.BATTERY_CHARGING)
icon_name = _icons.battery(battery_level, charging)
_model.set_value(item, _COLUMN.STATUS_ICON, icon_name)
if selected_device_id is None or need_popup:
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)
if bool(device) and item:
update_device(device, item, selected_device_id, need_popup)
elif item: elif item:
_model.remove(item) _model.remove(item)
_config_panel.clean(device) _config_panel.clean(device)
# make sure all rows are visible # make sure all rows are visible
_tree.expand_all() _tree.expand_all()
def update_device(device, item, selected_device_id, need_popup):
was_online = _model.get_value(item, _COLUMN.ACTIVE)
is_online = bool(device.online)
_model.set_value(item, _COLUMN.ACTIVE, is_online)
battery_level = device.status.get(_K.BATTERY_LEVEL)
battery_voltage = device.status.get(_K.BATTERY_VOLTAGE)
if battery_level is None:
_model.set_value(item, _COLUMN.STATUS_TEXT, _CAN_SET_ROW_NONE)
_model.set_value(item, _COLUMN.STATUS_ICON, _CAN_SET_ROW_NONE)
else:
if battery_voltage is not None:
status_text = '%(battery_voltage)dmV' % {'battery_voltage': battery_voltage}
elif isinstance(battery_level, _NamedInt):
status_text = _(str(battery_level))
else:
status_text = '%(battery_percent)d%%' % {'battery_percent': battery_level}
_model.set_value(item, _COLUMN.STATUS_TEXT, status_text)
charging = device.status.get(_K.BATTERY_CHARGING)
icon_name = _icons.battery(battery_level, charging)
_model.set_value(item, _COLUMN.STATUS_ICON, icon_name)
if selected_device_id is None or need_popup:
select(device.receiver.path if device.receiver else device.path, device.number)
elif selected_device_id == (device.receiver.path if device.receiver else device.path, device.number):
full_update = need_popup or was_online != is_online
_update_info_panel(device, full=full_update)