~ubuntu-branches/ubuntu/saucy/autopilot/saucy-proposed

« back to all changes in this revision

Viewing changes to autopilot/input/_uinput.py

  • Committer: Package Import Robot
  • Author(s): Didier Roche
  • Date: 2013-06-07 13:33:46 UTC
  • mfrom: (57.1.1 saucy-proposed)
  • Revision ID: package-import@ubuntu.com-20130607133346-42zvbl1h2k1v54ac
Tags: 1.3daily13.06.05-0ubuntu2
autopilot-touch only suggests python-ubuntu-platform-api for now.
It's not in distro and we need that requirement to be fulfilled to
have unity 7, 100 scopes and the touch stack to distro.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
 
2
#
 
3
# Autopilot Functional Test Tool
 
4
# Copyright (C) 2012-2013 Canonical
 
5
#
 
6
# This program is free software: you can redistribute it and/or modify
 
7
# it under the terms of the GNU General Public License as published by
 
8
# the Free Software Foundation, either version 3 of the License, or
 
9
# (at your option) any later version.
 
10
#
 
11
# This program is distributed in the hope that it will be useful,
 
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
# GNU General Public License for more details.
 
15
#
 
16
# You should have received a copy of the GNU General Public License
 
17
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
18
#
 
19
 
 
20
 
 
21
"""UInput device drivers."""
 
22
 
 
23
from autopilot.input import Keyboard as KeyboardBase
 
24
from autopilot.input import Touch as TouchBase
 
25
from autopilot.input._common import get_center_point
 
26
import autopilot.platform
 
27
 
 
28
import logging
 
29
from time import sleep
 
30
from evdev import UInput, ecodes as e
 
31
import os.path
 
32
 
 
33
logger = logging.getLogger(__name__)
 
34
 
 
35
PRESS = 1
 
36
RELEASE = 0
 
37
 
 
38
PRESSED_KEYS = []
 
39
 
 
40
 
 
41
class Keyboard(KeyboardBase):
 
42
 
 
43
    def __init__(self):
 
44
        super(Keyboard, self).__init__()
 
45
 
 
46
        self._device = UInput(devnode=_get_devnode_path())
 
47
 
 
48
    def _emit(self, event, value):
 
49
        self._device.write(e.EV_KEY, event, value)
 
50
        self._device.syn()
 
51
 
 
52
    def _sanitise_keys(self, keys):
 
53
        if keys == '+':
 
54
            return [keys]
 
55
        else:
 
56
            return keys.split('+')
 
57
 
 
58
    def press(self, keys, delay=0.1):
 
59
        """Send key press events only.
 
60
 
 
61
        The 'keys' argument must be a string of keys you want
 
62
        pressed. For example:
 
63
 
 
64
        press('Alt+F2')
 
65
 
 
66
        presses the 'Alt' and 'F2' keys.
 
67
 
 
68
        """
 
69
        if not isinstance(keys, basestring):
 
70
            raise TypeError("'keys' argument must be a string.")
 
71
 
 
72
        for key in self._sanitise_keys(keys):
 
73
            for event in Keyboard._get_events_for_key(key):
 
74
                logger.debug("Pressing %s (%r)", key, event)
 
75
                self._emit(event, PRESS)
 
76
                sleep(delay)
 
77
 
 
78
    def release(self, keys, delay=0.1):
 
79
        """Send key release events only.
 
80
 
 
81
        The 'keys' argument must be a string of keys you want
 
82
        released. For example:
 
83
 
 
84
        release('Alt+F2')
 
85
 
 
86
        releases the 'Alt' and 'F2' keys.
 
87
 
 
88
        Keys are released in the reverse order in which they are specified.
 
89
 
 
90
        """
 
91
        if not isinstance(keys, basestring):
 
92
            raise TypeError("'keys' argument must be a string.")
 
93
        # logger.debug("Releasing keys %r with delay %f", keys, delay)
 
94
        # # release keys in the reverse order they were pressed in.
 
95
        # keys = self.__translate_keys(keys)
 
96
        for key in reversed(self._sanitise_keys(keys)):
 
97
            for event in Keyboard._get_events_for_key(key):
 
98
                logger.debug("Releasing %s (%r)", key, event)
 
99
                self._emit(event, RELEASE)
 
100
                sleep(delay)
 
101
 
 
102
    def press_and_release(self, keys, delay=0.1):
 
103
        """Press and release all items in 'keys'.
 
104
 
 
105
        This is the same as calling 'press(keys);release(keys)'.
 
106
 
 
107
        The 'keys' argument must be a string of keys you want
 
108
        pressed and released.. For example:
 
109
 
 
110
        press_and_release('Alt+F2')
 
111
 
 
112
        presses both the 'Alt' and 'F2' keys, and then releases both keys.
 
113
 
 
114
        """
 
115
        logger.debug("Pressing and Releasing: %s", keys)
 
116
        self.press(keys, delay)
 
117
        self.release(keys, delay)
 
118
 
 
119
    def type(self, string, delay=0.1):
 
120
        """Simulate a user typing a string of text.
 
121
 
 
122
        Only 'normal' keys can be typed with this method. Control characters
 
123
        (such as 'Alt' will be interpreted as an 'A', and 'l', and a 't').
 
124
 
 
125
        """
 
126
        if not isinstance(string, basestring):
 
127
            raise TypeError("'keys' argument must be a string.")
 
128
        logger.debug("Typing text %r", string)
 
129
        for key in string:
 
130
            self.press(key, delay)
 
131
            self.release(key, delay)
 
132
 
 
133
    @classmethod
 
134
    def on_test_end():
 
135
        """Generate KeyRelease events for any un-released keys.
 
136
 
 
137
        Make sure you call this at the end of any test to release
 
138
        any keys that were pressed and not released.
 
139
 
 
140
        """
 
141
        # global _PRESSED_KEYS
 
142
        # for keycode in _PRESSED_KEYS:
 
143
        #     logger.warning("Releasing key %r as part of cleanup call.", keycode)
 
144
        #     fake_input(get_display(), X.KeyRelease, keycode)
 
145
        # _PRESSED_KEYS = []
 
146
 
 
147
    @staticmethod
 
148
    def _get_events_for_key(key):
 
149
        """Return a list of events required to generate 'key' as an input.
 
150
 
 
151
        Multiple keys will be returned when the key specified requires more than one
 
152
        keypress to generate (for example, upper-case letters).
 
153
 
 
154
        """
 
155
        events = []
 
156
        if key.isupper() or key in _SHIFTED_KEYS:
 
157
            events.append(e.KEY_LEFTSHIFT)
 
158
        keyname = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
 
159
        evt = getattr(e, 'KEY_' + keyname.upper(), None)
 
160
        if evt is None:
 
161
            raise ValueError("Unknown key name: '%s'" % key)
 
162
        events.append(evt)
 
163
        return events
 
164
 
 
165
 
 
166
def _get_devnode_path():
 
167
    """Provide a fallback uinput node for devices which don't support udev"""
 
168
    devnode = '/dev/autopilot-uinput'
 
169
    if not os.path.exists(devnode):
 
170
        devnode = '/dev/uinput'
 
171
    return devnode
 
172
 
 
173
 
 
174
last_tracking_id = 0
 
175
def get_next_tracking_id():
 
176
    global last_tracking_id
 
177
    last_tracking_id += 1
 
178
    return last_tracking_id
 
179
 
 
180
 
 
181
def create_touch_device(res_x=None, res_y=None):
 
182
    """Create and return a UInput touch device.
 
183
 
 
184
    If res_x and res_y are not specified, they will be queried from the system.
 
185
 
 
186
    """
 
187
 
 
188
    if res_x is None or res_y is None:
 
189
        from autopilot.display import Display
 
190
        display = Display.create()
 
191
        # TODO: This calculation needs to become part of the display module:
 
192
        l = r = t = b = 0
 
193
        for screen in range(display.get_num_screens()):
 
194
            geometry = display.get_screen_geometry(screen)
 
195
            if geometry[0] < l:
 
196
                l = geometry[0]
 
197
            if geometry[1] < t:
 
198
                t = geometry[1]
 
199
            if geometry[0] + geometry[2] > r:
 
200
                r = geometry[0] + geometry[2]
 
201
            if geometry[1] + geometry[3] > b:
 
202
                b = geometry[1] + geometry[3];
 
203
        res_x = r - l
 
204
        res_y = b - t
 
205
 
 
206
    # android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have no
 
207
    # idea why...
 
208
    touch_tool = e.BTN_TOOL_FINGER
 
209
    if autopilot.platform.model() == 'Desktop':
 
210
        touch_tool = e.BTN_TOUCH
 
211
 
 
212
    cap_mt = {
 
213
        e.EV_ABS : [
 
214
            (e.ABS_X, (0, res_x, 0, 0)),
 
215
            (e.ABS_Y, (0, res_y, 0, 0)),
 
216
            (e.ABS_PRESSURE, (0, 65535, 0, 0)),
 
217
            (e.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
 
218
            (e.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
 
219
            (e.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
 
220
            (e.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
 
221
            (e.ABS_MT_PRESSURE, (0, 255, 0, 0)),
 
222
            (e.ABS_MT_SLOT, (0, 9, 0, 0)),
 
223
        ],
 
224
        e.EV_KEY: [
 
225
            touch_tool,
 
226
        ]
 
227
    }
 
228
 
 
229
    return UInput(cap_mt, name='autopilot-finger', version=0x2,
 
230
                  devnode=_get_devnode_path())
 
231
 
 
232
_touch_device = create_touch_device()
 
233
 
 
234
# Multiouch notes:
 
235
# ----------------
 
236
 
 
237
# We're simulating a class of device that can track multiple touches, and keep
 
238
# them separate. This is how most modern track devices work anyway. The device
 
239
# is created with a capability to track a certain number of distinct touches at
 
240
# once. This is the ABS_MT_SLOT capability. Since our target device can track 9
 
241
# separate touches, we'll do the same.
 
242
 
 
243
# Each finger contact starts by registering a slot number (0-8) with a tracking
 
244
# Id. The Id should be unique for this touch - this can be an auto-inctrementing
 
245
# integer. The very first packets to tell the kernel that we have a touch happening
 
246
# should look like this:
 
247
 
 
248
#    ABS_MT_SLOT 0
 
249
#    ABS_MT_TRACKING_ID 45
 
250
#    ABS_MT_POSITION_X x[0]
 
251
#    ABS_MT_POSITION_Y y[0]
 
252
 
 
253
# This associates Tracking id 45 (could be any number) with slot 0. Slot 0 can now
 
254
# not be use by any other touch until it is released.
 
255
 
 
256
# If we want to move this contact's coordinates, we do this:
 
257
 
 
258
#    ABS_MT_SLOT 0
 
259
#    ABS_MT_POSITION_X 123
 
260
#    ABS_MT_POSITION_Y 234
 
261
 
 
262
# Technically, the 'SLOT 0' part isn't needed, since we're already in slot 0, but
 
263
# it doesn't hurt to have it there.
 
264
 
 
265
# To lift the contact, we simply specify a tracking Id of -1:
 
266
 
 
267
#    ABS_MT_SLOT 0
 
268
#    ABS_MT_TRACKING_ID -1
 
269
 
 
270
# The initial association between slot and tracking Id is made when the 'finger'
 
271
# first makes contact with the device (well, not technically true, but close
 
272
# enough). Multiple touches can be active simultaniously, as long as they all have
 
273
# unique slots, and tracking Ids. The simplest way to think about this is that the
 
274
# SLOT refers to a finger number, and the TRACKING_ID identifies a unique touch
 
275
# for the duration of it's existance.
 
276
 
 
277
_touch_fingers_in_use = []
 
278
def _get_touch_finger():
 
279
    """Claim a touch finger id for use.
 
280
 
 
281
    :raises: RuntimeError if no more fingers are available.
 
282
 
 
283
    """
 
284
    global _touch_fingers_in_use
 
285
 
 
286
    for i in range(9):
 
287
        if i not in _touch_fingers_in_use:
 
288
            _touch_fingers_in_use.append(i)
 
289
            return i
 
290
    raise RuntimeError("All available fingers have been used already.")
 
291
 
 
292
def _release_touch_finger(finger_num):
 
293
    """Relase a previously-claimed finger id.
 
294
 
 
295
    :raises: RuntimeError if the finger given was never claimed, or was already
 
296
    released.
 
297
 
 
298
    """
 
299
    global _touch_fingers_in_use
 
300
 
 
301
    if finger_num not in _touch_fingers_in_use:
 
302
        raise RuntimeError("Finger %d was never claimed, or has already been released." % (finger_num))
 
303
    _touch_fingers_in_use.remove(finger_num)
 
304
    assert(finger_num not in _touch_fingers_in_use)
 
305
 
 
306
 
 
307
class Touch(TouchBase):
 
308
    """Low level interface to generate single finger touch events."""
 
309
 
 
310
    def __init__(self):
 
311
        super(Touch, self).__init__()
 
312
        self._touch_finger = None
 
313
 
 
314
    @property
 
315
    def pressed(self):
 
316
        return self._touch_finger is not None
 
317
 
 
318
    def tap(self, x, y):
 
319
        """Click (or 'tap') at given x and y coordinates."""
 
320
        logger.debug("Tapping at: %d,%d", x,y)
 
321
        self._finger_down(x, y)
 
322
        sleep(0.1)
 
323
        self._finger_up()
 
324
 
 
325
    def tap_object(self, object):
 
326
        """Click (or 'tap') a given object"""
 
327
        logger.debug("Tapping object: %r", object)
 
328
        x,y = get_center_point(object)
 
329
        self.tap(x,y)
 
330
 
 
331
    def press(self, x, y):
 
332
        """Press and hold a given object or at the given coordinates
 
333
        Call release() when the object has been pressed long enough"""
 
334
        logger.debug("Pressing at: %d,%d", x,y)
 
335
        self._finger_down(x, y)
 
336
 
 
337
    def release(self):
 
338
        """Release a previously pressed finger"""
 
339
        logger.debug("Releasing")
 
340
        self._finger_up()
 
341
 
 
342
 
 
343
    def drag(self, x1, y1, x2, y2):
 
344
        """Perform a drag gesture from (x1,y1) to (x2,y2)"""
 
345
        logger.debug("Dragging from %d,%d to %d,%d", x1, y1, x2, y2)
 
346
        self._finger_down(x1, y1)
 
347
 
 
348
        # Let's drag in 100 steps for now...
 
349
        dx = 1.0 * (x2 - x1) / 100
 
350
        dy = 1.0 * (y2 - y1) / 100
 
351
        cur_x = x1 + dx
 
352
        cur_y = y1 + dy
 
353
        for i in range(0, 100):
 
354
            self._finger_move(int(cur_x), int(cur_y))
 
355
            sleep(0.002)
 
356
            cur_x += dx
 
357
            cur_y += dy
 
358
        # Make sure we actually end up at target
 
359
        self._finger_move(x2, y2)
 
360
        self._finger_up()
 
361
 
 
362
 
 
363
 
 
364
    def _finger_down(self, x, y):
 
365
        """Internal: moves finger "finger" down to the touchscreen at pos (x,y)"""
 
366
        if self._touch_finger is not None:
 
367
            raise RuntimeError("Cannot press finger: it's already pressed.")
 
368
        self._touch_finger = _get_touch_finger()
 
369
 
 
370
        _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
 
371
        _touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, get_next_tracking_id())
 
372
        _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 1)
 
373
        _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
 
374
        _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
 
375
        _touch_device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
 
376
        _touch_device.syn()
 
377
 
 
378
 
 
379
    def _finger_move(self, x, y):
 
380
        """Internal: moves finger "finger" on the touchscreen to pos (x,y)
 
381
           NOTE: The finger has to be down for this to have any effect."""
 
382
        if self._touch_finger is not None:
 
383
            _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
 
384
            _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
 
385
            _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
 
386
            _touch_device.syn()
 
387
 
 
388
 
 
389
    def _finger_up(self):
 
390
        """Internal: moves finger "finger" up from the touchscreen"""
 
391
        if self._touch_finger is None:
 
392
            raise RuntimeError("Cannot release finger: it's not pressed.")
 
393
        _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
 
394
        _touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1)
 
395
        _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 0)
 
396
        _touch_device.syn()
 
397
        self._touch_finger = _release_touch_finger(self._touch_finger)
 
398
 
 
399
 
 
400
# veebers: there should be a better way to handle this.
 
401
_SHIFTED_KEYS = "~!@#$%^&*()_+{}|:\"?><"
 
402
 
 
403
# The double-ups are due to the 'shifted' keys.
 
404
_UINPUT_CODE_TRANSLATIONS = {
 
405
    '/': 'SLASH',
 
406
    '?': 'SLASH',
 
407
    '.': 'DOT',
 
408
    ',': 'COMMA',
 
409
    '>': 'DOT',
 
410
    '<': 'COMMA',
 
411
    '\'': 'APOSTROPHE',
 
412
    '"': 'APOSTROPHE',
 
413
    ';': 'SEMICOLON',
 
414
    ':': 'SEMICOLON',
 
415
    '\\': 'BACKSLASH',
 
416
    '|': 'BACKSLASH',
 
417
    ']': 'RIGHTBRACE',
 
418
    '[': 'LEFTBRACE',
 
419
    '}': 'RIGHTBRACE',
 
420
    '{': 'LEFTBRACE',
 
421
    '=': 'EQUAL',
 
422
    '+': 'EQUAL',
 
423
    '-': 'MINUS',
 
424
    '_': 'MINUS',
 
425
    ')': '0',
 
426
    '(': '9',
 
427
    '*': '8',
 
428
    '&': '7',
 
429
    '^': '6',
 
430
    '%': '5',
 
431
    '$': '4',
 
432
    '#': '3',
 
433
    '@': '2',
 
434
    '!': '1',
 
435
    '~': 'GRAVE',
 
436
    '`': 'GRAVE',
 
437
    ' ': 'SPACE',
 
438
    '\t': 'TAB',
 
439
    'CTRL': 'LEFTCTRL',
 
440
    'ALT': 'LEFTALT',
 
441
    'SHIFT': 'LEFTSHIFT',
 
442
}