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

View File

@ -104,7 +104,7 @@ def wired_devices():
def notify_on_receivers_glib(callback):
"""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
}
_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)
UNIFYING_RECEIVER_C52B = _unifying_receiver(0xc52b)

View File

@ -1,5 +1,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import errno as _errno
from logging import INFO as _INFO
from logging import getLogger
@ -31,6 +33,7 @@ class Device(object):
def __init__(self, receiver, number, link_notification=None, info=None):
assert receiver or info
self.receiver = receiver
self.may_unpair = False
self.isDevice = True # some devices act as receiver so we need a property to distinguish them
if receiver:
@ -153,6 +156,7 @@ class Device(object):
self.handle = _hid.open_path(self.path)
self.product_id = info.product_id
self._serial = ''.join(info.serial.split('-')).upper()
self.online = True
if self._protocol is not None:
self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self)
@ -182,7 +186,7 @@ class Device(object):
# if _log.isEnabledFor(_DEBUG):
# _log.debug("device %d codename %s", self.number, self._codename)
else:
self._codename = '? (%s)' % self.wpid
self._codename = '? (%s)' % (self.wpid or self.product_id)
return self._codename
@property
@ -190,7 +194,7 @@ class Device(object):
if not self._name:
if self.online and self.protocol >= 2.0:
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
def kind(self):
@ -369,7 +373,10 @@ class Device(object):
def __hash__(self):
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):
return '<Device(%d,%s,%s,%s)>' % (
@ -377,3 +384,29 @@ class Device(object):
)
__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 getLogger
from logitech_receiver import Receiver
from logitech_receiver import Device, Receiver
from logitech_receiver import base as _base
from logitech_receiver import listener as _listener
from logitech_receiver import notifications as _notifications
from logitech_receiver import status as _status
from solaar.i18n import _
from . import configuration
# from solaar.i18n import _
_log = getLogger(__name__)
del getLogger
@ -94,7 +95,7 @@ class ReceiverListener(_listener.EventsListener):
# make sure to clean up in _all_listeners
_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:
try:
r.close()
@ -159,7 +160,7 @@ class ReceiverListener(_listener.EventsListener):
self.status_changed_callback(device, alert, reason)
return
assert device.receiver == self.receiver
# not true for wired devices - assert device.receiver == self.receiver
if not device:
# Device was unpaired, and isn't valid anymore.
# We replace it with a ghost so that the UI has something to work
@ -270,11 +271,19 @@ _all_listeners = {}
def _start(device_info):
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:
rl = ReceiverListener(receiver, _status_callback)
rl.start()
_all_listeners[device_info.path] = rl
if isDevice: # (wired) devices start as active
receiver.status.changed(True)
return rl
_log.warn('failed to open %s', device_info)
@ -288,6 +297,8 @@ def start_all():
_log.info('starting receiver listening threads')
for device_info in _base.receivers():
_process_receiver_event('add', device_info)
for device_info in _base.wired_devices():
_process_receiver_event('add', device_info)
def stop_all():

View File

@ -509,7 +509,7 @@ def create():
def update(device, is_online=None):
assert _box is not None
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:
is_online = bool(device.online)
@ -541,7 +541,7 @@ def clean(device):
Needed after the device has been unpaired.
"""
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()):
if k[0:2] == device_id:
_box.remove(_items[k])

View File

@ -351,28 +351,29 @@ def _pick_device_with_lowest_battery():
def _add_device(device):
assert device
assert device.receiver
receiver_path = device.receiver.path
assert receiver_path
# not true for wired devices - assert device.receiver
receiver_path = device.receiver.path if device.receiver is not None else device.path
# not true for wired devices - assert receiver_path
index = None
index = 0
for idx, (path, _ignore, _ignore, _ignore) in enumerate(_devices_info):
if path == receiver_path:
# the first entry matching the receiver serial should be for the receiver itself
index = idx + 1
break
assert index is not None
# assert index is not None
# proper ordering (according to device.number) for a receiver's devices
while True:
path, number, _ignore, _ignore = _devices_info[index]
if path == _RECEIVER_SEPARATOR[0]:
break
assert path == receiver_path
assert number != device.number
if number > device.number:
break
index = index + 1
if device.receiver:
# proper ordering (according to device.number) for a receiver's devices
while True:
path, number, _ignore, _ignore = _devices_info[index]
if path == _RECEIVER_SEPARATOR[0]:
break
assert path == receiver_path
assert number != device.number
if number > device.number:
break
index = index + 1
new_device_info = (receiver_path, device.number, device.name, device.status)
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 = ' '
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.show_all()
new_menu_item.connect('activate', _window_popup, receiver_path, device.number)
@ -519,7 +520,7 @@ def update(device=None):
else:
# peripheral
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
for idx, (path, number, _ignore, _ignore) in enumerate(_devices_info):
if path == receiver_path and number == device.number:
@ -529,9 +530,8 @@ def update(device=None):
if index is None:
index = _add_device(device)
_update_menu_item(index, device)
else:
# was just unpaired
if index:
else: # was just unpaired or unplugged
if index is not None:
_remove_device(index)
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
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)
new_child_index = 0
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)
status_text = None
status_icon = None
row_data = (
receiver_path, device_number, bool(device.online), device.codename, icon_name, status_text, status_icon, device
codename = device.codename if device.codename and device.codename[0] != '?' else (
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)
if _log.isEnabledFor(_DEBUG):
_log.debug('new device row %s at index %d', row_data, new_child_index)
@ -533,7 +537,10 @@ def _update_details(button):
else:
# yield ('Codename', device.codename)
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
yield (_('Protocol'), 'HID++ %1.1f' % hid_version if hid_version else _('Unknown'))
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)
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_next_level = device.status.get(_K.BATTERY_NEXT_LEVEL)
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)
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)
panel.set_visible(True)
@ -841,14 +851,14 @@ def update(device, need_popup=False):
selected_device_id = _find_selected_device_id()
if device.kind is None:
if device.kind is None: # receiver
# receiver
is_alive = bool(device)
item = _receiver_row(device.path, device if is_alive else None)
if is_alive and item:
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)
if selected_device_id == (device.path, 0):
@ -862,44 +872,45 @@ def update(device, need_popup=False):
_model.remove(item)
else:
# peripheral
is_paired = bool(device)
assert device.receiver
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)
path = device.receiver.path if device.receiver else device.path
assert device.number is not None and device.number >= 0, 'invalid device number' + str(device.number)
item = _device_row(path, device.number, device if bool(device) else None)
if bool(device) and item:
update_device(device, item, selected_device_id, need_popup)
elif item:
_model.remove(item)
_config_panel.clean(device)
# make sure all rows are visible
_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)