├── .gitignore ├── LICENSE ├── README.md ├── foohid.c ├── setup.py ├── test_joypad.py ├── test_keyboard.py ├── test_list.py └── test_mouse.py /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 unbit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # foohid-py 2 | Python wrapper for the foohid OSX driver. Compatible with Python 2 and 3. 3 | 4 | https://github.com/unbit/foohid 5 | 6 | Exposed functions 7 | ================= 8 | 9 | ```py 10 | import foohid 11 | 12 | foohid.create('your new device', report_descriptor, serial_number, vendor_id, device_id) 13 | foohid.destroy('your new device') 14 | foohid.send('your new device', hid_message) 15 | foohid.list() 16 | ``` 17 | 18 | Included tests/examples 19 | ======================= 20 | 21 | test_mouse.py -> creates a virtual mouse and randomly moves it (your cursor will move too ;) 22 | 23 | test_joypad.py -> creates a virtual joypad and randomly moves left and right axis (run a game with joypad support to check it) 24 | 25 | test_keyboard.py -> creates a virtual keyboard and presses the "a" key 26 | 27 | test_list.py -> tests listing feature 28 | 29 | Troubleshooting 30 | =============== 31 | 32 | Currently, there is a bug in the original foohid package where the userclient is not properly terminated. 33 | If the code is run repeatedly, this sometimes results in a "unable to open it_unbit_foohid service" error. 34 | If this happens, unload and remove the kernel extension: 35 | 36 | ```bash 37 | $ sudo kextunload /Library/Extensions/foohid.kext 38 | $ sudo rm -rf /System/Library/Extensions/foohid.kext 39 | ``` 40 | 41 | Then reinstall the kernel extension: https://github.com/unbit/foohid/releases/latest (no restart required) -------------------------------------------------------------------------------- /foohid.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define FOOHID_SERVICE "it_unbit_foohid" 5 | 6 | #define FOOHID_CREATE 0 7 | #define FOOHID_DESTROY 1 8 | #define FOOHID_SEND 2 9 | #define FOOHID_LIST 3 10 | 11 | static int foohid_connect(io_connect_t *conn) { 12 | io_iterator_t iterator; 13 | io_service_t service; 14 | kern_return_t ret = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching(FOOHID_SERVICE), &iterator); 15 | if (ret != KERN_SUCCESS) return -1; 16 | 17 | while ((service = IOIteratorNext(iterator)) != IO_OBJECT_NULL) { 18 | ret = IOServiceOpen(service, mach_task_self(), 0, conn); 19 | IOObjectRelease(service); 20 | if (ret == KERN_SUCCESS) { 21 | IOObjectRelease(iterator); 22 | return 0; 23 | } 24 | } 25 | 26 | IOObjectRelease(iterator); 27 | return -1; 28 | } 29 | 30 | static void foohid_close(io_connect_t conn) { 31 | IOServiceClose(conn); 32 | } 33 | 34 | static PyObject *foohid_create(PyObject *self, PyObject *args) { 35 | char *name; 36 | Py_ssize_t name_len; 37 | char *descriptor; 38 | Py_ssize_t descriptor_len; 39 | char *serial; 40 | Py_ssize_t serial_len; 41 | unsigned int vendor_id; 42 | unsigned int device_id; 43 | 44 | if (!PyArg_ParseTuple(args, "s#s#s#II", &name, &name_len, &descriptor, &descriptor_len, &serial, &serial_len, &vendor_id, &device_id)) { 45 | return NULL; 46 | } 47 | 48 | if (name_len == 0 || descriptor_len == 0 || serial_len == 0) { 49 | return PyErr_Format(PyExc_ValueError, "invalid values"); 50 | } 51 | 52 | io_connect_t conn; 53 | if (foohid_connect(&conn)) { 54 | return PyErr_Format(PyExc_SystemError, "unable to open " FOOHID_SERVICE " service"); 55 | } 56 | 57 | uint32_t input_count = 8; 58 | uint64_t input[input_count]; 59 | input[0] = (uint64_t) name; 60 | input[1] = (uint64_t) name_len; 61 | input[2] = (uint64_t) descriptor; 62 | input[3] = (uint64_t) descriptor_len; 63 | input[4] = (uint64_t) serial; 64 | input[5] = (uint64_t) serial_len; 65 | input[6] = (uint64_t) vendor_id; 66 | input[7] = (uint64_t) device_id; 67 | 68 | kern_return_t ret = IOConnectCallScalarMethod(conn, FOOHID_CREATE, input, input_count, NULL, 0); 69 | foohid_close(conn); 70 | 71 | if (ret != KERN_SUCCESS) { 72 | return PyErr_Format(PyExc_SystemError, "unable to create device"); 73 | } 74 | 75 | Py_INCREF(Py_True); 76 | return Py_True; 77 | } 78 | 79 | static PyObject *foohid_send(PyObject *self, PyObject *args) { 80 | char *name; 81 | Py_ssize_t name_len; 82 | char *report; 83 | Py_ssize_t report_len; 84 | 85 | if (!PyArg_ParseTuple(args, "s#s#", &name, &name_len, &report, &report_len)) { 86 | return NULL; 87 | } 88 | 89 | if (name_len == 0 || report_len == 0) { 90 | return PyErr_Format(PyExc_ValueError, "invalid values"); 91 | } 92 | 93 | io_connect_t conn; 94 | if (foohid_connect(&conn)) { 95 | return PyErr_Format(PyExc_SystemError, "unable to open " FOOHID_SERVICE " service"); 96 | } 97 | 98 | uint32_t input_count = 4; 99 | uint64_t input[input_count]; 100 | input[0] = (uint64_t) name; 101 | input[1] = (uint64_t) name_len; 102 | input[2] = (uint64_t) report; 103 | input[3] = (uint64_t) report_len; 104 | 105 | kern_return_t ret = IOConnectCallScalarMethod(conn, FOOHID_SEND, input, input_count, NULL, 0); 106 | foohid_close(conn); 107 | 108 | if (ret != KERN_SUCCESS) { 109 | return PyErr_Format(PyExc_SystemError, "unable to send hid message"); 110 | } 111 | 112 | Py_INCREF(Py_True); 113 | return Py_True; 114 | } 115 | 116 | static PyObject *foohid_destroy(PyObject *self, PyObject *args) { 117 | char *name; 118 | Py_ssize_t name_len; 119 | 120 | if (!PyArg_ParseTuple(args, "s#", &name, &name_len)) { 121 | return NULL; 122 | } 123 | 124 | if (name_len == 0) { 125 | return PyErr_Format(PyExc_ValueError, "invalid name"); 126 | } 127 | 128 | io_connect_t conn; 129 | if (foohid_connect(&conn)) { 130 | return PyErr_Format(PyExc_SystemError, "unable to open " FOOHID_SERVICE " service"); 131 | } 132 | 133 | uint32_t input_count = 2; 134 | uint64_t input[input_count]; 135 | input[0] = (uint64_t) name; 136 | input[1] = (uint64_t) name_len; 137 | 138 | kern_return_t ret = IOConnectCallScalarMethod(conn, FOOHID_DESTROY, input, input_count, NULL, 0); 139 | foohid_close(conn); 140 | 141 | if (ret != KERN_SUCCESS) { 142 | return PyErr_Format(PyExc_SystemError, "unable to destroy hid device"); 143 | } 144 | 145 | Py_INCREF(Py_True); 146 | return Py_True; 147 | } 148 | 149 | static PyObject *foohid_list(PyObject *self, PyObject *args) { 150 | 151 | if (!PyArg_ParseTuple(args, "")) { 152 | return NULL; 153 | } 154 | 155 | io_connect_t conn; 156 | if (foohid_connect(&conn)) { 157 | return PyErr_Format(PyExc_SystemError, "unable to open " FOOHID_SERVICE " service"); 158 | } 159 | 160 | uint32_t output_count = 2; 161 | uint64_t output[2] = {0, 0}; 162 | 163 | uint16_t buf_len = 4096; 164 | char *buf = malloc(buf_len); 165 | if (!buf) { 166 | return PyErr_Format(PyExc_MemoryError, "unable to allocate memory"); 167 | } 168 | 169 | uint64_t input[2]; 170 | 171 | for(;;) { 172 | input[0] = (uint64_t) buf; 173 | input[1] = (uint64_t) buf_len; 174 | kern_return_t ret = IOConnectCallScalarMethod(conn, FOOHID_LIST, input, 2, output, &output_count); 175 | foohid_close(conn); 176 | if (ret != KERN_SUCCESS) { 177 | free(buf); 178 | return PyErr_Format(PyExc_SystemError, "unable to list hid devices"); 179 | } 180 | // all is fine 181 | if (output[0] == 0) { 182 | PyObject *ret = PyTuple_New(output[1]); 183 | uint64_t i; 184 | char *ptr = buf; 185 | for(i = 0; i < output[1]; i++) { 186 | #if PY_MAJOR_VERSION >= 3 187 | PyTuple_SetItem(ret, i, PyUnicode_FromString(ptr)); 188 | #else 189 | PyTuple_SetItem(ret, i, PyString_FromString(ptr)); 190 | #endif 191 | ptr += strlen(ptr) + 1; 192 | } 193 | free(buf); 194 | return ret; 195 | } 196 | // realloc memory 197 | buf_len = output[0]; 198 | char *tmp = realloc(buf, buf_len); 199 | if (!tmp) { 200 | free(buf); 201 | return PyErr_Format(PyExc_MemoryError, "unable to allocate memory"); 202 | } 203 | buf = tmp; 204 | } 205 | } 206 | 207 | static PyMethodDef foohidMethods[] = { 208 | {"create", foohid_create, METH_VARARGS, "create a new foohid device"}, 209 | {"destroy", foohid_destroy, METH_VARARGS, "destroy a foohid device"}, 210 | {"send", foohid_send, METH_VARARGS, "send a hid message to a foohid device"}, 211 | {"list", foohid_list, METH_VARARGS, "list the currently available foohid devices"}, 212 | {NULL, NULL, 0, NULL} 213 | }; 214 | 215 | #if PY_MAJOR_VERSION >= 3 216 | static struct PyModuleDef foodhidModule = { 217 | PyModuleDef_HEAD_INIT, 218 | "foohid", 219 | "python wrapper for foohid", 220 | -1, 221 | foohidMethods 222 | }; 223 | PyMODINIT_FUNC PyInit_foohid(void) { 224 | return PyModule_Create(&foodhidModule); 225 | } 226 | #else 227 | PyMODINIT_FUNC initfoohid(void) { 228 | Py_InitModule3("foohid", foohidMethods, "python wrapper for foohid"); 229 | } 230 | #endif -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | 3 | module1 = Extension('foohid', 4 | sources = ['foohid.c'], 5 | extra_link_args = ['-framework', 'IOKit']) 6 | 7 | setup (name = 'foohid', 8 | version = '0.2', 9 | description = 'osx foohid iokit driver python wrapper', 10 | ext_modules = [module1]) 11 | -------------------------------------------------------------------------------- /test_joypad.py: -------------------------------------------------------------------------------- 1 | import foohid 2 | import struct 3 | import random 4 | import time 5 | 6 | joypad = ( 7 | 0x05, 0x01, 8 | 0x09, 0x05, 9 | 0xa1, 0x01, 10 | 0xa1, 0x00, 11 | 0x05, 0x09, 12 | 0x19, 0x01, 13 | 0x29, 0x10, 14 | 0x15, 0x00, 15 | 0x25, 0x01, 16 | 0x95, 0x10, 17 | 0x75, 0x01, 18 | 0x81, 0x02, 19 | 0x05, 0x01, 20 | 0x09, 0x30, 21 | 0x09, 0x31, 22 | 0x09, 0x32, 23 | 0x09, 0x33, 24 | 0x15, 0x81, 25 | 0x25, 0x7f, 26 | 0x75, 0x08, 27 | 0x95, 0x04, 28 | 0x81, 0x02, 29 | 0xc0, 30 | 0xc0) 31 | 32 | try: 33 | foohid.destroy("FooHID simple joypad") 34 | except: 35 | pass 36 | foohid.create("FooHID simple joypad", struct.pack('{0}B'.format(len(joypad)), *joypad), "SN 123", 2, 3) 37 | 38 | try: 39 | while True: 40 | x = random.randrange(0,255) 41 | y = random.randrange(0,255) 42 | z = random.randrange(0,255) 43 | rx = random.randrange(0,255) 44 | foohid.send("FooHID simple joypad", struct.pack('H4B', 0, x, y, z, rx)) 45 | time.sleep(1) 46 | except KeyboardInterrupt: 47 | foohid.destroy("FooHID simple joypad") -------------------------------------------------------------------------------- /test_keyboard.py: -------------------------------------------------------------------------------- 1 | import foohid 2 | import struct 3 | import time 4 | 5 | keyboard = ( 6 | 0x05, 0x01, 7 | 0x09, 0x06, 8 | 0xa1, 0x01, 9 | 0x05, 0x07, 10 | 0x19, 0xe0, 11 | 0x29, 0xe7, 12 | 0x15, 0x00, 13 | 0x25, 0x01, 14 | 0x75, 0x01, 15 | 0x95, 0x08, 16 | 0x81, 0x02, 17 | 0x95, 0x01, 18 | 0x75, 0x08, 19 | 0x81, 0x01, 20 | 0x95, 0x05, 21 | 0x75, 0x01, 22 | 0x05, 0x08, 23 | 0x19, 0x01, 24 | 0x29, 0x05, 25 | 0x91, 0x02, 26 | 0x95, 0x01, 27 | 0x75, 0x03, 28 | 0x91, 0x01, 29 | 0x95, 0x06, 30 | 0x75, 0x08, 31 | 0x15, 0x00, 32 | 0x25, 0x65, 33 | 0x05, 0x07, 34 | 0x19, 0x00, 35 | 0x29, 0x65, 36 | 0x81, 0x00, 37 | 0x09, 0x00, 38 | 0x75, 0x08, 39 | 0x95, 0x01, 40 | 0x15, 0x00, 41 | 0x25, 0x7f, 42 | 0xb1, 0x02, 43 | 0xc0 44 | ) 45 | 46 | try: 47 | foohid.destroy("FooHID simple keyboard") 48 | except: 49 | pass 50 | 51 | foohid.create("FooHID simple keyboard", struct.pack('{0}B'.format(len(keyboard)), *keyboard), "SN 123", 2, 3) 52 | 53 | try: 54 | while True: 55 | # press "a" key 56 | foohid.send("FooHID simple keyboard", struct.pack('8B', 0, 0, 4, 0, 0, 0, 0, 0)) 57 | time.sleep(0.1) 58 | foohid.send("FooHID simple keyboard", struct.pack('8B', 0, 0, 0, 0, 0, 0, 0, 0)) 59 | time.sleep(0.5) 60 | except KeyboardInterrupt: 61 | # make sure key is unpressed before exiting 62 | foohid.send("FooHID simple keyboard", struct.pack('8B', 0, 0, 0, 0, 0, 0, 0, 0)) 63 | foohid.destroy("FooHID simple keyboard") -------------------------------------------------------------------------------- /test_list.py: -------------------------------------------------------------------------------- 1 | import foohid 2 | 3 | for i in range(1, 10): 4 | foohid.create("FooHID {0}".format(i), "xxx", "123", 2, 3) 5 | 6 | print(foohid.list()) 7 | 8 | for i in range(1, 10): 9 | foohid.destroy("FooHID {0}".format(i)) 10 | -------------------------------------------------------------------------------- /test_mouse.py: -------------------------------------------------------------------------------- 1 | import foohid 2 | import struct 3 | import random 4 | import time 5 | 6 | mouse = ( 7 | 0x05, 0x01, 8 | 0x09, 0x02, 9 | 0xa1, 0x01, 10 | 0x09, 0x01, 11 | 0xa1, 0x00, 12 | 0x05, 0x09, 13 | 0x19, 0x01, 14 | 0x29, 0x03, 15 | 0x15, 0x00, 16 | 0x25, 0x01, 17 | 0x95, 0x03, 18 | 0x75, 0x01, 19 | 0x81, 0x02, 20 | 0x95, 0x01, 21 | 0x75, 0x05, 22 | 0x81, 0x03, 23 | 0x05, 0x01, 24 | 0x09, 0x30, 25 | 0x09, 0x31, 26 | 0x15, 0x81, 27 | 0x25, 0x7f, 28 | 0x75, 0x08, 29 | 0x95, 0x02, 30 | 0x81, 0x06, 31 | 0xc0, 32 | 0xc0) 33 | 34 | try: 35 | foohid.destroy("FooHID simple mouse") 36 | except: 37 | pass 38 | foohid.create("FooHID simple mouse", struct.pack('{0}B'.format(len(mouse)), *mouse), "SN 123", 2, 3) 39 | 40 | try: 41 | while True: 42 | x = random.randrange(0,255) 43 | y = random.randrange(0,255) 44 | foohid.send("FooHID simple mouse", struct.pack('3B', 0, x, y)) 45 | time.sleep(1) 46 | except KeyboardInterrupt: 47 | foohid.destroy("FooHID simple mouse") --------------------------------------------------------------------------------