diff --git a/bin/fftest-uinput-test.py b/bin/fftest-uinput-test.py new file mode 100755 index 0000000..62edf24 --- /dev/null +++ b/bin/fftest-uinput-test.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +''' +evdev example - uinput force feedback event monitor +''' + +from __future__ import print_function +import time, sys, select + +from evdev import UInput, UInputError, ecodes + + +cap = { + ecodes.EV_FF: [ + ecodes.FF_CONSTANT, + ecodes.FF_PERIODIC, + ecodes.FF_RAMP, + ecodes.FF_SPRING, + ecodes.FF_FRICTION, + ecodes.FF_DAMPER, + # ecodes.FF_RUMBLE, + ecodes.FF_INERTIA, + ] +} + +def ffcb(event): + import ipdb ; ipdb.set_trace() + print(event) + +ui = UInput(cap, ff_effects_max=10, ff_callback=ffcb) + +try: + print('uinput device created - run "fftest %s" to test' % ui.device.fn) +except AttributeError: + print('error: could not find corresponding /dev/input/eventXX device - check permissions') + sys.exit(1) + +print('waiting for events:') +while True: + r, w, x = select.select([ui.fd], [], []) + if r: + ui.read() diff --git a/bin/fftest.py b/bin/fftest.py new file mode 100755 index 0000000..2c1144a --- /dev/null +++ b/bin/fftest.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +# encoding: utf-8 + +''' +evdev example - tests the force feedback driver +''' + +from sys import argv, exit +from select import select +from getopt import gnu_getopt + +from evdev import ecodes, ff, InputDevice + + +usage = 'usage: fftest [options] ' + +device = InputDevice(argv[1]) + +print('Axes query:') diff --git a/doc/conf.py b/doc/conf.py index ee8ec57..93dc16c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -47,7 +47,7 @@ # General information about the project. project = u'python-evdev' -copyright = u'2012-2013, Georgi Valkov' +copyright = u'2012-2014, Georgi Valkov' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/doc/index.rst b/doc/index.rst index 1c53e02..87a15dc 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -30,6 +30,7 @@ Similar Projects ================ * `python-uinput`_ +* `uinput-mapper`_ * `ruby-evdev`_ * `evdev`_ (ctypes) @@ -69,3 +70,4 @@ Indices and Tables .. _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/ +.. _uinput-mapper: https://github.com/MerlijnWajer/uinput-mapper diff --git a/evdev/device.py b/evdev/device.py index 789a6a1..a83b4ee 100644 --- a/evdev/device.py +++ b/evdev/device.py @@ -68,7 +68,7 @@ def __str__(self): class DeviceInfo(_DeviceInfo): def __str__(self): - msg = 'bus: {:04x}, product {:04x}, vendor {:04x}, version {:04x}' + msg = 'bus: {:04x}, vendor {:04x}, product {:04x}, version {:04x}' return msg.format(*self) diff --git a/evdev/ecodes.py b/evdev/ecodes.py index c94b0c1..ce27301 100644 --- a/evdev/ecodes.py +++ b/evdev/ecodes.py @@ -1,12 +1,13 @@ # encoding: utf-8 ''' -This modules exposes the integer constants defined in ``linux/input.h``. +This modules exposes integer constants defined in ``linux/input.h`` +and ``linux/uinput.h``. Exposed constants:: KEY, ABS, REL, SW, MSC, LED, BTN, REP, SND, ID, EV, - BUS, SYN, FF, FF_STATUS + BUS, SYN, FF, FF_STATUS, UI, UINPUT This module also provides numerous reverse and forward mappings that are best illustrated by a few examples:: diff --git a/evdev/ff.py b/evdev/ff.py index e4598d7..187448d 100644 --- a/evdev/ff.py +++ b/evdev/ff.py @@ -8,6 +8,7 @@ _u16 = ctypes.c_uint16 _u32 = ctypes.c_uint32 _s16 = ctypes.c_int16 +_s32 = ctypes.c_int32 class Replay(ctypes.Structure): ''' @@ -152,7 +153,9 @@ class EffectType(ctypes.Union): ('ff_constant_effect', Constant), ('ff_ramp_effect', Ramp), ('ff_periodic_effect', Periodic), - ('ff_condition_effect', Condition * 2), # one for each axis + # ('ff_condition_effect', Condition * 2), # one for each axis + ('ff_condition_effect_1', Condition), + ('ff_condition_effect_2', Condition), ('ff_rumble_effect', Rumble), ] diff --git a/evdev/genecodes.py b/evdev/genecodes.py index 8572920..4fa2789 100755 --- a/evdev/genecodes.py +++ b/evdev/genecodes.py @@ -3,7 +3,7 @@ ''' Generate a Python extension module that exports macros from -/usr/include/linux/input.h +/usr/include/linux/input.h and /usr/include/linux/uinput.h ''' import os, sys, re @@ -12,12 +12,13 @@ template = r''' #include #include +#include /* Automatically generated by evdev.genecodes */ -/* Generated on %s */ +/* Generated on %(uname)s */ #define MODULE_NAME "_ecodes" -#define MODULE_HELP "linux/input.h macros" +#define MODULE_HELP "linux/input.h and linux/uinput.h macros" static PyMethodDef MethodTable[] = { { NULL, NULL, 0, NULL} @@ -49,7 +50,11 @@ if (m == NULL) return NULL; -%s + /* input.h constants */ +%(input)s + + /* uinput.h constants */ +%(uinput)s return m; } @@ -69,16 +74,21 @@ #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) +inputh = '/usr/include/linux/input.h' if len(sys.argv) == 1 else sys.argv[1] +uinputh = '/usr/include/linux/uinput.h' if len(sys.argv) < 3 else sys.argv[2] + +inputh_re = r'#define +((?:KEY|ABS|REL|SW|MSC|LED|BTN|REP|SND|ID|EV|BUS|SYN|FF)_\w+)' +inputh_re = re.compile(inputh_re) +uinputh_re = r'#define +((?:EV|UI_FF)_\w+)' +uinputh_re = re.compile(uinputh_re) -if not os.path.exists(header): - print('no such file: %s' % header) - sys.exit(1) +for fn in inputh, uinputh: + if not os.path.exists(fn): + print('no such file: %s' % inputh) + sys.exit(1) -def getmacros(): - for line in open(header): +def getmacros(fn, regex): + for line in open(fn): macro = regex.search(line) if macro: yield ' PyModule_AddIntMacro(m, %s);' % macro.group(1) @@ -86,5 +96,8 @@ def getmacros(): uname = list(os.uname()); del uname[1] uname = ' '.join(uname) -macros = os.linesep.join(getmacros()) -print(template % (uname, macros)) +input_macros = os.linesep.join(getmacros(inputh, inputh_re)) +uinput_macros = os.linesep.join(getmacros(uinputh, uinputh_re)) + +ctx = {'uname': uname, 'input': input_macros, 'uinput': uinput_macros} +print(template % ctx) diff --git a/evdev/input.c b/evdev/input.c index 30c5012..fed03c9 100644 --- a/evdev/input.c +++ b/evdev/input.c @@ -19,6 +19,7 @@ #include #include +#include "util.h" #define MAX_NAME_SIZE 256 @@ -354,32 +355,6 @@ ioctl_EVIOCGEFFECTS(PyObject *self, PyObject *args) return Py_BuildValue("i", res); } -void print_ff_effect(struct ff_effect* effect) { - fprintf(stderr, - "ff_effect:\n" - " type: %d \n" - " id: %d \n" - " direction: %d\n" - " trigger: (%d, %d)\n" - " replay: (%d, %d)\n", - effect->type, effect->id, effect->direction, - effect->trigger.button, effect->trigger.interval, - effect->replay.length, effect->replay.delay - ); - - - switch (effect->type) { - case FF_CONSTANT: - fprintf(stderr, " constant: (%d, (%d, %d, %d, %d))\n", effect->u.constant.level, - effect->u.constant.envelope.attack_length, - effect->u.constant.envelope.attack_level, - effect->u.constant.envelope.fade_length, - effect->u.constant.envelope.fade_level); - break; - } -} - - static PyObject * upload_effect(PyObject *self, PyObject *args) { diff --git a/evdev/uinput.c b/evdev/uinput.c index c885ed7..0aa5f56 100644 --- a/evdev/uinput.c +++ b/evdev/uinput.c @@ -11,9 +11,9 @@ #include #include +#include "util.h" -int _uinput_close(int fd) -{ +int _uinput_close(int fd) { if (ioctl(fd, UI_DEV_DESTROY) < 0) { int oerrno = errno; close(fd); @@ -33,7 +33,7 @@ uinput_open(PyObject *self, PyObject *args) int ret = PyArg_ParseTuple(args, "s", &devnode); if (!ret) return NULL; - int fd = open(devnode, O_WRONLY | O_NONBLOCK); + int fd = open(devnode, O_RDWR | O_NONBLOCK); if (fd < 0) { PyErr_SetString(PyExc_IOError, "could not open uinput device in write mode"); return NULL; @@ -47,15 +47,18 @@ static PyObject * uinput_create(PyObject *self, PyObject *args) { int fd, len, i, abscode; __u16 vendor, product, version, bustype; + __u32 ff_effects_max; 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; + int ret = PyArg_ParseTuple(args, "ishhhhOi", &fd, &name, &vendor, + &product, &version, &bustype, &absinfo, + &ff_effects_max); + if (!ret) + return NULL; memset(&uidev, 0, sizeof(uidev)); strncpy(uidev.name, name, UINPUT_MAX_NAME_SIZE); @@ -64,6 +67,9 @@ uinput_create(PyObject *self, PyObject *args) { uidev.id.version = version; uidev.id.bustype = bustype; + // force-feedback-capable driver + uidev.ff_effects_max = ff_effects_max; + len = PyList_Size(absinfo); for (i=0; i (ABS_X, 0, 255, 0, 0) @@ -79,14 +85,6 @@ uinput_create(PyObject *self, PyObject *args) { 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` instance #: for the fake input device. ``None`` if the device cannot be @@ -95,10 +127,10 @@ def __exit__(self, type, value, tb): 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)) + attr = 'name', 'bustype', 'vendor', 'product', 'version' + vals = (repr(getattr(self, i)) for i in attr) + vals = ', '.join(vals) + return '%s(%s)' % (self.__class__.__name__, vals) def __str__(self): msg = ('name "{}", bus "{}", vendor "{:04x}", product "{:04x}", version "{:04x}"\n' @@ -159,6 +191,36 @@ def write(self, etype, code, value): _uinput.write(self.fd, etype, code, value) + def read(self): + ''' + Read a queued event from the uinput device. Returns ``None`` + if no events are available. + ''' + event = _input.device_read(self.fd) + _, _, etype, code, value = event + + if etype == ecodes.EV_FF: + if self.ff_callback: + self.ff_callback(InputEvent(*event)) + + if etype == ecodes.EV_UINPUT: + if code == ecodes.UI_FF_UPLOAD: + data = _uinput.upload_effect(self.fd, value) + up_effect = uinput_ff_upload.from_buffer_copy(buffer(data)) + + if len(self.ff_effects) > ecodes.FF_EFFECT_MAX: + self.ff_effects.pop() + self.ff_effects.append(up_effect.effect) + + elif code == ecodes.UI_FF_ERASE: + data = _uinput.erase_effect(self.fd, value) + er_effect = uinput_ff_erase.from_buffer_copy(buffer(data)) + + for n, effect in enumerate(self.ff_effects): + if er_effect.effect_id == effect.id: + del self.ff_effects[n] + + def syn(self): ''' Inject a ``SYN_REPORT`` event into the input subsystem. Events diff --git a/evdev/util.h b/evdev/util.h new file mode 100644 index 0000000..10d9e48 --- /dev/null +++ b/evdev/util.h @@ -0,0 +1,23 @@ +void print_ff_effect(struct ff_effect* effect) { + fprintf(stderr, + "ff_effect:\n" + " type: %d \n" + " id: %d \n" + " direction: %d\n" + " trigger: (%d, %d)\n" + " replay: (%d, %d)\n", + effect->type, effect->id, effect->direction, + effect->trigger.button, effect->trigger.interval, + effect->replay.length, effect->replay.delay + ); + + switch (effect->type) { + case FF_CONSTANT: + fprintf(stderr, " constant: (%d, (%d, %d, %d, %d))\n", effect->u.constant.level, + effect->u.constant.envelope.attack_length, + effect->u.constant.envelope.attack_level, + effect->u.constant.envelope.fade_length, + effect->u.constant.envelope.fade_level); + break; + } +} diff --git a/evdev/util.py b/evdev/util.py index baad38c..e7618ed 100644 --- a/evdev/util.py +++ b/evdev/util.py @@ -3,22 +3,29 @@ import os import stat import glob +import functools from evdev import ecodes from evdev.events import event_factory -def list_devices(input_device_dir='/dev/input'): +def list_devices(input_device_dir='/dev/input', mode='rw'): '''List readable, character devices.''' - fns = glob.glob('{}/event*'.format(input_device_dir)) - fns = list(filter(is_device, fns)) + if isinstance(mode, str): + modemap = {'r': os.R_OK, 'w': os.W_OK} + mode = [modemap[i] for i in mode] + mode = functools.reduce(lambda x,y: x|y, mode) - return fns + files = glob.glob('%s/event*' % input_device_dir) + pred = functools.partial(is_device, mode=mode) + files = list(filter(pred, files)) + return files -def is_device(fn): - '''Check if ``fn`` exists, is readable and if it is a character device.''' + +def is_device(fn, mode=os.R_OK | os.W_OK): + '''Check if ``fn`` is a readable and writable character device.''' if not os.path.exists(fn): return False @@ -27,7 +34,7 @@ def is_device(fn): if not stat.S_ISCHR(m): return False - if not os.access(fn, os.R_OK): + if not os.access(fn, mode): return False return True diff --git a/setup.py b/setup.py index 2448671..3af9d5c 100755 --- a/setup.py +++ b/setup.py @@ -33,38 +33,39 @@ ecodes_c = Extension('evdev._ecodes', sources=['evdev/ecodes.c'], ) # extra_compile_args=['-O0']) kw = { - 'name' : 'evdev', - 'version' : '0.4.3', - - 'description' : 'Bindings for the linux input handling subsystem', - 'long_description' : open(pjoin(here, 'README.rst')).read(), - - 'author' : 'Georgi Valkov', - 'author_email' : 'georgi.t.valkov@gmail.com', - 'license' : 'Revised BSD License', - - 'keywords' : 'evdev input uinput', - 'classifiers' : classifiers, - 'url' : 'https://github.com/gvalkov/python-evdev', - - 'packages' : ['evdev'], - 'ext_modules' : [input_c, uinput_c, ecodes_c], - 'tests_require' : ['pytest'], - - 'include_package_data' : False, - 'zip_safe' : True, - 'cmdclass' : {}, + 'name': 'evdev', + 'version': '0.4.3', + 'description': 'Bindings for the linux input-handling subsystem', + 'long_description': open(pjoin(here, 'README.rst')).read(), + + 'author': 'Georgi Valkov', + 'author_email': 'georgi.t.valkov@gmail.com', + 'license': 'Revised BSD License', + + 'classifiers': classifiers, + 'keywords': 'evdev input uinput forcefeedback', + 'url': 'https://github.com/gvalkov/python-evdev', + + 'packages': ['evdev'], + 'ext_modules': [input_c, uinput_c, ecodes_c], + 'tests_require': ['pytest'], + + 'zip_safe': True, + 'cmdclass': {}, + 'include_package_data': False, } def create_ecodes(): # :todo: expose as a command option - header = '/usr/include/linux/input.h' + input_header = '/usr/include/linux/input.h' + uinput_header = '/usr/include/linux/uinput.h' - if not os.path.isfile(header): + if not all(map(os.path.isfile, (input_header, uinput_header))): msg = '''\ - The linux/input.h header file is missing. You will have to - install the headers for your kernel in order to continue: + The linux/input.h and linux/uinput.h header files are + missing. You will have to install the headers for your kernel + in order to continue: yum install kernel-headers-$(uname -r) apt-get intall linux-headers-$(uname -r) @@ -75,8 +76,9 @@ def create_ecodes(): from subprocess import check_call - print('writing ecodes.c (using %s)' % header) - cmd = '%s genecodes.py %s > ecodes.c' % (sys.executable, header) + headers = (input_header, uinput_header) + print('writing ecodes.c (using %s and %s)' % headers) + cmd = '%s genecodes.py %s > ecodes.c' % (sys.executable, ' '.join(headers)) check_call(cmd, cwd="%s/evdev" % here, shell=True)