diff --git a/.github/workflows/install.yaml b/.github/workflows/install.yaml new file mode 100644 index 0000000..e879179 --- /dev/null +++ b/.github/workflows/install.yaml @@ -0,0 +1,29 @@ +name: Test install + +on: + - push + - pull_request + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + include: + - os: ubuntu-latest + python-version: "3.9" + + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install python-evdev + run: | + python -m pip install -v . + (cd /tmp && python -c "import evdev.ecodes; print(evdev.ecodes)") diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..20d254b --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: + - push + - pull_request + +jobs: + pylint: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.14"] + + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Check for pylint errors + run: | + python -m pip install pylint setuptools + python setup.py build + python -m pylint --verbose -E build/lib*/evdev diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..073d524 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Test + +on: + - push + - pull_request + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.14"] + + steps: + - uses: actions/checkout@v6 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Run pytest tests + # sudo required to write to uinputs + run: | + sudo python -m pip install pytest setuptools + sudo python -m pip install -e . + sudo python -m pytest tests diff --git a/.gitignore b/.gitignore index 7623229..70ac303 100644 --- a/.gitignore +++ b/.gitignore @@ -5,17 +5,24 @@ develop-eggs/ dist/ build/ +wheelhouse/ dropin.cache pip-log.txt .installed.cfg .coverage tags TAGS -evdev/*.so -evdev/ecodes.c -doc/_build +.#* __pycache__ +.pytest_cache +.ruff_cache +.venv +uv.lock -evdev/_ecodes.py -evdev/_input.py -evdev/_uinput.py +src/evdev/*.so +src/evdev/ecodes.c +src/evdev/ecodes.pyi +docs/_build +src/evdev/_ecodes.py +src/evdev/_input.py +src/evdev/_uinput.py diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..4a50dff --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,15 @@ +# https://docs.readthedocs.io/en/stable/config-file/v2.html +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: requirements-dev.txt + - path: . \ No newline at end of file diff --git a/LICENSE b/LICENSE index ec85c1d..8482b07 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2013 Georgi Valkov. All rights reserved. +Copyright (c) 2012-2025 Georgi Valkov. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -6,7 +6,7 @@ met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the @@ -16,13 +16,14 @@ met: be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL GEORGI VALKOV BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in index fffc1f5..be2be3d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ -# make github and sdist happy -include README.rst -include evdev/genecodes.py -include LICENSE +# The _ecodes extension module source file needs to be generated against the +# evdev headers of the running kernel. Refer to the 'build_ecodes' distutils +# command in setup.py. +exclude src/evdev/ecodes.c +include src/evdev/ecodes.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..9746040 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# evdev + +

+ pypi version + License + Packaging status +

+ +This package provides bindings to the generic input event interface in Linux. +The *evdev* interface serves the purpose of passing events generated in the +kernel directly to userspace through character devices that are typically +located in `/dev/input/`. + +This package also comes with bindings to *uinput*, the userspace input +subsystem. *Uinput* allows userspace programs to create and handle input devices +that can inject events directly into the input subsystem. + +***Documentation:*** +https://python-evdev.readthedocs.io/en/latest/ + +***Development:*** +https://github.com/gvalkov/python-evdev + +***Package:*** +https://pypi.python.org/pypi/evdev + +***Changelog:*** +https://python-evdev.readthedocs.io/en/latest/changelog.html \ No newline at end of file diff --git a/README.rst b/README.rst deleted file mode 100644 index 46f8d67..0000000 --- a/README.rst +++ /dev/null @@ -1,24 +0,0 @@ -*evdev* -------- - -This package provides bindings to the generic input event interface in -Linux. The *evdev* interface serves the purpose of passing events -generated in the kernel directly to userspace through character -devices that are typically located in ``/dev/input/``. - -This package also comes with bindings to *uinput*, the userspace input -subsystem. *Uinput* allows userspace programs to create and handle -input devices that can inject events directly into the input -subsystem. - -Documentation: - http://python-evdev.readthedocs.org/en/latest/ - -Development: - https://github.com/gvalkov/python-evdev - -Package: - http://pypi.python.org/pypi/evdev - -Changelog: - http://python-evdev.readthedocs.org/en/latest/changelog.html diff --git a/bin/evtest.py b/bin/evtest.py deleted file mode 100755 index ae37673..0000000 --- a/bin/evtest.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 - -''' -evdev example - input device event monitor -''' - - -from sys import argv, exit -from select import select -from evdev import ecodes, InputDevice, list_devices, AbsInfo - - -usage = 'usage: evtest [ ]' -evfmt = 'time {:<16} type {} ({}), code {:<4} ({}), value {}' -device_dir = '/dev/input/' -query_type = None -query_value = None - - -def select_device(): - '''Select a device from the list of accessible input devices.''' - - devices = [InputDevice(i) for i in reversed(list_devices(device_dir))] - if not devices: - print('error: no input devices found (do you have rw permission on /dev/input/*?)') - exit(1) - - dev_fmt = '{0:<3} {1.fn:<20} {1.name:<35} {1.phys}' - dev_lns = [dev_fmt.format(n, d) for n, d in enumerate(devices)] - - print('ID {:<20} {:<35} {}'.format('Device', 'Name', 'Phys')) - print('-' * len(max(dev_lns, key=len))) - print('\n'.join(dev_lns)) - print('') - - choice = input('Select device [0-{}]:'.format(len(dev_lns)-1)) - return devices[int(choice)] - - -def print_event(e): - if e.type == ecodes.EV_SYN: - if e.code == ecodes.SYN_MT_REPORT: - print('time {:<16} +++++++++ {} ++++++++'.format(e.timestamp(), ecodes.SYN[e.code])) - else: - print('time {:<16} --------- {} --------'.format(e.timestamp(), ecodes.SYN[e.code])) - else: - if e.type in ecodes.bytype: - codename = ecodes.bytype[e.type][e.code] - else: - codename = '?' - - print(evfmt.format(e.timestamp(), e.type, ecodes.EV[e.type], e.code, codename, e.value)) - - -if len(argv) == 1: - device = select_device() - -elif len(argv) == 2: - device = InputDevice(argv[1]) - -elif len(argv) == 4: - device = InputDevice(argv[1]) - query_type = argv[2] - query_value = argv[3] -else: - print(usage) - exit(1) - -capabs = device.capabilities(verbose=True) - -print('Device name: {.name}'.format(device)) -print('Device info: {.info}'.format(device)) -print('Repeat settings: {}'.format(device.repeat)) - -if ('EV_LED', ecodes.EV_LED) in capabs: - print('Active LEDs: {}\n'.format(','.join(i[0] for i in device.leds(True)))) - -print('Device capabilities:') -for type, codes in capabs.items(): - print(' Type {} {}:'.format(*type)) - for i in codes: - # i <- ('BTN_RIGHT', 273) or (['BTN_LEFT', 'BTN_MOUSE'], 272) - if isinstance(i[1], AbsInfo): - print(' Code {:<4} {}:'.format(*i[0])) - print(' {}'.format(i[1])) - else: - # multiple names may resolve to one value - s = ', '.join(i[0]) if isinstance(i[0], list) else i[0] - print(' Code {:<4} {}'.format(s, i[1])) - print('') - - -print('Listening for events ...\n') -while True: - r, w, e = select([device], [], []) - - for ev in device.read(): - print_event(ev) diff --git a/doc/apidoc.rst b/doc/apidoc.rst deleted file mode 100644 index 48e4329..0000000 --- a/doc/apidoc.rst +++ /dev/null @@ -1,40 +0,0 @@ -API documentation ------------------ - -``events`` -============ - -.. automodule:: evdev.events - :members: InputEvent, KeyEvent, AbsEvent, RelEvent, SynEvent, event_factory - :undoc-members: - :member-order: groupwise - -``device`` -============ - -.. automodule:: evdev.device - :members: InputDevice, DeviceInfo, AbsInfo, KbdInfo - :undoc-members: - :special-members: - :member-order: groupwise - -``uinput`` -============ - -.. autoclass:: evdev.uinput.UInput - :members: - :special-members: - :member-order: groupwise - -``util`` -========== - -.. automodule:: evdev.util - :members: list_devices, is_device, categorize, resolve_ecodes - :member-order: groupwise - -``ecodes`` -============ - -.. automodule:: evdev.ecodes - :members: diff --git a/doc/changelog.rst b/doc/changelog.rst deleted file mode 100644 index 2b3b09f..0000000 --- a/doc/changelog.rst +++ /dev/null @@ -1,173 +0,0 @@ -Changelog -========= - -0.4.4 (Jun 04, 2014) -^^^^^^^^^^^^^^^^^^^^ - -Fixes: - - Calling ``InputDevice.read_one()`` should always return - ``None``, when there is nothing to be read, even in case of a - ``EAGAIN`` errno (thanks JPP). - -0.4.3 (Dec 19, 2013) -^^^^^^^^^^^^^^^^^^^^ - -Fixes: - - Silence ``OSError`` in destructor (thanks `@polyphemus`_). - - - Make ``InputDevice.close()`` work in cases in which stdin (fd 0) - has been closed (thanks `@polyphemus`_). - -0.4.2 (Dec 13, 2013) -^^^^^^^^^^^^^^^^^^^^ - -Enhancements: - - Rework documentation and docstrings. - -Fixes: - - Call ``InputDevice.close()`` in ``InputDevice.__del__()``. - -0.4.1 (Jul 24, 2013) -^^^^^^^^^^^^^^^^^^^^ - -Fixes: - - Fix reference counting in ``device_read``, ``device_read_many`` - and ``ioctl_capabilities``. - -0.4.0 (Jul 01, 2013) -^^^^^^^^^^^^^^^^^^^^ - -Enhancements: - - Add ``FF_*`` and ``FF_STATUS`` codes to ``ecodes`` (thanks `@bgilbert`_). - - - Reverse event code mappings (``ecodes.{KEY,FF,REL,ABS}`` and - etc.) will now map to a list of codes, whenever a value - corresponds to multiple codes:: - - >>> ecodes.KEY[152] - ... ['KEY_COFFEE', 'KEY_SCREENLOCK'] - >>> ecodes.KEY[30] - ... 'KEY_A' - - - Set the state of a LED through ``device.set_led()`` (thanks - `@accek`_). ``device.fd`` is opened in ``O_RDWR`` mode from now on. - -Fixes: - - Fix segfault in ``device_read_many()`` (thanks `@bgilbert`_). - -0.3.3 (May 29, 2013) -^^^^^^^^^^^^^^^^^^^^ - -Fixes: - - Raise ``IOError`` from ``device_read()`` and ``device_read_many()`` when - ``read()`` fails. - - - Several stability and style changes (thank you debian code reviewers). - -0.3.2 (Apr 05, 2013) -^^^^^^^^^^^^^^^^^^^^ - -Fixes: - - Fix vendor id and product id order in ``DeviceInfo`` (thanks `@kived`_). - -0.3.1 (Nov 23, 2012) -^^^^^^^^^^^^^^^^^^^^ - -Fixes: - - ``device.read()`` will return an empty tuple if the device has - nothing to offer (instead of segfaulting). - - - Exclude unnecessary package data in sdist and bdist. - -0.3.0 (Nov 06, 2012) -^^^^^^^^^^^^^^^^^^^^ - -Enhancements: - - Add ability to set/get auto-repeat settings with ``EVIOC{SG}REP``. - - - Add ``device.version`` - the value of ``EVIOCGVERSION``. - - - Add ``device.read_loop()``. - - - Add ``device.grab()`` and ``device.ungrab()`` - exposes ``EVIOCGRAB``. - - - Add ``device.leds`` - exposes ``EVIOCGLED``. - - - Replace ``DeviceInfo`` class with a namedtuple. - -Fixes: - - ``device.read_one()`` was dropping events. - - - Rename ``AbsData`` to ``AbsInfo`` (as in ``struct input_absinfo``). - - -0.2.0 (Aug 22, 2012) -^^^^^^^^^^^^^^^^^^^^ - -Enhancements: - - Add the ability to set arbitrary device capabilities on uinput - devices (defaults to all ``EV_KEY`` ecodes). - - - Add ``UInput.device`` which is an open ``InputDevice`` to the - input device that uinput 'spawns'. - - - Add ``UInput.capabilities()`` which is just a shortcut to - ``UInput.device.capabilities()``. - - - Rename ``UInput.write()`` to ``UInput.write_event()``. - - - Add a simpler ``UInput.write(type, code, value)`` method. - - - Make all ``UInput`` constructor arguments optional (default - device name is now ``py-evdev-uinput``). - - - Add the ability to set ``absmin``, ``absmax``, ``absfuzz`` and - ``absflat`` when specifying the uinput device's capabilities. - - - Remove the ``nophys`` argument - if a device fails the - ``EVIOCGPHYS`` ioctl, phys will equal the empty string. - - - Make ``InputDevice.capabilities()`` perform a ``EVIOCGABS`` ioctl - for devices that support ``EV_ABS`` and return that info wrapped in - an ``AbsData`` namedtuple. - - - Split ``ioctl_devinfo`` into ``ioctl_devinfo`` and - ``ioctl_capabilities``. - - - Split ``uinput_open()`` to ``uinput_open()`` and ``uinput_create()`` - - - Add more uinput usage examples and documentation. - - - Rewrite uinput tests. - - - Remove ``mouserel`` and ``mouseabs`` from ``UInput``. - - - Tie the sphinx version and release to the distutils version. - - - Set 'methods-before-attributes' sorting in the docs. - - -Fixes: - - Remove ``KEY_CNT`` and ``KEY_MAX`` from ``ecodes.keys``. - - -0.1.1 (May 18, 2012) -^^^^^^^^^^^^^^^^^^^^ - -Enhancements: - - Add ``events.keys``, which is a combination of all ``BTN_`` and - ``KEY_`` event codes. - -Fixes: - - ``ecodes.c`` was not generated when installing through ``pip``. - - -0.1.0 (May 17, 2012) -^^^^^^^^^^^^^^^^^^^^ - -*Initial Release* - -.. _`@polyphemus`: https://github.com/polyphemus -.. _`@bgilbert`: https://github.com/bgilbert -.. _`@accek`: https://github.com/accek -.. _`@kived`: https://github.com/kived diff --git a/doc/index.rst b/doc/index.rst deleted file mode 100644 index 1c53e02..0000000 --- a/doc/index.rst +++ /dev/null @@ -1,71 +0,0 @@ -*evdev* documentation ---------------------- - -This package provides bindings to the generic input event interface in -Linux. The *evdev* interface serves the purpose of passing events -generated in the kernel directly to userspace through character -devices that are typically located in ``/dev/input/``. - -This package also comes with bindings to *uinput*, the userspace input -subsystem. *Uinput* allows userspace programs to create and handle -input devices that can inject events directly into the input -subsystem. - -Please refer to the :doc:`tutorial ` and the :doc:`apidoc -` for usage information. - -Contents -======== - -.. toctree:: - :maxdepth: 1 - - tutorial - install - apidoc - changelog - - -Similar Projects -================ - -* `python-uinput`_ -* `ruby-evdev`_ -* `evdev`_ (ctypes) - - -License -======= - -Package :mod:`evdev` is released under the terms of the `Revised BSD License`_. - - -Todo -==== - -* Use libudev to find the uinput device node as well as the other input - devices. Their locations are currently assumed to be ``/dev/uinput`` and - ``/dev/input/*``. - -* More tests. - -* Better uinput support (setting device capabilities as in `python-uinput`_) - -* Expose more input subsystem functionality (``EVIOCSKEYCODE``, ``EVIOCGREP`` etc) - -* Figure out if using ``linux/input.h`` and other kernel headers in your - userspace program binds it to the GPL2. - - -Indices and Tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - - -.. _`Revised BSD License`: https://raw.github.com/gvalkov/python-evdev/master/LICENSE -.. _python-uinput: https://github.com/tuomasjjrasanen/python-uinput -.. _ruby-evdev: http://technofetish.net/repos/buffaloplay/ruby_evdev/doc/ -.. _evdev: http://svn.navi.cx/misc/trunk/python/evdev/ diff --git a/doc/install.rst b/doc/install.rst deleted file mode 100644 index 1b386ab..0000000 --- a/doc/install.rst +++ /dev/null @@ -1,42 +0,0 @@ -Installation -============ - -Before installing :mod:`evdev`, make sure that the Python and Linux -kernel headers are installed on your system. - -On a Debian compatible OS: - -.. code-block:: bash - - $ apt-get install python-dev - $ apt-get install linux-headers-$(uname -r) - -On a Redhat compatible OS: - -.. code-block:: bash - - $ yum install python-devel - $ yum install kernel-headers-$(uname -r) - -The latest stable version can be installed from pypi_, while the -development version can be installed from github_: - -.. code-block:: bash - - $ pip install evdev # latest stable version - $ pip install git+git://github.com/gvalkov/python-evdev.git # latest development version - -:mod:`evdev` can also be installed like any other :mod:`setuptools` -package. - -.. code-block:: bash - - $ git clone github.com/gvalkov/python-evdev.git - $ cd python-evdev - $ git checkout $versiontag - $ python setup.py install - -The :mod:`evdev` package works with CPython **>= 2.7**. - -.. _pypi: http://pypi.python.org/pypi/evdev -.. _github: https://github.com/gvalkov/python-evdev diff --git a/doc/tutorial.rst b/doc/tutorial.rst deleted file mode 100644 index 1c0349a..0000000 --- a/doc/tutorial.rst +++ /dev/null @@ -1,250 +0,0 @@ -Tutorial --------- - -Listing accessible event devices -================================ - -:: - - >>> from evdev import InputDevice, list_devices - - >>> devices = map(InputDevice, list_devices()) - - >>> for dev in devices: - ... print( '%-20s %-32s %s' % (dev.fn, dev.name, dev.phys) ) - /dev/input/event1 Dell Dell USB Keyboard usb-0000:00:12.1-2/input0 - /dev/input/event0 Dell USB Optical Mouse usb-0000:00:12.0-2/input0 - - -Listing device capabilities -=========================== - -:: - - >>> dev = InputDevice('/dev/input/event0') - - >>> print(dev) - device /dev/input/event0, name "Dell USB Optical Mouse", phys "usb-0000:00:12.0-2/input0" - - >>> dev.capabilities() - ... { 0: [0, 1, 2], 1: [272, 273, 274, 275], 2: [0, 1, 6, 8], 4: [4] } - - >>> dev.capabilities(verbose=True) - ... { ('EV_SYN', 0): [('SYN_REPORT', 0), ('SYN_CONFIG', 1), ('SYN_MT_REPORT', 2)], - ... ('EV_KEY', 1): [('BTN_MOUSE', 272), ('BTN_RIGHT', 273), ('BTN_MIDDLE', 274), ('BTN_SIDE', 275)], ... - - -Listing device capabilities for devices with absolute axes -========================================================== - -:: - - >>> dev = InputDevice('/dev/input/event7') - - >>> print(dev) - device /dev/input/event7, name "Wacom Bamboo 2FG 4x5 Finger", phys "" - - >>> dev.capabilities() - ... { 1: [272, 273, 277, 278, 325, 330, 333] , - ... 3: [(0, AbsInfo(min=0, max=15360, fuzz=128, flat=0)), - ... (1, AbsInfo(min=0, max=10240, fuzz=128, flat=0))] } - - >>> dev.capabilities(verbose=True) - ... { ('EV_KEY', 1): [('BTN_MOUSE', 272), ('BTN_RIGHT', 273), ...], - ... ('EV_ABS', 3): [(('ABS_X', 0), AbsInfo(min=0, max=15360, fuzz=128, flat=0)), - ... (('ABS_Y', 1), AbsInfo(min=0, max=10240, fuzz=128, flat=0)),] } - - >>> dev.capabilities(absinfo=False) - ... { 1: [272, 273, 277, 278, 325, 330, 333], - ... 3: [0, 1, 47, 53, 54, 57] } - - -Getting and setting LED states -============================== - -:: - - >>> dev.leds(verbose=True) - ... [('LED_NUML', 0), ('LED_CAPSL', 1)] - - >>> dev.leds() - ... [0, 1] - - >>> dev.set_led(ecodes.LED_NUML, 1) # enable numlock - >>> dev.set_led(ecodes.LED_NUML, 0) # disable numlock - - -Accessing input subsystem constants -=================================== - -:: - - >>> from evdev import ecodes - >>> ecodes.KEY_A, ecodes.ecodes['KEY_A'] - ... (30, 30) - >>> ecodes.KEY[30] - ... 'KEY_A' - >>> ecodes.bytype[ecodes.EV_KEY][30] - ... 'KEY_A' - # a single value in the reverse mappings may correspond to multiple codes - >>> ecodes.KEY[152] - ... ['KEY_COFFEE', 'KEY_SCREENLOCK'] - - -Reading events -============== - -:: - - >>> from evdev import InputDevice, categorize, ecodes - >>> dev = InputDevice('/dev/input/event1') - - >>> print(dev) - device /dev/input/event1, name "Dell Dell USB Keyboard", phys "usb-0000:00:12.1-2/input0" - - >>> for event in dev.read_loop(): - ... if event.type == ecodes.EV_KEY: - ... print(categorize(event)) - ... # pressing 'a' and holding 'space' - key event at 1337016188.396030, 30 (KEY_A), down - key event at 1337016188.492033, 30 (KEY_A), up - key event at 1337016189.772129, 57 (KEY_SPACE), down - key event at 1337016190.275396, 57 (KEY_SPACE), hold - key event at 1337016190.284160, 57 (KEY_SPACE), up - - -Reading events from multiple devices -==================================== - -:: - - >>> from evdev import InputDevice - >>> from select import select - - >>> devices = map(InputDevice, ('/dev/input/event1', '/dev/input/event2')) - >>> devices = {dev.fd : dev for dev in devices} - - >>> for dev in devices.values(): print(dev) - device /dev/input/event1, name "Dell Dell USB Keyboard", phys "usb-0000:00:12.1-2/input0" - device /dev/input/event2, name "Logitech USB Laser Mouse", phys "usb-0000:00:12.0-2/input0" - - >>> while True: - ... r,w,x = select(devices, [], []) - ... for fd in r: - ... for event in devices[fd].read(): - ... print(event) - event at 1351116708.002230, code 01, type 02, val 01 - event at 1351116708.002234, code 00, type 00, val 00 - event at 1351116708.782231, code 04, type 04, val 458782 - event at 1351116708.782237, code 02, type 01, val 01 - - -Reading events with asyncore -============================ - -:: - - >>> from asyncore import file_dispatcher, loop - >>> from evdev import InputDevice, categorize, ecodes - >>> dev = InputDevice('/dev/input/event1') - - >>> class InputDeviceDispatcher(file_dispatcher): - ... def __init__(self, device): - ... self.device = device - ... file_dispatcher.__init__(self, device) - ... - ... def recv(self, ign=None): - ... return self.device.read() - ... - ... def handle_read(self): - ... for event in self.recv(): - ... print(repr(event)) - - >>> InputDeviceDispatcher(dev) - >>> loop() - InputEvent(1337255905L, 358854L, 1, 30, 0L) - InputEvent(1337255905L, 358857L, 0, 0, 0L) - - -Getting exclusive access to a device -==================================== - -:: - - >>> dev.grab() # become the sole recipient of all incoming input events - >>> dev.ungrab() - - -Associating classes with event types -==================================== - -:: - - >>> from evdev import categorize, event_factory, ecodes - - >>> class SynEvent(object): - ... def __init__(self, event): - ... ... - - >>> event_factory[ecodes.EV_SYN] = SynEvent - -See :mod:`events ` for more information. - - -Injecting events -================ - -:: - - >>> from evdev import UInput, ecodes as e - - >>> ui = UInput() - - >>> # accepts only KEY_* events by default - >>> ui.write(e.EV_KEY, e.KEY_A, 1) # KEY_A down - >>> ui.write(e.EV_KEY, e.KEY_A, 0) # KEY_A up - >>> ui.syn() - - >>> ui.close() - - -Injecting events (2) -==================== - -:: - - >>> ev = InputEvent(1334414993, 274296, ecodes.EV_KEY, ecodes.KEY_A, 1) - >>> with UInput() as ui: - ... ui.write_event(ev) - ... ui.syn() - - -Specifying ``uinput`` device options -====================================== -:: - - >>> from evdev import UInput, AbsInfo, ecodes as e - - >>> cap = { - ... e.EV_KEY : [e.KEY_A, e.KEY_B], - ... e.EV_ABS : [ - ... (e.ABS_X, AbsInfo(min=0, max=255, fuzz=0, flat=0)), - ... (e.ABS_Y, AbsInfo(0, 255, 0, 0)), - ... (e.ABS_MT_POSITION_X, (0, 255, 128, 0)) ] - ... } - - >>> ui = UInput(cap, name='example-device', version=0x3) - >>> print(ui) - name "example-device", bus "BUS_USB", vendor "0001", product "0001", version "0003" - event types: EV_KEY EV_ABS EV_SYN - - >>> print(ui.capabilities()) - ... { 0: [0, 1, 3], 1: [30, 48], - ... 3: [(0, AbsInfo(min=0, max=255, fuzz=0, flat=0)), - ... (1, AbsInfo(min=0, max=255, fuzz=0, flat=0)), - ... (53, AbsInfo(min=0, max=255, fuzz=128, flat=0))] } - - >>> # move mouse cursor - >>> ui.write(e.EV_ABS, e.ABS_X, 20) - >>> ui.write(e.EV_ABS, e.ABS_Y, 20) - >>> ui.syn() diff --git a/doc/Makefile b/docs/Makefile similarity index 95% rename from doc/Makefile rename to docs/Makefile index 3af3204..68b86e6 100644 --- a/doc/Makefile +++ b/docs/Makefile @@ -41,10 +41,12 @@ help: clean: -rm -rf $(BUILDDIR)/* + html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + @sed -i $(BUILDDIR)/html/index.html -e 's,Synopsis,Python Bindings,' dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @@ -152,6 +154,13 @@ doctest: @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." + +# .PHONY: news.rst +# news.rst: changelog.rst +# cat $< | grep -FB1 '^^^^^^^^^' \ +# | sed -e '/^\^/d;/--/d' -e 's,\(.*\)(\(.*\)),* **\2**: Version \1released.\n,' \ +# > $@ + $(BUILDDIR)/html/evdev-doc.zip: html cd $(BUILDDIR)/html/ && zip -x \*.zip -r evdev-doc.zip . @echo `readlink -f $@` diff --git a/doc/_static/.keep b/docs/_static/.keep similarity index 100% rename from doc/_static/.keep rename to docs/_static/.keep diff --git a/doc/_templates/.keep b/docs/_templates/.keep similarity index 100% rename from doc/_templates/.keep rename to docs/_templates/.keep diff --git a/docs/apidoc.rst b/docs/apidoc.rst new file mode 100644 index 0000000..ba59e90 --- /dev/null +++ b/docs/apidoc.rst @@ -0,0 +1,75 @@ +API Reference +------------- + +``events`` +============ + +.. automodule:: evdev.events + :members: InputEvent, KeyEvent, AbsEvent, RelEvent, SynEvent, event_factory + :undoc-members: + :exclude-members: __dict__, __str__, __module__, __del__, __slots__, __repr__ + :member-order: bysource + + +``eventio`` +============ + +.. automodule:: evdev.eventio + :members: EventIO + :undoc-members: + :special-members: + :exclude-members: __dict__, __str__, __module__, __del__, __slots__, __repr__ + :member-order: bysource + +``eventio_async`` +================= + +.. automodule:: evdev.eventio_async + :members: EventIO + :undoc-members: + :special-members: + :exclude-members: __dict__, __str__, __module__, __del__, __slots__, __repr__ + :member-order: bysource + +``device`` +============ + +.. automodule:: evdev.device + :members: InputDevice, DeviceInfo, AbsInfo, KbdInfo + :undoc-members: + :special-members: + :exclude-members: __dict__, __str__, __module__, __del__, __slots__, __repr__ + :member-order: bysource + +``uinput`` +============ + +.. automodule:: evdev.uinput + :members: UInput + :special-members: + :exclude-members: __dict__, __str__, __module__, __del__, __slots__, __repr__ + :member-order: bysource + +``util`` +========== + +.. automodule:: evdev.util + :members: list_devices, is_device, categorize, resolve_ecodes, resolve_ecodes_dict + :member-order: bysource + +``ecodes`` +============ + +.. automodule:: evdev.ecodes + :members: + :exclude-members: __module__, keys, ecodes, bytype + :member-order: bysource + +.. autodata:: evdev.ecodes.keys + :annotation: {0: 'KEY_RESERVED', 1: 'KEY_ESC', 2: 'KEY_1', ...} + +.. autodata:: evdev.ecodes.ecodes + :annotation: {'KEY_END': 107, 'FF_RUMBLE': 80, 'KEY_KPDOT': 83, 'KEY_CNT': 768, ...}' + +.. autodata:: evdev.ecodes.bytype + :annotation: {0: {0: 'SYN_REPORT', 1: 'SYN_CONFIG', 2: 'SYN_MT_REPORT', 3: 'SYN_DROPPED'}, ...} diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..bcf1636 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,530 @@ +Changelog +--------- + +1.9.3 (Feb 05, 2025) +==================== + +- Fix several memory leaks in ``input.c``. + +- Raise the minimum supported Python version to 3.9 and the setuptools version to 77.0. + + +1.9.2 (May 01, 2025) +==================== + +- Add the "--reproducible" build option which removes the build date and used headers from the + generated ``ecodes.c``. Example usage:: + + python -m build --config-setting=--build-option='build_ecodes --reproducible' -n + +- Use ``Generic`` to set precise type for ``InputDevice.path``. + + +1.9.1 (Feb 22, 2025) +==================== + +- Fix fox missing ``UI_FF`` constants in generated ``ecodes.py``. + +- More type annotations. + + +1.9.0 (Feb 08, 2025) +==================== + +- Fix for ``CPATH/C_INCLUDE_PATH`` being ignored during build. + +- Slightly faster reading of events in ``device.read()`` and ``device.read_one()``. + +- Fix FreeBSD support. + +- Drop deprecated ``InputDevice.fn`` (use ``InputDevice.path`` instead). + +- Improve type hint coverage and add a ``py.typed`` file to the sdist. + + +1.8.0 (Jan 25, 2025) +==================== + +- Binary wheels are now provided by the `evdev-binary `_ package. + The package is compiled on manylinux_2_28 against kernel 4.18. + +- The ``evdev.ecodes`` module is now generated at install time and contains only constants. This allows type + checking and introspection of the ``evdev.ecodes`` module, without having to execute it first. The old + module is available as ``evdev.ecodes_runtime``. In case generation of the static ``ecodes.py`` fails, the + install process falls back to using ``ecodes_runtime.py`` as ``ecodes.py``. + +- Reverse mappings in ``evdev.ecodes`` that point to more than one value are now tuples instead of lists. For example:: + + >>> ecodes.KEY[153] + ('KEY_DIRECTION', 'KEY_ROTATE_DISPLAY') + +- Raise the minimum supported Python version to 3.8. + +- Fix keyboard delay and repeat being swapped (#227). + +- Move the ``syn()`` convenience method from ``InputDevice`` to ``EventIO`` (#224). + + +1.7.1 (May 8, 2024) +==================== + +- Provide fallback value for ``FF_MAX_EFFECTS``, which fixes the build on EL 7 (#219). + +- Add ``#ifdef`` guards around ``UI_GET_SYSNAME`` to improve kernel compatibility (#218) . + +- Wait up to two seconds for uinput devices to appear. (#215) + + +1.7.0 (Feb 18, 2024) +==================== + +- Respect the ``CPATH/C_INCLUDE_PATH`` environment variables during install. + +- Add the uniq address to the string representation of ``InputDevice``. + +- Improved method for finding the device node corresponding to a uinput device (`#206 `_). + +- Repository TLC (reformatted with ruff, fixed linting warnings, moved packaging metadata to ``pyproject.toml`` etc.). + + +1.6.1 (Jan 20, 2023) +==================== + +- Fix generation of ``ecodes.c`` when the path to ``sys.executable`` contains spaces. + + +1.6.0 (Jul 17, 2022) +==================== + +- Fix Python 3.11 compatibility (`#174 `_). + + +1.5.0 (Mar 24, 2022) +==================== + +- Fix documentation (`#163 `_, `#160 `_). + +- Re-enable TTY echo at evtest exit (`#155 `_). + +- Fix ``ImportError: sys.meta_path is None, Python is likely shutting down`` (`#154 `_). + +- Closing the input device file descriptor in ``InputDevice.close()`` now + happens in the main thread, instead of in a new thread (reverts `#146 + `_). + +- Fix ``util.find_ecodes_by_regex`` not working across all supported Python versions (`#152 `_). + + + +1.4.0 (Jan 16, 2021) +==================== + +- Fix ``InputDevice.set_absinfo`` to allow setting parameters to zero. + +- Fix off-by-one in ``ioctl_EVIOCG_bits``, which causes value at the end of the + list to not be reported back (`#131 `_). + +- Fix ``set_absinfo`` to allow setting parameters to zero (`#128 `_). + +- Fix leak when returning ``BlockingIOError`` from a read (`#143 `_). + +- Fix "There is no current event loop in thread" error for non asyncio code + (`#146 `_). + +- Prevent ``InputDevice`` destructor from blocking (`#145 `_). + +- Add missing return codes to ``os.strerror()`` calls and fix force feedback + example in docs (`#138 `_). + +- Add the ``util.find_ecodes_by_regex()`` helper function. + + + +1.3.0 (Jan 12, 2020) +==================== + +- Fix build on 32bit arches with 64bit time_t + +- Add functionality to query device properties. See ``InputDevice.input_props`` + and the ``input_props`` argument to ``Uinput``. + +- ``KeyEvent`` received an ``allow_unknown`` constructor argument, which + determines what will happen when an event code cannot be mapped to a keycode. + The default and behavior so far has been to raise ``KeyError``. If set to + ``True``, the keycode will be set to the event code formatted as a hex number. + +- Add ``InputDevice.set_absinfo()`` and ``InputDevice.absinfo()``. + +- Instruct the asyncio event loop to stop monitoring the fd of the input device + when the device is closed. + + +1.2.0 (Apr 7, 2019) +==================== + +- Add UInput support for the resolution parameter in AbsInfo. This brings + support for the new method of uinput device setup, which was `introduced in + Linux 4.5`_ (thanks to `@LinusCDE`_). + +- Vendor and product identifiers can be greater or equal to `0x8000` (thanks + `@ivaradi`_). + + +1.1.2 (Sep 1, 2018) +==================== + +- Fix installation on kernels <= 4.4. + +- Fix uinput creation ignoring absinfo settings. + + +1.1.0 (Aug 27, 2018) +==================== + +- Add support for handling force-feedback effect uploads (many thanks to `@ndreys`_). + +- Fix typo preventing ff effects that need left coefficients from working. + + +1.0.0 (Jun 02, 2018) +==================== + +- Prevent ``Uinput`` device creation raising ``Objects/longobject.c:415: bad + argument to internal function`` when a non-complete ``AbsInfo`` structure is + passed. All missing ``AbsInfo`` fields are set to 0. + +- Fix ``Uinput`` device creation raising ``KeyError`` when a capability filtered + by default is not present. + +- The ``InputDevice.fn`` attribute was deprecated in favor of ``InputDevice.path``. + Using the former will show a ``DeprecationWarning``, but would otherwise continue + working as before. + +- Fix ``InputDevice`` comparison raising ``AttributeError`` due to a non-existant + ``path`` attribute. + +- Fix asyncio support in Python 3.5+. + +- Uploading FF effect now works both on Python 2.7 and Python 3+. + +- Remove the ``asyncore`` example from the tutorial. + + +0.8.1 (Mar 24, 2018) +==================== + +- Fix Python 2 compatibility issue in with ``Uinput.from_device``. + +- Fix minor `evdev.evtest` formatting issue. + + +0.8.0 (Mar 22, 2018) +==================== + +- Fix ``InputDevice`` comparison on Python 2. + +- The device path is now considered when comparing two devices. + +- Fix ``UInput.from_device`` not correctly merging the capabilities of + selected devices. + +- The list of excluded event types in ``UInput.from_device`` is now + configurable. For example:: + + UInput.from_device(dev, filtered_types=(EV_SYN, EV_FF)) + + In addition, ``ecodes.EV_FF`` is now excluded by default. + +- Add a context manager for grabbing access to a device - + ``InputDevice.grab_context``. For example:: + + with dev.grab_context(): + pass + +- Add the ``InputDevice.uniq`` attribute, which contains the unique identifier + of the device. As with ``phys``, this attribute may be empty (i.e. `''`). + + +0.7.0 (Jun 16, 2017) +==================== + +- ``InputDevice`` now accepts objects that support the path protocol. + For example:: + + pth = pathlib.Path('/dev/input/event0') + dev = evdev.InputDevice(pth) + +- Support path protocol in ``InputDevice``. This means that ``InputDevice`` + instances can be passed to callers that expect a ``os.PathLike`` object. + +- Exceptions raised during ``InputDevice.async_read()`` (and similar) are now + handled properly (i.e. an exception is set on the returned future instead of + leaking that exception into the event loop) (Fixes `#67`_). + + +0.6.4 (Oct 07, 2016) +==================== + +- Exclude ``ecodes.c`` from source distribution (Fixes `#63`_). + + +0.6.3 (Oct 06, 2016) +==================== + +- Add the ``UInput.from_device`` class method, which allows uinput device to be + created with the capabiltiies of one or more existing input devices:: + + ui = UInput.from_device('/dev/input1', '/dev/input2', **constructor_kwargs) + +- Add the ``build_ecodes`` distutils command, which generates the ``ecodes.c`` + extension module. The new way of overwriting the evdev header locations is:: + + python setup.py build \ + build_ecodes --evdev-headers path/input.h:path/input-event-codes.h \ + build_ext --include-dirs path/ \ + install + + The ``build*`` and ``install`` commands no longer have to be part of the same + command-line (i.e. running ``install`` will reuse the outputs of the last + ``build``). + + +0.6.1 (Jun 04, 2016) +==================== + +- Disable tty echoing while evtest is running. +- Allow evtest to listen to more than one devices. + +- The setup.py script now allows the location of the input header files to be + overwritten. For example:: + + python setup.py build_ext \ + --evdev-headers path/input.h:path/input-event-codes.h \ + --include-dirs path/ \ + install + + +0.6.0 (Feb 14, 2016) +==================== + +- Asyncio and async/await support (many thanks to `@paulo-raca`_). +- Add the ability to set the `phys` property of uinput devices (thanks `@paulo-raca`_). +- Add a generic :func:`InputDevice.set` method (thanks `@paulo-raca`_). +- Distribute the evtest script along with evdev. +- Fix issue with generating :mod:`ecodes.c` in recent kernels (``>= 4.4.0``). +- Fix absinfo item indexes in :func:`UInput.uinput_create()` (thanks `@forsenonlhaimaisentito`_). +- More robust comparison of :class:`InputDevice` objects (thanks `@isia`_). + + +0.5.0 (Jun 16, 2015) +==================== + +- Write access to the input device is no longer mandatory. Evdev will + first try to open the device for reading and writing and fallback to + read-only. Methods that require write access (e.g. :func:`set_led()`) + will raise :class:`EvdevError` if the device is open only for reading. + + +0.4.7 (Oct 07, 2014) +==================== + +- Fallback to distutils if setuptools is not available. + + +0.4.6 (Oct 07, 2014) +==================== + +- Rework documentation and docstrings once more. + +- Fix install on Python 3.4 (works around issue21121_). + +- Fix :func:`ioctl()` requested buffer size (thanks Jakub Wojciech Klama). + + +0.4.5 (Jul 06, 2014) +==================== + +- Add method for returning a list of the currently active keys - + :func:`InputDevice.active_keys()` (thanks `@spasche`_). + +- Fix a potential buffer overflow in :func:`ioctl_capabilities()` (thanks `@spasche`_). + + +0.4.4 (Jun 04, 2014) +==================== + +- Calling :func:`InputDevice.read_one()` should always return ``None``, + when there is nothing to be read, even in case of a ``EAGAIN`` errno + (thanks JPP). + + +0.4.3 (Dec 19, 2013) +==================== + +- Silence :class:`OSError` in destructor (thanks `@polyphemus`_). + +- Make :func:`InputDevice.close()` work in cases in which stdin (fd 0) + has been closed (thanks `@polyphemus`_). + + +0.4.2 (Dec 13, 2013) +==================== + +- Rework documentation and docstrings. + +- Call :func:`InputDevice.close()` from :func:`InputDevice.__del__()`. + + +0.4.1 (Jul 24, 2013) +==================== + +- Fix reference counting in :func:`InputDevice.device_read()`, + :func:`InputDevice.device_read_many()` and :func:`ioctl_capabilities`. + + +0.4.0 (Jul 01, 2013) +==================== + +- Add ``FF_*`` and ``FF_STATUS`` codes to :func:`ecodes` (thanks `@bgilbert`_). + +- Reverse event code mappings (``ecodes.{KEY,FF,REL,ABS}`` and etc.) + will now map to a list of codes, whenever a value corresponds to + multiple codes:: + + >>> ecodes.KEY[152] + ... ['KEY_COFFEE', 'KEY_SCREENLOCK'] + >>> ecodes.KEY[30] + ... 'KEY_A' + +- Set the state of a LED through :func:`InputDevice.set_led()` (thanks + `@accek`_). + +- Open :attr:`InputDevice.fd` in ``O_RDWR`` mode from now on. + +- Fix segfault in :func:`InputDevice.device_read_many()` (thanks `@bgilbert`_). + + +0.3.3 (May 29, 2013) +==================== + +- Raise :class:`IOError` from :func:`InputDevice.device_read()` and + :func:`InputDevice.device_read_many()` when :func:`InputDevice.read()` + fails. + +- Several stability and style changes (thank you debian code reviewers). + + +0.3.2 (Apr 05, 2013) +==================== + +- Fix vendor id and product id order in :func:`DeviceInfo` (thanks `@kived`_). + + +0.3.1 (Nov 23, 2012) +==================== + +- :func:`InputDevice.read()` will return an empty tuple if the device + has nothing to offer (instead of segfaulting). + +- Exclude unnecessary package data in sdist and bdist. + + +0.3.0 (Nov 06, 2012) +==================== + +- Add ability to set/get auto-repeat settings with ``EVIOC{SG}REP``. + +- Add :func:`InputDevice.version` - the value of ``EVIOCGVERSION``. + +- Add :func:`InputDevice.read_loop()`. + +- Add :func:`InputDevice.grab()` and :func:`InputDevice.ungrab()` - + exposes ``EVIOCGRAB``. + +- Add :func:`InputDevice.leds` - exposes ``EVIOCGLED``. + +- Replace :class:`DeviceInfo` class with a namedtuple. + +- Prevent :func:`InputDevice.read_one()` from skipping events. + +- Rename :class:`AbsData` to :class:`AbsInfo` (as in ``struct input_absinfo``). + + +0.2.0 (Aug 22, 2012) +==================== + +- Add the ability to set arbitrary device capabilities on uinput + devices (defaults to all ``EV_KEY`` ecodes). + +- Add :attr:`UInput.device` which is an open :class:`InputDevice` to + the input device that uinput 'spawns'. + +- Add :func:`UInput.capabilities()` which is just a shortcut to + :func:`UInput.device.capabilities()`. + +- Rename :func:`UInput.write()` to :func:`UInput.write_event()`. + +- Add a simpler :func:`UInput.write(type, code, value)` method. + +- Make all :func:`UInput` constructor arguments optional (default + device name is now ``py-evdev-uinput``). + +- Add the ability to set ``absmin``, ``absmax``, ``absfuzz`` and + ``absflat`` when specifying the uinput device's capabilities. + +- Remove the ``nophys`` argument - if a device fails the + ``EVIOCGPHYS`` ioctl, phys will equal the empty string. + +- Make :func:`InputDevice.capabilities()` perform a ``EVIOCGABS`` + ioctl for devices that support ``EV_ABS`` and return that info + wrapped in an ``AbsData`` namedtuple. + +- Split ``ioctl_devinfo`` into ``ioctl_devinfo`` and + ``ioctl_capabilities``. + +- Split :func:`UInput.uinput_open()` to :func:`UInput.uinput_open()` + and :func:`UInput.uinput_create()` + +- Add more uinput usage examples and documentation. + +- Rewrite uinput tests. + +- Remove ``mouserel`` and ``mouseabs`` from :class:`UInput`. + +- Tie the sphinx version and release to the distutils version. + +- Set 'methods-before-attributes' sorting in the docs. + +- Remove ``KEY_CNT`` and ``KEY_MAX`` from :func:`ecodes.keys`. + + +0.1.1 (May 18, 2012) +==================== + +- Add ``events.keys``, which is a combination of all ``BTN_`` and + ``KEY_`` event codes. + +- ``ecodes.c`` was not generated when installing through ``pip``. + + +0.1.0 (May 17, 2012) +==================== + +*Initial Release* + +.. _`@polyphemus`: https://github.com/polyphemus +.. _`@bgilbert`: https://github.com/bgilbert +.. _`@accek`: https://github.com/accek +.. _`@kived`: https://github.com/kived +.. _`@spasche`: https://github.com/spasche +.. _`@isia`: https://github.com/isia +.. _`@forsenonlhaimaisentito`: https://github.com/forsenonlhaimaisentito +.. _`@paulo-raca`: https://github.com/paulo-raca +.. _`@ndreys`: https://github.com/ndreys +.. _`@LinusCDE`: https://github.com/gvalkov/python-evdev/pulls/LinusCDE +.. _`@ivaradi`: https://github.com/gvalkov/python-evdev/pull/104 + +.. _`introduced in Linux 4.5`: https://github.com/torvalds/linux/commit/052876f8e5aec887d22c4d06e54aa5531ffcec75 +.. _issue21121: http://bugs.python.org/issue21121 +.. _`#63`: https://github.com/gvalkov/python-evdev/issues/63 +.. _`#67`: https://github.com/gvalkov/python-evdev/issues/67 diff --git a/doc/conf.py b/docs/conf.py similarity index 68% rename from doc/conf.py rename to docs/conf.py index ee8ec57..0be06b3 100644 --- a/doc/conf.py +++ b/docs/conf.py @@ -1,97 +1,108 @@ -# -*- coding: utf-8 -*- - -import sys, os +import os +import sys +import sphinx_rtd_theme # Check if readthedocs is building us -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +on_rtd = os.environ.get("READTHEDOCS", None) == "True" # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) -from setup import kw # Trick autodoc into running without having built the extension modules. if on_rtd: - with open('../evdev/_ecodes.py', 'w') as fh: - fh.write(''' + with open("../src/evdev/_ecodes.py", "w") as fh: + fh.write( + """ KEY = ABS = REL = SW = MSC = LED = REP = SND = SYN = FF = FF_STATUS = BTN_A = KEY_A = 1 EV_KEY = EV_ABS = EV_REL = EV_SW = EV_MSC = EV_LED = EV_REP = 1 EV_SND = EV_SYN = EV_FF = EV_FF_STATUS = FF_STATUS = 1 -KEY_MAX, KEY_CNT = 1, 2''') +KEY_MAX, KEY_CNT = 1, 2""" + ) - with open('../evdev/_input.py', 'w'): pass - with open('../evdev/_uinput.py', 'w'): pass + with open("../src/evdev/_input.py", "w"): + pass + with open("../src/evdev/_uinput.py", "w"): + pass # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx_rtd_theme", + "sphinx_copybutton", +] + +autodoc_member_order = "bysource" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'python-evdev' -copyright = u'2012-2013, Georgi Valkov' +project = "python-evdev" +copyright = "2012-2025, Georgi Valkov and contributors" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. -release = kw['version'] +release = "1.9.3" # The short X.Y version. version = release # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +# pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- @@ -99,138 +110,127 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -if not on_rtd: - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -else: - html_theme = 'default' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {"full_logo" : True} # Add any paths that contain custom themes here, relative to this directory. # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +html_title = "Python-evdev" # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +html_short_title = "evdev" # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = '' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None -# Add any paths that contain custom static files (such as style sheets) here, +# Add any paths that contain custom static files (such as style sheets here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'python-evdev-doc' +htmlhelp_basename = "python-evdev-doc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'python-evdev.tex', u'evdev documentation', - u'Georgi Valkov', 'manual'), + ("index", "python-evdev.tex", "evdev documentation", "Georgi Valkov", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'python-evdev', u'python-evdev Documentation', - [u'Georgi Valkov'], 1) -] +man_pages = [("index", "python-evdev", "python-evdev Documentation", ["Georgi Valkov"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -239,18 +239,29 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'python-evdev', u'python-evdev Documentation', - u'Georgi Valkov', 'evdev', 'Bindings for the linux input handling subsystem.', - 'Miscellaneous'), + ( + "index", + "python-evdev", + "python-evdev Documentation", + "Georgi Valkov", + "evdev", + "Bindings for the linux input handling subsystem.", + "Miscellaneous", + ), ] -intersphinx_mapping = {'python': ('http://docs.python.org/3.3', None)} +intersphinx_mapping = {"python": ("http://docs.python.org/3", None)} # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' + +# Copybutton config +# copybutton_prompt_text = r">>> " +# copybutton_prompt_is_regexp = True +# copybutton_only_copy_prompt_lines = True diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..e7c861e --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,46 @@ +Introduction +------------ + +This package provides bindings to the generic input event interface in +Linux. The *evdev* interface serves the purpose of passing events +generated in the kernel directly to userspace through character +devices that are typically located in ``/dev/input/``. + +This package also comes with bindings to *uinput*, the userspace input +subsystem. *Uinput* allows userspace programs to create and handle +input devices that can inject events directly into the input +subsystem. + +In other words, *python-evdev* allows you to read and write input +events on Linux. An event can be a key or button press, a mouse +movement or a tap on a touchscreen. + + +.. toctree:: + :caption: Installation + :maxdepth: 2 + + install + +.. toctree:: + :caption: Usage + + usage + tutorial + apidoc + +.. toctree:: + :caption: Project + :maxdepth: 2 + + scope + changelog + + +License +------- + +This package is released under the terms of the `Revised BSD License`_. + +.. _`Revised BSD License`: https://raw.github.com/gvalkov/python-evdev/master/LICENSE +.. _evdev: http://svn.navi.cx/misc/trunk/python/evdev/ diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 0000000..f93e0b8 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,100 @@ +From an OS package +================== + +Python-evdev has been packaged for the following distributions: + +.. raw:: html + + + Packaging status + + +Consult the documentation of your OS package manager for installation instructions. + + +From source +=========== + +The latest stable version of *python-evdev* can be installed from pypi_, +provided that you have a compiler, pip_ and the Python and Linux development +headers installed on your system. Installing these is distribution specific and +typically falls into one of the following: + +On a Debian compatible OS: + +.. code-block:: bash + + $ apt install python-dev python-pip gcc + $ apt install linux-headers-$(uname -r) + +On a Redhat compatible OS: + +.. code-block:: bash + + $ dnf install python-devel python-pip gcc + $ dnf install kernel-headers-$(uname -r) + +On Arch Linux and derivatives: + +.. code-block:: bash + + $ pacman -S core/linux-api-headers python-pip gcc + +Once all OS dependencies are available, you may install *python-evdev* using +pip_, preferably in a [virtualenv]_: + +.. code-block:: bash + + # Install globally (not recommended). + $ sudo python3 -m pip install evdev + + # Install for the current user. + $ python3 -m pip install --user evdev + + # Install in a virtual environment. + $ python3 -m venv abc + $ source abc/bin/activate + $ python3 -m pip install evdev + + +Specifying header locations +--------------------------- + +By default, the setup script will look for the ``input.h`` and +``input-event-codes.h`` [#f1]_ header files ``/usr/include/linux``. + +You may use the ``--evdev-headers`` option to the ``build_ext`` setuptools +command to the location of these header files. It accepts one or more +colon-separated paths. For example: + +.. code-block:: bash + + $ python setup.py build_ext \ + --evdev-headers buildroot/input.h:buildroot/input-event-codes.h \ + --include-dirs buildroot/ \ + install # or any other command (e.g. develop, bdist, bdist_wheel) + + +From a binary package +===================== + +You may choose to install a precompiled version of *python-evdev* from pypi. The +`evdev-binary`_ package provides binary wheels that have been compiled on EL8 +against the 4.18.0 kernel headers. + +.. code-block:: bash + + $ python3 -m pip install evdev-binary + +While the evdev interface is stable, the precompiled version may not be fully +compatible or expose all the features of your running kernel. For best results, +it is recommended to use an OS package or to install from source. + + +.. [#f1] ``input-event-codes.h`` is found only in recent kernel versions. +.. _pypi: http://pypi.python.org/pypi/evdev +.. _evdev-binary: http://pypi.python.org/pypi/evdev-binary +.. _github: https://github.com/gvalkov/python-evdev +.. _pip: http://pip.readthedocs.org/en/latest/installing.html +.. _example: https://github.com/gvalkov/python-evdev/tree/master/examples +.. _virtualenv: https://docs.python.org/3/library/venv.html diff --git a/docs/scope.rst b/docs/scope.rst new file mode 100644 index 0000000..66a887d --- /dev/null +++ b/docs/scope.rst @@ -0,0 +1,31 @@ +Scope and status +---------------- + +Python-evdev exposes most of the more common interfaces defined in the evdev +subsystem. Reading and injecting events is well supported and has been tested +with nearly all event types. + +The basic functionality for reading and uploading force-feedback events is +there, but it has not been exercised sufficiently. A major shortcoming of the +uinput wrapper is that it does not support force-feedback devices at all (see +issue `#23`_). + +Some characters, such as ``:`` (colon), cannot be easily injected (see issue +`#7`_), Translating them into UInput events would require knowing the kernel +keyboard translation table, which is beyond the scope of python-evdev. Please +look into the following projects if you need more complete or convenient input +injection support. + +- python-uinput_ +- uinput-mapper_ +- pynput_ +- pygame_ (cross-platform) + + +.. _python-uinput: https://github.com/tuomasjjrasanen/python-uinput +.. _uinput-mapper: https://github.com/MerlijnWajer/uinput-mapper +.. _pynput: https://github.com/moses-palmer/pynput +.. _pygame: http://www.pygame.org/ + +.. _`#7`: https://github.com/gvalkov/python-evdev/issues/7 +.. _`#23`: https://github.com/gvalkov/python-evdev/pull/23 diff --git a/docs/tutorial.rst b/docs/tutorial.rst new file mode 100644 index 0000000..04ae42f --- /dev/null +++ b/docs/tutorial.rst @@ -0,0 +1,508 @@ +Tutorial +-------- + + +Listing accessible event devices +================================ + +:: + + >>> import evdev + + >>> devices = [evdev.InputDevice(path) for path in evdev.list_devices()] + >>> for device in devices: + >>> print(device.path, device.name, device.phys) + /dev/input/event1 Dell Dell USB Keyboard usb-0000:00:12.1-2/input0 + /dev/input/event0 Dell USB Optical Mouse usb-0000:00:12.0-2/input0 + + +Listing device capabilities +=========================== + +:: + + >>> import evdev + + >>> device = evdev.InputDevice('/dev/input/event0') + >>> print(device) + device /dev/input/event0, name "Dell USB Optical Mouse", phys "usb-0000:00:12.0-2/input0" + + >>> device.capabilities() + ... { 0: [0, 1, 2], 1: [272, 273, 274, 275], 2: [0, 1, 6, 8], 4: [4] } + + >>> device.capabilities(verbose=True) + ... { ('EV_SYN', 0): [('SYN_REPORT', 0), ('SYN_CONFIG', 1), ('SYN_MT_REPORT', 2)], + ... ('EV_KEY', 1): [('BTN_MOUSE', 272), ('BTN_RIGHT', 273), ('BTN_MIDDLE', 274), ('BTN_SIDE', 275)], ... + + +Listing device capabilities (devices with absolute axes) +======================================================== + +:: + + >>> import evdev + + >>> device = evdev.InputDevice('/dev/input/event7') + >>> print(device) + device /dev/input/event7, name "Wacom Bamboo 2FG 4x5 Finger", phys "" + + >>> device.capabilities() + ... { 1: [272, 273, 277, 278, 325, 330, 333] , + ... 3: [(0, AbsInfo(min=0, max=15360, fuzz=128, flat=0)), + ... (1, AbsInfo(min=0, max=10240, fuzz=128, flat=0))] } + + >>> device.capabilities(verbose=True) + ... { ('EV_KEY', 1): [('BTN_MOUSE', 272), ('BTN_RIGHT', 273), ...], + ... ('EV_ABS', 3): [(('ABS_X', 0), AbsInfo(min=0, max=15360, fuzz=128, flat=0)), + ... (('ABS_Y', 1), AbsInfo(min=0, max=10240, fuzz=128, flat=0)),] } + + >>> device.capabilities(absinfo=False) + ... { 1: [272, 273, 277, 278, 325, 330, 333], + ... 3: [0, 1, 47, 53, 54, 57] } + + +Getting and setting LED states +============================== + +:: + + >>> dev.leds(verbose=True) + ... [('LED_NUML', 0), ('LED_CAPSL', 1)] + + >>> dev.leds() + ... [0, 1] + + >>> dev.set_led(ecodes.LED_NUML, 1) # enable numlock + >>> dev.set_led(ecodes.LED_NUML, 0) # disable numlock + + +Getting currently active keys +============================= + +:: + + >>> dev.active_keys(verbose=True) + ... [('KEY_3', 4), ('KEY_LEFTSHIFT', 42)] + + >>> dev.active_keys() + ... [4, 42] + + +Reading events +============== + +Reading events from a single device in an endless loop. + +:: + + >>> from evdev import InputDevice, categorize, ecodes + >>> dev = InputDevice('/dev/input/event1') + + >>> print(dev) + device /dev/input/event1, name "Dell Dell USB Keyboard", phys "usb-0000:00:12.1-2/input0" + + >>> for event in dev.read_loop(): + ... if event.type == ecodes.EV_KEY: + ... print(categorize(event)) + ... # pressing 'a' and holding 'space' + key event at 1337016188.396030, 30 (KEY_A), down + key event at 1337016188.492033, 30 (KEY_A), up + key event at 1337016189.772129, 57 (KEY_SPACE), down + key event at 1337016190.275396, 57 (KEY_SPACE), hold + key event at 1337016190.284160, 57 (KEY_SPACE), up + + +Reading events (using :mod:`asyncio`) +====================================== + +.. note:: + + This requires Python 3.5+ for the async/await keywords. + + +:: + + >>> import asyncio + >>> from evdev import InputDevice + + >>> dev = InputDevice('/dev/input/event1') + + >>> async def main(dev): + ... async for ev in dev.async_read_loop(): + ... print(repr(ev)) + + >>> asyncio.run(main(dev)) + InputEvent(1527363738, 348740, 4, 4, 458792) + InputEvent(1527363738, 348740, 1, 28, 0) + InputEvent(1527363738, 348740, 0, 0, 0) + + +Reading events from multiple devices (using :mod:`select`) +========================================================== + +:: + + >>> from evdev import InputDevice + >>> from select import select + + # A mapping of file descriptors (integers) to InputDevice instances. + >>> devices = map(InputDevice, ('/dev/input/event1', '/dev/input/event2')) + >>> devices = {dev.fd: dev for dev in devices} + + >>> for dev in devices.values(): print(dev) + device /dev/input/event1, name "Dell Dell USB Keyboard", phys "usb-0000:00:12.1-2/input0" + device /dev/input/event2, name "Logitech USB Laser Mouse", phys "usb-0000:00:12.0-2/input0" + + >>> while True: + ... r, w, x = select(devices, [], []) + ... for fd in r: + ... for event in devices[fd].read(): + ... print(event) + event at 1351116708.002230, code 01, type 02, val 01 + event at 1351116708.002234, code 00, type 00, val 00 + event at 1351116708.782231, code 04, type 04, val 458782 + event at 1351116708.782237, code 02, type 01, val 01 + + +Reading events from multiple devices (using :mod:`selectors`) +============================================================= + +This can also be achieved using the :mod:`selectors` module in Python 3.4: + +:: + + from evdev import InputDevice + from selectors import DefaultSelector, EVENT_READ + + selector = DefaultSelector() + + mouse = InputDevice('/dev/input/event1') + keybd = InputDevice('/dev/input/event2') + + # This works because InputDevice has a `fileno()` method. + selector.register(mouse, EVENT_READ) + selector.register(keybd, EVENT_READ) + + while True: + for key, mask in selector.select(): + device = key.fileobj + for event in device.read(): + print(event) + + +Reading events from multiple devices (using :mod:`asyncio`) +=========================================================== + +Yet another possibility is the :mod:`asyncio` module from Python 3.4: + +:: + + import asyncio, evdev + + @asyncio.coroutine + def print_events(device): + while True: + events = yield from device.async_read() + for event in events: + print(device.path, evdev.categorize(event), sep=': ') + + mouse = evdev.InputDevice('/dev/input/eventX') + keybd = evdev.InputDevice('/dev/input/eventY') + + for device in mouse, keybd: + asyncio.async(print_events(device)) + + loop = asyncio.get_event_loop() + loop.run_forever() + +Since Python 3.5, the `async/await`_ syntax makes this even simpler: + +:: + + import asyncio, evdev + + mouse = evdev.InputDevice('/dev/input/event4') + keybd = evdev.InputDevice('/dev/input/event5') + + async def print_events(device): + async for event in device.async_read_loop(): + print(device.path, evdev.categorize(event), sep=': ') + + for device in mouse, keybd: + asyncio.ensure_future(print_events(device)) + + loop = asyncio.get_event_loop() + loop.run_forever() + + +Accessing evdev constants +========================= + +:: + + >>> from evdev import ecodes + + >>> ecodes.KEY_A, ecodes.ecodes['KEY_A'] + ... (30, 30) + >>> ecodes.KEY[30] + ... 'KEY_A' + >>> ecodes.bytype[ecodes.EV_KEY][30] + ... 'KEY_A' + >>> ecodes.KEY[152] # a single value may correspond to multiple codes + ... ['KEY_COFFEE', 'KEY_SCREENLOCK'] + + +Searching event codes by regex +============================== + +:: + + >>> from evdev import util + + >>> res = util.find_ecodes_by_regex(r'(ABS|KEY)_BR(AKE|EAK)') + >>> res + ... {1: [411], 3: [10]} + >>> util.resolve_ecodes_dict(res) + ... {('EV_KEY', 1): [('KEY_BREAK', 411)], ('EV_ABS', 3): [('ABS_BRAKE', 10)]} + + +Getting exclusive access to a device +==================================== + +:: + + >>> dev.grab() # become the sole recipient of all incoming input events + >>> dev.ungrab() + +This functionality is also available as a context manager. + +:: + + >>> with dev.grab_context(): + ... pass + + +Associating classes with event types +==================================== + +:: + + >>> from evdev import categorize, event_factory, ecodes + + >>> class SynEvent: + ... def __init__(self, event): + ... ... + + >>> event_factory[ecodes.EV_SYN] = SynEvent + +See :mod:`events ` for more information. + +Injecting input +=============== + +:: + + >>> from evdev import UInput, ecodes as e + + >>> ui = UInput() + + >>> # accepts only KEY_* events by default + >>> ui.write(e.EV_KEY, e.KEY_A, 1) # KEY_A down + >>> ui.write(e.EV_KEY, e.KEY_A, 0) # KEY_A up + >>> ui.syn() + + >>> ui.close() + + +Injecting events (using a context manager) +========================================== + +:: + + >>> ev = InputEvent(1334414993, 274296, ecodes.EV_KEY, ecodes.KEY_A, 1) + >>> with UInput() as ui: + ... ui.write_event(ev) + ... ui.syn() + + +Specifying ``uinput`` device options +==================================== + +.. note:: + + ``ecodes.EV_SYN`` cannot be in the ``cap`` dictionary or the device will not be created. + +:: + + >>> from evdev import UInput, AbsInfo, ecodes as e + + >>> cap = { + ... e.EV_KEY : [e.KEY_A, e.KEY_B], + ... e.EV_ABS : [ + ... (e.ABS_X, AbsInfo(value=0, min=0, max=255, + ... fuzz=0, flat=0, resolution=0)), + ... (e.ABS_Y, AbsInfo(0, 0, 255, 0, 0, 0)), + ... (e.ABS_MT_POSITION_X, (0, 128, 255, 0)) ] + ... } + + >>> ui = UInput(cap, name='example-device', version=0x3) + >>> print(ui) + name "example-device", bus "BUS_USB", vendor "0001", product "0001", version "0003" + event types: EV_KEY EV_ABS EV_SYN + + >>> print(ui.capabilities()) + {0: [0, 1, 3], + 1: [30, 48], + 3: [(0, AbsInfo(value=0, min=0, max=0, fuzz=255, flat=0, resolution=0)), + (1, AbsInfo(value=0, min=0, max=0, fuzz=255, flat=0, resolution=0)), + (53, AbsInfo(value=0, min=0, max=255, fuzz=128, flat=0, resolution=0))]} + + >>> # move mouse cursor + >>> ui.write(e.EV_ABS, e.ABS_X, 20) + >>> ui.write(e.EV_ABS, e.ABS_Y, 20) + >>> ui.syn() + + +Create ``uinput`` device with capabilities of another device +================================================================ + +:: + + >>> from evdev import UInput, InputDevice + + >>> mouse = InputDevice('/dev/input/event1') + >>> keybd = '/dev/input/event2' + + >>> ui = UInput.from_device(mouse, keybd, name='keyboard-mouse-device') + >>> ui.capabilities(verbose=True).keys() + dict_keys([('EV_LED', 17), ('EV_KEY', 1), ('EV_SYN', 0), ('EV_REL', 2), ('EV_MSC', 4)]) + + +.. _`async/await`: https://docs.python.org/3/library/asyncio-task.html + +Create ``uinput`` device capable of receiving FF-effects +======================================================== + +:: + + import asyncio + from evdev import UInput, categorize, ecodes + + cap = { + ecodes.EV_FF: [ecodes.FF_RUMBLE ], + ecodes.EV_KEY: [ecodes.KEY_A, ecodes.KEY_B] + } + + ui = UInput(cap, name='test-controller', version=0x3) + + async def print_events(device): + async for event in device.async_read_loop(): + print(categorize(event)) + + # Wait for an EV_UINPUT event that will signal us that an + # effect upload/erase operation is in progress. + if event.type != ecodes.EV_UINPUT: + continue + + if event.code == ecodes.UI_FF_UPLOAD: + upload = device.begin_upload(event.value) + upload.retval = 0 + + print(f'[upload] effect_id: {upload.effect.id}, type: {upload.effect.type}') + device.end_upload(upload) + + elif event.code == ecodes.UI_FF_ERASE: + erase = device.begin_erase(event.value) + print(f'[erase] effect_id {erase.effect_id}') + + erase.retval = 0 + device.end_erase(erase) + + asyncio.ensure_future(print_events(ui)) + loop = asyncio.get_event_loop() + loop.run_forever() + + +Injecting an FF-event into first FF-capable device found +======================================================== + +:: + + from evdev import ecodes, InputDevice, ff, list_devices + import time + + # Find first EV_FF capable event device (that we have permissions to use). + for name in list_devices(): + dev = InputDevice(name) + if ecodes.EV_FF in dev.capabilities(): + break + + rumble = ff.Rumble(strong_magnitude=0x0000, weak_magnitude=0xffff) + effect_type = ff.EffectType(ff_rumble_effect=rumble) + duration_ms = 1000 + + effect = ff.Effect( + ecodes.FF_RUMBLE, -1, 0, + ff.Trigger(0, 0), + ff.Replay(duration_ms, 0), + effect_type + ) + + repeat_count = 1 + effect_id = dev.upload_effect(effect) + dev.write(ecodes.EV_FF, effect_id, repeat_count) + time.sleep(duration_ms / 1000) + dev.erase_effect(effect_id) + + +Forwarding force-feedback from uinput to a real device +====================================================== + +:: + + import evdev + from evdev import ecodes as e + + # Find first EV_FF capable event device (that we have permissions to use). + for name in evdev.list_devices(): + dev = evdev.InputDevice(name) + if e.EV_FF in dev.capabilities(): + break + # To ensure forwarding works correctly it is important that `max_effects` + # of the uinput device is <= `dev.ff_effects_count`. + # `from_device()` will do this automatically, but in some situations you may + # want to set the `max_effects` parameter manually, such as when using `Uinput()`. + # `filtered_types` is specified as by default EV_FF events are filtered + uinput = evdev.UInput.from_device(dev, filtered_types=[e.EV_SYN]) + + # Keeps track of which effects have been uploaded to the device + effects = set() + + for event in uinput.read_loop(): + + # Handle the special uinput events + if event.type == e.EV_UINPUT: + + if event.code == e.UI_FF_UPLOAD: + upload = uinput.begin_upload(event.value) + + # Checks if this is a new effect + if upload.effect.id not in effects: + effects.add(upload.effect.id) + # Setting id to 1 indicates that a new effect must be allocated + upload.effect.id = -1 + + dev.upload_effect(upload.effect) + upload.retval = 0 + uinput.end_upload(upload) + + elif event.code == e.UI_FF_ERASE: + erase = uinput.begin_erase(event.value) + erase.retval = 0 + dev.erase_effect(erase.effect_id) + effects.remove(erase.effect_id) + uinput.end_erase(erase) + + # Forward writes to actual rumble device. + elif event.type == e.EV_FF: + dev.write(event.type, event.code, event.value) diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..f3d4d39 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,78 @@ +Quick Start +----------- + + +Listing accessible event devices +================================ + +:: + + >>> import evdev + + >>> devices = [evdev.InputDevice(path) for path in evdev.list_devices()] + >>> for device in devices: + ... print(device.path, device.name, device.phys) + /dev/input/event1 USB Keyboard usb-0000:00:12.1-2/input0 + /dev/input/event0 USB Optical Mouse usb-0000:00:12.0-2/input0 + +.. note:: + + If you do not see any devices, ensure that your user is in the + correct group (typically ``input``) to have read/write access. + + +Reading events from a device +============================ + +:: + + >>> import evdev + + >>> device = evdev.InputDevice('/dev/input/event1') + >>> print(device) + device /dev/input/event1, name "USB Keyboard", phys "usb-0000:00:12.1-2/input0" + + >>> for event in device.read_loop(): + ... if event.type == evdev.ecodes.EV_KEY: + ... print(evdev.categorize(event)) + ... # pressing 'a' and holding 'space' + key event at 1337016188.396030, 30 (KEY_A), down + key event at 1337016188.492033, 30 (KEY_A), up + key event at 1337016189.772129, 57 (KEY_SPACE), down + key event at 1337016190.275396, 57 (KEY_SPACE), hold + key event at 1337016190.284160, 57 (KEY_SPACE), up + + +Accessing event codes +===================== + +The ``evdev.ecodes`` module provides reverse and forward mappings between the +names and values of the event subsystem constants. + +:: + + >>> from evdev import ecodes + + >>> ecodes.KEY_A + ... 30 + >>> ecodes.ecodes['KEY_A'] + ... 30 + >>> ecodes.KEY[30] + ... 'KEY_A' + >>> ecodes.bytype[ecodes.EV_KEY][30] + ... 'KEY_A' + + # A single value may correspond to multiple event codes. + >>> ecodes.KEY[152] + ... ['KEY_COFFEE', 'KEY_SCREENLOCK'] + + +Listing and monitoring input devices +==================================== + +The *python-evdev* package also comes with a small command-line program for +listing and monitoring input devices: + +.. code-block:: bash + + $ python -m evdev.evtest diff --git a/evdev/__init__.py b/evdev/__init__.py deleted file mode 100644 index bb9a241..0000000 --- a/evdev/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# encoding: utf-8 - -# Gather everything into a convenient namespace - -from evdev.device import DeviceInfo, InputDevice, AbsInfo -from evdev.events import InputEvent, KeyEvent, RelEvent, SynEvent, AbsEvent, event_factory -from evdev.uinput import UInput, UInputError -from evdev.util import list_devices, categorize, resolve_ecodes -from evdev import ecodes -from evdev import ff diff --git a/evdev/device.py b/evdev/device.py deleted file mode 100644 index a83b4ee..0000000 --- a/evdev/device.py +++ /dev/null @@ -1,312 +0,0 @@ -# encoding: utf-8 - -import os -from select import select -from collections import namedtuple - -from evdev import _input, _uinput, ecodes, util -from evdev.events import InputEvent - - -_AbsInfo = namedtuple('AbsInfo', ['value', 'min', 'max', 'fuzz', 'flat', 'resolution']) -_KbdInfo = namedtuple('KbdInfo', ['repeat', 'delay']) -_DeviceInfo = namedtuple('DeviceInfo', ['bustype', 'vendor', 'product', 'version']) - - -class AbsInfo(_AbsInfo): - ''' - A ``namedtuple`` for storing absolut axis information - - corresponds to the ``input_absinfo`` struct: - - - value - Latest reported value for the axis. - - - min - Specifies minimum value for the axis. - - - max - Specifies maximum value for the axis. - - - fuzz - Specifies fuzz value that is used to filter noise from the - event stream. - - - flat - Values that are within this value will be discarded by joydev - interface and reported as 0 instead. - - - resolution - Specifies resolution for the values reported for the axis. - Resolution for main axes (``ABS_X, ABS_Y, ABS_Z``) is reported - in units per millimeter (units/mm), resolution for rotational - axes (``ABS_RX, ABS_RY, ABS_RZ``) is reported in units per - radian. - - .. note: The input core does not clamp reported values to the - ``[minimum, maximum]`` limits, such task is left to userspace. - ''' - - def __str__(self): - return 'val {}, min {}, max {}, fuzz {}, flat {}, res {}'.format(*self) - - -class KbdInfo(_KbdInfo): - ''' - Keyboard repeat rate: - - - repeat: - Keyboard repeat rate in characters per second. - - - delay: - Amount of time that a key must be depressed before it will start - to repeat (in milliseconds). - ''' - - def __str__(self): - return 'repeat {}, delay {}'.format(*self) - - -class DeviceInfo(_DeviceInfo): - def __str__(self): - msg = 'bus: {:04x}, vendor {:04x}, product {:04x}, version {:04x}' - return msg.format(*self) - - -class InputDevice(object): - ''' - A linux input device from which input events can be read. - ''' - - __slots__ = ('fn', 'fd', 'info', 'name', 'phys', '_rawcapabilities', - 'version', 'ff_effects_count') - - def __init__(self, dev): - ''' - :param dev: path to input device - ''' - - #: Path to input device. - self.fn = dev - - #: A non-blocking file descriptor to the device file. - self.fd = os.open(dev, os.O_RDWR | os.O_NONBLOCK) - - # Returns (bustype, vendor, product, version, name, phys, capabilities). - info_res = _input.ioctl_devinfo(self.fd) - - #: A :class:`DeviceInfo ` instance. - self.info = DeviceInfo(*info_res[:4]) - - #: The name of the event device. - self.name = info_res[4] - - #: The physical topology of the device. - self.phys = info_res[5] - - #: The evdev protocol version. - self.version = _input.ioctl_EVIOCGVERSION(self.fd) - - #: The raw dictionary of device capabilities - see `:func:capabilities()`. - self._rawcapabilities = _input.ioctl_capabilities(self.fd) - - #: The number of force feedback effects the device can keep in its memory. - self.ff_effects_count = _input.ioctl_EVIOCGEFFECTS(self.fd) - - def __del__(self): - if hasattr(self, 'fd') and self.fd is not None: - try: - self.close() - except OSError: - pass - - def _capabilities(self, absinfo=True): - res = {} - for etype, ecodes in self._rawcapabilities.items(): - for code in ecodes: - l = res.setdefault(etype, []) - if isinstance(code, tuple): - if absinfo: - a = code[1] # (0, 0, 0, 255, 0, 0) - i = AbsInfo(*a) - l.append((code[0], i)) - else: - l.append(code[0]) - else: - l.append(code) - - return res - - def capabilities(self, verbose=False, absinfo=True): - ''' - Return the event types that this device supports as a mapping of - supported event types to lists of handled event codes. Example:: - - { 1: [272, 273, 274], - 2: [0, 1, 6, 8] } - - If ``verbose`` is ``True``, event codes and types will be resolved - to their names. Example:: - - { ('EV_KEY', 1): [('BTN_MOUSE', 272), - ('BTN_RIGHT', 273), - ('BTN_MIDDLE', 273)], - ('EV_REL', 2): [('REL_X', 0), - ('REL_Y', 1), - ('REL_HWHEEL', 6), - ('REL_WHEEL', 8)] } - - Unknown codes or types will be resolved to ``'?'``. - - If ``absinfo`` is ``True``, the list of capabilities will also - include absolute axis information in the form of - :class:`AbsInfo` instances:: - - { 3: [ (0, AbsInfo(min=0, max=255, fuzz=0, flat=0)), - (1, AbsInfo(min=0, max=255, fuzz=0, flat=0)) ]} - - Combined with ``verbose`` the above becomes:: - - { ('EV_ABS', 3): [ (('ABS_X', 0), AbsInfo(min=0, max=255, fuzz=0, flat=0)), - (('ABS_Y', 1), AbsInfo(min=0, max=255, fuzz=0, flat=0)) ]} - - ''' - - if verbose: - return dict(util.resolve_ecodes(self._capabilities(absinfo))) - else: - return self._capabilities(absinfo) - - def leds(self, verbose=False): - ''' - Return currently set LED keys. Example:: - - [0, 1, 8, 9] - - If ``verbose`` is ``True``, event codes are resolved to - their names. Unknown codes are resolved to ``'?'``. Example:: - - [('LED_NUML', 0), ('LED_CAPSL', 1), ('LED_MISC', 8), ('LED_MAIL', 9)] - - ''' - leds = _input.get_sw_led_snd(self.fd, ecodes.EV_LED) - if verbose: - return [(ecodes.LED[l] if l in ecodes.LED else '?', l) for l in leds] - - return leds - - def set_led(self, led_num, value): - ''' - Set the state of the selected LED. Example:: - - device.set_led(ecodes.LED_NUML, 1) - - .. - ''' - _uinput.write(self.fd, ecodes.EV_LED, led_num, value) - - def __eq__(self, other): - '''Two devices are considered equal if their :data:`info` attributes are equal.''' - return self.info == other.info - - def __str__(self): - msg = 'device {}, name "{}", phys "{}"' - return msg.format(self.fn, self.name, self.phys) - - def __repr__(self): - msg = (self.__class__.__name__, self.fn) - return '{}({!r})'.format(*msg) - - def close(self): - if self.fd > -1: - try: - os.close(self.fd) - finally: - self.fd = -1 - - def fileno(self): - ''' - Return the file descriptor to the open event device. This - makes it possible to pass pass ``InputDevice`` instances - directly to :func:`select.select()` and - :class:`asyncore.file_dispatcher`.''' - - return self.fd - - def read_one(self): - ''' - Read and return a single input event as an instance of - :class:`InputEvent `. - - Return ``None`` if there are no pending input events. - ''' - - # event -> (sec, usec, type, code, val) - event = _input.device_read(self.fd) - - if event: - return InputEvent(*event) - - def read_loop(self): - '''Enter an endless ``select()`` loop that yields input events.''' - - while True: - r, w, x = select([self.fd], [], []) - for event in self.read(): - yield event - - def read(self): - ''' - Read multiple input events from device. Return a generator - object that yields :class:`InputEvent - ` instances. - ''' - - # events -> [(sec, usec, type, code, val), ...] - events = _input.device_read_many(self.fd) - - for i in events: - yield InputEvent(*i) - - def grab(self): - ''' - Grab input device using ``EVIOCGRAB`` - other applications will - be unable to receive events until the device is released. Only - one process can hold a ``EVIOCGRAB`` on a device. - - .. warning:: Grabbing an already grabbed device will raise an - ``IOError``.''' - - _input.ioctl_EVIOCGRAB(self.fd, 1) - - def ungrab(self): - '''Release device if it has been already grabbed (uses - `EVIOCGRAB`). - - .. warning:: Releasing an already released device will raise an - ``IOError('Invalid argument')``.''' - - _input.ioctl_EVIOCGRAB(self.fd, 0) - - def upload_effect(self, effect): - '''Upload a force feedback effect to a force feedback device.''' - - data = bytes(buffer(effect)[:]) - ff_id = _input.upload_effect(self.fd, data) - return ff_id - - def erase_effect(self, ff_id): - '''Erase a force effect from a force feedback device. This - also stops the effect.''' - - _input.erase_effect(self.fd, ff_id) - - @property - def repeat(self): - '''Get or set the keyboard repeat rate (in characters per - minute) and delay (in milliseconds).''' - - return KbdInfo(*_input.ioctl_EVIOCGREP(self.fd)) - - @repeat.setter - def repeat(self, value): - return _input.ioctl_EVIOCSREP(self.fd, *value) diff --git a/evdev/ecodes.sh b/evdev/ecodes.sh deleted file mode 100755 index 8408e3f..0000000 --- a/evdev/ecodes.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash - -# Generate a Python extension module that exports macros from -# /usr/include/linux/input.h - -# This script is obsolete. Please use `python -m evdev.genecodes`. - -header=${1:-/usr/include/linux/input.h} -[[ ! -e $header ]] && echo "no such file: $header" && exit 1 - - -function codes () { - awk ' - /#define +(KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF)_/ { - print " PyModule_AddIntMacro(m, "$2");" - }' ${header} -} - -cat << EOF -#include -#include - -/* Automatically generated by evdev/ecodes.sh */ - -#define MODULE_NAME "_ecodes" -#define MODULE_HELP "linux/input.h macros" - -static PyMethodDef MethodTable[] = { - { NULL, NULL, 0, NULL} -}; - -#if PY_MAJOR_VERSION >= 3 -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - MODULE_NAME, - MODULE_HELP, - -1, /* m_size */ - MethodTable, /* m_methods */ - NULL, /* m_reload */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ -}; -#endif - -static PyObject * -moduleinit(void) -{ - -#if PY_MAJOR_VERSION >= 3 - PyObject* m = PyModule_Create(&moduledef); -#else - PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP); -#endif - - if (m == NULL) return NULL; - -$(codes) - - return m; -} - -#if PY_MAJOR_VERSION >= 3 -PyMODINIT_FUNC -PyInit__ecodes(void) -{ - return moduleinit(); -} -#else -PyMODINIT_FUNC -init_ecodes(void) -{ - moduleinit(); -} -#endif -EOF diff --git a/evdev/events.py b/evdev/events.py deleted file mode 100644 index 8383d01..0000000 --- a/evdev/events.py +++ /dev/null @@ -1,184 +0,0 @@ -# encoding: utf-8 - -''' -This module provides the :class:`InputEvent` class, which closely -resembles the ``input_event`` struct defined in ``linux/input.h``: - -.. code-block:: c - - struct input_event { - struct timeval time; - __u16 type; - __u16 code; - __s32 value; - }; - -This module also defines :class:`InputEvent` sub-classes that know -more about the different types of events (key, abs, rel etc). The -:data:`event_factory` dictionary maps event types to these classes. - -Assuming you use the :func:`evdev.util.categorize()` function to -categorize events according to their type, adding or replacing a class -for a specific event type becomes a matter of modifying -:data:`event_factory`. - -All classes in this module have reasonable ``str()`` and ``repr()`` -methods:: - - >>> print(event) - event at 1337197425.477827, code 04, type 04, val 458792 - >>> print(repr(event)) - InputEvent(1337197425L, 477827L, 4, 4, 458792L) - - >>> print(key_event) - key event at 1337197425.477835, 28 (KEY_ENTER), up - >>> print(repr(key_event)) - KeyEvent(InputEvent(1337197425L, 477835L, 1, 28, 0L)) -''' - -# event type descriptions have been taken mot-a-mot from: -# http://www.kernel.org/doc/Documentation/input/event-codes.txt - -from evdev.ecodes import keys, KEY, SYN, REL, ABS, EV_KEY, EV_REL, EV_ABS, EV_SYN - - -class InputEvent(object): - '''A generic input event.''' - - __slots__ = 'sec', 'usec', 'type', 'code', 'value' - - def __init__(self, sec, usec, type, code, value): - #: Time in seconds since epoch at which event occurred. - self.sec = sec - - #: Microsecond portion of the timestamp. - self.usec = usec - - #: Event type - one of ``ecodes.EV_*``. - self.type = type - - #: Event code related to the event type. - self.code = code - - #: Event value related to the event type. - self.value = value - - def timestamp(self): - '''Return event timestamp as a float.''' - return self.sec + (self.usec / 1000000.0) - - def __str__(s): - msg = 'event at {:f}, code {:02d}, type {:02d}, val {:02d}' - return msg.format(s.timestamp(), s.code, s.type, s.value) - - def __repr__(s): - msg = '{}({!r}, {!r}, {!r}, {!r}, {!r})' - return msg.format(s.__class__.__name__, - s.sec, s.usec, s.type, s.code, s.value) - - -class KeyEvent(object): - '''An event generated by a keyboard, button or other key-like devices.''' - - key_up = 0x0 - key_down = 0x1 - key_hold = 0x2 - - __slots__ = 'scancode', 'keycode', 'keystate', 'event' - - def __init__(self, event): - if event.value == 0: - self.keystate = KeyEvent.key_up - elif event.value == 2: - self.keystate = KeyEvent.key_hold - elif event.value == 1: - self.keystate = KeyEvent.key_down - - self.keycode = keys[event.code] # :todo: - self.scancode = event.code - - #: Reference to an :class:`InputEvent` instance. - self.event = event - - def __str__(self): - try: - ks = ('up', 'down', 'hold')[self.keystate] - except IndexError: - ks = 'unknown' - - msg = 'key event at {:f}, {} ({}), {}' - return msg.format(self.event.timestamp(), - self.scancode, self.keycode, ks) - - def __repr__(s): - return '{}({!r})'.format(s.__class__.__name__, s.event) - - -class RelEvent(object): - '''A relative axis event (e.g moving the mouse 5 units to the left).''' - - __slots__ = 'event' - - def __init__(self, event): - #: Reference to an :class:`InputEvent` instance. - self.event = event - - def __str__(self): - msg = 'relative axis event at {:f}, {} ' - return msg.format(self.event.timestamp(), REL[self.event.code]) - - def __repr__(s): - return '{}({!r})'.format(s.__class__.__name__, s.event) - - -class AbsEvent(object): - '''An absolute axis event (e.g the coordinates of a tap on a touchscreen).''' - - __slots__ = 'event' - - def __init__(self, event): - #: Reference to an :class:`InputEvent` instance. - self.event = event - - def __str__(self): - msg = 'absolute axis event at {:f}, {} ' - return msg.format(self.event.timestamp(), ABS[self.event.code]) - - def __repr__(s): - return '{}({!r})'.format(s.__class__.__name__, s.event) - - -class SynEvent(object): - ''' - A synchronization event. Synchronization events are used as - markers to separate event. Used as markers to separate - events. Events may be separated in time or in space, such as with - the multitouch protocol. - ''' - - __slots__ = 'event' - - def __init__(self, event): - #: Reference to an :class:`InputEvent` instance. - self.event = event - - def __str__(self): - msg = 'synchronization event at {:f}, {} ' - return msg.format(self.event.timestamp(), SYN[self.event.code]) - - def __repr__(s): - return '{}({!r})'.format(s.__class__.__name__, s.event) - - -#: A mapping of event types to :class:`InputEvent` sub-classes. Used -#: by:func:`evdev.util.categorize()` -event_factory = { - EV_KEY: KeyEvent, - EV_REL: RelEvent, - EV_ABS: AbsEvent, - EV_SYN: SynEvent, -} - - -__all__ = ('InputEvent', 'KeyEvent', 'RelEvent', 'SynEvent', - 'AbsEvent', 'event_factory') diff --git a/evdev/genecodes.py b/evdev/genecodes.py deleted file mode 100755 index 8572920..0000000 --- a/evdev/genecodes.py +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8; -*- - -''' -Generate a Python extension module that exports macros from -/usr/include/linux/input.h -''' - -import os, sys, re - - -template = r''' -#include -#include - -/* Automatically generated by evdev.genecodes */ -/* Generated on %s */ - -#define MODULE_NAME "_ecodes" -#define MODULE_HELP "linux/input.h macros" - -static PyMethodDef MethodTable[] = { - { NULL, NULL, 0, NULL} -}; - -#if PY_MAJOR_VERSION >= 3 -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - MODULE_NAME, - MODULE_HELP, - -1, /* m_size */ - MethodTable, /* m_methods */ - NULL, /* m_reload */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ -}; -#endif - -static PyObject * -moduleinit(void) -{ - -#if PY_MAJOR_VERSION >= 3 - PyObject* m = PyModule_Create(&moduledef); -#else - PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP); -#endif - - if (m == NULL) return NULL; - -%s - - return m; -} - -#if PY_MAJOR_VERSION >= 3 -PyMODINIT_FUNC -PyInit__ecodes(void) -{ - return moduleinit(); -} -#else -PyMODINIT_FUNC -init_ecodes(void) -{ - moduleinit(); -} -#endif -''' - -header = '/usr/include/linux/input.h' if len(sys.argv) == 1 else sys.argv[1] -regex = r'#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF)_\w+)' -regex = re.compile(regex) - -if not os.path.exists(header): - print('no such file: %s' % header) - sys.exit(1) - -def getmacros(): - for line in open(header): - macro = regex.search(line) - if macro: - yield ' PyModule_AddIntMacro(m, %s);' % macro.group(1) - -uname = list(os.uname()); del uname[1] -uname = ' '.join(uname) - -macros = os.linesep.join(getmacros()) -print(template % (uname, macros)) diff --git a/evdev/uinput.c b/evdev/uinput.c deleted file mode 100644 index c885ed7..0000000 --- a/evdev/uinput.c +++ /dev/null @@ -1,251 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - - -int _uinput_close(int fd) -{ - if (ioctl(fd, UI_DEV_DESTROY) < 0) { - int oerrno = errno; - close(fd); - errno = oerrno; - return -1; - } - - return close(fd); -} - - -static PyObject * -uinput_open(PyObject *self, PyObject *args) -{ - const char* devnode; - - int ret = PyArg_ParseTuple(args, "s", &devnode); - if (!ret) return NULL; - - int fd = open(devnode, O_WRONLY | O_NONBLOCK); - if (fd < 0) { - PyErr_SetString(PyExc_IOError, "could not open uinput device in write mode"); - return NULL; - } - - return Py_BuildValue("i", fd); -} - - -static PyObject * -uinput_create(PyObject *self, PyObject *args) { - int fd, len, i, abscode; - __u16 vendor, product, version, bustype; - - PyObject *absinfo = NULL, *item = NULL; - - struct uinput_user_dev uidev; - const char* name; - - int ret = PyArg_ParseTuple(args, "ishhhhO", &fd, &name, &vendor, - &product, &version, &bustype, &absinfo); - if (!ret) return NULL; - - memset(&uidev, 0, sizeof(uidev)); - strncpy(uidev.name, name, UINPUT_MAX_NAME_SIZE); - uidev.id.vendor = vendor; - uidev.id.product = product; - uidev.id.version = version; - uidev.id.bustype = bustype; - - len = PyList_Size(absinfo); - for (i=0; i (ABS_X, 0, 255, 0, 0) - item = PyList_GetItem(absinfo, i); - abscode = (int)PyLong_AsLong(PyList_GetItem(item, 0)); - - uidev.absmin[abscode] = PyLong_AsLong(PyList_GetItem(item, 1)); - uidev.absmax[abscode] = PyLong_AsLong(PyList_GetItem(item, 2)); - uidev.absfuzz[abscode] = PyLong_AsLong(PyList_GetItem(item, 3)); - uidev.absflat[abscode] = PyLong_AsLong(PyList_GetItem(item, 4)); - } - - if (write(fd, &uidev, sizeof(uidev)) != sizeof(uidev)) - goto on_err; - - /* if (ioctl(fd, UI_SET_EVBIT, EV_KEY) < 0) */ - /* goto on_err; */ - /* int i; */ - /* for (i=0; i= 3 -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - MODULE_NAME, - MODULE_HELP, - -1, /* m_size */ - MethodTable, /* m_methods */ - NULL, /* m_reload */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ -}; - -static PyObject * -moduleinit(void) -{ - PyObject* m = PyModule_Create(&moduledef); - if (m == NULL) return NULL; - - PyModule_AddIntConstant(m, "maxnamelen", UINPUT_MAX_NAME_SIZE); - return m; -} - -PyMODINIT_FUNC -PyInit__uinput(void) -{ - return moduleinit(); -} - -#else -static PyObject * -moduleinit(void) -{ - PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP); - if (m == NULL) return NULL; - - PyModule_AddIntConstant(m, "maxnamelen", UINPUT_MAX_NAME_SIZE); - return m; -} - -PyMODINIT_FUNC -init_uinput(void) -{ - moduleinit(); -} -#endif diff --git a/evdev/uinput.py b/evdev/uinput.py deleted file mode 100644 index 30c3a06..0000000 --- a/evdev/uinput.py +++ /dev/null @@ -1,208 +0,0 @@ -# encoding: utf-8 - -import os -import stat -import time - -from evdev import _uinput -from evdev import ecodes, util, device - - -class UInputError(Exception): - pass - - -class UInput(object): - ''' - A userland input device and that can inject input events into the - linux input subsystem. - ''' - - __slots__ = ( - 'name', 'vendor', 'product', 'version', 'bustype', - 'events', 'devnode', 'fd', 'device', - ) - - def __init__(self, - events=None, - name='py-evdev-uinput', - vendor=0x1, product=0x1, version=0x1, bustype=0x3, - devnode='/dev/uinput'): - ''' - :param events: the event types and codes that the uinput - device will be able to inject - defaults to all - key codes. - - :type events: dictionary of event types mapping to lists of - event codes. - - :param name: the name of the input device. - :param vendor: vendor identifier. - :param product: product identifier. - :param version: version identifier. - :param bustype: bustype identifier. - - .. note:: If you do not specify any events, the uinput device - will be able to inject only ``KEY_*`` and ``BTN_*`` - event codes. - ''' - - self.name = name #: Uinput device name. - self.vendor = vendor #: Device vendor identifier. - self.product = product #: Device product identifier. - self.version = version #: Device version identifier. - self.bustype = bustype #: Device bustype - eg. ``BUS_USB``. - self.devnode = devnode #: Uinput device node - eg. ``/dev/uinput/``. - - if not events: - events = {ecodes.EV_KEY: ecodes.keys.keys()} - - # the min, max, fuzz and flat values for the absolute axis for - # a given code - absinfo = [] - - self._verify() - - #: Write-only, non-blocking file descriptor to the uinput device node. - self.fd = _uinput.open(devnode) - - # set device capabilities - for etype, codes in events.items(): - for code in codes: - # handle max, min, fuzz, flat - if isinstance(code, (tuple, list, device.AbsInfo)): - # flatten (ABS_Y, (0, 255, 0, 0)) to (ABS_Y, 0, 255, 0, 0) - f = [code[0]]; f += code[1] - absinfo.append(f) - code = code[0] - - #:todo: a lot of unnecessary packing/unpacking - _uinput.enable(self.fd, etype, code) - - # create uinput device - _uinput.create(self.fd, name, vendor, product, version, bustype, absinfo) - - #: An :class:`InputDevice ` instance - #: for the fake input device. ``None`` if the device cannot be - #: opened for reading and writing. - self.device = self._find_device() - - def __enter__(self): - return self - - def __exit__(self, type, value, tb): - if hasattr(self, 'fd'): - self.close() - - def __repr__(self): - # :todo: - v = (repr(getattr(self, i)) for i in - ('name', 'bustype', 'vendor', 'product', 'version')) - return '{}({})'.format(self.__class__.__name__, ', '.join(v)) - - def __str__(self): - msg = ('name "{}", bus "{}", vendor "{:04x}", product "{:04x}", version "{:04x}"\n' - 'event types: {}') - - evtypes = [i[0] for i in self.capabilities(True).keys()] - msg = msg.format(self.name, ecodes.BUS[self.bustype], - self.vendor, self.product, - self.version, ' '.join(evtypes)) - - return msg - - def close(self): - # close the associated InputDevice, if it was previously opened - if self.device is not None: - self.device.close() - - # destroy the uinput device - if self.fd > -1: - _uinput.close(self.fd) - self.fd = -1 - - def write_event(self, event): - ''' - Inject an input event into the input subsystem. Events are - queued until a synchronization event is received. - - :param event: InputEvent instance or an object with an - ``event`` attribute (:class:`KeyEvent - `, :class:`RelEvent - ` etc). - - Example:: - - ev = InputEvent(1334414993, 274296, ecodes.EV_KEY, ecodes.KEY_A, 1) - ui.write_event(ev) - ''' - - if hasattr(event, 'event'): - event = event.event - - self.write(event.type, event.code, event.value) - - def write(self, etype, code, value): - ''' - Inject an input event into the input subsystem. Events are - queued until a synchronization event is received. - - :param etype: event type (eg. ``EV_KEY``). - :param code: event code (eg. ``KEY_A``). - :param value: event value (eg. 0 1 2 - depends on event type). - - Example:: - - ui.write(e.EV_KEY, e.KEY_A, 1) # key A - down - ui.write(e.EV_KEY, e.KEY_A, 0) # key A - up - ''' - - _uinput.write(self.fd, etype, code, value) - - def syn(self): - ''' - Inject a ``SYN_REPORT`` event into the input subsystem. Events - queued by :func:`write()` will be fired. If possible, events - will be merged into an 'atomic' event. - ''' - - _uinput.write(self.fd, ecodes.EV_SYN, ecodes.SYN_REPORT, 0) - - def capabilities(self, verbose=False, absinfo=True): - '''See :func:`capabilities `.''' - if self.device is None: - raise UInputError('input device not opened - cannot read capabilites') - - return self.device.capabilities(verbose, absinfo) - - def _verify(self): - ''' - Verify that an uinput device exists and is readable and writable - by the current process. - ''' - - try: - m = os.stat(self.devnode)[stat.ST_MODE] - if not stat.S_ISCHR(m): - raise - except (IndexError, OSError): - msg = '"{}" does not exist or is not a character device file '\ - '- verify that the uinput module is loaded' - raise UInputError(msg.format(self.devnode)) - - if not os.access(self.devnode, os.W_OK): - msg = '"{}" cannot be opened for writing' - raise UInputError(msg.format(self.devnode)) - - if len(self.name) > _uinput.maxnamelen: - msg = 'uinput device name must not be longer than {} characters' - raise UInputError(msg.format(_uinput.maxnamelen)) - - def _find_device(self): - #:bug: the device node might not be immediately available - time.sleep(0.1) - - for fn in util.list_devices('/dev/input/'): - d = device.InputDevice(fn) - if d.name == self.name: - return d diff --git a/evdev/util.py b/evdev/util.py deleted file mode 100644 index ccc6b58..0000000 --- a/evdev/util.py +++ /dev/null @@ -1,99 +0,0 @@ -# encoding: utf-8 - -import os -import stat -import glob - -from evdev import ecodes -from evdev.events import event_factory - - -def list_devices(input_device_dir='/dev/input'): - '''List readable, character devices.''' - - fns = glob.glob('{}/event*'.format(input_device_dir)) - fns = list(filter(is_device, fns)) - - return fns - - -def is_device(fn): - '''Check if ``fn`` is a readable and writable character device.''' - - if not os.path.exists(fn): - return False - - m = os.stat(fn)[stat.ST_MODE] - if not stat.S_ISCHR(m): - return False - - if not os.access(fn, os.R_OK | os.W_OK): - return False - - return True - - -def categorize(event): - ''' - Categorize an event according to its type. - - The :data:`event_factory ` dictionary - maps event types to sub-classes of :class:`InputEvent - `. If there is no corresponding key, the - event is returned as it is. - ''' - - if event.type in event_factory: - return event_factory[event.type](event) - else: - return event - - -def resolve_ecodes(typecodemap, unknown='?'): - ''' - Resolve event codes and types to their verbose names. - - :param typecodemap: mapping of event types to lists of event codes. - :param unknown: symbol to which unknown types or codes will be resolved. - - Example:: - - resolve_ecodes({ 1: [272, 273, 274] }) - { ('EV_KEY', 1): [('BTN_MOUSE', 272), - ('BTN_RIGHT', 273), - ('BTN_MIDDLE', 274)] } - - If typecodemap contains absolute axis info (instances of - :class:`AbsInfo ` ) the result would look - like:: - - resolve_ecodes({ 3: [(0, AbsInfo(...))] }) - { ('EV_ABS', 3L): [(('ABS_X', 0L), AbsInfo(...))] } - ''' - - for etype, codes in typecodemap.items(): - type_name = ecodes.EV[etype] - - # ecodes.keys are a combination of KEY_ and BTN_ codes - if etype == ecodes.EV_KEY: - code_names = ecodes.keys - else: - code_names = getattr(ecodes, type_name.split('_')[-1]) - - res = [] - for i in codes: - # elements with AbsInfo(), eg { 3 : [(0, AbsInfo(...)), (1, AbsInfo(...))] } - if isinstance(i, tuple): - l = ((code_names[i[0]], i[0]), i[1]) if i[0] in code_names \ - else ((unknown, i[0]), i[1]) - - # just ecodes { 0 : [0, 1, 3], 1 : [30, 48] } - else: - l = (code_names[i], i) if i in code_names else (unknown, i) - - res.append(l) - - yield (type_name, etype), res - - -__all__ = ('list_devices', 'is_device', 'categorize', 'resolve_ecodes') diff --git a/bin/udev-example.py b/examples/udev-example.py similarity index 78% rename from bin/udev-example.py rename to examples/udev-example.py index 12a617c..8e827f6 100755 --- a/bin/udev-example.py +++ b/examples/udev-example.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -''' +""" This is an example of using pyudev[1] alongside evdev. [1]: https://pyudev.readthedocs.org/ -''' +""" import functools import pyudev @@ -13,7 +13,7 @@ context = pyudev.Context() monitor = pyudev.Monitor.from_netlink(context) -monitor.filter_by(subsystem='input') +monitor.filter_by(subsystem="input") monitor.start() fds = {monitor.fileno(): monitor} @@ -32,16 +32,16 @@ break # find the device we're interested in and add it to fds - for name in (i['NAME'] for i in udev.ancestors if 'NAME' in i): + for name in (i["NAME"] for i in udev.ancestors if "NAME" in i): # I used a virtual input device for this test - you # should adapt this to your needs - if u'py-evdev-uinput' in name: - if udev.action == u'add': - print('Device added: %s' % udev) + if "py-evdev-uinput" in name: + if udev.action == "add": + print("Device added: %s" % udev) fds[dev.fd] = InputDevice(udev.device_node) break - if udev.action == u'remove': - print('Device removed: %s' % udev) + if udev.action == "remove": + print("Device removed: %s" % udev) def helper(): global fds diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d0b4f7c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[build-system] +requires = ["setuptools>=77.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "evdev" +version = "1.9.3" +description = "Bindings to the Linux input handling subsystem" +keywords = ["evdev", "input", "uinput"] +readme = "README.md" +license = "BSD-3-Clause" +requires-python = ">=3.9" +authors = [ + { name="Georgi Valkov", email="georgi.t.valkov@gmail.com" }, +] +maintainers = [ + { name="Tobi", email="proxima@sezanzeb.de" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3", + "Operating System :: POSIX :: Linux", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries", + "Programming Language :: Python :: Implementation :: CPython", +] + +[project.urls] +"Homepage" = "https://github.com/gvalkov/python-evdev" + +[tool.ruff] +line-length = 120 + +[tool.ruff.lint] +ignore = ["E265", "E241", "F403", "F401", "E401", "E731"] + +[tool.bumpversion] +current_version = "1.9.3" +commit = true +tag = true +allow_dirty = true + +[[tool.bumpversion.files]] +filename = "pyproject.toml" + +[[tool.bumpversion.files]] +filename = "docs/conf.py" + +[tool.pylint.'MESSAGES CONTROL'] +disable = """ + no-member, +""" + +[tool.pylint.typecheck] +generated-members = ["evdev.ecodes.*"] +ignored-modules= ["evdev._*"] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..725ad7f --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,10 @@ +pytest +Sphinx +sphinx-copybutton ~= 0.5.2 +sphinx-rtd-theme +ruff +bump-my-version ~= 0.17.4 +build +twine +cibuildwheel +setuptools diff --git a/scripts/build-binary.sh b/scripts/build-binary.sh new file mode 100755 index 0000000..bbdae6c --- /dev/null +++ b/scripts/build-binary.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -o allexport +set -o nounset + +CIBW_MANYLINUX_X86_64_IMAGE="manylinux_2_28" +CIBW_MANYLINUX_I686_IMAGE="manylinux_2_28" +CIBW_CONTAINER_ENGINE="podman" +CIBW_SKIP="cp36-*" +CIBW_ARCHS_LINUX="auto64" +CIBW_BEFORE_ALL_LINUX=./scripts/cibw-before.sh +CIBW_TEST_COMMAND="python -c 'import evdev; print(evdev)'" +CIBW_ENVIRONMENT="PACKAGE_NAME=evdev-binary" + +exec cibuildwheel \ No newline at end of file diff --git a/scripts/cibw-before.sh b/scripts/cibw-before.sh new file mode 100755 index 0000000..25220d4 --- /dev/null +++ b/scripts/cibw-before.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + + +if [ -n "$PACKAGE_NAME" ]; then + sed -i -re 's,^(name = ")evdev("),\1'${PACKAGE_NAME}'\2,' pyproject.toml +fi \ No newline at end of file diff --git a/setup.py b/setup.py index c153d78..1f6eaac 100755 --- a/setup.py +++ b/setup.py @@ -1,123 +1,135 @@ -#!/usr/bin/env python -# encoding: utf-8 - import os import sys +import shutil import textwrap +import platform +from pathlib import Path +from subprocess import run -from os.path import abspath, dirname, join as pjoin -from distutils.command.build import build -from setuptools.command.develop import develop -from setuptools.command.bdist_egg import bdist_egg from setuptools import setup, Extension, Command +from setuptools.command import build_ext as _build_ext -here = abspath(dirname(__file__)) - -classifiers = ( - 'Development Status :: 5 - Production/Stable', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Operating System :: POSIX :: Linux', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python :: Implementation :: CPython', -) - -cflags = ['-std=c99'] -input_c = Extension('evdev._input', sources=['evdev/input.c'], extra_compile_args=cflags) -uinput_c = Extension('evdev._uinput', sources=['evdev/uinput.c'], extra_compile_args=cflags) -ecodes_c = Extension('evdev._ecodes', sources=['evdev/ecodes.c'], extra_compile_args=cflags) +curdir = Path(__file__).resolve().parent +ecodes_c_path = curdir / "src/evdev/ecodes.c" -kw = { - 'name': 'evdev', - 'version': '0.4.4', - 'description': 'Bindings for the linux input handling subsystem', - 'long_description': open(pjoin(here, 'README.rst')).read(), +def create_ecodes(headers=None, reproducible=False): + if not headers: + include_paths = set() + cpath = os.environ.get("CPATH", "").strip() + c_inc_path = os.environ.get("C_INCLUDE_PATH", "").strip() - 'author': 'Georgi Valkov', - 'author_email': 'georgi.t.valkov@gmail.com', - 'license': 'Revised BSD License', + if cpath: + include_paths.update(cpath.split(":")) + if c_inc_path: + include_paths.update(c_inc_path.split(":")) - 'keywords': 'evdev input uinput', - 'classifiers': classifiers, - 'url': 'https://github.com/gvalkov/python-evdev', + include_paths.add("/usr/include") + if platform.system().lower() == "freebsd": + files = ["dev/evdev/input.h", "dev/evdev/input-event-codes.h", "dev/evdev/uinput.h"] + else: + files = ["linux/input.h", "linux/input-event-codes.h", "linux/uinput.h"] - 'packages': ['evdev'], - 'ext_modules': [input_c, uinput_c, ecodes_c], - 'tests_require': ['pytest'], + headers = [os.path.join(path, file) for path in include_paths for file in files] - 'include_package_data': False, - 'zip_safe': True, - 'cmdclass': {}, -} + headers = [header for header in headers if os.path.isfile(header)] + if not headers: + msg = """\ + The 'linux/input.h' and 'linux/input-event-codes.h' include files + are missing. You will have to install the kernel header files in + order to continue: + dnf install kernel-headers-$(uname -r) + apt-get install linux-headers-$(uname -r) + emerge sys-kernel/linux-headers + pacman -S kernel-headers -def create_ecodes(): - # :todo: expose as a command option - header = '/usr/include/linux/input.h' + In case they are installed in a non-standard location, you may use + the '--evdev-headers' option to specify one or more colon-separated + paths. For example: - if not os.path.isfile(header): - msg = '''\ - The linux/input.h header file is missing. You will have to - install the headers for your kernel in order to continue: + python setup.py \\ + build \\ + build_ecodes --evdev-headers path/input.h:path/input-event-codes.h \\ + build_ext --include-dirs path/ \\ + install - yum install kernel-headers-$(uname -r) - apt-get intall linux-headers-$(uname -r) - pacman -S kernel-headers\n\n''' + If you want to avoid building this package from source, then please consider + installing the `evdev-binary` package instead. Keep in mind that it may not be + fully compatible with, or support all the features of your current kernel. + """ sys.stderr.write(textwrap.dedent(msg)) sys.exit(1) - from subprocess import check_call + print("writing %s (using %s)" % (ecodes_c_path, " ".join(headers))) + with ecodes_c_path.open("w") as fh: + cmd = [sys.executable, "src/evdev/genecodes_c.py"] + if reproducible: + cmd.append("--reproducible") + cmd.extend(["--ecodes", *headers]) + run(cmd, check=True, stdout=fh) - print('writing ecodes.c (using %s)' % header) - cmd = '%s genecodes.py %s > ecodes.c' % (sys.executable, header) - check_call(cmd, cwd="%s/evdev" % here, shell=True) +class build_ecodes(Command): + description = "generate ecodes.c" -# :todo: figure out a smarter way to do this -# :note: subclassing build_ext doesn't really cut it -class BuildCommand(build): - def run(self): - create_ecodes() - build.run(self) - -class DevelopCommand(develop): - def run(self): - create_ecodes() - develop.run(self) + user_options = [ + ("evdev-headers=", None, "colon-separated paths to input subsystem headers"), + ("reproducible", None, "hide host details (host/paths) to create a reproducible output"), + ] -class BdistEggCommand(bdist_egg): - def run(self): - create_ecodes() - bdist_egg.run(self) - - -class PyTest(Command): - '''setup.py test -> py.test tests''' - - user_options = [] def initialize_options(self): - pass + self.evdev_headers = None + self.reproducible = False def finalize_options(self): - pass + if self.evdev_headers: + self.evdev_headers = self.evdev_headers.split(":") + if self.reproducible is None: + self.reproducible = False def run(self): - from subprocess import call - errno = call(('py.test', 'tests')) - raise SystemExit(errno) + create_ecodes(self.evdev_headers, reproducible=self.reproducible) + +class build_ext(_build_ext.build_ext): + def has_ecodes(self): + if ecodes_c_path.exists(): + print("ecodes.c already exists ... skipping build_ecodes") + return False + return True -kw['cmdclass']['test'] = PyTest -kw['cmdclass']['build'] = BuildCommand -kw['cmdclass']['develop'] = DevelopCommand -kw['cmdclass']['bdist_egg'] = BdistEggCommand + def generate_ecodes_py(self): + ecodes_py = Path(self.build_lib) / "evdev/ecodes.py" + print(f"writing {ecodes_py}") + with ecodes_py.open("w") as fh: + cmd = [sys.executable, "-B", "src/evdev/genecodes_py.py"] + res = run(cmd, env={"PYTHONPATH": self.build_lib}, stdout=fh) -if __name__ == '__main__': - setup(**kw) + if res.returncode != 0: + print(f"failed to generate static {ecodes_py} - will use ecodes_runtime.py") + shutil.copy("src/evdev/ecodes_runtime.py", ecodes_py) + + def run(self): + for cmd_name in self.get_sub_commands(): + self.run_command(cmd_name) + _build_ext.build_ext.run(self) + self.generate_ecodes_py() + + sub_commands = [("build_ecodes", has_ecodes)] + _build_ext.build_ext.sub_commands + + +cflags = ["-std=c99", "-Wno-error=declaration-after-statement"] +setup( + ext_modules=[ + Extension("evdev._input", sources=["src/evdev/input.c"], extra_compile_args=cflags), + Extension("evdev._uinput", sources=["src/evdev/uinput.c"], extra_compile_args=cflags), + Extension("evdev._ecodes", sources=["src/evdev/ecodes.c"], extra_compile_args=cflags), + ], + cmdclass={ + "build_ext": build_ext, + "build_ecodes": build_ecodes, + }, +) diff --git a/src/evdev/__init__.py b/src/evdev/__init__.py new file mode 100644 index 0000000..bae0fec --- /dev/null +++ b/src/evdev/__init__.py @@ -0,0 +1,39 @@ +# -------------------------------------------------------------------------- +# Gather everything into a single, convenient namespace. +# -------------------------------------------------------------------------- + +# The superfluous "import name as name" syntax is here to satisfy mypy's attrs-defined rule. +# Alternatively all exported objects can be listed in __all__. + +from . import ( + ecodes as ecodes, + ff as ff, +) + +from .device import ( + AbsInfo as AbsInfo, + DeviceInfo as DeviceInfo, + EvdevError as EvdevError, + InputDevice as InputDevice, +) + +from .events import ( + AbsEvent as AbsEvent, + InputEvent as InputEvent, + KeyEvent as KeyEvent, + RelEvent as RelEvent, + SynEvent as SynEvent, + event_factory as event_factory, +) + +from .uinput import ( + UInput as UInput, + UInputError as UInputError, +) + +from .util import ( + categorize as categorize, + list_devices as list_devices, + resolve_ecodes as resolve_ecodes, + resolve_ecodes_dict as resolve_ecodes_dict, +) diff --git a/src/evdev/device.py b/src/evdev/device.py new file mode 100644 index 0000000..a7f9b92 --- /dev/null +++ b/src/evdev/device.py @@ -0,0 +1,440 @@ +import contextlib +import os +from typing import Dict, Generic, Iterator, List, Literal, NamedTuple, Tuple, TypeVar, Union, overload + +from . import _input, ecodes, util + +try: + from .eventio_async import EvdevError, EventIO +except ImportError: + from .eventio import EvdevError, EventIO + +_AnyStr = TypeVar("_AnyStr", str, bytes) + + +class AbsInfo(NamedTuple): + """Absolute axis information. + + A ``namedtuple`` with absolute axis information - + corresponds to the ``input_absinfo`` struct: + + Attributes + --------- + value + Latest reported value for the axis. + + min + Specifies minimum value for the axis. + + max + Specifies maximum value for the axis. + + fuzz + Specifies fuzz value that is used to filter noise from the + event stream. + + flat + Values that are within this value will be discarded by joydev + interface and reported as 0 instead. + + resolution + Specifies resolution for the values reported for the axis. + Resolution for main axes (``ABS_X, ABS_Y, ABS_Z``) is reported + in units per millimeter (units/mm), resolution for rotational + axes (``ABS_RX, ABS_RY, ABS_RZ``) is reported in units per + radian. + + Note + ---- + The input core does not clamp reported values to the ``[minimum, + maximum]`` limits, such task is left to userspace. + + """ + + value: int + min: int + max: int + fuzz: int + flat: int + resolution: int + + def __str__(self): + return "value {}, min {}, max {}, fuzz {}, flat {}, res {}".format(*self) # pylint: disable=not-an-iterable + + +class KbdInfo(NamedTuple): + """Keyboard repeat rate. + + Attributes + ---------- + delay + Amount of time that a key must be depressed before it will start + to repeat (in milliseconds). + + repeat + Keyboard repeat rate in characters per second. + """ + + delay: int + repeat: int + + def __str__(self): + return "delay {}, repeat {}".format(self.delay, self.repeat) + + +class DeviceInfo(NamedTuple): + """ + Attributes + ---------- + bustype + vendor + product + version + """ + + bustype: int + vendor: int + product: int + version: int + + def __str__(self) -> str: + msg = "bus: {:04x}, vendor {:04x}, product {:04x}, version {:04x}" + return msg.format(*self) # pylint: disable=not-an-iterable + + +class InputDevice(EventIO, Generic[_AnyStr]): + """ + A linux input device from which input events can be read. + """ + + __slots__ = ("path", "fd", "info", "name", "phys", "uniq", "_rawcapabilities", "version", "ff_effects_count") + + def __init__(self, dev: Union[_AnyStr, "os.PathLike[_AnyStr]"]): + """ + Arguments + --------- + dev : str|bytes|PathLike + Path to input device + """ + + #: Path to input device. + self.path: _AnyStr = dev if not hasattr(dev, "__fspath__") else dev.__fspath__() + + # Certain operations are possible only when the device is opened in read-write mode. + try: + fd = os.open(dev, os.O_RDWR | os.O_NONBLOCK) + except OSError: + fd = os.open(dev, os.O_RDONLY | os.O_NONBLOCK) + + #: A non-blocking file descriptor to the device file. + self.fd: int = fd + + # Returns (bustype, vendor, product, version, name, phys, capabilities). + info_res = _input.ioctl_devinfo(self.fd) + + #: A :class:`DeviceInfo ` instance. + self.info = DeviceInfo(*info_res[:4]) + + #: The name of the event device. + self.name: str = info_res[4] + + #: The physical topology of the device. + self.phys: str = info_res[5] + + #: The unique identifier of the device. + self.uniq: str = info_res[6] + + #: The evdev protocol version. + self.version: int = _input.ioctl_EVIOCGVERSION(self.fd) + + #: The raw dictionary of device capabilities - see `:func:capabilities()`. + self._rawcapabilities = _input.ioctl_capabilities(self.fd) + + #: The number of force feedback effects the device can keep in its memory. + self.ff_effects_count = _input.ioctl_EVIOCGEFFECTS(self.fd) + + def __del__(self) -> None: + if hasattr(self, "fd") and self.fd is not None: + try: + self.close() + except (OSError, ImportError, AttributeError): + pass + + def _capabilities(self, absinfo: bool = True): + res = {} + + for etype, _ecodes in self._rawcapabilities.items(): + for code in _ecodes: + l = res.setdefault(etype, []) + if isinstance(code, tuple): + if absinfo: + a = code[1] # (0, 0, 0, 255, 0, 0) + i = AbsInfo(*a) + l.append((code[0], i)) + else: + l.append(code[0]) + else: + l.append(code) + + return res + + @overload + def capabilities(self, verbose: Literal[False] = ..., absinfo: bool = ...) -> Dict[int, List[int]]: + ... + @overload + def capabilities(self, verbose: Literal[True], absinfo: bool = ...) -> Dict[Tuple[str, int], List[Tuple[str, int]]]: + ... + def capabilities(self, verbose: bool = False, absinfo: bool = True) -> Union[Dict[int, List[int]], Dict[Tuple[str, int], List[Tuple[str, int]]]]: + """ + Return the event types that this device supports as a mapping of + supported event types to lists of handled event codes. + + Example + -------- + >>> device.capabilities() + { 1: [272, 273, 274], + 2: [0, 1, 6, 8] } + + If ``verbose`` is ``True``, event codes and types will be resolved + to their names. + + :: + + { ('EV_KEY', 1): [('BTN_MOUSE', 272), + ('BTN_RIGHT', 273), + ('BTN_MIDDLE', 273)], + ('EV_REL', 2): [('REL_X', 0), + ('REL_Y', 1), + ('REL_HWHEEL', 6), + ('REL_WHEEL', 8)] } + + Unknown codes or types will be resolved to ``'?'``. + + If ``absinfo`` is ``True``, the list of capabilities will also + include absolute axis information in the form of + :class:`AbsInfo` instances:: + + { 3: [ (0, AbsInfo(min=0, max=255, fuzz=0, flat=0)), + (1, AbsInfo(min=0, max=255, fuzz=0, flat=0)) ]} + + Combined with ``verbose`` the above becomes:: + + { ('EV_ABS', 3): [ (('ABS_X', 0), AbsInfo(min=0, max=255, fuzz=0, flat=0)), + (('ABS_Y', 1), AbsInfo(min=0, max=255, fuzz=0, flat=0)) ]} + + """ + + if verbose: + return dict(util.resolve_ecodes_dict(self._capabilities(absinfo))) + else: + return self._capabilities(absinfo) + + def input_props(self, verbose: bool = False): + """ + Get device properties and quirks. + + Example + ------- + >>> device.input_props() + [0, 5] + + If ``verbose`` is ``True``, input properties are resolved to their + names. Unknown codes are resolved to ``'?'``:: + + [('INPUT_PROP_POINTER', 0), ('INPUT_PROP_POINTING_STICK', 5)] + + """ + props = _input.ioctl_EVIOCGPROP(self.fd) + if verbose: + return util.resolve_ecodes(ecodes.INPUT_PROP, props) + + return props + + def leds(self, verbose: bool = False): + """ + Return currently set LED keys. + + Example + ------- + >>> device.leds() + [0, 1, 8, 9] + + If ``verbose`` is ``True``, event codes are resolved to their + names. Unknown codes are resolved to ``'?'``:: + + [('LED_NUML', 0), ('LED_CAPSL', 1), ('LED_MISC', 8), ('LED_MAIL', 9)] + + """ + leds = _input.ioctl_EVIOCG_bits(self.fd, ecodes.EV_LED) + if verbose: + return util.resolve_ecodes(ecodes.LED, leds) + + return leds + + def set_led(self, led_num: int, value: int) -> None: + """ + Set the state of the selected LED. + + Example + ------- + >>> device.set_led(ecodes.LED_NUML, 1) + """ + self.write(ecodes.EV_LED, led_num, value) + + def __eq__(self, other): + """ + Two devices are equal if their :data:`info` attributes are equal. + """ + return isinstance(other, self.__class__) and self.info == other.info and self.path == other.path + + def __str__(self) -> str: + msg = 'device {}, name "{}", phys "{}", uniq "{}"' + return msg.format(self.path, self.name, self.phys, self.uniq or "") + + def __repr__(self) -> str: + msg = (self.__class__.__name__, self.path) + return "{}({!r})".format(*msg) + + def __fspath__(self): + return self.path + + def close(self) -> None: + if self.fd > -1: + try: + super().close() + os.close(self.fd) + finally: + self.fd = -1 + + def grab(self) -> None: + """ + Grab input device using ``EVIOCGRAB`` - other applications will + be unable to receive events until the device is released. Only + one process can hold a ``EVIOCGRAB`` on a device. + + Warning + ------- + Grabbing an already grabbed device will raise an ``OSError``. + """ + + _input.ioctl_EVIOCGRAB(self.fd, 1) + + def ungrab(self) -> None: + """ + Release device if it has been already grabbed (uses `EVIOCGRAB`). + + Warning + ------- + Releasing an already released device will raise an + ``OSError('Invalid argument')``. + """ + + _input.ioctl_EVIOCGRAB(self.fd, 0) + + @contextlib.contextmanager + def grab_context(self) -> Iterator[None]: + """ + A context manager for the duration of which only the current + process will be able to receive events from the device. + """ + self.grab() + yield + self.ungrab() + + def upload_effect(self, effect: "ff.Effect"): + """ + Upload a force feedback effect to a force feedback device. + """ + + data = memoryview(effect).tobytes() + ff_id = _input.upload_effect(self.fd, data) + return ff_id + + def erase_effect(self, ff_id) -> None: + """ + Erase a force effect from a force feedback device. This also + stops the effect. + """ + + _input.erase_effect(self.fd, ff_id) + + @property + def repeat(self): + """ + Get or set the keyboard repeat rate (in characters per + minute) and delay (in milliseconds). + """ + + return KbdInfo(*_input.ioctl_EVIOCGREP(self.fd)) + + @repeat.setter + def repeat(self, value: Tuple[int, int]): + return _input.ioctl_EVIOCSREP(self.fd, *value) + + def active_keys(self, verbose: bool = False): + """ + Return currently active keys. + + Example + ------- + + >>> device.active_keys() + [1, 42] + + If ``verbose`` is ``True``, key codes are resolved to their + verbose names. Unknown codes are resolved to ``'?'``. For + example:: + + [('KEY_ESC', 1), ('KEY_LEFTSHIFT', 42)] + + """ + active_keys = _input.ioctl_EVIOCG_bits(self.fd, ecodes.EV_KEY) + if verbose: + return util.resolve_ecodes(ecodes.KEY, active_keys) + + return active_keys + + def absinfo(self, axis_num: int): + """ + Return current :class:`AbsInfo` for input device axis + + Arguments + --------- + axis_num : int + EV_ABS keycode (example :attr:`ecodes.ABS_X`) + + Example + ------- + >>> device.absinfo(ecodes.ABS_X) + AbsInfo(value=1501, min=-32768, max=32767, fuzz=0, flat=128, resolution=0) + """ + return AbsInfo(*_input.ioctl_EVIOCGABS(self.fd, axis_num)) + + def set_absinfo(self, axis_num: int, value=None, min=None, max=None, fuzz=None, flat=None, resolution=None) -> None: + """ + Update :class:`AbsInfo` values. Only specified values will be overwritten. + + Arguments + --------- + axis_num : int + EV_ABS keycode (example :attr:`ecodes.ABS_X`) + + Example + ------- + >>> device.set_absinfo(ecodes.ABS_X, min=-2000, max=2000) + + You can also unpack AbsInfo tuple that will overwrite all values + + >>> device.set_absinfo(ecodes.ABS_Y, *AbsInfo(0, -2000, 2000, 0, 15, 0)) + """ + + cur_absinfo = self.absinfo(axis_num) + new_absinfo = AbsInfo( + value if value is not None else cur_absinfo.value, + min if min is not None else cur_absinfo.min, + max if max is not None else cur_absinfo.max, + fuzz if fuzz is not None else cur_absinfo.fuzz, + flat if flat is not None else cur_absinfo.flat, + resolution if resolution is not None else cur_absinfo.resolution, + ) + _input.ioctl_EVIOCSABS(self.fd, axis_num, new_absinfo) diff --git a/src/evdev/ecodes.py b/src/evdev/ecodes.py new file mode 100644 index 0000000..fd4afc4 --- /dev/null +++ b/src/evdev/ecodes.py @@ -0,0 +1,5 @@ +# When installed, this module is replaced by an ecodes.py generated at +# build time by genecodes_py.py (see build_ext in setup.py). + +# This stub exists to make development of evdev itself more convenient. +from .ecodes_runtime import * diff --git a/evdev/ecodes.py b/src/evdev/ecodes_runtime.py similarity index 68% rename from evdev/ecodes.py rename to src/evdev/ecodes_runtime.py index c94b0c1..47f3b23 100644 --- a/evdev/ecodes.py +++ b/src/evdev/ecodes_runtime.py @@ -1,15 +1,15 @@ -# encoding: utf-8 - -''' -This modules exposes the integer constants defined in ``linux/input.h``. +# pylint: disable=undefined-variable +""" +This modules exposes the integer constants defined in ``linux/input.h`` and +``linux/input-event-codes.h``. Exposed constants:: KEY, ABS, REL, SW, MSC, LED, BTN, REP, SND, ID, EV, - BUS, SYN, FF, FF_STATUS + BUS, SYN, FF, FF_STATUS, INPUT_PROP -This module also provides numerous reverse and forward mappings that are best -illustrated by a few examples:: +This module also provides reverse and forward mappings of the names and values +of the above mentioned constants:: >>> evdev.ecodes.KEY_A 30 @@ -29,29 +29,30 @@ >>> evdev.ecodes.bytype[evdev.ecodes.EV_REL][0] 'REL_X' -Values in reverse mappings may point to one or more ecodes. For example:: +Keep in mind that values in reverse mappings may point to one or more event +codes. For example:: >>> evdev.ecodes.FF[80] - ['FF_EFFECT_MIN', 'FF_RUMBLE'] + ('FF_EFFECT_MIN', 'FF_RUMBLE') >>> evdev.ecodes.FF[81] 'FF_PERIODIC' -''' +""" from inspect import getmembers -from evdev import _ecodes +from . import _ecodes #: Mapping of names to values. ecodes = {} -prefixes = 'KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF' -prev_prefix = '' +prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF INPUT_PROP UI_FF".split() +prev_prefix = "" g = globals() # eg. code: 'REL_Z', val: 2 for code, val in getmembers(_ecodes): - for prefix in prefixes.split(): # eg. 'REL' + for prefix in prefixes: # eg. 'REL' if code.startswith(prefix): ecodes[code] = val # FF_STATUS codes should not appear in the FF reverse mapping @@ -70,6 +71,15 @@ prev_prefix = prefix + +# Convert lists to tuples. +k, v = None, None +for prefix in prefixes: + for k, v in g[prefix].items(): + if isinstance(v, list): + g[prefix][k] = tuple(v) + + #: Keys are a combination of all BTN and KEY codes. keys = {} keys.update(BTN) @@ -85,16 +95,17 @@ _ecodes.EV_KEY: keys, _ecodes.EV_ABS: ABS, _ecodes.EV_REL: REL, - _ecodes.EV_SW: SW, + _ecodes.EV_SW: SW, _ecodes.EV_MSC: MSC, _ecodes.EV_LED: LED, _ecodes.EV_REP: REP, _ecodes.EV_SND: SND, _ecodes.EV_SYN: SYN, - _ecodes.EV_FF: FF, - _ecodes.EV_FF_STATUS: FF_STATUS, } + _ecodes.EV_FF: FF, + _ecodes.EV_FF_STATUS: FF_STATUS, +} from evdev._ecodes import * # cheaper than whitelisting in an __all__ -del code, val, prefix, getmembers, g, d, prefixes, prev_prefix +del code, val, prefix, getmembers, g, d, k, v, prefixes, prev_prefix diff --git a/src/evdev/eventio.py b/src/evdev/eventio.py new file mode 100644 index 0000000..bdb91a4 --- /dev/null +++ b/src/evdev/eventio.py @@ -0,0 +1,152 @@ +import fcntl +import functools +import os +import select +from typing import Iterator, Union + +from . import _input, _uinput, ecodes +from .events import InputEvent + + +# -------------------------------------------------------------------------- +class EvdevError(Exception): + pass + + +class EventIO: + """ + Base class for reading and writing input events. + + This class is used by :class:`InputDevice` and :class:`UInput`. + + - On, :class:`InputDevice` it used for reading user-generated events (e.g. + key presses, mouse movements) and writing feedback events (e.g. leds, + beeps). + + - On, :class:`UInput` it used for writing user-generated events (e.g. + key presses, mouse movements) and reading feedback events (e.g. leds, + beeps). + """ + + def fileno(self): + """ + Return the file descriptor to the open event device. This makes + it possible to pass instances directly to :func:`select.select()` and + :class:`asyncore.file_dispatcher`. + """ + return self.fd + + def read_loop(self) -> Iterator[InputEvent]: + """ + Enter an endless :func:`select.select()` loop that yields input events. + """ + + while True: + r, w, x = select.select([self.fd], [], []) + for event in self.read(): + yield event + + def read_one(self) -> Union[InputEvent, None]: + """ + Read and return a single input event as an instance of + :class:`InputEvent `. + + Return ``None`` if there are no pending input events. + """ + + # event -> (sec, usec, type, code, val) + event = _input.device_read(self.fd) + + if event: + return InputEvent(*event) + + def read(self) -> Iterator[InputEvent]: + """ + Read multiple input events from device. Return a generator object that + yields :class:`InputEvent ` instances. Raises + `BlockingIOError` if there are no available events at the moment. + """ + + # events -> ((sec, usec, type, code, val), ...) + events = _input.device_read_many(self.fd) + + for event in events: + yield InputEvent(*event) + + # pylint: disable=no-self-argument + def need_write(func): + """ + Decorator that raises :class:`EvdevError` if there is no write access to the + input device. + """ + + @functools.wraps(func) + def wrapper(*args): + fd = args[0].fd + if fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_RDWR: + # pylint: disable=not-callable + return func(*args) + msg = 'no write access to device "%s"' % args[0].path + raise EvdevError(msg) + + return wrapper + + def write_event(self, event): + """ + Inject an input event into the input subsystem. Events are + queued until a synchronization event is received. + + Arguments + --------- + event: InputEvent + InputEvent instance or an object with an ``event`` attribute + (:class:`KeyEvent `, :class:`RelEvent + ` etc). + + Example + ------- + >>> ev = InputEvent(1334414993, 274296, ecodes.EV_KEY, ecodes.KEY_A, 1) + >>> ui.write_event(ev) + """ + + if hasattr(event, "event"): + event = event.event + + self.write(event.type, event.code, event.value) + + @need_write + def write(self, etype: int, code: int, value: int): + """ + Inject an input event into the input subsystem. Events are + queued until a synchronization event is received. + + Arguments + --------- + etype + event type (e.g. ``EV_KEY``). + + code + event code (e.g. ``KEY_A``). + + value + event value (e.g. 0 1 2 - depends on event type). + + Example + --------- + >>> ui.write(e.EV_KEY, e.KEY_A, 1) # key A - down + >>> ui.write(e.EV_KEY, e.KEY_A, 0) # key A - up + """ + + _uinput.write(self.fd, etype, code, value) + + def syn(self): + """ + Inject a ``SYN_REPORT`` event into the input subsystem. Events + queued by :func:`write()` will be fired. If possible, events + will be merged into an 'atomic' event. + """ + + self.write(ecodes.EV_SYN, ecodes.SYN_REPORT, 0) + + def close(self): + pass diff --git a/src/evdev/eventio_async.py b/src/evdev/eventio_async.py new file mode 100644 index 0000000..4af1aab --- /dev/null +++ b/src/evdev/eventio_async.py @@ -0,0 +1,106 @@ +import asyncio +import select +import sys + +from . import eventio +from .events import InputEvent + +# needed for compatibility +from .eventio import EvdevError + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing import Any as Self + + +class ReadIterator: + def __init__(self, device): + self.current_batch = iter(()) + self.device = device + + # Standard iterator protocol. + def __iter__(self) -> Self: + return self + + def __next__(self) -> InputEvent: + try: + # Read from the previous batch of events. + return next(self.current_batch) + except StopIteration: + r, w, x = select.select([self.device.fd], [], []) + self.current_batch = self.device.read() + return next(self.current_batch) + + def __aiter__(self) -> Self: + return self + + def __anext__(self) -> "asyncio.Future[InputEvent]": + future = asyncio.Future() + try: + # Read from the previous batch of events. + future.set_result(next(self.current_batch)) + except StopIteration: + + def next_batch_ready(batch): + try: + self.current_batch = batch.result() + future.set_result(next(self.current_batch)) + except Exception as e: + future.set_exception(e) + + self.device.async_read().add_done_callback(next_batch_ready) + return future + + +class EventIO(eventio.EventIO): + def _do_when_readable(self, callback): + loop = asyncio.get_event_loop() + + def ready(): + loop.remove_reader(self.fileno()) + callback() + + loop.add_reader(self.fileno(), ready) + + def _set_result(self, future, cb): + try: + future.set_result(cb()) + except Exception as error: + future.set_exception(error) + + def async_read_one(self): + """ + Asyncio coroutine to read and return a single input event as + an instance of :class:`InputEvent `. + """ + future = asyncio.Future() + self._do_when_readable(lambda: self._set_result(future, self.read_one)) + return future + + def async_read(self): + """ + Asyncio coroutine to read multiple input events from device. Return + a generator object that yields :class:`InputEvent ` + instances. + """ + future = asyncio.Future() + self._do_when_readable(lambda: self._set_result(future, self.read)) + return future + + def async_read_loop(self) -> ReadIterator: + """ + Return an iterator that yields input events. This iterator is + compatible with the ``async for`` syntax. + + """ + return ReadIterator(self) + + def close(self): + try: + loop = asyncio.get_event_loop() + loop.remove_reader(self.fileno()) + except RuntimeError: + # no event loop present, so there is nothing to + # remove the reader from. Ignore + pass diff --git a/src/evdev/events.py b/src/evdev/events.py new file mode 100644 index 0000000..922bfe6 --- /dev/null +++ b/src/evdev/events.py @@ -0,0 +1,192 @@ +""" +This module provides the :class:`InputEvent` class, which closely +resembles the ``input_event`` struct defined in ``linux/input.h``: + +.. code-block:: c + + struct input_event { + struct timeval time; + __u16 type; + __u16 code; + __s32 value; + }; + +This module also defines several :class:`InputEvent` sub-classes that +know more about the different types of events (key, abs, rel etc). The +:data:`event_factory` dictionary maps event types to these classes. + +Assuming you use the :func:`evdev.util.categorize()` function to +categorize events according to their type, adding or replacing a class +for a specific event type becomes a matter of modifying +:data:`event_factory`. + +All classes in this module have reasonable ``str()`` and ``repr()`` +methods:: + + >>> print(event) + event at 1337197425.477827, code 04, type 04, val 458792 + >>> print(repr(event)) + InputEvent(1337197425L, 477827L, 4, 4, 458792L) + + >>> print(key_event) + key event at 1337197425.477835, 28 (KEY_ENTER), up + >>> print(repr(key_event)) + KeyEvent(InputEvent(1337197425L, 477835L, 1, 28, 0L)) +""" + +# event type descriptions have been taken mot-a-mot from: +# http://www.kernel.org/doc/Documentation/input/event-codes.txt + +# pylint: disable=no-name-in-module +from typing import Final +from .ecodes import ABS, EV_ABS, EV_KEY, EV_REL, EV_SYN, KEY, REL, SYN, keys + + +class InputEvent: + """A generic input event.""" + + __slots__ = "sec", "usec", "type", "code", "value" + + def __init__(self, sec, usec, type, code, value): + #: Time in seconds since epoch at which event occurred. + self.sec: int = sec + + #: Microsecond portion of the timestamp. + self.usec: int = usec + + #: Event type - one of ``ecodes.EV_*``. + self.type: int = type + + #: Event code related to the event type. + self.code: int = code + + #: Event value related to the event type. + self.value: int = value + + def timestamp(self) -> float: + """Return event timestamp as a float.""" + return self.sec + (self.usec / 1000000.0) + + def __str__(self): + msg = "event at {:f}, code {:02d}, type {:02d}, val {:02d}" + return msg.format(self.timestamp(), self.code, self.type, self.value) + + def __repr__(self): + msg = "{}({!r}, {!r}, {!r}, {!r}, {!r})" + return msg.format(self.__class__.__name__, self.sec, self.usec, self.type, self.code, self.value) + + +class KeyEvent: + """An event generated by a keyboard, button or other key-like devices.""" + + key_up: Final[int] = 0x0 + key_down: Final[int] = 0x1 + key_hold: Final[int] = 0x2 + + __slots__ = "scancode", "keycode", "keystate", "event" + + def __init__(self, event: InputEvent, allow_unknown: bool = False): + """ + The ``allow_unknown`` argument determines what to do in the event of an event code + for which a key code cannot be found. If ``False`` a ``KeyError`` will be raised. + If ``True`` the keycode will be set to the hex value of the event code. + """ + + self.scancode: int = event.code + + if event.value == 0: + self.keystate = KeyEvent.key_up + elif event.value == 2: + self.keystate = KeyEvent.key_hold + elif event.value == 1: + self.keystate = KeyEvent.key_down + + try: + self.keycode = keys[event.code] + except KeyError: + if allow_unknown: + self.keycode = "0x{:02X}".format(event.code) + else: + raise + + #: Reference to an :class:`InputEvent` instance. + self.event: InputEvent = event + + def __str__(self): + try: + ks = ("up", "down", "hold")[self.keystate] + except IndexError: + ks = "unknown" + + msg = "key event at {:f}, {} ({}), {}" + return msg.format(self.event.timestamp(), self.scancode, self.keycode, ks) + + def __repr__(self): + return "{}({!r})".format(self.__class__.__name__, self.event) + + +class RelEvent: + """A relative axis event (e.g moving the mouse 5 units to the left).""" + + __slots__ = "event" + + def __init__(self, event: InputEvent): + #: Reference to an :class:`InputEvent` instance. + self.event: InputEvent = event + + def __str__(self): + msg = "relative axis event at {:f}, {}" + return msg.format(self.event.timestamp(), REL[self.event.code]) + + def __repr__(self): + return "{}({!r})".format(self.__class__.__name__, self.event) + + +class AbsEvent: + """An absolute axis event (e.g the coordinates of a tap on a touchscreen).""" + + __slots__ = "event" + + def __init__(self, event: InputEvent): + #: Reference to an :class:`InputEvent` instance. + self.event: InputEvent = event + + def __str__(self): + msg = "absolute axis event at {:f}, {}" + return msg.format(self.event.timestamp(), ABS[self.event.code]) + + def __repr__(self): + return "{}({!r})".format(self.__class__.__name__, self.event) + + +class SynEvent: + """ + A synchronization event. Used as markers to separate events. Events may be + separated in time or in space, such as with the multitouch protocol. + """ + + __slots__ = "event" + + def __init__(self, event: InputEvent): + #: Reference to an :class:`InputEvent` instance. + self.event: InputEvent = event + + def __str__(self): + msg = "synchronization event at {:f}, {}" + return msg.format(self.event.timestamp(), SYN[self.event.code]) + + def __repr__(self): + return "{}({!r})".format(self.__class__.__name__, self.event) + + +#: A mapping of event types to :class:`InputEvent` sub-classes. Used +#: by :func:`evdev.util.categorize()` +event_factory = { + EV_KEY: KeyEvent, + EV_REL: RelEvent, + EV_ABS: AbsEvent, + EV_SYN: SynEvent, +} + + +__all__ = ("InputEvent", "KeyEvent", "RelEvent", "SynEvent", "AbsEvent", "event_factory") diff --git a/src/evdev/evtest.py b/src/evdev/evtest.py new file mode 100644 index 0000000..6ea3bb5 --- /dev/null +++ b/src/evdev/evtest.py @@ -0,0 +1,181 @@ +""" +Usage: evtest [options] [, ...] + +Input device enumerator and event monitor. + +Running evtest without any arguments will let you select +from a list of all readable input devices. + +Options: + -h, --help Show this help message and exit. + -c, --capabilities List device capabilities and exit. + -g, --grab Other applications will not receive events from + the selected devices while evtest is running. + +Examples: + evtest /dev/input/event0 /dev/input/event1 +""" + +import atexit +import optparse +import re +import select +import sys +import termios + +from . import AbsInfo, InputDevice, ecodes, list_devices + + +def parseopt(): + parser = optparse.OptionParser(add_help_option=False) + parser.add_option("-h", "--help", action="store_true") + parser.add_option("-g", "--grab", action="store_true") + parser.add_option("-c", "--capabilities", action="store_true") + return parser.parse_args() + + +def main(): + opts, devices = parseopt() + if opts.help: + print(__doc__.strip()) + return 0 + + if not devices: + devices = select_devices() + else: + devices = [InputDevice(path) for path in devices] + + if opts.capabilities: + for device in devices: + print_capabilities(device) + return 0 + + if opts.grab: + for device in devices: + device.grab() + + # Disable tty echoing if stdin is a tty. + if sys.stdin.isatty(): + toggle_tty_echo(sys.stdin, enable=False) + atexit.register(toggle_tty_echo, sys.stdin, enable=True) + + print("Listening for events (press ctrl-c to exit) ...") + fd_to_device = {dev.fd: dev for dev in devices} + while True: + r, w, e = select.select(fd_to_device, [], []) + + for fd in r: + for event in fd_to_device[fd].read(): + print_event(event) + + +def select_devices(device_dir="/dev/input"): + """ + Select one or more devices from a list of accessible input devices. + """ + + def devicenum(device_path): + digits = re.findall(r"\d+$", device_path) + return [int(i) for i in digits] + + devices = sorted(list_devices(device_dir), key=devicenum) + devices = [InputDevice(path) for path in devices] + if not devices: + msg = "error: no input devices found (do you have rw permission on %s/*?)" + print(msg % device_dir, file=sys.stderr) + sys.exit(1) + + dev_format = "{0:<3} {1.path:<20} {1.name:<35} {1.phys:<35} {1.uniq:<4}" + dev_lines = [dev_format.format(num, dev) for num, dev in enumerate(devices)] + + print("ID {:<20} {:<35} {:<35} {}".format("Device", "Name", "Phys", "Uniq")) + print("-" * len(max(dev_lines, key=len))) + print("\n".join(dev_lines)) + print() + + choices = input("Select devices [0-%s]: " % (len(dev_lines) - 1)) + + try: + choices = choices.split() + choices = [devices[int(num)] for num in choices] + except ValueError: + choices = None + + if not choices: + msg = "error: invalid input - please enter one or more numbers separated by spaces" + print(msg, file=sys.stderr) + sys.exit(1) + + return choices + + +def print_capabilities(device): + capabilities = device.capabilities(verbose=True) + input_props = device.input_props(verbose=True) + + print("Device name: {.name}".format(device)) + print("Device info: {.info}".format(device)) + print("Repeat settings: {}\n".format(device.repeat)) + + if ("EV_LED", ecodes.EV_LED) in capabilities: + leds = ",".join(i[0] for i in device.leds(True)) + print("Active LEDs: %s" % leds) + + active_keys = ",".join(k[0] for k in device.active_keys(True)) + print("Active keys: %s\n" % active_keys) + + if input_props: + print("Input properties:") + for type, code in input_props: + print(" %s %s" % (type, code)) + print() + + print("Device capabilities:") + for type, codes in capabilities.items(): + print(" Type {} {}:".format(*type)) + for code in codes: + # code <- ('BTN_RIGHT', 273) or (['BTN_LEFT', 'BTN_MOUSE'], 272) + if isinstance(code[1], AbsInfo): + print(" Code {:<4} {}:".format(*code[0])) + print(" {}".format(code[1])) + else: + # Multiple names may resolve to one value. + s = ", ".join(code[0]) if isinstance(code[0], list) else code[0] + print(" Code {:<4} {}".format(s, code[1])) + print("") + + +def print_event(e): + if e.type == ecodes.EV_SYN: + if e.code == ecodes.SYN_MT_REPORT: + msg = "time {:<17} +++++++++++++ {} +++++++++++++" + elif e.code == ecodes.SYN_DROPPED: + msg = "time {:<17} !!!!!!!!!!!!! {} !!!!!!!!!!!!!" + else: + msg = "time {:<17} ------------- {} -------------" + print(msg.format(e.timestamp(), ecodes.SYN[e.code])) + else: + if e.type in ecodes.bytype: + codename = ecodes.bytype[e.type][e.code] + else: + codename = "?" + + evfmt = "time {:<17} type {} ({}), code {:<4} ({}), value {}" + print(evfmt.format(e.timestamp(), e.type, ecodes.EV[e.type], e.code, codename, e.value)) + + +def toggle_tty_echo(fh, enable=True): + flags = termios.tcgetattr(fh.fileno()) + if enable: + flags[3] |= termios.ECHO + else: + flags[3] &= ~termios.ECHO + termios.tcsetattr(fh.fileno(), termios.TCSANOW, flags) + + +if __name__ == "__main__": + try: + ret = main() + except (KeyboardInterrupt, EOFError): + ret = 0 + sys.exit(ret) diff --git a/evdev/ff.py b/src/evdev/ff.py similarity index 64% rename from evdev/ff.py rename to src/evdev/ff.py index e4598d7..260c362 100644 --- a/evdev/ff.py +++ b/src/evdev/ff.py @@ -1,42 +1,42 @@ -# encoding: utf-8 - import ctypes -from evdev import ecodes +from . import ecodes -_u8 = ctypes.c_uint8 +_u8 = ctypes.c_uint8 _u16 = ctypes.c_uint16 _u32 = ctypes.c_uint32 _s16 = ctypes.c_int16 +_s32 = ctypes.c_int32 + class Replay(ctypes.Structure): - ''' + """ Defines scheduling of the force-feedback effect @length: duration of the effect @delay: delay before effect should start playing - ''' + """ _fields_ = [ - ('length', _u16), - ('delay', _u16), + ("length", _u16), + ("delay", _u16), ] class Trigger(ctypes.Structure): - ''' + """ Defines what triggers the force-feedback effect @button: number of the button triggering the effect @interval: controls how soon the effect can be re-triggered - ''' + """ _fields_ = [ - ('button', _u16), - ('interval', _u16), + ("button", _u16), + ("interval", _u16), ] class Envelope(ctypes.Structure): - ''' + """ Generic force-feedback effect envelope @attack_length: duration of the attack (ms) @attack_level: level at the beginning of the attack @@ -47,46 +47,46 @@ class Envelope(ctypes.Structure): envelope force-feedback core will convert to positive/negative value based on polarity of the default level of the effect. Valid range for the attack and fade levels is 0x0000 - 0x7fff - ''' + """ _fields_ = [ - ('attach_length', _u16), - ('attack_level', _u16), - ('fade_length', _u16), - ('fade_level', _u16), + ("attack_length", _u16), + ("attack_level", _u16), + ("fade_length", _u16), + ("fade_level", _u16), ] class Constant(ctypes.Structure): - ''' + """ Defines parameters of a constant force-feedback effect @level: strength of the effect; may be negative @envelope: envelope data - ''' + """ _fields_ = [ - ('level', _s16), - ('ff_envelope', Envelope), + ("level", _s16), + ("ff_envelope", Envelope), ] class Ramp(ctypes.Structure): - ''' + """ Defines parameters of a ramp force-feedback effect @start_level: beginning strength of the effect; may be negative @end_level: final strength of the effect; may be negative @envelope: envelope data - ''' + """ _fields_ = [ - ('start_level', _s16), - ('end_level', _s16), - ('ff_envelope', Envelope), + ("start_level", _s16), + ("end_level", _s16), + ("ff_envelope", Envelope), ] class Condition(ctypes.Structure): - ''' + """ Defines a spring or friction force-feedback effect @right_saturation: maximum level when joystick moved all way to the right @left_saturation: same for the left side @@ -94,20 +94,20 @@ class Condition(ctypes.Structure): @left_coeff: same for the left side @deadband: size of the dead zone, where no force is produced @center: position of the dead zone - ''' + """ _fields_ = [ - ('right_saturation', _u16), - ('left_saturation', _u16), - ('right_coeff', _s16), - ('left_foeff', _s16), - ('deadband', _u16), - ('center', _s16), + ("right_saturation", _u16), + ("left_saturation", _u16), + ("right_coeff", _s16), + ("left_coeff", _s16), + ("deadband", _u16), + ("center", _s16), ] class Periodic(ctypes.Structure): - ''' + """ Defines parameters of a periodic force-feedback effect @waveform: kind of the effect (wave) @period: period of the wave (ms) @@ -117,56 +117,74 @@ class Periodic(ctypes.Structure): @envelope: envelope data @custom_len: number of samples (FF_CUSTOM only) @custom_data: buffer of samples (FF_CUSTOM only) - ''' + """ _fields_ = [ - ('waveform', _u16), - ('period', _u16), - ('magnitude', _s16), - ('offset', _s16), - ('phase', _u16), - ('envelope', Envelope), - ('custom_len', _u32), - ('custom_data', ctypes.POINTER(_s16)), + ("waveform", _u16), + ("period", _u16), + ("magnitude", _s16), + ("offset", _s16), + ("phase", _u16), + ("envelope", Envelope), + ("custom_len", _u32), + ("custom_data", ctypes.POINTER(_s16)), ] class Rumble(ctypes.Structure): - ''' + """ Defines parameters of a periodic force-feedback effect @strong_magnitude: magnitude of the heavy motor @weak_magnitude: magnitude of the light one Some rumble pads have two motors of different weight. Strong_magnitude represents the magnitude of the vibration generated by the heavy one. - ''' + """ _fields_ = [ - ('strong_magnitude', _u16), - ('weak_magnitude', _u16), + ("strong_magnitude", _u16), + ("weak_magnitude", _u16), ] class EffectType(ctypes.Union): _fields_ = [ - ('ff_constant_effect', Constant), - ('ff_ramp_effect', Ramp), - ('ff_periodic_effect', Periodic), - ('ff_condition_effect', Condition * 2), # one for each axis - ('ff_rumble_effect', Rumble), + ("ff_constant_effect", Constant), + ("ff_ramp_effect", Ramp), + ("ff_periodic_effect", Periodic), + ("ff_condition_effect", Condition * 2), # one for each axis + ("ff_rumble_effect", Rumble), ] class Effect(ctypes.Structure): _fields_ = [ - ('type', _u16), - ('id', _s16), - ('direction', _u16), - ('ff_trigger', Trigger), - ('ff_replay', Replay), - ('u', EffectType) + ("type", _u16), + ("id", _s16), + ("direction", _u16), + ("ff_trigger", Trigger), + ("ff_replay", Replay), + ("u", EffectType), + ] + + +class UInputUpload(ctypes.Structure): + _fields_ = [ + ("request_id", _u32), + ("retval", _s32), + ("effect", Effect), + ("old", Effect), ] + +class UInputErase(ctypes.Structure): + _fields_ = [ + ("request_id", _u32), + ("retval", _s32), + ("effect_id", _u32), + ] + + # ff_types = { # ecodes.FF_CONSTANT, # ecodes.FF_PERIODIC, diff --git a/src/evdev/genecodes_c.py b/src/evdev/genecodes_c.py new file mode 100644 index 0000000..15a6693 --- /dev/null +++ b/src/evdev/genecodes_c.py @@ -0,0 +1,147 @@ +""" +Generate a Python extension module with the constants defined in linux/input.h. +""" + +import getopt +import os +import re +import sys + +# ----------------------------------------------------------------------------- +# The default header file locations to try. +headers = [ + "/usr/include/linux/input.h", + "/usr/include/linux/input-event-codes.h", + "/usr/include/linux/uinput.h", +] + +opts, args = getopt.getopt(sys.argv[1:], "", ["ecodes", "stubs", "reproducible"]) +if not opts: + print("usage: genecodes.py [--ecodes|--stubs] [--reproducible] ") + exit(2) + +if args: + headers = args + +reproducible = ("--reproducible", "") in opts + + +# ----------------------------------------------------------------------------- +macro_regex = r"#define\s+((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF|UI_FF|INPUT_PROP)_\w+)" +macro_regex = re.compile(macro_regex) + +if reproducible: + uname = "hidden for reproducibility" +else: + # Uname without hostname. + uname = list(os.uname()) + uname = " ".join((uname[0], *uname[2:])) + + +# ----------------------------------------------------------------------------- +template_ecodes = r""" +#include +#ifdef __FreeBSD__ +#include +#include +#else +#include +#include +#endif + +/* Automatically generated by evdev.genecodes */ +/* Generated on %s */ +/* Generated from %s */ + +#define MODULE_NAME "_ecodes" +#define MODULE_HELP "linux/input.h macros" + +static PyMethodDef MethodTable[] = { + { NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + MODULE_NAME, + MODULE_HELP, + -1, /* m_size */ + MethodTable, /* m_methods */ + NULL, /* m_reload */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL, /* m_free */ +}; + +PyMODINIT_FUNC +PyInit__ecodes(void) +{ + PyObject* m = PyModule_Create(&moduledef); + if (m == NULL) return NULL; + +%s + + return m; +} +""" + + +template_stubs = r""" +# Automatically generated by evdev.genecodes +# Generated on %s +# Generated from %s + +# pylint: skip-file + +ecodes: dict[str, int] +keys: dict[int, str|list[str]] +bytype: dict[int, dict[int, str|list[str]]] + +KEY: dict[int, str|list[str]] +ABS: dict[int, str|list[str]] +REL: dict[int, str|list[str]] +SW: dict[int, str|list[str]] +MSC: dict[int, str|list[str]] +LED: dict[int, str|list[str]] +BTN: dict[int, str|list[str]] +REP: dict[int, str|list[str]] +SND: dict[int, str|list[str]] +ID: dict[int, str|list[str]] +EV: dict[int, str|list[str]] +BUS: dict[int, str|list[str]] +SYN: dict[int, str|list[str]] +FF_STATUS: dict[int, str|list[str]] +FF_INPUT_PROP: dict[int, str|list[str]] + +%s +""" + + +def parse_headers(headers=headers): + for header in headers: + try: + fh = open(header) + except (IOError, OSError): + continue + + for line in fh: + macro = macro_regex.search(line) + if macro: + yield macro.group(1) + + +all_macros = list(parse_headers()) +if not all_macros: + print("no input macros found in: %s" % " ".join(headers), file=sys.stderr) + sys.exit(1) + +# pylint: disable=possibly-used-before-assignment, used-before-assignment +if ("--ecodes", "") in opts: + body = (" PyModule_AddIntMacro(m, %s);" % macro for macro in all_macros) + template = template_ecodes +elif ("--stubs", "") in opts: + body = ("%s: int" % macro for macro in all_macros) + template = template_stubs + +body = os.linesep.join(body) +text = template % (uname, headers if not reproducible else ["hidden for reproducibility"], body) +print(text.strip()) diff --git a/src/evdev/genecodes_py.py b/src/evdev/genecodes_py.py new file mode 100644 index 0000000..f00020c --- /dev/null +++ b/src/evdev/genecodes_py.py @@ -0,0 +1,54 @@ +import sys +from unittest import mock +from pprint import PrettyPrinter + +sys.modules["evdev.ecodes"] = mock.Mock() +from evdev import ecodes_runtime as ecodes + +pprint = PrettyPrinter(indent=2, sort_dicts=True, width=120).pprint + + +print("# Automatically generated by evdev.genecodes_py") +print() +print('"""') +print(ecodes.__doc__.strip()) +print('"""') + +print() +print("from typing import Final, Dict, Tuple, Union") +print() + +for name, value in ecodes.ecodes.items(): + print(f"{name}: Final[int] = {value}") +print() + +entries = [ + ("ecodes", "Dict[str, int]", "#: Mapping of names to values."), + ("bytype", "Dict[int, Dict[int, Union[str, Tuple[str]]]]", "#: Mapping of event types to other value/name mappings."), + ("keys", "Dict[int, Union[str, Tuple[str]]]", "#: Keys are a combination of all BTN and KEY codes."), + ("KEY", "Dict[int, Union[str, Tuple[str]]]", None), + ("ABS", "Dict[int, Union[str, Tuple[str]]]", None), + ("REL", "Dict[int, Union[str, Tuple[str]]]", None), + ("SW", "Dict[int, Union[str, Tuple[str]]]", None), + ("MSC", "Dict[int, Union[str, Tuple[str]]]", None), + ("LED", "Dict[int, Union[str, Tuple[str]]]", None), + ("BTN", "Dict[int, Union[str, Tuple[str]]]", None), + ("REP", "Dict[int, Union[str, Tuple[str]]]", None), + ("SND", "Dict[int, Union[str, Tuple[str]]]", None), + ("ID", "Dict[int, Union[str, Tuple[str]]]", None), + ("EV", "Dict[int, Union[str, Tuple[str]]]", None), + ("BUS", "Dict[int, Union[str, Tuple[str]]]", None), + ("SYN", "Dict[int, Union[str, Tuple[str]]]", None), + ("FF", "Dict[int, Union[str, Tuple[str]]]", None), + ("UI_FF", "Dict[int, Union[str, Tuple[str]]]", None), + ("FF_STATUS", "Dict[int, Union[str, Tuple[str]]]", None), + ("INPUT_PROP", "Dict[int, Union[str, Tuple[str]]]", None) +] + +for key, annotation, doc in entries: + if doc: + print(doc) + + print(f"{key}: {annotation} = ", end="") + pprint(getattr(ecodes, key)) + print() \ No newline at end of file diff --git a/evdev/input.c b/src/evdev/input.c similarity index 62% rename from evdev/input.c rename to src/evdev/input.c index 6a62f17..894db22 100644 --- a/evdev/input.c +++ b/src/evdev/input.c @@ -18,7 +18,16 @@ #include #include +#ifdef __FreeBSD__ +#include +#else #include +#endif + +#ifndef input_event_sec +#define input_event_sec time.tv_sec +#define input_event_usec time.tv_usec +#endif #define MAX_NAME_SIZE 256 @@ -37,12 +46,10 @@ int test_bit(const char* bitmask, int bit) { static PyObject * device_read(PyObject *self, PyObject *args) { - int fd; struct input_event event; // get device file descriptor (O_RDONLY|O_NONBLOCK) - if (PyArg_ParseTuple(args, "i", &fd) < 0) - return NULL; + int fd = (int)PyLong_AsLong(PyTuple_GET_ITEM(args, 0)); int n = read(fd, &event, sizeof(event)); @@ -52,19 +59,16 @@ device_read(PyObject *self, PyObject *args) return Py_None; } - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } - PyObject* sec = PyLong_FromLong(event.time.tv_sec); - PyObject* usec = PyLong_FromLong(event.time.tv_usec); - PyObject* val = PyLong_FromLong(event.value); - PyObject* py_input_event = NULL; - - py_input_event = Py_BuildValue("OOhhO", sec, usec, event.type, event.code, val); - Py_DECREF(sec); - Py_DECREF(usec); - Py_DECREF(val); + PyObject *py_input_event = PyTuple_New(5); + PyTuple_SET_ITEM(py_input_event, 0, PyLong_FromLong(event.input_event_sec)); + PyTuple_SET_ITEM(py_input_event, 1, PyLong_FromLong(event.input_event_usec)); + PyTuple_SET_ITEM(py_input_event, 2, PyLong_FromLong(event.type)); + PyTuple_SET_ITEM(py_input_event, 3, PyLong_FromLong(event.code)); + PyTuple_SET_ITEM(py_input_event, 4, PyLong_FromLong(event.value)); return py_input_event; } @@ -74,17 +78,8 @@ device_read(PyObject *self, PyObject *args) static PyObject * device_read_many(PyObject *self, PyObject *args) { - int fd, i; - // get device file descriptor (O_RDONLY|O_NONBLOCK) - int ret = PyArg_ParseTuple(args, "i", &fd); - if (!ret) return NULL; - - PyObject* event_list = PyList_New(0); - PyObject* py_input_event = NULL; - PyObject* sec = NULL; - PyObject* usec = NULL; - PyObject* val = NULL; + int fd = (int)PyLong_AsLong(PyTuple_GET_ITEM(args, 0)); struct input_event event[64]; @@ -92,45 +87,25 @@ device_read_many(PyObject *self, PyObject *args) ssize_t nread = read(fd, event, event_size*64); if (nread < 0) { - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } - // Construct a list of event tuples, which we'll make sense of in Python - for (i = 0 ; i < nread/event_size ; i++) { - sec = PyLong_FromLong(event[i].time.tv_sec); - usec = PyLong_FromLong(event[i].time.tv_usec); - val = PyLong_FromLong(event[i].value); - - py_input_event = Py_BuildValue("OOhhO", sec, usec, event[i].type, event[i].code, val); - PyList_Append(event_list, py_input_event); - - Py_DECREF(py_input_event); - Py_DECREF(sec); - Py_DECREF(usec); - Py_DECREF(val); + // Construct a tuple of event tuples. Each tuple is the arguments to InputEvent. + size_t num_events = nread / event_size; + + PyObject* events = PyTuple_New(num_events); + for (size_t i = 0 ; i < num_events; i++) { + PyObject *py_input_event = PyTuple_New(5); + PyTuple_SET_ITEM(py_input_event, 0, PyLong_FromLong(event[i].input_event_sec)); + PyTuple_SET_ITEM(py_input_event, 1, PyLong_FromLong(event[i].input_event_usec)); + PyTuple_SET_ITEM(py_input_event, 2, PyLong_FromLong(event[i].type)); + PyTuple_SET_ITEM(py_input_event, 3, PyLong_FromLong(event[i].code)); + PyTuple_SET_ITEM(py_input_event, 4, PyLong_FromLong(event[i].value)); + PyTuple_SET_ITEM(events, i, py_input_event); } - return event_list; -} - - -// Unpack a single event (this is essentially a struct.unpack(), without having -// to worry about word size. -static PyObject * -event_unpack(PyObject *self, PyObject *args) -{ - struct input_event event; - - const char *data; - int len; - - int ret = PyArg_ParseTuple(args, "s#", &data, &len); - if (!ret) return NULL; - - memcpy(&event, data, sizeof(event)); - - Py_RETURN_NONE; + return events; } @@ -139,7 +114,7 @@ static PyObject * ioctl_capabilities(PyObject *self, PyObject *args) { int fd, ev_type, ev_code; - char ev_bits[EV_MAX/8], code_bits[KEY_MAX/8]; + char ev_bits[EV_MAX/8 + 1], code_bits[KEY_MAX/8 + 1]; struct input_absinfo absinfo; int ret = PyArg_ParseTuple(args, "i", &fd); @@ -160,7 +135,7 @@ ioctl_capabilities(PyObject *self, PyObject *args) memset(&ev_bits, 0, sizeof(ev_bits)); - if (ioctl(_fd, EVIOCGBIT(0, EV_MAX), ev_bits) < 0) + if (ioctl(_fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) < 0) goto on_err; // Build a dictionary of the device's capabilities @@ -171,7 +146,7 @@ ioctl_capabilities(PyObject *self, PyObject *args) eventcodes = PyList_New(0); memset(&code_bits, 0, sizeof(code_bits)); - ioctl(_fd, EVIOCGBIT(ev_type, KEY_MAX), code_bits); + ioctl(_fd, EVIOCGBIT(ev_type, sizeof(code_bits)), code_bits); for (ev_code = 0; ev_code < KEY_MAX; ev_code++) { if (test_bit(code_bits, ev_code)) { @@ -217,7 +192,12 @@ ioctl_capabilities(PyObject *self, PyObject *args) return capabilities; on_err: - PyErr_SetFromErrno(PyExc_IOError); + Py_XDECREF(capabilities); + Py_XDECREF(eventcodes); + Py_XDECREF(capability); + Py_XDECREF(py_absinfo); + Py_XDECREF(absitem); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -231,6 +211,7 @@ ioctl_devinfo(PyObject *self, PyObject *args) struct input_id iid; char name[MAX_NAME_SIZE]; char phys[MAX_NAME_SIZE] = {0}; + char uniq[MAX_NAME_SIZE] = {0}; int ret = PyArg_ParseTuple(args, "i", &fd); if (!ret) return NULL; @@ -243,12 +224,73 @@ ioctl_devinfo(PyObject *self, PyObject *args) // Some devices do not have a physical topology associated with them ioctl(fd, EVIOCGPHYS(sizeof(phys)), phys); - return Py_BuildValue("hhhhss", iid.bustype, iid.vendor, iid.product, iid.version, - name, phys); + // Some kernels have started reporting bluetooth controller MACs as phys. + // This lets us get the real physical address. As with phys, it may be blank. + ioctl(fd, EVIOCGUNIQ(sizeof(uniq)), uniq); + + return Py_BuildValue("hhhhsss", iid.bustype, iid.vendor, iid.product, iid.version, + name, phys, uniq); on_err: - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} + + +static PyObject * +ioctl_EVIOCGABS(PyObject *self, PyObject *args) +{ + int fd, ev_code; + struct input_absinfo absinfo; + PyObject* py_absinfo = NULL; + + int ret = PyArg_ParseTuple(args, "ii", &fd, &ev_code); + if (!ret) return NULL; + + memset(&absinfo, 0, sizeof(absinfo)); + ret = ioctl(fd, EVIOCGABS(ev_code), &absinfo); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_OSError); return NULL; + } + + py_absinfo = Py_BuildValue("(iiiiii)", + absinfo.value, + absinfo.minimum, + absinfo.maximum, + absinfo.fuzz, + absinfo.flat, + absinfo.resolution); + return py_absinfo; +} + + +static PyObject * +ioctl_EVIOCSABS(PyObject *self, PyObject *args) +{ + int fd, ev_code; + struct input_absinfo absinfo; + + int ret = PyArg_ParseTuple(args, + "ii(iiiiii)", + &fd, + &ev_code, + &absinfo.value, + &absinfo.minimum, + &absinfo.maximum, + &absinfo.fuzz, + &absinfo.flat, + &absinfo.resolution); + if (!ret) return NULL; + + ret = ioctl(fd, EVIOCSABS(ev_code), &absinfo); + if (ret == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + Py_INCREF(Py_None); + return Py_None; } @@ -256,12 +298,15 @@ static PyObject * ioctl_EVIOCGREP(PyObject *self, PyObject *args) { int fd, ret; - unsigned int rep[2] = {0}; + unsigned int rep[REP_CNT] = {0}; ret = PyArg_ParseTuple(args, "i", &fd); if (!ret) return NULL; - ioctl(fd, EVIOCGREP, &rep); - return Py_BuildValue("(ii)", rep[0], rep[1]); + ret = ioctl(fd, EVIOCGREP, &rep); + if (ret == -1) + return NULL; + + return Py_BuildValue("(ii)", rep[REP_DELAY], rep[REP_PERIOD]); } @@ -269,12 +314,15 @@ static PyObject * ioctl_EVIOCSREP(PyObject *self, PyObject *args) { int fd, ret; - unsigned int rep[2] = {0}; + unsigned int rep[REP_CNT] = {0}; ret = PyArg_ParseTuple(args, "iii", &fd, &rep[0], &rep[1]); if (!ret) return NULL; ret = ioctl(fd, EVIOCSREP, &rep); + if (ret == -1) + return NULL; + return Py_BuildValue("i", ret); } @@ -287,6 +335,9 @@ ioctl_EVIOCGVERSION(PyObject *self, PyObject *args) if (!ret) return NULL; ret = ioctl(fd, EVIOCGVERSION, &res); + if (ret == -1) + return NULL; + return Py_BuildValue("i", res); } @@ -300,7 +351,7 @@ ioctl_EVIOCGRAB(PyObject *self, PyObject *args) ret = ioctl(fd, EVIOCGRAB, (intptr_t)flag); if (ret != 0) { - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -309,38 +360,54 @@ ioctl_EVIOCGRAB(PyObject *self, PyObject *args) } -// todo: this function needs a better name static PyObject * -get_sw_led_snd(PyObject *self, PyObject *args) +ioctl_EVIOCG_bits(PyObject *self, PyObject *args) { - int i, max, fd, evtype, ret; - PyObject* res = PyList_New(0); + int max, fd, evtype, ret; ret = PyArg_ParseTuple(args, "ii", &fd, &evtype); if (!ret) return NULL; - if (evtype == EV_LED) - max = LED_MAX; - else if (evtype == EV_SW) - max = SW_MAX; - else if (evtype == EV_SND) - max = SND_MAX; - else + switch (evtype) { + case EV_LED: + max = LED_MAX; break; + case EV_SND: + max = SND_MAX; break; + case EV_KEY: + max = KEY_MAX; break; + case EV_SW: + max = SW_MAX; break; + default: return NULL; + } char bytes[(max+7)/8]; memset(bytes, 0, sizeof bytes); - if (evtype == EV_LED) + switch (evtype) { + case EV_LED: ret = ioctl(fd, EVIOCGLED(sizeof(bytes)), &bytes); - else if (evtype == EV_SW) - ret = ioctl(fd, EVIOCGSW(sizeof(bytes)), &bytes); - else if (evtype == EV_SND) + break; + case EV_SND: ret = ioctl(fd, EVIOCGSND(sizeof(bytes)), &bytes); + break; + case EV_KEY: + ret = ioctl(fd, EVIOCGKEY(sizeof(bytes)), &bytes); + break; + case EV_SW: + ret = ioctl(fd, EVIOCGSW(sizeof(bytes)), &bytes); + break; + } - for (i=0 ; iu.constant.envelope.fade_length, effect->u.constant.envelope.fade_level); break; + case FF_RUMBLE: + fprintf(stderr, " rumble: (%d, %d)\n", + effect->u.rumble.strong_magnitude, + effect->u.rumble.weak_magnitude); + break; } } @@ -401,7 +476,7 @@ upload_effect(PyObject *self, PyObject *args) ret = ioctl(fd, EVIOCSFF, &effect); if (ret != 0) { - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -420,7 +495,7 @@ erase_effect(PyObject *self, PyObject *args) long ff_id = PyLong_AsLong(ff_id_obj); ret = ioctl(fd, EVIOCRMFF, ff_id); if (ret != 0) { - PyErr_SetFromErrno(PyExc_IOError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -428,17 +503,47 @@ erase_effect(PyObject *self, PyObject *args) return Py_None; } +static PyObject * +ioctl_EVIOCGPROP(PyObject *self, PyObject *args) +{ + int fd, ret; + + ret = PyArg_ParseTuple(args, "i", &fd); + if (!ret) return NULL; + + char bytes[(INPUT_PROP_MAX+7)/8]; + memset(bytes, 0, sizeof bytes); + + ret = ioctl(fd, EVIOCGPROP(sizeof(bytes)), &bytes); + + if (ret == -1) + return NULL; + + PyObject* res = PyList_New(0); + for (int i=0; i= 3 static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, - MODULE_NAME, - MODULE_HELP, + "_input", + "Python bindings to certain linux input subsystem functions", -1, /* m_size */ MethodTable, /* m_methods */ NULL, /* m_reload */ @@ -477,19 +578,3 @@ PyInit__input(void) { return moduleinit(); } - -#else -static PyObject * -moduleinit(void) -{ - PyObject* m = Py_InitModule3(MODULE_NAME, MethodTable, MODULE_HELP); - if (m == NULL) return NULL; - return m; -} - -PyMODINIT_FUNC -init_input(void) -{ - moduleinit(); -} -#endif diff --git a/src/evdev/py.typed b/src/evdev/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/evdev/uinput.c b/src/evdev/uinput.c new file mode 100644 index 0000000..8d2c096 --- /dev/null +++ b/src/evdev/uinput.c @@ -0,0 +1,417 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __FreeBSD__ +#include +#include +#else +#include +#include +#endif + +#ifndef input_event_sec +#define input_event_sec time.tv_sec +#define input_event_usec time.tv_usec +#endif + +// Workaround for installing on kernels newer than 4.4. +#ifndef FF_MAX_EFFECTS +#define FF_MAX_EFFECTS FF_GAIN; +#endif + +int _uinput_close(int fd) +{ + if (ioctl(fd, UI_DEV_DESTROY) < 0) { + int oerrno = errno; + close(fd); + errno = oerrno; + return -1; + } + + return close(fd); +} + + +static PyObject * +uinput_open(PyObject *self, PyObject *args) +{ + const char* devnode; + + int ret = PyArg_ParseTuple(args, "s", &devnode); + if (!ret) return NULL; + + int fd = open(devnode, O_RDWR | O_NONBLOCK); + if (fd < 0) { + PyErr_SetString(PyExc_OSError, "could not open uinput device in write mode"); + return NULL; + } + + return Py_BuildValue("i", fd); +} + + +static PyObject * +uinput_set_phys(PyObject *self, PyObject *args) +{ + int fd; + const char* phys; + + int ret = PyArg_ParseTuple(args, "is", &fd, &phys); + if (!ret) return NULL; + + if (ioctl(fd, UI_SET_PHYS, phys) < 0) + goto on_err; + + Py_RETURN_NONE; + + on_err: + _uinput_close(fd); + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} + +static PyObject * +uinput_set_prop(PyObject *self, PyObject *args) +{ + int fd; + uint16_t prop; + + int ret = PyArg_ParseTuple(args, "ih", &fd, &prop); + if (!ret) return NULL; + + if (ioctl(fd, UI_SET_PROPBIT, prop) < 0) + goto on_err; + + Py_RETURN_NONE; + + on_err: + _uinput_close(fd); + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} + +static PyObject * +uinput_get_sysname(PyObject *self, PyObject *args) +{ + int fd; + char sysname[64]; + + int ret = PyArg_ParseTuple(args, "i", &fd); + if (!ret) return NULL; + + #ifdef UI_GET_SYSNAME + if (ioctl(fd, UI_GET_SYSNAME(sizeof(sysname)), &sysname) < 0) + goto on_err; + + return Py_BuildValue("s", &sysname); + #endif + + on_err: + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} + +// Different kernel versions have different device setup methods. You can read +// more about it here: +// https://github.com/torvalds/linux/commit/052876f8e5aec887d22c4d06e54aa5531ffcec75 + +// Setup function for kernel >= v4.5 +#if defined(UI_DEV_SETUP) && defined(UI_ABS_SETUP) +static PyObject * +uinput_setup(PyObject *self, PyObject *args) { + int fd, len, i; + uint16_t vendor, product, version, bustype; + uint32_t max_effects; + + PyObject *absinfo = NULL, *item = NULL; + + struct uinput_abs_setup abs_setup; + + const char* name; + int ret = PyArg_ParseTuple(args, "isHHHHOI", &fd, &name, &vendor, + &product, &version, &bustype, &absinfo, &max_effects); + if (!ret) return NULL; + + // Setup absinfo: + len = PyList_Size(absinfo); + for (i=0; i (ABS_X, 0, 255, 0, 0, 0, 0) + item = PyList_GetItem(absinfo, i); + + memset(&abs_setup, 0, sizeof(abs_setup)); // Clear struct + abs_setup.code = PyLong_AsLong(PyList_GetItem(item, 0)); + abs_setup.absinfo.value = PyLong_AsLong(PyList_GetItem(item, 1)); + abs_setup.absinfo.minimum = PyLong_AsLong(PyList_GetItem(item, 2)); + abs_setup.absinfo.maximum = PyLong_AsLong(PyList_GetItem(item, 3)); + abs_setup.absinfo.fuzz = PyLong_AsLong(PyList_GetItem(item, 4)); + abs_setup.absinfo.flat = PyLong_AsLong(PyList_GetItem(item, 5)); + abs_setup.absinfo.resolution = PyLong_AsLong(PyList_GetItem(item, 6)); + + if(ioctl(fd, UI_ABS_SETUP, &abs_setup) < 0) + goto on_err; + } + + // Setup evdev: + struct uinput_setup usetup; + + memset(&usetup, 0, sizeof(usetup)); + strncpy(usetup.name, name, sizeof(usetup.name) - 1); + usetup.id.vendor = vendor; + usetup.id.product = product; + usetup.id.version = version; + usetup.id.bustype = bustype; + usetup.ff_effects_max = max_effects; + + if(ioctl(fd, UI_DEV_SETUP, &usetup) < 0) + goto on_err; + + Py_RETURN_NONE; + + on_err: + _uinput_close(fd); + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} + +// Fallback setup function (Linux <= 4.5 and FreeBSD). +#else +static PyObject * +uinput_setup(PyObject *self, PyObject *args) { + int fd, len, i, abscode; + uint16_t vendor, product, version, bustype; + uint32_t max_effects; + + PyObject *absinfo = NULL, *item = NULL; + + struct uinput_user_dev uidev; + const char* name; + + int ret = PyArg_ParseTuple(args, "isHHHHOI", &fd, &name, &vendor, + &product, &version, &bustype, &absinfo, &max_effects); + if (!ret) return NULL; + + memset(&uidev, 0, sizeof(uidev)); + strncpy(uidev.name, name, sizeof(uidev.name) - 1); + uidev.id.vendor = vendor; + uidev.id.product = product; + uidev.id.version = version; + uidev.id.bustype = bustype; + uidev.ff_effects_max = max_effects; + + len = PyList_Size(absinfo); + for (i=0; i (ABS_X, 0, 255, 0, 0, 0, 0) + item = PyList_GetItem(absinfo, i); + abscode = (int)PyLong_AsLong(PyList_GetItem(item, 0)); + + /* min/max/fuzz/flat start from index 2 because index 1 is value */ + uidev.absmin[abscode] = PyLong_AsLong(PyList_GetItem(item, 2)); + uidev.absmax[abscode] = PyLong_AsLong(PyList_GetItem(item, 3)); + uidev.absfuzz[abscode] = PyLong_AsLong(PyList_GetItem(item, 4)); + uidev.absflat[abscode] = PyLong_AsLong(PyList_GetItem(item, 5)); + } + + if (write(fd, &uidev, sizeof(uidev)) != sizeof(uidev)) + goto on_err; + + Py_RETURN_NONE; + + on_err: + _uinput_close(fd); + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} +#endif + + +static PyObject * +uinput_create(PyObject *self, PyObject *args) +{ + int fd; + + int ret = PyArg_ParseTuple(args, "i", &fd); + if (!ret) return NULL; + + if (ioctl(fd, UI_DEV_CREATE) < 0) + goto on_err; + + Py_RETURN_NONE; + + on_err: + _uinput_close(fd); + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} + + +static PyObject * +uinput_close(PyObject *self, PyObject *args) +{ + int fd; + + int ret = PyArg_ParseTuple(args, "i", &fd); + if (!ret) return NULL; + + if (_uinput_close(fd) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + Py_RETURN_NONE; +} + + +static PyObject * +uinput_write(PyObject *self, PyObject *args) +{ + int fd, type, code, value; + + int ret = PyArg_ParseTuple(args, "iiii", &fd, &type, &code, &value); + if (!ret) return NULL; + + struct input_event event; + struct timeval tval; + memset(&event, 0, sizeof(event)); + gettimeofday(&tval, 0); + event.input_event_usec = tval.tv_usec; + event.input_event_sec = tval.tv_sec; + event.type = type; + event.code = code; + event.value = value; + + if (write(fd, &event, sizeof(event)) != sizeof(event)) { + // @todo: elaborate + // PyErr_SetString(PyExc_OSError, "error writing event to uinput device"); + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + Py_RETURN_NONE; +} + + +static PyObject * +uinput_enable_event(PyObject *self, PyObject *args) +{ + int fd; + uint16_t type, code; + unsigned long req; + + int ret = PyArg_ParseTuple(args, "ihh", &fd, &type, &code); + if (!ret) return NULL; + + switch (type) { + case EV_KEY: req = UI_SET_KEYBIT; break; + case EV_ABS: req = UI_SET_ABSBIT; break; + case EV_REL: req = UI_SET_RELBIT; break; + case EV_MSC: req = UI_SET_MSCBIT; break; + case EV_SW: req = UI_SET_SWBIT; break; + case EV_LED: req = UI_SET_LEDBIT; break; + case EV_FF: req = UI_SET_FFBIT; break; + case EV_SND: req = UI_SET_SNDBIT; break; + default: + errno = EINVAL; + goto on_err; + } + + if (ioctl(fd, UI_SET_EVBIT, type) < 0) + goto on_err; + + if (ioctl(fd, req, code) < 0) + goto on_err; + + Py_RETURN_NONE; + + on_err: + _uinput_close(fd); + PyErr_SetFromErrno(PyExc_OSError); + return NULL; +} + +int _uinput_begin_upload(int fd, struct uinput_ff_upload *upload) +{ + return ioctl(fd, UI_BEGIN_FF_UPLOAD, upload); +} + +int _uinput_end_upload(int fd, struct uinput_ff_upload *upload) +{ + return ioctl(fd, UI_END_FF_UPLOAD, upload); +} + +int _uinput_begin_erase(int fd, struct uinput_ff_erase *upload) +{ + return ioctl(fd, UI_BEGIN_FF_ERASE, upload); +} + +int _uinput_end_erase(int fd, struct uinput_ff_erase *upload) +{ + return ioctl(fd, UI_END_FF_ERASE, upload); +} + + +static PyMethodDef MethodTable[] = { + { "open", uinput_open, METH_VARARGS, + "Open uinput device node."}, + + { "setup", uinput_setup, METH_VARARGS, + "Set an uinput device up."}, + + { "create", uinput_create, METH_VARARGS, + "Create an uinput device."}, + + { "close", uinput_close, METH_VARARGS, + "Destroy uinput device."}, + + { "write", uinput_write, METH_VARARGS, + "Write event to uinput device."}, + + { "enable", uinput_enable_event, METH_VARARGS, + "Enable a type of event."}, + + { "set_phys", uinput_set_phys, METH_VARARGS, + "Set physical path"}, + + { "get_sysname", uinput_get_sysname, METH_VARARGS, + "Obtain the sysname of the uinput device."}, + + { "set_prop", uinput_set_prop, METH_VARARGS, + "Set device input property"}, + + { NULL, NULL, 0, NULL} +}; + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_uinput", + "Python bindings for parts of linux/uinput.c", + -1, /* m_size */ + MethodTable, /* m_methods */ + NULL, /* m_reload */ + NULL, /* m_traverse */ + NULL, /* m_clear */ + NULL, /* m_free */ +}; + +static PyObject * +moduleinit(void) +{ + PyObject* m = PyModule_Create(&moduledef); + if (m == NULL) return NULL; + + PyModule_AddIntConstant(m, "maxnamelen", UINPUT_MAX_NAME_SIZE); + return m; +} + +PyMODINIT_FUNC +PyInit__uinput(void) +{ + return moduleinit(); +} diff --git a/src/evdev/uinput.py b/src/evdev/uinput.py new file mode 100644 index 0000000..2c69c2b --- /dev/null +++ b/src/evdev/uinput.py @@ -0,0 +1,375 @@ +import ctypes +import os +import platform +import re +import stat +import time +from collections import defaultdict +from typing import Union, Tuple, Dict, Sequence, Optional + +from . import _uinput, ecodes, ff, util +from .device import InputDevice, AbsInfo +from .events import InputEvent + +try: + from evdev.eventio_async import EventIO +except ImportError: + from evdev.eventio import EventIO + + +class UInputError(Exception): + pass + + +class UInput(EventIO): + """ + A userland input device and that can inject input events into the + linux input subsystem. + """ + + __slots__ = ( + "name", + "vendor", + "product", + "version", + "bustype", + "events", + "devnode", + "fd", + "device", + ) + + @classmethod + def from_device( + cls, + *devices: Union[InputDevice, Union[str, bytes, os.PathLike]], + filtered_types: Tuple[int] = (ecodes.EV_SYN, ecodes.EV_FF), + **kwargs, + ): + """ + Create an UInput device with the capabilities of one or more input + devices. + + Arguments + --------- + devices : InputDevice|str + Varargs of InputDevice instances or paths to input devices. + + filtered_types : Tuple[event type codes] + Event types to exclude from the capabilities of the uinput device. + + **kwargs + Keyword arguments to UInput constructor (i.e. name, vendor etc.). + """ + + device_instances = [] + for dev in devices: + if not isinstance(dev, InputDevice): + dev = InputDevice(str(dev)) + device_instances.append(dev) + + all_capabilities = defaultdict(set) + + if "max_effects" not in kwargs: + kwargs["max_effects"] = min([dev.ff_effects_count for dev in device_instances]) + + # Merge the capabilities of all devices into one dictionary. + for dev in device_instances: + for ev_type, ev_codes in dev.capabilities().items(): + all_capabilities[ev_type].update(ev_codes) + + for evtype in filtered_types: + if evtype in all_capabilities: + del all_capabilities[evtype] + + return cls(events=all_capabilities, **kwargs) + + def __init__( + self, + events: Optional[Dict[int, Sequence[int]]] = None, + name: str = "py-evdev-uinput", + vendor: int = 0x1, + product: int = 0x1, + version: int = 0x1, + bustype: int = 0x3, + devnode: str = "/dev/uinput", + phys: str = "py-evdev-uinput", + input_props=None, + # CentOS 7 has sufficiently old headers that FF_MAX_EFFECTS is not defined there, + # which causes the whole module to fail loading. Fallback on a hardcoded value of + # FF_MAX_EFFECTS if it is not defined in the ecodes. + max_effects=ecodes.ecodes.get("FF_MAX_EFFECTS", 96), + ): + """ + Arguments + --------- + events : dict + Dictionary of event types mapping to lists of event codes. The + event types and codes that the uinput device will be able to + inject - defaults to all key codes. + + name + The name of the input device. + + vendor + Vendor identifier. + + product + Product identifier. + + version + Version identifier. + + bustype + Bustype identifier. + + phys + Physical path. + + input_props + Input properties and quirks. + + max_effects + Maximum simultaneous force-feedback effects. + + Note + ---- + If you do not specify any events, the uinput device will be able + to inject only ``KEY_*`` and ``BTN_*`` event codes. + """ + + self.name: str = name #: Uinput device name. + self.vendor: int = vendor #: Device vendor identifier. + self.product: int = product #: Device product identifier. + self.version: int = version #: Device version identifier. + self.bustype: int = bustype #: Device bustype - e.g. ``BUS_USB``. + self.phys: str = phys #: Uinput device physical path. + self.devnode: str = devnode #: Uinput device node - e.g. ``/dev/uinput/``. + + if not events: + events = {ecodes.EV_KEY: ecodes.keys.keys()} + + self._verify() + + #: Write-only, non-blocking file descriptor to the uinput device node. + self.fd = _uinput.open(devnode) + + # Prepare the list of events for passing to _uinput.enable and _uinput.setup. + absinfo, prepared_events = self._prepare_events(events) + + # Set phys name + _uinput.set_phys(self.fd, phys) + + # Set properties + input_props = input_props or [] + for prop in input_props: + _uinput.set_prop(self.fd, prop) + + for etype, code in prepared_events: + _uinput.enable(self.fd, etype, code) + + _uinput.setup(self.fd, name, vendor, product, version, bustype, absinfo, max_effects) + + # Create the uinput device. + _uinput.create(self.fd) + + self.dll = ctypes.CDLL(_uinput.__file__) + self.dll._uinput_begin_upload.restype = ctypes.c_int + self.dll._uinput_end_upload.restype = ctypes.c_int + + #: An :class:`InputDevice ` instance + #: for the fake input device. ``None`` if the device cannot be + #: opened for reading and writing. + self.device: InputDevice = self._find_device(self.fd) + + def _prepare_events(self, events): + """Prepare events for passing to _uinput.enable and _uinput.setup""" + absinfo, prepared_events = [], [] + for etype, codes in events.items(): + for code in codes: + # Handle max, min, fuzz, flat. + if isinstance(code, (tuple, list, AbsInfo)): + # Flatten (ABS_Y, (0, 255, 0, 0, 0, 0)) to (ABS_Y, 0, 255, 0, 0, 0, 0). + f = [code[0]] + f.extend(code[1]) + # Ensure the tuple is always 6 ints long, since uinput.c:uinput_create + # does little in the way of checking the length. + f.extend([0] * (6 - len(code[1]))) + absinfo.append(f) + code = code[0] + prepared_events.append((etype, code)) + return absinfo, prepared_events + + def __enter__(self): + return self + + def __exit__(self, type, value, tb): + if hasattr(self, "fd"): + self.close() + + def __repr__(self): + # TODO: + v = (repr(getattr(self, i)) for i in ("name", "bustype", "vendor", "product", "version", "phys")) + return "{}({})".format(self.__class__.__name__, ", ".join(v)) + + def __str__(self): + msg = 'name "{}", bus "{}", vendor "{:04x}", product "{:04x}", version "{:04x}", phys "{}"\nevent types: {}' + + evtypes = [i[0] for i in self.capabilities(True).keys()] + msg = msg.format( + self.name, ecodes.BUS[self.bustype], self.vendor, self.product, self.version, self.phys, " ".join(evtypes) + ) + + return msg + + def close(self): + # Close the associated InputDevice, if it was previously opened. + if self.device is not None: + self.device.close() + + # Destroy the uinput device. + if self.fd > -1: + _uinput.close(self.fd) + self.fd = -1 + + def capabilities(self, verbose: bool = False, absinfo: bool = True): + """See :func:`capabilities `.""" + if self.device is None: + raise UInputError("input device not opened - cannot read capabilities") + + return self.device.capabilities(verbose, absinfo) + + def begin_upload(self, effect_id): + upload = ff.UInputUpload() + upload.effect_id = effect_id + + ret = self.dll._uinput_begin_upload(self.fd, ctypes.byref(upload)) + if ret: + raise UInputError("Failed to begin uinput upload: " + os.strerror(ret)) + + return upload + + def end_upload(self, upload): + ret = self.dll._uinput_end_upload(self.fd, ctypes.byref(upload)) + if ret: + raise UInputError("Failed to end uinput upload: " + os.strerror(ret)) + + def begin_erase(self, effect_id): + erase = ff.UInputErase() + erase.effect_id = effect_id + + ret = self.dll._uinput_begin_erase(self.fd, ctypes.byref(erase)) + if ret: + raise UInputError("Failed to begin uinput erase: " + os.strerror(ret)) + return erase + + def end_erase(self, erase): + ret = self.dll._uinput_end_erase(self.fd, ctypes.byref(erase)) + if ret: + raise UInputError("Failed to end uinput erase: " + os.strerror(ret)) + + def _verify(self): + """ + Verify that an uinput device exists and is readable and writable + by the current process. + """ + try: + m = os.stat(self.devnode)[stat.ST_MODE] + assert stat.S_ISCHR(m) + except (IndexError, OSError, AssertionError): + msg = '"{}" does not exist or is not a character device file - verify that the uinput module is loaded' + raise UInputError(msg.format(self.devnode)) + + if not os.access(self.devnode, os.W_OK): + msg = '"{}" cannot be opened for writing' + raise UInputError(msg.format(self.devnode)) + + if len(self.name) > _uinput.maxnamelen: + msg = "uinput device name must not be longer than {} characters" + raise UInputError(msg.format(_uinput.maxnamelen)) + + def _find_device(self, fd: int) -> InputDevice: + """ + Tries to find the device node. Will delegate this task to one of + several platform-specific functions. + """ + if platform.system() == "Linux": + try: + sysname = _uinput.get_sysname(fd) + return self._find_device_linux(sysname) + except OSError: + # UI_GET_SYSNAME returned an error code. We're likely dealing with + # an old kernel. Guess the device based on the filesystem. + pass + + # If we're not running or Linux or the above method fails for any reason, + # use the generic fallback method. + return self._find_device_fallback() + + def _find_device_linux(self, sysname: str) -> InputDevice: + """ + Tries to find the device node when running on Linux. + """ + + syspath = f"/sys/devices/virtual/input/{sysname}" + + # The sysfs entry for event devices should contain exactly one folder + # whose name matches the format "event[0-9]+". It is then assumed that + # the device node in /dev/input uses the same name. + regex = re.compile("event[0-9]+") + for entry in os.listdir(syspath): + if regex.fullmatch(entry): + device_path = f"/dev/input/{entry}" + break + else: # no break + raise FileNotFoundError() + + # It is possible that there is some delay before /dev/input/event* shows + # up on old systems that do not use devtmpfs, so if the device cannot be + # found, wait for a short amount and then try again once. + # + # Furthermore, even if devtmpfs is in use, it is possible that the device + # does show up immediately, but without the correct permissions that + # still need to be set by udev. Wait for up to two seconds for either the + # device to show up or the permissions to be set. + for attempt in range(19): + try: + return InputDevice(device_path) + except (FileNotFoundError, PermissionError): + time.sleep(0.1) + + # Last attempt. If this fails, whatever exception the last attempt raises + # shall be the exception that this function raises. + return InputDevice(device_path) + + def _find_device_fallback(self) -> Union[InputDevice, None]: + """ + Tries to find the device node when UI_GET_SYSNAME is not available or + we're running on a system sufficiently exotic that we do not know how + to interpret its return value. + """ + #:bug: the device node might not be immediately available + time.sleep(0.1) + + # There could also be another device with the same name already present, + # make sure to select the newest one. + # Strictly speaking, we cannot be certain that everything returned by list_devices() + # ends at event[0-9]+: it might return something like "/dev/input/events_all". Find + # the devices that have the expected structure and extract their device number. + path_number_pairs = [] + regex = re.compile("/dev/input/event([0-9]+)") + for path in util.list_devices("/dev/input/"): + regex_match = regex.fullmatch(path) + if not regex_match: + continue + device_number = int(regex_match[1]) + path_number_pairs.append((path, device_number)) + + # The modification date of the devnode is not reliable unfortunately, so we + # are sorting by the number in the name + path_number_pairs.sort(key=lambda pair: pair[1], reverse=True) + + for path, _ in path_number_pairs: + d = InputDevice(path) + if d.name == self.name: + return d diff --git a/src/evdev/util.py b/src/evdev/util.py new file mode 100644 index 0000000..db89a22 --- /dev/null +++ b/src/evdev/util.py @@ -0,0 +1,146 @@ +import collections +import glob +import os +import re +import stat +from typing import Union, List + +from . import ecodes +from .events import InputEvent, event_factory, KeyEvent, RelEvent, AbsEvent, SynEvent + + +def list_devices(input_device_dir: Union[str, bytes, os.PathLike] = "/dev/input") -> List[str]: + """List readable character devices in ``input_device_dir``.""" + + fns = glob.glob("{}/event*".format(input_device_dir)) + return list(filter(is_device, fns)) + + +def is_device(fn: Union[str, bytes, os.PathLike]) -> bool: + """Check if ``fn`` is a readable and writable character device.""" + + if not os.path.exists(fn): + return False + + m = os.stat(fn)[stat.ST_MODE] + if not stat.S_ISCHR(m): + return False + + if not os.access(fn, os.R_OK | os.W_OK): + return False + + return True + + +def categorize(event: InputEvent) -> Union[InputEvent, KeyEvent, RelEvent, AbsEvent, SynEvent]: + """ + Categorize an event according to its type. + + The :data:`event_factory ` dictionary + maps event types to sub-classes of :class:`InputEvent + `. If the event cannot be categorized, it + is returned unmodified.""" + + if event.type in event_factory: + return event_factory[event.type](event) + else: + return event + + +def resolve_ecodes_dict(typecodemap, unknown="?"): + """ + Resolve event codes and types to their verbose names. + + :param typecodemap: mapping of event types to lists of event codes. + :param unknown: symbol to which unknown types or codes will be resolved. + + Example + ------- + >>> resolve_ecodes_dict({ 1: [272, 273, 274] }) + { ('EV_KEY', 1): [('BTN_MOUSE', 272), + ('BTN_RIGHT', 273), + ('BTN_MIDDLE', 274)] } + + If ``typecodemap`` contains absolute axis info (instances of + :class:`AbsInfo ` ) the result would look + like: + + >>> resolve_ecodes_dict({ 3: [(0, AbsInfo(...))] }) + { ('EV_ABS', 3L): [(('ABS_X', 0L), AbsInfo(...))] } + """ + + for etype, codes in typecodemap.items(): + type_name = ecodes.EV[etype] + + # ecodes.keys are a combination of KEY_ and BTN_ codes + if etype == ecodes.EV_KEY: + ecode_dict = ecodes.keys + else: + ecode_dict = getattr(ecodes, type_name.split("_")[-1]) + + resolved = resolve_ecodes(ecode_dict, codes, unknown) + yield (type_name, etype), resolved + + +def resolve_ecodes(ecode_dict, ecode_list, unknown="?"): + """ + Resolve event codes and types to their verbose names. + + Example + ------- + >>> resolve_ecodes(ecodes.BTN, [272, 273, 274]) + [(['BTN_LEFT', 'BTN_MOUSE'], 272), ('BTN_RIGHT', 273), ('BTN_MIDDLE', 274)] + """ + res = [] + for ecode in ecode_list: + # elements with AbsInfo(), eg { 3 : [(0, AbsInfo(...)), (1, AbsInfo(...))] } + if isinstance(ecode, tuple): + if ecode[0] in ecode_dict: + l = ((ecode_dict[ecode[0]], ecode[0]), ecode[1]) + else: + l = ((unknown, ecode[0]), ecode[1]) + + # just ecodes, e.g: { 0 : [0, 1, 3], 1 : [30, 48] } + else: + if ecode in ecode_dict: + l = (ecode_dict[ecode], ecode) + else: + l = (unknown, ecode) + res.append(l) + + return res + + +def find_ecodes_by_regex(regex): + """ + Find ecodes matching a regex and return a mapping of event type to event codes. + + regex can be a pattern string or a compiled regular expression object. + + Example + ------- + >>> find_ecodes_by_regex(r'(ABS|KEY)_BR(AKE|EAK)') + {1: [411], 3: [10]} + >>> res = find_ecodes_by_regex(r'(ABS|KEY)_BR(AKE|EAK)') + >>> resolve_ecodes_dict(res) + { + ('EV_KEY', 1): [('KEY_BREAK', 411)], + ('EV_ABS', 3): [('ABS_BRAKE', 10)] + } + """ + + regex = re.compile(regex) # re.compile is idempotent + result = collections.defaultdict(list) + + for type_code, codes in ecodes.bytype.items(): + for code, names in codes.items(): + names = (names,) if isinstance(names, str) else names + for name in names: + if regex.match(name): + result[type_code].append(code) + break + + return dict(result) + + +__all__ = ("list_devices", "is_device", "categorize", "resolve_ecodes", "resolve_ecodes_dict", "find_ecodes_by_regex") diff --git a/tests/test_ecodes.py b/tests/test_ecodes.py index 622a06f..5c3e38d 100644 --- a/tests/test_ecodes.py +++ b/tests/test_ecodes.py @@ -1,12 +1,14 @@ -# encoding: utf-8 - from evdev import ecodes +from evdev import ecodes_runtime + + +prefixes = "KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF UI_FF" -prefixes = 'KEY ABS REL SW MSC LED BTN REP SND ID EV BUS SYN FF_STATUS FF' -def to_tuples(l): +def to_tuples(val): t = lambda x: tuple(x) if isinstance(x, list) else x - return map(t, l) + return map(t, val) + def test_equality(): keys = [] @@ -15,12 +17,25 @@ def test_equality(): assert set(keys) == set(ecodes.ecodes.values()) + def test_access(): - assert ecodes.KEY_A == ecodes.ecodes['KEY_A'] == ecodes.KEY_A - assert ecodes.KEY[ecodes.ecodes['KEY_A']] == 'KEY_A' - assert ecodes.REL[0] == 'REL_X' + assert ecodes.KEY_A == ecodes.ecodes["KEY_A"] == ecodes.KEY_A + assert ecodes.KEY[ecodes.ecodes["KEY_A"]] == "KEY_A" + assert ecodes.REL[0] == "REL_X" + def test_overlap(): vals_ff = set(to_tuples(ecodes.FF.values())) vals_ff_status = set(to_tuples(ecodes.FF_STATUS.values())) - assert bool(vals_ff & vals_ff_status) == False + assert bool(vals_ff & vals_ff_status) is False + + +def test_generated(): + e_run = vars(ecodes_runtime) + e_gen = vars(ecodes) + + def keys(v): + res = {k for k in v.keys() if not k.startswith("_") and not k[1].islower()} + return res + + assert keys(e_run) == keys(e_gen) \ No newline at end of file diff --git a/tests/test_events.py b/tests/test_events.py index d0717f2..f0f456c 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -16,6 +16,7 @@ def test_categorize(): e = events.InputEvent(1036996631, 984417, ecodes.EV_MSC, 0, 0) assert e == util.categorize(e) + def test_keyevent(): e = events.InputEvent(1036996631, 984417, ecodes.EV_KEY, ecodes.KEY_A, 2) k = events.KeyEvent(e) @@ -23,5 +24,4 @@ def test_keyevent(): assert k.keystate == events.KeyEvent.key_hold assert k.event == e assert k.scancode == ecodes.KEY_A - assert k.keycode == 'KEY_A' # :todo: - + assert k.keycode == "KEY_A" # :todo: diff --git a/tests/test_uinput.py b/tests/test_uinput.py index 69945ea..666361f 100644 --- a/tests/test_uinput.py +++ b/tests/test_uinput.py @@ -1,83 +1,95 @@ # encoding: utf-8 - +import os +import stat from select import select -from pytest import raises +from unittest.mock import patch -from evdev import uinput, ecodes, events, device, util +import pytest +from pytest import raises, fixture +from evdev import uinput, ecodes, device, UInputError +# ----------------------------------------------------------------------------- uinput_options = { - 'name' : 'test-py-evdev-uinput', - 'bustype' : ecodes.BUS_USB, - 'vendor' : 0x1100, - 'product' : 0x2200, - 'version' : 0x3300, + "name": "test-py-evdev-uinput", + "bustype": ecodes.BUS_USB, + "vendor": 0x1100, + "product": 0x2200, + "version": 0x3300, } -def pytest_funcarg__c(request): +@fixture +def c(): return uinput_options.copy() def device_exists(bustype, vendor, product, version): - match = 'I: Bus=%04hx Vendor=%04hx Product=%04hx Version=%04hx' % \ - (bustype, vendor, product, version) + match = "I: Bus=%04hx Vendor=%04hx Product=%04hx Version=%04hx" + match = match % (bustype, vendor, product, version) - for line in open('/proc/bus/input/devices'): - if line.strip() == match: return True + for line in open("/proc/bus/input/devices"): + if line.strip() == match: + return True return False +# ----------------------------------------------------------------------------- def test_open(c): ui = uinput.UInput(**c) - args = (c['bustype'], c['vendor'], c['product'], c['version']) + args = (c["bustype"], c["vendor"], c["product"], c["version"]) assert device_exists(*args) ui.close() assert not device_exists(*args) + def test_open_context(c): - args = (c['bustype'], c['vendor'], c['product'], c['version']) + args = (c["bustype"], c["vendor"], c["product"], c["version"]) with uinput.UInput(**c): assert device_exists(*args) assert not device_exists(*args) + def test_maxnamelen(c): with raises(uinput.UInputError): - c['name'] = 'a' * 150 + c["name"] = "a" * 150 uinput.UInput(**c) + def test_enable_events(c): e = ecodes - c['events'] = {e.EV_KEY : [e.KEY_A, e.KEY_B, e.KEY_C]} + c["events"] = {e.EV_KEY: [e.KEY_A, e.KEY_B, e.KEY_C]} with uinput.UInput(**c) as ui: cap = ui.capabilities() assert e.EV_KEY in cap - assert sorted(cap[e.EV_KEY]) == sorted(c['events'][e.EV_KEY]) + assert sorted(cap[e.EV_KEY]) == sorted(c["events"][e.EV_KEY]) + def test_abs_values(c): e = ecodes - c['events'] = { - e.EV_KEY : [e.KEY_A, e.KEY_B], - e.EV_ABS : [(e.ABS_X, (0, 255, 0, 0)), - (e.ABS_Y, device.AbsInfo(0, 255, 5, 10, 0, 0))], + c = { + e.EV_KEY: [e.KEY_A, e.KEY_B], + e.EV_ABS: [(e.ABS_X, (0, 0, 255, 0, 0)), (e.ABS_Y, device.AbsInfo(0, 0, 255, 5, 10, 0))], } - with uinput.UInput(**c) as ui: + with uinput.UInput(events=c) as ui: c = ui.capabilities() abs = device.AbsInfo(value=0, min=0, max=255, fuzz=0, flat=0, resolution=0) assert c[e.EV_ABS][0] == (0, abs) + abs = device.AbsInfo(value=0, min=0, max=255, fuzz=5, flat=10, resolution=0) assert c[e.EV_ABS][1] == (1, abs) c = ui.capabilities(verbose=True) abs = device.AbsInfo(value=0, min=0, max=255, fuzz=0, flat=0, resolution=0) - assert c[('EV_ABS', 3)][0] == (('ABS_X', 0), abs) + assert c[("EV_ABS", 3)][0] == (("ABS_X", 0), abs) c = ui.capabilities(verbose=False, absinfo=False) assert c[e.EV_ABS] == list((0, 1)) + def test_write(c): with uinput.UInput(**c) as ui: d = ui.device @@ -87,12 +99,12 @@ def test_write(c): r, w, x = select([d], [d], []) if w and not wrote: - ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down - ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down - ui.write(ecodes.EV_KEY, ecodes.KEY_P, 0) # KEY_P up - ui.write(ecodes.EV_KEY, ecodes.KEY_A, 1) # KEY_A down - ui.write(ecodes.EV_KEY, ecodes.KEY_A, 2) # KEY_A hold - ui.write(ecodes.EV_KEY, ecodes.KEY_A, 0) # KEY_P up + ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down + ui.write(ecodes.EV_KEY, ecodes.KEY_P, 1) # KEY_P down + ui.write(ecodes.EV_KEY, ecodes.KEY_P, 0) # KEY_P up + ui.write(ecodes.EV_KEY, ecodes.KEY_A, 1) # KEY_A down + ui.write(ecodes.EV_KEY, ecodes.KEY_A, 2) # KEY_A hold + ui.write(ecodes.EV_KEY, ecodes.KEY_A, 0) # KEY_P up ui.syn() wrote = True @@ -105,3 +117,21 @@ def test_write(c): assert evs[3].code == ecodes.KEY_A and evs[3].value == 2 assert evs[4].code == ecodes.KEY_A and evs[4].value == 0 break + + +@patch.object(stat, 'S_ISCHR', return_value=False) +def test_not_a_character_device(ischr_mock, c): + with pytest.raises(UInputError, match='not a character device file'): + uinput.UInput(**c) + +@patch.object(stat, 'S_ISCHR', return_value=True) +@patch.object(os, 'stat', side_effect=OSError()) +def test_not_a_character_device_2(stat_mock, ischr_mock, c): + with pytest.raises(UInputError, match='not a character device file'): + uinput.UInput(**c) + +@patch.object(stat, 'S_ISCHR', return_value=True) +@patch.object(os, 'stat', return_value=[]) +def test_not_a_character_device_3(stat_mock, ischr_mock, c): + with pytest.raises(UInputError, match='not a character device file'): + uinput.UInput(**c) diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 0000000..7112927 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,21 @@ +from evdev import util + + +def test_match_ecodes_a(): + res = util.find_ecodes_by_regex("KEY_ZOOM.*") + assert res == {1: [372, 418, 419, 420]} + assert dict(util.resolve_ecodes_dict(res)) == { + ("EV_KEY", 1): [ + (("KEY_FULL_SCREEN", "KEY_ZOOM"), 372), + ("KEY_ZOOMIN", 418), + ("KEY_ZOOMOUT", 419), + ("KEY_ZOOMRESET", 420), + ] + } + + res = util.find_ecodes_by_regex(r"(ABS|KEY)_BR(AKE|EAK)") + assert res == {1: [411], 3: [10]} + assert dict(util.resolve_ecodes_dict(res)) == { + ("EV_KEY", 1): [("KEY_BREAK", 411)], + ("EV_ABS", 3): [("ABS_BRAKE", 10)], + }