├── .gitignore ├── pyxid2 ├── constants.py ├── __init__.py ├── keymaps.py ├── internal.py └── pyxid_impl.py ├── sample ├── advanced.py ├── pulsetable_test.py ├── responses.py └── event_markers.py ├── setup.py ├── COPYING ├── README.txt └── distribute_setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .ropeproject 3 | distribute-* 4 | pyxid2.egg-info 5 | *.gz 6 | dist 7 | build 8 | -------------------------------------------------------------------------------- /pyxid2/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | NO_KEY_DETECTED, FOUND_KEY_DOWN, FOUND_KEY_UP = list(range(3)) 4 | KEY_RELEASE_BITMASK = 0x10 5 | INVALID_PORT_BITS = 0x0C 6 | XID_PACKET_SIZE = 6 7 | ST2_PACKET_SIZE = 9 8 | -------------------------------------------------------------------------------- /pyxid2/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .pyxid_impl import * # noqa 4 | 5 | scanner = XidScanner() 6 | 7 | def get_xid_devices(): 8 | """ 9 | Returns a list of all Xid devices connected to your computer. 10 | """ 11 | devices = [] 12 | 13 | scanner.detect_xid_devices() 14 | 15 | for i in range(scanner.device_count()): 16 | com = scanner.device_at_index(i) 17 | if com.open(): 18 | device = XidDevice(com) 19 | 20 | device.reset_timer() 21 | 22 | devices.append(device) 23 | else: 24 | continue 25 | return devices 26 | 27 | def get_xid_device(device_number): 28 | print("The function get_xid_device() was removed in pyxid2 version 1.0.7. Use get_xid_devices() instead. Refer to https://github.com/cedrus-opensource/pyxid/tree/master/sample for usage examples.") 29 | return -------------------------------------------------------------------------------- /pyxid2/keymaps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | rb_530_keymap = {1: 0, 4 | 2: -1, 5 | 3: 1, 6 | 4: 2, 7 | 5: 3, 8 | 6: 4, 9 | 7: -1, 10 | 8: -1} 11 | rb_730_keymap = {1: 0, 12 | 2: 1, 13 | 3: 2, 14 | 4: 3, 15 | 5: 4, 16 | 6: 5, 17 | 7: 6, 18 | 8: 7} 19 | rb_830_keymap = {1: 3, 20 | 2: 4, 21 | 3: 1, 22 | 4: 2, 23 | 5: 5, 24 | 6: 6, 25 | 7: 0, 26 | 8: 7} 27 | rb_834_keymap = {1: 0, 28 | 2: 1, 29 | 3: 2, 30 | 4: 3, 31 | 5: 4, 32 | 6: 5, 33 | 7: 6, 34 | 8: 7} 35 | lumina_keymap = {1: 0, 36 | 2: 1, 37 | 3: 2, 38 | 4: 3, 39 | 5: 4, 40 | 6: -1, 41 | 7: -1, 42 | 8: -1} 43 | -------------------------------------------------------------------------------- /sample/advanced.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is a sort of a work-in-progress sample containing commands not neccesary 3 | in most uses of pyxid. Their use is highly situational at best. 4 | ''' 5 | 6 | import pyxid2 7 | 8 | # get a list of all attached XID devices 9 | devices = pyxid2.get_xid_devices() 10 | 11 | if devices: 12 | print(devices) 13 | else: 14 | print("No XID devices detected") 15 | exit() 16 | 17 | dev = devices[0] # get the first device to use 18 | print(dev) 19 | 20 | # If you're trying to collect responses from a StimTracker Duo/Quad, 21 | # you'll have to enable USB output for the appropriate response type. 22 | # You can read about it here https://cedrus.com/support/xid/commands.htm 23 | # in the SIGNAL FILTERING & FLOW section. 24 | # Note that this can also be easily accomplished by downloading Xidon from 25 | # https://cedrus.com/support/xid/xidon.htm and going to Device -> Options 26 | #dev.enable_usb_output('K', True) 27 | 28 | # Note that not all XID commands are implemented in this library. You 29 | # can send any arbitrary string to the XID device if you need one of the 30 | # unimplemented commands, like so (second arg is return bytes expected): 31 | #dev._send_command('iuK1', 0) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | # read the contents of your README file 4 | from os import path 5 | this_directory = path.abspath(path.dirname(__file__)) 6 | with open(path.join(this_directory, 'README.txt'), encoding='utf-8') as f: 7 | long_description = f.read() 8 | 9 | setup( 10 | name = "pyxid2", 11 | version = "1.0.9", 12 | packages = find_packages(), 13 | install_requires = ["ftd2xx>=1.3.8"], 14 | author = "Eugene Matsak", 15 | author_email = "developers@cedrus.com", 16 | maintainer = "Cedrus Corporation", 17 | maintainer_email = "developers@cedrus.com", 18 | description = ("Python library for interfacing with Cedrus XID devices, e.g. StimTracker, Riponda, RB-x40, c-pod, and Lumina."), 19 | long_description = long_description, 20 | long_description_content_type='text/markdown', 21 | license = "BSD", 22 | keywords = "cedrus xid XID stimulus response data collection", 23 | url = "http://www.github.com/cedrus-opensource/pyxid/", 24 | classifiers = [ 25 | "Intended Audience :: Developers", 26 | "License :: OSI Approved :: BSD License", 27 | "Topic :: System :: Hardware", 28 | "Programming Language :: Python", 29 | "Operating System :: MacOS :: MacOS X", 30 | "Operating System :: Microsoft :: Windows", 31 | "Operating System :: POSIX", 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Cedrus Corporation 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | Neither the name of Cedrus Corporation nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /sample/pulsetable_test.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is a short sample of the Pulse Table feature of XID. Under the vast majority 3 | of circumstances it's not necessary, and users are better served sending separate 4 | event markers (see event_markers.py). 5 | ''' 6 | 7 | import pyxid2 8 | import time 9 | 10 | # get a list of all attached XID devices 11 | devices = pyxid2.get_xid_devices() 12 | 13 | dev = devices[0] # get the first device to use 14 | print(dev) 15 | 16 | #dev.get_pulse_table_bitmask() 17 | #dev.is_pulse_table_running() 18 | 19 | dev.set_pulse_duration(0) 20 | dev.set_lines(0xFFFF) #This is supposed to flash all lines, and will do so for 2 seconds before we set up the pulse table 21 | time.sleep(2) 22 | 23 | # Setting up the pulse table will reserve the lines used for pulse table use, so you will see those lines go low. 24 | dev.clear_pulse_table() 25 | dev.add_pulse_table_entry(0, 0x0101) 26 | dev.add_pulse_table_entry(500, 0x0202) 27 | dev.add_pulse_table_entry(1000, 0x0404) 28 | dev.add_pulse_table_entry(1500, 0x0808) 29 | dev.add_pulse_table_entry(2000, 0x0110) 30 | dev.add_pulse_table_entry(2500, 0x0220) 31 | dev.add_pulse_table_entry(3000, 0x0440) 32 | dev.add_pulse_table_entry(3500, 0x0880) 33 | dev.add_pulse_table_entry(4000, 0x0000) 34 | dev.add_pulse_table_entry(0, 0x0000) 35 | dev.run_pulse_table() 36 | print("Waiting 5s for the pulse table to finish, as the bit mask cannot be cleared while it's running.") 37 | # You could also end the program here and let the table run, but the sample is trying to avoid making lingering changes to the device. 38 | 39 | time.sleep(5) 40 | dev.set_lines(0x00) 41 | dev.clear_pulse_table() -------------------------------------------------------------------------------- /sample/responses.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This sample shows how to collect responses from an XID device. Note that you 3 | have to poll the device for responses manually (done in a while loop here). 4 | 5 | When a physical key is pressed on the device, a set of bytes describing it go 6 | into the serial buffer. This also occurs when the physical key is released. 7 | Calling poll_for_response() makes pyxid check the serial buffer for bytes 8 | constituting a response packet, and put a response object in its internal 9 | response queue. It does so once per poll_for_response() call. Calling 10 | get_next_response() pops a single response from the response queue. If you want 11 | to avoid seeing more responses than necessary, you can use 12 | flush_serial_buffer() to prevent more responses from being added to the queue 13 | by poll_for_response(), and you can clear already processed responses with 14 | clear_response_queue(). 15 | ''' 16 | import pyxid2 17 | 18 | # get a list of all attached XID devices 19 | devices = pyxid2.get_xid_devices() 20 | 21 | if devices: 22 | print(devices) 23 | else: 24 | print("No XID devices detected") 25 | exit() 26 | 27 | dev = devices[0] # get the first device to use 28 | 29 | print(dev) 30 | 31 | dev.reset_timer() 32 | 33 | print ("Press a key!") 34 | while not dev.has_response(): 35 | dev.poll_for_response() 36 | 37 | response = dev.get_next_response() 38 | # You can filter out key releases by simply ignoring them 39 | if response['pressed'] == True: 40 | # Process response as desired 41 | print(response) 42 | # response is a python dict with the following keys: 43 | # port: Device port the response was from (typically 0) 44 | # key: Response pad key pressed by the subject 45 | # pressed: True if the key was pressed, False if it was released 46 | # time: value of the Response Time timer when the key was pressed/released 47 | 48 | dev.flush_serial_buffer() 49 | dev.clear_response_queue() 50 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Python library for communicating with all Cedrus XID devices: StimTracker, RB-x40 response pads, c-pod, Lumina, and SV-1. 2 | 3 | XID (eXperiment Interface Device) devices are used with software such as SuperLab, Presentation, and E-Prime for receiving input as part of stimulus/response testing experiments. This handles all of the low level device handling for XID devices in python projects. 4 | 5 | Minimal samples for collecting responses and sending event markers are available in the Git repo. 6 | 7 | ------ 8 | Response collection in pyxid 9 | 10 | When a physical key is pressed on the device, a set of bytes describing it go into the serial buffer. This also occurs when the physical key is released. Calling poll_for_response() makes pyxid check the serial buffer for bytes constituting a response packet, and put a response object in its internal response queue. It does so once per poll_for_response() call. Calling get_next_response() pops a single response from the response queue. If you want to avoid seeing more responses than necessary, you can use flush_serial_buffer() to prevent more responses from being added to the queue by poll_for_response(), and you can clear already processed responses with clear_response_queue(). 11 | 12 | The response object is a python dict with the following keys: 13 | 14 | port: Device port the response was from (typically 0) 15 | key: Response pad key pressed by the subject 16 | pressed: True if the key was pressed, False if it was released 17 | time: value of the Response Time timer when the key was pressed/released 18 | 19 | For an example see sample/responses.py 20 | 21 | ------ 22 | Sending a TTL pulse signal via the library can be done via the following methods: 23 | 24 | set_pulse_duration() 25 | activate_line() 26 | clear_line() 27 | clear_all_lines() 28 | 29 | For an example see sample/event_markers.py 30 | 31 | ------ 32 | Timers 33 | 34 | Each XID device has an internal timer. This timer can be reset via reset_timer() or automatically on the onset of a light sensor or onset of audio. It's commonplace to reset the timer at the start of the experiment and/or at the onset of a simulus. 35 | -------------------------------------------------------------------------------- /sample/event_markers.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This sample outputs 8 event markers using an XID device on lines 1 through 8 3 | at 300ms intervals, producing what we call "marching lights". 4 | ''' 5 | import pyxid2 6 | import time 7 | 8 | # get a list of all attached XID devices 9 | devices = pyxid2.get_xid_devices() 10 | 11 | if devices: 12 | print(devices) 13 | else: 14 | print("No XID devices detected") 15 | exit() 16 | 17 | dev = devices[0] # get the first device to use 18 | 19 | print("Using ", dev) 20 | 21 | #Setting the pulse duration to 0 makes the lines stay activated until lowered 22 | #manually with clear_line() or clear_all_lines(). 23 | dev.set_pulse_duration(300) 24 | 25 | for line in range(0, 8): 26 | mask = 2 ** line 27 | print("raising line ", mask) 28 | dev.set_lines(mask) 29 | time.sleep(.3) 30 | 31 | dev.clear_all_lines() 32 | 33 | 34 | """ 35 | Here is the full description of activate_line() and clear_line(): 36 | 37 | activate_line() triggers an output line. clear_line() is the 38 | inverse of activate_line. If a line is active, it deactivates it. 39 | It has the same parameters as activate_line(). 40 | 41 | There are up to 16 output lines on XID devices that can be raised 42 | in any combination. To raise lines 1 and 7, for example, you pass 43 | in the list: activate_line(lines=[1, 7]). 44 | 45 | To raise a single line, pass in just an integer, or a list with a 46 | single element to the lines keyword argument: 47 | 48 | activate_line(lines=3) 49 | 50 | or 51 | 52 | activate_line(lines=[3]) 53 | 54 | The `lines` argument must either be an Integer, list of Integers, or 55 | None. 56 | 57 | If you'd rather specify a bitmask for setting the lines, you can use 58 | the bitmask keyword argument. Bitmask must be a Integer value between 59 | 0 and 255 where 0 specifies no lines, and 255 is all lines (65535 if 60 | using 16 lines of output). 61 | 62 | To use this, call the function as so to activate lines 1 and 6: 63 | 64 | activate_line(bitmask=33) 65 | 66 | leave_remaining_lines tells the function to only operate on the lines 67 | specified. For example, if lines 1 and 8 are active, and you make 68 | the following function call: 69 | 70 | activate_line(lines=4, leave_remaining_lines=True) 71 | 72 | This will result in lines 1, 4 and 8 being active. 73 | 74 | If you call activate_line(lines=4) with leave_remaining_lines=False 75 | (the default), if lines 1 and 8 were previously active, only line 4 76 | will be active after the call. 77 | """ -------------------------------------------------------------------------------- /pyxid2/internal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from struct import pack 3 | from struct import unpack 4 | import sys, time 5 | from .constants import NO_KEY_DETECTED, FOUND_KEY_DOWN, FOUND_KEY_UP, \ 6 | KEY_RELEASE_BITMASK, INVALID_PORT_BITS, XID_PACKET_SIZE, ST2_PACKET_SIZE 7 | 8 | try: 9 | import ftd2xx 10 | except OSError as e: 11 | if 'image not found' in str(e): 12 | raise OSError('ftd2xx drivers are not installed (or not in expected location)' 13 | ' and these are required for the Cedrus pyxid2 library.\n' 14 | '** Download from https://www.ftdichip.com/Drivers/D2XX.htm **') 15 | else: 16 | raise(e) # not an error we know so pass it on 17 | 18 | 19 | 20 | class XidConnection(object): 21 | def __init__(self, ftd2xx_index, baud_rate): 22 | self.ftd2xx_index = ftd2xx_index 23 | self.ftd2xx_con = 0 24 | self.baudrate = baud_rate 25 | self.__packet_size = XID_PACKET_SIZE 26 | self.__response_buffer = b'' 27 | self.__response_structs_queue = [] 28 | # The set lines cmd on XID 1 response devices (RB-x30 series, Lumina LP-400 and SV-1)'ah'. 29 | # In all other cases (ST-1, XID2 devices) 'mh' is used instead. 30 | self.__using_st_output = True 31 | self.__line_state = 0 32 | 33 | def set_using_stim_tracker_output(self, using_st=True): 34 | self.__using_st_output = using_st 35 | 36 | def set_resp_packet_size(self, st2_packet_size=True): 37 | if st2_packet_size: 38 | self.__packet_size = ST2_PACKET_SIZE # ST2 packets are larger 39 | 40 | def clear_digital_output_lines(self, lines, leave_remaining_lines=False): 41 | if lines not in list(range(0, 65536)): 42 | raise ValueError('lines must be between 0 and 65535') 43 | 44 | local_lines = ~lines 45 | if local_lines < 0: 46 | local_lines += 65536 47 | 48 | self.set_digital_output_lines(local_lines, leave_remaining_lines) 49 | 50 | def set_digital_output_lines(self, lines, leave_remaining_lines=False): 51 | if lines not in list(range(0, 65536)): 52 | raise ValueError('lines must be between 0 and 65535') 53 | 54 | if leave_remaining_lines: 55 | lines |= self.__line_state 56 | 57 | if self.__using_st_output: 58 | self.__set_lines_cmd = 'mh'+chr(lines & 0x000000FF)+chr((lines >> 8) & 0x000000FF) 59 | else: 60 | lines_tmp = ~lines 61 | if lines_tmp < 0: 62 | lines_tmp += 65536 63 | self.__set_lines_cmd = 'ah'+chr(lines_tmp & 0x000000FF) 64 | 65 | self.write(self.__set_lines_cmd) 66 | self.__line_state = lines 67 | 68 | def set_digio_lines_to_mask(self, lines): 69 | command_char = b'm' if self.__using_st_output else b'a' 70 | 71 | digio_cmd = pack('= (3, 0)): 133 | cmd_bytes += i.encode('latin1') 134 | else: 135 | cmd_bytes += i 136 | 137 | if True: 138 | for char in cmd_bytes: 139 | bytes_written += self.ftd2xx_con.write(bytes([char])) 140 | time.sleep(0.001) 141 | else: 142 | bytes_written = self.ftd2xx_con.write(bytes(cmd_bytes)) 143 | 144 | return bytes_written 145 | 146 | def write_bytes(self, command): 147 | bytes_written = 0 148 | 149 | self.ftd2xx_con.setTimeouts(100, 100) 150 | 151 | if True: 152 | for char in command: 153 | bytes_written += self.ftd2xx_con.write(bytes([char])) 154 | time.sleep(0.001) 155 | else: 156 | bytes_written = self.ftd2xx_con.write(command) 157 | 158 | return bytes_written 159 | 160 | def check_for_keypress(self): 161 | self.ftd2xx_con.setTimeouts(2, 50) 162 | response = self.read(self.__packet_size) 163 | 164 | response_found = NO_KEY_DETECTED 165 | if len(response) > 0: 166 | self.__response_buffer += response 167 | if self.__packet_size == 6: 168 | response_found = self.xid_input_found() 169 | else: 170 | response_found = self.st2_input_found() 171 | 172 | return response_found 173 | 174 | def xid_input_found(self): 175 | input_found = NO_KEY_DETECTED 176 | 177 | position_in_buf = 0 178 | 179 | while ((position_in_buf + self.__packet_size) <= 180 | len(self.__response_buffer)): 181 | 182 | exception_free = True 183 | 184 | try: 185 | (k, params, time) = unpack('> 5) 214 | response['pressed'] = (params & KEY_RELEASE_BITMASK) == \ 215 | KEY_RELEASE_BITMASK 216 | 217 | if response['key'] == 0: 218 | response['key'] = 8 219 | 220 | response['time'] = time 221 | 222 | if response['pressed']: 223 | input_found = FOUND_KEY_DOWN 224 | else: 225 | input_found = FOUND_KEY_UP 226 | 227 | self.__response_structs_queue += [response] 228 | 229 | position_in_buf += self.__packet_size 230 | 231 | self.__response_buffer = self.__response_buffer[position_in_buf:] 232 | 233 | return input_found 234 | 235 | def st2_input_found(self): 236 | input_found = NO_KEY_DETECTED 237 | 238 | position_in_buf = 0 239 | 240 | while ((position_in_buf + self.__packet_size) <= 241 | len(self.__response_buffer)): 242 | 243 | exception_free = True 244 | 245 | try: 246 | (o, port, key, pressed, time, null_byte) = unpack(' 0: 308 | # make a copy just in case any other internal members of 309 | # XidConnection were tracking the structure 310 | response = self.__response_structs_queue[0].copy() 311 | # we will now hand over 'response' to the calling code, 312 | # so remove it from the internal queue 313 | self.__response_structs_queue.pop(0) 314 | 315 | return response 316 | -------------------------------------------------------------------------------- /distribute_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap distribute installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from distribute_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import os 17 | import sys 18 | import time 19 | import fnmatch 20 | import tempfile 21 | import tarfile 22 | from distutils import log 23 | 24 | try: 25 | from site import USER_SITE 26 | except ImportError: 27 | USER_SITE = None 28 | 29 | try: 30 | import subprocess 31 | 32 | def _python_cmd(*args): 33 | args = (sys.executable,) + args 34 | return subprocess.call(args) == 0 35 | 36 | except ImportError: 37 | # will be used for python 2.3 38 | def _python_cmd(*args): 39 | args = (sys.executable,) + args 40 | # quoting arguments if windows 41 | if sys.platform == 'win32': 42 | def quote(arg): 43 | if ' ' in arg: 44 | return '"%s"' % arg 45 | return arg 46 | args = [quote(arg) for arg in args] 47 | return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 48 | 49 | DEFAULT_VERSION = "0.6.14" 50 | DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" 51 | SETUPTOOLS_FAKED_VERSION = "0.6c11" 52 | 53 | SETUPTOOLS_PKG_INFO = """\ 54 | Metadata-Version: 1.0 55 | Name: setuptools 56 | Version: %s 57 | Summary: xxxx 58 | Home-page: xxx 59 | Author: xxx 60 | Author-email: xxx 61 | License: xxx 62 | Description: xxx 63 | """ % SETUPTOOLS_FAKED_VERSION 64 | 65 | 66 | def _install(tarball): 67 | # extracting the tarball 68 | tmpdir = tempfile.mkdtemp() 69 | log.warn('Extracting in %s', tmpdir) 70 | old_wd = os.getcwd() 71 | try: 72 | os.chdir(tmpdir) 73 | tar = tarfile.open(tarball) 74 | _extractall(tar) 75 | tar.close() 76 | 77 | # going in the directory 78 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 79 | os.chdir(subdir) 80 | log.warn('Now working in %s', subdir) 81 | 82 | # installing 83 | log.warn('Installing Distribute') 84 | if not _python_cmd('setup.py', 'install'): 85 | log.warn('Something went wrong during the installation.') 86 | log.warn('See the error message above.') 87 | finally: 88 | os.chdir(old_wd) 89 | 90 | 91 | def _build_egg(egg, tarball, to_dir): 92 | # extracting the tarball 93 | tmpdir = tempfile.mkdtemp() 94 | log.warn('Extracting in %s', tmpdir) 95 | old_wd = os.getcwd() 96 | try: 97 | os.chdir(tmpdir) 98 | tar = tarfile.open(tarball) 99 | _extractall(tar) 100 | tar.close() 101 | 102 | # going in the directory 103 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 104 | os.chdir(subdir) 105 | log.warn('Now working in %s', subdir) 106 | 107 | # building an egg 108 | log.warn('Building a Distribute egg in %s', to_dir) 109 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 110 | 111 | finally: 112 | os.chdir(old_wd) 113 | # returning the result 114 | log.warn(egg) 115 | if not os.path.exists(egg): 116 | raise IOError('Could not build the egg.') 117 | 118 | 119 | def _do_download(version, download_base, to_dir, download_delay): 120 | egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' 121 | % (version, sys.version_info[0], sys.version_info[1])) 122 | if not os.path.exists(egg): 123 | tarball = download_setuptools(version, download_base, 124 | to_dir, download_delay) 125 | _build_egg(egg, tarball, to_dir) 126 | sys.path.insert(0, egg) 127 | import setuptools 128 | setuptools.bootstrap_install_from = egg 129 | 130 | 131 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 132 | to_dir=os.curdir, download_delay=15, no_fake=True): 133 | # making sure we use the absolute path 134 | to_dir = os.path.abspath(to_dir) 135 | was_imported = 'pkg_resources' in sys.modules or \ 136 | 'setuptools' in sys.modules 137 | try: 138 | try: 139 | import pkg_resources 140 | if not hasattr(pkg_resources, '_distribute'): 141 | if not no_fake: 142 | _fake_setuptools() 143 | raise ImportError 144 | except ImportError: 145 | return _do_download(version, download_base, to_dir, download_delay) 146 | try: 147 | pkg_resources.require("distribute>="+version) 148 | return 149 | except pkg_resources.VersionConflict: 150 | e = sys.exc_info()[1] 151 | if was_imported: 152 | sys.stderr.write( 153 | "The required version of distribute (>=%s) is not available,\n" 154 | "and can't be installed while this script is running. Please\n" 155 | "install a more recent version first, using\n" 156 | "'easy_install -U distribute'." 157 | "\n\n(Currently using %r)\n" % (version, e.args[0])) 158 | sys.exit(2) 159 | else: 160 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 161 | return _do_download(version, download_base, to_dir, 162 | download_delay) 163 | except pkg_resources.DistributionNotFound: 164 | return _do_download(version, download_base, to_dir, 165 | download_delay) 166 | finally: 167 | if not no_fake: 168 | _create_fake_setuptools_pkg_info(to_dir) 169 | 170 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 171 | to_dir=os.curdir, delay=15): 172 | """Download distribute from a specified location and return its filename 173 | 174 | `version` should be a valid distribute version number that is available 175 | as an egg for download under the `download_base` URL (which should end 176 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 177 | `delay` is the number of seconds to pause before an actual download 178 | attempt. 179 | """ 180 | # making sure we use the absolute path 181 | to_dir = os.path.abspath(to_dir) 182 | try: 183 | from urllib.request import urlopen 184 | except ImportError: 185 | from urllib2 import urlopen 186 | tgz_name = "distribute-%s.tar.gz" % version 187 | url = download_base + tgz_name 188 | saveto = os.path.join(to_dir, tgz_name) 189 | src = dst = None 190 | if not os.path.exists(saveto): # Avoid repeated downloads 191 | try: 192 | log.warn("Downloading %s", url) 193 | src = urlopen(url) 194 | # Read/write all in one block, so we don't create a corrupt file 195 | # if the download is interrupted. 196 | data = src.read() 197 | dst = open(saveto, "wb") 198 | dst.write(data) 199 | finally: 200 | if src: 201 | src.close() 202 | if dst: 203 | dst.close() 204 | return os.path.realpath(saveto) 205 | 206 | def _no_sandbox(function): 207 | def __no_sandbox(*args, **kw): 208 | try: 209 | from setuptools.sandbox import DirectorySandbox 210 | if not hasattr(DirectorySandbox, '_old'): 211 | def violation(*args): 212 | pass 213 | DirectorySandbox._old = DirectorySandbox._violation 214 | DirectorySandbox._violation = violation 215 | patched = True 216 | else: 217 | patched = False 218 | except ImportError: 219 | patched = False 220 | 221 | try: 222 | return function(*args, **kw) 223 | finally: 224 | if patched: 225 | DirectorySandbox._violation = DirectorySandbox._old 226 | del DirectorySandbox._old 227 | 228 | return __no_sandbox 229 | 230 | def _patch_file(path, content): 231 | """Will backup the file then patch it""" 232 | existing_content = open(path).read() 233 | if existing_content == content: 234 | # already patched 235 | log.warn('Already patched.') 236 | return False 237 | log.warn('Patching...') 238 | _rename_path(path) 239 | f = open(path, 'w') 240 | try: 241 | f.write(content) 242 | finally: 243 | f.close() 244 | return True 245 | 246 | _patch_file = _no_sandbox(_patch_file) 247 | 248 | def _same_content(path, content): 249 | return open(path).read() == content 250 | 251 | def _rename_path(path): 252 | new_name = path + '.OLD.%s' % time.time() 253 | log.warn('Renaming %s into %s', path, new_name) 254 | os.rename(path, new_name) 255 | return new_name 256 | 257 | def _remove_flat_installation(placeholder): 258 | if not os.path.isdir(placeholder): 259 | log.warn('Unkown installation at %s', placeholder) 260 | return False 261 | found = False 262 | for file in os.listdir(placeholder): 263 | if fnmatch.fnmatch(file, 'setuptools*.egg-info'): 264 | found = True 265 | break 266 | if not found: 267 | log.warn('Could not locate setuptools*.egg-info') 268 | return 269 | 270 | log.warn('Removing elements out of the way...') 271 | pkg_info = os.path.join(placeholder, file) 272 | if os.path.isdir(pkg_info): 273 | patched = _patch_egg_dir(pkg_info) 274 | else: 275 | patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) 276 | 277 | if not patched: 278 | log.warn('%s already patched.', pkg_info) 279 | return False 280 | # now let's move the files out of the way 281 | for element in ('setuptools', 'pkg_resources.py', 'site.py'): 282 | element = os.path.join(placeholder, element) 283 | if os.path.exists(element): 284 | _rename_path(element) 285 | else: 286 | log.warn('Could not find the %s element of the ' 287 | 'Setuptools distribution', element) 288 | return True 289 | 290 | _remove_flat_installation = _no_sandbox(_remove_flat_installation) 291 | 292 | def _after_install(dist): 293 | log.warn('After install bootstrap.') 294 | placeholder = dist.get_command_obj('install').install_purelib 295 | _create_fake_setuptools_pkg_info(placeholder) 296 | 297 | def _create_fake_setuptools_pkg_info(placeholder): 298 | if not placeholder or not os.path.exists(placeholder): 299 | log.warn('Could not find the install location') 300 | return 301 | pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) 302 | setuptools_file = 'setuptools-%s-py%s.egg-info' % \ 303 | (SETUPTOOLS_FAKED_VERSION, pyver) 304 | pkg_info = os.path.join(placeholder, setuptools_file) 305 | if os.path.exists(pkg_info): 306 | log.warn('%s already exists', pkg_info) 307 | return 308 | 309 | log.warn('Creating %s', pkg_info) 310 | f = open(pkg_info, 'w') 311 | try: 312 | f.write(SETUPTOOLS_PKG_INFO) 313 | finally: 314 | f.close() 315 | 316 | pth_file = os.path.join(placeholder, 'setuptools.pth') 317 | log.warn('Creating %s', pth_file) 318 | f = open(pth_file, 'w') 319 | try: 320 | f.write(os.path.join(os.curdir, setuptools_file)) 321 | finally: 322 | f.close() 323 | 324 | _create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) 325 | 326 | def _patch_egg_dir(path): 327 | # let's check if it's already patched 328 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 329 | if os.path.exists(pkg_info): 330 | if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): 331 | log.warn('%s already patched.', pkg_info) 332 | return False 333 | _rename_path(path) 334 | os.mkdir(path) 335 | os.mkdir(os.path.join(path, 'EGG-INFO')) 336 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 337 | f = open(pkg_info, 'w') 338 | try: 339 | f.write(SETUPTOOLS_PKG_INFO) 340 | finally: 341 | f.close() 342 | return True 343 | 344 | _patch_egg_dir = _no_sandbox(_patch_egg_dir) 345 | 346 | def _before_install(): 347 | log.warn('Before install bootstrap.') 348 | _fake_setuptools() 349 | 350 | 351 | def _under_prefix(location): 352 | if 'install' not in sys.argv: 353 | return True 354 | args = sys.argv[sys.argv.index('install')+1:] 355 | for index, arg in enumerate(args): 356 | for option in ('--root', '--prefix'): 357 | if arg.startswith('%s=' % option): 358 | top_dir = arg.split('root=')[-1] 359 | return location.startswith(top_dir) 360 | elif arg == option: 361 | if len(args) > index: 362 | top_dir = args[index+1] 363 | return location.startswith(top_dir) 364 | if arg == '--user' and USER_SITE is not None: 365 | return location.startswith(USER_SITE) 366 | return True 367 | 368 | 369 | def _fake_setuptools(): 370 | log.warn('Scanning installed packages') 371 | try: 372 | import pkg_resources 373 | except ImportError: 374 | # we're cool 375 | log.warn('Setuptools or Distribute does not seem to be installed.') 376 | return 377 | ws = pkg_resources.working_set 378 | try: 379 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', 380 | replacement=False)) 381 | except TypeError: 382 | # old distribute API 383 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) 384 | 385 | if setuptools_dist is None: 386 | log.warn('No setuptools distribution found') 387 | return 388 | # detecting if it was already faked 389 | setuptools_location = setuptools_dist.location 390 | log.warn('Setuptools installation detected at %s', setuptools_location) 391 | 392 | # if --root or --preix was provided, and if 393 | # setuptools is not located in them, we don't patch it 394 | if not _under_prefix(setuptools_location): 395 | log.warn('Not patching, --root or --prefix is installing Distribute' 396 | ' in another location') 397 | return 398 | 399 | # let's see if its an egg 400 | if not setuptools_location.endswith('.egg'): 401 | log.warn('Non-egg installation') 402 | res = _remove_flat_installation(setuptools_location) 403 | if not res: 404 | return 405 | else: 406 | log.warn('Egg installation') 407 | pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') 408 | if (os.path.exists(pkg_info) and 409 | _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): 410 | log.warn('Already patched.') 411 | return 412 | log.warn('Patching...') 413 | # let's create a fake egg replacing setuptools one 414 | res = _patch_egg_dir(setuptools_location) 415 | if not res: 416 | return 417 | log.warn('Patched done.') 418 | _relaunch() 419 | 420 | 421 | def _relaunch(): 422 | log.warn('Relaunching...') 423 | # we have to relaunch the process 424 | # pip marker to avoid a relaunch bug 425 | if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: 426 | sys.argv[0] = 'setup.py' 427 | args = [sys.executable] + sys.argv 428 | sys.exit(subprocess.call(args)) 429 | 430 | 431 | def _extractall(self, path=".", members=None): 432 | """Extract all members from the archive to the current working 433 | directory and set owner, modification time and permissions on 434 | directories afterwards. `path' specifies a different directory 435 | to extract to. `members' is optional and must be a subset of the 436 | list returned by getmembers(). 437 | """ 438 | import copy 439 | import operator 440 | from tarfile import ExtractError 441 | directories = [] 442 | 443 | if members is None: 444 | members = self 445 | 446 | for tarinfo in members: 447 | if tarinfo.isdir(): 448 | # Extract directories with a safe mode. 449 | directories.append(tarinfo) 450 | tarinfo = copy.copy(tarinfo) 451 | tarinfo.mode = 448 # decimal for oct 0700 452 | self.extract(tarinfo, path) 453 | 454 | # Reverse sort directories. 455 | if sys.version_info < (2, 4): 456 | def sorter(dir1, dir2): 457 | return cmp(dir1.name, dir2.name) 458 | directories.sort(sorter) 459 | directories.reverse() 460 | else: 461 | directories.sort(key=operator.attrgetter('name'), reverse=True) 462 | 463 | # Set correct owner, mtime and filemode on directories. 464 | for tarinfo in directories: 465 | dirpath = os.path.join(path, tarinfo.name) 466 | try: 467 | self.chown(tarinfo, dirpath) 468 | self.utime(tarinfo, dirpath) 469 | self.chmod(tarinfo, dirpath) 470 | except ExtractError: 471 | e = sys.exc_info()[1] 472 | if self.errorlevel > 1: 473 | raise 474 | else: 475 | self._dbg(1, "tarfile: %s" % e) 476 | 477 | 478 | def main(argv, version=DEFAULT_VERSION): 479 | """Install or upgrade setuptools and EasyInstall""" 480 | tarball = download_setuptools() 481 | _install(tarball) 482 | 483 | 484 | if __name__ == '__main__': 485 | main(sys.argv[1:]) 486 | -------------------------------------------------------------------------------- /pyxid2/pyxid_impl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from struct import pack 3 | from struct import unpack 4 | 5 | from .constants import NO_KEY_DETECTED 6 | from .internal import XidConnection 7 | from .keymaps import (rb_530_keymap, rb_730_keymap, rb_830_keymap, 8 | rb_834_keymap, lumina_keymap) 9 | 10 | import ftd2xx 11 | 12 | class XidScanner(object): 13 | """ 14 | Scan the computer for connected XID devices 15 | """ 16 | def __init__(self): 17 | self.__xid_cons = [] 18 | 19 | def detect_xid_devices(self): 20 | """ 21 | For all of the com ports connected to the computer, send an 22 | XID command '_c1'. If the device response with '_xid', it is 23 | an xid device. 24 | """ 25 | for con in self.__xid_cons: 26 | con.close() 27 | 28 | self.__xid_cons = [] 29 | 30 | ftd_dev_num = ftd2xx.createDeviceInfoList() 31 | 32 | for i in range (0, ftd_dev_num): 33 | device_found = False 34 | for b in [115200, 19200, 9600, 57600, 38400]: 35 | con = XidConnection(i, b) 36 | 37 | if con.open(): 38 | con.flush() 39 | 40 | try: 41 | returnval = con.send_xid_command("_c1", 5).decode('ASCII') 42 | except UnicodeDecodeError as e: 43 | # Assume this isn't an XID device, since it returned something weird. 44 | con.close() 45 | break 46 | 47 | if returnval.startswith('_xid'): 48 | device_found = True 49 | self.__xid_cons.append(con) 50 | 51 | if(returnval != '_xid0'): 52 | # set the device into XID mode 53 | con.send_xid_command('c10') 54 | con.flush() 55 | 56 | con.close() 57 | if device_found: 58 | # Device found, we're done. 59 | break 60 | 61 | def device_at_index(self, index): 62 | """ 63 | Returns the device at the specified index 64 | """ 65 | if index >= len(self.__xid_cons): 66 | raise ValueError("Invalid device index") 67 | 68 | return self.__xid_cons[index] 69 | 70 | def device_count(self): 71 | """ 72 | Number of XID devices connected to the computer 73 | """ 74 | return len(self.__xid_cons) 75 | 76 | 77 | class XidError(Exception): 78 | pass 79 | 80 | 81 | class XidDevice(object): 82 | def __init__(self, xid_connection): 83 | self.con = xid_connection 84 | self._impl = None 85 | self.product_id = -1 86 | self.model_id = -1 87 | self.major_fw_version = -1 88 | self.device_name = 'Uninitialized XID device' 89 | self.keymap = None 90 | self.response_queue = [] 91 | 92 | self.init_device() 93 | 94 | self.con.set_using_stim_tracker_output(self.major_fw_version == 2 or self.product_id == b'S') 95 | self.con.set_resp_packet_size(self.major_fw_version == 2 and self.product_id == b'S') 96 | if self.major_fw_version == 1: 97 | self.con.send_xid_command('a10') 98 | self.con.clear_digital_output_lines(0xff) 99 | 100 | def __del__(self): 101 | self.con.close() 102 | del self.con 103 | 104 | def reset_timer(self): 105 | """ 106 | Resets the timer. 107 | """ 108 | self.con.send_xid_command("e5") 109 | 110 | def query_timer(self): 111 | """ 112 | Gets the value from the device's timer 113 | """ 114 | time = -1 115 | if self.major_fw_version < 2: 116 | # There is no command equivalent to '_e5' in XID1. The response timer can be 117 | # reset but not queried (it's only used for timestamping responses). If you 118 | # absolutely must refer to the device's timer, you can send 'e1' for reset 119 | # and 'e3' for query (returns e3 followed by 4 bytes of timestamp) 120 | time = 0 121 | else: 122 | (_, _, _, time) = unpack(' 0 250 | 251 | def get_next_response(self): 252 | """ 253 | Pops the response at the beginning of the response queue 254 | and returns it. 255 | 256 | This function returns a dict object with the following keys: 257 | 258 | pressed: A boolean value of whether the event was a keypress 259 | or key release. 260 | key: The key on the device that was pressed. This is a 261 | 0 based index. 262 | port: Device port the response came from. Typically this 263 | is 0 on RB-series devices, and 2 on SV-1 voice key 264 | devices. 265 | time: For the time being, this just returns 0. There is 266 | currently an issue with clock drift in the Cedrus XID 267 | devices. Once we have this issue resolved, time will 268 | report the value of the RT timer in miliseconds. 269 | """ 270 | response = None 271 | if self.has_response(): 272 | response = self.response_queue.pop(0) 273 | return response 274 | 275 | def clear_response_queue(self): 276 | """ 277 | Clears the response queue 278 | """ 279 | self.response_queue = [] 280 | 281 | # Will flush both input and output buffers by default. 282 | # 1 is output (from device) only, 2 is input (to device) only 283 | def flush_serial_buffer(self, mask=0): 284 | self.con.flush(mask) 285 | 286 | def set_pulse_duration(self, duration): 287 | """ 288 | Sets the pulse duration for events in miliseconds when activate_line 289 | is called 290 | """ 291 | command = pack(' 16: 359 | raise ValueError('Line numbers must be between 1 and 16 ' 360 | '(inclusive)') 361 | bitmask |= 2 ** (l-1) 362 | 363 | self.con.set_digital_output_lines(bitmask, leave_remaining_lines) 364 | 365 | def clear_line(self, lines=None, bitmask=None, leave_remaining_lines=False): 366 | """ 367 | The inverse of activate_line. If a line is active, it deactivates it. 368 | 369 | This has the same parameters as activate_line() 370 | """ 371 | if lines is None and bitmask is None: 372 | raise ValueError('Must set one of lines or bitmask') 373 | if lines is not None and bitmask is not None: 374 | raise ValueError('Can only set one of lines or bitmask') 375 | 376 | if bitmask is not None: 377 | if bitmask not in list(range(0, 65536)): 378 | raise ValueError('bitmask must be an integer between ' 379 | '0 and 65535') 380 | 381 | if lines is not None: 382 | if not isinstance(lines, list): 383 | lines = [lines] 384 | 385 | bitmask = 0 386 | for l in lines: 387 | if l < 1 or l > 16: 388 | raise ValueError('Line numbers must be between 1 and 16 ' 389 | '(inclusive)') 390 | bitmask |= 2 ** (l-1) 391 | 392 | self.con.clear_digital_output_lines(bitmask, leave_remaining_lines) 393 | 394 | def set_lines(self, lines): 395 | self.con.set_digio_lines_to_mask(lines) 396 | 397 | def clear_all_lines(self): 398 | self.con.set_digio_lines_to_mask(0) 399 | 400 | def save_to_flash(self): 401 | if self.major_fw_version < 2: 402 | return 403 | 404 | self.con.send_xid_command("f9") 405 | 406 | # '_ia' 407 | # Example: get_single_shot('K') 408 | def get_single_shot(self, selector): 409 | if self.major_fw_version < 2: 410 | return 411 | (_, _, _, _, action, delay) = unpack(' 1: 497 | (_, _, _, lines) = unpack(' 1: 504 | self.con.send_xid_byte_command(ptable_mask) 505 | 506 | def clear_pulse_table(self): 507 | if self.major_fw_version > 1: 508 | self.con.send_xid_command("mc") 509 | 510 | def is_pulse_table_running(self): 511 | running = -1 512 | if self.major_fw_version > 1: 513 | (_, _, _, running) = unpack(' 1: 519 | self.con.send_xid_command("mr") 520 | 521 | def stop_pulse_table(self): 522 | if self.major_fw_version > 1: 523 | self.con.send_xid_command("ms") 524 | 525 | def add_pulse_table_entry(self, time, mask): 526 | ptable_mask = pack(' 1: 528 | self.con.send_xid_byte_command(ptable_mask) 529 | 530 | def reset_output_lines(self): 531 | if self.major_fw_version > 1: 532 | self.con.send_xid_command("mz") 533 | 534 | def __getattr__(self, attrname): 535 | return getattr(self._impl, attrname) 536 | 537 | def __str__(self): 538 | return '' % self.device_name 539 | 540 | def __repr__(self): 541 | return self.__str__() 542 | --------------------------------------------------------------------------------