"""Inputs - user input for humans. Inputs aims to provide easy to use, cross-platform, user input device support for Python. I.e. keyboards, mice, gamepads, etc. Currently supported platforms are the Raspberry Pi, Linux, Windows and Mac OS X. """ # Forked by Fabio Manganiello from: # Copyright (c) 2016, 2018: Zeth # All rights reserved. # # BSD Licence # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * 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 distribution. # # * Neither the name of the copyright holder nor the # names of its contributors may 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 THE COPYRIGHT HOLDER OR CONTRIBUTORS 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. from __future__ import print_function from __future__ import division import os import sys import io import glob import struct import platform import math import time import codecs from warnings import warn from itertools import count from operator import itemgetter from multiprocessing import Process, Pipe import ctypes __version__ = "0.5" WIN = platform.system() == 'Windows' MAC = platform.system() == 'Darwin' NIX = platform.system() == 'Linux' if WIN: # pylint: disable=wrong-import-position import ctypes.wintypes DWORD = ctypes.wintypes.DWORD HANDLE = ctypes.wintypes.HANDLE WPARAM = ctypes.wintypes.WPARAM LPARAM = ctypes.wintypes.WPARAM MSG = ctypes.wintypes.MSG else: DWORD = ctypes.c_ulong HANDLE = ctypes.c_void_p WPARAM = ctypes.c_ulonglong LPARAM = ctypes.c_ulonglong MSG = ctypes.Structure if NIX: from fcntl import ioctl OLD = sys.version_info < (3, 4) PERMISSIONS_ERROR_TEXT = ( "The user (that this program is being run as) does " "not have permission to access the input events, " "check groups and permissions, for example, on " "Debian, the user needs to be in the input group." ) # Standard event format for most devices. # long, long, unsigned short, unsigned short, int EVENT_FORMAT = str('llHHi') EVENT_SIZE = struct.calcsize(EVENT_FORMAT) def chunks(raw): """Yield successive EVENT_SIZE sized chunks from raw.""" for i in range(0, len(raw), EVENT_SIZE): yield struct.unpack(EVENT_FORMAT, raw[i : i + EVENT_SIZE]) if OLD: def iter_unpack(raw): """Yield successive EVENT_SIZE chunks from message.""" return chunks(raw) else: def iter_unpack(raw): """Yield successive EVENT_SIZE chunks from message.""" return struct.iter_unpack(EVENT_FORMAT, raw) def convert_timeval(seconds_since_epoch): """Convert time into C style timeval.""" frac, whole = math.modf(seconds_since_epoch) microseconds = math.floor(frac * 1000000) seconds = math.floor(whole) return seconds, microseconds SPECIAL_DEVICES = ( ( "Raspberry Pi Sense HAT Joystick", "/dev/input/by-id/gpio-Raspberry_Pi_Sense_HAT_Joystick-event-kbd", ), ( "Nintendo Wii Remote", "/dev/input/by-id/bluetooth-Nintendo_Wii_Remote-event-joystick", ), ( "FT5406 memory based driver", "/dev/input/by-id/gpio-Raspberry_Pi_Touchscreen_Display-event-mouse", ), ) XINPUT_MAPPING = ( (1, 0x11), (2, 0x11), (3, 0x10), (4, 0x10), (5, 0x13A), (6, 0x13B), (7, 0x13D), (8, 0x13E), (9, 0x136), (10, 0x137), (13, 0x130), (14, 0x131), (15, 0x134), (16, 0x133), (17, 0x11), ('l_thumb_x', 0x00), ('l_thumb_y', 0x01), ('left_trigger', 0x02), ('r_thumb_x', 0x03), ('r_thumb_y', 0x04), ('right_trigger', 0x05), ) XINPUT_DLL_NAMES = ( "XInput1_4.dll", "XInput9_1_0.dll", "XInput1_3.dll", "XInput1_2.dll", "XInput1_1.dll", ) XINPUT_ERROR_DEVICE_NOT_CONNECTED = 1167 XINPUT_ERROR_SUCCESS = 0 XBOX_STYLE_LED_CONTROL = { 0: 'off', 1: 'all blink, then previous setting', 2: '1/top-left blink, then on', 3: '2/top-right blink, then on', 4: '3/bottom-left blink, then on', 5: '4/bottom-right blink, then on', 6: '1/top-left on', 7: '2/top-right on', 8: '3/bottom-left on', 9: '4/bottom-right on', 10: 'rotate', 11: 'blink, based on previous setting', 12: 'slow blink, based on previous setting', 13: 'rotate with two lights', 14: 'persistent slow all blink', 15: 'blink once, then previous setting', } DEVICE_PROPERTIES = ( (0x00, "INPUT_PROP_POINTER"), # needs a pointer (0x01, "INPUT_PROP_DIRECT"), # direct input devices (0x02, "INPUT_PROP_BUTTONPAD"), # has button(s) under pad (0x03, "INPUT_PROP_SEMI_MT"), # touch rectangle only (0x04, "INPUT_PROP_TOPBUTTONPAD"), # softbuttons at top of pad (0x05, "INPUT_PROP_POINTING_STICK"), # is a pointing stick (0x06, "INPUT_PROP_ACCELEROMETER"), # has accelerometer (0x1F, "INPUT_PROP_MAX"), (0x1F + 1, "INPUT_PROP_CNT"), ) EVENT_TYPES = ( (0x00, "Sync"), (0x01, "Key"), (0x02, "Relative"), (0x03, "Absolute"), (0x04, "Misc"), (0x05, "Switch"), (0x11, "LED"), (0x12, "Sound"), (0x14, "Repeat"), (0x15, "ForceFeedback"), (0x16, "Power"), (0x17, "ForceFeedbackStatus"), (0x1F, "Max"), (0x1F + 1, "Current"), ) SYNCHRONIZATION_EVENTS = ( (0, "SYN_REPORT"), (1, "SYN_CONFIG"), (2, "SYN_MT_REPORT"), (3, "SYN_DROPPED"), (0xF, "SYN_MAX"), (0xF + 1, "SYN_CNT"), ) KEYS_AND_BUTTONS = ( (0, "KEY_RESERVED"), (1, "KEY_ESC"), (2, "KEY_1"), (3, "KEY_2"), (4, "KEY_3"), (5, "KEY_4"), (6, "KEY_5"), (7, "KEY_6"), (8, "KEY_7"), (9, "KEY_8"), (10, "KEY_9"), (11, "KEY_0"), (12, "KEY_MINUS"), (13, "KEY_EQUAL"), (14, "KEY_BACKSPACE"), (15, "KEY_TAB"), (16, "KEY_Q"), (17, "KEY_W"), (18, "KEY_E"), (19, "KEY_R"), (20, "KEY_T"), (21, "KEY_Y"), (22, "KEY_U"), (23, "KEY_I"), (24, "KEY_O"), (25, "KEY_P"), (26, "KEY_LEFTBRACE"), (27, "KEY_RIGHTBRACE"), (28, "KEY_ENTER"), (29, "KEY_LEFTCTRL"), (30, "KEY_A"), (31, "KEY_S"), (32, "KEY_D"), (33, "KEY_F"), (34, "KEY_G"), (35, "KEY_H"), (36, "KEY_J"), (37, "KEY_K"), (38, "KEY_L"), (39, "KEY_SEMICOLON"), (40, "KEY_APOSTROPHE"), (41, "KEY_GRAVE"), (42, "KEY_LEFTSHIFT"), (43, "KEY_BACKSLASH"), (44, "KEY_Z"), (45, "KEY_X"), (46, "KEY_C"), (47, "KEY_V"), (48, "KEY_B"), (49, "KEY_N"), (50, "KEY_M"), (51, "KEY_COMMA"), (52, "KEY_DOT"), (53, "KEY_SLASH"), (54, "KEY_RIGHTSHIFT"), (55, "KEY_KPASTERISK"), (56, "KEY_LEFTALT"), (57, "KEY_SPACE"), (58, "KEY_CAPSLOCK"), (59, "KEY_F1"), (60, "KEY_F2"), (61, "KEY_F3"), (62, "KEY_F4"), (63, "KEY_F5"), (64, "KEY_F6"), (65, "KEY_F7"), (66, "KEY_F8"), (67, "KEY_F9"), (68, "KEY_F10"), (69, "KEY_NUMLOCK"), (70, "KEY_SCROLLLOCK"), (71, "KEY_KP7"), (72, "KEY_KP8"), (73, "KEY_KP9"), (74, "KEY_KPMINUS"), (75, "KEY_KP4"), (76, "KEY_KP5"), (77, "KEY_KP6"), (78, "KEY_KPPLUS"), (79, "KEY_KP1"), (80, "KEY_KP2"), (81, "KEY_KP3"), (82, "KEY_KP0"), (83, "KEY_KPDOT"), (85, "KEY_ZENKAKUHANKAKU"), (86, "KEY_102ND"), (87, "KEY_F11"), (88, "KEY_F12"), (89, "KEY_RO"), (90, "KEY_KATAKANA"), (91, "KEY_HIRAGANA"), (92, "KEY_HENKAN"), (93, "KEY_KATAKANAHIRAGANA"), (94, "KEY_MUHENKAN"), (95, "KEY_KPJPCOMMA"), (96, "KEY_KPENTER"), (97, "KEY_RIGHTCTRL"), (98, "KEY_KPSLASH"), (99, "KEY_SYSRQ"), (100, "KEY_RIGHTALT"), (101, "KEY_LINEFEED"), (102, "KEY_HOME"), (103, "KEY_UP"), (104, "KEY_PAGEUP"), (105, "KEY_LEFT"), (106, "KEY_RIGHT"), (107, "KEY_END"), (108, "KEY_DOWN"), (109, "KEY_PAGEDOWN"), (110, "KEY_INSERT"), (111, "KEY_DELETE"), (112, "KEY_MACRO"), (113, "KEY_MUTE"), (114, "KEY_VOLUMEDOWN"), (115, "KEY_VOLUMEUP"), (116, "KEY_POWER"), # SC System Power Down (117, "KEY_KPEQUAL"), (118, "KEY_KPPLUSMINUS"), (119, "KEY_PAUSE"), (120, "KEY_SCALE"), # AL Compiz Scale (Expose) (121, "KEY_KPCOMMA"), (122, "KEY_HANGEUL"), (123, "KEY_HANJA"), (124, "KEY_YEN"), (125, "KEY_LEFTMETA"), (126, "KEY_RIGHTMETA"), (127, "KEY_COMPOSE"), (128, "KEY_STOP"), # AC Stop (129, "KEY_AGAIN"), (130, "KEY_PROPS"), # AC Properties (131, "KEY_UNDO"), # AC Undo (132, "KEY_FRONT"), (133, "KEY_COPY"), # AC Copy (134, "KEY_OPEN"), # AC Open (135, "KEY_PASTE"), # AC Paste (136, "KEY_FIND"), # AC Search (137, "KEY_CUT"), # AC Cut (138, "KEY_HELP"), # AL Integrated Help Center (139, "KEY_MENU"), # Menu (show menu) (140, "KEY_CALC"), # AL Calculator (141, "KEY_SETUP"), (142, "KEY_SLEEP"), # SC System Sleep (143, "KEY_WAKEUP"), # System Wake Up (144, "KEY_FILE"), # AL Local Machine Browser (145, "KEY_SENDFILE"), (146, "KEY_DELETEFILE"), (147, "KEY_XFER"), (148, "KEY_PROG1"), (149, "KEY_PROG2"), (150, "KEY_WWW"), # AL Internet Browser (151, "KEY_MSDOS"), (152, "KEY_COFFEE"), # AL Terminal Lock/Screensaver (153, "KEY_ROTATE_DISPLAY"), # Display orientation for e.g. tablets (154, "KEY_CYCLEWINDOWS"), (155, "KEY_MAIL"), (156, "KEY_BOOKMARKS"), # AC Bookmarks (157, "KEY_COMPUTER"), (158, "KEY_BACK"), # AC Back (159, "KEY_FORWARD"), # AC Forward (160, "KEY_CLOSECD"), (161, "KEY_EJECTCD"), (162, "KEY_EJECTCLOSECD"), (163, "KEY_NEXTSONG"), (164, "KEY_PLAYPAUSE"), (165, "KEY_PREVIOUSSONG"), (166, "KEY_STOPCD"), (167, "KEY_RECORD"), (168, "KEY_REWIND"), (169, "KEY_PHONE"), # Media Select Telephone (170, "KEY_ISO"), (171, "KEY_CONFIG"), # AL Consumer Control Configuration (172, "KEY_HOMEPAGE"), # AC Home (173, "KEY_REFRESH"), # AC Refresh (174, "KEY_EXIT"), # AC Exit (175, "KEY_MOVE"), (176, "KEY_EDIT"), (177, "KEY_SCROLLUP"), (178, "KEY_SCROLLDOWN"), (179, "KEY_KPLEFTPAREN"), (180, "KEY_KPRIGHTPAREN"), (181, "KEY_NEW"), # AC New (182, "KEY_REDO"), # AC Redo/Repeat (183, "KEY_F13"), (184, "KEY_F14"), (185, "KEY_F15"), (186, "KEY_F16"), (187, "KEY_F17"), (188, "KEY_F18"), (189, "KEY_F19"), (190, "KEY_F20"), (191, "KEY_F21"), (192, "KEY_F22"), (193, "KEY_F23"), (194, "KEY_F24"), (200, "KEY_PLAYCD"), (201, "KEY_PAUSECD"), (202, "KEY_PROG3"), (203, "KEY_PROG4"), (204, "KEY_DASHBOARD"), # AL Dashboard (205, "KEY_SUSPEND"), (206, "KEY_CLOSE"), # AC Close (207, "KEY_PLAY"), (208, "KEY_FASTFORWARD"), (209, "KEY_BASSBOOST"), (210, "KEY_PRINT"), # AC Print (211, "KEY_HP"), (212, "KEY_CAMERA"), (213, "KEY_SOUND"), (214, "KEY_QUESTION"), (215, "KEY_EMAIL"), (216, "KEY_CHAT"), (217, "KEY_SEARCH"), (218, "KEY_CONNECT"), (219, "KEY_FINANCE"), # AL Checkbook/Finance (220, "KEY_SPORT"), (221, "KEY_SHOP"), (222, "KEY_ALTERASE"), (223, "KEY_CANCEL"), # AC Cancel (224, "KEY_BRIGHTNESSDOWN"), (225, "KEY_BRIGHTNESSUP"), (226, "KEY_MEDIA"), (227, "KEY_SWITCHVIDEOMODE"), # Cycle between available video (228, "KEY_KBDILLUMTOGGLE"), (229, "KEY_KBDILLUMDOWN"), (230, "KEY_KBDILLUMUP"), (231, "KEY_SEND"), # AC Send (232, "KEY_REPLY"), # AC Reply (233, "KEY_FORWARDMAIL"), # AC Forward Msg (234, "KEY_SAVE"), # AC Save (235, "KEY_DOCUMENTS"), (236, "KEY_BATTERY"), (237, "KEY_BLUETOOTH"), (238, "KEY_WLAN"), (239, "KEY_UWB"), (240, "KEY_UNKNOWN"), (241, "KEY_VIDEO_NEXT"), # drive next video source (242, "KEY_VIDEO_PREV"), # drive previous video source (243, "KEY_BRIGHTNESS_CYCLE"), # brightness up, after max is min (244, "KEY_BRIGHTNESS_AUTO"), # Set Auto Brightness: manual (245, "KEY_DISPLAY_OFF"), # display device to off state (246, "KEY_WWAN"), # Wireless WAN (LTE, UMTS, GSM, etc.) (247, "KEY_RFKILL"), # Key that controls all radios (248, "KEY_MICMUTE"), # Mute / unmute the microphone (0x100, "BTN_MISC"), (0x100, "BTN_0"), (0x101, "BTN_1"), (0x102, "BTN_2"), (0x103, "BTN_3"), (0x104, "BTN_4"), (0x105, "BTN_5"), (0x106, "BTN_6"), (0x107, "BTN_7"), (0x108, "BTN_8"), (0x109, "BTN_9"), (0x110, "BTN_MOUSE"), (0x110, "BTN_LEFT"), (0x111, "BTN_RIGHT"), (0x112, "BTN_MIDDLE"), (0x113, "BTN_SIDE"), (0x114, "BTN_EXTRA"), (0x115, "BTN_FORWARD"), (0x116, "BTN_BACK"), (0x117, "BTN_TASK"), (0x120, "BTN_JOYSTICK"), (0x120, "BTN_TRIGGER"), (0x121, "BTN_THUMB"), (0x122, "BTN_THUMB2"), (0x123, "BTN_TOP"), (0x124, "BTN_TOP2"), (0x125, "BTN_PINKIE"), (0x126, "BTN_BASE"), (0x127, "BTN_BASE2"), (0x128, "BTN_BASE3"), (0x129, "BTN_BASE4"), (0x12A, "BTN_BASE5"), (0x12B, "BTN_BASE6"), (0x12F, "BTN_DEAD"), (0x130, "BTN_GAMEPAD"), (0x130, "BTN_SOUTH"), (0x131, "BTN_EAST"), (0x132, "BTN_C"), (0x133, "BTN_NORTH"), (0x134, "BTN_WEST"), (0x135, "BTN_Z"), (0x136, "BTN_TL"), (0x137, "BTN_TR"), (0x138, "BTN_TL2"), (0x139, "BTN_TR2"), (0x13A, "BTN_SELECT"), (0x13B, "BTN_START"), (0x13C, "BTN_MODE"), (0x13D, "BTN_THUMBL"), (0x13E, "BTN_THUMBR"), (0x140, "BTN_DIGI"), (0x140, "BTN_TOOL_PEN"), (0x141, "BTN_TOOL_RUBBER"), (0x142, "BTN_TOOL_BRUSH"), (0x143, "BTN_TOOL_PENCIL"), (0x144, "BTN_TOOL_AIRBRUSH"), (0x145, "BTN_TOOL_FINGER"), (0x146, "BTN_TOOL_MOUSE"), (0x147, "BTN_TOOL_LENS"), (0x148, "BTN_TOOL_QUINTTAP"), # Five fingers on trackpad (0x14A, "BTN_TOUCH"), (0x14B, "BTN_STYLUS"), (0x14C, "BTN_STYLUS2"), (0x14D, "BTN_TOOL_DOUBLETAP"), (0x14E, "BTN_TOOL_TRIPLETAP"), (0x14F, "BTN_TOOL_QUADTAP"), # Four fingers on trackpad (0x150, "BTN_WHEEL"), (0x150, "BTN_GEAR_DOWN"), (0x151, "BTN_GEAR_UP"), (0x160, "KEY_OK"), (0x161, "KEY_SELECT"), (0x162, "KEY_GOTO"), (0x163, "KEY_CLEAR"), (0x164, "KEY_POWER2"), (0x165, "KEY_OPTION"), (0x166, "KEY_INFO"), # AL OEM Features/Tips/Tutorial (0x167, "KEY_TIME"), (0x168, "KEY_VENDOR"), (0x169, "KEY_ARCHIVE"), (0x16A, "KEY_PROGRAM"), # Media Select Program Guide (0x16B, "KEY_CHANNEL"), (0x16C, "KEY_FAVORITES"), (0x16D, "KEY_EPG"), (0x16E, "KEY_PVR"), # Media Select Home (0x16F, "KEY_MHP"), (0x170, "KEY_LANGUAGE"), (0x171, "KEY_TITLE"), (0x172, "KEY_SUBTITLE"), (0x173, "KEY_ANGLE"), (0x174, "KEY_ZOOM"), (0x175, "KEY_MODE"), (0x176, "KEY_KEYBOARD"), (0x177, "KEY_SCREEN"), (0x178, "KEY_PC"), # Media Select Computer (0x179, "KEY_TV"), # Media Select TV (0x17A, "KEY_TV2"), # Media Select Cable (0x17B, "KEY_VCR"), # Media Select VCR (0x17C, "KEY_VCR2"), # VCR Plus (0x17D, "KEY_SAT"), # Media Select Satellite (0x17E, "KEY_SAT2"), (0x17F, "KEY_CD"), # Media Select CD (0x180, "KEY_TAPE"), # Media Select Tape (0x181, "KEY_RADIO"), (0x182, "KEY_TUNER"), # Media Select Tuner (0x183, "KEY_PLAYER"), (0x184, "KEY_TEXT"), (0x185, "KEY_DVD"), # Media Select DVD (0x186, "KEY_AUX"), (0x187, "KEY_MP3"), (0x188, "KEY_AUDIO"), # AL Audio Browser (0x189, "KEY_VIDEO"), # AL Movie Browser (0x18A, "KEY_DIRECTORY"), (0x18B, "KEY_LIST"), (0x18C, "KEY_MEMO"), # Media Select Messages (0x18D, "KEY_CALENDAR"), (0x18E, "KEY_RED"), (0x18F, "KEY_GREEN"), (0x190, "KEY_YELLOW"), (0x191, "KEY_BLUE"), (0x192, "KEY_CHANNELUP"), # Channel Increment (0x193, "KEY_CHANNELDOWN"), # Channel Decrement (0x194, "KEY_FIRST"), (0x195, "KEY_LAST"), # Recall Last (0x196, "KEY_AB"), (0x197, "KEY_NEXT"), (0x198, "KEY_RESTART"), (0x199, "KEY_SLOW"), (0x19A, "KEY_SHUFFLE"), (0x19B, "KEY_BREAK"), (0x19C, "KEY_PREVIOUS"), (0x19D, "KEY_DIGITS"), (0x19E, "KEY_TEEN"), (0x19F, "KEY_TWEN"), (0x1A0, "KEY_VIDEOPHONE"), # Media Select Video Phone (0x1A1, "KEY_GAMES"), # Media Select Games (0x1A2, "KEY_ZOOMIN"), # AC Zoom In (0x1A3, "KEY_ZOOMOUT"), # AC Zoom Out (0x1A4, "KEY_ZOOMRESET"), # AC Zoom (0x1A5, "KEY_WORDPROCESSOR"), # AL Word Processor (0x1A6, "KEY_EDITOR"), # AL Text Editor (0x1A7, "KEY_SPREADSHEET"), # AL Spreadsheet (0x1A8, "KEY_GRAPHICSEDITOR"), # AL Graphics Editor (0x1A9, "KEY_PRESENTATION"), # AL Presentation App (0x1AA, "KEY_DATABASE"), # AL Database App (0x1AB, "KEY_NEWS"), # AL Newsreader (0x1AC, "KEY_VOICEMAIL"), # AL Voicemail (0x1AD, "KEY_ADDRESSBOOK"), # AL Contacts/Address Book (0x1AE, "KEY_MESSENGER"), # AL Instant Messaging (0x1AF, "KEY_DISPLAYTOGGLE"), # Turn display (LCD) on and off (0x1B0, "KEY_SPELLCHECK"), # AL Spell Check (0x1B1, "KEY_LOGOFF"), # AL Logoff (0x1B2, "KEY_DOLLAR"), (0x1B3, "KEY_EURO"), (0x1B4, "KEY_FRAMEBACK"), # Consumer - transport controls (0x1B5, "KEY_FRAMEFORWARD"), (0x1B6, "KEY_CONTEXT_MENU"), # GenDesc - system context menu (0x1B7, "KEY_MEDIA_REPEAT"), # Consumer - transport control (0x1B8, "KEY_10CHANNELSUP"), # 10 channels up (10+) (0x1B9, "KEY_10CHANNELSDOWN"), # 10 channels down (10-) (0x1BA, "KEY_IMAGES"), # AL Image Browser (0x1C0, "KEY_DEL_EOL"), (0x1C1, "KEY_DEL_EOS"), (0x1C2, "KEY_INS_LINE"), (0x1C3, "KEY_DEL_LINE"), (0x1D0, "KEY_FN"), (0x1D1, "KEY_FN_ESC"), (0x1D2, "KEY_FN_F1"), (0x1D3, "KEY_FN_F2"), (0x1D4, "KEY_FN_F3"), (0x1D5, "KEY_FN_F4"), (0x1D6, "KEY_FN_F5"), (0x1D7, "KEY_FN_F6"), (0x1D8, "KEY_FN_F7"), (0x1D9, "KEY_FN_F8"), (0x1DA, "KEY_FN_F9"), (0x1DB, "KEY_FN_F10"), (0x1DC, "KEY_FN_F11"), (0x1DD, "KEY_FN_F12"), (0x1DE, "KEY_FN_1"), (0x1DF, "KEY_FN_2"), (0x1E0, "KEY_FN_D"), (0x1E1, "KEY_FN_E"), (0x1E2, "KEY_FN_F"), (0x1E3, "KEY_FN_S"), (0x1E4, "KEY_FN_B"), (0x1F1, "KEY_BRL_DOT1"), (0x1F2, "KEY_BRL_DOT2"), (0x1F3, "KEY_BRL_DOT3"), (0x1F4, "KEY_BRL_DOT4"), (0x1F5, "KEY_BRL_DOT5"), (0x1F6, "KEY_BRL_DOT6"), (0x1F7, "KEY_BRL_DOT7"), (0x1F8, "KEY_BRL_DOT8"), (0x1F9, "KEY_BRL_DOT9"), (0x1FA, "KEY_BRL_DOT10"), (0x200, "KEY_NUMERIC_0"), # used by phones, remote controls, (0x201, "KEY_NUMERIC_1"), # and other keypads (0x202, "KEY_NUMERIC_2"), (0x203, "KEY_NUMERIC_3"), (0x204, "KEY_NUMERIC_4"), (0x205, "KEY_NUMERIC_5"), (0x206, "KEY_NUMERIC_6"), (0x207, "KEY_NUMERIC_7"), (0x208, "KEY_NUMERIC_8"), (0x209, "KEY_NUMERIC_9"), (0x20A, "KEY_NUMERIC_STAR"), (0x20B, "KEY_NUMERIC_POUND"), (0x20C, "KEY_NUMERIC_A"), # Phone key A - HUT Telephony 0xb9 (0x20D, "KEY_NUMERIC_B"), (0x20E, "KEY_NUMERIC_C"), (0x20F, "KEY_NUMERIC_D"), (0x210, "KEY_CAMERA_FOCUS"), (0x211, "KEY_WPS_BUTTON"), # WiFi Protected Setup key (0x212, "KEY_TOUCHPAD_TOGGLE"), # Request switch touchpad on or off (0x213, "KEY_TOUCHPAD_ON"), (0x214, "KEY_TOUCHPAD_OFF"), (0x215, "KEY_CAMERA_ZOOMIN"), (0x216, "KEY_CAMERA_ZOOMOUT"), (0x217, "KEY_CAMERA_UP"), (0x218, "KEY_CAMERA_DOWN"), (0x219, "KEY_CAMERA_LEFT"), (0x21A, "KEY_CAMERA_RIGHT"), (0x21B, "KEY_ATTENDANT_ON"), (0x21C, "KEY_ATTENDANT_OFF"), (0x21D, "KEY_ATTENDANT_TOGGLE"), # Attendant call on or off (0x21E, "KEY_LIGHTS_TOGGLE"), # Reading light on or off (0x220, "BTN_DPAD_UP"), (0x221, "BTN_DPAD_DOWN"), (0x222, "BTN_DPAD_LEFT"), (0x223, "BTN_DPAD_RIGHT"), (0x230, "KEY_ALS_TOGGLE"), # Ambient light sensor (0x240, "KEY_BUTTONCONFIG"), # AL Button Configuration (0x241, "KEY_TASKMANAGER"), # AL Task/Project Manager (0x242, "KEY_JOURNAL"), # AL Log/Journal/Timecard (0x243, "KEY_CONTROLPANEL"), # AL Control Panel (0x244, "KEY_APPSELECT"), # AL Select Task/Application (0x245, "KEY_SCREENSAVER"), # AL Screen Saver (0x246, "KEY_VOICECOMMAND"), # Listening Voice Command (0x250, "KEY_BRIGHTNESS_MIN"), # Set Brightness to Minimum (0x251, "KEY_BRIGHTNESS_MAX"), # Set Brightness to Maximum (0x260, "KEY_KBDINPUTASSIST_PREV"), (0x261, "KEY_KBDINPUTASSIST_NEXT"), (0x262, "KEY_KBDINPUTASSIST_PREVGROUP"), (0x263, "KEY_KBDINPUTASSIST_NEXTGROUP"), (0x264, "KEY_KBDINPUTASSIST_ACCEPT"), (0x265, "KEY_KBDINPUTASSIST_CANCEL"), (0x2C0, "BTN_TRIGGER_HAPPY"), (0x2C0, "BTN_TRIGGER_HAPPY1"), (0x2C1, "BTN_TRIGGER_HAPPY2"), (0x2C2, "BTN_TRIGGER_HAPPY3"), (0x2C3, "BTN_TRIGGER_HAPPY4"), (0x2C4, "BTN_TRIGGER_HAPPY5"), (0x2C5, "BTN_TRIGGER_HAPPY6"), (0x2C6, "BTN_TRIGGER_HAPPY7"), (0x2C7, "BTN_TRIGGER_HAPPY8"), (0x2C8, "BTN_TRIGGER_HAPPY9"), (0x2C9, "BTN_TRIGGER_HAPPY10"), (0x2CA, "BTN_TRIGGER_HAPPY11"), (0x2CB, "BTN_TRIGGER_HAPPY12"), (0x2CC, "BTN_TRIGGER_HAPPY13"), (0x2CD, "BTN_TRIGGER_HAPPY14"), (0x2CE, "BTN_TRIGGER_HAPPY15"), (0x2CF, "BTN_TRIGGER_HAPPY16"), (0x2D0, "BTN_TRIGGER_HAPPY17"), (0x2D1, "BTN_TRIGGER_HAPPY18"), (0x2D2, "BTN_TRIGGER_HAPPY19"), (0x2D3, "BTN_TRIGGER_HAPPY20"), (0x2D4, "BTN_TRIGGER_HAPPY21"), (0x2D5, "BTN_TRIGGER_HAPPY22"), (0x2D6, "BTN_TRIGGER_HAPPY23"), (0x2D7, "BTN_TRIGGER_HAPPY24"), (0x2D8, "BTN_TRIGGER_HAPPY25"), (0x2D9, "BTN_TRIGGER_HAPPY26"), (0x2DA, "BTN_TRIGGER_HAPPY27"), (0x2DB, "BTN_TRIGGER_HAPPY28"), (0x2DC, "BTN_TRIGGER_HAPPY29"), (0x2DD, "BTN_TRIGGER_HAPPY30"), (0x2DE, "BTN_TRIGGER_HAPPY31"), (0x2DF, "BTN_TRIGGER_HAPPY32"), (0x2E0, "BTN_TRIGGER_HAPPY33"), (0x2E1, "BTN_TRIGGER_HAPPY34"), (0x2E2, "BTN_TRIGGER_HAPPY35"), (0x2E3, "BTN_TRIGGER_HAPPY36"), (0x2E4, "BTN_TRIGGER_HAPPY37"), (0x2E5, "BTN_TRIGGER_HAPPY38"), (0x2E6, "BTN_TRIGGER_HAPPY39"), (0x2E7, "BTN_TRIGGER_HAPPY40"), (0x2FF, "KEY_MAX"), (0x2FF + 1, "KEY_CNT"), ) RELATIVE_AXES = ( (0x00, "REL_X"), (0x01, "REL_Y"), (0x02, "REL_Z"), (0x03, "REL_RX"), (0x04, "REL_RY"), (0x05, "REL_RZ"), (0x06, "REL_HWHEEL"), (0x07, "REL_DIAL"), (0x08, "REL_WHEEL"), (0x09, "REL_MISC"), (0x0F, "REL_MAX"), (0x0F + 1, "REL_CNT"), ) ABSOLUTE_AXES = ( (0x00, "ABS_X"), (0x01, "ABS_Y"), (0x02, "ABS_Z"), (0x03, "ABS_RX"), (0x04, "ABS_RY"), (0x05, "ABS_RZ"), (0x06, "ABS_THROTTLE"), (0x07, "ABS_RUDDER"), (0x08, "ABS_WHEEL"), (0x09, "ABS_GAS"), (0x0A, "ABS_BRAKE"), (0x10, "ABS_HAT0X"), (0x11, "ABS_HAT0Y"), (0x12, "ABS_HAT1X"), (0x13, "ABS_HAT1Y"), (0x14, "ABS_HAT2X"), (0x15, "ABS_HAT2Y"), (0x16, "ABS_HAT3X"), (0x17, "ABS_HAT3Y"), (0x18, "ABS_PRESSURE"), (0x19, "ABS_DISTANCE"), (0x1A, "ABS_TILT_X"), (0x1B, "ABS_TILT_Y"), (0x1C, "ABS_TOOL_WIDTH"), (0x20, "ABS_VOLUME"), (0x28, "ABS_MISC"), (0x2F, "ABS_MT_SLOT"), # MT slot being modified (0x30, "ABS_MT_TOUCH_MAJOR"), # Major axis of touching ellipse (0x31, "ABS_MT_TOUCH_MINOR"), # Minor axis (omit if circular) (0x32, "ABS_MT_WIDTH_MAJOR"), # Major axis of approaching ellipse (0x33, "ABS_MT_WIDTH_MINOR"), # Minor axis (omit if circular) (0x34, "ABS_MT_ORIENTATION"), # Ellipse orientation (0x35, "ABS_MT_POSITION_X"), # Center X touch position (0x36, "ABS_MT_POSITION_Y"), # Center Y touch position (0x37, "ABS_MT_TOOL_TYPE"), # Type of touching device (0x38, "ABS_MT_BLOB_ID"), # Group a set of packets as a blob (0x39, "ABS_MT_TRACKING_ID"), # Unique ID of initiated contact (0x3A, "ABS_MT_PRESSURE"), # Pressure on contact area (0x3B, "ABS_MT_DISTANCE"), # Contact hover distance (0x3C, "ABS_MT_TOOL_X"), # Center X tool position (0x3D, "ABS_MT_TOOL_Y"), # Center Y tool position (0x3F, "ABS_MAX"), (0x3F + 1, "ABS_CNT"), ) SWITCH_EVENTS = ( (0x00, "SW_LID"), # set = lid shut (0x01, "SW_TABLET_MODE"), # set = tablet mode (0x02, "SW_HEADPHONE_INSERT"), # set = inserted (0x03, "SW_RFKILL_ALL"), # rfkill master switch, type "any" (0x04, "SW_MICROPHONE_INSERT"), # set = inserted (0x05, "SW_DOCK"), # set = plugged into dock (0x06, "SW_LINEOUT_INSERT"), # set = inserted (0x07, "SW_JACK_PHYSICAL_INSERT"), # set = mechanical switch set (0x08, "SW_VIDEOOUT_INSERT"), # set = inserted (0x09, "SW_CAMERA_LENS_COVER"), # set = lens covered (0x0A, "SW_KEYPAD_SLIDE"), # set = keypad slide out (0x0B, "SW_FRONT_PROXIMITY"), # set = front proximity sensor active (0x0C, "SW_ROTATE_LOCK"), # set = rotate locked/disabled (0x0D, "SW_LINEIN_INSERT"), # set = inserted (0x0E, "SW_MUTE_DEVICE"), # set = device disabled (0x0F, "SW_MAX"), (0x0F + 1, "SW_CNT"), ) MISC_EVENTS = ( (0x00, "MSC_SERIAL"), (0x01, "MSC_PULSELED"), (0x02, "MSC_GESTURE"), (0x03, "MSC_RAW"), (0x04, "MSC_SCAN"), (0x05, "MSC_TIMESTAMP"), (0x07, "MSC_MAX"), (0x07 + 1, "MSC_CNT"), ) LEDS = ( (0x00, "LED_NUML"), (0x01, "LED_CAPSL"), (0x02, "LED_SCROLLL"), (0x03, "LED_COMPOSE"), (0x04, "LED_KANA"), (0x05, "LED_SLEEP"), (0x06, "LED_SUSPEND"), (0x07, "LED_MUTE"), (0x08, "LED_MISC"), (0x09, "LED_MAIL"), (0x0A, "LED_CHARGING"), (0x0F, "LED_MAX"), (0x0F + 1, "LED_CNT"), ) LED_TYPE_CODES = ( ('numlock', 0x00), ('capslock', 0x01), ('scrolllock', 0x02), ('compose', 0x03), ('kana', 0x04), ('sleep', 0x05), ('suspend', 0x06), ('mute', 0x07), ('misc', 0x08), ('mail', 0x09), ('charging', 0x0A), ('max', 0x0F), ('cnt', 0x0F + 1), ) AUTOREPEAT_VALUES = ( (0x00, "REP_DELAY"), (0x01, "REP_PERIOD"), (0x01, "REP_MAX"), (0x01 + 1, "REP_CNT"), ) SOUNDS = ( (0x00, "SND_CLICK"), (0x01, "SND_BELL"), (0x02, "SND_TONE"), (0x07, "SND_MAX"), (0x07 + 1, "SND_CNT"), ) WIN_KEYBOARD_CODES = { 0x0100: 1, 0x0101: 0, 0x104: 1, 0x105: 0, } WIN_MOUSE_CODES = { 0x0201: (0x110, 1, 589825), # WM_LBUTTONDOWN --> BTN_LEFT 0x0202: (0x110, 0, 589825), # WM_LBUTTONUP --> BTN_LEFT 0x0204: (0x111, 1, 589826), # WM_RBUTTONDOWN --> BTN_RIGHT 0x0205: (0x111, 0, 589826), # WM_RBUTTONUP --> BTN_RIGHT 0x0207: (0x112, 1, 589827), # WM_MBUTTONDOWN --> BTN_MIDDLE 0x0208: (0x112, 0, 589827), # WM_MBUTTONU --> BTN_MIDDLE 0x020B: (0x113, 1, 589828), # WM_XBUTTONDOWN --> BTN_SIDE 0x020C: (0x113, 0, 589828), # WM_XBUTTONUP --> BTN_SIDE 0x020B2: (0x114, 1, 589829), # WM_XBUTTONDOWN --> BTN_EXTRA 0x020C2: (0x114, 0, 589829), # WM_XBUTTONUP --> BTN_EXTRA } # THING SING That thing can sing! # SONG LONG A long, long song. # Good-bye, Thing. You sing too long. # pylint: disable=too-many-lines WINCODES = ( (0x01, 0x110), # Left mouse button (0x02, 0x111), # Right mouse button (0x03, 0), # Control-break processing (0x04, 0x112), # Middle mouse button (three-button mouse) (0x05, 0x113), # X1 mouse button (0x06, 0x114), # X2 mouse button (0x07, 0), # Undefined (0x08, 14), # BACKSPACE key (0x09, 15), # TAB key (0x0A, 0), # Reserved (0x0B, 0), # Reserved (0x0C, 0x163), # CLEAR key (0x0D, 28), # ENTER key (0x0E, 0), # Undefined (0x0F, 0), # Undefined (0x10, 42), # SHIFT key (0x11, 29), # CTRL key (0x12, 56), # ALT key (0x13, 119), # PAUSE key (0x14, 58), # CAPS LOCK key (0x15, 90), # IME Kana mode (0x15, 91), # IME Hanguel mode (maintained for compatibility; use # VK_HANGUL) (0x15, 91), # IME Hangul mode (0x16, 0), # Undefined (0x17, 92), # IME Junja mode - These all need to be fixed (0x18, 93), # IME final mode - By someone who (0x19, 94), # IME Hanja mode - Knows how (0x19, 95), # IME Kanji mode - Japanese Keyboards work (0x1A, 0), # Undefined (0x1B, 1), # ESC key (0x1C, 0), # IME convert (0x1D, 0), # IME nonconvert (0x1E, 0), # IME accept (0x1F, 0), # IME mode change request (0x20, 57), # SPACEBAR (0x21, 104), # PAGE UP key (0x22, 109), # PAGE DOWN key (0x23, 107), # END key (0x24, 102), # HOME key (0x25, 105), # LEFT ARROW key (0x26, 103), # UP ARROW key (0x27, 106), # RIGHT ARROW key (0x28, 108), # DOWN ARROW key (0x29, 0x161), # SELECT key (0x2A, 210), # PRINT key (0x2B, 28), # EXECUTE key (0x2C, 99), # PRINT SCREEN key (0x2D, 110), # INS key (0x2E, 111), # DEL key (0x2F, 138), # HELP key (0x30, 11), # 0 key (0x31, 2), # 1 key (0x32, 3), # 2 key (0x33, 4), # 3 key (0x34, 5), # 4 key (0x35, 6), # 5 key (0x36, 7), # 6 key (0x37, 8), # 7 key (0x38, 9), # 8 key (0x39, 10), # 9 key # (0x3A-40, 0), # Undefined (0x41, 30), # A key (0x42, 48), # B key (0x43, 46), # C key (0x44, 32), # D key (0x45, 18), # E key (0x46, 33), # F key (0x47, 34), # G key (0x48, 35), # H key (0x49, 23), # I key (0x4A, 36), # J key (0x4B, 37), # K key (0x4C, 38), # L key (0x4D, 50), # M key (0x4E, 49), # N key (0x4F, 24), # O key (0x50, 25), # P key (0x51, 16), # Q key (0x52, 19), # R key (0x53, 31), # S key (0x54, 20), # T key (0x55, 22), # U key (0x56, 47), # V key (0x57, 17), # W key (0x58, 45), # X key (0x59, 21), # Y key (0x5A, 44), # Z key (0x5B, 125), # Left Windows key (Natural keyboard) (0x5C, 126), # Right Windows key (Natural keyboard) (0x5D, 139), # Applications key (Natural keyboard) (0x5E, 0), # Reserved (0x5F, 142), # Computer Sleep key (0x60, 82), # Numeric keypad 0 key (0x61, 79), # Numeric keypad 1 key (0x62, 80), # Numeric keypad 2 key (0x63, 81), # Numeric keypad 3 key (0x64, 75), # Numeric keypad 4 key (0x65, 76), # Numeric keypad 5 key (0x66, 77), # Numeric keypad 6 key (0x67, 71), # Numeric keypad 7 key (0x68, 72), # Numeric keypad 8 key (0x69, 73), # Numeric keypad 9 key (0x6A, 55), # Multiply key (0x6B, 78), # Add key (0x6C, 96), # Separator key (0x6D, 74), # Subtract key (0x6E, 83), # Decimal key (0x6F, 98), # Divide key (0x70, 59), # F1 key (0x71, 60), # F2 key (0x72, 61), # F3 key (0x73, 62), # F4 key (0x74, 63), # F5 key (0x75, 64), # F6 key (0x76, 65), # F7 key (0x77, 66), # F8 key (0x78, 67), # F9 key (0x79, 68), # F10 key (0x7A, 87), # F11 key (0x7B, 88), # F12 key (0x7C, 183), # F13 key (0x7D, 184), # F14 key (0x7E, 185), # F15 key (0x7F, 186), # F16 key (0x80, 187), # F17 key (0x81, 188), # F18 key (0x82, 189), # F19 key (0x83, 190), # F20 key (0x84, 191), # F21 key (0x85, 192), # F22 key (0x86, 192), # F23 key (0x87, 194), # F24 key # (0x88-8F, 0), # Unassigned (0x90, 69), # NUM LOCK key (0x91, 70), # SCROLL LOCK key # (0x92-96, 0), # OEM specific # (0x97-9F, 0), # Unassigned (0xA0, 42), # Left SHIFT key (0xA1, 54), # Right SHIFT key (0xA2, 29), # Left CONTROL key (0xA3, 97), # Right CONTROL key (0xA4, 125), # Left MENU key (0xA5, 126), # Right MENU key (0xA6, 158), # Browser Back key (0xA7, 159), # Browser Forward key (0xA8, 173), # Browser Refresh key (0xA9, 128), # Browser Stop key (0xAA, 217), # Browser Search key (0xAB, 0x16C), # Browser Favorites key (0xAC, 150), # Browser Start and Home key (0xAD, 113), # Volume Mute key (0xAE, 114), # Volume Down key (0xAF, 115), # Volume Up key (0xB0, 163), # Next Track key (0xB1, 165), # Previous Track key (0xB2, 166), # Stop Media key (0xB3, 164), # Play/Pause Media key (0xB4, 155), # Start Mail key (0xB5, 0x161), # Select Media key (0xB6, 148), # Start Application 1 key (0xB7, 149), # Start Application 2 key # (0xB8-B9, 0), # Reserved (0xBA, 39), # Used for miscellaneous characters; it can vary by keyboard. (0xBB, 13), # For any country/region, the '+' key (0xBC, 51), # For any country/region, the ',' key (0xBD, 12), # For any country/region, the '-' key (0xBE, 52), # For any country/region, the '.' key (0xBF, 53), # Slash (0xC0, 40), # Apostrophe # (0xC1-D7, 0), # Reserved # (0xD8-DA, 0), # Unassigned (0xDB, 26), # [ (0xDC, 86), # \ (0xDD, 27), # ] (0xDE, 43), # ' (0xDF, 119), # VK_OFF - What's that? (0xE0, 0), # Reserved (0xE1, 0), # OEM Specific (0xE2, 43), # Either the angle bracket key or the backslash key # on the RT 102-key keyboard (0xE3-E4, 0), # OEM # specific (0xE5, 0), # IME PROCESS key (0xE6, 0), # OEM specific (0xE7, 0), # Used to pass Unicode characters as if they were # keystrokes. The VK_PACKET key is the low word of a # 32-bit Virtual Key value used for non-keyboard input # methods. For more information, see Remark in # KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP (0xE8, 0), # Unassigned # (0xE9-F5, 0), # OEM specific (0xF6, 0), # Attn key (0xF7, 0), # CrSel key (0xF8, 0), # ExSel key (0xF9, 222), # Erase EOF key (0xFA, 207), # Play key (0xFB, 0x174), # Zoom key (0xFC, 0), # Reserved (0xFD, 0x19B), # PA1 key (0xFE, 0x163), # Clear key (0xFF, 185), ) MAC_EVENT_CODES = ( # NSLeftMouseDown Quartz.kCGEventLeftMouseDown (1, ("Key", 0x110, 1, 589825)), # NSLeftMouseUp Quartz.kCGEventLeftMouseUp (2, ("Key", 0x110, 0, 589825)), # NSRightMouseDown Quartz.kCGEventRightMouseDown (3, ("Key", 0x111, 1, 589826)), # NSRightMouseUp Quartz.kCGEventRightMouseUp (4, ("Key", 0x111, 0, 589826)), (5, (None, 0, 0, 0)), # NSMouseMoved Quartz.kCGEventMouseMoved (6, (None, 0, 0, 0)), # NSLeftMouseDragged Quartz.kCGEventLeftMouseDragged # NSRightMouseDragged Quartz.kCGEventRightMouseDragged (7, (None, 0, 0, 0)), (8, (None, 0, 0, 0)), # NSMouseEntered (9, (None, 0, 0, 0)), # NSMouseExited (10, (None, 0, 0, 0)), # NSKeyDown (11, (None, 0, 0, 0)), # NSKeyUp (12, (None, 0, 0, 0)), # NSFlagsChanged (13, (None, 0, 0, 0)), # NSAppKitDefined (14, (None, 0, 0, 0)), # NSSystemDefined (15, (None, 0, 0, 0)), # NSApplicationDefined (16, (None, 0, 0, 0)), # NSPeriodic (17, (None, 0, 0, 0)), # NSCursorUpdate (22, (None, 0, 0, 0)), # NSScrollWheel Quartz.kCGEventScrollWheel (23, (None, 0, 0, 0)), # NSTabletPoint Quartz.kCGEventTabletPointer (24, (None, 0, 0, 0)), # NSTabletProximity Quartz.kCGEventTabletProximity (25, (None, 0, 0, 0)), # NSOtherMouseDown Quartz.kCGEventOtherMouseDown (25.2, ("Key", 0x112, 1, 589827)), # BTN_MIDDLE (25.3, ("Key", 0x113, 1, 589828)), # BTN_SIDE (25.4, ("Key", 0x114, 1, 589829)), # BTN_EXTRA (26, (None, 0, 0, 0)), # NSOtherMouseUp Quartz.kCGEventOtherMouseUp (26.2, ("Key", 0x112, 0, 589827)), # BTN_MIDDLE (26.3, ("Key", 0x113, 0, 589828)), # BTN_SIDE (26.4, ("Key", 0x114, 0, 589829)), # BTN_EXTRA (27, (None, 0, 0, 0)), # NSOtherMouseDragged (29, (None, 0, 0, 0)), # NSEventTypeGesture (30, (None, 0, 0, 0)), # NSEventTypeMagnify (31, (None, 0, 0, 0)), # NSEventTypeSwipe (18, (None, 0, 0, 0)), # NSEventTypeRotate (19, (None, 0, 0, 0)), # NSEventTypeBeginGesture (20, (None, 0, 0, 0)), # NSEventTypeEndGesture (27, (None, 0, 0, 0)), # Quartz.kCGEventOtherMouseDragged (32, (None, 0, 0, 0)), # NSEventTypeSmartMagnify (33, (None, 0, 0, 0)), # NSEventTypeQuickLook (34, (None, 0, 0, 0)), # NSEventTypePressure ) MAC_KEYS = ( (0x00, 30), # kVK_ANSI_A (0x01, 31), # kVK_ANSI_S (0x02, 32), # kVK_ANSI_D (0x03, 33), # kVK_ANSI_F (0x04, 35), # kVK_ANSI_H (0x05, 34), # kVK_ANSI_G (0x06, 44), # kVK_ANSI_Z (0x07, 45), # kVK_ANSI_X (0x08, 46), # kVK_ANSI_C (0x09, 47), # kVK_ANSI_V (0x0B, 48), # kVK_ANSI_B (0x0C, 16), # kVK_ANSI_Q (0x0D, 17), # kVK_ANSI_W (0x0E, 18), # kVK_ANSI_E (0x0F, 33), # kVK_ANSI_R (0x10, 21), # kVK_ANSI_Y (0x11, 20), # kVK_ANSI_T (0x12, 2), # kVK_ANSI_1 (0x13, 3), # kVK_ANSI_2 (0x14, 4), # kVK_ANSI_3 (0x15, 5), # kVK_ANSI_4 (0x16, 7), # kVK_ANSI_6 (0x17, 6), # kVK_ANSI_5 (0x18, 13), # kVK_ANSI_Equal (0x19, 10), # kVK_ANSI_9 (0x1A, 8), # kVK_ANSI_7 (0x1B, 12), # kVK_ANSI_Minus (0x1C, 9), # kVK_ANSI_8 (0x1D, 11), # kVK_ANSI_0 (0x1E, 27), # kVK_ANSI_RightBracket (0x1F, 24), # kVK_ANSI_O (0x20, 22), # kVK_ANSI_U (0x21, 26), # kVK_ANSI_LeftBracket (0x22, 23), # kVK_ANSI_I (0x23, 25), # kVK_ANSI_P (0x25, 38), # kVK_ANSI_L (0x26, 36), # kVK_ANSI_J (0x27, 40), # kVK_ANSI_Quote (0x28, 37), # kVK_ANSI_K (0x29, 39), # kVK_ANSI_Semicolon (0x2A, 43), # kVK_ANSI_Backslash (0x2B, 51), # kVK_ANSI_Comma (0x2C, 53), # kVK_ANSI_Slash (0x2D, 49), # kVK_ANSI_N (0x2E, 50), # kVK_ANSI_M (0x2F, 52), # kVK_ANSI_Period (0x32, 41), # kVK_ANSI_Grave (0x41, 83), # kVK_ANSI_KeypadDecimal (0x43, 55), # kVK_ANSI_KeypadMultiply (0x45, 78), # kVK_ANSI_KeypadPlus (0x47, 69), # kVK_ANSI_KeypadClear (0x4B, 98), # kVK_ANSI_KeypadDivide (0x4C, 96), # kVK_ANSI_KeypadEnter (0x4E, 74), # kVK_ANSI_KeypadMinus (0x51, 117), # kVK_ANSI_KeypadEquals (0x52, 82), # kVK_ANSI_Keypad0 (0x53, 79), # kVK_ANSI_Keypad1 (0x54, 80), # kVK_ANSI_Keypad2 (0x55, 81), # kVK_ANSI_Keypad3 (0x56, 75), # kVK_ANSI_Keypad4 (0x57, 76), # kVK_ANSI_Keypad5 (0x58, 77), # kVK_ANSI_Keypad6 (0x59, 71), # kVK_ANSI_Keypad7 (0x5B, 72), # kVK_ANSI_Keypad8 (0x5C, 73), # kVK_ANSI_Keypad9 (0x24, 28), # kVK_Return (0x30, 15), # kVK_Tab (0x31, 57), # kVK_Space (0x33, 111), # kVK_Delete (0x35, 1), # kVK_Escape (0x37, 125), # kVK_Command (0x38, 42), # kVK_Shift (0x39, 58), # kVK_CapsLock (0x3A, 56), # kVK_Option (0x3B, 29), # kVK_Control (0x3C, 54), # kVK_RightShift (0x3D, 100), # kVK_RightOption (0x3E, 126), # kVK_RightControl (0x36, 126), # Right Meta (0x3F, 0x1D0), # kVK_Function (0x40, 187), # kVK_F17 (0x48, 115), # kVK_VolumeUp (0x49, 114), # kVK_VolumeDown (0x4A, 113), # kVK_Mute (0x4F, 188), # kVK_F18 (0x50, 189), # kVK_F19 (0x5A, 190), # kVK_F20 (0x60, 63), # kVK_F5 (0x61, 64), # kVK_F6 (0x62, 65), # kVK_F7 (0x63, 61), # kVK_F3 (0x64, 66), # kVK_F8 (0x65, 67), # kVK_F9 (0x67, 87), # kVK_F11 (0x69, 183), # kVK_F13 (0x6A, 186), # kVK_F16 (0x6B, 184), # kVK_F14 (0x6D, 68), # kVK_F10 (0x6F, 88), # kVK_F12 (0x71, 185), # kVK_F15 (0x72, 138), # kVK_Help (0x73, 102), # kVK_Home (0x74, 104), # kVK_PageUp (0x75, 111), # kVK_ForwardDelete (0x76, 62), # kVK_F4 (0x77, 107), # kVK_End (0x78, 60), # kVK_F2 (0x79, 109), # kVK_PageDown (0x7A, 59), # kVK_F1 (0x7B, 105), # kVK_LeftArrow (0x7C, 106), # kVK_RightArrow (0x7D, 108), # kVK_DownArrow (0x7E, 103), # kVK_UpArrow (0x0A, 170), # kVK_ISO_Section (0x5D, 124), # kVK_JIS_Yen (0x5E, 92), # kVK_JIS_Underscore (0x5F, 95), # kVK_JIS_KeypadComma (0x66, 94), # kVK_JIS_Eisu (0x68, 90), # kVK_JIS_Kana ) # We have yet to support force feedback but probably should # eventually: FORCE_FEEDBACK = () # Motor in gamepad FORCE_FEEDBACK_STATUS = () # Status of motor POWER = () # Power switch # These two are internal workings of evdev we probably will never care # about. MAX = () CURRENT = () EVENT_MAP = ( ('types', EVENT_TYPES), ('type_codes', ((value, key) for key, value in EVENT_TYPES)), ('wincodes', WINCODES), ('specials', SPECIAL_DEVICES), ('xpad', XINPUT_MAPPING), ('Sync', SYNCHRONIZATION_EVENTS), ('Key', KEYS_AND_BUTTONS), ('Relative', RELATIVE_AXES), ('Absolute', ABSOLUTE_AXES), ('Misc', MISC_EVENTS), ('Switch', SWITCH_EVENTS), ('LED', LEDS), ('LED_type_codes', LED_TYPE_CODES), ('Sound', SOUNDS), ('Repeat', AUTOREPEAT_VALUES), ('ForceFeedback', FORCE_FEEDBACK), ('Power', POWER), ('ForceFeedbackStatus', FORCE_FEEDBACK_STATUS), ('Max', MAX), ('Current', CURRENT), ) # Evdev style paths for the Mac APPKIT_KB_PATH = "/dev/input/by-id/usb-AppKit_Keyboard-event-kbd" QUARTZ_MOUSE_PATH = "/dev/input/by-id/usb-Quartz_Mouse-event-mouse" APPKIT_MOUSE_PATH = "/dev/input/by-id/usb-AppKit_Mouse-event-mouse" # Now comes all the structs we need to parse the infomation coming # from Windows. class KBDLLHookStruct(ctypes.Structure): """Contains information about a low-level keyboard input event. For full details see Microsoft's documentation: https://msdn.microsoft.com/en-us/library/windows/desktop/ ms644967%28v=vs.85%29.aspx """ # pylint: disable=too-few-public-methods _fields_ = [ ("vk_code", DWORD), ("scan_code", DWORD), ("flags", DWORD), ("time", ctypes.c_int), ] class MSLLHookStruct(ctypes.Structure): """Contains information about a low-level mouse input event. For full details see Microsoft's documentation: https://msdn.microsoft.com/en-us/library/windows/desktop/ ms644970%28v=vs.85%29.aspx """ # pylint: disable=too-few-public-methods _fields_ = [ ("x_pos", ctypes.c_long), ("y_pos", ctypes.c_long), ('reserved', ctypes.c_short), ('mousedata', ctypes.c_short), ("flags", DWORD), ("time", DWORD), ("extrainfo", ctypes.c_ulong), ] class XinputGamepad(ctypes.Structure): """Describes the current state of the Xbox 360 Controller. For full details see Microsoft's documentation: https://msdn.microsoft.com/en-us/library/windows/desktop/ microsoft.directx_sdk.reference.xinput_gamepad%28v=vs.85%29.aspx """ # pylint: disable=too-few-public-methods _fields_ = [ ('buttons', ctypes.c_ushort), # wButtons ('left_trigger', ctypes.c_ubyte), # bLeftTrigger ('right_trigger', ctypes.c_ubyte), # bLeftTrigger ('l_thumb_x', ctypes.c_short), # sThumbLX ('l_thumb_y', ctypes.c_short), # sThumbLY ('r_thumb_x', ctypes.c_short), # sThumbRx ('r_thumb_y', ctypes.c_short), # sThumbRy ] class XinputState(ctypes.Structure): """Represents the state of a controller. For full details see Microsoft's documentation: https://msdn.microsoft.com/en-us/library/windows/desktop/ microsoft.directx_sdk.reference.xinput_state%28v=vs.85%29.aspx """ # pylint: disable=too-few-public-methods _fields_ = [ ('packet_number', ctypes.c_ulong), # dwPacketNumber ('gamepad', XinputGamepad), # Gamepad ] class XinputVibration(ctypes.Structure): """Specifies motor speed levels for the vibration function of a controller. For full details see Microsoft's documentation: https://msdn.microsoft.com/en-us/library/windows/desktop/ microsoft.directx_sdk.reference.xinput_vibration%28v=vs.85%29.aspx """ # pylint: disable=too-few-public-methods _fields_ = [ ("wLeftMotorSpeed", ctypes.c_ushort), ("wRightMotorSpeed", ctypes.c_ushort), ] if sys.version_info.major == 2: # pylint: disable=redefined-builtin class PermissionError(IOError): """Raised when trying to run an operation without the adequate access rights - for example filesystem permissions. Corresponds to errno EACCES and EPERM.""" class UnpluggedError(RuntimeError): """The device requested is not plugged in.""" pass class NoDevicePath(RuntimeError): """No evdev device path was given.""" pass class UnknownEventType(IndexError): """We don't know what this event is.""" pass class UnknownEventCode(IndexError): """We don't know what this event is.""" pass class InputEvent: """A user event.""" # pylint: disable=too-few-public-methods def __init__(self, device, event_info): self.device = device self.timestamp = event_info["timestamp"] self.code = event_info["code"] self.state = event_info["state"] self.ev_type = event_info["ev_type"] class BaseListener: """Loosely emulate Evdev keyboard behaviour on other platforms. Listen (hook in Windows terminology) for key events then buffer them in a pipe. """ def __init__(self, pipe, events=None, codes=None): self.pipe = pipe self.events = events if events else [] self.codes = codes if codes else None self.app = None self.timeval = None self.type_codes = dict(EVENT_TYPES) self.install_handle_input() def install_handle_input(self): """Install the input handler.""" pass def uninstall_handle_input(self): """Un-install the input handler.""" pass def __del__(self): """Clean up when deleted.""" self.uninstall_handle_input() @staticmethod def get_timeval(): """Get the time in seconds and microseconds.""" return convert_timeval(time.time()) def update_timeval(self): """Update the timeval with the current time.""" self.timeval = self.get_timeval() def create_event_object(self, event_type, code, value, timeval=None): """Create an evdev style structure.""" if not timeval: self.update_timeval() timeval = self.timeval try: event_code = self.type_codes[event_type] except KeyError: raise UnknownEventType( "We don't know what kind of event a %s is." % event_type ) event = struct.pack( EVENT_FORMAT, timeval[0], timeval[1], event_code, code, value ) return event def write_to_pipe(self, event_list): """Send event back to the mouse object.""" self.pipe.send_bytes(b''.join(event_list)) def emulate_wheel(self, data, direction, timeval): """Emulate rel values for the mouse wheel. In evdev, a single click forwards of the mouse wheel is 1 and a click back is -1. Windows uses 120 and -120. We floor divide the Windows number by 120. This is fine for the digital scroll wheels found on the vast majority of mice. It also works on the analogue ball on the top of the Apple mouse. What do the analogue scroll wheels found on 200 quid high end gaming mice do? If the lowest unit is 120 then we are okay. If they report changes of less than 120 units Windows, then this might be an unacceptable loss of precision. Needless to say, I don't have such a mouse to test one way or the other. """ if direction == 'x': code = 0x06 elif direction == 'z': # Not enitely sure if this exists code = 0x07 else: code = 0x08 if WIN: data = data // 120 return self.create_event_object("Relative", code, data, timeval) def emulate_rel(self, key_code, value, timeval): """Emulate the relative changes of the mouse cursor.""" return self.create_event_object("Relative", key_code, value, timeval) def emulate_press(self, key_code, scan_code, value, timeval): """Emulate a button press. Currently supports 5 buttons. The Microsoft documentation does not define what happens with a mouse with more than five buttons, and I don't have such a mouse. From reading the Linux sources, I guess evdev can support up to 255 buttons. Therefore, I guess we could support more buttons quite easily, if we had any useful hardware. """ scan_event = self.create_event_object("Misc", 0x04, scan_code, timeval) key_event = self.create_event_object("Key", key_code, value, timeval) return scan_event, key_event def emulate_repeat(self, value, timeval): """The repeat press of a key/mouse button, e.g. double click.""" repeat_event = self.create_event_object("Repeat", 2, value, timeval) return repeat_event def sync_marker(self, timeval): """Separate groups of events.""" return self.create_event_object("Sync", 0, 0, timeval) def emulate_abs(self, x_val, y_val, timeval): """Emulate the absolute co-ordinates of the mouse cursor.""" x_event = self.create_event_object("Absolute", 0x00, x_val, timeval) y_event = self.create_event_object("Absolute", 0x01, y_val, timeval) return x_event, y_event class WindowsKeyboardListener(BaseListener): """Loosely emulate Evdev keyboard behaviour on Windows. Listen (hook in Windows terminology) for key events then buffer them in a pipe. """ def __init__(self, pipe, codes=None): self.pipe = pipe self.hooked = None self.pointer = None super(WindowsKeyboardListener, self).__init__(pipe, codes) @staticmethod def listen(): """Listen for keyboard input.""" msg = MSG() ctypes.windll.user32.GetMessageA(ctypes.byref(msg), 0, 0, 0) def get_fptr(self): """Get the function pointer.""" cmpfunc = ctypes.CFUNCTYPE( ctypes.c_int, WPARAM, LPARAM, ctypes.POINTER(KBDLLHookStruct) ) return cmpfunc(self.handle_input) def install_handle_input(self): """Install the hook.""" self.pointer = self.get_fptr() self.hooked = ctypes.windll.user32.SetWindowsHookExA( 13, self.pointer, ctypes.windll.kernel32.GetModuleHandleW(None), 0 ) if not self.hooked: return False return True def uninstall_handle_input(self): """Remove the hook.""" if self.hooked is None: return ctypes.windll.user32.UnhookWindowsHookEx(self.hooked) self.hooked = None def handle_input(self, ncode, wparam, lparam): """Process the key input.""" value = WIN_KEYBOARD_CODES[wparam] scan_code = lparam.contents.scan_code vk_code = lparam.contents.vk_code self.update_timeval() events = [] # Add key event scan_key, key_event = self.emulate_press( vk_code, scan_code, value, self.timeval ) events.append(scan_key) events.append(key_event) # End with a sync marker events.append(self.sync_marker(self.timeval)) # We are done self.write_to_pipe(events) return ctypes.windll.user32.CallNextHookEx(self.hooked, ncode, wparam, lparam) def keyboard_process(pipe): """Single subprocess for reading keyboard events on Windows.""" keyboard = WindowsKeyboardListener(pipe) keyboard.listen() class WindowsMouseListener(BaseListener): """Loosely emulate Evdev mouse behaviour on Windows. Listen (hook in Windows terminology) for key events then buffer them in a pipe. """ def __init__(self, pipe): self.pipe = pipe self.hooked = None self.pointer = None self.mouse_codes = WIN_MOUSE_CODES super(WindowsMouseListener, self).__init__(pipe) @staticmethod def listen(): """Listen for mouse input.""" msg = MSG() ctypes.windll.user32.GetMessageA(ctypes.byref(msg), 0, 0, 0) def get_fptr(self): """Get the function pointer.""" cmpfunc = ctypes.CFUNCTYPE( ctypes.c_int, WPARAM, LPARAM, ctypes.POINTER(MSLLHookStruct) ) return cmpfunc(self.handle_input) def install_handle_input(self): """Install the hook.""" self.pointer = self.get_fptr() self.hooked = ctypes.windll.user32.SetWindowsHookExA( 14, self.pointer, ctypes.windll.kernel32.GetModuleHandleW(None), 0 ) if not self.hooked: return False return True def uninstall_handle_input(self): """Remove the hook.""" if self.hooked is None: return ctypes.windll.user32.UnhookWindowsHookEx(self.hooked) self.hooked = None def handle_input(self, ncode, wparam, lparam): """Process the key input.""" x_pos = lparam.contents.x_pos y_pos = lparam.contents.y_pos data = lparam.contents.mousedata # This is how we can distinguish mouse 1 from mouse 2 # extrainfo = lparam.contents.extrainfo # The way windows seems to do it is there is primary mouse # and all other mouses report as mouse 2 # Also useful later will be to support the flags field # flags = lparam.contents.flags # This shows if the event was from a real device or whether it # was injected somehow via software self.emulate_mouse(wparam, x_pos, y_pos, data) # Give back control to Windows to wait for and process the # next event return ctypes.windll.user32.CallNextHookEx(self.hooked, ncode, wparam, lparam) def emulate_mouse(self, key_code, x_val, y_val, data): """Emulate the ev codes using the data Windows has given us. Note that by default in Windows, to recognise a double click, you just notice two clicks in a row within a reasonablely short time period. However, if the application developer sets the application window's class style to CS_DBLCLKS, the operating system will notice the four button events (down, up, down, up), intercept them and then send a single key code instead. There are no such special double click codes on other platforms, so not obvious what to do with them. It might be best to just convert them back to four events. Currently we do nothing. ((0x0203, 'WM_LBUTTONDBLCLK'), (0x0206, 'WM_RBUTTONDBLCLK'), (0x0209, 'WM_MBUTTONDBLCLK'), (0x020D, 'WM_XBUTTONDBLCLK')) """ # Once again ignore Windows' relative time (since system # startup) and use the absolute time (since epoch i.e. 1st Jan # 1970). self.update_timeval() events = [] if key_code == 0x0200: # We have a mouse move alone. # So just pass through to below pass elif key_code == 0x020A: # We have a vertical mouse wheel turn events.append(self.emulate_wheel(data, 'y', self.timeval)) elif key_code == 0x020E: # We have a horizontal mouse wheel turn # https://msdn.microsoft.com/en-us/library/windows/desktop/ # ms645614%28v=vs.85%29.aspx events.append(self.emulate_wheel(data, 'x', self.timeval)) else: # We have a button press. # Distinguish the second extra button if key_code == 0x020B and data == 2: key_code = 0x020B2 elif key_code == 0x020C and data == 2: key_code = 0x020C2 # Get the mouse codes code, value, scan_code = self.mouse_codes[key_code] # Add in the press events scan_event, key_event = self.emulate_press( code, scan_code, value, self.timeval ) events.append(scan_event) events.append(key_event) # Add in the absolute position of the mouse cursor x_event, y_event = self.emulate_abs(x_val, y_val, self.timeval) events.append(x_event) events.append(y_event) # End with a sync marker events.append(self.sync_marker(self.timeval)) # We are done self.write_to_pipe(events) def mouse_process(pipe): """Single subprocess for reading mouse events on Windows.""" mouse = WindowsMouseListener(pipe) mouse.listen() class QuartzMouseBaseListener(BaseListener): """Emulate evdev mouse behaviour on mac.""" def __init__(self, pipe): super(QuartzMouseBaseListener, self).__init__(pipe, codes=dict(MAC_EVENT_CODES)) self.active = True self.events = [] def _get_mouse_button_number(self, event): """Get the mouse button number from an event.""" raise NotImplementedError def _get_click_state(self, event): """The click state from an event.""" raise NotImplementedError def _get_scroll(self, event): """The scroll values from an event.""" raise NotImplementedError def _get_absolute(self, event): """Get abolute cursor location.""" raise NotImplementedError def _get_relative(self, event): """Get the relative mouse movement.""" raise NotImplementedError def handle_button(self, event, event_type): """Convert the button information from quartz into evdev format.""" # 0 for left # 1 for right # 2 for middle/center # 3 for side mouse_button_number = self._get_mouse_button_number(event) # Identify buttons 3,4,5 if event_type in (25, 26): event_type = event_type + (mouse_button_number * 0.1) # Add buttons to events event_type_string, event_code, value, scan = self.codes[event_type] if event_type_string == "Key": scan_event, key_event = self.emulate_press( event_code, scan, value, self.timeval ) self.events.append(scan_event) self.events.append(key_event) # doubleclick/n-click of button click_state = self._get_click_state(event) repeat = self.emulate_repeat(click_state, self.timeval) self.events.append(repeat) def handle_scrollwheel(self, event): """Handle the scrollwheel (it is a ball on the mighty mouse).""" # relative Scrollwheel scroll_x, scroll_y = self._get_scroll(event) if scroll_x: self.events.append(self.emulate_wheel(scroll_x, 'x', self.timeval)) if scroll_y: self.events.append(self.emulate_wheel(scroll_y, 'y', self.timeval)) def handle_absolute(self, event): """Absolute mouse position on the screen.""" (x_val, y_val) = self._get_absolute(event) x_event, y_event = self.emulate_abs(int(x_val), int(y_val), self.timeval) self.events.append(x_event) self.events.append(y_event) def handle_relative(self, event): """Relative mouse movement.""" delta_x, delta_y = self._get_relative(event) if delta_x: self.events.append(self.emulate_rel(0x00, delta_x, self.timeval)) if delta_y: self.events.append(self.emulate_rel(0x01, delta_y, self.timeval)) # pylint: disable=unused-argument def handle_input(self, proxy, event_type, event, refcon): """Handle an input event.""" self.update_timeval() self.events = [] if event_type in (1, 2, 3, 4, 25, 26, 27): self.handle_button(event, event_type) if event_type == 22: self.handle_scrollwheel(event) # Add in the absolute position of the mouse cursor self.handle_absolute(event) # Add in the relative position of the mouse cursor self.handle_relative(event) # End with a sync marker self.events.append(self.sync_marker(self.timeval)) # We are done self.write_to_pipe(self.events) def quartz_mouse_process(pipe): """Single subprocess for reading mouse events on Mac using newer Quartz.""" # Quartz only on the mac, so don't warn about Quartz # pylint: disable=import-error import Quartz # pylint: disable=no-member class QuartzMouseListener(QuartzMouseBaseListener): """Loosely emulate Evdev mouse behaviour on the Macs. Listen for key events then buffer them in a pipe. """ def install_handle_input(self): """Constants below listed at: https://developer.apple.com/documentation/coregraphics/ cgeventtype?language=objc#topics """ # Keep Mac Names to make it easy to find the documentation # pylint: disable=invalid-name NSMachPort = Quartz.CGEventTapCreate( Quartz.kCGSessionEventTap, Quartz.kCGHeadInsertEventTap, Quartz.kCGEventTapOptionDefault, Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDown) | Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseUp) | Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDown) | Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseUp) | Quartz.CGEventMaskBit(Quartz.kCGEventMouseMoved) | Quartz.CGEventMaskBit(Quartz.kCGEventLeftMouseDragged) | Quartz.CGEventMaskBit(Quartz.kCGEventRightMouseDragged) | Quartz.CGEventMaskBit(Quartz.kCGEventScrollWheel) | Quartz.CGEventMaskBit(Quartz.kCGEventTabletPointer) | Quartz.CGEventMaskBit(Quartz.kCGEventTabletProximity) | Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDown) | Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseUp) | Quartz.CGEventMaskBit(Quartz.kCGEventOtherMouseDragged), self.handle_input, None, ) CFRunLoopSourceRef = Quartz.CFMachPortCreateRunLoopSource( None, NSMachPort, 0 ) CFRunLoopRef = Quartz.CFRunLoopGetCurrent() Quartz.CFRunLoopAddSource( CFRunLoopRef, CFRunLoopSourceRef, Quartz.kCFRunLoopDefaultMode ) Quartz.CGEventTapEnable(NSMachPort, True) def listen(self): """Listen for quartz events.""" while self.active: Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 5, False) def uninstall_handle_input(self): self.active = False def _get_mouse_button_number(self, event): """Get the mouse button number from an event.""" return Quartz.CGEventGetIntegerValueField( event, Quartz.kCGMouseEventButtonNumber ) def _get_click_state(self, event): """The click state from an event.""" return Quartz.CGEventGetIntegerValueField( event, Quartz.kCGMouseEventClickState ) def _get_scroll(self, event): """The scroll values from an event.""" scroll_y = Quartz.CGEventGetIntegerValueField( event, Quartz.kCGScrollWheelEventDeltaAxis1 ) scroll_x = Quartz.CGEventGetIntegerValueField( event, Quartz.kCGScrollWheelEventDeltaAxis2 ) return scroll_x, scroll_y def _get_absolute(self, event): """Get abolute cursor location.""" return Quartz.CGEventGetLocation(event) def _get_relative(self, event): """Get the relative mouse movement.""" delta_x = Quartz.CGEventGetIntegerValueField( event, Quartz.kCGMouseEventDeltaX ) delta_y = Quartz.CGEventGetIntegerValueField( event, Quartz.kCGMouseEventDeltaY ) return delta_x, delta_y mouse = QuartzMouseListener(pipe) mouse.listen() class AppKitMouseBaseListener(BaseListener): """Emulate evdev behaviour on the the Mac.""" def __init__(self, pipe, events=None): super(AppKitMouseBaseListener, self).__init__( pipe, events, codes=dict(MAC_EVENT_CODES) ) @staticmethod def _get_mouse_button_number(event): """Get the button number.""" return event.buttonNumber() @staticmethod def _get_absolute(event): """Get the absolute (pixel) location of the mouse cursor.""" return event.locationInWindow() @staticmethod def _get_event_type(event): """Get the appkit event type of the event.""" return event.type() @staticmethod def _get_deltas(event): """Get the changes from the appkit event.""" delta_x = round(event.deltaX()) delta_y = round(event.deltaY()) delta_z = round(event.deltaZ()) return delta_x, delta_y, delta_z def handle_button(self, event, event_type): """Handle mouse click.""" mouse_button_number = self._get_mouse_button_number(event) # Identify buttons 3,4,5 if event_type in (25, 26): event_type = event_type + (mouse_button_number * 0.1) # Add buttons to events event_type_name, event_code, value, scan = self.codes[event_type] if event_type_name == "Key": scan_event, key_event = self.emulate_press( event_code, scan, value, self.timeval ) self.events.append(scan_event) self.events.append(key_event) def handle_absolute(self, event): """Absolute mouse position on the screen.""" point = self._get_absolute(event) x_pos = round(point.x) y_pos = round(point.y) x_event, y_event = self.emulate_abs(x_pos, y_pos, self.timeval) self.events.append(x_event) self.events.append(y_event) def handle_scrollwheel(self, event): """Make endev from appkit scroll wheel event.""" delta_x, delta_y, delta_z = self._get_deltas(event) if delta_x: self.events.append(self.emulate_wheel(delta_x, 'x', self.timeval)) if delta_y: self.events.append(self.emulate_wheel(delta_y, 'y', self.timeval)) if delta_z: self.events.append(self.emulate_wheel(delta_z, 'z', self.timeval)) def handle_relative(self, event): """Get the position of the mouse on the screen.""" delta_x, delta_y, delta_z = self._get_deltas(event) if delta_x: self.events.append(self.emulate_rel(0x00, delta_x, self.timeval)) if delta_y: self.events.append(self.emulate_rel(0x01, delta_y, self.timeval)) if delta_z: self.events.append(self.emulate_rel(0x02, delta_z, self.timeval)) def handle_input(self, event): """Process the mouse event.""" self.update_timeval() self.events = [] code = self._get_event_type(event) # Deal with buttons self.handle_button(event, code) # Mouse wheel if code == 22: self.handle_scrollwheel(event) # Other relative mouse movements else: self.handle_relative(event) # Add in the absolute position of the mouse cursor self.handle_absolute(event) # End with a sync marker self.events.append(self.sync_marker(self.timeval)) # We are done self.write_to_pipe(self.events) def appkit_mouse_process(pipe): """Single subprocess for reading mouse events on Mac using older AppKit.""" # pylint: disable=import-error,too-many-locals # Note Objective C does not support a Unix style fork. # So these imports have to be inside the child subprocess since # otherwise the child process cannot use them. # pylint: disable=no-member, no-name-in-module from Foundation import NSObject from AppKit import NSApplication, NSApp from Cocoa import ( NSEvent, NSLeftMouseDownMask, NSLeftMouseUpMask, NSRightMouseDownMask, NSRightMouseUpMask, NSMouseMovedMask, NSLeftMouseDraggedMask, NSRightMouseDraggedMask, NSMouseEnteredMask, NSMouseExitedMask, NSScrollWheelMask, NSOtherMouseDownMask, NSOtherMouseUpMask, ) from PyObjCTools import AppHelper import objc class MacMouseSetup(NSObject): """Setup the handler.""" @objc.python_method def init_with_handler(self, handler): """ Init method that receives the write end of the pipe. """ # ALWAYS call the super's designated initializer. # Also, make sure to re-bind "self" just in case it # returns something else! # pylint: disable=self-cls-assignment self = super(MacMouseSetup, self).init() self.handler = handler # Unlike Python's __init__, initializers MUST return self, # because they are allowed to return any object! return self # pylint: disable=invalid-name, unused-argument def applicationDidFinishLaunching_(self, notification): """Bind the listen method as the handler for mouse events.""" mask = ( NSLeftMouseDownMask | NSLeftMouseUpMask | NSRightMouseDownMask | NSRightMouseUpMask | NSMouseMovedMask | NSLeftMouseDraggedMask | NSRightMouseDraggedMask | NSScrollWheelMask | NSMouseEnteredMask | NSMouseExitedMask | NSOtherMouseDownMask | NSOtherMouseUpMask ) NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(mask, self.handler) class MacMouseListener(AppKitMouseBaseListener): """Loosely emulate Evdev mouse behaviour on the Macs. Listen for key events then buffer them in a pipe. """ def install_handle_input(self): """Install the hook.""" self.app = NSApplication.sharedApplication() # pylint: disable=no-member delegate = MacMouseSetup.alloc().init_with_handler(self.handle_input) NSApp().setDelegate_(delegate) AppHelper.runEventLoop() def __del__(self): """Stop the listener on deletion.""" AppHelper.stopEventLoop() MacMouseListener(pipe, events=[]) class AppKitKeyboardListener(BaseListener): """Emulate an evdev keyboard on the Mac.""" def __init__(self, pipe): super(AppKitKeyboardListener, self).__init__(pipe, codes=dict(MAC_KEYS)) @staticmethod def _get_event_key_code(event): """Get the key code.""" return event.keyCode() @staticmethod def _get_event_type(event): """Get the event type.""" return event.type() @staticmethod def _get_flag_value(event): """Note, this may be able to be made more accurate, i.e. handle two modifier keys at once.""" flags = event.modifierFlags() if flags == 0x100: value = 0 else: value = 1 return value def _get_key_value(self, event, event_type): """Get the key value.""" if event_type == 10: value = 1 elif event_type == 11: value = 0 elif event_type == 12: value = self._get_flag_value(event) else: value = -1 return value def handle_input(self, event): """Process they keyboard input.""" self.update_timeval() self.events = [] code = self._get_event_key_code(event) new_code = (self.codes or {}).get(code, 0) event_type = self._get_event_type(event) value = self._get_key_value(event, event_type) scan_event, key_event = self.emulate_press(new_code, code, value, self.timeval) self.events.append(scan_event) self.events.append(key_event) # End with a sync marker self.events.append(self.sync_marker(self.timeval)) # We are done self.write_to_pipe(self.events) def mac_keyboard_process(pipe): """Single subprocesses for reading keyboard on Mac.""" # pylint: disable=import-error,too-many-locals # Note Objective C does not support a Unix style fork. # So these imports have to be inside the child subprocess since # otherwise the child process cannot use them. # pylint: disable=no-member, no-name-in-module from AppKit import NSApplication, NSApp from Foundation import NSObject from Cocoa import NSEvent, NSKeyDownMask, NSKeyUpMask, NSFlagsChangedMask from PyObjCTools import AppHelper import objc class MacKeyboardSetup(NSObject): """Setup the handler.""" @objc.python_method def init_with_handler(self, handler): """ Init method that receives the write end of the pipe. """ # ALWAYS call the super's designated initializer. # Also, make sure to re-bind "self" just in case it # returns something else! # pylint: disable=self-cls-assignment self = super(MacKeyboardSetup, self).init() self.handler = handler # Unlike Python's __init__, initializers MUST return self, # because they are allowed to return any object! return self # pylint: disable=invalid-name, unused-argument def applicationDidFinishLaunching_(self, notification): """Bind the handler to listen to keyboard events.""" mask = NSKeyDownMask | NSKeyUpMask | NSFlagsChangedMask NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(mask, self.handler) class MacKeyboardListener(AppKitKeyboardListener): """Loosely emulate Evdev keyboard behaviour on the Mac. Listen for key events then buffer them in a pipe. """ def install_handle_input(self): """Install the hook.""" self.app = NSApplication.sharedApplication() # pylint: disable=no-member delegate = MacKeyboardSetup.alloc().init_with_handler(self.handle_input) NSApp().setDelegate_(delegate) AppHelper.runEventLoop() def __del__(self): """Stop the listener on deletion.""" AppHelper.stopEventLoop() MacKeyboardListener(pipe) class InputDevice: """A user input device.""" # pylint: disable=too-many-instance-attributes def __init__(self, manager, device_path=None, char_path_override=None, read_size=1): self.read_size = read_size self.manager = manager self.__pipe = None self._listener = None self.leds = None if device_path: self._device_path = device_path else: self._set_device_path() # We should by now have a device_path try: if not self._device_path: raise NoDevicePath except AttributeError: raise NoDevicePath self.protocol, _, self.device_type = self._get_path_infomation() if char_path_override: self._character_device_path = char_path_override else: self._character_device_path = os.path.realpath(self._device_path) self._character_file = None self._evdev = False self._set_evdev_state() self.name = "Unknown Device" self._set_name() def _set_device_path(self): """Set the device path, overridden on the MAC and Windows.""" pass def _set_evdev_state(self): """Set whether the device is a real evdev device.""" if NIX: self._evdev = True def _set_name(self): if NIX: with open( "/sys/class/input/%s/device/name" % self.get_char_name() ) as name_file: self.name = name_file.read().strip() self.leds = [] def _get_path_infomation(self): """Get useful infomation from the device path.""" long_identifier = self._device_path.split('/')[4] protocol, remainder = long_identifier.split('-', 1) identifier, _, device_type = remainder.rsplit('-', 2) return (protocol, identifier, device_type) def get_char_name(self): """Get short version of char device name.""" return self._character_device_path.split('/')[-1] def get_char_device_path(self): """Get the char device path.""" return self._character_device_path def __str__(self): try: return self.name except AttributeError: return "Unknown Device" def __repr__(self): return '%s.%s("%s")' % ( self.__module__, self.__class__.__name__, self._device_path, ) @property def _character_device(self): if not self._character_file: if WIN: self._character_file = io.BytesIO() return self._character_file try: self._character_file = io.open(self._character_device_path, 'rb') except PermissionError: # Python 3 raise PermissionError(PERMISSIONS_ERROR_TEXT) except IOError as err: # Python 2 if err.errno == 13: raise PermissionError(PERMISSIONS_ERROR_TEXT) else: raise return self._character_file def __iter__(self): while True: event = self._do_iter() if event: yield event def _get_data(self, read_size): """Get data from the character device.""" return self._character_device.read(read_size) @staticmethod def _get_target_function(): """Get the correct target function. This is only used by Windows subclasses.""" return False def _get_total_read_size(self): """How much event data to process at once.""" if self.read_size: read_size = EVENT_SIZE * self.read_size else: read_size = EVENT_SIZE return read_size def _do_iter(self): read_size = self._get_total_read_size() data = self._get_data(read_size) if not data: return None evdev_objects = iter_unpack(data) events = [self._make_event(*event) for event in evdev_objects] return events # pylint: disable=too-many-arguments def _make_event(self, tv_sec, tv_usec, ev_type, code, value): """Create a friendly Python object from an evdev style event.""" event_type = self.manager.get_event_type(ev_type) eventinfo = { "ev_type": event_type, "state": value, "timestamp": tv_sec + (tv_usec / 1000000), "code": self.manager.get_event_string(event_type, code), } return InputEvent(self, eventinfo) def read(self): """Read the next input event.""" return next(iter(self)) @property def _pipe(self): """On Windows we use a pipe to emulate a Linux style character buffer.""" if self._evdev: return None if not self.__pipe: target_function = self._get_target_function() if not target_function: return None self.__pipe, child_conn = Pipe(duplex=False) self._listener = Process(target=target_function, args=(child_conn,)) self._listener.start() return self.__pipe def __del__(self): if ('WIN' in globals() or 'MAC' in globals()) and (WIN or MAC) and self.__pipe: self._listener.terminate() class Keyboard(InputDevice): """A keyboard or other key-like device. Original umapped scan code, followed by the important key info followed by a sync. """ def _set_device_path(self): super(Keyboard, self)._set_device_path() if MAC: self._device_path = APPKIT_KB_PATH def _set_name(self): super(Keyboard, self)._set_name() if WIN: self.name = "Microsoft Keyboard" elif MAC: self.name = "AppKit Keyboard" @staticmethod def _get_target_function(): """Get the correct target function.""" if WIN: return keyboard_process if MAC: return mac_keyboard_process return None def _get_data(self, read_size): """Get data from the character device.""" if NIX: return super(Keyboard, self)._get_data(read_size) return self._pipe.recv_bytes() class Mouse(InputDevice): """A mouse or other pointing-like device.""" def _set_device_path(self): super(Mouse, self)._set_device_path() if MAC: self._device_path = APPKIT_MOUSE_PATH def _set_name(self): super(Mouse, self)._set_name() if WIN: self.name = "Microsoft Mouse" elif MAC: self.name = "AppKit Mouse" @staticmethod def _get_target_function(): """Get the correct target function.""" if WIN: return mouse_process if MAC: return appkit_mouse_process return None def _get_data(self, read_size): """Get data from the character device.""" if NIX: return super(Mouse, self)._get_data(read_size) return self._pipe.recv_bytes() class MightyMouse(Mouse): """A mouse or other pointing device on the Mac.""" def _set_device_path(self): super(MightyMouse, self)._set_device_path() if MAC: self._device_path = QUARTZ_MOUSE_PATH def _set_name(self): self.name = "Quartz Mouse" @staticmethod def _get_target_function(): """Get the correct target function.""" return quartz_mouse_process def delay_and_stop(duration, dll, device_number): """Stop vibration aka force feedback aka rumble on Windows after duration miliseconds.""" xinput = getattr(ctypes.windll, dll) time.sleep(duration / 1000) xinput_set_state = xinput.XInputSetState xinput_set_state.argtypes = [ctypes.c_uint, ctypes.POINTER(XinputVibration)] xinput_set_state.restype = ctypes.c_uint vibration = XinputVibration(0, 0) xinput_set_state(device_number, ctypes.byref(vibration)) # I made this GamePad class before Mouse and Keyboard above, and have # learned a lot about Windows in the process. This can probably be # simplified massively and made to match Mouse and Keyboard more. class GamePad(InputDevice): """A gamepad or other joystick-like device.""" def __init__(self, manager, device_path, char_path_override=None): super(GamePad, self).__init__(manager, device_path, char_path_override) self._write_file = None self.__device_number = None if WIN and "Microsoft_Corporation_Controller" in self._device_path: self.name = "Microsoft X-Box 360 pad" identifier = self._get_path_infomation()[1] self.__device_number = int(identifier.split('_')[-1]) self.__received_packets = 0 self.__missed_packets = 0 self.__last_state = self.__read_device() if NIX: self._number_xpad() def _number_xpad(self): """Get the number of the joystick.""" js_path = self._device_path.replace('-event', '') js_chardev = os.path.realpath(js_path) try: number_text = js_chardev.split('js')[1] except IndexError: return try: number = int(number_text) except ValueError: return self.__device_number = number def get_number(self): """Return the joystick number of the gamepad.""" return self.__device_number def __iter__(self): while True: if WIN: self.__check_state() event = self._do_iter() if event: yield event def __check_state(self): """On Windows, check the state and fill the event character device.""" state = self.__read_device() if not state: raise UnpluggedError("Gamepad %d is not connected" % self.__device_number) if state.packet_number != self.__last_state.packet_number: # state has changed, handle the change self.__handle_changed_state(state) self.__last_state = state @staticmethod def __get_timeval(): """Get the time and make it into C style timeval.""" return convert_timeval(time.time()) def create_event_object(self, event_type, code, value, timeval=None): """Create an evdev style object.""" if not timeval: timeval = self.__get_timeval() try: event_code = self.manager.codes['type_codes'][event_type] except KeyError: raise UnknownEventType( "We don't know what kind of event a %s is." % event_type ) event = struct.pack( EVENT_FORMAT, timeval[0], timeval[1], event_code, code, value ) return event def __write_to_character_device(self, event_list, timeval=None): """Emulate the Linux character device on other platforms such as Windows.""" # Remember the position of the stream pos = self._character_device.tell() # Go to the end of the stream self._character_device.seek(0, 2) # Write the new data to the end for event in event_list: self._character_device.write(event) # Add a sync marker sync = self.create_event_object("Sync", 0, 0, timeval) self._character_device.write(sync) # Put the stream back to its original position self._character_device.seek(pos) def __handle_changed_state(self, state): """ we need to pack a struct with the following five numbers: tv_sec, tv_usec, ev_type, code, value then write it using __write_to_character_device seconds, mircroseconds, ev_type, code, value time we just use now ev_type we look up code we look up value is 0 or 1 for the buttons axis value is maybe the same as Linux? Hope so! """ timeval = self.__get_timeval() events = self.__get_button_events(state, timeval) events.extend(self.__get_axis_events(state, timeval)) if events: self.__write_to_character_device(events, timeval) def __map_button(self, button): """Get the linux xpad code from the Windows xinput code.""" _, start_code, start_value = button value = start_value ev_type = "Key" code = self.manager.codes['xpad'][start_code] if 1 <= start_code <= 4: ev_type = "Absolute" if (start_code == 1 and start_value == 1) or ( start_code == 3 and start_value == 1 ): value = -1 return code, value, ev_type def __map_axis(self, axis): """Get the linux xpad code from the Windows xinput code.""" start_code, start_value = axis value = start_value code = self.manager.codes['xpad'][start_code] return code, value def __get_button_events(self, state, timeval=None): """Get the button events from xinput.""" changed_buttons = self.__detect_button_events(state) events = self.__emulate_buttons(changed_buttons, timeval) return events def __get_axis_events(self, state, timeval=None): """Get the stick events from xinput.""" axis_changes = self.__detect_axis_events(state) events = self.__emulate_axis(axis_changes, timeval) return events def __emulate_axis(self, axis_changes, timeval=None): """Make the axis events use the Linux style format.""" events = [] for axis in axis_changes: code, value = self.__map_axis(axis) event = self.create_event_object("Absolute", code, value, timeval=timeval) events.append(event) return events def __emulate_buttons(self, changed_buttons, timeval=None): """Make the button events use the Linux style format.""" events = [] for button in changed_buttons: code, value, ev_type = self.__map_button(button) event = self.create_event_object(ev_type, code, value, timeval=timeval) events.append(event) return events @staticmethod def __gen_bit_values(number): """ Return a zero or one for each bit of a numeric value up to the most significant 1 bit, beginning with the least significant bit. """ number = int(number) while number: yield number & 0x1 number >>= 1 def __get_bit_values(self, number, size=32): """Get bit values as a list for a given number >>> get_bit_values(1) == [0]*31 + [1] True >>> get_bit_values(0xDEADBEEF) [1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 0L, 1L, 0L, 1L, 1L, 0L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L] You may override the default word size of 32-bits to match your actual application. >>> get_bit_values(0x3, 2) [1L, 1L] >>> get_bit_values(0x3, 4) [0L, 0L, 1L, 1L] """ res = list(self.__gen_bit_values(number)) res.reverse() # 0-pad the most significant bit res = [0] * (size - len(res)) + res return res def __detect_button_events(self, state): changed = state.gamepad.buttons ^ self.__last_state.gamepad.buttons changed = self.__get_bit_values(changed, 16) buttons_state = self.__get_bit_values(state.gamepad.buttons, 16) changed.reverse() buttons_state.reverse() button_numbers = count(1) changed_buttons = list( filter(itemgetter(0), list(zip(changed, button_numbers, buttons_state))) ) # returns for example [(1,15,1)] type, code, value? return changed_buttons def __detect_axis_events(self, state): # axis fields are everything but the buttons # pylint: disable=protected-access # Attribute name _fields_ is special name set by ctypes axis_fields = dict(XinputGamepad._fields_) axis_fields.pop('buttons') changed_axes = [] # Ax_type might be useful when we support high-level deadzone # methods. # pylint: disable=unused-variable for axis, _ in list(axis_fields.items()): old_val = getattr(self.__last_state.gamepad, axis) new_val = getattr(state.gamepad, axis) if old_val != new_val: changed_axes.append((axis, new_val)) return changed_axes def __read_device(self): """Read the state of the gamepad.""" state = XinputState() res = self.manager.xinput.XInputGetState( self.__device_number, ctypes.byref(state) ) if res == XINPUT_ERROR_SUCCESS: return state if res != XINPUT_ERROR_DEVICE_NOT_CONNECTED: raise RuntimeError( "Unknown error %d attempting to get state of device %d" % (res, self.__device_number) ) # else (device is not connected) return None @property def _write_device(self): if not self._write_file: if not NIX: return None try: self._write_file = io.open(self._character_device_path, 'wb') except PermissionError: # Python 3 raise PermissionError(PERMISSIONS_ERROR_TEXT) except IOError as err: # Python 2 if err.errno == 13: raise PermissionError(PERMISSIONS_ERROR_TEXT) else: raise return self._write_file def _start_vibration_win(self, left_motor, right_motor): """Start the vibration, which will run until stopped.""" xinput_set_state = self.manager.xinput.XInputSetState xinput_set_state.argtypes = [ctypes.c_uint, ctypes.POINTER(XinputVibration)] xinput_set_state.restype = ctypes.c_uint vibration = XinputVibration(int(left_motor * 65535), int(right_motor * 65535)) xinput_set_state(self.__device_number, ctypes.byref(vibration)) def _stop_vibration_win(self): """Stop the vibration.""" xinput_set_state = self.manager.xinput.XInputSetState xinput_set_state.argtypes = [ctypes.c_uint, ctypes.POINTER(XinputVibration)] xinput_set_state.restype = ctypes.c_uint stop_vibration = ctypes.byref(XinputVibration(0, 0)) xinput_set_state(self.__device_number, stop_vibration) def _set_vibration_win(self, left_motor, right_motor, duration): """Control the motors on Windows.""" self._start_vibration_win(left_motor, right_motor) stop_process = Process( target=delay_and_stop, args=(duration, self.manager.xinput_dll, self.__device_number), ) stop_process.start() def __get_vibration_code(self, left_motor, right_motor, duration): """This is some crazy voodoo, if you can simplify it, please do.""" inner_event = struct.pack( '2h6x2h2x2H28x', 0x50, -1, duration, 0, int(left_motor * 65535), int(right_motor * 65535), ) buf_conts = ioctl(self._write_device, 1076905344, inner_event) return int(codecs.encode(buf_conts[1:3], 'hex'), 16) def _set_vibration_nix(self, left_motor, right_motor, duration): """Control the motors on Linux. Duration is in miliseconds.""" code = self.__get_vibration_code(left_motor, right_motor, duration) secs, msecs = convert_timeval(time.time()) outer_event = struct.pack(EVENT_FORMAT, secs, msecs, 0x15, code, 1) self._write_device.write(outer_event) self._write_device.flush() def set_vibration(self, left_motor, right_motor, duration): """Control the speed of both motors seperately or together. left_motor and right_motor arguments require a number between 0 (off) and 1 (full). duration is miliseconds, e.g. 1000 for a second.""" if WIN: self._set_vibration_win(left_motor, right_motor, duration) elif NIX: self._set_vibration_nix(left_motor, right_motor, duration) else: raise NotImplementedError class OtherDevice(InputDevice): """A device of which its is type is either undetectable or has not been implemented yet. """ pass class LED: """A light source.""" def __init__(self, manager, path, name): self.manager = manager self.path = path self.name = name self._write_file = None self._character_device_path = None self._post_init() def _post_init(self): """Post init setup.""" pass def __str__(self): return self.name def __repr__(self): return '%s.%s("%s")' % (self.__module__, self.__class__.__name__, self.path) def status(self): """Get the device status, i.e. the brightness level.""" status_filename = os.path.join(self.path, 'brightness') with open(status_filename) as status_fp: result = status_fp.read() status_text = result.strip() try: status = int(status_text) except ValueError: return status_text return status def max_brightness(self): """Get the device's maximum brightness level.""" status_filename = os.path.join(self.path, 'max_brightness') with open(status_filename) as status_fp: result = status_fp.read() status_text = result.strip() try: status = int(status_text) except ValueError: return status_text return status @property def _write_device(self): """The output device.""" if not self._write_file: if not NIX: return None try: self._write_file = io.open(self._character_device_path, 'wb') except PermissionError: # Python 3 raise PermissionError(PERMISSIONS_ERROR_TEXT) except IOError as err: # Python 2 only if err.errno == 13: # pragma: no cover raise PermissionError(PERMISSIONS_ERROR_TEXT) else: raise return self._write_file def _make_event(self, event_type, code, value): """Make a new event and send it to the character device.""" secs, msecs = convert_timeval(time.time()) data = struct.pack(EVENT_FORMAT, secs, msecs, event_type, code, value) self._write_device.write(data) self._write_device.flush() class SystemLED(LED): """An LED on your system e.g. caps lock.""" def __init__(self, manager, path, name): self.code = None self.device_path = None self.device = None super(SystemLED, self).__init__(manager, path, name) def _post_init(self): """Set up the device path and type code.""" self._led_type_code = self.manager.get_typecode('LED') self.device_path = os.path.realpath(os.path.join(self.path, 'device')) if '::' in self.name: chardev, code_name = self.name.split('::') if code_name in self.manager.codes['LED_type_codes']: self.code = self.manager.codes['LED_type_codes'][code_name] try: event_number = chardev.split('input')[1] except IndexError: print("Failed with", self.name) raise else: self._character_device_path = '/dev/input/event' + event_number self._match_device() def on(self): # pylint: disable=invalid-name """Turn the light on.""" self._make_event(1) def off(self): """Turn the light off.""" self._make_event(0) def _make_event(self, value): # pylint: disable=arguments-differ """Make a new event and send it to the character device.""" super(SystemLED, self)._make_event(self._led_type_code, self.code, value) def _match_device(self): """If the LED is connected to an input device, associate the objects.""" for device in self.manager.all_devices: if device.get_char_device_path() == self._character_device_path: self.device = device device.leds.append(self) break class GamepadLED(LED): """A light source on a gamepad.""" def __init__(self, manager, path, name): self.code = None self.device = None self.gamepad = None super(GamepadLED, self).__init__(manager, path, name) def _post_init(self): self._match_device() self._character_device_path = ( self.gamepad.get_char_device_path() if self.gamepad else None ) def _match_device(self): number = int(self.name.split('xpad')[1]) for gamepad in self.manager.gamepads: if number == gamepad.get_number(): self.gamepad = gamepad gamepad.leds.append(self) break class RawInputDeviceList(ctypes.Structure): """ Contains information about a raw input device. For full details see Microsoft's documentation: http://msdn.microsoft.com/en-us/library/windows/desktop/ ms645568(v=vs.85).aspx """ # pylint: disable=too-few-public-methods _fields_ = [("hDevice", HANDLE), ("dwType", DWORD)] class DeviceManager: """Provides access to all connected and detectible user input devices.""" # pylint: disable=too-many-instance-attributes def __init__(self): self.codes = {key: dict(value) for key, value in EVENT_MAP} self._raw = [] self.keyboards = [] self.mice = [] self.gamepads = [] self.other_devices = [] self.all_devices = [] self.leds = [] self.microbits = [] self.xinput = None self.xinput_dll = None if WIN: self._raw_device_counts = { 'mice': 0, 'keyboards': 0, 'otherhid': 0, 'unknown': 0, } self._post_init() def _post_init(self): """Call the find devices method for the relevant platform.""" if WIN: self._find_devices_win() elif MAC: self._find_devices_mac() else: self._find_devices() self._update_all_devices() if NIX: self._find_leds() def _update_all_devices(self): """Update the all_devices list.""" self.all_devices = [] self.all_devices.extend(self.keyboards) self.all_devices.extend(self.mice) self.all_devices.extend(self.gamepads) self.all_devices.extend(self.other_devices) def _parse_device_path(self, device_path, char_path_override=None): """Parse each device and add to the approriate list.""" # 1. Make sure that we can parse the device path. try: device_type = device_path.rsplit('-', 1)[1] except IndexError: warn( "The following device path was skipped as it could " "not be parsed: %s" % device_path, RuntimeWarning, ) return # 2. Make sure each device is only added once. realpath = os.path.realpath(device_path) if realpath in self._raw: return self._raw.append(realpath) # 3. All seems good, append the device to the relevant list. if device_type == 'kbd': self.keyboards.append(Keyboard(self, device_path, char_path_override)) elif device_type == 'mouse': self.mice.append(Mouse(self, device_path, char_path_override)) elif device_type == 'joystick': self.gamepads.append(GamePad(self, device_path, char_path_override)) else: self.other_devices.append( OtherDevice(self, device_path, char_path_override) ) def _find_xinput(self): """Find most recent xinput library.""" for dll in XINPUT_DLL_NAMES: try: self.xinput = getattr(ctypes.windll, dll) except OSError: pass else: # We found an xinput driver self.xinput_dll = dll break else: # We didn't find an xinput library warn("No xinput driver dll found, gamepads not supported.", RuntimeWarning) def _find_devices_win(self): """Find devices on Windows.""" self._find_xinput() self._detect_gamepads() self._count_devices() if self._raw_device_counts['keyboards'] > 0: self.keyboards.append( Keyboard(self, "/dev/input/by-id/usb-A_Nice_Keyboard-event-kbd") ) if self._raw_device_counts['mice'] > 0: self.mice.append( Mouse( self, "/dev/input/by-id/usb-A_Nice_Mouse_called_Arthur-event-mouse" ) ) def _find_devices_mac(self): """Find devices on Mac.""" self.keyboards.append(Keyboard(self)) self.mice.append(MightyMouse(self)) self.mice.append(Mouse(self)) def _detect_gamepads(self): """Find gamepads.""" state = XinputState() # Windows allows up to 4 gamepads. for device_number in range(4): res = self.xinput.XInputGetState(device_number, ctypes.byref(state)) if res == XINPUT_ERROR_SUCCESS: # We found a gamepad device_path = ( "/dev/input/by_id/" + "usb-Microsoft_Corporation_Controller_%s-event-joystick" % device_number ) self.gamepads.append(GamePad(self, device_path)) continue if res != XINPUT_ERROR_DEVICE_NOT_CONNECTED: raise RuntimeError( "Unknown error %d attempting to get state of device %d" % (res, device_number) ) def _count_devices(self): """See what Windows' GetRawInputDeviceList wants to tell us. For now, we are just seeing if there is at least one keyboard and/or mouse attached. GetRawInputDeviceList could be used to help distinguish between different keyboards and mice on the system in the way Linux can. However, Roma uno die non est condita. """ number_of_devices = ctypes.c_uint() if ( ctypes.windll.user32.GetRawInputDeviceList( ctypes.POINTER(ctypes.c_int)(), ctypes.byref(number_of_devices), ctypes.sizeof(RawInputDeviceList), ) == -1 ): warn( "Call to GetRawInputDeviceList was unsuccessful." "We have no idea if a mouse or keyboard is attached.", RuntimeWarning, ) return devices_found = (RawInputDeviceList * number_of_devices.value)() if ( ctypes.windll.user32.GetRawInputDeviceList( devices_found, ctypes.byref(number_of_devices), ctypes.sizeof(RawInputDeviceList), ) == -1 ): warn( "Call to GetRawInputDeviceList was unsuccessful." "We have no idea if a mouse or keyboard is attached.", RuntimeWarning, ) return for device in devices_found: if device.dwType == 0: self._raw_device_counts['mice'] += 1 elif device.dwType == 1: self._raw_device_counts['keyboards'] += 1 elif device.dwType == 2: self._raw_device_counts['otherhid'] += 1 else: self._raw_device_counts['unknown'] += 1 def _find_devices(self): """Find available devices.""" self._find_by('id') self._find_by('path') self._find_special() def _find_by(self, key): """Find devices.""" by_path = glob.glob('/dev/input/by-{key}/*-event-*'.format(key=key)) for device_path in by_path: self._parse_device_path(device_path) def _find_leds(self): """Find LED devices, Linux-only so far.""" for path in glob.glob('/sys/class/leds/*'): self._parse_led_path(path) def _parse_led_path(self, path): name = path.rsplit('/', 1)[1] if name.startswith('xpad'): self.leds.append(GamepadLED(self, path, name)) elif name.startswith('input'): self.leds.append(SystemLED(self, path, name)) else: self.leds.append(LED(self, path, name)) def _get_char_names(self): """Get a list of already found devices.""" return [device.get_char_name() for device in self.all_devices] def _find_special(self): """Look for special devices.""" charnames = self._get_char_names() for eventdir in glob.glob('/sys/class/input/event*'): char_name = os.path.split(eventdir)[1] if char_name in charnames: continue name_file = os.path.join(eventdir, 'device', 'name') with open(name_file) as name_file: device_name = name_file.read().strip() if device_name in self.codes['specials']: self._parse_device_path( self.codes['specials'][device_name], os.path.join('/dev/input', char_name), ) def __iter__(self): return iter(self.all_devices) def __getitem__(self, index): try: return self.all_devices[index] except IndexError: raise IndexError("list index out of range") def get_event_type(self, raw_type): """Convert the code to a useful string name.""" try: return self.codes['types'][raw_type] except KeyError: raise UnknownEventType("We don't know this event type") def get_event_string(self, evtype, code): """Get the string name of the event.""" if WIN and evtype == 'Key': # If we can map the code to a common one then do it try: code = self.codes['wincodes'][code] except KeyError: pass try: return self.codes[evtype][code] except KeyError: raise UnknownEventCode("We don't know this event.", evtype, code) def get_typecode(self, name): """Returns type code for `name`.""" return self.codes['type_codes'].get(name) def detect_microbit(self): """Detect a microbit.""" try: gpad = MicroBitPad(self) except ModuleNotFoundError: warn( "The microbit library could not be found in the pythonpath. \n" "For more information, please visit \n" "https://inputs.readthedocs.io/en/latest/user/microbit.html", RuntimeWarning, ) else: self.microbits.append(gpad) self.gamepads.append(gpad) SPIN_UP_MOTOR = ( '00000', '00001', '00011', '00111', '01111', '11111', '01111', '00011', '00001', '00000', '00001', '00011', '00111', '01111', '11111', '00000', '11111', '00000', '11111', '00000', ) class MicroBitPad(GamePad): """A BBC Micro:bit flashed with bitio.""" def __init__(self, manager, device_path=None, char_path_override=None): if not device_path: device_path = '/dev/input/by-id/dialup-BBC_MicroBit-event-joystick' if not char_path_override: char_path_override = '/dev/input/microbit0' super(MicroBitPad, self).__init__(manager, device_path, char_path_override) # pylint: disable=no-member,import-error import microbit self.microbit = microbit self.default_image = microbit.Image("00500:00500:00500:00500:00500") self._setup_rumble() self.set_display() def set_display(self, index=None): """Show an image on the display.""" # pylint: disable=no-member if index: image = self.microbit.Image.STD_IMAGES[index] else: image = self.default_image self.microbit.display.show(image) def _setup_rumble(self): """Setup the three animations which simulate a rumble.""" self.left_rumble = self._get_ready_to('99500') self.right_rumble = self._get_ready_to('00599') self.double_rumble = self._get_ready_to('99599') def _set_name(self): self.name = "BBC microbit Gamepad" def _set_evdev_state(self): self._evdev = False @staticmethod def _get_target_function(): return microbit_process def _get_data(self, read_size): """Get data from the character device.""" return self._pipe.recv_bytes() def _get_ready_to(self, rumble): """Watch us wreck the mike! PSYCHE!""" # pylint: disable=no-member return [ self.microbit.Image( ':'.join([rumble if char == '1' else '00500' for char in code]) ) for code in SPIN_UP_MOTOR ] def _full_speed_rumble(self, images, duration): """Simulate the motors running at full.""" while duration > 0: self.microbit.display.show(images[0]) # pylint: disable=no-member time.sleep(0.04) self.microbit.display.show(images[1]) # pylint: disable=no-member time.sleep(0.04) duration -= 0.08 def _spin_up(self, images, duration): """Simulate the motors getting warmed up.""" total = 0 # pylint: disable=no-member for image in images: self.microbit.display.show(image) time.sleep(0.05) total += 0.05 if total >= duration: return remaining = duration - total self._full_speed_rumble(images[-2:], remaining) self.set_display() def set_vibration(self, left_motor, right_motor, duration): """Control the speed of both motors seperately or together. left_motor and right_motor arguments require a number: 0 (off) or 1 (full). duration is miliseconds, e.g. 1000 for a second.""" if left_motor and right_motor: return self._spin_up(self.double_rumble, duration / 1000) if left_motor: return self._spin_up(self.left_rumble, duration / 1000) if right_motor: return self._spin_up(self.right_rumble, duration / 1000) return -1 def microbit_process(pipe): """Simple subprocess for reading mouse events on the microbit.""" gamepad_listener = MicroBitListener(pipe) gamepad_listener.listen() class MicroBitListener(BaseListener): """Tracks the current state and sends changes to the MicroBitPad device class.""" def __init__(self, pipe): super(MicroBitListener, self).__init__(pipe) self.active = True self.events = [] self.state = { ('Absolute', 0x10, 0), ('Absolute', 0x11, 0), ('Key', 0x130, 0), ('Key', 0x131, 0), ('Key', 0x13A, 0), ('Key', 0x133, 0), ('Key', 0x134, 0), } self.dpad = True self.sensitivity = 300 # pylint: disable=import-error import microbit self.microbit = microbit def listen(self): """Listen while the device is active.""" while self.active: self.handle_input() def uninstall_handle_input(self): """Stop listing when active is false.""" self.active = False def handle_new_events(self, events): """Add each new events to the event queue.""" for event in events: self.events.append( self.create_event_object(event[0], event[1], int(event[2])) ) def handle_abs(self): """Gets the state as the raw abolute numbers.""" # pylint: disable=no-member x_raw = self.microbit.accelerometer.get_x() y_raw = self.microbit.accelerometer.get_y() x_abs = ('Absolute', 0x00, x_raw) y_abs = ('Absolute', 0x01, y_raw) return x_abs, y_abs def handle_dpad(self): """Gets the state of the virtual dpad.""" # pylint: disable=no-member x_raw = self.microbit.accelerometer.get_x() y_raw = self.microbit.accelerometer.get_y() minus_sens = self.sensitivity * -1 if x_raw < minus_sens: x_state = ('Absolute', 0x10, -1) elif x_raw > self.sensitivity: x_state = ('Absolute', 0x10, 1) else: x_state = ('Absolute', 0x10, 0) if y_raw < minus_sens: y_state = ('Absolute', 0x11, -1) elif y_raw > self.sensitivity: y_state = ('Absolute', 0x11, 1) else: y_state = ('Absolute', 0x11, 1) return x_state, y_state def check_state(self): """Tracks differences in the device state.""" if self.dpad: x_state, y_state = self.handle_dpad() else: x_state, y_state = self.handle_abs() new_state = { x_state, y_state, ('Key', 0x130, int(self.microbit.button_a.is_pressed())), ('Key', 0x131, int(self.microbit.button_b.is_pressed())), ('Key', 0x13A, int(self.microbit.pin0.is_touched())), ('Key', 0x133, int(self.microbit.pin1.is_touched())), ('Key', 0x134, int(self.microbit.pin2.is_touched())), } events = new_state - self.state self.state = new_state return events def handle_input(self): """Sends differences in the device state to the MicroBitPad as events.""" difference = self.check_state() if not difference: return self.events = [] self.handle_new_events(difference) self.update_timeval() self.events.append(self.sync_marker(self.timeval)) self.write_to_pipe(self.events) devices = DeviceManager() # pylint: disable=invalid-name def get_key(): """Get a single keypress from a keyboard.""" try: keyboard = devices.keyboards[0] except IndexError: raise UnpluggedError("No keyboard found.") return keyboard.read() def get_mouse(): """Get a single movement or click from a mouse.""" try: mouse = devices.mice[0] except IndexError: raise UnpluggedError("No mice found.") return mouse.read() def get_gamepad(): """Get a single action from a gamepad.""" try: gamepad = devices.gamepads[0] except IndexError: raise UnpluggedError("No gamepad found.") return gamepad.read()