From 29ff35d553ac712838f7a8cbc227e7763e648068 Mon Sep 17 00:00:00 2001 From: markopy <48253511+markopy@users.noreply.github.com> Date: Tue, 28 Nov 2023 21:25:17 +0000 Subject: [PATCH] Partial support for macOS and Windows (#1971) * Add support for macOS via hidapi * Style fixes * Ignore keyboard and mouse input devices * Don't require pyudev on mac and windows * Fix debug log format error * More logging for failed hidpp checks * Don't try to load hid_darwin_set_open_exclusive on windows * Bring back button for rule editor since some rules will work --------- Co-authored-by: markopy <(none)> Co-authored-by: Peter F. Patel-Schneider --- docs/installation.md | 14 + lib/hidapi/__init__.py | 40 ++- lib/hidapi/hidapi.py | 500 +++++++++++++++++++++++++++++ lib/logitech_receiver/diversion.py | 53 +-- lib/logitech_receiver/listener.py | 6 +- lib/solaar/gtk.py | 4 +- setup.py | 2 +- 7 files changed, 585 insertions(+), 34 deletions(-) create mode 100644 lib/hidapi/hidapi.py diff --git a/docs/installation.md b/docs/installation.md index d8c76773..569822d6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -14,6 +14,20 @@ This will not install the Solaar udev rule, which you will need to install manua `~/.local/share/solaar/udev-rules.d/42-logitech-unify-permissions.rules` to `/etc/udev/rules.d` as root. +## macOS support + +Solaar has limited support for macOS. You can use it to pair devices and configure settings +but the rule system and diversion will not work. + +After installing Solaar via pip use homebrew to install the hidapi library: +``` +brew install hidapi +``` +If you only want to use the CLI that's all that is needed. To use the GUI you need to also +install GTK and its python bindings: +``` +brew install gtk+3 pygobject3 +``` # Manual installation from GitHub diff --git a/lib/hidapi/__init__.py b/lib/hidapi/__init__.py index e9db82dc..b29b8c2c 100644 --- a/lib/hidapi/__init__.py +++ b/lib/hidapi/__init__.py @@ -17,17 +17,33 @@ ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Generic Human Interface Device API.""" -from hidapi.udev import close # noqa: F401 -from hidapi.udev import enumerate # noqa: F401 -from hidapi.udev import find_paired_node # noqa: F401 -from hidapi.udev import find_paired_node_wpid # noqa: F401 -from hidapi.udev import get_manufacturer # noqa: F401 -from hidapi.udev import get_product # noqa: F401 -from hidapi.udev import get_serial # noqa: F401 -from hidapi.udev import monitor_glib # noqa: F401 -from hidapi.udev import open # noqa: F401 -from hidapi.udev import open_path # noqa: F401 -from hidapi.udev import read # noqa: F401 -from hidapi.udev import write # noqa: F401 +import platform as _platform + +if _platform.system() in ('Darwin', 'Windows'): + from hidapi.hidapi import close # noqa: F401 + from hidapi.hidapi import enumerate # noqa: F401 + from hidapi.hidapi import find_paired_node # noqa: F401 + from hidapi.hidapi import find_paired_node_wpid # noqa: F401 + from hidapi.hidapi import get_manufacturer # noqa: F401 + from hidapi.hidapi import get_product # noqa: F401 + from hidapi.hidapi import get_serial # noqa: F401 + from hidapi.hidapi import monitor_glib # noqa: F401 + from hidapi.hidapi import open # noqa: F401 + from hidapi.hidapi import open_path # noqa: F401 + from hidapi.hidapi import read # noqa: F401 + from hidapi.hidapi import write # noqa: F401 +else: + from hidapi.udev import close # noqa: F401 + from hidapi.udev import enumerate # noqa: F401 + from hidapi.udev import find_paired_node # noqa: F401 + from hidapi.udev import find_paired_node_wpid # noqa: F401 + from hidapi.udev import get_manufacturer # noqa: F401 + from hidapi.udev import get_product # noqa: F401 + from hidapi.udev import get_serial # noqa: F401 + from hidapi.udev import monitor_glib # noqa: F401 + from hidapi.udev import open # noqa: F401 + from hidapi.udev import open_path # noqa: F401 + from hidapi.udev import read # noqa: F401 + from hidapi.udev import write # noqa: F401 __version__ = '0.9' diff --git a/lib/hidapi/hidapi.py b/lib/hidapi/hidapi.py new file mode 100644 index 00000000..324cc329 --- /dev/null +++ b/lib/hidapi/hidapi.py @@ -0,0 +1,500 @@ +# -*- python-mode -*- + +## Copyright (C) 2012-2013 Daniel Pavel +## +## 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 +## the Free Software Foundation; either version 2 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License along +## with this program; if not, write to the Free Software Foundation, Inc., +## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Generic Human Interface Device API. + +This provides a python interface to libusb's hidapi library which, +unlike udev, is available for non-linux platforms. +See https://github.com/libusb/hidapi for how to obtain binaries. + +Parts of this code are adapted from https://github.com/apmorton/pyhidapi +which is MIT licensed. +""" +import atexit +import ctypes +import platform as _platform + +from collections import namedtuple +from logging import INFO as _INFO +from logging import getLogger +from threading import Thread +from time import sleep + +_log = getLogger(__name__) +del getLogger +native_implementation = 'hidapi' + +# Device info as expected by Solaar +DeviceInfo = namedtuple( + 'DeviceInfo', [ + 'path', + 'bus_id', + 'vendor_id', + 'product_id', + 'interface', + 'driver', + 'manufacturer', + 'product', + 'serial', + 'release', + 'isDevice', + 'hidpp_short', + 'hidpp_long', + ] +) +del namedtuple + +# Global handle to hidapi +_hidapi = None + +# hidapi binary names for various platforms +_library_paths = ( + 'libhidapi-hidraw.so', 'libhidapi-hidraw.so.0', 'libhidapi-libusb.so', 'libhidapi-libusb.so.0', + 'libhidapi-iohidmanager.so', 'libhidapi-iohidmanager.so.0', 'libhidapi.dylib', 'hidapi.dll', 'libhidapi-0.dll' +) + +for lib in _library_paths: + try: + _hidapi = ctypes.cdll.LoadLibrary(lib) + break + except OSError: + pass +else: + raise ImportError(f"Unable to load hdiapi library, tried: {' '.join(_library_paths)}") + + +# Retrieve version of hdiapi library +class _cHidApiVersion(ctypes.Structure): + _fields_ = [ + ('major', ctypes.c_int), + ('minor', ctypes.c_int), + ('patch', ctypes.c_int), + ] + + +_hidapi.hid_version.argtypes = [] +_hidapi.hid_version.restype = ctypes.POINTER(_cHidApiVersion) +_hid_version = _hidapi.hid_version() + + +# Construct device info struct based on API version +class _cDeviceInfo(ctypes.Structure): + + def as_dict(self): + return {name: getattr(self, name) for name, _t in self._fields_ if name != 'next'} + + +# Low level hdiapi device info struct +# See https://github.com/libusb/hidapi/blob/master/hidapi/hidapi.h#L143 +_cDeviceInfo_fields = [ + ('path', ctypes.c_char_p), + ('vendor_id', ctypes.c_ushort), + ('product_id', ctypes.c_ushort), + ('serial_number', ctypes.c_wchar_p), + ('release_number', ctypes.c_ushort), + ('manufacturer_string', ctypes.c_wchar_p), + ('product_string', ctypes.c_wchar_p), + ('usage_page', ctypes.c_ushort), + ('usage', ctypes.c_ushort), + ('interface_number', ctypes.c_int), + ('next', ctypes.POINTER(_cDeviceInfo)), +] +if _hid_version.contents.major >= 0 and _hid_version.contents.minor >= 13: + _cDeviceInfo_fields.append(('bus_type', ctypes.c_int)) +_cDeviceInfo._fields_ = _cDeviceInfo_fields + +# Set up hidapi functions +_hidapi.hid_init.argtypes = [] +_hidapi.hid_init.restype = ctypes.c_int +_hidapi.hid_exit.argtypes = [] +_hidapi.hid_exit.restype = ctypes.c_int +_hidapi.hid_enumerate.argtypes = [ctypes.c_ushort, ctypes.c_ushort] +_hidapi.hid_enumerate.restype = ctypes.POINTER(_cDeviceInfo) +_hidapi.hid_free_enumeration.argtypes = [ctypes.POINTER(_cDeviceInfo)] +_hidapi.hid_free_enumeration.restype = None +_hidapi.hid_open.argtypes = [ctypes.c_ushort, ctypes.c_ushort, ctypes.c_wchar_p] +_hidapi.hid_open.restype = ctypes.c_void_p +_hidapi.hid_open_path.argtypes = [ctypes.c_char_p] +_hidapi.hid_open_path.restype = ctypes.c_void_p +_hidapi.hid_write.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t] +_hidapi.hid_write.restype = ctypes.c_int +_hidapi.hid_read_timeout.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int] +_hidapi.hid_read_timeout.restype = ctypes.c_int +_hidapi.hid_read.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t] +_hidapi.hid_read.restype = ctypes.c_int +_hidapi.hid_get_input_report.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t] +_hidapi.hid_get_input_report.restype = ctypes.c_int +_hidapi.hid_set_nonblocking.argtypes = [ctypes.c_void_p, ctypes.c_int] +_hidapi.hid_set_nonblocking.restype = ctypes.c_int +_hidapi.hid_send_feature_report.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int] +_hidapi.hid_send_feature_report.restype = ctypes.c_int +_hidapi.hid_get_feature_report.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t] +_hidapi.hid_get_feature_report.restype = ctypes.c_int +_hidapi.hid_close.argtypes = [ctypes.c_void_p] +_hidapi.hid_close.restype = None +_hidapi.hid_get_manufacturer_string.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_size_t] +_hidapi.hid_get_manufacturer_string.restype = ctypes.c_int +_hidapi.hid_get_product_string.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_size_t] +_hidapi.hid_get_product_string.restype = ctypes.c_int +_hidapi.hid_get_serial_number_string.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_size_t] +_hidapi.hid_get_serial_number_string.restype = ctypes.c_int +_hidapi.hid_get_indexed_string.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_wchar_p, ctypes.c_size_t] +_hidapi.hid_get_indexed_string.restype = ctypes.c_int +_hidapi.hid_error.argtypes = [ctypes.c_void_p] +_hidapi.hid_error.restype = ctypes.c_wchar_p + +# Initialize hidapi +_hidapi.hid_init() +atexit.register(_hidapi.hid_exit) + +# Solaar opens the same device more than once which will fail unless we +# allow non-exclusive opening. On windows opening with shared access is +# the default, for macOS we need to set it explicitly. +if _platform.system() == 'Darwin': + _hidapi.hid_darwin_set_open_exclusive.argtypes = [ctypes.c_int] + _hidapi.hid_darwin_set_open_exclusive.restype = None + _hidapi.hid_darwin_set_open_exclusive(0) + + +class HIDError(Exception): + pass + + +def _enumerate_devices(): + """ Returns all HID devices which are potentially useful to us """ + devices = [] + c_devices = _hidapi.hid_enumerate(0, 0) + p = c_devices + while p: + devices.append(p.contents.as_dict()) + p = p.contents.next + _hidapi.hid_free_enumeration(c_devices) + + keyboard_or_mouse = {d['path'] for d in devices if d['usage_page'] == 1 and d['usage'] in (6, 2)} + unique_devices = {} + for device in devices: + # On macOS we cannot access keyboard or mouse devices without special permissions. Since + # we don't need them anyway we remove them so opening them doesn't cause errors later. + if device['path'] in keyboard_or_mouse: + # print(f"Ignoring keyboard or mouse device: {device}") + continue + + # hidapi returns separate entries for each usage page of a device. + # Deduplicate by path to only keep one device entry. + if device['path'] not in unique_devices: + unique_devices[device['path']] = device + + unique_devices = unique_devices.values() + # print("Unique devices:\n" + '\n'.join([f"{dev}" for dev in unique_devices])) + return unique_devices + + +# Use a separate thread to check if devices have been removed or connected +class _DeviceMonitor(Thread): + + def __init__(self, device_callback, polling_delay=5.0): + self.device_callback = device_callback + self.polling_delay = polling_delay + # daemon threads are automatically killed when main thread exits + super().__init__(daemon=True) + + def run(self): + # Populate initial set of devices so startup doesn't cause any callbacks + self.prev_devices = {tuple(dev.items()): dev for dev in _enumerate_devices()} + + # Continously enumerate devices and raise callback for changes + while True: + current_devices = {tuple(dev.items()): dev for dev in _enumerate_devices()} + for key, device in self.prev_devices.items(): + if key not in current_devices: + self.device_callback('remove', device) + for key, device in current_devices.items(): + if key not in self.prev_devices: + self.device_callback('add', device) + self.prev_devices = current_devices + sleep(self.polling_delay) + + +# The filterfn is used to determine whether this is a device of interest to Solaar. +# It is given the bus id, vendor id, and product id and returns a dictionary +# with the required hid_driver and usb_interface and whether this is a receiver or device. +def _match(action, device, filterfn): + vid = device['vendor_id'] + pid = device['product_id'] + + # Translate hidapi bus_type to the bus_id values Solaar expects + if device.get('bus_type') == 0x01: + bus_id = 0x03 # USB + elif device.get('bus_type') == 0x02: + bus_id = 0x05 # Bluetooth + else: + bus_id = None + + # Check for hidpp support + device['hidpp_short'] = False + device['hidpp_long'] = False + device_handle = None + try: + device_handle = open_path(device['path']) + report = get_input_report(device_handle, 0x10, 32) + if len(report) == 1 + 6 and report[0] == 0x10: + device['hidpp_short'] = True + report = get_input_report(device_handle, 0x11, 32) + if len(report) == 1 + 19 and report[0] == 0x11: + device['hidpp_long'] = True + except HIDError as e: # noqa: F841 + if _log.isEnabledFor(_INFO): + _log.info(f"Error opening device {device['path']} ({bus_id}/{vid:04X}/{pid:04X}) for hidpp check: {e}") + finally: + if device_handle: + close(device_handle) + + if _log.isEnabledFor(_INFO): + _log.info( + 'Found device BID %s VID %04X PID %04X HID++ %s %s', bus_id, vid, pid, device['hidpp_short'], device['hidpp_long'] + ) + + if not device['hidpp_short'] and not device['hidpp_long']: + return None + + filter = filterfn(bus_id, vid, pid, device['hidpp_short'], device['hidpp_long']) + if not filter: + return + isDevice = filter.get('isDevice') + + if action == 'add': + d_info = DeviceInfo( + path=device['path'].decode(), + bus_id=bus_id, + vendor_id=f'{vid:04X}', + product_id=f'{pid:04X}', + interface=None, + driver=None, + manufacturer=device['manufacturer_string'], + product=device['product_string'], + serial=device['serial_number'], + release=device['release_number'], + isDevice=isDevice, + hidpp_short=device['hidpp_short'], + hidpp_long=device['hidpp_long'], + ) + return d_info + + elif action == 'remove': + d_info = DeviceInfo( + path=device['path'].decode(), + bus_id=None, + vendor_id=f'{vid:04X}', + product_id=f'{pid:04X}', + interface=None, + driver=None, + manufacturer=None, + product=None, + serial=None, + release=None, + isDevice=isDevice, + hidpp_short=None, + hidpp_long=None, + ) + return d_info + + +def find_paired_node(receiver_path, index, timeout): + """Find the node of a device paired with a receiver""" + return None + + +def find_paired_node_wpid(receiver_path, index): + """Find the node of a device paired with a receiver, get wpid from udev""" + return None + + +def monitor_glib(callback, filterfn): + from gi.repository import GLib + + def device_callback(action, device): + # print(f"device_callback({action}): {device}") + if action == 'add': + d_info = _match(action, device, filterfn) + if d_info: + GLib.idle_add(callback, action, d_info) + elif action == 'remove': + # Removed devices will be detected by Solaar directly + pass + + monitor = _DeviceMonitor(device_callback=device_callback) + monitor.start() + + +def enumerate(filterfn): + """Enumerate the HID Devices. + + List all the HID devices attached to the system, optionally filtering by + vendor_id, product_id, and/or interface_number. + + :returns: a list of matching ``DeviceInfo`` tuples. + """ + for device in _enumerate_devices(): + d_info = _match('add', device, filterfn) + if d_info: + yield d_info + + +def open(vendor_id, product_id, serial=None): + """Open a HID device by its Vendor ID, Product ID and optional serial number. + + If no serial is provided, the first device with the specified IDs is opened. + + :returns: an opaque device handle, or ``None``. + """ + if serial is not None: + serial = ctypes.create_unicode_buffer(serial) + + device_handle = _hidapi.hid_open(vendor_id, product_id, serial) + if device_handle is None: + raise HIDError(_hidapi.hid_error(None)) + return device_handle + + +def open_path(device_path): + """Open a HID device by its path name. + + :param device_path: the path of a ``DeviceInfo`` tuple returned by enumerate(). + + :returns: an opaque device handle, or ``None``. + """ + if not isinstance(device_path, bytes): + device_path = device_path.encode() + + device_handle = _hidapi.hid_open_path(device_path) + if device_handle is None: + raise HIDError(_hidapi.hid_error(None)) + return device_handle + + +def close(device_handle): + """Close a HID device. + + :param device_handle: a device handle returned by open() or open_path(). + """ + assert device_handle + _hidapi.hid_close(device_handle) + + +def write(device_handle, data): + """Write an Output report to a HID device. + + :param device_handle: a device handle returned by open() or open_path(). + :param data: the data bytes to send including the report number as the + first byte. + + The first byte of data[] must contain the Report ID. For + devices which only support a single report, this must be set + to 0x0. The remaining bytes contain the report data. Since + the Report ID is mandatory, calls to hid_write() will always + contain one more byte than the report contains. For example, + if a hid report is 16 bytes long, 17 bytes must be passed to + hid_write(), the Report ID (or 0x0, for devices with a + single report), followed by the report data (16 bytes). In + this example, the length passed in would be 17. + + write() will send the data on the first OUT endpoint, if + one exists. If it does not, it will send the data through + the Control Endpoint (Endpoint 0). + """ + assert device_handle + assert data + assert isinstance(data, bytes), (repr(data), type(data)) + + bytes_written = _hidapi.hid_write(device_handle, data, len(data)) + if bytes_written < 0: + raise HIDError(_hidapi.hid_error(device_handle)) + return bytes_written + + +def read(device_handle, bytes_count, timeout_ms=None): + """Read an Input report from a HID device. + + :param device_handle: a device handle returned by open() or open_path(). + :param bytes_count: maximum number of bytes to read. + :param timeout_ms: can be -1 (default) to wait for data indefinitely, 0 to + read whatever is in the device's input buffer, or a positive integer to + wait that many milliseconds. + + Input reports are returned to the host through the INTERRUPT IN endpoint. + The first byte will contain the Report number if the device uses numbered + reports. + + :returns: the data packet read, an empty bytes string if a timeout was + reached, or None if there was an error while reading. + """ + assert device_handle + + data = ctypes.create_string_buffer(bytes_count) + if timeout_ms is None or timeout_ms < 0: + bytes_read = _hidapi.hid_read(device_handle, data, bytes_count) + else: + bytes_read = _hidapi.hid_read_timeout(device_handle, data, bytes_count, timeout_ms) + + if bytes_read < 0: + raise HIDError(_hidapi.hid_error(device_handle)) + return None + + return data.raw[:bytes_read] + + +def get_input_report(device_handle, report_id, size): + assert device_handle + data = ctypes.create_string_buffer(size) + data[0] = bytearray((report_id, )) + size = _hidapi.hid_get_input_report(device_handle, data, size) + if size < 0: + raise HIDError(_hidapi.hid_error(device_handle)) + return data.raw[:size] + + +def _readstring(device_handle, func, max_length=255): + assert device_handle + buf = ctypes.create_unicode_buffer(max_length) + ret = func(device_handle, buf, max_length) + if ret < 0: + raise HIDError('Error reading device property') + return buf.value + + +def get_manufacturer(device_handle): + """Get the Manufacturer String from a HID device. + + :param device_handle: a device handle returned by open() or open_path(). + """ + return _readstring(device_handle, _hidapi.get_manufacturer_string) + + +def get_product(device_handle): + """Get the Product String from a HID device. + + :param device_handle: a device handle returned by open() or open_path(). + """ + return _readstring(device_handle, _hidapi.get_product_string) + + +def get_serial(device_handle): + """Get the serial number from a HID device. + + :param device_handle: a device handle returned by open() or open_path(). + """ + return _readstring(device_handle, _hidapi.get_serial_number_string) diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py index b0e5b2ea..3d8895d7 100644 --- a/lib/logitech_receiver/diversion.py +++ b/lib/logitech_receiver/diversion.py @@ -19,6 +19,7 @@ import ctypes as _ctypes import os as _os import os.path as _path +import platform as _platform import sys as _sys import time as _time @@ -28,8 +29,15 @@ from logging import getLogger from math import sqrt as _sqrt from struct import unpack as _unpack +# There is no evdev on macOS or Windows. Diversion will not work without +# it but other Solaar functionality is available. +if _platform.system() in ('Darwin', 'Windows'): + evdev = None +else: + import evdev + import dbus -import evdev + import keysyms.keysymdef as _keysymdef import psutil @@ -182,25 +190,32 @@ def xkb_setup(): return Xkbdisplay -buttons = { - 'unknown': (None, None), - 'left': (1, evdev.ecodes.ecodes['BTN_LEFT']), - 'middle': (2, evdev.ecodes.ecodes['BTN_MIDDLE']), - 'right': (3, evdev.ecodes.ecodes['BTN_RIGHT']), - 'scroll_up': (4, evdev.ecodes.ecodes['BTN_4']), - 'scroll_down': (5, evdev.ecodes.ecodes['BTN_5']), - 'scroll_left': (6, evdev.ecodes.ecodes['BTN_6']), - 'scroll_right': (7, evdev.ecodes.ecodes['BTN_7']), - 'button8': (8, evdev.ecodes.ecodes['BTN_8']), - 'button9': (9, evdev.ecodes.ecodes['BTN_9']), -} +if evdev: + buttons = { + 'unknown': (None, None), + 'left': (1, evdev.ecodes.ecodes['BTN_LEFT']), + 'middle': (2, evdev.ecodes.ecodes['BTN_MIDDLE']), + 'right': (3, evdev.ecodes.ecodes['BTN_RIGHT']), + 'scroll_up': (4, evdev.ecodes.ecodes['BTN_4']), + 'scroll_down': (5, evdev.ecodes.ecodes['BTN_5']), + 'scroll_left': (6, evdev.ecodes.ecodes['BTN_6']), + 'scroll_right': (7, evdev.ecodes.ecodes['BTN_7']), + 'button8': (8, evdev.ecodes.ecodes['BTN_8']), + 'button9': (9, evdev.ecodes.ecodes['BTN_9']), + } + + # uinput capability for keyboard keys, mouse buttons, and scrolling + key_events = [c for n, c in evdev.ecodes.ecodes.items() if n.startswith('KEY') and n != 'KEY_CNT'] + for (_, evcode) in buttons.values(): + if evcode: + key_events.append(evcode) + devicecap = {evdev.ecodes.EV_KEY: key_events, evdev.ecodes.EV_REL: [evdev.ecodes.REL_WHEEL, evdev.ecodes.REL_HWHEEL]} +else: + # Just mock these since they won't be useful without evdev anyway + buttons = {} + key_events = [] + devicecap = {} -# uinput capability for keyboard keys, mouse buttons, and scrolling -key_events = [c for n, c in evdev.ecodes.ecodes.items() if n.startswith('KEY') and n != 'KEY_CNT'] -for (_, evcode) in buttons.values(): - if evcode: - key_events.append(evcode) -devicecap = {evdev.ecodes.EV_KEY: key_events, evdev.ecodes.EV_REL: [evdev.ecodes.REL_WHEEL, evdev.ecodes.REL_HWHEEL]} udevice = None diff --git a/lib/logitech_receiver/listener.py b/lib/logitech_receiver/listener.py index 8456f2c5..86dc7013 100644 --- a/lib/logitech_receiver/listener.py +++ b/lib/logitech_receiver/listener.py @@ -140,7 +140,11 @@ class EventsListener(_threading.Thread): """ def __init__(self, receiver, notifications_callback): - super().__init__(name=self.__class__.__name__ + ':' + receiver.path.split('/')[2]) + try: + path_name = receiver.path.split('/')[2] + except IndexError: + path_name = receiver.path + super().__init__(name=self.__class__.__name__ + ':' + path_name) self.daemon = True self._active = False self.receiver = receiver diff --git a/lib/solaar/gtk.py b/lib/solaar/gtk.py index cd49717e..f8042365 100755 --- a/lib/solaar/gtk.py +++ b/lib/solaar/gtk.py @@ -21,6 +21,7 @@ import importlib import logging import os.path +import platform import signal import sys import tempfile @@ -137,7 +138,8 @@ def _handlesig(signl, stack): def main(): - _require('pyudev', 'python3-pyudev') + if platform.system() not in ('Darwin', 'Windows'): + _require('pyudev', 'python3-pyudev') args = _parse_arguments() if not args: diff --git a/setup.py b/setup.py index 2973754e..d10a2cdd 100755 --- a/setup.py +++ b/setup.py @@ -76,7 +76,7 @@ For instructions on installing Solaar see https://pwr-solaar.github.io/Solaar/in # os_requires=['gi.repository.GObject (>= 2.0)', 'gi.repository.Gtk (>= 3.0)'], python_requires='>=3.7', install_requires=[ - 'evdev (>= 1.1.2)', + 'evdev (>= 1.1.2) ; platform_system=="Linux"', 'pyudev (>= 0.13)', 'PyYAML (>= 3.12)', 'python-xlib (>= 0.27)',