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 <pfpschneider@gmail.com>
This commit is contained in:
parent
d9e5e33947
commit
29ff35d553
|
@ -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`
|
`~/.local/share/solaar/udev-rules.d/42-logitech-unify-permissions.rules`
|
||||||
to `/etc/udev/rules.d` as root.
|
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
|
# Manual installation from GitHub
|
||||||
|
|
||||||
|
|
|
@ -17,17 +17,33 @@
|
||||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
"""Generic Human Interface Device API."""
|
"""Generic Human Interface Device API."""
|
||||||
|
|
||||||
from hidapi.udev import close # noqa: F401
|
import platform as _platform
|
||||||
from hidapi.udev import enumerate # noqa: F401
|
|
||||||
from hidapi.udev import find_paired_node # noqa: F401
|
if _platform.system() in ('Darwin', 'Windows'):
|
||||||
from hidapi.udev import find_paired_node_wpid # noqa: F401
|
from hidapi.hidapi import close # noqa: F401
|
||||||
from hidapi.udev import get_manufacturer # noqa: F401
|
from hidapi.hidapi import enumerate # noqa: F401
|
||||||
from hidapi.udev import get_product # noqa: F401
|
from hidapi.hidapi import find_paired_node # noqa: F401
|
||||||
from hidapi.udev import get_serial # noqa: F401
|
from hidapi.hidapi import find_paired_node_wpid # noqa: F401
|
||||||
from hidapi.udev import monitor_glib # noqa: F401
|
from hidapi.hidapi import get_manufacturer # noqa: F401
|
||||||
from hidapi.udev import open # noqa: F401
|
from hidapi.hidapi import get_product # noqa: F401
|
||||||
from hidapi.udev import open_path # noqa: F401
|
from hidapi.hidapi import get_serial # noqa: F401
|
||||||
from hidapi.udev import read # noqa: F401
|
from hidapi.hidapi import monitor_glib # noqa: F401
|
||||||
from hidapi.udev import write # 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'
|
__version__ = '0.9'
|
||||||
|
|
|
@ -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)
|
|
@ -19,6 +19,7 @@
|
||||||
import ctypes as _ctypes
|
import ctypes as _ctypes
|
||||||
import os as _os
|
import os as _os
|
||||||
import os.path as _path
|
import os.path as _path
|
||||||
|
import platform as _platform
|
||||||
import sys as _sys
|
import sys as _sys
|
||||||
import time as _time
|
import time as _time
|
||||||
|
|
||||||
|
@ -28,8 +29,15 @@ from logging import getLogger
|
||||||
from math import sqrt as _sqrt
|
from math import sqrt as _sqrt
|
||||||
from struct import unpack as _unpack
|
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 dbus
|
||||||
import evdev
|
|
||||||
import keysyms.keysymdef as _keysymdef
|
import keysyms.keysymdef as _keysymdef
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
|
@ -182,25 +190,32 @@ def xkb_setup():
|
||||||
return Xkbdisplay
|
return Xkbdisplay
|
||||||
|
|
||||||
|
|
||||||
buttons = {
|
if evdev:
|
||||||
'unknown': (None, None),
|
buttons = {
|
||||||
'left': (1, evdev.ecodes.ecodes['BTN_LEFT']),
|
'unknown': (None, None),
|
||||||
'middle': (2, evdev.ecodes.ecodes['BTN_MIDDLE']),
|
'left': (1, evdev.ecodes.ecodes['BTN_LEFT']),
|
||||||
'right': (3, evdev.ecodes.ecodes['BTN_RIGHT']),
|
'middle': (2, evdev.ecodes.ecodes['BTN_MIDDLE']),
|
||||||
'scroll_up': (4, evdev.ecodes.ecodes['BTN_4']),
|
'right': (3, evdev.ecodes.ecodes['BTN_RIGHT']),
|
||||||
'scroll_down': (5, evdev.ecodes.ecodes['BTN_5']),
|
'scroll_up': (4, evdev.ecodes.ecodes['BTN_4']),
|
||||||
'scroll_left': (6, evdev.ecodes.ecodes['BTN_6']),
|
'scroll_down': (5, evdev.ecodes.ecodes['BTN_5']),
|
||||||
'scroll_right': (7, evdev.ecodes.ecodes['BTN_7']),
|
'scroll_left': (6, evdev.ecodes.ecodes['BTN_6']),
|
||||||
'button8': (8, evdev.ecodes.ecodes['BTN_8']),
|
'scroll_right': (7, evdev.ecodes.ecodes['BTN_7']),
|
||||||
'button9': (9, evdev.ecodes.ecodes['BTN_9']),
|
'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
|
udevice = None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -140,7 +140,11 @@ class EventsListener(_threading.Thread):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, receiver, notifications_callback):
|
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.daemon = True
|
||||||
self._active = False
|
self._active = False
|
||||||
self.receiver = receiver
|
self.receiver = receiver
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
|
import platform
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
@ -137,7 +138,8 @@ def _handlesig(signl, stack):
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
_require('pyudev', 'python3-pyudev')
|
if platform.system() not in ('Darwin', 'Windows'):
|
||||||
|
_require('pyudev', 'python3-pyudev')
|
||||||
|
|
||||||
args = _parse_arguments()
|
args = _parse_arguments()
|
||||||
if not args:
|
if not args:
|
||||||
|
|
2
setup.py
2
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)'],
|
# os_requires=['gi.repository.GObject (>= 2.0)', 'gi.repository.Gtk (>= 3.0)'],
|
||||||
python_requires='>=3.7',
|
python_requires='>=3.7',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'evdev (>= 1.1.2)',
|
'evdev (>= 1.1.2) ; platform_system=="Linux"',
|
||||||
'pyudev (>= 0.13)',
|
'pyudev (>= 0.13)',
|
||||||
'PyYAML (>= 3.12)',
|
'PyYAML (>= 3.12)',
|
||||||
'python-xlib (>= 0.27)',
|
'python-xlib (>= 0.27)',
|
||||||
|
|
Loading…
Reference in New Issue